--- title: prop-acc · prepaid · 月初批量自动抵扣设计 aliases: - 自动抵扣设计 - 月初批量抵账单 - auto-deduction-design - 预存款自动消费 job tags: - 概念 - prop-acc - 预存款 - 架构设计 - 待补 audience: - 业务人员 - 架构师 status: 草稿 sub_feature: prepaid last_review: 2026-05-25 code_version: 2026-05-22 --- # 月初批量自动抵扣设计(待补) > [!warning] 本功能**代码未实现** > 当前所有 consume 操作都是**业务人员手动触发**(在后台 `ConsumeAction`)。月初批量自动抵扣**是预存款产品的核心卖点之一**,但代码层暂未实现。本文档描述设计意图,等业务方明确需求后落地。 > > issue.md Q4 "待补" 段记录: > > 月初批量自动抵扣 scheduled job:扫所有 status=Active AND balance>0 的预存款账户,找各自社区/业户下未付账单,自动调用 ConsumeFromPrepaidAccountAction 抵扣(优先抵 due_at 最早的账单)。 ## 为什么这个 job 重要 预存款的**产品价值主张**是: > "业户预存一次,以后账单自动扣,不用月月手动操作"。 如果没有自动抵扣 job: - 业户预存了 ¥5000,以为以后账单会自动扣 - 月底账单出来,**不会自动扣**,需要业务人员手动到每个账户上点 ConsumeAction - 业户次月发现还欠物业费,余额还有 → 投诉 - 业务人员手动抵扣 100+ 户 → 工作量大,容易遗漏 **没有 job = 产品价值缺失 50%**。 ## 设计意图(待实现) ### 1. 触发时机 - **月初**(每月 1 日 00:30,避开 0 点高峰) - 或**账单生成后立即触发**(若账单生成本身也在月初,可串行) ### 2. 扫描范围 ```sql -- 候选预存款账户 SELECT id, community_id, community_user_profile_id, balance FROM acc_prepaid_accounts WHERE status = 'active' AND balance > 0; ``` ### 3. 对每个候选账户,找未付账单 ```sql -- 该业户未付账单(按到期日升序,先抵最早的) SELECT id, amount, due_at, bill_type FROM acc_bills WHERE community_id = ? -- 与账户同社区(跨社区不抵) AND resident_id = ? -- 业户档案 AND status = 'unpaid' ORDER BY due_at ASC; ``` ### 4. 按账户余额贪心抵扣 ```python # 伪代码 balance = account.balance for bill in unpaid_bills: if balance <= 0: break if balance >= bill.amount: consume(account, bill, bill.amount) # 全额抵 balance -= bill.amount else: consume(account, bill, balance) # 部分抵(若 Bill 支持) balance = 0 ``` > [!info] 是否支持部分抵扣? > 当前 `ConsumeFromPrepaidAccountAction` 看实现是按完整账单金额走的。**部分抵扣**(余额不够全付时抵一部分)需要 `Bill.recordPayment()` 支持部分支付才能实现。若不支持,余额不够时**跳过该账单**,等下月或业户补充其他方式付。 ### 5. 复用既有 Action ```php // 伪代码 — Job 内调用既有 Action 类 app(ConsumeFromPrepaidAccountAction::class)->handle( account: $account, bill: $bill, amount: $consumeAmount, ); ``` **关键**:Job 不重新实现 consume 逻辑,直接调 `ConsumeFromPrepaidAccountAction` —— 与 Filament 手动触发**走同一份代码**。守护、事务、Receipt 生成全都自动复用。 ### 6. 失败容忍 - 单笔抵扣失败(余额不够 / 账户 Frozen / 数据异常) → 跳过,继续下一个 - 整个 Job 失败 → 记录到 `failed_jobs` 表,可重跑 - 不允许"全部成功才提交" —— 部分账户的抵扣成功不应因其他账户失败回滚 ### 7. 通知策略(设计待定) | 业户场景 | 通知 | |---|---| | 余额充足,账单全抵 | 推送"5 月账单已自动抵扣,余额还有 ¥X" | | 余额不够,部分抵 | 推送"已抵 ¥X,还差 ¥Y 请补缴" | | 账户冻结无法抵 | 不主动通知(等业户自己看到欠费) | ## 与手动 ConsumeAction 的关系 | 维度 | 手动 ConsumeAction(已实现)| 自动 Job(待实现)| |---|---|---| | 触发 | 业务人员后台点击 | Scheduled job(crontab / Laravel Scheduler)| | 单次范围 | 单个账户 + 单张账单 | 全社区所有账户 + 所有未付账单 | | 业务上 | 个别情况(业户主动来抵)| 月度默认行为(产品价值核心)| | 代码 | `Filament/Resources/.../Actions/ConsumeAction.php` + `Actions/Prepaids/ConsumeFromPrepaidAccountAction` | 待添加 `Console/Commands/PrepaidAutoDeductionCommand.php`(或类似) | | 复用业务 Action | ✅ 直接调 ConsumeFromPrepaidAccountAction | ✅ 同上 | 二者**共用业务层 Action**,只是触发方式不同。这是为什么 issue.md Q4 强调"未来批量自动抵扣 job 可直接复用此 Action"。 ## 数据流(自动抵扣场景) ```mermaid sequenceDiagram participant Scheduler participant Job participant Account[PrepaidAccount] participant Bill[Bill] participant Consume[ConsumeFromPrepaidAccountAction] participant 数据库 Note over Scheduler: 每月 1 日 00:30 Scheduler->>Job: dispatch PrepaidAutoDeductionJob Job->>数据库: SELECT 全部 Active 余额>0 的预存款账户 数据库-->>Job: [account1, account2, ..., accountN] loop 对每个 account Job->>数据库: SELECT 该业户未付账单 ORDER BY due_at 数据库-->>Job: [bill1, bill2, ...] loop 对每张账单 alt 余额够付 Job->>Consume: handle(account, bill, full_amount) Consume->>数据库: 建 CO + PrepaidTransaction + 更新 Bill else 余额不够 Job->>Job: 跳过(或部分抵,看 Bill 支持) end end end Job->>Scheduler: 完成 + 报告(抵扣总额、失败数) ``` ## 待讨论 / 决策 业务方拍板前,以下问题需明确: | 问题 | 选项 | |---|---| | **触发频率** | 每月 1 次 / 每周 / 每天扫(更及时但更频繁)| | **触发时点** | 月初固定时间 / 账单生成事件触发 / 业户手动充值后立即触发本户 | | **优先级排序** | due_at 升序(最早的先) / amount 升序(小额先抵清) / 账单类型(物业费先 → 水电费后)| | **部分抵扣** | 支持 / 不支持 / 取决于账单类型 | | **失败通知** | 业户立即通知 / 月底汇总通知 / 仅后台告警 | | **跨社区策略** | 跨社区一律不抵(已确认) / 跨社区可抵(需重新设计) | | **运维监控** | 抵扣金额 / 失败率 / 跳过原因分布 | | **回滚机制** | 抵错怎么办?(理论上事务保证,但业务上若抵了不该抵的账单需手工补正) | ## 关联场景 待 job 实现后,场景文档 [[consume-batch-auto-monthly]] 会描述: - Job 执行的完整时序 - 业户/业务人员在何处感知结果 - 失败排查 - 业务人员的运维介入入口 ## 相关文档 - [[transaction-types]] - [[consume-via-bill-collection-type]] - [[consume-monthly-property-bill]] - [[consume-multiple-bills-priority]] - [[consume-batch-auto-monthly]] - [[one-account-per-resident]]