Selaa lähdekoodia

微信公众号通知消息补丁

wany 1 vuosi sitten
vanhempi
sitoutus
6dc86a7e70

+ 91 - 0
src/main/java/org/rcisoft/aspect/wx/pubnum/WeChatMsgPutAspect.java

@@ -0,0 +1,91 @@
+package org.rcisoft.aspect.wx.pubnum;
+
+import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.rcisoft.aspect.wx.pubnum.entity.WxOpenIdMapping;
+import org.rcisoft.aspect.wx.pubnum.service.IWxOpenIdMappingService;
+import org.rcisoft.aspect.wx.pubnum.wx.WxPublicApi;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 应用模块名称</p>
+ * 代码描述</p>
+ * Copyright: Copyright (C) 2023 , Inc. All rights reserved. <p>
+ * Company: 成都诚唐科技有限责任公司</p>
+ *
+ * @author wany
+ * @since 2023/10/25
+ */
+@Aspect
+@Component
+public class WeChatMsgPutAspect{
+
+    private static final Logger log = LoggerFactory.getLogger(WeChatMsgPutAspect.class);
+
+    @Autowired
+    private IWxOpenIdMappingService wxOpenIdService;
+
+    @Autowired
+    private WxPublicApi api;
+
+    /**
+     * 切点
+     * wx登录时获取用户信息,保存openId、unionId到数据库
+     *
+     * @param point
+     * @return
+     */
+    @Around("execution (* org.rcisoft.tencent.service.WxUnionIdAware.setUnionId(..))")
+    public Object setUnionId(ProceedingJoinPoint point) throws Throwable{
+        log.info("------获取微信openId切面 开始执行------");
+        Object[] args = point.getArgs();
+        String openId = (String) args[0];
+        String unionId = (String) args[1];
+        if(StringUtils.isAnyEmpty(unionId, openId)){
+            log.warn("unionId不存在");
+        } else{
+            //查询是否存在,不存在就新建
+            wxOpenIdService.saveData(unionId, openId, null);
+        }
+        log.info("------获取微信openId切面 执行完成------");
+        return point.proceed();
+    }
+
+
+    @Around("execution (* org.rcisoft.tencent.service.impl.CyWxMiniServiceImpl.pushWeChatMessage(..))")
+    public Object pushWeChatMessage(ProceedingJoinPoint point) throws Throwable{
+        log.info("------统一发送微信公众号消息切面 开始执行------");
+        Object[] args = point.getArgs();
+        String accessToken = (String) args[0];
+        String openId = (String) args[1];
+        String templateId = (String) args[2];
+        String path = (String) args[3];
+        Map<String, Object> dataMap = (Map<String, Object>) args[4];
+        //根据小程序openId 获取 公众号openId
+        WxOpenIdMapping mapping = wxOpenIdService.findByAppOpenId(openId);
+        if(mapping != null && StringUtils.isNotEmpty(mapping.getPublicOId()) &&
+                api.getPublicApp().equals(mapping.getPublicAId())){
+            //测试环境用的信息
+            //            templateId = "-yIJ9E7vXQUa15PZcuwgsZZ_hzDPnwD1TqTQdSbiee0";
+            //            String dataStr = "{\"thing3\":{\"value\":\"测试单位\"},
+            //            \"time5\":{\"value\":\"2023年10月31日11:17\"}," +
+            //            "\"thing1\":{\"value\":\"测试人员\"},\"phone_number2\":{\"value\":\"18180694617\"}," +
+            //            "\"thing4\":{\"value\":\"测试\"}}";
+            //            api.sendMsg(mapping.getPublicOId(), templateId, null, JSONUtil.parseObj(dataStr));
+            api.sendMsg(mapping.getPublicOId(), templateId, path, dataMap);
+        } else{
+            log.error("openId映射缺失,调用之前的老接口");
+            return point.proceed();
+        }
+        log.info("------统一发送微信公众号消息切面 执行完成------");
+        return null;
+    }
+
+}

+ 102 - 0
src/main/java/org/rcisoft/aspect/wx/pubnum/controller/WxAspectController.java

