hyapp-server/docs/App消息Tab架构.md
2026-07-06 12:43:40 +08:00

28 KiB
Raw Blame History

App Message Tab Architecture

本文定义 App 底部 消息 tab 的产品边界、服务架构、数据模型、接口契约和开发顺序。消息 tab 固定分为 用户系统活动 三个分区。当前仓库不自研 IM 长连接,用户私聊仍由腾讯云 IM SDK 承担;后端只持久化系统和活动 inbox并提供列表、未读、已读、删除和运营触达能力。

Goals

  • App 底部消息 tab 展示三个分区:用户系统活动
  • 用户 分区来自腾讯云 IM C2C conversation list不把私聊内容重复落进业务库。
  • 系统活动 分区由后端 inbox 持久化,支持分页、未读、已读、删除、过期、撤回和跳转。
  • 消息事实必须按 app_codeuser_id 隔离,不允许跨 App、跨用户读取或写入。
  • 消息事实必须在 MySQL可用 Redis 做未读数短 TTL 缓存,但 Redis 不能作为恢复来源。
  • 后端外部入口统一在 gateway-service,响应使用 /api/v1 envelope。
  • 生产者只发领域事件或内部消息命令message inbox owner 负责模板快照、幂等、入箱和未读数。

Non-Goals

  • 不实现自研 IM 长连接、单聊投递、离线漫游或私聊已读同步。腾讯云 IM 继续负责实时用户消息。
  • 不把房间群消息、公屏消息或房间系统消息复制到个人消息 tab。
  • 不把后台管理端 admin_notifications 复用给 App 用户。后台通知属于 hyapp_adminApp 消息属于 App 业务库。
  • 不允许客户端自报消息归属、未读数、系统消息内容、活动消息内容或目标用户。
  • 不在 room-servicewallet-serviceuser-service 内直接写用户 inbox 表;它们只通过 outbox/事件或内部 RPC 触发入箱。

Current Implementation Snapshot

Capability Status Current Evidence Gap
腾讯云 IM 登录 DONE gateway-service 已有 /api/v1/im/usersig App 侧会话列表仍需客户端 SDK 接入
用户资料批量查询 DONE user-service.BatchGetUsers + gateway /api/v1/users/profiles:batch 仅返回 IM 会话展示字段
activity-service 底座 DONE ActivityService + MessageInboxService 共用 gRPC 入口 message inbox 事实 owner 已放在 activity-service
gateway 消息接口 DONE /api/v1/messages/tabs、列表、已读、read-all、删除 gateway 只透传 JWT user_id 和 app_code
inbox MySQL 表 DONE message_templatesuser_inbox_messagesmessage_fanout_jobs MySQL 是 system/activity inbox 唯一事实来源
系统消息生产者 TODO wallet、room、user host domain 已有各自业务/outbox 方向 尚无统一 CreateInboxMessage 或 MQ consumer
活动消息生产者 TODO activity-service 目前只有活动状态查询 需要活动上线、奖励、任务、运营触达入箱
未读数缓存 TODO 无 Redis key 设计落地 需要缓存失效和 fallback 统计
广播/fanout DONE message_fanout_jobs + CreateFanoutJob + ActivityCronService.ProcessMessageFanoutBatchregion/country/all_active_users/all_registered_users 通过 user-service ListUserIDs 游标取目标 还需要后台运营生产者接入

这张表只描述当前仓库事实和缺口。后续实现必须先补 Redis 未读缓存和第一个领域生产者,不能让生产服务直接写 inbox 表。

Message Sections

Section Meaning Source Of Truth Backend Responsibility
user 用户之间的单聊消息和会话 Tencent IM SDK 签发 UserSig、按需批量补用户资料
system 账号、钱包、房间、host/agency/bd、审核、提现等系统通知 message inbox MySQL 持久化、分页、未读、已读、删除、撤回
activity 活动上线、奖励、任务、运营触达 message inbox MySQL, activity producer 持久化、分页、未读、已读、删除、跳转参数

user 分区在客户端通过腾讯云 IM SDK 拉取 conversation list。后端不能把用户私聊复制一份做列表否则会引入隐私、合规、已读同步和消息一致性问题。

