本文面向第一次接触 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_logJpushProvider、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 业务目标

  1. 业务状态变化后,把必要信息送到正确服务站、账号或客户。
  2. PC 端能够查看历史消息、区分已读未读并展示弹窗。
  3. 重要消息可以通过悬浮提醒、APP、短信或微信增加触达率。
  4. 批量、定时和跨系统通知能够追踪发送结果并在失败后恢复。
  5. 通知失败不能误判成主业务失败,主业务成功也不能掩盖通知丢失。

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 的账号uidmSend 会把目标 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::sendCronMessageCLI 调度到期消息批量落入正式消息表
业务通知 MQapplication/Services/Mq/MqSer.php业务 Service发布到 dgj_notify 或 dgj
IM 消费tasks/ImCenterNotifyRabbitMQ同步确认消息并交给机器人处理
JPushtasks/Contact::pushAction定时客情任务按服务站账号 alias 批量推送
短信SmsProvider登录、财务、账号、任务调通知中心场景模板
对账发送Statement/SendService手动/自动对账任务建发送记录、入 MQ、生成文件、微信发送

6. 核心表和缓存字典

常量物理表/键作用关键字段或维度
MESSAGEt_sys_message每个收件人的消息关系和状态mID、messageID、sendID、recID、status、type、topic、source
MESSAGE_TEXTt_sys_message_text可复用的标题和正文mtID、title、content、sendTime
MESSAGE_CRONt_sys_message_cron未到期/已发送的定时消息mcID、recID、sendID、send_time、status、is_delete
APP_PUSH_LOGt_app_push_logAPP 推送日志常量当前客情 JPush 调用链未看到明确写入点
BS_CONTACT_REMINDt_bs_contact_remind客情提醒计划sid、remindDate、remindStatus、wayName
BS_CONTACT_REMIND_WAYt_bs_contact_remind_way客情提醒方式提醒方式配置
SCM_OUTBOUND_REMINDER_LOGt_scm_outbound_reminder_log销售出库提醒去重/审计具体唯一约束待 DDL 确认
T_BS_MQ_MESSAGEt_bs_mq_message部分异步任务本地状态status 等;tables.php 注释与实际模型用途不一致
T_BS_STATEMENT_SEND_RECORDt_bs_statement_send_record_v2对账单发送任务status、fail_reason、retry_count、target_binding_ids
HOME_CACHE_HASH_MESSAGEdgj:home_cache:hash_messagePC 首页未读数 Hashfield=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-19Robot/移动商城/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 最终可查询事实

  1. t_sys_message_text.mtID:消息内容 ID。
  2. t_sys_message.mID:收件关系 ID。
  3. t_sys_message.messageID=mtID:内容关联。
  4. t_sys_message.recID=主账号 uid:真正接收账号。
  5. 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发送表单
mAddSavecontact,type,title,content,topic标题 3-150、内容 5-20000;写内容和关系JSON status/msg
mReplyrID,mID读取接收人和原文,标题加“回复”回复页面
mListaction,act,skey,page,rows,topic收件/发件、已读/未读、标题搜索分页 JSON
mDetailid,flag非发件箱查看会置已读详情页面
mDeleteid 列表字符串isDel=1,刷新收件人未读缓存JSON
mBackid 列表字符串仅发送者可撤销,isDel=1,isBack=1JSON
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:

  1. 根据 JXCSID 查服务站主账号 uid。
  2. 同时使用当前登录 JXCUID。
  3. 收件条件为 recID in (主账号uid, 当前uid)。
  4. 追加 isDel=0 和 source in (0,8)。
  5. 可按 topic、标题、已读状态过滤。
  6. 首屏最多把 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_outsendSaOrderOutToMq调用方组装,通常含销售/出库标识销售已出库
app_sa_order_statussendSaOrderStatusToMq配送/出库状态数据配送状态变化
app_sa_order_closesendSaOrderCloseToAPPorder_no销售单关闭
app_sa_order_cut_offsendSaOrderCutOffToAPPorder_no销售单结单
app_apply_return_finishsendApplyReturnFinishToAPPorder_id,order_no,return_type退货入库完成
app_apply_return_checkedsendApplyReturnCheckedToAPPorder_id,order_no退货申请审核通过
coupon_send对应优惠券生产方法优惠券/接收人数据优惠券发放
activity_startsendActivityStartToSaassids[]活动开始
activity_endsendActivityEndToSaassids[]活动结束

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
platformaim='all'Android+iOS
notification.*.alertn_content通知栏内容
extras.from_where业务跳转标识例如 APP_CONTACT_REMIND
extras.displayTime当前秒级时间戳展示时间
options.apns_productiontrueiOS 生产环境
options.time_to_live86400 秒离线保留一天