@@ -0,0 +1,102 @@
+package org.rcisoft.aspect.wx.pubnum.controller;
+
+import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
+
+import com.alibaba.fastjson.JSONObject;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.rcisoft.aspect.wx.pubnum.service.IWxOpenIdMappingService;
+import org.rcisoft.aspect.wx.pubnum.utils.WxPublicUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 应用模块名称</p>
+ * 代码描述</p>
+ * Copyright: Copyright (C) 2023 , Inc. All rights reserved. <p>
+ * Company: 成都诚唐科技有限责任公司</p>
+ *
+ * @author wany
+ * @since 2023/10/25
+ */
+@Api(value = "WxAspectController")
+@RestController
+@RequestMapping("/aspect/wx/pubnum")
+public class WxAspectController{
+
+    //定义一个固定的token;
+    private static final String TOKEN = "6539d76d10103936d50a986c";
+
+    private static final Logger log = LoggerFactory.getLogger(WxAspectController.class);
+
+    @Autowired
+    private IWxOpenIdMappingService wxOpenIdService;
+
+    @ApiOperation(value = "初始化-获取微信公众号关注用户信息,并记录到数据库表中", notes = "", httpMethod = "GET")
+    @GetMapping("/init_data")
+    public String loadData(){
+        long r = wxOpenIdService.updateInitData();
+        return "共更新" + r + "个用户的微信公众号openId信息";
+    }
+
+    /**
+     * 微信服务器发送信息,进行验签
+     *
+     * @param request
+     * @return
+     */
+    @GetMapping("/token")
+    public String checkToken2(HttpServletRequest request){
+        try{
+            if(StringUtils.isNotBlank(request.getParameter("signature"))){
+                String signature = request.getParameter("signature");
+                String timestamp = request.getParameter("timestamp");
+                String nonce = request.getParameter("nonce");
+                String echostr = request.getParameter("echostr");
+                log.info("signature[{}], timestamp[{}], nonce[{}], echostr[{}]", signature, timestamp, nonce, echostr);
+                if(WxPublicUtils.checkSignature(signature, timestamp, nonce, TOKEN)){
+                    log.info("数据源为微信后台,将echostr[{}]返回!", echostr);
+                    return echostr;
+                }
+            }
+        } catch(Exception e){
+            log.error(e.getMessage(), e);
+        }
+        return "";
+    }
+
+    /**
+     * 关注公众号验签
+     */
+    @PostMapping("/token")
+    public String checkTokenPost(HttpServletRequest request){
+        try{
+            // 读取参数,解析Xml为map
+            Map<String, String> map = WxPublicUtils.transferXmlToMap(WxPublicUtils.readRequest(request));
+            JSONObject jsonObject = JSONObject.parseObject(JSONObject.toJSONString(map));
+            if(ObjectUtils.isNotEmpty(jsonObject)){
+                String msgType = jsonObject.getString("MsgType");
+                String toUserName = jsonObject.getString("ToUserName");
+                String fromUserName = jsonObject.getString("FromUserName");
+                String createTime = jsonObject.getString("CreateTime");
+                String event = jsonObject.getString("Event");
+                //用户关注
+                if("event".equals(msgType) && "subscribe".equals(event)){
+                    wxOpenIdService.saveDataByPublicOpenId(fromUserName);
+                }
+            }
+        } catch(Exception e){
+            log.error(e.getMessage(), e);
+        }
+        return "";
+    }
+
+}

+ 19 - 0
src/main/java/org/rcisoft/aspect/wx/pubnum/dao/WxOpenIdMappingRepository.java

@@ -0,0 +1,19 @@
+package org.rcisoft.aspect.wx.pubnum.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import org.rcisoft.aspect.wx.pubnum.entity.WxOpenIdMapping;
+
+/**
+ * 应用模块名称</p>
+ * 代码描述</p>
+ * Copyright: Copyright (C) 2023 , Inc. All rights reserved. <p>
+ * Company: 成都诚唐科技有限责任公司</p>
+ *
+ * @author wany
+ * @since 2023/10/25
+ */
+@Mapper
+public interface WxOpenIdMappingRepository extends BaseMapper<WxOpenIdMapping>{
+
+}

+ 50 - 0
src/main/java/org/rcisoft/aspect/wx/pubnum/entity/WxOpenIdMapping.java

@@ -0,0 +1,50 @@
+package org.rcisoft.aspect.wx.pubnum.entity;
+
+import java.io.Serial;
+import java.util.Date;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import org.rcisoft.core.entity.CyIdIncreEntity;
+import org.rcisoft.core.entity.CyIdNotDataEntity;
+
+/**
+ * 应用模块名称</p>
+ * 代码描述</p>
+ * Copyright: Copyright (C) 2023 , Inc. All rights reserved. <p>
+ * Company: 成都诚唐科技有限责任公司</p>
+ *
+ * @author wany
+ * @since 2023/10/25
+ */
+
+@Data
+@TableName("wx_open_id_mapping")
+public class WxOpenIdMapping extends CyIdNotDataEntity<WxOpenIdMapping>{
+
+    @Serial
+    private static final long serialVersionUID = 1481373963915591738L;
+
+    @TableField("union_id")
+    private String unionId;
+
+    @TableField("mini_a_id")
+    private String miniAId;
+
+    @TableField("mini_o_id")
+    private String miniOId;
+
+    @TableField("public_a_id")
+    private String publicAId;
+
+    @TableField("public_o_id")
+    private String publicOId;
+
+    @TableField("gmt_create")
+    private Date gmtCreate;
+
+    @TableField("gmt_modified")
+    private Date gmtModified;
+
+}

+ 29 - 0
src/main/java/org/rcisoft/aspect/wx/pubnum/service/IWxOpenIdMappingService.java

@@ -0,0 +1,29 @@
+package org.rcisoft.aspect.wx.pubnum.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import org.rcisoft.aspect.wx.pubnum.entity.WxOpenIdMapping;
+
+/**
+ * 应用模块名称</p>
+ * 代码描述</p>
+ * Copyright: Copyright (C) 2023 , Inc. All rights reserved. <p>
+ * Company: 成都诚唐科技有限责任公司</p>
+ *
+ * @author wany
+ * @since 2023/10/25
+ */
+public interface IWxOpenIdMappingService extends IService<WxOpenIdMapping>{
+
+    long updateInitData();
+
+    void saveData(String unionId, String appOpenId, String publicOpenId);
+
+    void saveDataByPublicOpenId(String publicOpenId);
+
+    WxOpenIdMapping findByUnionId(String unionId);
+
+    WxOpenIdMapping findByAppOpenId(String appOpenId);
+
+    WxOpenIdMapping findByPublicOpenId(String publicOpenId);
+
+}

+ 226 - 0
src/main/java/org/rcisoft/aspect/wx/pubnum/service/impl/WxOpenIdMappingServiceImpl.java

