From f4075697710450649d6cbb49cdc6588145a8d693 Mon Sep 17 00:00:00 2001 From: Willie Date: Mon, 25 May 2026 23:22:55 +0800 Subject: [PATCH] vault backup: 2026-05-25 23:22:55 --- .obsidian/workspace.json | 32 +-- .../prepaid/consume-monthly-property-bill.md | 195 ++++++++++++++++++ .../prepaid/deposit-additional-topup.md | 163 +++++++++++++++ .../scenarios/prepaid/deposit-first-time.md | 187 +++++++++++++++++ .../prepaid/deposit-via-miniapp-pending.md | 180 ++++++++++++++++ 5 files changed, 741 insertions(+), 16 deletions(-) create mode 100644 prop-acc/scenarios/prepaid/consume-monthly-property-bill.md create mode 100644 prop-acc/scenarios/prepaid/deposit-additional-topup.md create mode 100644 prop-acc/scenarios/prepaid/deposit-first-time.md create mode 100644 prop-acc/scenarios/prepaid/deposit-via-miniapp-pending.md diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json index bf55b9f..1141f14 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/prepaid/deposit-first-time.md", "mode": "source", "source": false }, "icon": "lucide-file", - "title": "index" + "title": "deposit-first-time" } } ] @@ -94,7 +94,7 @@ "state": { "type": "backlink", "state": { - "file": "prop-acc/index.md", + "file": "prop-acc/scenarios/prepaid/deposit-first-time.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-first-time" } }, { @@ -113,12 +113,12 @@ "state": { "type": "outgoing-link", "state": { - "file": "prop-acc/index.md", + "file": "prop-acc/scenarios/prepaid/deposit-first-time.md", "linksCollapsed": false, "unlinkedCollapsed": true }, "icon": "links-going-out", - "title": "Outgoing links from index" + "title": "Outgoing links from deposit-first-time" } }, { @@ -156,13 +156,13 @@ "state": { "type": "outline", "state": { - "file": "prop-acc/index.md", + "file": "prop-acc/scenarios/prepaid/deposit-first-time.md", "followCursor": false, "showSearch": false, "searchQuery": "" }, "icon": "lucide-list", - "title": "Outline of index" + "title": "Outline of deposit-first-time" } }, { @@ -197,16 +197,21 @@ }, "active": "b06ed69835363258", "lastOpenFiles": [ + "prop-acc/scenarios/prepaid/consume-monthly-property-bill.md", + "prop-acc/scenarios/prepaid/deposit-via-miniapp-pending.md", + "prop-acc/scenarios/prepaid/deposit-additional-topup.md", + "prop-acc/scenarios/prepaid/deposit-first-time.md", + "prop-acc/concepts/prepaid/account-state-machine.md", + "prop-acc/scenarios/prepaid", + "prop-acc/index.md", "prop-acc/scenarios/deposit/deposit-first-time-renovation.md", "prop-acc/scenarios/deposit/deposit-additional-topup.md", "prop-acc/maps/prepaid-knowledge-map.md", "prop-acc/concepts/prepaid/auto-deduction-design.md", "prop-acc/maps/deposit-knowledge-map.md", - "prop-acc/index.md", "prop-acc/concepts/prepaid/consume-via-bill-collection-type.md", "prop-acc/concepts/prepaid/transaction-types.md", "prop-acc/concepts/prepaid/one-account-per-resident.md", - "prop-acc/concepts/prepaid/account-state-machine.md", "prop-acc/concepts/prepaid/prepaid-account-vs-transaction.md", "prop-acc/concepts/prepaid", "prop-acc/concepts/deposit/deposit-vs-adhoc-vs-prepaid.md", @@ -220,10 +225,6 @@ "prop-acc/scenarios/deposit/close-after-zero-balance.md", "prop-acc/scenarios/deposit/unfreeze-after-mediation.md", "prop-acc/scenarios/deposit/freeze-during-dispute.md", - "prop-acc/scenarios/deposit/forfeit-violation-no-permit.md", - "prop-acc/scenarios/deposit/forfeit-damage-public-area.md", - "prop-acc/scenarios/deposit/refund-with-payment-channel-switch.md", - "prop-acc/scenarios/deposit/refund-partial-after-forfeit.md", "prop-acc/scenarios/deposit", "prop-acc/concepts/deposit", "prop-acc/scenarios/adhoc", @@ -231,7 +232,6 @@ "resident-portal/scenarios", "resident-portal/reference", "resident-portal/procedures", - "resident-portal/maps", - "resident-portal/glossary" + "resident-portal/maps" ] } \ No newline at end of file diff --git a/prop-acc/scenarios/prepaid/consume-monthly-property-bill.md b/prop-acc/scenarios/prepaid/consume-monthly-property-bill.md new file mode 100644 index 0000000..30070d4 --- /dev/null +++ b/prop-acc/scenarios/prepaid/consume-monthly-property-bill.md @@ -0,0 +1,195 @@ +--- +title: prop-acc · prepaid · 场景 - 手动抵扣月度物业费 +aliases: + - 抵扣物业费 + - 预存款抵账单 + - consume-monthly-property-bill + - 场景-预存款抵月物业费 +tags: + - 场景 + - prop-acc + - 预存款 + - 消费 +audience: + - 业户 + - 业务人员 +status: 已发布 +sub_feature: prepaid +last_review: 2026-05-25 +code_version: 2026-05-22 +--- + +# 场景:手动抵扣月度物业费 + +预存款最高频操作 —— 月底物业费账单出来后,业务人员后台**手动触发** `ConsumeAction`,从业户的预存款余额扣对应金额、Bill 状态翻 Paid。**未来批量自动 job 落地后,这条路径变成"运维个例兜底"**(详见 [[auto-deduction-design]])。 + +## 典型情境 + +> [!example] 真实情境 +> 张阿姨的 12-3-501 房 5 月物业费账单 ¥800 已出账。张阿姨预存款账户余额 ¥4,200。物业财务王主管月初批量为 100+ 户业户做物业费抵扣,张阿姨是其中一户。 + +## 业户视角 + +### 您会感受到什么 + +- 5 月底账单出来后,几天内收到推送: + > "您的 5 月物业费 ¥800 已自动从预存款扣减,余额 ¥3,400" +- 收到收据:"物业费 ¥800(5 月)" +- 小程序"我的预存款"显示新流水:`-800.00 抵扣 物业费(5月)` +- 小程序"我的账单"显示该账单 ✅ 已付 + +### 您要做什么 + +什么都不用做。看看就行 —— + +- 如果余额够,账单自动归零,无感 +- 如果余额不够,会收到"余额不足"提示,需要您手动充值或现金/微信付 + +> [!info] 与现金付的差异 +> 业户拿到的收据**长一样**(都是"物业费 ¥800"),只是结算来源不同。详见 [[consume-via-bill-collection-type]]。 + +## 业务人员视角 + +### 第 1 步:确认账单已生成 + +后台 → 账单(Bill)模块 → 5 月物业费账单批量 → 状态 Unpaid → 列表里有张阿姨的账单。 + +### 第 2 步:打开张阿姨的预存款账户 + +后台 → 预存款 → 账户列表 → 按业户姓名搜 → 找到 Active 账户(balance=4200)→ 进 `ViewPrepaidAccount`。 + +### 第 3 步:点击 `ConsumeAction`(标签"消费抵扣") + +> [!warning] 按钮可见性 +> `ConsumeAction` 守护:`canOperate()`(Active only)+ `balance > 0` + Policy `->authorize('consume')`。Frozen / Closed / 零余额账户灰化。 + +Modal 表单: + +| 字段 | 填什么 | +|---|---| +| **关联账单(Bill)** | 选业户的未付账单(下拉显示该业户 community 内 status=unpaid 的账单)| +| **抵扣金额** | 自动带入账单金额(可改,部分抵扣场景)| +| **备注** | 选填,如 "5 月物业费手动抵扣" | + +### 第 4 步:提交 + +系统调 `ConsumeFromPrepaidAccountAction`,事务内: + +1. 校验 `canOperate()`(Active only) +2. 校验跨社区(Bill 与 Account 必须同 community) +3. 校验余额(≥ 抵扣金额) +4. 建 `CollectionOrder`(`type=Bill`,`actual=+800`,`meta.fund_source=prepaid`,`Completed`) +5. 建 `CollectionOrderBill` 关联 CO 与 Bill +6. 调 `PrepaidAccount::consume($bill, $amount)`: + - 加 `PrepaidTransaction`(`type=consume`,`amount=800`,`balance_before=4200`,`balance_after=3400`,`related_bill_id=...`,关联 CO) + - 更新 `balance=3400` +7. 调 `Bill::recordPayment($amount)`: + - 更新 `Bill.status=Paid` +8. 触发 `CollectionOrderCompleted` → Listener 建 Receipt(走 Bill 渠道,文案"物业费 ¥800") + +### 第 5 步:给收据 / 通知 + +后台找到新建 Receipt → 发业户(微信 / 邮件)。 + +## 系统流程 + +```mermaid +sequenceDiagram + participant 业户 + participant 财务 + participant Filament + participant ConsumeAction + participant PrepaidAccount + participant Bill + participant 数据库 + participant 监听器 + + Note over 业户,财务: 5 月物业费账单已出,张阿姨 balance=4200 + + 财务->>Filament: ViewPrepaidAccount → ConsumeAction(选 Bill, 800) + Filament->>ConsumeAction: handle(account, bill, 800) + ConsumeAction->>PrepaidAccount: canOperate() ? Active=true + ConsumeAction->>PrepaidAccount: community_id match Bill? yes + ConsumeAction->>PrepaidAccount: balance >= 800? 4200≥800 yes + ConsumeAction->>数据库: 开启事务 + ConsumeAction->>数据库: 1. 建 CO(type=Bill, +800, meta.fund_source=prepaid) + ConsumeAction->>数据库: 2. 建 CollectionOrderBill 关联 + ConsumeAction->>PrepaidAccount: 3. consume(bill, 800) + PrepaidAccount->>数据库: 建 PrepaidTransaction(consume, 4200→3400, related_bill_id) + PrepaidAccount->>数据库: 更新 balance=3400 + ConsumeAction->>Bill: 4. recordPayment(800) + Bill->>数据库: status=Paid + ConsumeAction->>监听器: 5. 触发 CollectionOrderCompleted + 监听器->>数据库: 建 Receipt("物业费 ¥800") + ConsumeAction->>数据库: 提交事务 + Filament-->>财务: 成功 + 财务-->>业户: 推送 + 收据 +``` + +## 流水台账(本场景在累计流水中) + +| 流水 | type | amount | balance_before | balance_after | related_bill_id | 备注 | +|---|---|---|---|---|---|---| +| 1 | deposit | 5000 | 0 | 5000 | — | 首次充值 | +| **2** | **consume** | **800** | **5000** | **4200** | **Bill #5月物业费** | **本场景** | +| (后续 4 月 5 月各 800 ...) | + +5 月账单进入 Paid 状态,张阿姨账户余额变 ¥3,400。 + +## 部分抵扣的特殊情况 + +若业户余额**不够全付**(例如余额 ¥500,账单 ¥800): + +| 选项 | 当前实现 | +|---|---| +| 抵扣 ¥500,账单剩 ¥300 待付 | 看 `Bill::recordPayment()` 是否支持部分支付 | +| 全部跳过(不抵)| 等业户充值或其他方式付 | + +**当前推荐**:跳过,告知业户"余额不足,请充值或选其他方式付"。部分抵扣需 Bill 模块配合。 + +## 常见问题 + +> [!question] Modal 表单里"关联账单"下拉如何过滤? +> 系统只显示: +> - 与本账户同社区(community_id 一致) +> - 业户本人(resident_id 一致) +> - 状态 Unpaid +> - 按 due_at 升序(最早到期的先,引导业务人员优先抵) +> +> 多个账单 → [[consume-multiple-bills-priority|按优先级抵扣]] + +> [!question] 抵扣后业户问"我用预存款付的为啥收据写'物业费'?" +> 这是**有意设计**(详见 [[consume-via-bill-collection-type]]):业户感知一致,不管怎么付,收据都长一样。如果业户想知道"是用预存款付的",可在小程序"我的账单"看到付款方式 = "预存款抵扣"。 + +> [!question] 抵扣失败如何排查? +> 看后台 / 日志的错误信息: +> - "账户冻结" → 解冻 +> - "跨社区不允许" → 业务人员选错账单 / 账户 +> - "余额不足" → 业户先充值 +> - "账单已 Paid" → 不要重复抵扣 + +> [!question] 月底 100+ 户挨个 Modal 抵扣太慢了吧? +> 是的,这就是**月初批量自动抵扣 job** 的存在意义(详见 [[auto-deduction-design]])。**job 实现前**业务人员必须挨个手动。 + +> [!question] 已抵扣的账单想撤回怎么办? +> 不可变流水设计。如果抵错(例如抵了别人的账单): +> - 走退款 [[refund-partial-after-consume]] —— 但这退的是预存款余额,不是"撤销抵扣" +> - 撤销账单需 Bill 模块支持 reverse,不在本场景 +> - 实际:**预防胜于补救**,Modal 表单提交前再三确认 Bill ID + +## 异常分支 + +- 余额不够 → 业户先充 [[deposit-additional-topup]] 再来抵 +- 账户 Frozen → 先 [[unfreeze-after-verification]] +- 多张账单一起抵 → [[consume-multiple-bills-priority]] +- 计量类账单 → [[consume-meter-bill]] +- 月初批量(未来)→ [[consume-batch-auto-monthly]] + +## 相关文档 + +- [[transaction-types]] +- [[consume-via-bill-collection-type]] +- [[account-state-machine]] +- [[consume-multiple-bills-priority]] +- [[consume-batch-auto-monthly]] +- [[auto-deduction-design]] diff --git a/prop-acc/scenarios/prepaid/deposit-additional-topup.md b/prop-acc/scenarios/prepaid/deposit-additional-topup.md new file mode 100644 index 0000000..a665ab5 --- /dev/null +++ b/prop-acc/scenarios/prepaid/deposit-additional-topup.md @@ -0,0 +1,163 @@ +--- +title: prop-acc · prepaid · 场景 - 已有账户追加充值 +aliases: + - 追加充值预存款 + - 预存款续充 + - deposit-additional-topup + - 场景-预存款追加充值 +tags: + - 场景 + - prop-acc + - 预存款 + - 充值 +audience: + - 业户 + - 业务人员 +status: 已发布 +sub_feature: prepaid +last_review: 2026-05-25 +code_version: 2026-05-22 +--- + +# 场景:已有账户追加充值 + +业户**已有 Active 预存款账户**,余额不够 / 想多存,继续充值。比首次开户简单 —— 不建账户,只加流水。 + +## 典型情境 + +> [!example] 真实情境 +> 张阿姨 3 个月前充了 ¥5,000 预存款,期间扣了 ¥2,400(物业费 800 × 3),余额 ¥2,600。下个月还要扣 ¥800 + ¥600 水电费,觉得余额勉强够,**再充 ¥3,000** 凑个整。 + +## 业户视角 + +### 第 1 步:到前台 / 小程序 + +跟物业管家说"我预存账户加 ¥3,000"。 + +### 第 2 步:付款 + +支付方式同首次充值。 + +### 第 3 步:拿收据 + +"预付款充值 ¥3,000"。 + +### 第 4 步:余额查看 + +后台 / 小程序看到: + +- 上次余额 ¥2,600 + 本次 ¥3,000 = **当前余额 ¥5,600** +- 后续账单自动从这扣 + +## 业务人员视角 + +> [!info] 与首次充值的差异 +> **不开新账户**,在既有账户上 `DepositAction` 加流水。 + +### 第 1 步:找到既有账户 + +后台 → 预存款 → 账户列表 → 按业户姓名 / 房号搜索 → 找到 Active 账户。 + +### 第 2 步:进 `ViewPrepaidAccount` + +详情页右上角点 **`DepositAction`**(标签"充值")。 + +> [!warning] 按钮可见性 +> `DepositAction` 守护:`canOperate()`(Active only)+ Policy `->authorize('deposit')`。Frozen / Closed 灰化。 + +### 第 3 步:Modal 表单 + +| 字段 | 填什么 | +|---|---| +| **充值金额** | ¥3,000 | +| **支付方式** | 现金 / 微信 / POS / 银行转账 | +| **收款银行账户** | 微信/POS/转账选对应银行 | +| **备注** | 选填 | + +### 第 4 步:提交 + +系统调 `PrepaidAccount::deposit($amount, ...)`,事务内: + +1. 模型层校验 `canOperate()`(Active only) +2. 建 `CollectionOrder`(`type=Prepaid`,`actual=+3000`,`Completed`) +3. 建 `PrepaidTransaction`(`type=deposit`,`amount=3000`,`balance_before=2600`,`balance_after=5600`,关联 CO) +4. 更新 `PrepaidAccount.balance=5600` +5. 触发 `CollectionOrderCompleted` → Listener 建 Receipt"预付款充值 ¥3,000" + +### 第 5 步:给收据 + +打印 / 发微信。 + +## 系统流程 + +```mermaid +sequenceDiagram + participant 业户 + participant 前台 + participant Filament + participant PrepaidAccount + participant 数据库 + + 业户->>前台: 给预存账户加 3000 + 前台->>Filament: ViewPrepaidAccount → DepositAction(modal) + Filament->>PrepaidAccount: deposit(3000, ...) + PrepaidAccount->>PrepaidAccount: canOperate()? Active=true + PrepaidAccount->>数据库: 开启事务 + PrepaidAccount->>数据库: 1. 建 CO (Prepaid, +3000, Completed) + PrepaidAccount->>数据库: 2. 建 PrepaidTransaction (deposit, 2600→5600) + PrepaidAccount->>数据库: 3. 更新 balance=5600 + PrepaidAccount->>监听器: 触发 CollectionOrderCompleted + 监听器->>数据库: 建 Receipt (预付款充值 ¥3,000) + PrepaidAccount->>数据库: 提交 + Filament-->>前台: 成功 + 前台->>业户: 收据 +``` + +## 流水台账(本场景在累计流水中的位置) + +| 流水 | type | amount | balance_before | balance_after | 备注 | +|---|---|---|---|---|---| +| 1 | deposit | 5000 | 0 | 5000 | 3 个月前首次充值 | +| 2 | consume | 800 | 5000 | 4200 | 第 1 月物业费 | +| 3 | consume | 800 | 4200 | 3400 | 第 2 月物业费 | +| 4 | consume | 800 | 3400 | 2600 | 第 3 月物业费 | +| **5** | **deposit** | **3000** | **2600** | **5600** | **本次追加** | + +## 常见问题 + +> [!question] Frozen 账户能追加充值吗? +> **不能**。`canOperate()` 只允许 Active。详见 [[exception-refund-on-frozen|三层守护]](deposit / consume / refund 都一样)。 +> +> 如果业户硬要充: +> - 系统层无法绕过(模型层兜底) +> - **业务层** 需先 [[unfreeze-after-verification|解冻]] → 再充 +> - 不可"暂存钱等解冻后录入" —— 不合规 + +> [!question] Closed 账户能追加充值吗? +> **不能**(同上)。需要开新账户,但**一户一账约束阻塞**(详见 [[one-account-per-resident]] "已知设计 gap")。 + +> [!question] 同时多笔追加(一天充两次)可以吗? +> 可以。每次独立 Action,各自一笔 Transaction + CO + Receipt。账户 balance 累加。 + +> [!question] 充值过多担心退不出来? +> 任何时候可走 [[refund-partial-after-consume]] 或 [[refund-full-resident-moveout]] 退余。预存款不像押金有"装修结束才能退"的业务节点,**随时可退**。 + +> [!question] 业户问"我能用别人的微信付吗?" +> 系统不限制实际支付来源(微信扫码用谁付都行)。**业务上**: +> - 账面缴款人是业户本人(`PrepaidAccount.community_user_profile_id` 不变) +> - 实际付钱的是谁是业户自己的事 +> - 退款时**只退给账面缴款人**(业户本人),不是实际付钱的微信号 + +## 异常分支 + +- 业户从未充过 → 走 [[deposit-first-time]] 开户 +- 充错金额 → [[refund-partial-after-consume]] +- 账户冻结 → 先 [[unfreeze-after-verification]] 解冻 + +## 相关文档 + +- [[deposit-first-time]] +- [[account-state-machine]] +- [[consume-monthly-property-bill]] +- [[refund-partial-after-consume]] +- [[exception-refund-on-frozen]] diff --git a/prop-acc/scenarios/prepaid/deposit-first-time.md b/prop-acc/scenarios/prepaid/deposit-first-time.md new file mode 100644 index 0000000..fcffc2b --- /dev/null +++ b/prop-acc/scenarios/prepaid/deposit-first-time.md @@ -0,0 +1,187 @@ +--- +title: prop-acc · prepaid · 场景 - 首次开户充值 5000 +aliases: + - 首次充值预存款 + - 开预存款账户 + - deposit-first-time + - 场景-首次充值预存款 +tags: + - 场景 + - prop-acc + - 预存款 + - 充值 +audience: + - 业户 + - 业务人员 +status: 已发布 +sub_feature: prepaid +last_review: 2026-05-25 +code_version: 2026-05-22 +--- + +# 场景:首次开户充值 5000 + +业户**第一次**开预存款账户并充值。一户一账约束:同业户在同社区只能开一个,系统在提交时校验。 + +## 典型情境 + +> [!example] 真实情境 +> 张阿姨(12-3-501)每月物业费 ¥800,觉得月月去前台缴麻烦,跟物业管家说:"我一次充半年,以后从这里自动扣行不行?" +> +> 物业管家:"行,我帮您开个预存款账户,您充 ¥5,000 进去,以后账单出来自动从这里扣。" + +## 业户视角 + +### 第 1 步:跟物业说要充值 + +- 到前台 / 物业管家微信 / 小程序(若开通) +- 表达"我想预存,以后自动扣账单" + +### 第 2 步:确认充值金额 + +通常建议:**3-6 个月账单的金额**。少了频繁充值,多了占用资金。 + +- 月物业费 ¥800 → 充 ¥3,000-¥5,000(够 3-6 个月) +- 加水电费一起 → 充 ¥5,000-¥10,000 + +### 第 3 步:付款 + +支付方式: + +- **现金** +- **微信扫码** +- **POS 刷卡** +- **银行转账** + +### 第 4 步:拿收据 + +"预付款充值 ¥5,000"。 + +> [!info] 这张收据是普通正数收据 +> 跟付物业费的收据长一样,只是文案不同。详见 [[transaction-types]]。 + +### 第 5 步:后续 + +- 物业费账单出来 → 业务人员手动 / (未来)自动抵扣 → 您收到"物业费 ¥800" 收据 +- 余额 ¥4,200 留账户里,下月继续扣 + +## 业务人员视角 + +### 第 1 步:核实业户身份 + +- 业户档案存在(否则要先建) +- 业户当前社区(决定 community_id) + +### 第 2 步:打开后台 + +后台 → 预存款 → **新建账户**(`ListPrepaidAccounts` 的 Create 按钮)。 + +### 第 3 步:填表单 + +| 字段 | 填什么 | +|---|---| +| **业户档案(`community_user_profile_id`)** | 通过房号 / 手机号 / 姓名找到张阿姨 | +| **社区(`community_id`)** | 自动带入业户所在社区(或手动选) | +| **首次充值金额** | ¥5,000 | +| **支付方式** | 现金 / 微信 / POS / 银行转账 | +| **收款银行账户** | 微信/POS/转账选对应银行;现金可空 | +| **备注** | 选填,如 "业户要求月度自动扣账" | + +> [!warning] 一户一账校验 +> 系统提交时检查 `(community_id, community_user_profile_id)` 是否已存在: +> - 已有 Active → 提示"该业户在本社区已有预存款账户,请直接充值" → 引导到 [[deposit-additional-topup]] +> - 已有 Frozen → 提示"账户冻结中,请先解冻" +> - 已有 Closed → 当前阻塞(见 [[one-account-per-resident]] "已知设计 gap" 段) +> - 无 → 正常建账 + +### 第 4 步:提交 + +系统在事务内: + +1. 建 `PrepaidAccount`(`status=Active`,`balance=5000`) +2. 建 `CollectionOrder`(`collection_type=Prepaid`,`actual_amount=+5000`,`status=Completed`) +3. 建 `PrepaidTransaction`(`type=deposit`,`amount=5000`,`balance_before=0`,`balance_after=5000`,关联 CO) +4. 触发 `CollectionOrderCompleted` 事件 +5. Listener `generatePrepaidReceiptItems` 建 Receipt + ReceiptItem"预付款充值 ¥5,000" + +### 第 5 步:打印 / 发收据 + +后台收据列表找到新生成 Receipt → 打印 / 微信发业户。 + +## 系统流程 + +```mermaid +sequenceDiagram + participant 业户 + participant 前台 + participant Filament + participant 数据库 + participant 监听器 + + 业户->>前台: 充 5000 预存款 + 前台->>Filament: ListPrepaidAccounts → Create + Filament->>数据库: 校验 unique(community_id, profile_id) → 通过 + Filament->>数据库: 开启事务 + Filament->>数据库: 1. 建 PrepaidAccount (Active, balance=5000) + Filament->>数据库: 2. 建 CollectionOrder (type=Prepaid, +5000, Completed) + Filament->>数据库: 3. 建 PrepaidTransaction (deposit, 0→5000, 关联 CO) + Filament->>监听器: 4. 触发 CollectionOrderCompleted + 监听器->>数据库: 5. 建 Receipt ("预付款充值 ¥5,000") + Filament->>数据库: 提交事务 + Filament-->>前台: 成功 + 显示新账户 + 前台->>业户: 给收据 +``` + +## 与 deposit 首次缴款的对比 + +| 维度 | 押金首次缴款 | **预存款首次充值** | +|---|---|---| +| 表单字段 | payer_type / fee_type / asset 等多个 | **只需 community_user_profile** | +| 业户/缴款人差异 | 缴款人可与业户不同(装修公司代缴)| **缴款人必须是业户本人** | +| CollectionType | Deposit | **Prepaid** | +| 同业户多账户 | ✅ 多种费类多账户 | ❌ **一户一账** | +| 关账机制 | 退完自动 Closed | 退完仍 Active(可继续充) | + +## 常见问题 + +> [!question] 业户已有 Closed 账户,如何开新? +> 当前系统阻塞(unique 约束)。可选: +> - 联系运维 tinker 改账户名(罕见) +> - 业务上说服业户用现金 / 微信付,不开新预存账户 +> - 系统层加 `WHERE status != 'closed'` 软约束(待业务方拍板) +> +> 详见 [[one-account-per-resident]] "已知设计 gap" 段。 + +> [!question] 跨社区业户(同时住两个小区)怎么开? +> 各社区独立账户(各自 unique)。在 A 社区开一个、B 社区开一个,各自独立余额。 + +> [!question] 充值金额有上限吗? +> 系统层面无硬性上限。业务上建议: +> - 不超过 12 个月账单合计(避免资金被冻在物业账上太久) +> - 单笔大额(>10000)走银行转账,留银行流水 +> - 单笔超 50000 需财务上报(防风险) + +> [!question] 充错金额(把 5000 录成 50000)怎么办? +> 不要改流水。建一笔 `Refund` ¥45,000(走 [[refund-partial-after-consume]] 流程),业户拿到红字"预付款退款 ¥-45,000",事后审计完整可追。 + +> [!question] 业户不知道这账户怎么用,需要培训吗? +> 关键点: +> - 余额能抵物业费 / 水电费 / 其他账单 +> - 余额随时可查(小程序 / 微信对账单) +> - 余额随时可退(业务人员后台操作) +> - 余额不够时账单不会自动扣 → 业户仍需补缴 + +## 异常分支 + +- 业户已有账户 → [[deposit-additional-topup]] +- 业户充错金额想撤 → [[refund-partial-after-consume]](走部分退款) +- 业户后悔不想用预存了 → [[refund-full-resident-moveout]] + [[close-resident-moveout]] + +## 相关文档 + +- [[prepaid-account-vs-transaction]] +- [[account-state-machine]] +- [[one-account-per-resident]] +- [[transaction-types]] +- [[deposit-additional-topup]] +- [[consume-monthly-property-bill]] diff --git a/prop-acc/scenarios/prepaid/deposit-via-miniapp-pending.md b/prop-acc/scenarios/prepaid/deposit-via-miniapp-pending.md new file mode 100644 index 0000000..b07dffa --- /dev/null +++ b/prop-acc/scenarios/prepaid/deposit-via-miniapp-pending.md @@ -0,0 +1,180 @@ +--- +title: prop-acc · prepaid · 场景 - 小程序在线充值(待补) +aliases: + - 小程序充值预存款 + - 线上充值预存款 + - deposit-via-miniapp-pending + - 场景-小程序充值预存款 +tags: + - 场景 + - prop-acc + - 预存款 + - 充值 + - 待补 +audience: + - 业户 + - 产品 + - 架构师 +status: 草稿 +sub_feature: prepaid +last_review: 2026-05-25 +code_version: 2026-05-22 +--- + +# 场景:小程序在线充值(待补) + +> [!warning] 本场景**代码未实现** +> 当前所有充值都是**业务人员后台手动触发**。业户**没有自助充值入口**(微信小程序 / 公众号 / H5 都没接入)。本文档描述设计意图,等支付网关对接时一起落地。 +> +> issue.md Q4 "待补" 段记录: +> > 小程序在线充值 + 退款 webhook:同保证金模块,等支付网关对接时一起做 + +## 为什么这个场景重要 + +预存款的**真正价值**是业户**自助**预存 + 系统自动抵账单。如果业户每次充值都要"跑前台 / 找物业管家",体验跟去前台月月缴费没区别,预存款产品价值大打折扣。 + +**自助充值是产品落地的必备入口**。 + +## 业务场景(目标态) + +### 业户视角 + +> [!example] 真实情境(目标态) +> 陈先生(年轻业主)某晚 11 点查看本月物业费账单,看到余额不够,马上在小程序"我的预存款"页面充值: +> +> 1. 点"立即充值" +> 2. 选金额(500 / 1000 / 3000 / 5000 / 自定义) +> 3. 弹出微信支付确认 +> 4. 输入密码 / 指纹 → 付款成功 +> 5. 小程序 3 秒后提示"充值成功,余额已到账" +> +> 第二天物业费账单自动从余额扣,陈先生收到推送"已抵扣物业费 ¥800"。 + +### 业务人员视角 + +业务人员**几乎不感知**: + +- 不需要在后台手动建账户 / 录充值 +- 月底对账时,看 CollectionOrder 列表多了一批 `payment_channel=微信`、`fund_source=external` 的预存款充值单 +- 异常(支付掉单 / 超时未付)由系统自动处理 + +## 关键技术挑战 + +### 1. 支付网关对接 + +| 选项 | 优 | 缺 | +|---|---|---| +| 微信支付商户号 | 业户熟悉,转化高 | 资质要求高,手续费 0.6% | +| 支付宝 | 大额支付习惯 | 同上 | +| 银联 / 网银 | 大额转账 | 体验差 | + +**推荐**:微信支付 + 支付宝双通道,与一次性收费 B 流复用([[../adhoc/flow-b-miniapp-wechat-pay|adhoc B 流]])。 + +### 2. CollectionOrder 状态机 + +复用与 adhoc B 流相同的 Pending → Completed 流程([[../adhoc/flow-a-vs-flow-b|A 流与 B 流]]): + +```mermaid +sequenceDiagram + participant 业户 + participant 小程序 + participant 系统 + participant 支付网关 + + 业户->>小程序: 选 5000 充值 + 小程序->>系统: 建 CollectionOrder(type=Prepaid, +5000, **status=Pending**) + 系统-->>小程序: 返回订单号 + 锁定金额 + 小程序->>支付网关: 调起微信支付 + 业户->>支付网关: 输入密码 / 指纹付款 + 支付网关->>系统: 支付回调 webhook + 系统->>系统: 校验签名 + 金额 + 系统->>系统: CO.status = Completed + 系统->>系统: 调 PrepaidAccount::deposit(5000) + 系统->>系统: 触发 CollectionOrderCompleted → Receipt + 系统-->>业户: 推送"充值成功" +``` + +### 3. 自动开户 + +如果业户在小程序充值时**还没有预存款账户**: + +| 方案 A:必须先开户 | 方案 B:充值时自动开户 | +|---|---| +| 业户去前台开户 → 小程序充值 | 小程序充值流程内自动建账户 | +| 体验割裂(为啥要去前台?) | 体验顺畅 | +| 业务人员必须介入 | 全程自动 | + +**推荐方案 B**:充值时若 `PrepaidAccount` 不存在,自动建 Active 账户(`opened_at` = 充值时间)。 + +### 4. 失败处理 + +| 失败场景 | 处理 | +|---|---| +| 业户付了款,回调延迟 | CO 仍 Pending → 业户重复点充值 → 防重(检查 24h 内同业户同金额 Pending CO)| +| 业户付了款,回调丢失 | 定时任务扫描 Pending > 30 min 的 CO,主动查询支付网关 | +| 业户取消支付 | CO 翻 Failed,余额不变 | +| 业户付款金额与订单不符(异常)| 拒绝,告警,人工介入 | + +### 5. 退款 webhook + +业户在小程序自助申请退款: + +```mermaid +sequenceDiagram + 业户->>小程序: 申请退余 3000 + 小程序->>系统: 建退款申请单(待业务审批) + Note over 系统: 业务审批通过后: + 系统->>支付网关: 调退款 API + 支付网关->>系统: 退款回调 webhook + 系统->>系统: 建红字 CO + PrepaidTransaction(refund) + 系统-->>业户: 推送"退款成功" +``` + +**关键**:不像后台手动退款是"业务人员决定退多少",小程序自助退款必须**先建审批工单**,业务方审核(防止业户恶意大额退款),通过后才走支付网关退款。 + +## 待讨论 / 决策 + +| 问题 | 选项 | +|---|---| +| **支付通道** | 微信支付 / 支付宝 / 银联 / 全部都开 | +| **充值起步** | 最低 100 / 500 / 1000 | +| **充值上限** | 单笔 5000 / 10000 / 不限 | +| **自动开户** | 充值时自动建 Active 账户 vs 业户必须先去前台 | +| **退款审批** | 业户提交即退 vs 业务审批后退 / 大额(>5000)需审批 | +| **超时未付** | 30 分钟自动取消 / 24 小时 / 不取消(避免业户晚付被取消)| + +业务方拍板前,以上问题需明确。 + +## 当前替代方案(等代码就位前) + +业户想自助充值的,目前只能: + +| 方法 | 体验 | +|---|---| +| 联系物业管家微信 → 转账给物业财务 → 财务后台录入 | 还行,但有时延 | +| 到前台缴 → 现金 / POS | 慢、要跑 | +| 银行直接对公转账 → 备注业户姓名房号 → 财务认领 | 慢 + 复杂 | + +**所有路径都需要业务人员介入** —— 体验远不如小程序自助。这就是为什么这个场景"产品价值高、紧迫性强"。 + +## 关联场景 + +实现后,以下场景的设计会发生关联变化: + +- [[deposit-first-time]] / [[deposit-additional-topup]]:多一条"业户自助充值"路径,业务人员手动充值场景变少(但仍需保留,给老业主用) +- [[consume-batch-auto-monthly]]:小程序充值后自动到账,月初批量自动抵扣 job 可立即用上新余额 +- [[refund-full-resident-moveout]] / [[refund-partial-after-consume]]:多一条"业户小程序自助申请"路径,需要审批流 + +## 异常分支(未来落地后) + +- 支付网关掉单 / 超时 → 系统重试 / 业务介入 +- 业户充错金额 → 走退款流程 +- 业户重复提交 → 防重检测 + +## 相关文档 + +- [[auto-deduction-design]] +- [[deposit-first-time]] +- [[deposit-additional-topup]] +- [[../adhoc/flow-a-vs-flow-b|A 流与 B 流]](adhoc 的小程序在线模式,参考实现) +- [[../adhoc/flow-b-miniapp-wechat-pay|adhoc 小程序微信付]]