更新一下

This commit is contained in:
2026-07-21 15:26:07 +09:00
parent 2823ff13bf
commit 4902a02c87
32 changed files with 4138 additions and 294 deletions

View File

@@ -0,0 +1,100 @@
# PRDUR#97 代理主钱包固定 100 元低余额预警
Status: ready-for-agent
---
## Problem Statement
代理主钱包现金余额不足会导致后续购包失败,但当前资金概况只返回总余额和冻结余额,没有统一的现金可用余额或低余额提示,也没有跨越阈值时的可靠通知。钱包余额可由扣款、冻结、解冻、充值和退款回充等多个入口改变;若只在某个 Service 增加通知,会漏掉其他资金路径,并在持续低余额时反复刷屏。
七月迭代还会加入信用额度,但预警表达的是代理实际现金不足,不能因信用可用额度而隐藏现金风险。
## Solution
在统一代理主钱包聚合的每次余额或冻结金额变更中,以 `cash_available = balance - frozen_balance` 比较事务前后快照。只有从 `>10000` 分跨越到 `<=10000` 分时产生一次低余额领域事件;持续低位不再产生,回升到阈值以上后下一次再次跌破会自然形成新事件。
事件与钱包事务同事务进入 Outbox公共 Notification Worker 向当前启用的店铺主账号和可用店铺业务员分别发送一条站内通知。现有资金概况接口增加服务端计算的现金可用余额和预警布尔值,前端不自行拼阈值规则。
## User Stories
1. 作为代理主账号,我希望主钱包现金可用余额首次降至 100 元及以下时收到一次站内提醒。
2. 作为店铺的平台业务员,我希望我负责的店铺发生低余额时收到同一提醒。
3. 作为代理用户,我希望持续低余额的多次扣款不会不断收到重复通知。
4. 作为代理用户,我希望充值恢复到 100 元以上后,再次跌破还能收到新一轮提醒。
5. 作为运营人员,我希望资金概况明确展示现金可用余额和低余额状态,且不把信用额度算进来。
6. 作为财务维护人员,我希望通知系统失败不回滚正确的钱包资金事务,且能从事件和审计追踪本次跨阈值。
## Implementation Decisions
### 精确业务规则
- 固定常量 `AgentWalletLowBalanceThresholdCents int64 = 10000` 放入 `pkg/constants/` 并添加中文注释;本期不提供系统、角色或店铺级阈值配置。
- 只检查 `wallet_type=main` 的代理主钱包。分佣钱包、资产钱包、个人客户钱包均不参与。
- `cash_available = balance - frozen_balance`,单位为分;信用额度、可用授信和佣金余额全部不参与预警计算。
- 触发条件严格为 `before_cash_available > 10000 && after_cash_available <= 10000`。等于 100 元属于预警范围;从 100 元以下继续扣款不重复触发。
- 充值、退款回充或解冻使现金可用余额回到 `>10000` 后,无需单独发送“余额恢复”通知;下一次由 `>10000` 降到 `<=10000` 会产生新的预警轮次。
- 新建时余额为 0 的钱包不补发低余额通知,因为没有发生从安全区跌破阈值的事件;未来首次回升并再次跌破时正常通知。
- 冻结可能降低可用现金并触发,解冻可能重新布防;从冻结余额正式扣除时若 `balance``frozen_balance` 同额减少、现金可用值不变,则不触发。
### 钱包领域与并发
- 本需求复用七月统一 `AgentWallet` 聚合和资金事务边界,不在 `AgentWalletStore` 的通用 SQL 更新方法中偷偷发送通知,也不在订单、充值、退款等 Service 各写一份判断。
- 所有主钱包余额/冻结金额变更必须加载并锁定同一钱包快照,由领域方法计算变更前后 `cash_available`、校验资金不变量、增加 `version` 并记录跨阈值事件。
- 既有扣款、冻结、解冻、充值、退款回充及其他触碰主钱包的完整用例都必须接入统一变更边界;若仍有入口直接执行 `balance +/-``frozen_balance +/-`,不得把 UR#97 标记完成。
- 不需要单独持久化 `low_balance_alert_active`:前后现金可用值已经完整表达跌破、持续低位和回升后再跌破。并发由行锁/乐观锁和钱包 `version` 保证只提交一条实际跨越。
- 低余额事件至少快照 `event_id`、钱包 ID、店铺 ID、变更类型/业务引用、余额和冻结金额前后值、现金可用前后值、钱包提交后版本、`request_id/correlation_id`
- 稳定事件 ID 使用本次已提交的钱包业务变更/版本生成;同一 Outbox 事件重放 ID 不变。不得在每次 Worker 重试时生成新 ID。
- 钱包状态、流水、Audit Event 和 Outbox 在同一数据库事务中写入Outbox 写失败回滚资金事务。后续通知写入失败不回滚资金,由 Relay/Worker 重试。
### 接收人和防重
- 接收人为该店铺当前 `status=1``is_primary=true` 的代理主账号,以及 `business_owner_account_id` 指向且当前仍启用的平台业务员。
- 软删除、禁用或关系已解除的账号在消费时跳过;主账号与业务员 ID 去重后逐人发送。没有可用接收人时记录 `no_recipient`,不无限重试。
- 使用公共通知类型 `wallet.low_balance`,类别为 `system` 或公共注册表确定的资金告警类别,级别为 `warning`。正文快照包含店铺名称和脱敏业务信息,可展示现金可用金额,不包含信用额度。
- 公共通知唯一键 `event_id + recipient_kind + recipient_id` 防止 Outbox/Asynq 重放;每次真正再次跌破产生不同事件 ID因此可再次通知。
- 受控目标使用店铺资金概况类型和店铺 ID不保存任意 URL目标解析后仍按当前账号的店铺数据权限检查。
### API 与前端
- 复用现有 `GET /api/admin/shops/fund-summary`,不另建单店铺的同名新接口。其每个 `ShopFundSummaryItem` 在保留 `main_balance``main_frozen_balance` 等兼容字段的基础上新增:
- `cash_available:int64``main_balance - main_frozen_balance`
- `low_balance_warning:bool`:主钱包存在且 `cash_available <= 10000`
- 店铺没有主钱包的异常数据不可伪装成“0 元正常钱包”;返回 `cash_available=0``low_balance_warning=true` 的同时记录数据完整性告警,或按统一 Query 错误策略失败,实施时必须保持同一接口所有页面一致。
- Query 继续应用现有店铺层级数据权限和服务端分页,批量加载主钱包,禁止逐店铺 N+1信用额度字段即使同时存在也不能影响这两个新字段。
- 资金概况在 `low_balance_warning=true` 时显示红色“现金余额不足 100 元”状态,并展示现金余额与冻结金额;金额均按分转元,前端不得自行用信用额度或阈值重新计算布尔值。
- 通知中心和顶部抽屉复用公共通知 API点击后受控跳转到对应店铺资金概况。目标已无权限时按统一不可用语义处理。
- 不展示阈值输入框、余额恢复消息开关或企业微信通知选项。
### 审计与发布
- 钱包跨阈值时的 Audit Event 记录钱包/店铺、业务来源、余额/冻结/现金可用前后快照、领域事件 ID 和接收人解析结果;信用额度不得出现在预警判断依据字段中。
- 普通资金变化继续以钱包流水为领域事实,低余额 Audit Event/通知只表达告警,不替代流水。
- 发布依赖统一钱包变更边界、公共 Outbox、公共站内通知和 UR#96 业务员关系;依赖尚未完成时可以先完成领域事件,但不得另建临时通知表或直接发消息。
- 存量低余额钱包不批量补发,避免上线瞬间产生大量无业务变更通知;上线后只监听新提交的跨阈值事务。
## Testing Decisions
- 领域单元测试覆盖 `10100→10000``10001→10000``10000→9999``9000→8000``9000→10100→10000`,验证只在严格跨越时发事件。
- 覆盖扣款、冻结、解冻、充值、退款回充和冻结余额正式扣除;验证冻结可触发、解冻可重新布防、总额与冻结额同减导致现金不变时不触发。
- 并发集成测试让两个事务同时尝试把同一钱包从阈值上方扣到下方,验证乐观锁/行锁后只有实际提交的跨越产生一个事件、流水和 Outbox。
- 对所有主钱包写入口做回归测试或静态清单检查,证明没有绕开统一聚合直接更新余额/冻结金额的生产路径。
- Notification Worker 测试覆盖主账号+业务员两接收人、同一账号去重、停用/删除跳过、无接收人、重复事件投递和再次跌破的新事件。
- HTTP 集成测试使用开发 PostgreSQL/Redis穿过真实认证、权限、Query 和统一响应,验证平台、不同层级代理看到的资金行范围不变,新字段在大于、等于、小于 100 元时正确。
- 验证信用额度为 0、正数以及已使用产生负现金余额时`cash_available/low_balance_warning` 与通知规则均只看现金余额和冻结金额。
- 前端验收覆盖红色提示、分转元、加载/空/失败状态、通知已读和受控跳转;禁止对真实钱包执行测试扣款或发送真实通知。
## Out of Scope
- 不支持自定义阈值、分级阈值、按角色/店铺配置或余额恢复通知。
- 不把信用额度、佣金、资产钱包或个人钱包纳入预警。
- 不发送企业微信、短信、邮件或其他外部渠道消息。
- 不补发上线前已经低于阈值的存量钱包通知。
- 不在本需求实现公共站内通知中心或公共 Outbox 的另一份副本。
- 不通过前端定时查询余额来推断和发送预警。
## Further Notes
- 当前仓库已有主钱包 `balance/frozen_balance/version` 和资金概况接口,但加减余额仍分散在多个 Service/Store完整接入统一钱包写边界是本需求可靠性的核心不是附带重构。
- 现有资金概况字段名是 `main_balance/main_frozen_balance`,禅道稿中笼统写的 `balance/frozen_balance` 不能覆盖真实兼容契约。
- “不足 100 元”在本需求产品文案中包含恰好 100 元,后端判断以 `<=10000` 为准。