Files
junhong_cmp_fiber/openspec/specs/asset-auto-renewal/spec.md
break d52be16802
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 10m15s
feat(资产钱包自动续费): 新增全局配置、每日扫描续购与可靠复机
- 新增单行配置表 tb_asset_auto_renewal_config 与尝试记录表 tb_asset_auto_renewal_attempt(迁移 000229/000230)
- 每日按上海自然日扫描,窗口内以同一资产钱包可用余额续购当前主套餐,资金/订单/套餐/审计同一事务闭合
- 唯一键保证每资产每日至多一次尝试,占位中断由后续扫描收敛,当日不重试
- 四类失败原因向客户与店铺各投递每日至多一条站内通知,并注册通知类型与个人客户白名单
- 续费成功后按条件经 Outbox 可靠投递复机,新增恢复扫描只查询回填,不使用即发即弃调用
- 配置读写仅超级管理员与平台账号,保存记录操作者、前后值快照并登记统一审计
- tasks 7.1–7.15 全部验证通过(本机隔离 PostgreSQL/Redis,零外部渠道调用)
2026-09-17 16:39:26 +08:00

169 lines
14 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.
# 资产钱包自动续费当前行为
## Purpose
在套餐最终到期前的受控窗口内,仅以同一资产钱包可用余额自动续购当前有效主套餐,并使配置、每日尝试、资金闭合、通知与复机失败各自具有明确且可恢复的边界。
## Requirements
### Requirement: 自动续费配置与权限
系统 SHALL 只维护一份有效的全局自动续费配置,包含总开关、适用范围(全部主套餐或指定主套餐)、指定主套餐集合与到期前统一天数(正整数,上限 90。仅超级管理员与平台账号 SHALL 能读取或修改配置;代理、企业、个人客户 MUST NOT 有任何读取或修改入口,无权限与目标不存在 MUST NOT 形成可枚举差异。每次保存 SHALL 记录操作者、修改前后值快照与时间,并与配置保存同事务生效。指定范围时集合 MUST 非空且仅能选择当前可售主套餐;全部范围时集合 MUST 为空。每次保存 SHALL 递增配置版本;尝试记录 SHALL 保留触发时的配置版本快照。配置变更只影响后续扫描:已产生的尝试记录 MUST NOT 重算;关闭总开关后 MUST NOT 创建新的尝试或订单,既有尝试记录与通知保留。
#### Scenario: 无权限读取或修改配置
- **WHEN** 代理、企业或个人客户请求读取或修改自动续费配置
- **THEN** 系统拒绝请求、配置不变,且不产生任何执行任务或通知
#### Scenario: 关闭总开关后不再执行
- **WHEN** 总开关关闭后执行每日扫描
- **THEN** 系统不创建新的尝试记录、不创建订单、不扣款,既有尝试记录与通知保留
#### Scenario: 保存配置只影响后续扫描
- **WHEN** 管理员修改到期前天数或适用范围
- **THEN** 系统记录操作者、前后值与时间并递增配置版本,已产生的尝试记录保持原配置版本快照且不重算
### Requirement: 每日扫描与续购资格
系统 SHALL 按上海自然日执行自动续费扫描,处理对象为尚未到期的当前主套餐(主套餐记录、未退款、未删除、状态为生效或已用完,按优先级与创建时间取第一条),续购对象为其套餐商品。触发窗口 SHALL 为闭区间:最终到期剩余天数大于等于 0 且小于等于配置的到期前天数;剩余天数 SHALL 按上海自然日计算(固定东八区、无夏令时),并复用既有最终到期推算口径(含待生效主套餐顺延)。最终到期推算结果为「无有效主套餐」「等待激活」或「数据异常」时 MUST NOT 处理;剩余天数为负(已过期)时 MUST NOT 尝试。资产范围 SHALL 为独立卡与设备;已绑定设备的卡 MUST NOT 处理(与个人客户购买入口口径一致)。除窗口条件外,续购资格 MUST 同时满足:该资产当前不存在待生效主套餐(未退款、无主套餐归属且状态为待生效),当前主套餐商品在配置范围内(全部范围时不受此限),且该资产不存在未关闭(待支付)的个人资产钱包主套餐订单。系统 MUST NOT 按流量阈值或流量即将耗尽触发。
#### Scenario: 窗口闭区间边界
- **WHEN** 资产最终到期剩余天数分别为 0、等于配置天数、等于配置天数加一
- **THEN** 剩余天数为 0 与等于配置天数时进入执行,大于配置天数时不处理
#### Scenario: 已存在待生效主套餐
- **WHEN** 资产已存在待生效主套餐且当前主套餐进入窗口
- **THEN** 系统跳过该资产,不扣款、不创建订单,并在尝试记录标记跳过
#### Scenario: 无明确最终到期或已过期
- **WHEN** 资产无有效主套餐、最终到期推算为等待激活或数据异常,或最终到期已过期
- **THEN** 系统不进入执行,不创建尝试记录、不扣款、不通知
#### Scenario: 已绑定设备的卡不自动续费
- **WHEN** 卡已绑定设备
- **THEN** 系统只在设备维度按设备自身套餐判断续费,不对该卡执行自动续费
### Requirement: 每日一次尝试与尝试记录终态收敛
系统 SHALL 以(资产类型、资产 ID、触发日期唯一约束保证每项资产每天至多一次尝试资产类型取值域 SHALL 与资产钱包资源类型一致,卡与设备分别计数。尝试 SHALL 是执行级记录:只有进入执行并得出终态的资产才创建记录(成功、失败或跳过);未进入窗口或资格不足而未进入执行的资产 MUST NOT 创建记录、不构成尝试。扫描 SHALL 先以独立短事务写入当次尝试占位;唯一冲突即视为当日已尝试并跳过该资产。当天失败 MUST NOT 重试次日再评估MUST NOT 建立当日重试机制。跨日尝试次数 SHALL 以尝试序号字段累计。占位后进程中断留下的非终态记录 SHALL 由后续扫描收敛:触发日期早于当日的非终态记录 MUST 先被收敛为中断或未知,且当日不重试。扫描任务级失败 SHALL 返回错误交由既有任务重试机制重试;单个资产执行失败 MUST NOT 使扫描任务失败。
#### Scenario: 当日已存在尝试记录
- **WHEN** 该资产当日已存在成功、失败或跳过的尝试记录
- **THEN** 唯一约束冲突后该资产被跳过,当日不再尝试、不重复扣款
#### Scenario: 占位后进程中断
- **WHEN** 尝试占位写入后进程中断,记录停留在非终态,且当日扫描已结束
- **THEN** 次日扫描先将该记录收敛为中断或未知,且该资产当日至多仍只尝试一次
#### Scenario: 单资产失败不影响同批其他资产
- **WHEN** 同一批扫描中某个资产执行失败
- **THEN** 系统记录该资产失败原因并继续处理其余资产,扫描任务本身不因该资产失败而失败
### Requirement: 资金、价格与单事务闭合
自动续费 MUST 仅扣该续费资产钱包的可用余额余额减冻结余额MUST NOT 使用其他资产钱包、代理主钱包或外部支付渠道。续购价格 SHALL 取执行时该资产所属店铺渠道的当前可售续费价;资产无所属店铺时取平台价。续购 MUST 为同一套餐商品且 MUST 只产生一个周期的购买MUST NOT 在同一窗口内连续叠加多个周期。钱包扣款、订单与订单明细、支付记录、钱包流水、套餐生效事实与成功审计 MUST 在同一事务内闭合;任一步失败 MUST 整体回滚MUST NOT 产生「扣款已提交而套餐未生成」或其他部分成功状态。因此系统 MUST NOT 提供补偿或退款路径MUST NOT 建立自动续费异常记录页。当前条件不允许自动续购时,系统 MUST NOT 创建订单、MUST NOT 扣款,只记录失败原因并投递通知。不可续费至少覆盖:套餐商品被禁用;当前渠道下架且不满足续费豁免;生效零售价低于成本价;不在可购买范围或资产未关联套餐系列;资产钱包当前不可用于扣款同样记入不可续费。
#### Scenario: 可用余额不足
- **WHEN** 资产钱包可用余额小于执行时当前可售续费价
- **THEN** 系统不创建订单、不扣款,记录失败原因余额不足并投递通知
#### Scenario: 套餐不可续费
- **WHEN** 套餐商品被禁用,或当前渠道已下架且不满足续费豁免,或生效零售价低于成本价
- **THEN** 系统不创建订单、不扣款,记录失败原因不可续费并投递通知
#### Scenario: 成功续费的资金与套餐事实同时可见
- **WHEN** 自动续费成功
- **THEN** 续费订单、订单明细、已支付支付记录、钱包扣款流水(含扣款前后余额)与套餐使用记录在同一事务提交后同时可见,续购价格为该渠道执行时当前可售续费价
### Requirement: 失败通知与接收人
系统 SHALL 向当前个人客户与资产所属店铺当时有效业务员各创建站内通知,同一资产同一上海自然日同一原因对同一接收人至多一条。失败原因枚举 SHALL 固定为四类:余额不足、不可续费、订单失败、复机失败。通知幂等键 SHALL 内嵌资产类型与资产 ID、上海自然日、原因类型与接收人。复机失败通知 SHALL 沿用该次尝试的日期键,使同一尝试只通知一次且不跨日新增。资产所属店铺当时无有效业务员时 MUST NOT 阻断续费、失败记录或客户通知;资产无店铺归属时只创建客户通知。
#### Scenario: 余额不足的双接收人各一条
- **WHEN** 合格资产进入窗口但钱包可用余额不足,且资产所属店铺存在有效业务员
- **THEN** 系统为该客户与该业务员各创建一条余额不足通知,当日重复扫描不再新增
#### Scenario: 业务员不存在
- **WHEN** 资产所属店铺当时没有有效业务员
- **THEN** 系统不阻断续费流程与尝试记录,只创建客户通知
#### Scenario: 复机失败通知只发一次
- **WHEN** 续费成功但复机失败或结果未知,且恢复确认最终失败
- **THEN** 系统以该次尝试的日期键创建复机失败通知,同一尝试只通知一次且不跨日新增
### Requirement: 成功后的可靠复机
续费成功后,仅当资产处于可恢复停机状态且运营商状态不是风险停机或已销户时,系统 SHALL 通过可靠异步投递触发既有复机能力MUST NOT 使用提交后即发即弃的调用。复机失败或结果未知时,系统 MUST 保存执行结果含外部交互标识与失败原因并交由既有恢复机制查询确认MUST NOT 回滚已提交的订单、套餐生效、钱包扣款或续费成功事实。复机未投递(条件不成立)时 MUST NOT 创建复机失败通知。
#### Scenario: 复机条件不成立
- **WHEN** 续费成功但资产不满足可恢复停机条件,或运营商状态为风险停机或已销户
- **THEN** 系统记录复机跳过、不触发复机调用、不发送通知,续费事实保持不变
#### Scenario: 复机结果未知
- **WHEN** 复机调用后本地状态回写失败
- **THEN** 系统保存结果未知与外部交互标识,由恢复扫描查询确认最终结果,续费订单、套餐与钱包事实不变
#### Scenario: 复机失败不回滚续费
- **WHEN** 复机最终失败
- **THEN** 系统保留订单、套餐生效与钱包扣款事实,记录失败原因并投递复机失败通知
### Requirement: 尝试记录与可追溯字段
每次进入执行的尝试 SHALL 保留可追溯记录,至少包含:资产类型与资产 ID 与触发日期;触发时解析的当前个人客户与资产所属店铺快照;触发时配置版本与窗口快照;当前主套餐使用记录与当前套餐商品;待续购套餐商品与执行时续费价;钱包标识、钱包流水号、扣款金额与扣款前后余额;续费订单标识与订单号;尝试状态;失败原因;跳过原因;复机状态、复机外部交互标识与复机失败原因;操作者类型与标识(无人工处理入口,恒为系统任务);跨日尝试次数;时间戳。尝试状态 SHALL 为处理中、成功、失败、跳过四者之一。跳过原因 SHALL 至少覆盖人工已完成续购与人工订单在途。复机状态 SHALL 为未评估、跳过、已投递、成功、失败、未知之一。上述状态与原因枚举 SHALL 在规格与实现常量间共用同一份语义字面量。记录 MUST NOT 保存凭证、令牌或个人敏感信息。
#### Scenario: 成功尝试的可追溯内容
- **WHEN** 自动续费成功并按条件投递复机
- **THEN** 尝试记录包含扣款金额与扣款前后余额、钱包流水号、续费订单号、执行时续费价与复机状态
#### Scenario: 跳过尝试的可追溯内容
- **WHEN** 资产因已存在待生效主套餐或存在在途人工订单被跳过
- **THEN** 尝试记录状态为跳过并写明对应跳过原因,且不含订单号与扣款金额
### Requirement: 手动续购优先
自动任务 MUST 在提交资金事实前重新读取资格事实;人工续购已完成(存在待生效主套餐)时必须跳过,且 MUST NOT 产生第二笔订单或扣款。同一资产存在未关闭(待支付)的个人资产钱包主套餐订单时,自动任务 MUST 跳过并记录跳过原因人工订单在途MUST NOT 发送通知。手动续购优先的可观察定义:同一资产同一周期 MUST NOT 因自动任务产生两笔已支付续购。
#### Scenario: 人工成功续购后自动跳过
- **WHEN** 自动任务重新读取时发现该资产已由人工成功续购
- **THEN** 自动任务不创建第二笔订单、不扣款,并标记跳过
#### Scenario: 人工订单在途
- **WHEN** 自动任务执行时该资产存在未关闭的个人资产钱包主套餐订单
- **THEN** 自动任务跳过该资产、不扣款、不创建订单且不发送通知
### Requirement: 不得因钱包余额跳过停机判定
系统 MUST NOT 因资产钱包余额充足而跳过停机判定MUST NOT 为此新增任何运行时开关或配置项MUST NOT 改变既有停机与复机判据。服务不中断 SHALL 只由「窗口内提前续购使新主套餐待生效、到期由既有接续链生效、轮询在存在有效主套餐且流量未耗尽时自动复机」产生。
#### Scenario: 余额充足但命中既有停机条件
- **WHEN** 资产钱包可用余额充足且资产命中既有停机条件(无有效套餐或流量用尽)
- **THEN** 系统仍按既有规则执行停机判定,不因余额跳过停机,且不存在可开启的不停机开关
#### Scenario: 续购成功不立即改变停机状态
- **WHEN** 续购成功且新主套餐按既有规则为待生效
- **THEN** 系统不因续费成功提前改变停机状态,复机仍由既有接续与轮询判定决定