feat(MCA): TASK-010 定义 ChannelAdapter 接口 [AC-MCA-01]

- 创建 ChannelAdapter 核心能力接口

- 创建 ServiceStateCapable 可选能力接口

- 创建 TransferCapable 可选能力接口

- 创建 MessageSyncCapable 可选能力接口

接口定义与 design.md 3.1 一致,sendMessage 使用 OutboundMessage 参数
This commit is contained in:
MerCry 2026-02-24 01:03:06 +08:00
parent b9792c8673
commit 4e9c5ba2eb
10 changed files with 408 additions and 0 deletions

View File

@ -0,0 +1,31 @@
package com.wecom.robot.adapter;
import com.wecom.robot.dto.OutboundMessage;
/**
* 渠道适配器核心能力接口
* <p>
* 所有渠道适配器必须实现此接口提供渠道类型标识和消息发送能力
* [AC-MCA-01] 渠道适配层核心接口
*
* @see ServiceStateCapable
* @see TransferCapable
* @see MessageSyncCapable
*/
public interface ChannelAdapter {
/**
* 获取渠道类型标识
*
* @return 渠道类型 "wechat", "douyin", "jd"
*/
String getChannelType();
/**
* 发送消息到渠道
*
* @param message 出站消息对象
* @return 发送是否成功
*/
boolean sendMessage(OutboundMessage message);
}

View File

@ -0,0 +1,22 @@
package com.wecom.robot.adapter;
import com.wecom.robot.dto.SyncMsgResponse;
/**
* 消息同步能力接口可选
* <p>
* 提供从渠道同步历史消息的能力
* 渠道适配器可选择性实现此接口
* [AC-MCA-01] 渠道适配层可选能力接口
*/
public interface MessageSyncCapable {
/**
* 同步消息
*
* @param kfId 客服账号ID
* @param cursor 游标用于分页获取
* @return 同步消息响应
*/
SyncMsgResponse syncMessages(String kfId, String cursor);
}

View File

@ -0,0 +1,33 @@
package com.wecom.robot.adapter;
import com.wecom.robot.dto.ServiceStateResponse;
/**
* 服务状态管理能力接口可选
* <p>
* 提供渠道服务状态的获取和变更能力
* 渠道适配器可选择性实现此接口
* [AC-MCA-01] 渠道适配层可选能力接口
*/
public interface ServiceStateCapable {
/**
* 获取服务状态
*
* @param kfId 客服账号ID
* @param customerId 客户ID
* @return 服务状态响应
*/
ServiceStateResponse getServiceState(String kfId, String customerId);
/**
* 变更服务状态
*
* @param kfId 客服账号ID
* @param customerId 客户ID
* @param newState 新状态值
* @param servicerId 人工客服ID可选
* @return 变更是否成功
*/
boolean transServiceState(String kfId, String customerId, int newState, String servicerId);
}

View File

@ -0,0 +1,30 @@
package com.wecom.robot.adapter;
/**
* 转人工能力接口可选
* <p>
* 提供将客户转入待接入池或转给指定人工客服的能力
* 渠道适配器可选择性实现此接口
* [AC-MCA-01] 渠道适配层可选能力接口
*/
public interface TransferCapable {
/**
* 转入待接入池
*
* @param kfId 客服账号ID
* @param customerId 客户ID
* @return 转移是否成功
*/
boolean transferToPool(String kfId, String customerId);
/**
* 转给指定人工客服
*
* @param kfId 客服账号ID
* @param customerId 客户ID
* @param servicerId 人工客服ID
* @return 转移是否成功
*/
boolean transferToManual(String kfId, String customerId, String servicerId);
}

View File

@ -0,0 +1,49 @@
package com.wecom.robot.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InboundMessage {
private String channelType;
private String channelMessageId;
private String sessionKey;
private String customerId;
private String kfId;
private String sender;
private String content;
private String msgType;
private String rawPayload;
private Long timestamp;
private SignatureInfo signatureInfo;
private Map<String, Object> metadata;
public static final String CHANNEL_WECHAT = "wechat";
public static final String CHANNEL_DOUYIN = "douyin";
public static final String CHANNEL_JD = "jd";
public static final String MSG_TYPE_TEXT = "text";
public static final String MSG_TYPE_IMAGE = "image";
public static final String MSG_TYPE_VOICE = "voice";
public static final String MSG_TYPE_VIDEO = "video";
public static final String MSG_TYPE_EVENT = "event";
}

View File

@ -0,0 +1,27 @@
package com.wecom.robot.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OutboundMessage {
private String channelType;
private String receiver;
private String kfId;
private String content;
private String msgType;
private Map<String, Object> metadata;
}

View File

@ -0,0 +1,21 @@
package com.wecom.robot.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SignatureInfo {
private String signature;
private String timestamp;
private String nonce;
private String algorithm;
}

View File

