feat(channel-wecom): 实现企微会话存档接收骨架(Finance SDK 接口 + 回调 + 异步拉取)

This commit is contained in:
2026-07-07 19:42:49 +08:00
parent 7edbb0a178
commit 7b16160bba
11 changed files with 845 additions and 1 deletions

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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 绑定");
}
}

View File

@@ -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;
}