This commit is contained in:
@@ -38,8 +38,8 @@
|
|||||||
- 行为变化必须通过独立 OpenSpec Change;主 Specs 只描述当前行为。
|
- 行为变化必须通过独立 OpenSpec Change;主 Specs 只描述当前行为。
|
||||||
- 当前 Bug 与兼容行为按实际结果记录,禁止在基线任务中顺手修复。
|
- 当前 Bug 与兼容行为按实际结果记录,禁止在基线任务中顺手修复。
|
||||||
- 不修改既有迁移;新 Schema 变化使用新的成对迁移。
|
- 不修改既有迁移;新 Schema 变化使用新的成对迁移。
|
||||||
- 不访问生产服务、真实支付渠道或外部审批系统进行自动验证。
|
- 不访问真实支付渠道或外部审批系统进行自动验证。
|
||||||
- 生产环境为 systemd 管理的手工二进制发布,和仓库 Docker/CI 测试环境不同;生产发布、迁移与回滚事实见 [`docs/deployment/production-runbook.md`](docs/deployment/production-runbook.md)。Agent 不连接生产主机或数据库,生产操作由维护者执行并提供结果。
|
- 生产环境为 systemd 管理的手工二进制发布,和仓库 Docker/CI 测试环境不同;生产发布、迁移与回滚事实见 [`docs/deployment/production-runbook.md`](docs/deployment/production-runbook.md)。Agent 可仅通过 dbhub 对生产数据库执行只读诊断查询;不连接生产主机,生产发布、迁移、回滚及其他写操作由维护者执行并提供结果,除非明确要求。
|
||||||
- 不把密钥、Token、证书或个人敏感数据写入代码、文档和日志。
|
- 不把密钥、Token、证书或个人敏感数据写入代码、文档和日志。
|
||||||
- `docs/verification/context-reset/` 仅保存上下文健康检查证据,不是业务事实源;证据应随项目文档维护。
|
- `docs/verification/context-reset/` 仅保存上下文健康检查证据,不是业务事实源;证据应随项目文档维护。
|
||||||
- 自动化测试当前为 N/A(用户决策);不恢复旧测试,也不写虚假测试入口。
|
- 自动化测试当前为 N/A(用户决策);不恢复旧测试,也不写虚假测试入口。
|
||||||
|
|||||||
32
CONTEXT.md
Normal file
32
CONTEXT.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# 新卡管系统
|
||||||
|
|
||||||
|
新卡管系统保存物联网卡、设备、套餐、订单、钱包、分佣、审批与运营协作的本地业务事实,并协调外部支付、运营商和企业微信能力。
|
||||||
|
|
||||||
|
## 资金与审批
|
||||||
|
|
||||||
|
**员工代收款账单**:员工代客户完成套餐购买或充值等业务后生成的待核销记录,表示该员工经办业务形成的暂挂欠款;可由已匹配支付记录的客户付款凭证核销。
|
||||||
|
_Avoid_: 员工账单、客户应收款、销账单
|
||||||
|
|
||||||
|
**核销**:公司对员工代收款账单及已匹配支付记录的客户付款凭证作出的确认,使对应暂挂欠款减少或结清的业务决定。
|
||||||
|
_Avoid_: 客户付款、订单支付
|
||||||
|
|
||||||
|
**核销申请**:员工针对一笔外部付款提交的、包含凭证和一至多条账单分摊明细的审批业务单;一张申请对应一个企业微信审批实例。
|
||||||
|
_Avoid_: 单张账单审批、付款截图
|
||||||
|
|
||||||
|
**核销分摊明细**:核销申请对一张员工代收款账单确认的本次核销金额。
|
||||||
|
_Avoid_: 账单金额、付款金额
|
||||||
|
|
||||||
|
**账单核销状态**:员工代收款账单的结算状态,取待核销、部分核销、已核销或已关闭;与核销申请的审批状态相互独立。
|
||||||
|
_Avoid_: 审批状态、企业微信状态
|
||||||
|
|
||||||
|
**审批实例**:本地保存、唯一关联一笔业务单的审批生命周期事实;企业微信是该实例的审批渠道,而非本地业务事实的替代。
|
||||||
|
_Avoid_: 企业微信审批单
|
||||||
|
|
||||||
|
**交易流水号**:支付或退款渠道为一笔交易生成的外部标识;OCR 识别结果只能预填该字段,须由业务人员最终确认。
|
||||||
|
_Avoid_: OCR 结果、系统订单号
|
||||||
|
|
||||||
|
**线下收款方式**:业务字典中供核销等线下付款场景选择的收款路径标识,例如某个指定微信或银行卡;它不等同于线上支付渠道枚举。
|
||||||
|
_Avoid_: 支付方式枚举、固定收款人名单、收款账户目录
|
||||||
|
|
||||||
|
**业务字典**:由研发固定注册的业务分类及其由业务维护的字典项,用于稳定的业务选项;业务人员不能自行创建字典分类。
|
||||||
|
_Avoid_: 系统配置、任意自定义字段平台
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
- Owner:生产维护者。
|
- Owner:生产维护者。
|
||||||
- 最后核验:2026-08-13(基于维护者提供的信息,未连接生产环境)。
|
- 最后核验:2026-08-13(基于维护者提供的信息,未连接生产环境)。
|
||||||
- 更新触发:Unit、目录、进程数、Worker 角色、发布顺序、迁移方式或回滚方式变化。
|
- 更新触发:Unit、目录、进程数、Worker 角色、发布顺序、迁移方式或回滚方式变化。
|
||||||
- 验证:由维护者在服务器执行本文列出的只读核对命令,并回填结果;Agent 不连接生产环境。
|
- 验证:Agent 可通过 dbhub 对生产数据库执行只读诊断查询;服务器上的只读核对、生产发布、迁移、回滚及其他写操作由维护者执行并回填结果,Agent 不连接生产主机。
|
||||||
|
|
||||||
## 与测试环境的边界
|
## 与测试环境的边界
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o ./build/worker ./cmd/worker
|
|||||||
- 二进制只从嵌入默认配置和 `JUNHONG_` 环境变量读取;`.env.prod` 必须由 systemd Unit 显式加载,或由 Unit 启动脚本 `source` 后启动。需要以 Unit 内容核实实际方式。
|
- 二进制只从嵌入默认配置和 `JUNHONG_` 环境变量读取;`.env.prod` 必须由 systemd Unit 显式加载,或由 Unit 启动脚本 `source` 后启动。需要以 Unit 内容核实实际方式。
|
||||||
- API 与 Worker 共享数据库、Redis、日志、JWT、对象存储、Gateway、短信、支付等基础配置;只记录键名,不将实际凭据写入仓库文档。
|
- API 与 Worker 共享数据库、Redis、日志、JWT、对象存储、Gateway、短信、支付等基础配置;只记录键名,不将实际凭据写入仓库文档。
|
||||||
- 七月新增的 API/Worker 共用配置:`JUNHONG_APPROVAL_LEGACY_REFUND_MANUAL_ENABLED`、`JUNHONG_APPROVAL_LEGACY_OFFLINE_RECHARGE_PAY_ENABLED`、`JUNHONG_WECOM_BASE_URL`、`JUNHONG_WECOM_TIMEOUT`。
|
- 七月新增的 API/Worker 共用配置:`JUNHONG_APPROVAL_LEGACY_REFUND_MANUAL_ENABLED`、`JUNHONG_APPROVAL_LEGACY_OFFLINE_RECHARGE_PAY_ENABLED`、`JUNHONG_WECOM_BASE_URL`、`JUNHONG_WECOM_TIMEOUT`。
|
||||||
- 七月新增的 Worker 配置:`JUNHONG_WORKER_ROLE`、`JUNHONG_WORKER_INSTANCE_NAME`、`JUNHONG_WORKER_AUDIT_RETENTION_CLEANUP_ENABLED`。
|
- 本次新增的 Worker 配置:`JUNHONG_WORKER_ROLE`、`JUNHONG_WORKER_INSTANCE_NAME`、`JUNHONG_WORKER_POLLING_TOTAL_MAX_CONCURRENCY`、`JUNHONG_WORKER_AUDIT_RETENTION_CLEANUP_ENABLED`、`JUNHONG_WORKER_AUDIT_ARCHIVE_TASKS_ENABLED`。轮询总并发必须保持在 1-1000,初始生产值建议 100;归档/留存总开关默认关闭。
|
||||||
- 首发要求:退款人工入口、线下充值人工确认、企微审批、运营商实名回调、微信/支付宝在线充值均按维护者决定启用;审计物理清理保持关闭。
|
- 首发要求:退款人工入口、线下充值人工确认、企微审批、运营商实名回调、微信/支付宝在线充值均按维护者决定启用;审计物理清理保持关闭。
|
||||||
- 企微应用凭据由后台配置写入数据库明文字段;这不是启动环境变量。本文不记录其值。
|
- 企微应用凭据由后台配置写入数据库明文字段;这不是启动环境变量。本文不记录其值。
|
||||||
|
|
||||||
@@ -98,9 +98,9 @@ DB_PASSWORD='<密码>' DB_NAME=<库名> DB_SSLMODE=<模式> \
|
|||||||
|
|
||||||
## 日审计日志留存启用与恢复
|
## 日审计日志留存启用与恢复
|
||||||
|
|
||||||
日留存由调度 Worker 每日处理 Asia/Shanghai 的昨天及更早连续积压日期;`JUNHONG_WORKER_AUDIT_RETENTION_CLEANUP_ENABLED=false` 时仅验证归档、对象、manifest、数据库数量和日期连续性,不写清理断点、不删除在线数据。
|
归档与日留存由调度 Worker 受控处理 Asia/Shanghai 的已结束自然日;`JUNHONG_WORKER_AUDIT_ARCHIVE_TASKS_ENABLED=false` 时不注册调度,已入队任务也安全跳过、不扫描在线日志表。开启后每次最多推进一个日期。`JUNHONG_WORKER_AUDIT_RETENTION_CLEANUP_ENABLED=false` 时仅验证归档、对象、manifest、数据库数量和日期连续性,不写清理断点、不删除在线数据。Integration 的 `pending` 记录保留在线,但不会阻断同日终态日志、Audit 数据或后续日期的清理;若 pending 后续终结,Worker 会重建该日 revision 后续跑删除。
|
||||||
|
|
||||||
1. 发布新 Worker 后保持开关关闭。维护者先核对 `tb_log_archive_run` 中 Audit 与 Integration 两个来源从历史最早在线日期起没有缺失账本;缺失日期必须先受控补归档,不能跳过失败日。
|
1. 发布新 Worker 后保持 `JUNHONG_WORKER_AUDIT_ARCHIVE_TASKS_ENABLED=false` 和物理清理开关关闭;轮询总并发先设为 100。维护者先核对 `tb_log_archive_run` 中 Audit 与 Integration 两个来源从历史最早在线日期起没有缺失账本;缺失日期必须先受控补归档,不能跳过失败日。
|
||||||
2. 以关闭开关的 Worker 完成只读演练,观察 `/opt/junhong_cmp/worker/logs/audit-retention.log`:每个日期应有来源计数和耗时;若出现日期、来源、失败分类或安全错误摘要,先修复该日归档或 pending 记录后再演练。
|
2. 以关闭开关的 Worker 完成只读演练,观察 `/opt/junhong_cmp/worker/logs/audit-retention.log`:每个日期应有来源计数和耗时;若出现日期、来源、失败分类或安全错误摘要,先修复该来源的归档或校验问题后再演练。仅有 pending 不需要人工终结才可继续。
|
||||||
3. 低峰期将调度 Worker 的 `JUNHONG_WORKER_AUDIT_RETENTION_CLEANUP_ENABLED=true`,重启该 Worker,并持续观察上述独立日志、`tb_log_archive_run.cleanup_started_at` / `cleaned_at` 断点及表大小。任何日期失败都会阻断该日和后续日期,不得手工跳过。
|
3. 低峰期先将调度 Worker 的 `JUNHONG_WORKER_AUDIT_ARCHIVE_TASKS_ENABLED=true`,重启该 Worker;确认单日归档稳定后,再将 `JUNHONG_WORKER_AUDIT_RETENTION_CLEANUP_ENABLED=true`,并持续观察上述独立日志、`tb_log_archive_run.cleanup_started_at` / `cleaned_at` 断点及表大小。DELETE 释放的 PostgreSQL 页面会供后续写入复用,表文件不会立即缩小;维护者按既有运维窗口评估 VACUUM,Worker 不执行 VACUUM 或表重写。
|
||||||
4. 异常时立即将开关改回 `false` 并重启调度 Worker。已清理日期按已验证的对象和 manifest 执行归档恢复;尚未清理或阻断的日期仍保留在线,无需数据库恢复。恢复后先重新执行只读演练,再决定是否重新开启清理。
|
4. 异常时立即将开关改回 `false` 并重启调度 Worker。已清理日期按已验证的对象和 manifest 执行归档恢复;尚未清理或阻断的日期仍保留在线,无需数据库恢复。恢复后先重新执行只读演练,再决定是否重新开启清理。
|
||||||
|
|||||||
220
docs/product/2026-08-迭代-PRD-讨论稿.md
Normal file
220
docs/product/2026-08-迭代-PRD-讨论稿.md
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
# 新卡管 2026 年 8 月迭代 PRD(讨论稿)
|
||||||
|
|
||||||
|
> 状态:讨论中
|
||||||
|
>
|
||||||
|
> 本文是本轮需求澄清的唯一决策记录。确认后的需求将重编稳定需求 ID;每项实施前再以独立 OpenSpec Change 固化可观察行为、任务和验收。
|
||||||
|
>
|
||||||
|
> 来源:业务提供的《新卡管 8 月迭代需求》,2026-08-11。
|
||||||
|
|
||||||
|
## 0. 已确认的产品边界
|
||||||
|
|
||||||
|
1. 本文覆盖一个需求池,不承诺整体一次上线。完成完整 PRD 后,按影响范围由小到大拆分为独立 Change 实施。
|
||||||
|
2. 原文编号存在重复和漂移。讨论稿将建立新的稳定需求 ID;原编号仅用于追溯来源。
|
||||||
|
3. 审批类业务必须接入企业微信。企业微信是唯一终审来源;审批节点、审批人、条件和流转规则完全由企业微信模板配置,本系统不读取、校验或固化这些规则。系统只保存本地审批实例、业务状态、审计和幂等闭环,并按企业微信最终通过或驳回结果推进业务;不提供本地人工通过或拒绝来绕过企业微信。企业微信回调延迟、提交失败或结果未知时,使用既有兜底轮询查询渠道状态并同步本地实例。
|
||||||
|
4. OCR 是已有的外部能力,但本期仅作为交易流水号的预填来源。申请人或审核人必须能更正并最终确认;OCR 识别结果不是资金事实。OCR 协议、字段能力、失败和重试规则等待外部契约文档。
|
||||||
|
|
||||||
|
## 0.1 稳定需求 ID 与原文映射
|
||||||
|
|
||||||
|
| 稳定 ID | 能力 | 原文来源 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| AUG26-001 | 员工代收款账单与核销 | PRD-08-001、PRD-08-013(核销字段) |
|
||||||
|
| AUG26-002 | 支付商户池与微信授权配置 | PRD-08-002 |
|
||||||
|
| AUG26-003 | 店铺业务员与业务用户组 | PRD-08-004、PRD-08-005 |
|
||||||
|
| AUG26-004 | 套餐真流量预警 | PRD-08-006 |
|
||||||
|
| AUG26-005 | 换货业务数据迁移展示 | PRD-08-007 |
|
||||||
|
| AUG26-006 | 套餐退款与原路退款 | PRD-08-008、PRD-08-013(退款字段) |
|
||||||
|
| AUG26-007 | H5 风险换卡与运营通知 | PRD-08-009 |
|
||||||
|
| AUG26-008 | 代理分销码、资料资格与佣金提现 | PRD-08-010、PRD-08-013(提现/分销审批字段) |
|
||||||
|
| AUG26-009 | 手机号—资产关联 | PRD-08-011 |
|
||||||
|
| AUG26-010 | 自动续费 | PRD-08-012 |
|
||||||
|
| AUG26-011 | 运营商通道流量阈值 | PRD-08-017 |
|
||||||
|
| AUG26-012 | 佣金回溯明细 | PRD-08-015 |
|
||||||
|
| AUG26-013 | 资产套餐层级展示 | PRD-08-016(资产详情) |
|
||||||
|
| AUG26-014 | 导出与统一时间筛选 | PRD-08-014、PRD-08-020 |
|
||||||
|
| AUG26-015 | 报表管理 | PRD-08-016(报表,原编号重复) |
|
||||||
|
| AUG26-016 | 优先轮询通道 | PRD-08-019 |
|
||||||
|
| AUG26-017 | 代理自充收款方式 | PRD-08-021 |
|
||||||
|
|
||||||
|
## 1. 已确认的领域语言
|
||||||
|
|
||||||
|
### 1.1 员工代收款账单
|
||||||
|
|
||||||
|
员工代客户购买套餐、充值等业务时,系统按来源业务生成的待核销记录。它表示员工经办业务形成的暂挂欠款;员工提交客户付款凭证,企业微信审批人员可通过该凭证在外部第三方收款记录中核验支付,且企业微信审批通过后,欠款相应减少或结清。客户款项可支付至公司微信、银行卡或其他由业务维护并认可的线下收款方式,不要求必须支付给员工或由员工另行回款。
|
||||||
|
|
||||||
|
代理线下预存款/主钱包充值仍按既有企业微信审批决定是否入账;该审批通过并完成入账后,系统再创建相应员工代收款账单,使员工欠款增加。后续账单核销是独立于充值审批的业务流程。
|
||||||
|
|
||||||
|
员工代收款账单的欠款人固定为实际发起线下套餐订单或线下代理充值的后台账号,创建后不可修改;平台业务员和超级管理员实际经办时均生成本人账单。员工离职或账号禁用后,其欠款人身份不变;仅超级管理员可代办创建、修改或重新提交核销申请,且必须记录实际代办人及代办原因。
|
||||||
|
|
||||||
|
已确认:支持一笔外部支付记录分摊核销多张账单,也支持一张账单由多笔支付记录分次核销。员工应按一笔外部付款创建一张核销申请,在申请中上传该笔付款的凭证、填写交易流水号,并选择多张账单及各自分摊金额;一张申请只产生一个企业微信审批实例。每笔分摊必须独立保留账单、外部支付记录标识、本次核销金额、凭证、审批实例和剩余未核销金额。
|
||||||
|
|
||||||
|
员工代收款账单的核销状态独立于核销申请审批状态:账单状态为待核销、部分核销、已核销或已关闭;申请状态为审批中、已通过、已驳回、已撤销/已关闭。只有企业微信审批通过的分摊才减少员工欠款。账单列表需同时展示账单核销状态和是否存在审批中申请,避免部分核销与审批中互相覆盖。
|
||||||
|
|
||||||
|
企业微信最终驳回的核销申请允许员工在原申请上修改后重新提交;系统为同一申请保留每一次企业微信审批实例及其当次业务快照,历史材料和审批结果不得被覆盖。已驳回申请可修改全部申请内容:收款账户、外部付款信息、附件、备注、勾选账单和分摊金额;已通过的分摊不可修改。
|
||||||
|
|
||||||
|
超级管理员可关闭待核销、已驳回或部分核销账单,关闭即作废当时未核销余额且不再计入员工欠款;已核销金额保留。关闭必须填写原因、保留操作审计,且存在审批中核销申请时不得关闭。
|
||||||
|
|
||||||
|
已确认:员工代收款账单按业务场景确定公司应收金额。
|
||||||
|
|
||||||
|
- 代理代购套餐:取订单 `actual_paid_amount`(代理实际结算/成本金额)。对会生成员工代收款账单的后台线下套餐订单,创建订单时不再强制上传付款凭证;订单仍按当前规则立即激活,并同时创建待核销账单。付款凭证、OCR 与外部交易流水号只在后续核销申请提交。赠送套餐等不产生员工账单的线下订单继续保持创建时上传凭证的当前规则。
|
||||||
|
- 无代理归属的自营 C 端套餐购买:取订单 `actual_paid_amount`。当前实现该值等于 `total_amount`;未来优惠券等价格优惠也应使其表示实际收款金额。
|
||||||
|
- 代理线下充值预存款/主钱包:既有充值审批通过并入账后创建员工代收款账单,金额取 `tb_agent_recharge_record.amount`。
|
||||||
|
- 不支持平台代理 C 端客户充值资产钱包;原需求中对此类场景不纳入本期。
|
||||||
|
- “其他”来源第一版不支持手工创建,必须先定义可追溯来源单和金额口径。
|
||||||
|
|
||||||
|
> 待决:`actual_paid_amount` 在未来优惠场景的写入时机与权威性;外部支付记录在本地的最小留存字段、收款账户目录的范围和退款时的冲销规则。
|
||||||
|
|
||||||
|
## 2. 当前发现的需求结构问题
|
||||||
|
|
||||||
|
| 问题 | 处理原则 |
|
||||||
|
| --- | --- |
|
||||||
|
| `PRD-08-013`、`014`、`015`、`016` 在范围表与正文含义不一致 | 后续以新稳定 ID 重编,保留来源编号。 |
|
||||||
|
| 审批字段在第 17 节集中出现,但相关主需求中存在不同口径 | 先定义每个审批业务的业务单、状态、资金生效时点和企业微信模板,再定义页面字段。 |
|
||||||
|
| OCR 被写入字段说明但缺少外部契约 | OCR 仅预填,待提供契约后再定义使用范围。 |
|
||||||
|
| 收款方式被写为固定示例(如某人微信/支付宝) | 收款方式由业务字典维护,不得将名称硬编码为支付方式枚举;是否还需独立收款账户目录待确认。 |
|
||||||
|
| 多处要求“字典维护”,但当前系统没有业务字典 | 新建通用业务字典模块;字典分类由研发固定注册,业务只维护分类下的字典项。已被业务单引用的字典项不得物理删除,只能停用并保留历史名称快照。 |
|
||||||
|
| 自动续费含“有余额自动续费,不停机” | 必须先区分续费订单、钱包扣款、运营商停复机和轮询同步,不能将它们描述为同一动作。 |
|
||||||
|
| 运营商通道阈值、套餐流量预警都使用“阈值” | 前者是通道控制规则,后者是套餐预警规则;二者的流量口径、周期和触发后果必须独立定义。 |
|
||||||
|
|
||||||
|
## 2.1 已确认的外部付款分摊边界
|
||||||
|
|
||||||
|
1. 同一笔外部付款可覆盖多张员工代收款账单,也可由多个员工的多张核销申请引用。
|
||||||
|
2. 本期不对同一外部付款在多张申请中的累计分摊金额做系统防重或金额上限校验;企业微信审批人员以外部第三方记录为准核验。
|
||||||
|
3. 系统仍须逐核销申请保存所选线下收款方式、交易流水号、付款金额、付款方、付款时间、支付凭证、其他凭证、备注及分摊明细,供超级管理员和企业微信审批追溯。交易流水号和付款金额可由 OCR 预填,但必须允许人工确认或更正。
|
||||||
|
4. 员工通过勾选账单创建分摊明细,不手填账单编号、订单、客户或资产字段;系统自动带出这些只读信息。系统按账单产生时间由早至晚,使用本次外部付款金额自动填充分摊,最后一张填剩余金额;员工可以修改本次核销金额。对同一账单的审批中分摊应预占可核销余额,避免并发申请超额核销。
|
||||||
|
5. 本期只做一套固定分类的线下收款方式字典。超级管理员维护字典项名称、稳定编码、排序、启停、备注;核销申请和预存款审批选择字典项并冻结名称快照。不额外建设收款账户目录。
|
||||||
|
|
||||||
|
## 2.2 已确认的历史数据边界
|
||||||
|
|
||||||
|
1. 员工代收款账单仅对功能上线后新创建的符合条件线下套餐订单、功能上线后审批通过并入账的线下代理预存款/主钱包充值自动生成。
|
||||||
|
2. 不回填、不补建任何上线前历史业务;上线前业务不纳入员工账单和欠款统计。
|
||||||
|
|
||||||
|
## 2.6 已确认的商户池覆盖范围
|
||||||
|
|
||||||
|
1. C 端套餐购买、C 端资产钱包充值、代理在线预存款充值三类当前线上收款入口,均应按支付方式通过商户池选择实际收款商户;后台钱包/线下套餐订单不经过商户池。
|
||||||
|
2. 平台范围内每种支付方式最多一个启用商户池;可保留停用历史池,但不得同时启用多个相同支付方式的商户池。未来需要多池时,必须先定义订单/店铺/资产等路由维度。
|
||||||
|
3. 新建独立商户管理模块,收款凭证与商户支付能力从现有综合支付配置中分离,由商户池选择商户;不复用现有支付配置记录作为商户。一个商户仅对应一种支付方式:微信商户和支付宝商户分别建档、分别加入对应商户池。
|
||||||
|
4. 新建独立微信授权配置,平台范围内最多一个启用配置,保存 C 端公众号 H5 登录/JSSDK 和小程序登录所需参数;C 端微信登录、AppID、JSSDK 与 OpenID 只使用该全局配置。微信商户仅保存微信支付或富友收款凭证;商户池命中的微信商户统一使用全局授权配置的 AppID 发起支付,微信侧 AppID 与商户的绑定/授权由业务保证。微信直连商户和富友商户都属于微信支付商户,可混合加入唯一启用的微信商户池;系统按命中商户自身服务商凭证执行支付、回调验签和退款。
|
||||||
|
5. 商户停用后仅对新支付单自动跳过;已命中该商户的历史支付单不换商户,支付回调、状态查询及后续原路退款仍使用该商户凭证。被历史支付单引用的商户不得物理删除,只能停用。商户原路退款能力不提供人工开关,系统仅按服务商类型及退款所需凭证是否完整判定;服务商实现不支持退款的商户固定视为不支持。商户池没有可用商户或商户池停用时,创建支付单失败且不得回退到旧全局支付配置。
|
||||||
|
6. 上线时从当前生效综合支付配置一次性自动迁移:创建一条全局微信授权配置、具备完整凭证的微信/支付宝商户及各自包含一个商户的启用商户池。新订单仅走商户池;旧综合支付配置和历史订单引用保留,仅服务历史回调、查询和退款,不自动改写。缺少完整凭证的支付方式不创建商户池,新支付单按暂无可用商户失败。迁移仅在数据库内复制密钥/证书,不得在日志、审计详情或接口响应暴露敏感值。
|
||||||
|
7. 商户保存名称、支付方式、服务商类型、商户号/应用标识、敏感凭证、状态和备注。每笔新支付单保存实际商户 ID 并冻结名称、支付方式、服务商类型及商户号/应用标识快照,不复制敏感凭证。被支付单引用后,支付方式、服务商类型和商户号/应用标识不得修改;名称、备注、状态及凭证可更新。历史支付单和退款单优先展示快照,回调和退款读取商户当前有效凭证。
|
||||||
|
8. 超级管理员和平台用户均可创建、修改、启用、停用商户、商户池和微信授权配置;其他角色没有管理入口。为使配置人员可核对完整配置,管理 API 的列表和详情向上述已认证角色返回商户及微信授权配置的完整密钥、证书和私钥字段,不做脱敏;不得将这些字段写入日志、审计快照、错误信息或普通业务单据。未被任何支付单命中的商户,超级管理员或平台用户可先将其移出商户池后经二次确认删除;删除审计不得含敏感凭证。被支付单引用的商户一律不得删除。
|
||||||
|
9. 每笔通过商户池创建的支付单,除实际商户 ID 和商户身份快照外,还须保存商户池 ID、商户池名称快照和轮询方式快照;不另建逐次支付路由日志表。
|
||||||
|
10. 金额阈值、笔数阈值和时间周期均为商户池统一配置,池内所有商户共用,不支持成员单独配置。
|
||||||
|
6. 金额/笔数轮询创建池时必须配置统计周期,支持每轮累计、自然日累计、自然月累计。每轮累计在商户重新被轮到时清零;自然日/自然月累计在周期边界重置,达到阈值的商户在当期自动跳过。当前周期所有商户都达到阈值时,创建支付单失败并提示当前周期暂无可用商户。
|
||||||
|
7. 时间轮询创建池时必须配置周期数值、分钟/小时/天单位和起始时间。自起始时间起按固定周期、商户列表顺序轮换;进入新时段时仅在有新支付单时选择并记录商户。当前商户停用时即时跳到下一个可用商户,时间段不重置;修改时间周期、起始时间或商户顺序后,保存成功时间成为新起点并从列表第一项重新计算。时间周期最小为 1 分钟。
|
||||||
|
8. 金额/笔数轮询仅修改阈值时保留当前统计周期内的成功收款累计并立即按新阈值判断;修改统计周期或在金额/笔数两种方式间切换时,保存成功即开始新统计周期、所有商户从零累计。新增商户从零开始;移除/停用商户不再参与新订单选择。调整商户排序时,自然日/自然月累计保留未移除商户的当期累计;每轮累计从新列表第一项开启新一轮、所有商户从零累计。
|
||||||
|
9. 商户预下单失败时不自动切换商户或重试;任一失败按当前错误处理返回,客户再次发起支付时重新按商户池选择。失败订单不计入金额/笔数成功累计。
|
||||||
|
10. 金额/笔数轮询严格仅统计支付成功结果,不在预下单时预占商户额度或笔数;并发预下单可同时命中当前商户,已创建支付单不因后续轮询切换而改挂商户。
|
||||||
|
11. 支付成功后发生的全额或部分退款不回冲商户池金额/笔数成功累计;退款是独立后续资金动作。
|
||||||
|
|
||||||
|
## 2.3 已确认的退款完成规则
|
||||||
|
|
||||||
|
1. 客户提供收款信息的退款以企业微信最终通过为退款完成时点;系统不感知、也不以外部线下实际打款、交易流水号或付款凭证作为退款状态条件。企业微信审批通过即标记已退款。
|
||||||
|
2. 原路退款经企业微信最终通过后,系统必须以原实际收款商户自动调用渠道退款接口,超级管理员不得改选其他商户。只有渠道明确退款成功后退款单才标记已退款并保存渠道退款流水号;调用失败、超时或结果未知时,退款单保持原路退款处理中/失败,保留可恢复事实且不得重复退款。需改为凭证退款时,必须重新提交申请并重新走企业微信审批。
|
||||||
|
3. 原路退款在选择方式、提交申请和实际执行前均重新校验原支付单、原实际收款商户、原渠道交易流水号、商户退款能力/凭证以及可退金额。商户停用不阻断历史订单原路退款。渠道实际调用是退款资格的最终判断;渠道明确拒绝、超期、凭证失效或余额不足时不标记成功、保留失败原因。系统不额外提供退款凭证预探测接口。
|
||||||
|
4. 一笔订单最多只允许一张最终退款成功的退款申请;该申请可部分退款,成功后订单剩余未退款金额不再允许申请退款。
|
||||||
|
5. 退款申请提交金额即企业微信审批授权金额和最终允许退款金额。原路退款按此金额调用渠道;凭证退款实际转出此金额后才可成功。金额需变更时不得继续执行原申请,必须重新提交并重新走企业微信审批。
|
||||||
|
6. 一笔订单同一时间最多一张审批中、原路退款处理中或原路退款失败的退款申请。企业微信驳回、申请撤销/关闭或原路退款明确失败后,允许修改未成功退款申请并重提;可修改金额、原因、退款方式、客户收款信息和附件。每次重提必须新建企业微信审批实例及业务快照,历史材料不可覆盖。任一退款方式最终成功后,申请和订单均不得再发起退款。
|
||||||
|
7. 企业微信最终通过即按当前规则使退款关联套餐失效、接续下一套餐并在必要时停机;原路退款渠道或线下实际打款的后续结果不影响套餐失效,也不恢复权益。
|
||||||
|
8. 凭证退款申请必须提供客户收款账户信息和客户提供的收款凭证,作为企业微信审批材料;不要求、也不提供公司实际线下付款后的交易流水号或付款凭证回填。
|
||||||
|
9. 超级管理员、平台用户和代理均可在各自数据权限内对已支付套餐订单发起退款申请;企业微信是唯一终审来源,本地不提供人工通过、拒绝或退回操作。任一具有该订单数据权限的上述账号,均可修改并重新提交未成功退款申请。
|
||||||
|
10. 凭证退款的客户收款账户信息使用一个必填自由文本字段留存,客户提供的收款凭证附件必填;不新增退款收款账户类型字典、账户表,且不得复用公司线下收款方式字典。
|
||||||
|
11. 退款申请的实收金额必须由系统从来源订单/原支付记录自动带出并冻结,提交人不可填写或修改。线上支付取原成功支付记录金额;资产钱包、代理预存款和后台线下订单取来源订单的实际收款或实际扣款金额。退款金额不得超过冻结实收金额;无法确定权威实收金额时拒绝创建申请。
|
||||||
|
12. 本期不按套餐已用流量自动计算退款金额;提交人填写申请金额,系统仅校验不超过冻结实收金额,套餐使用情况作为退款原因、凭证和企业微信审批判断材料。
|
||||||
|
13. 企业微信在本地收到通过前发生驳回、撤销或删除时,退款申请进入审批未通过/已关闭,未发生退款且可修改重提。企业微信通过后撤销时,不回滚已失效套餐、已发起原路退款或已完成凭证退款;申请标记审批异常、保留审计、禁止自动重提,由超级管理员线下处理。企业微信提交失败或结果未知时,退款申请仍为在途,不允许另建或重提,使用既有查询/重试闭环确认渠道是否受理。
|
||||||
|
14. 对线上支付订单,系统在申请退款时预检可本地确定的原路退款条件:条件满足时同时提供原路退款和凭证退款;条件不满足时禁用原路退款并说明原因,只允许凭证退款。后端在提交及企微通过后的实际执行前均重复校验;后续条件失效时不得强行原路退款。钱包、预存款和后台线下订单不展示不适用的退款方式。
|
||||||
|
15. 所有退款申请必须填写退款原因;退款原因冻结在当次企业微信审批快照中。
|
||||||
|
16. 当前业务实际上限制一张订单仅购买一个套餐;虽数据模型预留多套餐订单能力,但本期退款申请不提供套餐选择,系统自动带出订单唯一关联套餐及其使用情况。企业微信通过后失效该套餐;若为主套餐,仍按现有规则连带失效其加油包。退款上限只按订单冻结实收金额校验。
|
||||||
|
|
||||||
|
## 2.4 已确认的退款方式矩阵
|
||||||
|
|
||||||
|
| 来源订单的实际支付方式 | 本期允许退款方式 |
|
||||||
|
| --- | --- |
|
||||||
|
| 微信/支付宝等线上支付套餐订单 | 原路退款、客户提供收款信息退款 |
|
||||||
|
| C 端资产钱包余额支付套餐订单 | 仅自动退回原资产钱包 |
|
||||||
|
| 代理预存款/主钱包支付套餐订单 | 仅自动退回原代理预存款钱包 |
|
||||||
|
| 后台线下套餐订单 / 员工代收款套餐订单 | 仅客户提供收款信息退款 |
|
||||||
|
| 代理充值预存款业务单本身 | 当前退款模块不覆盖,需另立需求 |
|
||||||
|
|
||||||
|
## 2.5 已确认的退款联动
|
||||||
|
|
||||||
|
1. 来源订单全额退款且账单从未核销时,系统自动关闭账单,关闭原因记为来源订单全额退款。
|
||||||
|
2. 来源订单部分退款且账单从未核销时,系统按退款金额冲减账单应收金额,并保留来源订单退款冲销记录。
|
||||||
|
3. 账单存在任一已通过核销分摊时,系统不得自动冲销该账单;后续由超级管理员人工处理。
|
||||||
|
4. 已核销账单的来源订单后续退款不恢复员工欠款;账单详情仅提示来源订单已退款并保留退款关联。正常订单退款不等于员工欠款重新产生。
|
||||||
|
|
||||||
|
## 2.8 已确认的手机号绑定基线
|
||||||
|
|
||||||
|
当前系统已有个人客户手机号绑定和全局 H5 强制绑定开关;是否强制绑定由全局配置决定,不存在、也不新增按资产导入批次设置“是否需要绑定手机号”的能力。原 PRD 第 15.4 节与当前行为冲突,不能作为本期新增功能依据。本期保留既有全局强制绑定逻辑,并新增已验证手机号与资产的关联记录:同一手机号最多关联 10 个当前有效资产,资产详情展示关联手机号,后台支持按资产解绑、批量解绑及操作记录。全局强制绑定开启时,客户首次登录一项尚未关联该手机号的资产,必须再次完成短信验证码校验后才建立关联;已关联该手机号的资产不再重复验证。全局强制绑定关闭时,H5 登录不要求短信验证码且不新增手机号—资产关联,已有关系保留并可后台查看、解绑。客户更换手机号并完成旧、新手机号验证码校验后,系统原子迁移旧手机号全部有效资产关联至新手机号;新手机号现有关联数加待迁移数超过 10 时整次换绑失败,原关系不变。资产详情列出全部当前关联手机号,单个解绑须明确选择一条资产手机号关系;批量解绑和 Excel 按资产标识导入解绑时,解除每项资产全部当前有效手机号关联,必须二次确认、填写原因并逐条记录实际解除关系。批量解绑逐资产独立执行:有权限且已绑定的资产成功解绑,其余资产失败;任务返回成功数、失败数和逐行失败明细,无权限项使用统一失败文案。具备资产数据权限的后台账号在资产详情、列表、导出和批量任务结果中均展示完整关联手机号,不做脱敏;操作日志不得记录完整手机号。仅超级管理员和平台用户可在资产数据权限范围内执行单个解绑、批量解绑和 Excel 导入解绑;代理、企业和个人客户没有后台解绑能力。手机号—资产关联只能由 H5 短信验证建立,后台不提供补录,后台仅查看和解绑。上线时不回填既有个人客户手机号与资产关系;功能上线后,既有客户首次登录每项资产仍须重新短信验证建立关联,超过 10 项时阻断本次新关联。换货不迁移手机号—资产关联,新资产首次访问时按全局开关重新验证。
|
||||||
|
|
||||||
|
## 2.7 已确认的业务用户组
|
||||||
|
|
||||||
|
1. 业务用户组用于标记平台用户所属业务,既不是后台权限角色,也不是代理店铺分组。
|
||||||
|
2. 每个启用的平台用户最多属于一个启用的业务用户组;用户组不改变后台角色权限、数据范围或店铺具体业务员归属。
|
||||||
|
3. 店铺所属用户组由当前绑定的平台业务员所属用户组实时推导;更换店铺负责人或负责人更换用户组后,店铺所属组随之变化。店铺未绑定负责人、负责人未分组或所属组已停用时,店铺所属组为空或显示已停用。店铺列表、详情和筛选均支持展示及按该推导用户组查询。
|
||||||
|
4. 用户组停用后不得新增成员;现有成员关系保留,用户及其负责店铺均显示该组已停用。停用不影响用户登录、权限、数据范围和店铺负责人,管理员可后续改组或清空成员。
|
||||||
|
5. 仅无成员的用户组可由超级管理员或平台用户二次确认删除;仍有成员时只能停用或先移走成员。
|
||||||
|
6. 超级管理员和平台用户可勾选多个平台用户,批量设置为某个启用用户组或批量清空所属组;设置会直接替换原所属组,停用组不得作为批量目标。
|
||||||
|
7. 超级管理员和平台用户可勾选多家有数据权限的店铺,批量设置为某个启用的平台业务员或批量清空业务员;店铺所属用户组随负责人实时推导更新。批量操作先校验全部目标店铺,任一店铺无权、不存在、已删除或目标业务员无效时整批不修改并统一失败。
|
||||||
|
8. 用户组维护名称、稳定编码、排序、启用状态和备注;不设上级组、层级或组管理员。编码创建时必填、当前未删除用户组内唯一且创建后不可修改;名称、排序、状态和备注可修改。
|
||||||
|
|
||||||
|
## 2.10 已确认的 H5 风险换卡弹窗
|
||||||
|
|
||||||
|
广电风险停机自动弹窗只在当前 H5 登录并访问的资产同时满足以下条件时展示:资产为广电卡、运营商回传扩展状态为风险停机、且不存在待填写收货信息、待发货、已发货待确认或已完成的物流换货单。命中后向当前客户展示换卡提醒。客户提交地址后自动创建一张关联该旧资产的物流换货单;不另建风险换卡待处理记录。首次提交地址即锁定,客户后续不得修改,只能联系后台处理。风险换卡自动创建的物流换货单不在客户提交地址时预设业务数据迁移;后台发货并选择新资产时,仍由操作人按既有换货流程决定是否迁移。同一客户、同一资产的风险换卡弹窗每天最多展示一次;客户点击稍后处理后当天不再展示,次日仍命中条件时可再次展示。运营主动弹窗按店铺、设备类型、卡类型等已配置维度同时匹配;同一维度多选满足任意一个即可,未配置任何适用范围则面向全量客户。主动弹窗配置必须设置优先级;后端按请求条件仅返回优先级最高的一条,优先级相同取最近更新时间最新的一条,风险换卡通知固定高于主动弹窗。H5 弹窗复用个人客户站内通知记录,投放后同时出现在 H5 通知列表供查看历史内容。后端创建或返回弹窗候选时通知保持未读;H5 在客户关闭弹窗、点击操作按钮或进入通知详情后调用现有已读接口。运营主动弹窗仅在客户请求配置页面的候选弹窗时实时匹配并创建或复用通知,未访问 H5 的客户不预生成通知。后台固定支持首页、资产详情、套餐购买、资产钱包充值四种展示页面。频率仅支持每个客户对每条配置仅一次或每天一次。运营配置修改标题、内容、范围、页面、频率或有效期只影响后续投放,既有通知保留原快照;已修改配置作为新版本,对原“仅一次”配置的命中客户可重新投放一次。运营弹窗可不设操作,或设置一个受控按钮,目标仅可为套餐购买或资产钱包充值;不得配置任意 URL。H5 请求候选必须携带当前页面资产标识,店铺、设备类型和卡类型均按该资产匹配,首页由 H5 传当前选中的资产。运营弹窗配置到期或停用后停止新投放,既有通知在通知中心保留 90 天。风险换卡地址提交后停止新投放,既有风险换卡通知保留 90 天。风险换卡收货信息保留收货人姓名、收货手机号和一个完整地址文本三项;地址不拆分省、市、区及详细地址字段。超级管理员和平台用户可管理全局 H5 弹窗配置,代理、企业和个人客户不可管理。
|
||||||
|
|
||||||
|
## 2.9 已确认的换货迁移展示
|
||||||
|
|
||||||
|
换货列表和详情使用“业务数据迁移”及状态:不迁移、待迁移、已迁移、迁移失败;迁移失败可查看失败原因。详情明确迁移范围仅包括资产钱包余额、有效套餐使用记录、累计充值字段和资产标签。资产归属及个人客户—资产绑定属于换货完成固有动作;手机号—资产关联不属于迁移范围,新资产首次访问时按全局开关重新验证。迁移失败时换货单保持原可完成状态,记录最近一次失败原因;超级管理员或平台用户修复条件后可再次确认完成,整套迁移重新原子执行。
|
||||||
|
|
||||||
|
## 2.11 已确认的自动续费基线
|
||||||
|
|
||||||
|
自动续费仅扣待续费同一资产的资产钱包可用余额;对当前有效主套餐续购同一套餐商品,价格按执行时当前渠道可售续费价计算。第一版仅按最终到期前 N 天触发,不按流量阈值触发。平台设置一个总开关,并配置全部主套餐或指定主套餐及统一的到期前 N 天;进入触发窗口后,每项资产每天最多尝试一次,成功即停止,套餐到期后不再自动尝试。余额不足或套餐不可续费时,向当前个人客户及资产所属店铺当时有效业务员创建站内通知;同一资产同一天不重复通知。自动续费成功后,仅当资产处于可恢复停机状态且运营商状态不是风险停机或已销户时,自动调用既有复机;复机失败不回滚已成功的续费和钱包扣款,记录失败并通知客户和业务员。自动任务与手动续购并发时,手动续购优先;自动任务加锁重读后发现人工已完成续购即跳过,避免重复扣款。客户需要额外购买第二个周期时,须在首笔手动订单成功后再次主动下单。
|
||||||
|
|
||||||
|
## 2.12 已确认的代理分销码与提现资料基线
|
||||||
|
|
||||||
|
每个代理店铺创建时系统生成不可修改、全局唯一的随机分销码;二维码仅编码 H5 注册入口和该码。新代理扫码后以手机号短信验证码注册、自行设置密码,并创建待企业微信审批的下级代理及店铺,审批通过后启用。审批通过时新店铺设为分销码所属店铺的直接下级,并复制上级当时业务员为初始业务员,后续双方可独立调整。代理停用后其分销码立即不可注册,既有下级代理和既有佣金关系不受级联影响。
|
||||||
|
|
||||||
|
代理首次提现前提交提现资料资格申请,合同与法人身份证必填,企业微信审批通过且资料未过期才有效;资料变更或过期必须重审。代理提交提现申请时冻结可提现余额、金额、手续费、收款信息和可选发票快照,并自动创建企业微信提现审批,本地不提供人工通过或驳回。合同与法人身份证通过后长期有效,仅资料被代理替换、被超级管理员作废或代理停用时失效。法人身份证正、反面附件均必传。企业代理必须填写统一社会信用代码,个人代理填写法人身份证号;可选发票仅企业代理可上传并校验统一社会信用代码。营业执照、门头照、发票均为可选。合同资格申请必须填写签约主体统一社会信用代码或身份证号;上传发票时由代理填写发票抬头和统一社会信用代码,系统校验其与合同主体代码一致,企业微信审批人员核验附件真实性。企业微信驳回提现申请后,解冻该申请冻结的佣金余额;代理可修改金额、收款信息及本次可选发票后重新提交,每次创建新的企业微信审批实例,资料资格仍有效。企业微信通过即视为代理提现已到账,系统不登记或等待实际线下打款。发票是每笔提现申请的可选材料,如上传则按当前有效合同主体代码校验,并冻结至当次提现企业微信审批快照。
|
||||||
|
|
||||||
|
## 2.13 已确认的流量预警与通道阈值基线
|
||||||
|
|
||||||
|
套餐真流量预警使用套餐实际真流量,阈值为 1%~100% 的小数百分比;每个套餐商品最多一条当前规则,修改覆盖当前值,既有预警冻结阈值快照。按同一资产全部当前有效套餐的真流量用量和总量汇总计算使用比例,并使用主套餐规则判断;同一套餐使用记录与命中阈值只创建一条预警。以实际消耗流量的套餐记录关联资产为准,插拔卡场景同时展示卡和当前关联设备但不汇总多张卡。达到阈值时仅通知资产所属店铺当时有效业务员。规则停用后停止新触发且保留历史;启用或降低阈值后,下次扫描发现已有有效套餐达到阈值即补建预警。
|
||||||
|
|
||||||
|
运营商通道阈值默认关闭;开启时按运营商回传的卡当前计费周期累计流量判断。通道下每张卡独立判断,达量后系统创建可靠停机任务并自动调用运营商停机;达阈值即写入本地通道阈值停机锁,当前周期内拒绝复机,停机调用通过可靠重试或人工恢复确认最终结果。每个通道配置计费周期起始日(1~28,上海时区)。新周期开始后,仅对仍持有通道阈值停机锁、存在有效主套餐且不存在风险停机、销户或其他停机锁的卡自动复机;不符合条件只解除通道锁,不调用运营商复机。自动复机失败记录结果并按既有可靠机制处理。
|
||||||
|
|
||||||
|
## 2.14 已确认的佣金回溯与套餐层级展示基线
|
||||||
|
|
||||||
|
套餐退款佣金回溯后,原佣金记录保持不变,另建关联原佣金记录及退款单的负数佣金明细,佣金明细新增不可提现的“回溯”终态;回溯明细冻结原订单号及其他原佣金字段。部分退款按本次退款金额与订单冻结实收金额的比例,对每条原佣金等比例回溯,按分向下取整、最后一条补足舍入差,累计不超过原佣金。退款发生时佣金计算尚未完成的,等待其终态后再生成全部应有回溯明细;确认无佣金才标记无需回溯。同一退款业务幂等,不重复生成回溯明细。本期只处理套餐退款回溯,换货回溯待独立定义。佣金回溯时直接扣减佣金钱包,允许余额为负;先拒绝并释放待审核提现,再生成回溯明细和扣款流水。
|
||||||
|
|
||||||
|
后台资产详情及 H5 资产套餐历史均返回主套餐及关联加油包的层级结构,直接依据现有 `PackageUsage.master_usage_id` 分组,不新增关联表。加油包按生效时间正序排列,待生效包按购买创建时间正序并排在已生效包之后。无论主套餐或加油包已失效、过期或用尽,均保留层级关系;仅关联主套餐物理缺失时以“关联主套餐缺失”的异常独立项展示。
|
||||||
|
|
||||||
|
## 2.15 已确认的优先轮询定位
|
||||||
|
|
||||||
|
优先轮询是与现有普通轮询并行的高优先级调度队列,而不是一套新的轮询业务逻辑。资产可同时存在于普通轮询和优先轮询,进入优先队列不移除、暂停或改变普通轮询;优先队列仅使该资产额外优先执行同一套既有轮询内容、外部调用及状态同步。一次优先轮询执行成功后任务退出优先队列,资产仍按普通轮询继续运行;外部调用失败或超时按既有失败重试。第一版纳入无有效套餐、套餐过期续购、流量用完购买加油包、人工触发和普通轮询异常补偿五类场景。同一资产有未完成优先任务时,后续触发合并到该任务,追加触发次数、最近时间及来源,不重复调用同一轮普通轮询。优先队列复用普通轮询既有并发上限、失败重试和外部调用保护,仅调度顺序优先,不另建参数配置。
|
||||||
|
|
||||||
|
## 2.16 已确认的导出、时间筛选与代理自充收款方式基线
|
||||||
|
|
||||||
|
临期列表、佣金明细和套餐流量达量预警均复用现有异步导出任务,创建时冻结筛选条件、操作者及可见店铺范围。临期导出一行对应一项资产,取其当前生效主套餐最终到期时间和剩余天数;加油包不单独成行。流量达量预警导出一行对应一条预警记录,套餐、用量、总量、阈值和到期时间使用触发快照,店铺、业务员和用户组按导出执行时当前归属补充。佣金明细的入账后金额冻结每次佣金钱包变动后的实际余额,回溯记录可为负。
|
||||||
|
|
||||||
|
所有要求时间筛选的页面使用统一“开始时间—结束时间”组件和 `start_time/end_time` 参数,支持带时区的 RFC3339 秒级时间闭区间,任一端可不传。IoT/设备任务、换货、分配、订单、代理充值、佣金、提现和导出按创建或申请时间筛选;授权按授权发生时间;临期列表按套餐最终到期时间。导出必须复用当前页面全部筛选条件和创建时数据权限快照。
|
||||||
|
|
||||||
|
代理自充方式由超级管理员维护允许范围(仅微信、仅支付宝、同时支持);代理实际可用方式取该允许范围与当前可用商户池方式的交集,交集为空时拒绝创建在线充值单。平台用户和代理只可查询实际可用方式。配置变更不影响已创建未支付充值单的支付方式及商户快照,只影响后续新单。
|
||||||
|
|
||||||
|
## 2.17 已确认的报表基线
|
||||||
|
|
||||||
|
激活报表的采购数量以成功导入系统的设备数量计算,不另建采购或入库台账。功能上线后每日生成稳定日报快照;上线前日期不提供报表或明确显示无快照数据,不回填历史。累计激活设备严格采用任一当前关联卡已实名的口径;每日快照中的累计在网设备为已实名且存在有效主套餐的设备,活跃设备为该套餐周期内任一卡真流量大于零的设备,用量为设备当前套餐周期内全部关联卡真流量之和。报表可选择设备名称、型号、制造商、用户组、代理、店铺、业务员中的一个分组维度;未选择时汇总为一行。套餐续费按资产去重,统计期内有主套餐到期的资产计一次到期,至少成功续购一次主套餐计一次续费,续费率不超过 100%。日报快照冻结当天店铺、业务员及用户组归属。后端提供日/月趋势汇总数据和异步导出,图表渲染由前端负责。
|
||||||
|
|
||||||
|
## 2.18 已确认的店铺批量换绑 Excel 基线
|
||||||
|
|
||||||
|
店铺列表勾选批量换绑保持全量预校验、任一项失败整批不更新。Excel 导入复用现有统一导入任务处理方式,逐行执行:成功行提交,失败行不影响其他行,并输出成功数、失败数和逐行失败明细;不设 1000 行硬上限。两种入口均保留。Excel 使用店铺编码唯一定位店铺;换绑时以目标平台用户登录账号唯一定位业务员。每家实际变更店铺复用现有统一审计,记录原业务员、新业务员、操作人、时间和 Excel 行备注;批量任务同时保留汇总结果。
|
||||||
|
|
||||||
|
## 3. 当前阻塞与待后续独立需求
|
||||||
|
|
||||||
|
以下不是本轮继续扩展的产品设计题:
|
||||||
|
|
||||||
|
1. **OCR 外部契约**:仅可作为交易流水号、付款金额等字段的预填能力;接口、字段置信度、失败和重试规则须以外部契约为准。
|
||||||
|
2. **支付渠道契约**:微信直连、富友和支付宝的原路退款接口、超时查询及可恢复错误处理,须以各渠道实际契约为准。
|
||||||
|
3. **运营商契约**:通道计费周期起始日、停复机实际能力及结果未知后的查询/恢复,以运营商接口及业务提供的通道政策为准。
|
||||||
|
4. **换货佣金回溯**:本期只处理套餐退款;“不同资产换货”何时、按何金额回溯佣金未定义,后续如需实施须单独提出需求。
|
||||||
|
|
||||||
|
除上述依赖及独立后续需求外,本轮原始需求的业务规则已收口;后续工作是重编稳定需求 ID、建立原编号映射,并按影响范围拆分独立 OpenSpec Change,不直接开始编码。
|
||||||
@@ -320,7 +320,6 @@ func initServices(s *stores, deps *Dependencies) *services {
|
|||||||
assetService := assetSvc.New(deps.DB, s.Device, s.IotCard, s.PackageUsage, s.Package, s.PackageSeries, s.DeviceSimBinding, s.Shop, deps.Redis, iotCard, deps.GatewayClient, s.AssetIdentifier, s.Order, s.OrderItem, s.ExchangeOrder)
|
assetService := assetSvc.New(deps.DB, s.Device, s.IotCard, s.PackageUsage, s.Package, s.PackageSeries, s.DeviceSimBinding, s.Shop, deps.Redis, iotCard, deps.GatewayClient, s.AssetIdentifier, s.Order, s.OrderItem, s.ExchangeOrder)
|
||||||
assetService.SetAccessAudit(auditWriter)
|
assetService.SetAccessAudit(auditWriter)
|
||||||
agentOpenAPI := agentOpenAPISvc.New(assetService, packageService, orderService, shopCommission, stopResumeService, device, s.IotCard, s.PackageUsage, s.Package, s.PackageSeries, s.AgentWallet, s.DeviceSimBinding, s.Device)
|
agentOpenAPI := agentOpenAPISvc.New(assetService, packageService, orderService, shopCommission, stopResumeService, device, s.IotCard, s.PackageUsage, s.Package, s.PackageSeries, s.AgentWallet, s.DeviceSimBinding, s.Device)
|
||||||
agentOpenAPI.SetObservationSeriesDispatcher(observationSeries)
|
|
||||||
wecomApplicationRepository := wecomInfra.NewApplicationRepository(deps.DB)
|
wecomApplicationRepository := wecomInfra.NewApplicationRepository(deps.DB)
|
||||||
wecomSceneRepository := wecomInfra.NewSceneRepository(deps.DB)
|
wecomSceneRepository := wecomInfra.NewSceneRepository(deps.DB)
|
||||||
wecomMemberRepository := wecomInfra.NewMemberRepository(deps.DB)
|
wecomMemberRepository := wecomInfra.NewMemberRepository(deps.DB)
|
||||||
|
|||||||
@@ -4,11 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
cardObservationApp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
|
|
||||||
domainwallet "github.com/break/junhong_cmp_fiber/internal/domain/wallet"
|
domainwallet "github.com/break/junhong_cmp_fiber/internal/domain/wallet"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||||
@@ -40,12 +38,6 @@ type Service struct {
|
|||||||
agentWalletStore *postgres.AgentWalletStore
|
agentWalletStore *postgres.AgentWalletStore
|
||||||
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
||||||
deviceStore *postgres.DeviceStore
|
deviceStore *postgres.DeviceStore
|
||||||
observationSeries cardObservationApp.BestEffortSeriesDispatcher
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetObservationSeriesDispatcher 注入 OpenAPI 读取后的后台观测序列端口。
|
|
||||||
func (s *Service) SetObservationSeriesDispatcher(dispatcher cardObservationApp.BestEffortSeriesDispatcher) {
|
|
||||||
s.observationSeries = dispatcher
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New 创建代理开放接口业务编排服务
|
// New 创建代理开放接口业务编排服务
|
||||||
@@ -91,11 +83,7 @@ func (s *Service) GetCardTraffic(ctx context.Context, req *dto.AgentOpenAPICardQ
|
|||||||
if cardErr == nil {
|
if cardErr == nil {
|
||||||
// 独立卡:直接查卡维度流量
|
// 独立卡:直接查卡维度流量
|
||||||
if card.IsStandalone {
|
if card.IsStandalone {
|
||||||
resp, err := s.buildCardTrafficResponse(ctx, req.CardNo, "iot_card", card.ID, "")
|
return s.buildCardTrafficResponse(ctx, req.CardNo, "iot_card", card.ID, "")
|
||||||
if err == nil {
|
|
||||||
s.dispatchCardObservation(ctx, constants.CardObservationSceneOpenCardTraffic, card.ID, constants.CardObservationSyncTypeTraffic)
|
|
||||||
}
|
|
||||||
return resp, err
|
|
||||||
}
|
}
|
||||||
// 已绑定设备的卡:反查设备后查设备维度流量
|
// 已绑定设备的卡:反查设备后查设备维度流量
|
||||||
binding, err := s.deviceSimBindingStore.GetActiveBindingByCardID(ctx, card.ID)
|
binding, err := s.deviceSimBindingStore.GetActiveBindingByCardID(ctx, card.ID)
|
||||||
@@ -106,11 +94,7 @@ func (s *Service) GetCardTraffic(ctx context.Context, req *dto.AgentOpenAPICardQ
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询绑定设备信息失败")
|
return nil, errors.Wrap(errors.CodeInternalError, err, "查询绑定设备信息失败")
|
||||||
}
|
}
|
||||||
resp, buildErr := s.buildCardTrafficResponse(ctx, req.CardNo, "device", device.ID, device.VirtualNo)
|
return s.buildCardTrafficResponse(ctx, req.CardNo, "device", device.ID, device.VirtualNo)
|
||||||
if buildErr == nil {
|
|
||||||
s.dispatchCardObservation(ctx, constants.CardObservationSceneOpenCardTraffic, card.ID, constants.CardObservationSyncTypeTraffic)
|
|
||||||
}
|
|
||||||
return resp, buildErr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兜底:尝试解析为设备标识(支持 IMEI/虚拟号)
|
// 兜底:尝试解析为设备标识(支持 IMEI/虚拟号)
|
||||||
@@ -119,11 +103,7 @@ func (s *Service) GetCardTraffic(ctx context.Context, req *dto.AgentOpenAPICardQ
|
|||||||
// 两种解析都失败,返回原始卡解析错误(语义更贴近入参)
|
// 两种解析都失败,返回原始卡解析错误(语义更贴近入参)
|
||||||
return nil, cardErr
|
return nil, cardErr
|
||||||
}
|
}
|
||||||
resp, buildErr := s.buildCardTrafficResponse(ctx, req.CardNo, "device", device.ID, device.VirtualNo)
|
return s.buildCardTrafficResponse(ctx, req.CardNo, "device", device.ID, device.VirtualNo)
|
||||||
if buildErr == nil {
|
|
||||||
s.dispatchDeviceCardObservations(ctx, constants.CardObservationSceneOpenCardTraffic, device.ID, constants.CardObservationSyncTypeTraffic)
|
|
||||||
}
|
|
||||||
return resp, buildErr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildCardTrafficResponse 统一构造卡流量响应,支持卡和设备两种载体维度
|
// buildCardTrafficResponse 统一构造卡流量响应,支持卡和设备两种载体维度
|
||||||
@@ -195,7 +175,6 @@ func (s *Service) GetCardStatus(ctx context.Context, req *dto.AgentOpenAPICardQu
|
|||||||
StopReason: stopReason,
|
StopReason: stopReason,
|
||||||
StopReasonName: constants.AgentOpenAPIStopReasonName(stopReason),
|
StopReasonName: constants.AgentOpenAPIStopReasonName(stopReason),
|
||||||
}
|
}
|
||||||
s.dispatchCardObservation(ctx, constants.CardObservationSceneOpenCardNetwork, card.ID, constants.CardObservationSyncTypeNetwork)
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +189,6 @@ func (s *Service) GetRealnameStatus(ctx context.Context, req *dto.AgentOpenAPICa
|
|||||||
CardNo: req.CardNo,
|
CardNo: req.CardNo,
|
||||||
IsRealnamed: card.RealNameStatus == constants.RealNameStatusVerified,
|
IsRealnamed: card.RealNameStatus == constants.RealNameStatusVerified,
|
||||||
}
|
}
|
||||||
s.dispatchCardObservation(ctx, constants.CardObservationSceneOpenCardRealname, card.ID, constants.CardObservationSyncTypeRealname)
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,41 +398,6 @@ func (s *Service) CreateWalletPackageOrders(ctx context.Context, req *dto.AgentO
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) dispatchDeviceCardObservations(ctx context.Context, scene string, deviceID uint, syncType string) {
|
|
||||||
if s.observationSeries == nil || deviceID == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
requestID := ""
|
|
||||||
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
|
|
||||||
requestID = *value
|
|
||||||
}
|
|
||||||
s.observationSeries.DispatchDeviceCards(ctx, cardObservationApp.DeviceCardsSeriesRequest{
|
|
||||||
DeviceID: deviceID,
|
|
||||||
Request: cardObservationApp.SeriesRequest{
|
|
||||||
Scene: scene, ResourceType: constants.CardObservationResourceTypeDevice,
|
|
||||||
ResourceID: strconv.FormatUint(uint64(deviceID), 10), SyncType: syncType,
|
|
||||||
Source: constants.CardObservationSourceBusinessEvent,
|
|
||||||
RequestID: requestID, CorrelationID: requestID,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) dispatchCardObservation(ctx context.Context, scene string, cardID uint, syncType string) {
|
|
||||||
if s.observationSeries == nil || cardID == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
requestID := ""
|
|
||||||
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
|
|
||||||
requestID = *value
|
|
||||||
}
|
|
||||||
s.observationSeries.Dispatch(ctx, cardObservationApp.SeriesRequest{
|
|
||||||
Scene: scene, ResourceType: constants.CardObservationResourceTypeCard,
|
|
||||||
ResourceID: strconv.FormatUint(uint64(cardID), 10), SyncType: syncType,
|
|
||||||
Source: constants.CardObservationSourceBusinessEvent,
|
|
||||||
RequestID: requestID, CorrelationID: requestID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolveOpenAPIDevice 将开放接口设备标识解析为当前代理可见的设备
|
// resolveOpenAPIDevice 将开放接口设备标识解析为当前代理可见的设备
|
||||||
// 设备不存在或不属于代理管辖店铺时统一返回 CodeForbidden,避免信息泄露
|
// 设备不存在或不属于代理管辖店铺时统一返回 CodeForbidden,避免信息泄露
|
||||||
func (s *Service) resolveOpenAPIDevice(ctx context.Context, deviceNo string) (*model.Device, error) {
|
func (s *Service) resolveOpenAPIDevice(ctx context.Context, deviceNo string) (*model.Device, error) {
|
||||||
@@ -532,7 +475,6 @@ func (s *Service) GetDeviceTraffic(ctx context.Context, req *dto.AgentOpenAPIDev
|
|||||||
resp.PendingPackages = append(resp.PendingPackages, s.buildTrafficItem(usage, packageMap, seriesMap, false))
|
resp.PendingPackages = append(resp.PendingPackages, s.buildTrafficItem(usage, packageMap, seriesMap, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
s.dispatchDeviceCardObservations(ctx, constants.CardObservationSceneOpenDeviceTraffic, device.ID, constants.CardObservationSyncTypeTraffic)
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-31
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
现有佣金提现有申请和钱包事实;本 Change 以资格及审批实例补齐其前置条件,不以外部线下打款作为完成条件。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- 分销码由店铺唯一约束保护;扫码注册与审批结果使用稳定审批实例幂等消费。
|
||||||
|
- 资格申请与附件版本分表/快照,替换、作废和代理停用以状态失效,不覆盖历史审批。
|
||||||
|
- 提现创建在事务内锁定佣金钱包并写冻结和审批提交;通过仅一次扣减/确认,驳回仅一次释放。
|
||||||
|
|
||||||
|
## 业务动作契约
|
||||||
|
|
||||||
|
### 分销码与扫码注册
|
||||||
|
|
||||||
|
- 代理店铺创建事务生成全局唯一、不可修改的随机 `distribution_code`;二维码只包含 H5 注册入口及该码。唯一冲突重试生成,不允许人工指定或编辑。
|
||||||
|
- `POST /api/c/v1/agent-distribution-registrations`:提交 `distribution_code`、短信已验证手机号、密码及既有注册必填资料。服务锁定上级店铺,校验其代理启用;失效码/停用代理统一返回“分销码不可用”。成功仅创建待审批代理/店铺和企业微信审批实例,不建立下级归属。
|
||||||
|
- 企业微信通过消费者幂等启用新代理/店铺,并在同一事务写直接上级店铺和上级当前业务员快照;驳回不启用。重复回调不重复创建层级或账号。
|
||||||
|
|
||||||
|
### 提现资格资料
|
||||||
|
|
||||||
|
- `POST /shops/:shop_id/withdrawal-qualifications`:仅本人代理店铺。请求合同、法人身份证正反面附件;企业必须提供统一社会信用代码,个人必须提供法人身份证号;可选营业执照、门头照;企业可选发票,发票抬头和统一社会信用代码必须与合同主体一致。创建新资料版本并提交企业微信,替换合同或身份证立即使旧有效资格失效。
|
||||||
|
- 超级管理员作废资格必须填写原因;代理停用自动失效全部有效资格。合同与身份证审批通过后长期有效,直至替换、作废或停用;历史版本、附件、审批实例永不覆盖。
|
||||||
|
|
||||||
|
### 提现申请与审批
|
||||||
|
|
||||||
|
- 既有 `POST /shops/:shop_id/withdrawal-requests` 增加资格有效校验。锁定该店铺佣金钱包后冻结可提现余额、申请金额、手续费/实际到账金额、收款信息和可选发票快照;余额不足、资格无效或非本人代理均不创建申请或冻结。
|
||||||
|
- 企业微信通过时以申请/审批实例条件更新一次确认到账;驳回时仅一次释放冻结。代理可在资格仍有效时修改被驳回申请的金额、收款信息和申请级发票并创建新审批实例;资料资格不因提现驳回失效。
|
||||||
|
- 新业务禁用本地 `approve/reject` 终审;提交/回调未知复用既有审批恢复,不允许通过重提制造第二笔冻结。每次资格、冻结、审批终态和重提均记录审计。
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
新增成对迁移及唯一/状态索引;隔离库验证码唯一和停用、资格失效、提现冻结/重提、重复审批回调及 up/down/up。
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
## Scope
|
||||||
|
|
||||||
|
- 迭代编号:`AUG26-008`。
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
代理下级归属和提现资料/资金缺少企业微信终审及失效边界,无法可靠追溯。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 代理店铺唯一分销码和审批后下级注册。
|
||||||
|
- 合同、法人身份证为核心的提现资格审批与失效。
|
||||||
|
- 提现余额/资料快照、企微终审和驳回释放。
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
- `agent-distribution-withdrawal`: 分销注册、资料资格与提现审批。
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
- 无。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
影响代理店铺、H5 注册、附件、佣金钱包、企业微信审批和 Schema。
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
## Purpose
|
||||||
|
|
||||||
|
使代理下级注册和佣金提现均以企业微信审批、资格有效性和不可变业务快照为准,避免代理层级或资金事实因资料变更、重复回调而漂移。
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 分销码与下级代理注册
|
||||||
|
系统 SHALL 在每个代理店铺创建时生成全局唯一、不可修改的随机分销码;二维码仅编码 H5 注册入口和该码。代理扫码后以手机号短信验证、设置密码并创建待企业微信审批的下级代理和店铺;仅审批通过时启用,并将新店铺设为分销码所属店铺的直接下级,复制上级当时业务员为初始业务员。代理停用后其码立即不可注册,既有下级和佣金关系不级联变更。
|
||||||
|
|
||||||
|
#### Scenario: 停用代理码注册
|
||||||
|
- **WHEN** 客户使用已停用代理所属店铺的分销码注册
|
||||||
|
- **THEN** 系统拒绝创建下级代理或店铺
|
||||||
|
|
||||||
|
### Requirement: 提现资料资格
|
||||||
|
代理首次提现前 SHALL 提交企业微信资料资格申请;合同和法人身份证正反面必填,企业代理填写统一社会信用代码、个人代理填写法人身份证号。合同资格主体必须填写统一社会信用代码或身份证号;营业执照、门头照可选,发票仅企业可选且其抬头/统一社会信用代码必须与合同主体一致。审批通过且资料未过期才有效;合同和身份证通过后长期有效,直到代理替换资料、超级管理员作废或代理停用。
|
||||||
|
|
||||||
|
#### Scenario: 资料替换后提现
|
||||||
|
- **WHEN** 有效资格的代理替换合同或法人身份证资料
|
||||||
|
- **THEN** 原资格失效,代理必须重新审批通过后才可提现
|
||||||
|
|
||||||
|
### Requirement: 提现冻结与企业微信终审
|
||||||
|
提现申请 SHALL 冻结可提现余额、金额、手续费、收款信息和可选发票快照,并创建企业微信审批。本地不得人工通过或驳回;企业微信通过即视为已到账,驳回时释放本申请冻结余额。驳回后代理可修改金额、收款信息和本次发票重新提交,每次新建审批实例;资料资格保持有效。发票为申请级材料,若上传必须按当时有效合同主体校验并冻结。
|
||||||
|
|
||||||
|
#### Scenario: 提现审批驳回
|
||||||
|
- **WHEN** 企业微信最终驳回一笔提现申请
|
||||||
|
- **THEN** 系统仅一次释放其冻结佣金余额,保留审批快照,并允许在资格仍有效时修改后重提
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
## 1. 分销与资格
|
||||||
|
- [ ] 1.1 追踪代理/店铺创建、H5 短信注册、佣金提现、附件、企微审批和钱包冻结链路。
|
||||||
|
- [ ] 1.2 新增分销码、资格申请/资料版本、提现审批快照的成对迁移、模型、状态与唯一约束。
|
||||||
|
- [ ] 1.3 实现分销注册、停用门禁、审批后下级归属/业务员快照及审计。
|
||||||
|
- [ ] 1.4 实现资格提交、主体/附件/发票校验、失效和企微回调。
|
||||||
|
|
||||||
|
## 2. 提现
|
||||||
|
- [ ] 2.1 实现有效资格校验、钱包锁定冻结、申请快照、企微终审、驳回释放和新实例重提。
|
||||||
|
- [ ] 2.2 注册后台/H5 路由及 OpenAPI,保障附件和数据范围。
|
||||||
|
|
||||||
|
## 3. 验证
|
||||||
|
- [ ] 3.1 隔离库验证迁移、分销码停用、资格替换、冻结/释放和重复回调。
|
||||||
|
- [ ] 3.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-agent-distribution-withdrawal-qualification --strict` 与 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-31
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
## Decisions
|
||||||
|
|
||||||
|
- 线上充值复用支付单和实际商户路由,成功消费者以支付单/钱包流水唯一约束入账。
|
||||||
|
- 线下申请保存不可变金额和付款证据快照;审批回调在主钱包锁事务中条件入账。
|
||||||
|
- 线上未知结果和线下审批未知均保持在途,复用既有查询/恢复,不把重试当作新入账。
|
||||||
|
|
||||||
|
## 配置与充值动作契约
|
||||||
|
|
||||||
|
### 允许方式与查询
|
||||||
|
|
||||||
|
- `GET /agent-self-recharge-payment-methods`:代理、平台用户仅返回“全局允许方式 ∩ 当前启用商户池可用方式”的有序 `wechat`/`alipay` 列表;不得返回商户身份、凭证或全局允许范围。交集为空返回空列表。
|
||||||
|
- `PUT /agent-self-recharge-payment-methods`:仅超级管理员,保存 `wechat_only`、`alipay_only` 或 `wechat_and_alipay`;记录操作者、前后值和时间。修改不更新任何已有充值/支付单的支付方式、商户 ID 或快照。
|
||||||
|
|
||||||
|
### 自身店铺充值
|
||||||
|
|
||||||
|
- `POST /agent-recharges` 的代理在线分支强制 `shop_id` 为空且认证店铺为已启用代理自身店铺;传入下级/其他店铺返回无权。线上请求 `amount`、`payment_method`、`request_id`,其中方式必须在当前交集内;以 `request_id` 与调用者/店铺唯一复用既有支付创建结果,失败预下单不入账。
|
||||||
|
- 在线创建在事务内选择实际商户、冻结支付/商户快照并创建充值记录;渠道成功消费者锁定支付、充值和主钱包,以支付 ID/钱包流水唯一约束一次入账。失败、关闭、退款或未知状态不加余额;未知结果由既有查单/回调恢复,禁止客户端重试直接增加余额。
|
||||||
|
|
||||||
|
### 线下转账
|
||||||
|
|
||||||
|
- 线下请求必须包含 `amount`(正分)、`payer_name`、`transferred_at`(带时区时间)、`transfer_channel_or_bank`、`transaction_no`、至少一个 `payment_voucher_key` 和可选备注;创建时冻结全部字段、状态为待企业微信审批,不增加主钱包。
|
||||||
|
- 企业微信通过消费者锁定申请、审批实例和主钱包,条件更新一次增加余额和钱包流水;驳回、撤回、关闭不入账。仅未成功申请可修改上述材料并创建新审批实例重提;未知审批保持在途。平台/超级管理员查询和处理一律先应用既有店铺数据范围,审计不记录凭证正文。
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
新增线下充值申请、附件快照、审批关联及唯一约束的成对迁移;验证权限、回调重放、未知、驳回重提和 up/down/up。
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
## Scope
|
||||||
|
|
||||||
|
- 迭代编号:`AUG26-017`。
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
代理自助充值需同时覆盖线上支付和可审计的线下转账,且不能将付款成功与钱包入账混淆。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增代理自身店铺线上微信/支付宝充值。
|
||||||
|
- 新增带凭证的线下转账申请及企业微信终审。
|
||||||
|
- 固化支付回调、审批回调和钱包入账幂等。
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
- `agent-self-recharge-payment`: 代理自助充值支付方式。
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
- 无。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
影响代理主钱包、支付商户、企业微信审批、附件、审计和 Schema。
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 代理自助充值支付方式
|
||||||
|
超级管理员 SHALL 维护代理在线自充允许方式:仅微信、仅支付宝或微信和支付宝;代理实际可用线上方式为该允许范围与当前可用对应商户池方式的交集,交集为空时拒绝创建线上充值单。平台用户和代理仅可查询实际可用方式,不得查看或修改允许范围。配置变更只影响后续新单,已创建未支付充值单保留其支付方式及商户快照。系统 SHALL 允许已启用代理在其自身店铺充值入口选择实际可用的线上微信、线上支付宝或线下转账;不得为下级店铺代充。线上方式创建支付单并按实际商户路由,渠道成功回调幂等增加该代理主钱包余额;失败、关闭或未知不得增加余额,未知结果通过既有支付查询/回调恢复,不允许重复支付单入账。
|
||||||
|
|
||||||
|
线下转账申请必须填写转账金额、付款人、转账时间、银行/支付渠道、流水号和凭证附件;创建后状态为待企业微信审批,不立即入账。企业微信通过时在事务内锁定申请并仅一次增加主钱包余额;驳回、关闭或撤回不入账,代理可修改未成功申请后以新审批实例重提。充值金额以分保存,展示元时两位小数;平台/超级管理员仅可查看和处理其既有数据范围。
|
||||||
|
|
||||||
|
#### Scenario: 配置与商户池交集为空
|
||||||
|
- **WHEN** 超级管理员允许一种线上方式,但该方式没有可用商户池成员
|
||||||
|
- **THEN** 代理可用线上方式列表不含该方式,创建该方式充值单被拒绝,已创建未支付单不受影响
|
||||||
|
|
||||||
|
#### Scenario: 重复线上成功回调
|
||||||
|
- **WHEN** 同一线上充值支付成功回调被重复投递
|
||||||
|
- **THEN** 系统只增加一次代理主钱包余额并保留幂等支付事实
|
||||||
|
|
||||||
|
#### Scenario: 线下申请审批驳回
|
||||||
|
- **WHEN** 企业微信驳回线下转账充值申请
|
||||||
|
- **THEN** 系统不增加钱包余额,并允许代理修改申请后创建新审批实例重提
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 代理自充支付方式
|
||||||
|
系统 SHALL 使代理在线自充可用支付方式等于超级管理员维护的允许范围与当前启用商户池支付方式的交集;交集为空时拒绝创建新充值单。配置变更仅影响后续订单,既有未支付订单保留其支付方式和商户快照。
|
||||||
|
|
||||||
|
#### Scenario: 规则命中
|
||||||
|
- **WHEN** 业务请求或任务满足本需求定义的前置条件
|
||||||
|
- **THEN** 系统按上述规则完成处理、保留可追溯事实,并拒绝与状态、权限或幂等约束冲突的重复操作
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
## 1. 充值实现
|
||||||
|
- [ ] 1.1 追踪代理钱包、线上支付、商户路由、线下附件和企业微信审批链路。
|
||||||
|
- [ ] 1.2 新增线下申请/快照/审批关联的成对迁移、模型、状态和幂等约束。
|
||||||
|
- [ ] 1.3 实现自身店铺门禁、线上支付创建/成功入账恢复、线下申请/企微终审/重提和钱包事务审计。
|
||||||
|
- [ ] 1.4 注册路由、OpenAPI及代理/平台查询数据范围。
|
||||||
|
|
||||||
|
## 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。
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-31
|
||||||
20
openspec/changes/add-asset-package-hierarchy/design.md
Normal file
20
openspec/changes/add-asset-package-hierarchy/design.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
套餐使用记录已有 `master_usage_id`,不能为展示重复存储关系。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- 查询批量读取资产全部套餐使用,按 `master_usage_id` 内存分组并稳定排序。
|
||||||
|
- 不改变套餐状态、金额或生命周期;缺失主记录只形成读模型异常项。
|
||||||
|
|
||||||
|
## 查询与响应契约
|
||||||
|
|
||||||
|
- 后台资产详情及 H5 `GET /api/c/v1/asset/package-history` 保持既有资产权限、分页和套餐字段;不新增写接口、迁移、关联表或状态变更。查询需在资产范围内一次读取该资产全部相关 `PackageUsage`,不能按每个主套餐逐条查询加油包。
|
||||||
|
- 响应主项包含原套餐使用字段、`children` 加油包数组和 `expand_by_default`;主项 `master_usage_id` 必须为 `null`。子项保留自身 `package_usage_id`、`master_usage_id`、状态、购买创建时间、生效时间和原历史字段。
|
||||||
|
- 以 `master_usage_id IS NULL` 的记录为主套餐;关联存在的加油包嵌入对应主项。已生效加油包先按生效时间正序,待生效加油包后按购买创建时间正序;排序字段相同再按 `package_usage_id` 正序,保证后台和 H5 一致。
|
||||||
|
- 主套餐只要有一个关联子项即 `expand_by_default=true`;无子项为 `false`。主套餐、子项失效、过期、用尽、退款或历史状态均不得删除或改写层级。
|
||||||
|
- 加油包的 `master_usage_id` 指向物理不存在记录时,返回顶层异常项:保留原字段、`relationship_status=master_missing`、`relationship_status_name=关联主套餐缺失`、`children=[]`、`expand_by_default=false`;不得猜测替代主套餐或丢弃该项。
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
验证多主套餐、待生效/失效加油包、缺失主记录、后台/H5 数据范围和分页。
|
||||||
24
openspec/changes/add-asset-package-hierarchy/proposal.md
Normal file
24
openspec/changes/add-asset-package-hierarchy/proposal.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
## Scope
|
||||||
|
|
||||||
|
- 迭代编号:`AUG26-013`。
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
资产套餐历史平铺展示,无法识别主套餐与其加油包的真实关联。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 在后台和 H5 按既有套餐使用关联投影主套餐—加油包层级。
|
||||||
|
- 保留失效历史;主套餐物理缺失作为可观察异常。
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
- 无。
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
- `package-lifecycle`: 套餐历史层级展示。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
影响套餐使用查询、后台资产详情、H5 和 OpenAPI。
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 资产套餐层级投影
|
||||||
|
系统 SHALL 在后台资产详情和 H5 资产套餐历史中,直接以既有 `PackageUsage.master_usage_id` 将主套餐和关联加油包投影为层级结构,不新增关联表。响应中每个主套餐必须返回 `expand_by_default`:存在至少一个关联加油包时为 `true`,否则为 `false`;后台与 H5 使用同一规则。加油包按生效时间正序;待生效加油包按购买创建时间正序并排在已生效包之后。主套餐或加油包失效、过期、用尽时仍保留层级;仅关联主套餐物理缺失时作为“关联主套餐缺失”异常独立项返回。
|
||||||
|
|
||||||
|
#### Scenario: 已失效加油包
|
||||||
|
- **WHEN** 某加油包及其主套餐已失效但主套餐记录仍存在
|
||||||
|
- **THEN** 系统仍将加油包嵌入该主套餐层级,不因状态失效拆散关系
|
||||||
|
|
||||||
|
#### Scenario: 主套餐含加油包
|
||||||
|
- **WHEN** 主套餐关联至少一条加油包使用记录
|
||||||
|
- **THEN** 后台和 H5 返回该主套餐时均将 `expand_by_default` 设为 `true`
|
||||||
|
|
||||||
|
#### Scenario: 主套餐物理缺失
|
||||||
|
- **WHEN** 加油包关联的主套餐使用记录不存在
|
||||||
|
- **THEN** 系统返回带“关联主套餐缺失”标识的异常独立项
|
||||||
7
openspec/changes/add-asset-package-hierarchy/tasks.md
Normal file
7
openspec/changes/add-asset-package-hierarchy/tasks.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
## 1. 查询契约
|
||||||
|
- [ ] 1.1 追踪后台资产详情、H5 套餐历史、`PackageUsage` 和 `master_usage_id` 查询链路。
|
||||||
|
- [ ] 1.2 实现批量层级投影、稳定排序和缺失主套餐异常项;更新 DTO、路由说明和 OpenAPI。
|
||||||
|
|
||||||
|
## 2. 验证
|
||||||
|
- [ ] 2.1 验证多层级、失效保留、待生效排序、缺失主记录及后台/H5 数据范围。
|
||||||
|
- [ ] 2.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-asset-package-hierarchy --strict` 和 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-31
|
||||||
31
openspec/changes/add-asset-wallet-auto-renewal/design.md
Normal file
31
openspec/changes/add-asset-wallet-auto-renewal/design.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
套餐续购、资产钱包和复机已有独立事实;自动续费仅编排这些既有能力,不能把复机失败当作资金失败。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- 保存全局配置和按资产/日期的尝试记录,用唯一约束保证每日一次。
|
||||||
|
- Worker 按资产锁重读资格、当前售价和余额,以既有订单/钱包事务完成续购;人工订单通过同一锁优先。
|
||||||
|
- 成功后以可靠任务调用复机,单独记录结果;通知使用既有事件去重。
|
||||||
|
|
||||||
|
## 配置、扫描与执行契约
|
||||||
|
|
||||||
|
### 配置维护
|
||||||
|
|
||||||
|
- `GET /asset-auto-renewal-config` 与 `PUT /asset-auto-renewal-config` 仅超级管理员、平台用户。配置为单例:`enabled`、`scope`(`all_main_packages`/`specified_main_packages`)、`package_ids`(指定范围时非空且只能是可售主套餐)、`days_before_expiry`(正整数)。保存时记录操作者、前后快照和时间;代理、企业、个人客户无读取或修改入口。
|
||||||
|
- 配置变更只影响后续扫描,已产生的尝试记录不重算;关闭开关后 Worker 不创建新尝试或订单。
|
||||||
|
|
||||||
|
### 每日扫描与尝试
|
||||||
|
|
||||||
|
- Worker 在上海自然日按资产扫描,先用 `(asset_id, attempt_date)` 唯一记录占位,确保每资产每天至多一次尝试。仅选择存在当前有效主套餐、套餐未到期、最终到期时间进入 `days_before_expiry` 窗口、套餐在配置范围且资产钱包正常的资产;加油包、已到期套餐、流量阈值事件均不是触发源。
|
||||||
|
- Worker 对每项候选锁定资产、当前主套餐、资产钱包和当日尝试,再次读取配置、到期时间、当前可售续费价与人工订单。人工成功续购已产生时,标记跳过且不扣款;余额不足或套餐不可续费时记录失败原因、不建订单、不扣款。
|
||||||
|
|
||||||
|
### 续费、通知与复机
|
||||||
|
|
||||||
|
- 合格项复用既有资产钱包订单/套餐生效事务,以执行时当前续费价扣同一资产钱包可用余额,创建同套餐商品续购订单、钱包流水和套餐使用事实;任一步失败整体回滚资金、订单和套餐,并写失败尝试。成功后当日不再处理该资产。
|
||||||
|
- 余额不足、不可续费、订单失败和复机失败均使用客户/业务员/日期/原因类型幂等键投递最多一条通知;接收人只在事件创建时解析,业务员不存在时不阻断续费或尝试记录。
|
||||||
|
- 续费成功后仅当资产处于可恢复停机且运营商状态不是风险停机或已销户时投递可靠复机任务。复机成功更新既有状态;失败/未知保存执行结果并走既有恢复,不回滚钱包扣款、订单、套餐生效或续费成功事实。
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
新增成对迁移;隔离库验证范围、窗口、每日去重、价格、余额、手动并发、复机和 up/down/up。
|
||||||
25
openspec/changes/add-asset-wallet-auto-renewal/proposal.md
Normal file
25
openspec/changes/add-asset-wallet-auto-renewal/proposal.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
## Scope
|
||||||
|
|
||||||
|
- 迭代编号:`AUG26-010`。
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
客户容易遗漏套餐续费;资产钱包余额充足时应在到期前自动续购,但不得与手动购买、停复机或钱包资金事实混淆。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增全局自动续费范围和最终到期前天数配置。
|
||||||
|
- 每资产每天一次从同资产钱包按当前续费价续购。
|
||||||
|
- 手动优先,失败通知,成功后条件复机且复机失败不回滚续费。
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
- `asset-auto-renewal`: 资产钱包自动续费。
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
- 无。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
影响套餐、资产钱包、订单、任务、运营商复机、通知和 Schema。
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
## Purpose
|
||||||
|
|
||||||
|
在套餐最终到期前的受控窗口内,仅以同一资产钱包余额自动续购当前有效主套餐,并使扣款、套餐生效和复机失败具有明确且可恢复的边界。
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 自动续费配置、权限与频率
|
||||||
|
仅超级管理员和平台用户 SHALL 查看或修改全局自动续费开关、适用全部或指定主套餐及统一到期前 N 天;每次新增、修改、启用、停用必须记录操作者、修改前后值和时间。系统 SHALL 对已保存的有效配置执行续费:仅对当前有效主套餐、尚未到期、进入最终到期前窗口的资产处理;不得按流量阈值触发。每项资产每天最多尝试一次,成功后停止,套餐到期后不再自动尝试。
|
||||||
|
|
||||||
|
续费 MUST 仅扣该资产钱包可用余额,续购同一套餐商品,价格取执行时当前渠道可售续费价。余额不足或套餐不可续费时,不创建订单、不扣款,并向当前个人客户和资产所属店铺当时有效业务员各创建每日至多一条通知。
|
||||||
|
|
||||||
|
#### Scenario: 无权限修改配置
|
||||||
|
- **WHEN** 代理、企业或个人客户请求修改自动续费配置
|
||||||
|
- **THEN** 系统拒绝请求且不改变配置或产生执行任务
|
||||||
|
|
||||||
|
#### Scenario: 窗口内余额不足
|
||||||
|
- **WHEN** 合格资产进入自动续费窗口但资产钱包余额不足
|
||||||
|
- **THEN** 系统记录当日尝试失败且不扣款,并向当前客户和有效业务员各投递一次通知
|
||||||
|
|
||||||
|
### Requirement: 并发、成功与复机
|
||||||
|
手动续购 SHALL 优先于自动续费。自动任务必须锁定并重读资产、套餐和钱包;发现人工已成功续购时跳过,避免重复扣款。成功续费后,仅当资产为可恢复停机且运营商状态不是风险停机或已销户时,系统调用既有复机;复机失败不得回滚已成功的订单、套餐或钱包扣款,必须保存失败结果并通知客户和业务员。
|
||||||
|
|
||||||
|
#### Scenario: 手动续购并发成功
|
||||||
|
- **WHEN** 自动任务锁定后发现同一资产已由人工成功续购
|
||||||
|
- **THEN** 自动任务不创建第二笔订单、不扣款,并结束本次尝试
|
||||||
9
openspec/changes/add-asset-wallet-auto-renewal/tasks.md
Normal file
9
openspec/changes/add-asset-wallet-auto-renewal/tasks.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
## 1. 配置与执行
|
||||||
|
- [ ] 1.1 追踪套餐最终到期、续购价格、资产钱包、手动订单、停复机和通知链路。
|
||||||
|
- [ ] 1.2 新增配置、每日尝试/结果的成对迁移、模型、唯一约束和管理接口。
|
||||||
|
- [ ] 1.3 实现每日扫描、资格判断、资产锁、当前价格订单与同钱包扣款,保证手动优先。
|
||||||
|
|
||||||
|
## 2. 副作用与验证
|
||||||
|
- [ ] 2.1 实现余额/不可续费通知、成功后的条件复机、复机失败记录和通知;不得回滚续费。
|
||||||
|
- [ ] 2.2 更新路由/OpenAPI。
|
||||||
|
- [ ] 2.3 隔离库验证窗口、每日一次、并发、资金、通知、复机及 up/down/up;运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-asset-wallet-auto-renewal --strict` 和 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-09-02
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
生产环境在异常重启恢复后,Worker 对审计与外部交互记录执行的大量读写和留存扫描使 PostgreSQL 出现严重磁盘 I/O 等待。维护者需要能在不修改代码的情况下临时停止新增统一审计记录和外部交互记录,并停止其归档、留存任务,以保护核心业务数据库可用性。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增默认启用的运行时配置开关,分别控制统一审计事件及资源快照、外部交互日志的新增持久化;关闭后不再向 `tb_audit_event`、`tb_audit_event_resource`、`tb_integration_log` 写入新记录。
|
||||||
|
- 关闭记录开关时,业务操作、外部调用、回调处理和既有领域状态处理继续执行;审计调查和外部交互日志查询仅返回已有历史记录。
|
||||||
|
- **BREAKING** 关闭开关期间不产生审计事实、资源快照或外部交互记录,也不建立新的审计与外部交互关联;依赖 Integration Log 作为内部重试或幂等辅助信息的调用必须在不落库时保持既有业务正确性。
|
||||||
|
- 新增默认启用的 Worker 开关,关闭后不注册、不调度且不消费 Audit 日归档、Integration Log 日归档和日志日留存任务;已入队的对应任务必须安全跳过,不扫描或修改三张表。
|
||||||
|
- 保持现有归档和留存开关的语义不变;新的任务总开关独立于“是否物理清理”的开关。
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `observability-write-controls`: 审计、外部交互记录和其后台归档/留存任务的运行时启停控制。
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `operations-audit`: 允许维护者在运行时关闭新增审计事实及审计相关后台任务,并定义关闭期间的查询与任务行为。
|
||||||
|
- `external-integration`: 允许维护者在运行时关闭新增外部交互日志及其归档/留存任务,并定义外部调用与回调的降级边界。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- 配置:`pkg/config` 默认配置、环境变量映射和生产运行说明。
|
||||||
|
- 运行装配:`cmd/api`、`cmd/worker`、`internal/bootstrap`、任务注册与处理。
|
||||||
|
- 基础设施:统一审计 Writer、Integration Log Repository 及其调用方的无记录降级路径。
|
||||||
|
- 任务:Audit 日归档、Integration 日归档、日志日留存任务及其 Asynq 队列处理。
|
||||||
|
- 文档:OpenSpec 契约、生产运维说明与配置说明。
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 审计与外部交互记录运行时写入开关
|
||||||
|
系统 SHALL 提供默认启用、可由配置文件及环境变量覆盖的独立运行时开关,分别控制统一审计事件/资源快照和外部交互日志的新建持久化。关闭审计开关时,不得向 `tb_audit_event` 或 `tb_audit_event_resource` 写入新记录;关闭外部交互开关时,不得向 `tb_integration_log` 写入新记录。已有历史记录仍可按既有权限查询。
|
||||||
|
|
||||||
|
关闭任一开关不得阻断业务状态变更、外部调用、回调处理、钱包/订单等领域事实或已有幂等语义;不得将 Integration Log 是否落库作为业务正确性前提。开关重新启用后只记录后续操作,不回填关闭期间事实。
|
||||||
|
|
||||||
|
#### Scenario: 关闭审计写入
|
||||||
|
- **WHEN** 审计写入开关关闭且业务操作成功执行
|
||||||
|
- **THEN** 业务操作正常完成,系统不创建审计事件或资源快照
|
||||||
|
|
||||||
|
#### Scenario: 关闭外部交互日志写入
|
||||||
|
- **WHEN** 外部调用或回调在外部交互日志开关关闭期间执行
|
||||||
|
- **THEN** 系统维持既有调用、回调和幂等业务结果,且不创建新的外部交互日志
|
||||||
|
|
||||||
|
### Requirement: 审计与外部交互后台任务总开关
|
||||||
|
系统 SHALL 提供默认启用的 Worker 总开关,分别控制审计日归档、外部交互日志日归档和日志日留存任务。关闭时 Worker 不注册、不调度且不消费对应任务;已经入队的任务被消费时必须安全跳过,不扫描、归档、删除或修改三张目标表。该总开关独立于既有物理清理配置。
|
||||||
|
|
||||||
|
#### Scenario: 已入队任务在关闭后执行
|
||||||
|
- **WHEN** 相关归档或留存任务已入队且对应 Worker 开关后来关闭
|
||||||
|
- **THEN** 任务安全跳过,目标表不发生扫描或写入
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-31
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
通道累计流量和停复机已有外部调用链;本能力增加本地停机锁及可靠任务,不改变套餐预警。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- 通道配置和卡周期锁分表;锁以通道、卡、周期唯一。
|
||||||
|
- 达量事务写锁和 Outbox 停机任务;新周期扫描按锁、主套餐和其他锁决定仅解锁或复机。
|
||||||
|
|
||||||
|
## 配置、锁与任务契约
|
||||||
|
|
||||||
|
### 通道配置
|
||||||
|
|
||||||
|
- 扩展既有运营商通道创建/编辑 DTO:`traffic_threshold_enabled`、`traffic_threshold_value`、`traffic_threshold_unit`、`traffic_period_type`、`traffic_period_start_day`、`traffic_threshold_status`。仅超级管理员、平台用户可读写;阈值必须为正数,单位只能是系统支持的 MB/GB,周期起始日为 1~28,所有时间边界按上海时区计算。
|
||||||
|
- 启用时完整校验字段;停用时停止后续达量判断但不删除当前周期锁或历史任务结果。新增、更新、启用、停用均写通道 ID、前后字段、操作者和时间审计;不向代理、企业、个人客户暴露配置字段。
|
||||||
|
|
||||||
|
### 达量停机
|
||||||
|
|
||||||
|
- 运营商流量同步/周期扫描按卡当前所属通道及上海时区周期边界读取运营商回传累计流量,换算至配置单位;不使用套餐真流量预警数据。达到或超过阈值时,事务中以 `(channel_id, card_id, period_start)` 唯一键创建通道阈值停机锁和 Outbox 停机任务。
|
||||||
|
- 唯一冲突表示该周期已处理;重复同步、并发扫描或任务重放不得创建第二把锁或重复发起停机。停机调用失败/未知保留锁、任务结果与安全失败原因,复用既有外部调用恢复;锁存在时所有人工或自动复机入口先拒绝。
|
||||||
|
|
||||||
|
### 新周期解锁与复机
|
||||||
|
|
||||||
|
- 周期转换任务锁定上周期仍有效的通道锁,解除其通道阈值限制;随后重新检查当前有效主套餐、风险停机、销户和其他停机锁。仅全部条件允许时写可靠复机任务;任一条件不满足时只解锁,不调用运营商。
|
||||||
|
- 复机任务成功记录结果;失败/未知保留可恢复执行结果,不重建通道锁或改变套餐状态。通道停用、卡换通道或删除配置均不得使历史锁/任务失去审计关联。
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
新增成对迁移;隔离库验证周期边界、重复达量、停机失败、复机条件和 up/down/up。
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
## Scope
|
||||||
|
|
||||||
|
- 迭代编号:`AUG26-011`。
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
运营商通道需按其计费周期流量自动停复机,不能与套餐预警混用。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增通道阈值、周期和停机锁。
|
||||||
|
- 达量可靠停机,新周期按套餐和其他锁条件复机。
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
- `carrier-channel-traffic-threshold`: 通道流量阈值控制。
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
- 无。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
影响通道、卡状态、可靠任务、外部运营商调用和 Schema。
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
## Purpose
|
||||||
|
|
||||||
|
按运营商通道自身计费周期和累计流量控制停复机,独立于套餐真流量预警。
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 通道阈值配置、权限与审计
|
||||||
|
仅超级管理员和平台用户 SHALL 在运营商通道新建或编辑时配置流量阈值开关、阈值数值、流量单位、统计周期和生效状态;阈值数值必须为正,单位必须为系统支持单位,统计周期必须能换算为明确起止边界。新增、修改、启用、停用均必须记录操作者、修改前后字段和时间。代理、企业和个人客户不得读取或修改通道阈值配置。
|
||||||
|
|
||||||
|
#### Scenario: 越权修改通道阈值
|
||||||
|
- **WHEN** 非超级管理员、非平台用户请求创建、编辑或启停通道阈值
|
||||||
|
- **THEN** 系统拒绝请求,不修改配置且不产生审计成功事实
|
||||||
|
|
||||||
|
### Requirement: 通道阈值停机与周期恢复
|
||||||
|
系统 SHALL 为每个已启用运营商通道按其配置统计周期判断运营商回传的每张卡当前周期累计流量;达量即写通道阈值停机锁、创建可靠停机任务并调用运营商停机。持锁卡在当前周期内 MUST 拒绝复机;调用失败或未知保留任务结果并按既有恢复机制处理。
|
||||||
|
|
||||||
|
新周期开始时,系统 SHALL 对仍持锁卡解除通道锁;仅存在有效主套餐且不存在风险停机、销户或其他停机锁时调用自动复机,不符合条件不得调用复机。复机失败记录结果并可靠处理。
|
||||||
|
|
||||||
|
#### Scenario: 达量后人工复机
|
||||||
|
- **WHEN** 当前计费周期内持有通道阈值停机锁的卡请求复机
|
||||||
|
- **THEN** 系统拒绝复机且保留该锁
|
||||||
|
|
||||||
|
#### Scenario: 新周期仍有其他停机锁
|
||||||
|
- **WHEN** 新周期开始的持锁卡没有风险停机但存在其他停机锁
|
||||||
|
- **THEN** 系统解除通道阈值锁但不调用运营商复机
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
## 1. 阈值控制
|
||||||
|
- [ ] 1.1 追踪通道流量、卡状态、停复机锁、运营商任务与恢复链路。
|
||||||
|
- [ ] 1.2 新增通道配置、周期停机锁、任务结果的成对迁移、模型、索引和管理接口。
|
||||||
|
- [ ] 1.3 实现达量写锁/可靠停机、周期扫描解锁/条件复机及幂等恢复。
|
||||||
|
|
||||||
|
## 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-carrier-channel-traffic-thresholds --strict` 与 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-31
|
||||||
30
openspec/changes/add-commission-clawback-records/design.md
Normal file
30
openspec/changes/add-commission-clawback-records/design.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
退款和佣金终态可能异步到达,回溯必须等待原佣金事实且不能修改其历史记录。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- 回溯表以退款+原佣金唯一,保存负数快照;退款消费者等待佣金终态再可靠重试。
|
||||||
|
- 在钱包事务内先处理待审提现释放,再插入回溯和扣款流水;唯一约束保证重放安全。
|
||||||
|
|
||||||
|
## 生成、资金与读侧契约
|
||||||
|
|
||||||
|
### 退款事件消费
|
||||||
|
|
||||||
|
- 退款完成可靠事件以 `refund_id`、`order_id` 进入回溯用例;锁定退款、订单和佣金终态。原订单佣金未终态时不写“无需回溯”,仅保留可重试事件;终态无佣金时写退款已处理且无需回溯的审计;换货事件不进入本用例。
|
||||||
|
- 查询原订单全部可回溯佣金,按稳定顺序计算。全额退款回溯每条剩余可回溯金额;部分退款以 `refund_amount / frozen_actual_paid_amount` 计算,每条向下取整,最后一条仅补足总额舍入差且不得超过该条剩余可回溯余额。冻结实收金额缺失或非正时记录可恢复失败,不以订单标价替代。
|
||||||
|
- 新表以 `(refund_id, original_commission_id)` 唯一,保存负数金额、原佣金/订单/退款快照、不可提现标识、生成时间;唯一冲突视为已生成,禁止第二次扣款。
|
||||||
|
|
||||||
|
### 钱包与提现原子边界
|
||||||
|
|
||||||
|
- 在同一钱包事务内,先锁定代理佣金钱包和所有待审核/审批中的提现申请;拒绝这些申请、释放其冻结余额并保存“退款回溯优先”原因,然后插入所有回溯明细和负数佣金钱包流水。允许钱包余额低于零。
|
||||||
|
- 原佣金记录、历史发放金额和已提现完成事实不更新、不删除;回溯是独立负数事实。事务任一步失败时不释放提现、不写部分回溯或部分流水,可靠事件保留重试。
|
||||||
|
|
||||||
|
### 查询与导出
|
||||||
|
|
||||||
|
- 扩展佣金明细列表/详情:原佣金返回 `clawback_records` 摘要,回溯明细返回 `original_commission_id`、退款单号、负数金额、不可提现、回溯后实际钱包余额和生成时间。关联查询先应用既有佣金数据范围,再按关联 ID 查询;越权不泄露存在性。
|
||||||
|
- 导出每条原佣金和回溯明细各一行,冻结筛选、操作者、可见范围和生成时间;金额保持分,展示层转换元不得改变负数或余额事实。
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
新增成对迁移;隔离库验证全额/部分、舍入、佣金延迟、重复事件、提现释放、负余额及 up/down/up。
|
||||||
25
openspec/changes/add-commission-clawback-records/proposal.md
Normal file
25
openspec/changes/add-commission-clawback-records/proposal.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
## Scope
|
||||||
|
|
||||||
|
- 迭代编号:`AUG26-012`。
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
套餐退款后佣金需以独立负数事实回溯,并与提现冻结和钱包余额一致。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增不可提现负数回溯明细及原佣金/退款关联。
|
||||||
|
- 按冻结实收比例、舍入和幂等规则扣回佣金。
|
||||||
|
- 回溯前释放待审提现,允许佣金钱包负余额。
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
- 无。
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
- `agent-funds-commission`: 退款佣金回溯。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
影响退款事件、佣金明细、钱包、提现和 Schema。
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 套餐退款佣金回溯
|
||||||
|
系统 SHALL 在套餐退款后保留原佣金不变,并创建关联原佣金记录和退款单的负数、不可提现回溯明细,冻结原订单号及原佣金关键字段。同一退款业务必须幂等;若佣金计算未终态则等待终态后生成,确认无佣金才标记无需回溯。换货不在本期范围。
|
||||||
|
|
||||||
|
部分退款按本次退款金额与订单冻结实收金额比例,对每条原佣金按分向下取整;最后一条补足舍入差,累计回溯不得超过原佣金。生成前系统 MUST 拒绝并释放待审核提现,再生成回溯明细和钱包扣款流水;佣金钱包允许负余额。
|
||||||
|
|
||||||
|
#### Scenario: 部分退款舍入
|
||||||
|
- **WHEN** 一笔部分退款关联多条原佣金且比例计算产生分级舍入差
|
||||||
|
- **THEN** 系统按各条向下取整并仅在最后一条补差,回溯总额等于应回溯额且不超过各原佣金可回溯余额
|
||||||
|
|
||||||
|
### Requirement: 回溯明细关联查询与导出
|
||||||
|
系统 SHALL 在佣金明细中分别展示原发放佣金和回溯扣款记录,并允许从任一记录查询其关联的退款单、原佣金或全部回溯明细。回溯记录必须显示负数金额、不可提现标识、来源退款单号、原佣金记录号、生成时间和回溯后佣金钱包实际余额。佣金明细及导出 MUST 使用既有佣金数据范围:代理仅可读取自身及其既有可见范围内的事实,平台/超级管理员遵循既有范围;无权记录不得通过关联 ID、汇总或导出泄露。
|
||||||
|
|
||||||
|
导出应冻结筛选条件、操作者和可见范围;原佣金与回溯记录均作为独立行导出,回溯后余额为对应钱包变动提交后的实际余额,可为负数。
|
||||||
|
|
||||||
|
#### Scenario: 代理查询越权回溯记录
|
||||||
|
- **WHEN** 代理使用回溯记录 ID、原佣金 ID 或退款单号查询其数据范围外的回溯关系
|
||||||
|
- **THEN** 系统按既有数据范围返回不存在或空结果,不泄露关联事实
|
||||||
|
|
||||||
|
#### Scenario: 重复退款消费
|
||||||
|
- **WHEN** 同一退款完成事件被重复消费
|
||||||
|
- **THEN** 系统不重复生成回溯明细、钱包扣款或提现释放事实
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
## 1. 回溯实现
|
||||||
|
- [ ] 1.1 追踪退款完成、佣金计算终态、提现冻结和佣金钱包链路。
|
||||||
|
- [ ] 1.2 新增回溯明细、退款/原佣金唯一约束、状态/索引的成对迁移和 DTO。
|
||||||
|
- [ ] 1.3 实现比例分摊、最后一条舍入补差、终态等待、待审提现释放、负余额扣款及幂等消费者。
|
||||||
|
|
||||||
|
## 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-commission-clawback-records --strict` 与 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-31
|
||||||
94
openspec/changes/add-employee-collection-bills/design.md
Normal file
94
openspec/changes/add-employee-collection-bills/design.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
见 `proposal.md` 与 `specs/employee-collection-bill/spec.md`。现有后台套餐订单、代理充值、企业微信审批和审计已有各自业务事实,但没有将“后台账号代客户经办后的公司应收”作为独立对象保存。现有 `tb_agent_recharge_record` 已有金额、支付凭证和审批实例关联;套餐订单已有 `actual_paid_amount`。这些来源只能提供已确定的金额和关联键,不能被新功能改写。
|
||||||
|
|
||||||
|
本设计只处理上线后事件。线下付款并非本系统支付渠道事实,企业微信审批人员以第三方记录核验,因此本地只留存申请人声明、附件、冻结快照和企业微信最终结果。
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- 让账单、申请和分摊形成可并发保护、可重提、可审计的本地财务事实。
|
||||||
|
- 使企业微信是唯一终审来源,同时沿用已有可靠提交、回调和轮询恢复机制。
|
||||||
|
- 在订单/充值入账、退款和核销之间建立明确且幂等的关联。
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- 不建设公司收款账户目录,不接入或改造 OCR 契约,不校验同一外部付款在不同申请间的累计分摊。
|
||||||
|
- 不回填历史业务,不允许“其他”来源手工建账,不处理平台代理 C 端资产钱包充值。
|
||||||
|
- 不以账单功能重构订单、代理充值或企业微信通用审批模块。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### 1. 账单、申请、分摊分表保存
|
||||||
|
|
||||||
|
建立员工代收款账单、核销申请、核销分摊和线下收款方式字典四类事实。账单绑定唯一来源业务;申请绑定一次外部付款和一个企业微信审批实例;分摊连接申请与账单并冻结账单来源摘要。附件复用现有对象存储键/附件模式,审批快照复用既有通用审批上下文能力。
|
||||||
|
|
||||||
|
不把多笔付款、账单状态或附件塞入来源订单 JSON:来源订单既有生命周期不等于员工欠款,且一笔付款对多账单是独立关系。
|
||||||
|
|
||||||
|
### 2. 金额统一使用分,分摊在锁定账单中预占
|
||||||
|
|
||||||
|
账单应收、已核销、预占和申请付款金额均用 `int64` 分。提交/重提在同一 GORM 事务中按账单 ID 升序 `FOR UPDATE` 锁定,重新计算“应收金额 - 已通过分摊 - 其他审批中分摊”,再写申请与分摊。企业微信通过消费同样锁定申请和相关账单,使用审批实例唯一关联/状态条件更新保证至多入账一次。
|
||||||
|
|
||||||
|
不使用乐观展示余额或仅在回调时校验;那会让并发审批中申请超额占用同一账单。
|
||||||
|
|
||||||
|
### 3. 企业微信审批作为唯一状态推进器
|
||||||
|
|
||||||
|
申请提交事务只写本地申请、冻结快照、审批实例及可靠提交请求。审批回调和既有兜底查询都进入同一个幂等消费用例:通过才计入账单已核销,驳回才释放预占。提交失败或渠道未知保持在途,禁止本地财务人工改审批结果。
|
||||||
|
|
||||||
|
已驳回“重提”保留原申请主键,但新增一次审批实例及当次不可变快照;已通过分摊永不更新。这样列表可以按申请聚合,审计仍能回放每次审批。
|
||||||
|
|
||||||
|
### 4. 来源事件采用幂等 Outbox/消费者
|
||||||
|
|
||||||
|
线下套餐订单创建成功和代理充值审批入账完成后,在各自成功事务中写唯一来源事件或直接以唯一来源约束创建账单;选择以现有 Outbox 可用模式为准。消费者以来源类型+来源 ID 唯一约束去重。订单创建不得等待企业微信或外部付款;代理充值必须以“已通过且已入账”这一既有终态作为来源。
|
||||||
|
|
||||||
|
### 5. 退款只自动影响未存在已通过分摊的账单
|
||||||
|
|
||||||
|
退款处理在退款成功业务事务中查找来源账单并锁定。无已通过分摊时写冲销/关闭事实及更新金额;有已通过分摊时不动账,只写可追溯关联提示。这避免已由企业微信核验的员工欠款被退款回调静默重建或冲销。
|
||||||
|
|
||||||
|
## 业务动作契约
|
||||||
|
|
||||||
|
以下是本 Change 新增后台动作的已确认设计;路径遵循既有 `/api/admin` 路由约定,所有金额字段均为 `int64` 分,所有成功响应使用既有 `pkg/response` 包装。
|
||||||
|
|
||||||
|
### 收款方式字典维护
|
||||||
|
|
||||||
|
- `POST /employee-collection-payment-methods`:仅超级管理员。请求包含 `code`(1~64 字符、全局唯一)、`name`(1~100 字符)、`sort`(非负整数)、`enabled`、`remark`(最多 500 字符)。创建后返回字典 ID、字段值和创建时间。
|
||||||
|
- `PUT /employee-collection-payment-methods/:id`:仅超级管理员;不得修改已引用项的 `code`,可修改名称、排序、启停和备注。不存在返回既有“资源不存在”错误;重复编码返回稳定“收款方式编码已存在”错误。
|
||||||
|
- `DELETE /employee-collection-payment-methods/:id`:仅未被申请引用的项可物理删除;已引用返回“收款方式已被引用,只能停用”。每个成功写操作记录操作者和前后快照。
|
||||||
|
|
||||||
|
### 账单查询与关闭
|
||||||
|
|
||||||
|
- `GET /employee-collection-bills`:员工强制加 `debtor_account_id=当前账号`;财务、超级管理员按既有数据范围过滤。支持来源类型、来源单号、账单状态、欠款人、客户/店铺、创建时间范围筛选和分页。每行返回账单 ID、来源摘要、欠款人快照、应收、已核销、预占、剩余、状态和创建时间。
|
||||||
|
- `GET /employee-collection-bills/:id`:在同一数据范围校验后返回账单、来源摘要、退款冲销、分摊、申请与审批历史;附件只返回既有授权下载所需的安全引用,不返回对象存储敏感内容。
|
||||||
|
- `POST /employee-collection-bills/:id/close`:仅超级管理员;请求 `reason` 必填、最长 500 字符。事务中锁定账单,存在审批中申请返回“账单存在审批中核销申请,不能关闭”;已关闭返回既有状态冲突;成功时仅作废未核销余额并写关闭审计。
|
||||||
|
|
||||||
|
### 核销申请创建、修改与重提
|
||||||
|
|
||||||
|
- `POST /employee-collection-applications`:员工为本人可见账单创建,超级管理员可代办但请求必须附 `acting_reason`(1~500 字符)。请求包含 `payment_method_id`、`paid_amount`(正分)、`payer_name`、`paid_at`(带时区 RFC3339 时间)、`external_transaction_no`、`payment_voucher_keys`(1~5 个既有附件键)、`remark`、`allocations[]`;每个分摊包含 `bill_id` 和正的 `amount`。
|
||||||
|
- 服务按账单 ID 升序锁定,校验字典启用、账单可见且未关闭、分摊不超过该账单 `应收-已核销-其他审批中预占`、分摊总额不超过 `paid_amount`。成功返回申请 ID、状态 `审批中`、审批实例 ID、冻结快照与各分摊;任一校验失败时不保存申请、分摊或预占。
|
||||||
|
- `PUT /employee-collection-applications/:id`:仅申请人或代办超级管理员,且仅已驳回申请可修改;入参同创建。事务释放旧驳回版本无预占事实,重新锁定和校验账单,保存新的不可变材料快照并创建新的企业微信审批实例。已通过、审批中、已撤销/关闭状态返回状态冲突。
|
||||||
|
|
||||||
|
### 企业微信审批结果消费
|
||||||
|
|
||||||
|
- 企业微信回调和既有状态恢复任务均按审批实例 ID 进入同一应用用例,不提供后台“通过/驳回”接口。
|
||||||
|
- 最终通过:锁定申请及按 ID 升序的全部账单;仅当申请仍为审批中时,将每笔分摊从预占转入已核销,重新计算账单 `待核销/部分核销/已核销` 状态,标记申请已通过,并记录审批结果。重复或乱序的同一终态不重复增加已核销金额。
|
||||||
|
- 最终驳回:仅当申请仍为审批中时释放全部预占,标记已驳回并保存审批意见;重复回调不重复释放。提交失败、回调延迟和未知结果维持在途,由既有查询恢复任务确认,不得人工改写终态。
|
||||||
|
|
||||||
|
### 来源建账与退款冲销
|
||||||
|
|
||||||
|
- 后台线下套餐订单成功提交后,以 `order.id`、`operator_account_id`、`operator_account_type=platform` 和非空 `actual_paid_amount` 判定建账;来源唯一键为 `order:{id}`。重复订单事务、可靠事件重放或消费者重试均返回同一账单,不重复建账。
|
||||||
|
- 代理线下充值仅在既有审批最终通过且钱包入账完成后,以 `agent_recharge.id` 为唯一来源建账;线上充值、审批未通过或未完成入账不建账。
|
||||||
|
- 套餐退款成功时锁定来源账单:无已通过分摊的全额退款关闭账单;无已通过分摊的部分退款冲减应收;存在已通过分摊时只新增退款关联提示,不修改应收、已核销或员工欠款。
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- [企业微信回调重复、乱序或未知] → 以审批实例、申请状态和分摊状态条件更新幂等消费,复用渠道查询恢复。
|
||||||
|
- [外部付款敏感信息泄露] → 附件使用对象键和既有授权访问;日志/审计只记录脱敏摘要与业务 ID。
|
||||||
|
- [来源事件与账单创建不一致] → 在来源成功事务写可靠事件,消费者以唯一来源约束重放。
|
||||||
|
- [超额核销] → 提交、重提和审批通过均锁定账单并校验预占余额。
|
||||||
|
- [退款与核销并发] → 退款和审批消费按相同账单锁顺序串行,已通过分摊优先保留。
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. 新增成对迁移创建字典、账单、申请、分摊、审批快照/冲销关联所需表、唯一约束和查询索引;不修改既有迁移。
|
||||||
|
2. 先部署可读新表和来源事件的兼容代码,再启用账单生产与核销入口;上线时间作为历史切割点写受控配置或迁移基准。
|
||||||
|
3. 在隔离环境验证上线前订单/充值不建账、重复事件不重复建账、并发预占、通过/驳回/重提、退款联动及迁移 up/down/up。
|
||||||
|
4. 回滚时先停止新入口和事件消费;已有账单事实保留,只有维护者确认未产生不可逆业务数据时才执行 down。
|
||||||
31
openspec/changes/add-employee-collection-bills/proposal.md
Normal file
31
openspec/changes/add-employee-collection-bills/proposal.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
平台代理或无代理归属的 C 端客户以线下方式购买套餐、或代理线下充值预存款时,实际经办后台账号形成公司应收欠款。当前系统只有订单或充值审批,无法将这笔欠款、客户外部付款凭证、企业微信核验和最终核销结果形成独立、可分摊且可审计的闭环。
|
||||||
|
|
||||||
|
本 Change 落实讨论稿 AUG26-001:只覆盖功能上线后的新增业务,不回填任何历史账单,避免把历史支付事实以推测方式写入新财务账。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增员工代收款账单:后台线下套餐订单创建成功后,或代理线下预存款/主钱包充值经企业微信审批通过并完成入账后,按确定金额为实际经办账号创建唯一账单。
|
||||||
|
- 新增核销申请和账单分摊:员工按一笔外部付款创建一张申请,可选择多张账单并填写各自分摊金额;同一账单可由多笔已通过申请分次核销。
|
||||||
|
- 新增固定分类的线下收款方式字典;申请冻结字典名称、外部付款、附件、账单分摊和审批快照。OCR 仅可预填流水号和付款金额,人工确认值才是业务事实。
|
||||||
|
- 核销申请仅由企业微信最终通过或驳回驱动;提交失败、回调延迟或结果未知复用既有审批查询/恢复闭环,禁止本地人工绕过终审。
|
||||||
|
- 新增账单、核销申请、分摊、附件与审批记录的权限受控查询;超级管理员关闭未结清账单、来源订单退款时的账单冲销均保留审计。
|
||||||
|
- **BREAKING**:会生成员工账单的后台线下套餐订单不再在创建时强制上传付款凭证;凭证和外部付款信息改为核销申请必填。赠送套餐等不生成账单的既有线下订单继续保持原凭证要求。
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `employee-collection-bill`: 员工代收款账单、线下收款方式、分摊核销申请、企业微信审批闭环、退款冲销和财务查询。
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- 无。本 Change 通过新能力监听并引用既有订单和代理充值的已确定业务事实,不重写其既有主规格。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- 数据:新增账单、收款方式字典、核销申请、分摊和审批快照等表;订单/充值来源只保存可追溯关联,不回填历史记录。
|
||||||
|
- 写侧:后台线下套餐下单、代理线下充值入账、核销提交/重提/关闭、企业微信审批结果消费、套餐退款。
|
||||||
|
- 读取:财务账单与申请列表、详情、导出;员工仅看本人,财务/超级管理员按既有数据范围看全部。
|
||||||
|
- 依赖:复用既有企业微信通用审批实例、可靠提交和状态恢复机制;OCR 与外部付款渠道不在本 Change 新建契约。
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
## Purpose
|
||||||
|
|
||||||
|
为后台账号代客户经办的线下套餐购买和代理预存款充值建立独立的员工应收、外部付款核验和企业微信终审闭环;该能力只保存可追溯的本地业务事实,不推测或回填历史第三方付款。
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 员工代收款账单来源、金额与上线边界
|
||||||
|
系统 SHALL 仅为功能上线后新发生的下列业务创建员工代收款账单,并以实际发起该业务的后台账号作为不可修改的欠款人:
|
||||||
|
|
||||||
|
- 平台业务员或超级管理员创建的、会生成账单的后台线下套餐订单,账单金额取订单 `actual_paid_amount`;代理代购和无代理归属自营 C 端均适用。该订单创建成功后仍按既有规则立即激活。
|
||||||
|
- 代理线下预存款/主钱包充值在企业微信最终通过且完成入账后,账单金额取对应充值记录 `amount`。
|
||||||
|
|
||||||
|
客户自行线上支付、平台代理 C 端客户充值资产钱包、订单失败或取消、赠送套餐等不产生员工账单的既有线下订单,以及“其他”手工来源 MUST NOT 创建账单。系统 MUST 为同一来源业务建立至多一张账单,并保存来源类型、来源 ID、来源单号、客户/店铺快照、欠款人、应收金额和创建时间;上线前业务不回填、不补建。
|
||||||
|
|
||||||
|
#### Scenario: 后台线下套餐订单产生账单
|
||||||
|
- **WHEN** 平台业务员或超级管理员成功创建一个需要生成账单的后台线下套餐订单
|
||||||
|
- **THEN** 系统以该操作账号为欠款人、以订单 `actual_paid_amount` 为应收金额创建唯一待核销账单,且订单无需因未上传付款凭证而阻断
|
||||||
|
|
||||||
|
#### Scenario: 审批入账的代理充值产生账单
|
||||||
|
- **WHEN** 功能上线后代理线下预存款/主钱包充值经企业微信最终通过并完成入账
|
||||||
|
- **THEN** 系统以实际发起充值的后台账号和充值 `amount` 创建唯一待核销账单
|
||||||
|
|
||||||
|
#### Scenario: 重复来源或历史业务不产生重复账单
|
||||||
|
- **WHEN** 同一来源业务被重复处理、重复回调,或业务发生在功能上线前
|
||||||
|
- **THEN** 系统至多保留一张来源关联账单,且不补建上线前账单
|
||||||
|
|
||||||
|
### Requirement: 账单余额、状态与关闭
|
||||||
|
账单 SHALL 独立维护 `待核销`、`部分核销`、`已核销`、`已关闭` 状态及应收金额、已核销金额、审批中预占金额和剩余可核销金额。只有企业微信最终通过的分摊增加已核销金额;审批中的分摊预占剩余可核销金额,防止并发申请超额核销。账单已核销金额等于应收金额时 MUST 为已核销;关闭账单只作废当时未核销余额,已核销金额必须保留。
|
||||||
|
|
||||||
|
欠款人离职、禁用或变更组织后,账单欠款人身份和既有账单范围 MUST 保持不变。员工仅可查询本人账单和申请;财务与超级管理员可按既有数据范围查询;仅超级管理员可代办创建、修改或重提申请,且必须记录实际代办人和原因。
|
||||||
|
|
||||||
|
#### Scenario: 部分核销后仍可继续核销
|
||||||
|
- **WHEN** 一张账单存在企业微信已通过但未结清的分摊
|
||||||
|
- **THEN** 系统增加已核销金额、将账单标记为部分核销,并仅允许新的分摊使用未被已通过或审批中分摊占用的余额
|
||||||
|
|
||||||
|
#### Scenario: 关闭未结清账单
|
||||||
|
- **WHEN** 超级管理员对待核销、部分核销或已驳回关联申请的账单填写关闭原因并执行关闭,且账单不存在审批中申请
|
||||||
|
- **THEN** 系统作废未核销余额、将账单标记为已关闭、保留已核销金额和操作审计
|
||||||
|
|
||||||
|
#### Scenario: 审批中账单不可关闭
|
||||||
|
- **WHEN** 超级管理员尝试关闭存在审批中核销申请的账单
|
||||||
|
- **THEN** 系统拒绝关闭,账单金额和状态不变
|
||||||
|
|
||||||
|
### Requirement: 外部付款核销申请与分摊
|
||||||
|
员工 SHALL 按一笔外部付款创建一张核销申请。申请 MUST 选择一个启用的线下收款方式字典项,并保存其稳定编码和名称快照;必须保存经人工确认的付款金额、付款方、付款时间、外部交易流水号、至少一个支付凭证和可选其他凭证、备注及一个或多个账单分摊。申请人只能通过勾选可见账单创建分摊,系统带出只读来源订单、客户和资产信息。
|
||||||
|
|
||||||
|
系统 SHALL 按账单产生时间从早到晚用本次付款金额预填分摊,最后一张填入剩余金额;申请人可修改各分摊金额。单笔分摊 MUST 大于零且不得超过该账单可核销余额;分摊总额 MUST 不超过本次人工确认付款金额。同一外部付款可被多个核销申请引用,本期 MUST NOT 对跨申请累计分摊金额实施系统防重或金额上限校验。
|
||||||
|
|
||||||
|
#### Scenario: 一笔付款分摊多张账单
|
||||||
|
- **WHEN** 员工选择多张可见账单并提交一笔外部付款的核销申请
|
||||||
|
- **THEN** 系统按账单时间预填分摊、校验每张账单可核销余额和申请总额,并为该申请创建唯一企业微信审批实例
|
||||||
|
|
||||||
|
#### Scenario: 账单并发申请预占
|
||||||
|
- **WHEN** 两个核销申请并发选择同一账单的剩余余额
|
||||||
|
- **THEN** 系统至多接受不超过该账单未核销余额的审批中和已通过分摊,其余申请返回余额不足且不创建超额分摊
|
||||||
|
|
||||||
|
### Requirement: 核销申请审批、重提与幂等
|
||||||
|
核销申请状态 SHALL 为 `审批中`、`已通过`、`已驳回`、`已撤销/已关闭`,且不得以申请状态覆盖账单核销状态。提交或重提时系统 MUST 冻结当次收款方式、外部付款、附件、备注、账单分摊及审批材料快照,并创建新的企业微信审批实例。
|
||||||
|
|
||||||
|
企业微信最终通过时,系统 MUST 幂等地将申请标记为已通过、将各分摊写入账单已核销金额并释放其预占;最终驳回时 MUST 标记申请已驳回、释放全部预占且保留审批意见。已驳回申请可修改全部申请内容后重提,历史审批实例、材料和结果不得覆盖;已通过分摊不可修改。企业微信提交失败、回调延迟或结果未知时申请保持在途,系统 MUST 使用既有查询/恢复机制确认渠道结果,且不得由本地人工通过或拒绝绕过企业微信。
|
||||||
|
|
||||||
|
#### Scenario: 企业微信通过核销申请
|
||||||
|
- **WHEN** 企业微信对含多笔分摊的核销申请返回最终通过,且该结果首次被消费
|
||||||
|
- **THEN** 系统仅一次更新申请、各账单已核销金额和状态,并保留审批实例及冻结快照
|
||||||
|
|
||||||
|
#### Scenario: 企业微信驳回后重提
|
||||||
|
- **WHEN** 企业微信最终驳回核销申请
|
||||||
|
- **THEN** 系统释放预占、保留驳回实例和意见;员工或有代办权限的超级管理员修改申请后重提时创建新的审批实例
|
||||||
|
|
||||||
|
### Requirement: 收款方式字典、退款联动与可追溯性
|
||||||
|
系统 SHALL 提供唯一固定分类的线下收款方式字典。超级管理员可维护名称、稳定编码、排序、启停和备注;已被业务引用的字典项 MUST NOT 被物理删除,只能停用,且历史申请继续显示冻结名称。
|
||||||
|
|
||||||
|
来源套餐订单全额退款且账单从未存在已通过分摊时,系统 MUST 自动关闭账单并记录“来源订单全额退款”;部分退款且从未存在已通过分摊时,系统 MUST 按退款金额冲减账单应收金额并保留来源订单退款冲销记录。账单存在任一已通过分摊时,系统 MUST NOT 自动冲销,仅在账单详情提示来源订单退款。退款不恢复已核销账单的员工欠款。
|
||||||
|
|
||||||
|
账单、申请、分摊、附件、审批实例、字典快照、关闭和退款冲销 MUST 可按权限查询并记录操作审计;审计和日志不得保存完整支付凭证敏感内容。OCR 若可用仅用于预填,必须允许申请人或审核人更正,且识别值不是资金事实。
|
||||||
|
|
||||||
|
#### Scenario: 来源订单部分退款且未核销
|
||||||
|
- **WHEN** 来源套餐订单部分退款,且其账单不存在任何已通过分摊
|
||||||
|
- **THEN** 系统按退款金额冲减账单应收金额,保留退款冲销关联,并重新计算账单状态和可核销余额
|
||||||
|
|
||||||
|
#### Scenario: 被引用字典项停用
|
||||||
|
- **WHEN** 超级管理员停用已被核销申请引用的线下收款方式
|
||||||
|
- **THEN** 新申请不可选择该方式,历史申请仍展示其冻结名称和稳定编码
|
||||||
27
openspec/changes/add-employee-collection-bills/tasks.md
Normal file
27
openspec/changes/add-employee-collection-bills/tasks.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
## 1. 账单数据与基础契约
|
||||||
|
|
||||||
|
- [ ] 1.1 盘点现有订单、代理充值、通用企业微信审批、附件、Outbox 和审计模型,确定来源事件、附件键和审批快照的复用点;不得复制敏感付款内容。
|
||||||
|
- [ ] 1.2 新增成对迁移及 GORM 模型:线下收款方式字典、员工代收款账单、核销申请、申请—账单分摊、退款冲销/审批快照关联;为来源唯一性、审批实例唯一性、账单查询和分摊锁定建立约束/索引。
|
||||||
|
- [ ] 1.3 定义金额分、账单状态、申请状态、来源类型和稳定错误码;实现中文名称、DTO 枚举说明及金额/附件/分摊校验。
|
||||||
|
- [ ] 1.4 实现超级管理员维护线下收款方式字典的新增、编辑、启停和受引用不可删除规则,并写配置审计。
|
||||||
|
|
||||||
|
## 2. 来源建账与账单读取
|
||||||
|
|
||||||
|
- [ ] 2.1 在后台线下套餐订单成功路径识别应建账场景,以实际操作后台账号和 `actual_paid_amount` 可靠、幂等地创建账单;调整仅该场景的创建时付款凭证要求,保留赠送等非建账订单的既有要求。
|
||||||
|
- [ ] 2.2 在代理线下预存款/主钱包充值企业微信通过且完成入账路径可靠、幂等地创建账单,金额取充值 `amount`;线上充值和未入账审批不得建账。
|
||||||
|
- [ ] 2.3 实现账单列表、详情和统计 Query:按来源、状态、时间、员工、客户筛选,员工仅见本人,财务/超级管理员按数据范围见全部;返回应收、已核销、预占和未核销金额及审批中标识。
|
||||||
|
- [ ] 2.4 实现账单关闭用例:仅超级管理员、仅允许无审批中申请的未结清账单、必须填写原因,并在事务内保存状态变化与成功审计。
|
||||||
|
|
||||||
|
## 3. 核销申请、审批与退款联动
|
||||||
|
|
||||||
|
- [ ] 3.1 实现核销申请创建和已驳回重提:锁定选中账单、按时间预填、校验付款金额与分摊、预占余额、冻结收款方式/外部付款/附件/账单摘要,并创建新的企业微信审批实例和可靠提交请求。
|
||||||
|
- [ ] 3.2 接入企业微信最终通过、驳回、提交失败和状态查询恢复:通过时一次性增加已核销并释放预占,驳回时释放预占;用审批实例、状态条件更新和账单锁保证重复/乱序回调不重复核销。
|
||||||
|
- [ ] 3.3 实现代办权限、申请/分摊/审批历史查询及附件授权访问;超级管理员代办创建、修改或重提时强制记录实际代办人和原因。
|
||||||
|
- [ ] 3.4 在套餐退款成功处理链路实现账单冲销:无已通过分摊的全额退款自动关闭、部分退款冲减应收;存在已通过分摊时只保留退款关联提示,不恢复欠款。
|
||||||
|
- [ ] 3.5 为建账、申请提交/重提、审批通过/驳回、账单关闭和退款冲销补齐事务内审计;检查日志、错误和导出不暴露附件内容、完整交易敏感体或 OCR 原始结果。
|
||||||
|
|
||||||
|
## 4. 路由、文档与验证
|
||||||
|
|
||||||
|
- [ ] 4.1 注册账单、申请、字典和导出所需路由及 RouteSpec,补齐 `internal/bootstrap`、`cmd/api/docs.go` 和 `cmd/gendocs/main.go` 装配;Handler 使用 `pkg/response` 和稳定错误。
|
||||||
|
- [ ] 4.2 在隔离数据库按显式 `DB_*` 和 `scripts/migrate.sh` 验证迁移 up/down/up;人工核对上线前来源不建账、来源幂等、并发预占、审批重放、驳回重提、关闭限制及退款三类联动。
|
||||||
|
- [ ] 4.3 运行 `gofmt -w`(变更 Go 文件)、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-employee-collection-bills --strict`、`openspec doctor --json` 和 `./scripts/context-health.sh`;自动化测试按项目决策为 N/A。
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-31
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
见 `proposal.md`。现有换货单以 `migrate_data` 和 `migration_completed` 两个布尔字段记录意图和成功结果;`Service.Complete` 在同一数据库事务中完成资产归属、客户绑定、资产状态和可选业务数据迁移。迁移报错会回滚整个事务,现有失败审计不保存可供列表查询的迁移失败状态。
|
||||||
|
|
||||||
|
现有迁移函数已在同一事务中处理钱包余额、套餐使用记录、累计字段和资产标签。物流换货的发货后状态允许再次确认完成;直接换货创建即在同一事务完成,创建失败时不持久化换货单。
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- 为已持久化换货单提供稳定、可查询的迁移状态和安全失败原因。
|
||||||
|
- 保持业务数据迁移和换货完成的原子性,并让物流换货的失败可由超级管理员或平台用户重试。
|
||||||
|
- 兼容既有响应字段和历史换货数据。
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- 不改变直接换货创建失败即整体回滚、无换货单留存的现有行为。
|
||||||
|
- 不修改迁移项目、增加迁移明细表、迁移手机号—资产关联,或改变资产归属和个人客户—资产绑定的既有换货动作。
|
||||||
|
- 不新增列表筛选、导出或路由。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### 1. 使用状态字段取代布尔字段推断
|
||||||
|
|
||||||
|
在 `tb_exchange_order` 新增非空 `migration_status` 和非空 `migration_failure_reason`。状态使用字符串 `not_migrated`、`pending`、`migrated`、`failed`,中文名称仅在应用层投影;失败原因最长 500 字符且为空表示无失败原因。
|
||||||
|
|
||||||
|
新字段表达完整结果,保留 `migrate_data`、`migration_completed` 和 `migration_balance` 供兼容客户端及既有业务使用。新建/发货时按是否选择迁移写入 `not_migrated` 或 `pending`;成功完成写入 `migrated` 并清空失败原因。
|
||||||
|
|
||||||
|
备选方案是在现有两个布尔字段上叠加前端规则。放弃原因是无法表达失败和失败原因,且容易把待迁移与失败混淆。
|
||||||
|
|
||||||
|
### 2. 主事务回滚后以短事务落失败状态与审计
|
||||||
|
|
||||||
|
业务数据迁移仍与换货完成共享原有 GORM 事务。任何一步失败均回滚资产状态、归属、客户绑定、钱包、套餐和标签的本次修改,确保重试从完整且未部分迁移的事实开始。
|
||||||
|
|
||||||
|
外层识别到迁移失败后,另开短事务,以换货单仍处于可确认完成状态为条件更新 `migration_status=failed` 与经安全截断的失败原因,并写入对应失败审计。失败状态的持久化不得与已回滚的业务数据迁移共用事务。
|
||||||
|
|
||||||
|
备选方案是让迁移失败提交部分换货结果。放弃原因是会产生无法可靠补偿的钱包和套餐事实,且违背本期整套迁移原子执行的产品边界。
|
||||||
|
|
||||||
|
### 3. 只为失败的物流换货增加受限重试
|
||||||
|
|
||||||
|
保持既有确认完成入口。换货单为物流流程、业务状态仍为已发货待确认且迁移状态为 `failed` 时,仅超级管理员或平台用户可再次确认;用例在事务内重新锁定并验证状态,然后从钱包余额开始重新执行全部迁移。未失败的换货沿用现有可确认权限和状态门禁。
|
||||||
|
|
||||||
|
直接换货继续创建即完成;其迁移失败会回滚整笔创建,不留换货单或失败状态,避免为单一失败路径引入新的直接换货中间状态与重试接口。
|
||||||
|
|
||||||
|
### 4. 一次成对迁移完成历史映射
|
||||||
|
|
||||||
|
新增一对当前根迁移,不修改历史迁移。迁移新增列后,以既有字段回填:`migrate_data=false` 映射为 `not_migrated`;`migrate_data=true AND migration_completed=true` 映射为 `migrated`;其余 `migrate_data=true` 映射为 `pending`。历史记录的失败原因置空。
|
||||||
|
|
||||||
|
不增加索引:本期没有迁移状态筛选或后台批处理查询,现有列表分页读取已直接投影换货单字段。
|
||||||
|
|
||||||
|
## 行为与数据契约
|
||||||
|
|
||||||
|
### 数据投影与历史映射
|
||||||
|
|
||||||
|
- `tb_exchange_order` 新增 `migration_status varchar(20) NOT NULL`、`migration_failure_reason varchar(500) NOT NULL DEFAULT ''`;DTO 列表与详情新增 `migration_status`、`migration_status_name`,并仅在状态为 `failed` 时返回 `migration_failure_reason`。
|
||||||
|
- 上线迁移将 `migrate_data=false` 映射 `not_migrated`,`migrate_data=true AND migration_completed=true` 映射 `migrated`,其余已存在 `migrate_data=true` 映射 `pending`;不推断历史失败原因。
|
||||||
|
|
||||||
|
### 创建、发货与确认完成
|
||||||
|
|
||||||
|
- `POST /exchanges`:沿用现有创建入参和权限。物流单创建时按 `migrate_data` 初始化 `not_migrated` 或 `pending`;直接换货在同一创建事务中执行完成和可选迁移,任一步失败则整个创建回滚,不返回换货单或 `failed` 状态。
|
||||||
|
- `POST /exchanges/:id/ship`:沿用既有物流状态机和发货字段;不改变迁移状态,选择迁移的单仍为 `pending`。
|
||||||
|
- `POST /exchanges/:id/complete`:先在同一事务锁定换货单并验证既有“已发货待确认”状态和数据范围。`not_migrated` 只执行固有资产归属及个人客户绑定;`pending` 执行钱包余额、有效套餐使用、累计充值、资产标签的完整迁移及固有动作。成功时写 `migrated`、清空失败原因、写完成时间和既有成功审计。
|
||||||
|
|
||||||
|
### 失败与受限重试
|
||||||
|
|
||||||
|
- 当 `pending` 迁移任一步失败时,主事务必须回滚资产归属、个人客户绑定、钱包、套餐、累计字段、标签和完成状态;外层另开短事务,条件为换货单仍是可确认完成状态,写 `failed`、安全截断至 500 字符的失败原因及失败审计。
|
||||||
|
- 对 `migration_status=failed` 的物流单,`POST /exchanges/:id/complete` 仅超级管理员或平台用户可重试;锁定后从钱包余额开始重跑全部迁移,禁止仅重试某一子项。非平台账号返回无权,非失败单沿用既有完成状态门禁,不将完成接口变成通用重复执行入口。
|
||||||
|
- 手机号—资产关联永不在上述动作中读取、复制或删除;新资产后续按自身 H5 手机号绑定规则处理。
|
||||||
|
|
||||||
|
### 读取行为
|
||||||
|
|
||||||
|
- `GET /exchanges` 与 `GET /exchanges/:id` 沿用既有换货数据范围,返回新状态字段;旧 `migrate_data`、`migration_completed`、`migration_balance` 保持原响应兼容,但调用方不得再以其组合判断迁移结果。
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- [失败原因可能包含底层敏感或不稳定信息] → 使用稳定错误的安全摘要并限制长度,禁止直接返回数据库、外部服务或敏感载荷。
|
||||||
|
- [失败状态更新与失败审计二次事务异常] → 复用既有换货失败审计的次级故障记录方式;状态更新与成功必达审计同事务,更新失败时返回原失败并保留诊断。
|
||||||
|
- [并发确认造成重复迁移] → 重用换货单 `FOR UPDATE` 锁与预期业务状态更新;只有仍为 `failed` 的失败重试可以进入受限路径。
|
||||||
|
- [旧客户端只读取布尔字段] → 保持原字段及其成功语义,新字段只增不删。
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. 在隔离数据库执行新迁移,核对历史映射、非空约束和 down 后 Schema。
|
||||||
|
2. 发布同时包含迁移、写侧状态转换、列表/详情 DTO 投影和审计更新的版本。
|
||||||
|
3. 发生应用回滚时,先回滚应用至仍兼容新增列的版本;仅在确认没有依赖新状态的数据或功能后执行 down 迁移。
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
现有换货单只以“是否要求迁移”和“是否完成迁移”两个布尔值表达迁移结果;迁移失败会回滚,后台无法在列表和详情中区分未迁移、待迁移、已迁移及迁移失败,也无法获知失败原因或在修复后重试。
|
||||||
|
|
||||||
|
本轮 AUG26-005 已收口迁移范围和失败处理,需让换货运营能准确判断业务数据迁移结果并追溯异常。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 将换货业务数据迁移结果统一为不迁移、待迁移、已迁移、迁移失败四个可观察状态,并保留最近一次失败原因。
|
||||||
|
- 在换货完成的迁移失败场景中保留换货单可完成状态及失败事实;超级管理员或平台用户修复条件后可再次确认完成,并重新原子执行完整迁移。
|
||||||
|
- 在换货列表和详情返回迁移状态及失败原因(仅迁移失败时),替代前端对现有布尔字段的推断。
|
||||||
|
- 将迁移范围明确限定为资产钱包余额、有效套餐使用记录、累计充值字段和资产标签;资产归属、个人客户—资产绑定及手机号—资产关联不属于该迁移范围。
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- 无。
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `order-refund-exchange`: 明确换货业务数据迁移的状态、失败恢复、范围和列表/详情可见性。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- 影响 `internal/model/exchange_order.go`、换货完成写用例、换货列表 Query、换货 DTO 及既有换货审计。
|
||||||
|
- 需要新增成对数据库迁移,以保存迁移状态和最近失败原因;不修改既有迁移。
|
||||||
|
- 既有换货列表与详情接口将新增/明确迁移状态字段,前端应改按状态展示。
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 换货业务数据迁移状态与失败恢复
|
||||||
|
系统 SHALL 为每张已持久化的物流换货单返回业务数据迁移状态 `not_migrated`(不迁移)、`pending`(待迁移)、`migrated`(已迁移)或 `failed`(迁移失败),以及对应的中文状态名称。未选择业务数据迁移的换货单状态 MUST 为 `not_migrated`;选择迁移但尚未成功完成的换货单状态 MUST 为 `pending`;完整迁移成功后状态 MUST 为 `migrated`;迁移执行失败后状态 MUST 为 `failed`,并保存最近一次可安全展示的失败原因。
|
||||||
|
|
||||||
|
换货列表和详情 SHALL 返回迁移状态及中文名称;仅当状态为 `failed` 时返回最近一次失败原因。既有 `migrate_data`、`migration_completed` 和迁移余额字段 SHALL 保持兼容,但客户端不得再通过它们推断迁移结果。直接换货创建失败继续按既有原子性整体回滚,不产生可查询的失败换货单。
|
||||||
|
|
||||||
|
#### Scenario: 不迁移的换货单
|
||||||
|
- **WHEN** 创建或发货时未选择业务数据迁移
|
||||||
|
- **THEN** 换货列表和详情返回 `not_migrated` 及“不迁移”,且不返回迁移失败原因
|
||||||
|
|
||||||
|
#### Scenario: 待迁移的换货单
|
||||||
|
- **WHEN** 换货单已选择业务数据迁移但尚未成功完成换货
|
||||||
|
- **THEN** 换货列表和详情返回 `pending` 及“待迁移”
|
||||||
|
|
||||||
|
#### Scenario: 成功完成业务数据迁移
|
||||||
|
- **WHEN** 换货完成时全部业务数据迁移成功
|
||||||
|
- **THEN** 系统原子完成换货及业务数据迁移,列表和详情返回 `migrated` 及“已迁移”,并清除最近一次失败原因
|
||||||
|
|
||||||
|
#### Scenario: 迁移失败后保留可恢复事实
|
||||||
|
- **WHEN** 换货完成时任一业务数据迁移步骤失败
|
||||||
|
- **THEN** 系统不得提交本次换货完成及任何部分迁移结果,换货单保持可确认完成状态,返回迁移失败,并在独立持久化事实中将迁移状态更新为 `failed` 和最近一次失败原因
|
||||||
|
|
||||||
|
#### Scenario: 管理员重试失败迁移
|
||||||
|
- **WHEN** 超级管理员或平台用户对处于可确认完成状态且迁移状态为 `failed` 的换货单再次确认完成
|
||||||
|
- **THEN** 系统重新原子执行完整业务数据迁移;成功后将状态更新为 `migrated`,再次失败则保留 `failed` 并覆盖为最近一次失败原因
|
||||||
|
|
||||||
|
#### Scenario: 非平台账号重试失败迁移
|
||||||
|
- **WHEN** 非超级管理员且非平台用户尝试再次确认迁移状态为 `failed` 的换货单
|
||||||
|
- **THEN** 系统拒绝该操作,换货单及迁移状态不变
|
||||||
|
|
||||||
|
### Requirement: 换货业务数据迁移范围
|
||||||
|
系统 SHALL 仅在选择业务数据迁移的换货完成中迁移旧资产的钱包余额、有效套餐使用记录、累计充值字段和资产标签。资产归属与个人客户—资产绑定 SHALL 继续作为换货完成固有动作,不受业务数据迁移选项控制;手机号—资产关联 MUST NOT 随换货或业务数据迁移转移,新资产首次访问时按其适用的手机号绑定规则处理。
|
||||||
|
|
||||||
|
#### Scenario: 选择业务数据迁移完成换货
|
||||||
|
- **WHEN** 换货单选择业务数据迁移并成功确认完成
|
||||||
|
- **THEN** 系统迁移钱包余额、有效套餐使用记录、累计充值字段和资产标签,且不迁移手机号—资产关联
|
||||||
|
|
||||||
|
#### Scenario: 不选择业务数据迁移完成换货
|
||||||
|
- **WHEN** 换货单未选择业务数据迁移并确认完成
|
||||||
|
- **THEN** 系统仍完成资产归属与个人客户—资产绑定的固有换货动作,但不迁移钱包余额、套餐使用记录、累计充值字段或资产标签
|
||||||
18
openspec/changes/add-exchange-data-migration-status/tasks.md
Normal file
18
openspec/changes/add-exchange-data-migration-status/tasks.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
## 1. 数据契约
|
||||||
|
|
||||||
|
- [ ] 1.1 新增一对当前根迁移,为 `tb_exchange_order` 添加迁移状态和失败原因字段,并按既有迁移布尔字段回填历史记录。
|
||||||
|
- [ ] 1.2 在换货模型和常量中定义四种迁移状态及中文名称,保留现有布尔字段的兼容语义。
|
||||||
|
- [ ] 1.3 扩展换货列表、详情 DTO 及两个读侧投影,返回迁移状态、中文名称及仅失败时的安全失败原因。
|
||||||
|
|
||||||
|
## 2. 换货完成与恢复
|
||||||
|
|
||||||
|
- [ ] 2.1 在创建、发货和成功完成的写路径维护不迁移、待迁移和已迁移状态,并在成功后清除失败原因。
|
||||||
|
- [ ] 2.2 保持完整换货和业务数据迁移在同一 GORM 事务;迁移失败时回滚全部业务修改,再以条件短事务保存物流换货单的失败状态、经安全处理的失败原因和审计事实。
|
||||||
|
- [ ] 2.3 限制迁移失败的物流换货重试仅由超级管理员或平台用户发起;重试须锁定换货单、重新执行全套迁移并防止并发重复完成。
|
||||||
|
- [ ] 2.4 保持直接换货失败时整体回滚且不持久化失败换货单,确认迁移范围不包含手机号—资产关联。
|
||||||
|
|
||||||
|
## 3. 文档与验证
|
||||||
|
|
||||||
|
- [ ] 3.1 更新换货接口 OpenAPI 描述并运行 `go run cmd/gendocs/main.go`,核对状态枚举及失败原因的响应契约。
|
||||||
|
- [ ] 3.2 在隔离数据库按 `scripts/migrate.sh` 使用显式 `DB_*` 参数验证新迁移 up/down/up、历史状态映射及回滚后的 Schema。
|
||||||
|
- [ ] 3.3 运行 `gofmt -w`(变更 Go 文件)、`go build ./cmd/api ./cmd/worker`、`openspec validate add-exchange-data-migration-status --strict` 和 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-31
|
||||||
21
openspec/changes/add-export-time-filter-standards/design.md
Normal file
21
openspec/changes/add-export-time-filter-standards/design.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
## Decisions
|
||||||
|
|
||||||
|
- 共享导出筛选解析器返回 UTC 边界和冻结筛选快照,查询不在导出 Worker 中重新解释日期。
|
||||||
|
- 导出任务保存授权范围快照而非执行时重新计算;列表/导出复用同一 Query 条件构造。
|
||||||
|
|
||||||
|
## 参数、查询与导出契约
|
||||||
|
|
||||||
|
### 统一时间解析
|
||||||
|
|
||||||
|
- 新增共享解析器,输入可选 `start_time`、`end_time` 字符串,必须以 RFC3339 秒级且带显式时区解析为瞬时 UTC 值;拒绝无时区、毫秒精度、非法日期和 `start_time > end_time`。两端均存在时 Query 使用 `time >= start_time AND time <= end_time`;单端只应用对应边界。
|
||||||
|
- IoT/设备任务、换货、分配、订单、代理充值、佣金、提现统一绑定该参数并按各自创建/申请时间过滤;授权记录按授权发生时间;临期列表按当前有效主套餐最终到期时间。DTO、OpenAPI 和导出筛选名均固定为 `start_time`/`end_time`,不再接受模块私有日期字段作为新契约。
|
||||||
|
|
||||||
|
### 导出任务快照
|
||||||
|
|
||||||
|
- 创建临期、佣金明细、达量预警导出时,先复用页面 Query 构造器解析全部筛选和时间边界,再保存规范化过滤器、操作者 ID、创建时可见店铺/资产范围、时区、创建时间和口径版本。Worker 只读取该快照,不重新从请求、当前角色或当前页面解析筛选。
|
||||||
|
- 临期导出以资产为粒度,选择当前有效主套餐最终到期时间和剩余天数;加油包不单独生成行。佣金导出以钱包变动明细为粒度,保存每次变动提交后的实际余额,允许回溯负数。预警导出以预警记录为粒度,套餐/流量/阈值/到期字段读触发快照,店铺/业务员/用户组可按执行时当前归属补全,但必须同时落在创建时冻结范围。
|
||||||
|
- 导出完成记录结果文件、行数、完成时间和失败安全摘要;任何权限变化、筛选条件变化或后台归属变化不得扩大已创建任务的数据集。失败重试继续使用原快照,不创建第二份不同口径文件。
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
为任务快照新增成对迁移;验证时区边界、空边界、非法/超长区间、权限变化及 up/down/up。
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
## Scope
|
||||||
|
|
||||||
|
- 迭代编号:`AUG26-014`。
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
后台导出和列表可能使用不同时间口径或在异步执行时漂移权限范围。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 统一日期/时间解析、左闭右开区间和最大范围校验。
|
||||||
|
- 导出冻结列表筛选、时区和数据范围。
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
- `export-time-filter`: 导出时间筛选标准。
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
- 无。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
影响后台导出任务、查询 DTO、权限快照和 OpenAPI。
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 统一时间筛选参数与字段
|
||||||
|
系统 SHALL 对 IoT/设备任务、换货、分配、订单、代理充值、佣金、提现及其导出统一使用可选 `start_time`、`end_time` 参数。参数必须为带时区的 RFC3339 秒级时间,区间为**闭区间**;任一端可缺省。上述业务按创建时间或申请时间筛选;授权记录按授权发生时间筛选;临期列表按当前生效主套餐最终到期时间筛选。格式非法或开始时间晚于结束时间时拒绝请求。
|
||||||
|
|
||||||
|
#### Scenario: 两端均传入
|
||||||
|
- **WHEN** 请求携带合法的 `start_time` 与 `end_time`
|
||||||
|
- **THEN** 系统仅返回权威时间大于等于开始时间且小于等于结束时间的记录
|
||||||
|
|
||||||
|
### Requirement: 三类异步导出及冻结口径
|
||||||
|
临期列表、佣金明细和套餐流量达量预警 SHALL 复用既有异步导出任务,并在创建时冻结全部页面筛选条件、操作者和可见店铺范围。临期导出一行对应一项资产,仅取当前生效主套餐最终到期时间和剩余天数,加油包不得单独成行。预警导出一行对应一条预警记录,套餐、用量、总量、阈值和到期时间使用触发快照,店铺、业务员和用户组在执行时按当前归属补充。佣金明细必须导出每次佣金钱包变动后的实际余额,回溯记录可为负数。
|
||||||
|
|
||||||
|
异步执行不得重新解释时间、扩大创建时店铺范围或遗漏页面筛选;文件结果只含创建时有权读取的事实。
|
||||||
|
|
||||||
|
#### Scenario: 预警归属在导出前变更
|
||||||
|
- **WHEN** 预警记录创建后资产所属店铺或业务员变更,再执行已创建导出任务
|
||||||
|
- **THEN** 套餐及流量字段仍使用触发快照,店铺、业务员和用户组使用执行时当前归属,且不得超出任务创建时冻结的可见店铺范围
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
## Purpose
|
||||||
|
|
||||||
|
为 2026 年 8 月迭代提供独立、可验证的 统一时间筛选与导出快照 行为契约,避免与既有模块的兼容行为混淆。
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 统一时间筛选与导出快照
|
||||||
|
系统 SHALL 对受影响列表使用可单端省略的 `start_time` 和 `end_time` RFC3339 秒级闭区间,并按规定业务时间筛选。异步导出必须冻结创建时筛选条件、操作者和可见店铺范围,且按各业务规定使用触发快照或执行时归属。
|
||||||
|
|
||||||
|
#### Scenario: 规则命中
|
||||||
|
- **WHEN** 业务请求或任务满足本需求定义的前置条件
|
||||||
|
- **THEN** 系统按上述规则完成处理、保留可追溯事实,并拒绝与状态、权限或幂等约束冲突的重复操作
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
## 1. 统一筛选
|
||||||
|
- [ ] 1.1 清点本期后台导出/列表入口及现有时间字段和权限 Query。
|
||||||
|
- [ ] 1.2 实现上海时区日期解析、左闭右开区间、最大范围校验和筛选快照。
|
||||||
|
- [ ] 1.3 改造导出任务以冻结范围并复用列表 Query;更新 DTO/OpenAPI。
|
||||||
|
|
||||||
|
## 2. 验证
|
||||||
|
- [ ] 2.1 验证日期边界、时间格式、空范围、超限、权限变更和导出/列表一致性。
|
||||||
|
- [ ] 2.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-export-time-filter-standards --strict` 和 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-31
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
个人客户通知已有隔离、已读和投递能力,物流换货已有状态机。弹窗配置不能替代通知事实,风险换卡不能另建待处理记录。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- 新增运营弹窗配置及版本/投放去重事实;候选查询在同一资产上下文计算风险优先级和配置匹配,创建/复用个人通知。
|
||||||
|
- 风险地址提交以客户+旧资产唯一约束和事务创建物流换货单,地址写入换货单而不单独建表。
|
||||||
|
- 频率去重使用客户、配置版本、资产/日期键;通知内容冻结在投放时,已读复用现有服务。
|
||||||
|
- 后台仅管理配置,不得写任意 URL;H5 只接收受控目标类型。
|
||||||
|
|
||||||
|
## 后台与 H5 动作契约
|
||||||
|
|
||||||
|
### 运营弹窗配置
|
||||||
|
|
||||||
|
- `POST /h5-popup-configurations`:仅超级管理员、平台用户。请求 `title`(1~100 字符)、`content`(1~2000 字符)、`starts_at`、`ends_at`、`enabled`、`priority`、`pages`(首页/资产详情/套餐购买/资产钱包充值)、可选店铺/设备类型/卡类型集合、`frequency`(`once`/`daily`)和可选 `action_type`(`package_purchase`/`asset_wallet_recharge`)。结束时间不得早于开始时间;不接受 URL、前端路由或任意动作参数。
|
||||||
|
- `PUT /h5-popup-configurations/:id` 更新时递增配置版本;旧版本通知不改写。`POST /:id/enable`、`/disable` 仅影响后续候选;全部成功写操作记录操作者、前后值、版本和时间。
|
||||||
|
|
||||||
|
### H5 候选查询与风险换卡
|
||||||
|
|
||||||
|
- `GET /api/c/v1/popup-candidates`:当前个人客户必须提交 `page` 和当前资产标识;首页也必须先由客户选定当前资产。服务校验该资产属于当前客户或其既有授权范围,否则按资源不可见返回。
|
||||||
|
- 查询先判断广电卡、运营商扩展状态风险停机、无活动物流换货单、未提交风险地址和“客户+资产+上海自然日”未展示;命中时创建/复用风险通知并只返回风险换卡候选。关闭或稍后处理只调用既有通知已读,不修改风险资格,次日允许再次投放。
|
||||||
|
- 未命中风险时,按当前时间、启用状态、页面、店铺/设备类型/卡类型范围和频率匹配运营配置;同维度多值取任一命中,无配置即全量。只返回优先级最高一条,同优先级取最近更新时间;以客户、配置版本、资产、日期/一次性键创建或复用通知。
|
||||||
|
|
||||||
|
### 风险地址提交与通知读取
|
||||||
|
|
||||||
|
- `POST /api/c/v1/risk-exchanges/:asset_id/address`:当前个人客户提交 `recipient_name`、`recipient_phone`、`recipient_address`;均必填且沿用既有换货地址字段长度校验。事务中锁定客户和旧资产,复核风险资格,以客户+旧资产唯一约束创建物流换货单,`migrate_data=false`;重复提交返回首次创建的换货单与首次地址,禁止覆盖。
|
||||||
|
- 弹窗通知内容、配置版本、资产和受控动作在投放时冻结并写个人站内通知,保留 90 天。候选查询不标记已读;关闭、点击受控操作、进入通知详情仅通过既有 `PUT /api/c/v1/notifications/:id/read` 幂等标记当前客户自己的通知。
|
||||||
|
- 通知受控操作只返回类型与资产关联,不返回 URL;前端按白名单映射页面。客户读取他人通知或不属于其资产的风险换卡均按既有隔离规则不可见。
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
新增成对迁移和索引;隔离库验证风险条件、每日限制、地址幂等、优先级、版本重投、范围匹配、通知隔离及 up/down/up。
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
## Scope
|
||||||
|
|
||||||
|
- 迭代编号:`AUG26-007`。
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
风险停机客户需在 H5 自助留下换卡地址,运营也需可控的资产定向弹窗;两者都必须保留投放事实且不预生成无访问客户的通知。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增风险换卡候选与地址提交,幂等创建物流换货单。
|
||||||
|
- 新增运营弹窗配置、范围/优先级/频率/版本投放和受控动作。
|
||||||
|
- 复用个人客户通知保存快照、已读和 90 天历史。
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
- `h5-popup-notification`: H5 风险换卡和运营弹窗。
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
- 无。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
影响 H5、个人通知、资产/换货查询、后台配置、审计和 Schema。
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
## Purpose
|
||||||
|
|
||||||
|
在客户实际访问 H5 时,按当前资产事实投放风险换卡或运营弹窗,并将投放内容作为个人客户通知留存,避免预生成通知或开放任意跳转链接。
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 风险换卡候选与地址提交
|
||||||
|
系统 SHALL 仅在当前 H5 客户访问的资产为广电卡、运营商扩展状态为风险停机、且不存在待填写信息、待发货、已发货待确认或已完成物流换货单时返回风险换卡弹窗。同一客户同一资产每天至多展示一次;稍后处理仅抑制当天,次日仍可命中。风险换卡优先级固定高于运营弹窗。
|
||||||
|
|
||||||
|
客户提交收货人姓名、收货手机号和完整地址文本后,系统 MUST 幂等创建关联旧资产的物流换货单;首次地址锁定,客户不得修改。自动换货单不预设业务数据迁移,发货选择新资产时仍由后台按既有换货流程决定。风险条件不再成立或地址已提交后停止新投放,已投放通知保留 90 天。
|
||||||
|
|
||||||
|
#### Scenario: 重复提交风险地址
|
||||||
|
- **WHEN** 客户对同一风险资产重复提交收货地址
|
||||||
|
- **THEN** 系统保留首次地址和唯一物流换货单,不创建第二张换货单
|
||||||
|
|
||||||
|
### Requirement: 运营弹窗实时匹配
|
||||||
|
系统 SHALL 允许超级管理员和平台用户管理全局运营弹窗的标题、内容、有效期、启停、优先级、店铺/设备类型/卡类型范围、四种页面(首页、资产详情、套餐购买、资产钱包充值)、频率和一个可选受控操作。范围同一维度多选为任一匹配,未配置范围即全量;H5 请求必须携带当前页面资产标识,首页使用当前选中资产。操作仅可为套餐购买或资产钱包充值,不得配置任意 URL。
|
||||||
|
|
||||||
|
运营弹窗仅在客户请求候选时实时匹配并创建或复用通知;每客户每配置支持仅一次或每天一次。候选只返回优先级最高一条,同优先级取最近更新时间最新;配置修改形成新版本,既有通知保留快照,修改后的仅一次配置可向原命中客户重新投放。配置到期/停用停止新投放,历史通知保留 90 天。
|
||||||
|
|
||||||
|
#### Scenario: 风险与运营候选同时命中
|
||||||
|
- **WHEN** 当前资产同时满足风险换卡和多个运营弹窗条件
|
||||||
|
- **THEN** 系统仅返回风险换卡候选,并保持通知未读
|
||||||
|
|
||||||
|
### Requirement: 通知留存与已读
|
||||||
|
弹窗投放 SHALL 复用个人客户站内通知,保存投放时内容、配置版本、资产和受控操作快照,并同时出现在通知列表。创建或返回候选不得自动已读;客户关闭、点击操作或进入通知详情后通过既有已读接口幂等标记已读。通知读取必须维持个人客户隔离。
|
||||||
|
|
||||||
|
#### Scenario: 关闭弹窗
|
||||||
|
- **WHEN** 当前个人客户关闭其未读弹窗
|
||||||
|
- **THEN** 系统仅标记该客户该通知已读,不影响其他客户或未来符合条件的投放
|
||||||
13
openspec/changes/add-h5-risk-exchange-notifications/tasks.md
Normal file
13
openspec/changes/add-h5-risk-exchange-notifications/tasks.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
## 1. 数据与配置
|
||||||
|
- [ ] 1.1 追踪个人通知、资产风险状态、物流换货、H5 认证和既有已读链路。
|
||||||
|
- [ ] 1.2 新增弹窗配置、版本/投放去重所需成对迁移、模型、范围和索引。
|
||||||
|
- [ ] 1.3 实现管理端配置 CRUD、启停、受控页面/动作/频率校验和审计。
|
||||||
|
|
||||||
|
## 2. H5 行为
|
||||||
|
- [ ] 2.1 实现携带资产标识的候选接口:风险条件、每日限制、运营范围/优先级/版本匹配和个人通知创建/复用。
|
||||||
|
- [ ] 2.2 实现风险地址提交、唯一物流换货单创建、首次地址锁定及数据迁移默认边界。
|
||||||
|
- [ ] 2.3 接入既有个人通知列表和已读,注册路由/OpenAPI。
|
||||||
|
|
||||||
|
## 3. 验证
|
||||||
|
- [ ] 3.1 隔离库验证风险/运营优先级、频率、范围、版本、地址重复提交、通知隔离及迁移 up/down/up。
|
||||||
|
- [ ] 3.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-h5-risk-exchange-notifications --strict` 和 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||||
2
openspec/changes/add-operations-reports/.openspec.yaml
Normal file
2
openspec/changes/add-operations-reports/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-31
|
||||||
27
openspec/changes/add-operations-reports/design.md
Normal file
27
openspec/changes/add-operations-reports/design.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
## Decisions
|
||||||
|
|
||||||
|
- 只读 Query 分别以卡首次激活成功事实和续购套餐实际生效事实为权威源;不以当前订单/资产状态反推历史。
|
||||||
|
- 激活按卡去重,续费同时计算订单计数和资产去重计数;金额始终聚合分。
|
||||||
|
- 先施加请求人数据范围,再关联设备、套餐、用户组、店铺、业务员维度;缺失维度用占位值保留事实。
|
||||||
|
- 导出复用同一 Query 并保存筛选、范围、时区和口径版本快照。
|
||||||
|
|
||||||
|
## 查询与导出契约
|
||||||
|
|
||||||
|
### 激活情况统计
|
||||||
|
|
||||||
|
- `GET /operations-reports/activations`:仅超级管理员、平台用户;请求 `start_time`、`end_time` 必填,上海时区左闭右开,及可选 `device_type`、`package_id`、`business_user_group_id`、`shop_id`、`business_owner_account_id`、`group_by`。先应用既有资产/店铺范围,再以卡 `activated_at` 的首次成功激活事实过滤和聚合;同一卡在区间内至多贡献 1。
|
||||||
|
- 响应返回统计边界、分组维度代码/名称、`activation_count`;关联的设备、套餐、用户组、店铺或业务员物理缺失时返回固定“未知/已删除”占位,不用当前资产状态、退款、换货或取消结果排除历史激活。
|
||||||
|
|
||||||
|
### 套餐续费情况统计
|
||||||
|
|
||||||
|
- `GET /operations-reports/package-renewals`:筛选与分组维度同激活报表。权威事实是续购订单支付成功且对应主套餐使用记录实际生效;新购、加油包、失败/关闭支付、仅支付成功未生效均排除。
|
||||||
|
- 每个分组返回 `renewal_order_count`、`renewal_asset_count`(按资产去重)、`received_renewal_amount`(分)。同一资产多笔生效续购增加订单数和金额但只增加一次资产数;金额从订单冻结实收金额读取,禁止由套餐当前售价反算。
|
||||||
|
|
||||||
|
### 权限与导出
|
||||||
|
|
||||||
|
- 不在调用者数据范围内的 `shop_id`、资产或维度筛选返回既有无权/空集合语义,不返回越权聚合。时间缺失、格式无效、开始不早于结束或不支持的 `group_by` 返回稳定参数错误,不执行聚合。
|
||||||
|
- `POST /operations-reports/activations/export` 与 `/package-renewals/export` 创建异步任务,保存请求人、报表类型、规范化筛选、上海时区、口径版本、授权范围和创建时间。Worker 复用相同 Query,生成的列与页面指标一致,并记录文件、行数、完成时间或安全失败摘要;后续角色/店铺变化不得扩大范围。
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
验证跨日边界、首次激活去重、退款后激活保留、续购未生效排除、多次续费、维度缺失、权限和导出一致性。
|
||||||
25
openspec/changes/add-operations-reports/proposal.md
Normal file
25
openspec/changes/add-operations-reports/proposal.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
## Scope
|
||||||
|
|
||||||
|
- 迭代编号:`AUG26-015`。
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
首期运营分析需有统一、可导出的激活和套餐续费统计,避免把订单、退款、钱包等不同行为混成一个泛化报表。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增按首次激活成功时间统计的激活情况统计表。
|
||||||
|
- 新增按套餐续购实际生效时间统计的套餐续费情况统计表。
|
||||||
|
- 固化设备、套餐、用户组、店铺、业务员维度、金额/数量口径、权限与导出快照。
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
- `operations-report`: 激活与套餐续费运营报表。
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
- 无。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
影响资产/卡激活、套餐使用、订单、店铺/业务员维度、导出和数据权限查询。
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 激活情况统计报表
|
||||||
|
系统 SHALL 提供激活情况统计表,按上海时区、卡实际首次激活成功时间统计。查询必须支持时间范围及设备类型、套餐、用户组、店铺、业务员维度筛选和分组;返回激活数量、各维度名称及“未知/已删除”历史维度占位值。取消、退款、换货或当前资产状态变化不得改写已发生的首次激活事实;同一卡仅计一次首次成功激活。
|
||||||
|
|
||||||
|
#### Scenario: 已激活资产后续退款
|
||||||
|
- **WHEN** 卡在统计区间内首次激活成功,后续套餐退款或资产状态变化
|
||||||
|
- **THEN** 系统仍按首次激活时间计入激活数量,不重复或撤销该历史激活事实
|
||||||
|
|
||||||
|
### Requirement: 套餐续费情况统计报表
|
||||||
|
系统 SHALL 提供套餐续费情况统计表,按上海时区、套餐续购订单支付成功并使套餐续期生效的时间统计。查询支持时间范围及设备类型、套餐、用户组、店铺、业务员维度筛选和分组;返回续费订单数、续费资产数、实收续费金额(分)和各维度名称。新购、加油包购买、失败/关闭支付和未生效续购不计入续费;同一资产在区间内多次成功续费按订单数累计,资产数按资产去重。
|
||||||
|
|
||||||
|
#### Scenario: 续购支付成功但套餐未生效
|
||||||
|
- **WHEN** 续购支付成功但套餐生效事务尚未完成或最终失败
|
||||||
|
- **THEN** 系统不将该订单计入套餐续费统计
|
||||||
|
|
||||||
|
### Requirement: 报表权限、导出与口径快照
|
||||||
|
仅超级管理员和平台用户 SHALL 查询或导出报表,并按既有数据范围过滤店铺、代理和资产。时间范围必填、按左闭右开区间解释;导出冻结请求人、筛选条件、时区、统计口径版本和授权范围,记录操作人、导出时间、筛选条件及导出结果。导出列与页面对应指标一致,异步执行不得因后续权限变化扩大数据范围。
|
||||||
|
|
||||||
|
#### Scenario: 无权范围筛选
|
||||||
|
- **WHEN** 请求人筛选其无权访问的店铺
|
||||||
|
- **THEN** 系统拒绝请求或按既有范围规则不返回该店铺聚合值,不泄露任何统计结果
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
## Purpose
|
||||||
|
|
||||||
|
为 2026 年 8 月迭代提供独立、可验证的 激活与套餐续费日报 行为契约,避免与既有模块的兼容行为混淆。
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 激活与套餐续费日报
|
||||||
|
系统 SHALL 自功能上线后每日生成激活和套餐续费稳定日报快照,不回填上线前历史。查询支持日/月趋势、单一业务维度分组及异步导出,并按已确认的采购、激活、在网、活跃和续费率口径计算。
|
||||||
|
|
||||||
|
#### Scenario: 规则命中
|
||||||
|
- **WHEN** 业务请求或任务满足本需求定义的前置条件
|
||||||
|
- **THEN** 系统按上述规则完成处理、保留可追溯事实,并拒绝与状态、权限或幂等约束冲突的重复操作
|
||||||
9
openspec/changes/add-operations-reports/tasks.md
Normal file
9
openspec/changes/add-operations-reports/tasks.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
## 1. 两张报表
|
||||||
|
- [ ] 1.1 确认首次激活成功和续购实际生效的权威表、终态/时间字段及维度关联。
|
||||||
|
- [ ] 1.2 实现激活情况统计:时间范围、五类维度、卡去重、历史维度占位和数据范围。
|
||||||
|
- [ ] 1.3 实现套餐续费统计:生效门槛、订单数/资产数/实收金额和数据范围。
|
||||||
|
- [ ] 1.4 实现同 Query 导出及筛选、时区、口径、权限快照和操作审计;更新路由/OpenAPI。
|
||||||
|
|
||||||
|
## 2. 验证
|
||||||
|
- [ ] 2.1 验证首次激活、多次续费、退款、未生效续购、跨日、维度缺失、权限和导出一致性。
|
||||||
|
- [ ] 2.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-operations-reports --strict` 和 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-31
|
||||||
39
openspec/changes/add-package-real-usage-alerts/design.md
Normal file
39
openspec/changes/add-package-real-usage-alerts/design.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
套餐商品已有 `real_data_mb`,套餐使用记录承载真实用量;现有通知以事件投递并按接收人隔离。预警必须是套餐级观测,不能复用运营商通道阈值停机锁。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- 新增套餐规则和预警表;规则以套餐唯一,预警以套餐使用记录+阈值快照唯一,保存触发时流量、资产/卡/设备、套餐和店铺/业务员快照。
|
||||||
|
- 扫描查询当前有效使用记录,按资产汇总真实用量与额度,并选出主套餐规则;用唯一约束和事务创建预警及通知事件,扫描可安全重跑。
|
||||||
|
- 不向未来业务员补发:接收人仅在预警创建事务中解析。读侧以资产范围过滤,导出复用现有快照任务。
|
||||||
|
|
||||||
|
## 管理、扫描与查询动作契约
|
||||||
|
|
||||||
|
### 规则维护
|
||||||
|
|
||||||
|
- `POST /package-traffic-alert-rules`:仅超级管理员、平台用户;请求 `package_id`、`threshold_percent`(大于等于 1、小于等于 100,允许小数)、`enabled`、`remark`(最多 500 字符)。套餐必须存在且 `real_data_mb > 0`;同套餐已有规则返回“套餐已存在真流量预警规则”。
|
||||||
|
- `PUT /package-traffic-alert-rules/:id`:允许修改阈值、启停、备注;不修改已产生预警快照。停用后扫描不建新预警;启用或降低阈值后不主动回填,仅由下一次扫描按当前有效套餐判断。
|
||||||
|
- `GET /package-traffic-alert-rules` 返回套餐、真流量总额度、阈值、启用状态、备注和更新时间。所有成功写操作记录操作者、前后值和时间。
|
||||||
|
|
||||||
|
### 扫描与预警创建
|
||||||
|
|
||||||
|
- Worker 只读取当前有效套餐使用记录;按同一资产聚合这些记录的真实已用量与套餐 `real_data_mb`,不读取虚流量、展示流量或运营商通道累计值。主套餐不存在有效规则、总额度不大于零或比例未达阈值时跳过。
|
||||||
|
- 对命中主套餐规则的套餐使用记录,在事务中写预警唯一键 `(package_usage_id, threshold_percent_snapshot)`,同时冻结套餐、资产、卡、当前设备、店铺、业务员、真实用量、额度、比例、阈值和触发时间。唯一冲突视为已处理,不重复投递通知。
|
||||||
|
- 创建时仅解析当前资产所属店铺的有效业务员;存在时在同一可靠事件链创建一条站内通知,保存预警 ID 作为幂等键;不存在时只保存预警。通知失败进入既有可靠投递恢复,不能删除预警或重新计算快照。
|
||||||
|
|
||||||
|
### 列表、详情与导出
|
||||||
|
|
||||||
|
- `GET /package-traffic-alerts`:仅超级管理员、平台用户,先应用既有资产数据范围;支持套餐、店铺、业务员、资产/卡标识、阈值、触发时间和通知投递状态筛选、分页。返回冻结快照与通知结果,当前归属变化不得改写预警事实。
|
||||||
|
- `GET /package-traffic-alerts/:id`:同一数据范围校验后返回完整预警快照和通知投递历史;越权与不存在统一按既有资源不可见处理。
|
||||||
|
- `POST /package-traffic-alerts/export`:复用异步导出;创建时冻结操作者、筛选、时间范围和可见资产范围。每行对应一条预警,执行时不得扩大范围或重算已冻结流量字段。
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- 流量数据延迟 → 下次扫描补建,不回写已冻结预警。
|
||||||
|
- 多卡设备 → 以使用记录资产归属聚合,不从当前设备反推流量。
|
||||||
|
- 扫描并发 → 唯一索引处理同一命中重复创建。
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
新增成对迁移和索引;隔离环境验证规则启停/降阈值、有效套餐汇总、去重、无业务员、权限导出和 up/down/up。
|
||||||
27
openspec/changes/add-package-real-usage-alerts/proposal.md
Normal file
27
openspec/changes/add-package-real-usage-alerts/proposal.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
## Scope
|
||||||
|
|
||||||
|
- 迭代编号:`AUG26-004`。
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
运营需要在套餐真实流量达量时通知资产所属店铺的有效业务员;现有套餐和通知能力没有规则、去重预警事实或可导出的触发快照。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 为每个套餐商品维护至多一条 1%~100% 真流量预警规则;规则变更只影响后续扫描,既有预警冻结快照。
|
||||||
|
- 扫描同一资产全部当前有效套餐的真流量汇总,以实际消耗流量的套餐记录关联资产和主套餐规则判断达量。
|
||||||
|
- 为同一套餐使用记录和阈值仅建一条预警,通知当时有效业务员,并提供权限受控列表与异步导出。
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `package-traffic-alert`: 真流量规则、预警事实、通知和导出。
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- 无。既有套餐状态与通知接收人隔离规则保持不变。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
影响套餐配置、套餐使用/流量扫描任务、资产投影、通知事件、导出、审计和新增 Schema。
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
## Purpose
|
||||||
|
|
||||||
|
按套餐真实流量和当前有效套餐事实生成一次性达量预警,向资产所属店铺当时有效业务员投递可追溯通知,而不将通道级停复机控制或虚流量混入套餐预警。
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 真流量预警规则
|
||||||
|
系统 SHALL 为每个套餐商品维护至多一条当前真流量预警规则,阈值为 1% 至 100% 的小数百分比。规则启用、修改或降低阈值只影响后续扫描;既有预警 MUST 保留触发时的套餐、阈值、流量和资产快照。规则停用后停止创建新预警;重新启用或降低阈值后,下次扫描发现已有有效套餐达量时必须补建符合条件的预警。
|
||||||
|
|
||||||
|
#### Scenario: 降低阈值后补建
|
||||||
|
- **WHEN** 管理员降低一个启用规则的阈值,下一次扫描发现其有效套餐已达到新阈值
|
||||||
|
- **THEN** 系统创建预警并冻结新阈值,不修改既有预警快照
|
||||||
|
|
||||||
|
### Requirement: 汇总口径、去重与通知
|
||||||
|
系统 SHALL 以同一资产全部当前有效套餐的 `真流量使用量 / 真流量总额度` 汇总比例判断达量,并使用该资产主套餐的规则。实际消耗流量的套餐使用记录所关联资产是权威归属;插拔卡时预警同时展示卡和当前关联设备,但 MUST NOT 汇总多张卡。虚流量、已失效/过期/非当前有效套餐不得计入。
|
||||||
|
|
||||||
|
同一套餐使用记录和同一命中阈值 MUST 至多创建一条预警。预警创建时仅向资产所属店铺当时有效业务员创建站内通知;无有效业务员时仍保留预警事实但不补发给未来新增业务员。通知和预警均须幂等,重复扫描不得重复创建。
|
||||||
|
|
||||||
|
#### Scenario: 多个有效套餐共同达量
|
||||||
|
- **WHEN** 某资产的多个当前有效套餐真流量汇总达到其主套餐规则阈值
|
||||||
|
- **THEN** 系统为命中套餐使用记录创建唯一预警,并只向扫描时该资产所属店铺的有效业务员投递通知
|
||||||
|
|
||||||
|
#### Scenario: 重复扫描
|
||||||
|
- **WHEN** 相同套餐使用记录和相同阈值被重复扫描命中
|
||||||
|
- **THEN** 系统保留原预警和通知,不创建重复记录
|
||||||
|
|
||||||
|
### Requirement: 预警查询与导出
|
||||||
|
超级管理员和平台用户 SHALL 在既有资产数据范围内查询和导出预警;列表和导出返回资产、卡、当前设备、套餐使用记录、真流量用量/额度/比例、阈值快照、触发时间及通知投递结果。导出必须使用既有异步任务并冻结创建时操作者、筛选条件和可见范围。
|
||||||
|
|
||||||
|
#### Scenario: 受限导出
|
||||||
|
- **WHEN** 平台用户在其资产数据范围内创建预警导出
|
||||||
|
- **THEN** 导出仅包含创建时可见预警,即使任务执行期间店铺归属发生变化
|
||||||
16
openspec/changes/add-package-real-usage-alerts/tasks.md
Normal file
16
openspec/changes/add-package-real-usage-alerts/tasks.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
## 1. 数据与规则
|
||||||
|
|
||||||
|
- [ ] 1.1 追踪套餐使用有效态、真流量字段、资产/卡/设备关联、有效业务员、通知事件及异步导出调用链。
|
||||||
|
- [ ] 1.2 新增成对迁移、模型和约束:套餐唯一规则、预警快照、使用记录+阈值唯一去重、查询/导出索引。
|
||||||
|
- [ ] 1.3 实现规则 CRUD、1%~100% 校验、启停和审计;更新套餐管理 OpenAPI。
|
||||||
|
|
||||||
|
## 2. 扫描与读侧
|
||||||
|
|
||||||
|
- [ ] 2.1 实现可重跑扫描:按资产汇总当前有效套餐真流量、选择主套餐规则、排除虚流量/失效记录,并原子创建预警与通知事件。
|
||||||
|
- [ ] 2.2 接入既有任务调度和通知投递,确保无有效业务员仍建预警、重复扫描不重复通知。
|
||||||
|
- [ ] 2.3 实现受资产数据范围保护的预警列表/详情和异步导出,冻结导出筛选与可见范围。
|
||||||
|
|
||||||
|
## 3. 验证
|
||||||
|
|
||||||
|
- [ ] 3.1 在隔离数据库验证迁移 up/down/up、规则变化补建、有效套餐汇总、重复扫描、插拔卡展示、通知接收人和导出权限。
|
||||||
|
- [ ] 3.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-package-real-usage-alerts --strict`、`openspec doctor --json` 和 `./scripts/context-health.sh`;自动化测试按项目决策为 N/A。
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-31
|
||||||
58
openspec/changes/add-payment-merchant-pools/design.md
Normal file
58
openspec/changes/add-payment-merchant-pools/design.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
现有 `tb_wechat_config` 同时承担支付渠道配置;订单、充值和 `tb_payment` 通过 `payment_config_id` 供回调加载创建时配置。`tb_payment` 已有非敏感 `merchant_identity` 快照,但不足以区分商户池、服务商和完整路由。新模型必须让新单和旧单分流,不能把历史订单指向迁移后新商户。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### 独立实体与不可变路由快照
|
||||||
|
新增商户、商户池、池成员、微信授权配置及金额/笔数统计事实;支付单扩展实际商户和商户池 ID 及非敏感快照。商户凭证只留在商户表受控字段,支付单/审计不复制。被引用后锁定商户支付方式、服务商与身份,避免历史验签与退款语义漂移。
|
||||||
|
|
||||||
|
### 路由和统计在支付创建/成功边界闭合
|
||||||
|
支付创建在事务内读取唯一启用池、按方式选择成员并冻结路由;无成员/池失败即返回。金额/笔数统计只在现有“支付成功首次生效”路径按实际商户条件递增,以支付 ID 唯一约束避免重复回调累计;不在预下单预占。时间方式由当前时间和受控起点计算,不写逐次路由日志。
|
||||||
|
|
||||||
|
### 新旧配置双读切换
|
||||||
|
新支付单具有 merchant ID 时,支付加载、回调验签、查询和退款均从商户加载服务商凭证;merchant ID 为空的历史单保持现有 `payment_config_id` 路径。迁移先复制生效配置中完整凭证,再发布新支付路径;不可将现有记录批量回填为新商户,因为其实际历史身份无法保证一致。
|
||||||
|
|
||||||
|
### 敏感配置边界
|
||||||
|
后台管理响应按 PRD 向两类已认证管理角色完整返回凭证;所有 logger、错误、审计 payload、支付单快照和导出只允许写 ID、名称和脱敏/非敏感身份。更新凭证后使现有配置加载缓存失效;历史回调读取最新有效凭证以支持轮换。
|
||||||
|
|
||||||
|
## 管理与支付动作契约
|
||||||
|
|
||||||
|
以下路径为本 Change 新增后台管理契约;所有管理写操作仅限超级管理员和平台用户,金额均为分,敏感凭证只在已认证管理请求的写入/详情响应中传输,绝不进入支付快照、审计、日志或导出。
|
||||||
|
|
||||||
|
### 商户
|
||||||
|
|
||||||
|
- `POST /payment-merchants`:请求 `name`、`payment_method`(`wechat`/`alipay`)、`provider_type`、`merchant_identity`、`credentials`、`enabled`、`remark`。同一支付方式下身份标识不得重复;凭证缺失或与服务商类型不匹配时拒绝。
|
||||||
|
- `PUT /payment-merchants/:id`:未被任何支付单引用时可修改全部字段;被引用后仅可改名称、凭证、启停、备注,修改支付方式、服务商类型或商户身份返回“已被支付单引用,不能修改收款身份”。
|
||||||
|
- `DELETE /payment-merchants/:id`:被引用或仍属于任一商户池时拒绝;删除前必须显式二次确认。停用不影响已冻结该商户的查单、验签和退款。
|
||||||
|
|
||||||
|
### 商户池
|
||||||
|
|
||||||
|
- `POST /payment-merchant-pools`:请求 `name`、`payment_method`、`enabled`、`strategy`(金额/笔数/时间)、策略参数和有序 `member_ids`。成员均须存在、启用、与池支付方式一致且不重复;同一支付方式最多一个启用池,冲突返回“该支付方式已有启用商户池”。
|
||||||
|
- `PUT /payment-merchant-pools/:id`:修改阈值只保留当前统计;修改金额/笔数统计周期、时间周期、起始时间或每轮成员排序时开启新统计周期;自然周期仅调整排序时保留未移除成员累计。成员移除后不再选择,但历史支付快照不改写。
|
||||||
|
- `POST /payment-merchant-pools/:id/enable` 与 `/disable`:启用时再次校验唯一启用池和可用成员;停用后新支付创建明确失败,不回退综合支付配置。
|
||||||
|
|
||||||
|
### 微信授权配置
|
||||||
|
|
||||||
|
- `GET /wechat-authorizations`:最多返回一个启用配置;仅管理角色可读取。`PUT /wechat-authorizations/current` 创建或更新唯一配置,写入公众号 H5/JSSDK、小程序登录及支付 AppID 所需字段。
|
||||||
|
- 启用第二个配置返回“平台已有启用微信授权配置”;停用后 C 端微信登录/OpenID/微信支付 AppID 不得静默回退其他支付商户或旧配置。
|
||||||
|
|
||||||
|
### 新旧支付分流
|
||||||
|
|
||||||
|
- C 端套餐订单、资产钱包充值、代理在线预存款创建支付时,在同一事务内读取对应支付方式唯一启用池并冻结 `merchant_id`、`merchant_pool_id`、非敏感收款身份与轮询快照;无可用成员返回“暂无可用商户”。
|
||||||
|
- `merchant_id` 非空的支付,在回调、查单、退款时加载该商户当前凭证;为空的历史支付仅按既有 `payment_config_id` 处理。不得按当前启用池为历史单推断商户。
|
||||||
|
- 首次支付成功消费者以支付 ID 唯一记账金额/笔数统计;重复回调不重复累计。预下单、失败、关闭和退款均不变更统计。
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- 并发成功回调超过阈值:这是“只统计成功、不预占”的明确结果,下一次选路才跳过。
|
||||||
|
- 商户停用后的退款:停用不阻断历史退款,实际调用仍由凭证/渠道结果决定。
|
||||||
|
- 迁移缺失凭证:不造空商户池,受影响新支付明确失败。
|
||||||
|
- 旧回调误入新路径:以支付单 merchant ID 为唯一分流条件,严禁按当前启用池推断。
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. 新增成对迁移创建商户/池/成员/微信授权表、唯一启用约束、支付单路由列和成功统计索引。
|
||||||
|
2. 在迁移事务中从唯一生效综合配置复制完整凭证并创建单成员池;全过程禁止日志输出密钥/证书。
|
||||||
|
3. 隔离库验证微信直连、富友、支付宝、空配置、停用历史商户、回调兼容、三种轮询及 up/down/up。
|
||||||
|
4. 回滚前停止新支付创建;有新路由单时仅回退应用流量,不执行会破坏新支付事实的 down。
|
||||||
27
openspec/changes/add-payment-merchant-pools/proposal.md
Normal file
27
openspec/changes/add-payment-merchant-pools/proposal.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
当前所有线上支付依赖一份综合支付配置,无法按支付方式在多个实际收款商户之间受控轮询;创建支付后的商户事实也不足以让停用商户的历史回调、查单和原路退款继续安全执行。
|
||||||
|
|
||||||
|
本 Change 落实 AUG26-002:将收款商户、支付路由和 C 端微信授权分离,并以商户池快照取代新业务对旧综合支付配置的依赖。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增独立商户管理:微信商户与支付宝商户分别建档;商户保存支付能力和敏感凭证,但历史支付单仅保存非敏感身份快照。
|
||||||
|
- 新增每种支付方式至多一个启用商户池,支持按成功收款金额、成功笔数或时间周期轮询;新支付仅从命中池选择实际商户。
|
||||||
|
- 新增全局唯一启用的微信授权配置,专供 C 端公众号 H5/JSSDK、小程序登录、OpenID 和支付 AppID;它不是微信收款商户。
|
||||||
|
- 新建 C 端套餐购买、资产钱包充值、代理在线预存款充值按商户池路由;后台线下/钱包支付不经过商户池。无可用商户时失败,不得回退旧配置或自动换商户重试。
|
||||||
|
- 从当前生效综合支付配置一次性复制完整凭证形成新商户、单成员商户池及微信授权配置;新支付切换后,旧配置和历史订单仅继续处理其自身回调、查询和退款。
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `merchant-payment-routing`: 商户、商户池、微信授权配置、轮询、历史商户快照和迁移切换行为。
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- 无。该能力向既有支付创建、回调和退款调用链提供已选商户事实,不改变其资金幂等不变量。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
影响支付配置模型和后台接口、`tb_payment`/订单/充值支付关联、微信/支付宝/富友支付加载和回调、支付与退款审计、敏感信息访问及新增成对迁移。
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
## Purpose
|
||||||
|
|
||||||
|
管理实际收款商户、商户池轮询和全局微信授权配置,使新线上支付的收款身份可冻结、历史支付可继续使用其原商户,并避免凭证泄露或无配置时静默回退。
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 商户与微信授权配置管理
|
||||||
|
系统 SHALL 将实际收款商户与微信授权配置分离。一个商户 MUST 仅对应 `wechat` 或 `alipay` 一种支付方式,并保存名称、支付方式、服务商类型、商户号或应用标识、敏感凭证、状态和备注;微信直连与富友均为微信支付商户。平台最多存在一个启用的微信授权配置,该配置保存 C 端公众号 H5/JSSDK、小程序登录所需参数,C 端微信登录、OpenID 和微信支付 AppID MUST 只读取该配置。
|
||||||
|
|
||||||
|
超级管理员和平台用户可创建、编辑、启用、停用商户、商户池和微信授权配置,其他角色无管理入口。被支付单引用的商户 MUST NOT 删除且其支付方式、服务商类型、商户号/应用标识不得修改;未被引用商户仅可移出所有商户池并经二次确认删除。停用只影响新支付单,历史支付的回调、查单和原路退款仍使用该商户当前凭证。管理 API 可向上述已认证管理角色返回完整凭证,但日志、审计快照、错误和普通业务响应 MUST NOT 保存或返回敏感凭证。
|
||||||
|
|
||||||
|
#### Scenario: 受引用商户停用
|
||||||
|
- **WHEN** 管理员停用已被支付单命中的商户
|
||||||
|
- **THEN** 新支付单不再选择该商户,已命中支付单的回调、查询和原路退款仍按该商户处理
|
||||||
|
|
||||||
|
#### Scenario: 非管理角色读取配置
|
||||||
|
- **WHEN** 不具备超级管理员或平台用户身份的账号请求商户或微信授权配置
|
||||||
|
- **THEN** 系统拒绝访问且不返回任何凭证或身份字段
|
||||||
|
|
||||||
|
### Requirement: 商户池唯一性与轮询配置
|
||||||
|
系统 SHALL 为每种支付方式最多启用一个商户池;停用历史池可保留,但不得同时启用多个同支付方式池。商户池成员支付方式 MUST 与池一致,成员按明确顺序排列;金额/笔数方式必须配置 `每轮累计`、`自然日累计` 或 `自然月累计` 统计周期,时间方式必须配置最小为 1 分钟的数值、单位和起始时间。
|
||||||
|
|
||||||
|
金额和笔数轮询只统计已确认支付成功结果,不在预下单时预占,也不因退款回冲。达到阈值的成员在当前周期跳过;所有成员达到阈值时新支付失败。时间轮询自起始时间按固定时段和成员顺序选择,成员停用即时跳下一个可用成员但不重置时段。预下单失败不得自动切换或重试,失败单不计入统计;客户再次发起时重新选择。修改阈值保留当前统计,修改统计周期、金额/笔数方式、时间周期、起始时间或每轮排序按 PRD 规则开启新周期;自然周期排序调整保留未移除成员累计。
|
||||||
|
|
||||||
|
#### Scenario: 并发预下单未预占额度
|
||||||
|
- **WHEN** 多个客户并发创建金额或笔数轮询支付单且当前成员尚未达到阈值
|
||||||
|
- **THEN** 系统可使这些支付单均命中当前成员,只有后续确认成功的支付才计入累计,已创建支付单不因轮询切换改挂商户
|
||||||
|
|
||||||
|
#### Scenario: 当期没有可用商户
|
||||||
|
- **WHEN** 启用商户池中不存在启用且未达阈值的成员,或商户池已停用
|
||||||
|
- **THEN** 系统拒绝创建新支付单并提示暂无可用商户,不回退到旧综合支付配置
|
||||||
|
|
||||||
|
### Requirement: 新支付商户快照与历史兼容
|
||||||
|
C 端套餐购买、C 端资产钱包充值及代理在线预存款充值 SHALL 按支付方式通过对应启用商户池选择实际商户;后台线下订单和钱包余额支付 MUST NOT 经过商户池。每笔通过商户池创建的支付单 MUST 保存商户 ID、商户名称/支付方式/服务商类型/商户号或应用标识快照、商户池 ID/名称快照及轮询方式快照,但不得复制敏感凭证。支付、回调验签、查单和原路退款读取该实际商户当前凭证;商户退款能力只由服务商类型和退款必需凭证完整性决定,不提供人工开关。
|
||||||
|
|
||||||
|
上线迁移 MUST 从当前生效综合支付配置复制完整凭证:创建全局微信授权配置、微信/支付宝商户和各自单成员启用池。凭证不完整的方式不建池;迁移仅在数据库复制敏感数据。新订单必须只走商户池;旧配置和其历史订单不改写,继续服务历史回调、查询和退款。
|
||||||
|
|
||||||
|
#### Scenario: 新支付冻结实际商户
|
||||||
|
- **WHEN** 客户以微信或支付宝创建覆盖范围内的新线上支付单
|
||||||
|
- **THEN** 系统选择并冻结一个实际商户和商户池路由快照,并使用该商户的服务商凭证发起支付
|
||||||
|
|
||||||
|
#### Scenario: 旧支付单回调
|
||||||
|
- **WHEN** 商户池切换后收到未带新商户快照的历史支付单回调
|
||||||
|
- **THEN** 系统按既有综合支付配置兼容处理该历史单,不将其改挂到任何新商户
|
||||||
20
openspec/changes/add-payment-merchant-pools/tasks.md
Normal file
20
openspec/changes/add-payment-merchant-pools/tasks.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
## 1. 数据与配置管理
|
||||||
|
|
||||||
|
- [ ] 1.1 追踪 `tb_wechat_config`、订单/充值、`tb_payment`、支付加载器和三类回调的现有 `payment_config_id` 读写链路,列出新旧分流点和敏感字段清单。
|
||||||
|
- [ ] 1.2 新增成对迁移:商户、商户池、成员、微信授权配置、成功累计/路由快照所需表列、唯一启用/成员支付方式/历史引用约束和查询索引;不得修改既有迁移。
|
||||||
|
- [ ] 1.3 实现商户、商户池及微信授权配置模型、管理 Query/Handler/RouteSpec:管理角色权限、启停、成员排序、受引用字段锁定、移出后确认删除及无敏感审计。
|
||||||
|
- [ ] 1.4 实现迁移时从当前生效综合支付配置复制完整微信授权、微信/支付宝商户和单成员池;不完整方式不创建池,迁移日志不得含凭证。
|
||||||
|
|
||||||
|
## 2. 商户池选择与支付链路
|
||||||
|
|
||||||
|
- [ ] 2.1 实现金额、笔数、时间轮询选择器及池配置变更重置规则;使用事务/受控查询保证唯一启用池和成员状态一致。
|
||||||
|
- [ ] 2.2 在 C 端套餐支付、资产钱包充值、代理在线充值的创建路径接入选择器,保存商户/池/方式非敏感快照;无可用商户和预下单失败不回退、不换商户。
|
||||||
|
- [ ] 2.3 在支付成功首次生效路径按支付 ID 幂等累计金额/笔数;退款不得回冲,失败/重复回调不得计入。
|
||||||
|
- [ ] 2.4 改造支付加载、微信/支付宝/富友回调、查单和原路退款:新单读取实际商户,历史 merchant ID 为空的单继续读取旧 `payment_config_id`;停用商户仍可处理历史单。
|
||||||
|
- [ ] 2.5 清理日志、错误、审计和普通 DTO 中的敏感凭证,管理 API 仅向超级管理员/平台用户按 PRD 返回完整配置并使凭证更新刷新加载缓存。
|
||||||
|
|
||||||
|
## 3. 文档与验证
|
||||||
|
|
||||||
|
- [ ] 3.1 更新支付、商户和微信授权管理接口 OpenAPI,并运行 `go run cmd/gendocs/main.go`。
|
||||||
|
- [ ] 3.2 在隔离数据库验证迁移 up/down/up、配置迁移、三类新支付、缺失配置、三种轮询、并发成功累计、商户停用历史回调/退款及旧单兼容。
|
||||||
|
- [ ] 3.3 运行 `gofmt -w`(变更 Go 文件)、`go build ./cmd/api ./cmd/worker`、`openspec validate add-payment-merchant-pools --strict`、`openspec doctor --json` 和 `./scripts/context-health.sh`;自动化测试按项目决策为 N/A。
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-31
|
||||||
32
openspec/changes/add-phone-asset-associations/design.md
Normal file
32
openspec/changes/add-phone-asset-associations/design.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
现有 H5 已有手机号绑定与全局开关;新关系不能由后台或换货推测建立。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- 新表以手机号、资产和有效状态保存关系,并对当前有效关系实施十项计数与唯一约束。
|
||||||
|
- 验证/换绑在事务内锁定相关手机号关系;换绑先检查总数再整体迁移。
|
||||||
|
- 批量导入复用逐行任务,读取按资产数据范围,日志仅保留脱敏手机号。
|
||||||
|
|
||||||
|
## H5 与后台动作契约
|
||||||
|
|
||||||
|
### H5 建联与换绑
|
||||||
|
|
||||||
|
- 既有登录签发 token 时,按全局强制绑定开关及当前访问资产查询有效关联;开关开启且当前手机号未关联该资产时,响应 `need_bind_phone=true`,并限制依赖该资产的业务入口直至验证成功。开关关闭时不创建新关联,也不删除历史关联。
|
||||||
|
- 既有 `bind_phone` 短信场景验证成功后,事务中锁定手机号和资产有效关系;同一手机号—资产已有效关联则幂等成功,否则先计算该手机号有效资产数。达到十项返回“该手机号最多关联10项有效资产”,不写关联或手机号变更。
|
||||||
|
- 既有 `change_phone_old`、`change_phone_new` 两个验证码均验证成功后,事务锁定旧、新手机号关系;计算新号码当前有效关系数加旧号码待迁移有效关系数,超过十项则整体失败。通过时把旧号码全部有效关系原子失效/迁移至新号码,并记录旧、新号码脱敏审计;任一步失败不变更任一关系。
|
||||||
|
|
||||||
|
### 后台查看与解除
|
||||||
|
|
||||||
|
- `GET /phone-asset-associations`:仅超级管理员、平台用户,先按资产数据范围过滤;支持资产标识、手机号(仅权限内完整值)、关联状态、创建时间筛选,返回资产、手机号、建立时间、建立来源固定为 `h5_sms_verification` 和状态。
|
||||||
|
- `DELETE /phone-asset-associations/:id`:请求必须含 `reason`(1~500 字符)和二次确认;锁定指定有效关联后复核资产数据范围,标记失效并审计操作者、资产、脱敏手机号、原因和时间。后台没有创建/补录接口。
|
||||||
|
- `POST /phone-asset-associations/batch-unbind`:请求去重的资产集合、原因、二次确认;每个资产独立解除其全部有效关系,返回成功数、失败数与逐资产结果。越权、资产不存在和已无有效关系对调用方使用统一失败文案。
|
||||||
|
- Excel 解绑复用既有异步导入:每行按资产标识处理,独立授权与事务,任务持久化行号、结果和失败原因;一行失败不得回滚已成功行。
|
||||||
|
|
||||||
|
### 边界
|
||||||
|
|
||||||
|
- 换货、资产导入、后台资产编辑和个人客户主手机号历史记录均不得创建、推断、复制或迁移该关系。新换货资产在首次 H5 访问时才依当前开关走验证。
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
新增成对迁移,不回填;隔离库验证开关、上限、换绑、权限解绑、换货边界及 up/down/up。
|
||||||
25
openspec/changes/add-phone-asset-associations/proposal.md
Normal file
25
openspec/changes/add-phone-asset-associations/proposal.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
## Scope
|
||||||
|
|
||||||
|
- 迭代编号:`AUG26-009`。
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
现有全局绑定不能保留手机号与资产的验证关系,也无法安全支持按资产解除。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增 H5 验证建立的手机号—资产关联及十项上限。
|
||||||
|
- 新增客户原子换绑和后台受权限控制的单项/批量/Excel 解绑。
|
||||||
|
- 保持全局强制绑定,换货不迁移关系,不回填历史。
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
- `phone-asset-association`: 已验证手机号与资产关联。
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
- 无。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
影响 H5 认证、资产查询、短信、导入、审计和 Schema。
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
## Purpose
|
||||||
|
|
||||||
|
保存仅由 H5 短信验证建立的手机号—资产当前有效关系,使全局强制绑定能逐资产执行,同时提供受资产数据范围控制的后台查看与解除能力。
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: H5 验证建立关联与数量上限
|
||||||
|
系统 SHALL 保留既有全局 H5 强制绑定开关。开关开启时,客户首次登录一项未关联当前手机号的资产必须完成短信验证后建立关系;已关联资产不重复验证。开关关闭时登录不要求验证且不新增关系,已有关系保留。关联只能由 H5 验证建立,单手机号最多关联十项当前有效资产;上线不回填历史客户—资产关系。
|
||||||
|
|
||||||
|
#### Scenario: 第十一项资产验证
|
||||||
|
- **WHEN** 已关联十项有效资产的手机号验证另一项资产
|
||||||
|
- **THEN** 系统拒绝本次新关联,已有十项关系不变
|
||||||
|
|
||||||
|
### Requirement: 换绑、查看和解绑
|
||||||
|
客户更换手机号时 MUST 同时验证旧、新号码,并原子将旧手机号全部有效资产关系迁至新号码;新号码现有关联数加迁移数超过十项时整次失败。后台仅超级管理员和平台用户可在资产数据范围内查看完整关联手机号、单项解绑、勾选批量解绑及 Excel 解绑;后台不得补录。
|
||||||
|
|
||||||
|
单项解绑必须指定一条资产—手机号关系。批量和 Excel 按资产解除该资产全部当前有效手机号关系,必须二次确认、填写原因、逐条审计;逐资产独立执行,返回成功数、失败数和统一越权失败文案。换货不得迁移该关系,新资产首次访问仍按全局开关验证。
|
||||||
|
|
||||||
|
#### Scenario: 换绑超过上限
|
||||||
|
- **WHEN** 客户换绑后新手机号关联总数将超过十项
|
||||||
|
- **THEN** 系统不迁移任何关系且旧、新手机号关系均保持原状
|
||||||
12
openspec/changes/add-phone-asset-associations/tasks.md
Normal file
12
openspec/changes/add-phone-asset-associations/tasks.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
## 1. 关联与 H5
|
||||||
|
- [ ] 1.1 追踪 H5 登录/短信验证、客户换绑、资产数据范围和换货调用链。
|
||||||
|
- [ ] 1.2 新增关联表、有效关系唯一/计数索引的成对迁移、模型和审计常量。
|
||||||
|
- [ ] 1.3 实现全局开关下的验证建联、十项限制和原子换绑。
|
||||||
|
|
||||||
|
## 2. 后台解绑
|
||||||
|
- [ ] 2.1 实现资产详情/列表完整手机号投影、单项解绑、二次确认批量和逐行 Excel 解绑及统一越权结果。
|
||||||
|
- [ ] 2.2 保持换货不迁移,补齐路由和 OpenAPI。
|
||||||
|
|
||||||
|
## 3. 验证
|
||||||
|
- [ ] 3.1 隔离库验证上限、换绑回滚、权限、批量部分成功、换货边界和 up/down/up。
|
||||||
|
- [ ] 3.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-phone-asset-associations --strict` 与 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-31
|
||||||
13
openspec/changes/add-priority-polling-queue/design.md
Normal file
13
openspec/changes/add-priority-polling-queue/design.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
优先轮询是普通轮询之上的高优先级调度,不是另一套轮询业务逻辑。自动套餐事件、无有效套餐、普通轮询异常补偿和受控人工触发均可入队;优先与普通轮询共享同一卡互斥和既有并发边界。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- 队列表保存卡、活动状态、合并来源、触发类型、来源订单/套餐使用、尝试次数和结果;同一卡活动项唯一。
|
||||||
|
- 在订单/套餐生效、无有效套餐识别、普通轮询异常后通过可靠事件入队;具备既有手动轮询权限的后台账号可在数据范围内通过 `POST /priority-polling-queue` 提交 `asset_identifier` 与必填 `reason`。消费者按来源事实幂等合并,不能由支付回调直接重复写队列。
|
||||||
|
- Worker 用行锁领取优先项,复用普通轮询既有并发上限、外部调用保护、套餐/流量/状态同步和失败重试;普通轮询查询活动项排除对应卡。完成或最终失败出队并写触发来源、操作来源、次数、时间和结果审计。
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
新增成对迁移、活动项唯一索引和来源索引;隔离库验证三种自动触发、回调重放、同卡合并、普通互斥、失败重试和 up/down/up。
|
||||||
24
openspec/changes/add-priority-polling-queue/proposal.md
Normal file
24
openspec/changes/add-priority-polling-queue/proposal.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
## Scope
|
||||||
|
|
||||||
|
- 迭代编号:`AUG26-016`。
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
紧急卡状态查询与普通轮询竞争,无法保证处理顺序或追踪加急事实。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增受限管理的卡轮询优先队列。
|
||||||
|
- Worker 优先领取队列项,并以唯一活动项和锁避免同卡并发。
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
- `priority-polling-queue`: 卡轮询加急队列。
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
- 无。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
影响后台卡管理、异步轮询、运营商调用、审计和 Schema。
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 优先轮询队列
|
||||||
|
系统 SHALL 在不改变普通轮询内容、并发上限和失败重试的前提下,为指定业务场景创建高优先级轮询任务。同一资产未完成优先任务必须合并触发来源、次数和最近时间,而不得重复执行同一轮轮询。
|
||||||
|
|
||||||
|
#### Scenario: 规则命中
|
||||||
|
- **WHEN** 业务请求或任务满足本需求定义的前置条件
|
||||||
|
- **THEN** 系统按上述规则完成处理、保留可追溯事实,并拒绝与状态、权限或幂等约束冲突的重复操作
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 套餐生命周期自动优先入队
|
||||||
|
系统 SHALL 在下列业务事实提交成功后,将关联资产对应卡自动加入优先轮询队列:无有效套餐、新购套餐首次成为有效套餐、已过期套餐完成续购、流量用尽后购买的加油包完成生效,及普通轮询出现既有异常补偿条件。具备既有轮询手动触发权限的后台账号也可为其数据范围内资产手动入队并必须填写原因。入队必须等待订单、支付、套餐使用记录提交成功;失败或取消订单不得入队。队列项记录触发类型(新购、过期续购、加油包)、来源订单/套餐使用记录、触发时间及执行结果。
|
||||||
|
|
||||||
|
同一卡只能有一条活动优先项。多个触发同时到达时,系统合并来源事实、保留最近触发时间,且最多执行一次未完成优先轮询;不得因重复支付回调、任务重放或相同订单重试重复入队。手动入队不允许修改调度优先级或绕过既有并发上限;它与自动/异常触发合并到同一卡活动项。
|
||||||
|
|
||||||
|
#### Scenario: 人工与异常补偿触发合并
|
||||||
|
- **WHEN** 同一卡已有自动入队活动项,管理员手动触发或普通轮询异常补偿再次触发
|
||||||
|
- **THEN** 系统仅追加触发来源、次数和最近时间,不创建第二项或重复调用轮询
|
||||||
|
|
||||||
|
#### Scenario: 重复支付成功回调
|
||||||
|
- **WHEN** 已生效套餐订单的支付成功回调重复投递
|
||||||
|
- **THEN** 系统仅创建或更新一条关联该卡的活动优先队列项
|
||||||
|
|
||||||
|
### Requirement: 优先领取、执行与出队审计
|
||||||
|
Worker SHALL 先领取活动优先项,再执行既有套餐、流量和卡状态轮询;普通轮询不得与已领取优先项并发。领取时使用行锁跳过已锁项,同一卡由队列状态互斥。轮询成功后标记完成并出队;可恢复失败按既有策略重试,超过策略标记失败并保留安全失败原因。入队、领取、每次失败、重试、完成和失败出队均记录触发来源、操作来源、时间和结果;仅具有既有轮询任务权限的后台账号可查询记录。
|
||||||
|
|
||||||
|
#### Scenario: 普通轮询遇到优先项
|
||||||
|
- **WHEN** 普通轮询准备处理一张存在活动或已领取优先项的卡
|
||||||
|
- **THEN** 普通轮询跳过该卡,直到优先项完成或失败出队
|
||||||
9
openspec/changes/add-priority-polling-queue/tasks.md
Normal file
9
openspec/changes/add-priority-polling-queue/tasks.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
## 1. 自动入队与执行
|
||||||
|
- [ ] 1.1 追踪新购生效、过期续购生效、流量用尽加油包生效、可靠事件、普通轮询和运营商调用链路。
|
||||||
|
- [ ] 1.2 新增队列、来源事实和执行审计的成对迁移、活动项唯一索引、模型与查询权限。
|
||||||
|
- [ ] 1.3 在三种套餐生命周期成功事件后发布可靠入队事件;实现同卡合并、回调/任务重放幂等和禁止人工入队。
|
||||||
|
- [ ] 1.4 实现锁定领取、普通轮询排除、套餐/流量/状态优先轮询、失败恢复和出队审计。
|
||||||
|
|
||||||
|
## 2. 验证
|
||||||
|
- [ ] 2.1 验证三种触发、失败订单不入队、重复回调、并发合并、普通互斥、失败重试和权限。
|
||||||
|
- [ ] 2.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-priority-polling-queue --strict` 和 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-31
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
退款服务已有申请、企业微信审批和钱包审计路径,但现有状态/人工入口不足以表达渠道退款未知结果。支付商户池 Change 完成后,新支付单可读取冻结实际商户;历史单继续走旧支付配置。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- 退款单新增方式、冻结实收金额、套餐使用/材料快照、渠道退款状态/流水/安全失败原因和审批实例历史;金额均为分。
|
||||||
|
- 创建、审批消费和渠道执行使用同一退款单锁及条件状态更新。一笔订单的活动退款唯一约束防止并发重复;渠道请求使用稳定请求标识,结果未知进入恢复任务而非重发。
|
||||||
|
- 企业微信通过事务内完成套餐失效与钱包退款准备;原路渠道调用使用可靠事件,成功回写退款终态。客户收款信息退款以企微通过终态完成。
|
||||||
|
- 移除/拒绝本地人工终审语义,保留历史兼容读取;未成功重提始终冻结新实例快照。
|
||||||
|
|
||||||
|
## 行为契约
|
||||||
|
|
||||||
|
### 退款申请与重提
|
||||||
|
|
||||||
|
- `POST /refunds`:调用者必须在订单数据范围内。服务锁定订单和既有活动退款,读取订单或原成功支付记录的权威实收金额;缺失、非正或已存在审批中/原路处理中/原路失败申请时拒绝。请求包含退款原因、退款方式、退款金额、客户收款信息及附件(仅客户收款信息方式);金额为正分且不超过冻结实收金额。
|
||||||
|
- 服务按实际支付方式生成可选方式:微信/支付宝线上支付为原路或客户收款信息,资产钱包/代理主钱包仅原钱包,后台线下/员工代收仅客户收款信息;不匹配的方式返回“该订单不支持此退款方式”。客户收款信息与至少一个凭证附件必须同时存在,且不读取员工收款方式字典。
|
||||||
|
- `PUT /refunds/:id` 或既有重提入口只允许驳回、关闭或渠道明确失败的未成功申请;重新锁定订单和申请,保存新的原因、方式、金额、收款信息、附件和套餐使用快照,创建新的企业微信审批实例。提交失败或审批未知不是可重提状态;已成功、审批中、原路处理中返回状态冲突。
|
||||||
|
|
||||||
|
### 企业微信终审与权益处理
|
||||||
|
|
||||||
|
- 回调和既有审批恢复任务以审批实例 ID 进入同一幂等用例;移除新业务的本地人工通过、拒绝和退回终审路径,历史接口仅保留兼容读取或明确拒绝。
|
||||||
|
- 首次最终通过时锁定退款和订单,校验批准金额不超过冻结实收金额;在事务中标记审批通过、使关联套餐失效、接续下一套餐、评估停机并建立退款执行事实。重复/乱序回调不得再次失效套餐或启动第二次渠道退款。
|
||||||
|
- 客户收款信息方式在企业微信通过时标记退款成功;原钱包方式沿用原钱包退款事务;原路方式只写待执行可靠事件,不能在审批事务中假定渠道已成功。
|
||||||
|
|
||||||
|
### 原路执行与恢复
|
||||||
|
|
||||||
|
- 原路执行消费者在调用前锁定退款,验证原支付单、实际收款商户、渠道流水、可退金额和当前商户退款凭证。新支付按冻结 `merchant_id` 加载商户当前凭证,历史支付按 `payment_config_id`;商户停用不阻断历史校验。
|
||||||
|
- 以退款 ID/稳定渠道请求号至多提交一次可确认请求。渠道明确成功时保存渠道退款流水并转退款成功;超时、未知、凭证失效、余额不足、拒绝均写安全原因并保持处理中或失败恢复状态,不得标记成功或盲目再次调用。
|
||||||
|
- 原路失败后若改为客户收款信息退款,必须修改申请材料并走新企业微信审批;审批通过后撤销只记录审批异常,不恢复套餐权益、不取消已提交渠道退款且不自动重提。
|
||||||
|
|
||||||
|
### 读取与审计
|
||||||
|
|
||||||
|
- 退款列表、详情和导出返回冻结实收金额、方式、申请/渠道状态、失败安全摘要、审批实例和渠道流水,并按既有订单数据范围过滤。审计记录申请、重提、审批终态、权益处理、渠道调用和恢复,但不得记录凭证内容、完整收款文本或商户密钥。
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
新增成对迁移和活动退款约束,先部署兼容读写与恢复消费者,再关闭人工审批入口;隔离库验证方式矩阵、重复回调、渠道未知、失败重提、权益时点和 up/down/up。
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
## Scope
|
||||||
|
|
||||||
|
- 迭代编号:`AUG26-006`。
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
现有退款以本地审批状态处理,不能按来源实际支付事实决定退款方式、冻结实收金额,或在企业微信通过后用原实际收款商户可靠执行原路退款。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 退款申请冻结来源订单、权威实收金额、唯一套餐使用情况、退款金额/原因、方式和当次审批材料;实收金额不可由提交人修改。
|
||||||
|
- 建立线上支付、资产钱包、代理主钱包和后台线下订单的退款方式矩阵,并在创建、提交和执行前重复校验。
|
||||||
|
- 企业微信是唯一审批终审:通过即按既有规则失效套餐;客户收款信息退款即完成,原路退款须待渠道明确成功才完成。
|
||||||
|
- 原路退款固定使用原支付单实际商户和渠道流水;超时/未知/失败保留可恢复状态,不重复退款;未成功申请可按规则修改并新建审批实例重提。
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- 无。
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `order-refund-exchange`: 退款申请、状态机、方式矩阵和套餐联动。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
影响退款模型/接口、企业微信审批、支付商户与渠道退款、资产/代理钱包、员工账单和佣金回溯;需新增成对迁移并淘汰本地人工终审入口。
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 退款实收金额与方式矩阵
|
||||||
|
系统 SHALL 从来源订单或原成功支付记录带出并冻结权威实收金额,提交人不得填写或修改;无法确定时拒绝申请。退款金额和企业微信授权金额不得超过该冻结额,且一笔订单最多一张最终成功退款申请,成功后不得再申请。
|
||||||
|
|
||||||
|
线上微信/支付宝套餐订单仅可选原路退款或客户收款信息退款;资产钱包支付仅自动退回原资产钱包;代理预存款/主钱包支付仅自动退回原代理钱包;后台线下/员工代收套餐订单仅可选客户收款信息退款。代理充值预存款业务单不在本期退款范围。客户收款信息退款必须包含客户收款信息自由文本和客户凭证附件,且不得复用公司收款方式字典。
|
||||||
|
|
||||||
|
#### Scenario: 无权威实收金额
|
||||||
|
- **WHEN** 来源订单无法取得权威实收金额
|
||||||
|
- **THEN** 系统拒绝创建退款申请,不允许提交人以自填金额替代
|
||||||
|
|
||||||
|
#### Scenario: 钱包订单申请退款
|
||||||
|
- **WHEN** 已支付套餐订单的实际支付方式为资产钱包或代理主钱包
|
||||||
|
- **THEN** 系统只提供退回对应原钱包方式,不展示原路或客户收款信息退款
|
||||||
|
|
||||||
|
### Requirement: 企业微信审批和未成功重提
|
||||||
|
退款申请 SHALL 保存退款原因、冻结实收金额、唯一关联套餐及其使用情况、方式、金额和当次材料快照。超级管理员、平台用户和代理可在各自订单数据范围内创建、修改并重提未成功申请;企业微信是唯一终审,本地不得人工通过、拒绝或退回。
|
||||||
|
|
||||||
|
同一订单同时至多存在一张审批中、原路处理中或原路失败申请。企业微信驳回、申请关闭或渠道明确失败后可修改未成功申请的金额、原因、方式、收款信息和附件并重提;每次必须新建审批实例及快照。提交失败或审批结果未知保持在途,使用既有查询/恢复闭环,不得另建或重提。企业微信通过后撤销不回滚套餐失效或已启动退款,标记审批异常并禁止自动重提。
|
||||||
|
|
||||||
|
#### Scenario: 审批通过前结果未知
|
||||||
|
- **WHEN** 企业微信提交成功性或最终结果暂时未知
|
||||||
|
- **THEN** 申请保持在途且订单不得创建第二张活动申请,系统通过既有恢复机制确认结果
|
||||||
|
|
||||||
|
### Requirement: 原路退款执行与权益时点
|
||||||
|
对线上订单,系统在创建、提交及企业微信通过后的执行前均 SHALL 校验原支付单、实际收款商户、渠道流水、可退金额及商户退款能力/凭证;商户停用不得阻断历史单校验。条件不满足时禁用原路并说明原因,只允许客户收款信息退款。
|
||||||
|
|
||||||
|
企业微信最终通过即按既有规则使关联套餐失效、接续下一套餐并评估停机;客户收款信息退款同时标记退款成功,不等待线下付款。原路退款必须以冻结金额调用原实际收款商户;仅渠道明确成功后标记成功并保存渠道退款流水。超时、未知、凭证失效、余额不足或渠道拒绝时保留处理中/失败与安全原因,不标记成功或重复调用;需改客户收款信息退款时必须修改后重新审批。
|
||||||
|
|
||||||
|
#### Scenario: 原路渠道调用未知
|
||||||
|
- **WHEN** 企业微信已通过的原路退款调用超时且无法确认渠道结果
|
||||||
|
- **THEN** 退款保持原路处理中或失败恢复状态,套餐权益不恢复,系统不得再次盲目提交退款
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
## 1. 退款契约与数据
|
||||||
|
|
||||||
|
- [ ] 1.1 追踪退款、订单/支付、钱包、套餐、企微审批和商户退款调用链;新增成对迁移、模型、状态/方式常量、实收/材料/渠道结果快照及活动申请唯一约束。
|
||||||
|
- [ ] 1.2 实现退款可选方式和权威实收金额投影,创建/提交时冻结金额、套餐使用情况、原因和材料;更新 DTO/OpenAPI。
|
||||||
|
|
||||||
|
## 2. 审批、资金与恢复
|
||||||
|
|
||||||
|
- [ ] 2.1 将退款终审切换为企业微信回调/查询恢复,移除新业务的本地人工通过/拒绝/退回;实现未成功申请的新实例重提。
|
||||||
|
- [ ] 2.2 在企微通过事务内执行套餐失效/接续及原钱包退款;客户收款信息退款直接完成。
|
||||||
|
- [ ] 2.3 实现原实际商户、渠道流水和退款能力三次校验、可靠原路退款调用、幂等回写与未知/失败恢复;联动员工账单冲销和佣金回溯入口。
|
||||||
|
|
||||||
|
## 3. 验证
|
||||||
|
|
||||||
|
- [ ] 3.1 在隔离库验证每种来源方式、实收上限、活动申请互斥、审批重放/未知、渠道失败重提、商户停用历史退款和套餐权益时点。
|
||||||
|
- [ ] 3.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-refund-methods-and-original-route-refunds --strict`、`openspec doctor --json` 和 `./scripts/context-health.sh`;自动化测试按项目决策为 N/A。
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-31
|
||||||
42
openspec/changes/add-shop-salesperson-groups/design.md
Normal file
42
openspec/changes/add-shop-salesperson-groups/design.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
现有店铺已存在负责人候选和数据范围能力;用户组是新的业务分类,不能复用 RBAC 角色或代理店铺层级。推导关系必须保持实时,避免负责人改组后大量回写店铺造成不一致。
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- 新增业务用户组表和平台用户—组关联(用户唯一)表;组编码唯一且不可改,停用不删除既有成员关联。
|
||||||
|
- 店铺不保存组 ID。列表/详情以店铺负责人关联平台用户,再左连接用户组得到组及停用状态;按组筛选同样使用该关系。
|
||||||
|
- 勾选批量交接采用一次事务:先按操作者数据范围锁定/校验全量店铺及目标用户,再统一更新和写审计。任何校验失败不写入。
|
||||||
|
- Excel 使用既有异步导入模式逐行事务;每行在数据范围内查询,统一拒绝文案不区分无权与不存在,并持久化任务明细。
|
||||||
|
- 用户组成员批量设置直接替换关联;不引入组管理员、层级、额外权限或数据范围计算。
|
||||||
|
|
||||||
|
## 管理动作契约
|
||||||
|
|
||||||
|
### 用户组及成员
|
||||||
|
|
||||||
|
- `POST /business-user-groups`:超级管理员、平台用户提交 `code`(1~64 字符,未删除组内唯一)、`name`(1~100 字符)、`sort`(非负整数)、`enabled`、`remark`(最多 500 字符)。成功返回组 ID 与字段;重复编码返回“业务用户组编码已存在”。
|
||||||
|
- `PUT /business-user-groups/:id`:允许更新名称、排序、启停、备注;`code` 永不允许修改。不存在/已删除返回既有资源不存在。
|
||||||
|
- `DELETE /business-user-groups/:id`:请求须带二次确认;存在成员时返回“用户组仍有成员,只能停用或先移走成员”,不物理删除。
|
||||||
|
- `PUT /business-user-groups/:id/members`:请求 `account_ids` 非空数组;所有账号必须是启用平台用户且目标组启用。事务内替换每个账号旧组关系,任一账号无效则全量回滚。`DELETE /business-user-groups/members` 使用同一校验清空指定账号归属。成功操作写成员前后审计。
|
||||||
|
|
||||||
|
### 店铺负责人批量交接
|
||||||
|
|
||||||
|
- `PUT /shops/business-owner/batch`:请求 `shop_ids`(非空、去重)及 `business_owner_account_id`(有效平台业务员)或显式 `null`(清空)。先按操作者数据范围锁定并校验所有店铺,再统一更新 `tb_shop.business_owner_account_id` 并逐店写审计;任何目标无权、不存在、已删除或负责人无效时,返回统一失败且整批无写入。
|
||||||
|
- Excel 导入使用既有异步导入任务;每行提供店铺标识及负责人账号标识或清空标识。每行独立授权、存在性、负责人有效性校验和事务更新;结果保存行号、成功/失败、失败原因、变更前后负责人及汇总。无权和不存在对调用方使用同一错误文案。
|
||||||
|
|
||||||
|
### 读侧投影
|
||||||
|
|
||||||
|
- 扩展既有店铺列表、详情、筛选与导出:返回 `business_owner_account_id`、负责人名称、`business_user_group_id`、组编码、组名称、组启用状态;组字段从当前负责人—成员关系实时左连接。
|
||||||
|
- 组筛选只匹配当前负责人所属组;负责人为空或无成员关系时归入“未分组”。历史店铺不回填;负责人改组/停用后下一次读立即反映变化。
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- 实时 join 增加列表复杂度 → 为负责人和成员关联建立查询索引,不以冗余字段换一致性风险。
|
||||||
|
- 负责人/分组并发更新 → 店铺交接和用户改组均使用事务与受影响行检查;读取接受当前已提交快照。
|
||||||
|
- 导入部分成功 → 明确为逐行语义,任务明细是唯一结果来源。
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. 新增成对迁移创建用户组、成员关联和导入/查询索引,不回填历史组归属。
|
||||||
|
2. 部署读侧空组兼容,再启用维护、批量和导入入口。
|
||||||
|
3. 隔离库验证成员唯一、停用保留、实时推导、批量原子失败、导入逐行结果及迁移 up/down/up。
|
||||||
25
openspec/changes/add-shop-salesperson-groups/proposal.md
Normal file
25
openspec/changes/add-shop-salesperson-groups/proposal.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
店铺负责人只能逐店维护,平台用户也没有稳定的业务分类;店铺按组统计、筛选和批量交接缺少统一、可追溯口径。
|
||||||
|
|
||||||
|
本 Change 落实 AUG26-003:业务用户组只描述平台用户的业务分类,不改变角色权限、数据范围或代理店铺分组;店铺所属组始终由当前负责人实时推导。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增业务用户组(名称、不可变唯一编码、排序、启停、备注)及平台用户单组归属。
|
||||||
|
- 新增店铺负责人批量设置/清空和 Excel 导入;勾选操作全量校验且原子,导入逐行独立执行并返回明细。
|
||||||
|
- 店铺列表、详情和筛选显示实时推导的负责人业务用户组;停用组保留成员和展示,不影响登录、权限、数据范围或负责人。
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `business-user-group`: 用户组生命周期、成员归属、店铺负责人交接及推导查询。
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- 无。现有身份权限和数据范围拒绝行为保持不变。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
影响平台用户、店铺列表/详情、导入任务、数据范围校验、审计、DTO/OpenAPI 和新增 Schema。
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
## Purpose
|
||||||
|
|
||||||
|
以不改变既有角色和数据范围的方式标记平台用户业务分类,并将店铺负责人和业务用户组的批量维护、推导展示与审计定义为一致的可观察行为。
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 业务用户组生命周期与成员归属
|
||||||
|
系统 SHALL 允许超级管理员和平台用户维护业务用户组的名称、创建时必填且在未删除组内唯一的稳定编码、排序、启用状态和备注;编码创建后 MUST NOT 修改。用户组不得设置上级、层级或组管理员。仅无成员用户组可由超级管理员或平台用户二次确认删除;有成员时只能停用或先移走成员。
|
||||||
|
|
||||||
|
每个启用平台用户最多属于一个启用业务用户组。超级管理员和平台用户可选择多个平台用户,批量设置至一个启用组或批量清空归属;设置直接替换原归属,停用组不得作为目标。业务用户组 MUST NOT 改变后台角色、登录、权限、数据范围或店铺具体负责人归属。停用后不得新增成员,已有成员关系保留并显示已停用,管理员仍可将成员改组或清空。
|
||||||
|
|
||||||
|
#### Scenario: 批量替换平台用户分组
|
||||||
|
- **WHEN** 管理员选择多个启用平台用户并指定一个启用业务用户组
|
||||||
|
- **THEN** 系统将每个目标用户的原分组直接替换为目标组,不改变其角色、数据范围和登录状态
|
||||||
|
|
||||||
|
#### Scenario: 停用含成员用户组
|
||||||
|
- **WHEN** 管理员停用仍含平台用户成员的业务用户组
|
||||||
|
- **THEN** 系统保留成员关系并标记组已停用,拒绝新增成员但允许后续改组或清空成员
|
||||||
|
|
||||||
|
### Requirement: 店铺负责人和所属组实时推导
|
||||||
|
店铺 SHALL 以当前绑定的平台业务员作为负责人。店铺所属业务用户组 MUST 实时由该负责人的当前用户组推导,不得把组 ID 冗余写入店铺;负责人变更、负责人改组或组停用后,店铺列表、详情和筛选的结果立即按新关系变化。店铺无负责人、负责人无分组时所属组为空;负责人所属组停用时仍返回该组并明确其已停用。
|
||||||
|
|
||||||
|
#### Scenario: 负责人改组改变店铺展示
|
||||||
|
- **WHEN** 某平台业务员的业务用户组被替换或清空
|
||||||
|
- **THEN** 该业务员当前负责的所有店铺在列表、详情和按组筛选中即时呈现新的组或空组,无需更新店铺记录
|
||||||
|
|
||||||
|
### Requirement: 店铺负责人批量交接与导入
|
||||||
|
超级管理员和平台用户 SHALL 仅在其店铺数据权限内勾选多家店铺,批量设置为一个有效平台业务员或批量清空负责人。勾选批量操作 MUST 在提交前校验全部目标店铺均存在、未删除且可管理,并校验目标业务员有效;任一项失败时整批不修改并返回统一失败结果。成功时必须为每家店铺记录负责人前后值、操作者、时间和入口审计。
|
||||||
|
|
||||||
|
Excel 导入 MUST 按每行店铺标识独立校验和执行:有效行成功更新,无权限、店铺不存在/已删除或目标业务员无效行失败;任务返回成功数、失败数和逐行失败原因。导入不得因一行失败回滚其他已成功行,并必须记录每行实际变更审计。
|
||||||
|
|
||||||
|
#### Scenario: 勾选批量包含越权店铺
|
||||||
|
- **WHEN** 管理员提交的店铺集合中任一店铺不在其数据范围、已删除或不存在
|
||||||
|
- **THEN** 系统不修改集合中任何店铺负责人,并返回统一失败结果且不泄露越权店铺存在性
|
||||||
|
|
||||||
|
#### Scenario: 导入包含有效和无效行
|
||||||
|
- **WHEN** 店铺负责人 Excel 导入同时包含可管理店铺和无权或无效店铺
|
||||||
|
- **THEN** 系统更新每个有效行、保留失败行原值,并返回逐行结果及成功/失败汇总
|
||||||
17
openspec/changes/add-shop-salesperson-groups/tasks.md
Normal file
17
openspec/changes/add-shop-salesperson-groups/tasks.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
## 1. 用户组与查询
|
||||||
|
|
||||||
|
- [ ] 1.1 追踪平台用户、店铺负责人、数据范围、现有批量导入和审计调用链,确认负责人字段及“有效平台业务员”的既有判定。
|
||||||
|
- [ ] 1.2 新增成对迁移、模型和常量:业务用户组、平台用户唯一组关联、编码唯一/启停约束及负责人—成员推导查询索引。
|
||||||
|
- [ ] 1.3 实现用户组 CRUD、启停、无成员二次确认删除、平台用户批量设置/清空及审计;禁止修改编码、向停用组新增成员和改变既有 RBAC。
|
||||||
|
- [ ] 1.4 扩展店铺列表、详情与筛选 Query/DTO/OpenAPI,实时返回负责人组、编码和停用标识,不将组写入店铺。
|
||||||
|
|
||||||
|
## 2. 店铺负责人批量维护
|
||||||
|
|
||||||
|
- [ ] 2.1 实现勾选店铺批量设置/清空:先以操作者数据范围校验所有店铺和目标业务员,在单事务中更新全部店铺并记录逐店审计;任一项失败整批不改。
|
||||||
|
- [ ] 2.2 接入既有异步 Excel 导入任务,逐行校验店铺标识、数据范围和目标业务员,保存成功/失败明细、汇总和实际审计;越权使用统一失败文案。
|
||||||
|
- [ ] 2.3 注册后台路由和权限,更新 `cmd/api/docs.go` 与 `cmd/gendocs/main.go`;Handler 使用全局错误处理和 `pkg/response`。
|
||||||
|
|
||||||
|
## 3. 验证
|
||||||
|
|
||||||
|
- [ ] 3.1 在隔离数据库验证迁移 up/down/up、编码/成员唯一、组停用、负责人改组实时展示、批量原子失败、导入混合结果和数据范围拒绝。
|
||||||
|
- [ ] 3.2 运行 `gofmt -w`(变更 Go 文件)、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-shop-salesperson-groups --strict`、`openspec doctor --json` 和 `./scripts/context-health.sh`;自动化测试按项目决策为 N/A。
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-09-01
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user