14 KiB
通用确认消息方案
本文定义 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_type、business_id、sender_user_id或receiver_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_invitations 和 ProcessRoleInvitation |
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首版支持host、agency、bd。business_id对role_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进入accepted或rejected。 - 如果服务在 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_confirm 且 confirm_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_confirm 且 confirm_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
}
]
}
相关 IM:App 打开会话或回到前台时,收集当前页面内的 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": ""
}
相关 IM:App 如果无法从本地消息列表快速收集 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 只依赖 type、confirm_id、msg、status 和 actions。business_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_invitationbusiness_subtype = invitation_typebusiness_id = invitation_idsender_user_id = inviter_user_idreceiver_user_id = target_user_idactions_json = ["accept","reject"]
错误码
| HTTP | code | 场景 |
|---|---|---|
| 400 | INVALID_ARGUMENT |
confirm_id、command_id 或 action 参数非法 |
| 401 | UNAUTHORIZED |
未登录或 token 无效 |
| 403 | PERMISSION_DENIED |
当前用户不是接收人 |
| 404 | NOT_FOUND |
confirm_id 不存在 |
| 409 | CONFLICT |
已处理、已过期、处理中或业务状态不允许操作 |
| 502 | UPSTREAM_ERROR |
owner service 不可用 |
开发顺序
- 在 activity-service message 模块新增
message_action_confirms和message_action_outbox。 - 新增
ActionConfirmServicegRPC:AcceptActionConfirm、RejectActionConfirm、BatchGetActionConfirmStatus、ListConversationActionConfirms。 - gateway 新增 HTTP 接口并调用 action confirm gRPC。
- user-service 邀请创建事务写
RoleInvitationCreatedoutbox。 - activity-service 增加 user outbox consumer,创建 confirm row 和 message action outbox。
- notice-service 增加 message action outbox consumer,发送
im_confirm。 - 补
role_invitationbusiness handler,接受/拒绝时调用 user-serviceProcessRoleInvitation。 - 补状态修复逻辑:confirm 仍 pending/processing 但 user-service 邀请已经终态时,以 user-service 状态回写 confirm。
验证
- 创建 Host 邀请后,能查到
role_invitations.pending、message_action_confirms.pending和一条待投递 outbox。 - notice-service 投递后,App 收到
type=im_confirm。 - 接收人接受后,
role_invitations.status=accepted,confirm 状态为accepted。 - 接收人拒绝后,
role_invitations.status=rejected,confirm 状态为rejected。 - 非接收人调用接受/拒绝接口返回
PERMISSION_DENIED。 - 重复点击同一个动作返回同一个终态,不重复创建业务事实。
- 已接受后再拒绝返回
CONFLICT,状态接口返回accepted。