28.2 当前高风险点

  • pushAction 无条件把硬编码 uid 401 加入每批业务推送,生产是否仍需要该测试账号必须确认。
  • 当前可见代码只打印 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,仓库中没有短信模板正文,只有场景码和参数。

方法场景码核心请求返回行为
sendVerifier200userId,bizType,phone返回 status/message/code
match200userId,vfCode成功 true,失败抛异常
baitiaoExpire300args[],receivers[]失败记日志并返回 false
accCheckBill301args[],receivers[]失败记日志并返回 false
installmentExpire435args[],receivers[]失败记日志并返回 false
wechatDown440args=[name],receivers=[mobile]捕获异常、记日志、返回 bool
adminAccountOpen455args[],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=[]):

  1. 校验原发送记录存在。
  2. 如果传目标绑定 ID,按 sid/contact_id 校验目标有效。
  3. 复制对账单、服务站、客户、发送优先级等字段。
  4. 新记录 send_type=手动,status=待处理,retry_count=0。
  5. 原记录结果不覆盖,新记录入 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指定 mcIDmcronID+recID 是否已存在
悬浮提醒未见本地重试记录再调内部接口用户可能已收到、MQ 是否部分成功
APP 业务事件取决于 Producer/平台专项补发脚本/业务重触发业务状态、下游幂等、事件白名单
JPushProvider 未见业务级重试重跑任务风险高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:站内信完全没有生成

  1. 用业务单号定位应触发通知的 Service/Task。
  2. 查调用是否进入 mSend,参数中的目标是 sid 还是 uid。
  3. 查目标服务站是否存在 roleid=0 主账号。
  4. 查 mSend 返回值;该方法捕获所有 Exception 后只返回 false,原始原因可能丢失。
  5. 按时间、标题查内容表,再按 messageID 查关系表。
  6. 如果内容存在但关系不存在,检查事务状态和第二次 insert。
  7. 如果业务调用发生在外层事务内,确认外层是否回滚。

只读 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;

判断顺序:

  1. 定时记录是否到期且未删除。
  2. status 是否仍为 0,但已经存在 mcronID 关系行。
  3. 是否两个调度进程在相近时间启动。
  4. 内容表是否生成多条同标题同时间内容。
  5. 重复的是 PC 站内信还是只有悬浮提醒。
  6. 人工是否使用指定 ID 重跑过已完成任务。

不要先删重复关系。应先确认用户是否已读、是否有关联审计、未读缓存如何回算,并形成可回滚修复清单。

48. SOP:APP/JPush/IM/短信/微信失败

48.1 APP 业务事件没到

  1. 查主业务状态是否真的达到事件触发点。
  2. 在生产者方法附近查业务单号、routing key 和异常。
  3. 确认 dgj_notify binding 包含该旧事件。
  4. 查消费者是否收到、ACK/NACK、是否因旧状态主动丢弃。
  5. 重发前用订单号和目标状态确认下游幂等。

48.2 JPush 没显示

  1. 任务是否查到提醒记录和服务站账号。
  2. 目标 uid 是否已经绑定 JPush alias。
  3. 分批请求是否返回 msg_id。
  4. iOS 生产环境、APP 通知权限、TTL 是否符合预期。
  5. 用户是否被硬编码测试账号或批次异常影响。

48.3 IM 没进入机器人

  1. 按上游 msgId 查 ImCenterNotify 日志。
  2. 区分同步确认失败、Robot 业务错误 ACK、系统错误 NACK。
  3. 查 Robot 会话/消息记录是否已经部分落库。
  4. 不用兜底 im_* ID 判断重复,必须找到上游稳定 ID。

48.4 短信没收到

  1. 确认调用的是验证码还是模板通知方法。
  2. 核对 scenarioCode 和参数顺序,不记录完整手机号到公共文档。
  3. 查返回 code/message 和 Sms 模块日志。
  4. 检查重复发送频控、手机号有效性和模板状态。

