Houjie
2025-07-03 c46e5e406ced41073eced2cd7a825ea8d84a58bb
Merge remote-tracking branch 'origin/master'
已添加11个文件
已修改1个文件
1020 ■■■■■ 文件已修改
lxzn-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
lxzn-module-system/lxzn-system-biz/src/main/java/org/jeecg/modules/qywx/message/MessageAPI.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
lxzn-module-system/lxzn-system-biz/src/main/java/org/jeecg/modules/qywx/message/controller/WeComCallbackController.java 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
lxzn-module-system/lxzn-system-biz/src/main/java/org/jeecg/modules/qywx/message/utils/AesException.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
lxzn-module-system/lxzn-system-biz/src/main/java/org/jeecg/modules/qywx/message/utils/ByteGroup.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
lxzn-module-system/lxzn-system-biz/src/main/java/org/jeecg/modules/qywx/message/utils/PKCS7Encoder.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
lxzn-module-system/lxzn-system-biz/src/main/java/org/jeecg/modules/qywx/message/utils/SHA1.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
lxzn-module-system/lxzn-system-biz/src/main/java/org/jeecg/modules/qywx/message/utils/Sample.java 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
lxzn-module-system/lxzn-system-biz/src/main/java/org/jeecg/modules/qywx/message/utils/WXBizMsgCrypt.java 289 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
lxzn-module-system/lxzn-system-biz/src/main/java/org/jeecg/modules/qywx/message/utils/XMLParse.java 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
lxzn-module-system/lxzn-system-biz/src/main/java/org/jeecg/modules/qywx/message/vo/TemplateCard.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
lxzn-module-system/lxzn-system-biz/src/main/java/org/jeecg/modules/qywx/message/vo/TemplateCardEntity.java 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
lxzn-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java
@@ -163,6 +163,8 @@
        //测试模块排除
        filterChainDefinitionMap.put("/test/seata/**", "anon");
        //企业微信消息回调接口
        filterChainDefinitionMap.put("/qywx/message/callback/**", "anon");
        // æ·»åŠ è‡ªå·±çš„è¿‡æ»¤å™¨å¹¶ä¸”å–åä¸ºjwt
lxzn-module-system/lxzn-system-biz/src/main/java/org/jeecg/modules/qywx/message/MessageAPI.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package org.jeecg.modules.qywx.message;
import com.alibaba.fastjson.JSONObject;
import com.jeecg.qywx.api.core.util.HttpUtil;
import org.jeecg.modules.qywx.message.vo.TemplateCard;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MessageAPI {
    private static final Logger logger = LoggerFactory.getLogger(MessageAPI.class);
    //发送消息(post)
    static String message_send_url="https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=ACCESS_TOKEN";
    public static JSONObject sendTemplateCardMessage(TemplateCard templateCard, String accessToken) {
        logger.info("[MessageAPI] sendTemplateCardMessage params:accessToken:{},templateCard:{}", new Object[]{accessToken, templateCard});
        // æ‹¼è£…发送信息的url
        String url = message_send_url.replace("ACCESS_TOKEN", accessToken);
        // å°†ä¿¡æ¯å¯¹è±¡è½¬æ¢æˆjson字符串
        String params = JSONObject.toJSONString(templateCard);
        logger.info("[MessageAPI] sendTemplateCardMessage params:jsonText:{}", new Object[]{params});
        // è°ƒç”¨æŽ¥å£å‘送信息
        JSONObject jsonObject = HttpUtil.sendPost(url, params);
        logger.info("[MessageAPI] sendTemplateCardMessage response:{}", new Object[]{jsonObject.toJSONString()});
        return jsonObject;
    }
}
lxzn-module-system/lxzn-system-biz/src/main/java/org/jeecg/modules/qywx/message/controller/WeComCallbackController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,136 @@
package org.jeecg.modules.qywx.message.controller;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.jeecg.modules.qywx.message.utils.WXBizMsgCrypt;
import org.springframework.web.bind.annotation.*;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
 * ä¼ä¸šå¾®ä¿¡æ¶ˆæ¯å›žè°ƒæŽ¥å£ï¼ˆæ”¯æŒ URL éªŒè¯ + æ¶ˆæ¯æŽ¥æ”¶ï¼‰
 */
