最近参与开发的项目有用到微信模板消息推送,在这离记录一下。
1.工具类;用于访问WeChat的API接口;
这里工具类使用了静态内部类,且禁止外部使用new创建对象。
ps:URL这里用final static修饰没问题,但切记不要把微信所需的令牌参数(access_token)也拼接上(之前项目的微信消息推送每次重启项目后一段时间就失效),问题所在就不过多细说了;
package com.icex.frame.utils.wechat;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;import com.icex.bus.sys.entity.WxTempMsgModel;import mon.Const;import com.icex.frame.utils.DateUtil;import com.icex.frame.utils.HttpClientUtil;import mons.lang3.StringUtils;import org.slf4j.Logger;import org.slf4j.LoggerFactory;/*** 微信推送模板消息方法* 微信工具类* @author Jjcc* @createTime 07月24日 21:03:00*/public class WeChatToolUtil {private static final String URL = "https://api./cgi-bin/message/wxopen/template/send?access_token=";private static Logger logger = LoggerFactory.getLogger(WeChatToolUtil.class.getName());/*** 构造方法私有化* @title WeChatToolUtil* @description* @author Jjcc* @return* @createTime* @throws*/private WeChatToolUtil() {if (null != WeChatUtilInnerClass.WE_CHAT_TOOL) {logger.error("实例对象已创建!");throw new RuntimeException("实例对象已创建!");}}private static class WeChatUtilInnerClass {private final static WeChatToolUtil WE_CHAT_TOOL = new WeChatToolUtil();}public static WeChatToolUtil getInstance() {return WeChatUtilInnerClass.WE_CHAT_TOOL;}/*** @title pushMessage* @description* @author Jjcc* @param openId 微信用户openId* @param templateId 模板消息Id* @param formId form_id 表单提交场景下,为 submit 事件带上的 formId* @param page 点击模板消息后进入的页面* @return String* @createTime /7/24 21:57* @throws*/public String pushMessage(String openId, String templateId, String formId, String page, JSONObject jsonObject) {System.out.println("推送微信消息-----token---------> " + Const.WX_ACCESS_TOKEN);WxTempMsgModel model = new WxTempMsgModel();model.setAccess_token(Const.WX_ACCESS_TOKEN);model.setTouser(openId);model.setTemplate_id(templateId);model.setForm_id(formId);model.setPage(page);model.setData(jsonObject);String jsonStr = JSON.toJSONString(model);String ret;try {ret = HttpClientUtil.doPost2(URL + Const.WX_ACCESS_TOKEN, jsonStr);System.out.println("微信推送模板消息结果ret----------> " + ret);JSONObject retObj = JSONObject.parseObject(ret);/** 返回状态码* 0:正常;* 40037:template_id不正确* 41028:form_id不正确,或者过期* 41029:form_id已被使用* 41030:page不正确*/String errCode = retObj.getString("errcode");//返回状态消息String errMsg = retObj.getString("errmsg");logger.info("weChatReturnJson:" + retObj.toJSONString() + ";msg:" + errMsg);if (StringUtils.isNotBlank(errCode) && "0".equals(errCode)) {// 推送成功logger.info("推送成功!");return errCode;} else {logger.error("微信模板消息推送失败!第二次尝试推送");//40001 获取access_token时AppSecret错误,或者access_token无效。请开发者认真比对AppSecret的正确性,或查看是否正在为恰当的公众号调用接口//42001 access_token超时,请检查access_token的有效期,请参考基础支持-获取access_token中,对access_token的详细机制说明if ("40001".equals(errCode) || "42001".equals(errCode)) {//getAccessTokenAndPush(model);}}} catch (Exception e) {logger.error("微信模板消息推送异常!");e.printStackTrace();}return "-1";}}
2. 实现WeChat消息模板的功能;
2.1 定义一个微信模板消息的Bean
本人这里并没有直接使用JSONObject组装json
package com.icex.bus.sys.entity;import java.util.Date;/*** 微信模板消息Bean* @author Jjcc* @version 1.0.0* @Description* @ClassName WeChatMessTempPara.java* @createTime 07月25日 12:52:00*/public class WeChatMessTempPara {/*** 受理时间*/private Date acceptTime;/*** 受理人*/private String dealAccountName;/*** 联系电话*/private String dealAccountPhone;/*** 工单内容*/private String flowContent;/*** 工单状态*/private String serverStatusName;/*** 进度说明*/private String progressExplain;/*** 处理状态*/private String dealStatus;/*** 当前进度*/private String presentProgress;/*** 工作内容*/private String dealContent;public Date getAcceptTime() {return acceptTime;}public void setAcceptTime(Date acceptTime) {this.acceptTime = acceptTime;}public String getDealAccountName() {return dealAccountName;}public void setDealAccountName(String dealAccountName) {this.dealAccountName = dealAccountName;}public String getDealAccountPhone() {return dealAccountPhone;}public void setDealAccountPhone(String dealAccountPhone) {this.dealAccountPhone = dealAccountPhone;}public String getFlowContent() {return flowContent;}public void setFlowContent(String flowContent) {this.flowContent = flowContent;}public String getServerStatusName() {return serverStatusName;}public void setServerStatusName(String serverStatusName) {this.serverStatusName = serverStatusName;}public String getProgressExplain() {return progressExplain;}public void setProgressExplain(String progressExplain) {this.progressExplain = progressExplain;}public String getDealStatus() {return dealStatus;}public void setDealStatus(String dealStatus) {this.dealStatus = dealStatus;}public String getPresentProgress() {return presentProgress;}public void setPresentProgress(String presentProgress) {this.presentProgress = presentProgress;}public String getDealContent() {return dealContent;}public void setDealContent(String dealContent) {this.dealContent = dealContent;}@Overridepublic String toString() {return "WeChatMessTempPara{" +"acceptTime=" + acceptTime +", dealAccountName='" + dealAccountName + '\'' +", dealAccountPhone='" + dealAccountPhone + '\'' +", flowContent='" + flowContent + '\'' +", serverStatusName='" + serverStatusName + '\'' +", progressExplain='" + progressExplain + '\'' +", dealStatus='" + dealStatus + '\'' +", presentProgress='" + presentProgress + '\'' +", dealContent='" + dealContent + '\'' +'}';}}
2.2 定义一个微信消息模板的接口
package com.icex.frame.utils.wechat.factory;import com.alibaba.fastjson.JSONObject;import com.icex.bus.sys.entity.SysFlow;import com.icex.bus.sys.entity.WeChatMessTempPara;/*** 模板接口* @author Jjcc* @version 1.0.0* @Description* @ClassName ITempParam.java* @createTime 07月24日 23:57:00*/public interface ITempParam {/*** 组合消息模板方法* @title createTempParam* @description* @author Jjcc* @param tempPara* @return com.alibaba.fastjson.JSONObject* @createTime /9/18 15:45* @throws*/JSONObject createTempParam(WeChatMessTempPara tempPara);}
2.3 定义具体的模板类
具体的模板类实现了克隆;
这里使用了设计模式的思想;面向接口开发,而不是具体的实现类。对扩展开发,对修改关闭;
package com.icex.frame.utils.wechat.factory.impl;import com.alibaba.fastjson.JSONObject;import com.icex.bus.sys.entity.TempParamModel;import com.icex.bus.sys.entity.WeChatMessTempPara;import com.icex.frame.utils.wechat.factory.ITempParam;/*** 工单处理中消息模板参数* @author Jjcc* @version 1.0.0* @Description* @ClassName BeingDisposeTempParamImpl.java* @createTime 07月25日 00:05:00*/public class BeingDisposeTempParamImpl implements ITempParam,Cloneable {@Overridepublic JSONObject createTempParam(WeChatMessTempPara tempPara) {JSONObject json = new JSONObject();json.put("keyword1", new TempParamModel(tempPara.getProgressExplain()));json.put("keyword2", new TempParamModel(tempPara.getDealStatus()));json.put("keyword3", new TempParamModel(tempPara.getPresentProgress()));json.put("keyword4", new TempParamModel(tempPara.getDealContent()));return json;}@Overridepublic BeingDisposeTempParamImpl clone() {BeingDisposeTempParamImpl tempParam = null;try {tempParam = (BeingDisposeTempParamImpl) super.clone();} catch (CloneNotSupportedException e) {e.printStackTrace();}return tempParam;}}
package com.icex.frame.utils.wechat.factory.impl;import com.alibaba.fastjson.JSONObject;import com.icex.bus.sys.entity.TempParamModel;import com.icex.bus.sys.entity.WeChatMessTempPara;import com.icex.frame.utils.wechat.factory.ITempParam;/*** 工单处理完成-消息模板参数* @author Jjcc* @version 1.0.0* @Description* @ClassName DisposeFinishTempParaImpl.java* @createTime 07月25日 00:09:00*/public class DisposeFinishTempParaImpl implements ITempParam,Cloneable {@Overridepublic JSONObject createTempParam(WeChatMessTempPara tempPara) {JSONObject json = new JSONObject();json.put("keyword1", new TempParamModel(tempPara.getDealStatus()));System.out.println("tempPara.getServerStatusName(): " + tempPara.getServerStatusName());json.put("keyword2", new TempParamModel(tempPara.getDealAccountPhone()));json.put("keyword3", new TempParamModel(tempPara.getDealAccountName()));return json;}@Overridepublic DisposeFinishTempParaImpl clone() {DisposeFinishTempParaImpl tempParam = null;try {tempParam = (DisposeFinishTempParaImpl) super.clone();} catch (CloneNotSupportedException e) {e.printStackTrace();}return tempParam;}}
2.4 定义一个用于创建具体的消息模板对象的工厂
该工厂使用的是静态工厂模式,通过枚举类型来决定创建具体的消息模板对象,同时也使用了克隆模式来优化频繁的创建对象;
package com.icex.frame.utils.wechat.factory;import com.icex.frame.utils.wechat.factory.impl.BeingDisposeTempParamImpl;import com.icex.frame.utils.wechat.factory.impl.DisposeFinishTempParaImpl;import com.icex.frame.utils.wechat.factory.impl.OrderReceivingTempParamImpl;import javax.validation.constraints.NotNull;/*** 微信模板消息工厂* @author Jjcc* @version 1.0.0* @Description* @ClassName WeChatTempMsgFactory.java* @createTime 07月26日 10:50:00*/public class WeChatTempMsgFactory {private static OrderReceivingTempParamImpl orderReceivingTempParam;private static BeingDisposeTempParamImpl beingDisposeTempParam;private static DisposeFinishTempParaImpl disposeFinishTempPara;/*** 静态工厂方法,通过参数返回指定的类实例对象* @title getTempMsgIns* @description* @author Jjcc* @param type* @return com.icex.frame.utils.wechat.factory.ITempParam* @createTime /7/26 14:36* @throws*/public static ITempParam getTempMsgIns(@NotNull WeChatTempMsgType type) {ITempParam tempParam = null;switch (type) {case ORDER_RECEIVING:if (null == orderReceivingTempParam) {orderReceivingTempParam = new OrderReceivingTempParamImpl();tempParam = orderReceivingTempParam;} else {tempParam = orderReceivingTempParam.clone();}break;case BEING_DISPOSE:if (null == beingDisposeTempParam) {beingDisposeTempParam = new BeingDisposeTempParamImpl();tempParam = beingDisposeTempParam;} else {tempParam = beingDisposeTempParam.clone();}break;case DISPOSE_FINISH:if (null == disposeFinishTempPara) {disposeFinishTempPara = new DisposeFinishTempParaImpl();tempParam = disposeFinishTempPara;} else {tempParam = disposeFinishTempPara.clone();}break;default:}return tempParam;}}
2.5 枚举类
用于限制工厂方法创建对象所传入的参数类型;
package com.icex.frame.utils.wechat.factory;/*** 选择微信模板消息参数* @author Jjcc* @version 1.0.0* @Description* @ClassName WeChatTempMsgType.java* @createTime 07月26日 14:45:00*/public enum WeChatTempMsgType {/*** 工单受理-模板消息参数*/ORDER_RECEIVING,/*** 工单处理中消息模板参数*/BEING_DISPOSE,/*** 工单处理完成-消息模板参数*/DISPOSE_FINISH,}
3. WeChat令牌刷新功能
微信的accept_token存活时间只有7200S,超过7200S该令牌就会失效,必须在令牌有效期内访问Wechat的API重新刷新accpet_token;我的解决思路是使用Spring中的定时任务组件import org.quartz.Job来定时请求WeChat提供的API来刷新令牌存活时间;
关于quartz这里不细说,配置quartz时,设置为项目启动成功后便执行定时任务;
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="/schema/beans"xmlns:mvc="/schema/mvc" xmlns:xsi="/2001/XMLSchema-instance"xmlns:p="/schema/p" xmlns:context="/schema/context"xsi:schemaLocation="/schema/beans /schema/beans/spring-beans-3.2.xsd /schema/context /schema/context/spring-context-3.2.xsd /schema/mvc /schema/mvc/spring-mvc-3.2.xsd"><!-- 项目用了数据库持久化,没用此配置文件的方式,供参考 --><!-- 启动触发器的配置开始 --><!-- 总管理类 如果将lazy-init='false'那么容器启动就会执行调度程序 --><bean name="startQuertz" lazy-init="false" autowire="no"class="org.springframework.scheduling.quartz.SchedulerFactoryBean"><property name="triggers"><list><ref bean="autoAccessTokenJobTrigger"/><ref bean="autoWechatFormIdJobTrigger"/></list></property><!--applicationContextSchedulerContextKey: 是org.springframework.scheduling.quartz.SchedulerFactoryBean这个类中把spring上下 文以key/value的方式存放在了quartz的上下文中了,可以用applicationContextSchedulerContextKey所定义的key得到对应的spring上下文--> <property name="applicationContextSchedulerContextKey" value="applicationContextKey"/></bean><!-- 启动触发器的配置结束 --><!-- 调度配置 --><!-- 定时任务1H执行一次刷新accessToken存活时间; --><bean id="autoAccessTokenJobTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean"><property name="jobDetail" ref="autoAccessTokenServerJobDetail" /><property name="startDelay" value="10000" /> <property name="repeatInterval" value="3600000" /></bean><bean name="autoAccessTokenServerJobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean"><property name="jobClass" value="com.icex.bus.job.WxAccessTokenJob" /><property name="durability" value="true" /></bean><!-- 调度配置结束 --><!-- 调度配置 --><!-- 定时任务24H执行一次删除t_sys_formid表中创建时间超过7day的数据; --><bean id="autoWechatFormIdJobTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean"><property name="jobDetail" ref="autoFormIdServerJobDetail" /><property name="cronExpression" value="0 0 0 1/1 * ?" /></bean><bean name="autoFormIdServerJobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean"><property name="jobClass" value="com.icex.bus.job.WeChatFormIdJob" /><property name="durability" value="true" /></bean><!-- 调度配置结束 --></beans>
package com.icex.bus.job;import mons.lang3.StringUtils;import org.quartz.Job;import org.quartz.JobExecutionContext;import com.alibaba.fastjson.JSONObject;import mon.Const;import com.icex.frame.utils.DateUtil;import com.icex.frame.utils.HttpClientUtil;/*** 刷新微信accessToken;* accessToken的存活时间:7200Second;* 定时任务3200Second执行一次刷新accessToken存活时间;* 服务器重启后自动执行;* @description* @author Jjcc* @createTime /7/24 20:55*/public class WxAccessTokenJob extends BaseJob implements Job {private static final String ACCESS_TOKEN_URL = "https://api./cgi-bin/token?grant_type=client_credential&appid=" + Const.WX_APP_ID + "&secret=" + Const.WX_APP_SECRET;@Overridepublic void execute(JobExecutionContext arg0) {System.out.println("------------获取微信token--------------");try {int i = 1;while(i < 11) {i++;String ret = HttpClientUtil.doGet(ACCESS_TOKEN_URL, null);JSONObject json = JSONObject.parseObject(ret);String errcode = json.getString("errcode");if ("0".equals(errcode) || StringUtils.isBlank(errcode)) {//请求成功Const.WX_ACCESS_TOKEN = json.getString("access_token");System.out.println("时间:" + DateUtil.getCurrentTime() + "刷新token成功;token为---> " + Const.WX_ACCESS_TOKEN);break;}}System.out.println("本次刷新token使用次数为:---> " + (i-1));} catch (Exception e) {e.printStackTrace();}}}
4. formeId问题
小程序中,生成的formId只能使用一次,且每个formeId的生命期只有7day,创建时间超过7day的formeId无法成功使用;
这里本人写了一个定时任务,每天的凌晨12点会删除存储formId表中超过7day的formId;具体的代码就不贴了,定时任务的配置同令牌刷新一起贴出了;
5. 实例
该方法通过openId找到对应formId,并执行工具类中封装的方法,这里不管推送是否成功,都会删除拿到的formId;
/*** 微信模板消息推送* @title WeChatMsgPush* @description* @author Jjcc* @param openId* @param informTempId 微信消息模板Id* @param page 跳转的页面* @param weChatMessTempPara 模板消息参数对象* @param msgTempParaType 模板类型* @return void* @createTime /7/26 12:01* @throws*/public void weChatMsgPush(String openId, String informTempId, String page, WeChatMessTempPara weChatMessTempPara, WeChatTempMsgType msgTempParaType, ISysFormidService formidService) {try {SysFormid sysFormid = new SysFormid();sysFormid.setOpenId(openId);List<SysFormid> sysFormidList = formidService.select(sysFormid);if (0 != sysFormidList.size()) {String formId = sysFormidList.get(0).getFormId();if (StringUtils.isNotBlank(openId) && StringUtils.isNotBlank(formId)) {WeChatToolUtil toolUtil = WeChatToolUtil.getInstance();ITempParam tempMsgIns = WeChatTempMsgFactory.getTempMsgIns(msgTempParaType);JSONObject jsonObject = tempMsgIns.createTempParam(weChatMessTempPara);String resultStr = toolUtil.pushMessage(openId, informTempId, formId, page, jsonObject);//删除formIdExample example = new Example(SysFormid.class);example.createCriteria().andEqualTo("formId", formId);int i = formidService.deleteByExample(example);logger.error("微信模板消息推送结果:" + resultStr);logger.error("删除formId结果:" + i);} else {logger.error("微信模板消息推送失败------->formId或openId参数为空!");}} else {logger.error("当前openId无可用的formId");}} catch (Exception e) {e.printStackTrace();}}
使用方式
模板参数实体beanWeChatMessTempPara weChatMessTempPara = new WeChatMessTempPara();weChatMessTempPara.setAcceptTime(now);weChatMessTempPara.setServerStatusName("已受理");weChatMessTempPara.setFlowContent(obj.getFlowContent());/*** openId:微信账号唯一标识符;* ACCEPT_INFORM:微信的消息模板Id;* PAGES:点击推送的消息后跳转至小程序中的页面;* weChatMessTempPara:模板参数实体bean* WeChatTempMsgType.ORDER_RECEIVING:选择何种模板* sysFormidService:用于获取openId拥有的formId的对象*/weChatMsgPush(openId, ACCEPT_INFORM, PAGES + id, weChatMessTempPara, WeChatTempMsgType.ORDER_RECEIVING, sysFormidService);
end!!!