Browse Source

微信公众号服务配置

chao 3 years ago
parent
commit
1a9d18dbd9

+ 29 - 0
pom.xml

@@ -26,11 +26,40 @@
             <artifactId>fastjson</artifactId>
             <version>1.2.79</version>
         </dependency>
+        <dependency>
+            <groupId>org.dom4j</groupId>
+            <artifactId>dom4j</artifactId>
+            <version>2.1.3</version>
+        </dependency>
         <dependency>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>
             <optional>true</optional>
         </dependency>
+        <dependency>
+            <groupId>org.mybatis.spring.boot</groupId>
+            <artifactId>mybatis-spring-boot-starter</artifactId>
+            <version>2.2.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.github.pagehelper</groupId>
+            <artifactId>pagehelper-spring-boot-starter</artifactId>
+            <version>1.4.1</version>
+            <exclusions>
+                <exclusion>
+                    <artifactId>mybatis-spring-boot-starter</artifactId>
+                    <groupId>org.mybatis.spring.boot</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+
+
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-test</artifactId>

+ 119 - 0
src/main/java/com/caimei365/wechat/controller/WechatServerApi.java

@@ -0,0 +1,119 @@
+package com.caimei365.wechat.controller;
+
+import com.caimei365.wechat.entity.WeChatConstant;
+import com.caimei365.wechat.service.WechatServerService;
+import com.caimei365.wechat.utils.MessageUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import java.io.UnsupportedEncodingException;
+import java.util.Map;
+
+/**
+ * 微信公众号服务配置
+ *
+ * @author : Charles
+ * @date : 2022/1/13
+ */
+@Slf4j
+@RestController
+public class WechatServerApi {
+
+    @Resource
+    private WechatServerService wechatServerService;
+
+    /**
+     * 验证消息的确来自微信服务器
+     */
+    @GetMapping("")
+    public String validateSignature(HttpServletRequest request) throws UnsupportedEncodingException {
+        request.setCharacterEncoding("UTF-8");
+        return wechatServerService.validateSignature(request);
+    }
+
+    /**
+     * 处理微信服务器的消息转发
+     */
+    @PostMapping("")
+    public String handleMessage(HttpServletRequest request) throws UnsupportedEncodingException {
+        request.setCharacterEncoding("UTF-8");
+        long startProcessTime = System.currentTimeMillis();
+        // xml格式的消息数据
+        String respXml = null;
+        try {
+            // 调用parseXml方法解析请求消息
+            Map<String, String> requestMap = MessageUtil.parseXml(request);
+            // 消息类型
+            String msgType = requestMap.get(WeChatConstant.MSG_TYPE);
+            // 消息类型:事件推送
+            if (msgType.equals(WeChatConstant.MESSAGE_TYPE_EVENT)) {
+                // 事件类型
+                String eventType = requestMap.get(WeChatConstant.EVENT);
+                // subscribe(订阅/关注)
+                if (eventType.equals(WeChatConstant.EVENT_TYPE_SUBSCRIBE)) {
+                    respXml = wechatServerService.handleSubscribeMsg(requestMap);
+                }
+                // unsubscribe(取消订阅),取消订阅后用户不会再收到公众账号发送的消息,因此回复"success"
+                else if (eventType.equals(WeChatConstant.EVENT_TYPE_UNSUBSCRIBE)) {
+                    respXml = "success";
+                }
+                // 扫描带参数二维码
+                else if (eventType.equals(WeChatConstant.EVENT_TYPE_SCAN)) {
+                    respXml = wechatServerService.handleScanMsg(requestMap);
+                }
+                // 上报地理位置
+                else if (eventType.equals(WeChatConstant.EVENT_TYPE_LOCATION)) {
+                    respXml = wechatServerService.handleLocationMsg(requestMap);
+                }
+                // 点击自定义菜单
+                else if (eventType.equals(WeChatConstant.EVENT_TYPE_CLICK)) {
+                    respXml = wechatServerService.handleClickMenuMsg(requestMap);
+                }
+            }
+            // 图片消息
+            else if (msgType.equals(WeChatConstant.MESSAGE_TYPE_IMAGE)) {
+                // TODO 暂未开发
+                respXml = "success";
+            }
+            // 语音消息
+            else if (msgType.equals(WeChatConstant.MESSAGE_TYPE_VOICE)) {
+                // TODO 暂未开发
+                respXml = "success";
+            }
+            // 视频消息
+            else if (msgType.equals(WeChatConstant.MESSAGE_TYPE_VIDEO)) {
+                // TODO 暂未开发
+                respXml = "success";
+            }
+            // 地理位置消息
+            else if (msgType.equals(WeChatConstant.MESSAGE_TYPE_LOCATION)) {
+                // TODO 暂未开发
+                respXml = "success";
+            }
+            // 链接消息
+            else if (msgType.equals(WeChatConstant.MESSAGE_TYPE_LINK)) {
+                // TODO 暂未开发
+                respXml = "success";
+            }
+            // 文本消息
+            else if (msgType.equals(WeChatConstant.MESSAGE_TYPE_TEXT)) {
+                respXml = wechatServerService.handleTextMsg(requestMap);
+            }
+            // 默认文本消息
+            else {
+                respXml = wechatServerService.handleTextMsg(requestMap);
+            }
+            if (respXml == null) { respXml = "success"; }
+        } catch (Exception e) {
+            log.info("微信公众号消息处理时出现异常:" + e.getMessage());
+        }
+        log.info(">>>>>>>>>>微信回复用户消息:" + respXml);
+        long endProcessTime = System.currentTimeMillis();
+        log.info(">>>>>>>>>>处理请求耗时:" + (endProcessTime - startProcessTime) + "ms");
+        return respXml;
+    }
+}