@RestController
@RequestMapping("/qywx/message/callback")
@Slf4j
public class WeComCallbackController {
    // æ›¿æ¢ä¸ºä½ çš„应用配置
    private static final String TOKEN = "bPGpQimYTtjAfAj2uTE4fb";
    private static final String AES_KEY = "hkbscaEtxWAydMIoWe8EW4drpHXaYroQPKBMoKCs2bf"; // EncodingAESKey
    private static final String CORP_ID = "ww5999b2643c95fa75"; // ä¼ä¸š ID æˆ– AppId
    private static WXBizMsgCrypt wxBizMsgCrypt;
    static {
        try {
            wxBizMsgCrypt = new WXBizMsgCrypt(TOKEN, AES_KEY, CORP_ID);
        } catch (Exception e) {
            throw new RuntimeException("初始化 WXBizMsgCrypt å¤±è´¥ï¼Œè¯·æ£€æŸ¥ Token/AESKey/AppId æ˜¯å¦æ­£ç¡®", e);
        }
    }
    /**
     * GET è¯·æ±‚:用于 URL éªŒè¯ï¼ˆToken æ ¡éªŒï¼‰
     */
    @GetMapping(produces = "text/plain;charset=utf-8")
    public String verifyUrl(
            @RequestParam("msg_signature") String msgSignature,
            @RequestParam("timestamp") String timestamp,
            @RequestParam("nonce") String nonce,
            @RequestParam("echostr") String echostr) {
        try {
            log.info("收到企业微信 URL éªŒè¯è¯·æ±‚:msg_signature={}, timestamp={}, nonce={}, echostr={}", msgSignature, timestamp, nonce, echostr);
            // è°ƒç”¨å®˜æ–¹ SDK éªŒè¯å¹¶è§£å¯†
            String decryptedEchoStr = wxBizMsgCrypt.VerifyURL(msgSignature, timestamp, nonce, echostr);
            log.info("解密后的 echostr: {}", decryptedEchoStr);
            // è¿”回解密后的内容(企业微信要求必须原样返回)
            return decryptedEchoStr;
        } catch (Exception e) {
            log.error("URL éªŒè¯å¤±è´¥", e);
            return "fail";
        }
    }
    /**
     * POST è¯·æ±‚:接收企业微信推送的消息
     */
    @PostMapping(produces = "text/xml;charset=utf-8")
    public String handleWeComCallback(@RequestBody String xmlData,
                                      @RequestParam("msg_signature") String msgSignature,
                                      @RequestParam("timestamp") String timestamp,
                                      @RequestParam("nonce") String nonce) {
        try {
            log.info("收到企业微信消息推送:msg_signature={}, timestamp={}, nonce={}", msgSignature, timestamp, nonce);
            log.info("原始 XML æ•°æ®: {}", xmlData);
            // Step 1: è§£å¯†æ¶ˆæ¯
            String plainXml = wxBizMsgCrypt.DecryptMsg(msgSignature, timestamp, nonce, xmlData);
            log.info("解密后的明文消息: {}", plainXml);
            // Step 2: è§£æž XML å¹¶å¤„理业务逻辑
            Map<String, String> msgMap = parseXmlToMap(plainXml);
            String eventType = msgMap.get("Event");
            String content = msgMap.get("Content");
            log.info("事件类型: {}, æ¶ˆæ¯å†…容: {}", eventType, content);
            // Step 3: ä¸šåŠ¡å¤„ç†ï¼ˆç¤ºä¾‹ï¼šå›žå¤ success)
            String responseXml = "<xml><returncode>0</returncode><returndata>success</returndata></xml>";
            return responseXml;
        } catch (Exception e) {
            log.error("处理企业微信消息失败", e);
            return "<xml><returncode>1</returncode><returndata>fail</returndata></xml>";
        }
    }
    /**
     * å°† XML è½¬ä¸º Map
     */
    private Map<String, String> parseXmlToMap(String xml) throws Exception {
        Map<String, String> map = new HashMap<>();
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        StringReader reader = new StringReader(xml);
        InputSource source = new InputSource(reader);
        Document doc = builder.parse(source);
        Element root = doc.getDocumentElement();
        NodeList list = root.getChildNodes();
        for (int i = 0; i < list.getLength(); i++) {
            if (list.item(i).getNodeType() == Document.ELEMENT_NODE) {
                Element element = (Element) list.item(i);
                map.put(element.getNodeName(), element.getTextContent());
            }
        }
        return map;
    }
    //public static void main(String[] args) throws UnsupportedEncodingException {
    //    String msg_signature = "eae930cbae5d988ed29f8aac6cf221016f81669e";
    //    String timestamp = "1751356163";
    //    String nonce = "16ljuxd4gd8";
    //    String echostr = "Ax30h%2BharWss%2FeNNQl8x4KdzggrpmLFH8i%2F0mHqVGbcKcq1NeascovZV%2B08ooq5o0ng8RJ2WPyo69A8oGhKpxA%3D%3D";
    //
    //    String decode = URLDecoder.decode(echostr, "UTF-8");
    //    //System.out.println(decode);
    //    WeComCallbackController weComCallbackController = new WeComCallbackController();
    //    String decryptedEchoStr = weComCallbackController.verifyUrl(msg_signature, timestamp, nonce, decode);
    //    System.out.println(decryptedEchoStr);
    //}
}
lxzn-module-system/lxzn-system-biz/src/main/java/org/jeecg/modules/qywx/message/utils/AesException.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
package org.jeecg.modules.qywx.message.utils;
@SuppressWarnings("serial")
public class AesException extends Exception {
    public final static int OK = 0;
    public final static int ValidateSignatureError = -40001;
    public final static int ParseXmlError = -40002;
    public final static int ComputeSignatureError = -40003;
    public final static int IllegalAesKey = -40004;
    public final static int ValidateCorpidError = -40005;
    public final static int EncryptAESError = -40006;
    public final static int DecryptAESError = -40007;
    public final static int IllegalBuffer = -40008;
    //public final static int EncodeBase64Error = -40009;
    //public final static int DecodeBase64Error = -40010;
    //public final static int GenReturnXmlError = -40011;
    private int code;
    private static String getMessage(int code) {
        switch (code) {
        case ValidateSignatureError:
            return "签名验证错误";
        case ParseXmlError:
            return "xml解析失败";
        case ComputeSignatureError:
            return "sha加密生成签名失败";
        case IllegalAesKey:
            return "SymmetricKey非法";
        case ValidateCorpidError:
            return "corpid校验失败";
        case EncryptAESError:
            return "aes加密失败";
        case DecryptAESError:
            return "aes解密失败";
        case IllegalBuffer:
            return "解密后得到的buffer非法";
//        case EncodeBase64Error:
//            return "base64加密错误";
//        case DecodeBase64Error:
//            return "base64解密错误";
//        case GenReturnXmlError:
//            return "xml生成失败";
        default:
            return null; // cannot be
        }
    }
    public int getCode() {
        return code;
    }
    AesException(int code) {
        super(getMessage(code));
        this.code = code;
    }
}
lxzn-module-system/lxzn-system-biz/src/main/java/org/jeecg/modules/qywx/message/utils/ByteGroup.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package org.jeecg.modules.qywx.message.utils;
import java.util.ArrayList;
class ByteGroup {
    ArrayList<Byte> byteContainer = new ArrayList<Byte>();
    public byte[] toBytes() {
        byte[] bytes = new byte[byteContainer.size()];
        for (int i = 0; i < byteContainer.size(); i++) {
            bytes[i] = byteContainer.get(i);
        }
        return bytes;
    }
    public ByteGroup addBytes(byte[] bytes) {
        for (byte b : bytes) {
            byteContainer.add(b);
        }
        return this;
    }
    public int size() {
        return byteContainer.size();
    }
}
lxzn-module-system/lxzn-system-biz/src/main/java/org/jeecg/modules/qywx/message/utils/PKCS7Encoder.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,67 @@
/**
 * å¯¹ä¼ä¸šå¾®ä¿¡å‘送给企业后台的消息加解密示例代码.
 *
 * @copyright Copyright (c) 1998-2014 Tencent Inc.
 */
