由于我要完成的功能是公众号的定时推送,因此我需要有一个公众号。但是模板推送功能需要企业验证才可以使用,获取难度高,所以我选择了申请测试号,除了公众号名字,头像等内容是无法自定义的,在功能上都是一样的。微信公众测试号申请。
申请完成后可以得到自己测试号的appId,appsecret信息。这两个信息主要用于获取公众号全局调用的唯一凭证Access_Token。怎么获取我们后面再说。
URL(即:你的公网访问域名+你的服务器对应的接口Url)代表的是你服务器对应的校验接口,Token是自己定义的字符串。
具体校验操作看如下
signature | timestamp | nonce | echostr |
---|---|---|---|
微信加密签名,signature结合了开发者填写的 token 参数和请求中的 timestamp 参数、nonce参数 | 时间戳 | 随机数 | 随机字符串 |
public 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);
}
}
}
```java
package com.tencent.wxcloudrun.utils;
@SuppressWarnings("serial")
public class AesException extends Exception {
public final static int OK = 0;
public final static int ValidateSignatureError = -40001;
public final static int ParseXmlError = -40002;
public final static int ComputeSignatureError = -40003;
public final static int IllegalAesKey = -40004;
public final static int ValidateAppidError = -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 ValidateAppidError:
return "appid校验失败";
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;
}
}
/**
* 用于校验服务器是否合规,此处校验方式可以根据自己选择进行加密算法的选择
* 理论上此处可以省略工具类的校验,直接返回请求中的参数。
* 但是笔者直接返回却报错,不知道原因为何,看到这里的小伙伴可以尝试一下直接返回,即:return request.getEchostr();
* @return String
*/
@GetMapping(value = "/checkToken")
public String checkToken(VerifyRequest request) {
//配置中自己填写的Token
String token = "网页端你填写的token";
String sha1 = "";
try {
sha1 = SHA1.getSHA1(token, request.getTimestamp(), request.getNonce(), "");
} catch (AesException e) {
e.printStackTrace();
}
System.out.println("加密:"+sha1);
System.out.println("本身:"+request.getSignature());
//如果校验成功,则返回请求中的echostr参数。
if(sha1.equals(request.getSignature())){
return request.getEchostr();
}
else {
return "非法访问";
}
}
模板推送接口文档
编写完模板,获取用户ID后,我们就可以调用微信公众号的模板推送接口,进行推送啦。以下为一个标准的POST请求格式。
//请求URL:https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN(此处的ACCESS_TOKEN的获取会在后面进行补充)
//请求体
{
//关注你公众号的用户ID
"touser":"OPENID",
//你创建的模板ID
"template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY",
//点击消息模板跳转的地址
"url":"http://weixin.qq.com/download",
//顶色
"topcolor":"#FF0000",
//相应的具体数据,此处的User,Date,Type就是你模板里面对应的{{XXX.DATA}},如{{User.DATA}},{{Date.DATA}}
"data":{
"User": {
"value":"黄先生",
"color":"#173177"
},
"Date":{
"value":"06月07日 19时24分",
"color":"#173177"
},
"CardNumber": {
"value":"0426",
"color":"#173177"
},
"Type":{
"value":"消费",
"color":"#173177"
},
"Money":{
"value":"人民币260.00元",
"color":"#173177"
},
"DeadTime":{
"value":"06月07日19时24分",
"color":"#173177"
},
"Left":{
"value":"6504.09",
"color":"#173177"
}
}
}
access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。关于Access_Token的说明请参考Access token
此处获取Access_Token的方法也很简单,直接调用接口即可:https://api.weixin.qq.com/cgi-bin/token?appid=APPID&secret=APPSECRET。其中appId以及appSecret来自于测试号信息。
获取Access_Token工具类
/**
* 获取微信token工具类
*/
@Component
public class WeChetAccessToken {
//http请求类
@Autowired
private RestTemplate restTemplate;
//微信配置类文件
@Autowired
private WxMpProperties wxMpProperties;
public String getToken() {
//如果缓存中的Token过期,则请求获取Token
if (WxChatCache.AccessToken.expiration <= System.currentTimeMillis()) {
//URL:https://api.weixin.qq.com/cgi-bin/token?appid=APPID&secret=APPSECRET
String url = WxChatConstant.Url.ACCESS_TOKEN_URL
.replace("APPID", wxMpProperties.getAppId())
.replace("APPSECRET", wxMpProperties.getSecret());
//使用restTemplate发起Http请求
ResponseEntity<String> forEntity = restTemplate.getForEntity(url, String.class);
JSONObject jsonObject = JSON.parseObject(forEntity.getBody());
Object errcode = jsonObject.get("errcode");
if (errcode != null && "40013".equals(errcode.toString())) {
System.out.println("不合法的APPID");
}
// expiration:为当前执行到此处的时间+2小时有效时间为过期时间
WxChatCache.AccessToken.token = jsonObject.get("access_token").toString();
WxChatCache.AccessToken.expiration = System.currentTimeMillis()+7200000;
return WxChatCache.AccessToken.token;
}
//如果没过期则使用缓存中的Token
else {
System.out.println("返回缓存中的token:"+ WxChatCache.AccessToken.token);
return WxChatCache.AccessToken.token;
}
}
}
/**
* RestTemplate工具类,主要用来提供RestTemplate对象
*/
@Configuration//加上这个注解作用,可以被Spring扫描
public class RestTemplateConfig {
/**
* 创建RestTemplate对象,将RestTemplate对象的生命周期的管理交给Spring----踩坑一,编码问题
*/
@Bean
public RestTemplate restTemplate(){
RestTemplate restTemplate = new RestTemplate();
//设置中文乱码问题方式一
//restTemplate.getMessageConverters().add(1,new StringHttpMessageConverter(Charset.forName("UTF-8")));
// 设置中文乱码问题方式二
restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
return restTemplate;
}
}
public class WxChatCache {
/**
* 微信 accessToken缓存
*/
public static class AccessToken {
public static String token = null; // accessToken
public static Long expiration = 0L; // accessToken 过期时间(获取的token 默认有效期2小时)
}
}
# 容器默认时区为UTC,如需使用上海时间请启用以下时区设置命令
RUN apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo Asia/Shanghai > /etc/timezone
在经过上面的步骤准备即可开始实现定时推送啦。以下展示一个完整的demo示例。
启动类
@SpringBootApplication
@MapperScan(basePackages = {"com.tencent.wxcloudrun.dao"})
//开启定时任务
@EnableScheduling
public class WxCloudRunApplication {
public static void main(String[] args) {
SpringApplication.run(WxCloudRunApplication.class, args);
}
}
@RestController
public class GirlFriendController {
//上下文,用于策略模式获取对应策略
@Autowired
private ApplicationContext applicationContext;
//表示每个月星期一到星期五下午4点50分执行
@Scheduled(cron = "0 50 16 ? * MON-FRI")
public void sendOffWork() throws ExecutionException, InterruptedException {
System.out.println("开始发送下班提醒");
//参数一发送类型,参数二是推送的对象OpenId
SendTypeRequest sendTypeRequest = new SendTypeRequest("OffWorkSend","推送的对象OpenId");
WxChatService chatService = applicationContext.getBean(sendTypeRequest.getType(),WxChatService.class);
chatService.sendTest(sendTypeRequest);
}
}
public interface WxChatService {
/**
* 向一个用户推送消息(测试)
* @param
*/
void sendTest(SendTypeRequest send) throws ExecutionException, InterruptedException;
}
/**
* 下班推送策略
* */
@Service("OffWorkSend")
public class OffWorkSend implements WxChatService {
@Autowired
protected WxSendMessageUtils wxSendMessageUtils;
@Override
public void sendTest(SendTypeRequest send) {
// 下班模板Id
String templateId = "你的模板Id";
// 模板参数
Map<String, WeChatTemplateMsg> sendMag = new HashMap<String, WeChatTemplateMsg>();
sendMag.put("offWork", new WeChatTemplateMsg("宝~马上就要下班咯,收拾好随身物品准备早退啦!","#b89485"));
// 发送
wxSendMessageUtils.send(send.getOpenId(), templateId, sendMag);
}
}
@Component
public class WxSendMessageUtils{
//获取Access_Token工具类
@Autowired
protected WeChetAccessToken weChetAccessToken;
//restTemplate的请求方式
@Autowired
protected RestTemplate restTemplate;
/**
* 发送方法
* */
public String send(String openId, String templateId, Map<String, WeChatTemplateMsg> data) {
String accessToken = weChetAccessToken.getToken();
//System.out.println("send方法里的token:"+accessToken);
String url = WxChatConstant.Url.SEND_URL.replace("ACCESS_TOKEN", accessToken);
//String url = "http://api.weixin.qq.com/cgi-bin/message/template/send";
//拼接base参数
System.out.println("调用的接口地址为:"+url);
Map<String, Object> sendBody = new HashMap<>();
sendBody.put("touser", openId); // openId
sendBody.put("url","www.baidu.com"); // 跳转url
sendBody.put("topcolor", "#FF0000"); // 顶色
sendBody.put("data", data); // 模板参数
sendBody.put("template_id", templateId); // 模板Id
ResponseEntity<String> forEntity = restTemplate.postForEntity(url, sendBody, String.class);
System.out.println("响应:"+forEntity.getBody());
return forEntity.getBody();
}
}
@Data
@AllArgsConstructor
public class SendTypeRequest {
/**
* 推送类型
* */
private String type;
/**
* 接收人的openId
* */
private String openId;
}
关于这个问题,无解。尝试了网上的一些主流的方案,无果。最后还是使用了HttpClient进行http的请求调用
出现这个问题,请及时查看是否在“服务-服务列表”中开启是否允许公网访问。
由于动态扩容的原因,超过30分钟没有调用,实例缩容为0。此时服务停止。需要在「服务设置」中,将「实例副本数」的最小值设为1,保持服务常驻,无论服务是否被请求都不会缩容到0,从根本上避免冷启动。(会产生更多资源消耗及费用,请自行权衡,学生党可以将规格修改为0.25核,0.5G内存,够用了);