feat(channel-wecom): 实现企微会话存档接收骨架(Finance SDK 接口 + 回调 + 异步拉取)
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
package com.sino.mci.channel.wecom.archive;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 企微会话存档 Finance SDK 客户端自动配置。
|
||||
*
|
||||
* <p>默认注入 {@link MockFinanceSdkClient};设置 {@code sino.channel.wecom.finance-sdk.native=true}
|
||||
* 可切换为 {@link NativeFinanceSdkClient}(需自行完成 JNI 接入)。</p>
|
||||
*/
|
||||
@Configuration
|
||||
public class FinanceSdkAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(FinanceSdkClient.class)
|
||||
@ConditionalOnProperty(name = "sino.channel.wecom.finance-sdk.native", havingValue = "true")
|
||||
public FinanceSdkClient nativeFinanceSdkClient() {
|
||||
return new NativeFinanceSdkClient();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(FinanceSdkClient.class)
|
||||
public FinanceSdkClient mockFinanceSdkClient() {
|
||||
return new MockFinanceSdkClient();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.sino.mci.channel.wecom.archive;
|
||||
|
||||
import com.sino.mci.channel.wecom.archive.model.FinanceMessage;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 企业微信会话存档 Finance SDK 客户端接口。
|
||||
*
|
||||
* <p>对应官方 C/JNI SDK 的封装:</p>
|
||||
* <ul>
|
||||
* <li>{@link #init(String, String)} 对应 NewSdk / DestroySdk 的初始化</li>
|
||||
* <li>{@link #getChatData(long, int)} 对应 GetChatData 拉取加密数据</li>
|
||||
* <li>{@link #decryptData(String, String)} 对应 DecryptData 解密单条消息</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>实现说明:</p>
|
||||
* <ul>
|
||||
* <li>{@link MockFinanceSdkClient}:默认实现,返回模拟数据,用于无 JNI 环境开发与测试。</li>
|
||||
* <li>{@link NativeFinanceSdkClient}:预留 JNI 接入路径,真实环境可替换。</li>
|
||||
* </ul>
|
||||
*/
|
||||
public interface FinanceSdkClient {
|
||||
|
||||
/**
|
||||
* 初始化 SDK 会话。
|
||||
*
|
||||
* @param corpId 企业 ID
|
||||
* @param secret 会话存档 Secret
|
||||
* @return 是否初始化成功
|
||||
*/
|
||||
boolean init(String corpId, String secret);
|
||||
|
||||
/**
|
||||
* 按 seq 游标分页拉取加密聊天数据。
|
||||
*
|
||||
* @param seq 起始 seq(游标)
|
||||
* @param limit 本次拉取条数,建议 100-1000
|
||||
* @return 加密消息列表;无新消息时返回空列表
|
||||
*/
|
||||
List<FinanceMessage> getChatData(long seq, int limit);
|
||||
|
||||
/**
|
||||
* 解密单条加密聊天消息。
|
||||
*
|
||||
* @param encryptKey RSA 解密后的消息密钥(encrypt_random_key 解密结果)
|
||||
* @param encryptChatMsg 加密消息体(encrypt_chat_msg)
|
||||
* @return 解密后的明文 JSON 字符串
|
||||
*/
|
||||
String decryptData(String encryptKey, String encryptChatMsg);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.sino.mci.channel.wecom.archive;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.sino.mci.channel.wecom.archive.model.FinanceMessage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* 企业微信会话存档 Finance SDK 模拟客户端。
|
||||
*
|
||||
* <p>默认实现,不依赖 JNI/.so 库,返回固定结构的模拟数据,用于开发、联调与单元测试。</p>
|
||||
*/
|
||||
@Slf4j
|
||||
public class MockFinanceSdkClient implements FinanceSdkClient {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
private final AtomicLong seqGenerator = new AtomicLong(1000);
|
||||
|
||||
@Override
|
||||
public boolean init(String corpId, String secret) {
|
||||
log.info("[MockFinanceSdk] init corpId={}, secret=***", corpId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FinanceMessage> getChatData(long seq, int limit) {
|
||||
log.info("[MockFinanceSdk] getChatData seq={}, limit={}", seq, limit);
|
||||
List<FinanceMessage> messages = new ArrayList<>();
|
||||
int count = Math.min(limit, 2);
|
||||
for (int i = 0; i < count; i++) {
|
||||
long currentSeq = seq + i;
|
||||
String msgId = "mock-msg-" + currentSeq;
|
||||
messages.add(FinanceMessage.builder()
|
||||
.seq(currentSeq)
|
||||
.msgId(msgId)
|
||||
.publicKeyVer(1)
|
||||
.encryptRandomKey("mock-random-key-" + currentSeq)
|
||||
.encryptChatMsg("mock-chat-msg-" + currentSeq)
|
||||
.build());
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decryptData(String encryptKey, String encryptChatMsg) {
|
||||
log.info("[MockFinanceSdk] decryptData encryptKey={}, msg={}", encryptKey, encryptChatMsg);
|
||||
try {
|
||||
// 模拟解密:直接返回一个结构化的 JSON 明文
|
||||
Map<String, Object> payload = Map.of(
|
||||
"msgid", extractSeq(encryptChatMsg),
|
||||
"action", "send",
|
||||
"from", "MockUser",
|
||||
"tolist", List.of("ExternalUser"),
|
||||
"msgtype", "text",
|
||||
"text", Map.of("content", "模拟会话存档消息:" + encryptChatMsg),
|
||||
"msgtime", System.currentTimeMillis()
|
||||
);
|
||||
return OBJECT_MAPPER.writeValueAsString(payload);
|
||||
} catch (Exception e) {
|
||||
log.warn("[MockFinanceSdk] decryptData failed", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String extractSeq(String encryptChatMsg) {
|
||||
if (encryptChatMsg == null) {
|
||||
return "mock-msg";
|
||||
}
|
||||
int idx = encryptChatMsg.lastIndexOf('-');
|
||||
return idx > 0 ? "mock-msg-" + encryptChatMsg.substring(idx + 1) : encryptChatMsg;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.sino.mci.channel.wecom.archive;
|
||||
|
||||
import com.sino.mci.channel.wecom.archive.model.FinanceMessage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 企业微信会话存档 Finance SDK 原生 JNI 客户端(预留实现)。
|
||||
*
|
||||
* <p>该实现基于官方 C 库 {@code com.tencent.wework.Finance},通过 JNI 调用:</p>
|
||||
* <ul>
|
||||
* <li>{@code Finance.NewSdk()} / {@code Finance.DestroySdk(long)}</li>
|
||||
* <li>{@code Finance.GetChatData(long, long, long, String, String, int, long)}</li>
|
||||
* <li>{@code Finance.DecryptData(long, String, String, long)}</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>注意:官方 SDK 为 C 库 + JNI,不提供 Maven 坐标,也不应把 {@code .so/.dll} 二进制提交到仓库。
|
||||
* 本期仅保留骨架;接入真实 JNI 时,请在运行环境放置对应平台的动态库,并在启动参数中配置
|
||||
* {@code -Djava.library.path} 或通过 {@link System#loadLibrary(String)} 加载。</p>
|
||||
*/
|
||||
@Slf4j
|
||||
public class NativeFinanceSdkClient implements FinanceSdkClient {
|
||||
|
||||
@Override
|
||||
public boolean init(String corpId, String secret) {
|
||||
throw new UnsupportedOperationException(
|
||||
"NativeFinanceSdkClient 尚未接入 JNI;请先放置官方 .so/.dll 并完成 JNI 绑定");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FinanceMessage> getChatData(long seq, int limit) {
|
||||
throw new UnsupportedOperationException(
|
||||
"NativeFinanceSdkClient 尚未接入 JNI;请先放置官方 .so/.dll 并完成 JNI 绑定");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decryptData(String encryptKey, String encryptChatMsg) {
|
||||
throw new UnsupportedOperationException(
|
||||
"NativeFinanceSdkClient 尚未接入 JNI;请先放置官方 .so/.dll 并完成 JNI 绑定");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.sino.mci.channel.wecom.archive.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 企业微信会话存档单条消息(GetChatData 返回的加密消息结构)。
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class FinanceMessage {
|
||||
|
||||
/**
|
||||
* 消息序号,单调递增,用于断点续拉。
|
||||
*/
|
||||
private long seq;
|
||||
|
||||
/**
|
||||
* 消息唯一 ID。
|
||||
*/
|
||||
private String msgId;
|
||||
|
||||
/**
|
||||
* 公钥版本。
|
||||
*/
|
||||
private int publicKeyVer;
|
||||
|
||||
/**
|
||||
* 经过 RSA 加密的随机密钥,需用企业私钥解密后得到消息密钥。
|
||||
*/
|
||||
private String encryptRandomKey;
|
||||
|
||||
/**
|
||||
* 经过 AES 加密的消息体,需用消息密钥解密。
|
||||
*/
|
||||
private String encryptChatMsg;
|
||||
}
|
||||
Reference in New Issue
Block a user