diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json
index 0a77673..4e612e4 100644
--- a/.obsidian/workspace.json
+++ b/.obsidian/workspace.json
@@ -197,6 +197,9 @@
},
"active": "b06ed69835363258",
"lastOpenFiles": [
+ "prop-acc/scenarios/prepaid/audit-low-balance-and-overdue.md",
+ "prop-acc/scenarios/prepaid/exception-refund-on-frozen.md",
+ "prop-acc/scenarios/prepaid/exception-cross-community-consume.md",
"prop-acc/scenarios/prepaid/close-with-zero-balance-decision.md",
"prop-acc/scenarios/prepaid/close-resident-moveout.md",
"prop-acc/scenarios/prepaid/unfreeze-after-verification.md",
@@ -221,10 +224,7 @@
"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/prepaid-account-vs-transaction.md",
"prop-acc/concepts/prepaid",
- "prop-acc/concepts/deposit/deposit-vs-adhoc-vs-prepaid.md",
- "prop-acc/scenarios/deposit/audit-long-pending-accounts.md",
"prop-acc/scenarios/deposit",
"prop-acc/concepts/deposit",
"prop-acc/scenarios/adhoc",
diff --git a/prop-acc/maps/prepaid-knowledge-map.md b/prop-acc/maps/prepaid-knowledge-map.md
index b155a69..860dae4 100644
--- a/prop-acc/maps/prepaid-knowledge-map.md
+++ b/prop-acc/maps/prepaid-knowledge-map.md
@@ -53,43 +53,41 @@ code_version: 2026-05-22
| [Consume 走 CollectionType=Bill 的设计](../concepts/prepaid/consume-via-bill-collection-type.md) | 独特设计:抵账单用 Bill 视角,资金来源标 meta.fund_source=prepaid |
| [月初批量自动抵扣设计](../concepts/prepaid/auto-deduction-design.md) | 待补 scheduled job,产品核心卖点 |
-## 场景手册(16 篇,**待补充 ✋**)
-
-> 🚧 概念骨架已就位,场景文档将在下一轮(轮 2)产出。预定结构如下。
+## 场景手册(16 篇,**全部完成 ✅**)
### 📥 充值(Deposit)— 3 篇
-- 🚧 [首次开户充值 5000](../scenarios/prepaid/deposit-first-time.md)
-- 🚧 [已有账户追加充值](../scenarios/prepaid/deposit-additional-topup.md)
-- 🚧 [小程序在线充值(待补设计意图)](../scenarios/prepaid/deposit-via-miniapp-pending.md)
+- ✅ [首次开户充值 5000](../scenarios/prepaid/deposit-first-time.md)
+- ✅ [已有账户追加充值](../scenarios/prepaid/deposit-additional-topup.md)
+- ✅ [小程序在线充值(待补设计意图)](../scenarios/prepaid/deposit-via-miniapp-pending.md)
### 🧹 消费 Consume — 4 篇(最核心)
-- 🚧 [手动抵扣月度物业费](../scenarios/prepaid/consume-monthly-property-bill.md)
-- 🚧 [多个未付账单按 due_at 优先级抵扣](../scenarios/prepaid/consume-multiple-bills-priority.md)
-- 🚧 [抵扣计量账单(水电费)](../scenarios/prepaid/consume-meter-bill.md)
-- 🚧 [月初批量自动抵扣 job(设计意图 + 业务流程)](../scenarios/prepaid/consume-batch-auto-monthly.md)
+- ✅ [手动抵扣月度物业费](../scenarios/prepaid/consume-monthly-property-bill.md)
+- ✅ [多个未付账单按 due_at 优先级抵扣](../scenarios/prepaid/consume-multiple-bills-priority.md)
+- ✅ [抵扣计量账单(水电费)](../scenarios/prepaid/consume-meter-bill.md)
+- ✅ [月初批量自动抵扣 job(设计意图 + 业务流程)](../scenarios/prepaid/consume-batch-auto-monthly.md)
### 💰 退款(Refund)— 2 篇
-- 🚧 [业户搬走全额退余](../scenarios/prepaid/refund-full-resident-moveout.md)
-- 🚧 [部分消费后退余(不自动关账)](../scenarios/prepaid/refund-partial-after-consume.md)
+- ✅ [业户搬走全额退余](../scenarios/prepaid/refund-full-resident-moveout.md)
+- ✅ [部分消费后退余(不自动关账)](../scenarios/prepaid/refund-partial-after-consume.md)
### 🧊 冻结 / 解冻(Freeze / Unfreeze)— 2 篇
-- 🚧 [疑似欺诈 / 风控冻结](../scenarios/prepaid/freeze-suspected-fraud.md)
-- 🚧 [核实后解冻](../scenarios/prepaid/unfreeze-after-verification.md)
+- ✅ [疑似欺诈 / 风控冻结](../scenarios/prepaid/freeze-suspected-fraud.md)
+- ✅ [核实后解冻](../scenarios/prepaid/unfreeze-after-verification.md)
### 🔒 结清(Close)— 2 篇
-- 🚧 [业户搬走主动关账](../scenarios/prepaid/close-resident-moveout.md)
-- 🚧 [余额清零后不自动关,业户决定](../scenarios/prepaid/close-with-zero-balance-decision.md)
+- ✅ [业户搬走主动关账](../scenarios/prepaid/close-resident-moveout.md)
+- ✅ [余额清零后不自动关,业户决定](../scenarios/prepaid/close-with-zero-balance-decision.md)
### 🛡️ 异常 / 审计(3 篇)
-- 🚧 [跨社区消费防御](../scenarios/prepaid/exception-cross-community-consume.md)
-- 🚧 [冻结状态退款被三层守护拦截](../scenarios/prepaid/exception-refund-on-frozen.md)
-- 🚧 [低余额业户预警 + 逾期账单排查](../scenarios/prepaid/audit-low-balance-and-overdue.md)
+- ✅ [跨社区消费防御](../scenarios/prepaid/exception-cross-community-consume.md)
+- ✅ [冻结状态退款被三层守护拦截](../scenarios/prepaid/exception-refund-on-frozen.md)
+- ✅ [低余额业户预警 + 逾期账单排查](../scenarios/prepaid/audit-low-balance-and-overdue.md)
## 跨域引用
@@ -125,6 +123,9 @@ code_version: 2026-05-22
---
-> [!info] 概念已完成,场景待补
-> 本轮(轮 1)产出:6 个概念 + 本子模块地图 + 域总图更新。
-> 下一轮(轮 2)产出:16 个场景文档,基于本知识地图骨架填充。
+> [!success] prepaid 子模块:6 概念 + 16 场景 + 1 知识地图 = **23 篇完成**
+>
+> 写作日期:2026-05-25
+> 对应代码版本:2026-05-22(详见 `packages/prop-acc/issue.md` Q4 段)
+>
+> 如果发现遗漏的场景或需要补充的细节,告诉我,可以单独补充新文档。
diff --git a/prop-acc/scenarios/prepaid/audit-low-balance-and-overdue.md b/prop-acc/scenarios/prepaid/audit-low-balance-and-overdue.md
new file mode 100644
index 0000000..a07bce3
--- /dev/null
+++ b/prop-acc/scenarios/prepaid/audit-low-balance-and-overdue.md
@@ -0,0 +1,247 @@
+---
+title: prop-acc · prepaid · 场景 - 低余额业户预警 + 逾期账单排查
+aliases:
+ - 低余额预警
+ - 预存款余额告警
+ - audit-low-balance-and-overdue
+ - 场景-预存款低余额预警
+tags:
+ - 场景
+ - prop-acc
+ - 预存款
+ - 审计
+audience:
+ - 业务人员
+ - 财务
+ - 产品
+status: 已发布
+sub_feature: prepaid
+last_review: 2026-05-25
+code_version: 2026-05-22
+---
+
+# 场景:低余额业户预警 + 逾期账单排查
+
+物业业务人员**每周** / 每月扫描:
+
+1. **低余额预存款业户**(下月预计账单 > 当前余额) → 主动提醒充值
+2. **预存款余额不够付未付账单的业户** → 跨核对
+
+`LowBalancePrepaidListWidget` 是后台 Dashboard 上专门的 Widget,展示低余额账户清单。
+
+## 典型情境
+
+> [!example] 真实情境
+> 物业财务王主管每周一上午看 `DepositPrepaidDashboard`:
+>
+> - 低余额业户清单 widget 显示 **45 户**业户的预存款余额 < 下月预计账单
+> - 其中 **12 户**已有当月未付账单
+> - 主管按清单逐户联系:
+> - 12 户已欠 → 督促立即充值
+> - 33 户预警(还没欠)→ 友好提醒"下月账单 ¥X,余额 ¥Y 不够,建议充值"
+
+## 业务人员视角
+
+### Dashboard 查看
+
+后台 → 仪表盘 → `DepositPrepaidDashboard` 页面 → 看 `LowBalancePrepaidListWidget`。
+
+Widget 显示:
+
+| 业户 | 当前余额 | 下月预计账单 | 差额 | 当前未付账单 |
+|---|---|---|---|---|
+| 张阿姨(12-3-501)| ¥200 | ¥800 | -¥600 | 0 |
+| 陈先生(12-3-502)| ¥1,500 | ¥2,200 | -¥700 | 1(水电费 ¥220 已逾期 3 天)|
+| 刘先生(12-3-503)| ¥0 | ¥800 | -¥800 | 1(物业费 ¥800 未付)|
+| (省略)| | | | |
+
+### 处置策略(分级)
+
+| 紧急度 | 业户特征 | 处置 |
+|---|---|---|
+| **🔴 紧急** | 已有逾期账单 + 余额不够 | 立即联系 + 督促充值 + 必要时手动催收 |
+| **🟡 警告** | 当前余额低于下月预计账单 | 提前 3-7 天友好提醒 |
+| **🟢 关注** | 余额低于 2 个月账单合计 | 月度提醒一次,无需紧急动作 |
+
+### 第 1 步:扫描清单
+
+打开 `DepositPrepaidDashboard` → 看清单 widget。也可手动 SQL 查:
+
+```sql
+-- 低余额预存款业户(余额 < 下月预计账单)
+SELECT
+ p.id AS account_id,
+ p.community_user_profile_id,
+ cup.name AS resident_name,
+ p.balance AS current_balance,
+ estimated_next_bill, -- 子查询算下月预计
+ (estimated_next_bill - p.balance) AS shortage,
+ COUNT(b.id) AS overdue_bills_count
+FROM acc_prepaid_accounts p
+JOIN community_user_profiles cup ON p.community_user_profile_id = cup.id
+LEFT JOIN acc_bills b ON b.resident_id = cup.id
+ AND b.community_id = p.community_id
+ AND b.status = 'unpaid'
+ AND b.due_at < NOW()
+WHERE p.status = 'active'
+GROUP BY p.id, ...
+HAVING current_balance < estimated_next_bill
+ORDER BY shortage DESC;
+```
+
+### 第 2 步:分级处置
+
+**🔴 紧急(已欠款)**:
+
+- 短信 / 微信 / 电话联系业户
+- 告知"您欠 X 月物业费 ¥800,请立即处理(充值预存款 / 现金 / 微信付)"
+- 业户回应 → 处理(走 [[deposit-additional-topup]] 或其他渠道收款)
+- 业户不回应 → 走逾期催收流程(本文不展开)
+
+**🟡 警告(还没欠)**:
+
+- 微信 / App 推送"友好提醒"
+- "您的预存款余额 ¥X,下月账单约 ¥Y,建议提前充值"
+- 不强制
+
+**🟢 关注**:
+
+- 月度汇总报告(给业户的"预存款健康度月报")
+- 不打扰
+
+### 第 3 步:出周报
+
+```markdown
+# 2026 年 5 月 第 4 周 预存款健康度周报
+
+## 低余额业户清单(共 45 户)
+- 🔴 紧急(已欠款):12 户,合计欠款 ¥9,860
+- 🟡 警告:24 户
+- 🟢 关注:9 户
+
+## 已处置
+- 紧急 12 户:已联系
+ - 5 户已充值 / 付清
+ - 4 户承诺本周内处理
+ - 3 户失联(进入催收)
+- 警告 24 户:已推送提醒
+
+## 趋势
+- 比上周(38 户低余额)增加 7 户 → 趋势变差
+- 可能原因:5 月账单出账,部分业户余额不够付
+
+## 建议
+- 加强自动抵扣 job 落地的紧迫性(目前业户充值后还得业务人员手动抵)
+- 主动给余额接近 0 的业户 push 充值提醒(改 widget 配置)
+```
+
+## 业户视角
+
+### 您可能收到的通知
+
+#### 🟡 警告(友好提醒)
+
+> 张阿姨您好,您的预存款余额 ¥200,下月物业费约 ¥800,**预计余额不够付**。建议提前充值,避免账单逾期产生提醒费用。
+
+业户可选择:
+
+- 立即充值
+- 现金 / 微信付下月账单
+- 不管(后果自负)
+
+#### 🔴 紧急(欠款提醒)
+
+> 张阿姨您好,您 5 月物业费 ¥800 **已逾期 3 天**未付。请尽快通过以下方式付清:
+> 1. 微信小程序"我的预存款"充值后系统自动抵
+> 2. 到前台现金 / POS 付
+> 3. 微信扫码付
+
+## 系统流程
+
+```mermaid
+flowchart TD
+ A[每周/月触发扫描] --> B[SQL 查低余额账户 + 未付账单]
+ B --> C{分级}
+ C -->|🔴 紧急已欠| D[强提醒 + 业务人员介入催收]
+ C -->|🟡 警告未欠| E[友好推送提醒]
+ C -->|🟢 关注| F[月度汇总报告]
+
+ D --> G{业户响应?}
+ G -->|充值| H[走 deposit-additional-topup]
+ G -->|其他渠道付| I[正常收款流程]
+ G -->|无响应| J[逾期催收流程
本文外]
+
+ E --> K{业户行动?}
+ K -->|充值| H
+ K -->|不充| L[转入🔴紧急]
+
+ H --> M[业务人员手动 ConsumeAction 抵账单]
+ Note over M: 月初自动 job 落地后此步自动
+```
+
+## 关联工具
+
+- **`DepositPrepaidDashboard`**:后台 Dashboard 页面,统一展示押金 + 预存款的健康指标
+- **`LowBalancePrepaidListWidget`**:本场景核心 Widget,实时列出低余额账户
+- **`MonthlyPrepaidFlowChart`**:展示预存款流入 / 流出趋势(充值 vs 消费 vs 退款),业务总监层面看
+- **`DepositPrepaidStatsOverviewWidget`**:总览数字(总账户数、总余额、本月流水量)
+
+## 常见问题
+
+> [!question] "下月预计账单" 怎么算?
+> 不在系统内的硬规则,看 Widget 实现:
+>
+> - 取业户近 3 个月平均月账单
+> - 或取上月账单
+> - 或固定基数(物业费固定 ¥800)
+>
+> 实际取哪种,看 `LowBalancePrepaidListWidget` 内置逻辑。可调整。
+
+> [!question] 业户被提醒"低余额"但其实人家就喜欢月月手动付,不想预存,怎么办?
+> 这种业户应**关账户**(走 [[close-resident-moveout|主动关账]]),避免后续骚扰。或者 Widget 上加"忽略"按钮,业户的"预存款关账"决策可以让业务人员跟进。
+
+> [!question] 低余额预警有没有自动化(短信)?
+> 看产品决策。**强烈推荐**:
+>
+> - 🟢 关注:不推送(避免骚扰)
+> - 🟡 警告:每月 1 次推送(可控)
+> - 🔴 紧急:立即推送(必要)
+>
+> 实施需要短信 / 微信模板消息接入。
+
+> [!question] 月初批量自动抵扣 job 落地后,本场景作用还大吗?
+> **仍重要**。job 跑完后,跳过的余额不足业户**仍需要业务人员介入**:
+>
+> - 通知充值
+> - 跟踪是否充值
+> - 充值后 / 业户其他渠道付后,手动 ConsumeAction 补抵
+>
+> 详见 [[consume-batch-auto-monthly]] "业务人员视角 - 异常介入" 段。
+
+> [!question] 多社区的业户低余额清单怎么展示?
+> Widget 按当前 panel 的 community 过滤(若是社区级 Filament Panel)。或者展示"按社区分组"。如果业务人员管多个社区,可在 Dashboard 选社区切换。
+
+## 与 deposit 长期未关账户排查的对比
+
+| 维度 | deposit audit-long-pending-accounts | prepaid 本场景 |
+|---|---|---|
+| 关注什么 | 长期 Active 未关账户(>2 年)| 低余额账户(下月可能欠)|
+| 业务侧重 | 清理代管资金边界 | 预防业户欠费 |
+| 频率 | 季度 / 半年 | 每周 / 每月 |
+| 处置 | 关账 / retain / 联系业户 | 通知充值 / 催收 |
+
+两者**都是审计扫描场景**,但关注点和频率不同。
+
+## 异常分支
+
+- 业户充值后忘了抵 → 业务人员手动 ConsumeAction
+- 业户长期不响应低余额预警 → 进入逾期催收流程(本文外)
+- 业户决定不再用预存款 → [[close-with-zero-balance-decision]] / [[close-resident-moveout]]
+
+## 相关文档
+
+- [[consume-batch-auto-monthly]]
+- [[auto-deduction-design]]
+- [[deposit-additional-topup]]
+- [[close-resident-moveout]]
+- [[../deposit/audit-long-pending-accounts]](deposit 对应审计场景对比)
diff --git a/prop-acc/scenarios/prepaid/exception-cross-community-consume.md b/prop-acc/scenarios/prepaid/exception-cross-community-consume.md
new file mode 100644
index 0000000..3c8c84f
--- /dev/null
+++ b/prop-acc/scenarios/prepaid/exception-cross-community-consume.md
@@ -0,0 +1,197 @@
+---
+title: prop-acc · prepaid · 场景 - 跨社区消费防御
+aliases:
+ - 跨社区消费拦截
+ - 跨小区抵账单防御
+ - exception-cross-community-consume
+ - 场景-预存款跨社区消费防御
+tags:
+ - 场景
+ - prop-acc
+ - 预存款
+ - 异常
+audience:
+ - 业务人员
+ - 架构师
+status: 已发布
+sub_feature: prepaid
+last_review: 2026-05-25
+code_version: 2026-05-22
+---
+
+# 场景:跨社区消费防御
+
+业务人员**误选**了"A 社区业户预存款账户"去抵"B 社区账单",系统**直接拦截**,不允许。`PrepaidAccount::consume()` 模型方法内置 community 校验,任何调用方都跑不掉。
+
+## 典型情境
+
+> [!example] 真实情境
+> 业户陈先生在 A 社区(自住)和 B 社区(投资房出租)各有预存款账户:
+>
+> - A 社区账户余额 ¥5,000
+> - B 社区账户余额 ¥200
+>
+> B 社区刚出账单"出租房物业费 ¥800",**B 社区余额 ¥200 不够付**。业务人员小李心想"陈先生 A 社区还有 ¥5,000,先抵 B 社区账单凑合下,以后再给陈先生说"。他打开陈先生 A 社区账户,选 B 社区账单,点抵扣 —— **系统直接拦截**,提示"预存款账户与账单不在同一社区,无法抵扣"。
+
+## 业务人员视角
+
+### 您看到什么
+
+| 时刻 | 看到 |
+|---|---|
+| 进入 A 社区账户的 ViewPrepaidAccount | 状态 Active,balance 5000 |
+| `ConsumeAction` Modal 表单 → 账单下拉 | **下拉只显示 A 社区账单**(B 社区账单不在下拉里,UI 层已过滤) |
+| 如果硬调 API / tinker | 抛 InvalidArgumentException:"预存款账户与账单不在同一社区,无法抵扣" |
+
+### 三道防御
+
+防御层级与 [[exception-refund-on-frozen]] 类似:
+
+1. **UI 层**:Modal 的账单下拉**只显示当前账户所在 community 的账单**
+2. **Action 层**:`ConsumeFromPrepaidAccountAction` 入口校验 community 匹配
+3. **模型层**(最严):`PrepaidAccount::consume()` 内置 community 校验,抛 InvalidArgumentException
+
+```php
+// PrepaidAccount.php
+public function consume(Bill $bill, ...): PrepaidTransaction
+{
+ if ($bill->community_id !== $this->community_id) {
+ throw new InvalidArgumentException(
+ '预存款账户与账单不在同一社区,无法抵扣'
+ );
+ }
+ // ... 其余逻辑
+}
+```
+
+任何对 `PrepaidAccount::consume()` 的修改都会触发测试,确保守护不被无意中放宽。
+
+### 正确路径
+
+不同社区**独立处理**:
+
+| 业户场景 | 正确处置 |
+|---|---|
+| A 社区想抵 A 社区账单 | 用 A 社区账户(本场景正常情况)|
+| B 社区想抵 B 社区账单 | 用 B 社区账户 |
+| **A 社区有钱 + B 社区缺钱** | **业户先在 B 社区充值**(走 [[deposit-additional-topup]]),再用 B 社区账户抵 B 社区账单 |
+
+> [!info] 业务上能"A 社区退款 → 业户拿钱 → B 社区充值"吗?
+> 完全可以,但**是业户自己的操作**:
+>
+> 1. 业务人员从 A 社区账户退 ¥800 给业户
+> 2. 业户拿到 ¥800
+> 3. 业户在 B 社区充 ¥800
+> 4. B 社区账户抵账单
+>
+> 三步业务流程,**资金不直接跨社区流动** —— 各社区财务独立核算。
+
+## 为什么这条守护这么严
+
+> [!warning] 跨社区抵扣的灾难性后果
+>
+> 假设系统允许跨社区抵扣:
+>
+> | 反例 | 后果 |
+> |---|---|
+> | A 社区物业的钱**流出**到 B 社区物业 | 账面对不上,银行流水追溯困难 |
+> | A 社区财务报表显示"代收 B 社区物业费" | 越权,A 社区无权管 B 社区收款 |
+> | 业户提现:"我在 A 社区有 ¥1,000,在 B 社区抵 ¥1,000,然后从 A 社区退 ¥1,000" | A 社区净流出 ¥2,000(实际只该 ¥1,000)|
+> | 各社区物业可能独立公司化,跨社区抵扣 = 关联交易 | 法务 / 税务问题 |
+>
+> 每个物业项目独立财务核算是行业基本要求。**跨社区抵扣 = 财务边界破坏**。
+
+## 业户视角
+
+业户在小程序"我的预存款"看到自己有 A、B 两个独立账户。**互不联通**,各自余额、各自流水、各自抵扣范围。
+
+如果想跨社区"调资金",**只能业户自己做**:A 社区退款 → 自己拿钱 → B 社区充值。
+
+## 系统流程
+
+```mermaid
+sequenceDiagram
+ participant 业务
+ participant Filament
+ participant ConsumeFromPrepaidAccountAction
+ participant PrepaidAccount[A 社区账户]
+ participant Bill[B 社区账单]
+
+ Note over 业务: 业务人员误想抵跨社区
+
+ 业务->>Filament: 直接调 API 或 tinker:account_A.consume(bill_B, 800)
+ Filament->>ConsumeFromPrepaidAccountAction: handle(account_A, bill_B, 800)
+ ConsumeFromPrepaidAccountAction->>PrepaidAccount: consume(bill_B, 800)
+ PrepaidAccount->>PrepaidAccount: bill_B.community_id != self.community_id?
+ PrepaidAccount-->>ConsumeFromPrepaidAccountAction: throw InvalidArgumentException
+ ConsumeFromPrepaidAccountAction-->>Filament: 拦截 + 日志
+ Filament-->>业务: 报错:"预存款账户与账单不在同一社区,无法抵扣"
+
+ Note over PrepaidAccount,Bill: 无任何资金动作 / 流水产生
+```
+
+## 测试断言
+
+代码层有专门测试覆盖此异常路径:
+
+```php
+test('cannot consume cross-community bill', function () {
+ $accountA = PrepaidAccount::factory()->for($communityA)->create(['balance' => 5000]);
+ $billB = Bill::factory()->for($communityB)->create(['amount' => 800]);
+
+ expect(fn () => $accountA->consume($billB, 800))
+ ->toThrow(InvalidArgumentException::class, '预存款账户与账单不在同一社区');
+
+ expect($accountA->fresh()->balance)->toBe(5000.0); // 余额未变
+ expect(PrepaidTransaction::count())->toBe(0); // 流水未建
+});
+```
+
+## 常见问题
+
+> [!question] 同一物业公司管理多个社区,能不能允许跨社区抵?
+> **业务层面**也不行。即使物业公司一家,每个社区**独立财务核算**(看营业执照、税务登记)。除非:
+>
+> - 多社区合并财务(罕见,需法务批准)
+> - 业务方明确要求(走架构师评估)
+>
+> 当前设计假设"社区独立",未来若改变,需重新评估守护逻辑。
+
+> [!question] 业户在小程序操作时能看到跨社区账户吗?
+> 设计上**应该分开显示**。例如:
+>
+> ```
+> 我的预存款
+> ├── A 社区(自住):¥5,000
+> └── B 社区(投资):¥200
+> ```
+>
+> 不要混合显示总余额(避免业户误以为"跨社区可用")。
+
+> [!question] 业户跨社区调资金的体验差,有什么改进?
+> 长期可考虑:
+>
+> - **跨社区互转**:业户在小程序点"从 A 社区转 ¥1000 到 B 社区" → 系统两步操作(A 退 + B 充)+ 一张统一凭证
+> - 但**资金仍走业户**(银行 / 微信回退再充值),系统不直接跨社区流动
+> - 当前没有,需业务方推动
+
+> [!question] 业户失联,A 社区有钱,B 社区欠费严重,能挪吗?
+> **不能挪**。业户失联是业户的事,各社区独立催收。B 社区欠费走法务流程。
+
+## 与 deposit 的对比
+
+deposit 也有类似多账户(同业户可以有多种押金类型账户),但**没有跨社区消费场景**(押金不抵账单,不存在跨账户消费需求)。所以这条守护是 prepaid 独有。
+
+## 异常分支
+
+- 业务人员真的需要跨社区操作 → 业户自己走"A 退 + B 充"两步
+- 多社区合并财务的特殊业务 → 架构师评估后改设计(目前无)
+- 业务方提需求要跨社区抵扣 → 走架构评审,理由要充分
+
+## 相关文档
+
+- [[one-account-per-resident]]
+- [[consume-monthly-property-bill]]
+- [[consume-via-bill-collection-type]]
+- [[exception-refund-on-frozen]]
+- [[../cross/concepts/org-hierarchy|组织结构]]
diff --git a/prop-acc/scenarios/prepaid/exception-refund-on-frozen.md b/prop-acc/scenarios/prepaid/exception-refund-on-frozen.md
new file mode 100644
index 0000000..3f5bb3e
--- /dev/null
+++ b/prop-acc/scenarios/prepaid/exception-refund-on-frozen.md
@@ -0,0 +1,233 @@
+---
+title: prop-acc · prepaid · 场景 - 冻结状态退款被三层守护拦截
+aliases:
+ - 冻结状态退款被拒
+ - exception-refund-on-frozen
+ - 场景-冻结状态退款拦截
+tags:
+ - 场景
+ - prop-acc
+ - 预存款
+ - 异常
+audience:
+ - 业务人员
+ - 架构师
+status: 已发布
+sub_feature: prepaid
+last_review: 2026-05-25
+code_version: 2026-05-22
+---
+
+# 场景:冻结状态退款被三层守护拦截
+
+业务人员对 Frozen 账户**误**点退款 / 充值 / 消费 等按钮,系统**三层**(UI / Policy / 模型)守护拦截。**最严**在模型层 —— 即使绕过 UI 和 Policy,模型方法的 `canOperate()` 检查兜底,任何调用方都跑不掉。
+
+> [!info] 历史教训
+> 这是 issue.md Q4 第二轮明确修复的**严重漏洞**(项目第 9 项):原 `PrepaidAccount::refund()` 方法**完全不查状态**(只查金额),Frozen / Closed 账户都能被退款。模型层是最底层防御,Action 类绕过 = 没人挡住。修复后所有写入模型方法(`deposit / consume / refund`)统一调 `canOperate()`,严格只允许 Active。
+
+## 典型情境
+
+> [!example] 真实情境
+> 王女士预存款账户因风控异常被冻结(详见 [[freeze-suspected-fraud]]),余额 ¥50,000。王女士的"亲戚"找到物业说:"她身体不好,委托我领回余额。" 出示了模糊的身份证复印件(无授权书)。
+>
+> 物业职员小李没核实关系真实性,**直接打开账户点 RefundAction**。系统拦截:
+>
+> | 拦截层 | 触发 |
+> |---|---|
+> | UI 层(Filament Action visible)| `canOperate()` 返 false → 按钮**灰化**(理论上点不到)|
+> | Policy 层(`RefundAction->authorize('refund')`)| 即使绕 UI 直接调,Policy 拦 → 抛 AuthorizationException |
+> | 模型层(`PrepaidAccount::refund()`)| 即使绕 Policy 调模型方法,`canOperate()` 内置检查 → 抛 RuntimeException |
+>
+> 任何一层挡住即拦截。**模型层是最后兜底**,即使 tinker / artisan / 第三方包都跑不掉。
+
+## 业务人员视角
+
+### 您看到什么
+
+| 时刻 | 看到 |
+|---|---|
+| 进入 Frozen 账户的 ViewPrepaidAccount | 状态显示 🧊 Frozen |
+| 状态管理组按钮 | "充值 / 消费 / 退款 / 冻结 / 关账" 全部**灰化**,只剩 "解冻" 可点 |
+| 如果硬调 API | 抛错(看不同层抛哪个)|
+
+### 三道防御详解
+
+```mermaid
+flowchart TD
+ A[业务人员点 RefundAction] --> B[UI 层:button.visible
canOperate]
+ B -->|false| C[按钮灰化,点不到]
+ B -->|绕过 UI
直接调表单| D[Policy 层:->authorize 'refund'
PrepaidAccountPolicy::refund]
+ D -->|拒绝| E[抛 AuthorizationException]
+ D -->|绕过 Policy
直接调模型| F[模型层:account->refund
canOperate 检查]
+ F -->|拒绝| G[抛 RuntimeException
账户处于非可操作状态]
+```
+
+**三道独立** —— 任意一道挡住即拦截。
+
+### 业务上正确做法
+
+不要硬绕过。**沟通业户 + 走调解 / 法务流程**:
+
+| 业户/请求方反应 | 处理 |
+|---|---|
+| "我急用钱" | 解释:账户冻结调查中,需先完成风控核实 → [[unfreeze-after-verification]] |
+| "我证明身份了你为什么不退" | 核实材料是否充分(身份证复印件不够,需原件 + 当面确认 + 业户授权书)|
+| "我亲戚委托我" | 委托关系需公证书,否则**绝不退**(常见欺诈套路)|
+| 真的本人核实通过 | 走 [[unfreeze-after-verification|解冻]] → 解冻后可正常退款 |
+
+## 三层守护的代码层面
+
+### UI 层(Filament Action)
+
+```php
+RefundAction::make()
+ ->visible(fn (PrepaidAccount $record) =>
+ $record->canOperate() && $record->balance > 0
+ )
+```
+
+Frozen → `canOperate()=false` → 按钮 hidden。
+
+### Policy 层
+
+```php
+// PrepaidAccountPolicy.php
+public function refund(AuthUser $user, PrepaidAccount $record): bool
+{
+ return $user->can('update prepaid accounts')
+ && $record->canOperate()
+ && $record->hasBalance();
+}
+```
+
+`RefundAction::make()->authorize('refund')` 触发此方法,Frozen → 返 false → 抛 AuthorizationException。
+
+### 模型层(最严)
+
+```php
+// PrepaidAccount.php
+public function refund(float $amount, ...): PrepaidTransaction
+{
+ if (! $this->canOperate()) {
+ throw new RuntimeException(
+ "账户处于 {$this->status->value} 状态,无法操作"
+ );
+ }
+ // ... 其余逻辑
+}
+```
+
+任何调用方(Filament Action / Action 类 / tinker / artisan / 测试)调 `$account->refund()`,模型层 `canOperate()` 检查兜底。
+
+## 系统流程(API 被绕过的极端情况)
+
+```mermaid
+sequenceDiagram
+ participant 调用方[非 Filament 调用方]
+ participant Action[RefundFromPrepaidAccountAction]
+ participant Model[PrepaidAccount]
+ participant DB
+
+ Note over 调用方: 例如 tinker:RefundFromPrepaidAccountAction::handle(frozen_account, 5000)
+
+ 调用方->>Action: handle(frozen_account, 5000, channel)
+ Action->>Model: refund(5000)
+ Model->>Model: canOperate()? Frozen → false
+ Model-->>Action: throw RuntimeException
+ Action-->>调用方: 抛出 + 日志记录
+
+ Note over DB: 无任何写入,事务自动回滚
+```
+
+## 测试断言
+
+```php
+test('cannot refund on frozen account', function () {
+ $account = PrepaidAccount::factory()->frozen()->create(['balance' => 5000]);
+
+ // 模型层
+ expect(fn () => $account->refund(2000))
+ ->toThrow(RuntimeException::class, '无法操作');
+
+ // Action 层
+ expect(fn () => app(RefundFromPrepaidAccountAction::class)
+ ->handle($account, 2000, $channel))
+ ->toThrow(RuntimeException::class);
+
+ expect($account->fresh()->balance)->toBe(5000.0); // 余额未变
+ expect(PrepaidTransaction::count())->toBe(0); // 流水未建
+});
+
+test('cannot consume on frozen account', function () {
+ // 同样的三层守护
+});
+
+test('cannot deposit on frozen account', function () {
+ // 同样的三层守护
+});
+```
+
+**3 个测试覆盖三种写入操作的 Frozen 状态拒绝**,确保守护不被无意中放宽。
+
+## 与 deposit 的对比
+
+deposit 模块同样三层守护,但**守护方法粒度不同**:
+
+| 模块 | 模型层守护方法 |
+|---|---|
+| deposit | `canDeposit()` / `canWithdraw()`(分二种)|
+| prepaid | `canOperate()`(统一)|
+
+理由:deposit 业务上有"只能加不能减"的中间状态(理论上 Frozen 时**加**没问题?现已收紧为都不允许)。prepaid 设计简单,统一拒绝所有写入。
+
+详见 [[account-state-machine]] "canOperate 是模型层的最严防御" 段。
+
+## 常见问题
+
+> [!question] 业户特别紧急要钱(如医疗),冻结状态能绕过吗?
+> **绝对不能从系统层绕**。业务流程上:
+>
+> 1. 业务人员加急核实业户身份 + 紧急情况
+> 2. 走 [[unfreeze-after-verification|解冻]] 流程
+> 3. 解冻后立即 RefundAction
+>
+> 整个流程**1-2 小时内可完成**,比"擅自绕守护退款"的合规风险小得多。
+
+> [!question] 三层守护是不是过度设计了?一层就够吧?
+> **不是过度**。每一层都有可能被绕:
+>
+> - UI 灰化 → 用户可能开浏览器开发者工具篡改 DOM
+> - Policy → API 调用者可能直接调 Action 类(绕过 Filament Action)
+> - **模型层** → tinker / artisan / 测试 / 第三方包都直接操作模型
+>
+> 多层独立 = 任意一层挡住即安全。代码层面成本极低(每层一两行)。
+
+> [!question] 为什么 prepaid 的修复是"第二轮"才做?
+> issue.md Q4 提到的"第一轮"是 prepaid 模块刚做时,只有 `voidReverse` 一个 Policy 方法(其他都没)。第二轮全面审查发现这个**严重漏洞**(模型层不查状态),补齐了 9 个 Policy 方法 + 模型方法守护 + 测试。
+
+> [!question] 已发现的所有漏洞都补齐了吗?
+> 详见 issue.md Q4 "第二轮已落地 (2026-05-22)" 段,8 项修复:
+>
+> 7. 删 DeleteAction / DeleteBulkAction
+> 8. Policy 从 1 个补到 9 个
+> 9. **`PrepaidAccount.refund()` 加 canOperate 守护(最严重)**
+> 10. RefundAction UI 守护改为 canOperate && balance>0
+> 11. 6 个 Filament Action 加显式 authorize 调用
+> 12. EditAction 加 visible 守护
+> 13. PrepaidAccount.hasBalance() 辅助方法
+>
+> 当前未发现其他漏洞,但任何重要修改都应增量做安全审计。
+
+## 异常分支
+
+- 业务上需要退冻结账户 → 先 [[unfreeze-after-verification]]
+- 业户失联无法核实 → 留 Frozen,等业户出现
+- 冻结期间充值被拦 → 同样三层守护,处理一致
+
+## 相关文档
+
+- [[freeze-suspected-fraud]]
+- [[unfreeze-after-verification]]
+- [[account-state-machine]]
+- [[exception-cross-community-consume]]
+- [[../deposit/account-state-machine]](deposit 状态机对比)