17 KiB
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
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 统一结构
所有事件统一外层:
{
"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
{
"login_status": "success",
"online_status": "online",
"risk_level": 10,
"sent_count_today": 5,
"server_host": "win-svr-01"
}
login_status_changed
{
"previous": "pending",
"current": "success",
"qr_code_url": "https://mock.pywechat.qrcode/...",
"error_message": null
}
online_status_changed
{
"previous": "offline",
"current": "online"
}
send_result
{
"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
{
"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 一致:
send_text/send_image收到请求后,先校验账号在线、风控等级。- 校验通过后返回
accepted=true,并立即写入send_result事件。 - 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 += 1next_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 配置项
# 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 推送的统一事件。
请求头:
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。
响应:
{
"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:
SendService.dispatchRecord(record)调用ChannelAdapter.sendMessage(request)。SendMessageRequest.extra中带上record_id。WechatPersonalChannelAdapter.sendMessage()把record_id放入 body,调用 channel-service/v1/wechat-personal/send/text或/send/image。- 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 不可达 | 事件留在 SQLite,worker 按指数退避重试 |
| 服务重启 | 启动时恢复 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 幂等
- heartbeat 更新
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 新增环境变量
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 数据卷
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驱动,事件模型保持不变。
设计完成,待评审。