Files
junhong_cmp_fiber/openspec/specs/agent-distribution-withdrawal/spec.md
break 5ed6b39deb
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
feat(收口): 补齐 8 月迭代缺口并同步 Spec 与证据链
- 新增六对成对迁移 000232–000237:H5 弹窗类型、退款结算标识与申请人备注、优先轮询事实字段与两个新终态、通道阈值命中留痕、手机号最近解绑人、提现资格校验留痕
- 退款:原因必填与申请人备注、来源支付与渠道流水冻结、线下处理流水号补录审计、按订单查询可选退款方式、企微审批材料补齐且新增字段缺失映射即明确失败
- 优先轮询:人工关闭、有效期到期独立周期任务、失败与过期人工重触发、事实字段与异常重试查询、资产解析端点只读投影
- 通道阈值:命中事实同事务留痕与命中记录查询;员工账单:列表筛选与详情投影;商户池:列表投影与统计周期语义;H5:弹窗类型与类别排序
- 手机号:有效关联数量与最近解绑人、短信验证码失败次数限制;导出:佣金明细十五列与报表序号列
- 时间筛选:三处新增筛选纳入统一严格解析契约,员工账单产生时间参数改名
- 同步 12 份主 Spec 需求、两端点与异步任务证据链,门禁 context-health 与 OpenSpec 校验通过
2026-09-18 15:34:29 +08:00

