Browse Source

mp:实现消息推送的处理接口

YunaiV 2 years ago
parent
commit
a7e4ff0d76

+ 26 - 0
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/util/TenantUtils.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.tenant.core.util;
 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
 
 import java.util.Map;
+import java.util.concurrent.Callable;
 
 import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
 
@@ -36,6 +37,31 @@ public class TenantUtils {
         }
     }
 
+    /**
+     * 使用指定租户,执行对应的逻辑
+     *
+     * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户
+     * 当然,执行完成后,还是会恢复回去
+     *
+     * @param tenantId 租户编号
+     * @param callable 逻辑
+     */
+    public static <V> V execute(Long tenantId, Callable<V> callable) {
+        Long oldTenantId = TenantContextHolder.getTenantId();
+        Boolean oldIgnore = TenantContextHolder.isIgnore();
+        try {
+            TenantContextHolder.setTenantId(tenantId);
+            TenantContextHolder.setIgnore(false);
+            // 执行逻辑
+            return callable.call();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        } finally {
+            TenantContextHolder.setTenantId(oldTenantId);
+            TenantContextHolder.setIgnore(oldIgnore);
+        }
+    }
+
     /**
      * 忽略租户,执行对应的逻辑
      *

+ 59 - 2
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/open/MpOpenController.java

@@ -1,16 +1,27 @@
 package cn.iocoder.yudao.module.mp.controller.admin.open;
 
+import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
 import cn.iocoder.yudao.module.mp.controller.admin.open.vo.MpOpenCheckSignatureReqVO;
+import cn.iocoder.yudao.module.mp.controller.admin.open.vo.MpOpenHandleMessageReqVO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
 import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory;
+import cn.iocoder.yudao.module.mp.framework.mp.core.context.MpContextHolder;
+import cn.iocoder.yudao.module.mp.service.account.MpAccountService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 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.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
+import java.util.Objects;
 
 @Api(tags = "管理后台 - 公众号回调")
 @RestController
@@ -22,6 +33,9 @@ public class MpOpenController {
     @Resource
     private MpServiceFactory mpServiceFactory;
 
+    @Resource
+    private MpAccountService mpAccountService;
+
     /**
      * 接收微信公众号的校验签名
      *
@@ -49,8 +63,51 @@ public class MpOpenController {
      */
     @ApiOperation("处理消息")
     @PostMapping(value = "/{appId}", produces = "application/xml; charset=UTF-8")
-    public String handleMessage() {
-        return "123";
+    @OperateLog(enable = false) // 回调地址,无需记录操作日志
+    public String handleMessage(@PathVariable("appId") String appId,
+                                @RequestBody String content,
+                                MpOpenHandleMessageReqVO reqVO) {
+        log.info("[handleMessage][appId({}) 推送消息,参数({}) 内容({})]", appId, reqVO, content);
+
+        // 处理 appId + 多租户的上下文
+        MpAccountDO account = mpAccountService.getAccountFromCache(appId);
+        Assert.notNull(account, "公众号 appId({}) 不存在", appId);
+        try {
+            MpContextHolder.setAppId(appId);
+            return TenantUtils.execute(account.getTenantId(),
+                    () -> handleMessage0(appId, content, reqVO));
+        } finally {
+            MpContextHolder.clear();
+        }
+    }
+
+    private String handleMessage0(String appId, String content, MpOpenHandleMessageReqVO reqVO) {
+        // 校验请求签名
+        WxMpService mppService = mpServiceFactory.getRequiredMpService(appId);
+        Assert.isTrue(mppService.checkSignature(reqVO.getTimestamp(), reqVO.getNonce(), reqVO.getSignature()),
+                "非法请求");
+
+        // 第一步,解析消息
+        WxMpXmlMessage inMessage = null;
+        if (StrUtil.isBlank(reqVO.getEncrypt_type())) { // 明文模式
+            inMessage = WxMpXmlMessage.fromXml(content);
+        } else if (Objects.equals(reqVO.getEncrypt_type(), MpOpenHandleMessageReqVO.ENCRYPT_TYPE_AES)) { // AES 加密模式
+            inMessage = WxMpXmlMessage.fromEncryptedXml(content, mppService.getWxMpConfigStorage(),
+                    reqVO.getTimestamp(), reqVO.getNonce(), reqVO.getMsg_signature());
+        }
+        Assert.notNull(inMessage, "消息解析失败,原因:消息为空");
+
+        // 第二步,处理消息
+        WxMpMessageRouter mpMessageRouter = mpServiceFactory.getRequiredMpMessageRouter(appId);
+        WxMpXmlOutMessage outMessage = mpMessageRouter.route(inMessage);
+
+        // 第三步,返回消息
+        if (StrUtil.isBlank(reqVO.getEncrypt_type())) { // 明文模式
+            return outMessage.toXml();
+        } else if (Objects.equals(reqVO.getEncrypt_type(), MpOpenHandleMessageReqVO.ENCRYPT_TYPE_AES)) { // AES 加密模式
+            return outMessage.toEncryptedXml(mppService.getWxMpConfigStorage());
+        }
+        return null;
     }
 
 }

+ 39 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/open/vo/MpOpenHandleMessageReqVO.java