@ -0,0 +1,84 @@
package com.wecom.robot.dto;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
class InboundMessageTest {
@Test
void testInboundMessageBuilder() {
SignatureInfo signatureInfo = SignatureInfo.builder()
.signature("test-signature")
.timestamp("1234567890")
.nonce("test-nonce")
.algorithm("sha256")
.build();
Map<String, Object> metadata = new HashMap<>();
metadata.put("key1", "value1");
InboundMessage message = InboundMessage.builder()
.channelType(InboundMessage.CHANNEL_WECHAT)
.channelMessageId("msg-123")
.sessionKey("session-key-001")
.customerId("customer-001")
.kfId("kf-001")
.sender("user-001")
.content("Hello World")
.msgType(InboundMessage.MSG_TYPE_TEXT)
.rawPayload("{\"raw\":\"data\"}")
.timestamp(1234567890L)
.signatureInfo(signatureInfo)
.metadata(metadata)
.build();
assertEquals(InboundMessage.CHANNEL_WECHAT, message.getChannelType());
assertEquals("msg-123", message.getChannelMessageId());
assertEquals("session-key-001", message.getSessionKey());
assertEquals("customer-001", message.getCustomerId());
assertEquals("kf-001", message.getKfId());
assertEquals("user-001", message.getSender());
assertEquals("Hello World", message.getContent());
assertEquals(InboundMessage.MSG_TYPE_TEXT, message.getMsgType());
assertEquals("{\"raw\":\"data\"}", message.getRawPayload());
assertEquals(1234567890L, message.getTimestamp());
assertNotNull(message.getSignatureInfo());
assertEquals("test-signature", message.getSignatureInfo().getSignature());
assertNotNull(message.getMetadata());
assertEquals("value1", message.getMetadata().get("key1"));
}
@Test
void testInboundMessageSetters() {
InboundMessage message = new InboundMessage();
message.setChannelType(InboundMessage.CHANNEL_DOUYIN);
message.setChannelMessageId("msg-456");
message.setSessionKey("session-key-002");
message.setContent("Test message");
assertEquals(InboundMessage.CHANNEL_DOUYIN, message.getChannelType());
assertEquals("msg-456", message.getChannelMessageId());
assertEquals("session-key-002", message.getSessionKey());
assertEquals("Test message", message.getContent());
}
@Test
void testChannelTypeConstants() {
assertEquals("wechat", InboundMessage.CHANNEL_WECHAT);
assertEquals("douyin", InboundMessage.CHANNEL_DOUYIN);
assertEquals("jd", InboundMessage.CHANNEL_JD);
}
@Test
void testMsgTypeConstants() {
assertEquals("text", InboundMessage.MSG_TYPE_TEXT);
assertEquals("image", InboundMessage.MSG_TYPE_IMAGE);
assertEquals("voice", InboundMessage.MSG_TYPE_VOICE);
assertEquals("video", InboundMessage.MSG_TYPE_VIDEO);
assertEquals("event", InboundMessage.MSG_TYPE_EVENT);
}
}

View File

@ -0,0 +1,50 @@
package com.wecom.robot.dto;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
class OutboundMessageTest {
@Test
void testOutboundMessageBuilder() {
Map<String, Object> metadata = new HashMap<>();
metadata.put("priority", "high");
OutboundMessage message = OutboundMessage.builder()
.channelType(InboundMessage.CHANNEL_WECHAT)
.receiver("customer-001")
.kfId("kf-001")
.content("Reply message")
.msgType("text")
.metadata(metadata)
.build();
assertEquals(InboundMessage.CHANNEL_WECHAT, message.getChannelType());
assertEquals("customer-001", message.getReceiver());
assertEquals("kf-001", message.getKfId());
assertEquals("Reply message", message.getContent());
assertEquals("text", message.getMsgType());
assertNotNull(message.getMetadata());
assertEquals("high", message.getMetadata().get("priority"));
}
@Test
void testOutboundMessageSetters() {
OutboundMessage message = new OutboundMessage();
message.setChannelType(InboundMessage.CHANNEL_JD);
message.setReceiver("jd-customer-001");
message.setKfId("jd-kf-001");
message.setContent("JD reply");
message.setMsgType("text");
assertEquals(InboundMessage.CHANNEL_JD, message.getChannelType());
assertEquals("jd-customer-001", message.getReceiver());
assertEquals("jd-kf-001", message.getKfId());
assertEquals("JD reply", message.getContent());
assertEquals("text", message.getMsgType());
}
}

View File

@ -0,0 +1,61 @@
package com.wecom.robot.dto;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class SignatureInfoTest {
@Test
void testSignatureInfoBuilder() {
SignatureInfo signatureInfo = SignatureInfo.builder()
.signature("abc123signature")
.timestamp("1708700000")
.nonce("random-nonce-value")
.algorithm("hmac-sha256")
.build();
assertEquals("abc123signature", signatureInfo.getSignature());
assertEquals("1708700000", signatureInfo.getTimestamp());
assertEquals("random-nonce-value", signatureInfo.getNonce());
assertEquals("hmac-sha256", signatureInfo.getAlgorithm());
}
@Test
void testSignatureInfoSetters() {
SignatureInfo signatureInfo = new SignatureInfo();
signatureInfo.setSignature("test-sig");
signatureInfo.setTimestamp("12345");
signatureInfo.setNonce("test-nonce");
signatureInfo.setAlgorithm("md5");
assertEquals("test-sig", signatureInfo.getSignature());
assertEquals("12345", signatureInfo.getTimestamp());
assertEquals("test-nonce", signatureInfo.getNonce());
assertEquals("md5", signatureInfo.getAlgorithm());
}
@Test
void testSignatureInfoNoArgsConstructor() {
SignatureInfo signatureInfo = new SignatureInfo();
assertNull(signatureInfo.getSignature());
assertNull(signatureInfo.getTimestamp());
assertNull(signatureInfo.getNonce());
assertNull(signatureInfo.getAlgorithm());
}
@Test
void testSignatureInfoAllArgsConstructor() {
SignatureInfo signatureInfo = new SignatureInfo(
"full-sig",
"9999",
"full-nonce",
"sha1"
);
assertEquals("full-sig", signatureInfo.getSignature());
assertEquals("9999", signatureInfo.getTimestamp());
assertEquals("full-nonce", signatureInfo.getNonce());
assertEquals("sha1", signatureInfo.getAlgorithm());
}
}