From 754fcadaf413af1764ecc1145d39419bbbb18dae Mon Sep 17 00:00:00 2001 From: Willie Date: Mon, 25 May 2026 22:57:50 +0800 Subject: [PATCH] vault backup: 2026-05-25 22:57:50 --- .obsidian/workspace.json | 27 ++-- .../prepaid/prepaid-account-vs-transaction.md | 148 ++++++++++++++++++ 2 files changed, 162 insertions(+), 13 deletions(-) create mode 100644 prop-acc/concepts/prepaid/prepaid-account-vs-transaction.md diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json index c6347d5..5fe0bad 100644 --- a/.obsidian/workspace.json +++ b/.obsidian/workspace.json @@ -13,12 +13,12 @@ "state": { "type": "markdown", "state": { - "file": "prop-acc/index.md", + "file": "prop-acc/scenarios/deposit/deposit-additional-topup.md", "mode": "source", "source": false }, "icon": "lucide-file", - "title": "index" + "title": "deposit-additional-topup" } } ] @@ -94,7 +94,7 @@ "state": { "type": "backlink", "state": { - "file": "prop-acc/index.md", + "file": "prop-acc/scenarios/deposit/deposit-additional-topup.md", "collapseAll": false, "extraContext": false, "sortOrder": "alphabetical", @@ -104,7 +104,7 @@ "unlinkedCollapsed": true }, "icon": "links-coming-in", - "title": "Backlinks for index" + "title": "Backlinks for deposit-additional-topup" } }, { @@ -113,12 +113,12 @@ "state": { "type": "outgoing-link", "state": { - "file": "prop-acc/index.md", + "file": "prop-acc/scenarios/deposit/deposit-additional-topup.md", "linksCollapsed": false, "unlinkedCollapsed": true }, "icon": "links-going-out", - "title": "Outgoing links from index" + "title": "Outgoing links from deposit-additional-topup" } }, { @@ -156,13 +156,13 @@ "state": { "type": "outline", "state": { - "file": "prop-acc/index.md", + "file": "prop-acc/scenarios/deposit/deposit-additional-topup.md", "followCursor": false, "showSearch": false, "searchQuery": "" }, "icon": "lucide-list", - "title": "Outline of index" + "title": "Outline of deposit-additional-topup" } }, { @@ -180,7 +180,8 @@ } ], "direction": "horizontal", - "width": 300 + "width": 300, + "collapsed": true }, "left-ribbon": { "hiddenItems": { @@ -196,9 +197,11 @@ }, "active": "b06ed69835363258", "lastOpenFiles": [ - "prop-acc/concepts/deposit/deposit-vs-adhoc-vs-prepaid.md", + "prop-acc/concepts/prepaid/prepaid-account-vs-transaction.md", + "prop-acc/concepts/prepaid", "prop-acc/maps/deposit-knowledge-map.md", "prop-acc/index.md", + "prop-acc/concepts/deposit/deposit-vs-adhoc-vs-prepaid.md", "prop-acc/scenarios/deposit/audit-long-pending-accounts.md", "prop-acc/scenarios/deposit/audit-monthly-deposit-balance.md", "prop-acc/scenarios/deposit/exception-deposit-on-frozen.md", @@ -222,7 +225,6 @@ "prop-acc/concepts/deposit/red-receipt-design.md", "prop-acc/concepts/deposit/transaction-types.md", "prop-acc/concepts/deposit/payer-types.md", - "prop-acc/concepts/deposit/account-state-machine.md", "prop-acc/concepts/deposit", "prop-acc/scenarios/adhoc", "prop-acc/concepts/adhoc", @@ -230,7 +232,6 @@ "resident-portal/reference", "resident-portal/procedures", "resident-portal/maps", - "resident-portal/glossary", - "resident-portal/features" + "resident-portal/glossary" ] } \ No newline at end of file diff --git a/prop-acc/concepts/prepaid/prepaid-account-vs-transaction.md b/prop-acc/concepts/prepaid/prepaid-account-vs-transaction.md new file mode 100644 index 0000000..3260746 --- /dev/null +++ b/prop-acc/concepts/prepaid/prepaid-account-vs-transaction.md @@ -0,0 +1,148 @@ +--- +title: prop-acc · prepaid · 预存款账户与流水 +aliases: + - 预存款账户与流水 + - PrepaidAccount 与 PrepaidTransaction + - 预存款的双对象模式 +tags: + - 概念 + - prop-acc + - 预存款 + - 核心概念 +audience: + - 业户 + - 业务人员 +status: 已发布 +sub_feature: prepaid +last_review: 2026-05-25 +code_version: 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_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 必须等于流水按时间累加的净值**。 + +```php +$account->verifyBalance(); // bool +$account->getBalanceDifference(); // float +$account->calculateBalanceFromTransactions(); +``` + +`canOperate()` 守护所有写入(deposit / consume / refund 都调): + +```php +public function canOperate(): bool +{ + return $this->status === PrepaidAccountStatus::Active; +} +``` + +Frozen / Closed 都不允许操作。这条由模型层兜底,即使 Action 类、Filament UI 全部绕过,模型方法 `deposit()` / `consume()` / `refund()` 内置 `canOperate()` 检查,任何调用方都跑不掉(详见 [[exception-refund-on-frozen]])。 + +## 资金流概览 + +```mermaid +flowchart LR + A[业户充值 5000] -->|deposit| B[PrepaidAccount
balance=5000] + B -->|consume 抵账单 800| C[balance=4200
Bill 状态翻 Paid] + C -->|consume 抵账单 1200| D[balance=3000] + D -->|refund 退余 3000| E[balance=0
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、模型方法三层都有 + +## 相关文档 + +- [[account-state-machine]] +- [[one-account-per-resident]] +- [[transaction-types]] +- [[consume-via-bill-collection-type]] +- [[deposit-vs-adhoc-vs-prepaid]] +- [[deposit-account-vs-transaction]]