5.2 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 · 预存款账户与流水 |
|
|
|
已发布 | prepaid | 2026-05-25 | 2026-05-22 |
预存款账户与流水
预存款模块底层是账户(PrepaidAccount)+ 流水(PrepaidTransaction)的双对象模式 —— 与 deposit-account-vs-transaction同构。但预存款多了两条独有的约束:
- 一户一账(每社区每业户最多一个预存款账户)
- 零余额不自动关账(账户随时可继续充值,不像押金清零就 Closed)
详细差异见 deposit-vs-adhoc-vs-prepaid。
为什么也用双对象
[!info] 类比:超市充值卡
- 账户 = 卡面上的"当前余额 ¥1,500"
- 流水 = 每次充值 + 每次消费的明细列表
业户充一笔(deposit)、消费抵账单(consume)、退余额(refund)—— 每一笔都需要可追溯。账户只记当前余额和状态,流水记每一笔变动。
字段速查
PrepaidAccount(账户)
| 字段 | 含义 |
|---|---|
id |
账户 ID |
community_id |
所属物业项目 |
community_user_profile_id |
业户档案 ID(必填,与 community_id 组合唯一) |
balance |
当前余额 |
status |
账户状态(详见 account-state-machine) |
opened_at |
开户时间 |
meta |
JSON 扩展字段 |
[!warning] 一户一账约束
(community_id, community_user_profile_id)数据库唯一约束,保证同业户在同社区不会有两个预存款账户。开户时违反这条会抛 unique violation。详见 one-account-per-resident。
PrepaidTransaction(流水)
| 字段 | 含义 |
|---|---|
id |
流水 ID |
prepaid_account_id |
归属账户 |
type |
流水类型(详见 transaction-types) |
amount |
本笔金额(正数;refund 也是正数,方向由 type 表达) |
balance_before |
本笔之前余额 |
balance_after |
本笔之后余额 |
related_collection_order_id |
关联收款单(deposit / consume / refund 都关联) |
related_bill_id |
(consume 独有)关联被抵扣的账单 |
memo |
备注 |
operated_by |
操作员 ID |
| 创建后不可变 | 一旦生成只读 |
与 deposit 的关键差异
| 维度 | DepositAccount | PrepaidAccount |
|---|---|---|
| 缴款人 | 多种(payer_type:业主/租户/装修公司/...) |
只能是业户本人 |
| 每业户账户数 | 多个(按 fee_type、按 asset) | 1 个(每社区每业户) |
| 关联对象 | fee_type_id、asset_id 可选 |
只关联 community_user_profile_id |
| 核心写入操作 | deposit / refund / forfeiture(3 种) | deposit / consume / refund / adjustment(4 种,consume 是高频) |
| Bill 关联 | ❌ | ✅(consume 时通过 related_bill_id) |
| 零余额行为 | 自动关账(canBeClosed() 触发) |
不关,可继续充值 |
| ForceClose | ✅(Frozen + 有余额 → 关账) | ❌(一户一账纠纷罕见,不需要) |
两者的契约
与 deposit 完全一致:账户.balance 必须等于流水按时间累加的净值。
$account->verifyBalance(); // bool
$account->getBalanceDifference(); // float
$account->calculateBalanceFromTransactions();
canOperate() 守护所有写入(deposit / consume / refund 都调):
public function canOperate(): bool
{
return $this->status === PrepaidAccountStatus::Active;
}
Frozen / Closed 都不允许操作。这条由模型层兜底,即使 Action 类、Filament UI 全部绕过,模型方法 deposit() / consume() / refund() 内置 canOperate() 检查,任何调用方都跑不掉(详见 exception-refund-on-frozen)。
资金流概览
flowchart LR
A[业户充值 5000] -->|deposit| B[PrepaidAccount<br/>balance=5000]
B -->|consume 抵账单 800| C[balance=4200<br/>Bill 状态翻 Paid]
C -->|consume 抵账单 1200| D[balance=3000]
D -->|refund 退余 3000| E[balance=0<br/>Active 仍可继续充值]
注意最后一步:余额清零账户保持 Active,不像 deposit 那样自动 Closed。理由见 account-state-machine "零余额不自动关账" 段。
业户视角
业户在小程序"我的预存款"看到:
账户余额:¥3,000.00 [+ 充值]
最近流水:
2026-05-15 -800.00 抵扣 物业费(5月)
2026-05-15 -1200.00 抵扣 水电费(5月)
2026-05-01 +5000.00 预存款充值
充值随时可做,余额可看,消费明细可追。
业务人员视角
后台 → 预存款 → 账户列表 → 找到业户的账户 → ViewPrepaidAccount。右侧"流水"标签是 PrepaidTransaction 列表(只读、按时间倒序)。
所有写入 Action(DepositAction / ConsumeAction / RefundAction):
- 同时写账户余额 + 流水 + CollectionOrder + Receipt(事务内)
- 任何只写一边的代码都是 bug
- 状态守护(
canOperate())在 Filament Action、Policy、模型方法三层都有