diff --git a/pom.xml b/pom.xml index 246cf46..bf58eed 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,15 @@ commons-codec commons-codec + + org.dom4j + dom4j + 2.1.4 + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + diff --git a/src/main/java/com/f8q8/qywechatarchive/common/utils/JsonTools.java b/src/main/java/com/f8q8/qywechatarchive/common/utils/JsonTools.java new file mode 100644 index 0000000..c9446b4 --- /dev/null +++ b/src/main/java/com/f8q8/qywechatarchive/common/utils/JsonTools.java @@ -0,0 +1,143 @@ +package com.f8q8.qywechatarchive.common.utils; + +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import lombok.extern.slf4j.Slf4j; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.json.JsonMapper; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.TimeZone; + +@Slf4j +public class JsonTools { + + private static final JsonMapper DEFAULT_MAPPER = buildDefaultMapper(); + + private static final JsonMapper PRETTY_MAPPER = buildPrettyMapper(); + + private static final XmlMapper XML_MAPPER = buildXmlMapper(); + + private JsonTools() { + } + + public static JsonMapper getDefaultMapper() { + return DEFAULT_MAPPER; + } + + public static JsonMapper getPrettyMapper() { + return PRETTY_MAPPER; + } + + private static JsonMapper buildDefaultMapper() { + return JsonMapper.builder().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .defaultDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")) + .defaultTimeZone(TimeZone.getDefault()).build(); + } + + private static JsonMapper buildPrettyMapper() { + return JsonMapper.builder().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .defaultDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")) + .defaultTimeZone(TimeZone.getDefault()).enable(SerializationFeature.INDENT_OUTPUT).build(); + } + + private static XmlMapper buildXmlMapper() { + return XmlMapper.builder().build(); + } + + // 序列化 + public static String toJson(Object Obj) { + try { + return DEFAULT_MAPPER.writeValueAsString(Obj); + } catch (Exception e) { + log.error(e.getMessage(), e); + throw new RuntimeException("json序列化失败", e); + } + } + + public static String toPrettyJson(Object obj) { + try { + return PRETTY_MAPPER.writeValueAsString(obj); + } catch (Exception e) { + throw new RuntimeException("JSON序列化失败:", e); + } + } + + public static void writeToStream(Object obj, OutputStream outputStream) { + DEFAULT_MAPPER.writeValue(outputStream, obj); + } + + // 反序列化 + public static T fromJson(String json, Class claszz) { + try { + return DEFAULT_MAPPER.readValue(json, claszz); + } catch (Exception e) { + log.error(e.getMessage(), e); + throw new RuntimeException("JSON反序列化失败:", e); + } + } + + public static T fromJson(String json, TypeReference typeRef) { + try { + return DEFAULT_MAPPER.readValue(json, typeRef); + } catch (Exception e) { + throw new RuntimeException("JSON 反序列化失败", e); + } + } + + // 泛型集合的辅助方法 + public static List fromJsonToList(String json, Class elementClass) { + try { + JavaType type = DEFAULT_MAPPER.getTypeFactory().constructCollectionType(List.class, elementClass); + return DEFAULT_MAPPER.readValue(json, type); + } catch (Exception e) { + throw new RuntimeException("JSON 反序列化为List失败", e); + } + } + + public static T fromStream(InputStream in, Class clazz) throws IOException { + return DEFAULT_MAPPER.readValue(in, clazz); + } + + public static T fromStream(InputStream in, TypeReference typeRef) throws IOException { + return DEFAULT_MAPPER.readValue(in, typeRef); + } + + // ---------- JsonNode 操作 ---------- + public static JsonNode toJsonNode(String json) { + try { + return DEFAULT_MAPPER.readTree(json); + } catch (Exception e) { + throw new RuntimeException("转换为 JsonNode 失败", e); + } + } + + public static JsonNode valueToTree(Object obj) { + return DEFAULT_MAPPER.valueToTree(obj); + } + + public static T treeToValue(JsonNode node, Class clazz) { + try { + return DEFAULT_MAPPER.treeToValue(node, clazz); + } catch (Exception e) { + throw new RuntimeException("JsonNode 转换对象失败", e); + } + } + + public static JsonNode parseXml(String xmlText) { + try { + com.fasterxml.jackson.databind.JsonNode jsonNode = XML_MAPPER.readTree(xmlText); + return toJsonNode(jsonNode.toString()); + } catch (Exception e) { + log.error(e.getMessage(), e); + throw new RuntimeException("XML转JSON失败:", e); + } + } +} diff --git a/src/main/java/com/f8q8/qywechatarchive/modules/api/action/CallbackController.java b/src/main/java/com/f8q8/qywechatarchive/modules/api/action/CallbackController.java index 73b9dac..3012217 100644 --- a/src/main/java/com/f8q8/qywechatarchive/modules/api/action/CallbackController.java +++ b/src/main/java/com/f8q8/qywechatarchive/modules/api/action/CallbackController.java @@ -1,19 +1,35 @@ package com.f8q8.qywechatarchive.modules.api.action; -import com.f8q8.qywechatarchive.core.utils.ResultUtils; +import com.f8q8.qywechatarchive.common.utils.JsonTools; +import com.f8q8.qywechatarchive.modules.api.client.WeComSdkClient; +import com.f8q8.qywechatarchive.modules.api.service.WeComSyncService; +import com.f8q8.qywechatarchive.modules.api.utils.AesException; import com.f8q8.qywechatarchive.modules.api.utils.WXBizJsonMsgCrypt; import com.f8q8.qywechatarchive.modules.api.vo.CallbackVO; +import jakarta.annotation.Resource; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; -import org.springframework.util.StringUtils; +import org.springframework.util.StreamUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import tools.jackson.databind.JsonNode; + +import java.nio.charset.StandardCharsets; @Slf4j @RestController @RequestMapping(value = "api/v1/callback") public class CallbackController { + @Resource + WeComSdkClient weComSdkClient; + @Resource + WeComSyncService weComSyncService; + @Value("${wecom.token}") String token; @Value("${wecom.encodingAESKey}") @@ -21,24 +37,62 @@ public class CallbackController { @Value("${wecom.corp-id}") String corpId; - @RequestMapping(value = "notify") - public Object notify(CallbackVO callbackVO) { - log.info("企业微信回调数据:{}", callbackVO); - if (StringUtils.hasText(callbackVO.getEchostr())) { - // 下面逻辑用于校验 - return verifyRequest(callbackVO); - } else { - // 下面逻辑用于消息解密 - return ResultUtils.success(); + private static WXBizJsonMsgCrypt wxcpt; + + private WXBizJsonMsgCrypt getWxCpt() { + if (wxcpt == null) { + try { + wxcpt = new WXBizJsonMsgCrypt(token, encodingAESKey, corpId); + } catch (AesException e) { + log.error(e.getMessage(), e); + throw new RuntimeException(e); + } } + return wxcpt; + } + + + @GetMapping(value = "notify") + public Object verify(CallbackVO callbackVO) { + log.info("企业微信验证数据:{}", callbackVO); + return verifyRequest(callbackVO); + } + + @PostMapping(value = "notify") + public Object notify(HttpServletRequest request) { + try { + // 1. 读取 URL 上的签名参数 + String msgSignature = request.getParameter("msg_signature"); + String timestamp = request.getParameter("timestamp"); + String nonce = request.getParameter("nonce"); + + // 2. 读取请求体中的 XML + ServletInputStream inputStream = request.getInputStream(); + String postData = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); + // msg_signature=cb7349c730ce79a415d977e7de3fcee7da709597, timestamp=1779089871, nonce=1778976713, body= + log.info("收到回调消息: msg_signature={}, timestamp={}, nonce={}, body={}", + msgSignature, timestamp, nonce, postData); + String message = getWxCpt().DecryptMsg(msgSignature, timestamp, nonce, postData); + // 判断是否是消息通知,如果是,拉取数据 + JsonNode jsonNode = JsonTools.parseXml(message); + if (jsonNode != null && jsonNode.has("Event") && "msgaudit_notify".equals(jsonNode.get("Event").asString())) { + // 是会话消息事件,拉取消息内容 + int count = weComSyncService.syncMessages(corpId); + log.info("同步了{}条消息", count); + } + log.info(message); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + + return null; } private String verifyRequest(CallbackVO callbackVO) { String result = null; if (verifyParam(callbackVO)) { try { - WXBizJsonMsgCrypt wxcpt = new WXBizJsonMsgCrypt(token, encodingAESKey, corpId); - result = wxcpt.VerifyURL(callbackVO.getMsg_signature(), String.valueOf(callbackVO.getTimestamp()), + result = getWxCpt().VerifyURL(callbackVO.getMsg_signature(), String.valueOf(callbackVO.getTimestamp()), callbackVO.getNonce(), callbackVO.getEchostr()); } catch (Exception e) { log.error(e.getMessage(), e); diff --git a/src/main/java/com/f8q8/qywechatarchive/modules/api/utils/JsonParse.java b/src/main/java/com/f8q8/qywechatarchive/modules/api/utils/JsonParse.java index 96df378..18f0b7f 100644 --- a/src/main/java/com/f8q8/qywechatarchive/modules/api/utils/JsonParse.java +++ b/src/main/java/com/f8q8/qywechatarchive/modules/api/utils/JsonParse.java @@ -15,46 +15,51 @@ package com.f8q8.qywechatarchive.modules.api.utils; */ +import com.f8q8.qywechatarchive.common.utils.JsonTools; +import lombok.extern.slf4j.Slf4j; +import tools.jackson.databind.JsonNode; + /** * JsonParse class - * + *