systemactivity 分区使用同一套 inbox 表,通过 message_type 区分。活动服务或后台运营系统只生产消息事件,最终个人收件箱由 message inbox owner 落库。

Service Boundary

graph LR
  Client["Client"] --> Gateway["gateway-service"]
  Client --> TIM["Tencent IM SDK"]

  Gateway --> User["user-service"]
  Gateway --> Message["message inbox owner\nactivity-service inbox module first"]
  Gateway --> Activity["activity-service"]

  TIM --> TIMCloud["Tencent IM"]

  User --> UserDB[("hyapp_user")]
  Message --> MessageDB[("hyapp_activity message tables")]
  Activity --> ActivityDB[("hyapp_activity")]

  RoomOutbox[("room_outbox")] --> MQ["MQ / event bus"]
  WalletOutbox[("wallet_outbox")] --> MQ
  UserOutbox[("user/host outbox")] --> MQ
  ActivityOutbox[("activity_outbox")] --> MQ
  MQ --> Message

  Admin["hyapp-admin-server"] --> Message

首版把 message inbox owner 放在 activity-service 内的 inbox 模块,原因是当前已经有 activity-servicehyapp_activity 库,且活动通知天然需要 message fanout。后续如果系统/活动消息量、部署策略或权限模型明显独立,再拆为独立 message-service

无论放在哪个进程,都必须遵守以下边界:

  • gateway-service 只做 HTTP 鉴权、协议转换、app_code 解析、内部 gRPC 编排,不保存 inbox 状态。
  • activity-service inbox modulesystem/activity 消息事实 owner负责 MySQL 表、未读数、fanout job 和幂等。
  • user-service 只提供用户资料、区域、账号状态和批量查询,不承载消息内容。
  • room-service 只拥有房间状态和房间腾讯 IM 群消息,不直接写个人消息 tab。
  • wallet-service 只拥有账务事实,通过 wallet outbox 或内部事件触发系统通知。
  • hyapp-admin-server 只做后台入口、权限和审计;创建 App 消息时必须调用 message inbox owner不能直接写业务表。

Product Behavior

Tab Summary

  • App 打开消息 tab 时展示三块:用户系统活动
  • 用户 分区的标题、最近会话、未读数由腾讯云 IM SDK 返回。
  • 后端 /api/v1/messages/tabs 返回 system/activity 未读数,并明确 user.source=tencent_im_sdk
  • App 总红点 = Tencent IM unread + backend system unread + backend activity unread。

Message List

  • system/activity 列表按 sent_at_ms DESC, inbox_message_id DESC 排序。
  • 只展示当前 app_code、当前 user_idstatus=visible、未删除、未过期的消息。
  • 列表项必须包含 title、summary、icon/image、action、read 状态、sent_at_ms。
  • body 用于详情或富文本,不要求首版做详情页;首版可以在列表响应里返回简短 body。
  • 客户端展示跳转动作时只能使用服务端白名单 action不能执行任意 URL 或脚本。

Read And Delete

  • 标记已读是用户侧状态变更,重复请求返回成功,不覆盖首次 read_at_ms
  • 删除是用户侧隐藏,设置 deleted_at_ms,不物理删除事实行。
  • 删除未读消息后不再计入未读。
  • 过期消息不展示、不计入未读,但保留审计行。
  • 撤回消息不展示、不计入未读;撤回原因仅用于审计和后台排查。

Data Model

Message Templates

模板只保存服务端可控内容。App 展示标题、摘要、图标和跳转动作时,以消息实例 snapshot 为准,避免模板更新影响历史消息。

message_templates(
  app_code VARCHAR(32) NOT NULL,
  template_id VARCHAR(96) NOT NULL,
  template_version VARCHAR(64) NOT NULL,
  message_type VARCHAR(32) NOT NULL,
  locale VARCHAR(32) NOT NULL DEFAULT '',
  title VARCHAR(160) NOT NULL,
  summary VARCHAR(512) NOT NULL,
  body VARCHAR(2048) NOT NULL DEFAULT '',
  icon_url VARCHAR(512) NOT NULL DEFAULT '',
  image_url VARCHAR(512) NOT NULL DEFAULT '',
  action_type VARCHAR(64) NOT NULL DEFAULT '',
  action_param VARCHAR(1024) NOT NULL DEFAULT '',
  status VARCHAR(32) NOT NULL,
  created_by VARCHAR(64) NOT NULL DEFAULT '',
  created_at_ms BIGINT NOT NULL,
  updated_at_ms BIGINT NOT NULL,
  PRIMARY KEY (app_code, template_id, template_version),
  KEY idx_message_templates_type_status (app_code, message_type, status),
  KEY idx_message_templates_template_status (app_code, template_id, status)
);