// ------------------------------------------------------------------------
package org.jeecg.modules.qywx.message.utils;
import java.nio.charset.Charset;
import java.util.Arrays;
/**
 * æä¾›åŸºäºŽPKCS7算法的加解密接口.
 */
class PKCS7Encoder {
    static Charset CHARSET = Charset.forName("utf-8");
    static int BLOCK_SIZE = 32;
    /**
     * èŽ·å¾—å¯¹æ˜Žæ–‡è¿›è¡Œè¡¥ä½å¡«å……çš„å­—èŠ‚.
     *
     * @param count éœ€è¦è¿›è¡Œå¡«å……补位操作的明文字节个数
     * @return è¡¥é½ç”¨çš„字节数组
     */
    static byte[] encode(int count) {
        // è®¡ç®—需要填充的位数
        int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE);
        if (amountToPad == 0) {
            amountToPad = BLOCK_SIZE;
        }
        // èŽ·å¾—è¡¥ä½æ‰€ç”¨çš„å­—ç¬¦
        char padChr = chr(amountToPad);
        String tmp = new String();
        for (int index = 0; index < amountToPad; index++) {
            tmp += padChr;
        }
        return tmp.getBytes(CHARSET);
    }
    /**
     * åˆ é™¤è§£å¯†åŽæ˜Žæ–‡çš„补位字符
     *
     * @param decrypted è§£å¯†åŽçš„æ˜Žæ–‡
     * @return åˆ é™¤è¡¥ä½å­—符后的明文
     */
    static byte[] decode(byte[] decrypted) {
        int pad = (int) decrypted[decrypted.length - 1];
        if (pad < 1 || pad > 32) {
            pad = 0;
        }
        return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
    }
    /**
     * å°†æ•°å­—转化成ASCII码对应的字符,用于对明文进行补码
     *
     * @param a éœ€è¦è½¬åŒ–的数字
     * @return è½¬åŒ–得到的字符
     */
    static char chr(int a) {
        byte target = (byte) (a & 0xFF);
        return (char) target;
    }
}
lxzn-module-system/lxzn-system-biz/src/main/java/org/jeecg/modules/qywx/message/utils/SHA1.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
/**
 * å¯¹ä¼ä¸šå¾®ä¿¡å‘送给企业后台的消息加解密示例代码.
 *
 * @copyright Copyright (c) 1998-2014 Tencent Inc.
 */
// ------------------------------------------------------------------------
package org.jeecg.modules.qywx.message.utils;
import java.security.MessageDigest;
import java.util.Arrays;
/**
 * SHA1 class
 *
 * è®¡ç®—消息签名接口.
 */
