Files
junhong_cmp_fiber/.scratch/ur97-wallet-low-balance-alert/PRD.md

107 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 业务员关系;依赖尚未完成时可以先完成领域事件,但不得另建临时通知表或直接发消息。
- 存量低余额钱包不批量补发,避免上线瞬间产生大量无业务变更通知;上线后只监听新提交的跨阈值事务。
## 公共能力发布依赖
- 最终发布票必须明确阻塞于公共基础 12 号票、全局审计 19 号票、公共通知 08 号票和 UR#38 钱包切换 10 号票。
- 低余额事件必须登记载荷版本、消费者幂等键、通知接收人和防重策略;钱包 Domain Ledger 是余额权威,通知事实不替代资金审计。
- 当前 PRD 尚未拆票;生成 tickets 时必须把上述依赖落到最终发布门禁,不得只写“依赖公共基础”。
## 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` 为准。