字段规则:

  • message_type 只允许 systemactivity
  • template_version 必须进入消息实例 snapshot避免模板修改影响历史消息。
  • locale 首版可以为空;后续多语言按 app_code + template_id + template_version + locale 选择。
  • action_type 必须是服务端白名单枚举,例如 wallet_withdraw_detailhost_application_detailactivity_detailroom_detail
  • 模板可以被停用,但停用不影响已入箱消息展示。

User Inbox

user_inbox_messages(
  app_code VARCHAR(32) NOT NULL,
  inbox_message_id VARCHAR(96) NOT NULL,
  user_id BIGINT NOT NULL,
  message_type VARCHAR(32) NOT NULL,
  producer VARCHAR(64) NOT NULL,
  producer_event_id VARCHAR(128) NOT NULL,
  producer_event_type VARCHAR(96) NOT NULL DEFAULT '',
  aggregate_type VARCHAR(64) NOT NULL DEFAULT '',
  aggregate_id VARCHAR(128) NOT NULL DEFAULT '',
  template_id VARCHAR(96) NOT NULL DEFAULT '',
  template_version VARCHAR(64) NOT NULL DEFAULT '',
  title VARCHAR(160) NOT NULL,
  summary VARCHAR(512) NOT NULL,
  body VARCHAR(2048) NOT NULL DEFAULT '',
  icon_url VARCHAR(512) NOT NULL DEFAULT '',
  image_url VARCHAR(512) NOT NULL DEFAULT '',
  action_type VARCHAR(64) NOT NULL DEFAULT '',
  action_param VARCHAR(1024) NOT NULL DEFAULT '',
  priority INT NOT NULL DEFAULT 0,
  status VARCHAR(32) NOT NULL,
  read_at_ms BIGINT NULL,
  deleted_at_ms BIGINT NULL,
  sent_at_ms BIGINT NOT NULL,
  expire_at_ms BIGINT NULL,
  metadata_json JSON NULL,
  created_at_ms BIGINT NOT NULL,
  updated_at_ms BIGINT NOT NULL,
  PRIMARY KEY (app_code, inbox_message_id),
  UNIQUE KEY uk_user_inbox_producer_event (app_code, user_id, producer, producer_event_id),
  KEY idx_user_inbox_list (app_code, user_id, message_type, deleted_at_ms, status, sent_at_ms, inbox_message_id),
  KEY idx_user_inbox_unread (app_code, user_id, message_type, read_at_ms, deleted_at_ms, status, expire_at_ms),
  KEY idx_user_inbox_aggregate (app_code, user_id, aggregate_type, aggregate_id)
);

字段规则:

  • producer_event_id 是入箱幂等键。钱包提现审核、host 申请、活动奖励等每个业务事件必须生成稳定 ID。
  • producer_event_type 保留源事件类型,方便排查和消费策略分流。
  • aggregate_type/aggregate_id 用于同一业务对象查询,例如 withdraw_request/wd_01host_application/ha_01
  • status=visible 才能被 App 列表看到。recalledhiddenexpired 不展示。
  • deleted_at_ms 是用户侧删除标记,不删除事实行。
  • expire_at_ms 只影响展示和未读,不影响审计。
  • metadata_json 只放低敏业务快照不放支付凭证、手机号、证件号、provider receipt 或密钥。

Fanout Jobs

大范围活动消息不要在 HTTP 请求里同步 fanout。后台只创建任务cron-service 触发 activity-service 分批写入,并记录进度和失败原因。