class SHA1 {
    /**
     * ç”¨SHA1算法生成安全签名
     * @param token ç¥¨æ®
     * @param timestamp æ—¶é—´æˆ³
     * @param nonce éšæœºå­—符串
     * @param encrypt å¯†æ–‡
     * @return å®‰å…¨ç­¾å
     * @throws AesException
     */
    public static String getSHA1(String token, String timestamp, String nonce, String encrypt) throws AesException
              {
        try {
            String[] array = new String[] { token, timestamp, nonce, encrypt };
            StringBuffer sb = new StringBuffer();
            // å­—符串排序
            Arrays.sort(array);
            for (int i = 0; i < 4; i++) {
                sb.append(array[i]);
            }
            String str = sb.toString();
            // SHA1签名生成
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            md.update(str.getBytes());
            byte[] digest = md.digest();
            StringBuffer hexstr = new StringBuffer();
            String shaHex = "";
            for (int i = 0; i < digest.length; i++) {
                shaHex = Integer.toHexString(digest[i] & 0xFF);
                if (shaHex.length() < 2) {
                    hexstr.append(0);
                }
                hexstr.append(shaHex);
            }
            return hexstr.toString();
        } catch (Exception e) {
            e.printStackTrace();
            throw new AesException(AesException.ComputeSignatureError);
        }
    }
}
lxzn-module-system/lxzn-system-biz/src/main/java/org/jeecg/modules/qywx/message/utils/Sample.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,136 @@
package org.jeecg.modules.qywx.message.utils;
import java.io.StringReader;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
public class Sample {
    public static void main(String[] args) throws Exception {
        String sToken = "QDG6eK";
        String sCorpID = "wx5823bf96d3bd56c7";
        String sEncodingAESKey = "jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C";
        WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(sToken, sEncodingAESKey, sCorpID);
        /*
        ------------使用示例一:验证回调URL---------------
        *企业开启回调模式时,企业微信会向验证url发送一个get请求
        å‡è®¾ç‚¹å‡»éªŒè¯æ—¶ï¼Œä¼ä¸šæ”¶åˆ°ç±»ä¼¼è¯·æ±‚:
        * GET /cgi-bin/wxpush?msg_signature=5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3&timestamp=1409659589&nonce=263014780&echostr=P9nAzCzyDtyTWESHep1vC5X9xho%2FqYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp%2B4RPcs8TgAE7OaBO%2BFZXvnaqQ%3D%3D
        * HTTP/1.1 Host: qy.weixin.qq.com
        æŽ¥æ”¶åˆ°è¯¥è¯·æ±‚时,企业应        1.解析出Get请求的参数,包括消息体签名(msg_signature),时间戳(timestamp),随机数字串(nonce)以及企业微信推送过来的随机加密字符串(echostr),
        è¿™ä¸€æ­¥æ³¨æ„ä½œURL解码。
        2.验证消息体签名的正确性
        3. è§£å¯†å‡ºechostr原文,将原文当作Get请求的response,返回给企业微信
        ç¬¬2,3步可以用企业微信提供的库函数VerifyURL来实现。
        */
        // è§£æžå‡ºurl上的参数值如下:
        // String sVerifyMsgSig = HttpUtils.ParseUrl("msg_signature");
        String sVerifyMsgSig = "5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3";
        // String sVerifyTimeStamp = HttpUtils.ParseUrl("timestamp");
        String sVerifyTimeStamp = "1409659589";
        // String sVerifyNonce = HttpUtils.ParseUrl("nonce");
        String sVerifyNonce = "263014780";
        // String sVerifyEchoStr = HttpUtils.ParseUrl("echostr");
        String sVerifyEchoStr = "P9nAzCzyDtyTWESHep1vC5X9xho/qYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp+4RPcs8TgAE7OaBO+FZXvnaqQ==";
        String sEchoStr; //需要返回的明文
        try {
            sEchoStr = wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp,
                    sVerifyNonce, sVerifyEchoStr);
            System.out.println("verifyurl echostr: " + sEchoStr);
            // éªŒè¯URL成功,将sEchoStr返回
            // HttpUtils.SetResponse(sEchoStr);
        } catch (Exception e) {
            //验证URL失败,错误原因请查看异常
            e.printStackTrace();
        }
        /*
        ------------使用示例二:对用户回复的消息解密---------------
        ç”¨æˆ·å›žå¤æ¶ˆæ¯æˆ–者点击事件响应时,企业会收到回调消息,此消息是经过企业微信加密之后的密文以post形式发送给企业,密文格式请参考官方文档
        å‡è®¾ä¼ä¸šæ”¶åˆ°ä¼ä¸šå¾®ä¿¡çš„回调消息如下:
        POST /cgi-bin/wxpush? msg_signature=477715d11cdb4164915debcba66cb864d751f3e6&timestamp=1409659813&nonce=1372623149 HTTP/1.1
        Host: qy.weixin.qq.com
        Content-Length: 613
        <xml>        <ToUserName><![CDATA[wx5823bf96d3bd56c7]]></ToUserName><Encrypt><![CDATA[RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q==]]></Encrypt>
        <AgentID><![CDATA[218]]></AgentID>
        </xml>
        ä¼ä¸šæ”¶åˆ°post请求之后应该        1.解析出url上的参数,包括消息体签名(msg_signature),时间戳(timestamp)以及随机数字串(nonce)
        2.验证消息体签名的正确性。
        3.将post请求的数据进行xml解析,并将<Encrypt>标签的内容进行解密,解密出来的明文即是用户回复消息的明文,明文格式请参考官方文档
        ç¬¬2,3步可以用企业微信提供的库函数DecryptMsg来实现。
        */
        // String sReqMsgSig = HttpUtils.ParseUrl("msg_signature");
        String sReqMsgSig = "477715d11cdb4164915debcba66cb864d751f3e6";
        // String sReqTimeStamp = HttpUtils.ParseUrl("timestamp");
        String sReqTimeStamp = "1409659813";
        // String sReqNonce = HttpUtils.ParseUrl("nonce");
        String sReqNonce = "1372623149";
        // post请求的密文数据
        // sReqData = HttpUtils.PostData();
        String sReqData = "<xml><ToUserName><![CDATA[wx5823bf96d3bd56c7]]></ToUserName><Encrypt><![CDATA[RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q==]]></Encrypt><AgentID><![CDATA[218]]></AgentID></xml>";
        try {
            String sMsg = wxcpt.DecryptMsg(sReqMsgSig, sReqTimeStamp, sReqNonce, sReqData);
            System.out.println("after decrypt msg: " + sMsg);
            // TODO: è§£æžå‡ºæ˜Žæ–‡xml标签的内容进行处理
            // For example:
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            DocumentBuilder db = dbf.newDocumentBuilder();
            StringReader sr = new StringReader(sMsg);
            InputSource is = new InputSource(sr);
            Document document = db.parse(is);
            Element root = document.getDocumentElement();
            NodeList nodelist1 = root.getElementsByTagName("Content");
            String Content = nodelist1.item(0).getTextContent();
            System.out.println("Content:" + Content);
        } catch (Exception e) {
            // TODO
            // è§£å¯†å¤±è´¥ï¼Œå¤±è´¥åŽŸå› è¯·æŸ¥çœ‹å¼‚å¸¸
            e.printStackTrace();
        }
    /*
        ------------使用示例三:企业回复用户消息的加密---------------
        ä¼ä¸šè¢«åŠ¨å›žå¤ç”¨æˆ·çš„æ¶ˆæ¯ä¹Ÿéœ€è¦è¿›è¡ŒåŠ å¯†ï¼Œå¹¶ä¸”æ‹¼æŽ¥æˆå¯†æ–‡æ ¼å¼çš„xml串。
        å‡è®¾ä¼ä¸šéœ€è¦å›žå¤ç”¨æˆ·çš„æ˜Žæ–‡å¦‚下:
        <xml>
        <ToUserName><![CDATA[mycreate]]></ToUserName>
        <FromUserName><![CDATA[wx5823bf96d3bd56c7]]></FromUserName>
        <CreateTime>1348831860</CreateTime>
        <MsgType><![CDATA[text]]></MsgType>
        <Content><![CDATA[this is a test]]></Content>
        <MsgId>1234567890123456</MsgId>
        <AgentID>128</AgentID>
        </xml>
        ä¸ºäº†å°†æ­¤æ®µæ˜Žæ–‡å›žå¤ç»™ç”¨æˆ·ï¼Œä¼ä¸šåº”:            1.自己生成时间时间戳(timestamp),随机数字串(nonce)以便生成消息体签名,也可以直接用从企业微信的post url上解析出的对应值。
        2.将明文加密得到密文。    3.用密文,步骤1生成的timestamp,nonce和企业在企业微信设定的token生成消息体签名。            4.将密文,消息体签名,时间戳,随机数字串拼接成xml格式的字符串,发送给企业。
        ä»¥ä¸Š2,3,4步可以用企业微信提供的库函数EncryptMsg来实现。
        */
        String sRespData = "<xml><ToUserName><![CDATA[mycreate]]></ToUserName><FromUserName><![CDATA[wx5823bf96d3bd56c7]]></FromUserName><CreateTime>1348831860</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[this is a test]]></Content><MsgId>1234567890123456</MsgId><AgentID>128</AgentID></xml>";
        try{
            String sEncryptMsg = wxcpt.EncryptMsg(sRespData, sReqTimeStamp, sReqNonce);
            System.out.println("after encrypt sEncrytMsg: " + sEncryptMsg);
            // åŠ å¯†æˆåŠŸ
            // TODO:
            // HttpUtils.SetResponse(sEncryptMsg);
        }
        catch(Exception e)
        {
            e.printStackTrace();
            // åŠ å¯†å¤±è´¥
        }
    }
}
lxzn-module-system/lxzn-system-biz/src/main/java/org/jeecg/modules/qywx/message/utils/WXBizMsgCrypt.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,289 @@
/**
 * å¯¹ä¼ä¸šå¾®ä¿¡å‘送给企业后台的消息加解密示例代码.
 *
 * @copyright Copyright (c) 1998-2014 Tencent Inc.
 */
