6.8 KiB
6.8 KiB
title, aliases, tags, audience, status, sub_feature, last_review, code_version
| title | aliases | tags | audience | status | sub_feature | last_review | code_version | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| prop-acc · prepaid · 月初批量自动抵扣设计 |
|
|
|
草稿 | prepaid | 2026-05-25 | 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. 扫描范围
-- 候选预存款账户
SELECT id, community_id, community_user_profile_id, balance
FROM acc_prepaid_accounts
WHERE status = 'active'
AND balance > 0;
3. 对每个候选账户,找未付账单
-- 该业户未付账单(按到期日升序,先抵最早的)
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. 按账户余额贪心抵扣
# 伪代码
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
// 伪代码 — 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"。
数据流(自动抵扣场景)
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 执行的完整时序
- 业户/业务人员在何处感知结果
- 失败排查
- 业务人员的运维介入入口