255 lines
8.7 KiB
Markdown
255 lines
8.7 KiB
Markdown
---
|
||
title: prop-acc · billing · 周期账单生成机制
|
||
aliases:
|
||
- 周期账单生成
|
||
- 月度物业费生成
|
||
- GeneratePeriodicBillsAction
|
||
- BillingMergeStrategy
|
||
- 合单策略
|
||
tags:
|
||
- 概念
|
||
- prop-acc
|
||
- 账单
|
||
- 业务流程
|
||
audience:
|
||
- 业务人员
|
||
- 财务
|
||
- 架构师
|
||
status: 已发布
|
||
sub_feature: billing
|
||
last_review: 2026-05-26
|
||
code_version: 2026-05-22
|
||
---
|
||
|
||
# 周期账单生成机制
|
||
|
||
物业费、停车费、电视费等**按周期(月 / 季 / 年)固定收**的账单,通过 **`GeneratePeriodicBillsAction`** 批量生成。配合 **`BillingMergeStrategy` 枚举**决定"同业户多个费用类型是否合并到一张账单"。
|
||
|
||
## 业务场景
|
||
|
||
每月 1 日,物业为本社区 **300 户业主**生成本月物业费账单(每户 ¥800 起步,按房屋面积浮动)。系统应:
|
||
|
||
- 自动算每户的金额(按 RatePlan + 房屋面积)
|
||
- 批量建 Bill(300 张)
|
||
- 各张 Bill 期次清晰(billing_period 5/1 - 5/31)
|
||
- 不重复生成(本月已生成的不再生成)
|
||
- 业务人员收到结果报告
|
||
|
||
## 批量生成流程
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A[业务人员触发 GeneratePeriodicBillsAction] --> B[Modal 输入参数]
|
||
B --> C[参数:期次 / 费用类型 / 社区范围 / 合并策略]
|
||
C --> D[扫描候选业户清单]
|
||
|
||
D --> E[每户:计算应付金额]
|
||
E --> F{已有本期 Bill?}
|
||
|
||
F -->|否| G[建新 Bill]
|
||
F -->|是,根据策略| H{合并策略}
|
||
|
||
H -->|MERGE 合单| I[追加到既有 Bill]
|
||
H -->|SKIP 跳过| J[不动]
|
||
H -->|REPLACE 替换| K[作废旧 Bill + 建新]
|
||
|
||
G --> L[报告完成]
|
||
I --> L
|
||
J --> L
|
||
K --> L
|
||
```
|
||
|
||
## `GeneratePeriodicBillsAction` 参数
|
||
|
||
业务人员触发(`Bills` List 页 → 顶部 "批量生成周期账单" 按钮):
|
||
|
||
| 参数 | 说明 |
|
||
|---|---|
|
||
| **期次(billing_period)** | 如 "2026 年 5 月",决定 start / end |
|
||
| **费用类型(fee_type_id)** | 物业费 / 停车费 / 有线电视费 / ... |
|
||
| **社区范围(community_id)** | 单社区 / 全平台 |
|
||
| **业户范围** | 单个业户 / 全社区业户 / 自定义清单 |
|
||
| **合并策略(merge_strategy)** | MERGE / SKIP / REPLACE |
|
||
| 备注 | 选填 |
|
||
|
||
提交后系统扫描候选业户清单,逐户计算 + 建账。
|
||
|
||
## `BillingMergeStrategy` 枚举
|
||
|
||
> [!info] 实际枚举值看 `packages/prop-acc/src/Enums/BillingMergeStrategy.php`
|
||
> 可能值(推测):
|
||
>
|
||
> | 值 | 含义 |
|
||
> |---|---|
|
||
> | `SkipExisting` | 同业户同期次已有 Bill → 跳过(不重复生成)|
|
||
> | `Merge` | 追加到既有 Bill(同业户同期次的不同费用类型合一张)|
|
||
> | `Replace` | 作废旧 Bill + 建新(罕见,数据修复用)|
|
||
>
|
||
> 默认策略通常是 **`SkipExisting`**(最安全)。
|
||
|
||
## 三种策略对照
|
||
|
||
### 策略 1:SkipExisting(默认,推荐)
|
||
|
||
业务人员 5 月 1 日点击"生成 5 月物业费"。系统:
|
||
|
||
- 张阿姨已经有 5 月物业费 Bill → **跳过**(避免重复)
|
||
- 陈先生没有 5 月物业费 Bill → 新建
|
||
|
||
**适用**:正常月度操作,业务人员不确定是否之前已经生成过,跳过最安全。
|
||
|
||
### 策略 2:Merge(合单)
|
||
|
||
业务人员同时为业户生成"5 月物业费 + 5 月电视费"。系统:
|
||
|
||
- 找到张阿姨已有 5 月物业费 Bill(¥800)
|
||
- **追加电视费 ¥40 到同一张 Bill**(amount: 800 + 40 = 840)
|
||
- 业户收到**一张账单 ¥840**(明细两项)
|
||
|
||
**适用**:多种费用类型一起发(减少业户收到的账单数,体验好)。
|
||
|
||
> [!warning] Merge 的局限
|
||
> 合并后**单一 Bill 的金额 = 各费用类型总和**,但**关联的 sourceable** 怎么处理?Bill 表 sourceable 字段是 1:1。**当前实现可能**:
|
||
> - 不挂 sourceable(`sourceable_type=null`)
|
||
> - 或挂主费用类型的 source
|
||
>
|
||
> 具体看代码。Merge 策略实施细节复杂,生产环境**谨慎用**。
|
||
|
||
### 策略 3:Replace(替换)
|
||
|
||
业务人员发现 5 月物业费金额算错了(RatePlan 改过),要全部重算:
|
||
|
||
- 找到张阿姨已有的 5 月物业费 Bill
|
||
- **作废**(VoidBillAction,状态翻 Void)
|
||
- **重新生成**新 Bill(按新 RatePlan)
|
||
|
||
**适用**:数据修复场景(罕见,需高权限 + 必填原因)。
|
||
|
||
> [!warning] Replace 的风险
|
||
> 已付的 Bill 作废后,业户已经付的钱**怎么办**?
|
||
>
|
||
> - Bill 状态翻 Void
|
||
> - 已付的 CollectionOrderBill 关联保留(审计需要)
|
||
> - 但业户的钱 = 物业账面多收了
|
||
> - **必须走退款**(建红字 CollectionOrder)
|
||
>
|
||
> Replace 策略**只对 Unpaid Bill 使用相对安全**;Paid Bill 慎用,需配套退款流程。
|
||
|
||
## 金额计算
|
||
|
||
周期账单金额怎么算?取决于 RatePlan 配置:
|
||
|
||
| 算法 | 说明 |
|
||
|---|---|
|
||
| **固定金额** | 每户每月固定 ¥800(简单)|
|
||
| **按面积** | 房屋面积 × 单价(例如物业费 ¥3 / m² / 月)|
|
||
| **按车位** | 每个停车位固定金额(停车费)|
|
||
| **按设备** | 每台空调 ¥X / 月(中央空调费)|
|
||
| **阶梯** | 类似计量账单的阶梯(罕见)|
|
||
|
||
具体看 `RatePlan` 的配置 + 业务计算逻辑。可能在 `PeriodicBillGenerationService`(若有)实现,或在 Action 内联。
|
||
|
||
## 生成的 Bill 字段
|
||
|
||
每张生成的 Bill:
|
||
|
||
| 字段 | 值 |
|
||
|---|---|
|
||
| `bill_no` | 自动编号(`B-202605-501-001` 或类似)|
|
||
| `community_id` | 所属社区 |
|
||
| `asset_id` | 业户房屋 |
|
||
| `resident_id` | 业户 |
|
||
| `fee_type_id` | 费用类型 |
|
||
| `bill_type` | `Periodic`(枚举) |
|
||
| `amount` | 算出的金额 |
|
||
| `paid_amount` | 0(刚生成,未付)|
|
||
| `status` | `Unpaid` |
|
||
| `billing_period_start` | 期次开始(2026-05-01)|
|
||
| `billing_period_end` | 期次结束(2026-05-31)|
|
||
| `due_at` | 到期日(通常本期末 + 宽限期,如 2026-06-15)|
|
||
| `sourceable_*` | null(周期账单通常无 source)|
|
||
|
||
## 业户视角
|
||
|
||
业户每月初(或月底,看物业策略)收到推送:
|
||
|
||
> 张阿姨您好,您的 2026 年 5 月物业费 ¥800 已生成。请于 6 月 15 日前付清。
|
||
|
||
业户可选:
|
||
|
||
- 现金 / 微信付 → 走 [[collect-payment-single]]
|
||
- 预存款抵 → 走 [[collect-via-prepaid-auto]](自动)
|
||
- 与其他账单一起付 → 走 [[collect-payment-batch]]
|
||
- 暂时不付 → 到期日后变逾期,走 [[exception-overdue-bills]] 催收
|
||
|
||
## 业务人员视角
|
||
|
||
### 月初执行
|
||
|
||
每月 1-3 日,业务人员王主管:
|
||
|
||
1. 打开 BillsListPage → 顶部 "生成周期账单"
|
||
2. 选费用类型(物业费 / 停车费 / 等)+ 期次 + 策略 + 备注
|
||
3. 提交 → 系统扫描 + 生成 → 报告"已生成 N 张账单,跳过 M 张"
|
||
4. 抽样核对几张 Bill 的金额 + 业户
|
||
|
||
### 异常处置
|
||
|
||
| 异常 | 处置 |
|
||
|---|---|
|
||
| 某户 RatePlan 配置缺失 | 跳过该户 + 在报告里标记 → 单独建 RatePlan + 单独生成 |
|
||
| 某户已有同期 Bill(被跳过)| 看是否需要 Merge / Replace |
|
||
| 全部失败(系统错)| 联系运维查日志 |
|
||
|
||
## 自动化的可能
|
||
|
||
issue.md Q6 未列入"待补",但业务上可能需要:
|
||
|
||
- **Scheduled job 月初自动跑** — 不需要业务人员手动触发
|
||
- **跟 prepaid 自动抵扣 job 串联** — 账单生成 → 立即触发 [[../prepaid/auto-deduction-design|预存款自动抵]] → 业户感受"无感扣账单"
|
||
|
||
当前**仍是手动触发**。
|
||
|
||
## 常见问题
|
||
|
||
> [!question] 同业户同期次同费用类型能有两张 Bill 吗?
|
||
> 业务上**不应该**(违反业务规则:一户一月一物业费)。系统层面看是否有 unique 约束:
|
||
>
|
||
> ```sql
|
||
> UNIQUE INDEX (community_id, resident_id, fee_type_id, billing_period_start)
|
||
> ```
|
||
>
|
||
> 若有 → 数据库层挡住重复;若无 → 全靠 Action 的 SkipExisting 策略判断,理论上可能因并发产生重复。
|
||
|
||
> [!question] 业户搬走中途,本月物业费要不要全收?
|
||
> 看物业策略 + 合同约定:
|
||
>
|
||
> - **全月收**(简单,业户合同到月底)
|
||
> - **按天分摊**(按搬走前的天数算)
|
||
> - **不收**(罕见)
|
||
>
|
||
> 系统**当前简版**通常是全月收。按天分摊需要业务层算法支持。
|
||
|
||
> [!question] 周期账单生成后才发现 RatePlan 配错怎么办?
|
||
> 走 Replace 策略**只对 Unpaid 安全**。如果已付:
|
||
>
|
||
> - 看错的金额方向:
|
||
> - 收多了 → 退差额(走 [[void-paid-bill]] 类似流程)
|
||
> - 收少了 → 补开账单(新建一张 ¥差额 的临时账单)
|
||
|
||
> [!question] 批量生成多久?300 户 5 秒?
|
||
> 单户 ~50-100ms(查 RatePlan + 查业户 + 建 Bill + 日志)。300 户**顺序执行** ~15-30 秒。可优化为并发(若业务量大)。
|
||
|
||
> [!question] 周期账单和计量账单可以一起生成吗?
|
||
> 不在同一个 Action(周期是 `GeneratePeriodicBillsAction`,计量是 [[../meter/bill-generation-pipeline|GenerateBillsFromMeterReadingsAction]])。**业务流程上**业务人员通常先做完抄表 + 计量账单生成 → 再触发周期账单生成。
|
||
|
||
## 相关文档
|
||
|
||
- [[bill-six-state-machine]]
|
||
- [[bill-types-and-sources]]
|
||
- [[bill-vs-collection-order]]
|
||
- [[create-periodic-property-fee]]
|
||
- [[create-meter-bill-auto]]
|
||
- [[../meter/bill-generation-pipeline]](类似的 Calculator + Service + Action 模式)
|
||
- [[../prepaid/auto-deduction-design]](账单生成后的下游消费)
|