微信公众号相关开发

前段时间因公司业务需要,进行了微信公众号相关功能的开发,在此之前这方面我也是没有接触过,所以做的过程中也踩了很多坑,查了不少资料,应小伙伴要求,就把代码贴出来,做个记录,也方便以后再开发这方面的功能。

首先是几个model类

静态常量类,这里是一些微信公众号的几个核心信息

public class WechatConstants {
    //公众号appid
	public static final String APPID = "xxxxxxxx";
	  
    //公众号appsecert
	public static final String APPSECRET = "xxxxxxxx";
    
    //获取access_token_Url
    private static String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
   
    //将appid与appsecert填入后得到的URL
    public static String getAccess_token_url(){
        return ACCESS_TOKEN_URL.replace("APPID",APPID).replace("APPSECRET",APPSECRET);
    }
    
    
}

access_token实体类

public class AccessToken {
    //access_toekn凭证
    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 {
    /**
     * 模板消息id
     */
    private String template_id;
    /**
     * 用户openId
     */
    private String touser;
    /**
     * URL置空,则在发送后,点击模板消息会进入一个空白页面(ios),或无法点击(android)
     */
    private String url;
    /**
     * 标题颜色
     */
    private String topcolor;
    /**
     * 详细内容(map中的第一个元素,也就是标题数据,key为first,最后一个元素,也就是模板结尾,key为remark,
     * 中间的从第二个开始依次key为keyword1,keyword2.。。。。。)
     */
    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 {

    }

    /**
     * 返回收信任的X509证书数组
     */
    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return null;
    }
}

发起https请求工具类

public class WeixinUtil {
    private static Logger log = LoggerFactory.getLogger(WeixinUtil.class);

    /**
     * 发起https请求并获取结果
     *
     * @param requestUrl 请求地址
     * @param requestMethod 请求方式(GET、POST)
     * @param outputStr 提交的数据
     * @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值)
     */
    public static JSONObject httpRequest(String requestUrl, String requestMethod, String outputStr) {
        JSONObject jsonObject = null;
        StringBuffer buffer = new StringBuffer();
        try {
            // 创建SSLContext对象,并使用我们指定的信任管理器初始化
            TrustManager[] tm = { new MyX509TrustManager() };
            SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
            sslContext.init(null, tm, new java.security.SecureRandom());
            // 从上述SSLContext对象中得到SSLSocketFactory对象
            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);
            // 设置请求方式(GET/POST)
            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;
    

    //定时任务,定时刷新access_token,90分钟执行一次,token过期时间为两个小时
    @Scheduled(fixedDelay = 2*2700*1000)
    public void getAccessToken(){
        //获取微信服务器返回的json
        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{
    	//从redis获取当天手动获取accesstoken的数量
    	String maxCount = redisUtils.get("maxCount", 1);
    	//判断是否为空
    	if(StringUtils.isNoneBlank(maxCount)){
    		//如果手动获取次数大于500就抛异常发邮件		
    		if(Integer.parseInt(maxCount)>500){
    			//"accesstoken获取次数频繁"
    			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"));
                //判断当返回参数是40001(accesstoken失效状态码)时重新获取accesstoken
                if(result.equals(40001)){
                	//在redis中设置自增键值
                	Integer incr = redisUtils.incr("accesstoken_req_count");              	
                	//当出现状态码失效次数为5次重新获取一次access_token
                	if(incr>5){
                		try {
                			//打印日志
                            log.error("重新获取access_token:");
                            //重新获取access_token的方法
                			getAccessToken();
						} catch (Exception e) {
							//捕获异常当无论请求是否成功也要清除redis中的键值
						} 
                		//删除键值
                		redisUtils.del(1,"accesstoken_req_count"); 
                		//判断是否今天第一次手动获取
                		if(StringUtils.isBlank(maxCount)){
                			//设置键
                			redisUtils.set("maxCount", "0", 1);
                			//设置生存时间--生存时间为今天还剩下多少秒
                    		redisUtils.expire("maxCount", DateUtil.getSeconds(), 1);			              			
                		}              
                		//自增加1
                		redisUtils.incr("maxCount");              				
                	}                	
                }     
            }
        }        
        log.info("模板消息发送结果(0代表发送成功):"+result);
        return result;
    }

    //判断用户是否关注微信公众号verifyAttention
    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;
    }

    //微信授权获取用户openId
    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()){
                    // 获得key
                    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';//这里是微信公众号的appid,跟你后台常量类里面写的一样
    var href = window.location.href;
    var code = getUrlParam('code');
    var openid = '';
    if(code){
        //获取到code之后用code去获取用户的openid
        $.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;
                   //获取openid之后再去判断啊该用户有没有关注微信公众号
                   $.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{
	//这里是点击回复链接跳转之后第三方回调的一个地址,获取code
        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; //返回参数值
    }
    
    //输入账号密码并且绑定保存用户openid
	$(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还是会执行的)并且将失败的消息记录日志。具体代码也在上面有。如果你的消息可以非实时发送,可以将失败的消息放入消息队列进行异步处理。

猜你喜欢

转载自blog.csdn.net/w1453114339/article/details/106424953