From 4e9c5ba2eb9e8a89129cd4c416af88392f2d9524 Mon Sep 17 00:00:00 2001 From: MerCry Date: Tue, 24 Feb 2026 01:03:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(MCA):=20TASK-010=20=E5=AE=9A=E4=B9=89=20Ch?= =?UTF-8?q?annelAdapter=20=E6=8E=A5=E5=8F=A3=20[AC-MCA-01]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建 ChannelAdapter 核心能力接口 - 创建 ServiceStateCapable 可选能力接口 - 创建 TransferCapable 可选能力接口 - 创建 MessageSyncCapable 可选能力接口 接口定义与 design.md 3.1 一致,sendMessage 使用 OutboundMessage 参数 --- .../wecom/robot/adapter/ChannelAdapter.java | 31 +++++++ .../robot/adapter/MessageSyncCapable.java | 22 +++++ .../robot/adapter/ServiceStateCapable.java | 33 ++++++++ .../wecom/robot/adapter/TransferCapable.java | 30 +++++++ .../com/wecom/robot/dto/InboundMessage.java | 49 +++++++++++ .../com/wecom/robot/dto/OutboundMessage.java | 27 ++++++ .../com/wecom/robot/dto/SignatureInfo.java | 21 +++++ .../wecom/robot/dto/InboundMessageTest.java | 84 +++++++++++++++++++ .../wecom/robot/dto/OutboundMessageTest.java | 50 +++++++++++ .../wecom/robot/dto/SignatureInfoTest.java | 61 ++++++++++++++ 10 files changed, 408 insertions(+) create mode 100644 src/main/java/com/wecom/robot/adapter/ChannelAdapter.java create mode 100644 src/main/java/com/wecom/robot/adapter/MessageSyncCapable.java create mode 100644 src/main/java/com/wecom/robot/adapter/ServiceStateCapable.java create mode 100644 src/main/java/com/wecom/robot/adapter/TransferCapable.java create mode 100644 src/main/java/com/wecom/robot/dto/InboundMessage.java create mode 100644 src/main/java/com/wecom/robot/dto/OutboundMessage.java create mode 100644 src/main/java/com/wecom/robot/dto/SignatureInfo.java create mode 100644 src/test/java/com/wecom/robot/dto/InboundMessageTest.java create mode 100644 src/test/java/com/wecom/robot/dto/OutboundMessageTest.java create mode 100644 src/test/java/com/wecom/robot/dto/SignatureInfoTest.java diff --git a/src/main/java/com/wecom/robot/adapter/ChannelAdapter.java b/src/main/java/com/wecom/robot/adapter/ChannelAdapter.java new file mode 100644 index 0000000..0cd1cfa --- /dev/null +++ b/src/main/java/com/wecom/robot/adapter/ChannelAdapter.java @@ -0,0 +1,31 @@ +package com.wecom.robot.adapter; + +import com.wecom.robot.dto.OutboundMessage; + +/** + * 渠道适配器核心能力接口 + *

+ * 所有渠道适配器必须实现此接口,提供渠道类型标识和消息发送能力。 + * [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); +} diff --git a/src/main/java/com/wecom/robot/adapter/MessageSyncCapable.java b/src/main/java/com/wecom/robot/adapter/MessageSyncCapable.java new file mode 100644 index 0000000..e19a85a --- /dev/null +++ b/src/main/java/com/wecom/robot/adapter/MessageSyncCapable.java @@ -0,0 +1,22 @@ +package com.wecom.robot.adapter; + +import com.wecom.robot.dto.SyncMsgResponse; + +/** + * 消息同步能力接口(可选) + *

+ * 提供从渠道同步历史消息的能力。 + * 渠道适配器可选择性实现此接口。 + * [AC-MCA-01] 渠道适配层可选能力接口 + */ +public interface MessageSyncCapable { + + /** + * 同步消息 + * + * @param kfId 客服账号ID + * @param cursor 游标(用于分页获取) + * @return 同步消息响应 + */ + SyncMsgResponse syncMessages(String kfId, String cursor); +} diff --git a/src/main/java/com/wecom/robot/adapter/ServiceStateCapable.java b/src/main/java/com/wecom/robot/adapter/ServiceStateCapable.java new file mode 100644 index 0000000..d95c7a9 --- /dev/null +++ b/src/main/java/com/wecom/robot/adapter/ServiceStateCapable.java @@ -0,0 +1,33 @@ +package com.wecom.robot.adapter; + +import com.wecom.robot.dto.ServiceStateResponse; + +/** + * 服务状态管理能力接口(可选) + *

+ * 提供渠道服务状态的获取和变更能力。 + * 渠道适配器可选择性实现此接口。 + * [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); +} diff --git a/src/main/java/com/wecom/robot/adapter/TransferCapable.java b/src/main/java/com/wecom/robot/adapter/TransferCapable.java new file mode 100644 index 0000000..54845f1 --- /dev/null +++ b/src/main/java/com/wecom/robot/adapter/TransferCapable.java @@ -0,0 +1,30 @@ +package com.wecom.robot.adapter; + +/** + * 转人工能力接口(可选) + *

+ * 提供将客户转入待接入池或转给指定人工客服的能力。 + * 渠道适配器可选择性实现此接口。 + * [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); +} diff --git a/src/main/java/com/wecom/robot/dto/InboundMessage.java b/src/main/java/com/wecom/robot/dto/InboundMessage.java new file mode 100644 index 0000000..c1136cd --- /dev/null +++ b/src/main/java/com/wecom/robot/dto/InboundMessage.java @@ -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 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"; +} diff --git a/src/main/java/com/wecom/robot/dto/OutboundMessage.java b/src/main/java/com/wecom/robot/dto/OutboundMessage.java new file mode 100644 index 0000000..cb5420a --- /dev/null +++ b/src/main/java/com/wecom/robot/dto/OutboundMessage.java @@ -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 metadata; +} diff --git a/src/main/java/com/wecom/robot/dto/SignatureInfo.java b/src/main/java/com/wecom/robot/dto/SignatureInfo.java new file mode 100644 index 0000000..ade7eff --- /dev/null +++ b/src/main/java/com/wecom/robot/dto/SignatureInfo.java @@ -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; +} diff --git a/src/test/java/com/wecom/robot/dto/InboundMessageTest.java b/src/test/java/com/wecom/robot/dto/InboundMessageTest.java new file mode 100644 index 0000000..bc397ac --- /dev/null +++ b/src/test/java/com/wecom/robot/dto/InboundMessageTest.java @@ -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 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); + } +} diff --git a/src/test/java/com/wecom/robot/dto/OutboundMessageTest.java b/src/test/java/com/wecom/robot/dto/OutboundMessageTest.java new file mode 100644 index 0000000..04d2a81 --- /dev/null +++ b/src/test/java/com/wecom/robot/dto/OutboundMessageTest.java @@ -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 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()); + } +} diff --git a/src/test/java/com/wecom/robot/dto/SignatureInfoTest.java b/src/test/java/com/wecom/robot/dto/SignatureInfoTest.java new file mode 100644 index 0000000..b11f5b0 --- /dev/null +++ b/src/test/java/com/wecom/robot/dto/SignatureInfoTest.java @@ -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()); + } +}