概述
在第三方平台方创建成功并最终开发测试完毕,提交全网发布申请时,微信服务器会通过自动化测试的方式,检测服务的基础逻辑是否可用,在确保基础可用的情况下,才会允许公众号第三方平台提交全网发布。
微信后台会自动将下述公众号配置为第三方平台方的一个额外的测试公众号,并通过该帐号,执行如下所述的测试步骤,第三方平台方需要根据各步骤描述的自动化测试规则实现相关逻辑,才能通过接入检测,达到全网发布的前提条件。
请注意,必须预先按照测试各步骤要求,代码实现相关逻辑后,去点击“全网发布”按钮,才有可能全网发布成功。
此外,请注意,在自动执行测试Case过程中,仍需遵循 【消息加解密接入指引】的要求。
自动化测试的专用测试公众号的信息如下:
(1)appid: wx570bc396a51b8ff8
(2)Username: gh_3c884a361561
自动化测试的专用测试小程序的信息如下:
(1)appid:wxd101a85aa106f53e
(2)Username: gh_8dad206e9538
如果勾选了公众号的消息管理权限集,无论是否勾选了小程序的客服消息管理权限集都会做以下检测。具体测试步骤如下(微信后台会提前自动将专用测试公众号授权给第三方平台方,并且将会在专用测试公众号自动授权给第三方平台时,推送query_auth_code给服务方),但请注意,如果第三方平台未勾选消息管理权限集,则会省去相应的全网发布检测步骤,包括第1步和第2步。:
1、模拟粉丝发送文本消息给专用测试公众号,第三方平台方需根据文本消息的内容进行相应的响应:
1)微信模推送给第三方平台方:文本消息,其中Content字段的内容固定为:TESTCOMPONENT_MSG_TYPE_TEXT
2)第三方平台方立马回应文本消息并最终触达粉丝:Content必须固定为:TESTCOMPONENT_MSG_TYPE_TEXT_callback
2、模拟粉丝发送文本消息给专用测试公众号,第三方平台方需在5秒内返回空串表明暂时不回复,然后再立即使用客服消息接口发送消息回复粉丝
1)微信模推送给第三方平台方:文本消息,其中Content字段的内容固定为: QUERY_AUTH_CODE:$query_auth_code$(query_auth_code会在专用测试公众号自动授权给第三方平台方时,由微信后台推送给开发者)
2)第三方平台方拿到$query_auth_code$的值后,通过接口文档页中的“使用授权码换取公众号的授权信息”API,将$query_auth_code$的值赋值给API所需的参数authorization_code。然后,调用发送客服消息api回复文本消息给粉丝,其中文本消息的content字段设为:$query_auth_code$_from_api(其中$query_auth_code$需要替换成推送过来的query_auth_code)
3、模拟推送component_verify_ticket给开发者,开发者需按要求回复(接收到后必须直接返回字符串success)。
如果只勾选了小程序的客服消息管理权限集,没有勾选公众号的消息管理权限集,具体测试步骤如下(微信后台会提前自动将专用测试小程序授权给第三方平台方,并且将会在专用测试小程序自动授权给第三方平台时,推送query_auth_code给服务方),但请注意,如果第三方平台未勾选客服消息管理权限集,则会省去相应的全网发布检测步骤:
1、模拟粉丝发送文本消息给专用测试小程序,第三方平台方需立即使用客服消息接口发送消息回复粉丝
1)微信模推送给第三方平台方:文本消息,其中Content字段的内容固定为: QUERY_AUTH_CODE:$query_auth_code$(query_auth_code会在专用测试小程序自动授权给第三方平台方时,由微信后台推送给开发者)
2)第三方平台方拿到$query_auth_code$的值后,通过接口文档页中的“使用授权码换取公众号的授权信息”API,将$query_auth_code$的值赋值给API所需的参数authorization_code。然后,调用发送客服消息api回复文本消息给粉丝,其中文本消息的content字段设为:$query_auth_code$_from_api(其中$query_auth_code$需要替换成推送过来的query_auth_code)
说实话写的真含蓄
步骤1:第三方平台方获取预授权码(pre_auth_code)
对应的代码
@RequestMapping("/authorizedEventReception.do")
public void authorizedEventReception(HttpServletRequest request, HttpServletResponse response) {
PrintWriter writer = null;
try {
writer = response.getWriter();
if (request != null) {
Map requestMap = WechatUtils.xmlToMap(request);
Iterator> iter = requestMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
String key = entry.getKey();
String val = entry.getValue();
System.out.println("====================请求的参数名:" + key + "===============对应的参数值:" + val);
}
// 1413192605
// 第三方平台appid
String appId = requestMap.get("AppId");
String encrypt = requestMap.get("Encrypt");
if (appId.equals(componentAppid)) {
WXBizMsgCrypt pc = new WXBizMsgCrypt(componentMsgToken, componentMsgAESKey, componentAppid);
String decryptMsg = pc.decrypt(encrypt);
Map encryptMap = XMLParse.extract2(decryptMsg);
if (encryptMap != null && encryptMap.size() > 0 && encryptMap.containsKey("infoType")) {
Iterator> encryptiter = encryptMap.entrySet().iterator();
while (encryptiter.hasNext()) {
Map.Entry entry = (Map.Entry) encryptiter.next();
String key = entry.getKey();
String val = entry.getValue();
System.out.println("====================解析出来的数据为:" + key + "===============对应的参数值:" + val);
}
String infoType = encryptMap.get("infoType");
if (infoType.equals("component_verify_ticket")) {
Jedis jedis = RedisUtils.getJedis(3);
jedis.set("ComponentVerifyTicket", encryptMap.get("componentVerifyTicket"));
}
}
}
}
writer.write("success");
writer.flush();
writer.close();
} catch (AesException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if (writer != null) {
writer.flush();
writer.close();
}
}
}
步骤2:引入用户进入授权页
这里我是用的微信给出的方式一:授权注册页面扫码授权,不过方式二也很简单
对应的代码
/**
* 引入用户进入授权页
*
* @return
*/
@RequestMapping(value = "/getComponentLoginPage.do", method = RequestMethod.POST)
@ResponseBody
public JsonUtilesClass getComponentLoginPage() {
JsonUtilesClass jsonUtilesClass = new JsonUtilesClass();
// 步骤1:第三方平台方获取预授权码(pre_auth_code)
String apiCreatePreauthcode = WechatUtils.getApiCreatePreauthcode(componentAppid, componentAppsecret);
if (StringUtils.isBlank(apiCreatePreauthcode)) {
jsonUtilesClass.setStatus(400);
jsonUtilesClass.setMessage("获取预授权码失败!");
return jsonUtilesClass;
}
// 步骤2:引入用户进入授权页
// https://mp.weixin.qq.com/cgi-bin/componentloginpage?component\_appid=xxxx&pre\_auth\_code=xxxxx&redirect\_uri=xxxx&auth\_type=xxx%E3%80%82
String url = "https://mp.weixin.qq.com/cgi-bin/componentloginpage" + "?component_appid=" + componentAppid
+ "&pre_auth_code=" + apiCreatePreauthcode + "&redirect_uri=" + componentRedirectUri + "&auth_type=2";
// response.sendRedirect(url);
jsonUtilesClass.setStatus(200);
jsonUtilesClass.setMessage("成功!");
jsonUtilesClass.setData(url);
return jsonUtilesClass;
}
步骤3:用户确认并同意登录授权给第三方平台方
用户进入第三方平台授权页后,需要确认并同意将自己的公众号或小程序授权给第三方平台方,完成授权流程。
这里有一个坑点
得到微对应的链接,如果在浏览器上直接访问,将会是错误的,当时反复对应文档查找原因,没什么错误,但是就是显示错误,二维码不出来。之后返回到前台页面,通过跳转解决了。
对应的代码
这里我是将component_verify_ticket,预授权码,获取第三方平台component_access_token,都存储在了缓存redis中并设置了过期时间
接下来获取对应的公众号或小程序对应的信息,根据微信API调取就行,这里就不多解释了。
一、接受处理微信消息和事件信息
URL地址
格式为:http://xxxx.com/b/weixin2/APPID/callback
当用户在发送文本信息,或者取消关注,上报地理位置等一些列操作时,微信会向该接口地址推送一段加密的xml文件。需要解密后才能获取详细消息类型和事件类型。(相关解密的操作可以查看:java微信第三方平台开发(二))
相关代码:
@RequestMapping(value="/{appid}/callback",method={RequestMethod.GET,RequestMethod.POST})
public void callBackEvent(@PathVariable String appid,
HttpServletResponse response,HttpServletRequest request){
try {
Log.logger.info(appid+"进入callback+++++++++++++++++++++++++++++++++");
weChatThridService.handleMessage(request,response);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
1348831860
1234567890123456
ToUserName:公众号的原始ID,接收方
FromUserName:用户的openID,发送方
CreateTime:发送的时间
MsgType:类型,文本时text,如果是图片消息是image
Content: 内容
关于普通消息更多的类型介绍可以参考微信开发文档:
https://mp.weixin.qq.com/wiki/10/79502792eef98d6e0c6e1739da387346.html
2、事件消息
关注取消关注、上报地理位置等等一系列事件消息。
解密后的xml文件:
1479953039
44.541485
125.696594
40.000000
我们发现前3项是相同的,但是第四个虽然Element相同都是MsgType,但是内容却不相同,后续可以根据event和text的不同,做出不同的处理。
处理消息xml
既然是处理消息那当然少不了解密xml,然后写一个公用的方法,根据解密后的msgtype的不同来进一步实现不同的业务逻辑(中间可能包括耗时操作,比如用户上报的地理位置的经纬想存在数据库中,方便绘制用户的移动轨迹了(这是有点坏- 。-)),最后需要返回给微信响应。
说到给微信响应的这个就比较有复杂了,微信在推送xml后5s内收不到响应后,会断开连接,并且重新发起请求,总共重试三次,如果不能开发者不能确保自己服务器能响应的话可以直接回复空字符串。如果有业务要求,可以调用客服高级接口回复用户内容。内容可以自己定义。但是不回复的话,微信会推送重复的xml进来,这时就需要排重啦。一个static arraylist< string>(1000),当超过10000时清除key。
/**
* 接收消息 排重
* @param type 消息的类型: 1 文本 0 事件
* @param msgId 文本的内容
* @param FromUserName 消息的发送方,此处为openId
* @param CreateTime 消息创建的时间
* @return false 重复的 true 不重复
*/
public boolean messageExcludeRepeat(int type,String msgId,String FromUserName,String CreateTime ){
//当数量大于10000时,删除
if(excludeRepeatList.size()>=10000){
excludeRepeatList.clear();
}
if(type==1){
String key=msgId+FromUserName+CreateTime;
//判断该文本消息是否是重复的
if(excludeRepeatList.contains(key)){
return false;
}
excludeRepeatList.add(key);
}else{
String key=FromUserName+CreateTime;
//判断该事件消息是否是重复的
if(excludeRepeatList.contains(key)){
return false;
}
excludeRepeatList.add(key);
}
return true;
}
当我们有耗时操作时,不能在5s内回复微信的时候。解决办法有两种:
(1)、先回复微信空串,后续再24小时内利用客服api回复用户,这种办法需要公众号有高级接口权限。
(2)、利用selvet3.0的新特性AsyncContext来实现,微信第一个5s内如果没能完成耗时操作,直接return,但是连接不会断开,重新重request中解析xml。直到完成操作,在返回给微信响应。相关AsyncContext的用法,后续介绍。
下面就第一种方法结合全网发布的必要的规范流程详细写写,包括回复微信的响应内容。
二、全网发布详细流程
微信要求开发者在接到xml文件后,经过一系列处理后,返回给为微信服务器响应。响应的内容为xml格式的文件,并且是加密的xml文件。对于不同的消息类型有不同回复格式,接下来要做的就是,根据不同的msgtype一方面完成我们的业务,另一方面封装好不同的xml文件加密,发送给微信服务器。
根据msgtype来判断
/**
* 处理全网检测发布,回复微信xml
*
* @param request
* @param response
* @throws Exception
*/
public void handleMessage(HttpServletRequest request,
HttpServletResponse response) throws Exception {
String msgSignature = request.getParameter("msg_signature");
if(!StringUtils.isNotBlank(msgSignature)){
return;
}
String timestamp=request.getParameter("timestamp");
String encrypt_type=request.getParameter("encrypt_type");
String nonce=request.getParameter("nonce");
String msg_signature=request.getParameter("msg_signature");
Log.logger.info("timestamp:"+timestamp);
Log.logger.info("encrypt_type:"+encrypt_type);
Log.logger.info("nonce:"+nonce);
Log.logger.info("msg_signature:"+msg_signature);
//验证通过后
StringBuilder sb = new StringBuilder();
BufferedReader in = request.getReader();
String line;
while ((line = in.readLine()) != null) {
sb.append(line);
}
String xml = sb.toString();
Log.logger.info("微信推送的原生:"+xml);
String encodingAesKey =WeChatContants.encodingAesKey;// 第三方平台组件加密密钥
String appId=WeChatContants.THRID_APPID;//从xml中解析
WXBizMsgCrypt pc = new WXBizMsgCrypt(WeChatContants.token, encodingAesKey,appId);
xml = pc.decryptMsg(msg_signature, timestamp, nonce, xml);
Log.logger.info("解密后的:"+xml);
Map parseXml = WeChatUtils.parseXml(xml);
String msgType=parseXml.get("MsgType");
String toUserName=parseXml.get("ToUserName");
String fromUserName=parseXml.get("FromUserName");
if("event".equals(msgType)){
Log.logger.info("---------------事件消息--------");
//排重
boolean repeatFlag = messageExcludeRepeat(0, null, fromUserName,parseXml.get("CreateTime"));
if(repeatFlag){
//不重复的
String event = parseXml.get("Event");
replyEventMessage(request,response,event,toUserName,fromUserName);
}else{
//重复的,先回复空串,稍后调用客服接口回复
WeChatUtils.responseReplyMessage(response, "");
//调用客服
replyApiTextMessage(request,response,null,fromUserName,0);
}
}else if("text".equals(msgType)){
Log.logger.info("---------------文本消息--------");
//排重
boolean repeatFlag = messageExcludeRepeat(1, parseXml.get("MsgId"), fromUserName,parseXml.get("CreateTime"));
if(repeatFlag){
//不重复的
String content = parseXml.get("Content");
processTextMessage(request,response,content,toUserName,fromUserName);
}else{
//重复的,先回复空串,稍后调用客服接口回复
WeChatUtils.responseReplyMessage(response, "");
//调用客服
replyApiTextMessage(request,response,null,fromUserName,0);
}
}
}
sb.append("");
sb.append(" ");
sb.append(" ");
sb.append(""+createTime+" ");
sb.append(" ");
sb.append(" ");
sb.append(" ");
微信对于事件回复格式的要求:xml中content格式为:文本消息的:event + “from_callback”(例如:LOCATIONfrom_callback)。
微信对于文本时要判断推送过的xml里的content的内容来回复不同的内容。
微信官方参考文档:https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419318611&token=5b3df85fa31c9a24daff2e2eac40b549d17a555e&lang=zh_CN
回复事件代码:
/**
* 微信全网接入 事件消息
* @param request
* @param response
* @param event
* @param toUserName
* @param fromUserName
* @throws Exception
*/
private void replyEventMessage(HttpServletRequest request,HttpServletResponse response, String event, String toUserName,String fromUserName) throws Exception {
String content = event + "from_callback";
Log.logger.info("--------------事件回复消息 content="+content + " toUserName="+toUserName+" fromUserName="+fromUserName);
replyTextMessage(request,response,content,toUserName,fromUserName);
}
回复文本消息:
/**
* 微信全网接入 文本消息
* @param request
* @param response
* @param event
* @param toUserName
* @param fromUserName
*/
private void processTextMessage(HttpServletRequest request, HttpServletResponse response,String content,String toUserName, String fromUserName) throws Exception{
if("TESTCOMPONENT_MSG_TYPE_TEXT".equals(content)){
String returnContent = content+"_callback";
replyTextMessage(request,response,returnContent,toUserName,fromUserName);
}else if(StringUtils.startsWithIgnoreCase(content, "QUERY_AUTH_CODE")){
//先回复空串
WeChatUtils.responseReplyMessage(response,"");
//接下来客服API再回复一次消息
replyApiTextMessage(request,response,content.split(":")[1],fromUserName,1);
}
}
/**
* 回复微信服务器"文本消息"
* @param request
* @param response
* @param content
* @param toUserName
* @param fromUserName
* @throws DocumentException
* @throws IOException
*/
public void replyTextMessage(HttpServletRequest request, HttpServletResponse response, String content, String toUserName, String fromUserName) throws Exception {
Long createTime = Calendar.getInstance().getTimeInMillis() / 1000;
StringBuffer sb = new StringBuffer();
sb.append("");
sb.append(" ");
sb.append(" ");
sb.append(""+createTime+" ");
sb.append(" ");
sb.append(" ");
sb.append(" ");
String replyMsg = sb.toString();
String returnvaleue = "";
try {
String encodingAesKey =WeChatContants.encodingAesKey;// 第三方平台组件加密密钥
String appId=WeChatContants.THRID_APPID;//从xml中解析
WXBizMsgCrypt pc = new WXBizMsgCrypt(WeChatContants.token,encodingAesKey,appId);
returnvaleue = pc.encryptMsg(replyMsg, createTime.toString(),request.getParameter("nonce"));
Log.logger.info("------------------加密后的返回内容 returnvaleue: "+returnvaleue);
} catch (AesException e) {
e.printStackTrace();
}
WeChatUtils.responseReplyMessage(response, returnvaleue);
}
/**
* 客服接口回复粉丝信息
* @param response
* @param auth_code 当type=1是才有值,type=0为null
* @param fromUserName
* @param type 1 表示全网发布时回复
* 0 表示普通消息回复
* @throws Exception
*/
public void replyApiTextMessage(HttpServletRequest request, HttpServletResponse response, String auth_code, String fromUserName,int type) throws Exception {
//从数据库中获取access_token
int companyId=Integer.parseInt(ConfigUtil.getString("resource/resource","BZ_ID"));
String access_token = getThridToken(companyId);
//模拟客户回复文本消息
Object[]objects={access_token};
String sendMessageTextUrl = MessageFormat.format(WeChatContants.THRID_KEFU_SENDMESSAGE_URL, objects);
//组装post数据
WeChatKeFuSendTextMessageVo textmessageVo=new WeChatKeFuSendTextMessageVo();
textmessageVo.setMsgtype("text");
textmessageVo.setTouser(fromUserName);
WeChatKeFuSendTextVo textContentVo=new WeChatKeFuSendTextVo();
String textContent = "";
if(type==1){
//全网发布回复的内容
textContent=auth_code+"_from_api";
}else{
//普通文本消息回复的内容
textContent="hello,ok!";
}
textContentVo.setContent(textContent);
textmessageVo.setText(textContentVo);
String result = HttpNetUtils.getInstance().httpByJson(sendMessageTextUrl,"POST",textmessageVo);
Log.logger.info("客服回复结果:"+result);
}
/**
* 统一回复微信服务器
* @param response
* @param content
* @throws IOException
*/
public static void responseReplyMessage(HttpServletResponse response,String content) throws IOException{
PrintWriter pw = response.getWriter();
pw.write(content);
pw.flush();
pw.close();
}
有什么问题请指正!