// ------------------------------------------------------------------------
/**
 * é’ˆå¯¹org.apache.commons.codec.binary.Base64,
 * éœ€è¦å¯¼å…¥æž¶åŒ…commons-codec-1.9(或commons-codec-1.8等其他版本)
 * å®˜æ–¹ä¸‹è½½åœ°å€ï¼šhttp://commons.apache.org/proper/commons-codec/download_codec.cgi
 */
package org.jeecg.modules.qywx.message.utils;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Random;
/**
 * æä¾›æŽ¥æ”¶å’ŒæŽ¨é€ç»™ä¼ä¸šå¾®ä¿¡æ¶ˆæ¯çš„加解密接口(UTF8编码的字符串).
 * <ol>
 *     <li>第三方回复加密消息给企业微信</li>
 *     <li>第三方收到企业微信发送的消息,验证消息的安全性,并对消息进行解密。</li>
 * </ol>
 * è¯´æ˜Žï¼šå¼‚常java.security.InvalidKeyException:illegal Key Size的解决方案
 * <ol>
 *     <li>在官方网站下载JCE无限制权限策略文件(JDK7的下载地址:
 *      http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html</li>
 *     <li>下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt</li>
 *     <li>如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件</li>
 *     <li>如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件</li>
 * </ol>
 */
