348 changed files with 118137 additions and 47139 deletions
@ -1,8 +1,8 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project version="4"> |
|||
<component name="Encoding"> |
|||
<component name="Encoding" native2AsciiForPropertiesFiles="true" defaultCharsetForPropertiesFiles="UTF-8"> |
|||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" /> |
|||
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" /> |
|||
<file url="file://$PROJECT_DIR$/src/main/resources/templates/pages/scanQrCode/scanQrCode2.html" charset="UTF-8" /> |
|||
<file url="PROJECT" charset="UTF-8" /> |
|||
</component> |
|||
</project> |
|||
@ -0,0 +1,78 @@ |
|||
package com.dreamchaser.depository_manage.config; |
|||
|
|||
import org.apache.commons.codec.digest.DigestUtils; |
|||
import sun.misc.BASE64Decoder; |
|||
import sun.misc.BASE64Encoder; |
|||
|
|||
import javax.crypto.Cipher; |
|||
import javax.crypto.SecretKey; |
|||
import javax.crypto.spec.SecretKeySpec; |
|||
|
|||
|
|||
/** |
|||
* 用于3DES加密解密 |
|||
*/ |
|||
public class JM_3DES { |
|||
|
|||
public static String JM_Key = "scanQrCode"; |
|||
/** |
|||
* 获取key |
|||
* @param key |
|||
* @return |
|||
*/ |
|||
public static byte[] hex(String key){ |
|||
String f = DigestUtils.md5Hex(key); |
|||
byte[] bkeys = new String(f).getBytes(); |
|||
byte[] enk = new byte[24]; |
|||
for (int i=0;i<24;i++){ |
|||
enk[i] = bkeys[i]; |
|||
} |
|||
return enk; |
|||
} |
|||
|
|||
/** |
|||
* 3DES加密 |
|||
* @param key 密钥 |
|||
* @param srcStr 需要加密的字符串 |
|||
* @return |
|||
*/ |
|||
public static String encode3Des(String key, String srcStr){ |
|||
byte[] keybyte = hex(key); |
|||
byte[] src = srcStr.getBytes(); |
|||
try { |
|||
//生成密钥
|
|||
SecretKey deskey = new SecretKeySpec(keybyte, "DESede"); |
|||
//加密
|
|||
Cipher c1 = Cipher.getInstance("DESede"); |
|||
c1.init(Cipher.ENCRYPT_MODE, deskey); |
|||
String pwd = (new BASE64Encoder()).encodeBuffer(c1.doFinal(src)); |
|||
return pwd; |
|||
}catch(Exception e){ |
|||
e.printStackTrace(); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
/** |
|||
* 3DES解密 |
|||
* @param key 加密密钥 |
|||
* @param desStr 需要解密的字符串 |
|||
* @return |
|||
*/ |
|||
public static String decode3Des(String key, String desStr){ |
|||
byte[] keybyte = hex(key); |
|||
try { |
|||
byte[] src = (new BASE64Decoder()).decodeBuffer(desStr); |
|||
//生成密钥
|
|||
SecretKey deskey = new SecretKeySpec(keybyte, "DESede"); |
|||
//解密
|
|||
Cipher c1 = Cipher.getInstance("DESede"); |
|||
c1.init(Cipher.DECRYPT_MODE, deskey); |
|||
String pwd = new String(c1.doFinal(src)); |
|||
return pwd; |
|||
}catch(Exception e){ |
|||
e.printStackTrace(); |
|||
} |
|||
return null; |
|||
} |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
package com.dreamchaser.depository_manage.config; |
|||
|
|||
|
|||
import com.alibaba.fastjson.JSON; |
|||
import com.alibaba.fastjson.JSONObject; |
|||
import com.dreamchaser.depository_manage.config.QyWx_template_card.*; |
|||
import com.dreamchaser.depository_manage.utils.HttpUtils; |
|||
import com.dreamchaser.depository_manage.utils.ObjectFormatUtil; |
|||
import lombok.Data; |
|||
|
|||
import java.io.UnsupportedEncodingException; |
|||
import java.net.URLEncoder; |
|||
import java.util.ArrayList; |
|||
import java.util.HashMap; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
|
|||
// 用于实现企业微信相关功能
|
|||
|
|||
@Data |
|||
public class QyWxConfig { |
|||
public static String corpid = "ww02f310301953277a"; // 企业的CorpID
|
|||
public static String secret = "GYwyoAGwMwumAVFrFn-RZIc2q11P3pm8NWY9pWDjLqw"; // 应用的凭证密钥
|
|||
public static int AgentId = 1000037; //应用agentid
|
|||
public static String callBackUrl = "https://jy.hxgk.group/QyWxLogin"; |
|||
public static String token = ""; //access_token
|
|||
public static String code = ""; //userCode
|
|||
public static String sendMessage_url = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=ACCESS_TOKEN"; |
|||
|
|||
// 用于回调配置的token
|
|||
public static String sToken = "sM4MFE44fAKdtqvq81HYygqmrdUn"; |
|||
// 用于回调配置的EncodingAESKey
|
|||
public static String sEncodingAESKey = "10cruMoq3ixrQQngJcMN6CzOYrHWmHMpxp2Xn5iYrsk"; |
|||
|
|||
// 用于获取企业微信对应token
|
|||
public static String GetQYWXToken(){ |
|||
String url = String.format(" https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s",QyWxConfig.corpid,QyWxConfig.secret); |
|||
String get = HttpUtils.doGet(url); |
|||
JSONObject jsonObject = JSONObject.parseObject(get); |
|||
Integer errcord = ObjectFormatUtil.toInteger(jsonObject.get("errcode")); |
|||
String accessToken = (String) jsonObject.get("access_token"); |
|||
String errmsg = (String) jsonObject.get("errmsg"); |
|||
if(errcord == 0){ |
|||
// 如果成功获取access_token
|
|||
return accessToken; |
|||
}else{ |
|||
// 否则返回空值
|
|||
return "visitToFail:"+errmsg; |
|||
} |
|||
} |
|||
|
|||
|
|||
// 根据获取到的用户code以及token获取用户id
|
|||
public static JSONObject GetQYWXUserId(){ |
|||
String url = String.format("https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=%s&code=%s",QyWxConfig.token,QyWxConfig.code); |
|||
String get = HttpUtils.doGet(url); |
|||
JSONObject jsonObject = JSONObject.parseObject(get); |
|||
return jsonObject; |
|||
|
|||
} |
|||
|
|||
// 用于拼接发送链接
|
|||
public static String getQYWXCodeUrl(){ |
|||
String encode = null; |
|||
try { |
|||
encode = URLEncoder.encode(QyWxConfig.callBackUrl, "utf-8"); |
|||
} catch (UnsupportedEncodingException e) { |
|||
e.printStackTrace(); |
|||
} |
|||
String url = String.format("https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_base&agentid=%s#wechat_redirect",QyWxConfig.corpid,encode,QyWxConfig.secret); |
|||
return url; |
|||
} |
|||
|
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
package com.dreamchaser.depository_manage.config.QyWxJMJM.com.qq.weixin.mp.aes; |
|||
|
|||
|
|||
|
|||
@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; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
package com.dreamchaser.depository_manage.config.QyWxJMJM.com.qq.weixin.mp.aes; |
|||
|
|||
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(); |
|||
} |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
/** |
|||
* 对企业微信发送给企业后台的消息加解密示例代码. |
|||
* |
|||
* @copyright Copyright (c) 1998-2014 Tencent Inc. |
|||
*/ |
|||
|
|||
// ------------------------------------------------------------------------
|
|||
|
|||
package com.dreamchaser.depository_manage.config.QyWxJMJM.com.qq.weixin.mp.aes; |
|||
|
|||
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; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
/** |
|||
* 对企业微信发送给企业后台的消息加解密示例代码. |
|||
* |
|||
* @copyright Copyright (c) 1998-2014 Tencent Inc. |
|||
*/ |
|||
|
|||
// ------------------------------------------------------------------------
|
|||
|
|||
package com.dreamchaser.depository_manage.config.QyWxJMJM.com.qq.weixin.mp.aes; |
|||
|
|||
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); |
|||
} |
|||
} |
|||
} |
|||
@ -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 com.dreamchaser.depository_manage.config.QyWxJMJM.com.qq.weixin.mp.aes; |
|||
|
|||
import java.nio.charset.Charset; |
|||
import java.util.Arrays; |
|||
import java.util.Random; |
|||
|
|||
import javax.crypto.Cipher; |
|||
import javax.crypto.spec.IvParameterSpec; |
|||
import javax.crypto.spec.SecretKeySpec; |
|||
|
|||
import org.apache.commons.codec.binary.Base64; |
|||
|
|||
/** |
|||
* 提供接收和推送给企业微信消息的加解密接口(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)); |
|||
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; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,104 @@ |
|||
/** |
|||
* 对企业微信发送给企业后台的消息加解密示例代码. |
|||
* |
|||
* @copyright Copyright (c) 1998-2014 Tencent Inc. |
|||
*/ |
|||
|
|||
// ------------------------------------------------------------------------
|
|||
|
|||
package com.dreamchaser.depository_manage.config.QyWxJMJM.com.qq.weixin.mp.aes; |
|||
|
|||
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); |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
package com.dreamchaser.depository_manage.config.QyWx_template_card; |
|||
|
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* 消息基类(企业号 -> 普通用户) |
|||
* |
|||
*/ |
|||
@Data |
|||
public class BaseMessage { |
|||
// 否 成员ID列表(消息接收者,多个接收者用'|'分隔,最多支持1000个)。特殊情况:指定为@all,则向该企业应用的全部成员发送
|
|||
private String touser; |
|||
// 否 部门ID列表,多个接收者用'|'分隔,最多支持100个。当touser为@all时忽略本参数
|
|||
private String toparty; |
|||
// 否 标签ID列表,多个接收者用'|'分隔,最多支持100个。当touser为@all时忽略本参数
|
|||
private String totag; |
|||
// 是 消息类型
|
|||
private String msgtype; |
|||
// 是 企业应用的id,整型。可在应用的设置页面查看
|
|||
private int agentid; |
|||
// 否 表示是否开启id转译,0表示否,1表示是,默认0
|
|||
private int enable_id_trans; |
|||
// 否 表示是否开启重复消息检查,0表示否,1表示是,默认0
|
|||
private int enable_duplicate_check; |
|||
// 否 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时
|
|||
private int duplicate_check_interval; |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
package com.dreamchaser.depository_manage.config.QyWx_template_card; |
|||
|
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* 按钮交互性 |
|||
* |
|||
*/ |
|||
@Data |
|||
public class ButtonInteraction extends BaseMessage { |
|||
// 模板卡片
|
|||
private TemplateCard_button_interaction template_card; |
|||
// 否 表示是否是保密消息,0表示否,1表示是,默认0
|
|||
private int safe; |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
package com.dreamchaser.depository_manage.config.QyWx_template_card; |
|||
|
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
public class MessageByMarkDown extends BaseMessage { |
|||
private Object markdown; |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
package com.dreamchaser.depository_manage.config.QyWx_template_card; |
|||
|
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* 操作 |
|||
*/ |
|||
@Data |
|||
public class TemplateCard_action { |
|||
/** |
|||
* 操作的描述文案 |
|||
*/ |
|||
private String text; |
|||
/** |
|||
* 操作key值,用户点击后,会产生回调事件将本参数作为EventKey返回,回调事件会带上该key值,最长支持1024字节,不可重复 |
|||
*/ |
|||
private String key; |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
package com.dreamchaser.depository_manage.config.QyWx_template_card; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlRootElement; |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 卡片右上角更多操作按钮 |
|||
*/ |
|||
@XmlRootElement |
|||
@Data |
|||
public class TemplateCard_action_menu { |
|||
/** |
|||
* 更多操作界面的描述 |
|||
*/ |
|||
private String desc; |
|||
|
|||
/** |
|||
* 操作列表,列表长度取值范围为 [1, 3] |
|||
*/ |
|||
private List<TemplateCard_action> action_list; |
|||
|
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
package com.dreamchaser.depository_manage.config.QyWx_template_card; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlRootElement; |
|||
|
|||
/** |
|||
* 按钮 |
|||
*/ |
|||
@XmlRootElement |
|||
@Data |
|||
public class TemplateCard_button { |
|||
/** |
|||
* 按钮点击事件类型,0 或不填代表回调点击事件,1 代表跳转url |
|||
*/ |
|||
private Integer type; |
|||
/** |
|||
* 按钮文案,建议不超过10个字 |
|||
*/ |
|||
private String text; |
|||
/** |
|||
* 按钮样式,目前可填1~4,不填或错填默认1 |
|||
*/ |
|||
private Integer style; |
|||
/** |
|||
* 按钮key值,用户点击后,会产生回调事件将本参数作为EventKey返回,回调事件会带上该key值,最长支持1024字节,不可重复,button_list.type是0时必填 |
|||
*/ |
|||
private String key; |
|||
/** |
|||
* 跳转事件的url,button_list.type是1时必填 |
|||
*/ |
|||
private String url; |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
package com.dreamchaser.depository_manage.config.QyWx_template_card; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlRootElement; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 卡片模板--按钮交互型 |
|||
* |
|||
*/ |
|||
@XmlRootElement |
|||
@Data |
|||
public class TemplateCard_button_interaction { |
|||
/** |
|||
* 模板卡片类型,投票选择型卡片填写"vote_interaction" |
|||
*/ |
|||
|
|||
private String card_type; |
|||
/** |
|||
* 卡片来源样式信息,不需要来源样式可不填写 |
|||
*/ |
|||
private TemplateCard_source source; |
|||
|
|||
/** |
|||
* 卡片右上角更多操作按钮 |
|||
*/ |
|||
private TemplateCard_action_menu action_menu; |
|||
|
|||
/** |
|||
* 一级标题 |
|||
*/ |
|||
private TemplateCard_main_title main_title; |
|||
|
|||
/** |
|||
* 引用文献样式 |
|||
*/ |
|||
private Template_quote_area quote_area; |
|||
|
|||
/** |
|||
* 二级普通文本,建议不超过160个字,(支持id转译) |
|||
*/ |
|||
private String sub_title_text; |
|||
|
|||
|
|||
/** |
|||
* 二级标题+文本列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过6 |
|||
*/ |
|||
private List<TemplateCard_horizontal_content> horizontal_content_list; |
|||
|
|||
|
|||
/** |
|||
* 整体卡片的点击跳转事件 |
|||
*/ |
|||
private TemplateCard_card_action card_action; |
|||
|
|||
/** |
|||
* 任务id,同一个应用任务id不能重复,只能由数字、字母和“_-@”组成,最长128字节 |
|||
*/ |
|||
private String task_id; |
|||
|
|||
|
|||
/** |
|||
* 下拉式的选择器 |
|||
*/ |
|||
private TemplateCard_button_selection button_selection; |
|||
|
|||
/** |
|||
* 按钮列表,列表长度不超过6 |
|||
*/ |
|||
private List<TemplateCard_button> button_list; |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
package com.dreamchaser.depository_manage.config.QyWx_template_card; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlRootElement; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 下拉式的选择器 |
|||
*/ |
|||
@XmlRootElement |
|||
@Data |
|||
public class TemplateCard_button_selection { |
|||
/** |
|||
* 下拉式的选择器的key,用户提交选项后,会产生回调事件,回调事件会带上该key值表示该题,最长支持1024字节 |
|||
*/ |
|||
private String question_key; |
|||
|
|||
/** |
|||
* 下拉式的选择器左边的标题 |
|||
*/ |
|||
private String title; |
|||
|
|||
/** |
|||
* 选项列表,下拉选项不超过 10 个,最少1个 |
|||
*/ |
|||
private List<TemplateCard_button_selection_option> option_list; |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
package com.dreamchaser.depository_manage.config.QyWx_template_card; |
|||
|
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* 下拉选项 |
|||
*/ |
|||
@Data |
|||
public class TemplateCard_button_selection_option { |
|||
/** |
|||
* 下拉式的选择器选项的id,用户提交后,会产生回调事件,回调事件会带上该id值表示该选项,最长支持128字节,不可重复 |
|||
*/ |
|||
private String id; |
|||
/** |
|||
* 下拉式的选择器选项的文案,建议不超过16个字 |
|||
*/ |
|||
private String text; |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
package com.dreamchaser.depository_manage.config.QyWx_template_card; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlRootElement; |
|||
|
|||
|
|||
/** |
|||
* 整体卡片的点击跳转事件 |
|||
*/ |
|||
@XmlRootElement |
|||
@Data |
|||
public class TemplateCard_card_action { |
|||
/** |
|||
* 跳转事件类型,0或不填代表不是链接,1 代表跳转url,2 代表打开小程序 |
|||
*/ |
|||
private Integer type; |
|||
|
|||
/** |
|||
* 跳转事件的url,card_action.type是1时必填 |
|||
*/ |
|||
private String url; |
|||
|
|||
/** |
|||
* 跳转事件的小程序的appid,必须是与当前应用关联的小程序,card_action.type是2时必填 |
|||
*/ |
|||
private Integer appid; |
|||
|
|||
/** |
|||
* 跳转事件的小程序的pagepath,card_action.type是2时选填 |
|||
*/ |
|||
private String pagepath; |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
package com.dreamchaser.depository_manage.config.QyWx_template_card; |
|||
|
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* 关键数据样式 |
|||
*/ |
|||
@Data |
|||
public class TemplateCard_emphasis_content { |
|||
/** |
|||
* 关键数据样式的数据内容,建议不超过14个字 |
|||
*/ |
|||
private String title; |
|||
/** |
|||
* 关键数据样式的数据描述内容,建议不超过22个字 |
|||
*/ |
|||
private String desc; |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
package com.dreamchaser.depository_manage.config.QyWx_template_card; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlRootElement; |
|||
|
|||
/** |
|||
* 二级标题+文本列表 |
|||
*/ |
|||
@XmlRootElement |
|||
@Data |
|||
public class TemplateCard_horizontal_content { |
|||
/** |
|||
* 链接类型,0或不填代表不是链接,1 代表跳转url,2 代表下载附件,3 代表点击跳转成员详情 |
|||
*/ |
|||
private Integer type; |
|||
|
|||
/** |
|||
* 二级标题,建议不超过5个字 |
|||
*/ |
|||
private String keyname; |
|||
|
|||
/** |
|||
* 二级文本,如果horizontal_content_list.type是2,该字段代表文件名称(要包含文件类型),建议不超过30个字,(支持id转译) |
|||
*/ |
|||
private String value; |
|||
|
|||
/** |
|||
* 链接跳转的url,horizontal_content_list.type是1时必填 |
|||
*/ |
|||
private String url; |
|||
|
|||
/** |
|||
* 附件的media_id,horizontal_content_list.type是2时必填 |
|||
*/ |
|||
private Integer media_id; |
|||
|
|||
|
|||
/** |
|||
* 成员详情的userid,horizontal_content_list.type是3时必填 |
|||
*/ |
|||
private String userid; |
|||
|
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
package com.dreamchaser.depository_manage.config.QyWx_template_card; |
|||
|
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* 跳转指引样式 |
|||
*/ |
|||
@Data |
|||
public class TemplateCard_jump { |
|||
/** |
|||
* 跳转链接样式的文案内容,建议不超过18个字 |
|||
*/ |
|||
private String title; |
|||
/** |
|||
* 跳转链接类型,0或不填代表不是链接,1 代表跳转url,2 代表跳转小程序 |
|||
*/ |
|||
private String type; |
|||
/** |
|||
* 跳转链接的url,jump_list.type是1时必填 |
|||
*/ |
|||
private String url; |
|||
/** |
|||
* 跳转链接的小程序的appid,必须是与当前应用关联的小程序,jump_list.type是2时必填 |
|||
*/ |
|||
private String appid; |
|||
/** |
|||
* 跳转链接的小程序的pagepath,jump_list.type是2时选填 |
|||
*/ |
|||
private String pagepath; |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
package com.dreamchaser.depository_manage.config.QyWx_template_card; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlRootElement; |
|||
|
|||
|
|||
/** |
|||
* 一级标题 |
|||
*/ |
|||
@XmlRootElement |
|||
@Data |
|||
public class TemplateCard_main_title { |
|||
/** |
|||
* 一级标题,建议不超过36个字,(支持id转译) |
|||
*/ |
|||
private String title; |
|||
/** |
|||
* 标题辅助信息,建议不超过44个字,(支持id转译) |
|||
*/ |
|||
private String desc; |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
package com.dreamchaser.depository_manage.config.QyWx_template_card; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlRootElement; |
|||
|
|||
/* |
|||
卡片来源样式信息 |
|||
*/ |
|||
@XmlRootElement |
|||
@Data |
|||
public class TemplateCard_source { |
|||
/** |
|||
* 来源图片的url,来源图片的尺寸建议为72*72 |
|||
*/ |
|||
private String icon_url; |
|||
|
|||
/** |
|||
* 来源图片的描述,建议不超过20个字,(支持id转译) |
|||
*/ |
|||
private String desc; |
|||
|
|||
/** |
|||
* 来源文字的颜色,目前支持:0(默认) 灰色,1 黑色,2 红色,3 绿色 |
|||
*/ |
|||
private Integer desc_color; |
|||
|
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
package com.dreamchaser.depository_manage.config.QyWx_template_card; |
|||
|
|||
|
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlRootElement; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 卡片模板--文本通知型 |
|||
*/ |
|||
@XmlRootElement |
|||
@Data |
|||
public class TemplateCard_text_notice { |
|||
/** |
|||
* 模板卡片类型,投票选择型卡片填写"vote_interaction" |
|||
*/ |
|||
|
|||
private String card_type; |
|||
/** |
|||
* 卡片来源样式信息,不需要来源样式可不填写 |
|||
*/ |
|||
private TemplateCard_source source; |
|||
|
|||
/** |
|||
* 卡片右上角更多操作按钮 |
|||
*/ |
|||
private TemplateCard_action_menu action_menu; |
|||
|
|||
/** |
|||
* 一级标题 |
|||
*/ |
|||
private TemplateCard_main_title main_title; |
|||
|
|||
/** |
|||
* 引用文献样式 |
|||
*/ |
|||
private Template_quote_area quote_area; |
|||
|
|||
/** |
|||
* 关键数据样式 |
|||
*/ |
|||
private TemplateCard_emphasis_content emphasis_content; |
|||
|
|||
/** |
|||
* 二级普通文本,建议不超过160个字,(支持id转译) |
|||
*/ |
|||
private String sub_title_text; |
|||
|
|||
|
|||
/** |
|||
* 二级标题+文本列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过6 |
|||
*/ |
|||
private List<TemplateCard_horizontal_content> horizontal_content_list; |
|||
|
|||
/** |
|||
* 跳转指引样式的列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过3 |
|||
*/ |
|||
private List<TemplateCard_jump> jump_list; |
|||
|
|||
|
|||
/** |
|||
* 整体卡片的点击跳转事件 |
|||
*/ |
|||
private TemplateCard_card_action card_action; |
|||
|
|||
/** |
|||
* 任务id,同一个应用任务id不能重复,只能由数字、字母和“_-@”组成,最长128字节 |
|||
*/ |
|||
private String task_id; |
|||
|
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
package com.dreamchaser.depository_manage.config.QyWx_template_card; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlRootElement; |
|||
|
|||
/** |
|||
* 引用文献样式 |
|||
*/ |
|||
@XmlRootElement |
|||
@Data |
|||
public class Template_quote_area { |
|||
/** |
|||
* 引用文献样式区域点击事件,0或不填代表没有点击事件,1 代表跳转url,2 代表跳转小程序 |
|||
*/ |
|||
private Integer type; |
|||
/** |
|||
* 点击跳转的url,quote_area.type是1时必填 |
|||
*/ |
|||
private String url; |
|||
/** |
|||
* 点击跳转的小程序的appid,必须是与当前应用关联的小程序,quote_area.type是2时必填 |
|||
*/ |
|||
private Integer appid; |
|||
/** |
|||
* 点击跳转的小程序的pagepath,quote_area.type是2时选填 |
|||
*/ |
|||
private String pagepath; |
|||
/** |
|||
* 引用文献样式的标题 |
|||
*/ |
|||
private String title; |
|||
/** |
|||
* 引用文献样式的引用文案 |
|||
*/ |
|||
private String quote_text; |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
package com.dreamchaser.depository_manage.config.QyWx_template_card; |
|||
|
|||
|
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* 文本通知型 |
|||
*/ |
|||
|
|||
@Data |
|||
public class TextNotice extends BaseMessage{ |
|||
// 模板卡片
|
|||
private TemplateCard_text_notice template_card; |
|||
// 否 表示是否是保密消息,0表示否,1表示是,默认0
|
|||
private int safe; |
|||
} |
|||
@ -0,0 +1,276 @@ |
|||
package com.dreamchaser.depository_manage.controller; |
|||
|
|||
import cn.hutool.http.HttpUtil; |
|||
import com.alibaba.fastjson.JSONObject; |
|||
import com.dreamchaser.depository_manage.config.PortConfig; |
|||
import com.dreamchaser.depository_manage.config.QyWxConfig; |
|||
import com.dreamchaser.depository_manage.config.QyWxJMJM.com.qq.weixin.mp.aes.AesException; |
|||
import com.dreamchaser.depository_manage.config.QyWxJMJM.com.qq.weixin.mp.aes.WXBizMsgCrypt; |
|||
import com.dreamchaser.depository_manage.entity.CallBackLog; |
|||
import com.dreamchaser.depository_manage.entity.UserByPort; |
|||
import com.dreamchaser.depository_manage.pojo.callBackXml.CallBackBaseXml; |
|||
import com.dreamchaser.depository_manage.pojo.callBackXml.callBackXml_button_templatecard.TemplateCard; |
|||
import com.dreamchaser.depository_manage.security.pool.AuthenticationTokenPool; |
|||
import com.dreamchaser.depository_manage.security.pool.UserKeyAndTokenPool; |
|||
import com.dreamchaser.depository_manage.service.CallBackLogService; |
|||
import com.dreamchaser.depository_manage.service.DepositoryRecordService; |
|||
import com.dreamchaser.depository_manage.service.DepositoryService; |
|||
import com.dreamchaser.depository_manage.service.impl.QyWxOperationService; |
|||
import com.dreamchaser.depository_manage.utils.ObjectFormatUtil; |
|||
import com.dreamchaser.depository_manage.utils.QyWxXMLUtils; |
|||
import io.micrometer.core.instrument.util.IOUtils; |
|||
import org.joda.time.format.FormatUtils; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.http.MediaType; |
|||
import org.springframework.stereotype.Controller; |
|||
import org.springframework.web.bind.annotation.*; |
|||
import org.springframework.web.servlet.ModelAndView; |
|||
|
|||
import javax.servlet.http.HttpServletRequest; |
|||
import javax.servlet.http.HttpServletResponse; |
|||
import javax.servlet.http.HttpSession; |
|||
import java.io.IOException; |
|||
import java.io.InputStream; |
|||
import java.io.PrintWriter; |
|||
import java.time.Instant; |
|||
import java.util.Enumeration; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
|
|||
/** |
|||
* 用于企业微信相关操作的控制器 |
|||
*/ |
|||
@Controller |
|||
public class QyWxOperationController { |
|||
|
|||
|
|||
@Autowired |
|||
CallBackLogService callBackLogService; |
|||
|
|||
|
|||
@Autowired |
|||
QyWxOperationService qyWxOperationService; |
|||
|
|||
|
|||
@Autowired |
|||
DepositoryRecordService depositoryRecordService; |
|||
|
|||
|
|||
/** |
|||
* 用于接收企业微信的回调,get方式 |
|||
*/ |
|||
@GetMapping("/callback") |
|||
public void callBackForGet(@RequestParam Map<String,Object> map, HttpServletResponse response){ |
|||
try { |
|||
// 构造解密对象
|
|||
WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(QyWxConfig.sToken,QyWxConfig.sEncodingAESKey,QyWxConfig.corpid); |
|||
// 企业微信加密签名
|
|||
String sVerifyMsgSig = (String) map.get("msg_signature"); |
|||
// 时间戳
|
|||
Integer sVerifyTimeStamp = ObjectFormatUtil.toInteger(map.get("timestamp")); |
|||
// 随机数
|
|||
String sVerifyNonce = (String) map.get("nonce"); |
|||
// 加密的字符串
|
|||
String sVerifyEchoStr = (String) map.get("echostr"); |
|||
String sEchoStr; //需要返回的明文
|
|||
sEchoStr = wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp.toString(), |
|||
sVerifyNonce, sVerifyEchoStr); |
|||
|
|||
// 添加日志
|
|||
CallBackLog callBackLog = new CallBackLog(); |
|||
callBackLog.setTimestamp(sVerifyTimeStamp); |
|||
callBackLog.setNonce(sVerifyNonce); |
|||
callBackLog.setEchostr(sVerifyEchoStr); |
|||
callBackLogService.addCallBackLog(callBackLog); |
|||
|
|||
//返回明文
|
|||
PrintWriter writer = response.getWriter(); |
|||
writer.println(sEchoStr); |
|||
|
|||
System.out.println(sEchoStr); |
|||
} catch (AesException | IOException e) { |
|||
e.printStackTrace(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 用于接收企业微信的回调,post方式 |
|||
*/ |
|||
@PostMapping("/callback") |
|||
public void callBackForPost(@RequestParam Map<String,Object> param, |
|||
@RequestBody(required = false) Map<String,Object> map, |
|||
HttpServletRequest request,HttpServletResponse response){ |
|||
try { |
|||
|
|||
|
|||
// 构造解密对象
|
|||
WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(QyWxConfig.sToken,QyWxConfig.sEncodingAESKey,QyWxConfig.corpid); |
|||
// 企业微信加密签名
|
|||
String sVerifyMsgSig = (String) param.get("msg_signature"); |
|||
// 时间戳
|
|||
Integer sVerifyTimeStamp = ObjectFormatUtil.toInteger(param.get("timestamp")); |
|||
// 随机数
|
|||
String sVerifyNonce = (String) param.get("nonce"); |
|||
// 加密的字符串
|
|||
String sVerifyEchoStr = (String) param.get("echostr"); |
|||
|
|||
|
|||
if(sVerifyEchoStr != null) { |
|||
// 如果是验证url
|
|||
|
|||
String sEchoStr; //需要返回的明文
|
|||
sEchoStr = wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp.toString(), |
|||
sVerifyNonce, sVerifyEchoStr); |
|||
|
|||
// 添加日志
|
|||
CallBackLog callBackLog = new CallBackLog(); |
|||
callBackLog.setTimestamp(sVerifyTimeStamp); |
|||
callBackLog.setNonce(sVerifyNonce); |
|||
callBackLog.setEchostr(sVerifyEchoStr); |
|||
callBackLogService.addCallBackLog(callBackLog); |
|||
|
|||
//返回明文
|
|||
PrintWriter writer = response.getWriter(); |
|||
writer.println(sEchoStr); |
|||
writer.close(); |
|||
}else{ |
|||
// 如果是响应事件
|
|||
|
|||
String ToUserName = (String) map.get("ToUserName"); |
|||
String Encrypt = (String) map.get("Encrypt"); |
|||
String AgentID = (String) map.get("AgentID"); |
|||
|
|||
// 需要解密的xml
|
|||
String sReqData = String.format("<xml><ToUserName><![CDATA[%s]]></ToUserName>" + |
|||
"<Encrypt><![CDATA[%s]]></Encrypt>" + |
|||
"<AgentID><![CDATA[%s]]></AgentID></xml>",ToUserName,Encrypt,AgentID); |
|||
// 解析后的数据
|
|||
String sMsg = wxcpt.DecryptMsg(sVerifyMsgSig, sVerifyTimeStamp.toString(), sVerifyNonce, sReqData); |
|||
// 将数据转为java对象
|
|||
TemplateCard templateCard = (TemplateCard) QyWxXMLUtils.convertXmlStrToObject(TemplateCard.class, sMsg); |
|||
// 点击用户
|
|||
String fromUserName = templateCard.getFromUserName(); |
|||
// 根据userId获取处理人
|
|||
Map<String, Object> portInfo = PortConfig.findUserByQyWxUserId(fromUserName); |
|||
UserByPort userByPort = (UserByPort) portInfo.get("user"); |
|||
|
|||
// 获取点击的按钮
|
|||
String clickKey = templateCard.getEventKey().split("_")[1]; |
|||
String result = ""; |
|||
if("pass".equals(clickKey)){ |
|||
result = "通过"; |
|||
}else{ |
|||
result = "驳回"; |
|||
} |
|||
// 开启线程处理审批
|
|||
new Thread(new Runnable() { |
|||
@Override |
|||
public void run() { |
|||
depositoryRecordService.reviewByQyWx(templateCard); |
|||
} |
|||
}).start(); |
|||
|
|||
// 开启线程更改其他用户卡片模板样式
|
|||
String finalResult = result; |
|||
new Thread(new Runnable() { |
|||
@Override |
|||
public void run() { |
|||
qyWxOperationService.updateTemplateCard(templateCard.getResponseCode(),userByPort.getName(), finalResult); |
|||
} |
|||
}).start(); |
|||
|
|||
|
|||
// 待加密模板
|
|||
String sRespData = String.format("<xml><ToUserName><![CDATA[%s]]></ToUserName>" + |
|||
"<FromUserName><![CDATA[%s]]></FromUserName>" + |
|||
"<CreateTime>%s</CreateTime>"+ |
|||
"<MsgType><![CDATA[update_button]]></MsgType>"+ |
|||
"<Button>" + |
|||
"<ReplaceName><![CDATA[%s]]></ReplaceName></Button>" + |
|||
"</xml>",ToUserName,QyWxConfig.corpid,templateCard.getCreateTime(),"已"+result); |
|||
// 加密
|
|||
String sEncryptMsg = wxcpt.EncryptMsg(sRespData, sVerifyTimeStamp.toString(), sVerifyNonce); |
|||
//3.响应消息
|
|||
PrintWriter out = response.getWriter(); |
|||
out.print(sEncryptMsg); |
|||
out.close(); |
|||
} |
|||
} catch (AesException | IOException e) { |
|||
e.printStackTrace(); |
|||
} |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 用于企业微信登录 |
|||
* @param code |
|||
* @param action |
|||
* @param state |
|||
* @param request |
|||
* @return |
|||
*/ |
|||
@GetMapping("/QyWxLogin") |
|||
public ModelAndView QyWxLogin(@RequestParam(required = false)String code, |
|||
@RequestParam(required = false)String action, |
|||
@RequestParam(required = false)String state, |
|||
HttpServletRequest request) |
|||
{ |
|||
ModelAndView mv = new ModelAndView(); |
|||
mv.addObject("userWxId",""); |
|||
mv.setViewName("pages/user/login"); |
|||
if(code != null) { |
|||
QyWxConfig.code = code; |
|||
JSONObject jsonObject = QyWxConfig.GetQYWXUserId(); |
|||
Integer errCode = jsonObject.getInteger("errcode"); |
|||
String userId = jsonObject.getString("userid"); |
|||
if (errCode == 0) { |
|||
// 如果成功获取userid
|
|||
Map<String, Object> portInfo = PortConfig.findUserByQyWxUserId(userId); |
|||
UserByPort userByPort =(UserByPort) portInfo.get("user"); |
|||
String key = (String) portInfo.get("key"); |
|||
String token = (String) portInfo.get("token"); |
|||
if (userByPort != null) { |
|||
// 如果数据库中存在该用户
|
|||
String keyAndToken = key + "&" +token; |
|||
// 将key与token暂存至池中保存
|
|||
UserKeyAndTokenPool.addKeyAndToken(userByPort.getNumber(),keyAndToken); |
|||
// 设置放入时间
|
|||
userByPort.setInstant(Instant.now()); |
|||
AuthenticationTokenPool.addToken(token, userByPort); |
|||
HttpSession session = request.getSession(); |
|||
session.setAttribute("token"+userByPort.getId(), token); |
|||
session.setAttribute("userToken",userByPort); |
|||
session.setMaxInactiveInterval(1800); |
|||
mv.addObject("user",userByPort); |
|||
mv.setViewName("index"); |
|||
}else{ |
|||
JSONObject captcha = PageController.Captcha(request); |
|||
String picPath = (String) captcha.get("picPath"); |
|||
String captchaid = (String) captcha.get("captchaid"); |
|||
mv.addObject("picPath", picPath); |
|||
mv.addObject("captchaid", captchaid); |
|||
mv.addObject("userWxId",userId); |
|||
} |
|||
}else{ |
|||
JSONObject captcha = PageController.Captcha(request); |
|||
String picPath = (String) captcha.get("picPath"); |
|||
String captchaid = (String) captcha.get("captchaid"); |
|||
mv.addObject("picPath", picPath); |
|||
mv.addObject("captchaid", captchaid); |
|||
} |
|||
}else{ |
|||
JSONObject captcha = PageController.Captcha(request); |
|||
String picPath = (String) captcha.get("picPath"); |
|||
String captchaid = (String) captcha.get("captchaid"); |
|||
mv.addObject("picPath", picPath); |
|||
mv.addObject("captchaid", captchaid); |
|||
} |
|||
return mv; |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
package com.dreamchaser.depository_manage.entity; |
|||
|
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
public class CallBackLog { |
|||
public Integer id; |
|||
public String msg_signature; |
|||
public Integer timestamp; |
|||
public String nonce; |
|||
public String echostr; |
|||
public String xmlstr; |
|||
public String jsonstr; |
|||
public String reqdata; |
|||
public Integer addtime; |
|||
|
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
package com.dreamchaser.depository_manage.mapper; |
|||
|
|||
|
|||
import com.dreamchaser.depository_manage.entity.CallBackLog; |
|||
import org.apache.ibatis.annotations.Mapper; |
|||
import org.springframework.stereotype.Repository; |
|||
|
|||
import java.util.Map; |
|||
|
|||
@Mapper |
|||
@Repository |
|||
public interface CallBackLogMapper { |
|||
|
|||
/** |
|||
* 添加回调日志 |
|||
* @param map |
|||
* @return |
|||
*/ |
|||
Integer addCallBackLog(Map<String,Object> map); |
|||
|
|||
|
|||
/** |
|||
* 添加回调日志 |
|||
* @param callBackLog |
|||
* @return |
|||
*/ |
|||
Integer addCallBackLog(CallBackLog callBackLog); |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
|||
|
|||
<!-- depository --> |
|||
<mapper namespace="com.dreamchaser.depository_manage.mapper.CallBackLogMapper"> |
|||
<resultMap id="callBackMap" type="com.dreamchaser.depository_manage.entity.CallBackLog"> |
|||
<id column="id" property="id" jdbcType="INTEGER" /> |
|||
<result column="msg_signature" property="msg_signature" jdbcType="VARCHAR" /> |
|||
<result column="timestamp" property="timestamp" jdbcType="INTEGER" /> |
|||
<result column="nonce" property="nonce" jdbcType="VARCHAR" /> |
|||
<result column="echostr" property="echostr" jdbcType="VARCHAR" /> |
|||
<result column="xmlstr" property="xmlstr" jdbcType="VARCHAR" /> |
|||
<result column="jsonstr" property="jsonstr" jdbcType="VARCHAR" /> |
|||
<result column="reqdata" property="reqdata" jdbcType="VARCHAR"/> |
|||
<result column="addtime" property="addtime" jdbcType="INTEGER"/> |
|||
</resultMap> |
|||
|
|||
<insert id="addCallBackLog"> |
|||
|
|||
INSERT INTO callback_log ( |
|||
id, msg_signature, timestamp,nonce,echostr,xmlstr,jsonstr,reqdata,addtime |
|||
) VALUES ( |
|||
#{id}, |
|||
#{msg_signature}, |
|||
#{timestamp}, |
|||
#{nonce}, |
|||
#{echostr}, |
|||
#{xmlstr}, |
|||
#{jsonstr}, |
|||
#{reqdata}, |
|||
#{addtime} |
|||
) |
|||
|
|||
</insert> |
|||
</mapper> |
|||
@ -0,0 +1,102 @@ |
|||
package com.dreamchaser.depository_manage.pojo; |
|||
|
|||
import com.dreamchaser.depository_manage.entity.ApplicationOutRecordMin; |
|||
import lombok.Data; |
|||
|
|||
import java.math.BigInteger; |
|||
|
|||
@Data |
|||
public class ApplicationOutRecordMinP { |
|||
/** |
|||
* id |
|||
*/ |
|||
private Integer id; |
|||
|
|||
/** |
|||
* 物料id |
|||
*/ |
|||
private Integer mid; |
|||
|
|||
/** |
|||
* 物料名称 |
|||
*/ |
|||
private String mname; |
|||
|
|||
/** |
|||
* 物料编码 |
|||
*/ |
|||
private BigInteger mcode; |
|||
/** |
|||
* 仓库id |
|||
*/ |
|||
private Integer depositoryId; |
|||
|
|||
|
|||
/** |
|||
* 仓库名称 |
|||
*/ |
|||
private String depositoryName; |
|||
|
|||
/** |
|||
* 对应库位id |
|||
*/ |
|||
private Integer placeId; |
|||
|
|||
|
|||
/** |
|||
* 库位编码 |
|||
*/ |
|||
private String placeCode; |
|||
/** |
|||
* 数量 |
|||
*/ |
|||
private Integer quantity; |
|||
/** |
|||
* 出库单号 |
|||
*/ |
|||
private String code; |
|||
|
|||
/** |
|||
* 审核人编号 |
|||
*/ |
|||
private Integer checkId; |
|||
|
|||
|
|||
/** |
|||
* 审核人姓名 |
|||
*/ |
|||
private String checkerName; |
|||
|
|||
/** |
|||
* 申请人姓名 |
|||
*/ |
|||
private String applicantName; |
|||
/** |
|||
* 主订单编号 |
|||
*/ |
|||
private Integer parentId; |
|||
|
|||
|
|||
/** |
|||
* 当前申请金额 |
|||
*/ |
|||
private Double price; |
|||
|
|||
/** |
|||
* 子订单状态(1未完成,2完成) |
|||
*/ |
|||
private Integer state; |
|||
|
|||
|
|||
public ApplicationOutRecordMinP(ApplicationOutRecordMin recordMin) { |
|||
this.id = recordMin.getId(); |
|||
this.mid = recordMin.getMid(); |
|||
this.depositoryId = recordMin.getDepositoryId(); |
|||
this.checkId = recordMin.getCheckId(); |
|||
this.code = recordMin.getCode(); |
|||
this.quantity = recordMin.getQuantity(); |
|||
this.state = recordMin.getState(); |
|||
this.parentId = recordMin.getParentId(); |
|||
this.placeId = recordMin.getPlaceId(); |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
package com.dreamchaser.depository_manage.pojo.callBackXml; |
|||
|
|||
|
|||
import lombok.Data; |
|||
import org.apache.commons.math3.util.Precision; |
|||
|
|||
import javax.xml.bind.annotation.*; |
|||
|
|||
|
|||
/** |
|||
* 基础xml |
|||
*/ |
|||
@Data |
|||
//@XmlRootElement(name = "xml")
|
|||
@XmlAccessorType(XmlAccessType.FIELD) |
|||
public class CallBackBaseXml { |
|||
|
|||
|
|||
private String ToUserName; //企业微信CorpID
|
|||
private String FromUserName; //成员UserID
|
|||
private String CreateTime; //消息创建时间(整型)
|
|||
private String MsgType; //消息类型,此时固定为:event
|
|||
private String AgentID; //企业应用的id,整型。可在应用的设置页面查看
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
package com.dreamchaser.depository_manage.pojo.callBackXml; |
|||
|
|||
|
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlRootElement; |
|||
|
|||
@Data |
|||
@XmlRootElement(name = "xml") |
|||
public class CallBackXMl_DLWZ extends CallBackBaseXml { |
|||
|
|||
/** |
|||
* 用于上报地理位置时的回调xml |
|||
*/ |
|||
|
|||
private String Event; // 事件类型,subscribe(关注)、unsubscribe(取消关注)
|
|||
private String EventKey; // 事件KEY值,此事件该值为空
|
|||
private String Latitude; //地理位置纬度
|
|||
private String Longitude; //地理位置经度
|
|||
private String Precision; // 地理位置精度
|
|||
private String AppType; // app类型,在企业微信固定返回wxwork,在微信不返回该字段
|
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
package com.dreamchaser.depository_manage.pojo.callBackXml; |
|||
|
|||
import com.dreamchaser.depository_manage.pojo.callBackXml.callBackXml_button_templatecard.TemplateCard; |
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlAccessType; |
|||
import javax.xml.bind.annotation.XmlAccessorType; |
|||
import javax.xml.bind.annotation.XmlRootElement; |
|||
|
|||
|
|||
@XmlAccessorType(XmlAccessType.FIELD) |
|||
@Data |
|||
public class CallBackXml_button extends CallBackBaseXml { |
|||
private TemplateCard TemplateCard; |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
package com.dreamchaser.depository_manage.pojo.callBackXml.callBackXml_button_templatecard; |
|||
|
|||
import com.dreamchaser.depository_manage.config.QyWx_template_card.BaseMessage; |
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* 文本消息 |
|||
* |
|||
*/ |
|||
@Data |
|||
public class ButtonInteraction extends BaseMessage { |
|||
// 模板卡片
|
|||
private TemplateCard template_card; |
|||
// 否 表示是否是保密消息,0表示否,1表示是,默认0
|
|||
private int safe; |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
package com.dreamchaser.depository_manage.pojo.callBackXml.callBackXml_button_templatecard; |
|||
|
|||
import com.dreamchaser.depository_manage.pojo.callBackXml.CallBackBaseXml; |
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlAccessType; |
|||
import javax.xml.bind.annotation.XmlAccessorType; |
|||
import javax.xml.bind.annotation.XmlRootElement; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 卡片模板 |
|||
* |
|||
*/ |
|||
@XmlAccessorType(XmlAccessType.FIELD) |
|||
@Data |
|||
@XmlRootElement(name="xml")//根节点
|
|||
public class TemplateCard { |
|||
|
|||
private String ToUserName; //企业微信CorpID
|
|||
private String FromUserName; //成员UserID
|
|||
private String CreateTime; //消息创建时间(整型)
|
|||
private String MsgType; //消息类型,此时固定为:event
|
|||
private String AgentID; //企业应用的id,整型。可在应用的设置页面查看
|
|||
/** |
|||
* 模板卡片类型,投票选择型卡片填写"vote_interaction" |
|||
*/ |
|||
|
|||
private String CardType; |
|||
|
|||
/** |
|||
* 二级普通文本,建议不超过160个字,(支持id转译) |
|||
*/ |
|||
private String SubTitleText; |
|||
|
|||
|
|||
/** |
|||
* 响应事件key |
|||
*/ |
|||
private String EventKey; |
|||
|
|||
/** |
|||
* 任务id,同一个应用任务id不能重复,只能由数字、字母和“_-@”组成,最长128字节 |
|||
*/ |
|||
private String TaskId; |
|||
|
|||
/** |
|||
* 响应的事件类型 |
|||
*/ |
|||
private String Event; |
|||
|
|||
|
|||
/** |
|||
* 下拉式的选择器 |
|||
*/ |
|||
|
|||
private TemplateCard_SelectedItems SelectedItems; |
|||
|
|||
|
|||
/** |
|||
* 按钮替换文案,填写本字段后会展现灰色不可点击按钮 |
|||
*/ |
|||
private String ReplaceText; |
|||
|
|||
/** |
|||
* ResponseCode |
|||
*/ |
|||
private String ResponseCode; |
|||
|
|||
|
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
package com.dreamchaser.depository_manage.pojo.callBackXml.callBackXml_button_templatecard; |
|||
|
|||
|
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlAccessType; |
|||
import javax.xml.bind.annotation.XmlAccessorType; |
|||
import javax.xml.bind.annotation.XmlRootElement; |
|||
import java.util.List; |
|||
|
|||
@XmlAccessorType(XmlAccessType.FIELD) |
|||
@Data |
|||
//@XmlRootElement(name="SelectedItems")//根节点
|
|||
public class TemplateCard_SelectedItems { |
|||
TemplateCard_button_selection SelectedItem; |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
package com.dreamchaser.depository_manage.pojo.callBackXml.callBackXml_button_templatecard; |
|||
|
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* 操作 |
|||
*/ |
|||
@Data |
|||
public class TemplateCard_action { |
|||
/** |
|||
* 操作的描述文案 |
|||
*/ |
|||
private String Text; |
|||
/** |
|||
* 操作key值,用户点击后,会产生回调事件将本参数作为EventKey返回,回调事件会带上该key值,最长支持1024字节,不可重复 |
|||
*/ |
|||
private String Key; |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
package com.dreamchaser.depository_manage.pojo.callBackXml.callBackXml_button_templatecard; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlAccessType; |
|||
import javax.xml.bind.annotation.XmlAccessorType; |
|||
import javax.xml.bind.annotation.XmlRootElement; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 卡片右上角更多操作按钮 |
|||
*/ |
|||
@XmlAccessorType(XmlAccessType.FIELD) |
|||
@Data |
|||
@XmlRootElement(name = "ActionMenu") |
|||
public class TemplateCard_action_menu { |
|||
/** |
|||
* 更多操作界面的描述 |
|||
*/ |
|||
private String Desc; |
|||
|
|||
/** |
|||
* 操作列表,列表长度取值范围为 [1, 3] |
|||
*/ |
|||
private List<TemplateCard_action> ActionList; |
|||
|
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
package com.dreamchaser.depository_manage.pojo.callBackXml.callBackXml_button_templatecard; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlAccessType; |
|||
import javax.xml.bind.annotation.XmlAccessorType; |
|||
|
|||
/** |
|||
* 按钮 |
|||
*/ |
|||
@XmlAccessorType(XmlAccessType.FIELD) |
|||
@Data |
|||
public class TemplateCard_button { |
|||
/** |
|||
* 按钮点击事件类型,0 或不填代表回调点击事件,1 代表跳转url |
|||
*/ |
|||
private Integer Yype; |
|||
/** |
|||
* 按钮文案,建议不超过10个字 |
|||
*/ |
|||
private String Text; |
|||
/** |
|||
* 按钮样式,目前可填1~4,不填或错填默认1 |
|||
*/ |
|||
private Integer Style; |
|||
/** |
|||
* 按钮key值,用户点击后,会产生回调事件将本参数作为EventKey返回,回调事件会带上该key值,最长支持1024字节,不可重复,button_list.type是0时必填 |
|||
*/ |
|||
private String Key; |
|||
/** |
|||
* 跳转事件的url,button_list.type是1时必填 |
|||
*/ |
|||
private String Url; |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
package com.dreamchaser.depository_manage.pojo.callBackXml.callBackXml_button_templatecard; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlAccessType; |
|||
import javax.xml.bind.annotation.XmlAccessorType; |
|||
import javax.xml.bind.annotation.XmlRootElement; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 下拉式的选择器 |
|||
*/ |
|||
@XmlAccessorType(XmlAccessType.FIELD) |
|||
@Data |
|||
public class TemplateCard_button_selection { |
|||
/** |
|||
* 下拉式的选择器的key,用户提交选项后,会产生回调事件,回调事件会带上该key值表示该题,最长支持1024字节 |
|||
*/ |
|||
private String QuestionKey; |
|||
|
|||
/** |
|||
* 下拉式的选择器左边的标题 |
|||
*/ |
|||
private String Title; |
|||
|
|||
/** |
|||
* 选项列表,下拉选项不超过 10 个,最少1个 |
|||
*/ |
|||
private TemplateCard_button_selection_option OptionIds; |
|||
|
|||
/** |
|||
* 下拉式的选择器默认选定的选项 |
|||
*/ |
|||
private String SelectedId; |
|||
|
|||
/** |
|||
* 是否可以选择状态 |
|||
*/ |
|||
private Boolean Disable; |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
package com.dreamchaser.depository_manage.pojo.callBackXml.callBackXml_button_templatecard; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlAccessType; |
|||
import javax.xml.bind.annotation.XmlAccessorType; |
|||
import javax.xml.bind.annotation.XmlRootElement; |
|||
|
|||
/** |
|||
* 下拉选项 |
|||
*/ |
|||
@Data |
|||
@XmlAccessorType(XmlAccessType.FIELD) |
|||
public class TemplateCard_button_selection_option { |
|||
/** |
|||
* 下拉式的选择器选项的id,用户提交后,会产生回调事件,回调事件会带上该id值表示该选项,最长支持128字节,不可重复 |
|||
*/ |
|||
private String OptionId; |
|||
/** |
|||
* 下拉式的选择器选项的文案,建议不超过16个字 |
|||
*/ |
|||
private String Text; |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
package com.dreamchaser.depository_manage.pojo.callBackXml.callBackXml_button_templatecard; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlAccessType; |
|||
import javax.xml.bind.annotation.XmlAccessorType; |
|||
|
|||
|
|||
/** |
|||
* 整体卡片的点击跳转事件 |
|||
*/ |
|||
@XmlAccessorType(XmlAccessType.FIELD) |
|||
@Data |
|||
public class TemplateCard_card_action { |
|||
/** |
|||
* 跳转事件类型,0或不填代表不是链接,1 代表跳转url,2 代表打开小程序 |
|||
*/ |
|||
private Integer Type; |
|||
|
|||
/** |
|||
* 跳转事件的url,card_action.type是1时必填 |
|||
*/ |
|||
private String Url; |
|||
|
|||
/** |
|||
* 跳转事件的小程序的appid,必须是与当前应用关联的小程序,card_action.type是2时必填 |
|||
*/ |
|||
private Integer AppId; |
|||
|
|||
/** |
|||
* 跳转事件的小程序的pagepath,card_action.type是2时选填 |
|||
*/ |
|||
private String PagePath; |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
package com.dreamchaser.depository_manage.pojo.callBackXml.callBackXml_button_templatecard; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlAccessType; |
|||
import javax.xml.bind.annotation.XmlAccessorType; |
|||
|
|||
/** |
|||
* 二级标题+文本列表 |
|||
*/ |
|||
@XmlAccessorType(XmlAccessType.FIELD) |
|||
@Data |
|||
public class TemplateCard_horizontal_content { |
|||
/** |
|||
* 链接类型,0或不填代表不是链接,1 代表跳转url,2 代表下载附件,3 代表点击跳转成员详情 |
|||
*/ |
|||
private Integer Type; |
|||
|
|||
/** |
|||
* 二级标题,建议不超过5个字 |
|||
*/ |
|||
private String KeyName; |
|||
|
|||
/** |
|||
* 二级文本,如果horizontal_content_list.type是2,该字段代表文件名称(要包含文件类型),建议不超过30个字,(支持id转译) |
|||
*/ |
|||
private String Value; |
|||
|
|||
/** |
|||
* 链接跳转的url,horizontal_content_list.type是1时必填 |
|||
*/ |
|||
private String Url; |
|||
|
|||
/** |
|||
* 附件的media_id,horizontal_content_list.type是2时必填 |
|||
*/ |
|||
private Integer MediaId; |
|||
|
|||
|
|||
/** |
|||
* 成员详情的userid,horizontal_content_list.type是3时必填 |
|||
*/ |
|||
private String UserId; |
|||
|
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
package com.dreamchaser.depository_manage.pojo.callBackXml.callBackXml_button_templatecard; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlAccessType; |
|||
import javax.xml.bind.annotation.XmlAccessorType; |
|||
import javax.xml.bind.annotation.XmlRootElement; |
|||
|
|||
|
|||
/** |
|||
* 一级标题 |
|||
*/ |
|||
@XmlAccessorType(XmlAccessType.FIELD) |
|||
@Data |
|||
@XmlRootElement(name = "MainTitle") |
|||
public class TemplateCard_main_title { |
|||
/** |
|||
* 一级标题,建议不超过36个字,(支持id转译) |
|||
*/ |
|||
private String Title; |
|||
/** |
|||
* 标题辅助信息,建议不超过44个字,(支持id转译) |
|||
*/ |
|||
private String Desc; |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
package com.dreamchaser.depository_manage.pojo.callBackXml.callBackXml_button_templatecard; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlAccessType; |
|||
import javax.xml.bind.annotation.XmlAccessorType; |
|||
import javax.xml.bind.annotation.XmlRootElement; |
|||
|
|||
/* |
|||
卡片来源样式信息 |
|||
*/ |
|||
@XmlAccessorType(XmlAccessType.FIELD) |
|||
@Data |
|||
@XmlRootElement(name = "Source") |
|||
public class TemplateCard_source { |
|||
/** |
|||
* 来源图片的url,来源图片的尺寸建议为72*72 |
|||
*/ |
|||
private String IconUrl; |
|||
|
|||
/** |
|||
* 来源图片的描述,建议不超过20个字,(支持id转译) |
|||
*/ |
|||
private String Desc; |
|||
|
|||
/** |
|||
* 来源文字的颜色,目前支持:0(默认) 灰色,1 黑色,2 红色,3 绿色 |
|||
*/ |
|||
private Integer DescColor; |
|||
|
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
package com.dreamchaser.depository_manage.pojo.callBackXml.callBackXml_button_templatecard; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import javax.xml.bind.annotation.XmlAccessType; |
|||
import javax.xml.bind.annotation.XmlAccessorType; |
|||
|
|||
/** |
|||
* 引用文献样式 |
|||
*/ |
|||
@XmlAccessorType(XmlAccessType.FIELD) |
|||
@Data |
|||
public class Template_quote_area { |
|||
/** |
|||
* 引用文献样式区域点击事件,0或不填代表没有点击事件,1 代表跳转url,2 代表跳转小程序 |
|||
*/ |
|||
private Integer Type; |
|||
/** |
|||
* 点击跳转的url,quote_area.type是1时必填 |
|||
*/ |
|||
private String Url; |
|||
/** |
|||
* 点击跳转的小程序的appid,必须是与当前应用关联的小程序,quote_area.type是2时必填 |
|||
*/ |
|||
private Integer Appid; |
|||
/** |
|||
* 点击跳转的小程序的pagepath,quote_area.type是2时选填 |
|||
*/ |
|||
private String PagePath; |
|||
/** |
|||
* 引用文献样式的标题 |
|||
*/ |
|||
private String Title; |
|||
/** |
|||
* 引用文献样式的引用文案 |
|||
*/ |
|||
private String QuoteText; |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
package com.dreamchaser.depository_manage.security.pool; |
|||
|
|||
|
|||
import com.dreamchaser.depository_manage.entity.UserByPort; |
|||
|
|||
import java.util.Map; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
|
|||
/** |
|||
* 用户登录后的key与token池 |
|||
*/ |
|||
public class UserKeyAndTokenPool { |
|||
private static Map<String, String> pool = new ConcurrentHashMap<>(200); |
|||
|
|||
|
|||
/** |
|||
* 用于暂存当前用户的key和token |
|||
* @param key 用户工号 |
|||
* @param keyAndToken key+token => key & token |
|||
*/ |
|||
public static void addKeyAndToken(String key, String keyAndToken){ |
|||
pool.put(key, keyAndToken); |
|||
} |
|||
|
|||
public static String getKeyAndToken(String key){ |
|||
String keyAndToken = pool.get(key); |
|||
return keyAndToken; |
|||
} |
|||
|
|||
public static void removeKeyAndToken(String key){ |
|||
pool.remove(key); |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
package com.dreamchaser.depository_manage.service; |
|||
|
|||
import com.dreamchaser.depository_manage.entity.CallBackLog; |
|||
|
|||
import java.util.Map; |
|||
|
|||
public interface CallBackLogService { |
|||
|
|||
/** |
|||
* 添加回调日志 |
|||
* @param map |
|||
* @return |
|||
*/ |
|||
Integer addCallBackLog(Map<String,Object> map); |
|||
/** |
|||
* 添加回调日志 |
|||
* @param callBackLog |
|||
* @return |
|||
*/ |
|||
Integer addCallBackLog(CallBackLog callBackLog); |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
package com.dreamchaser.depository_manage.service.impl; |
|||
|
|||
import com.dreamchaser.depository_manage.entity.CallBackLog; |
|||
import com.dreamchaser.depository_manage.mapper.CallBackLogMapper; |
|||
import com.dreamchaser.depository_manage.service.CallBackLogService; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import java.util.Map; |
|||
|
|||
@Service |
|||
public class CallBackLogServiceImpl implements CallBackLogService { |
|||
/** |
|||
* 添加回调日志 |
|||
* @param map |
|||
* @return |
|||
*/ |
|||
@Autowired |
|||
CallBackLogMapper callBackLogMapper; |
|||
|
|||
@Override |
|||
public Integer addCallBackLog(Map<String, Object> map) { |
|||
return callBackLogMapper.addCallBackLog(map); |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 添加回调日志 |
|||
* @param callBackLog |
|||
* @return |
|||
*/ |
|||
public Integer addCallBackLog(CallBackLog callBackLog){ |
|||
return callBackLogMapper.addCallBackLog(callBackLog); |
|||
} |
|||
} |
|||
@ -0,0 +1,497 @@ |
|||
package com.dreamchaser.depository_manage.service.impl; |
|||
|
|||
import cn.hutool.core.lang.Snowflake; |
|||
import com.alibaba.fastjson.JSON; |
|||
import com.alibaba.fastjson.JSONObject; |
|||
import com.dreamchaser.depository_manage.config.QyWxConfig; |
|||
import com.dreamchaser.depository_manage.config.QyWx_template_card.*; |
|||
import com.dreamchaser.depository_manage.controller.PageController; |
|||
import com.dreamchaser.depository_manage.entity.*; |
|||
import com.dreamchaser.depository_manage.mapper.DepositoryMapper; |
|||
import com.dreamchaser.depository_manage.mapper.DepositoryRecordMapper; |
|||
import com.dreamchaser.depository_manage.mapper.MaterialMapper; |
|||
import com.dreamchaser.depository_manage.pojo.ApplicationOutRecordP; |
|||
import com.dreamchaser.depository_manage.pojo.callBackXml.callBackXml_button_templatecard.TemplateCard; |
|||
import com.dreamchaser.depository_manage.utils.DateUtil; |
|||
import com.dreamchaser.depository_manage.utils.HttpUtils; |
|||
import org.apache.poi.ss.formula.functions.T; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.data.redis.core.RedisTemplate; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.HashMap; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
|
|||
@Service |
|||
public class QyWxOperationService { |
|||
|
|||
|
|||
@Autowired |
|||
DepositoryRecordMapper depositoryRecordMapper; |
|||
|
|||
@Autowired |
|||
MaterialMapper materialMapper; |
|||
|
|||
@Autowired |
|||
DepositoryMapper depositoryMapper; |
|||
|
|||
@Autowired |
|||
RedisTemplate<String,String> redisTemplate; |
|||
|
|||
/** |
|||
* 用于向企业微信发送消息 |
|||
* @param uid 接收人 |
|||
* @param outId 申请出库编号 |
|||
* @param flag 用于判断发送类型是部门负责人还是仓储负责人(true为部门,false为仓储) |
|||
* @return |
|||
*/ |
|||
public JSONObject sendQyWxMessage(String uid, Integer outId, Boolean flag) { |
|||
|
|||
// 获取将要发送申请的订单记录
|
|||
ApplicationOutRecordP applicationOutRecordPById = depositoryRecordMapper.findApplicationOutRecordPById(outId); |
|||
// 申请人id
|
|||
Integer applicantId = applicationOutRecordPById.getApplicantId(); |
|||
// 申请人
|
|||
UserByPort applicant = PageController.FindUserById(applicantId, null); |
|||
|
|||
// 获取所有子订单
|
|||
List<ApplicationOutRecordMin> applicationOutRecordMinByParent = depositoryRecordMapper.findApplicationOutRecordMinByParent(outId); |
|||
StringBuilder mname = new StringBuilder(); |
|||
StringBuilder depositoryName = new StringBuilder(); |
|||
StringBuilder sumQuantity = new StringBuilder(); |
|||
for (int i = 0; i < applicationOutRecordMinByParent.size(); i++) { |
|||
// 获取子订单信息
|
|||
ApplicationOutRecordMin applicationOutRecordMin = applicationOutRecordMinByParent.get(i); |
|||
// 获取当前申请物料
|
|||
Material materialById = materialMapper.findMaterialById(applicationOutRecordMin.getMid()); |
|||
// 获取当前物料所在仓库
|
|||
Depository depositoryRecordById = depositoryMapper.findDepositoryRecordById(materialById.getDepositoryId()); |
|||
sumQuantity.append(applicationOutRecordMin.getQuantity()).append(","); |
|||
mname.append(materialById.getMname()).append(","); |
|||
depositoryName.append(depositoryRecordById.getDname()).append(","); |
|||
} |
|||
|
|||
// 1.获取access_token:根据企业id和应用密钥获取access_token,并拼接请求url
|
|||
String accessToken = "".equals(QyWxConfig.token)?QyWxConfig.GetQYWXToken():QyWxConfig.token; |
|||
// 2.获取发送对象,并转成json
|
|||
ButtonInteraction buttonInteraction = new ButtonInteraction(); |
|||
// 1.1非必需
|
|||
//设置消息接收者
|
|||
String[] split = uid.split(","); |
|||
StringBuilder toUserName = new StringBuilder(); |
|||
for (int i = 0; i < split.length - 1; i++) { |
|||
toUserName.append(split[i]).append("|"); |
|||
} |
|||
toUserName.append(split[split.length - 1]); |
|||
buttonInteraction.setTouser(toUserName.toString()); // 不区分大小写
|
|||
|
|||
// 1.2必需
|
|||
// 消息类型
|
|||
buttonInteraction.setMsgtype("template_card"); |
|||
// 企业应用的id,整型
|
|||
buttonInteraction.setAgentid(QyWxConfig.AgentId); |
|||
// 卡片模板
|
|||
TemplateCard_button_interaction templateCard_button_interaction = new TemplateCard_button_interaction(); |
|||
// 模板卡片类型,按钮交互型卡片填写"button_interaction"
|
|||
templateCard_button_interaction.setCard_type("button_interaction"); |
|||
// 卡片右上角更多操作按钮
|
|||
TemplateCard_action_menu action_menu = new TemplateCard_action_menu(); |
|||
action_menu.setDesc("卡片副交互辅助文本说明"); |
|||
|
|||
// 卡片右上角操作按钮
|
|||
TemplateCard_action action1 = new TemplateCard_action(); |
|||
action1.setKey("AcceptThePush"); |
|||
action1.setText("接受推送"); |
|||
TemplateCard_action action2 = new TemplateCard_action(); |
|||
action2.setKey("NoPush"); |
|||
action2.setText("不再推送"); |
|||
List<TemplateCard_action> actionList = new ArrayList<>(); |
|||
actionList.add(action1); |
|||
actionList.add(action2); |
|||
action_menu.setAction_list(actionList); |
|||
// 设置操作按钮
|
|||
templateCard_button_interaction.setAction_menu(action_menu); |
|||
|
|||
// 一级标题
|
|||
TemplateCard_main_title main_title = new TemplateCard_main_title(); |
|||
// main_title.setTitle(applicant+"的出库申请");
|
|||
main_title.setTitle(applicant.getName()+"的出库申请"); |
|||
main_title.setDesc("申请时间:"+ DateUtil.TimeStampToDateTime(Long.valueOf(applicationOutRecordPById.getApplicantTime()))); |
|||
|
|||
// 设置一级标题
|
|||
templateCard_button_interaction.setMain_title(main_title); |
|||
|
|||
// 二级标题+文本列表,用于设置物料名称
|
|||
TemplateCard_horizontal_content horizontal_content_mname = new TemplateCard_horizontal_content(); |
|||
// 链接类型 0代表不是链接
|
|||
horizontal_content_mname.setType(0); |
|||
// 二级标题
|
|||
horizontal_content_mname.setKeyname("申请物料:"); |
|||
horizontal_content_mname.setValue(mname.toString()); |
|||
|
|||
// 二级标题+文本列表,用于设置物料数量
|
|||
TemplateCard_horizontal_content horizontal_content_quantity = new TemplateCard_horizontal_content(); |
|||
// 链接类型 0代表不是链接
|
|||
horizontal_content_quantity.setType(0); |
|||
// 二级标题
|
|||
horizontal_content_quantity.setKeyname("数量"); |
|||
horizontal_content_quantity.setValue(sumQuantity.toString()); |
|||
|
|||
// 二级标题+文本列表,用于设置物料对应仓库
|
|||
TemplateCard_horizontal_content horizontal_content_depositoryName = new TemplateCard_horizontal_content(); |
|||
// 链接类型 0代表不是链接
|
|||
horizontal_content_depositoryName.setType(0); |
|||
// 二级标题
|
|||
horizontal_content_depositoryName.setKeyname("仓库名称"); |
|||
horizontal_content_depositoryName.setValue(depositoryName.toString()); |
|||
|
|||
|
|||
// 二级标题+文本列表,用于设置申请查看明细
|
|||
TemplateCard_horizontal_content horizontal_content_detail = new TemplateCard_horizontal_content(); |
|||
// 链接类型 0代表不是链接
|
|||
horizontal_content_detail.setType(1); |
|||
// 二级标题
|
|||
horizontal_content_detail.setKeyname("申请明细"); |
|||
horizontal_content_detail.setValue("查看明细"); |
|||
horizontal_content_detail.setUrl("https://jy.hxgk.group/ApplicationOutView?id="+outId); |
|||
|
|||
|
|||
List<TemplateCard_horizontal_content> horizontal_contentList = new ArrayList<>(); |
|||
horizontal_contentList.add(horizontal_content_mname); |
|||
horizontal_contentList.add(horizontal_content_quantity); |
|||
horizontal_contentList.add(horizontal_content_depositoryName); |
|||
horizontal_contentList.add(horizontal_content_detail); |
|||
|
|||
// 设置二级标题
|
|||
templateCard_button_interaction.setHorizontal_content_list(horizontal_contentList); |
|||
|
|||
|
|||
// 任务id,同一个应用任务id不能重复,只能由数字、字母和“_-@”组成,最长128字节
|
|||
// 通过雪花算法获取taskId
|
|||
Snowflake snowflake = new Snowflake(10,10,true); |
|||
templateCard_button_interaction.setTask_id(snowflake.nextIdStr()); |
|||
|
|||
// 下拉式的选择器
|
|||
TemplateCard_button_selection button_selection = new TemplateCard_button_selection(); |
|||
// 下拉式的选择器的key
|
|||
button_selection.setQuestion_key("btn_status"); |
|||
button_selection.setTitle("您的身份"); |
|||
|
|||
List<TemplateCard_button_selection_option> optionList = new ArrayList<>(); |
|||
// 选项
|
|||
TemplateCard_button_selection_option button_selection_option = new TemplateCard_button_selection_option(); |
|||
button_selection_option.setText("部门负责人"); |
|||
if(flag) { |
|||
// 如果是发送给部门负责人
|
|||
button_selection_option.setId("btn_status_departManagerHead"); |
|||
}else{ |
|||
// 如果是发送给仓储负责人
|
|||
button_selection_option.setId("btn_status_depositoryManager"); |
|||
} |
|||
optionList.add(button_selection_option); |
|||
|
|||
button_selection.setOption_list(optionList); |
|||
|
|||
templateCard_button_interaction.setButton_selection(button_selection); |
|||
|
|||
// 按钮列表,列表长度不超过6
|
|||
List<TemplateCard_button> buttonList = new ArrayList<>(); |
|||
|
|||
TemplateCard_button button1 = new TemplateCard_button(); |
|||
button1.setKey("wms_pass_outId"+outId); |
|||
button1.setStyle(1); |
|||
button1.setText("通过"); |
|||
|
|||
TemplateCard_button button2 = new TemplateCard_button(); |
|||
button2.setKey("wms_reject_outId"+outId); |
|||
button2.setStyle(2); |
|||
button2.setText("驳回"); |
|||
|
|||
buttonList.add(button1); |
|||
buttonList.add(button2); |
|||
|
|||
templateCard_button_interaction.setButton_list(buttonList); |
|||
|
|||
buttonInteraction.setTemplate_card(templateCard_button_interaction); |
|||
|
|||
String s = JSONObject.toJSONString(buttonInteraction); |
|||
|
|||
// 3.获取请求的url
|
|||
String url = QyWxConfig.sendMessage_url.replace("ACCESS_TOKEN", accessToken); |
|||
|
|||
// 4.调用接口,发送消息
|
|||
String s1 = HttpUtils.doPost(url, s); |
|||
|
|||
// 将返回结果转为json对象
|
|||
JSONObject jsonObject = JSON.parseObject(s1); |
|||
|
|||
// 返回
|
|||
return jsonObject; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 将最终完成的订单抄送给仓储负责人 |
|||
* @param uid 仓储负责人编号 |
|||
* @param outId 订单编号 |
|||
* @return |
|||
*/ |
|||
public JSONObject sendCcMessageToUsers(String uid,Integer outId){ |
|||
// 获取已经完成的订单
|
|||
ApplicationOutRecordP recordP = depositoryRecordMapper.findApplicationOutRecordPById(outId); |
|||
// 申请人id
|
|||
Integer applicantId = recordP.getApplicantId(); |
|||
// 申请人
|
|||
UserByPort applicant = PageController.FindUserById(applicantId, null); |
|||
// 获取所有子订单
|
|||
List<ApplicationOutRecordMin> applicationOutRecordMinByParent = depositoryRecordMapper.findApplicationOutRecordMinByParent(outId); |
|||
|
|||
MessageByMarkDown markDown = new MessageByMarkDown(); |
|||
//设置消息接收者
|
|||
String[] split = uid.split(","); |
|||
StringBuilder toUserName = new StringBuilder(); |
|||
for (int i = 0; i < split.length - 1; i++) { |
|||
toUserName.append(split[i]).append("|"); |
|||
} |
|||
toUserName.append(split[split.length - 1]); |
|||
markDown.setTouser(toUserName.toString()); // 不区分大小写
|
|||
|
|||
|
|||
|
|||
// 设置agentId
|
|||
markDown.setAgentid(QyWxConfig.AgentId); |
|||
markDown.setMsgtype("markdown"); |
|||
|
|||
|
|||
// 设置content
|
|||
Map<String,String> markdown = new HashMap<>(); |
|||
StringBuilder content = new StringBuilder("## `抄送信息:`%n"); |
|||
content.append(">### **"+applicant.getName()+"的出库申请** %n<font color='warning'>申请时间:2022-10-23 14:30:18</font> %n"); |
|||
content.append("%n---%n"); |
|||
for (ApplicationOutRecordMin recordMin : applicationOutRecordMinByParent) { |
|||
// 获取子订单信息
|
|||
// 获取申请物料信息
|
|||
Material materialById = materialMapper.findMaterialById(recordMin.getMid()); |
|||
// 获取仓库信息
|
|||
Depository depositoryRecordById = depositoryMapper.findDepositoryRecordById(recordMin.getDepositoryId()); |
|||
// 获取处理人信息
|
|||
UserByPort userByPort = PageController.FindUserById(recordMin.getCheckId(), null); |
|||
content.append(">- 物料名称:").append(materialById.getMname()).append("%n"); |
|||
content.append(">- 申请数量:").append(recordMin.getQuantity()).append("%n"); |
|||
content.append(">- 所处仓库:").append(depositoryRecordById.getDname()).append("%n"); |
|||
content.append(">- 出库人员:").append(userByPort.getName()).append("%n"); |
|||
content.append("%n---%n"); |
|||
} |
|||
content.append(">## '''%n" + |
|||
">如需要查看详细信息,请点击:[查看信息](https://jy.hxgk.group/ApplicationOutView?id="+recordP.getId()+")"); |
|||
markdown.put("content",content.toString()); |
|||
markDown.setMarkdown(markdown); |
|||
String jsonString = JSONObject.toJSONString(markDown); |
|||
jsonString = String.format(jsonString); |
|||
System.out.println(jsonString); |
|||
// 3.获取请求的url
|
|||
// 获取access_token:根据企业id和应用密钥获取access_token,并拼接请求url
|
|||
String accessToken = "".equals(QyWxConfig.token)?QyWxConfig.GetQYWXToken():QyWxConfig.token; |
|||
String url = QyWxConfig.sendMessage_url.replace("ACCESS_TOKEN", accessToken); |
|||
|
|||
// 4.调用接口,发送消息
|
|||
String s1 = HttpUtils.doPost(url, jsonString); |
|||
|
|||
// 将返回结果转为json对象
|
|||
JSONObject jsonObject = JSON.parseObject(s1); |
|||
|
|||
// 返回
|
|||
return jsonObject; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 给仓库管理员发送出库通知 |
|||
* @param uid |
|||
* @param outMinId |
|||
* @return |
|||
*/ |
|||
public JSONObject sendNotificationToDepositoryManager(String uid,Integer outMinId){ |
|||
// 需要出库的子订单
|
|||
ApplicationOutRecordMin recordMin = depositoryRecordMapper.findApplicationOutMinById(outMinId); |
|||
// 获取其主订单
|
|||
ApplicationOutRecordP outRecordP = depositoryRecordMapper.findApplicationOutRecordPById(recordMin.getParentId()); |
|||
// 申请人id
|
|||
Integer applicantId = outRecordP.getApplicantId(); |
|||
// 申请人
|
|||
UserByPort applicant = PageController.FindUserById(applicantId, null); |
|||
// 定义文本通知型卡片
|
|||
TextNotice textNotice = new TextNotice(); |
|||
|
|||
//设置消息接收者
|
|||
String[] split = uid.split(","); |
|||
StringBuilder toUserName = new StringBuilder(); |
|||
for (int i = 0; i < split.length - 1; i++) { |
|||
toUserName.append(split[i]).append("|"); |
|||
} |
|||
toUserName.append(split[split.length - 1]); |
|||
textNotice.setTouser(toUserName.toString()); // 不区分大小写
|
|||
|
|||
|
|||
// 设置agentId
|
|||
textNotice.setAgentid(QyWxConfig.AgentId); |
|||
|
|||
// 定义卡片模板
|
|||
TemplateCard_text_notice text_notice = new TemplateCard_text_notice(); |
|||
text_notice.setCard_type("text_notice"); |
|||
|
|||
// 任务id,同一个应用任务id不能重复,只能由数字、字母和“_-@”组成,最长128字节
|
|||
// 通过雪花算法获取taskId
|
|||
Snowflake snowflake = new Snowflake(10,10,true); |
|||
text_notice.setTask_id(snowflake.nextIdStr()); |
|||
|
|||
textNotice.setMsgtype("template_card"); |
|||
|
|||
// 设置主标题
|
|||
TemplateCard_main_title main_title = new TemplateCard_main_title(); |
|||
main_title.setTitle(applicant.getName()+"的出库请求"); |
|||
main_title.setDesc("申请时间:"+ DateUtil.TimeStampToDateTime(Long.valueOf(outRecordP.getApplicantTime()))); |
|||
|
|||
text_notice.setMain_title(main_title); |
|||
// 卡片右上角更多操作按钮
|
|||
TemplateCard_action_menu action_menu = new TemplateCard_action_menu(); |
|||
action_menu.setDesc("卡片副交互辅助文本说明"); |
|||
|
|||
// 卡片右上角操作按钮
|
|||
TemplateCard_action action1 = new TemplateCard_action(); |
|||
action1.setKey("AcceptThePush"); |
|||
action1.setText("接受推送"); |
|||
TemplateCard_action action2 = new TemplateCard_action(); |
|||
action2.setKey("NoPush"); |
|||
action2.setText("不再推送"); |
|||
List<TemplateCard_action> actionList = new ArrayList<>(); |
|||
actionList.add(action1); |
|||
actionList.add(action2); |
|||
action_menu.setAction_list(actionList); |
|||
text_notice.setAction_menu(action_menu); |
|||
|
|||
|
|||
|
|||
List<TemplateCard_horizontal_content> horizontalContentList = new ArrayList<>(); |
|||
|
|||
// 获取申请物料信息
|
|||
Material materialById = materialMapper.findMaterialById(recordMin.getMid()); |
|||
// 获取仓库信息
|
|||
Depository depositoryRecordById = depositoryMapper.findDepositoryRecordById(recordMin.getDepositoryId()); |
|||
// 设至二级标题
|
|||
// 物料名称
|
|||
TemplateCard_horizontal_content horizontal_content_mname = new TemplateCard_horizontal_content(); |
|||
horizontal_content_mname.setType(0); |
|||
horizontal_content_mname.setKeyname("物料名称:"); |
|||
horizontal_content_mname.setValue(materialById.getMname()); |
|||
// 物料编码
|
|||
TemplateCard_horizontal_content horizontal_content_mcode = new TemplateCard_horizontal_content(); |
|||
horizontal_content_mcode.setType(0); |
|||
horizontal_content_mcode.setKeyname("物料编码:"); |
|||
horizontal_content_mcode.setValue(materialById.getCode().toString()); |
|||
// 申请数量
|
|||
TemplateCard_horizontal_content horizontal_content_quantity = new TemplateCard_horizontal_content(); |
|||
horizontal_content_quantity.setType(0); |
|||
horizontal_content_quantity.setKeyname("申请数量:"); |
|||
horizontal_content_quantity.setValue(recordMin.getQuantity().toString()); |
|||
// 所在仓库
|
|||
TemplateCard_horizontal_content horizontal_content_depository = new TemplateCard_horizontal_content(); |
|||
horizontal_content_depository.setType(0); |
|||
horizontal_content_depository.setKeyname("所在仓库:"); |
|||
horizontal_content_depository.setValue(depositoryRecordById.getDname()); |
|||
|
|||
// 申请备注
|
|||
TemplateCard_horizontal_content horizontal_content_applyRemark = new TemplateCard_horizontal_content(); |
|||
horizontal_content_applyRemark.setType(0); |
|||
horizontal_content_applyRemark.setKeyname("申请备注:"); |
|||
horizontal_content_applyRemark.setValue(outRecordP.getApplyRemark()); |
|||
|
|||
horizontalContentList.add(horizontal_content_mname); |
|||
horizontalContentList.add(horizontal_content_mcode); |
|||
horizontalContentList.add(horizontal_content_quantity); |
|||
horizontalContentList.add(horizontal_content_depository); |
|||
horizontalContentList.add(horizontal_content_applyRemark); |
|||
|
|||
text_notice.setHorizontal_content_list(horizontalContentList); |
|||
|
|||
// 卡片整体点击事件
|
|||
TemplateCard_card_action card_action = new TemplateCard_card_action(); |
|||
card_action.setType(1); |
|||
card_action.setUrl("https://jy.hxgk.group/ApplicationOutMinByDidForMobile?depositoryId="+depositoryRecordById.getId()+"&state=0"); |
|||
text_notice.setCard_action(card_action); |
|||
|
|||
//跳转指引样式的列表
|
|||
List<TemplateCard_jump> jumpList = new ArrayList<>(); |
|||
TemplateCard_jump jump = new TemplateCard_jump(); |
|||
jump.setType("1"); |
|||
jump.setUrl("https://jy.hxgk.group"); |
|||
jump.setTitle("进入系统"); |
|||
jumpList.add(jump); |
|||
|
|||
text_notice.setJump_list(jumpList); |
|||
|
|||
textNotice.setTemplate_card(text_notice); |
|||
String s = JSONObject.toJSONString(textNotice); |
|||
|
|||
// 3.获取请求的url
|
|||
// 获取access_token:根据企业id和应用密钥获取access_token,并拼接请求url
|
|||
String accessToken = "".equals(QyWxConfig.token)?QyWxConfig.GetQYWXToken():QyWxConfig.token; |
|||
String url = QyWxConfig.sendMessage_url.replace("ACCESS_TOKEN", accessToken); |
|||
|
|||
// 4.调用接口,发送消息
|
|||
String s1 = HttpUtils.doPost(url, s); |
|||
|
|||
// 将返回结果转为json对象
|
|||
JSONObject jsonObject = JSON.parseObject(s1); |
|||
|
|||
// 返回
|
|||
return jsonObject; |
|||
} |
|||
/** |
|||
* 用于撤回发送给企业微信的消息 |
|||
* @param msgid 待撤回消息的id |
|||
* @return |
|||
*/ |
|||
public String withdrawQyWxMessage(String msgid){ |
|||
String url = String.format("https://qyapi.weixin.qq.com/cgi-bin/message/recall?access_token=%s", QyWxConfig.token); |
|||
Map<String,Object> param = new HashMap<>(); |
|||
param.put("msgid",msgid); |
|||
String jsonString = JSONObject.toJSONString(param); |
|||
String post = HttpUtils.doPost(url, jsonString); |
|||
JSONObject jsonObject = JSON.parseObject(post); |
|||
String errmsg = jsonObject.getString("errmsg"); |
|||
Integer errcode = jsonObject.getInteger("errcode"); |
|||
if(errcode == 0){ |
|||
// 如果撤回成功
|
|||
return errmsg; |
|||
}else{ |
|||
return errmsg; |
|||
} |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 用于更新卡片中的按钮为不可点击状态 |
|||
* @param response_code |
|||
* @return |
|||
*/ |
|||
public JSONObject updateTemplateCard(String response_code,String userName,String state){ |
|||
String url = String.format("https://qyapi.weixin.qq.com/cgi-bin/message/update_template_card?access_token="+QyWxConfig.token+"&debug=1"); |
|||
Map<String,Object> map = new HashMap<>(); |
|||
map.put("atall",1); |
|||
map.put("agentid",QyWxConfig.AgentId); |
|||
map.put("response_code",response_code); |
|||
Map<String,Object> button = new HashMap<>(); |
|||
button.put("replace_name",userName+"已"+state); |
|||
map.put("button",button); |
|||
String jsonString = JSONObject.toJSONString(map); |
|||
String s1 = HttpUtils.doPost(url, jsonString); |
|||
JSONObject jsonObject = JSON.parseObject(s1); |
|||
return jsonObject; |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -1,4 +1,121 @@ |
|||
package com.dreamchaser.depository_manage.utils; |
|||
|
|||
import com.dreamchaser.depository_manage.config.QyWxConfig; |
|||
import org.apache.commons.lang.StringEscapeUtils; |
|||
import org.dom4j.Document; |
|||
import org.dom4j.DocumentHelper; |
|||
import org.dom4j.Element; |
|||
import org.dom4j.io.OutputFormat; |
|||
|
|||
import javax.xml.bind.JAXBContext; |
|||
import javax.xml.bind.JAXBException; |
|||
import javax.xml.bind.Marshaller; |
|||
import javax.xml.bind.Unmarshaller; |
|||
import java.io.FileOutputStream; |
|||
import java.io.StringReader; |
|||
import java.io.StringWriter; |
|||
|
|||
/** |
|||
* 用于企业微信xml的工具类 |
|||
*/ |
|||
|
|||
|
|||
|
|||
public class QyWxXMLUtils { |
|||
|
|||
/** |
|||
* 用于拼接回调post请求时的xml |
|||
* @param Encrypt |
|||
* @return |
|||
*/ |
|||
public static String formatDataToXml(String Encrypt){ |
|||
// 创建document对象
|
|||
Document document = DocumentHelper.createDocument(); |
|||
// 创建根节点xml
|
|||
Element root = document.addElement("xml"); |
|||
// 4、生成子节点及子节点内容
|
|||
Element ToUserName = root.addElement("ToUserName"); |
|||
ToUserName.setText("<![CDATA["+QyWxConfig.corpid+"]]>"); |
|||
|
|||
Element agentID = root.addElement("AgentID"); |
|||
agentID.setText("<![CDATA["+QyWxConfig.AgentId+"]]>"); |
|||
|
|||
Element encrypt = root.addElement("Encrypt"); |
|||
encrypt.setText("<![CDATA["+Encrypt+"]]>"); |
|||
// 5、设置生成xml的格式
|
|||
OutputFormat format = OutputFormat.createPrettyPrint(); |
|||
// 设置编码格式
|
|||
format.setEncoding("UTF-8"); |
|||
String xml = document.asXML(); |
|||
xml = QyWxXMLUtils.dealxmlHeader(xml); |
|||
xml = StringEscapeUtils.unescapeXml(xml); |
|||
return xml; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 用于删除企业微信中的头标识 |
|||
* @param xmlText |
|||
* @return |
|||
*/ |
|||
public static String dealxmlHeader(String xmlText) |
|||
{ |
|||
StringBuilder sb=new StringBuilder(xmlText); |
|||
StringBuilder newsb=new StringBuilder(""); |
|||
String startstring=sb.substring(0,xmlText.indexOf("<?xml")); |
|||
String endstring=sb.substring(xmlText.indexOf("?>") + 2,xmlText.length()); |
|||
newsb.append(startstring+endstring); |
|||
xmlText=new String(newsb.toString()); |
|||
return xmlText; |
|||
|
|||
} |
|||
|
|||
|
|||
/** |
|||
* 将String类型的xml转换成对象 |
|||
* @param clazz |
|||
* @param xmlStr |
|||
* @return |
|||
*/ |
|||
public static Object convertXmlStrToObject(Class clazz, String xmlStr) { |
|||
Object xmlObject = null; |
|||
try { |
|||
JAXBContext context = JAXBContext.newInstance(clazz); |
|||
// 进行将Xml转成对象的核心接口
|
|||
Unmarshaller unmarshaller = context.createUnmarshaller(); |
|||
StringReader sr = new StringReader(xmlStr); |
|||
xmlObject = unmarshaller.unmarshal(sr); |
|||
} catch (JAXBException e) { |
|||
e.printStackTrace(); |
|||
} |
|||
return xmlObject; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 将对象直接转换成String类型的 XML输出 |
|||
* |
|||
* @param obj |
|||
* @return |
|||
*/ |
|||
public static String convertToXml(Object obj) { |
|||
// 创建输出流
|
|||
StringWriter sw = new StringWriter(); |
|||
try { |
|||
// 利用jdk中自带的转换类实现
|
|||
JAXBContext context = JAXBContext.newInstance(obj.getClass()); |
|||
|
|||
Marshaller marshaller = context.createMarshaller(); |
|||
// 格式化xml输出的格式
|
|||
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, |
|||
Boolean.TRUE); |
|||
// 将对象转换成输出流形式的xml
|
|||
marshaller.marshal(obj, sw); |
|||
} catch (JAXBException e) { |
|||
e.printStackTrace(); |
|||
} |
|||
return sw.toString(); |
|||
} |
|||
|
|||
|
|||
} |
|||
|
|||
|
After Width: | Height: | Size: 119 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
File diff suppressed because one or more lines are too long
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue