概述

公众号类型功能介绍

微信官方介绍
公众号,是订阅号和服务号的统称。订阅号是公众号、服务号也是公众号。

订阅号:偏于为用户传达资讯,每天只可以发送一条群发,用户不会收到即时消息提醒,因为所有的订阅号都被收在了订阅号文件夹中。订阅号适用于个人和组织。
想看一个订阅号的消息,有两种方法:

  • 打开订阅号文件夹查看订阅号有没有更新消息;
  • 打开微信手机通讯录,点开公众号选项,是你关注的所有公众号(包括订阅号和服务号)。

服务号:偏于服务交互(如:招商银行信用卡、微信读书、京东白条),每月可群发四条消息,收费,不适用于个人,微信支付需要微信认证服务号才可以。

选择:
想简单的发送消息,达到宣传效果,建议选择订阅号。
想用公众号获得更多功能,如开通微信支付,建议选择服务号。

服务号、订阅号功能对比:

微信开放平台、公众平台区别

  • 公众平台
    官网
    技术文档
    微信公众平台是运营者通过公众号为微信用户提供资讯和服务的平台,登录公众平台账号后,可以看到它有一个不错的交互界面。可以提供给公司的运营人员使用,用来发布消息和提供服务。

  • 开放平台
    官网
    是为开发者(程序员)提供的一个平台,在这里你可以将你的公众平台下的公众号(订阅号、服务号)绑定到你的开放平台账号下,从而可以基于订阅号、服务号做更多的开发。公众号中的订阅号接口权限是有限的,比如它无法获得网页授权的权限,也就无法通过网页授权获取用户的基本信息(比如openID、unionID等)。

微信h5分享

参考地址

流程图:

准备:公众号、已经备案好的域名

步骤:

  1. 配置JS回调域名和ip白名单
  2. 获取appId和secret
  3. 从官方代码copy签名函数
  4. 获取access_token、ticket
  5. 获取url,并进行签名
  6. 集成进java web
  7. 前端config函数配置

获取appId和secret

可以在基本配置中找到AppID和AppSecret。

配置JS回调域名:
登录微信公众平台->公众号设置->功能设置->JS接口安全域名。填写你要分享的网页所在的域名。

配置ip白名单:
登录微信公众平台->基本配置->IP白名单。
必须配置ip白名单,否则无法获取access_token

从官方代码copy签名函数

打开微信分享官方文档,在页面最下面找到并下载示例代码
我们将java中的sign.java直接放到项目中备用。

公众号获取access_token官方文档
小程序获取access_token官方文档

