Files
sino-mci/docs/superpowers/specs/2026-07-10-channel-service-mci-server-integration-design.md

428 lines
17 KiB
Markdown
Raw 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.

# channel-service 与 mci-server 双向集成设计
> **需求来源**:继续完成 pywechat 客户端功能,打通 Python 渠道服务与 Java 消息中台的双向通信。
> **日期**2026-07-10
> **状态**:待实现
> **关联文档**`docs/architecture/2026-07-06-message-customer-interaction-design.md`
---
## 1. 目标与范围
### 1.1 目标
`channel-service`Python FastAPI`mci-server`Java Spring Boot完成双向 HTTP 集成:
- **下行mci-server → channel-service**mci-server 继续通过现有 REST API 调用 channel-service 的发送、登录控制等指令。
- **上行channel-service → mci-server**channel-service 把心跳、登录/在线状态变更、发送结果、异常事件持久化到本地 SQLite并由后台 worker 可靠地推送到 mci-server。
- **可靠性**:服务重启不丢事件;网络异常按指数退避重试;超次进死信;本地事件保留 10 天后自动清理。
### 1.2 范围
- **channel-service**新增事件模型、SQLite 持久化、后台事件发布 worker、mci-server HTTP 客户端、配置项扩展、API 行为调整。
- **mci-server**:新增 `/api/v1/channel-account/event` 接收端点,处理心跳/状态/发送结果事件并更新 `mci_channel_account``mci_send_record`
- **不在本期范围**
- 真实 pywechat/pywinauto 接入(仍为 mock
- RabbitMQ/MQ 等外部消息总线。
- 入站消息从 channel-service 回传(本期只传运行态事件和发送结果)。
---
## 2. 整体架构
```
┌─────────────────────────────────────────────────────────────┐
│ mci-server │
│ Java 21 + Spring Boot 4.1.x │
│ POST /api/v1/channel-account/event │
└───────────────────────┬─────────────────────────────────────┘
│ HTTP 上行事件
┌─────────────────────────────────────────────────────────────┐
│ channel-service │
│ Python 3.11 + FastAPI │
│ │
│ ┌─────────────┐ ┌──────────────────┐ ┌────────────┐ │
│ │ HTTP API │───►│ 业务操作 + 事件 │───►│ outbound_ │ │
│ │ │ │ 写入 SQLite │ │ events │ │
│ └─────────────┘ └──────────────────┘ └─────┬──────┘ │
│ │ │
│ ┌────────────────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ EventPublisher │ │
│ │ 单线程 worker │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ mci_server_client│ │
│ │ HTTP POST │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
核心原则:**状态变更/发送操作与事件写入在同一个 SQLite 事务内完成**,保证原子性。
---
## 3. channel-service 设计
### 3.1 新增模块
| 模块 | 路径 | 职责 |
|------|------|------|
| 事件模型 | `app/models/event.py` | `OutboundEvent` 数据类、事件类型枚举、payload schema |
| 事件存储 | `app/repositories/event_store.py` | SQLite 建表、CRUD、事务、清理 |
| 事件发布 | `app/services/event_publisher.py` | 后台单线程 worker、重试、死信 |
| mci-server 客户端 | `app/clients/mci_server_client.py` | HTTP POST 到 mci-server |
| 配置扩展 | `app/config.py` | 新增事件存储、重试、清理相关配置 |
### 3.2 SQLite 事件表 `outbound_events`
```sql
CREATE TABLE IF NOT EXISTS outbound_events (
id TEXT PRIMARY KEY,
event_type TEXT NOT NULL,
payload TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
attempt_count INTEGER NOT NULL DEFAULT 0,
next_retry_at TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_error TEXT
);
CREATE INDEX IF NOT EXISTS idx_status_next_retry
ON outbound_events(status, next_retry_at);
CREATE INDEX IF NOT EXISTS idx_created_at
ON outbound_events(created_at);
```
字段说明:
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | TEXT PK | UUID v4`event_id` |
| `event_type` | TEXT | `heartbeat` / `login_status_changed` / `online_status_changed` / `send_result` / `error` |
| `payload` | TEXT | JSON 字符串,见第 3.3 节 |
| `status` | TEXT | `pending` / `retrying` / `delivered` / `dead_letter` |
| `attempt_count` | INTEGER | 已尝试次数,上限 `max_retry` |
| `next_retry_at` | TIMESTAMP | 下次重试时间;`pending` 事件为创建时间 |
| `created_at` | TIMESTAMP | 创建时间,用于 10 天清理 |
| `updated_at` | TIMESTAMP | 更新时间 |
| `last_error` | TEXT | 最近一次失败摘要 |
### 3.3 事件 payload 统一结构
所有事件统一外层:
```json
{
"event_id": "evt_20260710_xxx",
"event_type": "heartbeat",
"account_id": "wx-bot-01",
"tenant_code": "rescue",
"timestamp": "2026-07-10T10:00:00Z",
"data": { }
}
```
`data` 按事件类型定义:
#### heartbeat
```json
{
"login_status": "success",
"online_status": "online",
"risk_level": 10,
"sent_count_today": 5,
"server_host": "win-svr-01"
}
```
#### login_status_changed
```json
{
"previous": "pending",
"current": "success",
"qr_code_url": "https://mock.pywechat.qrcode/...",
"error_message": null
}
```
#### online_status_changed
```json
{
"previous": "offline",
"current": "online"
}
```
#### send_result
```json
{
"task_id": "task_001",
"record_id": 1001,
"conversation_id": "room-001",
"success": true,
"message_id": "msg-123",
"channel_result": {},
"error": null
}
```
> 说明:`record_id` 由 mci-server 在调用 send API 时传入channel-service 原样回传,用于 mci-server 更新 `mci_send_record`。
#### error
```json
{
"operation": "send_text",
"error_code": "ACCOUNT_OFFLINE",
"error_message": "account wx-bot-01 not online"
}
```
### 3.4 API 行为变更
| API | 变更 |
|------|------|
| `POST /v1/wechat-personal/login/start` | 创建/更新账号、生成二维码,并在同一事务写入 `login_status_changed` 事件 |
| `POST /v1/wechat-personal/login/{id}/confirm` | 确认登录后,在同一事务写入 `login_status_changed` + `online_status_changed` |
| `POST /v1/wechat-personal/login/{id}/expire` | 二维码过期后,写入 `login_status_changed` 事件 |
| `POST /v1/wechat-personal/send/text` | 改为**异步受理**:立即返回 `{"accepted": true, "message_id": "..."}`;真实发送完成后写入 `send_result` 事件 |
| `POST /v1/wechat-personal/send/image` | 同上 |
| `POST /v1/heartbeat` | 更新本地状态,并在同一事务写入 `heartbeat` 事件 |
| `POST /v1/admin/retry-dead-letter`(新增) | 手动把 `dead_letter` 状态重置为 `retrying`,用于人工恢复 |
### 3.5 发送异步化与风控
当前仍为 mock 客户端,但接口行为需与真实 pywechat 一致:
1. `send_text` / `send_image` 收到请求后,先校验账号在线、风控等级。
2. 校验通过后返回 `accepted=true`,并立即写入 `send_result` 事件。
3. mock 场景下该事件直接表示成功;真实 pywechat 场景下,发送完成后用同一 `event_id` 更新 `send_result` 事件内容,或删除旧事件后写入最终结果事件。
**风控处理**
- 风控等级 `risk_level >= 80` 的账号,发送请求直接返回失败,不进入重试。
- 因微信风控导致的发送失败(如 `error_code``RISK_CONTROL_BLOCKED``ACCOUNT_BANNED`),事件直接标记为 `dead_letter`,不再重试。
- 因网络超时、mci-server 不可达等**临时错误**,才进入指数退避重试。
- `sent_count_today` 每日重置channel-service 在心跳中上报mci-server 可据此做策略判断。
### 3.6 EventPublisher 后台 worker
- **单线程执行**:使用一个 `asyncio` 任务循环消费,避免并发导致 SQLite 锁竞争或事件乱序。
- **扫描频率**:每 5 秒一次。
- **每次拉取**:最多 100 条 `status IN ('pending','retrying') AND next_retry_at <= now()`,按 `next_retry_at` 升序。
- **推送目标**`{mci_server_url}/api/v1/channel-account/event`
- **成功**`status='delivered'``attempt_count` 不变。
- **失败**
- `attempt_count += 1`
- `next_retry_at = now + base_delay * 2^(attempt_count - 1)`(首次重试为 `base_delay`,第二次为 `2*base_delay`,依此类推)
- `status='retrying'`
- `last_error` 记录 HTTP 状态码或异常摘要
-`attempt_count >= max_retry`,则 `status='dead_letter'`
- **最大重试次数**:由配置 `max_retry` 控制,默认 `5`,上限 `10`(代码中硬上限,防止配置错误)。
- **服务启动**:从 `main.py` 启动时创建 worker启动后自动恢复 `pending` / `retrying` 事件。
- **服务关闭**:优雅关闭时等待当前批次处理完成(超时 5 秒)。
### 3.7 数据清理
- 每天凌晨 3:00 启动清理任务(或每次 worker 循环顺带清理)。
- 删除 `status IN ('delivered','dead_letter')``created_at < CURRENT_TIMESTAMP - INTERVAL '10 days'` 的事件。
- 保留 `pending` / `retrying` 事件,无论创建时间。
### 3.8 配置项
```env
# mci-server 地址
MCI_SERVER_URL=http://localhost:8080/msg-platform
# SQLite 事件库路径
EVENT_STORE_PATH=./data/events.db
# 心跳间隔(秒)
HEARTBEAT_INTERVAL_SECONDS=30
# 最大重试次数(代码硬上限 10
MAX_RETRY=5
# 基础重试延迟(秒)
BASE_RETRY_DELAY_SECONDS=5
# 事件保留天数
EVENT_RETENTION_DAYS=10
# 后台 worker 扫描间隔(秒)
EVENT_PUBLISH_INTERVAL_SECONDS=5
```
---
## 4. mci-server 设计
### 4.1 新增端点
#### POST `/api/v1/channel-account/event`
接收 channel-service 推送的统一事件。
**请求头**
```http
Content-Type: application/json
```
**请求体**:见第 3.3 节统一 payload。
**处理逻辑**
| event_type | 处理 |
|------------|------|
| `heartbeat` | 根据 `account_id` 查询 `mci_channel_account`;更新 `login_status``online_status``last_heartbeat``risk_level``sent_count_today` |
| `login_status_changed` | 更新 `mci_channel_account.login_status`;如果 `current=success` 则同时更新 `online_status=1` |
| `online_status_changed` | 更新 `mci_channel_account.online_status` |
| `send_result` | 根据 `record_id` 查询 `mci_send_record`;更新 `send_status``success=true` 则为 2否则 3`finish_time``channel_result`;成功后调用 `SendCallbackService.sendCallback()` 向业务系统回调 |
| `error` | 记录告警日志,可扩展发送告警通知 |
**状态字段映射**channel-service 字符串 → mci-server 整数):
| channel-service 字段 | mci-server 字段 | 映射关系 |
|----------------------|-----------------|----------|
| `login_status` | `mci_channel_account.login_status` | `pending=0`, `scanning=0`, `success=1`, `expired=2`, `failed=3` |
| `online_status` | `mci_channel_account.online_status` | `offline=0`, `online=1`, `error=2` |
账号定位:使用 `tenant_code` + `account_id` 联合查询 `mci_channel_account`,确保租户隔离。
**幂等**
- 使用 `event_id` 作为幂等键。
- 第一次收到时写入 `mci_channel_event` 去重表;重复事件直接返回 200。
**响应**
```json
{
"code": 0,
"message": "ok",
"data": null,
"request_id": "..."
}
```
### 4.2 保持现有端点
- `POST /api/v1/channel-account/heartbeat`:保持兼容,但建议新接入走 `/event`
- `POST /api/v1/channel-account/init`:继续调用 `WechatPersonalChannelAdapter`,由 adapter 调用 channel-service `/v1/wechat-personal/login/start`
### 4.3 发送流程补齐
为了让 `send_result` 能回写到正确的 `mci_send_record`,需要在 mci-server 调用 channel-service 发送时传入 `record_id`
1. `SendService.dispatchRecord(record)` 调用 `ChannelAdapter.sendMessage(request)`
2. `SendMessageRequest.extra` 中带上 `record_id`
3. `WechatPersonalChannelAdapter.sendMessage()``record_id` 放入 body调用 channel-service `/v1/wechat-personal/send/text``/send/image`
4. channel-service 异步完成后,通过 `/event` 回传 `send_result` 并携带 `record_id`
### 4.4 数据库表扩展
#### 4.4.1 新增幂等表 `mci_channel_event`
用于防止 channel-service 重试导致重复处理。
| 字段 | 类型 | 说明 |
|------|------|------|
| `event_id` | VARCHAR PK | channel-service 事件 ID |
| `account_id` | VARCHAR | 账号 ID |
| `event_type` | VARCHAR | 事件类型 |
| `create_time` | DATETIME | 接收时间 |
索引:`CREATE INDEX idx_account_time ON mci_channel_event(account_id, create_time);`
清理策略:保留 30 天后删除(可配置)。
---
## 5. 错误处理与边界
| 场景 | 处理 |
|------|------|
| mci-server 不可达 | 事件留在 SQLiteworker 按指数退避重试 |
| 服务重启 | 启动时恢复 worker继续推送 `pending` / `retrying` 事件 |
| payload 序列化失败 | 直接标记 `dead_letter`,不阻塞其他事件 |
| mci-server 返回业务错误(如账号不存在) | 视为失败重试;若连续失败达到上限,进 `dead_letter` |
| 微信风控/账号被封 | 对应 `error_code` 直接标记 `dead_letter`,不重试 |
| SQLite 磁盘满/损坏 | worker 记录错误并停止推送;监控告警 |
| 单条事件推送超时 | 使用 `httpx` timeout默认 10s超时即失败进入重试 |
| 重试次数达到上限 | `status='dead_letter'`,记录 `last_error`,等待人工处理或手动重试 |
| 事件堆积 | worker 每批最多 100 条;如果持续堆积,考虑后续改为批量 POST 或引入 MQ |
---
## 6. 测试策略
### 6.1 channel-service 单元测试
- `test_event_store.py`:验证 SQLite CRUD、事务、状态流转、清理。
- `test_event_publisher.py`mock mci-server HTTP验证重试、死信、幂等。
- `test_send_async.py`:验证 send API 改为异步受理后,事件正确写入 `outbound_events`
- `test_heartbeat_event.py`:验证心跳接口产生事件。
### 6.2 mci-server 单元测试
- `ChannelAccountEventControllerTest`:使用 MockWebServer 或 `MockMvc` 接收 channel-service 事件,验证:
- heartbeat 更新 `mci_channel_account`
- send_result 更新 `mci_send_record`
- event_id 幂等
### 6.3 联调测试
- docker-compose 启动 mci-server + channel-service。
- 调用 mci-server 发送 API。
- 验证 channel-service 收到指令、产生 send_result 事件、推回 mci-server。
- 验证 mci-server 更新 SendRecord 并向业务系统 callback_url 回调。
---
## 7. 部署与配置
### 7.1 channel-service 新增环境变量
```yaml
environment:
MCI_SERVER_URL: http://mci-server:8080/msg-platform
EVENT_STORE_PATH: /app/data/events.db
HEARTBEAT_INTERVAL_SECONDS: "30"
MAX_RETRY: "5"
BASE_RETRY_DELAY_SECONDS: "5"
EVENT_RETENTION_DAYS: "10"
EVENT_PUBLISH_INTERVAL_SECONDS: "5"
```
### 7.2 数据卷
```yaml
volumes:
- channel-service-data:/app/data
```
---
## 8. 后续扩展
- 入站消息从 channel-service 回传 mci-server复用 `/event` 新增 `message_received` 类型)。
- 批量事件上报:把多条事件打包为一个 HTTP 请求,降低 mci-server 压力。
- 当事件量增大时,可考虑用 RabbitMQ 替代 SQLite + HTTP worker。
- 真实 pywechat 接入时,把 mock 客户端替换为 `pywechat`/`pywinauto` 驱动,事件模型保持不变。
---
*设计完成,待评审。*