Files
uniprop-manual/prop-acc/concepts/billing/periodic-bill-generation.md
2026-05-26 00:48:12 +08:00

255 lines
8.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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]](账单生成后的下游消费)