JAVA客服消息验证及自动回复

1,763 阅读5分钟

1.背景说明

最近做微信小程序的付费项目,IOS端小程序直接调用微信的JSAPI下单。违规记录如图。IOS端是不允许含有虚拟支付功能。所以专用客服服务付款

违规类型链接

2.解决方案

用户点击页面联系客服->跳转至服务号->发送小程序给客服->客服返回付款链接->用户点击链接付款成功。那么,客服自动回复如何实现呢?

1.gif

3.操作步骤

3.1小程序开启消息推送

第一步:填写服务器配置

登录小程序后台后,在「开发」-「开发设置」-「消息推送」中,管理员扫码启用消息服务,填写服务器地址(URL)、令牌(Token) 和 消息加密密钥(EncodingAESKey)等信息。

  • URL: 开发者用来接收微信消息和事件的接口 URL。开发者所填写的URL 必须以 http:// 或 https:// 开头,分别支持 80 端口和 443 端口。
  • Token: 可由开发者可以任意填写,用作生成签名(该 Token 会和接口 URL 中包含的 Token 进行比对,从而验证安全性)。
  • EncodingAESKey: 由开发者手动填写或随机生成,将用作消息体加解密密钥。

同时,开发者可选择消息加解密方式:明文模式(默认)、兼容模式和安全模式。可以选择消息数据格式:XML 格式(默认)或 JSON 格式。

登录微信管理平台,点击开发管理->开发设置->往下拖找到消息推送。

第二步:验证消息的确来自微信服务器

开发者提交信息后,微信服务器将发送GET请求到填写的服务器地址URL上,GET请求携带参数如下表所示:

第三步:接收消息和事件

当某些特定的用户操作引发事件推送时(如用户向小程序客服发送消息、或者进入会话等情况),微信服务器会将消息(或事件)的数据包以 POST 请求发送到开发者配置的 URL,开发者可以依据自身业务逻辑进行响应。

微信服务器在将用户的消息发给开发者服务器地址后,微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次。如果在调试中,发现用户无法收到响应的消息,可以检查是否消息处理超时。关于重试的消息排重,有 msgid 的消息推荐使用 msgid 排重。事件类型消息推荐使用 FromUserName + CreateTime 排重。

服务器收到请求必须做出下述回复,这样微信服务器才不会对此作任何处理,并且不会发起重试,否则,将出现严重的错误提示。详见下面说明:

  1. 直接回复success(推荐方式)
  2. 直接回复空串(指字节长度为0的空字符串,而不是结构体中content字段的内容为空)
  3. 若接口文档有指定返回内容,应按文档说明返回

对于客服消息,一旦遇到以下情况,微信会在小程序会话中向用户下发系统提示“该小程序客服暂时无法提供服务,请稍后再试”:

  1. 开发者在5秒内未回复任何内容
  2. 开发者回复了异常数据

3.验证消息来自微信

直接将微信发送给我们的echostr返回就好了,不用做解密。在页面点击提交就可以完成验证了。

@RequestMapping(value = "/checkWeixinValid",method=RequestMethod.GET)
    public String checkWeixinValid(@RequestParam(name="signature")String signature,
                                   @RequestParam(name="timestamp")String timestamp,
                                   @RequestParam(name="nonce")String nonce,
                                   @RequestParam(name="echostr")String echostr){
        // .......
        return echostr;
    }

4.实现客服消息自动回复

重点:如果你不知道微信传给你的MsgType是什么类型的可以打个断点。查看下类型然后做逻辑处理。

/**
 * 微信小程序客服消息自动回复
 *
 * @author 13
 * @return
 * @throws Exception
 */
@RequestMapping(value = "/checkWeixinValid", method = RequestMethod.POST)
@UserNotRequired
public String replyMessage(HttpServletRequest request, HttpServletResponse response) throws Exception {
    Map<String,String> map = null;
    //从工具类中获取XML解析之后的map
    try {
        map =  WeixinUtils.readWeixinXml(request);
    } catch (IOException e) {
        log.error("获取输入流失败", e);
    } catch (DocumentException e) {
        log.error("读取XML失败", e);
    }
    return appletCommonService.replyMessage(map);
}

WeixinUtils工具类

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author luboyang
 * @Date 2021-02-25
 */
