订阅消息推送

master
guochaojie 5 months ago
parent 35602b9bf8
commit 17cbf7a390

@ -49,6 +49,8 @@ public class WebMvcConfig implements WebMvcConfigurer {
"/static/**",
"/upload/**",
"/qrcode/**.txt",
"/wx-message/**",//微信消息推送验证
"/wx-message/**",//微信消息推送验证
"/doc.html"
);

@ -14,4 +14,9 @@ import org.springframework.stereotype.Component;
public class WxHsyProperties {
private String appId;
private String appSecret;
private String tokenUrl;
private String sendMessageUrl;
private String EncodingAESKey;
private String token;
}

@ -14,4 +14,8 @@ import org.springframework.stereotype.Component;
public class WxShProperties {
private String appId;
private String appSecret;
private String tokenUrl;
private String sendMessageUrl;
private String EncodingAESKey;
private String token;
}

@ -0,0 +1,153 @@
package cc.yunxi.controller;
import cc.yunxi.common.domain.CommonResult;
import cc.yunxi.domain.vo.vxmessage.AccessToken;
import cc.yunxi.domain.vo.vxmessage.MessageTemplate;
import cc.yunxi.domain.vo.vxmessage.OrderNew;
import cc.yunxi.domain.vo.vxmessage.ResultVo;
import cc.yunxi.utils.VerifyUtil;
import cc.yunxi.utils.WeChatMessageUtil;
import cc.yunxi.utils.WeChatUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@Api(tags = "对接微信推送认证")
@RestController
@RequestMapping("/wx-message")
@Slf4j
public class WxMessageController {
@Resource
private WeChatMessageUtil weChatMessageUtil;
@Resource
private VerifyUtil verifyUtil;
/**
*
*
* @param signature
* @param timestamp
* @param nonce
* @param echostr
* @return
*/
@GetMapping("/clientReceive")
public String clientVerify(@RequestParam String signature,
@RequestParam String timestamp,
@RequestParam String nonce,
@RequestParam String echostr) {
log.info("微信推送验证");
boolean isOfficial = verifyUtil.checkSignature("client", signature, timestamp, nonce);
if (isOfficial) return echostr;
else {
log.error("微信推送验证失败!非官方推送,{},{},{},{}", signature, timestamp, nonce, echostr);
return "error";
}
}
/**
*
*
* @param signature
* @param timestamp
* @param nonce
* @param echostr
* @return
*/
@GetMapping("/recyclerReceive")
public String recyclerVerify(@RequestParam String signature,
@RequestParam String timestamp,
@RequestParam String nonce,
@RequestParam String echostr) {
log.info("微信推送验证");
boolean isOfficial = verifyUtil.checkSignature("recycler", signature, timestamp, nonce);
if (isOfficial) return echostr;
else {
log.error("微信推送验证失败!非官方推送,{},{},{},{}", signature, timestamp, nonce, echostr);
return "error";
}
}
//散户端端接收微信平台消息
@PostMapping("/clientReceive")
public String clientReceive(@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam("openid") String openid,
@RequestParam("encrypt_type") String encryptType,
@RequestParam("msg_signature") String msgSignature,
@RequestBody String encryptedData) {
JSONObject _encryptedData = JSONUtil.parseObj(encryptedData);
String encrypt = _encryptedData.getStr("Encrypt");
log.info("微信推送验证");
boolean isOfficial = verifyUtil.checkSignature("client", timestamp, nonce, encrypt,msgSignature);
if (!isOfficial) {
log.error("接收推送消息失败!非官方推送,{},{},{},{}", timestamp, nonce, encrypt, msgSignature);
return "error";
}
String message = weChatMessageUtil.decryptMessage(encrypt, "recycler");
log.info("收到微信推送消息,{}", message);
return "success";
}
//回收端接收微信平台消息
@PostMapping("/recyclerReceive")
public String recyclerReceive(@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam("openid") String openid,
@RequestParam("encrypt_type") String encryptType,
@RequestParam("msg_signature") String msgSignature,
@RequestBody String encryptedData) {
JSONObject _encryptedData = JSONUtil.parseObj(encryptedData);
String encrypt = _encryptedData.getStr("Encrypt");
log.info("微信推送验证");
boolean isOfficial = verifyUtil.checkSignature("recycler", timestamp, nonce, encrypt,msgSignature);
if (!isOfficial) {
log.error("接收推送消息失败!非官方推送,{},{},{},{}", timestamp, nonce, encrypt, msgSignature);
return "error";
}
String message = weChatMessageUtil.decryptMessage(encrypt, "recycler");
log.info("收到微信推送消息,{}", message);
return "success";
}
@PostMapping("/getToken")
public CommonResult<AccessToken> getToken(@RequestBody String message) {
AccessToken client = weChatMessageUtil.getAccessToken("client");
return CommonResult.success(client);
}
@PostMapping("/send")
public CommonResult<ResultVo> sendMessage(@RequestBody String message) {
AccessToken client = weChatMessageUtil.getAccessToken("client");
OrderNew orderNew = new OrderNew();
orderNew.setCharacter_string22("11");//订单号
orderNew.setTime1("2024/06/04 16:30~17:30");//预约时间
orderNew.setName15("上门回收");//服务名称
orderNew.setThing9("地址");//预约地址
orderNew.setPhone_number43("13183060802");//联系电话
MessageTemplate template = new MessageTemplate();
template.setTemplate_id(OrderNew.TEMPLATE_ID);
template.setData(orderNew);
template.setTouser("oYkV866Jjz197Iya3kJQwdypNPq8");
template.setMiniprogram_state("trial");
template.setPage("/pages/orderDetail/orderDetail");
template.setLang("zh_CN");
ResultVo result = weChatMessageUtil.sendMessage(template, "client");
return CommonResult.success(result);
}
}