public class WXBizMsgCrypt {
    static Charset CHARSET = Charset.forName("utf-8");
    Base64 base64 = new Base64();
    byte[] aesKey;
    String token;
    String receiveid;
    /**
     * æž„造函数
     * @param token ä¼ä¸šå¾®ä¿¡åŽå°ï¼Œå¼€å‘者设置的token
     * @param encodingAesKey ä¼ä¸šå¾®ä¿¡åŽå°ï¼Œå¼€å‘者设置的EncodingAESKey
     * @param receiveid, ä¸åŒåœºæ™¯å«ä¹‰ä¸åŒï¼Œè¯¦è§æ–‡æ¡£
     *
     * @throws AesException æ‰§è¡Œå¤±è´¥ï¼Œè¯·æŸ¥çœ‹è¯¥å¼‚常的错误码和具体的错误信息
     */
    public WXBizMsgCrypt(String token, String encodingAesKey, String receiveid) throws AesException {
        if (encodingAesKey.length() != 43) {
            throw new AesException(AesException.IllegalAesKey);
        }
        this.token = token;
        this.receiveid = receiveid;
        aesKey = Base64.decodeBase64(encodingAesKey + "=");
    }
    // ç”Ÿæˆ4个字节的网络字节序
    byte[] getNetworkBytesOrder(int sourceNumber) {
        byte[] orderBytes = new byte[4];
        orderBytes[3] = (byte) (sourceNumber & 0xFF);
        orderBytes[2] = (byte) (sourceNumber >> 8 & 0xFF);
        orderBytes[1] = (byte) (sourceNumber >> 16 & 0xFF);
        orderBytes[0] = (byte) (sourceNumber >> 24 & 0xFF);
        return orderBytes;
    }
    // è¿˜åŽŸ4个字节的网络字节序
    int recoverNetworkBytesOrder(byte[] orderBytes) {
        int sourceNumber = 0;
        for (int i = 0; i < 4; i++) {
            sourceNumber <<= 8;
            sourceNumber |= orderBytes[i] & 0xff;
        }
        return sourceNumber;
    }
    // éšæœºç”Ÿæˆ16位字符串
    String getRandomStr() {
        String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        Random random = new Random();
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < 16; i++) {
            int number = random.nextInt(base.length());
            sb.append(base.charAt(number));
        }
        return sb.toString();
    }
    /**
     * å¯¹æ˜Žæ–‡è¿›è¡ŒåР坆.
     *
     * @param text éœ€è¦åŠ å¯†çš„æ˜Žæ–‡
     * @return åŠ å¯†åŽbase64编码的字符串
     * @throws AesException aes加密失败
     */
    String encrypt(String randomStr, String text) throws AesException {
        ByteGroup byteCollector = new ByteGroup();
        byte[] randomStrBytes = randomStr.getBytes(CHARSET);
        byte[] textBytes = text.getBytes(CHARSET);
        byte[] networkBytesOrder = getNetworkBytesOrder(textBytes.length);
        byte[] receiveidBytes = receiveid.getBytes(CHARSET);
        // randomStr + networkBytesOrder + text + receiveid
        byteCollector.addBytes(randomStrBytes);
        byteCollector.addBytes(networkBytesOrder);
        byteCollector.addBytes(textBytes);
        byteCollector.addBytes(receiveidBytes);
        // ... + pad: ä½¿ç”¨è‡ªå®šä¹‰çš„填充方式对明文进行补位填充
        byte[] padBytes = PKCS7Encoder.encode(byteCollector.size());
        byteCollector.addBytes(padBytes);
        // èŽ·å¾—æœ€ç»ˆçš„å­—èŠ‚æµ, æœªåР坆
        byte[] unencrypted = byteCollector.toBytes();
        try {
            // è®¾ç½®åŠ å¯†æ¨¡å¼ä¸ºAES的CBC模式
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
            IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
            cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
            // åР坆
            byte[] encrypted = cipher.doFinal(unencrypted);
            // ä½¿ç”¨BASE64对加密后的字符串进行编码
            String base64Encrypted = base64.encodeToString(encrypted);
            return base64Encrypted;
        } catch (Exception e) {
            e.printStackTrace();
            throw new AesException(AesException.EncryptAESError);
        }
    }
    /**
     * å¯¹å¯†æ–‡è¿›è¡Œè§£å¯†.
     *
     * @param text éœ€è¦è§£å¯†çš„密文
     * @return è§£å¯†å¾—到的明文
     * @throws AesException aes解密失败
     */
    String decrypt(String text) throws AesException {
        byte[] original;
        try {
            // è®¾ç½®è§£å¯†æ¨¡å¼ä¸ºAES的CBC模式
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            SecretKeySpec key_spec = new SecretKeySpec(aesKey, "AES");
            IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
            //IvParameterSpec iv = new IvParameterSpec(new byte[16]); // å›ºå®š IV
            cipher.init(Cipher.DECRYPT_MODE, key_spec, iv);
            // ä½¿ç”¨BASE64对密文进行解码
            byte[] encrypted = Base64.decodeBase64(text);
            // è§£å¯†
            original = cipher.doFinal(encrypted);
        } catch (Exception e) {
            e.printStackTrace();
            throw new AesException(AesException.DecryptAESError);
        }
        String xmlContent, from_receiveid;
        try {
            // åŽ»é™¤è¡¥ä½å­—ç¬¦
            byte[] bytes = PKCS7Encoder.decode(original);
            // åˆ†ç¦»16位随机字符串,网络字节序和receiveid
            byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
            int xmlLength = recoverNetworkBytesOrder(networkOrder);
            xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);
            from_receiveid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length),
                    CHARSET);
        } catch (Exception e) {
            e.printStackTrace();
            throw new AesException(AesException.IllegalBuffer);
        }
        // receiveid不相同的情况
        if (!from_receiveid.equals(receiveid)) {
            throw new AesException(AesException.ValidateCorpidError);
        }
        return xmlContent;
    }
    /**
     * å°†ä¼ä¸šå¾®ä¿¡å›žå¤ç”¨æˆ·çš„æ¶ˆæ¯åŠ å¯†æ‰“åŒ….
     * <ol>
     *     <li>对要发送的消息进行AES-CBC加密</li>
     *     <li>生成安全签名</li>
     *     <li>将消息密文和安全签名打包成xml格式</li>
     * </ol>
     *
     * @param replyMsg ä¼ä¸šå¾®ä¿¡å¾…回复用户的消息,xml格式的字符串
     * @param timeStamp æ—¶é—´æˆ³ï¼Œå¯ä»¥è‡ªå·±ç”Ÿæˆï¼Œä¹Ÿå¯ä»¥ç”¨URL参数的timestamp
     * @param nonce éšæœºä¸²ï¼Œå¯ä»¥è‡ªå·±ç”Ÿæˆï¼Œä¹Ÿå¯ä»¥ç”¨URL参数的nonce
     *
     * @return åŠ å¯†åŽçš„å¯ä»¥ç›´æŽ¥å›žå¤ç”¨æˆ·çš„å¯†æ–‡ï¼ŒåŒ…æ‹¬msg_signature, timestamp, nonce, encrypt的xml格式的字符串
     * @throws AesException æ‰§è¡Œå¤±è´¥ï¼Œè¯·æŸ¥çœ‹è¯¥å¼‚常的错误码和具体的错误信息
     */
    public String EncryptMsg(String replyMsg, String timeStamp, String nonce) throws AesException {
        // åР坆
        String encrypt = encrypt(getRandomStr(), replyMsg);
        // ç”Ÿæˆå®‰å…¨ç­¾å
        if (timeStamp == "") {
            timeStamp = Long.toString(System.currentTimeMillis());
        }
        String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt);
        // System.out.println("发送给平台的签名是: " + signature[1].toString());
        // ç”Ÿæˆå‘送的xml
        String result = XMLParse.generate(encrypt, signature, timeStamp, nonce);
        return result;
    }
    /**
     * æ£€éªŒæ¶ˆæ¯çš„真实性,并且获取解密后的明文.
     * <ol>
     *     <li>利用收到的密文生成安全签名,进行签名验证</li>
     *     <li>若验证通过,则提取xml中的加密消息</li>
     *     <li>对消息进行解密</li>
     * </ol>
     *
     * @param msgSignature ç­¾åä¸²ï¼Œå¯¹åº”URL参数的msg_signature
     * @param timeStamp æ—¶é—´æˆ³ï¼Œå¯¹åº”URL参数的timestamp
     * @param nonce éšæœºä¸²ï¼Œå¯¹åº”URL参数的nonce
     * @param postData å¯†æ–‡ï¼Œå¯¹åº”POST请求的数据
     *
     * @return è§£å¯†åŽçš„原文
     * @throws AesException æ‰§è¡Œå¤±è´¥ï¼Œè¯·æŸ¥çœ‹è¯¥å¼‚常的错误码和具体的错误信息
     */
    public String DecryptMsg(String msgSignature, String timeStamp, String nonce, String postData)
            throws AesException {
        // å¯†é’¥ï¼Œå…¬ä¼—账号的app secret
        // æå–密文
        Object[] encrypt = XMLParse.extract(postData);
        // éªŒè¯å®‰å…¨ç­¾å
        String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt[1].toString());
        // å’ŒURL中的签名比较是否相等
        // System.out.println("第三方收到URL中的签名:" + msg_sign);
        // System.out.println("第三方校验签名:" + signature);
        if (!signature.equals(msgSignature)) {
            throw new AesException(AesException.ValidateSignatureError);
        }
        // è§£å¯†
        String result = decrypt(encrypt[1].toString());
        return result;
    }
    /**
     * éªŒè¯URL
     * @param msgSignature ç­¾åä¸²ï¼Œå¯¹åº”URL参数的msg_signature
     * @param timeStamp æ—¶é—´æˆ³ï¼Œå¯¹åº”URL参数的timestamp
     * @param nonce éšæœºä¸²ï¼Œå¯¹åº”URL参数的nonce
     * @param echoStr éšæœºä¸²ï¼Œå¯¹åº”URL参数的echostr
     *
     * @return è§£å¯†ä¹‹åŽçš„echostr
     * @throws AesException æ‰§è¡Œå¤±è´¥ï¼Œè¯·æŸ¥çœ‹è¯¥å¼‚常的错误码和具体的错误信息
     */
    public String VerifyURL(String msgSignature, String timeStamp, String nonce, String echoStr)
            throws AesException {
        String signature = SHA1.getSHA1(token, timeStamp, nonce, echoStr);
        if (!signature.equals(msgSignature)) {
            throw new AesException(AesException.ValidateSignatureError);
        }
        String result = decrypt(echoStr);
        return result;
    }
}
lxzn-module-system/lxzn-system-biz/src/main/java/org/jeecg/modules/qywx/message/utils/XMLParse.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,104 @@
/**
 * å¯¹ä¼ä¸šå¾®ä¿¡å‘送给企业后台的消息加解密示例代码.
 *
 * @copyright Copyright (c) 1998-2014 Tencent Inc.
 */
