feat(代理自充): AUG26-017 代理自充收款方式与线下预存款审批字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m52s

- 受控配置新增代理在线自充允许范围(仅微信/仅支付宝/同时支持),读侧与创建侧取允许范围与可用商户池交集,两侧失败关闭
- 新增允许范围查询与修改端点,读限代理与平台账号、写限超级管理员,复用受控配置写服务留痕
- tb_agent_recharge_record 新增交易流水号、线下收款方式三列快照与其他凭证列(成对迁移 000213)
- 线下申请校验启用的收款方式字典项与必填交易流水号,交易流水号独立于在线渠道交易号、不参与去重
- 扩展 offline_recharge_approval 场景可映射字段白名单与字典引用保护
- 新增付款凭证识别能力与交易流水号预填接口,识别不落库、日志不记录载荷
This commit is contained in:
2026-09-11 15:21:23 +08:00
parent e687a266e6
commit 7891189712
32 changed files with 1168 additions and 172 deletions

View File

@@ -1,26 +1,140 @@
## Context
现有代理在线充值链路已完整可用,本 Change 只在其外层补"允许范围"与"申报字段",不改造支付与入账闭环:
- 创建与门禁:`POST /api/admin/agent-recharges``admin.AgentRechargeHandler.Create`,在线分支为 `createOnline` 并显式拒绝 `shop_id`、凭证与备注(`internal/handler/admin/agent_recharge.go:69-85`);用例为 `agentrecharge.OnlineCreationService.Execute``internal/application/agentrecharge/online_creation.go:64`)。
- 自身店铺门禁已内联在 `loadCreationFacts`:账号 `user_type=agent ∧ status=1 ∧ account.shop_id == CurrentShopID`、店铺启用、主钱包 `main+normal``internal/application/agentrecharge/online_creation.go:174-200`)。店铺取自认证上下文,客户端无法指定。
- 可用方式当前只按商户池推导:`OnlineCreationService.AvailablePaymentMethods` 遍历固定顺序 `[wechat, alipay]`,过滤启用池 + 启用商户 + `MerchantConfig` + `adapter.Available``internal/application/agentrecharge/online_creation.go:131-172`),且对非代理直接 403。
- 商户选路与快照冻结在创建事务内完成:`merchantpayment.SelectForNewPaymentWithTx``FreezeRoute``internal/application/merchantpayment/routing.go:203,357`)。无可用商户时返回 `CodeNoPaymentConfig`,归档契约明确禁止回退旧综合支付配置。
- 受控系统配置已具备完整写路径:`PUT /api/admin/system-configs/:key``systemconfig.UpdateService.Execute`,仅超级管理员、`pg_advisory_xact_lock` 串行、首次写入即 upsert、前后值审计、提交后缓存失效`internal/application/systemconfig/update.go:72,104,110-140`。Registry 原生支持 `ValueType=string + EnumValues``internal/infrastructure/systemconfig/registry.go:104-140``Reader.GetStrict` 在无行时返回代码注册的 `DefaultValue`,已落库的非法值失败关闭(`internal/infrastructure/systemconfig/reader.go:118-143`)。
- 线下预存款申请事实就是 `tb_agent_recharge_record``payment_method='offline'`),审批实例一业务单一个(`uq_approval_instance_business``migrations/000170_create_approval_core.up.sql:18`),驳回即终态、无重提。创建用例 `OfflineCreationService.Execute` 与命令 `CreateOfflineCommand` 只含 `ShopID/Amount/PaymentVoucherKeys/Remark``internal/application/agentrecharge/offline_creation.go:21-29`)。现表无收款方式引用、无交易流水号、无其他凭证列(`internal/model/agent_wallet.go:76-100`)。
- 场景可映射字段是白名单硬编码:`offline_recharge_approval` 现有 9 个字段,不含收款方式与其他凭证(`internal/application/wecom/scene.go:309-320`)。
- 线下收款方式字典已由员工账单 Change 建立并被其声明为共享分类:`tb_employee_collection_payment_method``migrations/000212_add_employee_collection_bills.up.sql:6-32`),契约明确"由本能力独占维护MUST NOT 被定义为核销专用"`openspec/specs/employee-collection-bill/spec.md`)。当前字典读写与"被引用"判定只在 `internal/application/employeecollection/payment_method.go` 内,且只扫核销申请与尝试。
- Gateway 客户端已具备 AES-128-ECB 加密、MD5 签名、网络级重试与统一错误映射(`internal/gateway/client.go``crypto.go`;契约见 `docs/integrations/gateway/README.md`)。附件对象存储提供按 key 读取:`Provider.Download``DownloadToTemp``Stat``Exists``pkg/storage/storage.go:12-18`)。
- 主 spec 中 `agent-funds-commission` 的"代理在线充值可用支付方式按支付配置判定"仍描述商户池改造前的旧口径,与本 Change 的目标判定冲突,需一并修正。
## Goals / Non-Goals
**Goals:**
- 让代理在线自充可用方式成为"超管允许范围 ∩ 可用商户池方式",且允许范围对代理与平台不可见。
- 让线下预存款申请按 §17.1 留存可维护的收款方式、交易流水号与其他凭证。
- 以既有 Gateway 通道提供付款凭证识别预填,且识别结果不成为资金事实、不进入日志。
**Non-Goals:**
- 不新增代理自助线下充值入口,不改造 `offline_recharge_approval` 的一业务单一个审批实例语义与终态推进。
- 不引入审批尝试记录模型,不实现驳回后重提。
- 不改造员工账单建账判据、核销申请流程与退款冲销。
- 不新建业务字典模块或收款账户目录。
- 不改造 Gateway 客户端的加密、签名、重试与错误映射;不改动其既有能力文件。
## Decisions
- 线上充值复用支付单和实际商户路由,成功消费者以支付单/钱包流水唯一约束入账。
- 线下申请保存不可变金额和付款证据快照;审批回调在主钱包锁事务中条件入账。
- 线上未知结果和线下审批未知均保持在途,复用既有查询/恢复,不把重试当作新入账。
### 1. 允许范围落在受控系统配置,不新建表
## 配置与充值动作契约
新增一个注册 Key`ValueType=string``EnumValues` 为三个稳定取值(仅微信 / 仅支付宝 / 同时支持),默认值为"同时支持"以保持既有行为。读写复用既有 `systemconfig.UpdateService``Reader.GetStrict`,从而直接获得超管限定、咨询锁串行、前后值审计、枚举校验与缓存失效,无需新表或第二套配置约定。
### 允许方式与查询
替代方案(新建专用配置表)被拒绝:会与既有受控配置重复审计与缓存语义,且 `GET /system-configs` 已限定超级管理员读取(`internal/query/systemconfig/list.go:29-31`),天然满足"允许范围不得泄漏给代理与平台"。
- `GET /agent-self-recharge-payment-methods`:代理、平台用户仅返回“全局允许方式 ∩ 当前启用商户池可用方式”的有序 `wechat`/`alipay` 列表;不得返回商户身份、凭证或全局允许范围。交集为空返回空列表
- `PUT /agent-self-recharge-payment-methods`:仅超级管理员,保存 `wechat_only``alipay_only``wechat_and_alipay`;记录操作者、前后值和时间。修改不更新任何已有充值/支付单的支付方式、商户 ID 或快照。
管理端点按需求命名为 `GET/PUT /agent-self-recharge-payment-methods`,实际注册为 `/api/admin` 下的独立单段路由组(`internal/routes/agent_self_recharge_payment_method.go`)。需求路径与既有代理充值组前缀 `/api/admin/agent-recharges` 不同,无法挂在该组下,因而也无法继承该组「企业账号 403」的门禁读取与写入的角色门禁改由该路由组内显式声明读取仅代理与平台账号写入仅超级管理员且显式拒绝企业账号。写入仍复用同一配置写服务不新增第二条配置写路径
### 自身店铺充值
### 2. 交集在读侧与创建侧各判定一次,两侧均失败关闭
- `POST /agent-recharges` 的代理在线分支强制 `shop_id` 为空且认证店铺为已启用代理自身店铺;传入下级/其他店铺返回无权。线上请求 `amount``payment_method``request_id`,其中方式必须在当前交集内;以 `request_id` 与调用者/店铺唯一复用既有支付创建结果,失败预下单不入账
- 在线创建在事务内选择实际商户、冻结支付/商户快照并创建充值记录;渠道成功消费者锁定支付、充值和主钱包,以支付 ID/钱包流水唯一约束一次入账。失败、关闭、退款或未知状态不加余额;未知结果由既有查单/回调恢复,禁止客户端重试直接增加余额。
读侧 `AvailablePaymentMethods` 增加允许范围过滤,并把可见角色从"仅代理"放开为"代理与平台账号";创建侧在 `Execute` 的域校验之后、读取店铺事实之前,用同一次配置读取结果判定请求方式是否在交集内,不在则拒绝
### 线下转账
只做创建侧校验会让代理看到无法使用的方式;只做读侧校验会让绕过查询的请求创建被禁用方式。两侧都必须走 `GetStrict`,配置非法时失败关闭,而不是回退默认值后继续放行。
- 线下请求必须包含 `amount`(正分)、`payer_name``transferred_at`(带时区时间)、`transfer_channel_or_bank``transaction_no`、至少一个 `payment_voucher_key` 和可选备注;创建时冻结全部字段、状态为待企业微信审批,不增加主钱包
- 企业微信通过消费者锁定申请、审批实例和主钱包,条件更新一次增加余额和钱包流水;驳回、撤回、关闭不入账。仅未成功申请可修改上述材料并创建新审批实例重提;未知审批保持在途。平台/超级管理员查询和处理一律先应用既有店铺数据范围,审计不记录凭证正文。
创建侧允许范围门禁位于幂等回放之后:同一 `request_id` 的重试属于既有单而非新单,不因允许范围变更被拒绝;门禁只拦截会真正新建充值单与支付单的路径,被拒方式仍不新建任何行(`internal/application/agentrecharge/online_creation.go:90-97`
### 3. 交易流水号使用独立列,不复用在线渠道交易号
`tb_agent_recharge_record` 新增 `external_transaction_no` 列保存该笔充值的交易流水号,其值来自付款凭证识别的支付单号并经人工确认或更正。列名与语义对齐既有员工账单能力的外部交易流水号(`tb_employee_collection_application.external_transaction_no`,注释"外部交易流水号,可由 OCR 预填但以人工确认值为准"`migrations/000212_add_employee_collection_bills.up.sql:262`),全仓保持一套词汇,展示名仍为"交易流水号"。
**不复用 `payment_transaction_id`**,理由是结构性的而非命名偏好:
- 该列是在线支付的**渠道权威事实**,只由在线回调写入(`internal/application/agentrecharge/confirm_online_payment.go:127``internal/service/agent_recharge/service.go:303`),并作为入账对账键被 Outbox 消费者强校验:`recharge.PaymentTransactionID` 必须等于事件里的渠道交易号,否则返回冲突(`internal/infrastructure/payment/agent_recharge_consumer.go:118`)。一旦允许人工修改该列,在线入账对账即被破坏。
- 该列同时是财务调查视图的交易流水号检索域(`internal/query/audit/finance.go:480,1007`)与代理充值审计身份字段(`internal/infrastructure/audit/registry.go:795`)。把人工申报值并入,会让渠道权威值与人工申报值在同一检索域内不可区分。
- 申报值本质是"人工确认的付款证据",渠道值本质是"渠道返回的权威事实",二者来源与可信级别不同,`111.md` §0 第 4 条的"识别结果不是资金事实"要求它们在存储上可区分。
替代方案(复用 `payment_transaction_id`)被拒绝,原因如上;替代方案(另建申报表)过度:该字段始终 1:1 属于充值单。
不新增唯一约束:同一外部付款的重复申报由企业微信终审人员以第三方记录核验,与员工账单能力对跨申请累计分摊不做系统防重的既有裁决一致(`openspec/specs/employee-collection-bill/spec.md`)。因此线下申报的交易流水号不做跨记录去重,也不作为入账前置条件。
在线充值单的 `payment_transaction_id` 语义与写入方保持不变;线下列可空以满足历史行兼容与 down 回滚。
### 4. 收款方式以三列快照引用既有字典,命名与支付方式枚举区分
`tb_agent_recharge_record` 新增 `offline_payment_method_id``offline_payment_method_code``offline_payment_method_name` 三列,引用 `tb_employee_collection_payment_method`。刻意不命名为 `payment_method_*`:该表已有 `payment_method` 列表示 `wechat/alipay/bank/offline` 支付方式枚举,同前缀会造成两套语义混淆。快照列与员工账单申请/尝试的 `payment_method_id/code/name` 同构。
新建申请时校验字典项存在且启用;历史行三列为空。
字典的"被引用"判定需从只扫核销申请与尝试扩展为同时覆盖代理充值申请,否则超管可删除或改码一个仍被引用的字典项,破坏"被引用只可停用"的可观察语义。该扩展是引用判定的最小补齐,不改动员工账单的建账、核销与退款逻辑。
### 5. 其他凭证使用独立列
新增 `other_voucher_keys`jsonb 数组,对象键引用),与既有 `payment_voucher_key` 并列不合并为单一列表§17.1 要求"支付凭证"与"其他凭证"在审批表单中分别呈现,合并后无法区分两类。
### 6. 付款凭证识别新增 Gateway 能力,不复用带响应日志的泛型入口
`internal/gateway` 新增能力文件封装 `POST /ai/ocr/extract-payment`,入参为 `image_base64`。图片字节由后端按附件对象键读取对象存储后编码,避免把 Gateway 凭证下发给前端。
该能力刻意既不用 `doRequest` 也不用 `doRequestWithResponse`,两条既有路径都会把识别载荷写进日志:
- `doRequest` 在 Info 级别打印**加密前的完整明文请求体**`internal/gateway/client.go``zap.ByteString("body", dataBytes)`),而本能力的请求体正是凭证图片的 base64。
- `doRequestWithResponse``doRequest` 之上再于 Info 级别打印**完整原始响应**(同文件的 `zap.ByteString("data", data)`),会把识别出的付款人、金额与单号一并写入日志。
因此本能力改用 `Client.doRequestWithoutPayloadLog`:它与 `doRequest` 共用同一套序列化、AES-128-ECB 加密、MD5 签名与网络级重试流程(`executeWithRetry``logPayload` 分支),只在记录内容上分叉——仅记录路径、耗时与结果字节数摘要,响应由能力文件自行反序列化。既有能力的请求行为与日志语义保持原样,不受本能力影响。
识别结果不落库、不写入任何资金事实字段系统只在请求内向调用方返回支付单号供交易流水号表单预填。Gateway 返回的其余字段不进入本接口响应。
#### 识别结果到表单字段的映射
Gateway 识别响应含 `amount``order_number``payee``payment_method``payment_time``remark`,但本 Change 只消费和返回一个字段:
| 识别字段 | 表单字段 | 处置 |
| --- | --- | --- |
| `order_number` | 交易流水号(`external_transaction_no` | 唯一预填字段§17.1 的“交易流水号”即此值 |
| `amount` | 无 | 不预填、不返回 |
| `remark` | 无 | 不预填、不返回 |
| `payment_time` | 无 | 不预填、不返回 |
| `payment_method` | 无 | 不预填、不返回 |
| `payee` | 无 | 不预填、不返回 |
`payment_method` 不可映射到收款方式字典:它描述付款方使用的支付工具(如某银行信用购),而 §17.1 的收款方式是公司侧业务维护的收款账户类目。`payee` 也不可映射到付款方字段名意为收款人示例值为公司名称但外部文档描述写作“付款人”§17.1 的付款方口径则是“对应店铺”。即使未来扩大 OCR 返回字段,二者仍不得自动映射。
识别值仅作预填,实测可能不完整(上游对长号码存在只返回部分位数的情况,见 `docs/integrations/gateway/README.md` 的限制说明),必须由提交人人工核对后以人工确认值作为业务事实;系统不得将识别值当作可信的完整流水号使用,也不得据此做任何自动比对或防重。
### 7. 识别接口面向本 Change 场景,并做附件归属校验
新增 `POST /api/admin/agent-recharges/payment-voucher-ocr`,落在既有代理充值路由组(组级已拒绝企业账号),入参为附件对象键。创建申请前调用,因此不带充值单 ID。
接口对每个对象键校验对象存在且为非空,并以对象元数据判定为图片类型;非图片或对象不存在返回明确失败,不阻断人工填写。既有线下与核销路径只校验键的数量与长度、不校验归属,此处不扩大该缺口,但至少保证不因识别接口把任意对象内容回显为字段。
### 8. OCR 预填必须在创建申请前可人工更正
交易流水号是付款凭证识别的预填值,不是 OCR 写入的冻结值。提交人必须能在创建线下预存款申请前修改 `order_number` 预填的交易流水号;创建时保存人工确认后的 `external_transaction_no`,并按既有申请创建审计记录操作者与最终值。
本 Change 不新增在企业微信审批已提交后原地修改材料的语义:审批表单由 Worker 在提交时从 `tb_approval_instance.request_snapshot` 渲染(`internal/infrastructure/wecom/approval_submission_consumer.go:51-52`)。已被企业微信受理的表单不可由本地字段覆盖,否则本地记录与审批人看到的材料不一致。若后续需要“提交后修正且审批人看到修正值”,必须以新审批实例重提,届时再独立引入审批尝试记录与重提状态机,不能在本 Change 伪造同步修改。
## Risks / Trade-offs
- [识别返回的 `payment_method` 与收款方式字典语义不同] → 识别返回的支付方式描述的是付款方使用的支付工具如某银行信用购§17.1 的收款方式是公司侧业务维护的收款账户类目。设计明确二者不可互相自动映射:收款方式必须由提交人从字典中选择,识别值只可作提示,不得自动命中字典项。
- [重复交易流水号] → 交易流水号是人工确认的申报值而非系统去重键,不设唯一约束、不做跨记录防重,与员工账单能力对跨申请累计分摊不做系统防重的既有裁决一致;重复申报由企业微信终审人员以第三方记录核验。
- [base64 图片体积导致请求过大或超时] → 单次识别只接受单个附件键;复用既有 Gateway 超时配置与网络级重试;识别失败返回明确失败而不阻断人工填写。
- [识别接口放大对象读取面] → 校验对象存在、非空且为图片类型;仅返回识别字段,不返回对象内容;接口不承诺资源级附件鉴权,与既有附件契约一致。
- [字典引用判定扩展触及员工账单代码] → 只扩展引用查询范围,不改建账判据、核销申请、分摊或退款冲销;变更点集中在字典删除与改码两处校验。
- [修正主 spec 的可用方式 Requirement 可能与其他 Change 冲突] → 该 Requirement 描述的是商户池改造前的旧口径,与实际实现和归档商户池契约都冲突;本 Change 以 RENAMED改名+ MODIFIED全量替换同步保留既有四个场景名以免归档时丢弃场景并按商户池口径重写其内容。
## Migration Plan
新增线下充值申请、附件快照、审批关联及唯一约束的成对迁移;验证权限、回调重放、未知、驳回重提和 up/down/up。
1. 新增成对迁移(`.up.sql`/`.down.sql`)为 `tb_agent_recharge_record` 增加 `external_transaction_no`、收款方式三列与 `other_voucher_keys`,并为其补充注释与查询索引;不修改任何既有迁移。在线渠道列 `payment_transaction_id` 的定义与写入方保持不变。迁移编号在实施开始前按当时 `migrations/` 目录最大编号顺延,不预占。
2. 新增列的线下必填由应用层保证,数据库列保持可空以兼容历史行,使 down 迁移无需清理数据即可回滚。
3. 不新增业务类型、不扩展 `tb_wecom_approval_scene` 的业务类型 CHECK、不新增审批场景配置行本 Change 只扩展 `offline_recharge_approval` 场景的可映射业务字段白名单,需由超级管理员在既有场景配置中把新字段映射到企业微信模板控件后才在详情中可见。
4. 允许范围未配置时使用代码注册默认值,因此不存在需要迁移写入的基准数据;配置由超级管理员按需设置。
5. 按 ENG-TEST-001 在维护者指定的 `junhong_cmp_test` PostgreSQL 与 Redis DB 6 验证:迁移从本地工作区以显式 `DB_*` 执行 `scripts/migrate.sh`,只创建、删除本 Change 自己的 fixture禁止重置整库仅连接、迁移或实际行为失败时才阻塞对应场景。验证项见 tasks。
6. 回滚:仅在本 Change fixture 已清理、且维护者确认新增收款方式、交易流水号与凭证快照没有留存需求时,先停用新入口,再执行 down 删除新增列。允许范围配置行保留且不影响旧版本行为down 不得影响既有充值记录读取。存在仍需保留的真实新增列数据时,禁止将 down 作为正常回滚场景执行MUST NOT 通过重置整个 `junhong_cmp_test` 规避该边界。
## Open Questions
无。识别接口的字段规模、置信度与失败分级以 `docs/integrations/gateway/README.md` 的契约为准;契约变化时更新该文档,不影响本 Change 的规格与任务拆分。

View File

@@ -1,25 +1,43 @@
## Scope
- 迭代编号:`AUG26-017`
- 需求出处:
- `111.md` §25PRD-008-021 代理自充收款方式可配置)与讨论稿 §2.16 第 3 段:超管维护允许范围、与商户池方式取交集、只读交集、配置不影响已创建未支付单。
- 讨论稿 §2.16 第 4 段:代理充值记录新增交易流水号字段。
- `111.md` §17.1(预存款审批字段)与讨论稿 §2.1 第 5 点:预存款审批选择线下收款方式字典项并冻结名称快照;讨论稿 §0.1 已将该子节归入 AUG26-017。
- 讨论稿 §0 第 4 条与 §2.1 第 3 点OCR 仅作为交易流水号的预填来源,人工确认值才是业务事实。
## Why
代理自助充值需同时覆盖线上支付和可审计的线下转账,且不能将付款成功与钱包入账混淆
代理在线自充当前只能由商户池可用性推导可用方式,平台无法收窄代理可选范围;需求方要求超级管理员可配置"仅微信 / 仅支付宝 / 同时支持",并使代理实际可用方式等于该范围与商户池方式的交集。同时线下预存款审批缺少可维护的公司收款方式与交易流水号,无法按 §17.1 完整留存付款事实
## What Changes
- 新增代理自身店铺线上微信/支付宝充值
- 新增带凭证的线下转账申请及企业微信终审
- 固化支付回调、审批回调和钱包入账幂等
- 新增超级管理员可维护的代理在线自充允许方式,取值仅微信、仅支付宝、同时支持三种之一;修改记录操作者、前后值与时间
- 代理实际可用线上方式改为"允许范围 ∩ 当前可用商户池支付方式",查询返回该交集的有序 `wechat`/`alipay` 列表,不返回商户身份、凭证或允许范围本身;代理与平台账号均可查询
- 代理以某方式创建在线充值单时校验该方式在交集内;交集为空时查询返回空列表并拒绝创建,不回退任何历史支付配置。配置变更只影响后续新单,已创建未支付单保留其支付方式与商户路由快照
- 代理充值记录新增交易流水号,保存该笔充值对应的交易流水号。
- 线下预存款审批补齐收款方式(引用既有线下收款方式字典并冻结标识、编码、名称快照)、其他凭证与交易流水号,并接入 Gateway 付款凭证识别作为预填能力;识别结果不是资金事实。
## Capabilities
### New Capabilities
- `agent-self-recharge-payment`: 代理自助充值支付方式。
无。
### Modified Capabilities
- 无。
- `agent-funds-commission`:代理在线充值可用支付方式由"按当前生效支付配置判定"改为"按超级管理员允许范围与可用商户池方式的交集判定",并新增允许范围维护与审计、交集查询与创建门禁、充值记录交易流水号、线下预存款审批收款方式与其他凭证、付款凭证识别预填,以及线下收款方式字典对代理充值引用的删除与改码保护。
## Impact
影响代理主钱包、支付商户、企业微信审批、附件、审计和 Schema。
影响代理在线充值可用方式判定与创建门禁、代理充值记录 Schema、线下预存款企业微信审批场景的业务字段白名单与详情映射、受控系统配置注册、Gateway 外部集成、路由与 OpenAPI 文档。不影响 C 端个人客户能力、员工代收款账单建账判据与核销流程、套餐退款与原路退款。
## Non-Goals
- 不新增代理自助线下转账充值入口;代理商线下充值申请仍由平台账号经办,不改造其创建门禁、状态机或审批实例语义。
- 不为线下充值引入"驳回后可修改重提"或多审批实例的审批尝试记录模型。
- 不新建第二份线下收款方式字典,不建设收款账户目录。
- 不改造员工代收款核销申请的字段与流程,不改动员工账单建账与退款冲销判据。
- 不实现套餐退款、OCR 之外的第三方契约变更,不新增支付渠道能力。
- 不实现自动化测试(项目决策为 N/A

View File

@@ -0,0 +1,170 @@
## RENAMED Requirements
- FROM: `### Requirement: 代理在线充值可用支付方式按支付配置判定`
- TO: `### Requirement: 代理在线充值可用支付方式按允许范围与可用商户池判定`
## MODIFIED Requirements
### Requirement: 代理在线充值可用支付方式按允许范围与可用商户池判定
系统 SHALL 以超级管理员维护的允许范围与当前可用商户池支付方式的交集判定代理在线充值的实际可用支付方式。允许范围 MUST 仅取仅微信、仅支付宝、同时支持三者之一;商户池侧 MUST 沿用"该支付方式存在启用商户池且存在启用、凭证完整且当前适配器可用的商户成员"的既有判定。对外支付方式枚举 MUST 固定为 `wechat``alipay`MUST NOT 返回 `fuiou`、商户身份、凭证或允许范围本身。代理账号与平台账号均可查询该交集列表,查询 MUST 返回有序列且不因交集为空而报错;交集为空时创建对应方式的在线充值单 MUST 被拒绝且 MUST NOT 新建充值单或支付单MUST NOT 回退任何历史生效支付配置。代理以 `wechat` 创建在线充值单时,若命中的商户为富友服务商,系统 MUST 使用富友主扫统一下单并将返回的二维码链接作为支付链接。允许范围变更 MUST 只影响后续新单;已创建未支付充值单 MUST 保留其支付方式与商户路由快照。
#### Scenario: 富友配置完整时微信可用
- **GIVEN** 允许范围包含微信,且微信商户池存在启用、凭证完整的富友服务商商户成员
- **WHEN** 代理账号查询可用支付方式
- **THEN** 系统返回包含 `wechat` 的方式列表且不包含 `fuiou`
#### Scenario: 微信直连配置完整时微信可用
- **GIVEN** 允许范围包含微信,且微信商户池存在启用、凭证完整的微信直连商户成员
- **WHEN** 代理账号查询可用支付方式
- **THEN** 系统返回包含 `wechat` 的方式列表
#### Scenario: 支付宝字段完整时支付宝可用
- **GIVEN** 允许范围包含支付宝,且支付宝商户池存在启用、凭证完整的商户成员
- **WHEN** 代理账号查询可用支付方式
- **THEN** 系统返回包含 `alipay` 的方式列表
#### Scenario: 富友配置下微信创建走主扫下单
- **GIVEN** 允许范围包含微信,且微信商户池命中的商户为富友服务商且字段完整
- **WHEN** 代理账号以 `wechat` 创建在线充值单
- **THEN** 系统调用富友主扫统一下单并返回二维码链接作为支付链接,本地充值单支付方式为 `wechat`、支付渠道为 `fuiou`
#### Scenario: 允许范围收窄后代理只看到被允许的方式
- **GIVEN** 微信与支付宝商户池均存在可用成员
- **WHEN** 超级管理员将允许范围设置为仅支付宝,代理账号随后查询可用支付方式
- **THEN** 系统只返回 `alipay`
#### Scenario: 交集为空
- **GIVEN** 允许范围只包含微信,而当前微信商户池不存在可用成员
- **WHEN** 代理账号查询可用支付方式并尝试以 `wechat` 创建在线充值单
- **THEN** 查询返回空列表且不报错,创建被拒绝且不新建充值单或支付单,不发生任何历史配置回退
#### Scenario: 配置变更不影响已创建未支付单
- **GIVEN** 代理已创建待支付在线充值单并冻结了支付方式与商户路由快照
- **WHEN** 超级管理员修改允许范围
- **THEN** 该充值单的支付方式与商户快照保持不变,仅后续新单按新范围判定
#### Scenario: 平台账号可查询且看不到允许范围
- **GIVEN** 请求账号为平台账号
- **WHEN** 查询可用支付方式
- **THEN** 系统返回与代理一致的交集列表,且响应不含允许范围本身、商户身份或凭证
#### Scenario: 非超级管理员不能读取或修改允许范围
- **GIVEN** 请求账号为代理或平台账号
- **WHEN** 读取或修改全局允许范围
- **THEN** 系统拒绝访问且不返回允许范围取值
## ADDED Requirements
### Requirement: 代理自充允许方式配置可审计
系统 SHALL 允许超级管理员读取并修改代理在线自充允许方式,取值 MUST 仅限仅微信、仅支付宝、同时支持三种之一;每次成功修改 MUST 记录操作者、修改前后值与修改时间。其他角色 MUST NOT 读取或修改该允许范围。允许范围未被修改过时,系统 MUST 使用代码注册的安全默认值MUST NOT 因缺少配置而拒绝查询或创建。
#### Scenario: 超级管理员修改允许范围
- **GIVEN** 当前允许范围为同时支持
- **WHEN** 超级管理员将其修改为仅微信
- **THEN** 系统保存新值并记录操作者、修改前后值与修改时间
#### Scenario: 非法取值被拒绝
- **WHEN** 提交三种允许取值之外的任何值
- **THEN** 系统拒绝保存并保留原值
#### Scenario: 未配置时使用默认值
- **GIVEN** 允许范围从未被修改
- **WHEN** 查询实际可用支付方式
- **THEN** 系统按代码注册的默认允许范围计算交集,不因缺少配置而失败
### Requirement: 线下预存款审批收款方式、其他凭证与交易流水号
线下代理预存款/主钱包充值申请 SHALL 由提交人选择一项启用的线下收款方式字典项,并保存其标识、稳定编码与名称快照;字典项改名或停用 MUST NOT 改变历史申请已冻结的快照,已停用项 MUST NOT 可被新申请选取。申请 MUST 保存该笔充值对应的交易流水号,其取值来源为付款凭证识别出的支付单号,且 MUST 以人工确认或更正后的值为准;该系统字段 MUST 独立于在线支付由渠道返回的第三方交易号MUST NOT 因该值影响任何在线支付事实、钱包入账或幂等判定,且 MUST NOT 作为跨记录去重键或入账前置条件。申请 MUST 支持至少一个支付凭证,并 MUST 支持可选的其他凭证;两类凭证均只保存既有附件对象键引用。系统 MUST 拒绝引用不存在或已停用的收款方式创建申请MUST NOT 因未填写交易流水号而创建申请。
#### Scenario: 收款方式快照冻结
- **GIVEN** 一笔线下预存款申请引用了某启用的线下收款方式
- **WHEN** 超级管理员随后修改该字典项名称或将其停用
- **THEN** 历史申请仍展示提交时冻结的编码与名称,新申请不可再选择该已停用项
#### Scenario: 引用不存在或已停用的收款方式被拒绝
- **WHEN** 提交的收款方式字典项不存在或已停用
- **THEN** 系统拒绝创建申请且不保存审批实例
#### Scenario: 支付凭证与其他凭证分别留存
- **WHEN** 提交人上传支付凭证与若干其他凭证并创建申请
- **THEN** 系统分别保存支付凭证与其他凭证的对象键引用,且企业微信审批详情可取得两类凭证
#### Scenario: 交易流水号留存并可在审批详情取得
- **WHEN** 线下预存款申请创建成功
- **THEN** 该充值记录保存人工确认后的交易流水号,并可在企业微信审批详情中取得
#### Scenario: 缺少交易流水号时拒绝创建
- **WHEN** 提交人未填写或填写空白交易流水号
- **THEN** 系统拒绝创建申请且不增加钱包余额
#### Scenario: 交易流水号独立于在线渠道交易号
- **GIVEN** 一笔线下预存款申请已保存人工确认的交易流水号
- **WHEN** 向同一充值记录写入或修正该交易流水号
- **THEN** 在线支付由渠道返回的第三方交易号、支付渠道与支付状态均不受影响,在线充值的入账对账与幂等判定结果不变
#### Scenario: 相同交易流水号不被系统拒绝
- **WHEN** 两笔不同的线下预存款申请保存了完全相同的交易流水号
- **THEN** 系统均接受创建,不因该值重复而拒绝或去重
### Requirement: 付款凭证识别仅作交易流水号预填
系统 SHALL 提供付款凭证识别,以上传附件的对象键请求识别并只返回支付单号供交易流水号表单预填。识别结果 MUST NOT 写入任何资金事实字段;申请最终保存的交易流水号 MUST 以人工确认或更正后的提交值为准。支付金额、备注、付款人、支付方式与支付时间即使由 Gateway 返回MUST NOT 被本接口返回或自动预填。识别失败 MUST 返回可理解的失败结果且 MUST NOT 阻断人工填写与提交。系统 MUST NOT 在日志、审计或错误响应中记录凭证内容或识别原始结果。
#### Scenario: 识别成功仅返回交易流水号预填值
- **GIVEN** 提交人已上传一张图片类型的支付凭证并以其对象键请求识别
- **WHEN** 识别成功
- **THEN** 系统只返回识别出的支付单号供交易流水号预填,且此时未创建任何充值申请
#### Scenario: 识别结果被人工更正
- **GIVEN** 识别返回的支付单号与实际不符
- **WHEN** 提交人更正后提交申请
- **THEN** 系统保存更正后的值,识别原值不影响任何业务事实
#### Scenario: 识别失败不阻断人工填写
- **WHEN** 凭证非图片、对象不存在或识别服务失败
- **THEN** 系统返回明确失败,且提交人仍可人工填写全部字段后提交
#### Scenario: 日志不记录识别原始结果
- **WHEN** 付款凭证识别被调用并返回结果
- **THEN** 日志、审计与错误记录只包含业务标识与结果摘要,不包含凭证内容或识别原始结果
### Requirement: 线下收款方式字典对代理充值引用的可见性
系统 SHALL 使线下收款方式字典的引用保护覆盖引用它的代理充值申请:已被任一代理充值申请引用的字典项 MUST NOT 被物理删除MUST 只允许停用,且 MUST 保留历史申请的名称快照。字典项稳定编码在存在引用后 MUST NOT 被修改。
#### Scenario: 被代理充值引用的字典项不可删除
- **GIVEN** 某线下收款方式字典项已被至少一笔代理充值申请引用
- **WHEN** 超级管理员请求删除该字典项
- **THEN** 系统拒绝删除并返回只能停用的稳定错误
#### Scenario: 被代理充值引用的字典项编码不可修改
- **GIVEN** 某线下收款方式字典项已被至少一笔代理充值申请引用
- **WHEN** 超级管理员请求修改该字典项的稳定编码
- **THEN** 系统拒绝修改编码,但允许修改名称、排序、启停与备注

View File

@@ -1,18 +0,0 @@
## ADDED Requirements
### Requirement: 代理自助充值支付方式
超级管理员 SHALL 维护代理在线自充允许方式:仅微信、仅支付宝或微信和支付宝;代理实际可用线上方式为该允许范围与当前可用对应商户池方式的交集,交集为空时拒绝创建线上充值单。平台用户和代理仅可查询实际可用方式,不得查看或修改允许范围。配置变更只影响后续新单,已创建未支付充值单保留其支付方式及商户快照。系统 SHALL 允许已启用代理在其自身店铺充值入口选择实际可用的线上微信、线上支付宝或线下转账;不得为下级店铺代充。线上方式创建支付单并按实际商户路由,渠道成功回调幂等增加该代理主钱包余额;失败、关闭或未知不得增加余额,未知结果通过既有支付查询/回调恢复,不允许重复支付单入账。
线下转账申请必须填写转账金额、付款人、转账时间、银行/支付渠道、流水号和凭证附件;创建后状态为待企业微信审批,不立即入账。企业微信通过时在事务内锁定申请并仅一次增加主钱包余额;驳回、关闭或撤回不入账,代理可修改未成功申请后以新审批实例重提。充值金额以分保存,展示元时两位小数;平台/超级管理员仅可查看和处理其既有数据范围。
#### Scenario: 配置与商户池交集为空
- **WHEN** 超级管理员允许一种线上方式,但该方式没有可用商户池成员
- **THEN** 代理可用线上方式列表不含该方式,创建该方式充值单被拒绝,已创建未支付单不受影响
#### Scenario: 重复线上成功回调
- **WHEN** 同一线上充值支付成功回调被重复投递
- **THEN** 系统只增加一次代理主钱包余额并保留幂等支付事实
#### Scenario: 线下申请审批驳回
- **WHEN** 企业微信驳回线下转账充值申请
- **THEN** 系统不增加钱包余额,并允许代理修改申请后创建新审批实例重提

View File

@@ -1,8 +0,0 @@
## ADDED Requirements
### Requirement: 代理自充支付方式
系统 SHALL 使代理在线自充可用支付方式等于超级管理员维护的允许范围与当前启用商户池支付方式的交集;交集为空时拒绝创建新充值单。配置变更仅影响后续订单,既有未支付订单保留其支付方式和商户快照。
#### Scenario: 规则命中
- **WHEN** 业务请求或任务满足本需求定义的前置条件
- **THEN** 系统按上述规则完成处理、保留可追溯事实,并拒绝与状态、权限或幂等约束冲突的重复操作

View File

@@ -1,9 +1,41 @@
## 1. 充值实现
- [ ] 1.1 追踪代理钱包、线上支付、商户路由、线下附件和企业微信审批链路。
- [ ] 1.2 新增线下申请/快照/审批关联的成对迁移、模型、状态和幂等约束。
- [ ] 1.3 实现自身店铺门禁、线上支付创建/成功入账恢复、线下申请/企微终审/重提和钱包事务审计。
- [ ] 1.4 注册路由、OpenAPI及代理/平台查询数据范围。
## 1. 允许范围与交集
## 2. 验证
- [ ] 2.1 隔离库验证支付方式、越权代充、重复回调、未知恢复、线下驳回重提和 up/down/up
- [ ] 2.2 运行 `gofmt -w``go build ./cmd/api ./cmd/worker``go run cmd/gendocs/main.go``openspec validate add-agent-self-recharge-payment-methods --strict``openspec doctor --json`;自动化测试按项目决策为 N/A。
- [x] 1.1 在 `pkg/constants/system_config.go` 新增代理在线自充允许方式的配置 Key 与三个稳定取值常量,默认值为同时支持;在 `internal/bootstrap` 按既有支付方式配置注册该 Key使用 `ValueType=string``EnumValues` 约束取值。
- [x] 1.2 实现允许范围的读取封装:以 `systemconfig.Reader.GetStrict` 读取并把取值映射为允许的 `wechat`/`alipay` 集合,配置未落库时使用代码注册默认值,配置非法时失败关闭
- [x] 1.3 在 `internal/application/agentrecharge/online_creation.go``AvailablePaymentMethods` 中把可见角色从仅代理放开为代理与平台账号,并将返回方式改为"允许范围 ∩ 商户池可用方式"的有序列表;交集为空时返回空列表而不报错。
- [x] 1.4 在 `OnlineCreationService.Execute` 的域校验之后、读取店铺事实之前加入允许范围校验,拒绝不在交集内的支付方式,且不新建充值单或支付单;确认无可用商户时既有 `CodeNoPaymentConfig` 失败路径保持不回退历史支付配置。
- [x] 1.5 新增 `GET /agent-self-recharge-payment-methods`(代理与平台账号可查实际可用方式)与 `PUT /agent-self-recharge-payment-methods`(仅超级管理员,复用受控配置写服务以获得前后值审计),注册为 `/api/admin` 下的独立单段路由组并在组内显式声明读写角色门禁(需求路径与既有 `/api/admin/agent-recharges` 前缀不同,无法继承该组的企业账号门禁),并确保响应不包含允许范围、商户身份或凭证。
## 2. 线下预存款审批字段与 Schema
- [x] 2.1 新增成对迁移为 `tb_agent_recharge_record` 增加 `external_transaction_no``offline_payment_method_id``offline_payment_method_code``offline_payment_method_name``other_voucher_keys` 列(含注释与查询索引),保持可空以兼容历史行;不修改任何既有迁移,不新增唯一约束,不改动既有 `payment_transaction_id` 的定义与写入方。
- [x] 2.2 扩展 `OfflineCreationCommand` 与线下创建用例:接收并校验启用的线下收款方式字典项(不存在或已停用即拒绝)、必填的交易流水号(取值来自识别支付单号并经人工确认)与可选其他凭证,在同一事务保存收款方式三列快照、交易流水号与其他凭证对象键,并保持既有审批实例关联与审计不变;交易流水号不得参与去重或入账前置判定。
- [x] 2.3 确认交易流水号写入路径与在线渠道事实隔离:`external_transaction_no` 的写入 MUST NOT 触碰 `payment_transaction_id``payment_channel`、支付状态与钱包,且不影响 Outbox 入账消费者的对账判定。
- [x] 2.4 扩展 `internal/application/wecom/scene.go``offline_recharge_approval` 的可映射业务字段白名单,加入线下收款方式与其他凭证字段,并在审批请求快照中带入对应值;不修改其他场景的字段定义与业务类型 CHECK。
- [x] 2.5 扩展线下收款方式字典的"被引用"判定,使已被代理充值申请引用的字典项不可物理删除且编码不可修改,仅可停用;不改变员工账单建账、核销申请、分摊与退款冲销逻辑。
- [x] 2.6 在代理充值详情与列表投影中返回收款方式快照、交易流水号与其他凭证对象键引用,且不返回凭证内容。
## 3. 付款凭证识别预填
- [x] 3.1 在 `internal/gateway` 新增付款凭证识别能力,封装识别接口并按需传入 `image_base64`;不使用会打印完整响应体的泛型入口,改为自行反序列化并只记录路径、耗时与结果摘要。
- [x] 3.2 实现应用层识别用例:按附件对象键读取对象存储、校验对象存在非空且为图片类型、编码为 base64 后调用 Gateway只将 `order_number` 返回为交易流水号预填值;`amount``remark``payment_method``payee``payment_time` 均不返回、不预填、不写入任何业务字段。识别结果不落库、不写入任何资金事实字段。
- [x] 3.3 新增 `POST /agent-recharges/payment-voucher-ocr` 路由与 Handler复用既有代理充值组门禁成功响应只含交易流水号预填值非图片、对象不存在或识别服务失败时返回明确失败且不阻断人工填写。
- [x] 3.4 确认识别调用路径的日志、审计与错误不包含凭证内容、base64 载荷或识别原始结果。
## 4. 路由、文档与装配
- [x] 4.1 为新增的两个端点补齐可执行路由注册、`cmd/api/docs.go``cmd/gendocs/main.go` 的占位装配,以及 `pkg/openapi/handlers.go``internal/bootstrap/types.go``internal/bootstrap/handlers.go` 的 Handler 装配(沿用既有代理充值 Handler 时确认无需新增装配点)。
- [x] 4.2 按 ENG-DTO-001 补齐请求与响应 DTO 的中文 description、枚举与 `pkg/constants` 一致,金额使用分,时间使用带时区 RFC3339。
- [x] 4.3 更新 `docs/integrations/gateway/README.md` 的当前实际使用范围与核验证据,纳入付款凭证识别能力;不得写入凭证、密钥或识别样本。
- [x] 4.4 为允许范围修改复用既有受控配置审计动作并在 Change 内确认审计覆盖;新增的申请创建与识别调用按 ENG-AUDIT-001 归入既有事实类型,不新增重复事实。
## 5. 验证
- [x] 5.1 按 ENG-TEST-001 在维护者指定的 `junhong_cmp_test` PostgreSQL 与 Redis DB 6 上验证:迁移从本地工作区以显式 `DB_*` 执行 `scripts/migrate.sh`,只创建、删除本 Change 自己的 fixture禁止重置整库。
- [x] 5.2 验证允许范围:三态取值可维护并留痕、非法取值被拒、未配置时使用默认值、非超级管理员不可读不可写。
- [x] 5.3 验证交集:允许范围收窄后查询只返回被允许方式;允许方式无可用商户池成员时查询返回空列表且创建被拒绝、不新建充值单或支付单;配置变更后已创建未支付单的支付方式与商户快照不变。
- [x] 5.4 验证线下字段:引用不存在或已停用收款方式被拒、收款方式快照在改名与停用后保持冻结、缺少交易流水号被拒、重复交易流水号不阻断线下申请、企业微信审批或钱包入账,且不改变 `payment_transaction_id``payment_channel`、支付状态、钱包或 Outbox 入账消费者的对账判定;支付凭证与其他凭证分别留存并可在审批详情取得。
- [x] 5.5 验证识别预填:图片凭证识别成功后返回预填值且未创建申请、人工更正值生效、非图片与识别失败不阻断人工填写、日志与审计不含凭证内容或识别原始结果。
- [x] 5.6 验证字典引用保护:被代理充值引用的字典项不可删除、编码不可修改、仅可停用。
- [ ] 5.7 执行 `gofmt -w``go build ./cmd/api ./cmd/worker``go run cmd/gendocs/main.go``./scripts/context-health.sh``openspec validate add-agent-self-recharge-payment-methods --strict``openspec doctor --json`;上述全局健康门禁必须一并通过,自动化测试按项目决策为 N/A。
- [x] 5.8 仅在本 Change fixture 已清理、且维护者确认新增收款方式、交易流水号与凭证快照没有留存需求时验证迁移 up/down/updown 可以删除新增列,但不得影响既有充值记录读取。存在仍需保留的真实新增列数据时不得将 down 视为正常可执行场景MUST NOT 重置整个 `junhong_cmp_test`