@ -0,0 +1,33 @@
package cc.yunxi.domain.vo.vxmessage;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Slf4j
@Data
@NoArgsConstructor
@ApiModel("对接微信开发平台获取发送推送消息的access_token")
public class AccessToken {
/*---------------- 返回体 ---------------------*/
@ApiModelProperty("token内容")
private String access_token;
@ApiModelProperty("token过期时间 转换为毫秒")
private AtomicLong expires_in = new AtomicLong(System.currentTimeMillis() * 1000);
}

@ -0,0 +1,10 @@
package cc.yunxi.domain.vo.vxmessage;
import io.swagger.annotations.ApiModel;
import lombok.Data;
@Data
@ApiModel(description = "订单信息基类")
public class BaseMessage {
}

@ -0,0 +1,21 @@
package cc.yunxi.domain.vo.vxmessage;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("订单完成")
public class FinishOrder extends BaseMessage{
@ApiModelProperty("订单号")
private String character_string5;
@ApiModelProperty("订单状态")
private String thing3;
@ApiModelProperty("实付金额")
private String amount22;
@ApiModelProperty("服务方")
private String thing10;
@ApiModelProperty("完成时间")
private String date9;
}

@ -0,0 +1,23 @@
package cc.yunxi.domain.vo.vxmessage;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "微信推送消息")
public class MessageTemplate {
@ApiModelProperty("消息模版")
private String template_id;
@ApiModelProperty("跳转页面")
private String page;
@ApiModelProperty("接受者")
private String touser;
@ApiModelProperty("模板内容")
private BaseMessage data;
@ApiModelProperty("跳转小程序类型developer为开发版trial为体验版formal为正式版默认为正式版")
private String miniprogram_state;
@ApiModelProperty("语言支持zh_CN(简体中文)、en_US(英文)、zh_HK(繁体中文)、zh_TW(繁体中文)默认为zh_CN")
private String lang = "zh_CN";
}

@ -0,0 +1,20 @@
package cc.yunxi.domain.vo.vxmessage;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("订单取消")
public class OrderCancel extends BaseMessage {
@ApiModelProperty("订单编号")
private String character_string3;
@ApiModelProperty("订单状态")
private String phrase4;
@ApiModelProperty("服务地址")
private String thing29;
@ApiModelProperty("预约时间")
private String date10;
@ApiModelProperty("操作人")
private String thing25;
}

@ -0,0 +1,25 @@
package cc.yunxi.domain.vo.vxmessage;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "新订单预约成功通知消息模版")
public class OrderNew extends BaseMessage{
@JsonIgnore//不在data属性中
@ApiModelProperty("消息模版ID")
public static final String TEMPLATE_ID = "TfLbZu3DxLSp4TnuTVCsDTY2U4zSV2M7MNTrGHaUL5s";
@ApiModelProperty("订单编号")
private String character_string22;
@ApiModelProperty("预约时间")
private String time1;
@ApiModelProperty("服务名称")
private String name15;
@ApiModelProperty("预约地址")
private String thing9;
@ApiModelProperty("联系电话")
private String phone_number43;
}

@ -0,0 +1,21 @@
package cc.yunxi.domain.vo.vxmessage;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiOperation;
import lombok.Data;
@ApiModel("已接单消息模板")
@Data
public class OrderTaken extends BaseMessage{
@ApiModelProperty("订单号")
private String character_string5;
@ApiModelProperty("订单状态")
private String thing4;
@ApiModelProperty("联系人")
private String thing18;
@ApiModelProperty("联系电话")
private String phone_number14;
@ApiModelProperty("服务时间")
private String thing6;
}

