本文整理 DGJ2.0 对账单 V2 从策略配置、客户绑定、任务生成、账单快照、核对/取消核对、文件导出、微信发送、失败重发到收款状态同步的完整业务链。

对账单不是实时查询页面,而是把某个客户、某段时间内的销售、退货、收款和收款撤销固化为可发送、可追溯的账单快照。它跨越策略表、任务表、64 个账单分表、客户微信绑定、发送记录、RabbitMQ 和销售收款状态。

阅读后应能回答:

  • 日账单和月账单的统计区间、执行时间如何计算。
  • 一个服务站为什么只能有一条日策略和一条月策略。
  • 客户绑定/解绑或策略改期后,旧任务如何同步。
  • 自动任务如何从待执行变成 MQ 消息,再生成账单。
  • 账单主表、业务明细和商品明细为什么都按 sid%64 分表。
  • 草稿、已核对、未发送、发送失败、未收款、部分收款、已收款分别属于哪套状态。
  • 重新生成为什么是“软删旧快照 + 创建新快照”,而不是原地覆盖。
  • 发送记录为何可能成功、部分目标失败,重发会新建什么。
  • 销售收款和撤销收款如何回算一个或多个对账单。
  • 出现“任务成功但没有文件”“发送成功但账单仍未收款”等现象时如何排查。
结论先行:生成任务状态、账单核对状态、机器人发送状态、发送记录状态和收款状态是五套并行状态。它们有关联,但不能互相替代。发送成功只说明消息链路成功;收款状态必须根据账单关联的销售出库单逐单回算。

1. 业务范围

1.1 本文覆盖

能力主要入口形成的业务事实
策略管理reports/StatementStrategy日/月策略、周期、时间、单据类型
客户绑定bindCustomers/unbindCustomers客户使用哪条策略
任务初始化StatementTasks::statementInitDay每客户每策略执行任务
到期投递sendMqBatch生成任务进入 MQ
自动生成StatementNotify::createStatementBill64 分片账单快照
手工生成StatementBill::createByUser指定客户和区间快照
重新生成reGenerate旧账单软删、新账单创建
核对/取消check/cancelCheck账单确认状态和源单关联
文件导出exportExcel、PDF/PNGOSS/文件路径
单发/批量发sendMsg/sendMsgBatch发送记录和 MQ
重发resend新发送记录
收款回算updateStatementReceiveStatus未收/部分/已收
无需发送syncNoNeedSendRecords明确记录本周期无账单/无欠款

1.2 与报表专题的区别

《22_报表口径专题》回答“数据口径从哪里来、金额怎么算”。本文回答“策略何时触发、如何固化账单、如何核对发送和回算收款”。前者偏查询口径,后者偏状态和任务。

2. 参与者与系统边界

参与者职责
服务站管理员配置日/月策略、绑定客户
财务/业务人员手工生成、核对、取消核对、发送
定时任务初始化任务、扫描到期任务、无账单记录、失败告警
RabbitMQ 消费者策略同步、账单生成、发送、收款状态更新
销售/收款链路提供出库、退货、收款和撤销收款事实
客户微信绑定决定个人/群聊接收目标
文件/对象存储保存 PDF、PNG、Excel
flowchart TB
  ADMIN["服务站管理员"] --> STR["策略与客户绑定"]
  CRON["定时任务"] --> TASK[("bill_task")]
  STR --> TASK
  TASK --> MQ1["账单生成 MQ"]
  MQ1 --> GEN["StatementSer"]
  GEN --> BILL[("账单64分片")]
  BILL --> CHECK["核对/取消核对"]
  CHECK --> SEND[("发送记录")]
  SEND --> MQ2["发送 MQ"]
  MQ2 --> ROBOT["微信机器人/客户"]
  SALE["销售与收款"] --> MQ3["收款状态 MQ"]
  MQ3 --> BILL

3. 代码地图

3.1 控制器

控制器主要方法责任
reports/StatementStrategy.php列表、创建、编辑、启停、绑定策略配置
reports/StatementBill.php生成、详情、核对、发送、重发账单业务入口
reports/StatementSend.phpcreateTask/getProgress独立发送任务入口
reports/StatementCustomerWechat.php绑定、解绑、设置接收人管理
tasks/StatementTasks.php日初始化、投 MQ、失败通知调度入口
tasks/StatementStrategyConsumer.phpstatementStrategySync策略任务同步
tasks/StatementNotify.php生成、收款状态账单消费者
tasks/StatementSendConsumer.phpstatementSend发送消费者
inner/Statement.php单/批量创建和账单视图内部服务入口

3.2 Service

Service责任
StrategyService策略 CRUD、客户关系、任务重排
StatementTaskSer创建计划任务、到期投递、无需发送记录
StatementSer构建账单、状态、收款回算、发送前业务
SendService创建发送记录、接收人、PDF、发送、重发
StatementExportServicePDF/PNG/Excel 视图和文件
NeedStatementUserSer记录发生出库/退货/收款的客户

4. 表和分片

4.1 核心表

常量物理表是否分片用途
T_BS_STATEMENT_STRATEGYt_bs_statement_strategy_v2否策略
T_BS_STATEMENT_STRATEGY_RELATIONt_bs_statement_strategy_relation_v2否策略客户关系
T_BS_STATEMENT_BILL_TASKt_bs_statement_bill_task_v2否生成任务
T_BS_NEED_STATEMENT_USERt_bs_need_statement_user否有对账变化客户
T_BS_STATEMENT_BILLt_bs_statement_bill_v2_{sid%64}64账单主表
T_BS_STATEMENT_BILL_DETAILt_bs_statement_bill_detail_v2_{sid%64}64源单明细
T_BS_STATEMENT_BILL_DETAIL_ITEMt_bs_statement_bill_detail_item_v2_{sid%64}64商品明细
T_BS_STATEMENT_SEND_RECORDt_bs_statement_send_record_v2否发送任务和结果
机器人绑定绑定模型对应表否客户个人/群聊绑定