48.5 对账微信失败

  1. 查 t_bs_statement_send_record_v2.status/fail_reason/retry_count。
  2. 查账单、客户、目标绑定是否有效。
  3. 查 PDF/Excel 生成和 OSS 上传。
  4. 查每个微信目标的结果,而不只看外层记录。
  5. 必须通过 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 和具体方法名
IMImCenterNotify、msgId、同步确认、ACK/NACK、Robot
对账发送记录 ID、statement ID、fail_reason、上传/微信目标

50. 风险登记册

编号风险影响建议
R47-01mSend 在 DB commit 前刷新 Redis未读数多/少提交后刷新,失败删除 field
R47-02mSend 捕获异常只返回 false根因不可观测记录结构化错误和业务键
R47-03目标 sid 只投递主账号子账号可能收不到明确产品接收人策略
R47-04人工发送 contact 是 uid,业务发送 toid 是 sid调用混淆DTO/方法名区分目标类型
R47-05列表只显示 source in (0,8)其他来源不可见与消息中心统一来源契约
R47-06新旧未读接口口径不同数量不一致下线旧接口或统一条件
R47-07Redis 未读 Hash 无 TTL错误值长期存在增加自愈和定期对账
R47-08弹窗正文依赖硬编码 HTML 截取模板改动导致空正文内容正文结构化存储
R47-09定时发送无原子抢占并发重复发送processing 状态+条件更新
R47-10定时状态晚于消息和 MQ 更新崩溃后重发收件唯一键+执行审计
R47-11指定 cron ID 可重跑已完成任务人工重复重跑前强制检查/确认
R47-12悬浮提醒 billNo=111无法按业务追踪/幂等使用真实业务键或通知任务 ID
R47-13MQ message key 使用 uniqid()重试不能天然幂等消费者使用业务键
R47-14sendMailToMq 复用订单创建 routing key历史契约难理解明确消费者并逐步迁移专用事件
R47-15旧 APP 事件不在事件关系白名单SDK升级后可能阻断线上发布/消费联调
R47-16JPush 无条件加入 uid 401非业务账号收到通知确认后移除或配置化
R47-17JPush 发送缺少本地目标级审计无法判断部分成功建发送任务/目标结果表
R47-18IM 先 msgSyncResult 后业务处理上下游确认不一致处理成功后确认或引入阶段状态
R47-19IM 缺 msgId 时随机生成无法幂等强制上游稳定 msgId
R47-20业务错误 ACK 后无自动恢复消息永久丢业务动作错误队列/人工重放台
R47-21短信方法失败合同不一致调用方误判统一 Result DTO
R47-22通知模板正文在外部系统仓库无法评审内容变化建模板版本清单和联调快照
R47-23普通通知缺 outbox业务成功通知丢失采用可持久任务模型
R47-24mDelete 所有权校验不明显越权删除风险Controller/Service 双重校验
R47-25fixTopic 变量可能跨循环污染历史主题误分类每条初始化并先 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 Producersapplication/Services/Mq/MqSer.php
IM ACK/NACKapplication/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. 待环境确认

  1. 生产 RabbitMQ 中 DEST_DGJ/TYPE_CREATE_SAAS_ORDER 的真实消费者,以及为何承载消息提醒。
  2. dgj_notify 中被注释旧 APP 事件是否仍有 binding、消费量和迁移计划。
  3. MQ Producer 是否启用 confirm、失败是否抛异常、队列 TTL、DLX、重试次数和保序策略。
  4. t_sys_message、t_sys_message_cron、发送记录表的 DDL、索引和唯一约束。
  5. 定时消息真实调度频率、是否多实例重叠执行、历史重复率。
  6. JPush uid alias 的客户端绑定规则、硬编码 uid 401 的业务归属和 t_app_push_log 实际写入口。
  7. NotifyCenter 场景 200/300/301/435/440/455 的线上模板版本、频控和错误码全集。
  8. IM 中心 msgSyncResult 的准确语义、真实消息 envelope 和重复投递合同。
  9. 对账单微信目标结果表、文件保留期、外部发送幂等和重发去重策略。
  10. 用户通知是否有统一监控、值班告警、人工重放权限和审计要求。

55. 修改后的最小发布验证

