Files
uniprop-manual/prop-acc/concepts/prepaid/auto-deduction-design.md
2026-05-25 23:02:51 +08:00

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 · 月初批量自动抵扣设计
自动抵扣设计
月初批量抵账单
auto-deduction-design
预存款自动消费 job
概念
prop-acc
预存款
架构设计
待补
业务人员
架构师
草稿 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 执行的完整时序
  • 业户/业务人员在何处感知结果
  • 失败排查
  • 业务人员的运维介入入口

相关文档