4.2 关系

erDiagram
  STRATEGY ||--o{ STRATEGY_RELATION : "strategy_id"
  CONTACT ||--o{ STRATEGY_RELATION : "contact_id"
  STRATEGY_RELATION ||--o{ BILL_TASK : "客户策略周期"
  BILL_TASK ||--o| BILL : "bill_id"
  BILL ||--|{ BILL_DETAIL : "bill_id"
  BILL_DETAIL ||--o{ BILL_DETAIL_ITEM : "bill_detail_id"
  BILL ||--o{ SEND_RECORD : "statement_id"
  CONTACT ||--o{ ROBOT_BINDING : "contact_id"

4.3 分片纪律

账单主表、明细、商品明细必须使用同一个 sid%64 后缀。账单 ID 本身不足以确定物理表,排查 API 必须同时拿到 sid。

shard = sid % 64
主表   = t_bs_statement_bill_v2_{shard}
明细   = t_bs_statement_bill_detail_v2_{shard}
商品项 = t_bs_statement_bill_detail_item_v2_{shard}

5. 五套状态

5.1 策略状态

值含义
0禁用
1启用

5.2 生成任务状态

值常量含义
0TASK_STATUS_UNDO未执行
1TASK_STATUS_DONE已投递/执行中语义
2TASK_STATUS_SUCCESS生成成功
3TASK_STATUS_FAILED生成失败

5.3 账单核对状态

值含义
0草稿
1已核对

5.4 收款状态

值含义
0未收款
1部分收款
2已收款

5.5 发送记录状态

值含义
1待处理
2处理中
3成功
4失败
5无需发送

账单主表还有 robot_status:0 未发送、1 已发送、2 失败。它是账单级汇总,不等同于每一条发送记录状态。

flowchart LR
  T["任务 0/1/2/3"] --> B["账单 草稿/已核对"]
  B --> R["robot_status 0/1/2"]
  B --> S["发送记录 1/2/3/4/5"]
  B --> P["收款 0/1/2"]
  S -.不代表.-> P
  R -.不代表.-> P

6. 策略配置

6.1 策略类型

值类型典型范围
1日账单当日截止生成时刻,或前一自然日
2月账单上一个自然月

一个服务站只允许一条日策略和一条月策略。已绑定客户的策略禁止修改类型。

6.2 主要字段

字段含义
strategy_name/code名称和系统编号
strategy_type日/月
generate_delay_days日策略延迟或月策略几号生成
generate_hour/minute执行时分
generate_delay_month月策略相关延迟参数
document_types包含销售/退货/收款及细分
summary_fields展示哪些汇总
file_formatPDF/PNG 等
show_payment_code是否展示付款码
auto_send当前代码创建时强制为 1
status启用/禁用

6.3 默认汇总

  • 期初应收。
  • 期内销售。
  • 期内退货。
  • 期内收款。
  • 实收金额。
  • 期末应收。
  • 期末应收大写。

6.4 服务站发送配置

配置作用
send_statement_when_no_debt无欠款是否仍发送
first_send_daily_statement首次日账单今日/明日生效
first_send_monthly_statement首次月账单当月/次月语义

7. 日/月时间窗口

7.1 日账单

flowchart TD
  A["日策略"] --> B{"generate_delay_days=1?"}
  B -->|是| C["范围=昨天00:00:00到23:59:59"]
  C --> D["今天配置时刻执行"]
  B -->|否| E{"late_document_next_day"}
  E -->|0| F["今天00:00到今天配置时刻"]
  E -->|1| G["昨天配置时刻到今天配置时刻"]

代码注释称 late_document_next_day 已废弃,但时间计算仍读取该字段,新建默认值又设为 1。修改此字段前必须确认线上历史策略语义。

7.2 月账单

自动月账单统计上一个自然月:

start_time = 上月1日 00:00:00
end_time   = 上月最后一天 23:59:59
execute    = 本月 generate_delay_days 日 + 配置时分

7.3 手工生成

手工生成可指定区间。重生成复用旧账单的 start_time/end_time/contact_id/strategy_id,因此不会自动跟随策略当前时间窗口漂移。

8. 客户绑定和任务同步

8.1 绑定规则

  • 客户与策略关系写 statement_strategy_relation_v2。
  • 同客户同类型策略需要避免重复。
  • 绑定/解绑会触发任务同步。
  • 策略禁用会删除或取消尚未执行任务。
  • 修改生成日期、时间或状态可能重排任务,部分字段还会触发成功任务重发逻辑。
sequenceDiagram
  participant U as 管理员
  participant S as StrategyService
  participant R as Relation
  participant Q as Strategy MQ
  participant C as StrategyConsumer
  participant T as BillTask
  U->>S: 绑定/解绑/修改策略
  S->>R: 更新客户关系或策略
  S->>Q: statement_strategy_sync
  Q->>C: 消费
  C->>T: 删除、重排或新增计划任务

8.2 策略更新事务边界

策略字段和服务站配置在本地事务中提交;任务同步 MQ 在事务后发送。策略已更新但 MQ 失败时,任务计划可能仍是旧时间,必须补发策略同步而不是回滚业务配置。

9. 任务初始化和唯一键

9.1 每日初始化

StatementTaskSer::statementInitDay():

  1. 查询全部启用策略。
  2. 逐策略设置 sid。
  3. 调 syncStrategyTaskPublic()。
  4. 单个策略失败记日志,不中断其他策略。

9.2 任务唯一键

旧生成函数的任务键:

{contact_id}_{strategy_type}_{current_date}

实际同步服务还存在更完整的策略/执行时间维度检查。数据库唯一索引必须线上确认;代码查询去重不能完全替代并发唯一约束。

9.3 任务内容

字段含义
sid服务站
contact_id客户
strategy_id策略
start_time/end_time对账区间
execute_time到期时间
unique_key幂等键
status初始 0
bill_id成功后账单 ID

10. 到期任务投递

StatementTaskSer::sendMqBatch() 查询:

status = 0
execute_time <= 当前时间

然后先批量把状态改为 1,再逐条发布:

{
  "task_id": 123,
  "sid": 10001,
  "contact_id": 20001
}
MQ值
destinationdgj_statement
routing keystatement_bill_create
producerMqSer::sendStatementBillCreate()
consumerStatementNotify::createStatementBill()
sequenceDiagram
  participant C as 定时任务
  participant T as BillTask
  participant Q as RabbitMQ
  participant N as StatementNotify
  participant S as StatementSer
  C->>T: 查status=0且已到期
  C->>T: 批量改status=1
  loop 每个任务
    C->>Q: statement_bill_create
    Q->>N: 消费
    N->>S: createStatementBillByTasks
  end

风险:任务先改为 1,再逐条发 MQ。若中间发布失败,剩余任务可能停在 1 而没有消息。需要调度补偿或人工按任务 ID 重投,不能只等消费者重试。

11. 自动和手工生成

11.1 自动生成

消费者根据 task_id 读取任务,注入:

参数值
generate_type2 任务生成
task_id当前任务
sid/contact_id任务字段
strategy_id任务关联策略
start_time/end_time任务已计算区间

11.2 手工生成

手工入口 StatementBill::createByUser() 接受客户和时间范围,generate_type=1。手工失败直接返回业务错误;自动生成失败则记录任务失败并写“无需发送/失败原因”记录。

11.3 生成任务结果

结果任务状态bill_id发送记录
成功2新账单 ID由自动发送逻辑继续创建
失败30自动任务插 status=5 记录和失败原因
flowchart TD
  A["createStatement"] --> B["createStatementOrder"]
  B -->|成功| C["得到billId"]
  B -->|失败且自动| D["捕获错误"]
  B -->|失败且手工| E["抛给页面"]
  C --> F["任务status=2,bill_id=新ID"]
  D --> G["任务status=3,fail_reason"]
  G --> H["发送记录status=5 无需发送"]

12. 账单数据构建

12.1 数据来源

文档类型来源账单业务类型
销售出库150601普通销售/普通铺货/微仓铺货
销售退货150602人工销退/自动销退
收款核销pay销售收款
收款撤销pay-cancel收款撤销/退款方向

document_types 决定哪些来源参与。策略还可细分普通销售、普通铺货、微仓铺货。

12.2 三层账单

层级内容
主表客户、周期、期初/销售/退货/收款/期末、状态
Detail每张源单、业务类型、应收、实收、优惠、余额
DetailItem商品、数量、单价、折扣、金额
flowchart LR
  A["销售/退货/收款源事实"] --> B["按客户和区间查询"]
  B --> C["过滤策略单据类型"]
  C --> D["构建Detail源单"]
  D --> E["构建DetailItem商品"]
  D --> F["聚合主表金额和状态"]
  F --> G["同一sid的64分片事务写入"]

12.3 汇总口径

主表主要字段:

字段业务含义
opening_receivable期初应收
sale_amount期内销售
return_amount期内退货
received_amount期内收款
closing_receivable期末应收
current_should_pay_amount本期应收/发送判断使用
document_count源单数量

期末应收应由“期初 + 期内销售 - 退货 - 收款及其他调整”解释,精确字段口径以 buildData() 和报表专题为准。

12.4 初始状态

当前构建代码将主表 order_status 直接设为已核对,并在全部源订单已收款时同步形成已收款状态。旧注释和部分手工路径仍存在草稿语义,因此排查必须看真实记录和调用路径,不能假设所有新账单初始都是草稿。

13. 重复生成和唯一性

13.1 账单存在判断

代码按:

  • sid
  • contact_id
  • bill_date
  • strategy_type
  • is_delete=0

查询同周期账单。

generateBillUk() 还组合客户、策略、站点和账单日期。线上是否存在数据库唯一索引必须确认。

13.2 并发风险

flowchart TD
  A["两个生成请求"] --> B["都查询暂无账单"]
  B --> C["请求1写主/明细"]
  B --> D["请求2也尝试写主/明细"]
  C --> E{"有数据库唯一键?"}
  D --> E
  E -->|有| F["一个成功,一个唯一冲突"]
  E -->|无| G["可能重复账单"]

任务唯一键、账单唯一键和业务存在查询是三层不同防线,不能只验证其中一层。

14. 重新生成和删除

14.1 重新生成

reGenerateStatement(billId):

  1. 读取旧账单的策略、区间、客户和站点。
  2. 确认客户仍绑定该策略。
  3. 在事务中软删旧主表、Detail、DetailItem。
  4. 以原区间手工方式创建新账单。
sequenceDiagram
  participant U as 用户
  participant S as StatementSer
  participant O as 旧账单三层表
  participant N as 新账单三层表
  U->>S: reGenerate(oldBillId)
  S->>S: 读取原客户/策略/区间
  S->>S: 确认策略关系仍存在
  S->>O: is_delete=1
  S->>N: createStatement(原区间,手工)
  S-->>U: 新账单

旧账单 ID、文件路径和发送记录不会变成新账单 ID 的记录。已经发给客户的旧文件也不能因软删而自动收回。

14.2 删除

deleteStatementBill() 软删主、明细、商品项。当前 Service 不在该方法中自动删除发送记录和远端文件。因此删除是业务库不可见,不等于客户侧撤回或文件物理删除。

15. 核对和取消核对

15.1 状态机

stateDiagram-v2
  [*] --> 草稿
  草稿 --> 已核对: check
  已核对 --> 草稿: cancelCheck
  已核对 --> 已核对: 收款状态更新不改变核对

15.2 核对

checkStatement() 只允许草稿。它会:

  • 获取账单 Detail 关联的销售出库单。
  • 更新关联源单的对账/核对标识。
  • 主表改为已核对并写 confirm_time。
  • 若需要,生成并上传 PDF,回写 file_path。

15.3 取消核对

cancelCheckStatement() 只允许已核对。它反向处理源单对账标识,主表回草稿,confirm_time=null。

取消核对不是撤销发送,也不会自动改变已发微信消息。

15.4 部分取消

cancelCheckStatementPart() 可按销售出库单找到多个账单,并把已核对账单批量回草稿。修改该方法时要回归一张出库单是否可能关联多个账单和多个 sid 分片。

16. 发送前检查

16.1 发送资格

单发/批量发送通常要求账单存在且状态正确,批量路径明确筛选已核对账单。

16.2 无欠款规则

自动账单若:

send_statement_when_no_debt = 0
current_should_pay_amount = 0
generate_type = 任务

则 can_send=false,记录为无需发送。手工账单不一定被该条件阻断。

16.3 文件格式

手工发送前 V2 路径按对账区间跨度选择:

区间格式
小于等于 24 小时PNG
大于 24 小时PDF

Excel 导出走 StatementExportService::FORMAT_EXCEL,生成后回写 file_path。

17. 创建发送任务

17.1 发送记录

每个账单创建一条 t_bs_statement_send_record_v2:

字段含义
statement_id账单 ID
sid/contact_id租户和客户
target_binding_ids指定个人/群绑定,可为空表示全部支持目标
send_type1 手工,2 自动
task_id自动生成任务 ID
priority1/2/3
status初始 1
retry_count初始 0

17.2 创建时前置失败

若账单不存在、客户没有可用绑定或其他前置失败,Service 会直接创建状态 4 的发送记录及失败原因,而不是完全不留痕。

17.3 投递 MQ

MQ值
destinationdgj_statement_send
routing keystatement_send
producerSendService::sendToMQ
consumerStatementSendConsumer::statementSend
sequenceDiagram
  participant U as 页面/自动任务
  participant S as SendService
  participant R as SendRecord
  participant Q as RabbitMQ
  participant C as SendConsumer
  U->>S: createSendTask
  S->>R: status=1
  S->>Q: statement_send(record_id)
  Q->>C: 消费
  C->>S: processSendTask

18. 发送执行

18.1 执行时序

sequenceDiagram
  participant C as SendConsumer
  participant S as SendService
  participant R as SendRecord
  participant B as Bill
  participant F as 文件服务
  participant W as 微信机器人
  C->>S: processSendTask(recordId)
  S->>R: 1到2,写execute_time
  S->>B: 读取账单和客户
  alt file_path为空
    S->>F: 生成并上传PDF/PNG
    S->>B: 回写file_path
  end
  S->>S: 解析目标绑定
  S->>W: 逐目标发送文本和文件
  W-->>S: 每目标结果
  S->>R: 成功则3+send_result
  S->>B: robot_status=1

18.2 失败

异常时:

  • 发送记录标为 4。
  • retry_count + 1。
  • 写 fail_reason 和完成时间。
  • 账单 robot_status=2。

模型注释明确 markAsFailed() 当前“直接失败,不进行自动重试”,虽然枚举仍保留 MAX_RETRY_COUNT=3。不能因为存在最大重试常量就断言消费者自动重试三次。

18.3 多目标结果

send_result 保存成功数、失败数和目标详情。账单级 robot_status=1 只说明本次 Service 按成功路径结束,排查个人/群部分失败必须查看发送记录的目标级结果。

19. 手工重发

SendService::resend(recordId) 读取原发送记录,沿用或覆盖目标绑定,创建一条新的待处理发送记录,并重新投 MQ。

flowchart LR
  A["原失败记录 status=4"] --> B["resend"]
  B --> C["新记录 status=1,retry_count=0"]
  C --> D["重新投递MQ"]
  D --> E["新旧记录都保留"]

重发不是把旧记录原地改回待处理,因此:

  • 发送进度要看新记录 ID。
  • 失败次数不能只看某一条记录的 retry_count。
  • 同一个账单可能有多条手工/自动发送记录。
  • 重发成功后旧失败记录仍是历史事实。

20. 无需发送记录

SEND_STATUS_NO_NEED=5 用来区分“系统明确判定无需发送”和“发送失败”。

形成场景包括:

  • 自动生成失败且没有账单。
  • 策略周期没有生成任务。
  • 自动账单本期应收为 0 且站点配置不发送无欠款账单。

syncNoNeedSendRecords() 每 5 分钟扫描日/月策略,对任务表缺失的客户/周期补无需发送记录,并按 sid+contact+strategy+execute_time 检查重复。

无需发送不等于任务成功,也不应进入失败重试队列。

21. 收款状态同步

21.1 事件入口

销售收款或撤销收款发生后,生产:

MQ值
destinationdgj_statement
routing keystatement_bill_receive_status
producerMqSer::sendStatementBillReceiveStatus()
consumerStatementNotify::updateStatementReceiveStatus()

消息应包含可定位销售出库单和站点的字段。消费者先找到相关对账单,再逐账单回算。

21.2 回算规则

updateStatementReceiveStatus(billId,sid):

  1. 查账单全部有效 Detail。
  2. 提取销售出库单 ID。
  3. 查询每张出库单当前收款状态。
  4. 统计状态 0/1/2 的数量。
  5. 所有订单为 2 -> 账单已收款。
  6. 所有订单为 0 -> 账单未收款。
  7. 其他组合 -> 部分收款。
flowchart TD
  A["收款/撤销事件"] --> B["由销售出库单找账单ID"]
  B --> C["逐账单查全部销售出库单"]
  C --> D{"每单收款状态"}
  D -->|全部2| E["receive_status=2"]
  D -->|全部0| F["receive_status=0"]
  D -->|混合或含1| G["receive_status=1"]
  E --> H["更新账单64分片主表"]
  F --> H
  G --> H

21.3 全部收款与核对状态

账单构建阶段若全部源销售单已收款,代码会把 order_status 置为已核对。后续 updateStatementReceiveStatus() 主要更新 receive_status,不会在每次回算时都自动切换核对状态。

21.4 撤销收款

pay-cancel 会进入账单明细口径,也会触发收款状态回算。已收款账单可能因撤销降为部分或未收款,因此消费必须支持重复、乱序和迟到事件后按当前源单事实重算,而不是对状态做简单加减。

22. 客户微信绑定

22.1 绑定类型

值类型
1个人
2群聊

绑定还包含启用/禁用和是否支持发送。发送前可指定 target_binding_ids;不指定时取客户所有有效、支持发送的绑定。

22.2 边界

  • 同一个微信目标是否可绑多个客户,由绑定模型校验。
  • 客户可有个人和群聊多个目标。
  • 发送记录保存目标 ID 和目标级结果。
  • 解绑不会撤回历史消息。
  • 绑定配置变更后重发可选择新目标。

23. 事务、幂等和补偿

23.1 事务边界

链路本地事务事务外
策略创建/编辑策略、站点发送配置客户绑定/任务同步部分路径、MQ
任务生成任务批量写到期 MQ
账单生成主/Detail/Item自动文件和发送任务
重生成旧三层软删 + 新账单历史文件/发送记录
核对主状态和源单标识PDF 上传可能为外部副作用
创建发送发送记录发送 MQ
发送状态更新文件、微信机器人
收款回算账单主状态触发消息来自外部事务

23.2 ACK/NACK

三个消费者都应在成功后 ACK,异常时 NACK/重试;实际重试次数、死信、预取和并发由 RabbitMQ 部署配置决定。业务代码中的发送失败记录重试与 RabbitMQ 消费重投是两回事。

23.3 补偿原则

故障正确补偿
策略已改,任务未重排补发策略同步 MQ
任务状态 1 但无生成消息按任务 ID 重投生成 MQ
账单存在但文件缺失重新导出文件并回写路径
发送记录 1 长期不动检查 MQ 后按记录 ID 重投
发送失败使用 resend 新建记录
收款状态滞后按账单关联出库单重算
重复账单核对唯一键和已发送情况后软删错误快照

禁止通过重复创建销售收款、重复生成源单或手改多个状态来“推动”对账单。

24. 典型故障决策树

24.1 策略启用但未生成账单

flowchart TD
  A["策略启用未出账"] --> B["查策略和客户关系"]
  B --> C["计算当日execute_time和统计区间"]
  C --> D["查bill_task unique_key"]
  D -->|无任务| E["查初始化/策略同步"]
  D -->|status=0| F["未到期或sendMqBatch未跑"]
  D -->|status=1| G["查生成MQ和消费者"]
  D -->|status=3| H["查fail_reason和无需发送记录"]
  D -->|status=2| I["按sid%64查bill_id"]

24.2 任务成功但页面没有账单

  1. 查任务 bill_id 是否非 0。
  2. 用任务 sid%64 计算物理表。
  3. 查主表 is_delete。
  4. 查页面筛选条件、客户和区间。
  5. 查是否已重生成,任务仍指向被软删旧账单。

24.3 账单金额缺一张单

  1. 查缺失源单业务日期是否落在闭区间。
  2. 查策略 document_types 和销售细分开关。
  3. 查源单状态、删除状态、客户 ID。
  4. 查是否已在其他周期/账单 Detail 中。
  5. 查 late_document_next_day 导致跨周期。
  6. 手工查询与任务查询的结束时间语义是否不同。

24.4 发送记录成功但客户未收到

flowchart TD
  A["发送成功但客户称未收到"] --> B["查send_result目标级详情"]
  B --> C{"目标是否正确?"}
  C -->|否| D["绑定/指定target错误"]
  C -->|是| E["查个人/群机器人返回和消息ID"]
  E --> F["确认文件URL可访问"]
  F --> G["确认客户是否在正确群/会话查看"]
  G --> H["必要时选择正确绑定重发"]

24.5 收款完成但账单仍未收款

  1. 查收款是否真实核销到该销售出库单。
  2. 查出库单自己的收款状态。
  3. 查收款状态 MQ 是否生产/消费。
  4. 查账单 Detail 是否包含该出库单。
  5. 查同账单其他出库单是否仍未收/部分收。
  6. 用 updateStatementReceiveStatus 口径重算,而不是直接把账单改为 2。

25. 只读 SQL

先确认环境。{shard}=sid%64。字段以线上 DDL 为准,禁止直接修改状态。

25.1 策略与客户

select id, sid, strategy_name, strategy_type, status,
       generate_delay_days, generate_hour, generate_minute,
       auto_send, document_types, file_format, is_delete
from t_bs_statement_strategy_v2
where sid = :sid
order by id;
select strategy_id, contact_id, sid
from t_bs_statement_strategy_relation_v2
where sid = :sid
  and (strategy_id = :strategy_id or contact_id = :contact_id);

25.2 生成任务

select id, unique_key, sid, contact_id, strategy_id,
       start_time, end_time, execute_time, status,
       bill_id, duration_ms, fail_reason
from t_bs_statement_bill_task_v2
where sid = :sid and contact_id = :contact_id
order by id desc;

25.3 账单主表

select id, bill_no, sid, contact_id, strategy_id, strategy_type,
       start_time, end_time, order_status, receive_status,
       robot_status, generate_type, opening_receivable,
       sale_amount, return_amount, received_amount,
       closing_receivable, current_should_pay_amount,
       file_path, confirm_time, is_delete
from t_bs_statement_bill_v2_{shard}
where id = :bill_id or bill_no = :bill_no;

25.4 账单源单

select id, bill_id, source_date, source_no, source_order_id,
       source_type, business_type, receivable_amount,
       received_amount, discount_amount, period_balance,
       related_payment_summary, is_delete
from t_bs_statement_bill_detail_v2_{shard}
where bill_id = :bill_id
order by source_date, id;

25.5 商品明细

select id, bill_id, bill_detail_id, goods_name, qty,
       category_id, price, discount, amount, is_delete
from t_bs_statement_bill_detail_item_v2_{shard}
where bill_id = :bill_id
order by bill_detail_id, id;

25.6 发送记录

select id, statement_id, sid, contact_id, target_binding_ids,
       send_type, task_id, priority, status, retry_count,
       fail_reason, execute_time, finish_time, created_at
from t_bs_statement_send_record_v2
where statement_id = :bill_id or id = :send_record_id
order by id desc;

25.7 查长期卡住任务

select id, unique_key, sid, contact_id, execute_time, status
from t_bs_statement_bill_task_v2
where status in (0,1)
  and execute_time < date_sub(now(), interval 10 minute)
order by execute_time;

26. 代码和日志定位

26.1 策略和任务

rg -n "create\\(|bindCustomers|unbindCustomers|syncStrategyTaskPublic|sendSyncTaskMessage" application/Services/Statement/StrategyService.php
rg -n "statementInitDay|sendMqBatch|syncNoNeedSendRecords|generateTaskUk" application/Services/Statement/StatementTaskSer.php
rg -n "statement_strategy_sync|statement_bill_create" application/KzData/Enums/MqEventEnums.php application/controllers/tasks

26.2 生成和状态

rg -n "createStatementOrder|buildData|getBeginAmount|querySaleInvoice|querySaPaymentOrder" application/Services/Statement/StatementSer.php
rg -n "checkStatement|cancelCheckStatement|reGenerateStatement|deleteStatementBill" application/Services/Statement/StatementSer.php
rg -n "updateStatementReceiveStatus|SOURCE_TYPE_PAY|pay-cancel" application/Services/Statement application/controllers/tasks

26.3 发送

rg -n "createSendTask|processSendTask|resend|markAsFailed|sendToMQ" application/Services/Statement/SendService.php
rg -n "statement_send|SEND_STATUS_|robot_status|target_binding_ids" application/Services/Statement application/models/statement
rg -n "generateAndUploadPdf|exportByBillId|file_path" application/Services/Statement

26.4 分片

rg -n "T_BS_STATEMENT_BILL_NUM|setSid\\(\\$sid\\)|% T_BS_STATEMENT_BILL_NUM" application/config application/models/statement

27. 回归测试矩阵

27.1 策略

场景预期
创建首条日策略成功并可绑定客户
同站第二条日策略拒绝
日/月各一条允许
已绑定客户改策略类型拒绝
修改生成时间策略成功,任务异步重排
禁用策略待执行任务取消/删除
解绑客户不再生成后续任务
MQ 同步失败策略仍保存,可补发同步
日策略延迟 0/1区间计算符合规则
月末不同天数生成日被安全截断

27.2 任务和生成

场景预期
同客户同周期重复初始化只一条有效任务
未到执行时间保持 0
到期投递0 到 1 并发送 MQ
生成成功任务 2、bill_id 有值、三层表完整
生成失败任务 3、fail_reason、无需发送记录
多任务中一条失败其他客户继续处理
并发生成数据库唯一性防重复账单
手工生成错误直接返回,不伪装无需发送
64 个 shard 边界主/明细/商品同分片

27.3 账单内容

场景预期
普通销售150601 Detail 和商品项正确
销售退货150602 金额方向正确
收款核销pay 进入账单
收款撤销pay-cancel 方向和摘要正确
区间边界秒开始/结束边界不漏不重
无业务变化按规则不生成或无需发送
重生成旧三层软删,新 ID/快照正确
删除三层均软删,历史发送仍可追溯

27.4 核对和发送

场景预期
草稿核对源单标识和主状态一致
重复核对拒绝
已核对取消回草稿并清确认时间
未核对发送按入口规则拒绝
无绑定发送留失败记录
个人/群多目标每目标结果完整
文件缺失发送前生成并回写
发送成功记录 3、robot_status 1
发送失败记录 4、robot_status 2、失败原因
重发新建记录,不覆盖旧失败
无欠款自动账单按站点配置发送或状态 5

27.5 收款同步

场景预期
全部出库单未收账单 0
任一部分收账单 1
一部分全收一部分未收账单 1
全部全收账单 2
收款撤销2 可降为 1/0
重复/乱序事件按当前源单事实重算,结果幂等
一张出库关联多账单每张账单分别回算

28. 风险、证据与一页排查

28.1 高风险代码

位置风险
StrategyService策略事务后 MQ 失败导致任务未重排
StatementTaskSer::sendMqBatch先改状态 1,再逐条发 MQ
StatementSer::createStatementOrder账单三层、金额和唯一性核心
reGenerateStatement已发送旧文件不可撤回
check/cancelCheck会反向影响销售源单标识
SendService文件、绑定、机器人、多目标结果
updateStatementReceiveStatus所有出库单收款状态聚合
64 分片模型缺 sid 会查错表

28.2 已确认事实

  • 一个站最多一条日策略、一条月策略。
  • 创建策略时 auto_send 被强制设为 1。
  • 账单主、Detail、Item 按 sid%64 同后缀。
  • 到期任务先从 0 改 1,再发生成 MQ。
  • 自动生成失败会记录任务失败和无需发送记录。
  • 重生成软删旧三层并新建账单。
  • 发送失败当前不会按 MAX_RETRY_COUNT 自动循环重试。
  • 重发会新建发送记录。
  • 发送成功不代表已收款。
  • 收款状态按账单全部销售出库单实时聚合。

28.3 环境待确认

待确认项验证方式
四个定时入口真实调度频率查 crontab/容器任务
三类 MQ binding/重试/死信查 RabbitMQ
任务和账单唯一索引show create table
文件存储生命周期和访问权限查 OSS/对象存储配置
微信机器人幂等和限流联调外部服务
源单核对标识跨系统影响销售和收款全链回归
历史策略 late_document_next_day抽样线上策略和账单区间

28.4 源码证据

结论路径
状态枚举application/KzData/Enums/StatementEnums.php
策略application/Services/Statement/StrategyService.php
时间计算application/models/statement/StatementStrategyModel.php
任务application/Services/Statement/StatementTaskSer.php
账单构建/状态application/Services/Statement/StatementSer.php
发送application/Services/Statement/SendService.php
文件application/Services/Statement/StatementExportService.php
账单分片模型application/models/statement/StatementBill*Model.php
发送记录application/models/statement/BsStatementSendRecordModel.php
MQ 常量/生产application/KzData/Enums/MqEventEnums.php、MqSer.php
消费者application/controllers/tasks/Statement*Consumer.php、StatementNotify.php

28.5 一页式排查顺序

flowchart TD
  A["收到对账单问题"] --> B["确认sid、客户、策略、周期"]
  B --> C["计算策略区间和执行时间"]
  C --> D["查任务unique_key/status/fail_reason"]
  D --> E["按sid%64查账单三层"]
  E --> F["核对源单、金额、核对和收款状态"]
  F --> G["查file_path和发送记录"]
  G --> H["查发送MQ、目标绑定和目标级结果"]
  H --> I["收款问题按所有出库单重算"]
  I --> J["按缺失环节补偿,不重复生成业务源单"]

最小闭环是:策略和客户关系正确、任务可解释、账单三层快照完整、核对状态有来源、文件可访问、发送记录有目标结果、收款状态能由全部源出库单重算。

请求-日志-数据变更追踪卡

多入口请求链路

场景调用方与入口请求载荷/上下文Controller/ConsumerService/Provider汇合点最终业务事实
策略维护OPS/财务页面账期、客户范围、生成/发送规则Statement ControllerStrategyServicestrategy ID策略与客户关系生效
账单生成定时任务/MQ策略、客户、账期、task IDStatement Task ConsumerStatementTaskSer、StatementSerbill task/bill ID冻结三层账单快照
文件发送task/人工重发bill ID、渠道、接收目标发送入口/ConsumerSendService、ExportServicesend record ID文件生成并记录发送结果
收款同步销售/财务 MQ源出库单、收款/核销结果Statement NotifyStatement Servicebill + source invoice重算账单收款状态

日志证据矩阵

| 链路段 | 日志来源 | 可检索锚点 | 成功信号 | 失败信号 | 与下一段关联方式 | | --- | --- | --- | --- | --- | --- | --- | | 策略选择 | Strategy/Task 日志 | strategy ID、客户、账期、task ID | 生成唯一候选任务 | 客户关系漏配、周期重复 | task ID 查任务表 | | 账单快照 | StatementSer | task/bill ID、源单号、明细数/金额 | 三层快照和汇总闭合 | 部分明细、重复源单、事务失败 | bill ID 查 detail/item | | 文件发送 | Export/Send Service | bill ID、file ID、send record、目标 | 文件可访问且发送 SUCCESS | 文件失败、目标无效、超重试 | send record 关联 bill/file | | 收款同步 | Statement Consumer | message ID、源出库单、bill ID | 所有来源核销后状态完成 | 漏消息、部分源单仍未收 | 源单集合重算账单 |

环节数据变更台账

步骤代码位置事务读取事实写入表/缓存/MQ字段或数量变化回查证据
配置策略StrategyService配置事务现有策略和客户关系T_BS_STATEMENT_STRATEGY(_RELATION)insert/update/disable;关系增删strategy+客户集合
建任务StatementTaskSertask 事务到期策略、账期、客户T_BS_STATEMENT_BILL_TASKstatus UNDO,retry=0;唯一周期任务strategy+period+customer
生成快照StatementSer账单事务符合口径的源出库/资金事实BILL、DETAIL、DETAIL_ITEM汇总金额=明细=项目合计;task -> DONE/SUCCESSbill ID、源单集合、金额
发送Export/Send Service文件与外部发送非原子账单快照、接收目标文件、SEND_RECORD、MQ/通知send status pending -> success/failed;retry +1file ID、send record、回执
收款重算Statement Service单消息事务账单全部源单收款状态bill/statusreceived amount old -> recomputed;全收才完成源单核销合计=账单金额

子模块追踪:statement-strategy 对账策略维护

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
保存策略新建/编辑/停用账期策略strategy ID、period rule、scope、operatorapplication/Services/Statement/StrategyService.php原策略、周期、业务类型和关联客户配置本地事务 insert/update/disable old -> newrequest ID + strategy ID + operator非法周期/重复规则零写入;停用不删除历史账单
生效回查策略未生成任务strategy ID、next periodapplication/models/statement/StatementStrategyModel.phpenabled、时间窗口、最近任务和时区查询只读 不写strategy + period + last task调度时间与时区需环境确认;不手工重复建同周期任务

子模块追踪:statement-relation 策略客户关系

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
配置关系策略增删客户/站点strategy ID、customer/sid setapplication/Services/Statement/StrategyService.php客户有效性、原关系和冲突策略关系本地事务 old set -> new setrequest ID + strategy + customer count关系全量替换失败整批回滚;禁止一个周期重复归属
任务取数到期扫描策略客户strategy、period、customerapplication/Services/Statement/StatementTaskSer.php生效关系、客户状态和已有任务查询后任务本地事务创建唯一键strategy + period + customer + task ID已有同键返回既有任务,不生成重复账单

子模块追踪:statement-task 账单生成任务

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
建/执行任务调度扫描到期策略task ID、strategy、period、customerapplication/Services/Statement/StatementTaskSer.php到期条件、唯一周期键、retry/statustask 本地事务 none -> UNDO -> processing,retry 按次 +1task/batch + strategy/period/customer并发用唯一键;失败保留原因和可重试状态
完成回查任务卡住/重复task ID、bill ID、source countapplication/Services/Statement/StatementTaskSer.php账单三层快照和任务当前态仅快照完整才 processing -> DONE/SUCCESStask + bill + source/row counts已生成账单只补任务状态,不重复生成快照

子模块追踪:statement-snapshot 账单三层快照

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
生成快照账单任务执行bill/task ID、period、source ordersapplication/Services/Statement/StatementSer.php截止水位、源出库/资金事实和口径账单本地事务写 BILL/DETAIL/DETAIL_ITEM 三层 none -> snapshottask/bill + source IDs + amount/row counts三层任一失败整事务回滚;历史账单不随源数据自动漂移
守恒核对汇总/明细/项目不平bill ID、detail/item IDsapplication/Services/Statement/StatementSer.php三层金额、数量和源单集合查询只读;bill total=detail sum=item sumbill + layer counts/totals源事实变更走重建/调整业务,不手改某一层金额

子模块追踪:statement-export 账单文件生成

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
导出文件账单完成后生成文件bill ID、export/file taskapplication/Services/Statement/StatementExportService.php固定账单快照、列模板和文件状态业务账单只读;事务外文件 pending -> ready/failedbill + task/file ID + rows/size文件失败不重建账单;只重做导出
下载验收用户/发送服务取文件bill/file ID、permissionapplication/Services/Statement/StatementExportService.php文件归属、URL/有效期和快照版本下载只读 不写request ID + bill/file + HTTP codeURL 失效刷新文件链接;环境 OSS 配置待验证

子模块追踪:statement-send 微信、短信与文件发送

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
发送账单文件完成后通知客户bill ID、send record、channel、recipient hashapplication/Services/Statement/SendService.php账单状态、文件、接收目标和去重键发送记录本地事务 none/pending -> accepted/failed;外部调用事务外bill + send/file ID + channel code受理不等于送达;失败按 send record 重试,不重生成账单
回执/重试通道回执或调度重试external message ID、retry countapplication/models/statement/BsStatementSendRecordModel.php当前发送态、通道结果和最大重试单回执事务 accepted -> success/failed,retry +1both message IDs + retry/result多通道分别记录;敏感接收方只存脱敏日志

子模块追踪:statement-receipt 收款同步与账单状态重算

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
收款同步来源销售/收款状态消息message ID、bill ID、sourceOrderNoapplication/Services/Statement/StatementSer.php固定源单集合、每单有效收款/退款和当前账单态单消息本地事务 receivedAmount old -> recomputed,状态 partial/completemessage ID + bill/source order + amount重复消息按源单重算不累加;缺源关系记录失败
完成回查已收款但账单未完成bill ID、all source paymentsapplication/Services/Statement/StatementSer.php全部源单核销合计、账单总额和同步水位查询只读;全收才允许 status -> completebill + source/payment IDs + totalsOLTP 正确仅补收款同步事件;金额不平先查退款/软删口径