@@ -0,0 +1,226 @@
+package org.rcisoft.aspect.wx.pubnum.service.impl;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import cn.hutool.core.util.IdUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.google.common.collect.Lists;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.rcisoft.aspect.wx.pubnum.dao.WxOpenIdMappingRepository;
+import org.rcisoft.aspect.wx.pubnum.entity.WxOpenIdMapping;
+import org.rcisoft.aspect.wx.pubnum.service.IWxOpenIdMappingService;
+import org.rcisoft.aspect.wx.pubnum.wx.WxPublicApi;
+import org.rcisoft.aspect.wx.pubnum.wx.WxPublicUserInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * 应用模块名称</p>
+ * 代码描述</p>
+ * Copyright: Copyright (C) 2023 , Inc. All rights reserved. <p>
+ * Company: 成都诚唐科技有限责任公司</p>
+ *
+ * @author wany
+ * @since 2023/10/25
+ */
+@Service
+@Transactional(rollbackFor = Exception.class)
+public class WxOpenIdMappingServiceImpl extends ServiceImpl<WxOpenIdMappingRepository, WxOpenIdMapping>
+        implements IWxOpenIdMappingService{
+
+    @Autowired
+    private WxPublicApi api;
+
+    @Override
+    public long updateInitData(){
+        long r = 0;
+        List<WxPublicUserInfo> wxPublicUserInfos = getWxPublicUserInfos();
+        if(wxPublicUserInfos.stream().anyMatch(e -> StringUtils.isEmpty(e.getUnionId()))){
+            throw new RuntimeException("微信小程序和微信公众号");
+        }
+        Date date = new Date();
+        List<WxOpenIdMapping> updateDatas = createUpdateMapping(wxPublicUserInfos, date);
+        //更新
+        if(CollectionUtils.isNotEmpty(updateDatas)){
+            r += updateDatas.size();
+            this.updateBatchById(updateDatas);
+        }
+        //新建
+        if(CollectionUtils.isNotEmpty(wxPublicUserInfos)){
+            List<WxOpenIdMapping> mappings = wxPublicUserInfos.stream().map(e -> {
+                WxOpenIdMapping wxOpenId = new WxOpenIdMapping();
+                wxOpenId.setBusinessId(IdUtil.objectId());
+                wxOpenId.setUnionId(e.getUnionId());
+                wxOpenId.setMiniAId(api.getMiniApp());
+                wxOpenId.setMiniOId(null);
+                wxOpenId.setPublicAId(api.getPublicApp());
+                wxOpenId.setPublicOId(e.getOpenId());
+                wxOpenId.setGmtCreate(date);
+                wxOpenId.setGmtModified(date);
+                return wxOpenId;
+            }).collect(Collectors.toList());
+            this.saveBatch(mappings);
+            r += mappings.size();
+        }
+        return r;
+    }
+
+    private List<WxOpenIdMapping> createUpdateMapping(List<WxPublicUserInfo> wxPublicUserInfos, Date date){
+        List<WxOpenIdMapping> updateDatas = new ArrayList<>();
+        Map<String, WxPublicUserInfo> map =
+                wxPublicUserInfos.stream().collect(Collectors.toMap(WxPublicUserInfo::getUnionId, e -> e));
+        boolean next = true;
+        int pageNum = 1;
+        do{
+            IPage<WxOpenIdMapping> page = this.page(new Page<>(pageNum, 1000));
+            List<WxOpenIdMapping> list = page.getRecords();
+            if(CollectionUtils.isEmpty(list)){
+                next = false;
+            }
+            for(WxOpenIdMapping mapping : list){
+                WxPublicUserInfo info = map.get(mapping.getUnionId());
+                if(info == null){
+                    continue;
+                }
+                wxPublicUserInfos.remove(info);
+                String openId = info.getOpenId();
+                String appId = api.getPublicApp();
+                if(mapping.getPublicAId().equals(appId) && mapping.getPublicOId().equals(openId)){
+                    continue;
+                }
+                WxOpenIdMapping update = new WxOpenIdMapping();
+                update.setBusinessId(mapping.getBusinessId());
+                update.setPublicAId(appId);
+                update.setPublicOId(openId);
+                update.setGmtModified(date);
+                updateDatas.add(update);
+            }
+            pageNum++;
+        } while(next);
+        return updateDatas;
+    }
+
+    private List<WxPublicUserInfo> getWxPublicUserInfos(){
+        List<WxPublicUserInfo> wxPublicUserInfos = new ArrayList<>();
+        String nextOpenId = null;
+        do{
+            //循环获取关注用户的openId
+            Object[] usersOpenIdList = api.getUsersOpenIdList(nextOpenId);
+            Long total = (Long) usersOpenIdList[0];
+            Long count = (Long) usersOpenIdList[1];
+            List<String> openIds = (List<String>) usersOpenIdList[2];
+            //没有查询到数据,就直接跳出循环
+            if(total == null || count == null || CollectionUtils.isEmpty(openIds)){
+                nextOpenId = null;
+                break;
+            }
+            String lastOpenId = openIds.get(openIds.size() - 1);
+            if(lastOpenId.equals(nextOpenId)){
+                //查询不到新关注用户了,就跳出循环
+                nextOpenId = null;
+                break;
+            } else{
+                nextOpenId = lastOpenId;
+            }
+
+            List<List<String>> lists = Lists.partition(openIds, 100);
+            for(List<String> list : lists){
+                List<WxPublicUserInfo> userInfos = api.getUserInfos(list);
+                if(CollectionUtils.isNotEmpty(userInfos)){
+                    wxPublicUserInfos.addAll(userInfos);
+                }
+            }
+        } while(nextOpenId != null);
+        return wxPublicUserInfos;
+    }
+
+    @Override
+    public void saveData(String unionId, String appOpenId, String publicOpenId){
+        if(StringUtils.isEmpty(unionId)){
+            log.error("unionId为空,无法更新数据");
+            return;
+        }
+        if(StringUtils.isAllEmpty(appOpenId, publicOpenId)){
+            log.error("openId为空,无法更新数据");
+            return;
+        }
+        WxOpenIdMapping mapping = findByUnionId(unionId);
+        if(mapping == null){
+            WxOpenIdMapping wxOpenId = new WxOpenIdMapping();
+            wxOpenId.setBusinessId(IdUtil.objectId());
+            wxOpenId.setUnionId(unionId);
+            wxOpenId.setMiniAId(api.getMiniApp());
+            wxOpenId.setMiniOId(appOpenId);
+            wxOpenId.setPublicAId(api.getPublicApp());
+            wxOpenId.setPublicOId(publicOpenId);
+            wxOpenId.setGmtCreate(new Date());
+            wxOpenId.setGmtModified(wxOpenId.getGmtCreate());
+            this.save(wxOpenId);
+            return;
+        }
+        WxOpenIdMapping update = new WxOpenIdMapping();
+        if(StringUtils.isNotEmpty(appOpenId) && !appOpenId.equals(mapping.getMiniOId())){
+            update.setMiniOId(appOpenId);
+            update.setMiniAId(api.getMiniApp());
+        }
+        if(StringUtils.isNotEmpty(publicOpenId) && !publicOpenId.equals(mapping.getPublicOId())){
+            update.setPublicOId(publicOpenId);
+            update.setPublicAId(api.getPublicApp());
+        }
+        if(update.getPublicOId() != null || update.getMiniOId() != null){
+            update.setBusinessId(mapping.getBusinessId());
+            update.setGmtModified(new Date());
+            this.updateById(update);
+        }
+    }
+
+    @Override
+    public void saveDataByPublicOpenId(String publicOpenId){
+        WxPublicUserInfo userInfo = api.getUserInfo(publicOpenId);
+        if(userInfo != null){
+            this.saveData(userInfo.getUnionId(), null, publicOpenId);
+        }
+    }
+
+    @Override
+    public WxOpenIdMapping findByUnionId(String unionId){
+        if(StringUtils.isEmpty(unionId)){
+            return null;
+        }
+        LambdaQueryWrapper<WxOpenIdMapping> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(WxOpenIdMapping::getUnionId, unionId);
+        return this.getOne(wrapper);
+    }
+
+    @Override
+    public WxOpenIdMapping findByAppOpenId(String appOpenId){
+        if(StringUtils.isEmpty(appOpenId)){
+            return null;
+        }
+        LambdaQueryWrapper<WxOpenIdMapping> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(WxOpenIdMapping::getMiniOId, appOpenId);
+        wrapper.eq(WxOpenIdMapping::getMiniAId, api.getMiniApp());
+        return this.getOne(wrapper);
+    }
+
+    @Override
+    public WxOpenIdMapping findByPublicOpenId(String publicOpenId){
+        if(StringUtils.isEmpty(publicOpenId)){
+            return null;
+        }
+        LambdaQueryWrapper<WxOpenIdMapping> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(WxOpenIdMapping::getPublicOId, publicOpenId);
+        wrapper.eq(WxOpenIdMapping::getPublicAId, api.getPublicApp());
+        return this.getOne(wrapper);
+    }
+
+}

