Commit a7e4ff0d76898b8c951c27ad7c8a55d06ef8e5fa

Authored by YunaiV
1 parent f0cdc8d2

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

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; @@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.tenant.core.util;
3 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; 3 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
4 4
5 import java.util.Map; 5 import java.util.Map;
  6 +import java.util.concurrent.Callable;
6 7
7 import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; 8 import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
8 9
@@ -37,6 +38,31 @@ public class TenantUtils { @@ -37,6 +38,31 @@ public class TenantUtils {
37 } 38 }
38 39
39 /** 40 /**
  41 + * 使用指定租户,执行对应的逻辑
  42 + *
  43 + * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户
  44 + * 当然,执行完成后,还是会恢复回去
  45 + *
  46 + * @param tenantId 租户编号
  47 + * @param callable 逻辑
  48 + */
  49 + public static <V> V execute(Long tenantId, Callable<V> callable) {
  50 + Long oldTenantId = TenantContextHolder.getTenantId();
  51 + Boolean oldIgnore = TenantContextHolder.isIgnore();
  52 + try {
  53 + TenantContextHolder.setTenantId(tenantId);
  54 + TenantContextHolder.setIgnore(false);
  55 + // 执行逻辑
  56 + return callable.call();
  57 + } catch (Exception e) {
  58 + throw new RuntimeException(e);
  59 + } finally {
  60 + TenantContextHolder.setTenantId(oldTenantId);
  61 + TenantContextHolder.setIgnore(oldIgnore);
  62 + }
  63 + }
  64 +
  65 + /**
40 * 忽略租户,执行对应的逻辑 66 * 忽略租户,执行对应的逻辑
41 * 67 *
42 * @param runnable 逻辑 68 * @param runnable 逻辑
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/open/MpOpenController.java
1 package cn.iocoder.yudao.module.mp.controller.admin.open; 1 package cn.iocoder.yudao.module.mp.controller.admin.open;
2 2
  3 +import cn.hutool.core.lang.Assert;
3 import cn.hutool.core.util.StrUtil; 4 import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
  6 +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
4 import cn.iocoder.yudao.module.mp.controller.admin.open.vo.MpOpenCheckSignatureReqVO; 7 import cn.iocoder.yudao.module.mp.controller.admin.open.vo.MpOpenCheckSignatureReqVO;
  8 +import cn.iocoder.yudao.module.mp.controller.admin.open.vo.MpOpenHandleMessageReqVO;
  9 +import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
5 import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; 10 import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory;
  11 +import cn.iocoder.yudao.module.mp.framework.mp.core.context.MpContextHolder;
  12 +import cn.iocoder.yudao.module.mp.service.account.MpAccountService;
6 import io.swagger.annotations.Api; 13 import io.swagger.annotations.Api;
7 import io.swagger.annotations.ApiOperation; 14 import io.swagger.annotations.ApiOperation;
8 import lombok.extern.slf4j.Slf4j; 15 import lombok.extern.slf4j.Slf4j;
  16 +import me.chanjar.weixin.mp.api.WxMpMessageRouter;
9 import me.chanjar.weixin.mp.api.WxMpService; 17 import me.chanjar.weixin.mp.api.WxMpService;
  18 +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
  19 +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
10 import org.springframework.validation.annotation.Validated; 20 import org.springframework.validation.annotation.Validated;
11 import org.springframework.web.bind.annotation.*; 21 import org.springframework.web.bind.annotation.*;
12 22
13 import javax.annotation.Resource; 23 import javax.annotation.Resource;
  24 +import java.util.Objects;
14 25
15 @Api(tags = "管理后台 - 公众号回调") 26 @Api(tags = "管理后台 - 公众号回调")
16 @RestController 27 @RestController
@@ -22,6 +33,9 @@ public class MpOpenController { @@ -22,6 +33,9 @@ public class MpOpenController {
22 @Resource 33 @Resource
23 private MpServiceFactory mpServiceFactory; 34 private MpServiceFactory mpServiceFactory;
24 35
  36 + @Resource
  37 + private MpAccountService mpAccountService;
  38 +
25 /** 39 /**
26 * 接收微信公众号的校验签名 40 * 接收微信公众号的校验签名
27 * 41 *
@@ -49,8 +63,51 @@ public class MpOpenController { @@ -49,8 +63,51 @@ public class MpOpenController {
49 */ 63 */
50 @ApiOperation("处理消息") 64 @ApiOperation("处理消息")
51 @PostMapping(value = "/{appId}", produces = "application/xml; charset=UTF-8") 65 @PostMapping(value = "/{appId}", produces = "application/xml; charset=UTF-8")
52 - public String handleMessage() {  
53 - return "123"; 66 + @OperateLog(enable = false) // 回调地址,无需记录操作日志
  67 + public String handleMessage(@PathVariable("appId") String appId,
  68 + @RequestBody String content,
  69 + MpOpenHandleMessageReqVO reqVO) {
  70 + log.info("[handleMessage][appId({}) 推送消息,参数({}) 内容({})]", appId, reqVO, content);
  71 +
  72 + // 处理 appId + 多租户的上下文
  73 + MpAccountDO account = mpAccountService.getAccountFromCache(appId);
  74 + Assert.notNull(account, "公众号 appId({}) 不存在", appId);
  75 + try {
  76 + MpContextHolder.setAppId(appId);
  77 + return TenantUtils.execute(account.getTenantId(),
  78 + () -> handleMessage0(appId, content, reqVO));
  79 + } finally {
  80 + MpContextHolder.clear();
  81 + }
  82 + }
  83 +
  84 + private String handleMessage0(String appId, String content, MpOpenHandleMessageReqVO reqVO) {
  85 + // 校验请求签名
  86 + WxMpService mppService = mpServiceFactory.getRequiredMpService(appId);
  87 + Assert.isTrue(mppService.checkSignature(reqVO.getTimestamp(), reqVO.getNonce(), reqVO.getSignature()),
  88 + "非法请求");
  89 +
  90 + // 第一步,解析消息
  91 + WxMpXmlMessage inMessage = null;
  92 + if (StrUtil.isBlank(reqVO.getEncrypt_type())) { // 明文模式
  93 + inMessage = WxMpXmlMessage.fromXml(content);
  94 + } else if (Objects.equals(reqVO.getEncrypt_type(), MpOpenHandleMessageReqVO.ENCRYPT_TYPE_AES)) { // AES 加密模式
  95 + inMessage = WxMpXmlMessage.fromEncryptedXml(content, mppService.getWxMpConfigStorage(),
  96 + reqVO.getTimestamp(), reqVO.getNonce(), reqVO.getMsg_signature());
  97 + }
  98 + Assert.notNull(inMessage, "消息解析失败,原因:消息为空");
  99 +
  100 + // 第二步,处理消息
  101 + WxMpMessageRouter mpMessageRouter = mpServiceFactory.getRequiredMpMessageRouter(appId);
  102 + WxMpXmlOutMessage outMessage = mpMessageRouter.route(inMessage);
  103 +
  104 + // 第三步,返回消息
  105 + if (StrUtil.isBlank(reqVO.getEncrypt_type())) { // 明文模式
  106 + return outMessage.toXml();
  107 + } else if (Objects.equals(reqVO.getEncrypt_type(), MpOpenHandleMessageReqVO.ENCRYPT_TYPE_AES)) { // AES 加密模式
  108 + return outMessage.toEncryptedXml(mppService.getWxMpConfigStorage());
  109 + }
  110 + return null;
54 } 111 }
55 112
56 } 113 }
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/open/vo/MpOpenHandleMessageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.mp.controller.admin.open.vo;
  2 +
  3 +
  4 +import io.swagger.annotations.ApiModel;
  5 +import io.swagger.annotations.ApiModelProperty;
  6 +import lombok.Data;
  7 +import org.springframework.web.bind.annotation.RequestParam;
  8 +
  9 +import javax.validation.constraints.NotEmpty;
  10 +
  11 +@ApiModel("管理后台 - 公众号处理消息 Request VO")
  12 +@Data
  13 +public class MpOpenHandleMessageReqVO {
  14 +
  15 + public static final String ENCRYPT_TYPE_AES = "aes";
  16 +
  17 + @ApiModelProperty(value = "微信加密签名", required = true, example = "490eb57f448b87bd5f20ccef58aa4de46aa1908e")
  18 + @NotEmpty(message = "微信加密签名不能为空")
  19 + private String signature;
  20 +
  21 + @ApiModelProperty(value = "时间戳", required = true, example = "1672587863")
  22 + @NotEmpty(message = "时间戳不能为空")
  23 + private String timestamp;
  24 +
  25 + @ApiModelProperty(value = "随机数", required = true, example = "1827365808")
  26 + @NotEmpty(message = "随机数不能为空")
  27 + private String nonce;
  28 +
  29 + @ApiModelProperty(value = "用户 openid", required = true, example = "oz-Jdtyn-WGm4C4I5Z-nvBMO_ZfY")
  30 + @NotEmpty(message = "用户 openid 不能为空")
  31 + private String openid;
  32 +
  33 + @ApiModelProperty(value = "消息加密类型", example = "aes")
  34 + private String encrypt_type;
  35 +
  36 + @ApiModelProperty(value = "微信签名", example = "QW5kcm9pZCBUaGUgQmFzZTY0IGlzIGEgZ2VuZXJhdGVkIHN0cmluZw==")
  37 + private String msg_signature;
  38 +
  39 +}
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/account/MpAccountDO.java
1 package cn.iocoder.yudao.module.mp.dal.dataobject.account; 1 package cn.iocoder.yudao.module.mp.dal.dataobject.account;
2 2
3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; 3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  4 +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
4 import com.baomidou.mybatisplus.annotation.KeySequence; 5 import com.baomidou.mybatisplus.annotation.KeySequence;
5 import com.baomidou.mybatisplus.annotation.TableId; 6 import com.baomidou.mybatisplus.annotation.TableId;
6 import com.baomidou.mybatisplus.annotation.TableName; 7 import com.baomidou.mybatisplus.annotation.TableName;
@@ -23,7 +24,7 @@ import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl; @@ -23,7 +24,7 @@ import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl;
23 @Builder 24 @Builder
24 @NoArgsConstructor 25 @NoArgsConstructor
25 @AllArgsConstructor 26 @AllArgsConstructor
26 -public class MpAccountDO extends BaseDO { 27 +public class MpAccountDO extends TenantBaseDO {
27 28
28 /** 29 /**
29 * 编号 30 * 编号
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 { @@ -42,4 +42,11 @@ public interface MpServiceFactory {
42 * @return WxMpMessageRouter 实例 42 * @return WxMpMessageRouter 实例
43 */ 43 */
44 WxMpMessageRouter getMpMessageRouter(String appId); 44 WxMpMessageRouter getMpMessageRouter(String appId);
  45 +
  46 + default WxMpMessageRouter getRequiredMpMessageRouter(String appId) {
  47 + WxMpMessageRouter wxMpMessageRouter = getMpMessageRouter(appId);
  48 + Assert.notNull(wxMpMessageRouter, "找到对应 appId({}) 的 WxMpMessageRouter,请核实!", appId);
  49 + return wxMpMessageRouter;
  50 + }
  51 +
45 } 52 }
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/context/MpContextHolder.java 0 → 100644
  1 +/*
  2 + * Copyright (c) 2018-2025, lengleng All rights reserved.
  3 + *
  4 + * Redistribution and use in source and binary forms, with or without
  5 + * modification, are permitted provided that the following conditions are met:
  6 + *
  7 + * Redistributions of source code must retain the above copyright notice,
  8 + * this list of conditions and the following disclaimer.
  9 + * Redistributions in binary form must reproduce the above copyright
  10 + * notice, this list of conditions and the following disclaimer in the
  11 + * documentation and/or other materials provided with the distribution.
  12 + * Neither the name of the pig4cloud.com developer nor the names of its
  13 + * contributors may be used to endorse or promote products derived from
  14 + * this software without specific prior written permission.
  15 + * Author: lengleng (wangiegie@gmail.com)
  16 + */
  17 +
  18 +package cn.iocoder.yudao.module.mp.framework.mp.core.context;
  19 +
  20 +import cn.iocoder.yudao.module.mp.controller.admin.open.vo.MpOpenHandleMessageReqVO;
  21 +import com.alibaba.ttl.TransmittableThreadLocal;
  22 +import lombok.experimental.UtilityClass;
  23 +import me.chanjar.weixin.mp.api.WxMpMessageHandler;
  24 +
  25 +/**
  26 + * 微信上下文 Context
  27 + *
  28 + * 目的:解决微信多公众号的问题,在 {@link WxMpMessageHandler} 实现类中,可以通过 {@link #getAppId()} 获取到当前的 appId
  29 + *
  30 + * @see cn.iocoder.yudao.module.mp.controller.admin.open.MpOpenController#handleMessage(String, String, MpOpenHandleMessageReqVO)
  31 + *
  32 + * @author 芋道源码
  33 + */
  34 +public class MpContextHolder {
  35 +
  36 + /**
  37 + * 微信公众号的 appId 上下文
  38 + */
  39 + private static final ThreadLocal<String> APPID = new TransmittableThreadLocal<>();
  40 +
  41 + public static void setAppId(String appId) {
  42 + APPID.set(appId);
  43 + }
  44 +
  45 + public static String getAppId() {
  46 + return APPID.get();
  47 + }
  48 +
  49 + public static void clear() {
  50 + APPID.remove();
  51 + }
  52 +
  53 +}
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/account/MpAccountService.java
@@ -52,6 +52,14 @@ public interface MpAccountService { @@ -52,6 +52,14 @@ public interface MpAccountService {
52 MpAccountDO getAccount(Long id); 52 MpAccountDO getAccount(Long id);
53 53
54 /** 54 /**
  55 + * 从缓存中,获得公众号账户
  56 + *
  57 + * @param appId 微信公众号 appId
  58 + * @return 公众号账户
  59 + */
  60 + MpAccountDO getAccountFromCache(String appId);
  61 +
  62 + /**
55 * 获得公众号账户分页 63 * 获得公众号账户分页
56 * 64 *
57 * @param pageReqVO 分页查询 65 * @param pageReqVO 分页查询
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; @@ -25,6 +25,7 @@ import javax.annotation.PostConstruct;
25 import javax.annotation.Resource; 25 import javax.annotation.Resource;
26 import java.time.LocalDateTime; 26 import java.time.LocalDateTime;
27 import java.util.List; 27 import java.util.List;
  28 +import java.util.Map;
28 29
29 30
30 /** 31 /**
@@ -44,6 +45,14 @@ public class MpAccountServiceImpl implements MpAccountService { @@ -44,6 +45,14 @@ public class MpAccountServiceImpl implements MpAccountService {
44 private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L; 45 private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L;
45 46
46 /** 47 /**
  48 + * 账号缓存
  49 + * key:账号编号 {@link MpAccountDO#getAppId()}
  50 + *
  51 + * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向
  52 + */
  53 + @Getter
  54 + private volatile Map<String, MpAccountDO> accountCache;
  55 + /**
47 * 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新 56 * 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新
48 */ 57 */
49 @Getter 58 @Getter
@@ -92,6 +101,7 @@ public class MpAccountServiceImpl implements MpAccountService { @@ -92,6 +101,7 @@ public class MpAccountServiceImpl implements MpAccountService {
92 101
93 // 第二步:构建缓存。创建或更新支付 Client 102 // 第二步:构建缓存。创建或更新支付 Client
94 mpServiceFactory.init(accounts); 103 mpServiceFactory.init(accounts);
  104 + accountCache = CollectionUtils.convertMap(accounts, MpAccountDO::getAppId);
95 105
96 // 第三步:设置最新的 maxUpdateTime,用于下次的增量判断。 106 // 第三步:设置最新的 maxUpdateTime,用于下次的增量判断。
97 this.maxUpdateTime = CollectionUtils.getMaxValue(accounts, MpAccountDO::getUpdateTime); 107 this.maxUpdateTime = CollectionUtils.getMaxValue(accounts, MpAccountDO::getUpdateTime);
@@ -147,6 +157,11 @@ public class MpAccountServiceImpl implements MpAccountService { @@ -147,6 +157,11 @@ public class MpAccountServiceImpl implements MpAccountService {
147 } 157 }
148 158
149 @Override 159 @Override
  160 + public MpAccountDO getAccountFromCache(String appId) {
  161 + return accountCache.get(appId);
  162 + }
  163 +
  164 + @Override
150 public PageResult<MpAccountDO> getAccountPage(MpAccountPageReqVO pageReqVO) { 165 public PageResult<MpAccountDO> getAccountPage(MpAccountPageReqVO pageReqVO) {
151 return mpAccountMapper.selectPage(pageReqVO); 166 return mpAccountMapper.selectPage(pageReqVO);
152 } 167 }