vault backup: 2026-05-25 23:37:58
This commit is contained in:
197
prop-acc/scenarios/prepaid/exception-cross-community-consume.md
Normal file
197
prop-acc/scenarios/prepaid/exception-cross-community-consume.md
Normal file
@@ -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|组织结构]]
|
||||
Reference in New Issue
Block a user