Files
uniprop-manual/prop-acc/concepts/prepaid/prepaid-account-vs-transaction.md
2026-05-25 22:57:50 +08:00

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 · 预存款账户与流水
预存款账户与流水
PrepaidAccount 与 PrepaidTransaction
预存款的双对象模式
概念
prop-acc
预存款
核心概念
业户
业务人员
已发布 prepaid 2026-05-25 2026-05-22

预存款账户与流水

预存款模块底层是账户(PrepaidAccount)+ 流水(PrepaidTransaction)的双对象模式 —— 与 deposit-account-vs-transaction同构。但预存款多了两条独有的约束:

  1. 一户一账(每社区每业户最多一个预存款账户)
  2. 零余额不自动关账(账户随时可继续充值,不像押金清零就 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_idasset_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、模型方法三层都有

相关文档