From c0b960451538b374954ecf6e728eb1d57d550de1 Mon Sep 17 00:00:00 2001 From: break Date: Tue, 23 Jun 2026 11:05:58 +0900 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iot-risk-import-voucher-remark/PRD.md | 124 ++++++++++++++++++ .../issues/01-risk-resume-block.md | 28 ++++ .../issues/02-polling-auto-stop-risk-card.md | 28 ++++ .../issues/03-import-task-creator-name.md | 30 +++++ .../04-batch-invalidate-package-task.md | 43 ++++++ .../issues/05-voucher-multi-migration.md | 36 +++++ .../issues/06-order-voucher-multi.md | 27 ++++ .../issues/07-refund-voucher-multi.md | 29 ++++ .../08-agent-recharge-voucher-remark.md | 35 +++++ 9 files changed, 380 insertions(+) create mode 100644 .scratch/iot-risk-import-voucher-remark/PRD.md create mode 100644 .scratch/iot-risk-import-voucher-remark/issues/01-risk-resume-block.md create mode 100644 .scratch/iot-risk-import-voucher-remark/issues/02-polling-auto-stop-risk-card.md create mode 100644 .scratch/iot-risk-import-voucher-remark/issues/03-import-task-creator-name.md create mode 100644 .scratch/iot-risk-import-voucher-remark/issues/04-batch-invalidate-package-task.md create mode 100644 .scratch/iot-risk-import-voucher-remark/issues/05-voucher-multi-migration.md create mode 100644 .scratch/iot-risk-import-voucher-remark/issues/06-order-voucher-multi.md create mode 100644 .scratch/iot-risk-import-voucher-remark/issues/07-refund-voucher-multi.md create mode 100644 .scratch/iot-risk-import-voucher-remark/issues/08-agent-recharge-voucher-remark.md diff --git a/.scratch/iot-risk-import-voucher-remark/PRD.md b/.scratch/iot-risk-import-voucher-remark/PRD.md new file mode 100644 index 0000000..f40d84a --- /dev/null +++ b/.scratch/iot-risk-import-voucher-remark/PRD.md @@ -0,0 +1,124 @@ +# PRD:IoT 卡风险状态限制 / 导入任务操作人与批量失效 / 凭证多图 / 预充值备注 + +Status: ready-for-agent + +--- + +## Problem Statement + +运营团队在日常工作中遇到四个独立痛点: + +1. **风险停机/已销户卡无法有效拦截**:运营商侧将某些卡置为"风险停机"或"已销户"状态后,系统仍允许对这些卡发起复机操作,且轮询系统仍持续轮询这些卡,浪费资源并可能产生错误数据。 + +2. **导入任务无操作人信息**:IoT 卡导入任务和设备导入任务的列表中没有"操作人"字段,无法追溯是谁发起的批量操作。同时缺少批量失效订单套餐的操作入口,运营只能逐条手动处理。 + +3. **凭证只能上传一张**:后台创建订单和发起退款时,只允许上传一张凭证图片,无法满足多张凭证的业务场景(如多笔转账记录、多页合同等)。 + +4. **代理预充值缺少备注字段**:代理预充值操作无法附加运营备注,导致事后无法追溯充值的业务原因。 + +--- + +## Solution + +1. 在复机接口和轮询系统中增加对 `gateway_extend` 字段的检查,当独立卡处于"风险停机"或"已销户"状态时,拒绝复机并停止轮询。 + +2. 在所有导入任务中快照操作人姓名;新增"批量失效订单套餐"导入任务,支持通过 CSV 批量将订单下的套餐置为失效状态。 + +3. 将订单、退款、代理预充值的凭证字段改为支持最多5张的 jsonb 数组存储。 + +4. 在代理预充值记录上新增 `remark` 备注字段,供运营创建时填写,并在列表/详情接口中返回。 + +--- + +## User Stories + +1. 作为运营人员,当我尝试对"风险停机"状态的独立卡发起复机时,我希望系统拒绝并提示原因,以免产生无效操作。 +2. 作为运营人员,当我尝试对"已销户"状态的独立卡发起复机时,我希望系统拒绝并提示原因,以免对已注销的卡发起无意义请求。 +3. 作为运营人员,我希望"风险停机"和"已销户"的独立卡不再参与系统轮询,以免浪费轮询资源并产生噪音数据。 +4. 作为运营人员,当轮询系统检测到某张独立卡的网关状态变为"风险停机"或"已销户"时,我希望系统自动将该卡的轮询关闭,而不需要手动干预。 +5. 作为运营人员,我希望在 IoT 卡导入任务列表中看到"操作人"姓名,以便追溯每次批量导入是谁发起的。 +6. 作为运营人员,我希望在设备导入任务列表中看到"操作人"姓名,以便追溯每次批量导入是谁发起的。 +7. 作为运营人员,我希望通过上传 CSV 文件批量将一批订单的套餐置为失效,以便快速处理异常订单。 +8. 作为运营人员,我希望在创建批量失效任务时能填写备注和上传凭证(最多5张),以便记录操作原因。 +9. 作为运营人员,我希望批量失效任务完成后能看到成功数、失败数,以及失败原因(如订单号不存在),以便核查处理结果。 +10. 作为运营人员,我希望批量失效套餐只影响套餐记录本身,不触发退款和其他业务流程,让轮询系统自行根据套餐状态处理停机。 +11. 作为运营人员,我希望在创建后台订单时可以上传最多5张凭证,以便完整记录线下付款证明。 +12. 作为运营人员,我希望在发起退款申请时可以上传最多5张凭证,以便完整记录退款证明材料。 +13. 作为运营人员,我希望在代理预充值列表中看到备注内容,以便了解每笔预充值的业务背景。 +14. 作为运营人员,我希望在创建代理预充值订单时填写备注,以便记录本次充值的原因或说明。 +15. 作为开放 API 调用方,当我通过 Open API 对"风险停机"或"已销户"的独立卡调用复机接口时,我希望收到明确的错误响应。 + +--- + +## Implementation Decisions + +### 需求一:风险停机/已销户卡限制 + +- **判断字段**:`tb_iot_card.gateway_extend` 原文值 = `"风险停机"` 或 `"已销户"`。新增两个常量 `GatewayCardExtendRiskStop`、`GatewayCardExtendCancelled`。 +- **判断范围**:仅 `is_standalone = true` 的独立卡(未绑定设备的卡)。绑定设备的卡不受此限制。 +- **复机拦截**:在 `ManualStartCard`(后台管理员入口)和 `ResumeCard`(Open API 入口)两个函数中,调用网关前先读取 DB 中已落库的 `gateway_extend`,若命中则返回业务错误,不实时查询网关。 +- **轮询停止**:轮询状态处理器(`PollingCardStatusHandler`)在将新的 `gateway_extend` 写入 DB 后,检查若命中风险状态且 `is_standalone=true`,则调用现有的 `UpdatePollingStatus(false)` 将 `enable_polling` 写为 `false`,并跳过 `requeueCard`,终止该卡的轮询循环。 +- **错误码**:复机被拒绝时返回业务错误,提示信息明确说明原因("风险停机"/"已销户")。 + +### 需求二:导入任务操作人与批量失效 + +**操作人快照:** +- `tb_iot_card_import_task` 和 `tb_device_import_task` 表各新增 `creator_name varchar` 字段。 +- 新的批量失效任务表同样包含此字段。 +- 创建任务时从当前登录用户快照姓名写入,旧数据留空(不回填)。 +- 列表响应 DTO 新增 `creator_name` 字段。 + +**批量失效订单套餐任务:** +- 新建模型、Store、Service、Handler,参照现有 IoT 卡导入任务的结构(CSV 上传 → 异步任务处理)。 +- CSV 格式:单列 `order_no`(无表头行约定沿用现有导入任务的处理方式)。 +- 创建接口额外支持:`remark`(字符串备注)和最多5张凭证(jsonb 存储)。 +- 处理逻辑:按行读取 `order_no` → 查询 `Order` → 找到 `PackageUsage WHERE order_id = ? AND status NOT IN (3, 4)` → 批量更新 `status = 4`(已失效)。 +- 订单不存在:记为失败(fail),继续处理后续行。 +- 订单存在但旗下套餐全部已是终态(status 3 或 4):记为成功(success)。 +- 不触发退款,不直接操作 IoT 卡停机,由轮询系统根据套餐状态自行评估。 +- 任务表命名:`tb_order_package_invalidate_task`。 + +### 需求三:凭证多图改造 + +- `tb_order.payment_voucher_key`(varchar 500)→ `jsonb`,存储 `[]string`,最多5个元素。 +- `tb_refund.refund_voucher_key`(varchar 500)→ `jsonb`,存储 `[]string`,最多5个元素。 +- `tb_agent_recharge_record.payment_voucher_key`(varchar 500)→ `jsonb`,存储 `[]string`,最多5个元素。 +- **数据迁移**(同一 Migration 语句):非空旧值 `"some_key"` → `["some_key"]`;空字符串 → `[]`。生产环境由运营手动执行 Migration。 +- 所有涉及凭证的请求 DTO 字段由 `string` 改为 `[]string`,validate 标签限制 `max=5`,每项 `max=500`。 +- 所有涉及凭证的响应 DTO 字段改为 `[]string`。 +- 不限制文件类型(上传 URL 获取接口层面不变,仅存储 key 列表)。 + +### 需求四:代理预充值备注 + +- `tb_agent_recharge_record` 新增 `remark text` 字段(可为空)。 +- `CreateAgentRechargeRequest` 新增 `remark` 字段(`omitempty`,无最大长度强限制,建议 1000 字符内)。 +- `AgentRechargeResponse` 新增 `remark` 字段(列表和详情接口均返回)。 +- 创建后不可修改,无需新增修改接口。 + +--- + +## Testing Decisions + +本项目禁止自动化测试。验证方式: + +- **PostgreSQL MCP**:核查 `tb_iot_card.enable_polling` 在风险状态卡被轮询后是否正确置为 `false`;核查 `tb_package_usage.status` 在批量失效任务执行后是否按预期更新;核查 jsonb 字段迁移结果。 +- **curl / Postman**:对风险停机卡调用复机接口,验证返回正确错误码;上传多张凭证创建订单,验证存储和返回正确;创建代理预充值时附带备注,验证列表返回。 + +--- + +## Out of Scope + +- 已绑定设备的卡(`is_standalone=false`)不受"风险停机/已销户"限制约束。 +- 批量失效订单套餐不触发退款流程。 +- 批量失效不直接调用网关停机,依赖轮询系统自行评估。 +- 历史导入任务的 `creator_name` 不回填。 +- 凭证文件类型校验(文件类型限制不在本次范围内)。 +- 代理预充值备注创建后不可修改(无修改接口)。 + +--- + +## Further Notes + +- 凭证字段改为 jsonb 后,前端需同步更新上传组件支持多选;本 PRD 只覆盖后端改造。 +- 批量失效任务的凭证和备注字段,与代理预充值备注字段、以及需求三的多凭证设计保持一致的数据形态,便于后续统一处理。 +- "风险停机"和"已销户"的 `gateway_extend` 原文值来自上游网关,若上游修改返回值需同步更新常量。 diff --git a/.scratch/iot-risk-import-voucher-remark/issues/01-risk-resume-block.md b/.scratch/iot-risk-import-voucher-remark/issues/01-risk-resume-block.md new file mode 100644 index 0000000..e759219 --- /dev/null +++ b/.scratch/iot-risk-import-voucher-remark/issues/01-risk-resume-block.md @@ -0,0 +1,28 @@ +Status: done + +## Parent + +`.scratch/iot-risk-import-voucher-remark/PRD.md` + +## What to build + +新增两个 gateway_extend 常量(`GatewayCardExtendRiskStop = "风险停机"`、`GatewayCardExtendCancelled = "已销户"`),然后在后台复机入口(`ManualStartCard`)和 Open API 复机入口(`ResumeCard`)中增加前置拦截: + +- 仅对 `is_standalone = true` 的独立卡生效 +- 读取 DB 中已落库的 `gateway_extend` 字段(不实时查网关) +- 若命中 `"风险停机"` 或 `"已销户"`,立即返回业务错误,不再继续调用网关 +- 错误提示需明确说明被拒绝的原因(如"该卡已被运营商风险停机,不允许复机") + +无 Schema 变更,不需要 Migration。 + +## Acceptance criteria + +- [ ] `pkg/constants/iot.go` 新增 `GatewayCardExtendRiskStop`、`GatewayCardExtendCancelled` 两个常量 +- [ ] 后台 `ManualStartCard` 在调用网关前检查独立卡 `gateway_extend`,命中风险值时返回业务错误 +- [ ] Open API `ResumeCard` 在调用网关前检查独立卡 `gateway_extend`,命中风险值时返回业务错误 +- [ ] 已绑定设备的卡(`is_standalone = false`)不受此限制,复机正常进行 +- [ ] 返回的错误信息能区分"风险停机"和"已销户"两种情况 + +## Blocked by + +None - can start immediately diff --git a/.scratch/iot-risk-import-voucher-remark/issues/02-polling-auto-stop-risk-card.md b/.scratch/iot-risk-import-voucher-remark/issues/02-polling-auto-stop-risk-card.md new file mode 100644 index 0000000..6b393ca --- /dev/null +++ b/.scratch/iot-risk-import-voucher-remark/issues/02-polling-auto-stop-risk-card.md @@ -0,0 +1,28 @@ +Status: done + +## Parent + +`.scratch/iot-risk-import-voucher-remark/PRD.md` + +## What to build + +在 `PollingCardStatusHandler` 中,当轮询查询网关后将新的 `gateway_extend` 写入 DB,若新值命中风险常量(`GatewayCardExtendRiskStop` 或 `GatewayCardExtendCancelled`)且该卡 `is_standalone = true`,则: + +1. 调用现有的 `UpdatePollingStatus(false)` 将 `enable_polling` 写为 `false` +2. 跳过末尾的 `requeueCard`,终止该卡的轮询循环 + +检查时机:在 `UpdateFields` 写入 `gateway_extend` 成功之后,`requeueCard` 调用之前。 + +不需要 Schema 变更,不需要 Migration。 + +## Acceptance criteria + +- [ ] 轮询处理器在写入 `gateway_extend` 后检查风险状态 +- [ ] 独立卡(`is_standalone=true`)命中风险值时调用 `UpdatePollingStatus(false)`,`tb_iot_card.enable_polling` 被置为 `false` +- [ ] 该卡不再入队(不调用 `requeueCard`),轮询循环终止 +- [ ] 已绑定设备的卡(`is_standalone=false`)命中风险值时不触发此逻辑,轮询正常继续 +- [ ] 可通过 PostgreSQL MCP 验证:风险状态卡处理后 `enable_polling = false` + +## Blocked by + +- `issues/01-risk-resume-block.md`(依赖 `GatewayCardExtendRiskStop`、`GatewayCardExtendCancelled` 常量) diff --git a/.scratch/iot-risk-import-voucher-remark/issues/03-import-task-creator-name.md b/.scratch/iot-risk-import-voucher-remark/issues/03-import-task-creator-name.md new file mode 100644 index 0000000..e4a2163 --- /dev/null +++ b/.scratch/iot-risk-import-voucher-remark/issues/03-import-task-creator-name.md @@ -0,0 +1,30 @@ +Status: done + +## Parent + +`.scratch/iot-risk-import-voucher-remark/PRD.md` + +## What to build + +在现有两个导入任务表中新增操作人快照字段,并在列表接口返回: + +- `tb_iot_card_import_task` 新增 `creator_name varchar(100)` 字段 +- `tb_device_import_task` 新增 `creator_name varchar(100)` 字段 +- 创建任务时从当前登录用户快照姓名写入(不可为 null,旧数据留空字符串) +- IoT 卡导入任务列表响应 DTO 新增 `creator_name` 字段 +- 设备导入任务列表响应 DTO 新增 `creator_name` 字段 + +历史数据不回填,旧记录 `creator_name` 为空字符串。 + +## Acceptance criteria + +- [ ] Migration 为两个现有任务表各添加 `creator_name varchar(100) not null default ''` 字段 +- [ ] IoT 卡导入任务创建时写入当前用户姓名快照 +- [ ] 设备导入任务创建时写入当前用户姓名快照 +- [ ] IoT 卡导入任务列表接口返回 `creator_name` 字段 +- [ ] 设备导入任务列表接口返回 `creator_name` 字段 +- [ ] 旧记录 `creator_name` 为空字符串,不报错 + +## Blocked by + +None - can start immediately diff --git a/.scratch/iot-risk-import-voucher-remark/issues/04-batch-invalidate-package-task.md b/.scratch/iot-risk-import-voucher-remark/issues/04-batch-invalidate-package-task.md new file mode 100644 index 0000000..85bbdec --- /dev/null +++ b/.scratch/iot-risk-import-voucher-remark/issues/04-batch-invalidate-package-task.md @@ -0,0 +1,43 @@ +Status: done + +## Parent + +`.scratch/iot-risk-import-voucher-remark/PRD.md` + +## What to build + +新增"批量失效订单套餐"导入任务,完整实现 Model → Store → Service → Handler → Route → Worker 全链路,参照现有 IoT 卡导入任务的结构。 + +**数据模型**(新建 `tb_order_package_invalidate_task`): +- 参照 `tb_iot_card_import_task` 的基础字段(任务编号、状态、计数、失败/跳过明细、文件存储 key、时间戳等) +- 额外字段:`creator_name varchar(100)`、`remark text`、`voucher_keys jsonb`(存储 `[]string`,最多5个元素) + +**上传接口**(创建任务): +- 接受 CSV 文件(单列 `order_no`) +- 额外参数:`remark`(字符串,可选)和 `voucher_keys`(`[]string`,最多5个,可选) +- 写入 `creator_name` 快照 + +**Worker 处理逻辑**: +- 逐行读取 `order_no` +- 按 `order_no` 查询 `tb_order` 获取 `order_id`;找不到则记为失败,继续处理 +- 查询 `tb_package_usage WHERE order_id = ? AND status NOT IN (3, 4)`,批量更新 `status = 4`(已失效) +- 订单存在但套餐全部已是终态(status 3 或 4):记为成功 +- 不触发退款,不调用网关,不直接停机 + +**列表/详情接口**:参照现有导入任务接口结构。 + +## Acceptance criteria + +- [ ] Migration 创建 `tb_order_package_invalidate_task` 表,包含所有必要字段 +- [ ] 创建任务接口:接受 CSV + remark + voucher_keys(最多5个),写入 creator_name 快照 +- [ ] Worker 按行处理:`order_no` 不存在 → 失败并记录原因,继续下一行 +- [ ] Worker 按行处理:订单存在且有可失效套餐 → 批量更新 `status=4` → 记为成功 +- [ ] Worker 按行处理:订单存在但套餐全为终态 → 记为成功 +- [ ] 处理过程中不调用退款接口、不操作网关 +- [ ] 列表接口返回 `creator_name`、`remark`、任务状态和计数 +- [ ] 详情接口返回失败明细(含 order_no 和原因) +- [ ] 已在路由和文档生成器中注册 + +## Blocked by + +None - can start immediately diff --git a/.scratch/iot-risk-import-voucher-remark/issues/05-voucher-multi-migration.md b/.scratch/iot-risk-import-voucher-remark/issues/05-voucher-multi-migration.md new file mode 100644 index 0000000..01a7a3a --- /dev/null +++ b/.scratch/iot-risk-import-voucher-remark/issues/05-voucher-multi-migration.md @@ -0,0 +1,36 @@ +Status: done + +## Parent + +`.scratch/iot-risk-import-voucher-remark/PRD.md` + +## What to build + +一次 Migration 完成三个现有表的凭证字段类型变更,并同时新增代理预充值备注字段: + +**字段类型变更**(varchar(500) → jsonb): +- `tb_order.payment_voucher_key` +- `tb_refund.refund_voucher_key` +- `tb_agent_recharge_record.payment_voucher_key` + +**数据转换规则**(在同一 Migration 的 USING 子句中完成): +- 非空旧值 `"some_key"` → `["some_key"]` +- 空字符串 `""` → `[]` +- NULL → `[]` + +**新增字段**: +- `tb_agent_recharge_record.remark text`(可为空,默认 null) + +生产环境由运营手动执行,Migration 文件需包含完整的 UP 和 DOWN 语句。 + +## Acceptance criteria + +- [ ] Migration UP:三个凭证字段成功从 varchar 转为 jsonb,历史单值数据转换为单元素数组 +- [ ] Migration UP:空字符串/NULL 转换为空数组 `[]` +- [ ] Migration UP:`tb_agent_recharge_record` 新增 `remark text` 字段 +- [ ] Migration DOWN:可回滚(jsonb → varchar,取数组第一个元素;remark 字段删除) +- [ ] 迁移后可用 PostgreSQL MCP 验证数据格式正确 + +## Blocked by + +None - can start immediately diff --git a/.scratch/iot-risk-import-voucher-remark/issues/06-order-voucher-multi.md b/.scratch/iot-risk-import-voucher-remark/issues/06-order-voucher-multi.md new file mode 100644 index 0000000..1ba951c --- /dev/null +++ b/.scratch/iot-risk-import-voucher-remark/issues/06-order-voucher-multi.md @@ -0,0 +1,27 @@ +Status: done + +## Parent + +`.scratch/iot-risk-import-voucher-remark/PRD.md` + +## What to build + +将后台创建订单接口的凭证字段从单个字符串改为最多5个字符串的列表: + +- `CreateAdminOrderRequest.payment_voucher_key`:`string` → `[]string`,validate 限制 `max=5`,每项 `max=500` +- `OrderResponse.payment_voucher_key`(以及所有订单相关响应 DTO):`string` → `[]string` +- Order Model 的 `PaymentVoucherKey` 字段类型改为适配 jsonb 的 `pq.StringArray` 或自定义 JSON 类型 +- Order Service 中读写 `payment_voucher_key` 的逻辑同步更新 + +Schema 变更已由 `issues/05-voucher-multi-migration.md` 完成。 + +## Acceptance criteria + +- [ ] 后台创建订单接口接受 `payment_voucher_key []string`(0-5个元素),offline 支付时至少需要1个 +- [ ] 订单列表/详情接口返回 `payment_voucher_key []string` +- [ ] 传入超过5个凭证 key 时返回参数校验错误 +- [ ] 现有无凭证的订单(wallet 支付)返回空数组,不报错 + +## Blocked by + +- `issues/05-voucher-multi-migration.md` diff --git a/.scratch/iot-risk-import-voucher-remark/issues/07-refund-voucher-multi.md b/.scratch/iot-risk-import-voucher-remark/issues/07-refund-voucher-multi.md new file mode 100644 index 0000000..b191a52 --- /dev/null +++ b/.scratch/iot-risk-import-voucher-remark/issues/07-refund-voucher-multi.md @@ -0,0 +1,29 @@ +Status: done + +## Parent + +`.scratch/iot-risk-import-voucher-remark/PRD.md` + +## What to build + +将退款申请接口的凭证字段从单个字符串改为最多5个字符串的列表: + +- `CreateRefundRequest.refund_voucher_key`:`string` → `[]string`,validate 限制 `max=5`,每项 `max=500`,required(至少1个) +- `ResubmitRefundRequest.refund_voucher_key`:`*string` → `*[]string`(重新提交时可选替换) +- `RefundResponse.refund_voucher_key`(以及所有退款相关响应 DTO):`string` → `[]string` +- Refund Model 的 `RefundVoucherKey` 字段类型改为适配 jsonb 的类型 +- Refund Service 中读写 `refund_voucher_key` 的逻辑同步更新 + +Schema 变更已由 `issues/05-voucher-multi-migration.md` 完成。 + +## Acceptance criteria + +- [ ] 创建退款申请接口接受 `refund_voucher_key []string`(1-5个元素,必填) +- [ ] 重新提交退款接口的 `refund_voucher_key` 可选,传入时替换全部凭证 +- [ ] 退款列表/详情接口返回 `refund_voucher_key []string` +- [ ] 传入超过5个凭证 key 时返回参数校验错误 +- [ ] 传入空数组时返回参数校验错误(至少需要1个) + +## Blocked by + +- `issues/05-voucher-multi-migration.md` diff --git a/.scratch/iot-risk-import-voucher-remark/issues/08-agent-recharge-voucher-remark.md b/.scratch/iot-risk-import-voucher-remark/issues/08-agent-recharge-voucher-remark.md new file mode 100644 index 0000000..97984a5 --- /dev/null +++ b/.scratch/iot-risk-import-voucher-remark/issues/08-agent-recharge-voucher-remark.md @@ -0,0 +1,35 @@ +Status: done + +## Parent + +`.scratch/iot-risk-import-voucher-remark/PRD.md` + +## What to build + +同时完成代理预充值的凭证多图改造和备注字段接入: + +**凭证多图**: +- `CreateAgentRechargeRequest.payment_voucher_key`:`string` → `[]string`,validate 限制 `max=5`,每项 `max=500`(offline 支付时至少1个,微信支付时忽略) +- `AgentRechargeResponse.payment_voucher_key`:`string` → `[]string` +- `AgentRechargeRecord.PaymentVoucherKey` Model 字段类型改为适配 jsonb 的类型 + +**备注字段**: +- `CreateAgentRechargeRequest` 新增 `remark string`(可选,建议不超过1000字符) +- `AgentRechargeResponse` 新增 `remark string`(列表和详情接口均返回) +- `AgentRechargeRecord.Remark` 在创建时写入,后续不可修改 + +AgentRecharge Service 中读写相关字段的逻辑同步更新。 + +Schema 变更(payment_voucher_key 类型 + remark 字段)已由 `issues/05-voucher-multi-migration.md` 完成。 + +## Acceptance criteria + +- [ ] 创建代理预充值接口接受 `payment_voucher_key []string`(offline 时至少1个)和可选 `remark` +- [ ] 代理预充值列表/详情接口返回 `payment_voucher_key []string` 和 `remark` +- [ ] 传入超过5个凭证 key 时返回参数校验错误 +- [ ] 不新增 remark 修改接口,创建后备注字段只读 +- [ ] 微信支付创建预充值时可不传凭证(与现有逻辑一致) + +## Blocked by + +- `issues/05-voucher-multi-migration.md`