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

423 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 通用确认消息方案
本文定义 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 自定义消息。
## 总流程
```mermaid
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
确认消息的当前状态表。
```sql
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。
```sql
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。
### 接受确认消息
地址:
```text
POST /api/v1/message/action-confirms/{confirm_id}/accept
```
参数:
```json
{
"command_id": "cfm_123_accept_001"
}
```
返回值:
```json
{
"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` 调此接口。
### 拒绝确认消息
地址:
```text
POST /api/v1/message/action-confirms/{confirm_id}/reject
```
参数:
```json
{
"command_id": "cfm_123_reject_001",
"reason": ""
}
```
返回值:
```json
{
"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` 调此接口。
### 批量查询确认状态
地址:
```text
POST /api/v1/message/action-confirms/status
```
参数:
```json
{
"confirm_ids": ["cfm_123", "cfm_456"]
}
```
返回值:
```json
{
"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` 后调用该接口刷新按钮状态。
### 按会话查询确认状态
地址:
```text
GET /api/v1/message/action-confirms?peer_user_id=10001&page_size=50
```
参数:
| 参数 | 必填 | 说明 |
| --- | --- | --- |
| `peer_user_id` | 是 | 对方用户 ID当前用户从 token 取,不能由客户端传 |
| `page_size` | 否 | 默认 50最大 100 |
| `page_token` | 否 | 分页游标 |
返回值:
```json
{
"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`
```json
{
"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 必须包含:
```json
{
"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_id``command_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_confirms``message_action_outbox`
2. 新增 `ActionConfirmService` gRPC`AcceptActionConfirm``RejectActionConfirm``BatchGetActionConfirmStatus``ListConversationActionConfirms`
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.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`