+ 180 - 0
src/main/java/org/rcisoft/aspect/wx/pubnum/utils/WxPublicUtils.java

@@ -0,0 +1,180 @@
+package org.rcisoft.aspect.wx.pubnum.utils;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
+
+import cn.hutool.core.util.IdUtil;
+import org.apache.commons.lang3.StringUtils;
+import org.jdom.Document;
+import org.jdom.Element;
+import org.jdom.JDOMException;
+import org.jdom.input.SAXBuilder;
+
+/**
+ * 应用模块名称</p>
+ * 代码描述</p>
+ * Copyright: Copyright (C) 2023 , Inc. All rights reserved. <p>
+ * Company: 成都诚唐科技有限责任公司</p>
+ *
+ * @author wany
+ * @since 2023/10/25
+ */
+public class WxPublicUtils{
+
+    /**
+     * 验证微信签名
+     */
+    public static boolean checkSignature(String signature, String timestamp, String nonce, String token){
+        // 1.将token、timestamp、nonce三个参数进行字典序排序
+        String[] arr = new String[]{token, timestamp, nonce};
+        Arrays.sort(arr);
+        // 2. 将三个参数字符串拼接成一个字符串进行sha1加密
+        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();
+        }
+        content = null;
+        // 3.将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);
+    }
+
+    //工具类
+    public static Map<String, String> transferXmlToMap(String strxml){
+        strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
+        if(StringUtils.isEmpty(strxml)){
+            return null;
+        }
+        InputStream in = null;
+        Document doc = null;
+        try{
+            in = new ByteArrayInputStream(strxml.getBytes(StandardCharsets.UTF_8));
+            doc = new SAXBuilder().build(in);
+        } catch(JDOMException e){
+            // 统一转化为 IO 异常输出
+            e.printStackTrace();
+        } catch(IOException e){
+            e.printStackTrace();
+        } finally{
+            try{
+                if(in != null){
+                    in.close();
+                }
+            } catch(IOException e){
+                e.printStackTrace();
+            }
+        }
+        Map<String, String> m = new HashMap<>();
+        if(doc == null){
+            return m;
+        }
+        // 解析 DOM
+        Element root = doc.getRootElement();
+        List list = root.getChildren();
+        for(Object o : list){
+            Element e = (Element) o;
+            String k = e.getName();
+            String v = "";
+            List children = e.getChildren();
+            if(children.isEmpty()){
+                v = e.getTextNormalize();
+            } else{
+                v = getChildrenText(children);
+            }
+            m.put(k, v);
+        }
+        return m;
+    }
+
+    /**
+     * 辅助 transferXmlToMap 方法递归提取子节点数据
+     */
+    private static String getChildrenText(List<Element> children){
+        StringBuilder sb = new StringBuilder();
+        if(!children.isEmpty()){
+            for(Element e : children){
+                String name = e.getName();
+                String value = e.getTextNormalize();
+                List list = e.getChildren();
+                sb.append("<").append(name).append(">");
+                if(!list.isEmpty()){
+                    sb.append(getChildrenText(list));
+                }
+                sb.append(value);
+                sb.append("</").append(name).append(">");
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 读取 request body 内容作为字符串
+     *
+     * @param request
+     * @return
+     */
+    public static String readRequest(HttpServletRequest request){
+        InputStream inputStream = null;
+        BufferedReader in = null;
+        StringBuilder sb = new StringBuilder();
+        try{
+            String str;
+            inputStream = request.getInputStream();
+            in = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+            while((str = in.readLine()) != null){
+                sb.append(str);
+            }
+        } catch(IOException e){
+            e.printStackTrace();
+        } finally{
+            if(in != null){
+                try{
+                    in.close();
+                } catch(IOException e){
+                    e.printStackTrace();
+                }
+            }
+            if(inputStream != null){
+                try{
+                    inputStream.close();
+                } catch(IOException e){
+                    e.printStackTrace();
+                }
+            }
+        }
+        return sb.toString();
+    }
+
+}

+ 417 - 0
src/main/java/org/rcisoft/aspect/wx/pubnum/wx/WxPublicApi.java

@@ -0,0 +1,417 @@
+package org.rcisoft.aspect.wx.pubnum.wx;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import lombok.Data;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+/**
+ * 应用模块名称</p>
+ * 公众号接口</p>
+ * Copyright: Copyright (C) 2023 , Inc. All rights reserved. <p>
+ * Company: 成都诚唐科技有限责任公司</p>
+ *
+ * @author wany
+ * @since 2023 /10/25
+ */
+@Data
+@Component
+public class WxPublicApi{
+
+    private static final Logger log = LoggerFactory.getLogger(WxPublicApi.class);
+
+    // appId
+    @Value("${wx.appId:}")
+    private String miniApp;
+
+    // appId
+    @Value("${wx.publicNumberAppId:}")
+    private String publicApp;
+
+    // secret
+    @Value("${wx.publicNumberSecret:}")
+    private String publicSecret;
+
+    /**
+     * 获取公众号token
+     */
+    private static final String TOKEN_URL =
+            "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
+
+    /**
+     * 发送公众号消息
+     */
+    private static final String MSG_URL = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=%s";
+
+    /**
+     * 批量获取公众号关注用户列表
+     */
+    private static final String USER_LIST = "https://api.weixin.qq.com/cgi-bin/user/get?access_token=%s";
+
+    /**
+     *
+     */
+    private static final String USER_INFO =
+            "https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN";
+
+    /**
+     * 批量获取公众号用户信息
+     */
+    public static final String BATCH_USER_INFO = "https://api.weixin.qq.com/cgi-bin/user/info/batchget?access_token=%s";
+
+    /**
+     * 获取token
+     *
+     * @return string
+     */
+    public String getAccessToken(){
+        log.info("------获取公众号token 开始------");
+        String getTokenUrl = String.format(TOKEN_URL, publicApp, publicSecret);
+        CloseableHttpClient client = null;
+        CloseableHttpResponse response = null;
+        String token = "";
+        try{
+            // 创建http GET请求
+            HttpGet httpGet = new HttpGet(getTokenUrl);
+            client = HttpClients.createDefault();
+            // 执行请求
+            response = client.execute(httpGet);
+            if(response.getStatusLine().getStatusCode() == HttpStatus.SC_OK){
+                HttpEntity entity = response.getEntity();//得到返回数据
+                String result = EntityUtils.toString(entity);
+                log.info("请求结果:" + result);
+                JSONObject jsonObject = JSON.parseObject(result);
+                token = jsonObject.getString("access_token");
+                if(StringUtils.isEmpty(token)){
+                    log.error("数据出错了!");
+                } else{
+                    token = jsonObject.getString("access_token");
+                    String expires_in = jsonObject.getString("expires_in");
+                }
+            } else{
+                log.error("请求失败:状态码:" + response.getStatusLine().getStatusCode());
+            }
+        } catch(Exception e){
+            log.error(e.getMessage(), e);
+        } finally{
+            if(response != null){
+                try{
+                    response.close();
+                } catch(IOException e){
+                    log.error(e.getMessage(), e);
+                }
+            }
+            if(client != null){
+                try{
+                    client.close();
+                } catch(IOException e){
+                    log.error(e.getMessage(), e);
+                }
+            }
+        }
+        log.info("------获取公众号token 结束------");
+        return token;
+    }
+
+    /**
+     * 发送模板消息
+     *
+     * @param openId     the open id
+     * @param templateId the template id
+     * @param path       the path
+     * @param dataMap    the data map
+     */
+    public void sendMsg(String openId, String templateId, String path, Map<String, Object> dataMap){
+        String accessToken = getAccessToken();
+        log.info("------发送公众号消息 开始------");
+        if(StringUtils.isEmpty(accessToken)){
+            log.error("发送公众号消息失败,token为空!");
+            return;
+        }
+        String sendUrl = String.format(MSG_URL, accessToken);
+        CloseableHttpClient client = null;
+        CloseableHttpResponse response = null;
+        try{
+            // 创建http post请求
+            HttpPost httpPost = new HttpPost(sendUrl);
+            client = HttpClients.createDefault();
+            //构建请求参数
+            JSONObject map = new JSONObject();
+            map.put("touser", openId);
+            map.put("template_id", templateId);
+            Map<String, String> miniProgram = new HashMap<>();
+            miniProgram.put("appid", miniApp);
+            if(StringUtils.isNotEmpty(path)){
+                miniProgram.put("pagepath", path);
+            }
+            map.put("miniprogram", miniProgram);
+            map.put("data", dataMap);
+            log.info("请求参数:" + JSON.toJSONString(map));
+            StringEntity entity = new StringEntity(map.toString(), StandardCharsets.UTF_8);
+            entity.setContentEncoding("UTF-8");
+            entity.setContentType("application/json");
+            httpPost.setEntity(entity);
+            response = client.execute(httpPost);
+            if(response.getStatusLine().getStatusCode() == HttpStatus.SC_OK){
+                String result = EntityUtils.toString(response.getEntity());
+                log.info("请求结果:" + result);
+                JSONObject jsonObject = JSON.parseObject(result);
+                if("ok".equals(jsonObject.getString("errmsg"))){
+                    log.info("公众号模板消息推送成功---------------------");
+                } else{
+                    log.error("数据出错了!");
+                }
+            } else{
+                log.error("请求失败:状态码:" + response.getStatusLine().getStatusCode());
+            }
+        } catch(Exception e){
+            log.error(e.getMessage(), e);
+        } finally{
+            if(response != null){
+                try{
+                    response.close();
+                } catch(IOException e){
+                    log.error(e.getMessage(), e);
+                }
+            }
+            if(client != null){
+                try{
+                    client.close();
+                } catch(IOException e){
+                    log.error(e.getMessage(), e);
+                }
+            }
+        }
+        log.info("------发送公众号消息 结束------");
+    }
+
+    /**
+     * 获取关注用户列表
+     *
+     * @param nextOpenId the next open id
+     * @return object [ ]
+     */
+    public Object[] getUsersOpenIdList(String nextOpenId){
+        String accessToken = getAccessToken();
+        Long total = null;
+        Long count = null;
+        List<String> list = new ArrayList<>();
+        if(StringUtils.isEmpty(accessToken)){
+            return new Object[]{total, count, list};
+        }
+        log.info("------批量获取公众号关注用户列表 开始------");
+        String url = String.format(USER_LIST, accessToken);
+        if(StringUtils.isNotEmpty(nextOpenId)){
+            url = url + "&next_openid=" + nextOpenId;
+        }
+        CloseableHttpClient client = null;
+        CloseableHttpResponse response = null;
+        try{
+            // 创建http GET请求
+            HttpGet httpGet = new HttpGet(url);
+            client = HttpClients.createDefault();
+            //执行请求
+            response = client.execute(httpGet);
+            if(response.getStatusLine().getStatusCode() == HttpStatus.SC_OK){
+                //得到返回数据
+                HttpEntity entity = response.getEntity();
+                String result = EntityUtils.toString(entity);
+                log.info("请求结果:" + result);
+                JSONObject json = JSON.parseObject(result);
+                if(StringUtils.isNotEmpty(json.getString("errmsg")) ||
+                        StringUtils.isNotEmpty(json.getString("errcode"))){
+                    //出错了
+                    log.error("数据出错了!");
+                } else{
+                    total = json.getLong("total");
+                    count = json.getLong("count");
+                    JSONObject data = json.getJSONObject("data");
+                    if(data != null){
+                        JSONArray openids = data.getJSONArray("openid");
+                        if(CollectionUtils.isNotEmpty(openids)){
+                            for(Object o : openids){
+                                String openId = o.toString();
+                                if(StringUtils.isNotEmpty(openId)){
+                                    list.add(openId);
+                                }
+                            }
+                        }
+                    }
+                }
+            } else{
+                log.error("请求失败:状态码:" + response.getStatusLine().getStatusCode());
+            }
+        } catch(Exception e){
+            log.error(e.getMessage(), e);
+        } finally{
+            if(response != null){
+                try{
+                    response.close();
+                } catch(IOException e){
+                    log.error(e.getMessage(), e);
+                }
+            }
+            if(client != null){
+                try{
+                    client.close();
+                } catch(IOException e){
+                    log.error(e.getMessage(), e);
+                }
+            }
+        }
+        log.info("------批量获取公众号关注用户列表 结束------");
+        return new Object[]{total, count, list};
+    }
+
+    public WxPublicUserInfo getUserInfo(String publicOpenId){
+        WxPublicUserInfo r = null;
+        if(StringUtils.isEmpty(publicOpenId)){
+            return r;
+        }
+        String accessToken = getAccessToken();
+        if(StringUtils.isEmpty(accessToken)){
+            return r;
+        }
+        log.info("------获取公众号用户信息 开始------");
+        String sendUrl = String.format(USER_INFO, accessToken, publicOpenId);
+        CloseableHttpClient client = null;
+        CloseableHttpResponse response = null;
+        try{
+            // 创建http post请求
+            HttpGet httpGet = new HttpGet(sendUrl);
+            client = HttpClients.createDefault();
+            response = client.execute(httpGet);
+            if(response.getStatusLine().getStatusCode() == HttpStatus.SC_OK){
+                String result = EntityUtils.toString(response.getEntity());
+                log.info("请求结果:" + result);
+                JSONObject json = JSON.parseObject(result);
+                if(StringUtils.isNotEmpty(json.getString("errmsg")) ||
+                        StringUtils.isNotEmpty(json.getString("errcode"))){
+                    log.error("数据出错了!");
+                } else{
+                    r = WxPublicUserInfo.build(json);
+                }
+            } else{
+                log.error("请求失败:状态码:" + response.getStatusLine().getStatusCode());
+            }
+        } catch(Exception e){
+            log.error(e.getMessage(), e);
+        } finally{
+            if(response != null){
+                try{
+                    response.close();
+                } catch(IOException e){
+                    log.error(e.getMessage(), e);
+                }
+            }
+            if(client != null){
+                try{
+                    client.close();
+                } catch(IOException e){
+                    log.error(e.getMessage(), e);
+                }
+            }
+        }
+        log.info("------获取公众号用户信息 结束------");
+        return r;
+    }
+
+    public List<WxPublicUserInfo> getUserInfos(List<String> openIds){
+        List<WxPublicUserInfo> list = new ArrayList<>();
+        if(CollectionUtils.isEmpty(openIds)){
+            return list;
+        }
+        String accessToken = getAccessToken();
+        if(StringUtils.isEmpty(accessToken)){
+            return list;
+        }
+        log.info("------批量获取公众号用户信息 开始------");
+        String sendUrl = String.format(BATCH_USER_INFO, accessToken);
+        CloseableHttpClient client = null;
+        CloseableHttpResponse response = null;
+        try{
+            // 创建http post请求
+            HttpPost httpPost = new HttpPost(sendUrl);
+            client = HttpClients.createDefault();
+            //构建请求参数
+            JSONObject map = new JSONObject();
+            map.put("user_list", new JSONArray());
+            for(int i = 0; i < openIds.size() && i < 100; i++){
+                JSONObject user = new JSONObject();
+                user.put("openid", openIds.get(i));
+                //user.put("lang", "zh_CN");
+                map.getJSONArray("user_list").add(user);
+            }
+            log.info("请求参数:" + JSON.toJSONString(map));
+            StringEntity entity = new StringEntity(map.toString(), StandardCharsets.UTF_8);
+            entity.setContentEncoding("UTF-8");
+            entity.setContentType("application/json");
+            httpPost.setEntity(entity);
+            response = client.execute(httpPost);
+            if(response.getStatusLine().getStatusCode() == HttpStatus.SC_OK){
+                String result = EntityUtils.toString(response.getEntity());
+                log.info("请求结果:" + result);
+                JSONObject json = JSON.parseObject(result);
+                if(StringUtils.isNotEmpty(json.getString("errmsg")) ||
+                        StringUtils.isNotEmpty(json.getString("errcode"))){
+                    log.error("数据出错了!");
+                } else{
+                    JSONArray array = json.getJSONArray("user_info_list");
+                    if(CollectionUtils.isNotEmpty(array)){
+                        for(Object o : array){
+                            WxPublicUserInfo user = WxPublicUserInfo.build((JSONObject) o);
+                            if(user != null){
+                                list.add(user);
+                            }
+                        }
+                    }
+                }
+            } else{
+                log.error("请求失败:状态码:" + response.getStatusLine().getStatusCode());
+            }
+        } catch(Exception e){
+            log.error(e.getMessage(), e);
+        } finally{
+            if(response != null){
+                try{
+                    response.close();
+                } catch(IOException e){
+                    log.error(e.getMessage(), e);
+                }
+            }
+            if(client != null){
+                try{
+                    client.close();
+                } catch(IOException e){
+                    log.error(e.getMessage(), e);
+                }
+            }
+        }
+        log.info("------批量获取公众号用户信息 结束------");
+        return list;
+
+    }
+
+}

+ 65 - 0
src/main/java/org/rcisoft/aspect/wx/pubnum/wx/WxPublicUserInfo.java

@@ -0,0 +1,65 @@
+package org.rcisoft.aspect.wx.pubnum.wx;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.Data;
+
+/**
+ * 应用模块名称</p>
+ * 代码描述</p>
+ * Copyright: Copyright (C) 2023 , Inc. All rights reserved. <p>
+ * Company: 成都诚唐科技有限责任公司</p>
+ *
+ * @author wany
+ * @since 2023/10/25
+ */
+@Data
+public class WxPublicUserInfo{
+
+    private String openId;
+
+    private String unionId;
+
+    private String remark;
+
+    private Integer groupId;
+
+    private List<Integer> tagIdList;
+
+    private String language;
+
+    private Integer qrScene;
+
+    private String qrSceneStr;
+
+    private Integer subscribe;
+
+    private String subscribeTime;
+
+    private String subscribeScene;
+
+    public static WxPublicUserInfo build(JSONObject jsonObject){
+        if(jsonObject == null){
+            return null;
+        }
+        WxPublicUserInfo user = new WxPublicUserInfo();
+        user.setOpenId(jsonObject.getString("openid"));
+        user.setUnionId(jsonObject.getString("unionid"));
+        user.setLanguage(jsonObject.getString("language"));
+        user.setRemark(jsonObject.getString("remark"));
+        user.setGroupId(jsonObject.getInteger("groupid"));
+        user.setQrScene(jsonObject.getInteger("qr_scene"));
+        user.setQrSceneStr(jsonObject.getString("qr_scene_str"));
+        user.setSubscribe(jsonObject.getInteger("subscribe"));
+        user.setSubscribeTime(String.valueOf(jsonObject.getLong("subscribe_time")));
+        user.setSubscribeScene(jsonObject.getString("subscribe_scene"));
+        user.setTagIdList(new ArrayList<>());
+        for(Object tagId : jsonObject.getJSONArray("tagid_list")){
+            user.getTagIdList().add((Integer) tagId);
+        }
+        return user;
+    }
+
+}

+ 28 - 0
src/main/java/org/rcisoft/tencent/service/WxUnionIdAware.java

@@ -0,0 +1,28 @@
+package org.rcisoft.tencent.service;
+
+import org.springframework.stereotype.Component;
+
+/**
+ * 应用模块名称</p>
+ * 代码描述</p>
+ * Copyright: Copyright (C) 2023 , Inc. All rights reserved. <p>
+ * Company: 成都诚唐科技有限责任公司</p>
+ *
+ * @author wany
+ * @since 2023 /10/30
+ */
+@Component
+public class WxUnionIdAware{
+
+    /**
+     * 处理微信公众号通知无法发送问题 代理连接点
+     *
+     * @param openId  the open id
+     * @param unionId the union id
+     * @return string string
+     */
+    public String setUnionId(String openId, String unionId){
+        return unionId;
+    }
+
+}

+ 6 - 0
src/main/java/org/rcisoft/tencent/service/impl/CyWxMiniServiceImpl.java

@@ -34,6 +34,7 @@ import org.rcisoft.sys.sysuser.dao.SysUserRepositorys;
 import org.rcisoft.tencent.cons.CyWxMiniCons;
 import org.rcisoft.tencent.dto.UserDto;
 import org.rcisoft.tencent.service.CyWxMiniService;
+import org.rcisoft.tencent.service.WxUnionIdAware;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.http.ResponseEntity;
@@ -103,6 +104,9 @@ public class CyWxMiniServiceImpl implements CyWxMiniService {
     @Autowired(required = false)
     private SysMenuRbacRepository sysMenuRbacRepository;
 
+    @Autowired
+    private WxUnionIdAware unionIdAware;
+
     // appId
     @Value("${wx.publicNumberAppId:}")
     private String publicNumberAppId;
@@ -311,6 +315,8 @@ public class CyWxMiniServiceImpl implements CyWxMiniService {
             JSONObject json_test = JSONUtil.parseObj(result);
             String wxOpenid = json_test.getStr(CyWxMiniCons.OPEN_ID_STR);
             String sessionKey = json_test.getStr(CyWxMiniCons.SESSION_KEY_STR);
+            String unionId = json_test.getStr("unionid");
+            unionIdAware.setUnionId(wxOpenid, unionId);
             log.error("openId---------------------" + wxOpenid);
             log.error("sessionKey---------------------" + sessionKey);
             res.put(CyWxMiniCons.OPEN_ID_STR, wxOpenid);

+ 1 - 0
src/main/resources/application-dev-conf.yml

@@ -80,6 +80,7 @@ cy:
           - "/excelUtil/**"
           #- "/cros/**"
           - "/nlttest/add/**"
+          - "/aspect/wx/pubnum/**"
           - "/**/**"
         permitStatic: [ "/", "/*.html", "/favicon.ico", "/**/*.html", "/**/*.js", "/**/*.css" ]
         logoutSuccessUrl: "/login"

+ 1 - 0
src/main/resources/application-jie-conf.yml

@@ -78,6 +78,7 @@ cy:
           - "/jieLinkInter/**"
           - "/aep/**"
           - "/sysUserManage/captchaImage"
+          - "/aspect/wx/pubnum/**"
           #- "/**/**"
         permitStatic: [ "/", "/*.html", "/favicon.ico", "/**/*.html", "/**/*.js", "/**/*.css" ]
         logoutSuccessUrl: "/login"

+ 3 - 2
src/main/resources/application-prod-conf.yml

@@ -79,6 +79,7 @@ cy:
           - "/jieLinkInter/**"
           - "/aep/**"
           - "/sysUserManage/captchaImage"
+          - "/aspect/wx/pubnum/**"
           #- "/**/**"
         permitStatic: [ "/", "/*.html", "/favicon.ico", "/**/*.html", "/**/*.js", "/**/*.css" ]
         logoutSuccessUrl: "/login"
@@ -109,8 +110,8 @@ wx:
   # 3f26659f870cb181b5f9133ccd2db4aa 夏图CD办公
   secret: 3f26659f870cb181b5f9133ccd2db4aa
   timeOut: 3600
-  publicNumberAppId: wx5c5692ab423047f0
-  publicNumberSecret: f6e29d8eb8de47132256add6817d8ee7
+  publicNumberAppId: wx6999b384761dc916
+  publicNumberSecret: b0455448f6028eda1626924e9da0ec82
   #  访问邀请信息
   inviteTemplateId: vl1-wI9jNJwaA4b6279F-4a0ov8ho0qYAlDzVa0fdEU
   #  访问信息审核