本文面向第一次接触 DGJ2 通知系统的产品、前端、后端、测试和运维人员。目标不是把所有带有 Message、Notify、Push 的类罗列出来,而是回答:一条业务变化如何变成用户可见的通知、接收人怎样确定、消息落在哪里、哪些链路可重试、哪些链路只记日志、收到或没收到时应从哪里开始排查。
静态代码能够确认当前仓库中的入口、表、字段、事件和异常分支;生产环境的 RabbitMQ binding、JPush 应用配置、通知中心模板、设备别名、真实唯一索引和调度并发度不能由仓库单独证明,统一列在“待环境确认”章节。
1. 先读结论:DGJ2 中不存在一个统一通知中心
“通知”至少包含八条不同链路,不能只看 t_sys_message 判断是否发送成功。
| 通道 | 用户看到什么 | DGJ2 是否先落库 | 主要入口 | 失败后怎样发现 |
|---|---|---|---|---|
| PC 站内信 | 消息列表、未读数、弹窗 | 是,t_sys_message* | MessageService::mSend、Message::mAddSave | 查消息主表、内容表和 Redis |
| 定时站内信 | 到点后出现的普通/重要/弹窗消息 | 先写定时表,到点再写消息表 | tasks/Message::sendCronMessage | 查定时状态、消息关系、任务日志 |
| 悬浮窗/语音提醒 | APP 或前端即时提醒 | 站内信已落库,提醒自身不单独落本地审计 | MessageService::sendMessageWindow | 查站内信、MQ 发布和下游日志 |
| 业务通知 MQ | 销售、退货、活动等状态通知 | 通常不写站内信表 | MqSer::*ToAPP | 查业务事实、生产日志、MQ 和下游 |
| JPush | 手机系统通知栏 | 当前调用链未明确写 t_app_push_log | JpushProvider、tasks/Contact | 查任务输出、别名、JPush 返回 |
| IM 业务消息 | 腾讯 IM 群消息进入机器人业务 | 上游消息不先落通知表;机器人链另有会话表 | tasks/ImCenterNotify | 查 MQ、msgId、Robot 日志和会话表 |
| 短信 | 验证码、授信、账单、账号开通提醒 | 由外部通知中心管理,DGJ2 主要记日志 | SmsProvider | 查场景码、接收号码、外部响应和日志 |
| 微信对账通知 | 对账单文件和发送结果 | 是,独立发送记录和目标结果 | Statement/SendService | 查发送记录、绑定、文件、MQ 和失败原因 |
flowchart LR
A[业务事实或人工操作] --> B{选择通知通道}
B -->|PC消息| C[t_sys_message_text]
C --> D[t_sys_message]
D --> E[Redis未读数]
B -->|APP业务事件| F[dgj_notify MQ]
B -->|手机通知栏| G[JPush]
B -->|IM入站| H[imcenter_notify MQ]
H --> I[RobotFacade]
B -->|短信| J[NotifyCenter]
B -->|对账微信| K[发送记录]
K --> L[MQ/文件/微信]
2. 业务目标、范围与不在本文范围内的内容
2.1 业务目标
- 业务状态变化后,把必要信息送到正确服务站、账号或客户。
- PC 端能够查看历史消息、区分已读未读并展示弹窗。
- 重要消息可以通过悬浮提醒、APP、短信或微信增加触达率。
- 批量、定时和跨系统通知能够追踪发送结果并在失败后恢复。
- 通知失败不能误判成主业务失败,主业务成功也不能掩盖通知丢失。
2.2 本文包含
- PC 站内信的创建、列表、详情、已读、删除、撤销和未读缓存。
t_sys_message_cron定时消息的生成和发送。- 站内信转悬浮提醒的 MQ 链路。
- 销售、退货、优惠券、活动等 APP 通知事件。
- 客情提醒 JPush、腾讯 IM 入站消息、通知中心短信。
- 对账单微信发送记录和重发模型。
- 通知链路的幂等、事务、重试、告警、排查和回归。
2.3 本文不重复展开
- RobotV2 会话状态和报价逻辑:见第 39、41 篇。
- 销售、退货、活动、财务的业务状态机:见第 04、05、18、29、37 篇。
- MQ 基础设施和所有 destination:见第 13 篇。
- 对账单生成、核销和策略:见第 28 篇。
3. 角色、接收对象与容易混淆的 ID
| 名称 | 含义 | 典型字段 | 注意事项 |
|---|---|---|---|
| 服务站 | 业务租户/经营主体 | sid | 业务服务通常以 sid 作为接收方入参 |
| 服务站主账号 | t_sys_admin.roleid=0 的账号 | uid | mSend 会把目标 sid 转为主账号 uid |
| 当前登录账号 | 当前 PC/App 操作人 | JXCUID | 可能不是主账号;列表查询会同时包含主账号和当前账号 |
| OPS/系统用户 | 平台运营后台账号 | 负数 sendID/recID | 负数绝对值关联 t_admin_users.id |
| 客户联系人 | 服务站客户或对账接收人 | contact_id、手机号、微信绑定 | 不等于服务站主账号 |
| APP 推送别名 | JPush 的 alias | 当前代码传 uid 数组 | 是否已绑定到设备由客户端/JPush 管理 |
| IM 账号 | 腾讯 IM 的 from/to account | 消息体账号字段 | 与 DGJ uid、sid 的映射在机器人适配链内 |
erDiagram
BS_STORE ||--o{ SYS_ADMIN : "sid"
SYS_ADMIN ||--o{ SYS_MESSAGE : "uid=recID/sendID"
SYS_MESSAGE_TEXT ||--o{ SYS_MESSAGE : "mtID=messageID"
SYS_MESSAGE_CRON ||--o{ SYS_MESSAGE : "mcID=mcronID"
BS_CONTACT ||--o{ STATEMENT_SEND_RECORD : "contact_id"
BS_STORE ||--o{ STATEMENT_SEND_RECORD : "sid"
4. 总体系统边界
flowchart TB
subgraph DGJ[DGJ2]
Biz[业务Service/回调/任务]
Mail[MessageService]
DB[(MySQL)]
Cache[(Redis unread hash)]
MQ[MqSer/RabbitMQ]
SMS[SmsProvider]
JP[JpushProvider]
ST[Statement SendService]
IMC[ImCenterNotify]
end
Biz --> Mail
Mail --> DB
Mail --> Cache
Mail --> MQ
Biz --> MQ
Biz --> SMS
Biz --> JP
Biz --> ST
MQ --> App[APP/SAAS/其他消费者]
SMS --> NC[通知中心]
JP --> JPS[JPush]
ExternalIM[腾讯IM中心] --> IMC
IMC --> Robot[RobotFacade]
ST --> Wechat[微信/机器人发送服务]
5. 代码入口地图
| 入口类型 | 文件/方法 | 调用方 | 核心副作用 |
|---|---|---|---|
| PC 消息页面 | application/controllers/Message.php | 登录后的 PC 用户 | 列表、发送、详情、删除、撤销、未读查询 |
| 内部刷新未读 | inner/Message::updateUnreadMessage | 内部可信调用方 | 重算指定 uid 的 Redis 未读数 |
| 内部悬浮提醒 | inner/Message::sendMessageWindow | 内部调用方 | 从消息内容 ID 找收件人并发 MQ |
| 业务站内信 | MessageService::mSend | 订单、支付、活动、预订单等 Service/Task | 新增内容和收件关系,刷新未读缓存 |
| 定时站内信 | tasks/Message::sendCronMessage | CLI 调度 | 到期消息批量落入正式消息表 |
| 业务通知 MQ | application/Services/Mq/MqSer.php | 业务 Service | 发布到 dgj_notify 或 dgj |
| IM 消费 | tasks/ImCenterNotify | RabbitMQ | 同步确认消息并交给机器人处理 |
| JPush | tasks/Contact::pushAction | 定时客情任务 | 按服务站账号 alias 批量推送 |
| 短信 | SmsProvider | 登录、财务、账号、任务 | 调通知中心场景模板 |
| 对账发送 | Statement/SendService | 手动/自动对账任务 | 建发送记录、入 MQ、生成文件、微信发送 |
6. 核心表和缓存字典
| 常量 | 物理表/键 | 作用 | 关键字段或维度 |
|---|---|---|---|
MESSAGE | t_sys_message | 每个收件人的消息关系和状态 | mID、messageID、sendID、recID、status、type、topic、source |
MESSAGE_TEXT | t_sys_message_text | 可复用的标题和正文 | mtID、title、content、sendTime |
MESSAGE_CRON | t_sys_message_cron | 未到期/已发送的定时消息 | mcID、recID、sendID、send_time、status、is_delete |
APP_PUSH_LOG | t_app_push_log | APP 推送日志常量 | 当前客情 JPush 调用链未看到明确写入点 |
BS_CONTACT_REMIND | t_bs_contact_remind | 客情提醒计划 | sid、remindDate、remindStatus、wayName |
BS_CONTACT_REMIND_WAY | t_bs_contact_remind_way | 客情提醒方式 | 提醒方式配置 |
SCM_OUTBOUND_REMINDER_LOG | t_scm_outbound_reminder_log | 销售出库提醒去重/审计 | 具体唯一约束待 DDL 确认 |
T_BS_MQ_MESSAGE | t_bs_mq_message | 部分异步任务本地状态 | status 等;tables.php 注释与实际模型用途不一致 |
T_BS_STATEMENT_SEND_RECORD | t_bs_statement_send_record_v2 | 对账单发送任务 | status、fail_reason、retry_count、target_binding_ids |
HOME_CACHE_HASH_MESSAGE | dgj:home_cache:hash_message | PC 首页未读数 Hash | field=uid,value=未读数量 |
7. 站内信的数据模型:内容和收件关系分离
一条内容可以对应一个或多个收件关系。messageID 不是业务单号,而是 t_sys_message_text.mtID。
flowchart LR
A[标题+正文] --> B[t_sys_message_text 1行]
B -->|mtID=messageID| C1[t_sys_message 收件人A]
B -->|mtID=messageID| C2[t_sys_message 收件人B]
B -->|mtID=messageID| C3[t_sys_message 收件人N]
C1 --> D1[独立已读/删除/撤销状态]
C2 --> D2[独立已读/删除/撤销状态]
这意味着:
- 修改内容表会影响所有引用同一
messageID的收件人。 - 已读、删除、撤销是关系行级别状态。
- 定时群发只写一条内容,再批量写多条关系。
- 排查时必须同时查
mID和messageID,不能混用。
8. 站内信类型、主题和来源不是同一个概念
8.1 type:展示强度
| 值 | 当前展示语义 | 额外行为 |
|---|---|---|
0 | 普通 | 列表展示 |
1 | 重要 | 定时发送后会触发 sendMessageWindow |
2 | 弹窗 | getUnreadMessage 返回到 jumpMessage |
8.2 topic:消息中心分类
| 值 | 分类 | 常量 |
|---|---|---|
1 | 活动通知 | MESSAGE_TOPIC_ACTIVITY |
2 | 公司通知 | MESSAGE_TOPIC_COMPANY |
3 | 订单消息 | MESSAGE_TOPIC_ORDER |
4 | 账户消息 | MESSAGE_TOPIC_ACCOUNT |
5 | 系统通知 | MESSAGE_TOPIC_SYSTEM |
8.3 source:业务来源和跳转目的地
MessageTypeEnums 定义了站内信、采购/销售、客户审核、采购关闭、轮胎理赔、Robot/移动商城、全车件、仓库时效、待支付、预订单、对账发送失败等来源。常见值如下。
| source | 名称 | 典型跳转/业务 |
|---|---|---|
0 | 站内信 | 消息详情 |
1 | 销售单新订单 | 销售订单 |
3 | 销售单关闭 | 销售订单 |
4 | 销售退货 | 退货申请 |
5 | 客户审核 | 客户审核 |
6 | 采购单关闭 | 采购订单 |
9-19 | Robot/移动商城/E站 | 报价、提货、补货等页面 |
20-23 | 全车件 | 渠道订单/售后 |
50 | 仓库时效提醒 | 仓储/履约相关页面 |
51 | 订单待支付 | 待支付订单 |
60 | 预订单提货 | 预订单 |
70 | 对账单发送失败 | 对账单发送记录 |
高风险点:MessageService::getList 当前固定追加 source in (0,8),普通消息列表并不会展示所有 MessageTypeEnums 来源;消息中心接口则通过 MESSAGE_SOURCE_NAME 和 MESSAGE_INFO 格式化来源及跳转。修改来源枚举时必须同时核对列表过滤、名称映射和页面路由。
9. 业务站内信 mSend 的调用契约
MessageService::mSend($content, $title, $fromid, $toid, $type=0, $topic=null) 不是 HTTP API,但被多个业务服务直接调用,是最重要的站内信写入口。
| 参数 | 实际含义 | 规则 |
|---|---|---|
content | 业务正文 HTML/文本 | 方法会在前面拼接“原始邮件”信息并整体 htmlspecialchars |
title | 消息标题 | 方法本身未做长度校验;PC 人工发送有校验 |
fromid | 发送方 sid,或 -1 系统管理员 | 非 -1 时查该站主账号 |
toid | 接收方 sid,或 -1 系统管理员 | 非 -1 时转换为 roleid=0 的主账号 uid |
type | 普通/重要/弹窗 | 默认 0 |
topic | 活动/公司/订单/账户/系统 | 可空;非空才写入 |
代表性调用:
$this->messageService->mSend(
$content,
$title,
-1,
$sid,
1,
MessageTypeEnums::MESSAGE_TOPIC_ORDER
);
10. mSend 完整调用链和数据流
sequenceDiagram
participant B as 业务Service/Task
participant M as MessageService
participant A as t_sys_admin
participant T as t_sys_message_text
participant R as t_sys_message
participant C as Redis unread hash
B->>M: mSend(content,title,fromSid,toSid,type,topic)
M->>A: 查发送方/接收方主账号(roleid=0)
alt 接收主账号不存在
M-->>B: false
else 接收人存在
M->>M: begin transaction
M->>T: INSERT title/content/sendTime
T-->>M: mtID
M->>R: INSERT sendID/recID/messageID/type/topic
M->>R: COUNT 未读
M->>C: HSET uid qty
alt DB状态失败
M->>M: rollback
M-->>B: false
else DB状态正常
M->>M: commit
M-->>B: true
end
end
10.1 最终可查询事实
t_sys_message_text.mtID:消息内容 ID。t_sys_message.mID:收件关系 ID。t_sys_message.messageID=mtID:内容关联。t_sys_message.recID=主账号 uid:真正接收账号。- Redis Hash 的
uid -> qty:首页快速展示的未读数量。
10.2 当前事务边界风险
saveMessageCache 在数据库 commit 之前执行。若 Redis 在事务内读不到未提交行,缓存可能少 1;若 Redis 已更新但数据库随后回滚,缓存可能多 1。读取端在缓存值缺失时会重算,但“存在但错误”的非零值不会自动修复。
推荐改进顺序:数据库提交成功后刷新缓存;失败时删除该 uid 的 Hash field 让下次读取回源;更稳妥时使用事务后事件/outbox。
11. 人工发送入口和请求字段
PC 页面入口是 application/controllers/Message.php。
| 方法 | 请求 | 校验/行为 | 返回或页面 |
|---|---|---|---|
mAdd | 无核心业务参数 | 加载服务站主账号、OPS 用户和 topic | 发送表单 |
mAddSave | contact,type,title,content,topic | 标题 3-150、内容 5-20000;写内容和关系 | JSON status/msg |
mReply | rID,mID | 读取接收人和原文,标题加“回复” | 回复页面 |
mList | action,act,skey,page,rows,topic | 收件/发件、已读/未读、标题搜索 | 分页 JSON |
mDetail | id,flag | 非发件箱查看会置已读 | 详情页面 |
mDelete | id 列表字符串 | isDel=1,刷新收件人未读缓存 | JSON |
mBack | id 列表字符串 | 仅发送者可撤销,isDel=1,isBack=1 | JSON |
getUnreadMessage | 登录上下文 | Redis 未读数和弹窗消息 | splash JSON |
示例表单请求:
POST /Message/mAddSave
Content-Type: application/x-www-form-urlencoded
contact=12345&type=1&topic=3&title=采购单已关闭&content=请进入采购订单查看详情
这里的 contact 是账号 uid,而不是 mSend 中使用的目标 sid。同名“发送消息”存在两种接收人语义,调用方必须区分。
12. 人工发送、删除和撤销的状态图
stateDiagram-v2
[*] --> 未读: INSERT status=0
未读 --> 已读: 查看详情/弹窗确认
未读 --> 已删除: 收件人删除 isDel=1
已读 --> 已删除: 收件人删除 isDel=1
未读 --> 已撤销: 发送者撤销 isBack=1,isDel=1
已读 --> 已撤销: 当前代码未阻止已读后撤销
已删除 --> [*]
已撤销 --> [*]
当前代码风险:mDelete 查询指定 mID 后没有显式校验这些消息是否属于当前用户;mBack 只校验 sendID。权限边界是否由上游页面/网关完全保证需要专项安全回归。
13. 列表收件人规则
收件箱不是简单的 recID=当前uid:
- 根据
JXCSID查服务站主账号uid。 - 同时使用当前登录
JXCUID。 - 收件条件为
recID in (主账号uid, 当前uid)。 - 追加
isDel=0和source in (0,8)。 - 可按
topic、标题、已读状态过滤。 - 首屏最多把 3 条置顶消息插到普通分页结果前。
flowchart TD
A[JXCSID/JXCUID] --> B[查roleid=0主账号uid]
B --> C[recID in 主uid,当前uid]
C --> D[isDel=0]
D --> E[source in 0,8]
E --> F{筛选}
F -->|topic| G[按主题]
F -->|read/unread| H[按status]
F -->|skey| I[标题LIKE]
G --> J[置顶最多3条+分页]
H --> J
I --> J
14. 未读数量的数据库口径
MessageModel::getUnreadMessageNumByUids 的严格口径是:
recID in (uids)
AND isDel = 0
AND status = 0
AND isBack = 0
AND source = 0
GROUP BY recID
旧接口 countMessageUnread 只检查 recID/isDel/status,没有 isBack/source 条件。新旧接口可能返回不同数量,排查时必须先确认页面调用的是哪一个。
15. Redis 未读缓存读写流程
flowchart TD
A["getUnreadMessage uid"] --> B["HGET dgj:home_cache:hash_message uid"]
B --> C{"值是否可用"}
C -->|"存在,包括字符串 0"| D["直接作为 num"]
C -->|"false 或 null 等假值"| E["按 DB 严格口径 COUNT"]
E --> F["HSET uid qty 或 0"]
F --> D
D --> G["查询 type=2 未读弹窗"]
G --> H["返回 num、isClick、jumpMessage"]
缓存刷新发生在:
mSend新增消息。- 人工
mAddSave。 - 查看详情置已读。
- 弹窗
readMessage。 - 删除和撤销。
- 定时消息落库后。
- 内部接口
updateUnreadMessage手动重算。
高风险点:缓存没有 TTL;只要某次刷新得到错误的非零值,它会长期存在,直到下一次读写动作触发重算。
16. 弹窗消息的读取和正文截取
弹窗条件是 type=2,status=0,isDel=0,isBack=0。读取时会对正文 htmlspecialchars_decode,然后用硬编码 HTML 片段定位“原始邮件”结束位置并截取业务正文。
flowchart LR
A[t_sys_message type=2] --> B[JOIN message_text]
B --> C[HTML decode]
C --> D[查找固定分隔div]
D --> E[substr截取业务正文]
E --> F[前端弹窗]
如果历史正文模板、HTML 标签或字符长度发生变化,截取可能为空或错位。模板改动必须用历史消息、中文标题、纯文本和富文本一起回归。
17. 定时站内信的对象和入口
tasks/Message::sendCronMessage($id='') 只能从 CLI 执行。
- 传
id:读取指定mcID,不附加当前状态和发送时间条件。 - 不传
id:查询status=0,is_delete=0,send_time<=当前时间。 recID是逗号分隔的账号 ID 列表。- 每个定时任务写一条内容,收件关系每 500 条批量插入。
- 事务提交后批量刷新未读缓存。
type=1时额外触发悬浮提醒。- 最后更新定时任务
status=1。
命令示例:
php index.php tasks/Message sendCronMessage
php index.php tasks/Message sendCronMessage 123
第二条是定点重跑,高风险:代码不会在读取时强制 status=0,执行前必须查正式消息表是否已经存在 mcronID=123。
18. 定时发送时序和事务边界
sequenceDiagram
participant C as Scheduler/CLI
participant T as t_sys_message_cron
participant X as t_sys_message_text
participant M as t_sys_message
participant R as Redis
participant Q as MQ
C->>T: SELECT 到期且status=0
loop 每个定时消息
C->>X: INSERT 内容
C->>M: 每500条批量INSERT收件关系
alt DB失败
C->>C: rollback并记录失败
else DB成功
C->>C: commit
C->>R: 重算所有收件人未读
opt type=1
C->>Q: 按sid逐个发悬浮提醒
end
C->>T: UPDATE status=1
end
end
status=1 在消息落库、缓存和悬浮提醒之后才更新,不在同一个数据库事务里。进程在中间退出会形成“消息已经生成,但定时任务仍待发送”的状态。
19. 定时消息状态和重复发送风险
stateDiagram-v2
[*] --> 待发送: status=0
待发送 --> 内容已落库但仍待发送: 提交后进程退出/状态更新失败
待发送 --> 已发送: 完成全部副作用后status=1
内容已落库但仍待发送 --> 重复消息: 下次调度再次执行
已发送 --> 重复消息: 指定id人工重跑
当前代码未看到:
SELECT ... FOR UPDATE或原子status 0 -> processing抢占。(mcronID,recID)唯一约束的代码证据。- 批次 ID、尝试次数和失败原因字段的使用。
- 悬浮提醒的独立发送审计。
推荐改造成 pending -> processing -> success/failed,通过条件更新抢占,并把每次尝试写入审计表。
20. 定时历史主题修复任务
tasks/Message::fixTopic($id=0) 按标题/正文关键词给历史消息补 topic,每批 30 条。它是数据修复而非在线通知。
风险点:循环内 $topic 若没有在每条记录开始时重置,上一条命中的主题可能污染下一条未命中的消息。执行前应先抽样查询、限制 ID 范围、提供 dry-run,并在更新后按关键词分组复核。
21. 重要站内信转悬浮提醒
sendMessageWindow 的输入 msgId 实际是内容表 mtID/messageID,不是关系表 mID。
flowchart TD
A[msgId=message_text.mtID] --> B[查t_sys_message.messageID]
B --> C[收集recID]
C --> D[每500个uid查t_sys_admin.sid]
D --> E[去重sid]
E --> F[标题截断15字]
F --> G[每个sid构造billNo=111,type=IN_MAIL]
G --> H[MqSer::sendMailToMq]
示例内部请求:
{
"msgId": 98765,
"title": "您有一条重要订单消息"
}
高风险事实:
billNo固定为字符串111,不能作为幂等或业务追踪键。- 逐
sid发布,部分成功时没有本地记录说明哪些站已发送。 sendMailToMq使用随机uniqid()作为 MQ 消息 key,重试会产生新 key。- 方法只说明“发布尝试完成”,不能证明客户端已展示。
22. sendMailToMq 的真实路由需要特别警惕
当前实现不是发送到 DEST_DGJ_NOTIFY,而是:
destination = MqEventEnums::DEST_DGJ
routing key = MqEventEnums::TYPE_CREATE_SAAS_ORDER
message key = uniqid()
payload = billNo/sid/type/title/...
路由键名称看起来像“创建 SAAS 订单”,但被大量消息提醒场景复用。不要仅根据方法名或 routing key 名称推断消费者;必须在生产 binding、实际消费者代码和消息样例中确认这一历史契约。
23. 业务通知 MQ 事件地图
以下事件由 MqSer 发布到 dgj_notify,主要面向 APP/SAAS 等下游。
| routing key | 生产方法 | 核心 payload | 触发事实 |
|---|---|---|---|
app_sa_order_out | sendSaOrderOutToMq | 调用方组装,通常含销售/出库标识 | 销售已出库 |
app_sa_order_status | sendSaOrderStatusToMq | 配送/出库状态数据 | 配送状态变化 |
app_sa_order_close | sendSaOrderCloseToAPP | order_no | 销售单关闭 |
app_sa_order_cut_off | sendSaOrderCutOffToAPP | order_no | 销售单结单 |
app_apply_return_finish | sendApplyReturnFinishToAPP | order_id,order_no,return_type | 退货入库完成 |
app_apply_return_checked | sendApplyReturnCheckedToAPP | order_id,order_no | 退货申请审核通过 |
coupon_send | 对应优惠券生产方法 | 优惠券/接收人数据 | 优惠券发放 |
activity_start | sendActivityStartToSaas | sids[] | 活动开始 |
activity_end | sendActivityEndToSaas | sids[] | 活动结束 |
24. 销售出库通知的事实顺序
sequenceDiagram
participant U as PC/App操作人
participant S as 销售/出库Service
participant DB as 销售与出库表
participant MQ as dgj_notify
participant A as APP/SAAS消费者
U->>S: 出库/配送/关闭操作
S->>DB: 写业务状态和明细
S->>MQ: 发布APP_SA_*事件
MQ-->>A: 异步消费
A-->>U: APP展示/提醒
排查原则:先证明销售/出库业务事实是否成功,再判断 MQ 是否生产,最后判断下游是否展示。用户没收到通知不等于销售单没有出库。
25. 退货通知的数据流
flowchart LR
A[退货申请审核] --> B[APP_APPLY_RETURN_CHECKED]
B --> C[order_id/order_no]
A --> D[后续退货入库]
D --> E[APP_APPLY_RETURN_FINISH]
E --> F[order_id/order_no/return_type]
C --> G[APP更新待退货状态]
F --> H[APP更新退货完成状态]
checked 和 finish 是两个不同业务事实,不能用同一事件替代。乱序或重复消费时,下游应按业务单状态和事件类型幂等,而不是只按 MQ 的随机 message key。
26. 活动开始/结束通知
活动事件只携带 sids 数组,活动 ID、批次和生效时间是否由消费者另行查询需联调确认。
flowchart TD
A[活动状态/时间变化] --> B{开始还是结束}
B -->|开始| C[activity_start sids]
B -->|结束| D[activity_end sids]
C --> E[SAAS/APP刷新活动可见性]
D --> E
如果同一服务站同时有多个活动,只按 sid 通知会要求下游重新拉取全量活动;测试应覆盖同站多活动、重复开始、延迟结束和大批量 sids。
27. 旧 APP 事件与当前事件白名单的兼容风险
MqEventEnums::DEST_TYPE_RELATION[DEST_DGJ_NOTIFY] 当前保留维修厂订单同步事件,但 APP_SA_ORDER_OUT、APP_SA_ORDER_CLOSE、APP_SA_ORDER_CUT_OFF、APP_APPLY_RETURN_*、APP_BIND_MOVE_STORAGE、APP_COUPON_SEND 被注释。
这不必然证明消息不能发布,因为 Producer 可能不使用该数组校验;但它证明“枚举中仍有生产方法”与“当前允许事件关系”不一致。升级 MQ SDK、增加统一校验或迁移消费者时,这些历史事件可能突然失效。
发布前必须做真实联调:生产端发布成功、队列 binding 命中、消费者收到、重复消息幂等、旧客户端仍能解释字段。
28. JPush 客情提醒链路
tasks/Contact 的客情提醒任务先按服务站查所有未删除账号 uid,然后把 uid 当作 JPush alias,每 500 个一批异步推送。
sequenceDiagram
participant C as Contact定时任务
participant R as t_bs_contact_remind
participant A as t_sys_admin
participant J as JpushProvider
participant P as JPush平台
C->>R: 查未来24小时提醒
C->>C: 事务更新提醒状态/年度日期
C->>A: 按sid查询所有有效uid
A-->>C: uid aliases
loop 每500个uid
C->>J: jpushApi(notification, aliases)
J->>P: 异步POST,TTL=86400
end
C->>J: asyncWait
J-->>C: msg_id列表或空结果
28.1 推送请求结构
| 字段 | 当前值/来源 | 说明 |
|---|---|---|
audience.alias | 服务站有效账号 uid[] | 客户端必须用相同 uid 绑定 alias |
platform | aim='all' | Android+iOS |
notification.*.alert | n_content | 通知栏内容 |
extras.from_where | 业务跳转标识 | 例如 APP_CONTACT_REMIND |
extras.displayTime | 当前秒级时间戳 | 展示时间 |
options.apns_production | true | iOS 生产环境 |
options.time_to_live | 86400 秒 | 离线保留一天 |
28.2 当前高风险点
pushAction无条件把硬编码 uid401加入每批业务推送,生产是否仍需要该测试账号必须确认。- 当前可见代码只打印
msg_id,没有明确写t_app_push_log。 - 回调收到空数组时后续读取
$result['msg_id']的健壮性需要验证。 - 数据库事务提交后才调用 JPush;推送失败不会回滚提醒状态。
- 用户多个设备、alias 重绑、禁用通知权限和卸载 APP 都不在 DGJ 数据库可见范围内。
29. 客情提醒状态流转
stateDiagram-v2
[*] --> 待提醒: remindStatus=0
待提醒 --> 提醒中或完成: 进入未来24小时窗口
提醒中或完成 --> 下一年待提醒: wayName=每年重复,日期+1年
提醒中或完成 --> 终态: 一次性提醒
当前注释、变量名和赋值之间存在语义不完全一致的迹象,不能只看 status0/status1/status2 变量名理解状态。应以实际 SQL、更新值和页面枚举联合确认。
30. 短信通知中心链路
SmsProvider 统一调用外部 NotifyCenter,仓库中没有短信模板正文,只有场景码和参数。
| 方法 | 场景码 | 核心请求 | 返回行为 |
|---|---|---|---|
sendVerifier | 200 | userId,bizType,phone | 返回 status/message/code |
match | 200 | userId,vfCode | 成功 true,失败抛异常 |
baitiaoExpire | 300 | args[],receivers[] | 失败记日志并返回 false |
accCheckBill | 301 | args[],receivers[] | 失败记日志并返回 false |
installmentExpire | 435 | args[],receivers[] | 失败记日志并返回 false |
wechatDown | 440 | args=[name],receivers=[mobile] | 捕获异常、记日志、返回 bool |
adminAccountOpen | 455 | args[],receivers[] | 失败记日志并返回 false |
sequenceDiagram
participant B as 业务入口
participant S as SmsProvider
participant N as NotifyCenter
B->>S: 方法(业务参数,手机号)
S->>N: POST 场景码+args/receivers
alt code=SUCCESS
N-->>S: success
S-->>B: true或status=true
else 外部失败
N-->>S: code/message
S->>S: 记录Sms日志
S-->>B: false/异常/结构化失败
end
不同方法的失败合同不一致:有的返回 false,有的返回数组,有的抛异常。调用方不能统一写成 if (!$result),必须按具体方法回归。
31. 验证码发送和校验边界
flowchart TD
A[登录/敏感操作] --> B[sendVerifier userId+phone+bizType]
B --> C[通知中心生成并发送验证码]
C --> D[用户提交vfCode]
D --> E[match userId+scenarioCode=200+vfCode]
E --> F{匹配成功}
F -->|是| G[继续登录/授权/支付操作]
F -->|否| H[抛异常阻断]
SmsEnums 记录重复发送错误码 -298402 和两分钟重复提示。频控的真实窗口、错误码全集和验证码生命周期由外部通知中心负责,需要联调确认。
32. IM 入站消息的业务链
tasks/ImCenterNotify 消费 DEST_IMCENTER_NOTIFY 的 im_app_biz_message。
代表性消息结构:
{
"msgContext": {
"msgId": "im-message-id",
"fromAccount": "sender-account",
"toAccount": "receiver-account",
"msgType": "TIMTextElem",
"msgTime": 1710000000,
"content": "{...}"
}
}
具体字段名以消费者当前取值为准;外部网关是否还包一层 data 需要用真实脱敏消息确认。
33. IM 消费时序、ACK 与重试
sequenceDiagram
participant MQ as imcenter_notify
participant C as ImCenterNotify
participant I as ImCenterProvider
participant R as RobotFacade
MQ->>C: im_app_biz_message
C->>C: 解析msgContext/msgId
C->>I: msgSyncResult([msgId])
alt 同步确认失败
I-->>C: 异常
C->>C: warning日志,继续处理
end
C->>R: handleMessage(platform=tencent_im,format=json,autoSend=true)
alt 参数/业务错误或400/404/422
C-->>MQ: ACK,不重试
else 系统异常
C-->>MQ: NACK,允许重试
else 成功
C-->>MQ: ACK
end
关键风险:代码先调用 msgSyncResult 告知 IM 中心“已同步”,再交给 RobotFacade 处理。若后续系统异常导致 NACK,IM 中心和 RabbitMQ 对同一消息的认知可能不一致。
如果上游缺少 msgId,代码使用 uniqid('im_') 兜底;每次重试会产生不同 ID,无法据此幂等。应把上游稳定消息 ID 设为强制字段,并在本地建立唯一消费记录。
34. IM 错误分类决策
flowchart TD
A[捕获Throwable] --> B{异常类型/状态码}
B -->|InvalidArgument/BizError/ParamsError| C[业务不可重试]
B -->|400/404/422| C
B -->|其他系统异常| D[可重试]
C --> E[记录错误并ACK]
D --> F[记录错误并NACK]
业务错误 ACK 是为了避免毒消息无限重试,但也意味着消息不会自动恢复。需要至少有错误指标、原消息 ID、账号和人工重放入口。
35. 对账单微信发送为何比普通通知更可追踪
Statement/SendService 使用独立任务记录,是当前仓库中相对完整的通知执行模型。
flowchart LR
A[手动/自动创建发送任务] --> B[t_bs_statement_send_record_v2 pending]
B --> C[sendToMQ recordId+priority]
C --> D[processSendTask]
D --> E[加载对账单]
E --> F[生成PDF/Excel并上传]
F --> G[解析目标微信绑定]
G --> H[逐目标发送]
H --> I[汇总成功/失败结果]
I --> J[success/failed+fail_reason+retry_count]
36. 对账发送状态机
| 值 | 状态 | 含义 |
|---|---|---|
1 | 待处理 | 已建发送记录,等待消费 |
2 | 处理中 | 消费者已开始执行 |
3 | 成功 | 当前记录发送完成 |
4 | 失败 | 记录包含 fail_reason |
5 | 无需发送 | 没有符合条件目标等无需执行情况 |
stateDiagram-v2
[*] --> 待处理: createSendTask/resend
待处理 --> 处理中: processSendTask
处理中 --> 成功: 文件和目标发送完成
处理中 --> 失败: 生成/上传/绑定/发送异常
处理中 --> 无需发送: 无有效接收目标
失败 --> 新待处理记录: resend复制原记录
成功 --> [*]
无需发送 --> [*]
重发不是把原记录改回 pending,而是创建新记录并保留原记录关联语义,适合审计。其他通知链路可参考这一模式。
37. 对账重发请求和幂等边界
resend($recordId,$targetBindingIds=[]):
- 校验原发送记录存在。
- 如果传目标绑定 ID,按
sid/contact_id校验目标有效。 - 复制对账单、服务站、客户、发送优先级等字段。
- 新记录
send_type=手动,status=待处理,retry_count=0。 - 原记录结果不覆盖,新记录入 MQ。
示例服务调用:
{
"record_id": 12345,
"target_binding_ids": [81, 82]
}
当前代码注释写“保留 original_record_id”,实际新记录构造片段是否写入该字段应以完整方法和生产 DDL 联合确认。重发前还要检查原消息是否可能已经到达,避免客户收到重复账单。
38. 用户通知与运维告警必须分开
| 类型 | 目标 | 代表实现 | 是否业务事实 |
|---|---|---|---|
| 用户通知 | 服务站、客户、APP 用户 | 站内信、JPush、短信、微信 | 业务事实的可见投影 |
| 系统集成消息 | 下游服务 | dgj_notify、DEST_DGJ | 可能驱动下游状态 |
| 运维告警 | 研发/值班人员 | DDAlert 等 | 说明系统异常,不替代业务补偿 |
告警成功不等于用户通知成功;用户通知失败也不应通过伪造主业务失败来重试。两者需要不同的接收人、严重级别和审计表。
39. 本地异步状态表 t_bs_mq_message
MqMessageEnums 定义:0待处理、1处理中、2错误、3成功。商品中心相关 Job 使用 t_bs_mq_message 保存部分异步任务状态,每次取待处理记录并更新结果。
stateDiagram-v2
[*] --> 待处理: status=0
待处理 --> 处理中: status=1
处理中 --> 成功: status=3
处理中 --> 错误: status=2
错误 --> 待处理: 人工/任务重置,需确认入口
它不是 PC 通知表,但提供了“本地持久任务状态”的思路。注意 tables.php 对该常量的注释写成“物料简码表”,与 MqMessageModel 实际用途不一致,排查时以模型和调用方为准。
40. 全通道表读写矩阵
| 对象 | 读取时机 | 新增/更新时机 | 事务边界 | 最终事实 |
|---|---|---|---|---|
t_sys_message_text | 列表 JOIN、详情、弹窗 | 人工、业务、定时发送时新增 | 与关系行同本地事务 | 标题和正文 |
t_sys_message | 列表、未读、详情、悬浮接收人 | 新增关系;读/删/撤销更新 | 写消息同事务,缓存不在可靠事务后 | 收件人状态 |
t_sys_message_cron | 调度查询 | 创建定时任务;完成后 status=1 | 完成状态与正式消息不同事务 | 调度意图和粗粒度完成标志 |
| Redis 未读 Hash | 首页读取 | 消息写、读、删、撤销后重算 | 非事务缓存 | 快速未读投影 |
t_bs_contact_remind | 每日任务 | 状态、年度日期更新 | 先提交后推送 | 提醒计划状态,不证明推送到达 |
| JPush 平台 | 设备触达 | 外部异步发送 | 外部系统 | msg_id 只证明平台受理 |
t_bs_statement_send_record_v2 | 进度、列表、重发 | 创建、处理中、成功/失败 | 独立任务事务 | 可审计发送结果 |
t_bs_mq_message | 异步 Job 拉取 | 处理状态更新 | 各 Job 自有边界 | 本地异步任务状态 |
41. 通知链路的一致性模型
flowchart TD
A[主业务数据库提交] --> B{通知实现}
B -->|事务内直接调用| C[外部失败可能影响/拖慢主事务]
B -->|提交后同步调用| D[主业务成功但通知可能丢]
B -->|发布MQ无outbox| E[数据库成功与MQ发布存在间隙]
B -->|本地任务记录+MQ| F[可查询、可重发、仍需消费幂等]
当前仓库四种模式同时存在。设计新通知时优先使用“业务事实提交 + outbox/任务记录 + 异步发送 + 幂等消费 + 可查询结果”,不要再增加事务内直接远程调用。
42. 幂等键设计建议
| 场景 | 当前可用业务键 | 不应使用 | 推荐唯一语义 |
|---|---|---|---|
| 站内信 | 业务类型+业务单号+接收uid+模板版本 | mID、随机 uniqid | 同业务事实同接收人只生成一次 |
| 定时消息 | mcID+recID | 进程时间戳 | 每个定时计划对每个收件人一次 |
| 销售通知 | routingKey+order_no+目标状态 | MQ message key | 同单同目标状态一次 |
| 退货通知 | event+order_id+return_type | 随机消息 ID | 审核和完成事件分别幂等 |
| 活动通知 | event+activity_id+sid | 只有 sid | 同活动同站同阶段一次 |
| IM 入站 | 上游稳定 msgId | 本地 uniqid('im_') | 同上游消息只处理一次 |
| 对账发送 | record_id,业务去重可加账单+目标+版本 | 文件 URL | 每个发送尝试独立审计,用户侧防重复 |
43. 重试、补偿和人工操作边界
| 通道 | 自动重试证据 | 人工恢复入口 | 重试前必须确认 |
|---|---|---|---|
站内信 mSend | 无 | 业务重新触发或人工发送 | 是否已经生成关系行 |
| 定时站内信 | 调度会再次捞 status=0 | 指定 mcID | mcronID+recID 是否已存在 |
| 悬浮提醒 | 未见本地重试记录 | 再调内部接口 | 用户可能已收到、MQ 是否部分成功 |
| APP 业务事件 | 取决于 Producer/平台 | 专项补发脚本/业务重触发 | 业务状态、下游幂等、事件白名单 |
| JPush | Provider 未见业务级重试 | 重跑任务风险高 | reminder 状态已推进、是否会全量重复 |
| IM | 系统异常 NACK | 错误 ACK 需人工重放 | 上游 msgId 和 Robot 是否已部分执行 |
| 短信 | 外部通知中心规则 | 再发送受频控 | 手机号、场景码、是否触发重复限制 |
| 对账微信 | MQ/任务机制 | resend 创建新记录 | 原记录和各目标真实送达结果 |
44. 排查总决策树
flowchart TD
A[用户反馈没收到/重复收到] --> B{哪种可见形式}
B -->|PC消息/未读/弹窗| C[查message关系和内容]
B -->|APP业务状态| D[查主业务事实和dgj_notify]
B -->|手机通知栏| E[查JPush任务、alias和msg_id]
B -->|IM机器人| F[查msgId、ACK/NACK和会话]
B -->|短信| G[查场景码、号码和NotifyCenter响应]
B -->|微信账单| H[查发送记录和目标结果]
C --> I{数据已落库}
I -->|否| J[查调用条件/事务/接收主账号]
I -->|是| K[查已读删除撤销/缓存/前端]
45. SOP:站内信完全没有生成
- 用业务单号定位应触发通知的 Service/Task。
- 查调用是否进入
mSend,参数中的目标是sid还是uid。 - 查目标服务站是否存在
roleid=0主账号。 - 查
mSend返回值;该方法捕获所有Exception后只返回 false,原始原因可能丢失。 - 按时间、标题查内容表,再按
messageID查关系表。 - 如果内容存在但关系不存在,检查事务状态和第二次 insert。
- 如果业务调用发生在外层事务内,确认外层是否回滚。
只读 SQL 模板:
SELECT uid, sid, roleid, isDelete
FROM t_sys_admin
WHERE sid = :sid
ORDER BY roleid, uid;
SELECT mtID, title, sendTime
FROM t_sys_message_text
WHERE title LIKE CONCAT('%', :title_keyword, '%')
ORDER BY mtID DESC
LIMIT 50;
SELECT mID, messageID, sendID, recID, status, type, topic, source,
isDel, isBack, sendTimes, mcronID
FROM t_sys_message
WHERE messageID = :message_id
ORDER BY mID;
46. SOP:消息存在但未读数错误
flowchart TD
A[未读数错误] --> B[按严格DB口径COUNT]
B --> C[读取Redis Hash field]
C --> D{DB与Redis一致}
D -->|否| E[调用内部刷新或删除field回源]
D -->|是| F[确认页面调用新/旧接口]
F --> G[核对主账号uid与当前JXCUID]
G --> H[核对source/isBack/isDel/status]
SELECT recID, COUNT(*) AS unread_qty
FROM t_sys_message
WHERE recID IN (:main_uid, :current_uid)
AND isDel = 0
AND isBack = 0
AND status = 0
AND source = 0
GROUP BY recID;
Redis 只读检查示例,禁止在生产直接清整张 Hash:
redis-cli HGET dgj:home_cache:hash_message <uid>
47. SOP:定时消息重复或漏发
SELECT mcID, status, is_delete, send_time, sendID, recID, type, topic
FROM t_sys_message_cron
WHERE mcID = :cron_id;
SELECT recID, COUNT(*) AS qty, MIN(mID) AS first_mid, MAX(mID) AS last_mid
FROM t_sys_message
WHERE mcronID = :cron_id
GROUP BY recID
HAVING COUNT(*) <> 1;
判断顺序:
- 定时记录是否到期且未删除。
status是否仍为 0,但已经存在mcronID关系行。- 是否两个调度进程在相近时间启动。
- 内容表是否生成多条同标题同时间内容。
- 重复的是 PC 站内信还是只有悬浮提醒。
- 人工是否使用指定 ID 重跑过已完成任务。
不要先删重复关系。应先确认用户是否已读、是否有关联审计、未读缓存如何回算,并形成可回滚修复清单。
48. SOP:APP/JPush/IM/短信/微信失败
48.1 APP 业务事件没到
- 查主业务状态是否真的达到事件触发点。
- 在生产者方法附近查业务单号、routing key 和异常。
- 确认
dgj_notifybinding 包含该旧事件。 - 查消费者是否收到、ACK/NACK、是否因旧状态主动丢弃。
- 重发前用订单号和目标状态确认下游幂等。
48.2 JPush 没显示
- 任务是否查到提醒记录和服务站账号。
- 目标 uid 是否已经绑定 JPush alias。
- 分批请求是否返回
msg_id。 - iOS 生产环境、APP 通知权限、TTL 是否符合预期。
- 用户是否被硬编码测试账号或批次异常影响。
48.3 IM 没进入机器人
- 按上游
msgId查ImCenterNotify日志。 - 区分同步确认失败、Robot 业务错误 ACK、系统错误 NACK。
- 查 Robot 会话/消息记录是否已经部分落库。
- 不用兜底
im_*ID 判断重复,必须找到上游稳定 ID。
48.4 短信没收到
- 确认调用的是验证码还是模板通知方法。
- 核对
scenarioCode和参数顺序,不记录完整手机号到公共文档。 - 查返回 code/message 和
Sms模块日志。 - 检查重复发送频控、手机号有效性和模板状态。
48.5 对账微信失败
- 查
t_bs_statement_send_record_v2.status/fail_reason/retry_count。 - 查账单、客户、目标绑定是否有效。
- 查 PDF/Excel 生成和 OSS 上传。
- 查每个微信目标的结果,而不只看外层记录。
- 必须通过
resend建新记录,不直接篡改原成功/失败事实。
49. 常用日志和代码定位命令
rg -n "mSend\(|saveMessageCache|getUnreadMessage|sendMessageWindow" \
application/controllers application/service application/Services
rg -n "APP_SA_ORDER_OUT|APP_SA_ORDER_STATUS|APP_SA_ORDER_CLOSE|APP_SA_ORDER_CUT_OFF|APP_APPLY_RETURN" \
application/KzData/Enums application/Services
rg -n "JpushProvider|pushAction|contactRemind|APP_CONTACT_REMIND" \
application/controllers/tasks application/Providers
rg -n "scenarioCode|sendVerifier|accCheckBill|installmentExpire|adminAccountOpen" \
application/Providers/NotifyCenter application/Services application/controllers
rg -n "ImCenterNotify|im_app_biz_message|msgSyncResult|handleMessage" application
rg -n "createSendTask|processSendTask|sendToMQ|resend|SEND_STATUS_" \
application/Services/Statement application/KzData/Enums/StatementEnums.php
任务日志关键字:
| 链路 | 日志/关键词 |
|---|---|
| 定时站内信 | logger CronMessage/send、定时站内信发送开始/失败/成功 |
| JPush | [Jpush]、push content、msg_id |
| 短信 | logger module Sms 和具体方法名 |
| IM | ImCenterNotify、msgId、同步确认、ACK/NACK、Robot |
| 对账 | 发送记录 ID、statement ID、fail_reason、上传/微信目标 |
50. 风险登记册
| 编号 | 风险 | 影响 | 建议 |
|---|---|---|---|
| R47-01 | mSend 在 DB commit 前刷新 Redis | 未读数多/少 | 提交后刷新,失败删除 field |
| R47-02 | mSend 捕获异常只返回 false | 根因不可观测 | 记录结构化错误和业务键 |
| R47-03 | 目标 sid 只投递主账号 | 子账号可能收不到 | 明确产品接收人策略 |
| R47-04 | 人工发送 contact 是 uid,业务发送 toid 是 sid | 调用混淆 | DTO/方法名区分目标类型 |
| R47-05 | 列表只显示 source in (0,8) | 其他来源不可见 | 与消息中心统一来源契约 |
| R47-06 | 新旧未读接口口径不同 | 数量不一致 | 下线旧接口或统一条件 |
| R47-07 | Redis 未读 Hash 无 TTL | 错误值长期存在 | 增加自愈和定期对账 |
| R47-08 | 弹窗正文依赖硬编码 HTML 截取 | 模板改动导致空正文 | 内容正文结构化存储 |
| R47-09 | 定时发送无原子抢占 | 并发重复发送 | processing 状态+条件更新 |
| R47-10 | 定时状态晚于消息和 MQ 更新 | 崩溃后重发 | 收件唯一键+执行审计 |
| R47-11 | 指定 cron ID 可重跑已完成任务 | 人工重复 | 重跑前强制检查/确认 |
| R47-12 | 悬浮提醒 billNo=111 | 无法按业务追踪/幂等 | 使用真实业务键或通知任务 ID |
| R47-13 | MQ message key 使用 uniqid() | 重试不能天然幂等 | 消费者使用业务键 |
| R47-14 | sendMailToMq 复用订单创建 routing key | 历史契约难理解 | 明确消费者并逐步迁移专用事件 |
| R47-15 | 旧 APP 事件不在事件关系白名单 | SDK升级后可能阻断 | 线上发布/消费联调 |
| R47-16 | JPush 无条件加入 uid 401 | 非业务账号收到通知 | 确认后移除或配置化 |
| R47-17 | JPush 发送缺少本地目标级审计 | 无法判断部分成功 | 建发送任务/目标结果表 |
| R47-18 | IM 先 msgSyncResult 后业务处理 | 上下游确认不一致 | 处理成功后确认或引入阶段状态 |
| R47-19 | IM 缺 msgId 时随机生成 | 无法幂等 | 强制上游稳定 msgId |
| R47-20 | 业务错误 ACK 后无自动恢复 | 消息永久丢业务动作 | 错误队列/人工重放台 |
| R47-21 | 短信方法失败合同不一致 | 调用方误判 | 统一 Result DTO |
| R47-22 | 通知模板正文在外部系统 | 仓库无法评审内容变化 | 建模板版本清单和联调快照 |
| R47-23 | 普通通知缺 outbox | 业务成功通知丢失 | 采用可持久任务模型 |
| R47-24 | mDelete 所有权校验不明显 | 越权删除风险 | Controller/Service 双重校验 |
| R47-25 | fixTopic 变量可能跨循环污染 | 历史主题误分类 | 每条初始化并先 dry-run |
51. 回归测试清单
51.1 站内信
- 系统给存在主账号的服务站发送普通、重要、弹窗消息。
- 服务站没有
roleid=0主账号时返回 false 且有可定位日志。 - 主账号和子账号登录时列表、未读数量符合产品规则。
- 查看详情、弹窗已读、删除、撤销后数据库和 Redis 一致。
- 富文本、纯文本、超长中文标题、特殊字符不破坏正文。
- 同一内容多收件人时,各自已读和删除互不影响。
- 非发送者不能撤销;非所有者不能删除他人消息。
51.2 定时和悬浮提醒
- 未到期不发送,到期只发送一次,删除任务不发送。
- 501、1001 个接收人验证 500 条分批边界。
- 两个调度进程并发时不重复。
- DB 提交后进程退出可安全恢复。
- 指定 ID 重跑必须有重复保护。
- 重要消息悬浮标题 15 字边界、多个账号同一 sid 去重。
51.3 APP、JPush、IM 和短信
- 销售出库/配送/关闭/结单事件字段和旧客户端兼容。
- 退货审核与完成事件不混淆,重复/乱序不回退状态。
- 活动同站多活动、大批量服务站、重复开始/结束。
- JPush 500 alias 分批、无 alias、失效 alias、Android/iOS、离线 TTL。
- IM 成功、业务错误 ACK、系统错误 NACK、重复 msgId、缺失 msgId。
- 短信成功、外部失败、超时、重复频控、验证码错误和过期。
51.4 对账单发送
- 手动和自动任务、优先级、单条和批量账单。
- 无绑定、部分绑定失效、文件生成失败、上传失败、微信部分成功。
- pending/processing/success/failed/no-need 状态完整。
- 重发创建新记录,原记录不被覆盖,指定目标必须属于当前客户。
52. 监控和治理建议
flowchart LR
A[业务通知任务] --> B[统一notification_task]
B --> C[notification_target]
C --> D[渠道适配器]
D --> E[站内信]
D --> F[APP/JPush]
D --> G[短信]
D --> H[微信/IM]
E --> I[结果/重试/耗时]
F --> I
G --> I
H --> I
I --> J[指标+告警+人工重放]
建议最少监控:
- 各通道创建量、成功量、失败量、无需发送量。
- 从业务事实到通知创建、到平台受理的延迟。
- 同业务键重复任务数。
- 定时任务长期 pending/processing 数。
- Redis 未读数和数据库抽样差异。
- IM ACK/NACK、业务错误 ACK 和缺失 msgId 数。
- JPush 空
msg_id、无 alias、批次部分失败。 - 对账发送失败原因 TopN 和重发成功率。
53. 证据来源
| 结论 | 代码证据 |
|---|---|
| 站内信写入、未读缓存和悬浮提醒 | application/service/sysitem/MessageService.php |
| 人工发送、列表、详情、删除、撤销 | application/controllers/Message.php |
| 内部缓存刷新和悬浮入口 | application/controllers/inner/Message.php |
| 定时发送和历史主题修复 | application/controllers/tasks/Message.php |
| 未读严格口径、弹窗、消息中心 | application/models/message/MessageModel.php |
| 类型、来源、主题和跳转 | application/KzData/Enums/MessageTypeEnums.php |
| MQ 事件和当前关系白名单 | application/KzData/Enums/MqEventEnums.php |
| 消息 MQ Producers | application/Services/Mq/MqSer.php |
| IM ACK/NACK | application/controllers/tasks/ImCenterNotify.php |
| JPush 请求和客情任务 | application/Providers/DataService/JpushProvider.php、application/controllers/tasks/Contact.php |
| 短信场景和返回合同 | application/Providers/NotifyCenter/Sms/SmsProvider.php |
| 对账发送记录、处理和重发 | application/Services/Statement/SendService.php、StatementEnums.php |
| 表常量 | application/config/tables.php |
54. 待环境确认
- 生产 RabbitMQ 中
DEST_DGJ/TYPE_CREATE_SAAS_ORDER的真实消费者,以及为何承载消息提醒。 dgj_notify中被注释旧 APP 事件是否仍有 binding、消费量和迁移计划。- MQ Producer 是否启用 confirm、失败是否抛异常、队列 TTL、DLX、重试次数和保序策略。
t_sys_message、t_sys_message_cron、发送记录表的 DDL、索引和唯一约束。- 定时消息真实调度频率、是否多实例重叠执行、历史重复率。
- JPush uid alias 的客户端绑定规则、硬编码 uid 401 的业务归属和
t_app_push_log实际写入口。 - NotifyCenter 场景 200/300/301/435/440/455 的线上模板版本、频控和错误码全集。
- IM 中心
msgSyncResult的准确语义、真实消息 envelope 和重复投递合同。 - 对账单微信目标结果表、文件保留期、外部发送幂等和重发去重策略。
- 用户通知是否有统一监控、值班告警、人工重放权限和审计要求。
55. 修改后的最小发布验证
任何通知相关公共代码改动,至少按下面顺序验证:
- 用测试服务站创建普通站内信,确认内容、关系和 Redis 未读数。
- 查看详情并确认已读回算,随后验证删除和撤销权限。
- 创建一分钟后到期的定时消息,确认只生成一次并记录
mcronID。 - 触发一条重要消息,确认站内信与悬浮提醒是两个可分别观测的结果。
- 触发一种
dgj_notify业务事件并在真实消费者侧确认 payload。 - 分别测试 JPush、短信或 IM 中本次改动涉及的通道。
- 涉及对账发送时验证发送记录状态、失败原因和新记录重发。
- 最后做主业务回归,确认通知失败不会错误回滚或重复执行订单、库存、资金事实。
请求-日志-数据变更追踪卡
多入口请求链路
| 场景 | 调用方与入口 | 请求载荷/上下文 | Controller/Consumer | Service/Provider | 汇合点 | 最终业务事实 |
|---|---|---|---|---|---|---|
| 站内信 | 业务 Service/后台 | topic、用户/站点、标题、正文、业务链接 | Message.php/调用 Service | Message Model/Service | message ID + receiver | 消息主文、接收/已读状态 |
| App 推送 | 业务 Service/MQ | 用户/设备、模板、业务参数 | Task/Service | MqSer/Jpush/Notify Provider | push/message ID | 外部推送受理和本地日志 |
| 定时提醒 | MESSAGE_CRON/联系人/出库提醒 task | 计划时间、对象、业务键 | Cron Task | Reminder Service | cron/reminder ID | 到期生成一次通知 |
| 对账/业务通知 | Statement/Order/Activity Service | bill/order/activity、接收目标 | 领域 Service | MqSer/SendService | business ID + send record | 发送记录保留目标和结果 |
日志证据矩阵
| 链路段 | 日志来源 | 可检索锚点 | 成功信号 | 失败信号 | 与下一段关联方式 | | --- | --- | --- | --- | --- | --- | --- | | 业务触发 | 调用 Service | request_id、业务单号、topic、receiver | 主业务 commit 后触发通知 | 通知在 commit 前发送、业务键缺失 | business ID 进入消息记录 | | 本地消息 | Message Service/Model | message ID、receiver、source/topic | MESSAGE/MESSAGE_TEXT 关系完整 | 主文无接收人、重复生成 | message ID 进入 MQ/Provider | | 推送发送 | MqSer/Jpush/Notify Provider | push/message ID、external request ID、模板 | 对方 accepted/ACK | timeout、token 无效、业务码失败 | external request ID 查回执 | | 定时重试 | Message cron/reminder task | task batch、message/reminder ID、retry | 成功后不再扫描 | 重复推送、失败无限重试 | stable business key + attempt |
环节数据变更台账
| 步骤 | 代码位置 | 事务 | 读取事实 | 写入表/缓存/MQ | 字段或数量变化 | 回查证据 |
|---|---|---|---|---|---|---|
| 主业务完成 | Order/Inventory/Payment 等 Service | 主业务事务 | 业务动作 | 主业务表 | status/qty/amount old -> new | 业务单号、commit 时间 |
| 生成站内信 | Message Service | 主业务后或独立事务 | topic、接收人、业务链接 | MESSAGE、MESSAGE_TEXT、接收关系 | message insert;read=0 | message+receiver+business ID |
| 计划提醒 | Reminder Service | 独立事务 | 提醒规则、是否已计划 | MESSAGE_CRON/BS_CONTACT_REMIND* | cron status pending、scheduledAt | reminder ID、稳定业务键 |
| 外部推送 | MqSer/Provider | DB 外部边界 | 消息内容、设备 token(脱敏) | MQ、APP_PUSH_LOG、外部服务 | pending -> accepted/success/failed;attempt +1 | push ID、Provider 回执 |
| 已读/重试 | Message.php/Cron Task | 单消息事务 | 当前 read/send status、幂等键 | 接收/发送记录、首页缓存 | read 0 -> 1;失败重试不重建主业务 | user+message、attempt、主业务无新增 |
子模块追踪:message-inbox 站内信创建、收件与已读
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 创建收件 | 主业务完成/系统事件创建站内信 | topic、business ID、receiver IDs、message key | application/service/sysitem/MessageService.php -> application/models/message/MessageModel.php | 主业务终态、模板、接收范围和重复键 | 独立消息本地事务 insert MESSAGE/TEXT/receiver,read=0 | request/task + message/business + receiver count | 消息失败不回滚主业务;稳定键防重复收件 |
| 列表已读 | 用户查询/标记已读 | user、message ID/list filters | application/controllers/Message.php | 用户接收关系、删除/撤销态和当前 read | 查询只读;已读本地事务 read 0 -> 1、重复 0 变化 | request ID + user/message + affected rows | 不能读/改他人消息;首页缓存 commit 后刷新 |
子模块追踪:message-manual 人工发送、删除与撤销
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 人工发送 | 运营选择接收人提交消息 | operator、manual batch/message、receivers | application/controllers/inner/Message.php -> application/service/sysitem/MessageService.php | 发送权限、内容、接收范围和批次幂等 | 每批消息本地事务 insert 主文/收件关系,status active | request ID + operator + batch/message + receiver count | 越权/空接收零写入;正文日志摘要化 |
| 删除撤销 | 用户删除收件/运营撤回 | message ID、user/operator、action | application/service/sysitem/MessageService.php | 所有权、撤销权限、已读和发送状态 | 用户关系软删或主消息 active -> revoked | request ID + message/user + action | 撤销不抹审计;外部推送已送达不能“收回”,仅补撤销提示 |
子模块追踪:message-cron 定时站内信
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 计划任务 | 创建未来发送的站内信 | cron/reminder ID、scheduledAt、rule/business key | application/controllers/tasks/Message.php | 提醒规则、时区、接收人和已有计划 | 计划本地事务 insert,status none -> pending | request ID + cron/reminder + scheduledAt | 同业务键同时间不重复计划;时区需环境确认 |
| 到期发送 | 任务扫描 pending 到期项 | task batch、cron IDs | application/controllers/tasks/Message.php | pending、scheduledAt、主业务是否仍满足和已发记录 | 每项本地事务 pending -> sent/canceled/failed 并创建收件 | task + batch + cron/message + result | 业务已失效则取消;失败项重试不重复收件 |
子模块追踪:message-remind 弹窗与悬浮提醒
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 提醒查询 | 首页加载弹窗/悬浮提醒 | user/sid、client、time | application/controllers/Message.php | 未读收件、提醒类型、展示窗口、已关闭/频次 | 查询只读;返回符合规则提醒 | request ID + user/sid + reminder/message IDs | 缓存旧与业务未读分开;无权消息不展示 |
| 关闭确认 | 用户关闭/稍后提醒 | user、reminder/message ID、action | application/service/sysitem/MessageService.php | 当前展示/已读状态和频控 | 本地事务 displayed/open -> dismissed/snoozed/read | request ID + user/message + old/new | 多端重复操作幂等;不删除消息正文 |
子模块追踪:message-app-mq APP 业务通知 MQ
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 生产消息 | 订单/库存/支付等 commit 后 | business ID、event、receiver/sid | application/Services/Mq/MqSer.php | 已提交业务快照、模板/事件和稳定幂等键 | 本地事务外发 APP notification MQ,主业务 DB 不变 | business ID + destination/routing + message ID | 发布失败只补消息,不重做订单/库存/资金 |
| 消费回查 | APP 未收到 | message ID、business key、receiver | application/KzData/Enums/MqEventEnums.php | 生产结果、队列/消费 ACK 和 App 发送记录 | 查询只读;确认未处理后按业务键补发 | business/message + ACK/send status | 随机 messageId 不作幂等;环境队列事实需验证 |
子模块追踪:message-jpush JPush 客情提醒
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 推送受理 | 客情/业务提醒发 JPush | push ID、template、device/alias hash | application/Providers/DataService/JpushProvider.php | 通知内容、接收目标、去重和设备状态 | 外部调用事务外;本地发送记录 pending -> accepted/failed | request ID + push/business + provider code | accepted 不等于展示;设备 token 只记录 hash |
| 失败重试 | Provider 失败/无回执 | push ID、attempt、last error code | application/service/sysitem/MessageService.php | 主业务、发送记录、最大次数和下次时间 | 每次本地事务 attempt +1、status retry/failed | push + attempt + provider result | 重试不重建站内信/主业务;无效设备按规则停止 |
子模块追踪:message-sms 短信与验证码
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 发送短信 | 验证码/业务通知 | sms/request ID、template、mobile hash、scene | application/Providers/NotifyCenter/Sms/SmsProvider.php | 模板、频控、接收方、验证码 hash/有效期和去重 | 外部调用事务外;本地发送态 pending -> accepted/failed | request ID + sms/template + mobile hash + code | 不记录验证码/手机号明文;频控失败零外部调用 |
| 验证/回执 | 用户提交验证码或通道结果 | verify/sms ID、input hash、status | application/service/sysitem/MessageService.php | hash、过期、尝试次数和使用标记 | 验证本地事务成功时 unused -> used,尝试数 +1 | verify/sms + result/attempt | 同码仅一次;通道受理不等于送达,失败按 scene 重试 |
子模块追踪:message-im IM 入站消息
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| IM 消费 | IM Center 投递消息/事件 | message ID、conversation/user、event | application/controllers/tasks/ImCenterNotify.php | 消息幂等、用户绑定、会话和事件版本 | 单消息本地事务 insert record/update conversation old -> new | message + conversation/user + record | 重复/旧事件零非法变化;ACK 在 commit 后 |
| 通知分流 | IM 消息转机器人/人工/站内提醒 | conversation、record、route decision | application/Services/Mq/MqSer.php | owner/state、场景和目标 | commit 后事务外分发,主消息不重插 | record/conversation + routing/message result | 分发失败只补目标通知,不重消费 IM 入站 |
子模块追踪:message-statement 对账单微信发送与重发
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 微信发送 | 对账单文件 ready 后 | statement/file/send ID、recipient hash | application/Services/Statement/SendService.php | 账单快照、文件、接收目标和已有发送记录 | 本地发送记录 none/pending -> accepted/failed;外部发送事务外 | statement + file/send + provider result | 文件/账单不重生成;accepted 不等于送达 |
| 定向重发 | 失败或用户请求重发 | send ID、attempt、same statement/file | application/KzData/Enums/StatementEnums.php | 原发送记录、账单/文件仍有效和重试上限 | 本地事务 attempt +1、status retry/success/failed | send + attempt + external message ID | 稳定 send key 防重复多份;接收人变更另建记录 |
子模块追踪:message-ops-alert 用户通知与运维告警隔离
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 分类路由 | 业务事件/系统异常生成消息 | message type、severity、business/incident ID | application/KzData/Enums/MessageTypeEnums.php | 用户通知模板、运维告警规则、接收群和敏感字段 | 路由判断只读;分别创建用户消息或告警发送记录 | event/incident + type/severity + target | 运维异常不得发给用户;用户通知不泄露堆栈/内部 ID |
| 告警重试 | 运维通道失败 | alert ID、channel、attempt | application/controllers/tasks/Contact.php | 事件仍有效、发送记录和抑制/聚合键 | 独立告警事务 attempt +1,主业务 DB 不变 | alert/incident + channel/result | 告警失败不触发业务补偿;按 incident 聚合防风暴 |