@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.module.mp.controller.admin.open.vo;
+
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import javax.validation.constraints.NotEmpty;
+
+@ApiModel("管理后台 - 公众号处理消息 Request VO")
+@Data
+public class MpOpenHandleMessageReqVO {
+
+    public static final String ENCRYPT_TYPE_AES = "aes";
+
+    @ApiModelProperty(value = "微信加密签名", required = true, example = "490eb57f448b87bd5f20ccef58aa4de46aa1908e")
+    @NotEmpty(message = "微信加密签名不能为空")
+    private String signature;
+
+    @ApiModelProperty(value = "时间戳", required = true, example = "1672587863")
+    @NotEmpty(message = "时间戳不能为空")
+    private String timestamp;
+
+    @ApiModelProperty(value = "随机数", required = true, example = "1827365808")
+    @NotEmpty(message = "随机数不能为空")
+    private String nonce;
+
+    @ApiModelProperty(value = "用户 openid", required = true, example = "oz-Jdtyn-WGm4C4I5Z-nvBMO_ZfY")
+    @NotEmpty(message = "用户 openid 不能为空")
+    private String openid;
+
+    @ApiModelProperty(value = "消息加密类型", example = "aes")
+    private String encrypt_type;
+
+    @ApiModelProperty(value = "微信签名", example = "QW5kcm9pZCBUaGUgQmFzZTY0IGlzIGEgZ2VuZXJhdGVkIHN0cmluZw==")
+    private String msg_signature;
+
+}

+ 2 - 1
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/account/MpAccountDO.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.mp.dal.dataobject.account;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
@@ -23,7 +24,7 @@ import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl;
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
-public class MpAccountDO extends BaseDO {
+public class MpAccountDO extends TenantBaseDO {
 
     /**
      * 编号

+ 7 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/MpServiceFactory.java

@@ -42,4 +42,11 @@ public interface MpServiceFactory {
      * @return WxMpMessageRouter 实例
      */
     WxMpMessageRouter getMpMessageRouter(String appId);
+
+    default WxMpMessageRouter getRequiredMpMessageRouter(String appId) {
+        WxMpMessageRouter wxMpMessageRouter = getMpMessageRouter(appId);
+        Assert.notNull(wxMpMessageRouter, "找到对应 appId({}) 的 WxMpMessageRouter,请核实!", appId);
+        return wxMpMessageRouter;
+    }
+
 }

+ 53 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/context/MpContextHolder.java

@@ -0,0 +1,53 @@
+/*
+ *    Copyright (c) 2018-2025, lengleng All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * Neither the name of the pig4cloud.com developer nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * Author: lengleng (wangiegie@gmail.com)
+ */
+
+package cn.iocoder.yudao.module.mp.framework.mp.core.context;
+
+import cn.iocoder.yudao.module.mp.controller.admin.open.vo.MpOpenHandleMessageReqVO;
+import com.alibaba.ttl.TransmittableThreadLocal;
+import lombok.experimental.UtilityClass;
+import me.chanjar.weixin.mp.api.WxMpMessageHandler;
+
+/**
+ * 微信上下文 Context
+ *
+ * 目的:解决微信多公众号的问题,在 {@link WxMpMessageHandler} 实现类中,可以通过 {@link #getAppId()} 获取到当前的 appId
+ *
+ * @see cn.iocoder.yudao.module.mp.controller.admin.open.MpOpenController#handleMessage(String, String, MpOpenHandleMessageReqVO)
+ *
+ * @author 芋道源码
+ */
+public class MpContextHolder {
+
+    /**
+     * 微信公众号的 appId 上下文
+     */
+	private static final ThreadLocal<String> APPID = new TransmittableThreadLocal<>();
+
+	public static void setAppId(String appId) {
+		APPID.set(appId);
+	}
+
+	public static String getAppId() {
+		return APPID.get();
+	}
+
+	public static void clear() {
+		APPID.remove();
+	}
+
+}

+ 8 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/account/MpAccountService.java

@@ -51,6 +51,14 @@ public interface MpAccountService {
      */
     MpAccountDO getAccount(Long id);
 
+    /**
+     * 从缓存中,获得公众号账户
+     *
+     * @param appId 微信公众号 appId
+     * @return 公众号账户
+     */
+    MpAccountDO getAccountFromCache(String appId);
+
     /**
      * 获得公众号账户分页
      *

+ 15 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/account/MpAccountServiceImpl.java

@@ -25,6 +25,7 @@ import javax.annotation.PostConstruct;
 import javax.annotation.Resource;
 import java.time.LocalDateTime;
 import java.util.List;
+import java.util.Map;
 
 
 /**
@@ -43,6 +44,14 @@ public class MpAccountServiceImpl implements MpAccountService {
      */
     private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L;
 
+    /**
+     * 账号缓存
+     * key:账号编号 {@link MpAccountDO#getAppId()}
+     *
+     * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向
+     */
+    @Getter
+    private volatile Map<String, MpAccountDO> accountCache;
     /**
      * 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新
      */
@@ -92,6 +101,7 @@ public class MpAccountServiceImpl implements MpAccountService {
 
             // 第二步:构建缓存。创建或更新支付 Client
             mpServiceFactory.init(accounts);
+            accountCache = CollectionUtils.convertMap(accounts, MpAccountDO::getAppId);
 
             // 第三步:设置最新的 maxUpdateTime,用于下次的增量判断。
             this.maxUpdateTime = CollectionUtils.getMaxValue(accounts, MpAccountDO::getUpdateTime);
@@ -146,6 +156,11 @@ public class MpAccountServiceImpl implements MpAccountService {
         return mpAccountMapper.selectById(id);
     }
 
+    @Override
+    public MpAccountDO getAccountFromCache(String appId) {
+        return accountCache.get(appId);
+    }
+
     @Override
     public PageResult<MpAccountDO> getAccountPage(MpAccountPageReqVO pageReqVO) {
         return mpAccountMapper.selectPage(pageReqVO);