message_fanout_jobs(
  app_code VARCHAR(32) NOT NULL,
  job_id VARCHAR(96) NOT NULL,
  command_id VARCHAR(128) NOT NULL,
  message_type VARCHAR(32) NOT NULL,
  target_scope VARCHAR(64) NOT NULL,
  target_filter_json JSON NOT NULL,
  template_snapshot_json JSON NOT NULL,
  status VARCHAR(32) NOT NULL,
  cursor_user_id BIGINT NOT NULL DEFAULT 0,
  total_count BIGINT NOT NULL DEFAULT 0,
  success_count BIGINT NOT NULL DEFAULT 0,
  failure_count BIGINT NOT NULL DEFAULT 0,
  batch_size INT NOT NULL DEFAULT 500,
  next_run_at_ms BIGINT NOT NULL DEFAULT 0,
  locked_by VARCHAR(96) NOT NULL DEFAULT '',
  locked_until_ms BIGINT NOT NULL DEFAULT 0,
  error_message VARCHAR(512) NOT NULL DEFAULT '',
  created_by VARCHAR(64) NOT NULL,
  created_at_ms BIGINT NOT NULL,
  updated_at_ms BIGINT NOT NULL,
  PRIMARY KEY (app_code, job_id),
  UNIQUE KEY uk_message_fanout_command (app_code, command_id),
  KEY idx_message_fanout_status (app_code, status, next_run_at_ms, updated_at_ms)
);

target_filter_json 必须由后台服务端生成,不能直接信任客户端或运营前端提交的任意 SQL 条件。首版支持以下 scope

Scope Meaning Target Source
single_user 单个用户消息 request target_user_id
user_ids 指定用户列表 admin 上传或活动产出名单
region 区域活动、区域公告 user-service users.region_id
country 国家定向通知 user-service users.country
all_active_users 全 App 活跃用户 user-service active users cursor
all_registered_users 全 App 注册用户 user-service registered users cursor

Global Broadcast State

如果全量公告需要“未读、已读、删除”语义,不能只保存一条公告定义后让客户端自行判断。可以二选一:

  • P0 方案:全量公告也通过 message_fanout_jobs 分批物化到 user_inbox_messages,实现简单,查询统一。
  • P1 方案:新增 message_broadcastsuser_broadcast_states,列表时合并个人 inbox 和广播定义,适合超大规模全量公告。

首版建议采用 P0 物化方案,避免 read-all、delete 和未读数出现两套语义。只有当全量 fanout 成本明显不可接受时,再实现 P1 懒加载模型。

Unread Counter Cache

未读数可以缓存,但缓存不是事实。

message:unread:{app_code}:{user_id}:{message_type}

缓存规则:

  • cache miss 时从 user_inbox_messages 统计。
  • 入箱、已读、read-all、删除、撤回后删除或更新缓存。
  • 缓存 TTL 建议 30-120 秒,防止缓存失效遗漏导致长期错误。
  • 未读数不得由客户端上报覆盖。

Internal RPC Surface

首版建议在 api/proto/activity/v1 追加 message inbox RPC或者单独新增 api/proto/message/v1。如果实现仍放在 activity-service,可以先追加到 activity proto但命名必须保持 message inbox 边界。

service MessageInboxService {
  rpc ListMessageTabs(ListMessageTabsRequest) returns (ListMessageTabsResponse);
  rpc ListInboxMessages(ListInboxMessagesRequest) returns (ListInboxMessagesResponse);
  rpc MarkInboxMessageRead(MarkInboxMessageReadRequest) returns (MarkInboxMessageReadResponse);
  rpc MarkInboxSectionRead(MarkInboxSectionReadRequest) returns (MarkInboxSectionReadResponse);
  rpc DeleteInboxMessage(DeleteInboxMessageRequest) returns (DeleteInboxMessageResponse);
  rpc CreateInboxMessage(CreateInboxMessageRequest) returns (CreateInboxMessageResponse);
  rpc CreateFanoutJob(CreateFanoutJobRequest) returns (CreateFanoutJobResponse);
}

RPC 要求:

  • 所有请求必须携带 RequestMeta.app_coderequest_id
  • App 用户读写请求必须携带 user_id,并由 gateway 使用 JWT 中的用户 ID 填充。
  • CreateInboxMessage 必须携带 producerproducer_event_idmessage_typetarget_user_id、内容 snapshot 或 template 引用。
  • CreateFanoutJob 必须携带后台 command_id,同 command 重试返回原 job。
  • 查询接口不需要 command_id,但必须透传 request_id 用于追踪。
  • 修改 proto 后必须运行 make proto,并提交生成后的 .pb.go / _grpc.pb.go

