前段时间因公司业务需要,进行了微信公众号相关功能的开发,在此之前这方面我也是没有接触过,所以做的过程中也踩了很多坑,查了不少资料,应小伙伴要求,就把代码贴出来,做个记录,也方便以后再开发这方面的功能。
首先是几个model类
静态常量类,这里是一些微信公众号的几个核心信息
public class WechatConstants {
public static final String APPID = "xxxxxxxx";
public static final String APPSECRET = "xxxxxxxx";
private static String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
public static String getAccess_token_url(){
return ACCESS_TOKEN_URL.replace("APPID",APPID).replace("APPSECRET",APPSECRET);
}
}
access_token实体类
public class AccessToken {
private String access_token;
private int expires_in;
public AccessToken(String access_token, int expires_in) {
this.access_token = access_token;
this.expires_in = expires_in;
}
public String getAccess_token() {
return access_token;
}
public void setAccess_token(String access_token) {
this.access_token = access_token;
}
public int getExpires_in() {
return expires_in;
}
public void setExpires_in(int expires_in) {
this.expires_in = expires_in;
}
}
微信模板实体类(显示内容)
public class WxTemplate {
private String template_id;
private String touser;
private String url;
private String topcolor;
private Map<String, TemplateData> data;
public String getTemplate_id() {
return template_id;
}
public void setTemplate_id(String template_id) {
this.template_id = template_id;
}
public String getTouser() {
return touser;
}
public void setTouser(String touser) {
this.touser = touser;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getTopcolor() {
return topcolor;
}
public void setTopcolor(String topcolor) {
this.topcolor = topcolor;
}
public Map<String, TemplateData> getData() {
return data;
}
public void setData(Map<String, TemplateData> data) {
this.data = data;
}
@Override
public String toString() {
return "WxTemplate [template_id=" + template_id + ", touser=" + touser + ", url=" + url + ", topcolor="
+ topcolor + ", data=" + data + "]";
}
}
模板消息中的一个数据的实体类,比如{{first.DATA}}
public class TemplateData {
private String value;
private String color;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
@Override
public String toString() {
return "TemplateData [value=" + value + ", color=" + color + "]";
}
}
然后是两个核心util类
用于检查证书
public class MyX509TrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateEncodingException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateEncodingException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
发起https请求工具类
public class WeixinUtil {
private static Logger log = LoggerFactory.getLogger(WeixinUtil.class);
public static JSONObject httpRequest(String requestUrl, String requestMethod, String outputStr) {
JSONObject jsonObject = null;
StringBuffer buffer = new StringBuffer();
try {
TrustManager[] tm = { new MyX509TrustManager() };
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
sslContext.init(null, tm, new java.security.SecureRandom());
SSLSocketFactory ssf = sslContext.getSocketFactory();
URL url = new URL(requestUrl);
HttpsURLConnection httpUrlConn = (HttpsURLConnection) url.openConnection();
httpUrlConn.setSSLSocketFactory(ssf);
httpUrlConn.setDoOutput(true);
httpUrlConn.setDoInput(true);
httpUrlConn.setUseCaches(false);
httpUrlConn.setRequestMethod(requestMethod);
if ("GET".equalsIgnoreCase(requestMethod))
httpUrlConn.connect();
if (null != outputStr && outputStr != "") {
OutputStream outputStream = httpUrlConn.getOutputStream();
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
InputStream inputStream = httpUrlConn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
inputStream = null;
httpUrlConn.disconnect();
jsonObject = JSONObject.fromObject(buffer.toString());
} catch (ConnectException ce) {
log.error("Weixin server connection timed out.");
} catch (Exception e) {
log.error("https request error:{}", e);
}
return jsonObject;
}
}
最后就是调用微信官方接口的util类
调用官方接口工具类
@Component
public class SendTemplateUtil {
private static Logger log = LoggerFactory.getLogger(SendTemplateUtil.class);
public static AccessToken accessToken = null;
@Autowired
private RedisUtils redisUtils;
@Scheduled(fixedDelay = 2*2700*1000)
public void getAccessToken(){
JSONObject accessTokenJson = WeixinUtil.httpRequest(WechatConstants.getAccess_token_url(), "GET", null);
System.out.println(accessTokenJson.toString());
String access_token = accessTokenJson.getString("access_token");
int expires_in = accessTokenJson.getInt("expires_in");
log.info("成功获取access_token:"+access_token);
accessToken = new AccessToken(access_token,expires_in);
}
public int WeiXinSend(WxTemplate wxTemplate) throws Exception{
String maxCount = redisUtils.get("maxCount", 1);
if(StringUtils.isNoneBlank(maxCount)){
if(Integer.parseInt(maxCount)>500){
throw new Exception("accesstoken获取次数频繁");
}
}
Integer result = 0;
String access_token = accessToken.getAccess_token();
log.info("access_token**********"+access_token);
log.info("执行微信公众号发送模板消息方法**********");
String url = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token="+access_token;
String jsonString = JSONObject.fromObject(wxTemplate).toString();
JSONObject jsonObject = WeixinUtil.httpRequest(url, "POST", jsonString);
if (null != jsonObject) {
if (0 != jsonObject.getInt("errcode")) {
result = jsonObject.getInt("errcode");
log.error("错误 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));
if(result.equals(40001)){
Integer incr = redisUtils.incr("accesstoken_req_count");
if(incr>5){
try {
log.error("重新获取access_token:");
getAccessToken();
} catch (Exception e) {
}
redisUtils.del(1,"accesstoken_req_count");
if(StringUtils.isBlank(maxCount)){
redisUtils.set("maxCount", "0", 1);
redisUtils.expire("maxCount", DateUtil.getSeconds(), 1);
}
redisUtils.incr("maxCount");
}
}
}
}
log.info("模板消息发送结果(0代表发送成功):"+result);
return result;
}
public int verifyAttention(String openId){
int subscribe = -1;
String access_token = accessToken.getAccess_token();
log.info("执行验证用户是否关注公众号方法**********");
String url = "https://api.weixin.qq.com/cgi-bin/user/info?access_token="+access_token+"&openid="+openId+"&lang=zh_CN";
JSONObject jsonObject = WeixinUtil.httpRequest(url, "GET", "");
if (null != jsonObject){
if (jsonObject.has("errcode")){
log.error("错误 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));
}else {
subscribe = jsonObject.getInt("subscribe");
System.out.println(jsonObject.getInt("subscribe")+"--------------"+subscribe);
log.info("****************验证返回结果:", jsonObject.getInt("subscribe"));
}
}
return subscribe;
}
public String getUserOpenId(String code){
try {
System.out.println(WechatConstants.APPID);
String appId = WechatConstants.APPID;
String appSecret = WechatConstants.APPSECRET;
log.info("执行微信授权获取用户openId方法**********");
String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid="+appId+"&secret="+appSecret+"&code="+code+"&grant_type=authorization_code";
String jsonString = "";
String access_token = "";
String openid = "";
JSONObject jsonObject = WeixinUtil.httpRequest(url, "POST", jsonString);
if (null != jsonObject) {
Iterator<String> it = jsonObject.keys();
while(it.hasNext()){
String key = it.next();
String value = jsonObject.getString(key);
log.info("key: "+key+",value:"+value);
}
openid = jsonObject.getString("openid");
access_token = jsonObject.getString("access_token");
}
return openid;
} catch (Exception e) {
return null;
}
}
}
微信公众号后台的IP白名单和授权域名别忘了配置哈!access_token也可以选择存到redis,定时去刷新redis,我这里是写了一个全局变量。获取access_token每天次数是有限的哦,不能超过两千次,access_token是请求微信第三方接口的重要凭证,如果失效了很多请求都会失败哦!看一下access_token官方的说明:
踩过的坑
1.access_token不定期会失效,按理说access_token失效时间还没到的时候我这边就会定时刷新access_token,为什么会失效呢?很明显就是定时器刷新之前access_token就失效了,官方不是说失效时间2小时吗,怎么还没到就不间断失效呢?这里注意看官方一个很重要的说明:
五分钟内新老access_token都可以用,过了五分钟老的就失效了,经过排查才知道,因为其他系统也去刷新了access_token,所以导致我这边的失效了。
2.由于微信公众号授权域名最多只能配置两个,但是系统数量较多,每个系统都需要用同一个公众号进行微信授权登录。怎么办呢?那就公用一个授权域名,将授权页面、js、css等静态文件从项目中抽出来,单独做成一个纯前端的web项目,然后放到web容器中,我是将整个web项目放到了Nginx服务器(一款轻量级的Web服务器、反向代理服务器)上面,这样不仅解决了这个问题,还可以保证再静态资源内容改变的时候后台不用重新打包发版。我这边是做了一个关键字自动回复,然后点击回复链接跳转登录页进行静默授权,如果你要获取用户微信的信息,可以采用非静默授权的方式。下面是核心的js代码:
var appid = 'xxxxxx';
var href = window.location.href;
var code = getUrlParam('code');
var openid = '';
if(code){
$.ajax({
url: 'https://配置的授权域名/loginapp/getUserOpenId',
type:'post',
dataType:'json',
data:{code:code},
success:function(datas){
if(datas.code>0){
console.log("用户openid获取成功:"+datas.data);
openid = datas.data;
$.ajax({
type:"post",
url:"https:///配置的授权域名/loginapp/verifyAttendation",
dataType:'json',
data:{openId:openid},
success:function(datas){
if(datas.code>0){
console.log("验证结果:"+datas.data);
var subscribe = datas.data;
if(subscribe == 0){
console.log("没有关注公众号");
window.location.href = "auth.html";
}else if(subscribe == 1){
console.log("已经关注公众号");
}else{
console.log("错误判断返回")
}
}else{
console.log("验证失败!!!");
}
}
});
}else{
console.log("用户openid获取失败");
}
}
});
}else{
window.location.href = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid='+appid+'&redirect_uri='+encodeURIComponent(href)+'&response_type=code&scope=snsapi_base&state=1#wechat_redirect'
}
function getUrlParam(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if (r != null) return unescape(r[2]);
return null;
}
$(function(){
$('#auth-btn').on('click',function(){
var accountVal = $('#account').val(),
pwdVal = $('#pwd').val();
if(fnCheckAccount(accountVal) && fnCheckPwd(pwdVal)){
if(openid){
$.ajax({
url: 'https://配置的授权域名/loginapp/authorization',
type:'post',
dataType:'json',
data:{account:accountVal,password:hex_md5(pwdVal),openId:openid},
success:function(datas){
if(datas.data>0){
console.log("授权成功!")
window.location.href = "success.html";
}else{
console.log("授权失败!")
window.location.href = "fail.html";
}
}
});
} else{
wDialog.toast({
msg: "网络慢,请重试。"
})
}
}
});
});
3.这也是一个让我最蛋疼的坑,微信模板消息发送在access_token未失效的情况,发送接口间歇性出现40001错误,这个错误官方说明是access_token失效或者appsecert错误,经过排查。appsecert是没错的,那么就一定是access_token失效咯,经过再一次排查,我发现模板消息推送失败几次之后又会成功,而失败的时候和成功的时候access_token是一样,那就说明access_token也没有失效,那真是见鬼了哦,查了很多资料都没找到解决方法,都说这是微信官方的bug,怎么办呢?问题总是要解决的,于是我就换一个思路。因为是间接性发送失败,所以我就加了重试机制,发送失败就重新发送,并且记录该条模板消息重发的次数,如果重发次数大于5次,就自动重新刷新一次access_token,因为获取access_token每天的次数有限,所以还会记录自动刷新access_token的次数,如果大于500次就不再自动刷新access_token了(定时器任务刷新access_token还是会执行的)并且将失败的消息记录日志。具体代码也在上面有。如果你的消息可以非实时发送,可以将失败的消息放入消息队列进行异步处理。