@ -0,0 +1,14 @@
package cc.yunxi.domain.vo.vxmessage;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@ApiModel("发送订阅消息的结果")
@Data
public class ResultVo {
@ApiModelProperty("错误代码")
private int errcode;
@ApiModelProperty("错误描述")
private String errmsg;
}

@ -0,0 +1,133 @@
package cc.yunxi.utils;
import cc.yunxi.config.props.WxHsyProperties;
import cc.yunxi.config.props.WxShProperties;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
@Component
@Slf4j
public class VerifyUtil {
@Resource
private WxShProperties clientProperties;
@Resource
private WxHsyProperties recyclerProperties;
/**
*
* URLhttps://www.qq.com/revice?signature=f464b24fc39322e44b38aa78f5edd27bd1441696&echostr=4375120948345356249&timestamp=1714036504&nonce=1514711492
* tokentimestampnonce:["1514711492","1714036504","AAAAA"]
* "15147114921714036504AAAAA"
* sha1f464b24fc39322e44b38aa78f5edd27bd1441696
* URLsignature
* URLechostr4375120948345356249
*
* @param endpoint client recycler
* @param signature
* @param timestamp
* @param nonce
* @return
*/
public boolean checkSignature(String endpoint, String signature, String timestamp, String nonce) {
String token;
if ("client".equals(endpoint)) {
token = clientProperties.getToken();
} else if ("recycler".equals(endpoint)) {
token = recyclerProperties.getToken();
} else {
log.error("endpoint错误,微信认证失败:{}", endpoint);
return false;
}
String[] arr = new String[]{token, timestamp, nonce};
Arrays.sort(arr);
// 拼接排序后的字符串
StringBuilder content = new StringBuilder();
for (String s : arr) {
content.append(s);
}
try {
MessageDigest instance = MessageDigest.getInstance("SHA-1");
instance.update(content.toString().getBytes(StandardCharsets.UTF_8));
// 完成哈希计算
byte[] hash = instance.digest();
// 转换为十六进制字符串
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
// 比较签名
return signature.equals(hexString.toString());
} catch (NoSuchAlgorithmException e) {
// SHA-1 应该总是可用的,但以防万一
e.printStackTrace();
}
// 签名不匹配或其他错误,返回 false
return false;
}
/**
*
*
* @param endpoint client recycler
* @param timestamp
* @param nonce
* @param Encrypt
* @param msgSignature
* @return
*/
public boolean checkSignature(String endpoint, String timestamp, String nonce, String Encrypt, String msgSignature) {
String token;
if ("client".equals(endpoint)) {
token = clientProperties.getToken();
} else if ("recycler".equals(endpoint)) {
token = recyclerProperties.getToken();
} else {
log.error("endpoint错误,微信认证失败:{}", endpoint);
return false;
}
String[] arr = new String[]{token, timestamp, nonce, Encrypt};
Arrays.sort(arr);
// 拼接排序后的字符串
StringBuilder content = new StringBuilder();
for (String s : arr) {
content.append(s);
}
try {
MessageDigest instance = MessageDigest.getInstance("SHA-1");
instance.update(content.toString().getBytes(StandardCharsets.UTF_8));
// 完成哈希计算
byte[] hash = instance.digest();
// 转换为十六进制字符串
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
// 比较签名
return msgSignature.equals(hexString.toString());
} catch (NoSuchAlgorithmException e) {
// SHA-1 应该总是可用的,但以防万一
e.printStackTrace();
}
// 签名不匹配或其他错误,返回 false
return false;
}
}