+ 34 - 0
src/main/java/com/caimei365/wechat/dao/WeChatDao.java

@@ -0,0 +1,34 @@
+package com.caimei365.wechat.dao;
+
+import com.caimei365.wechat.entity.WechatArticleDetail;
+import com.caimei365.wechat.entity.WechatReply;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * Description
+ *
+ * @author : Charles
+ * @date : 2022/1/13
+ */
+@Mapper
+public interface WeChatDao {
+    /**
+     * 根据参数匹配数据库回复配置
+     * @param msgType 回复类型
+     * @param keyword 关键词
+     * @param wxType  微信Id
+     */
+    WechatReply getReplyByParams(String msgType, String keyword, String wxType);
+
+    /**
+     * 根据图文ID获取详情列表
+     */
+    List<WechatArticleDetail> getArticleDetailList(Integer articleId);
+
+    /**
+     * 根据ID获取文本内容
+     */
+    String getTextContent(Integer id);
+}

+ 86 - 0
src/main/java/com/caimei365/wechat/entity/WeChatConstant.java

@@ -0,0 +1,86 @@
+package com.caimei365.wechat.entity;
+
+/**
+ * Description
+ *
+ * @author : Charles
+ * @date : 2022/1/13
+ */
+public class WeChatConstant {
+    /**
+     * 服务器配置 -令牌(Token)
+     */
+    public static final String TOKEN = "caimei";
+    /**
+     * 消息类型:文本
+     */
+    public static final String MESSAGE_TYPE_TEXT = "text";
+    /**
+     * 消息类型:图片
+     */
+    public static final String MESSAGE_TYPE_IMAGE = "image";
+    /**
+     * 消息类型:音频
+     */
+    public static final String MESSAGE_TYPE_VOICE = "voice";
+    /**
+     * 消息类型:视频
+     */
+    public static final String MESSAGE_TYPE_VIDEO = "video";
+    /**
+     * 消息类型:音乐
+     */
+    public static final String MESSAGE_TYPE_MUSIC = "music";
+    /**
+     * 消息类型:图文
+     */
+    public static final String MESSAGE_TYPE_NEWS = "news";
+    /**
+     * 消息类型:链接
+     */
+    public static final String MESSAGE_TYPE_LINK = "link";
+    /**
+     * 消息类型:地理位置
+     */
+    public static final String MESSAGE_TYPE_LOCATION = "location";
+    /**
+     * 消息类型:事件推送
+     */
+    public static final String MESSAGE_TYPE_EVENT = "event";
+    /**
+     * 消息类型:转发到客服
+     */
+    public static final String MESSAGE_TYPE_TRANSFER = "transfer_customer_service";
+    /**
+     * 事件类型:subscribe(订阅)
+     */
+    public static final String EVENT_TYPE_SUBSCRIBE = "subscribe";
+    /**
+     * 事件类型:unsubscribe(取消订阅)
+     */
+    public static final String EVENT_TYPE_UNSUBSCRIBE = "unsubscribe";
+    /**
+     * 事件类型:CLICK(自定义菜单点击事件)
+     */
+    public static final String EVENT_TYPE_CLICK = "CLICK";
+    /**
+     * 事件类型:LOCATION(上报地理位置事件)
+     */
+    public static final String EVENT_TYPE_LOCATION = "LOCATION";
+    /**
+     * 事件类型:SCAN(扫描事件)
+     */
+    public static final String EVENT_TYPE_SCAN = "SCAN";
+    /**
+     * xml节点
+     */
+    public static final String FROM_USER_NAME = "FromUserName";
+    public static final String TO_USER_NAME = "ToUserName";
+    public static final String MSG_TYPE = "MsgType";
+    public static final String CONTENT = "Content";
+    public static final String EVENT = "Event";
+    public static final String EVENT_KEY = "EventKey";
+    public static final String LONGITUDE = "Longitude";
+    public static final String LATITUDE = "Latitude";
+    public static final String PRECISION = "Precision";
+}