* 提供提取消息格式中的密文及生成回复消息格式的接口. */ +@Slf4j class JsonParse { /** * 提取出 JSON 包中的加密消息 + * * @param jsontext 待提取的json字符串 * @return 提取出的加密消息字符串 * @throws AesException */ - public static Object[] extract(String jsontext) throws AesException { + public static Object[] extract(String jsonText) throws AesException { Object[] result = new Object[3]; - /* - try { - JSONObject json = new JSONObject(jsontext); - String encrypt_msg = json.getString("encrypt"); - String tousername = json.getString("tousername"); - String agentid = json.getString("agentid"); - result[0] = tousername; - result[1] = encrypt_msg; - result[2] = agentid; - return result; - } catch (Exception e) { - e.printStackTrace(); - throw new AesException(AesException.ParseJsonError); - } - */ - return null; + try { + JsonNode json = JsonTools.toJsonNode(jsonText); + String encrypt_msg = json.get("encrypt").asString(); + String tousername = json.get("tousername").asString(); + String agentid = json.get("agentid").asString(); + + result[0] = tousername; + result[1] = encrypt_msg; + result[2] = agentid; + return result; + } catch (Exception e) { + log.error(e.getMessage(), e); + throw new AesException(AesException.ParseJsonError); + } } /** * 生成json消息 - * @param encrypt 加密后的消息密文 + * + * @param encrypt 加密后的消息密文 * @param signature 安全签名 * @param timestamp 时间戳 - * @param nonce 随机字符串 + * @param nonce 随机字符串 * @return 生成的json字符串 */ public static String generate(String encrypt, String signature, String timestamp, String nonce) { diff --git a/src/main/java/com/f8q8/qywechatarchive/modules/api/utils/WXBizJsonMsgCrypt.java b/src/main/java/com/f8q8/qywechatarchive/modules/api/utils/WXBizJsonMsgCrypt.java index 38dbbec..75d7986 100644 --- a/src/main/java/com/f8q8/qywechatarchive/modules/api/utils/WXBizJsonMsgCrypt.java +++ b/src/main/java/com/f8q8/qywechatarchive/modules/api/utils/WXBizJsonMsgCrypt.java @@ -13,7 +13,9 @@ */ package com.f8q8.qywechatarchive.modules.api.utils; +import com.f8q8.qywechatarchive.common.utils.JsonTools; import org.apache.commons.codec.binary.Base64; +import tools.jackson.databind.JsonNode; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; @@ -46,10 +48,10 @@ public class WXBizJsonMsgCrypt { /** * 构造函数 - * @param token 企业微信后台,开发者设置的token - * @param encodingAesKey 企业微信后台,开发者设置的EncodingAESKey - * @param receiveid, 不同场景含义不同,详见文档 * + * @param token 企业微信后台,开发者设置的token + * @param encodingAesKey 企业微信后台,开发者设置的EncodingAESKey + * @param receiveid, 不同场景含义不同,详见文档 * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息 */ public WXBizJsonMsgCrypt(String token, String encodingAesKey, String receiveid) throws AesException { @@ -201,10 +203,9 @@ public class WXBizJsonMsgCrypt { *

  • 将消息密文和安全签名打包成json格式
  • * * - * @param replyMsg 企业微信待回复用户的消息,json格式的字符串 + * @param replyMsg 企业微信待回复用户的消息,json格式的字符串 * @param timeStamp 时间戳,可以自己生成,也可以用URL参数的timestamp - * @param nonce 随机串,可以自己生成,也可以用URL参数的nonce - * + * @param nonce 随机串,可以自己生成,也可以用URL参数的nonce * @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的json格式的字符串 * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息 */ @@ -234,10 +235,9 @@ public class WXBizJsonMsgCrypt { * * * @param msgSignature 签名串,对应URL参数的msg_signature - * @param timeStamp 时间戳,对应URL参数的timestamp - * @param nonce 随机串,对应URL参数的nonce - * @param postData 密文,对应POST请求的数据 - * + * @param timeStamp 时间戳,对应URL参数的timestamp + * @param nonce 随机串,对应URL参数的nonce + * @param postData 密文,对应POST请求的数据 * @return 解密后的原文 * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息 */ @@ -246,10 +246,10 @@ public class WXBizJsonMsgCrypt { // 密钥,公众账号的app secret // 提取密文 - Object[] encrypt = JsonParse.extract(postData); + JsonNode encrypt = JsonTools.parseXml(postData); // 验证安全签名 - String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt[1].toString()); + String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt.get("Encrypt").asString()); // 和URL中的签名比较是否相等 // System.out.println("第三方收到URL中的签名:" + msg_sign); @@ -257,19 +257,17 @@ public class WXBizJsonMsgCrypt { if (!signature.equals(msgSignature)) { throw new AesException(AesException.ValidateSignatureError); } - // 解密 - String result = decrypt(encrypt[1].toString()); - return result; + return decrypt(encrypt.get("Encrypt").asString()); } /** * 验证URL - * @param msgSignature 签名串,对应URL参数的msg_signature - * @param timeStamp 时间戳,对应URL参数的timestamp - * @param nonce 随机串,对应URL参数的nonce - * @param echoStr 随机串,对应URL参数的echostr * + * @param msgSignature 签名串,对应URL参数的msg_signature + * @param timeStamp 时间戳,对应URL参数的timestamp + * @param nonce 随机串,对应URL参数的nonce + * @param echoStr 随机串,对应URL参数的echostr * @return 解密之后的echostr * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息 */ @@ -281,8 +279,7 @@ public class WXBizJsonMsgCrypt { throw new AesException(AesException.ValidateSignatureError); } - String result = decrypt(echoStr); - return result; + return decrypt(echoStr); } } \ No newline at end of file diff --git a/src/main/java/com/f8q8/qywechatarchive/modules/api/utils/XmlTool.java b/src/main/java/com/f8q8/qywechatarchive/modules/api/utils/XmlTool.java new file mode 100644 index 0000000..8ad2bbd --- /dev/null +++ b/src/main/java/com/f8q8/qywechatarchive/modules/api/utils/XmlTool.java @@ -0,0 +1,30 @@ +package com.f8q8.qywechatarchive.modules.api.utils; + +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Document; +import org.dom4j.DocumentHelper; +import org.dom4j.Element; + +import java.util.List; + +@Slf4j +public class XmlTool { + + public static Object[] extract(String xmlText) throws AesException { + Object[] result = null; + try { + Document document = DocumentHelper.parseText(xmlText); + Element root = document.getRootElement(); + List elements = root.elements(); + result = new Object[elements.size()]; + for (int i = 0; i < elements.size(); i++) { + Element element = elements.get(i); + result[i] = element.attributeValue("name"); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return result; + } + +} diff --git a/src/main/java/com/tencent/wework/Finance.java b/src/main/java/com/tencent/wework/Finance.java index 004c0a8..2ef0da7 100644 --- a/src/main/java/com/tencent/wework/Finance.java +++ b/src/main/java/com/tencent/wework/Finance.java @@ -16,6 +16,9 @@ typedef struct MediaData { */ public class Finance { + + private static final Object SDK_LOCK = new Object(); + public native static long NewSdk(); /** diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index cfbff71..1483b47 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -1,8 +1,8 @@ spring: datasource: - url: jdbc:mysql://frps.zxl78585.com:3306/qy_wechat_archive?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false - username: qyWechat - password: qyWechat@2026Test + url: jdbc:mysql://frps.zxl78585.com:3306/qy_wechat_archive?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true + username: wecom + password: wecom@2026Prod hikari: maximum-pool-size: 20 minimum-idle: 2 @@ -25,7 +25,7 @@ wecom: # 同步间隔(秒) interval: 300 # 每次拉取条数 - batch-size: 500 + batch-size: 50 logging: file: name: d:/logs/wecomArchive/wecomArchive.log diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 415fcf9..d804d63 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -1,6 +1,6 @@ spring: datasource: - url: jdbc:mysql://127.0.0.1:3306/wecom?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false + url: jdbc:mysql://127.0.0.1:3306/wecom?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: wecom password: wecom@2026Prod hikari: diff --git a/src/main/resources/native/win/WeWorkFinanceSdk.dll b/src/main/resources/native/win/WeWorkFinanceSdk.dll index 4ce7286..8bf13a1 100644 Binary files a/src/main/resources/native/win/WeWorkFinanceSdk.dll and b/src/main/resources/native/win/WeWorkFinanceSdk.dll differ diff --git a/src/main/resources/native/win/libcrypto-1_1-x64.dll b/src/main/resources/native/win/libcrypto-1_1-x64.dll new file mode 100644 index 0000000..809b633 Binary files /dev/null and b/src/main/resources/native/win/libcrypto-1_1-x64.dll differ diff --git a/src/main/resources/native/win/libcrypto-3-x64.dll b/src/main/resources/native/win/libcrypto-3-x64.dll deleted file mode 100644 index b1a6dbe..0000000 Binary files a/src/main/resources/native/win/libcrypto-3-x64.dll and /dev/null differ diff --git a/src/main/resources/native/win/libcurl-x64.dll b/src/main/resources/native/win/libcurl-x64.dll index 8b79e41..e5c54cd 100644 Binary files a/src/main/resources/native/win/libcurl-x64.dll and b/src/main/resources/native/win/libcurl-x64.dll differ diff --git a/src/main/resources/native/win/libssl-1_1-x64.dll b/src/main/resources/native/win/libssl-1_1-x64.dll new file mode 100644 index 0000000..832b725 Binary files /dev/null and b/src/main/resources/native/win/libssl-1_1-x64.dll differ diff --git a/src/test/java/com/f8q8/qywechatarchive/XmlTest.java b/src/test/java/com/f8q8/qywechatarchive/XmlTest.java new file mode 100644 index 0000000..9899a0c --- /dev/null +++ b/src/test/java/com/f8q8/qywechatarchive/XmlTest.java @@ -0,0 +1,33 @@ +package com.f8q8.qywechatarchive; + +import com.f8q8.qywechatarchive.common.utils.JsonTools; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Document; +import org.dom4j.DocumentHelper; +import org.dom4j.Element; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import tools.jackson.databind.JsonNode; + +@Slf4j +@SpringBootTest +public class XmlTest { + + @Test + public void testXml() { + String xml = ""; + try { + JsonNode json = JsonTools.parseXml(xml); + log.info(json.toPrettyString()); + Document document = DocumentHelper.parseText(xml); + Element root = document.getRootElement(); + for (Element element : root.elements()) { + log.info(element.getName()); + log.info(element.getTextTrim()); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + + } +}