# 代理充值管理 API 规范 ## Purpose 定义代理钱包在线扫码自充、平台线下代充、第三方支付确认、异步钱包入账、状态查询、异常恢复与多租户权限控制的统一业务契约,确保充值资金事实可核对、可恢复且不会重复入账。 ## Requirements --- ### Requirement: 创建代理充值订单 系统 SHALL 通过 `POST /api/admin/agent-recharges` 保留同一代理充值资源,并按登录账号类型与 `payment_method` 执行严格分流。 代理在线请求 MUST 仅接受 `amount`、`payment_method` 与 `request_id`: - `payment_method` MUST 为 `wechat` 或 `alipay`。 - `amount` MUST 使用分为单位,范围 MUST 为 `10000~100000000`。 - 目标店铺与主钱包 MUST 从当前认证上下文确定;请求 MUST NOT 接受或信任 `shop_id`、支付凭证或运营备注。 - 每次新的主动提交 MUST 创建新的代理充值单和 `tb_payment` 支付单,不得按金额或待支付记录复用旧单。 - 同一提交账号与 `request_id` MUST 形成持久化幂等作用域;相同指纹重放 MUST 返回原业务结果,不同指纹重放 MUST 返回统一冲突错误。 平台或超级管理员的 `offline` 线下代充 MUST 继续使用现有目标 `shop_id`、付款凭证和企业微信审批契约,在线充值 100 元最低金额 MUST NOT 改变线下代充金额规则。平台、超级管理员和企业账号 MUST NOT 创建代理在线扫码充值单。 在线创建 MUST 在同一 GORM 事务中保存充值单、支付单及请求幂等事实,再调用创建时选定的支付 Adapter:微信 MUST 使用 Native 支付,支付宝 MUST 使用 `alipay.trade.precreate`。成功响应 MUST 使用统一 `{code,msg,data,timestamp}` 格式,并在 `data` 中至少返回 `recharge_id`、`recharge_no`、`payment_no`、`payment_method`、`recharge_source`、`recharge_source_name`、`amount`、`qr_content`、`status` 与 `status_name`。后端 MUST 返回支付渠道原始付款字符串,不得生成或保存二维码图片。 充值订单创建、列表、详情和在线支付状态响应 MUST 使用稳定来源枚举区分创建路径:`platform_offline` 表示平台线下代充,`agent_online` 表示代理在线自充。列表 MUST 支持使用 `recharge_source` 筛选;来源可由受控创建方式推导,不要求新增重复数据库字段。 支付单 MUST 同时保存创建时的收款身份快照:微信记录商户号,支付宝记录应用 ID,并保留支付方式与 `payment_config_id`,供后续导出对账。创建响应 MUST NOT 返回该内部收款身份快照。 第三方预下单失败时,系统 MUST 记录 Integration Log,并将本次支付单标记为失败、充值单标记为已关闭;不得返回缺少有效 `qr_content` 的成功响应。 #### Scenario: 代理创建微信 Native 扫码充值 - **WHEN** 代理提交 `amount=10000`、`payment_method=wechat` 和新的 `request_id` - **THEN** 系统从登录上下文确定当前店铺主钱包,创建充值单和支付单,调用微信 Native 预下单,并将 `code_url` 映射为 `qr_content` - **THEN** 响应不得包含支付配置 ID、商户密钥或其他店铺信息 #### Scenario: 代理创建支付宝当面付扫码充值 - **WHEN** 代理提交有效金额、`payment_method=alipay` 和新的 `request_id` - **THEN** 系统创建充值单和支付单,调用 `alipay.trade.precreate`,并将 `qr_code` 映射为 `qr_content` #### Scenario: 区分平台代充与代理自充 - **WHEN** 调用方查询充值订单列表、详情或在线支付状态 - **THEN** 平台线下代充 MUST 返回 `recharge_source=platform_offline`,代理微信或支付宝在线自充 MUST 返回 `recharge_source=agent_online` #### Scenario: 在线充值金额边界 - **WHEN** 代理提交的在线充值金额小于 `10000` 分或大于 `100000000` 分 - **THEN** 系统 MUST 返回 `CodeInvalidParam`,且不得创建充值单、支付单或调用第三方支付 #### Scenario: 代理请求携带目标店铺 - **WHEN** 代理在线请求携带 `shop_id` 或其他仅线下代充允许的字段 - **THEN** 系统 MUST 拒绝请求,不得允许代理选择本店、下级店铺或其他店铺作为受益方 #### Scenario: 相同请求重放 - **WHEN** 同一提交账号使用相同 `request_id` 和相同业务字段重试 - **THEN** 系统 MUST 返回首次创建的充值单、支付单和付款内容,不得再次创建业务单或再次向第三方预下单 #### Scenario: 幂等请求载荷冲突 - **WHEN** 同一提交账号使用已有 `request_id` 但改变金额或支付方式 - **THEN** 系统 MUST 返回 `CodeConflict`,不得改变原充值单或创建新单 #### Scenario: 支付方式不可用 - **WHEN** 所选支付方式缺少完整配置、扫码预下单能力或回调验签能力 - **THEN** 系统 MUST 返回统一支付配置不可用错误,且不得创建只有本地记录而无法付款的待支付订单 --- ### Requirement: 线下充值确认 **接口描述**:平台账号确认线下转账已到账,完成充值并为代理钱包增加余额。 系统 SHALL 仅允许平台账号对符合条件的存量线下充值记录执行确认,并在校验操作密码后完成一次性钱包入账。 **HTTP 方法与路径** ``` POST /api/admin/agent-recharges/:id/offline-pay ``` **鉴权** - 需要登录态(Bearer Token) - 仅平台账号可调用,其他账号类型返回 `1005 CodeForbidden` --- **请求体示例** ```json { "operation_password": "Abc123456" } ``` **请求字段说明** | 字段名 | 类型 | 必填 | 说明 | |--------|------|------|------| | operation_password | string | 是 | 操作密码,用于二次身份验证 | **路径参数说明** | 参数名 | 类型 | 说明 | |--------|------|------| | id | integer | 充值记录 ID | **业务规则** - 操作密码验证失败返回 `1043 CodeInvalidOldPassword` - 充值记录必须存在且 `payment_method=offline`,否则返回 `1121 CodeRechargeNotFound` - 充值记录状态必须为 `1`(待支付),否则返回 `1050 CodeInvalidStatus` - 确认成功后: 1. 充值记录状态更新为 `2`(已完成),记录 `paid_at` 和 `completed_at` 2. 代理主钱包余额增加对应金额(使用乐观锁 version 字段防并发) 3. 创建钱包流水记录 4. 记录审计日志(操作人、操作前后数据) --- **成功响应示例** ```json { "code": 0, "msg": "success", "data": { "id": 88, "recharge_no": "ARCH20260316100001", "shop_id": 101, "amount": 200000, "payment_method": "offline", "payment_channel": "offline", "payment_config_id": null, "status": 2, "paid_at": "2026-03-16T11:00:00+08:00", "completed_at": "2026-03-16T11:00:00+08:00", "created_at": "2026-03-16T10:00:00+08:00" }, "timestamp": "2026-03-16T11:00:00+08:00" } ``` --- **错误响应示例** 操作密码错误: ```json { "code": 1043, "msg": "操作密码错误", "data": null, "timestamp": "2026-03-16T11:00:00+08:00" } ``` 充值记录不存在: ```json { "code": 1121, "msg": "充值记录不存在", "data": null, "timestamp": "2026-03-16T11:00:00+08:00" } ``` 充值记录状态不允许操作: ```json { "code": 1050, "msg": "当前充值记录状态不允许此操作", "data": null, "timestamp": "2026-03-16T11:00:00+08:00" } ``` 非平台账号调用: ```json { "code": 1005, "msg": "只有平台账号可以使用线下充值", "data": null, "timestamp": "2026-03-16T11:00:00+08:00" } ``` #### Scenario: 平台确认存量线下充值 - **WHEN** 平台账号对待支付的线下充值记录提交正确操作密码 - **THEN** 系统 MUST 完成充值记录、主钱包余额和唯一钱包流水更新,重复操作不得重复入账 --- ### Requirement: 代理充值查询 系统 SHALL 按当前账号数据范围提供代理充值列表与详情查询,并返回稳定的充值来源、状态和时间字段。 #### 接口一:充值记录列表 **接口描述**:分页查询代理充值记录,支持按店铺、状态、充值来源、日期范围过滤。 **HTTP 方法与路径** ``` GET /api/admin/agent-recharges ``` **鉴权** - 需要登录态(Bearer Token) - 代理账号:只能查看自己所属店铺的充值记录 - 平台账号:可查看所有店铺的充值记录 --- **请求参数(Query String)** ``` GET /api/admin/agent-recharges?page=1&page_size=20&shop_id=101&status=3&recharge_source=agent_online&start_date=2026-03-01&end_date=2026-03-31 ``` **请求参数说明** | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | page | integer | 否 | 页码,默认 1 | | page_size | integer | 否 | 每页条数,默认 20,最大 100 | | shop_id | integer | 否 | 按店铺 ID 过滤(平台账号可用) | | status | integer | 否 | 按状态过滤:1=待支付,2=已支付,3=已完成,4=已关闭 | | recharge_source | string | 否 | 按充值来源过滤:`platform_offline`=平台线下代充,`agent_online`=代理在线自充 | | start_date | string | 否 | 创建时间起始日期,格式 `YYYY-MM-DD` | | end_date | string | 否 | 创建时间截止日期,格式 `YYYY-MM-DD` | --- **成功响应示例** ```json { "code": 0, "msg": "success", "data": { "total": 56, "page": 1, "page_size": 20, "list": [ { "id": 88, "recharge_no": "ARCH20260316100001", "shop_id": 101, "shop_name": "测试店铺A", "amount": 50000, "payment_method": "wechat", "payment_channel": "wechat_direct", "payment_config_id": 3, "recharge_source": "agent_online", "recharge_source_name": "代理在线自充", "status": 3, "paid_at": "2026-03-16T10:05:00+08:00", "completed_at": "2026-03-16T10:05:00+08:00", "created_at": "2026-03-16T10:00:00+08:00" }, { "id": 87, "recharge_no": "ARCH20260315090001", "shop_id": 101, "shop_name": "测试店铺A", "amount": 200000, "payment_method": "offline", "payment_channel": "offline", "payment_config_id": null, "recharge_source": "platform_offline", "recharge_source_name": "平台线下代充", "status": 3, "paid_at": "2026-03-15T11:00:00+08:00", "completed_at": "2026-03-15T11:00:00+08:00", "created_at": "2026-03-15T09:00:00+08:00" } ] }, "timestamp": "2026-03-16T12:00:00+08:00" } ``` **列表项字段说明** | 字段名 | 类型 | 说明 | |--------|------|------| | id | integer | 充值记录 ID | | recharge_no | string | 充值单号 | | shop_id | integer | 店铺 ID | | shop_name | string | 店铺名称 | | amount | integer | 充值金额(分) | | payment_method | string | 支付方式 | | payment_channel | string | 实际支付通道 | | payment_config_id | integer\|null | 关联支付配置 ID | | recharge_source | string | 充值来源:`platform_offline` 或 `agent_online` | | recharge_source_name | string | 充值来源中文名称 | | status | integer | 状态:1=待支付,2=已支付,3=已完成,4=已关闭 | | paid_at | string\|null | 支付时间 | | completed_at | string\|null | 完成时间 | | created_at | string | 创建时间 | --- **错误响应示例** 参数错误: ```json { "code": 1001, "msg": "参数验证失败", "data": null, "timestamp": "2026-03-16T12:00:00+08:00" } ``` --- #### 接口二:充值记录详情 **接口描述**:查询单条充值记录的完整详情。 **HTTP 方法与路径** ``` GET /api/admin/agent-recharges/:id ``` **鉴权** - 需要登录态(Bearer Token) - 代理账号:只能查看自己所属店铺的充值记录,否则返回 `1121 CodeRechargeNotFound` - 平台账号:可查看任意充值记录 --- **路径参数说明** | 参数名 | 类型 | 说明 | |--------|------|------| | id | integer | 充值记录 ID | --- **成功响应示例** ```json { "code": 0, "msg": "success", "data": { "id": 88, "recharge_no": "ARCH20260316100001", "shop_id": 101, "shop_name": "测试店铺A", "agent_wallet_id": 55, "amount": 50000, "payment_method": "wechat", "payment_channel": "wechat_direct", "payment_config_id": 3, "payment_transaction_id": "wx_txn_20260316_abc123", "recharge_source": "agent_online", "recharge_source_name": "代理在线自充", "status": 3, "paid_at": "2026-03-16T10:05:00+08:00", "completed_at": "2026-03-16T10:05:00+08:00", "created_at": "2026-03-16T10:00:00+08:00", "updated_at": "2026-03-16T10:05:00+08:00" }, "timestamp": "2026-03-16T12:00:00+08:00" } ``` **详情字段说明** | 字段名 | 类型 | 说明 | |--------|------|------| | id | integer | 充值记录 ID | | recharge_no | string | 充值单号 | | shop_id | integer | 店铺 ID | | shop_name | string | 店铺名称 | | agent_wallet_id | integer | 代理钱包 ID | | amount | integer | 充值金额(分) | | payment_method | string | 支付方式 | | payment_channel | string | 实际支付通道 | | payment_config_id | integer\|null | 关联支付配置 ID | | payment_transaction_id | string\|null | 第三方支付流水号 | | recharge_source | string | 充值来源:`platform_offline` 或 `agent_online` | | recharge_source_name | string | 充值来源中文名称 | | status | integer | 状态:1=待支付,2=已支付,3=已完成,4=已关闭 | | paid_at | string\|null | 支付时间 | | completed_at | string\|null | 完成时间 | | created_at | string | 创建时间 | | updated_at | string | 最后更新时间 | --- **错误响应示例** 充值记录不存在或无权限: ```json { "code": 1121, "msg": "充值记录不存在", "data": null, "timestamp": "2026-03-16T12:00:00+08:00" } ``` #### Scenario: 按数据范围查询充值记录 - **WHEN** 已认证账号查询充值列表或详情 - **THEN** 系统 MUST 仅返回其数据范围内的记录,并使用 `recharge_source` 区分平台线下代充与代理在线自充 --- ### Requirement: 代理充值回调处理 现有微信和支付宝异步回调入口 MUST 在完成渠道验签后,按 `tb_payment.payment_no` 和 `order_type=agent_recharge` 分发到代理充值支付确认用例,不得仅依赖 `ARCH` 单号前缀判断业务类型。旧代理充值支付单可在迁移期保留受控兼容分支,但新支付单 MUST 走统一支付记录分发。 支付确认 MUST 校验支付方式、创建时的 `payment_config_id`、回调商户或应用身份、支付单金额、充值单金额、支付单与充值单关联以及第三方交易号唯一性。校验失败 MUST 不改变支付单、充值单或钱包事实,并写入中文安全日志和 Integration Log;日志不得记录密钥或完整敏感正文。 成功确认 MUST 在同一 GORM 事务中: 1. 条件更新支付单为已支付并保存第三方交易号与支付时间; 2. 条件更新代理充值单从 `1=待支付` 为 `2=已支付`; 3. 写入稳定版本的代理充值入账 Outbox。 支付渠道成功响应 MUST 在上述事务提交后返回。钱包入账不得继续作为支付回调事务中的同步步骤。 Outbox 消费者 MUST 在独立事务中复用统一代理主钱包入账能力,锁定目标主钱包、增加余额、创建唯一成功流水、将充值单从 `2=已支付` 更新为 `3=已完成`,并写入现有钱包入账事件。消费者 MUST 以充值记录 ID 作为业务幂等引用;重复回调、重复 Outbox 或 Worker 重试不得重复增加余额。 #### Scenario: 微信支付成功回调 - **WHEN** 微信回调验签通过,支付单类型为 `agent_recharge`,且金额、配置、商户身份和业务关联全部一致 - **THEN** 系统 MUST 固化支付成功事实和入账 Outbox,并向微信返回渠道成功响应 - **THEN** 钱包余额由独立消费者完成,不得在回调事务中同步增加 #### Scenario: 支付宝支付成功回调 - **WHEN** 支付宝通知验签通过,交易状态为成功,支付单类型为 `agent_recharge`,且金额、配置、应用身份和业务关联全部一致 - **THEN** 系统 MUST 固化支付成功事实和入账 Outbox,并向支付宝返回 `success` #### Scenario: 回调金额或关联不一致 - **WHEN** 回调金额与支付单或充值单不一致,或支付单未关联该代理充值记录 - **THEN** 系统 MUST 拒绝处理并保留原状态,钱包余额和流水 MUST NOT 改变 #### Scenario: 重复支付回调 - **WHEN** 同一第三方交易号对同一已支付或已完成充值单重复回调 - **THEN** 系统 MUST 幂等返回渠道成功响应,不得重复写支付事实、入账 Outbox 或钱包流水 #### Scenario: 第三方交易号被其他支付单占用 - **WHEN** 回调中的第三方交易号已绑定另一张支付单或代理充值单 - **THEN** 系统 MUST 返回冲突并记录不含敏感信息的严重错误,任何钱包 MUST NOT 入账 #### Scenario: 钱包入账暂时失败 - **WHEN** 支付事实已提交但钱包入账消费者因锁冲突或暂时性基础设施错误失败 - **THEN** 充值单 MUST 保持 `2=已支付`,Outbox/Worker MUST 重试,且支付成功事实不得回滚或要求代理再次付款 #### Scenario: 重复执行钱包入账 - **WHEN** 同一代理充值入账任务被重复消费 - **THEN** 数据库唯一流水约束和状态条件更新 MUST 保证余额最多增加一次,并最终将充值单收敛为 `3=已完成` ### Requirement: 权限控制 系统 MUST 使用以下权限边界: | 操作 | 平台/超级管理员 | 代理账号 | 企业账号 | |------|-----------------|----------|----------| | 创建在线扫码充值 | 禁止 | 仅当前所属店铺主钱包 | 禁止 | | 创建线下代充 | 按现有权限指定目标店铺 | 禁止 | 禁止 | | 线下充值确认 | 允许 | 禁止 | 禁止 | | 查询可用在线支付方式 | 禁止 | 允许 | 禁止 | | 查询充值列表与详情 | 按既有数据范围 | 按既有店铺层级与查看权限 | 禁止 | | 查询在线支付状态 | 按既有数据范围 | 按既有店铺层级与查看权限 | 禁止 | 路由层 MUST 对企业账号执行粗粒度拦截;Application/Query MUST 执行创建角色、当前店铺、资源归属和查看权限校验;GORM 数据范围过滤保持启用。越权与资源不存在 MUST 统一返回 `CodeForbidden` 和“无权限操作该资源或资源不存在”,不得泄露资源是否存在。 线下充值确认继续使用既有平台操作密码契约;密码验证失败返回 `CodeInvalidOldPassword`,响应和日志均不得记录密码明文。 #### Scenario: 代理为当前店铺充值 - **WHEN** 代理账号创建在线扫码充值且当前店铺主钱包可用 - **THEN** 系统 MUST 以认证上下文中的店铺和主钱包作为唯一受益方 #### Scenario: 平台尝试创建在线扫码充值 - **WHEN** 平台或超级管理员提交 `wechat` 或 `alipay` 代理充值请求 - **THEN** 系统 MUST 返回 `CodeForbidden`,并引导其使用既有线下代充审批路径 #### Scenario: 无权读取支付状态 - **WHEN** 登录账号查询其数据范围外充值单的支付状态 - **THEN** 系统 MUST 返回统一禁止访问错误,不得返回支付状态、付款内容或钱包余额 --- ### Requirement: 查询代理在线充值可用支付方式 系统 SHALL 提供 `GET /api/admin/agent-recharges/payment-methods`,仅根据当前生效支付配置返回真正具备扫码预下单、回调验签和查单能力的在线支付方式。路由 MUST 注册在 `/:id` 动态路由之前。 成功响应 MUST 使用统一 `{code,msg,data,timestamp}` 格式;`data.methods` MUST 为按 `wechat`、`alipay` 固定顺序排列的字符串数组,并同时返回 `min_amount=10000` 与 `max_amount=100000000`。接口不得返回支付配置 ID、商户号、应用 ID、密钥或具体缺失的敏感配置。 #### Scenario: 微信与支付宝均可用 - **WHEN** 当前支付配置完整支持微信 Native 和支付宝 PreCreate - **THEN** 接口 MUST 返回 `methods=["wechat","alipay"]` 及在线金额上下限 #### Scenario: 没有可用扫码支付方式 - **WHEN** 当前配置不支持任何已约定的扫码支付方式 - **THEN** 接口 MUST 成功返回空数组,不得伪造可用渠道 ### Requirement: 查询代理在线充值支付状态 系统 SHALL 提供 `GET /api/admin/agent-recharges/:id/payment-status` 供桌面端轮询本地事实。接口 MUST 仅查询 PostgreSQL 本地支付单、充值单及必要的钱包入账结果,不得在每次轮询时调用第三方支付渠道。 响应 MUST 使用统一 `{code,msg,data,timestamp}` 格式,并至少返回 `status`、`status_name`、`payment_status`、`payment_status_name`、`paid_at` 与 `completed_at`。`1=待支付` MUST 表示尚未确认收款,`2=已支付` MUST 表示第三方收款已确认但钱包仍在入账,`3=已完成` MUST 表示钱包余额和唯一流水已经提交。 接口 MUST NOT 返回 `qr_content`、支付密钥、签名参数、内部重试错误或其他店铺余额。第一期 MUST NOT 提供主动取消、支付方式切换、在线退款或本地推算的精确二维码倒计时。 #### Scenario: 等待扫码支付 - **WHEN** 充值单仍为待支付且支付单未确认成功 - **THEN** 接口 MUST 返回待支付状态,不得调用第三方查单 #### Scenario: 已支付等待钱包入账 - **WHEN** 支付单已支付且充值单状态为 `2=已支付` - **THEN** 接口 MUST 明确返回支付成功、入账处理中,不得显示支付失败 #### Scenario: 钱包已经到账 - **WHEN** 充值单状态为 `3=已完成` 且唯一钱包流水存在 - **THEN** 接口 MUST 返回已完成和完成时间 ### Requirement: 待支付订单受控收敛 系统 MUST 通过后台受控任务查询长期待支付的代理在线充值支付单,以弥补第三方回调丢失。任务 MUST 使用创建支付单时记录的支付方式和 `payment_config_id` 调用对应查单 Adapter,并为每次外部尝试记录 Integration Log。 查单确认成功 MUST 复用与回调相同的支付确认用例;查单确认关闭或失效 MUST 条件关闭仍为待支付的支付单与充值单;未知、超时或渠道异常 MUST 保持原业务状态并按任务策略重试。迟到的真实成功通知经完整校验后 MUST 仍能固化支付事实并入账,不得因本地曾判断待支付或关闭而吞掉已收款资金。 #### Scenario: 回调丢失但查单成功 - **WHEN** 待支付收敛任务从第三方查询到交易成功且金额与配置校验通过 - **THEN** 系统 MUST 调用统一支付确认用例,写入支付事实和钱包入账 Outbox #### Scenario: 第三方明确订单关闭 - **WHEN** 查单结果明确表示订单关闭或失效,且本地支付单仍为待支付 - **THEN** 系统 MUST 条件更新支付单为失败并将充值单关闭,不得增加钱包余额 #### Scenario: 查单结果未知 - **WHEN** 第三方超时、返回未知状态或暂时不可用 - **THEN** 系统 MUST 保持本地待支付状态并重试,不得猜测支付失败 --- ## 数据模型补充说明 **tb_agent_recharge_record 新增字段** | 字段名 | 类型 | 可空 | 说明 | |--------|------|------|------| | payment_config_id | bigint | 是 | 关联支付配置 ID,线下充值为 NULL,在线充值记录实际使用的支付配置 | **充值状态枚举** | 值 | 含义 | |----|------| | 1 | 待支付(订单已创建,等待支付) | | 2 | 已支付(第三方收款已确认,钱包入账处理中) | | 3 | 已完成(钱包余额和唯一流水已提交) | | 4 | 已关闭(第三方预下单失败或确认订单已关闭) | **支付方式枚举** | 值 | 含义 | |----|------| | wechat | 微信 Native 扫码支付 | | alipay | 支付宝当面付扫码支付 | | offline | 线下转账(仅平台账号可用) | **支付通道枚举** | 值 | 含义 | |----|------| | wechat_direct | 微信直连通道 | | alipay | 支付宝通道 | | offline | 线下转账 |