# 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 不可达 | 事件留在 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 幂等 ### 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` 驱动,事件模型保持不变。 --- *设计完成,待评审。*