637 lines
33 KiB
Markdown
637 lines
33 KiB
Markdown
|
|
---
|
|||
|
|
feature_id: "MCA"
|
|||
|
|
title: "多渠道适配主框架架构设计"
|
|||
|
|
status: "draft"
|
|||
|
|
version: "0.2.0"
|
|||
|
|
owners:
|
|||
|
|
- "architect"
|
|||
|
|
- "backend"
|
|||
|
|
last_updated: "2026-02-24"
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 多渠道适配主框架架构设计(design.md)
|
|||
|
|
|
|||
|
|
## 1. 系统架构
|
|||
|
|
|
|||
|
|
### 1.1 整体架构图
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ 外部系统 │
|
|||
|
|
├─────────────────┬─────────────────┬─────────────────┬───────────────────────┤
|
|||
|
|
│ 企业微信 API │ 抖音 API │ 京东 API │ 前端工作台 │
|
|||
|
|
└────────┬────────┴────────┬────────┴────────┬────────┴──────────┬────────────┘
|
|||
|
|
│ │ │ │
|
|||
|
|
▼ ▼ ▼ ▼
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ Java 主框架 (Spring Boot) │
|
|||
|
|
├─────────────────────────────────────────────────────────────────────────────┤
|
|||
|
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
|||
|
|
│ │ 入口层 (Controller Layer) │ │
|
|||
|
|
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
|
|||
|
|
│ │ │WecomCallback │ │DouyinCallback│ │ JdCallback │ (预留) │ │
|
|||
|
|
│ │ │ Controller │ │ Controller │ │ Controller │ │ │
|
|||
|
|
│ │ └──────┬───────┘ └──────────────┘ └──────────────┘ │ │
|
|||
|
|
│ │ │ │ │
|
|||
|
|
│ │ │ 验签/解密/解析 → InboundMessage │ │
|
|||
|
|
│ │ ▼ │ │
|
|||
|
|
│ └─────────┼───────────────────────────────────────────────────────────┘ │
|
|||
|
|
│ │ │
|
|||
|
|
│ ▼ │
|
|||
|
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
|||
|
|
│ │ 消息路由层 (Message Router) │ │
|
|||
|
|
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
|
|||
|
|
│ │ │ MessageRouterService (渠道无关) │ │ │
|
|||
|
|
│ │ │ - processInboundMessage(InboundMessage) │ │ │
|
|||
|
|
│ │ │ - routeBySessionState(Session, InboundMessage) │ │ │
|
|||
|
|
│ │ │ - dispatchToAiService(Session, InboundMessage) │ │ │
|
|||
|
|
│ │ │ - dispatchToManualCs(Session, InboundMessage) │ │ │
|
|||
|
|
│ │ └──────────────────────────────────────────────────────────────┘ │ │
|
|||
|
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
|||
|
|
│ │ │
|
|||
|
|
│ ▼ │
|
|||
|
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
|||
|
|
│ │ 渠道适配层 (Channel Adapter) │ │
|
|||
|
|
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
|
|||
|
|
│ │ │ ChannelAdapter 接口 (核心能力) │ │ │
|
|||
|
|
│ │ │ - getChannelType() │ │ │
|
|||
|
|
│ │ │ - sendMessage(OutboundMessage) │ │ │
|
|||
|
|
│ │ └──────────────────────────────────────────────────────────────┘ │ │
|
|||
|
|
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
|
|||
|
|
│ │ │ 可选能力接口 (Optional Capabilities) │ │ │
|
|||
|
|
│ │ │ - ServiceStateCapable - TransferCapable - MessageSyncCapable│ │ │
|
|||
|
|
│ │ └──────────────────────────────────────────────────────────────┘ │ │
|
|||
|
|
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
|
|||
|
|
│ │ │ WeChatAdapter│ │DouyinAdapter │ │ JdAdapter │ (预留) │ │
|
|||
|
|
│ │ │ (已实现) │ │ (预留) │ │ (预留) │ │ │
|
|||
|
|
│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
|
|||
|
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
|||
|
|
│ │ │
|
|||
|
|
│ ┌─────────────────────────┼─────────────────────────┐ │
|
|||
|
|
│ ▼ ▼ ▼ │
|
|||
|
|
│ ┌──────────────┐ ┌──────────────────────┐ ┌──────────────┐ │
|
|||
|
|
│ │ AI 服务客户端 │ │ 会话管理层 │ │ WebSocket 服务│ │
|
|||
|
|
│ │AiServiceClient│ │SessionManagerService │ │WebSocketService│ │
|
|||
|
|
│ └──────┬───────┘ └──────────────────────┘ └──────────────┘ │
|
|||
|
|
│ │ │
|
|||
|
|
└─────────┼───────────────────────────────────────────────────────────────────┘
|
|||
|
|
│ HTTP
|
|||
|
|
▼
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ Python AI 服务 (独立部署) │
|
|||
|
|
├─────────────────────────────────────────────────────────────────────────────┤
|
|||
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|||
|
|
│ │ OpenAI Client│ │DeepSeek Client│ │ 其他模型 │ │
|
|||
|
|
│ └──────┬───────┘ └──────┬───────┘ └──────────────┘ │
|
|||
|
|
│ │ │ │
|
|||
|
|
│ ▼ ▼ │
|
|||
|
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
|||
|
|
│ │ AI 服务核心逻辑 │ │
|
|||
|
|
│ │ - /ai/chat 生成 AI 回复 │ │
|
|||
|
|
│ │ - /ai/health 健康检查 │ │
|
|||
|
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
|||
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 1.2 模块职责
|
|||
|
|
|
|||
|
|
| 模块 | 职责 | 关联 AC |
|
|||
|
|
|-----|------|--------|
|
|||
|
|
| **入口层** | 接收渠道回调,验签/解密/解析,转换为统一的 InboundMessage | AC-MCA-08 |
|
|||
|
|
| **消息路由层** | 渠道无关的消息路由,根据会话状态分发到 AI 或人工 | AC-MCA-08 ~ AC-MCA-10 |
|
|||
|
|
| **渠道适配层** | 封装各渠道 API 差异,提供统一的消息发送接口 | AC-MCA-01 ~ AC-MCA-03 |
|
|||
|
|
| **AI 服务客户端** | 调用 Python AI 服务,处理超时/降级 | AC-MCA-04 ~ AC-MCA-07 |
|
|||
|
|
| **会话管理层** | 管理会话生命周期、状态变更、消息持久化 | AC-MCA-11 ~ AC-MCA-12 |
|
|||
|
|
| **WebSocket 服务** | 实时推送消息到人工客服工作台 | AC-MCA-10 |
|
|||
|
|
| **Python AI 服务** | AI 模型推理、置信度评估、转人工建议 | AC-MCA-04 ~ AC-MCA-05 |
|
|||
|
|
|
|||
|
|
## 2. 统一消息模型
|
|||
|
|
|
|||
|
|
### 2.1 入站消息 (InboundMessage)
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Data
|
|||
|
|
public class InboundMessage {
|
|||
|
|
private String channelType; // 渠道类型: wechat/douyin/jd
|
|||
|
|
private String channelMessageId; // 渠道原始消息ID (用于幂等)
|
|||
|
|
private String sessionKey; // 会话标识 (customerId + kfId 组合)
|
|||
|
|
private String customerId; // 客户ID
|
|||
|
|
private String kfId; // 客服账号ID
|
|||
|
|
private String sender; // 发送者标识
|
|||
|
|
private String content; // 消息内容 (统一字段名)
|
|||
|
|
private String msgType; // 消息类型: text/image/voice 等
|
|||
|
|
private String rawPayload; // 原始消息体 (JSON/XML)
|
|||
|
|
private Long timestamp; // 消息时间戳
|
|||
|
|
private SignatureInfo signatureInfo; // 签名信息
|
|||
|
|
private Map<String, Object> metadata; // 扩展元数据
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@Data
|
|||
|
|
public class SignatureInfo {
|
|||
|
|
private String signature; // 签名值
|
|||
|
|
private String timestamp; // 签名时间戳
|
|||
|
|
private String nonce; // 随机数
|
|||
|
|
private String algorithm; // 签名算法 (可选)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.2 出站消息 (OutboundMessage)
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Data
|
|||
|
|
public class OutboundMessage {
|
|||
|
|
private String channelType; // 渠道类型
|
|||
|
|
private String receiver; // 接收者ID (customerId)
|
|||
|
|
private String kfId; // 客服账号ID
|
|||
|
|
private String content; // 消息内容
|
|||
|
|
private String msgType; // 消息类型
|
|||
|
|
private Map<String, Object> metadata; // 扩展元数据
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.3 字段映射策略
|
|||
|
|
|
|||
|
|
> **重要**:内部统一使用 `content` 字段名,与 AI 服务契约 (`currentMessage`) 的映射在 AiServiceClient 层处理。
|
|||
|
|
|
|||
|
|
| 内部字段 | AI 服务契约字段 | 映射位置 |
|
|||
|
|
|---------|----------------|---------|
|
|||
|
|
| `InboundMessage.content` | `ChatRequest.currentMessage` | `AiServiceClient.generateReply()` |
|
|||
|
|
| `InboundMessage.sessionKey` | `ChatRequest.sessionId` | `AiServiceClient.generateReply()` |
|
|||
|
|
| `InboundMessage.channelType` | `ChatRequest.channelType` | `AiServiceClient.generateReply()` |
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
public ChatRequest toChatRequest(InboundMessage msg, List<Message> history) {
|
|||
|
|
ChatRequest request = new ChatRequest();
|
|||
|
|
request.setSessionId(msg.getSessionKey());
|
|||
|
|
request.setCurrentMessage(msg.getContent()); // content → currentMessage
|
|||
|
|
request.setChannelType(msg.getChannelType());
|
|||
|
|
request.setHistory(history);
|
|||
|
|
return request;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 3. 核心接口设计
|
|||
|
|
|
|||
|
|
### 3.1 渠道适配器接口
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// 核心能力接口(所有渠道必须实现)
|
|||
|
|
public interface ChannelAdapter {
|
|||
|
|
String getChannelType();
|
|||
|
|
void sendMessage(OutboundMessage message);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 可选能力接口:服务状态管理
|
|||
|
|
public interface ServiceStateCapable {
|
|||
|
|
ServiceState getServiceState(String kfId, String customerId);
|
|||
|
|
boolean transServiceState(String kfId, String customerId, int newState, String servicerId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 可选能力接口:转人工
|
|||
|
|
public interface TransferCapable {
|
|||
|
|
boolean transferToPool(String kfId, String customerId);
|
|||
|
|
boolean transferToManual(String kfId, String customerId, String servicerId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 可选能力接口:消息同步
|
|||
|
|
public interface MessageSyncCapable {
|
|||
|
|
SyncMsgResponse syncMessages(String kfId, String cursor);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 消息路由服务接口
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
public interface MessageRouterService {
|
|||
|
|
void processInboundMessage(InboundMessage message);
|
|||
|
|
void routeBySessionState(Session session, InboundMessage message);
|
|||
|
|
void dispatchToAiService(Session session, InboundMessage message);
|
|||
|
|
void dispatchToManualCs(Session session, InboundMessage message);
|
|||
|
|
void dispatchToPendingPool(Session session, InboundMessage message);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.3 AI 服务客户端接口
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
public interface AiServiceClient {
|
|||
|
|
ChatResponse generateReply(ChatRequest request);
|
|||
|
|
boolean healthCheck();
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 4. 核心流程
|
|||
|
|
|
|||
|
|
### 4.1 消息处理主流程(渠道无关)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
|||
|
|
│ 渠道回调入口 │────▶│ 验签/解密/解析 │────▶│ 构建 InboundMessage│
|
|||
|
|
│ (Controller) │ │ (渠道专属逻辑) │ │ (统一消息模型) │
|
|||
|
|
└──────────────────┘ └────────┬─────────┘ └──────────────────┘
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌──────────────────┐
|
|||
|
|
│ MessageRouter │
|
|||
|
|
│ processInbound │
|
|||
|
|
│ Message() │
|
|||
|
|
└────────┬─────────┘
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌──────────────────┐
|
|||
|
|
│ 幂等检查 (msgId) │
|
|||
|
|
│ Redis SETNX │
|
|||
|
|
└────────┬─────────┘
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌──────────────────┐
|
|||
|
|
│ 获取/创建会话 │
|
|||
|
|
│ SessionManager │
|
|||
|
|
└────────┬─────────┘
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌──────────────────┐
|
|||
|
|
│ 获取渠道服务状态 │
|
|||
|
|
│ (可选能力检测) │
|
|||
|
|
└────────┬─────────┘
|
|||
|
|
│
|
|||
|
|
┌───────────────┼───────────────┐
|
|||
|
|
▼ ▼ ▼
|
|||
|
|
┌──────────┐ ┌──────────┐ ┌──────────┐
|
|||
|
|
│ AI 状态 │ │ POOL 状态 │ │MANUAL 状态│
|
|||
|
|
│ │ │ │ │ │
|
|||
|
|
└────┬─────┘ └────┬─────┘ └────┬─────┘
|
|||
|
|
│ │ │
|
|||
|
|
▼ ▼ ▼
|
|||
|
|
┌──────────────┐ ┌──────────┐ ┌──────────────┐
|
|||
|
|
│dispatchTo │ │dispatchTo│ │dispatchTo │
|
|||
|
|
│ AiService │ │PendingPool│ │ ManualCs │
|
|||
|
|
└──────┬───────┘ └──────────┘ └──────────────┘
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌──────────────┐
|
|||
|
|
│ 判断是否转人工│
|
|||
|
|
│shouldTransfer│
|
|||
|
|
└──────┬───────┘
|
|||
|
|
│
|
|||
|
|
┌───────┴───────┐
|
|||
|
|
▼ ▼
|
|||
|
|
┌───────┐ ┌──────────────┐
|
|||
|
|
│发送回复│ │ 转入待接入池 │
|
|||
|
|
│给用户 │ │ TransferCapable│
|
|||
|
|
└───────┘ └──────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.2 AI 服务调用流程
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌──────────────────┐
|
|||
|
|
│ 构造 ChatRequest │
|
|||
|
|
│ sessionId │
|
|||
|
|
│ currentMessage │←── content 映射
|
|||
|
|
│ channelType │
|
|||
|
|
│ history (可选) │
|
|||
|
|
└────────┬─────────┘
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌──────────────────┐ ┌──────────────────┐
|
|||
|
|
│ HTTP POST │────▶│ Python AI 服务 │
|
|||
|
|
│ /ai/chat │ │ 超时: 5s │
|
|||
|
|
└────────┬─────────┘ └────────┬─────────┘
|
|||
|
|
│ │
|
|||
|
|
│ ┌──────────────┼──────────────┐
|
|||
|
|
│ ▼ ▼ ▼
|
|||
|
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
|||
|
|
│ │ 成功响应 │ │ 超时/失败 │ │ 服务不可用│
|
|||
|
|
│ │ 200 OK │ │ Timeout │ │ 503 │
|
|||
|
|
│ └────┬─────┘ └────┬─────┘ └────┬─────┘
|
|||
|
|
│ │ │ │
|
|||
|
|
│ ▼ ▼ ▼
|
|||
|
|
│ ┌──────────┐ ┌──────────────────────┐
|
|||
|
|
│ │ 返回回复 │ │ 降级处理 │
|
|||
|
|
│ │ reply │ │ 返回固定回复 │
|
|||
|
|
│ │confidence│ │ "正在转接人工客服..." │
|
|||
|
|
│ │shouldTransfer│ └──────────┬───────────┘
|
|||
|
|
│ └──────────┘ │
|
|||
|
|
│ ▼
|
|||
|
|
│ ┌──────────────┐
|
|||
|
|
│ │ 触发转人工 │
|
|||
|
|
│ │ TransferCapable│
|
|||
|
|
│ └──────────────┘
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌──────────────────┐
|
|||
|
|
│ 处理响应 │
|
|||
|
|
│ - 保存消息 │
|
|||
|
|
│ - 发送给用户 │
|
|||
|
|
│ - 判断转人工 │
|
|||
|
|
└──────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 5. 数据模型
|
|||
|
|
|
|||
|
|
### 5.1 实体关系图
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌──────────────────┐ ┌──────────────────┐
|
|||
|
|
│ Session │ │ Message │
|
|||
|
|
├──────────────────┤ ├──────────────────┤
|
|||
|
|
│ sessionId (PK) │──────▶│ msgId (PK) │
|
|||
|
|
│ customerId │ 1:N │ sessionId (FK) │
|
|||
|
|
│ kfId │ │ senderType │
|
|||
|
|
│ channelType (新) │ │ senderId │
|
|||
|
|
│ status │ │ content │
|
|||
|
|
│ wxServiceState │ │ msgType │
|
|||
|
|
│ manualCsId │ │ rawData │
|
|||
|
|
│ createdAt │ │ createdAt │
|
|||
|
|
│ updatedAt │ └──────────────────┘
|
|||
|
|
└──────────────────┘
|
|||
|
|
│
|
|||
|
|
│ 1:N
|
|||
|
|
▼
|
|||
|
|
┌──────────────────┐
|
|||
|
|
│ TransferLog │
|
|||
|
|
├──────────────────┤
|
|||
|
|
│ id (PK) │
|
|||
|
|
│ sessionId (FK) │
|
|||
|
|
│ triggerReason │
|
|||
|
|
│ triggerTime │
|
|||
|
|
│ acceptedCsId │
|
|||
|
|
│ acceptedTime │
|
|||
|
|
└──────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.2 数据库变更
|
|||
|
|
|
|||
|
|
> **口径说明**:本次仅做最小 schema 变更,新增 `channel_type` 字段,默认值为 `wechat`;可通过在线 DDL 方式执行;不涉及数据迁移。符合 requirements.md 中"仅增加渠道类型字段,不进行大规模迁移"的范围约定。
|
|||
|
|
|
|||
|
|
| 表名 | 变更类型 | 变更内容 |
|
|||
|
|
|-----|---------|---------|
|
|||
|
|
| `session` | 新增字段 | `channel_type VARCHAR(20) DEFAULT 'wechat'` |
|
|||
|
|
|
|||
|
|
**DDL 示例**:
|
|||
|
|
```sql
|
|||
|
|
ALTER TABLE session ADD COLUMN channel_type VARCHAR(20) DEFAULT 'wechat'
|
|||
|
|
COMMENT '渠道类型: wechat/douyin/jd';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.3 Redis 缓存结构
|
|||
|
|
|
|||
|
|
| Key 模式 | 类型 | 说明 | TTL |
|
|||
|
|
|---------|------|------|-----|
|
|||
|
|
| `wecom:access_token` | String | 微信 access_token | 7200s - 300s |
|
|||
|
|
| `wecom:cursor:{openKfId}` | String | 消息同步游标 | 永久 |
|
|||
|
|
| `session:status:{sessionId}` | String | 会话状态缓存 | 24h |
|
|||
|
|
| `session:msg_count:{sessionId}` | String | 消息计数 | 24h |
|
|||
|
|
| `idempotent:{msgId}` | String | 消息幂等键 | 1h |
|
|||
|
|
|
|||
|
|
## 6. 跨模块调用策略
|
|||
|
|
|
|||
|
|
### 6.1 AI 服务调用
|
|||
|
|
|
|||
|
|
| 配置项 | 值 | 说明 |
|
|||
|
|
|-------|---|------|
|
|||
|
|
| **超时时间** | 5 秒 | 连接 + 读取总超时 |
|
|||
|
|
| **重试次数** | 0 | 不重试,直接降级 |
|
|||
|
|
| **熔断阈值** | 5 次/分钟 | 连续失败 5 次触发熔断 |
|
|||
|
|
| **熔断时间** | 30 秒 | 熔断后等待时间 |
|
|||
|
|
| **降级策略** | 返回固定回复 + 转人工 | 见下方降级逻辑 |
|
|||
|
|
|
|||
|
|
### 6.2 熔断器选型
|
|||
|
|
|
|||
|
|
> **选型决策**:使用 **Resilience4j** 作为熔断器实现,与 Spring Boot 2.7 兼容。
|
|||
|
|
|
|||
|
|
| 方案 | 说明 |
|
|||
|
|
|-----|------|
|
|||
|
|
| **Resilience4j** | 推荐。轻量级,支持断路器、限流、重试,与 Spring Boot 2.7 兼容良好 |
|
|||
|
|
| 最小实现 | 仅做 timeout + fallback,不做熔断(不推荐,与 requirements 不一致) |
|
|||
|
|
|
|||
|
|
**熔断状态存储**:
|
|||
|
|
- 单实例:内存存储(CircuitBreakerRegistry)
|
|||
|
|
- 多实例:可扩展为 Redis 存储(通过 Resilience4j + Redis 实现)
|
|||
|
|
|
|||
|
|
**依赖配置**:
|
|||
|
|
```xml
|
|||
|
|
<dependency>
|
|||
|
|
<groupId>io.github.resilience4j</groupId>
|
|||
|
|
<artifactId>resilience4j-spring-boot2</artifactId>
|
|||
|
|
<version>2.1.0</version>
|
|||
|
|
</dependency>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.3 降级逻辑
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Service
|
|||
|
|
public class AiServiceClientImpl implements AiServiceClient {
|
|||
|
|
|
|||
|
|
@CircuitBreaker(name = "aiService", fallbackMethod = "fallback")
|
|||
|
|
@TimeLimiter(name = "aiService")
|
|||
|
|
public ChatResponse generateReply(ChatRequest request) {
|
|||
|
|
// HTTP 调用 Python AI 服务
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public ChatResponse fallback(ChatRequest request, Throwable cause) {
|
|||
|
|
log.warn("AI 服务降级: sessionId={}, cause={}",
|
|||
|
|
request.getSessionId(), cause.getMessage());
|
|||
|
|
|
|||
|
|
ChatResponse response = new ChatResponse();
|
|||
|
|
response.setReply("抱歉,我暂时无法回答您的问题,正在为您转接人工客服...");
|
|||
|
|
response.setConfidence(0.0);
|
|||
|
|
response.setShouldTransfer(true);
|
|||
|
|
return response;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.4 错误映射
|
|||
|
|
|
|||
|
|
| AI 服务错误 | 主框架处理 | 用户感知 |
|
|||
|
|
|------------|-----------|---------|
|
|||
|
|
| 200 OK | 正常处理 | 返回 AI 回复 |
|
|||
|
|
| 400 Bad Request | 记录日志,降级 | 转人工 |
|
|||
|
|
| 500 Internal Error | 记录日志,降级 | 转人工 |
|
|||
|
|
| 503 Service Unavailable | 记录日志,降级 | 转人工 |
|
|||
|
|
| Timeout | 记录日志,降级 | 转人工 |
|
|||
|
|
| Connection Refused | 触发熔断,降级 | 转人工 |
|
|||
|
|
|
|||
|
|
## 7. 消息幂等性设计
|
|||
|
|
|
|||
|
|
### 7.1 幂等键
|
|||
|
|
|
|||
|
|
- 使用 `InboundMessage.channelMessageId` 作为幂等键
|
|||
|
|
- 微信渠道:使用微信返回的 `msgId`
|
|||
|
|
- 其他渠道:使用渠道返回的消息 ID 或生成唯一 ID
|
|||
|
|
|
|||
|
|
### 7.2 幂等处理流程
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌──────────────────┐
|
|||
|
|
│ 收到消息 │
|
|||
|
|
│ channelMessageId │
|
|||
|
|
└────────┬─────────┘
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌──────────────────┐ ┌──────────────────┐
|
|||
|
|
│ Redis 检查 │────▶│ Key 不存在 │
|
|||
|
|
│ idempotent:{msgId}│ │ 继续处理 │
|
|||
|
|
└────────┬─────────┘ └────────┬─────────┘
|
|||
|
|
│ │
|
|||
|
|
▼ ▼
|
|||
|
|
┌──────────────────┐ ┌──────────────────┐
|
|||
|
|
│ Key 已存在 │ │ 设置 Key (TTL 1h)│
|
|||
|
|
│ 跳过处理 │ │ 处理消息 │
|
|||
|
|
└──────────────────┘ └──────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 7.3 实现代码
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
public boolean processMessageIdempotent(String channelMessageId, Runnable processor) {
|
|||
|
|
String key = "idempotent:" + channelMessageId;
|
|||
|
|
Boolean absent = redisTemplate.opsForValue()
|
|||
|
|
.setIfAbsent(key, "1", 1, TimeUnit.HOURS);
|
|||
|
|
|
|||
|
|
if (Boolean.TRUE.equals(absent)) {
|
|||
|
|
processor.run();
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
log.info("重复消息,跳过处理: channelMessageId={}", channelMessageId);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 8. 配置管理
|
|||
|
|
|
|||
|
|
### 8.1 新增配置项
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
# application.yml 新增配置
|
|||
|
|
ai-service:
|
|||
|
|
url: http://ai-service:8080
|
|||
|
|
timeout: 5000
|
|||
|
|
|
|||
|
|
resilience4j:
|
|||
|
|
circuitbreaker:
|
|||
|
|
instances:
|
|||
|
|
aiService:
|
|||
|
|
failure-rate-threshold: 50
|
|||
|
|
sliding-window-size: 10
|
|||
|
|
sliding-window-type: COUNT_BASED
|
|||
|
|
wait-duration-in-open-state: 30s
|
|||
|
|
permitted-number-of-calls-in-half-open-state: 3
|
|||
|
|
timelimiter:
|
|||
|
|
instances:
|
|||
|
|
aiService:
|
|||
|
|
timeout-duration: 5s
|
|||
|
|
|
|||
|
|
channel:
|
|||
|
|
default: wechat
|
|||
|
|
adapters:
|
|||
|
|
wechat:
|
|||
|
|
enabled: true
|
|||
|
|
douyin:
|
|||
|
|
enabled: false
|
|||
|
|
jd:
|
|||
|
|
enabled: false
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 8.2 配置类
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Data
|
|||
|
|
@Component
|
|||
|
|
@ConfigurationProperties(prefix = "ai-service")
|
|||
|
|
public class AiServiceConfig {
|
|||
|
|
private String url;
|
|||
|
|
private int timeout = 5000;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@Data
|
|||
|
|
@Component
|
|||
|
|
@ConfigurationProperties(prefix = "channel")
|
|||
|
|
public class ChannelConfig {
|
|||
|
|
private String default;
|
|||
|
|
private Map<String, AdapterConfig> adapters;
|
|||
|
|
|
|||
|
|
@Data
|
|||
|
|
public static class AdapterConfig {
|
|||
|
|
private boolean enabled;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 9. 部署架构
|
|||
|
|
|
|||
|
|
### 9.1 部署拓扑
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────┐
|
|||
|
|
│ 负载均衡器 │
|
|||
|
|
└─────────────────────────────┬───────────────────────────────┘
|
|||
|
|
│
|
|||
|
|
┌───────────────┼───────────────┐
|
|||
|
|
▼ ▼ ▼
|
|||
|
|
┌──────────┐ ┌──────────┐ ┌──────────┐
|
|||
|
|
│ Java 主 │ │ Java 主 │ │ Java 主 │
|
|||
|
|
│ 框架实例1│ │ 框架实例2│ │ 框架实例3│
|
|||
|
|
└────┬─────┘ └────┬─────┘ └────┬─────┘
|
|||
|
|
│ │ │
|
|||
|
|
└──────────────┼──────────────┘
|
|||
|
|
│
|
|||
|
|
┌──────────────────┼──────────────────┐
|
|||
|
|
▼ ▼ ▼
|
|||
|
|
┌──────────┐ ┌──────────┐ ┌──────────┐
|
|||
|
|
│ Redis │ │ MySQL │ │Python AI │
|
|||
|
|
│ (Cluster)│ │ (Master) │ │ 服务 │
|
|||
|
|
└──────────┘ └──────────┘ └──────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 9.2 服务依赖
|
|||
|
|
|
|||
|
|
| 服务 | 依赖关系 | 健康检查 |
|
|||
|
|
|-----|---------|---------|
|
|||
|
|
| Java 主框架 | 依赖 Redis, MySQL, Python AI | `/actuator/health` |
|
|||
|
|
| Python AI 服务 | 无外部依赖 | `/ai/health` |
|
|||
|
|
|
|||
|
|
## 10. 安全设计
|
|||
|
|
|
|||
|
|
### 10.1 渠道回调鉴权
|
|||
|
|
|
|||
|
|
| 渠道 | 鉴权方式 | 验证逻辑 |
|
|||
|
|
|-----|---------|---------|
|
|||
|
|
| 微信 | msg_signature + timestamp + nonce | **沿用现有 WeCom 官方验签/解密方案**(复用现有 `WXBizMsgCrypt` 实现) |
|
|||
|
|
| 抖音 | X-Signature + X-Timestamp | 待实现 |
|
|||
|
|
| 京东 | signature + timestamp | 待实现 |
|
|||
|
|
|
|||
|
|
> **说明**:微信回调验签/加解密使用企业微信官方方案,具体算法细节封装在现有 `WXBizMsgCrypt` 类中,不在本设计文档展开。
|
|||
|
|
|
|||
|
|
### 10.2 内部服务鉴权
|
|||
|
|
|
|||
|
|
- Java 主框架 → Python AI 服务:内网调用,无需鉴权(可扩展为 mTLS)
|
|||
|
|
- WebSocket 连接:路径参数 `{csId}` 标识身份(可扩展为 Token 验证)
|
|||
|
|
|
|||
|
|
## 11. 监控与告警
|
|||
|
|
|
|||
|
|
> **说明**:本节为后续演进预留,MVP 阶段可暂不实现。
|
|||
|
|
|
|||
|
|
### 11.1 关键指标
|
|||
|
|
|
|||
|
|
| 指标 | 类型 | 说明 |
|
|||
|
|
|-----|------|------|
|
|||
|
|
| `ai.service.latency` | Histogram | AI 服务调用延迟 |
|
|||
|
|
| `ai.service.error.rate` | Counter | AI 服务错误率 |
|
|||
|
|
| `ai.service.circuit.breaker.open` | Gauge | 熔断器状态 |
|
|||
|
|
| `message.process.count` | Counter | 消息处理数量 |
|
|||
|
|
| `message.idempotent.skip` | Counter | 幂等跳过数量 |
|
|||
|
|
| `session.active.count` | Gauge | 活跃会话数 |
|
|||
|
|
|
|||
|
|
### 11.2 告警规则
|
|||
|
|
|
|||
|
|
| 规则 | 条件 | 级别 |
|
|||
|
|
|-----|------|------|
|
|||
|
|
| AI 服务不可用 | 连续失败 5 次 | Critical |
|
|||
|
|
| AI 服务延迟过高 | P99 > 3s | Warning |
|
|||
|
|
| 熔断器触发 | circuit.breaker.open = 1 | Critical |
|