hyapp-server/docs/通用确认消息方案.md
2026-06-25 19:57:16 +08:00

14 KiB
Raw Permalink Blame History

通用确认消息方案

本文定义 App 私聊中可操作确认消息的后端方案。首个业务场景是 host / agency / bd 身份邀请,后续其他业务只新增 business_type 处理器,不改变 Flutter 的通用消息渲染和确认接口。

目标

  • 腾讯云 IM 私聊里支持通用确认消息App 可以展示文案和 Accept / Reject 等操作按钮。
  • App 点击按钮后只提交 confirm_id 和动作,不提交业务参数。
  • 后端根据 confirm_id 查出业务类型、业务主键、发送人、接收人和当前状态,再调用对应 owner service 执行业务。
  • 处理成功后确认消息进入终态App 再次渲染同一条 IM 时按钮置灰。
  • 当前用户只能处理发给自己的确认消息,不能替别人确认,也不能枚举别人的消息状态。

非目标

  • 不用 confirm 表替代业务事实表。role_invitations.status 仍然是身份邀请的权威状态。
  • 不让客户端传 business_typebusiness_idsender_user_idreceiver_user_id 来决定业务行为。
  • 不把确认消息做成房间状态、连接态或腾讯云 IM 会话状态的 owner。
  • 本方案不包含 bd_leader 邀请,当前只覆盖 host / agency / bd

服务边界

模块 职责
gateway-service 提供 /api/v1 HTTP 入口,透传当前登录用户和 app_code,不直接写确认表
message action module 确认消息 owner首版落在 activity-service/internal/service/message;负责确认表、状态机、幂等和业务分发
user-service host / agency / bd 业务 owner负责 role_invitationsProcessRoleInvitation
notice-service 只负责把确认消息投递到腾讯云 IM C2C不执行确认业务
腾讯云 IM 私聊消息、离线、漫游和客户端长连接

首版物理落点建议:

  • 确认表放在 hyapp_activity,由 activity-service 的 message 模块管理。
  • gateway-service 新增 action confirm HTTP handler内部调用 activity-service gRPC。
  • notice-service 消费 message action outbox发送腾讯 IM C2C 自定义消息。

总流程

sequenceDiagram
  participant A as Sender App
  participant Gateway as gateway-service
  participant User as user-service
  participant UserDB as hyapp_user
  participant Message as activity-service message module
  participant MsgDB as hyapp_activity
  participant Notice as notice-service
  participant TIM as Tencent IM
  participant B as Receiver App

  A->>Gateway: 创建 host/agency/bd 邀请
  Gateway->>User: InviteHost / InviteAgency / InviteBD
  User->>UserDB: tx 写 role_invitations + user_outbox
  User-->>Gateway: invitation
  Gateway-->>A: invitation
  Message->>UserDB: consume RoleInvitationCreated
  Message->>MsgDB: 创建 message_action_confirms + message_action_outbox
  Notice->>MsgDB: consume ConfirmMessageCreated
  Notice->>TIM: C2C TIMCustomElem(type=im_confirm)
  TIM-->>B: IM 确认消息
  B->>Gateway: POST /api/v1/message/action-confirms/{confirm_id}/accept
  Gateway->>Message: AcceptActionConfirm
  Message->>MsgDB: pending -> processing
  Message->>User: ProcessRoleInvitation(action=accept)
  User->>UserDB: tx 推进 role_invitations 终态
  Message->>MsgDB: processing -> accepted
  Gateway-->>B: confirm status accepted

数据表

message_action_confirms

确认消息的当前状态表。

CREATE TABLE message_action_confirms (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
  confirm_id VARCHAR(96) NOT NULL,
  business_type VARCHAR(64) NOT NULL,
  business_subtype VARCHAR(64) NOT NULL DEFAULT '',
  business_id VARCHAR(96) NOT NULL,
  sender_user_id BIGINT NOT NULL,
  receiver_user_id BIGINT NOT NULL,
  status VARCHAR(32) NOT NULL,
  message_text VARCHAR(1024) NOT NULL,
  actions_json JSON NOT NULL,
  payload_json JSON NULL,
  source_name VARCHAR(64) NOT NULL,
  source_event_id VARCHAR(128) NOT NULL,
  processing_action VARCHAR(32) NOT NULL DEFAULT '',
  processing_command_id VARCHAR(96) NOT NULL DEFAULT '',
  locked_by VARCHAR(128) NOT NULL DEFAULT '',
  lock_until_ms BIGINT NOT NULL DEFAULT 0,
  processed_action VARCHAR(32) NOT NULL DEFAULT '',
  processed_command_id VARCHAR(96) NOT NULL DEFAULT '',
  processed_by_user_id BIGINT NULL,
  processed_at_ms BIGINT NULL,
  expire_at_ms BIGINT NULL,
  created_at_ms BIGINT NOT NULL,
  updated_at_ms BIGINT NOT NULL,
  PRIMARY KEY (app_code, confirm_id),
  UNIQUE KEY uk_confirm_source_event (app_code, source_name, source_event_id),
  KEY idx_confirm_receiver_status (app_code, receiver_user_id, status, created_at_ms),
  KEY idx_confirm_peer (app_code, receiver_user_id, sender_user_id, created_at_ms),
  KEY idx_confirm_business (app_code, business_type, business_id)
);

