mirror of
https://gitee.com/huangge1199_admin/vue-pro.git
synced 2024-11-22 15:21:53 +08:00
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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 忽略租户,执行对应的逻辑
|
* 忽略租户,执行对应的逻辑
|
||||||
*
|
*
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
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 {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 编号
|
* 编号
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
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 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);
|
||||||
|
Loading…
Reference in New Issue
Block a user