Files
uniprop-manual/prop-acc/concepts/prepaid/account-state-machine.md
2026-05-25 23:02:51 +08:00

5.3 KiB

title, aliases, tags, audience, status, sub_feature, last_review, code_version
title aliases tags audience status sub_feature last_review code_version
prop-acc · prepaid · 预存款账户状态机
预存款账户状态机
PrepaidAccount 状态机
canOperate
概念
prop-acc
预存款
状态机
业户
业务人员
已发布 prepaid 2026-05-25 2026-05-22

预存款账户状态机

预存款账户三种状态:Active(可用) / Frozen(冻结) / Closed(已关闭)。状态机骨架与 ../deposit/account-state-machine相同,但有两条重要差异:

  1. 零余额不自动关账 —— 业户可以继续充值复用账户
  2. 没有 ForceClose —— 一户一账 + 预存款纠纷罕见,不需要 deposit 那种 Frozen + 有余额的解困出口

三状态速查

状态 中文 何时进入 能做什么
Active 可用 新账户首次充值后 充值 / 消费 / 退款 / 冻结 / 关账(主动)
Frozen 冻结 风控、欺诈嫌疑、内审等 看流水(只读)/ 解冻
Closed 已关闭 业户搬走 / 主动关账 / 长期失联归档 看流水(只读)

状态机图

stateDiagram-v2
  [*] --> Active : 开户 + 首次充值
  Active --> Active : 充值 / 消费 / 退款(任意次数,余额可清零回升)
  Active --> Frozen : freeze() 风控
  Frozen --> Active : reactivate() 等同解冻
  Active --> Closed : close() 业户搬走等
  Closed --> [*]

  note right of Active
    余额 0 时不自动关账
    业户可继续充值复用
  end note

  note right of Frozen
    冻结期间禁止任何资金动作
    canOperate = false
  end note

  note left of Closed
    永久终态
    新业务请开新账户
    (开新账户会撞一户一账约束 —— 业务上需要重新审视)
  end note

守护方法

方法 返回 true 的状态 用途
canOperate() Active only 统一守护:deposit / consume / refund 都调
hasBalance() balance > 0 配合判断"账户有钱"语义
isClosed() status == Closed 给 UI / Policy 判断用

[!info] canOperate 是模型层的最严防御 与 deposit 的 canDeposit / canWithdraw 二分不同,prepaid 用统一 canOperate() —— 因为预存款的所有资金动作(充值 / 消费 / 退款)在 Frozen 状态下都要拒绝,没有"只能加不能减"的中间逻辑。

这条规则的实施有三层兜底(同 deposit):

  • UI 层:Filament Action 的 visiblecanOperate()
  • Policy 层:PrepaidAccountPolicy::deposit / consume / refund 各自检查状态
  • 模型层(最严):PrepaidAccount::deposit() / consume() / refund() 内置 canOperate() 检查

即使 Filament / Policy 全部绕过(tinker、artisan、第三方调用),模型方法仍会抛错。详见 exception-refund-on-frozen "三层守护" 段。

与 deposit 状态机的关键差异

差异 1:零余额不自动关账

deposit:

balance > 0  ─ refund/forfeit ──→ balance == 0 ──自动──→ Closed

prepaid:

balance > 0  ─ consume/refund ──→ balance == 0  ──不自动关──→ Active(可继续充值)

为什么 prepaid 不自动关?

理由 解释
一户一账 关了再开会撞 unique 约束;关掉是浪费
业户长期使用 预存款是"我以后用"的账户,余额清零只意味着这一轮用完,可以继续充
业务高频 业户可能下周又充值,频繁开关账户毫无意义
Bill 关联 关账后再有未付账单,业户充值无处去

deposit 自动关是因为:

理由 解释
业务完结 押金交完 → 装修完 → 退或扣 → 业务结束
多账户合理 业户可以同时有装修押金、入驻押金、设备押金多个账户

差异 2:没有 ForceClose

../deposit/account-state-machine有 ForceClose 解决"Frozen + 有余额 + 想关账"的困境。预存款没有这个机制,因为:

ReactivateAccountAction = 解冻

历史代码里有个 ReactivateAccountAction,字面意思"重新激活"。实际行为只允许 Frozen → Active(等同解冻),不允许 Closed → Active(那条永久禁止,同 deposit)。

UI 文案已统一为"解冻"(图标 lock-open),与 deposit 模块对齐。

[!warning] 不要被名字误导 "Reactivate" 字面给人"撤销关账"的暗示,实际做不到。Policy 守护 + UI visible 都只在 status === Frozen 时显示。

业务人员视角

后台账户列表的"状态"列对应三个值:

  • 看到 Active:绿色,所有操作可用
  • 看到 Frozen:橙色,所有写入按钮变灰,只剩 ReactivateAccountAction
  • 看到 Closed:灰色,完全只读

业户视角

业户感受不到状态机内部,只感受到:

  • 余额能正常用 → Active
  • 充值 / 抵扣失败,提示"账户冻结中" → Frozen
  • 账户已关 → 无法再充值 / 抵扣,看流水可

相关文档