App API Surface

所有接口都由 gateway-service 暴露,使用 /api/v1 envelope{code,message,request_id,data}app_code 来自 gateway 的 App 解析或 JWT不接受客户端 body 自报。

Tab Summary

GET /api/v1/messages/tabs

Response data:

{
  "sections": [
    {
      "section": "user",
      "title": "用户",
      "unread_count": 0,
      "source": "tencent_im_sdk"
    },
    {
      "section": "system",
      "title": "系统",
      "unread_count": 2,
      "source": "backend"
    },
    {
      "section": "activity",
      "title": "活动",
      "unread_count": 1,
      "source": "backend"
    }
  ]
}

user.unread_count 推荐由客户端腾讯云 IM SDK 直接提供。后端返回 source=tencent_im_sdk,提醒客户端不要等待后端私聊列表。

System And Activity List

GET /api/v1/messages?section=system&page_size=20&page_token=
GET /api/v1/messages?section=activity&page_size=20&page_token=

Response data:

{
  "section": "system",
  "items": [
    {
      "message_id": "msg_01",
      "title": "提现审核通过",
      "summary": "你的提现申请已通过,请等待人工转账。",
      "body": "",
      "icon_url": "",
      "image_url": "",
      "action_type": "wallet_withdraw_detail",
      "action_param": "withdrawal_id=wd_01",
      "read": false,
      "sent_at_ms": 1777996800000
    }
  ],
  "next_page_token": ""
}

分页使用 (sent_at_ms, inbox_message_id) 游标。不要使用 offset大用户消息量上来后 offset 翻页会越来越慢且容易重复或漏项。

Mark Read

POST /api/v1/messages/{message_id}/read
POST /api/v1/messages/read-all

read-all body:

{
  "section": "system"
}

已读接口必须幂等:重复标记已读返回成功,不改变 read_at_ms 的首次已读时间。read-all 只影响当前 app_code/user_id/section 下未删除、未过期、visible 的消息。

Delete

DELETE /api/v1/messages/{message_id}

删除是用户侧隐藏,设置 deleted_at_ms。已经删除的消息不进入列表,也不计入未读。删除不存在或不属于当前用户的消息应返回 NOT_FOUND,不能泄露其他用户消息是否存在。

User Message Section

用户消息列表由客户端腾讯云 IM SDK 获取:

Tencent IM SDK getConversationList()

后端只补充两个能力:

  • GET /api/v1/im/usersig:已有,给客户端登录腾讯云 IM。
  • GET /api/v1/users/profiles:batch?user_ids=...:后续可新增,用于会话列表头像、昵称、短号批量补全。底层复用 user-service.BatchGetUsers

profiles:batch 只返回展示字段:user_iddisplay_user_idusernameavatargendercountryregion_idapp_code。不能返回手机号、登录凭证、实名信息或后台身份详情。

Producer Rules

系统消息生产者:

Producer Event Section Target
wallet-service 充值到账、提现审核、提现失败、提现打款完成 system affected user
user-service 登录安全、账号状态、资料审核、host/agency/bd 申请结果 system affected user
room-service 被踢、被禁言、被解除 ban 等个人强相关通知 system affected user
activity-service 活动奖励、任务完成、排行榜奖励 activity affected user
hyapp-admin-server 定向运营通知、全量公告、区域活动通知 activity selected users

生产规则:

  • 生产者只发领域事件或调用内部 CreateInboxMessage,不能直接写 user_inbox_messages
  • 每个生产者事件必须提供稳定 producer_event_id,重复消费只能生成一条 inbox。
  • 房间公屏、房间礼物、麦位变更等群内事件仍走腾讯云 IM 群,不进入个人消息 tab。
  • room-service 只有个人强相关的管理结果才进入消息 tab例如被踢出房间、被禁言这类消息不替代房间 IM 系统消息。
  • 钱包消息必须以 wallet 交易或提现单状态为事实来源,不能由 gateway 根据接口返回自行拼通知。
  • 后台运营通知必须经过 admin 权限和审计,最终由 message inbox owner 入箱。