+ 38 - 0
src/main/java/com/caimei365/wechat/entity/WechatArticleDetail.java

@@ -0,0 +1,38 @@
+package com.caimei365.wechat.entity;
+
+import lombok.Data;
+
+/**
+ * Description
+ *
+ * @author : Charles
+ * @date : 2022/1/14
+ */
+@Data
+public class WechatArticleDetail {
+    private static final long serialVersionUID = 1L;
+    /**
+     * Id
+     */
+    private Integer id;
+    /**
+     * 图文Id
+     */
+    private Integer articleId;
+    /**
+     * 文章标题
+     */
+    private String title;
+    /**
+     * 跳转链接
+     */
+    private String linkUrl;
+    /**
+     * 图片链接
+     */
+    private String picUrl;
+    /**
+     * 文章内容
+     */
+    private String description;
+}

+ 38 - 0
src/main/java/com/caimei365/wechat/entity/WechatReply.java

@@ -0,0 +1,38 @@
+package com.caimei365.wechat.entity;
+
+import lombok.Data;
+
+/**
+ * Description
+ *
+ * @author : Charles
+ * @date : 2022/1/13
+ */
+@Data
+public class WechatReply {
+    private static final long serialVersionUID = 1L;
+    /**
+     * Id
+     */
+    private Integer id;
+    /**
+     * 关键字
+     */
+    private String keyword;
+    /**
+     * 回复类型
+     */
+    private String msgType;
+    /**
+     * 事件类型
+     */
+    private String responseType;
+    /**
+     * 素材id
+     */
+    private Integer relateId;
+    /**
+     * 回复素材标题
+     */
+    private String title;
+}

+ 43 - 0
src/main/java/com/caimei365/wechat/service/WechatServerService.java

@@ -0,0 +1,43 @@
+package com.caimei365.wechat.service;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Map;
+
+/**
+ * Description
+ *
+ * @author : Charles
+ * @date : 2022/1/13
+ */
+public interface WechatServerService {
+
+    /**
+     * 验证消息的确来自微信服务器
+     */
+    String validateSignature(HttpServletRequest request);
+
+    /**
+     * 处理接收的文本消息
+     */
+    String handleTextMsg(Map<String, String> requestMap);
+
+    /**
+     * subscribe(订阅/关注)
+     */
+    String handleSubscribeMsg(Map<String, String> requestMap);
+
+    /**
+     * 扫描带参数二维码事件
+     */
+    String handleScanMsg(Map<String, String> requestMap);
+
+    /**
+     * 点击自定义菜单
+     */
+    String handleClickMenuMsg(Map<String, String> requestMap);
+
+    /**
+     * 上报地理位置
+     */
+    String handleLocationMsg(Map<String, String> requestMap);
+}

+ 292 - 0
src/main/java/com/caimei365/wechat/service/impl/WechatServerServiceImpl.java