任何通知相关公共代码改动,至少按下面顺序验证:

  1. 用测试服务站创建普通站内信,确认内容、关系和 Redis 未读数。
  2. 查看详情并确认已读回算,随后验证删除和撤销权限。
  3. 创建一分钟后到期的定时消息,确认只生成一次并记录 mcronID。
  4. 触发一条重要消息,确认站内信与悬浮提醒是两个可分别观测的结果。
  5. 触发一种 dgj_notify 业务事件并在真实消费者侧确认 payload。
  6. 分别测试 JPush、短信或 IM 中本次改动涉及的通道。
  7. 涉及对账发送时验证发送记录状态、失败原因和新记录重发。
  8. 最后做主业务回归,确认通知失败不会错误回滚或重复执行订单、库存、资金事实。

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

多入口请求链路

场景调用方与入口请求载荷/上下文Controller/ConsumerService/Provider汇合点最终业务事实
站内信业务 Service/后台topic、用户/站点、标题、正文、业务链接Message.php/调用 ServiceMessage Model/Servicemessage ID + receiver消息主文、接收/已读状态
App 推送业务 Service/MQ用户/设备、模板、业务参数Task/ServiceMqSer/Jpush/Notify Providerpush/message ID外部推送受理和本地日志
定时提醒MESSAGE_CRON/联系人/出库提醒 task计划时间、对象、业务键Cron TaskReminder Servicecron/reminder ID到期生成一次通知
对账/业务通知Statement/Order/Activity Servicebill/order/activity、接收目标领域 ServiceMqSer/SendServicebusiness 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=0message+receiver+business ID
计划提醒Reminder Service独立事务提醒规则、是否已计划MESSAGE_CRON/BS_CONTACT_REMIND*cron status pending、scheduledAtreminder ID、稳定业务键
外部推送MqSer/ProviderDB 外部边界消息内容、设备 token(脱敏)MQ、APP_PUSH_LOG、外部服务pending -> accepted/success/failed;attempt +1push ID、Provider 回执
已读/重试Message.php/Cron Task单消息事务当前 read/send status、幂等键接收/发送记录、首页缓存read 0 -> 1;失败重试不重建主业务user+message、attempt、主业务无新增

子模块追踪:message-inbox 站内信创建、收件与已读

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
创建收件主业务完成/系统事件创建站内信topic、business ID、receiver IDs、message keyapplication/service/sysitem/MessageService.php -> application/models/message/MessageModel.php主业务终态、模板、接收范围和重复键独立消息本地事务 insert MESSAGE/TEXT/receiver,read=0request/task + message/business + receiver count消息失败不回滚主业务;稳定键防重复收件
列表已读用户查询/标记已读user、message ID/list filtersapplication/controllers/Message.php用户接收关系、删除/撤销态和当前 read查询只读;已读本地事务 read 0 -> 1、重复 0 变化request ID + user/message + affected rows不能读/改他人消息;首页缓存 commit 后刷新

子模块追踪:message-manual 人工发送、删除与撤销

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
人工发送运营选择接收人提交消息operator、manual batch/message、receiversapplication/controllers/inner/Message.php -> application/service/sysitem/MessageService.php发送权限、内容、接收范围和批次幂等每批消息本地事务 insert 主文/收件关系,status activerequest ID + operator + batch/message + receiver count越权/空接收零写入;正文日志摘要化
删除撤销用户删除收件/运营撤回message ID、user/operator、actionapplication/service/sysitem/MessageService.php所有权、撤销权限、已读和发送状态用户关系软删或主消息 active -> revokedrequest ID + message/user + action撤销不抹审计;外部推送已送达不能“收回”,仅补撤销提示

子模块追踪:message-cron 定时站内信

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
计划任务创建未来发送的站内信cron/reminder ID、scheduledAt、rule/business keyapplication/controllers/tasks/Message.php提醒规则、时区、接收人和已有计划计划本地事务 insert,status none -> pendingrequest ID + cron/reminder + scheduledAt同业务键同时间不重复计划;时区需环境确认
到期发送任务扫描 pending 到期项task batch、cron IDsapplication/controllers/tasks/Message.phppending、scheduledAt、主业务是否仍满足和已发记录每项本地事务 pending -> sent/canceled/failed 并创建收件task + batch + cron/message + result业务已失效则取消;失败项重试不重复收件

子模块追踪:message-remind 弹窗与悬浮提醒

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
提醒查询首页加载弹窗/悬浮提醒user/sid、client、timeapplication/controllers/Message.php未读收件、提醒类型、展示窗口、已关闭/频次查询只读;返回符合规则提醒request ID + user/sid + reminder/message IDs缓存旧与业务未读分开;无权消息不展示
关闭确认用户关闭/稍后提醒user、reminder/message ID、actionapplication/service/sysitem/MessageService.php当前展示/已读状态和频控本地事务 displayed/open -> dismissed/snoozed/readrequest ID + user/message + old/new多端重复操作幂等;不删除消息正文