Event To Inbox Flow

sequenceDiagram
  participant P as Producer Service
  participant O as Producer Outbox
  participant M as MQ / Consumer
  participant I as Message Inbox Owner
  participant DB as message MySQL
  participant R as Redis

  P->>O: commit domain fact + outbox
  M->>O: poll or consume event
  M->>I: CreateInboxMessage(producer_event_id)
  I->>DB: insert user_inbox_messages with unique key
  I->>R: invalidate unread cache
  I-->>M: idempotent result

一致性要求:

  • 生产者业务事实提交不能依赖消息入箱成功。
  • 入箱失败由生产者 outbox 或 MQ consumer 重试。
  • message inbox owner 以唯一键保证重复消费不重复发消息。
  • 如果模板不存在或已停用,消费者应进入 retry 或 dead-letter并告警不能生成空标题消息。
  • 如果目标用户不存在、跨 app 或已注销,按业务策略记录 skipped不应无限重试。

Broadcast And Fanout

消息分发分三种:

Mode Use Case Rule
single_user 提现审核、申请结果、个人奖励 直接写一条 inbox
user_group 区域活动、主播群体通知、指定用户列表 创建 fanout job分批写 inbox
global 全 App 公告 首版仍建议 fanout 物化;超大规模后再做 broadcast lazy state

Fanout batch 要求:

  • 每轮锁定一批待执行 job使用 locked_by/locked_until_ms 防止多实例重复处理。
  • cursor_user_id 分页查询目标用户,不使用 offset。
  • 每个目标用户的 producer_event_id 必须稳定,例如 fanout:{job_id}:{user_id}
  • 单批失败不能让整个 job 丢失,记录 failure_count/error_message 并继续或进入 retry。
  • 目标范围按区域/国家选择时,使用 user-service 当前主数据;是否需要冻结目标用户快照由具体活动决定。
  • 大型 fanout 配置必须支持 cron intervalbatch_sizelock_ttl;失败重试次数由 activity-service owner 侧状态机控制。

Read Consistency

  • 用户消息的已读、未读和最后一条消息以腾讯云 IM 为准。
  • 系统和活动消息的已读、未读以 user_inbox_messages 为准。
  • App tab 总红点 = IM SDK unread + backend system unread + backend activity unread。
  • 后端不接收客户端上报的 IM unread 作为事实,只能作为埋点或调试信息。
  • read-all 和删除成功后,必须清理对应 section 的 Redis 未读缓存。
  • 列表接口如果读到 Redis 未读数和 MySQL 列表状态短暂不一致,以 MySQL 为最终事实。

Error Semantics

Error Meaning HTTP Mapping
INVALID_SECTION section 不是 system/activity 400
MESSAGE_NOT_FOUND 消息不存在、已删除或不属于当前用户 404
MESSAGE_RECALLED 消息已撤回,不允许展示或操作 404
PAGE_TOKEN_INVALID 游标无法解析或不属于当前查询 400
REQUEST_CONFLICT 同 command_id/fanout job 不同 payload 409
PRODUCER_EVENT_CONFLICT 同 producer_event_id 不同 payload 409
MESSAGE_INTERNAL DB、缓存或内部未知错误 500

request_id 只做链路追踪,不作为消息入箱幂等键。入箱幂等键必须是 producer_event_id 或后台 command_id

Security And Privacy

  • app_code 由 gateway 解析或 token 携带,不接受客户端 body 覆盖。
  • user_id 由 JWT 提供,读写接口不能让客户端指定目标用户。
  • 后台创建消息必须通过 admin 鉴权和审计,审计留在 hyapp_admin
  • action_param 必须是白名单动作参数,不允许任意外链、脚本、内嵌 HTML。
  • 消息 title/summary/body 不得包含支付凭证、provider receipt、敏感证件、完整手机号或密钥。
  • 用户删除消息只影响自己,不影响其他用户和后台审计。
  • 用户私聊内容不进入后端业务库,不出现在系统/活动消息搜索里。

Configuration