@@ -0,0 +1,292 @@
+package com.caimei365.wechat.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.caimei365.wechat.dao.WeChatDao;
+import com.caimei365.wechat.entity.WeChatConstant;
+import com.caimei365.wechat.entity.WechatArticleDetail;
+import com.caimei365.wechat.entity.WechatReply;
+import com.caimei365.wechat.service.WechatServerService;
+import com.caimei365.wechat.utils.MessageUtil;
+import com.caimei365.wechat.utils.SignUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.RestTemplate;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 微信公众号服务配置
+ *
+ * @author : Charles
+ * @date : 2022/1/13
+ */
+@Slf4j
+@Service
+public class WechatServerServiceImpl implements WechatServerService {
+
+    @Value("${caimei.wwwDomain}")
+    private String wwwDomain;
+    @Value("${caimei.coreDomain}")
+    private String coreDomain;
+    @Value("${caimei.imageDomain}")
+    private String imageDomain;
+    @Value("${wechat.caimei.appid}")
+    private String caimeiAppid;
+    @Value("${wechat.caimei.secret}")
+    private String caimeiSecret;
+
+    @Resource
+    private WeChatDao weChatDao;
+
+    /**
+     * 验证消息的确来自微信服务器
+     *
+     * 通过检验signature对请求进行校验。
+     * 若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。
+     * 加密/校验流程如下:
+     *    1)将token、timestamp、nonce三个参数进行字典序排序
+     *    2)将三个参数字符串拼接成一个字符串进行sha1加密
+     *    3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
+     */
+    @Override
+    public String validateSignature(HttpServletRequest request) {
+        String signature = request.getParameter("signature");
+        String timestamp = request.getParameter("timestamp");
+        String nonce = request.getParameter("nonce");
+        String echostr = request.getParameter("echostr");
+        boolean flag = SignUtil.checkSignature(signature, timestamp, nonce);
+        if (flag) {
+            return echostr;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * 处理接收的文本消息
+     * <xml>
+     *   <ToUserName><![CDATA[toUser]]></ToUserName>
+     *   <FromUserName><![CDATA[fromUser]]></FromUserName>
+     *   <CreateTime>12345678</CreateTime>
+     *   <MsgType><![CDATA[text]]></MsgType>
+     *   <Content><![CDATA[你好]]></Content>
+     * </xml>
+     */
+    @Override
+    public String handleTextMsg(Map<String, String> requestMap) {
+        // 根据接收的关键字回复
+        String keyword = requestMap.get(WeChatConstant.CONTENT);
+        // 用户openid
+        String openid = requestMap.get(WeChatConstant.FROM_USER_NAME);
+        // 微信号公众号Id
+        String wxType = requestMap.get(WeChatConstant.TO_USER_NAME);
+        log.info(">>>>>>>>>>处理接收的文本消息,keyword:" + keyword + ",openid:" + openid + ",wxType:" + wxType);
+        // 根据口令获取用户openid
+        if("caimei:get_openid".equals(keyword)) {
+            return MessageUtil.setTextXml(openid, wxType, openid);
+        }
+        // 根据keyword匹配数据库回复配置
+        WechatReply reply = weChatDao.getReplyByParams("input", keyword, wxType);
+//        if (null == reply) {
+//            reply = weChatDao.getReplyByParams("input", "autoReply", wxType);
+//        }
+//        if (null == reply) {
+//            // 将消息转发到到客服系统
+//            return MessageUtil.setCustomerXml(openid, wxType);
+//        }
+        log.info(">>>>>>>>>>匹配已配置关键词:" + keyword);
+        String replyXml = setXmlByDatabaseReply(openid, wxType, reply);
+        if (replyXml != null) {
+            return replyXml;
+        }
+        log.info(">>>>>>>>>>模糊搜索:" + keyword);
+        WechatArticleDetail article = null;
+        RestTemplate restTemplate = new RestTemplate();
+        // https://core.caimei365.com/commodity/search/query/product?keyword=
+        String uri = coreDomain + "commodity/search/query/product?keyword="+keyword;
+        JSONObject forObject = restTemplate.getForObject(uri, JSONObject.class);
+        if(forObject != null){
+            String data = forObject.getString("data");
+            if(StringUtils.hasLength(data)){
+                JSONObject parse = JSONObject.parseObject(data);
+                if(parse != null){
+                    Integer total = parse.getInteger("total");
+                    if(total>0){
+                        article = new WechatArticleDetail();
+                        article.setTitle("帮您匹配到"+ total + "个商品,点击查看");
+                        article.setPicUrl(imageDomain + "/group1/M00/03/EC/rB-lGGHg9ZOAe8VdAAJevlD1ofg562.png");
+                        String url = wwwDomain + "product/list.html?keyword="+keyword;
+                        try {
+                            url = URLEncoder.encode(url, "UTF-8");
+                        } catch (UnsupportedEncodingException ignored) {}
+                        article.setLinkUrl("https://open.weixin.qq.com/connect/oauth2/authorize?appid=" + caimeiAppid + "&redirect_uri=" + url +"&response_type=code&scope=snsapi_base&state=reLogin#wechat_redirect");
+                    }
+                }
+            }
+        }
+        if (null != article) {
+            List<WechatArticleDetail> articles = new ArrayList<>();
+            articles.add(article);
+            return MessageUtil.setArticleXml(openid, wxType, articles);
+        }
+        log.info(">>>>>>>>>>默认无关键字匹配回复图文1220");
+        List<WechatArticleDetail> defaultList = weChatDao.getArticleDetailList(1220);
+        if (CollectionUtils.isEmpty(defaultList)) {
+            return MessageUtil.setArticleXml(openid, wxType, defaultList);
+        }
+        return null;
+    }
+
+    /**
+     * 点击自定义菜单事件
+     *
+     * 点击菜单拉取消息时的事件推送:
+     * <xml>
+     *   <ToUserName><![CDATA[toUser]]></ToUserName>
+     *   <FromUserName><![CDATA[FromUser]]></FromUserName>
+     *   <CreateTime>123456789</CreateTime>
+     *   <MsgType><![CDATA[event]]></MsgType>
+     *   <Event><![CDATA[CLICK]]></Event>
+     *   <EventKey><![CDATA[EVENTKEY]]></EventKey>
+     * </xml>
+     *
+     * 点击菜单跳转链接时的事件推送:
+     * <xml>
+     *   <ToUserName><![CDATA[toUser]]></ToUserName>
+     *   <FromUserName><![CDATA[FromUser]]></FromUserName>
+     *   <CreateTime>123456789</CreateTime>
+     *   <MsgType><![CDATA[event]]></MsgType>
+     *   <Event><![CDATA[VIEW]]></Event>
+     *   <EventKey><![CDATA[www.qq.com]]></EventKey>
+     * </xml>
+     */
+    @Override
+    public String handleClickMenuMsg(Map<String, String> requestMap) {
+        // 用户openid
+        String openid = requestMap.get(WeChatConstant.FROM_USER_NAME);
+        // 微信号公众号Id
+        String wxType = requestMap.get(WeChatConstant.TO_USER_NAME);
+        // 事件类型
+        String event = requestMap.get(WeChatConstant.EVENT);
+        // 事件KEY值,设置的跳转URL/与自定义菜单接口中KEY值对应
+        String eventKey = requestMap.get(WeChatConstant.EVENT_KEY);
+        log.info(">>>>>>>>>>处理点击自定义菜单事件,event:" + event + ",eventKey:" + eventKey + ",openid:" + openid + ",wxType:" + wxType);
+        // 匹配数据库回复配置
+        WechatReply reply = weChatDao.getReplyByParams("click", eventKey, wxType);
+        return setXmlByDatabaseReply(openid, wxType, reply);
+    }
+
+    /**
+     * 上报地理位置
+     * <xml>
+     *   <ToUserName><![CDATA[toUser]]></ToUserName>
+     *   <FromUserName><![CDATA[fromUser]]></FromUserName>
+     *   <CreateTime>123456789</CreateTime>
+     *   <MsgType><![CDATA[event]]></MsgType>
+     *   <Event><![CDATA[LOCATION]]></Event>
+     *   <Latitude>23.137466</Latitude>
+     *   <Longitude>113.352425</Longitude>
+     *   <Precision>119.385040</Precision>
+     * </xml>
+     *
+     * * <纬度>23.137466</纬度>
+     *       * <经度>113.352425</经度>
+     *       * <精度>119.385040</精度>
+     */
+    @Override
+    public String handleLocationMsg(Map<String, String> requestMap) {
+        // 用户openid
+        String openid = requestMap.get(WeChatConstant.FROM_USER_NAME);
+        // 微信号公众号Id
+        String wxType = requestMap.get(WeChatConstant.TO_USER_NAME);
+        //Longitude	地理位置经度
+        String longitude = requestMap.get(WeChatConstant.LONGITUDE);
+        //Latitude	地理位置纬度
+        String latitude = requestMap.get(WeChatConstant.LATITUDE);
+        //Precision	地理位置精度
+        String precision = requestMap.get(WeChatConstant.PRECISION);
+        String content = "您当前位置,经度:" + longitude + ",纬度:" + latitude +  ",精度:" + precision;
+        log.info(">>>>>>>>>>处理上报地理位置事件," + content + ",openid:" + openid + ",wxType:" + wxType);
+        return MessageUtil.setTextXml(openid, wxType, content);
+    }
+
+    /**
+     * subscribe(订阅/关注)
+     * <xml>
+     *   <ToUserName><![CDATA[toUser]]></ToUserName>
+     *   <FromUserName><![CDATA[FromUser]]></FromUserName>
+     *   <CreateTime>123456789</CreateTime>
+     *   <MsgType><![CDATA[event]]></MsgType>
+     *   <Event><![CDATA[subscribe]]></Event>
+     * </xml>
+     */
+    @Override
+    public String handleSubscribeMsg(Map<String, String> requestMap) {
+        // 用户openid
+        String openid = requestMap.get(WeChatConstant.FROM_USER_NAME);
+        // 微信号公众号Id
+        String wxType = requestMap.get(WeChatConstant.TO_USER_NAME);
+        log.info(">>>>>>>>>>处理接收的订阅关注事件,openid:" + openid + ",wxType:" + wxType);
+        // 匹配数据库回复配置
+        WechatReply reply = weChatDao.getReplyByParams("subscribe", null, wxType);
+        return setXmlByDatabaseReply(openid, wxType, reply);
+    }
+
+    /**
+     * 扫描带参数二维码事件
+     * <xml>
+     *   <ToUserName><![CDATA[toUser]]></ToUserName>
+     *   <FromUserName><![CDATA[FromUser]]></FromUserName>
+     *   <CreateTime>123456789</CreateTime>
+     *   <MsgType><![CDATA[event]]></MsgType>
+     *   <Event><![CDATA[subscribe]]></Event>
+     *   <EventKey><![CDATA[qrscene_123123]]></EventKey>
+     *   <Ticket><![CDATA[TICKET]]></Ticket>
+     * </xml>
+     */
+    @Override
+    public String handleScanMsg(Map<String, String> requestMap) {
+        // 用户openid
+        String openid = requestMap.get(WeChatConstant.FROM_USER_NAME);
+        // 微信号公众号Id
+        String wxType = requestMap.get(WeChatConstant.TO_USER_NAME);
+        // 扫码参数
+        String eventKey = requestMap.get(WeChatConstant.EVENT_KEY);
+        log.info(">>>>>>>>>>处理扫描带参数二维码事件,二维码参数:" + eventKey + ",openid:" + openid + ",wxType:" + wxType);
+        //todo 用户绑定微信
+
+
+        // 匹配数据库回复配置
+        WechatReply reply = weChatDao.getReplyByParams("subscribe", null, wxType);
+        return setXmlByDatabaseReply(openid, wxType, reply);
+    }
+
+    /**
+     * 根据数据库回复配置设置
+     */
+    private String setXmlByDatabaseReply(String openid, String wxType, WechatReply reply) {
+        if ("news".equals(reply.getResponseType())) {
+            // 回复图文
+            List<WechatArticleDetail> articleList = weChatDao.getArticleDetailList(reply.getRelateId());
+            if (CollectionUtils.isEmpty(articleList)) {
+                return MessageUtil.setArticleXml(openid, wxType, articleList);
+            }
+        } else {
+            // 回复文本
+            String textContent = weChatDao.getTextContent(reply.getRelateId());
+            if (StringUtils.hasLength(textContent)) {
+                return MessageUtil.setTextXml(openid, wxType, textContent);
+            }
+        }
+        return null;
+    }
+}

+ 224 - 0
src/main/java/com/caimei365/wechat/utils/MessageUtil.java

@@ -0,0 +1,224 @@
+package com.caimei365.wechat.utils;
+
+import com.caimei365.wechat.entity.WeChatConstant;
+import com.caimei365.wechat.entity.WechatArticleDetail;
+import lombok.extern.slf4j.Slf4j;
+import org.dom4j.Document;
+import org.dom4j.DocumentHelper;
+import org.dom4j.Element;
+import org.dom4j.io.SAXReader;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Description
+ *
+ * @author : Charles
+ * @date : 2022/1/13
+ */
+@Slf4j
+public class MessageUtil {
+
+    /**
+     * 解析XML
+     */
+    public static Map<String, String> parseXml(HttpServletRequest request) throws Exception {
+        // 将解析结果存储在HashMap中
+        Map<String, String> map = new HashMap<>();
+        // 从request中取得输入流
+        InputStream inputStream = request.getInputStream();
+        // 读取输入流
+        SAXReader reader = new SAXReader();
+        Document document = reader.read(inputStream);
+        // 得到xml根元素
+        Element root = document.getRootElement();
+        // 得到根元素的所有子节点
+        List<Element> elementList = root.elements();
+        // 遍历所有子节点
+        for (Element e : elementList) { map.put(e.getName(), e.getText()); }
+        // 释放资源
+        inputStream.close();
+        return map;
+    }
+
+    /**
+     * 包装xml根返回消息
+     *  <xml>
+     *   <ToUserName><![CDATA[toUser]]></ToUserName>
+     *   <FromUserName><![CDATA[fromUser]]></FromUserName>
+     *   <CreateTime>1399197672</CreateTime>
+     *   <消息内容></消息内容>
+     * </xml>
+     * @return xml字符串
+     */
+    public static String setRootXml(String toUser, String fromUser, String xmlContent) {
+        return "<xml>" +
+                "<ToUserName><![CDATA[" + toUser + "]]></ToUserName>" +
+                "<FromUserName><![CDATA[" + fromUser + "]]></FromUserName>" +
+                "<CreateTime>" + System.currentTimeMillis() + "</CreateTime>" +
+                xmlContent +
+                "</xml>";
+    }
+
+    /**
+     * 将消息转发到到客服系统
+     *  <xml>
+     *   <ToUserName><![CDATA[toUser]]></ToUserName>
+     *   <FromUserName><![CDATA[fromUser]]></FromUserName>
+     *   <CreateTime>1399197672</CreateTime>
+     *   <MsgType><![CDATA[transfer_customer_service]]></MsgType>
+     * </xml>
+     */
+    public static String setCustomerXml(String toUser, String fromUser) {
+        String xmlContent = "<MsgType><![CDATA[transfer_customer_service]]></MsgType>";
+        return setRootXml(toUser, fromUser, xmlContent);
+    }
+
+    /**
+     * 包装xml文本内容
+     * <xml>
+     *   <ToUserName><![CDATA[toUser]]></ToUserName>
+     *   <FromUserName><![CDATA[fromUser]]></FromUserName>
+     *   <CreateTime>12345678</CreateTime>
+     *   <MsgType><![CDATA[text]]></MsgType>
+     *   <Content><![CDATA[你好]]></Content>
+     * </xml>
+     */
+    public static String setTextXml(String toUser, String fromUser, String content) {
+        String xmlContent = "<MsgType><![CDATA[" + WeChatConstant.MESSAGE_TYPE_TEXT + "]]></MsgType>"+
+                "<Content><![CDATA[" + content + "]]></Content>";
+        return setRootXml(toUser, fromUser, xmlContent);
+    }
+
+    /**
+     * 包装xml图文内容
+     * <xml>
+     *   <ToUserName><![CDATA[toUser]]></ToUserName>
+     *   <FromUserName><![CDATA[fromUser]]></FromUserName>
+     *   <CreateTime>12345678</CreateTime>
+     *   <MsgType><![CDATA[news]]></MsgType>
+     *   <ArticleCount>1</ArticleCount>
+     *   <Articles>
+     *     <item>
+     *       <Title><![CDATA[title1]]></Title>
+     *       <Description><![CDATA[description1]]></Description>
+     *       <PicUrl><![CDATA[picurl]]></PicUrl>
+     *       <Url><![CDATA[url]]></Url>
+     *     </item>
+     *   </Articles>
+     * </xml>
+     */
+    public static String setArticleXml(String toUser, String fromUser, List<WechatArticleDetail> articleList) {
+        StringBuilder content = new StringBuilder();
+        content.append("<MsgType><![CDATA[").append(WeChatConstant.MESSAGE_TYPE_NEWS).append("]]></MsgType>");
+        content.append("<ArticleCount><![CDATA[").append(articleList.size()).append("]]></ArticleCount>");
+        content.append("<Articles>");
+        for (WechatArticleDetail article : articleList) {
+            if (null != article) {
+                content.append("<item>");
+                content.append("<Title><![CDATA[").append(article.getTitle()).append("]]></Title>");
+                content.append("<Description><![CDATA[").append(article.getDescription()).append("]]></Description>");
+                content.append("<Url><![CDATA[").append(article.getLinkUrl()).append("]]></Url>");
+                content.append("<PicUrl><![CDATA[").append(article.getPicUrl()).append("]]></PicUrl>");
+                content.append("</item>");
+            }
+        }
+        content.append("</Articles>");
+        return setRootXml(toUser, fromUser, content.toString());
+    }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+//    /**
+//     * (多层)map转换为xml格式字符串
+//     *
+//     * @param map 需要转换为xml的map
+//     * @param isCDATA 是否加入CDATA标识符 true:加入 false:不加入
+//     * @return xml字符串
+//     */
+//    public static String multiMapToXml(Map<String, Object> map, boolean isCDATA){
+//        String parentName = "xml";
+//        Document doc = DocumentHelper.createDocument();
+//        doc.addElement(parentName);
+//        return recursionMapToXml(doc.getRootElement(), parentName, map, isCDATA);
+//    }
+//
+//    /**
+//     * multilayerMapToXml核心方法,递归调用
+//     *
+//     * @param element 节点元素
+//     * @param parentName 根元素属性名
+//     * @param map 需要转换为xml的map
+//     * @param isCDATA 是否加入CDATA标识符 true:加入 false:不加入
+//     * @return xml字符串
+//     */
+//    private static String recursionMapToXml(Element element, String parentName, Map<String, Object> map, boolean isCDATA) {
+//        Element xmlElement = element.addElement(parentName);
+//        map.keySet().forEach(key -> {
+//            Object obj = map.get(key);
+//            if (obj instanceof Map) {
+//                recursionMapToXml(xmlElement, key, (Map<String, Object>)obj, isCDATA);
+//            } else {
+//                String value = obj == null ? "" : obj.toString();
+//                if (isCDATA) {
+//                    xmlElement.addElement(key).addCDATA(value);
+//                } else {
+//                    xmlElement.addElement(key).addText(value);
+//                }
+//            }
+//        });
+//        return xmlElement.asXML();
+//    }
+
+
+//    /**
+//     * 格式化xml,显示为容易看的XML格式
+//     *
+//     * @param xml 需要格式化的xml字符串
+//     * @return
+//     */
+//    public static String formatXML(String xml) {
+//        String requestXML = null;
+//        try {
+//            // 拿取解析器
+//            SAXReader reader = new SAXReader();
+//            Document document = reader.read(new StringReader(xml));
+//            if (null != document) {
+//                StringWriter stringWriter = new StringWriter();
+//                // 格式化,每一级前的空格
+//                OutputFormat format = new OutputFormat("    ", true);
+//                // xml声明与内容是否添加空行
+//                format.setNewLineAfterDeclaration(false);
+//                // 是否设置xml声明头部
+//                format.setSuppressDeclaration(false);
+//                // 是否分行
+//                format.setNewlines(true);
+//                XMLWriter writer = new XMLWriter(stringWriter, format);
+//                writer.write(document);
+//                writer.flush();
+//                writer.close();
+//                requestXML = stringWriter.getBuffer().toString();
+//            }
+//            return requestXML;
+//        } catch (Exception e) {
+//            log.error("格式化xml,失败 --> {0}", e);
+//            return null;
+//        }
+//    }
+}

+ 71 - 0
src/main/java/com/caimei365/wechat/utils/SignUtil.java

@@ -0,0 +1,71 @@
+package com.caimei365.wechat.utils;
+
+import com.caimei365.wechat.entity.WeChatConstant;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+/**
+ * Description
+ *
+ * @author : Charles
+ * @date : 2022/1/13
+ */
+public class SignUtil {
+
+    /**
+     * 加密/校验流程如下:
+     *    1)将token、timestamp、nonce三个参数进行字典序排序
+     *    2)将三个参数字符串拼接成一个字符串进行sha1加密
+     *    3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
+     * @param signature 表示微信加密签名,signature结合了开发者填写的 token 参数和请求中的timestamp参数、nonce参数
+     * @param timestamp 表示时间戳
+     * @param nonce 表示随机数
+     */
+    public static boolean checkSignature(String signature, String timestamp, String nonce) {
+        // 微信 - 服务器配置 -令牌(Token)
+        String token = WeChatConstant.TOKEN;
+        String[] arr = new String[] {token, timestamp, nonce};
+        // 将token、timestamp、nonce三个参数进行字典序排序
+        Arrays.sort(arr);
+        StringBuilder content = new StringBuilder();
+        for (String s : arr) {
+            content.append(s);
+        }
+        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();
+        }
+        // 将sha1加密后的字符串可与signature对比,标识该请求来源于微信
+        return tmpStr != null && tmpStr.equals(signature.toUpperCase());
+    }
+
+    /**
+     * 将字节数组转换为十六进制字符串
+     */
+    private static String byteToStr(byte[] byteArray) {
+        StringBuilder strDigest = new StringBuilder();
+        for (byte b : byteArray) {
+            strDigest.append(byteToHexStr(b));
+        }
+        return strDigest.toString();
+    }
+
+    /**
+     * 将字节转换为十六进制字符串
+     */
+    private static String byteToHexStr(byte mByte) {
+        char[] digit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
+        char[] tempArr = new char[2];
+        tempArr[0] = digit[(mByte >>> 4) & 0X0F];
+        tempArr[1] = digit[mByte & 0X0F];
+        return new String(tempArr);
+    }
+}

+ 1 - 1
src/main/resources/application.yml

@@ -1,5 +1,5 @@
 server:
-  port: 18016
+  port: 30000
   servlet:
     encoding:
       charset: UTF-8

+ 6 - 0
src/main/resources/config/beta/application-beta.yml

@@ -24,6 +24,12 @@ logging:
   level:
     root: info
 
+# 服务域名
+caimei:
+  wwwDomain: https://www-b.caimei365.com
+  coreDomain: https://core-b.caimei365.com
+  imageDomain: https://img-b.caimei365.com
+
 # 微信公众号
 wechat:
   apiUrl: https://api.weixin.qq.com

+ 6 - 0
src/main/resources/config/dev/application-dev.yml

@@ -24,6 +24,12 @@ logging:
     root: info
     com.caimei.wechat.mapper: debug
 
+# 服务域名
+caimei:
+  wwwDomain: https://www-b.caimei365.com
+  coreDomain: https://core-b.caimei365.com
+  imageDomain: https://img-b.caimei365.com
+
 # 微信公众号
 wechat:
   apiUrl: https://api.weixin.qq.com

+ 6 - 0
src/main/resources/config/prod/application-prod.yml

@@ -24,6 +24,12 @@ logging:
   level:
     root: info
 
+# 服务域名
+caimei:
+  wwwDomain: https://www.caimei365.com
+  coreDomain: https://core.caimei365.com
+  imageDomain: https://img.caimei365.com
+
 # 微信公众号
 wechat:
   apiUrl: https://api.weixin.qq.com

+ 21 - 0
src/main/resources/mapper/WeChatDao.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.caimei365.wechat.dao.WeChatDao">
+    <select id="getReplyByParams" resultType="com.caimei365.wechat.entity.WechatReply">
+        SELECT DISTINCT a.id, a.keyword, a.title, a.responsetype AS responseType, a.msgtype AS msgType, a.relateId AS relateId
+        FROM wechat_reply a
+        WHERE msgType = #{msgType} AND (wxType = '0' OR wxType = #{wxType})
+        <if test="keyword != null and keyword != ''">
+            AND	keyword = #{keyword}
+        </if>
+        ORDER BY updateTime DESC
+    </select>
+    <select id="getArticleDetailList" resultType="com.caimei365.wechat.entity.WechatArticleDetail">
+        SELECT a.id AS id,a.articleId AS articleId, a.title, a.linkurl AS linkUrl, a.picurl AS picUrl
+        FROM wechat_article_detail a WHERE a.articleId = #{articleId}
+        LIMIT 0,7
+    </select>
+    <select id="getTextContent" resultType="java.lang.String">
+        SELECT content FROM wechat_text WHERE id = #{id}
+    </select>
+</mapper>