Files
uniprop-manual/prop-acc/concepts/billing/periodic-bill-generation.md

255 lines
8.7 KiB
Markdown
Raw Permalink Normal View History

2026-05-26 00:48:12 +08:00
---
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]](账单生成后的下游消费)