12 KiB
12 KiB
通用确认消息 Flutter 对接
本文定义 Flutter 对接 im_confirm 私聊确认消息所需的 IM 解析、HTTP 接口、状态刷新和按钮置灰规则。业务类型首版是 host / agency / bd 身份邀请,Flutter 只做通用展示,不写业务分支。
通用约定
Base URL
https://{api_host}
本地开发:
http://127.0.0.1:13000
Headers
| Header | 必填 | 说明 |
|---|---|---|
Authorization: Bearer {access_token} |
是 | 登录后的 App token |
Content-Type: application/json |
POST 必填 | 请求体 JSON |
X-App-Code |
否 | 多 App 包场景可传,通常由 gateway 解析 |
响应 Envelope
成功:
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {}
}
失败:
{
"code": "CONFLICT",
"message": "conflict",
"request_id": "req_xxx"
}
IM 消息格式
腾讯 IM C2C 自定义消息:
| TIM 字段 | 值 |
|---|---|
Desc |
im_confirm |
Ext |
im_confirm |
Data |
JSON 字符串 |
Data 示例:
{
"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
}
字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
type |
string | 固定 im_confirm |
version |
int | 当前为 1 |
confirm_id |
string | 确认消息 ID,后续接口只传这个 |
business_type |
string | 业务类型,只用于日志和埋点 |
business_subtype |
string | 业务子类型,只用于展示或埋点 |
msg |
string | 气泡文案 |
status |
string | 初始状态,一般为 pending |
actions |
array | 按钮配置 |
created_at_ms |
int64 | 创建时间 |
expire_at_ms |
int64 | 过期时间,可能为 0 |
Flutter 不要把 business_type、business_subtype 当成接口参数。确认接口只需要 confirm_id。
状态含义
| 状态 | Flutter 展示 |
|---|---|
pending |
展示可点击按钮 |
processing |
按钮置灰,显示处理中或本地 loading |
accepted |
按钮置灰,接受态 |
rejected |
按钮置灰,拒绝态 |
cancelled |
按钮置灰,已取消 |
expired |
按钮置灰,已过期 |
failed |
按钮置灰,可提示稍后重试或联系客服 |
按钮是否可点只看最新状态是否为 pending。
接口 1:接受
地址:
POST /api/v1/message/action-confirms/{confirm_id}/accept
参数:
{
"command_id": "cfm_123_accept_001"
}
返回值 data:
{
"confirm_id": "cfm_123",
"business_type": "role_invitation",
"business_subtype": "host",
"business_id": "987",
"status": "accepted",
"processed_action": "accept",
"processed_at_ms": 1710000001000
}
相关 IM:用户点击 actions.action=accept 的按钮时调用。
接口 2:拒绝
地址:
POST /api/v1/message/action-confirms/{confirm_id}/reject
参数:
{
"command_id": "cfm_123_reject_001",
"reason": ""
}
返回值 data:
{
"confirm_id": "cfm_123",
"business_type": "role_invitation",
"business_subtype": "host",
"business_id": "987",
"status": "rejected",
"processed_action": "reject",
"processed_at_ms": 1710000001000
}
相关 IM:用户点击 actions.action=reject 的按钮时调用。
接口 3:批量刷新状态
地址:
POST /api/v1/message/action-confirms/status
参数:
{
"confirm_ids": ["cfm_123", "cfm_456"]
}
返回值 data:
{
"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_confirm.confirm_id后批量刷新。 - App 从后台回到前台时,刷新当前可见页面的确认消息状态。
- 接口返回的状态覆盖 IM 初始
status。
接口 4:按会话刷新状态
地址:
GET /api/v1/message/action-confirms?peer_user_id=10001&page_size=50
参数:
| 参数 | 必填 | 说明 |
|---|---|---|
peer_user_id |
是 | 当前会话对方用户 ID |
page_size |
否 | 默认 50,最大 100 |
page_token |
否 | 下一页游标 |
返回值 data:
{
"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 SDK 本地消息无法快速遍历
confirm_id时使用。 - 当前用户由 token 决定,Flutter 只传对方
peer_user_id。
Flutter 数据模型
class ImConfirmMessage {
final String confirmId;
final String businessType;
final String businessSubtype;
final String msg;
final String status;
final List<ConfirmAction> actions;
final int createdAtMs;
final int expireAtMs;
ImConfirmMessage({
required this.confirmId,
required this.businessType,
required this.businessSubtype,
required this.msg,
required this.status,
required this.actions,
required this.createdAtMs,
required this.expireAtMs,
});
bool get canOperate => status == 'pending';
factory ImConfirmMessage.fromJson(Map<String, dynamic> json) {
return ImConfirmMessage(
confirmId: (json['confirm_id'] ?? '').toString(),
businessType: (json['business_type'] ?? '').toString(),
businessSubtype: (json['business_subtype'] ?? '').toString(),
msg: (json['msg'] ?? '').toString(),
status: (json['status'] ?? 'pending').toString(),
actions: ((json['actions'] as List?) ?? [])
.map((item) => ConfirmAction.fromJson(Map<String, dynamic>.from(item as Map)))
.toList(),
createdAtMs: (json['created_at_ms'] as num?)?.toInt() ?? 0,
expireAtMs: (json['expire_at_ms'] as num?)?.toInt() ?? 0,
);
}
}
class ConfirmAction {
final String action;
final String label;
final String style;
ConfirmAction({
required this.action,
required this.label,
required this.style,
});
factory ConfirmAction.fromJson(Map<String, dynamic> json) {
return ConfirmAction(
action: (json['action'] ?? '').toString(),
label: (json['label'] ?? '').toString(),
style: (json['style'] ?? '').toString(),
);
}
}
class ConfirmStatus {
final String confirmId;
final String status;
final String processedAction;
final int processedAtMs;
ConfirmStatus({
required this.confirmId,
required this.status,
required this.processedAction,
required this.processedAtMs,
});
bool get canOperate => status == 'pending';
factory ConfirmStatus.fromJson(Map<String, dynamic> json) {
return ConfirmStatus(
confirmId: (json['confirm_id'] ?? '').toString(),
status: (json['status'] ?? '').toString(),
processedAction: (json['processed_action'] ?? '').toString(),
processedAtMs: (json['processed_at_ms'] as num?)?.toInt() ?? 0,
);
}
}
IM 解析
ImConfirmMessage? parseImConfirmCustomData(String data) {
final dynamic decoded = jsonDecode(data);
if (decoded is! Map) return null;
final map = Map<String, dynamic>.from(decoded);
if (map['type'] != 'im_confirm') return null;
final confirmId = (map['confirm_id'] ?? '').toString();
if (confirmId.isEmpty) return null;
return ImConfirmMessage.fromJson(map);
}
解析规则:
- 只处理
type == im_confirm。 confirm_id为空时按普通不支持消息处理。msg直接展示,不在 Flutter 端拼接业务文案。actions为空时只展示文案,不展示按钮。
API 调用
String newConfirmCommandId(String confirmId, String action) {
return '${confirmId}_${action}_${DateTime.now().millisecondsSinceEpoch}';
}
Future<ConfirmStatus> acceptConfirm(String confirmId) async {
final response = await http.post(
Uri.parse('$baseUrl/api/v1/message/action-confirms/$confirmId/accept'),
headers: authJsonHeaders(),
body: jsonEncode({
'command_id': newConfirmCommandId(confirmId, 'accept'),
}),
);
return parseConfirmStatusEnvelope(response);
}
Future<ConfirmStatus> rejectConfirm(String confirmId, {String reason = ''}) async {
final response = await http.post(
Uri.parse('$baseUrl/api/v1/message/action-confirms/$confirmId/reject'),
headers: authJsonHeaders(),
body: jsonEncode({
'command_id': newConfirmCommandId(confirmId, 'reject'),
'reason': reason,
}),
);
return parseConfirmStatusEnvelope(response);
}
command_id 每次点击生成一个新值。网络重试同一次请求时可以复用同一个 command_id。
状态刷新
Future<Map<String, ConfirmStatus>> batchRefreshConfirmStatus(List<String> confirmIds) async {
final ids = confirmIds.where((id) => id.isNotEmpty).toSet().toList();
if (ids.isEmpty) return {};
final response = await http.post(
Uri.parse('$baseUrl/api/v1/message/action-confirms/status'),
headers: authJsonHeaders(),
body: jsonEncode({'confirm_ids': ids}),
);
final data = parseEnvelopeData(response);
final items = (data['items'] as List? ?? []);
return {
for (final item in items)
ConfirmStatus.fromJson(Map<String, dynamic>.from(item as Map)).confirmId:
ConfirmStatus.fromJson(Map<String, dynamic>.from(item as Map)),
};
}
刷新时机:
- 进入 IM 会话页。
- App resume。
- 收到新的
im_confirm。 - 接受或拒绝接口返回
CONFLICT。
UI 规则
- 气泡文案展示
msg。 status == pending时展示actions里的按钮。status != pending时按钮置灰,不能再次点击。- 本地发起请求后,当前按钮进入 loading,同一条消息两个按钮都置灰。
- 接口成功后,用返回状态覆盖本地状态。
- 接口失败但不是鉴权错误时,调用状态接口刷新一次。
按钮样式建议:
| style | 展示 |
|---|---|
primary |
主按钮,例如绿色 Accept |
secondary |
次按钮,例如描边 Reject |
Flutter 不要根据 business_subtype 写死按钮文案,按钮文案以 actions.label 为准。
错误处理
| code | Flutter 处理 |
|---|---|
UNAUTHORIZED |
重新登录 |
PERMISSION_DENIED |
置灰并提示无权限 |
NOT_FOUND |
置灰,消息已失效 |
CONFLICT |
调状态接口刷新,按最新状态置灰 |
UPSTREAM_ERROR |
解除 loading,允许稍后重试 |
INTERNAL_ERROR |
解除 loading,提示稍后重试 |
重复点击、跨设备已处理、旧消息重新打开,都以状态接口返回为准。
对接检查
- 收到
im_confirm后能渲染文案和两个按钮。 - 点击 Accept 后调用接受接口,成功后按钮置灰。
- 点击 Reject 后调用拒绝接口,成功后按钮置灰。
- 同一条消息重复点击不会重复请求;如果请求已经发出,两个按钮都置灰。
- App 重新进入会话后,通过状态接口刷新旧消息状态。
- 另一个设备处理后,本设备 resume 能刷新为终态。
CONFLICT时不弹业务成功,只刷新状态后展示最终状态。