子模块追踪:message-app-mq APP 业务通知 MQ

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
生产消息订单/库存/支付等 commit 后business ID、event、receiver/sidapplication/Services/Mq/MqSer.php已提交业务快照、模板/事件和稳定幂等键本地事务外发 APP notification MQ,主业务 DB 不变business ID + destination/routing + message ID发布失败只补消息,不重做订单/库存/资金
消费回查APP 未收到message ID、business key、receiverapplication/KzData/Enums/MqEventEnums.php生产结果、队列/消费 ACK 和 App 发送记录查询只读;确认未处理后按业务键补发business/message + ACK/send status随机 messageId 不作幂等;环境队列事实需验证

子模块追踪:message-jpush JPush 客情提醒

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
推送受理客情/业务提醒发 JPushpush ID、template、device/alias hashapplication/Providers/DataService/JpushProvider.php通知内容、接收目标、去重和设备状态外部调用事务外;本地发送记录 pending -> accepted/failedrequest ID + push/business + provider codeaccepted 不等于展示;设备 token 只记录 hash
失败重试Provider 失败/无回执push ID、attempt、last error codeapplication/service/sysitem/MessageService.php主业务、发送记录、最大次数和下次时间每次本地事务 attempt +1、status retry/failedpush + attempt + provider result重试不重建站内信/主业务;无效设备按规则停止

子模块追踪:message-sms 短信与验证码

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
发送短信验证码/业务通知sms/request ID、template、mobile hash、sceneapplication/Providers/NotifyCenter/Sms/SmsProvider.php模板、频控、接收方、验证码 hash/有效期和去重外部调用事务外;本地发送态 pending -> accepted/failedrequest ID + sms/template + mobile hash + code不记录验证码/手机号明文;频控失败零外部调用
验证/回执用户提交验证码或通道结果verify/sms ID、input hash、statusapplication/service/sysitem/MessageService.phphash、过期、尝试次数和使用标记验证本地事务成功时 unused -> used,尝试数 +1verify/sms + result/attempt同码仅一次;通道受理不等于送达,失败按 scene 重试

子模块追踪:message-im IM 入站消息

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
IM 消费IM Center 投递消息/事件message ID、conversation/user、eventapplication/controllers/tasks/ImCenterNotify.php消息幂等、用户绑定、会话和事件版本单消息本地事务 insert record/update conversation old -> newmessage + conversation/user + record重复/旧事件零非法变化;ACK 在 commit 后
通知分流IM 消息转机器人/人工/站内提醒conversation、record、route decisionapplication/Services/Mq/MqSer.phpowner/state、场景和目标commit 后事务外分发,主消息不重插record/conversation + routing/message result分发失败只补目标通知,不重消费 IM 入站

子模块追踪:message-statement 对账单微信发送与重发

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
微信发送对账单文件 ready 后statement/file/send ID、recipient hashapplication/Services/Statement/SendService.php账单快照、文件、接收目标和已有发送记录本地发送记录 none/pending -> accepted/failed;外部发送事务外statement + file/send + provider result文件/账单不重生成;accepted 不等于送达
定向重发失败或用户请求重发send ID、attempt、same statement/fileapplication/KzData/Enums/StatementEnums.php原发送记录、账单/文件仍有效和重试上限本地事务 attempt +1、status retry/success/failedsend + attempt + external message ID稳定 send key 防重复多份;接收人变更另建记录

子模块追踪:message-ops-alert 用户通知与运维告警隔离

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
分类路由业务事件/系统异常生成消息message type、severity、business/incident IDapplication/KzData/Enums/MessageTypeEnums.php用户通知模板、运维告警规则、接收群和敏感字段路由判断只读;分别创建用户消息或告警发送记录event/incident + type/severity + target运维异常不得发给用户;用户通知不泄露堆栈/内部 ID
告警重试运维通道失败alert ID、channel、attemptapplication/controllers/tasks/Contact.php事件仍有效、发送记录和抑制/聚合键独立告警事务 attempt +1,主业务 DB 不变alert/incident + channel/result告警失败不触发业务补偿;按 incident 聚合防风暴