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 3 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
4 4  
5 5 import java.util.Map;
  6 +import java.util.concurrent.Callable;
6 7  
7 8 import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
8 9  
... ... @@ -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 68 * @param runnable 逻辑
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/open/MpOpenController.java
1 1 package cn.iocoder.yudao.module.mp.controller.admin.open;
2 2  
  3 +import cn.hutool.core.lang.Assert;
3 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 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 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 13 import io.swagger.annotations.Api;
7 14 import io.swagger.annotations.ApiOperation;
8 15 import lombok.extern.slf4j.Slf4j;
  16 +import me.chanjar.weixin.mp.api.WxMpMessageRouter;
9 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 20 import org.springframework.validation.annotation.Validated;
11 21 import org.springframework.web.bind.annotation.*;
12 22  
13 23 import javax.annotation.Resource;
  24 +import java.util.Objects;
14 25  
15 26 @Api(tags = "管理后台 - 公众号回调")
16 27 @RestController
... ... @@ -22,6 +33,9 @@ public class MpOpenController {
22 33 @Resource
23 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 63 */
50 64 @ApiOperation("处理消息")
51 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 1 package cn.iocoder.yudao.module.mp.dal.dataobject.account;
2 2  
3 3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  4 +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
4 5 import com.baomidou.mybatisplus.annotation.KeySequence;
5 6 import com.baomidou.mybatisplus.annotation.TableId;
6 7 import com.baomidou.mybatisplus.annotation.TableName;
... ... @@ -23,7 +24,7 @@ import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl;
23 24 @Builder
24 25 @NoArgsConstructor
25 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 42 * @return WxMpMessageRouter 实例
43 43 */
44 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 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 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 25 import javax.annotation.Resource;
26 26 import java.time.LocalDateTime;
27 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 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 58 @Getter
... ... @@ -92,6 +101,7 @@ public class MpAccountServiceImpl implements MpAccountService {
92 101  
93 102 // 第二步:构建缓存。创建或更新支付 Client
94 103 mpServiceFactory.init(accounts);
  104 + accountCache = CollectionUtils.convertMap(accounts, MpAccountDO::getAppId);
95 105  
96 106 // 第三步:设置最新的 maxUpdateTime,用于下次的增量判断。
97 107 this.maxUpdateTime = CollectionUtils.getMaxValue(accounts, MpAccountDO::getUpdateTime);
... ... @@ -147,6 +157,11 @@ public class MpAccountServiceImpl implements MpAccountService {
147 157 }
148 158  
149 159 @Override
  160 + public MpAccountDO getAccountFromCache(String appId) {
  161 + return accountCache.get(appId);
  162 + }
  163 +
  164 + @Override
150 165 public PageResult<MpAccountDO> getAccountPage(MpAccountPageReqVO pageReqVO) {
151 166 return mpAccountMapper.selectPage(pageReqVO);
152 167 }
... ...