mp:实现消息推送的处理接口
This commit is contained in:
parent
f0cdc8d296
commit
a7e4ff0d76
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 忽略租户,执行对应的逻辑
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
||||
/**
|
||||
* 编号
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -51,6 +51,14 @@ public interface MpAccountService {
|
|||
*/
|
||||
MpAccountDO getAccount(Long id);
|
||||
|
||||
/**
|
||||
* 从缓存中,获得公众号账户
|
||||
*
|
||||
* @param appId 微信公众号 appId
|
||||
* @return 公众号账户
|
||||
*/
|
||||
MpAccountDO getAccountFromCache(String appId);
|
||||
|
||||
/**
|
||||
* 获得公众号账户分页
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue