Преглед на файлове

feat:微信公众号菜单创建、关注/取消关注事件监听

陈长荣 преди 5 месеца
родител
ревизия
7cd2a4291d

+ 0 - 14
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/push/entity/PushEntity.java

@@ -1,14 +0,0 @@
-package vip.xiaonuo.coldchain.modular.push.entity;
-
-import lombok.Getter;
-import lombok.Setter;
-
-@Getter
-@Setter
-public class PushEntity {
-    private String appId;
-    private String secret;
-    private String userId;
-    private String templateId;
-    private String miniProgram;
-}

+ 163 - 0
snowy-web-app/src/main/java/vip/xiaonuo/weixin/gongzhong/config/WxMpConfiguration.java

@@ -0,0 +1,163 @@
+package vip.xiaonuo.weixin.gongzhong.config;
+
+import cn.hutool.core.net.URLEncodeUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.api.WxConsts;
+import me.chanjar.weixin.common.bean.menu.WxMenu;
+import me.chanjar.weixin.common.bean.menu.WxMenuButton;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.session.WxSessionManager;
+import me.chanjar.weixin.mp.api.WxMpMessageHandler;
+import me.chanjar.weixin.mp.api.WxMpMessageRouter;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
+import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
+import me.chanjar.weixin.mp.config.impl.WxMpMapConfigImpl;
+import me.chanjar.weixin.mp.enums.WxMpApiUrl;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import vip.xiaonuo.coldchain.modular.alarmuser.service.AlarmUserService;
+import vip.xiaonuo.coldchain.modular.push.config.PushConfigure;
+
+import java.util.Map;
+
+/**
+ * WxJava 公众号配置
+ */
+@Slf4j
+@Configuration
+@RequiredArgsConstructor
+public class WxMpConfiguration {
+
+    private final PushConfigure properties;
+
+    private final AlarmUserService alarmUserService;
+
+    /**
+     * 授权页面地址
+     */
+    private static final String REDIRECT_URL = URLEncodeUtil.encodePathSegment("https://coldchain.nzkcloud.com/coldchain/wx/mp/auth/page");
+
+    @SneakyThrows
+    @Bean
+    public WxMpService wxMpService() {
+        WxMpDefaultConfigImpl configStorage = new WxMpMapConfigImpl();
+        configStorage.setAppId(properties.getAppId());
+        configStorage.setSecret(properties.getSecret());
+        configStorage.setTemplateId(properties.getTemplateId());
+
+        WxMpService service = new WxMpServiceImpl();
+        service.setWxMpConfigStorage(configStorage);
+        //创建菜单
+        service.getMenuService().menuCreate(createMenu());
+        return service;
+    }
+
+    /**
+     * 创建公众号菜单
+     */
+    private WxMenu createMenu() {
+        //小程序
+        WxMenuButton button1 = new WxMenuButton();
+        button1.setType("miniprogram");
+        button1.setName("冷链中心");
+        button1.setAppId(properties.getMiniProgram());
+        button1.setUrl("http://mp.weixin.qq.com");
+        button1.setPagePath("pages/home/home");
+
+        WxMenuButton button2 = new WxMenuButton();
+        button2.setType("click");
+        button2.setName("关于我们");
+        button2.setKey("aboutUs");
+
+        WxMenuButton button3 = new WxMenuButton();
+        button3.setType("view");
+        button3.setName("授权用户");
+        button3.setUrl(getAuthUrl());
+
+        WxMenu wxMenu = new WxMenu();
+        wxMenu.getButtons().add(button1);
+        wxMenu.getButtons().add(button2);
+        wxMenu.getButtons().add(button3);
+
+        return wxMenu;
+    }
+
+    /**
+     * 获取授权地址
+     * @return
+     */
+    private String getAuthUrl() {
+        return String.format(WxMpApiUrl.OAuth2.CONNECT_OAUTH2_AUTHORIZE_URL.getUrl(null),
+                properties.getAppId(), REDIRECT_URL, "snsapi_userinfo", "STATE");
+    }
+
+    @Bean
+    public WxMpMessageRouter wxMpMessageRouter() {
+        WxMpMessageRouter router = new WxMpMessageRouter(wxMpService());
+
+        // 关注事件
+        router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT).event(WxConsts.EventType.SUBSCRIBE)
+                .handler(new WxMpMessageHandler() {
+                    @Override
+                    public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) throws WxErrorException {
+                        log.info("用户关注 {}", wxMessage);
+
+                        return WxMpXmlOutMessage.TEXT()
+                                .content("【冷链驿站】为您提供温湿度、二氧化碳等环境监控数据查询及实时报警通知服务。请先<a href=\"" + getAuthUrl() + "\">完善信息</a>")
+                                .fromUser(wxMessage.getToUser())
+                                .toUser(wxMessage.getFromUser())
+                                .build();
+                    }
+                })
+                .end();
+
+        // 取消关注事件
+        router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT).event(WxConsts.EventType.UNSUBSCRIBE)
+                .handler(new WxMpMessageHandler() {
+                    @Override
+                    public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) {
+                        log.info("用户取消关注 {}", wxMessage);
+                        return null;
+                    }
+                })
+                .end();
+
+        // 点击菜单事件
+        router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT).event(WxConsts.EventType.CLICK)
+                .handler(new WxMpMessageHandler() {
+                    @Override
+                    public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) {
+                        log.info("用户点击 {}", wxMessage);
+
+                        if (wxMessage.getEventKey().equals("aboutUs")) {
+                            return WxMpXmlOutMessage.TEXT()
+                                    .content("【冷链驿站】为您提供温湿度、二氧化碳等环境监控数据查询及实时报警通知服务。您可以随时查看设备数据、设置报警阈值,确保冷链环境的安全与稳定。如有任何问题或需要帮助,请随时留言,我们将尽快为您服务。感谢您的支持!")
+                                    .fromUser(wxMessage.getToUser())
+                                    .toUser(wxMessage.getFromUser())
+                                    .build();
+                        }
+
+                        return null;
+                    }
+                })
+                .end();
+
+        // 点击view事件
+        router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT).event(WxConsts.EventType.VIEW)
+                .handler(new WxMpMessageHandler() {
+                    @Override
+                    public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) {
+                        log.info("用户打开view {}", wxMessage);
+                        return null;
+                    }
+                })
+                .end();
+
+        return router;
+    }
+}

+ 104 - 0
snowy-web-app/src/main/java/vip/xiaonuo/weixin/gongzhong/controller/WxMpController.java

@@ -0,0 +1,104 @@
+package vip.xiaonuo.weixin.gongzhong.controller;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.AllArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.mp.api.WxMpMessageRouter;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
+import org.springframework.web.bind.annotation.*;
+import vip.xiaonuo.coldchain.modular.alarmuser.entity.AlarmUser;
+import vip.xiaonuo.coldchain.modular.alarmuser.service.AlarmUserService;
+import vip.xiaonuo.coldchain.modular.push.entity.WeChatUser;
+import vip.xiaonuo.coldchain.modular.push.utils.PushUtil;
+import vip.xiaonuo.common.enums.CommonDeleteFlagEnum;
+
+import java.util.List;
+
+@CrossOrigin
+@RestController
+@AllArgsConstructor
+@Slf4j
+@RequestMapping("/wx/mp")
+public class WxMpController {
+
+    private final WxMpService wxMpService;
+    private final WxMpMessageRouter wxMpMessageRouter;
+    private final AlarmUserService alarmUserService;
+
+    private static final String FRONT_USER_INFO_PAGE = "https://coldchain.nzkcloud.com/check";
+
+    @GetMapping("/auth")
+    public String check(@RequestParam String signature, @RequestParam String timestamp, @RequestParam String nonce, @RequestParam String echostr) {
+        log.info("\n接收到来自微信服务器的认证消息:[{}, {}, {}, {}]", signature,timestamp, nonce, echostr);
+        if (wxMpService.checkSignature(timestamp, nonce, signature)) {
+            return echostr;
+        }
+        return "非法请求";
+    }
+
+    @SneakyThrows
+    @GetMapping("/auth/page")
+    public void check(@RequestParam(required = false) String code, @RequestParam(required = false) String state, HttpServletResponse response) {
+        log.info("\n接收到来自微信服务器的授权消息:code={},state={}", code, state);
+        if (StrUtil.isBlank(code)) {
+            response.getWriter().println("授权失败,请重新授权");
+            return;
+        }
+
+        WeChatUser weChatUser = PushUtil.getUserInfo(code);
+        log.info("获取用户信息:{}", JSONUtil.toJsonStr(weChatUser));
+
+        List<AlarmUser> userList = alarmUserService.list(new LambdaQueryWrapper<>(AlarmUser.class)
+                .eq(AlarmUser::getOpenId, weChatUser.getOpenid())
+                .eq(AlarmUser::getDeleteFlag, CommonDeleteFlagEnum.NOT_DELETE)
+                .orderByDesc(AlarmUser::getCreateTime));
+        AlarmUser alarmUser;
+
+        if (CollUtil.isNotEmpty(userList)) {
+            //检查用户的组织机构
+            long count = userList.stream().filter(x -> StrUtil.isNotBlank(x.getCreateOrg())).count();
+            if (count > 0) {
+                response.getWriter().println("您已授权!\\^o^/");
+                return;
+            }
+            alarmUser = userList.get(0);
+        } else {
+            //没有用户信息则创建用户
+            alarmUserService.getUserInfo(code, null, null);
+            alarmUser = alarmUserService.getOne(new LambdaQueryWrapper<>(AlarmUser.class)
+                    .eq(AlarmUser::getOpenId, weChatUser.getOpenid())
+                    .isNull(AlarmUser::getCreateOrg)
+                    .eq(AlarmUser::getDeleteFlag, CommonDeleteFlagEnum.NOT_DELETE)
+                    .orderByDesc(AlarmUser::getCreateTime)
+                    .last("limit 1"));
+        }
+
+        String redirect = FRONT_USER_INFO_PAGE + "?id=" + alarmUser.getId();
+        log.info("进入用户授权页面 {}", redirect);
+        response.sendRedirect(redirect);
+    }
+
+
+    @SneakyThrows
+    @PostMapping("/event")
+    public String wxEventListener(@RequestBody String param,
+                            @RequestParam(required = false) String signature,
+                            @RequestParam(required = false) String timestamp,
+                            @RequestParam(required = false) String nonce) {
+        log.info("\n接收微信事件:[signature=[{}], timestamp=[{}], nonce=[{}], requestBody=[\n{}\n] ",
+                signature, timestamp, nonce, param);
+
+        // 微信时间在wxMpMessageRouter中处理
+        WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(param);
+        WxMpXmlOutMessage outMessage = wxMpMessageRouter.route(inMessage);
+        // 将响应消息转换为xml格式返回
+        return outMessage == null ? "" : outMessage.toXml();
+    }
+}

+ 15 - 14
snowy-web-app/src/main/resources/application.properties

@@ -36,23 +36,23 @@ spring.datasource.dynamic.strict=true
 spring.data.influxdb.url=${INFLUXDB_URL:http://jfcloud-k6-influxdb:8086}
 ## 10.0.1.200
 #spring.data.influxdb.token=${INFLUXDB_TOKEN:1NdDyN3LCKIEBFkQ1AqAmRSitZGfdCs0nuF11nLQbTcnN_5mp_asnfFnmAXTvDDQXGr83llxuXJu5wNTPtrsIw==}
-# \u516C\u7F51\u7684
+# 公网的
 spring.data.influxdb.token=${INFLUXDB_TOKEN:g2gXWbSJar3fmUKFkv4SVtTzPE-rX0-6aW5bDwgNOZXsD3tWL2PgAcCneH5WYcE9OmrMvLbvR-TMZrtX2d8ihQ==}
 spring.data.influxdb.org=${INFLUXDB_ORG:coldchain}
 spring.data.influxdb.bucket=${INFLUXDB_BUCKET:coldchain}
 
-## \u6E29\u5EA6\u8D85\u6807\u62A5\u8B66\u6A21\u677F
-#coldchain.alarm.message.temperatureOverLimit=\u6E29\u5EA6\u62A5\u8B66\uFF1A\u8BBE\u5907\u3010{deviceName}\u3011\u7684\u6E29\u5EA6\u8D85\u6807\uFF01\n\u5F53\u524D\u6E29\u5EA6\uFF1A{value} {unit}\uFF0C\u5DF2\u8D85\u51FA\u4E0A\u9650\uFF08\u9608\u503C\uFF1A{thresholdUp}\uFF09\u3002\n\u62A5\u8B66\u65F6\u95F4\uFF1A{time}
-## \u6E29\u5EA6\u8FC7\u4F4E\u62A5\u8B66\u6A21\u677F
-#coldchain.alarm.message.temperatureBelowLimit=\u6E29\u5EA6\u62A5\u8B66\uFF1A\u8BBE\u5907\u3010{deviceName}\u3011\u7684\u6E29\u5EA6\u8FC7\u4F4E\uFF01\n\u5F53\u524D\u6E29\u5EA6\uFF1A{value} {unit}\uFF0C\u5DF2\u4F4E\u4E8E\u4E0B\u9650\uFF08\u9608\u503C\uFF1A{thresholdDown}\uFF09\u3002\n\u62A5\u8B66\u65F6\u95F4\uFF1A{time}
-## \u6E7F\u5EA6\u8D85\u6807\u62A5\u8B66\u6A21\u677F
-#coldchain.alarm.message.humidityOverLimit=\u6E7F\u5EA6\u62A5\u8B66\uFF1A\u8BBE\u5907\u3010{deviceName}\u3011\u7684\u6E7F\u5EA6\u8D85\u6807\uFF01\n\u5F53\u524D\u6E7F\u5EA6\uFF1A{value} {unit}\uFF0C\u5DF2\u8D85\u51FA\u4E0A\u9650\uFF08\u9608\u503C\uFF1A{thresholdUp}\uFF09\u3002\n\u62A5\u8B66\u65F6\u95F4\uFF1A{time}
-## \u6E7F\u5EA6\u8FC7\u4F4E\u62A5\u8B66\u6A21\u677F
-#coldchain.alarm.message.humidityBelowLimit=\u6E7F\u5EA6\u62A5\u8B66\uFF1A\u8BBE\u5907\u3010{deviceName}\u3011\u7684\u6E7F\u5EA6\u8FC7\u4F4E\uFF01\n\u5F53\u524D\u6E7F\u5EA6\uFF1A{value} {unit}\uFF0C\u5DF2\u4F4E\u4E8E\u4E0B\u9650\uFF08\u9608\u503C\uFF1A{thresholdDown}\uFF09\u3002\n\u62A5\u8B66\u65F6\u95F4\uFF1A{time}
-## \u4E8C\u6C27\u5316\u78B3\u8D85\u6807\u62A5\u8B66\u6A21\u677F
-#coldchain.alarm.message.co2OverLimit=\u4E8C\u6C27\u5316\u78B3\u62A5\u8B66\uFF1A\u8BBE\u5907\u3010{deviceName}\u3011\u7684\u4E8C\u6C27\u5316\u78B3\u6D53\u5EA6\u8D85\u6807\uFF01\n\u5F53\u524D\u6D53\u5EA6\uFF1A{value} {unit}\uFF0C\u5DF2\u8D85\u51FA\u4E0A\u9650\uFF08\u9608\u503C\uFF1A{thresholdUp}\uFF09\u3002\n\u62A5\u8B66\u65F6\u95F4\uFF1A{time}
-## \u4E8C\u6C27\u5316\u78B3\u8FC7\u4F4E\u62A5\u8B66\u6A21\u677F
-#coldchain.alarm.message.co2BelowLimit=\u4E8C\u6C27\u5316\u78B3\u62A5\u8B66\uFF1A\u8BBE\u5907\u3010{deviceName}\u3011\u7684\u4E8C\u6C27\u5316\u78B3\u6D53\u5EA6\u8FC7\u4F4E\uFF01\n\u5F53\u524D\u6D53\u5EA6\uFF1A{value} {unit}\uFF0C\u5DF2\u4F4E\u4E8E\u4E0B\u9650\uFF08\u9608\u503C\uFF1A{thresholdDown}\uFF09\u3002\n\u62A5\u8B66\u65F6\u95F4\uFF1A{time}
+## 温度超标报警模板
+#coldchain.alarm.message.temperatureOverLimit=温度报警:设备【{deviceName}】的温度超标!\n当前温度:{value} {unit},已超出上限(阈值:{thresholdUp})。\n报警时间:{time}
+## 温度过低报警模板
+#coldchain.alarm.message.temperatureBelowLimit=温度报警:设备【{deviceName}】的温度过低!\n当前温度:{value} {unit},已低于下限(阈值:{thresholdDown})。\n报警时间:{time}
+## 湿度超标报警模板
+#coldchain.alarm.message.humidityOverLimit=湿度报警:设备【{deviceName}】的湿度超标!\n当前湿度:{value} {unit},已超出上限(阈值:{thresholdUp})。\n报警时间:{time}
+## 湿度过低报警模板
+#coldchain.alarm.message.humidityBelowLimit=湿度报警:设备【{deviceName}】的湿度过低!\n当前湿度:{value} {unit},已低于下限(阈值:{thresholdDown})。\n报警时间:{time}
+## 二氧化碳超标报警模板
+#coldchain.alarm.message.co2OverLimit=二氧化碳报警:设备【{deviceName}】的二氧化碳浓度超标!\n当前浓度:{value} {unit},已超出上限(阈值:{thresholdUp})。\n报警时间:{time}
+## 二氧化碳过低报警模板
+#coldchain.alarm.message.co2BelowLimit=二氧化碳报警:设备【{deviceName}】的二氧化碳浓度过低!\n当前浓度:{value} {unit},已低于下限(阈值:{thresholdDown})。\n报警时间:{time}
 
 # postgres
 #spring.datasource.dynamic.datasource.master.driver-class-name=org.postgresql.Driver
@@ -232,7 +232,7 @@ snowy.config.common.front-url=http://localhost:81
 snowy.config.common.backend-url=http://localhost:82
 
 
-# \u5FAE\u4FE1\u5C0F\u7A0B\u5E8F
+# 微信小程序
 logging.level.org.springframework.web=info
 logging.level.com.github.binarywang.demo.wx.miniapp=debug
 logging.level.cn.binarywang.wx.miniapp=debug
@@ -242,6 +242,7 @@ wx.miniapp.configs[0].token=jfcloudjfcloud
 wx.miniapp.configs[0].aesKey=G7NLugvrJHQJTjn9fk26LdAXcen1nXKOtb1hQemautk
 wx.miniapp.configs[0].msgDataFormat=JSON
 
+logging.level.me.chanjar.weixin.mp.api.impl=DEBUG
 wechat.appID=wx49b1fb6f9c0d0b8d
 wechat.secret=6b177348efc2b21da105fc126b933db9
 wechat.templateId=WgKOAJrnNhnr2lIkfI_vppfY--7oImjddrd4GPnE_UA