--- 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]](账单生成后的下游消费)