本文整理 DGJ2.0 对账单 V2 从策略配置、客户绑定、任务生成、账单快照、核对/取消核对、文件导出、微信发送、失败重发到收款状态同步的完整业务链。
对账单不是实时查询页面,而是把某个客户、某段时间内的销售、退货、收款和收款撤销固化为可发送、可追溯的账单快照。它跨越策略表、任务表、64 个账单分表、客户微信绑定、发送记录、RabbitMQ 和销售收款状态。
阅读后应能回答:
- 日账单和月账单的统计区间、执行时间如何计算。
- 一个服务站为什么只能有一条日策略和一条月策略。
- 客户绑定/解绑或策略改期后,旧任务如何同步。
- 自动任务如何从待执行变成 MQ 消息,再生成账单。
- 账单主表、业务明细和商品明细为什么都按
sid%64分表。 - 草稿、已核对、未发送、发送失败、未收款、部分收款、已收款分别属于哪套状态。
- 重新生成为什么是“软删旧快照 + 创建新快照”,而不是原地覆盖。
- 发送记录为何可能成功、部分目标失败,重发会新建什么。
- 销售收款和撤销收款如何回算一个或多个对账单。
- 出现“任务成功但没有文件”“发送成功但账单仍未收款”等现象时如何排查。
结论先行:生成任务状态、账单核对状态、机器人发送状态、发送记录状态和收款状态是五套并行状态。它们有关联,但不能互相替代。发送成功只说明消息链路成功;收款状态必须根据账单关联的销售出库单逐单回算。
1. 业务范围
1.1 本文覆盖
| 能力 | 主要入口 | 形成的业务事实 |
|---|---|---|
| 策略管理 | reports/StatementStrategy | 日/月策略、周期、时间、单据类型 |
| 客户绑定 | bindCustomers/unbindCustomers | 客户使用哪条策略 |
| 任务初始化 | StatementTasks::statementInitDay | 每客户每策略执行任务 |
| 到期投递 | sendMqBatch | 生成任务进入 MQ |
| 自动生成 | StatementNotify::createStatementBill | 64 分片账单快照 |
| 手工生成 | StatementBill::createByUser | 指定客户和区间快照 |
| 重新生成 | reGenerate | 旧账单软删、新账单创建 |
| 核对/取消 | check/cancelCheck | 账单确认状态和源单关联 |
| 文件导出 | exportExcel、PDF/PNG | OSS/文件路径 |
| 单发/批量发 | 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.php | createTask/getProgress | 独立发送任务入口 |
reports/StatementCustomerWechat.php | 绑定、解绑、设置 | 接收人管理 |
tasks/StatementTasks.php | 日初始化、投 MQ、失败通知 | 调度入口 |
tasks/StatementStrategyConsumer.php | statementStrategySync | 策略任务同步 |
tasks/StatementNotify.php | 生成、收款状态 | 账单消费者 |
tasks/StatementSendConsumer.php | statementSend | 发送消费者 |
inner/Statement.php | 单/批量创建和账单视图 | 内部服务入口 |
3.2 Service
| Service | 责任 |
|---|---|
StrategyService | 策略 CRUD、客户关系、任务重排 |
StatementTaskSer | 创建计划任务、到期投递、无需发送记录 |
StatementSer | 构建账单、状态、收款回算、发送前业务 |
SendService | 创建发送记录、接收人、PDF、发送、重发 |
StatementExportService | PDF/PNG/Excel 视图和文件 |
NeedStatementUserSer | 记录发生出库/退货/收款的客户 |
4. 表和分片
4.1 核心表
| 常量 | 物理表 | 是否分片 | 用途 |
|---|---|---|---|
T_BS_STATEMENT_STRATEGY | t_bs_statement_strategy_v2 | 否 | 策略 |
T_BS_STATEMENT_STRATEGY_RELATION | t_bs_statement_strategy_relation_v2 | 否 | 策略客户关系 |
T_BS_STATEMENT_BILL_TASK | t_bs_statement_bill_task_v2 | 否 | 生成任务 |
T_BS_NEED_STATEMENT_USER | t_bs_need_statement_user | 否 | 有对账变化客户 |
T_BS_STATEMENT_BILL | t_bs_statement_bill_v2_{sid%64} | 64 | 账单主表 |
T_BS_STATEMENT_BILL_DETAIL | t_bs_statement_bill_detail_v2_{sid%64} | 64 | 源单明细 |
T_BS_STATEMENT_BILL_DETAIL_ITEM | t_bs_statement_bill_detail_item_v2_{sid%64} | 64 | 商品明细 |
T_BS_STATEMENT_SEND_RECORD | t_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 生成任务状态
| 值 | 常量 | 含义 |
|---|---|---|
| 0 | TASK_STATUS_UNDO | 未执行 |
| 1 | TASK_STATUS_DONE | 已投递/执行中语义 |
| 2 | TASK_STATUS_SUCCESS | 生成成功 |
| 3 | TASK_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_format | PDF/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():
- 查询全部启用策略。
- 逐策略设置
sid。 - 调
syncStrategyTaskPublic()。 - 单个策略失败记日志,不中断其他策略。
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 | 值 |
|---|---|
| destination | dgj_statement |
| routing key | statement_bill_create |
| producer | MqSer::sendStatementBillCreate() |
| consumer | StatementNotify::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_type | 2 任务生成 |
task_id | 当前任务 |
sid/contact_id | 任务字段 |
strategy_id | 任务关联策略 |
start_time/end_time | 任务已计算区间 |
11.2 手工生成
手工入口 StatementBill::createByUser() 接受客户和时间范围,generate_type=1。手工失败直接返回业务错误;自动生成失败则记录任务失败并写“无需发送/失败原因”记录。
11.3 生成任务结果
| 结果 | 任务状态 | bill_id | 发送记录 |
|---|---|---|---|
| 成功 | 2 | 新账单 ID | 由自动发送逻辑继续创建 |
| 失败 | 3 | 0 | 自动任务插 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 账单存在判断
代码按:
sidcontact_idbill_datestrategy_typeis_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):
- 读取旧账单的策略、区间、客户和站点。
- 确认客户仍绑定该策略。
- 在事务中软删旧主表、Detail、DetailItem。
- 以原区间手工方式创建新账单。
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 小时 |
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_type | 1 手工,2 自动 |
task_id | 自动生成任务 ID |
priority | 1/2/3 |
status | 初始 1 |
retry_count | 初始 0 |
17.2 创建时前置失败
若账单不存在、客户没有可用绑定或其他前置失败,Service 会直接创建状态 4 的发送记录及失败原因,而不是完全不留痕。
17.3 投递 MQ
| MQ | 值 |
|---|---|
| destination | dgj_statement_send |
| routing key | statement_send |
| producer | SendService::sendToMQ |
| consumer | StatementSendConsumer::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 | 值 |
|---|---|
| destination | dgj_statement |
| routing key | statement_bill_receive_status |
| producer | MqSer::sendStatementBillReceiveStatus() |
| consumer | StatementNotify::updateStatementReceiveStatus() |
消息应包含可定位销售出库单和站点的字段。消费者先找到相关对账单,再逐账单回算。
21.2 回算规则
updateStatementReceiveStatus(billId,sid):
- 查账单全部有效 Detail。
- 提取销售出库单 ID。
- 查询每张出库单当前收款状态。
- 统计状态 0/1/2 的数量。
- 所有订单为 2 -> 账单已收款。
- 所有订单为 0 -> 账单未收款。
- 其他组合 -> 部分收款。
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 任务成功但页面没有账单
- 查任务
bill_id是否非 0。 - 用任务
sid%64计算物理表。 - 查主表
is_delete。 - 查页面筛选条件、客户和区间。
- 查是否已重生成,任务仍指向被软删旧账单。
24.3 账单金额缺一张单
- 查缺失源单业务日期是否落在闭区间。
- 查策略
document_types和销售细分开关。 - 查源单状态、删除状态、客户 ID。
- 查是否已在其他周期/账单 Detail 中。
- 查
late_document_next_day导致跨周期。 - 手工查询与任务查询的结束时间语义是否不同。
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 收款完成但账单仍未收款
- 查收款是否真实核销到该销售出库单。
- 查出库单自己的收款状态。
- 查收款状态 MQ 是否生产/消费。
- 查账单 Detail 是否包含该出库单。
- 查同账单其他出库单是否仍未收/部分收。
- 用
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/Consumer | Service/Provider | 汇合点 | 最终业务事实 |
|---|---|---|---|---|---|---|
| 策略维护 | OPS/财务页面 | 账期、客户范围、生成/发送规则 | Statement Controller | StrategyService | strategy ID | 策略与客户关系生效 |
| 账单生成 | 定时任务/MQ | 策略、客户、账期、task ID | Statement Task Consumer | StatementTaskSer、StatementSer | bill task/bill ID | 冻结三层账单快照 |
| 文件发送 | task/人工重发 | bill ID、渠道、接收目标 | 发送入口/Consumer | SendService、ExportService | send record ID | 文件生成并记录发送结果 |
| 收款同步 | 销售/财务 MQ | 源出库单、收款/核销结果 | Statement Notify | Statement Service | bill + 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+客户集合 |
| 建任务 | StatementTaskSer | task 事务 | 到期策略、账期、客户 | T_BS_STATEMENT_BILL_TASK | status UNDO,retry=0;唯一周期任务 | strategy+period+customer |
| 生成快照 | StatementSer | 账单事务 | 符合口径的源出库/资金事实 | BILL、DETAIL、DETAIL_ITEM | 汇总金额=明细=项目合计;task -> DONE/SUCCESS | bill ID、源单集合、金额 |
| 发送 | Export/Send Service | 文件与外部发送非原子 | 账单快照、接收目标 | 文件、SEND_RECORD、MQ/通知 | send status pending -> success/failed;retry +1 | file ID、send record、回执 |
| 收款重算 | Statement Service | 单消息事务 | 账单全部源单收款状态 | bill/status | received amount old -> recomputed;全收才完成 | 源单核销合计=账单金额 |
子模块追踪:statement-strategy 对账策略维护
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 保存策略 | 新建/编辑/停用账期策略 | strategy ID、period rule、scope、operator | application/Services/Statement/StrategyService.php | 原策略、周期、业务类型和关联客户 | 配置本地事务 insert/update/disable old -> new | request ID + strategy ID + operator | 非法周期/重复规则零写入;停用不删除历史账单 |
| 生效回查 | 策略未生成任务 | strategy ID、next period | application/models/statement/StatementStrategyModel.php | enabled、时间窗口、最近任务和时区 | 查询只读 不写 | strategy + period + last task | 调度时间与时区需环境确认;不手工重复建同周期任务 |
子模块追踪:statement-relation 策略客户关系
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 配置关系 | 策略增删客户/站点 | strategy ID、customer/sid set | application/Services/Statement/StrategyService.php | 客户有效性、原关系和冲突策略 | 关系本地事务 old set -> new set | request ID + strategy + customer count | 关系全量替换失败整批回滚;禁止一个周期重复归属 |
| 任务取数 | 到期扫描策略客户 | strategy、period、customer | application/Services/Statement/StatementTaskSer.php | 生效关系、客户状态和已有任务 | 查询后任务本地事务创建唯一键 | strategy + period + customer + task ID | 已有同键返回既有任务,不生成重复账单 |
子模块追踪:statement-task 账单生成任务
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 建/执行任务 | 调度扫描到期策略 | task ID、strategy、period、customer | application/Services/Statement/StatementTaskSer.php | 到期条件、唯一周期键、retry/status | task 本地事务 none -> UNDO -> processing,retry 按次 +1 | task/batch + strategy/period/customer | 并发用唯一键;失败保留原因和可重试状态 |
| 完成回查 | 任务卡住/重复 | task ID、bill ID、source count | application/Services/Statement/StatementTaskSer.php | 账单三层快照和任务当前态 | 仅快照完整才 processing -> DONE/SUCCESS | task + bill + source/row counts | 已生成账单只补任务状态,不重复生成快照 |
子模块追踪:statement-snapshot 账单三层快照
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 生成快照 | 账单任务执行 | bill/task ID、period、source orders | application/Services/Statement/StatementSer.php | 截止水位、源出库/资金事实和口径 | 账单本地事务写 BILL/DETAIL/DETAIL_ITEM 三层 none -> snapshot | task/bill + source IDs + amount/row counts | 三层任一失败整事务回滚;历史账单不随源数据自动漂移 |
| 守恒核对 | 汇总/明细/项目不平 | bill ID、detail/item IDs | application/Services/Statement/StatementSer.php | 三层金额、数量和源单集合 | 查询只读;bill total=detail sum=item sum | bill + layer counts/totals | 源事实变更走重建/调整业务,不手改某一层金额 |
子模块追踪:statement-export 账单文件生成
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 导出文件 | 账单完成后生成文件 | bill ID、export/file task | application/Services/Statement/StatementExportService.php | 固定账单快照、列模板和文件状态 | 业务账单只读;事务外文件 pending -> ready/failed | bill + task/file ID + rows/size | 文件失败不重建账单;只重做导出 |
| 下载验收 | 用户/发送服务取文件 | bill/file ID、permission | application/Services/Statement/StatementExportService.php | 文件归属、URL/有效期和快照版本 | 下载只读 不写 | request ID + bill/file + HTTP code | URL 失效刷新文件链接;环境 OSS 配置待验证 |
子模块追踪:statement-send 微信、短信与文件发送
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 发送 | 账单文件完成后通知客户 | bill ID、send record、channel、recipient hash | application/Services/Statement/SendService.php | 账单状态、文件、接收目标和去重键 | 发送记录本地事务 none/pending -> accepted/failed;外部调用事务外 | bill + send/file ID + channel code | 受理不等于送达;失败按 send record 重试,不重生成账单 |
| 回执/重试 | 通道回执或调度重试 | external message ID、retry count | application/models/statement/BsStatementSendRecordModel.php | 当前发送态、通道结果和最大重试 | 单回执事务 accepted -> success/failed,retry +1 | both message IDs + retry/result | 多通道分别记录;敏感接收方只存脱敏日志 |
子模块追踪:statement-receipt 收款同步与账单状态重算
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 收款同步 | 来源销售/收款状态消息 | message ID、bill ID、sourceOrderNo | application/Services/Statement/StatementSer.php | 固定源单集合、每单有效收款/退款和当前账单态 | 单消息本地事务 receivedAmount old -> recomputed,状态 partial/complete | message ID + bill/source order + amount | 重复消息按源单重算不累加;缺源关系记录失败 |
| 完成回查 | 已收款但账单未完成 | bill ID、all source payments | application/Services/Statement/StatementSer.php | 全部源单核销合计、账单总额和同步水位 | 查询只读;全收才允许 status -> complete | bill + source/payment IDs + totals | OLTP 正确仅补收款同步事件;金额不平先查退款/软删口径 |