首版实现需要在 activity-service 增加 message inbox 读取配置;message_fanout_jobscron-service 调度,不再由 activity-service 内置 worker 轮询:

message_inbox:
  unread_cache_ttl: 60s

# services/cron-service/configs/config*.yaml
tasks:
  message_fanout:
    enabled: true
    interval: 1s
    timeout: 10s
    lock_ttl: 30s
    batch_size: 500
    app_codes: ["lalu"]

如果消息 owner 后续拆成独立 message-service这些配置迁移到新服务gateway 只需要更新 message gRPC 地址。

Implementation Order

  1. 追加 proto新增 MessageInboxService、消息列表、未读、已读、删除、创建入箱、fanout job RPC。
  2. 更新 activity-service MySQL initdb新增 message_templatesuser_inbox_messagesmessage_fanout_jobs
  3. activity-service 增加 inbox domain/repository/service先实现单用户入箱、列表、未读、已读、删除。
  4. gateway-service 增加 activity/message client 和 App HTTP/api/v1/messages/tabs/api/v1/messages/api/v1/messages/{id}/read/api/v1/messages/read-allDELETE /api/v1/messages/{id}
  5. 接入第一个系统生产者,建议从提现审核或 host 申请结果开始,因为它们天然是单用户消息。
  6. 增加 fanout job 和 ActivityCronService.ProcessMessageFanoutBatch,由 cron-service 调度,先支持 regioncountryuser_idsall_active_usersall_registered_users
  7. 增加 /api/v1/users/profiles:batch,给腾讯云 IM 用户会话列表补头像、昵称、短号。
  8. 增加 Redis 未读数缓存和缓存失效逻辑。
  9. 补 OpenAPI 文档、Docker 配置、线上配置示例和端到端冒烟脚本。

Verification Matrix

Scenario Expected
打开消息 tab 返回 user/system/activity 三个分区
user 分区 response 标记 source=tencent_im_sdk,不返回私聊内容
拉系统消息列表 只返回当前 app_code、当前用户、未删除、未过期、visible 的 system 消息
拉活动消息列表 只返回当前 app_code、当前用户、未删除、未过期、visible 的 activity 消息
重复消费同一 producer event 只生成一条 inbox 消息
同 producer event 不同 payload 返回或记录冲突,不覆盖原消息
标记已读重复请求 返回成功,read_at_ms 不被覆盖,未读数不变成负数
read-all 当前 section 未读全部归零,其他 section 不受影响
删除未读消息 消息不再展示,未读数减少
删除其他用户消息 返回 NOT_FOUND
过期消息 不展示、不计入未读,但数据库事实保留
全量活动通知 fanout job 分批写入,不阻塞 Admin HTTP 请求
区域活动通知 只给当前 app 下目标 region 用户入箱
用户私聊会话 App 从腾讯云 IM SDK 获取,后端不返回私聊内容
Redis unread cache miss 从 MySQL 统计后回填缓存

Test And Release Requirements

涉及 proto 时运行:

make proto
go test ./...

涉及 DB、配置、Docker 时运行:

docker compose config
docker compose build

涉及启动链路时实际拉起:

docker compose up -d
docker compose ps
docker compose down

最小测试覆盖:

  • activity-service inbox repository 单元/集成测试:幂等、分页、未读、已读、删除、过期。
  • gateway transport 测试JWT 用户 ID 生效、app_code 透传、envelope、错误映射。
  • fanout batch 测试:锁 job、分页、重复 target 幂等、失败重试。
  • producer consumer 测试:同 event 重复消费只入箱一次。
  • profiles batch 测试:只返回安全展示字段。

Critical Rules

  • user 分区不落业务库,不复制腾讯云 IM 私聊内容。
  • systemactivity 必须持久化到 MySQL不能只靠 Redis 或推送。
  • 所有 inbox 事实必须带 app_code
  • 已读和删除必须幂等。
  • 大范围活动消息必须异步 fanout。
  • 房间群消息仍走腾讯云 IM 群,不进入个人消息 tab。
  • request_id 只做追踪,不做幂等。
  • 后台运营消息必须有 admin 鉴权和审计。
  • 生产者不能直接写 inbox 表,只能发事件或调用 message inbox owner。