265 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# agent-distribution-withdrawal Specification
## Purpose
使代理下级注册和佣金提现均以企业微信审批、资格有效性和不可变业务快照为准,避免代理层级或资金事实因资料变更、重复回调而漂移。
## Requirements
### Requirement: 分销码与待审批代理注册
系统 SHALL 在每个代理店铺创建时生成全局唯一、不可修改的随机分销码;二维码仅编码 H5 注册入口和该码,二维码渲染与 H5 页面不属于本能力。分销码 MUST NOT 支持人工指定或编辑。
系统 SHALL 提供公开后端接口 `POST /api/c/v1/agent-distribution-registrations`不要求登录、JWT、角色或权限。请求 MUST 携带有效 `distribution_code`、短信已验证手机号、密码及既有注册必填资料;系统 MUST 复用既有短信验证码校验与限流规则,并 MUST 保证同一验证码至多被消费一次。系统 MUST 为该次申请创建唯一的待审批注册记录,并 MUST NOT 在审批通过前创建店铺、代理账号、钱包或上下级归属。
系统 MUST 在创建注册记录与审批实例前校验注册关键字段手机号、用户名、店铺编号的唯一性任一字段与既有未删除账号或店铺冲突时MUST 按字段返回各自可定位的错误码与提示(手机号已存在、用户名已存在、店铺编号已存在);任一字段与其它待审批(`status=0`注册记录冲突时MUST 返回资源冲突错误并指明冲突字段。关键字段冲突时 MUST NOT 创建注册记录或审批实例MUST NOT 消费短信验证码,客户修正资料后 MUST 能用同一验证码重试。
已驳回(含企业微信通过后撤销按驳回处理)与已通过的注册记录 MUST NOT 阻止同一手机号、用户名或店铺编号再次提交;状态为终态的历史注册记录只作为历史事实保留。同一关键字段的并发提交 MUST 被串行裁决同一手机号、用户名或店铺编号至多存在一条待审批注册记录MUST NOT 出现两条指向同一关键字段的待审批申请。店铺名称不参与唯一性校验。
分销码无效、分销码所属店铺已停用、上级店铺缺少启用的主账号、短信验证码无效或已被消费时,系统 MUST NOT 创建注册记录或审批实例,且 MUST 按失败原因返回各自可定位的错误码与提示MUST NOT 统一为同一不可用结果。短信验证码 MUST 在注册记录与审批实例落库成功后才消费;落库前的任何失败 MUST NOT 消费验证码,客户 MUST 能用同一验证码直接重试。分销码所属店铺停用 MUST NOT 级联变更既有下级与既有佣金关系。
注册审批 MUST 使用业务类型 `agent_distribution_approval`,其业务标识 MUST 为待审批注册记录主键。企业微信最终通过时,系统 MUST 在同一事务内创建启用店铺、代理账号、所需钱包,写入上级店铺、初始业务员快照,标记注册记录已通过并记录审计;创建店铺与代理账号前 MUST 复检注册关键字段未被既有账号或店铺占用,被占用时 MUST 整体回滚并返回可定位冲突错误MUST NOT 创建半套实体。最终驳回时 MUST NOT 创建店铺、账号、钱包或层级,仅保留注册记录与审批结果。
同一手机号在驳回后再次扫码 MUST 形成新的注册记录与新的审批实例;重复或乱序回调 MUST NOT 重复创建账号、层级或钱包。
#### Scenario: 扫码注册进入待审批
- **WHEN** 客户使用有效分销码提交已验证手机号、密码与注册必填资料,且手机号、用户名、店铺编号均未被占用
- **THEN** 系统仅创建一条待审批注册记录及企业微信审批实例,不创建店铺、账号或钱包
#### Scenario: 关键字段与既有账号或店铺重复
- **WHEN** 客户提交的注册手机号、用户名或店铺编号已存在未删除的账号或店铺
- **THEN** 系统不创建注册记录与审批实例,按冲突字段返回对应错误码与提示(手机号已存在 / 用户名已存在 / 店铺编号已存在),且不消费该次短信验证码
#### Scenario: 关键字段与其它待审批申请重复
- **WHEN** 客户提交的注册手机号、用户名或店铺编号已存在另一条待审批注册记录
- **THEN** 系统不创建新的注册记录与审批实例,返回资源冲突错误并指明冲突字段,且不消费该次短信验证码
#### Scenario: 驳回后重新注册
- **WHEN** 同一手机号、用户名或店铺编号的上一次申请已被企业微信驳回(含通过后撤销)
- **THEN** 系统允许再次提交并创建新的待审批注册记录与新的审批实例,不复用也不改写既有终态记录
#### Scenario: 并发重复提交
- **WHEN** 同一手机号、用户名或店铺编号在并发请求中被同时提交
- **THEN** 系统至多创建一条待审批注册记录,其余请求返回资源冲突错误
#### Scenario: 停用代理码注册
- **WHEN** 客户使用已停用代理所属店铺的分销码注册
- **THEN** 系统拒绝创建注册记录与审批实例,且不产生任何店铺或账号
#### Scenario: 验证码无效或已消费
- **WHEN** 请求携带的短信验证码无效、过期或已被消费
- **THEN** 系统拒绝创建注册记录,且不消耗该分销码的注册名额
#### Scenario: 落库前失败不消耗验证码
- **WHEN** 短信验证码校验通过后,注册记录或审批实例创建失败(含关键字段冲突)
- **THEN** 系统保留该验证码,客户可用同一验证码重试,重试得到的仍是同一失败原因而非验证码失效
#### Scenario: 审批通过建立层级与业务员快照
- **WHEN** 企业微信最终通过一条待审批注册记录,且该记录的手机号、用户名与店铺编号仍未被占用
- **THEN** 系统在同一事务内创建启用店铺与代理账号、写入分销码所属店铺为直接上级、复制上级当时业务员为初始业务员,并标记注册记录已通过
#### Scenario: 审批通过前关键字段已被占用
- **WHEN** 企业微信通过一条待审批注册记录,但其手机号、用户名或店铺编号已被并发创建的账号或店铺占用
- **THEN** 系统整体回滚本次建店建号,返回可定位的冲突错误,注册记录保持待审批且不产生半套实体
#### Scenario: 审批驳回不产生实体
- **WHEN** 企业微信最终驳回一条待审批注册记录
- **THEN** 系统不创建店铺、账号、钱包或层级,且同一手机号、用户名或店铺编号可再次扫码形成新的注册记录
#### Scenario: 重复回调不重复建层级
- **WHEN** 同一注册审批终态被重复投递
- **THEN** 系统至多创建一次店铺、账号、钱包与层级关系
### Requirement: 提现资料资格
代理首次提现前 SHALL 提交企业微信资料资格申请;合同和法人身份证正反面必填,企业代理填写统一社会信用代码、个人代理填写法人身份证号。合同资格主体 MUST 填写统一社会信用代码或身份证号;营业执照、门头照可选,发票仅企业可选且其抬头/统一社会信用代码 MUST 与合同主体一致。审批通过且资料未过期才有效;合同和身份证通过后长期有效,直到代理替换资料、超级管理员作废或代理停用。
系统 SHALL 以不可变的资料版本保存资格事实与附件引用,历史版本、附件与审批实例 MUST NOT 被覆盖。审批 MUST 使用业务类型 `withdrawal_qualification_approval`,其业务标识 MUST 为资料版本主键。代理替换合同或法人身份证 MUST 新增资料版本并创建新的审批实例,同时在同一事务内使旧有效版本失效;代理 MUST 重新审批通过后才可提现。系统 MUST NOT 为资格资料提供同一版本的重新提交或重提状态机。
超级管理员作废资格 MUST 填写原因;代理停用 MUST 自动使该店铺全部有效资格失效。
资格申请、资格作废与后台查询 MUST 沿用既有认证、自身店铺或数据范围校验MUST NOT 公开。
#### Scenario: 资料替换后提现
- **WHEN** 有效资格的代理替换合同或法人身份证资料
- **THEN** 旧有效版本立即失效,新版本进入企业微信审批,代理重新审批通过后才可提现
#### Scenario: 历史版本不被覆盖
- **WHEN** 同一店铺存在多个资料版本
- **THEN** 每个版本保留各自的附件引用、审批实例与审批结果,历史版本不被改写
#### Scenario: 超级管理员作废资格
- **WHEN** 超级管理员填写原因后作废某有效资格
- **THEN** 该资格失效且原因可查询,代理提现被拒绝直至重新审批通过
#### Scenario: 代理停用使资格失效
- **WHEN** 代理店铺被停用
- **THEN** 该店铺全部有效资格失效,历史版本与审批结果保留
#### Scenario: 未认证或越权访问资格接口
- **WHEN** 未认证请求,或请求的店铺超出当前账号数据范围
- **THEN** 系统拒绝访问且不披露资格是否存在
### Requirement: 提现冻结与企业微信终审
提现申请 SHALL 先校验申请店铺存在有效资料资格,再冻结可提现余额、金额、手续费、收款信息和可选发票快照,并创建企业微信审批。余额不足、资格无效或非本人代理时,系统 MUST NOT 创建提现申请、审批实例或任何冻结。发票为申请级材料,若上传 MUST 按当时有效合同主体校验并冻结至该次审批快照。本地 MUST NOT 提供人工通过或驳回终审。审批 MUST 使用业务类型 `commission_withdrawal_approval`
提现申请 SHALL 记录本次提现所依据的有效资料资格版本标识与当次校验结果,校验结果至少包含校验时间、是否通过,以及未通过时的稳定原因(资格不存在、已失效、已停用或资料未通过审批)。校验通过时,该记录 MUST 随每次提交或重提冻结进当次审批尝试快照,并在提现详情可查询(此处「审批材料」指本地不可变审批尝试快照,本能力 MUST NOT 要求新增企业微信模板控件),详情返回的资格校验结果与快照一致:通过时未通过原因为空字符串;上线前产生、无资格校验留痕的历史尝试按「无留痕」返回(版本标识为 0、校验时间为空、是否通过为 0、未通过原因为空字符串。校验未通过时系统 MUST 按既有资料资格校验的拒绝语义处理MUST NOT 创建提现申请、审批实例与审批尝试,因此该情形 MUST NOT 产生带未通过原因的审批尝试快照,未通过稳定原因 MUST 记录在该次拒绝的结果与审计事实中。两种情形均 MUST NOT 改写历史尝试的校验结果。该记录 MUST NOT 改变既有冻结、扣减、释放、驳回重提与通过后撤销的任何语义。
每次提交或重提 MUST 新增一条不可变的审批尝试记录冻结该次金额、手续费、实际到账、收款信息与申请级发票快照并为其创建独立的审批实例提现申请行只保存最新审批实例标识用于列表投影MUST NOT 作为历史事实来源。企业微信通过即视为已到账:系统 MUST 保持 `WithdrawalStatusApproved`2并在同一事务内仅一次从冻结余额扣减同时写入到账时间MUST NOT 使用已到账状态值 4。
企业微信驳回时,系统 MUST 仅一次释放本次尝试的冻结余额并记录释放时间;`cancelled``deleted` MUST 按驳回同等处理。驳回后代理可修改金额、收款信息和本次发票重新提交,每次新建审批尝试记录与审批实例;资料资格保持有效。企业微信对已通过申请撤销时,系统 MUST NOT 回滚已到账金额、MUST NOT 重新冻结MUST 保持通过状态并写入正交的异常标记与原因供详情展示与人工处理MUST NOT 自动重提。
重复、乱序或结果未知的回调 MUST NOT 造成重复扣减、重复释放或第二笔冻结;结果未知时申请保持在途并由既有查询恢复机制收敛。
#### Scenario: 资格无效或余额不足不创建提现
- **WHEN** 代理店铺不存在有效资料资格、资格已失效,或可提现余额不足
- **THEN** 系统拒绝创建提现申请,不创建审批实例且不冻结任何余额
#### Scenario: 提现审批通过
- **WHEN** 企业微信对一笔待审提现返回最终通过且该结果首次被消费
- **THEN** 系统仅一次从冻结余额扣减,申请保持已通过状态并记录到账时间
#### Scenario: 提现审批驳回
- **WHEN** 企业微信最终驳回一笔提现申请
- **THEN** 系统仅一次释放该次尝试的冻结余额并记录释放时间,保留审批快照,并允许在资格仍有效时修改后重提
#### Scenario: 驳回后重提
- **WHEN** 代理修改金额、收款信息或本次发票后重新提交被驳回的提现申请
- **THEN** 系统新增审批尝试记录并创建新的审批实例,历史尝试、快照与审批结果不被覆盖
#### Scenario: 企微取消或删除
- **WHEN** 企业微信对待审提现返回取消或删除
- **THEN** 系统按驳回同等处理,仅一次释放冻结余额并记录释放时间
#### Scenario: 通过后撤销
- **WHEN** 企业微信对已通过的提现申请返回通过后撤销
- **THEN** 系统不回滚已到账金额、不重新冻结、不自动重提,仅写入异常标记与原因供详情展示
#### Scenario: 重复回调不重复扣减
- **WHEN** 同一审批终态被重复投递或乱序到达
- **THEN** 系统至多扣减或释放一次,不产生第二笔冻结
#### Scenario: 资料校验结果可追溯
- **WHEN** 代理在有效资料资格下提交提现申请,随后该资格被替换
- **THEN** 该次提现尝试仍返回提交时冻结的资格版本标识与校验结果,不被新版本改写
#### Scenario: 资格失效时的校验原因
- **GIVEN** 代理店铺资料资格已被超级管理员作废
- **WHEN** 代理发起提现并被拒绝
- **THEN** 拒绝结果与审计记录包含稳定原因(资格已失效),且不创建提现申请、审批实例或冻结
### Requirement: 提现本地人工终审边界
系统 SHALL 拒绝本地人工通过与驳回已关联审批实例的提现申请;该判定 MUST 以申请记录中是否已关联审批实例为准MUST NOT 依赖任何新增配置开关。本地接口拒绝时 MUST 返回稳定的状态冲突错误且 MUST NOT 改变申请状态、余额或流水。
`approval_instance_id` 为空的存量提现申请 MUST 保持既有本地人工处理行为不变;其他审批场景的本地历史语义 MUST NOT 被本能力改变。
#### Scenario: 新提现申请拒绝本地终审
- **WHEN** 平台账号对已关联企业微信审批实例的提现申请调用本地通过或驳回接口
- **THEN** 系统返回状态冲突、申请状态与钱包余额不变
#### Scenario: 存量提现保持兼容
- **WHEN** 本地通过或驳回接口作用于从未关联审批实例的存量提现申请
- **THEN** 系统按既有规则完成扣减或释放,行为与本次变更前一致
### Requirement: 待审提现与佣金回溯的释放接缝
系统 SHALL 保证后续佣金回溯处理待审提现时,每个未结算审批尝试的冻结余额可被幂等释放:释放金额 MUST 取该尝试的金额事实,释放完成 MUST 写入该尝试的释放时间事实,且释放 MUST 先于同一店铺佣金余额扣减完成。已释放的尝试 MUST NOT 被重复释放,重复处理 MUST NOT 重复增加可提现余额。
#### Scenario: 回溯先释放后扣减
- **WHEN** 佣金回溯处理存在待审提现的店铺
- **THEN** 系统先释放每个未结算尝试的冻结余额并记录释放时间,再扣减佣金余额
#### Scenario: 重复释放不重复入账
- **WHEN** 同一未结算尝试的释放被重复执行
- **THEN** 系统至多释放一次,冻结余额与可提现余额不被重复调整
### Requirement: 注册审批材料带出业务员
代理注册审批材料 SHALL 在既有字段之外带出「业务员」,取上级代理店铺当前业务员的名称快照,用于审批人在企业微信侧确认将同步给新代理的初始业务员。上级店铺当前无业务员时该字段 MUST 以空值提交并在材料中标记为无MUST NOT 以提交人、上级店铺主账号或其它账号填充。该字段 MUST 为只读快照审批通过时写入新店铺的初始业务员仍按既有规则取上级业务员当时值MUST NOT 因审批材料快照与通过时值不同而改写通过结果或回滚。企业微信模板控件缺失导致该字段无法提交时 MUST 明确失败并提示需配置控件MUST NOT 静默丢弃。
#### Scenario: 审批材料展示将同步的业务员
- **GIVEN** 上级代理店铺存在启用的业务员
- **WHEN** 创建代理注册审批实例
- **THEN** 审批材料包含该业务员名称快照
#### Scenario: 上级无业务员
- **GIVEN** 上级代理店铺当前没有启用业务员
- **WHEN** 创建代理注册审批实例
- **THEN** 业务员字段以空值提交并标记为无,不使用其它账号填充
#### Scenario: 快照与实际同步值不同
- **GIVEN** 审批材料创建后上级店铺业务员发生变更
- **WHEN** 企业微信最终通过该注册申请
- **THEN** 新店铺初始业务员按通过时上级业务员写入,历史审批材料快照不被改写
### Requirement: 店铺下级代理数量投影
店铺列表与详情 SHALL 返回该店铺的直接下级代理数量,口径为未删除且上级店铺标识等于该店铺的店铺数。数量 MUST 一次批量聚合完成MUST NOT 逐店查询放大查询次数。数量 MUST 受既有店铺数据范围约束:范围外店铺按不可见处理。该字段 MUST 只读MUST NOT 影响上下级关系、佣金关系、通知范围或任何既有写入语义。
#### Scenario: 展示下级数量
- **GIVEN** 某代理店铺存在三家直接下级店铺
- **WHEN** 查询该店铺详情或列表
- **THEN** 下级代理数量返回三
#### Scenario: 无下级返回零
- **WHEN** 查询一家没有任何直接下级的店铺
- **THEN** 下级代理数量返回零而非空值
#### Scenario: 数据范围外不泄露
- **WHEN** 平台用户查询其数据范围外店铺
- **THEN** 系统不返回该店铺及其下级数量
## 可达操作索引
本节只用于入口导航,不是行为 Requirement业务义务以上述 Requirements 为准。
### 代理分销注册
`POST /api/c/v1/agent-distribution-registrations`(代理扫码注册,公开接口)。
### 提现资料资格
`POST /api/admin/shops/{shop_id}/withdrawal-qualifications`(提交提现资料资格);`GET /api/admin/shops/{shop_id}/withdrawal-qualifications`(查询提现资料资格版本);`POST /api/admin/withdrawal-qualifications/{id}/void`(作废提现资料资格)。
### 佣金提现
`GET /api/admin/shops/{shop_id}/withdrawal-requests`(代理商提现记录);`POST /api/admin/shops/{shop_id}/withdrawal-requests`(发起提现申请);`PUT /api/admin/shops/{shop_id}/withdrawal-requests/{id}`(重提被驳回的提现申请);`GET /api/admin/shops/{shop_id}/withdrawal-requests/{id}`(提现申请详情);`POST /api/admin/commission/withdrawal-requests/{id}/approve`(本地人工通过提现,仅存量无审批实例申请);`POST /api/admin/commission/withdrawal-requests/{id}/reject`(本地人工驳回提现,仅存量无审批实例申请)。