@ -0,0 +1,143 @@
package cc.yunxi.utils;
import cc.yunxi.config.props.WxHsyProperties;
import cc.yunxi.config.props.WxShProperties;
import cc.yunxi.domain.vo.vxmessage.AccessToken;
import cc.yunxi.domain.vo.vxmessage.MessageTemplate;
import cc.yunxi.domain.vo.vxmessage.ResultVo;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Slf4j
@Component
public class WeChatMessageUtil {
@Resource
private WxShProperties clientProperties;
@Resource
private WxHsyProperties recyclerProperties;
private static final Map<String, AccessToken> accessTokenMap = new ConcurrentHashMap<>();
//推送消息给微信用户
public ResultVo sendMessage(MessageTemplate messageVo, String endpointType) {
ResultVo vo = new ResultVo();
try {
if ("client".equals(endpointType)) {
String url = clientProperties.getSendMessageUrl();
AccessToken accessToken = getAccessToken(endpointType);
String token = accessToken.getAccess_token();
String post = HttpUtil.post(url + "?access_token=" + token, JSONUtil.toJsonStr(messageVo));
vo = JSONUtil.toBean(post, ResultVo.class);
} else if ("recycler".equals(endpointType)) {
String url = recyclerProperties.getSendMessageUrl();
AccessToken accessToken = getAccessToken(endpointType);
String token = accessToken.getAccess_token();
String post = HttpUtil.post(url + "?access_token=" + token, JSONUtil.toJsonStr(messageVo));
vo = JSONUtil.toBean(post, ResultVo.class);
} else {
vo.setErrcode(400);
vo.setErrmsg("请求未发送,参数错误!");
}
} catch (RuntimeException e) {
e.printStackTrace();
vo.setErrcode(500);
vo.setErrmsg(e.getMessage());
}
return vo;
}
/**
* @param endpointType client recycler
* @return access_token
*/
public AccessToken getAccessToken(String endpointType) {
AccessToken token = accessTokenMap.get(endpointType);
boolean expired = isExpired(token);
if (expired) {//过期重新获取
// 获取access_token
String result;
Map<String, Object> param = new HashMap<>();
param.put("grant_type", "client_credential");//固定值
if ("client".equals(endpointType)) {//预约端
param.put("appid", clientProperties.getAppId());
param.put("secret", clientProperties.getAppSecret());
result = HttpUtil.get(clientProperties.getTokenUrl(), param);
} else if ("recycler".equals(endpointType)) {//回收端
param.put("appid", recyclerProperties.getAppId());
param.put("secret", recyclerProperties.getAppSecret());
result = HttpUtil.get(recyclerProperties.getTokenUrl(), param);
} else {
return null;
}
AccessToken access_token = JSONUtil.toBean(result, AccessToken.class);
access_token.setExpires_in(new AtomicLong(10 * 1000 + access_token.getExpires_in().get() * 1000));
accessTokenMap.put(endpointType, access_token);
}
log.info("当前微信access_token为{}", token);
return token;
}
/**
*
*
* @param token
* @return
*/
private boolean isExpired(AccessToken token) {
if (null != token &&
token.getExpires_in().get() > System.currentTimeMillis() + 10 * 1000) {
return false;
}
return true;
}
//消息解码
public String decryptMessage(String encryptedMsg, String endpointType) {
try {
String encodingAESKey;
if ("client".equals(endpointType)) {
encodingAESKey = clientProperties.getEncodingAESKey();
} else if ("recycler".equals(endpointType)) {
encodingAESKey = recyclerProperties.getEncodingAESKey();
} else {
log.error("无法获取解密秘钥:{}", endpointType);
return "error";
}
byte[] aesKey = Base64.getDecoder().decode(encodingAESKey + "=");
byte[] iv = Arrays.copyOfRange(aesKey, 0, 16);
byte[] encryptedBytes = Base64.getDecoder().decode(encryptedMsg);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
// PKCS#7 unpadding
int pad = decryptedBytes[decryptedBytes.length - 1];
return new String(Arrays.copyOfRange(decryptedBytes, 16, decryptedBytes.length - pad));
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

@ -102,6 +102,8 @@ nxhs:
- /api/file/download
- /api/index/products
- /api/device/**
- /api/qrcode/**.txt
- /api/wx-message/**
adminKey: 8bd2aa89033ead51c505e44994e42189 # 后台接口访问Key
upload:
basePath: /upload/
@ -115,7 +117,16 @@ nxhs:
client: # 散户端
appid: wx630bc4f43990c80c
appsecret: 37028048aad5e1877c2b2aeacdfdc01b
token: AAAAA #消息推送对接令牌
tokenUrl: https://api.weixin.qq.com/cgi-bin/token #获取微信平台token地址
sendMessageUrl: https://api.weixin.qq.com/cgi-bin/message/subscribe/send #调用消息推送接口地址
EncodingAESKey: g30DNk4Rf9RW0MLwCk0FfcfIsmiPnQ1NN7yLT8pcKNJ #消息解码秘钥
recycler: # 回收员端
appid: wxf82bcc798891a29d
appsecret: f37fb0ab2b5f691d8507acced60a58fb
token: AAAAA #消息推送对接令牌
tokenUrl: https://api.weixin.qq.com/cgi-bin/token #获取微信平台token地址
sendMessageUrl: https://api.weixin.qq.com/cgi-bin/message/subscribe/send #调用消息推送接口地址
EncodingAESKey: #消息解码秘钥
# 微信支付
# keytool -genkeypair -alias nxhs -keyalg RSA -keypass nxhs2024 -keystore nxhs.jks -storepass nxhs2024
Loading…
Cancel
Save