获取access_token、ticket

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
public static WxShare getWxEntity(String url,StringRedisTemplate template) {
        String access_token = template.opsForValue().get("wx_base_access_token");
        String ticket = template.opsForValue().get("wx_jsapi_ticket");
        if(StringUtils.isEmpty(access_token)){
            System.out.println(">>>>>>>>>>>>>>>>>>>>>>重新获取access_token和ticket>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
            //已经超时,重新获取
            access_token = getAccessToken(template);
            //如果重新获取了access_token,ticket也要重新获取
            ticket = getTicket(access_token,template);
        }
        Map<String,String> resultMap = Sign.sign(ticket, url);
        WxShare wx = new WxShare();
        wx.setTicket(resultMap.get("jsapi_ticket"));
        wx.setSignature(resultMap.get("signature"));
        wx.setNoncestr(resultMap.get("nonceStr"));
        wx.setTimestamp(resultMap.get("timestamp"));
        return wx;
    }

    //获取token
    private static String getAccessToken(StringRedisTemplate template) {
        String access_token = "";
        //获取access_token填写client_credential
        String grant_type = "client_credential";
        //第三方用户唯一凭证
        String AppId= PayConstant.WX_H5_APPID;
        //第三方用户唯一凭证密钥,即appsecret
        String secret=PayConstant.WX_H5_APPSECRET;
        //访问链接,这个url链接地址和参数皆不能变
        String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type="+grant_type+"&appid="+AppId+"&secret="+secret;
        try {
            URL urlGet = new URL(url);
            HttpURLConnection http = (HttpURLConnection) urlGet.openConnection();
            http.setRequestMethod("GET"); // 必须是get方式请求
            http.setRequestProperty("Content-Type","application/x-www-form-urlencoded");
            http.setDoOutput(true);
            http.setDoInput(true);
            /*System.setProperty("sun.net.client.defaultConnectTimeout", "30000");// 连接超时30秒
            System.setProperty("sun.net.client.defaultReadTimeout", "30000"); // 读取超时30秒 */
            http.connect();
            InputStream is = http.getInputStream();
            int size = is.available();
            byte[] jsonBytes = new byte[size];
            is.read(jsonBytes);
            String message = new String(jsonBytes, "UTF-8");
            JSONObject demoJson = JSONObject.fromObject(message);
            access_token = demoJson.getString("access_token");
            //缓存到redis,一定要缓存,每天只能请求2000次
            template.opsForValue().set("wx_base_access_token",access_token,2, TimeUnit.HOURS);
            is.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return access_token;
    }

    //获取ticket
    private static String getTicket(String access_token,StringRedisTemplate template) {
        String ticket = null;
        //这个url链接和参数不能变
        String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token="+ access_token +"&type=jsapi";
        try {
            URL urlGet = new URL(url);
            HttpURLConnection http = (HttpURLConnection) urlGet.openConnection();
            http.setRequestMethod("GET"); // 必须是get方式请求
            http.setRequestProperty("Content-Type","application/x-www-form-urlencoded");
            http.setDoOutput(true);
            http.setDoInput(true);
            System.setProperty("sun.net.client.defaultConnectTimeout", "30000");// 连接超时30秒
            System.setProperty("sun.net.client.defaultReadTimeout", "30000"); // 读取超时30秒
            http.connect();
            InputStream is = http.getInputStream();
            int size = is.available();
            byte[] jsonBytes = new byte[size];
            is.read(jsonBytes);
            String message = new String(jsonBytes, "UTF-8");
            JSONObject demoJson = JSONObject.fromObject(message);
            ticket = demoJson.getString("ticket");
            //缓存到redis,一定要缓存,每天只能请求2000次
            template.opsForValue().set("wx_jsapi_ticket",ticket,2, TimeUnit.HOURS);
            is.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ticket;
    }

获取url,并进行签名

url的获取要特别注意,因为微信在分享时会在原来的url上加上一些&from=singlemessage、&from=…的,所以url必须要动态获取,网上有些用ajax,在网页通过“location.href.split(‘#’)“ 获取url,在用ajax传给后台,这种方法可行,但是不推荐,一方面用ajax返回,就要访问分享的逻辑,这样后台分享的逻辑增加复杂度,带来不便,是代码不易于维护,可读性低!另一方面分享是返回页面,而ajax是返回json,又增加了复杂度。所以,一个java程序员是不会通过ajax从前台获取url的,这里我们用HttpRequest的方法即可,不管微信加多少后缀,都可以获取到完整的当前url。

获取url的controller如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@PostMapping("/share")
public ResponseUtil getQuestionList(@RequestBody Map<String,String> paramMap){
	LOGGER.info(
	"\n***************************************" + "\n" +
			"start share" + "\n" +
			"分享" + "\n" +
			"\n********************************************"
	);
	ResponseUtil response = ResponseUtil.success();
	CodeEnum code = CodeEnum.FAIL;
	try {
		LOGGER.info("前端传过来的>>>>>>>>>>>>>>>>>>>>>>>>>><<<<<<<<<<<<<<<<<<<<<<<<<<<:" + paramMap.get("strUrl"));
		LOGGER.info("URI解码>>>>>>>>>>>>>>>>>>>>>>>>>><<<<<<<<<<<<<<<<<<<<<<<<<<<:" + URI.create(paramMap.get("strUrl")).getPath());
		response.setData(wxUtil.getWxEntity(URI.create(paramMap.get("strUrl")).getPath(),redisTemplate));
	} catch (Exception e) {
		e.printStackTrace();
		response.setCode(code);
		response.setMessage(e.getMessage());
	}
	return response;
}

需要注意:这个url是前端动态获取的,不能写死。如果带参数,或是中文,前端要对url做encodeURIComponent转码,后台再使用URI.create(paramMap.get("strUrl")).getPath()进行解码。

我们再新建一个存放微信信息的实体类:wx.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class wx {

    private String access_token;
    private String ticket ;
    private String noncestr;
    private String timestamp;
    private String str;
    private String signature;

   ...setter and getter...
}

sign类里面有一个签名的方法sign,传入ticket和url即可。也就是WeinXinUtil的getWinXinEntity方法,并将返回的map的信息读取存入WinXinEntity中。
在调试时,把sign返回的map打印出来,主要看看生成的signature。然后,把jsapi_ticket、noncestr、timestamp、url 复制到微信提供的微信 JS 接口签名校验工具
比较代码签名生成的signature与校验工具生成的签名signature是否一致,如果一致,说明前面的步骤都是正确的,如果不一致,仔细检查。

controller

我们把微信分享分解成3个工具类,现在在处理分享的controller,只要两句话就可以调用微信分享,一句获取url,一句获取WinXinEntity,代码已经在上边给出了。

前端config函数配置

下面的代码放在网页js代码的最前面!
”var url = location.href.split(‘#’)[0];“ 页面的url也可以从后台传过来,也可以通过location.href.split(‘#’)[0]获取。
为了一点微不足道的速度,这里才用网页获取方式。(网页的url跟前面的后台签名时得url是一样的,只是绕过了ajax)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<script src="http://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
<script>
var url = location.href.split('#')[0];
    wx.config({
    debug: false,
    appId: 'xxxxxxxxxxxxxx',
    timestamp: "${wx.timestamp}",
    nonceStr: "${wx.noncestr}",
    signature: "${wx.signature}",
    jsApiList: [
      // 所有要调用的 API 都要加到这个列表中
       'checkJsApi',
       'onMenuShareTimeline',
       'onMenuShareAppMessage',
    ]
  });
  wx.ready(function () {
    // 在这里调用 API
     wx.onMenuShareTimeline({
    title: 'xxxxxxxxxx', // 分享标题
    link: url, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
    imgUrl: 'xxxxxxxxxxxxxx', // 分享图标
    success: function () {
        // 用户确认分享后执行的回调函数
    },
    cancel: function () {
        // 用户取消分享后执行的回调函数
    }
});
wx.onMenuShareAppMessage({
   title: 'xxxxxxxxxxx', // 分享标题
    desc: 'xxxxxxxxxxx', // 分享描述
    link: url, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
    imgUrl: 'xxxxxxxxxx', // 分享图标
    type: '', // 分享类型,music、video或link,不填默认为link
    dataUrl: '', // 如果type是music或video,则要提供数据链接,默认为空
    success: function () {
        // 用户确认分享后执行的回调函数
    },
    cancel: function () {
        // 用户取消分享后执行的回调函数
    }
});
});
</script>

分享链接带参数的情况

前端的链接如果带参数,www.xxx.com/xxx?a=1&b=2,传给后台必须使用encodeURIComponent转码,参考官方文档
但是官方文档并没有说,后台要把前端传的url再解码!即:
URI.create(paramMap.get("strUrl")).getPath()
不要使用URLDecoder.decode(paramMap.get("strUrl")),该方法将要被废弃。

不带参数可以不转码,若带参数必须转码,后台再解码!!
参考地址

微信小程序登录

官方文档
登录流程时序图

微信的文档写的像狗屎,所以我抄了一份时序图:

大致流程如下:

  • 程序客户端调用wx.login,回调里面包含js_code。
  • 然后将js_code发送到服务器A(开发者服务器),服务器A向微信服务器发起请求附带js_code、appId、secretkey和grant_type参数,以换取用户的openid和session_key(会话密钥)。
  • 服务器A拿到session_key后,生成一个随机数我们叫3rd_session,以3rdSessionId为key,以session_key + openid为value缓存到redis或memcached中;因为微信团队不建议直接将session_key在网络上传输,由开发者自行生成唯一键与session_key关联。其作用是:
    • 将3rdSessionId返回给客户端,维护小程序登录态。
    • 通过3rdSessionId找到用户session_key和openid。
  • 客户端拿到3rdSessionId后缓存到storage。
  • 客户端通过wx.getUserIinfo可以获取到用户敏感数据encryptedData。
  • 客户端将encryptedData、3rdSessionId和偏移量一起发送到服务器A
  • 服务器A根据3rdSessionId从缓存中获取session_key
  • 在服务器A使用AES解密encryptedData,从而实现用户敏感数据解密

重点在6、7、8三个环节。
AES解密三个参数:

  • 密文 encryptedData
  • 密钥 aesKey
  • 偏移向量 iv

服务端解密流程:

  • 密文和偏移向量由客户端发送给服务端,对这两个参数在服务端进行Base64_decode解编码操作。
  • 根据3rdSessionId从缓存中获取session_key,对session_key进行Base64_decode可以得到aesKey,aes密钥。
  • 调用aes解密方法,算法为 AES-128-CBC,数据采用PKCS#7填充。

下面结合小程序实例说明解密流程:

  1. 微信登录,获取用户信息
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var that = this;
wx.login({
success: function (res) {
    //微信js_code
    that.setData({wxcode: res.code});
    //获取用户信息
    wx.getUserInfo({
        success: function (res) {
            //获取用户敏感数据密文和偏移向量
            that.setData({encryptedData: res.encryptedData})
            that.setData({iv: res.iv})
        }
    })
}
})
  1. 使用code换取3rdSessionId
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
var httpclient = require('../../utils/httpclient.js')
VAR that = this
//httpclient.req(url, data, method, success, fail)
httpclient.req(
  'http://localhost:8090/wxappservice/api/v1/wx/getSession',
  {
      apiName: 'WX_CODE',
      code: this.data.wxcode
  },
  'GET',
  function(result){
    var thirdSessionId = result.data.data.sessionId;
    that.setData({thirdSessionId: thirdSessionId})
    //将thirdSessionId放入小程序缓存
    wx.setStorageSync('thirdSessionId', thirdSessionId)
  },
  function(result){
    console.log(result)
  }
);
  1. 发起解密请求
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//httpclient.req(url, data, method, success, fail)
httpclient.req(
'http://localhost:8090/wxappservice/api/v1/wx/decodeUserInfo',
  {
    apiName: 'WX_DECODE_USERINFO',
    encryptedData: this.data.encryptedData,
    iv: this.data.iv,
    sessionId: wx.getStorageSync('thirdSessionId')
  },
  'GET',
  function(result){
  //解密后的数据
    console.log(result.data)
  },
  function(result){
    console.log(result)
  }
);
  1. 通过前端获取的code,获取openid和session_key
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
 * @author: wjy
 * @description: 小程序,通过code获取session_key和openid
 */
@ResponseBody
@PostMapping("getSessionId")
public ResponseUtil getSessionId(@RequestBody Map<String,String> paramMap){
	LOGGER.info(
			"\n***************************************" + "\n" +
					"start getSessionId" + "\n" +
					"通过code获取session_key和openid" + "\n" +
					"paramMap" + paramMap +"\n" +
					"\n********************************************"
	);
	ResponseUtil response = ResponseUtil.success();
	CodeEnum code = CodeEnum.FAIL;
	JSONObject jsonObject;
	try{
		AssertUtil.assertValidate(code,CodeEnum.ERROR_2001.getCode(),"code不能为空",StringUtils.isNotEmpty(paramMap.get("code")));

		//完整地址示例
		//https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
		String url = "https://api.weixin.qq.com/sns/jscode2session";
		String param =
				"appid=" + PayConstant.XCX_APPID +
				"&secret=" + PayConstant.XCX_APPSECRET +
				"&js_code=" + paramMap.get("code") +
				"&grant_type=authorization_code";
		//向微信服务器发送get请求,获取session_key和openid
		String sessionKeyAndOpenid = HttpUtil.sendGet(url, param);
		jsonObject = JSONObject.parseObject(sessionKeyAndOpenid);

		String uuid = UUID.randomUUID().toString().replaceAll("-","");

		LOGGER.info("session_key是:~~~~~~~~~~~~~~~~" + jsonObject.getString("session_key"));

		//将获取到的session_key存入redis,这里不用设置过期时间
		stringRedisTemplate.opsForValue().set(uuid,jsonObject.getString("session_key"));

		//把uuid返回给前端
		response.setData(uuid);
	}catch (Exception e){
		e.printStackTrace();
		response.setCode(code);
		response.setMessage(e.getMessage());
	}
	return response;
}
  1. 后端用session_key,encryptedData,iv解密获取用户信息userinfo。
    后端解密其实就这么简单,只要流程对了就可以解密,如果解密出错,基本就是流程出错了。不用再去换什么解密算法。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public Object getPhoneNumber(String encryptedData, String session_key, String iv) {
		 // 被加密的数据
		byte[] dataByte = Base64.decode(encryptedData);
		// 加密秘钥
		byte[] keyByte = Base64.decode(session_key);
		// 偏移量
		byte[] ivByte = Base64.decode(iv);
		try {
			// 如果密钥不足16位,那么就补足.  这个if 中的内容很重要
			int base = 16;
			if (keyByte.length % base != 0) {
				int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0);
				byte[] temp = new byte[groups * base];
				Arrays.fill(temp, (byte) 0);
				System.arraycopy(keyByte, 0, temp, 0, keyByte.length);
				keyByte = temp;
			}
			// 初始化
			Security.addProvider(new BouncyCastleProvider());
			Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
			SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");
			AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
			parameters.init(new IvParameterSpec(ivByte));
			cipher.init(Cipher.DECRYPT_MODE, spec, parameters);// 初始化
			byte[] resultByte = cipher.doFinal(dataByte);
			if (null != resultByte && resultByte.length > 0) {
				String result = new String(resultByte, "UTF-8");
				return JSONObject.parseObject(result);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	return null;
}

上边分别请求了两次后台接口,其实完全可以合并成一次请求,参考下边获取手机号的内容。

参考1
参考2
参考3

小程序获取手机号

官方文档

  1. 前端调用 wx.login() 获取code
1
2
3
4
5
wx.login({
    success:function(res){
        console.log('loginCode:', res.code)
    }
});
  1. 后端拿到该code发送get请求微信接口获取 session_key和openid,返回结果类似:
1
2
3
4
{
	"session_key": "kI+3ookOAYC9olOcGzYPmQ",
	"openid": "oNZE65Pu0XfTg2yFP-dFks"
}

后台拿到数据后,我们把session_key存到redis,然后把session_key存在redis中的key返回给前端。因为微信官方说:
为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥

  1. 前端调用 getPhoneNumber组件,用户确认授权。拿到encryptedData和iv。并传给后端。
    <button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber"> </button>
1
2
3
4
5
getPhoneNumber: function(e) { 
    console.log(e.detail.errMsg) 
    console.log(e.detail.iv) 
    console.log(e.detail.encryptedData) 
}
  1. 后端用session_key,encryptedData,iv解密获取手机号。
    后端解密其实就这么简单,只要流程对了就可以解密,如果解密出错,基本就是流程出错了。不用再去换什么解密算法。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public Object getPhoneNumber(String encryptedData, String session_key, String iv) {
		 // 被加密的数据
		byte[] dataByte = Base64.decode(encryptedData);
		// 加密秘钥
		byte[] keyByte = Base64.decode(session_key);
		// 偏移量
		byte[] ivByte = Base64.decode(iv);
		try {
			// 如果密钥不足16位,那么就补足.  这个if 中的内容很重要
			int base = 16;
			if (keyByte.length % base != 0) {
				int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0);
				byte[] temp = new byte[groups * base];
				Arrays.fill(temp, (byte) 0);
				System.arraycopy(keyByte, 0, temp, 0, keyByte.length);
				keyByte = temp;
			}
			// 初始化
			Security.addProvider(new BouncyCastleProvider());
			Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
			SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");
			AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
			parameters.init(new IvParameterSpec(ivByte));
			cipher.init(Cipher.DECRYPT_MODE, spec, parameters);// 初始化
			byte[] resultByte = cipher.doFinal(dataByte);
			if (null != resultByte && resultByte.length > 0) {
				String result = new String(resultByte, "UTF-8");
				return JSONObject.parseObject(result);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	return null;
}

上边是请求了两次接口,其实完全可以只请求一次接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
 * @author: wjy
 * @description: 小程序登录,返回用户信息
 */
@ResponseBody
@PostMapping("xcxRegisterOrLogin")
public ResponseUtil xcxRegisterOrLogin(@RequestBody Map<String,String> paramMap){
	LOGGER.info(
			"\n***************************************" + "\n" +
					"start xcxRegisterOrLogin" + "\n" +
					"小程序登录,返回用户信息" + "\n" +
					"paramMap" + paramMap +"\n" +
					"\n********************************************"
	);
	ResponseUtil response = ResponseUtil.success();
	CodeEnum code = CodeEnum.FAIL;
	JSONObject jsonObject;
	try{
		AssertUtil.assertValidate(code,CodeEnum.ERROR_2001.getCode(),"iv不能为空",StringUtils.isNotEmpty(paramMap.get("iv")));
		AssertUtil.assertValidate(code,CodeEnum.ERROR_2001.getCode(),"code不能为空",StringUtils.isNotEmpty(paramMap.get("code")));
		AssertUtil.assertValidate(code,CodeEnum.ERROR_2003.getCode(),"encryptedData不能为空",StringUtils.isNotEmpty(paramMap.get("encryptedData")));

		//完整地址示例
		//https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
		String url = "https://api.weixin.qq.com/sns/jscode2session";
		String param = "appid=" + PayConstant.XCX_APPID +
				"&secret=" + PayConstant.XCX_APPSECRET +
				"&js_code=" + paramMap.get("code") +
				"&grant_type=authorization_code";
		//向微信服务器发送get请求,获取session_key和openid
		String wxReturnJson = HttpUtil.sendGet(url, param);
		LOGGER.info("微信返回结果是:~~~~~~~~~~~~~~~~" + wxReturnJson);
		jsonObject = JSONObject.parseObject(wxReturnJson);

		//解密用户信息
		JSONObject userinfo = XcxUtil.decodeUserInfo(paramMap.get("encryptedData"),jsonObject.getString("session_key"),paramMap.get("iv"));
		//用户手机号
		String phoneNum = userinfo.getString("purePhoneNumber");
		LOGGER.info("用户手机号是:~~~~~~~~~~~~~~~~" + phoneNum);

		paramMap.put("mobile",phoneNum);
		//根据手机号,注册或登录
		response = usersService.registerOrLogin(paramMap,response);
	}catch (Exception e){
		e.printStackTrace();
		response.setCode(code);
		response.setMessage(e.getMessage());
	}
	return response;
}

这个接口,我接收了code、iv、encryptedData三个参数。
其中code是前端请求wx.login()返回的,iv、encryptedData是前端请求wx.userinfo()返回的。
我的接口中,先通过code请求微信服务器,获取session_keyopenid,然后通过session_key、iv、encryptedData进行解密,就得到了用户手机号。
后续是自己业务的逻辑,即通过手机号判断自己的数据库是否有这个用户,有->返回用户信息(即登录),没有->注册并返回用户信息。

参考1
参考2

获取手机号和获取用户信息,两套流程的区别,只在于前端第二次请求微信接口的不同。每个步骤,对于后台来说都是一样的。
后台只需要通过前端获取的code,获取session_id和openid,然后再用三个参数session_key,iv,encryptedData进行解密即可。

  • 获取用户信息,前端调用wx.getUserInfo(Object object)官方文档
  • 获取手机号,前端调用getPhoneNumber官方文档

所以,获取用户信息和获取用户手机号,后台只需一个接口即可,但是需要注意,解密出来的,一个是手机号,一个是用户信息,所以可能还需要在自己的业务中,对两种不同的参数采取不同的处理。

后台请求微信授权,在本地即可以测试,只要有code或encryptedData和iv(这三个参数需要前端给你),不用非拿到线上去测。而且,小程序后台,也无需配置服务器域名啥的。

小程序发送消息

官方文档
文档2
微信的官方文档真的是屎一样,毫无逻辑调理。每次要写和微信相关的功能头都很大。

功能介绍:
前端添加按钮后,用户点击按钮,即可跳转到小程序客服界面。
此时有三种做法:
1.前端直接请求微信接口给客服发送消息。
2.前端请求开发者的服务器接口,由开发者服务器请求微信服务器,发送消息。
3.通过小程序后台的配置,由微信服务器请求开发者服务器,发送消息。
这里我们使用第三种方法。

  • 登录微信公众平台
    小程序管理后台->开发->开发设置->消息推送->启用。
    几个需要填写的参数:
    URL(服务器地址):填写我们自己服务器可以get请求的一个方法,必须是方法的全路径,如:http://test.api.zjxk12.com/zjx/api/pay/checkSignature
    token令牌:随便写,但是要保存在程序中,用于验证一致性。
    EncodingAESKey(消息加密密钥):在当前页面自动生成。
    消息加密方式:推荐兼容模式。
    数据格式:我选的json。

此时点确定还是不行的,因为我们的服务端还没有接口,这个接口就是上边我们填写的地址,点击确定的时候,微信服务器会调用这个接口。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@ResponseBody
@GetMapping("checkSignature")
public void checkSignature(HttpServletRequest request, HttpServletResponse response) throws Exception{
	// 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
	String signature = request.getParameter("signature");
	// 时间戳
	String timestamp = request.getParameter("timestamp");
	// 随机数
	String nonce = request.getParameter("nonce");
	// 随机字符串
	String echostr = request.getParameter("echostr");

	PrintWriter out = null;
	try {
		out = response.getWriter();
		// 通过检验signature对请求进行校验,若校验成功则原样返回echostr,否则接入失败
		if (XcxUtil.checkSignature(signature, timestamp, nonce)) {
			out.print(echostr);
			out.flush();   //这个地方必须画重点,消息推送配置Token令牌错误校验失败,搞了我很久,必须要刷新!!!!!!!
		}
	} catch (IOException e) {
		e.printStackTrace();
	} finally {
		out.close();
		out = null;
	}
}

内部调用的方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
 * 验证签名
 * @param signature
 * @param timestamp
 * @param nonce
 * @return
 */
public static boolean checkSignature(String signature, String timestamp, String nonce) {
	private static final String XCX_TONEN = "zjx666888";
	//与token 比较
	String[] arr = new String[] { XCX_TONEN, timestamp, nonce };
	// 将token、timestamp、nonce三个参数进行字典排序
	Arrays.sort(arr);
	StringBuilder content = new StringBuilder();
	for (int i = 0; i < arr.length; i++) {
		content.append(arr[i]);
	}
	MessageDigest md = null;
	String tmpStr = null;
	try {
		md = MessageDigest.getInstance("SHA-1");
		// 将三个参数字符串拼接成一个字符串进行sha1加密
		byte[] digest = md.digest(content.toString().getBytes());
		tmpStr = byteToStr(digest);
	} catch (NoSuchAlgorithmException e) {
		e.printStackTrace();
	}
	content = null;
	// 将sha1加密后的字符串可与signature对比
	return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false;
}

这个接口写好,传到服务器上,点确定,就可以验证成功了。

因为微信发送消息调用的也是这个接口,所以还需要改这个接口,否则就需要改小程序的配置。接口更改后如下:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
/**
 * @author: wjy
 * @description: 微信服务器发送小程序消息
 */
@ResponseBody
@RequestMapping(value= {"/checkSignature"},method= {RequestMethod.GET, RequestMethod.POST})
public void checkSignature(HttpServletRequest request, HttpServletResponse response,@RequestBody Map<String,String> map) throws Exception{
	LOGGER.info(
			"\n***************************************" + "\n" +
					"start xcxGetPhoneNum" + "\n" +
					"微信服务器发送小程序消息" + "\n" +
					"\n********************************************"
	);
	final String kf_url = "https://api.weixin.qq.com/cgi-bin/message/custom/send";
	boolean isGet=request.getMethod().toLowerCase().equals("get");
	LOGGER.info(isGet+"---------------");
	if(isGet){//首次验证token
		// 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数nonce参数
		String signature = request.getParameter("signature");
		// 时间戳
		String timestamp = request.getParameter("timestamp");
		// 随机数
		String nonce = request.getParameter("nonce");
		// 随机字符串
		String echostr = request.getParameter("echostr");

		PrintWriter out = null;
		try {
			out = response.getWriter();
			// 通过检验signature对请求进行校验,若校验成功则原样返回echostr,否则接入失败
			if (XcxUtil.checkSignature(signature, timestamp, nonce)) {
				out.print(echostr);
				out.flush();   //这个地方必须画重点,消息推送配置Token令牌错误校验失败,搞了我很久,必须要刷新!!!!!!!
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			out.close();
			out = null;
		}
	}else{// 进入POST聊天处理
		LOGGER.info("进入了聊天界面");
		// 接收消息并返回消息  
		try {
			// 接收消息并返回消息
			JSONObject json = null;
			//这是通过通过get方式去url 拼接的键值对,post方式取不到值
//                String openid=request.getParameter("FromUserName");
			String openid = map.get("FromUserName");
			System.out.println("openid是:" + openid);

			//返回页面防止出现中文乱码
			request.setCharacterEncoding("UTF-8");
			//post方式传递读取字符流
			BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
			String jsonStr = null;
			StringBuilder result = new StringBuilder();
			try {
				while ((jsonStr = reader.readLine()) != null) {
					result.append(jsonStr);
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
			reader.close();// 关闭输入流

			//获取小程序access_token
			String access_token = stringRedisTemplate.opsForValue().get("xcx_access_token");
			if(StringUtils.isEmpty(access_token)){
				LOGGER.info(">>>>>>>>>>>>>>>>>>>>>>重新获取小程序access_token>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
				//已经超时,重新获取
				access_token = getXcxAccessToken(stringRedisTemplate);
			}
			PrintWriter out=null;
			BufferedReader in=null;
			try {
				String media_id = stringRedisTemplate.opsForValue().get("media_id");
				if(null == media_id){
					//重新获取media_id
					media_id = XcxUtil.uploadMedia(access_token, "image","http://file.zjxk12.com/jpg/1587005725424.jpg",stringRedisTemplate);
				}

				//组装参数体1,图片
				Map<String,Object> picMap = new HashMap<>(4);
				picMap.put("msgtype","image");
				picMap.put("touser",openid);
				Map<String,String> inPicMap = new HashMap<>(1);
				inPicMap.put("media_id",media_id);
				picMap.put("image",inPicMap);

				JSONObject param1 = new JSONObject(picMap);
				JSONObject result1 = HttpUtil.sendPost(kf_url + "?access_token=" + access_token,param1);
				log.info("微信返回的结果2: {}", result1);

				//组装参数2,文字
				Map<String,Object> wordMap = new HashMap<>(4);
				wordMap.put("msgtype","text");
				wordMap.put("touser",openid);
				Map<String,String> inMap = new HashMap<>(1);
				inMap.put("content","由于微信规则变动,您需要点击上方二维码,然后长按识别,关注【最美课本家长】公众号,注册家长端,并绑定学生端APP手机号,就可以在最美课本APP内学习全部内容!");
				wordMap.put("text",inMap);

				//参数体2
				JSONObject param2 = new JSONObject(wordMap);
				JSONObject result2 = HttpUtil.sendPost(kf_url + "?access_token=" + access_token,param2);
				log.info("微信返回的结果2: {}", result2);
			} catch (Exception e) {
				System.out.println("发送post请求异常");
				e.printStackTrace();
			}finally{
				if(out!=null)out.close();
				if(in!=null)in.close();
			}

		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

传到服务器上,小程序客服就可以发送消息了。

传媒体文件到微信服务器

官方文档
如果想发送图片消息,根据官方文档提示,我们需要media_id。现在讲如何获取这个media_id。
首先需要准备3个参数:

  • access_token:小程序的access_token(注意和公众号的access_token区分)
  • type:文件类型,合法值:image,就是说只能写image。写死好了
  • media:form-data 中媒体文件标识,有filename、filelength、content-type等信息

调用微信的接口成功,微信会返回:

1
2
3
4
5
6
7
{
  "errcode": 0,
  "errmsg": "ok",
  "type": "image",
  "media_id": "MEDIA_ID",
  "created_at": "xxx"
}

这个media_id就是我们需要的。

然后编写上传方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/**
 * 上传多媒体数据到微信服务器
 * @param accessToken 从微信获取到的access_token
 * @param mediaFileUrl 来自网络上面的媒体文件地址
 * @return
 */
public static String uploadMedia(String accessToken, String type, String mediaFileUrl, StringRedisTemplate stringRedisTemplate) {
	//旧地址,微信官方已经废弃,狗比也没有通知
	//String MEDIA_URL = "http://file.api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE";
	//新地址
	String MEDIA_URL = "https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE";
	StringBuffer resultStr = null;
	//拼装url地址
	String mediaStr = MEDIA_URL.replace("ACCESS_TOKEN", accessToken).replace("TYPE", type);
	System.out.println("mediaStr:" + mediaStr);
	URL mediaUrl;
	try {
		String boundary = "----WebKitFormBoundaryOYXo8heIv9pgpGjT";
		URL url = new URL(mediaStr);
		HttpURLConnection urlConn = (HttpURLConnection)url.openConnection();
		//让输入输出流开启
		urlConn.setDoInput(true);
		urlConn.setDoOutput(true);
		//使用post方式请求的时候必须关闭缓存
		urlConn.setUseCaches(false);
		//设置请求头的Content-Type属性
		urlConn.setRequestProperty("Content-Type", "multipart/form-data; boundary="+boundary);
		urlConn.setRequestMethod("POST");
		//获取输出流,使用输出流拼接请求体
		OutputStream out = urlConn.getOutputStream();

		//读取文件的数据,构建一个GET请求,然后读取指定地址中的数据
		mediaUrl = new URL(mediaFileUrl);
		HttpURLConnection mediaConn = (HttpURLConnection)mediaUrl.openConnection();
		//设置请求方式
		mediaConn.setRequestMethod("GET");
		//设置可以打开输入流
		mediaConn.setDoInput(true);
		//获取传输的数据类型
		String contentType = mediaConn.getHeaderField("Content-Type");
		//将获取大到的类型转换成扩展名
		String fileExt = judgeType(contentType);
		//获取输入流,从mediaURL里面读取数据
		InputStream in = mediaConn.getInputStream();
		BufferedInputStream bufferedIn = new BufferedInputStream(in);
		//数据读取到这个数组里面
		byte[] bytes = new byte[1024];
		int size = 0;
		//使用outputStream流输出信息到请求体当中去
		out.write(("--"+boundary+"\r\n").getBytes());
		out.write(("Content-Disposition: form-data; name=\"media\";\r\n"
				+ "filename=\""+(new Date().getTime())+fileExt+"\"\r\n"
				+ "Content-Type: "+contentType+"\r\n\r\n").getBytes());
		while( (size = bufferedIn.read(bytes)) != -1) {
			out.write(bytes, 0, size);
		}
		//切记,这里的换行符不能少,否则将会报41005错误
		out.write(("\r\n--"+boundary+"--\r\n").getBytes());

		bufferedIn.close();
		in.close();
		mediaConn.disconnect();

		InputStream resultIn = urlConn.getInputStream();
		InputStreamReader reader = new InputStreamReader(resultIn);
		BufferedReader bufferedReader = new BufferedReader(reader);
		String tempStr = null;
		resultStr = new StringBuffer();
		while((tempStr = bufferedReader.readLine()) != null) {
			resultStr.append(tempStr);
		}
		bufferedReader.close();
		reader.close();
		resultIn.close();
		urlConn.disconnect();
	} catch (MalformedURLException e) {
		e.printStackTrace();
	} catch (IOException e) {
		e.printStackTrace();
	}
	JSONObject jo = JSON.parseObject(resultStr.toString());
	log.info("上传媒体文件到微信,返回的内容是:" + jo);
	String media_id = jo.get("media_id").toString();
	//缓存到redis
	stringRedisTemplate.opsForValue().set("media_id",media_id,72, TimeUnit.HOURS);
	return media_id;
}

media_id的有消息为3天,即72小时,一定要缓存到redis,时间也是72小时即可,避免重复上传。超时之后,再重新上传,重新获取media_id。
有了media_id,我们就可以在发客服消息时,使用media_id来发送图片了。

信公众号发送模板消息

公众号发送消息官方文档
要推送微信的模板消息,我们需要准备的条件有:
1、有效的access_token
2、微信公众号提供的消息模板的Template_id
access_token:
公众平台以access_token为接口调用凭据,来调用接口,所有接口的调用需要先获取access_token,access_token在2小时内有效,过期需要重新获取,但1天内获取次数有限,开发者需自行存储,详见获取接口调用凭据(access_token)文档。
我将access_token存到了redis中,有效期两小时,两小时过期后,再到微信服务器重新获取access_token。
Template_id:
Template_id是我们需要推送的模板消息的模板的唯一标识,微信公众平台提供了很多模板,也支持自定义格式。在实际使用时,我们需要到微信公众平台申请开启模板消息功能。在微信公众平台的首页,左侧菜单有一个“添加功能插件”,选择模板消息,微信平台审核需要一定时间。

消息模板举例:

1
2
3
4
{{first.DATA}}
告警内容:{{keyword1.DATA}}
告警发生时间:{{keyword2.DATA}}
{{keyword3.DATA}}

为了方便封装数据,我们使用模板类来包装要发送的信息:
TemplateParam 类封装了模板消息中的一个数据,比如上面的“{{first.DATA}} ”,具体属性可阅读注释:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class TemplateParam {
    // 参数名称
    private String name;
    // 参数值
    private String value;
    // 颜色
    private String color;

    public TemplateParam(String name, String value, String color) {
        this.name = name;
        this.value = value;
        this.color = color;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    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;
    }
}

Template类代表了整个微信模板消息,这里我使用了特定的toJSON()方法生成要发送给微信云平台的json数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public class Template {

    // 消息接收方
    private String toUser;
    // 模板id
    private String templateId;
    // 模板消息详情链接
    private String url;
    // 消息顶部的颜色
//    private String topColor;
    // 参数列表
    private List<TemplateParam> templateParamList;

    public String getToUser() {
        return toUser;
    }

    public void setToUser(String toUser) {
        this.toUser = toUser;
    }

    public String getTemplateId() {
        return templateId;
    }

    public void setTemplateId(String templateId) {
        this.templateId = templateId;
    }

    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 String toJSON() {
        StringBuffer buffer = new StringBuffer();
        buffer.append("{");
        buffer.append(String.format("\"touser\":\"%s\"", this.toUser)).append(",");
        buffer.append(String.format("\"template_id\":\"%s\"", this.templateId)).append(",");
		buffer.append(String.format("\"url\":\"%s\"", this.url)).append(",");
//        buffer.append(String.format("\"topcolor\":\"%s\"", this.topColor)).append(",");
        buffer.append("\"data\":{");
        TemplateParam param = null;
        for (int i = 0; i < this.templateParamList.size(); i++) {
            param = templateParamList.get(i);
            // 判断是否追加逗号
            if (i < this.templateParamList.size() - 1) {

                buffer.append(String.format("\"%s\": {\"value\":\"%s\",\"color\":\"%s\"},", param.getName(), param.getValue(), param.getColor()));
            } else {
                buffer.append(String.format("\"%s\": {\"value\":\"%s\",\"color\":\"%s\"}", param.getName(), param.getValue(), param.getColor()));
            }

        }
        buffer.append("}");
        buffer.append("}");
        return buffer.toString();
    }

    public List<TemplateParam> getTemplateParamList() {
        return templateParamList;
    }

    public void setTemplateParamList(List<TemplateParam> templateParamList) {
        this.templateParamList = templateParamList;
    }
}

准备好了模板消息实体,就可以封装数据并发送了。关注公众号的微信用户,会使用openid来唯一标识,下面的方法是实现了多用户群发,for循环逐一发送,目前没有找到更好的群发方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@RequestMapping("/sendWarningByWechat")
@ResponseBody
public ResultMap sendWarningByWechat(String openIds, String content, String alarmDescriptions, String sendDateTime) {
	ResultMap res = new ResultMap();
	StringBuffer resBuff = new StringBuffer();
	try {
		String templateMsgUrl = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN";
		templateMsgUrl = templateMsgUrl.replace("ACCESS_TOKEN", MemoryData.access_token);

		//封装请求体
		Template template = new Template();
		template.setTemplateId("0ZUeqUJQ7jxB3G-fxgKyP17SPoo44wFuxnYBqLu5zA4E");
		List<TemplateParam> templateParams = new ArrayList<>();
		String[] failures = alarmDescriptions.split("\\|\\|");
		String alarmDescStr = "\\r\\n";
		if (failures != null && failures.length > 0) {
			for (String failure : failures) {
				alarmDescStr += failure + "\\r\\n";
			}
			//去掉最后的换行符号
			alarmDescStr = alarmDescStr.substring(0, alarmDescStr.lastIndexOf("\\r\\n"));
		}
		String allStr = content + alarmDescStr;
		while (allStr.length() > 180) {
			alarmDescStr = alarmDescStr.substring(0, alarmDescStr.lastIndexOf("\\r\\n"))+"...";
			allStr = content + alarmDescStr;
		}
		TemplateParam first = new TemplateParam("first", content + "\\r\\n", "#DB1A1B");
		TemplateParam keyword1 = new TemplateParam("keyword1", alarmDescStr + "\\r\\n", "#DB1A1B");
		TemplateParam keyword2 = new TemplateParam("keyword2", sendDateTime + "", "#DB1A1B");
		TemplateParam keyword3 = new TemplateParam("keyword3", "", "#DB1A1B");
		templateParams.add(first);
		templateParams.add(keyword1);
		templateParams.add(keyword2);
		templateParams.add(keyword3);
		template.setTemplateParamList(templateParams);
		String[] openIdArr = openIds.split(",");
		if (openIdArr.length > 0) {
			for (String openID : openIdArr) {
				template.setToUser(openID);
				String resJson = HttpClientUtil.doTemplateMsgPost(templateMsgUrl, template.toJSON());
			}
		}
	} catch (Exception e) {
		log.error(e.getMessage());
		e.printStackTrace();
		res.setFailureResult();
	}
	return res;
}

击推送消息跳转到网页或者小程序

需求:消息推送后,点击推送的消息,可以跳转到公众号内部的某些页面,这个页面是我们自己的。
模板消息官方文档一定仔细阅读官方文档。
根据微信官网提供的接口文档,可以知道要跳转的条件有以下四个参数:
url:模板跳转链接(海外帐号没有跳转能力)
miniprogram:跳小程序所需数据,不需跳小程序可不用传该数据
appid:所需跳转到的小程序appid(该小程序appid必须与发模板消息的公众号是绑定关联关系,暂不支持小游戏)
pagepath:所需跳转到小程序的具体页面路径,支持带参数,(示例index?foo=bar),要求该小程序已发布,暂不支持小游戏
参考

小程序发送模板消息

后端需要实现的功能

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * 获取 accessToken
 */
@GetMapping("/getAccessToken")
public String getAccessToken() {
    Object accessToken = RedisUtils.get("accessToken");
    if (accessToken != null) {
        return accessToken.toString();
    }
    RestTemplate restTemplate = new RestTemplate();
    Map<String, String> params = new HashMap<>();
    params.put("APPID", appId);
    params.put("APPSECRET", secret);
    ResponseEntity<String> responseEntity = restTemplate.getForEntity(
            "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={APPID}&secret={APPSECRET}", String.class, params);
    String body = responseEntity.getBody();
    JSONObject object = JSON.parseObject(body);
    String access_Token = object.getString("access_token");
    String expires_in = object.getString("expires_in");
    log.info("access_token重新获取成功:{}", access_Token);
    // 存入redis,过期时间比官方提前200秒
    RedisUtils.set("accessToken",access_Token,7000);
    return access_Token;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/**
 * 发送订阅消息
 */
@GetMapping("/pushOneUser")
public String pushOneUser(String openid,String templateId) {
    RestTemplate restTemplate = new RestTemplate();
    String url = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=" + getAccessToken();
    XcxPushMessageDTO pushDTO = new XcxPushMessageDTO();
    pushDTO.setTouser(openid);
    pushDTO.setTemplate_id(templateId);
    pushDTO.setPage("pages/index/index");
    Map<String, TemplateDataDTO> map = new HashMap<>(3);
    map.put("thing1", new TemplateDataDTO("这是thing1"));
    map.put("data2", new TemplateDataDTO(LocalDateTime.now().toString()));
    map.put("thing3", new TemplateDataDTO("这是thing3"));
    pushDTO.setData(map);
    ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, pushDTO, String.class);
    return responseEntity.getBody();
}

其他

全局返回码说明

官方说明

微信分享JSSDK-invalid signature签名错误的解决方案

核对官方步骤,确认签名算法。

  • 确认签名算法正确,可用 http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=jsapisign 页面工具进行校验。
  • 确认config中nonceStr(js中驼峰标准大写S), timestamp与用以签名中的对应noncestr, timestamp一致。
  • 确认url是页面完整的url(请在当前页面alert(location.href.split(’#’)[0])确认),包括’http(s)://‘部分,以及’?‘后面的GET参数部分,但不包括’#‘hash后面的部分。
  • 确认 config 中的 appid 与用来获取 jsapi_ticket 的 appid 一致。
  • 确保一定缓存access_token和jsapi_ticket。
  • 确保你获取用来签名的url是动态获取的,动态页面可参见实例代码中php的实现方式。如果是html的静态页面在前端通过ajax将url传到后台签名,前端需要用js获取当前页面除去’#‘hash部分的链接(可用location.href.split(’#’)[0]获取,而且需要encodeURIComponent,后台decodeURIComponent解码),因为页面一旦分享,微信客户端会在你的链接末尾加入其它参数,如果不是动态获取当前链接,将导致分享后的页面签名失败。

小程序报错

{"errcode":40163,"errmsg":"code been used, hints: [ req_id: UGKfDXXBe-rIpWva ]"}
前端获取code,通过code请求后台接口,后台通过code获取openid和session_key。偶尔会报上边的错。原因:前端传了重复的code,小程序的code只能用一次,每次使用都要重新获取,不能缓存。