// ------------------------------------------------------------------------
package org.jeecg.modules.qywx.message.utils;
import java.io.StringReader;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
/**
 * XMLParse class
 *
 * æä¾›æå–消息格式中的密文及生成回复消息格式的接口.
 */
class XMLParse {
    /**
     * æå–出xml数据包中的加密消息
     * @param xmltext å¾…提取的xml字符串
     * @return æå–出的加密消息字符串
     * @throws AesException
     */
    public static Object[] extract(String xmltext) throws AesException     {
        Object[] result = new Object[3];
        try {
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            String FEATURE = null;
            // This is the PRIMARY defense. If DTDs (doctypes) are disallowed, almost all XML entity attacks are prevented
            // Xerces 2 only - http://xerces.apache.org/xerces2-j/features.html#disallow-doctype-decl
            FEATURE = "http://apache.org/xml/features/disallow-doctype-decl";
            dbf.setFeature(FEATURE, true);
            // If you can't completely disable DTDs, then at least do the following:
            // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-general-entities
            // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-general-entities
            // JDK7+ - http://xml.org/sax/features/external-general-entities
            FEATURE = "http://xml.org/sax/features/external-general-entities";
            dbf.setFeature(FEATURE, false);
            // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-parameter-entities
            // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-parameter-entities
            // JDK7+ - http://xml.org/sax/features/external-parameter-entities
            FEATURE = "http://xml.org/sax/features/external-parameter-entities";
            dbf.setFeature(FEATURE, false);
            // Disable external DTDs as well
            FEATURE = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
            dbf.setFeature(FEATURE, false);
            // and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks"
            dbf.setXIncludeAware(false);
            dbf.setExpandEntityReferences(false);
            // And, per Timothy Morgan: "If for some reason support for inline DOCTYPEs are a requirement, then
            // ensure the entity settings are disabled (as shown above) and beware that SSRF attacks
            // (http://cwe.mitre.org/data/definitions/918.html) and denial
            // of service attacks (such as billion laughs or decompression bombs via "jar:") are a risk."
            // remaining parser logic
            DocumentBuilder db = dbf.newDocumentBuilder();
            StringReader sr = new StringReader(xmltext);
            InputSource is = new InputSource(sr);
            Document document = db.parse(is);
            Element root = document.getDocumentElement();
            NodeList nodelist1 = root.getElementsByTagName("Encrypt");
            result[0] = 0;
            result[1] = nodelist1.item(0).getTextContent();
            return result;
        } catch (Exception e) {
            e.printStackTrace();
            throw new AesException(AesException.ParseXmlError);
        }
    }
    /**
     * ç”Ÿæˆxml消息
     * @param encrypt åŠ å¯†åŽçš„æ¶ˆæ¯å¯†æ–‡
     * @param signature å®‰å…¨ç­¾å
     * @param timestamp æ—¶é—´æˆ³
     * @param nonce éšæœºå­—符串
     * @return ç”Ÿæˆçš„xml字符串
     */
    public static String generate(String encrypt, String signature, String timestamp, String nonce) {
        String format = "<xml>\n" + "<Encrypt><![CDATA[%1$s]]></Encrypt>\n"
                + "<MsgSignature><![CDATA[%2$s]]></MsgSignature>\n"
                + "<TimeStamp>%3$s</TimeStamp>\n" + "<Nonce><![CDATA[%4$s]]></Nonce>\n" + "</xml>";
        return String.format(format, encrypt, signature, timestamp, nonce);
    }
}
lxzn-module-system/lxzn-system-biz/src/main/java/org/jeecg/modules/qywx/message/vo/TemplateCard.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package org.jeecg.modules.qywx.message.vo;
import lombok.Data;
/**
 * ä¼ä¸šå¾®ä¿¡æ¨¡æ¿å¡ç‰‡æ¶ˆæ¯ï¼ˆæ–‡æœ¬é€šçŸ¥åž‹ï¼‰
 */
@Data
public class TemplateCard {
    private String touser;//成员ID列表(消息接收者,多个接收者用‘|’分隔,最多支持1000个)。特殊情况:指定为@all,则向关注该企业应用的全部成员发送
    private String toparty;//部门ID列表,多个接收者用‘|’分隔,最多支持100个。当touser为@all时忽略本参
    private String totag;//标签ID列表,多个接收者用‘|’分隔,最多支持100个。当touser为@all时忽略本参数
    private int agentid;//企业应用的id,整型。可在应用的设置页面查看
    private TemplateCardEntity template_card;//消息实体
    private String enable_id_trans;// å¦    è¡¨ç¤ºæ˜¯å¦å¼€å¯id转译,0表示否,1表示是,默认0
    private String enable_duplicate_check;// å¦    è¡¨ç¤ºæ˜¯å¦å¼€å¯é‡å¤æ¶ˆæ¯æ£€æŸ¥ï¼Œ0表示否,1表示是,默认0
    private String duplicate_check_interval;// å¦    è¡¨ç¤ºæ˜¯å¦é‡å¤æ¶ˆæ¯æ£€æŸ¥çš„æ—¶é—´é—´éš”,默认1800s,最大不超过4小时
    private String msgtype = "template_card";//消息类型,此时固定为:template_card
}
lxzn-module-system/lxzn-system-biz/src/main/java/org/jeecg/modules/qywx/message/vo/TemplateCardEntity.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,94 @@
package org.jeecg.modules.qywx.message.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.List;
/**
 * æ¨¡æ¿å¡ç‰‡æ¶ˆæ¯å®žä½“
 */
