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

This commit is contained in:
YunaiV 2023-01-02 17:44:37 +08:00
parent f0cdc8d296
commit a7e4ff0d76
8 changed files with 209 additions and 3 deletions

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.tenant.core.util;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Callable;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; 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);
}
}
/** /**
* 忽略租户执行对应的逻辑 * 忽略租户执行对应的逻辑
* *

View File

@ -1,16 +1,27 @@
package cn.iocoder.yudao.module.mp.controller.admin.open; package cn.iocoder.yudao.module.mp.controller.admin.open;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil; 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.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.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.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.mp.api.WxMpMessageRouter;
import me.chanjar.weixin.mp.api.WxMpService; 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.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.Objects;
@Api(tags = "管理后台 - 公众号回调") @Api(tags = "管理后台 - 公众号回调")
@RestController @RestController
@ -22,6 +33,9 @@ public class MpOpenController {
@Resource @Resource
private MpServiceFactory mpServiceFactory; private MpServiceFactory mpServiceFactory;
@Resource
private MpAccountService mpAccountService;
/** /**
* 接收微信公众号的校验签名 * 接收微信公众号的校验签名
* *
@ -49,8 +63,51 @@ public class MpOpenController {
*/ */
@ApiOperation("处理消息") @ApiOperation("处理消息")
@PostMapping(value = "/{appId}", produces = "application/xml; charset=UTF-8") @PostMapping(value = "/{appId}", produces = "application/xml; charset=UTF-8")
public String handleMessage() { @OperateLog(enable = false) // 回调地址无需记录操作日志
return "123"; 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;
} }
} }

View File

@ -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;
}

View File

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

View File

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

View File

@ -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();
}
}

View File

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

View File

@ -25,6 +25,7 @@ import javax.annotation.PostConstruct;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; 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; 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 // 第二步构建缓存创建或更新支付 Client
mpServiceFactory.init(accounts); mpServiceFactory.init(accounts);
accountCache = CollectionUtils.convertMap(accounts, MpAccountDO::getAppId);
// 第三步设置最新的 maxUpdateTime用于下次的增量判断 // 第三步设置最新的 maxUpdateTime用于下次的增量判断
this.maxUpdateTime = CollectionUtils.getMaxValue(accounts, MpAccountDO::getUpdateTime); this.maxUpdateTime = CollectionUtils.getMaxValue(accounts, MpAccountDO::getUpdateTime);
@ -146,6 +156,11 @@ public class MpAccountServiceImpl implements MpAccountService {
return mpAccountMapper.selectById(id); return mpAccountMapper.selectById(id);
} }
@Override
public MpAccountDO getAccountFromCache(String appId) {
return accountCache.get(appId);
}
@Override @Override
public PageResult<MpAccountDO> getAccountPage(MpAccountPageReqVO pageReqVO) { public PageResult<MpAccountDO> getAccountPage(MpAccountPageReqVO pageReqVO) {
return mpAccountMapper.selectPage(pageReqVO); return mpAccountMapper.selectPage(pageReqVO);