字段规则:

  • confirm_id 由服务端生成,例如 cfm_xxx
  • business_type 首版支持 role_invitation
  • business_subtype 首版支持 hostagencybd
  • business_idrole_invitation 来说就是 role_invitations.invitation_id
  • sender_user_id 是邀请发起人,receiver_user_id 是目标用户。
  • message_text 是 IM 展示快照,业务判断不能依赖它。
  • actions_json 首版为 ["accept","reject"]
  • payload_json 只放展示扩展,例如邀请人昵称、公会名、头像等。
  • source_name + source_event_id 用于从 owner outbox 重试时幂等创建确认消息。

message_action_outbox

确认消息投递事件表,由 notice-service 消费后发腾讯 IM。

CREATE TABLE message_action_outbox (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
  event_id VARCHAR(128) NOT NULL,
  event_type VARCHAR(64) NOT NULL,
  confirm_id VARCHAR(96) NOT NULL,
  receiver_user_id BIGINT NOT NULL,
  status VARCHAR(32) NOT NULL,
  payload_json JSON NOT NULL,
  created_at_ms BIGINT NOT NULL,
  retry_count INT NOT NULL DEFAULT 0,
  last_error VARCHAR(512) NOT NULL DEFAULT '',
  PRIMARY KEY (app_code, event_id),
  KEY idx_message_action_outbox_status (app_code, status, created_at_ms)
);

首版事件:

  • ConfirmMessageCreated:发送一条新的 im_confirm
  • 后续如果需要跨设备实时置灰,可新增 ConfirmMessageProcessed,投递状态变更 IM。首版可以通过状态接口刷新。

状态机

状态 说明
pending 待处理App 展示可点击按钮
processing 后端正在执行App 按钮置灰并显示处理中
accepted 已接受,终态
rejected 已拒绝,终态
cancelled 业务方取消,终态
expired 超时未处理,终态
failed 后端处理失败且需要人工排查,终态

状态推进规则:

  • 只有 receiver_user_id == 当前登录用户 才能接受或拒绝。
  • pending 才能进入 processing
  • processing 通过 locked_by + lock_until_ms 防止重复点击并发执行。
  • owner service 成功后,processing 进入 acceptedrejected
  • 如果服务在 owner service 成功后、confirm 状态更新前崩溃,下一次同 confirm_id 请求必须用 business_type + business_id 查询 owner 当前事实,修复 confirm 状态。

业务类型

role_invitation

首版处理 host / agency / bd

subtype business_id accept 动作 reject 动作
host role_invitations.invitation_id 调 user-service ProcessRoleInvitation(action=accept) 调 user-service ProcessRoleInvitation(action=reject)
agency role_invitations.invitation_id 调 user-service ProcessRoleInvitation(action=accept) 调 user-service ProcessRoleInvitation(action=reject)
bd role_invitations.invitation_id 调 user-service ProcessRoleInvitation(action=accept) 调 user-service ProcessRoleInvitation(action=reject)

role_invitation 的最终状态以 user-service 为准。confirm 模块只保存按钮状态和业务路由。

HTTP 接口

所有接口都走 /api/v1 envelope。

接受确认消息

地址:

POST /api/v1/message/action-confirms/{confirm_id}/accept

参数:

{
  "command_id": "cfm_123_accept_001"
}

返回值:

{
  "confirm_id": "cfm_123",
  "business_type": "role_invitation",
  "business_subtype": "host",
  "business_id": "987",
  "status": "accepted",
  "processed_action": "accept",
  "processed_at_ms": 1710000001000
}

相关 IM收到 type=im_confirmconfirm_id=cfm_123 的消息后,点击 accept 调此接口。

拒绝确认消息

地址:

POST /api/v1/message/action-confirms/{confirm_id}/reject

参数:

{
  "command_id": "cfm_123_reject_001",
  "reason": ""
}

返回值:

{
  "confirm_id": "cfm_123",
  "business_type": "role_invitation",
  "business_subtype": "host",
  "business_id": "987",
  "status": "rejected",
  "processed_action": "reject",
  "processed_at_ms": 1710000001000
}

相关 IM收到 type=im_confirmconfirm_id=cfm_123 的消息后,点击 reject 调此接口。

批量查询确认状态

地址:

POST /api/v1/message/action-confirms/status

参数:

{
  "confirm_ids": ["cfm_123", "cfm_456"]
}

返回值:

{
  "items": [
    {
      "confirm_id": "cfm_123",
      "business_type": "role_invitation",
      "business_subtype": "host",
      "business_id": "987",
      "status": "accepted",
      "processed_action": "accept",
      "processed_at_ms": 1710000001000
    }
  ]
}

相关 IMApp 打开会话或回到前台时,收集当前页面内的 confirm_id 后调用该接口刷新按钮状态。

按会话查询确认状态

地址:

GET /api/v1/message/action-confirms?peer_user_id=10001&page_size=50

参数:

参数 必填 说明
peer_user_id 对方用户 ID当前用户从 token 取,不能由客户端传
page_size 默认 50最大 100
page_token 分页游标

返回值:

{
  "items": [
    {
      "confirm_id": "cfm_123",
      "sender_user_id": "10001",
      "receiver_user_id": "10002",
      "business_type": "role_invitation",
      "business_subtype": "host",
      "business_id": "987",
      "status": "pending",
      "processed_action": "",
      "processed_at_ms": 0,
      "created_at_ms": 1710000000000
    }
  ],
  "next_page_token": ""
}

相关 IMApp 如果无法从本地消息列表快速收集 confirm_id,可以按当前会话刷新最近确认消息状态。

IM 负载

腾讯 IM 使用 C2C TIMCustomElem

{
  "type": "im_confirm",
  "version": 1,
  "confirm_id": "cfm_123",
  "business_type": "role_invitation",
  "business_subtype": "host",
  "msg": "Alice has invited you to join ABC guild",
  "status": "pending",
  "actions": [
    {
      "action": "reject",
      "label": "Reject",
      "style": "secondary"
    },
    {
      "action": "accept",
      "label": "Accept",
      "style": "primary"
    }
  ],
  "created_at_ms": 1710000000000,
  "expire_at_ms": 1710604800000
}

腾讯 IM 字段建议:

TIM 字段
Desc im_confirm
Ext im_confirm
Data 上面的 JSON 字符串

App 只依赖 typeconfirm_idmsgstatusactionsbusiness_type 只用于日志和埋点,不用于 App 决定业务接口。

创建消息规则

host / agency / bd 邀请创建成功后user-service 写 RoleInvitationCreated outbox。payload 必须包含:

{
  "event_type": "RoleInvitationCreated",
  "invitation_id": "987",
  "invitation_type": "host",
  "status": "pending",
  "inviter_user_id": "10001",
  "target_user_id": "10002",
  "agency_name": "ABC guild",
  "created_at_ms": 1710000000000
}

message action worker 根据该 payload 创建:

  • business_type = role_invitation
  • business_subtype = invitation_type
  • business_id = invitation_id
  • sender_user_id = inviter_user_id
  • receiver_user_id = target_user_id
  • actions_json = ["accept","reject"]

错误码

HTTP code 场景
400 INVALID_ARGUMENT confirm_idcommand_id 或 action 参数非法
401 UNAUTHORIZED 未登录或 token 无效
403 PERMISSION_DENIED 当前用户不是接收人
404 NOT_FOUND confirm_id 不存在
409 CONFLICT 已处理、已过期、处理中或业务状态不允许操作
502 UPSTREAM_ERROR owner service 不可用

开发顺序

  1. 在 activity-service message 模块新增 message_action_confirmsmessage_action_outbox
  2. 新增 ActionConfirmService gRPCAcceptActionConfirmRejectActionConfirmBatchGetActionConfirmStatusListConversationActionConfirms
  3. gateway 新增 HTTP 接口并调用 action confirm gRPC。
  4. user-service 邀请创建事务写 RoleInvitationCreated outbox。
  5. activity-service 增加 user outbox consumer创建 confirm row 和 message action outbox。
  6. notice-service 增加 message action outbox consumer发送 im_confirm
  7. role_invitation business handler接受/拒绝时调用 user-service ProcessRoleInvitation
  8. 补状态修复逻辑confirm 仍 pending/processing 但 user-service 邀请已经终态时,以 user-service 状态回写 confirm。

验证

  • 创建 Host 邀请后,能查到 role_invitations.pendingmessage_action_confirms.pending 和一条待投递 outbox。
  • notice-service 投递后App 收到 type=im_confirm
  • 接收人接受后,role_invitations.status=acceptedconfirm 状态为 accepted
  • 接收人拒绝后,role_invitations.status=rejectedconfirm 状态为 rejected
  • 非接收人调用接受/拒绝接口返回 PERMISSION_DENIED
  • 重复点击同一个动作返回同一个终态,不重复创建业务事实。
  • 已接受后再拒绝返回 CONFLICT,状态接口返回 accepted