@Data
public class TemplateCardEntity {
    private Source source;//否    å¡ç‰‡æ¥æºæ ·å¼ä¿¡æ¯ï¼Œä¸éœ€è¦æ¥æºæ ·å¼å¯ä¸å¡«å†™
    private ActionMenu action_menu;//否    å¡ç‰‡å³ä¸Šè§’更多操作按钮
    private String task_id;//否    ä»»åŠ¡id,同一个应用任务id不能重复,只能由数字、字母和“_-@”组成,最长128字节,填了action_menu字段的话本字段必填
    private MainTitle main_title;
    private QuoteArea quote_area;//否    å¼•用文献样式
    private EmphasisContent emphasis_content;
    private String sub_title_text;//二级普通文本,建议不超过160个字,(支持id转译)
    private List<HorizontalContent> horizontal_content_list;
    private List<Jump> jump_list;//否    è·³è½¬æŒ‡å¼•样式的列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过3
    private CardAction card_action;//是    æ•´ä½“卡片的点击跳转事件,text_notice必填本字段
    //图文展示型卡票独有的参数
    private CardImage card_image;
    private String card_type = "text_notice";// æ¨¡æ¿å¡ç‰‡ç±»åž‹ï¼Œæ–‡æœ¬é€šçŸ¥åž‹å¡ç‰‡å¡«å†™ "text_notice"
    @Data
    public static class Source {
        private String icon_url;//否    æ¥æºå›¾ç‰‡çš„url,来源图片的尺寸建议为72*72
        private String desc;//否    æ¥æºå›¾ç‰‡çš„æè¿°ï¼Œå»ºè®®ä¸è¶…过20个字,(支持id转译)
        private String desc_color;//否    æ¥æºæ–‡å­—的颜色,目前支持:0(默认) ç°è‰²ï¼Œ1 é»‘色,2 çº¢è‰²ï¼Œ3 ç»¿è‰²
    }
    @Data
    public static class ActionMenu {
        private String desc;//否    æ›´å¤šæ“ä½œç•Œé¢çš„æè¿°
        private List<ActionList> action_list;//是    æ“ä½œåˆ—表,列表长度取值范围为 [1, 3]
        @Data
        @AllArgsConstructor
        public static class ActionList {
            private String key;//是    æ“ä½œkey值,用户点击后,会产生回调事件将本参数作为EventKey返回,回调事件会带上该key值,最长支持1024字节,不可重复
            private String text;//是    æ“ä½œçš„æè¿°æ–‡æ¡ˆ
        }
    }
    @Data
    public static class MainTitle {
        private String title;//否    ä¸€çº§æ ‡é¢˜ï¼Œå»ºè®®ä¸è¶…过36个字,文本通知型卡片本字段非必填,但不可本字段和sub_title_text都不填,(支持id转译)
        private String desc;//否    æ ‡é¢˜è¾…助信息,建议不超过44个字,(支持id转译)
    }
    @Data
    private class QuoteArea {
        private int type;//否    å¼•用文献样式区域点击事件,0或不填代表没有点击事件,1 ä»£è¡¨è·³è½¬url,2 ä»£è¡¨è·³è½¬å°ç¨‹åº
        private String url;//否    ç‚¹å‡»è·³è½¬çš„url,quote_area.type是1时必填
        private String title;//否    å¼•用文献样式的标题
        private String quote_text;//否    å¼•用文献样式的引用文案
    }
    @Data
    private class EmphasisContent {
        private String title;//否    å…³é”®æ•°æ®æ ·å¼çš„æ•°æ®å†…容,建议不超过14个字
        private String desc;//否    å…³é”®æ•°æ®æ ·å¼çš„æ•°æ®æè¿°å†…容,建议不超过22个字
    }
    @Data
    public static class HorizontalContent {
        private String keyname;//是    äºŒçº§æ ‡é¢˜ï¼Œå»ºè®®ä¸è¶…过5个字
        private String value;//否    äºŒçº§æ–‡æœ¬ï¼Œå¦‚æžœhorizontal_content_list.type是2,该字段代表文件名称(要包含文件类型),建议不超过30个字,(支持id转译)
    }
    @Data
    @AllArgsConstructor
    public static class Jump {
        private int type;//否    è·³è½¬é“¾æŽ¥ç±»åž‹ï¼Œ0或不填代表不是链接,1 ä»£è¡¨è·³è½¬url,2 ä»£è¡¨è·³è½¬å°ç¨‹åº
        private String title;//是    è·³è½¬é“¾æŽ¥æ ·å¼çš„æ–‡æ¡ˆå†…容,建议不超过18个字
        private String url;//否    è·³è½¬é“¾æŽ¥çš„url,jump_list.type是1时必填
    }
    @Data
    public static class CardAction {
        private int type;//是    è·³è½¬äº‹ä»¶ç±»åž‹ï¼Œ1 ä»£è¡¨è·³è½¬url,2 ä»£è¡¨æ‰“开小程序。text_notice卡片模版中该字段取值范围为[1,2]
        private String url;//否    è·³è½¬äº‹ä»¶çš„url,card_action.type是1时必填
        private String appid;//否    è·³è½¬äº‹ä»¶çš„小程序的appid,必须是与当前应用关联的小程序,card_action.type是2时必填
        private String pagepath;//否    è·³è½¬äº‹ä»¶çš„小程序的pagepath,card_action.type是2时选填
    }
    @Data
    public static class CardImage {
        private String url;//是    å›¾ç‰‡çš„url
        private String aspect_ratio;//否    å›¾ç‰‡çš„宽高比,宽高比要小于2.25,大于1.3,不填该参数默认1.3
    }
}