public class WeixinUtils {
    /**
     * 解析微信请求并读取XML
     * @param request
     * @return
     * @throws IOException
     * @throws DocumentException
     */
    public static Map<String,String> readWeixinXml(HttpServletRequest request) throws IOException, DocumentException {
        Map<String,String> map = new HashMap<String,String>();
        //获取输入流
        InputStream input = request.getInputStream();
        //使用dom4j的SAXReader读取(org.dom4j.io.SAXReader;)
        SAXReader sax = new SAXReader();
        Document doc = sax.read(input);
        //获取XML数据包根元素
        Element root = doc.getRootElement();
        //得到根元素的所有子节点
        @SuppressWarnings("unchecked")
        List<Element> elementList = root.elements();
        //遍历所有节点并将其放进map
        for(Element e : elementList){
            map.put(e.getName(), e.getText());
        }
        //释放资源
        input.close();
        input = null;
        return map;

    }
}

sxhi

import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Maps;
import com.macro.contacts.logic.api.OrderLogic;
import com.macro.contacts.model.dto.OrderDTO;
import com.macro.contacts.utils.wx.WxAccessTokenRes;
import com.macro.contacts.utils.wx.WxConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.UnsupportedEncodingException;
import java.util.Map;

/**
 * @author luboyang
 * @Date 2021-02-25
 */
@Service
@Slf4j
public class AppletCommonService {

    public static final String MSG_TYPE = "miniprogrampage";
    public static final String TYPE = "MsgType";

    @Autowired
    private OrderLogic orderLogic;

    /**
     * 小程序客服自动回复功能
     *
     * @param msg 微信传递的参数集合
     * @return "SUCCEE"告知微信处理成功
     * @throws Exception
     * @author yixiu
     */
    public String replyMessage(Map<String, String> msg) throws Exception {
        System.out.println("客服消息自动回复 ====> start");
        String opendId = msg.get("FromUserName");
        Map<String, String> contenMap = Maps.newHashMapWithExpectedSize(2);
        // 若用户消息包含关键字则自动回复
      	//并不是所有的消息我都需要回复,所以进行MsgType的判断。这处理我想要类型的消息
        if (msg.get(TYPE).equals(MSG_TYPE)) {
          //这边填写你要发送给用户的消息
            contenMap.put("content", "请点击下发链接进行付款:"+""+orderLogic.h5CreateOrder(orderDTO,pagePath));
            sendMessage(opendId, "text", contenMap);
            // 此处将消息转发至人工客服,不然人工客服窗会没有消息
            Map<String, Object> tranMap = Maps.newHashMapWithExpectedSize(4);
            tranMap.put("ToUserName", msg.get("FromUserName"));
            tranMap.put("FromUserName", msg.get("ToUserName"));
            tranMap.put("CreateTime", msg.get("CreateTime"));
            tranMap.put("MsgType", "transfer_customer_service");
            return JSONObject.toJSONString(tranMap);
        }
        return "SUCCESS";
    }

    /**
     * 客服消息自动回复
     *
     * @param opendId    接收者openId
     * @param msgType    消息格式:text 文本;image 图片; link 图文链接
     * @param contentMap 正文map
     * @return
     * @author yixiu
     */
    private void sendMessage(String opendId, String msgType, Map<String, String> contentMap) throws UnsupportedEncodingException {
        Map<String, Object> replyMessageMap = Maps.newHashMapWithExpectedSize(4);
        replyMessageMap.put("touser", opendId);
        replyMessageMap.put("msgtype", msgType);
        replyMessageMap.put(msgType, contentMap);
        String messageJson = JSONObject.toJSONString(replyMessageMap);
        messageJson = new String(messageJson.getBytes(), "UTF-8");
        String url = WxConstants.GET_ACCESS_TOKEN
                .replace("APPID", "你的appid")
                .replace("APPSECRET", "你的secret");
        String response = HttpUtil.get(url);
        if (response == null) {
            return;
        }
        WxAccessTokenRes wxAccessTokenRes = JSONUtil.toBean(response, WxAccessTokenRes.class);
    
        String geturl = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=" + wxAccessTokenRes.getAccess_token();
        // 发送结果
        HttpUtil.post(geturl, messageJson);
    }

}

WxAccessTokenRess实体类

import lombok.Data;

@Data
public class WxAccessTokenRes {
    /**
     * 获取到的凭证
     */
    private String access_token;
    /**
     * 凭证有效时间,单位:秒。目前是7200秒之内的值
     */
    private Integer expires_in;
    /**
     * 错误码
     */
    private Integer errcode;
    /**
     * 错误信息
     */
    private String errmsg;

}

//api.weixin.qq.com/cgi-bin/mes… developers.weixin.qq.com/miniprogram…