diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index 7484e7f3d..e7610ec83 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -298,13 +298,8 @@ justauth: # 腾讯云IM配置 tencent: im: - # 腾讯云 SDKAppId - sdkappid: 1600080789 - # 密钥 - secretkey: 311b5309d714a20f7f5b54360ee21b1e24ec208ebcd25ce8f47d24753bccc091 - # 签名过期时间(秒) - expire: 604800 - # 管理员账号 - admin: administrator - # API调用密钥 - api-secret: 311b5309d714a20f7f5b54360ee21b1e24ec208ebcd25ce8f47d24753bccc091 + enabled: true # 启用腾讯云IM + sdk-app-id: 1600080789 # 你的腾讯云 SDKAppID + secret-key: "311b5309d714a20f7f5b54360ee21b1e24ec208ebcd25ce8f47d24753bccc091" # 你的密钥 + administrator: "administrator" # 管理员账号 + expire-time: 604800 # UserSig 过期时间(7天,单位:秒) diff --git a/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/aspect/LogAspect.java b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/aspect/LogAspect.java index 8ab2719e1..d91ae3bf4 100644 --- a/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/aspect/LogAspect.java +++ b/ruoyi-common/ruoyi-common-log/src/main/java/org/dromara/common/log/aspect/LogAspect.java @@ -113,7 +113,7 @@ public class LogAspect { // 设置消耗时间 StopWatch stopWatch = KEY_CACHE.get(); stopWatch.stop(); - operLog.setCostTime(stopWatch.getDuration().toMillis()); + operLog.setCostTime(stopWatch.getTime()); // 发布事件保存数据库 SpringUtils.context().publishEvent(operLog); } catch (Exception exp) { diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/interceptor/PlusWebInvokeTimeInterceptor.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/interceptor/PlusWebInvokeTimeInterceptor.java index f25601572..7cb6b7877 100644 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/interceptor/PlusWebInvokeTimeInterceptor.java +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/interceptor/PlusWebInvokeTimeInterceptor.java @@ -67,7 +67,7 @@ public class PlusWebInvokeTimeInterceptor implements HandlerInterceptor { StopWatch stopWatch = KEY_CACHE.get(); if (ObjectUtil.isNotNull(stopWatch)) { stopWatch.stop(); - log.info("[PLUS]结束请求 => URL[{}],耗时:[{}]毫秒", request.getMethod() + " " + request.getRequestURI(), stopWatch.getDuration().toMillis()); + log.info("[PLUS]结束请求 => URL[{}],耗时:[{}]毫秒", request.getMethod() + " " + request.getRequestURI(), stopWatch.getTime()); KEY_CACHE.remove(); } } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/consumer/MessageRocketMQConsumer.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/consumer/MessageRocketMQConsumer.java index 33062a200..db6ee580f 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/consumer/MessageRocketMQConsumer.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/consumer/MessageRocketMQConsumer.java @@ -8,14 +8,10 @@ import org.apache.rocketmq.spring.core.RocketMQListener; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.json.utils.JsonUtils; import org.dromara.system.config.RocketMQConfig; -import org.dromara.system.domain.SysMessageUser; import org.dromara.system.domain.vo.SysMessageVo; -import org.dromara.system.mapper.SysMessageUserMapper; import org.dromara.system.websocket.MessageWebSocketServer; import org.springframework.stereotype.Component; -import java.util.Date; - /** * RocketMQ消息消费者 * @@ -32,7 +28,6 @@ import java.util.Date; public class MessageRocketMQConsumer implements RocketMQListener { private final MessageWebSocketServer messageWebSocketServer; - private final SysMessageUserMapper messageUserMapper; @Override public void onMessage(String message) { @@ -53,18 +48,6 @@ public class MessageRocketMQConsumer implements RocketMQListener { } if (userIdLong != null) { - // 保存消息用户关联记录 - try { - SysMessageUser messageUser = new SysMessageUser(); - messageUser.setMessageId(wrapper.getMessage().getId()); - messageUser.setUserId(userIdLong); - messageUser.setIsRead(false); - messageUserMapper.insert(messageUser); - log.info("消息用户关联记录保存成功,messageId: {}, userId: {}", wrapper.getMessage().getId(), userIdLong); - } catch (Exception e) { - log.error("保存消息用户关联记录失败", e); - } - // 发送WebSocket消息 messageWebSocketServer.sendMessage(userIdLong, JsonUtils.toJsonString(wrapper.getMessage())); log.info("通过WebSocket发送消息成功,userId: {}", userIdLong); diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/SysMessageController.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/SysMessageController.java index a08c9dd3e..523b1cae5 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/SysMessageController.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/SysMessageController.java @@ -2,6 +2,8 @@ package org.dromara.system.controller; import cn.dev33.satoken.annotation.SaCheckPermission; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.wzj.soopin.member.domain.po.Member; +import com.wzj.soopin.member.mapper.MemberMapper; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.constraints.NotEmpty; @@ -31,6 +33,9 @@ import org.dromara.system.domain.vo.SysUserVo; import org.dromara.system.service.ISysMessageService; import org.dromara.system.service.ISysMessageTemplateService; import org.dromara.system.service.ISysUserService; +import org.dromara.common.satoken.utils.LoginHelper; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -51,6 +56,7 @@ public class SysMessageController extends BaseController { private final ISysMessageService messageService; private final ISysUserService userService; private final ISysMessageTemplateService templateService; + private final MemberMapper umsMemberMapper; /** * 获取当前用户ID @@ -107,12 +113,12 @@ public class SysMessageController extends BaseController { return R.fail("未找到指定的消息模板"); } } - + // 验证消息内容是否为空 if (StringUtils.isBlank(bo.getContent())) { return R.fail("消息内容不能为空"); } - + // 验证发送范围是否为空 List sendScope = bo.getSendScope(); if (sendScope == null || sendScope.isEmpty()) { @@ -129,37 +135,35 @@ public class SysMessageController extends BaseController { if (sendScope == null || sendScope.isEmpty()) { return R.fail("发送范围不能为空"); } - + String scope = sendScope.get(0); // 获取第一个范围值 switch (logmess) { case 1: // 指定角色 // 当logmess=1时,sendScope接收的是角色ID或特殊标识 - if ("all".equals(scope) || "expert".equals(scope) || "merchant".equals(scope) || "user".equals(scope)) { + if ("all".equals(scope)) { + // 全部会员用户(查ums_member表) + List members = umsMemberMapper.selectList( + new QueryWrapper().eq("status", 1) + ); + userIdStrings = members.stream().map(m -> String.valueOf(m.getId())).toList(); + } else if ("expert".equals(scope) || "merchant".equals(scope) || "user".equals(scope)) { List users = userService.selectUserListByDept(null); List userIds; - switch (scope) { - case "all": - // 全部用户 - userIds = users.stream().map(SysUserVo::getUserId).toList(); - break; case "expert": - // 达人 userIds = users.stream() .filter(user -> "expert".equals(user.getUserType())) .map(SysUserVo::getUserId) .toList(); break; case "merchant": - // 商户 userIds = users.stream() .filter(user -> "merchant".equals(user.getUserType())) .map(SysUserVo::getUserId) .toList(); break; case "user": - // 普通用户 userIds = users.stream() .filter(user -> "user".equals(user.getUserType())) .map(SysUserVo::getUserId) @@ -220,14 +224,14 @@ public class SysMessageController extends BaseController { if (sendScope == null || sendScope.isEmpty()) { return R.fail("发送范围不能为空"); } - + String scope = sendScope.get(0); // 获取第一个范围值 // 如果是群发消息类型,则根据类型筛选用户 if ("all".equals(scope) || "expert".equals(scope) || "merchant".equals(scope) || "user".equals(scope)) { List users = userService.selectUserListByDept(null); List userIds; - + switch (scope) { case "all": // 全部用户 diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysMessageBo.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysMessageBo.java index a1a6b8f15..c11009710 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysMessageBo.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysMessageBo.java @@ -14,6 +14,8 @@ import org.dromara.common.mybatis.core.domain.BaseEntity; import org.dromara.system.domain.SysMessage; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.LocalDateTime; import java.util.Date; import java.util.List; @@ -85,16 +87,25 @@ public class SysMessageBo extends BaseAudit { /** 备注 */ private String remark; + /** 创建时间-起始 */ + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC") + private LocalDateTime startTime; + /** 创建时间-结束 */ + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC") + private LocalDateTime sendTime; + /** * 转换为查询条件 */ public LambdaQueryWrapper toWrapper() { LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); lqw.like(StringUtils.isNotBlank(this.getTitle()), SysMessage::getTitle, this.getTitle()) + .like(StringUtils.isNotBlank(this.getContent()), SysMessage::getContent, this.getContent()) .eq(StringUtils.isNotBlank(this.getMsgType()), SysMessage::getMsgType, this.getMsgType()) .eq(StringUtils.isNotBlank(this.getSubType()), SysMessage::getSubType, this.getSubType()) .eq(this.getSenderId() != null, SysMessage::getSenderId, this.getSenderId()) -// .eq(StringUtils.isNotBlank(this.getStatus()), SysMessage::getStatus, this.getStatus()) + .ge(this.getStartTime() != null, SysMessage::getCreateTime, this.getStartTime()) + .le(this.getSendTime() != null, SysMessage::getCreateTime, this.getSendTime()) .orderByDesc(SysMessage::getCreateTime); return lqw; } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/event/MessageEventListener.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/event/MessageEventListener.java index 760d0eed2..d6cb9ed7d 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/event/MessageEventListener.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/event/MessageEventListener.java @@ -3,6 +3,7 @@ package org.dromara.system.event; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.utils.StringUtils; import org.dromara.common.json.utils.JsonUtils; import org.dromara.system.config.RocketMQConfig; import org.dromara.system.consumer.MessageRocketMQConsumer; @@ -61,7 +62,7 @@ public class MessageEventListener { // 消息发送者可能是系统或管理员,这里使用固定的管理员账号作为发送者 String fromUserId = "administrator"; String toUserId = userId; // 接收者是事件中的用户ID - String content = JsonUtils.toJsonString(event.getMessage()); // 消息内容序列化为JSON + String content = event.getMessage().getContent(); // 只取content字段 // 处理消息变量替换(如果有) Map variables = new HashMap<>(); @@ -127,7 +128,7 @@ public class MessageEventListener { } } else { // 默认为单用户推送 - tencentIMSendSuccess = tencentIMService.sendMessageToTencentIM(fromUserId, toUserId, content); + tencentIMSendSuccess = tencentIMService.sendMessageToTencentIM(fromUserId, toUserId, content); } if (tencentIMSendSuccess) { @@ -141,6 +142,7 @@ public class MessageEventListener { // 无论腾讯IM是否发送成功,都继续尝试通过RocketMQ发送消息 // 这样可以确保消息至少通过一种方式发送出去 + /* boolean rocketMQSendSuccess = false; try { rocketMQService.sendMessage( @@ -153,6 +155,8 @@ public class MessageEventListener { } catch (Exception e) { log.error("通过RocketMQ发送消息失败,将尝试直接通过WebSocket发送", e); } + */ + boolean rocketMQSendSuccess = false; // 直接设为false,表示不走MQ // 如果前两种方式都失败,则尝试直接通过WebSocket发送 if (!tencentIMSendSuccess && !rocketMQSendSuccess && event.getUserId() != null) { diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysMessageServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysMessageServiceImpl.java index 2ddee744d..0414ad8a2 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysMessageServiceImpl.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysMessageServiceImpl.java @@ -22,7 +22,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; /** @@ -128,6 +131,11 @@ public class SysMessageServiceImpl extends ServiceImpl validUserIds = new ArrayList<>(); + Map userIdStrMap = new HashMap<>(); + for (String userIdStr : userIds) { if (StringUtils.isBlank(userIdStr)) { continue; @@ -135,37 +143,61 @@ public class SysMessageServiceImpl extends ServiceImpl queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(SysMessageUser::getMessageId, entity.getId()) - .eq(SysMessageUser::getUserId, userId); - int existCount = Math.toIntExact(messageUserMapper.selectCount(queryWrapper)); - - // 只有当关联不存在时才创建 - if (existCount == 0) { - SysMessageUser messageUser = new SysMessageUser(); - messageUser.setMessageId(entity.getId()); - messageUser.setUserId(userId); - messageUser.setIsRead(false); - int rows = messageUserMapper.insert(messageUser); - if (rows > 0) { - count++; + validUserIds.add(userId); + userIdStrMap.put(userId, userIdStr); + } catch (NumberFormatException e) { + log.error("无法将String类型的用户ID转换为Long: {}", userIdStr); + } + } + + // 一次性查询所有已存在的关联记录 + if (!validUserIds.isEmpty()) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysMessageUser::getMessageId, entity.getId()) + .in(SysMessageUser::getUserId, validUserIds); + List existingRecords = messageUserMapper.selectList(queryWrapper); + + // 创建已存在用户ID的集合 + Set existingUserIds = existingRecords.stream() + .map(SysMessageUser::getUserId) + .collect(Collectors.toSet()); + + log.info("已存在的消息关联记录数: {}", existingUserIds.size()); + + // 对不存在关联的用户批量插入 + for (Long userId : validUserIds) { + if (!existingUserIds.contains(userId)) { + try { + SysMessageUser messageUser = new SysMessageUser(); + messageUser.setMessageId(entity.getId()); + messageUser.setUserId(userId); + messageUser.setIsRead(false); + int rows = messageUserMapper.insert(messageUser); + if (rows > 0) { + count++; + } + } catch (Exception e) { + log.error("创建消息用户关联失败: messageId={}, userId={}, error={}", + entity.getId(), userId, e.getMessage()); } } else { log.info("消息与用户关联已存在,跳过创建: messageId={}, userId={}", entity.getId(), userId); // 已存在也计入成功数量 count++; } - - // 无论关联是否新建,都发送事件通知 - SysMessageVo messageVo = MapstructUtils.convert(entity, SysMessageVo.class); - eventPublisher.publishEvent(MessageEvent.createWithStringUserId(this, messageVo, userIdStr)); - - } catch (NumberFormatException e) { - log.error("无法将String类型的用户ID转换为Long: {}", userIdStr); } } + + // 最后只发送一次批量事件通知 + if (count > 0) { + SysMessageVo messageVo = MapstructUtils.convert(entity, SysMessageVo.class); + // 为所有有效用户ID组装一个逗号分隔的字符串 + String batchUserIds = validUserIds.stream() + .map(String::valueOf) + .collect(Collectors.joining(",")); + // 发布一个批量事件,在MessageEvent和MessageEventListener中进行处理 + eventPublisher.publishEvent(MessageEvent.createWithStringUserId(this, messageVo, batchUserIds)); + } return count; } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/TencentIMServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/TencentIMServiceImpl.java index d3ba5773d..e3af563e4 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/TencentIMServiceImpl.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/TencentIMServiceImpl.java @@ -1,6 +1,7 @@ package org.dromara.system.service.impl; import com.alibaba.fastjson.JSONObject; +import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.dromara.system.service.ITencentIMService; import org.springframework.beans.factory.annotation.Value; @@ -8,9 +9,17 @@ import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.zip.Deflater; +import java.util.Base64; +import com.wzj.soopin.member.util.TLSSigAPIv2; /** * 腾讯IM服务实现类 @@ -21,20 +30,30 @@ import java.util.regex.Pattern; @Service("systemTencentIMService") public class TencentIMServiceImpl implements ITencentIMService { - @Value("${tencent.im.sdkappid}") + @Value("${tencent.im.sdk-app-id}") private long sdkAppId; - @Value("${tencent.im.secretkey}") + @Value("${tencent.im.secret-key}") private String secretKey; - @Value("${tencent.im.admin}") + @Value("${tencent.im.administrator}") private String adminAccount; + @Value("${tencent.im.expire-time:180}") + private int expireTime; + private final RestTemplate restTemplate = new RestTemplate(); - + // 变量替换的正则表达式模式 private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\$(\\w+)"); + private TLSSigAPIv2 tlsSigApi; + + @PostConstruct + private void initTlsSigApi() { + this.tlsSigApi = new TLSSigAPIv2(sdkAppId, secretKey); + } + /** * 生成用户签名 * @@ -42,28 +61,84 @@ public class TencentIMServiceImpl implements ITencentIMService { * @return 用户签名 */ private String generateUserSig(String userId) { - // 注意: 这里需要实现腾讯云IM SDK的TLSSigAPIv2签名生成 - // 参考腾讯云文档: https://cloud.tencent.com/document/product/269/32688 - // 实现方法: - // 1. 导入腾讯云IM SDK依赖 - // 2. 使用sdkAppId和secretKey生成签名 - // 示例代码: - // TLSSigAPIv2 tlsSigApi = new TLSSigAPIv2(sdkAppId, secretKey); - // return tlsSigApi.genUserSig(userId, 86400); // 有效期设为1天(86400秒) - - log.info("生成用户签名: {}", userId); - - // 当前为模拟实现,实际项目中请替换为真实签名生成逻辑 + // 直接调用官方TLSSigAPIv2生成 + return tlsSigApi.genUserSig("administrator", expireTime); + } + + /** + * 生成 tls 票据 + * + * @param sdkappid 应用的 appid + * @param userId 用户id + * @param expire 有效期 (时间戳 单位秒) + * @param priKeyContent 私钥文件内容 + * @return 通过 base64 编码的 tls 票据 + */ + private String genTLSSignature(long sdkappid, String userId, long expire, String priKeyContent) { try { - // 临时返回一个固定值用于测试 - // 注意:这是临时方案,正式环境必须替换为正确实现 - return "eJyrVgrxDXFSsrS0MDA2MzY1NTawNDAzsTQ1sTCyMDI2NdRRKs9ILUpVsjKoLEgsKk7NswlPzYnIsagoyIgrsYl3Lg5IALITi5BkS1JzSlOLbMJKi32DKtNCglONDSNNXfMqk3wN0myrvVUYGBgYGZiaGBoamCgZ6hlYGOUb6Rmaq1QHAAD--wJe"; + JSONObject sigDoc = new JSONObject(); + sigDoc.put("TLS.ver", "2.0"); + sigDoc.put("TLS.identifier", userId); + sigDoc.put("TLS.sdkappid", sdkappid); + sigDoc.put("TLS.expire", expire); + sigDoc.put("TLS.time", System.currentTimeMillis() / 1000); + + String base64UserBuf = null; + if (null != userId) { + base64UserBuf = base64EncodeUrl(userId.getBytes(StandardCharsets.UTF_8)); + sigDoc.put("TLS.userbuf", base64UserBuf); + } + + String sig = hmacSHA256(sdkappid, userId, expire, priKeyContent, base64UserBuf); + if (sig.length() == 0) { + return ""; + } + sigDoc.put("TLS.sig", sig); + Deflater compressor = new Deflater(); + compressor.setInput(sigDoc.toString().getBytes(StandardCharsets.UTF_8)); + compressor.finish(); + byte[] compressedBytes = new byte[2048]; + int compressedBytesLength = compressor.deflate(compressedBytes); + compressor.end(); + return base64EncodeUrl(Arrays.copyOfRange(compressedBytes, 0, compressedBytesLength)); } catch (Exception e) { - log.error("生成用户签名异常", e); - return null; + log.error("生成tls票据异常", e); + return ""; } } + /** + * 使用 HMAC-SHA256 生成签名 + */ + private String hmacSHA256(long sdkappid, String userId, long expire, String priKeyContent, String base64UserBuf) { + try { + String contentToBeSigned = "TLS.identifier:" + "administrator" + "\n" + + "TLS.sdkappid:" + sdkappid + "\n" + + "TLS.time:" + System.currentTimeMillis() / 1000 + "\n" + + "TLS.expire:" + expire + "\n"; + if (null != base64UserBuf) { + contentToBeSigned += "TLS.userbuf:" + base64UserBuf + "\n"; + } + byte[] byteKey = priKeyContent.getBytes(StandardCharsets.UTF_8); + Mac hmac = Mac.getInstance("HmacSHA256"); + SecretKeySpec keySpec = new SecretKeySpec(byteKey, "HmacSHA256"); + hmac.init(keySpec); + byte[] byteSig = hmac.doFinal(contentToBeSigned.getBytes(StandardCharsets.UTF_8)); + return base64EncodeUrl(byteSig); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + return ""; + } + } + + /** + * 将字节数组使用 base64 编码(URL 安全) + */ + private String base64EncodeUrl(byte[] data) { + byte[] encodedBytes = Base64.getEncoder().encode(data); + String encodedStr = new String(encodedBytes); + return encodedStr.replace('+', '*').replace('/', '-').replace('=', '_'); + } + /** * 生成管理员签名 * @@ -92,17 +167,17 @@ public class TencentIMServiceImpl implements ITencentIMService { Map requestBody = new HashMap<>(); requestBody.put("UserID", userId); requestBody.put("Nick", userId); - + // 设置请求头 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); - + // 发送请求 HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); - + log.info("创建腾讯IM账号结果: {}", response.getBody()); - + // 生成并返回用户签名 return generateUserSig(userId); } catch (Exception e) { @@ -146,13 +221,13 @@ public class TencentIMServiceImpl implements ITencentIMService { // 设置请求头 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); - + // 发送请求 HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); - + log.info("发送消息到腾讯IM结果: {}", response.getBody()); - + // 解析响应判断是否成功 JSONObject responseJson = JSONObject.parseObject(response.getBody()); return "OK".equals(responseJson.getString("ActionStatus")); @@ -161,7 +236,7 @@ public class TencentIMServiceImpl implements ITencentIMService { return false; } } - + @Override public boolean pushToAll(String title, String desc, boolean offlinePush, String ext) { try { @@ -181,12 +256,12 @@ public class TencentIMServiceImpl implements ITencentIMService { Map msgContent = new HashMap<>(); msgContent.put("Title", title); msgContent.put("Desc", desc); - + // 构建消息体 Map msgBody = new HashMap<>(); msgBody.put("MsgType", "TIMCustomElem"); msgBody.put("MsgContent", msgContent); - + // 如果有扩展字段,添加到消息内容中 if (ext != null && !ext.isEmpty()) { msgContent.put("Ext", ext); @@ -197,7 +272,7 @@ public class TencentIMServiceImpl implements ITencentIMService { requestBody.put("From_Account", adminAccount); requestBody.put("MsgRandom", Integer.parseInt(random)); requestBody.put("MsgBody", new Object[]{msgBody}); - + // 设置离线推送信息 if (offlinePush) { Map offlinePushInfo = new HashMap<>(); @@ -211,13 +286,13 @@ public class TencentIMServiceImpl implements ITencentIMService { // 设置请求头 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); - + // 发送请求 HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); - + log.info("全员推送消息结果: {}", response.getBody()); - + // 解析响应判断是否成功 JSONObject responseJson = JSONObject.parseObject(response.getBody()); return "OK".equals(responseJson.getString("ActionStatus")); @@ -226,7 +301,7 @@ public class TencentIMServiceImpl implements ITencentIMService { return false; } } - + @Override public boolean pushByAttributes(String title, String desc, Map attributes, boolean offlinePush, String ext) { try { @@ -246,12 +321,12 @@ public class TencentIMServiceImpl implements ITencentIMService { Map msgContent = new HashMap<>(); msgContent.put("Title", title); msgContent.put("Desc", desc); - + // 构建消息体 Map msgBody = new HashMap<>(); msgBody.put("MsgType", "TIMCustomElem"); msgBody.put("MsgContent", msgContent); - + // 如果有扩展字段,添加到消息内容中 if (ext != null && !ext.isEmpty()) { msgContent.put("Ext", ext); @@ -264,7 +339,7 @@ public class TencentIMServiceImpl implements ITencentIMService { requestBody.put("MsgBody", new Object[]{msgBody}); requestBody.put("AttrNames", attributes.keySet().toArray(new String[0])); requestBody.put("AttrValues", attributes.values().toArray()); - + // 设置离线推送信息 if (offlinePush) { Map offlinePushInfo = new HashMap<>(); @@ -278,13 +353,13 @@ public class TencentIMServiceImpl implements ITencentIMService { // 设置请求头 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); - + // 发送请求 HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); - + log.info("属性推送消息结果: {}", response.getBody()); - + // 解析响应判断是否成功 JSONObject responseJson = JSONObject.parseObject(response.getBody()); return "OK".equals(responseJson.getString("ActionStatus")); @@ -293,7 +368,7 @@ public class TencentIMServiceImpl implements ITencentIMService { return false; } } - + @Override public boolean pushByTags(String title, String desc, List tags, boolean offlinePush, String ext) { try { @@ -313,12 +388,12 @@ public class TencentIMServiceImpl implements ITencentIMService { Map msgContent = new HashMap<>(); msgContent.put("Title", title); msgContent.put("Desc", desc); - + // 构建消息体 Map msgBody = new HashMap<>(); msgBody.put("MsgType", "TIMCustomElem"); msgBody.put("MsgContent", msgContent); - + // 如果有扩展字段,添加到消息内容中 if (ext != null && !ext.isEmpty()) { msgContent.put("Ext", ext); @@ -330,7 +405,7 @@ public class TencentIMServiceImpl implements ITencentIMService { requestBody.put("MsgRandom", Integer.parseInt(random)); requestBody.put("MsgBody", new Object[]{msgBody}); requestBody.put("TagList", tags); - + // 设置离线推送信息 if (offlinePush) { Map offlinePushInfo = new HashMap<>(); @@ -344,13 +419,13 @@ public class TencentIMServiceImpl implements ITencentIMService { // 设置请求头 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); - + // 发送请求 HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); - + log.info("标签推送消息结果: {}", response.getBody()); - + // 解析响应判断是否成功 JSONObject responseJson = JSONObject.parseObject(response.getBody()); return "OK".equals(responseJson.getString("ActionStatus")); @@ -359,7 +434,7 @@ public class TencentIMServiceImpl implements ITencentIMService { return false; } } - + @Override public boolean pushToUsers(String title, String desc, List userIds, boolean offlinePush, String ext) { try { @@ -379,12 +454,12 @@ public class TencentIMServiceImpl implements ITencentIMService { Map msgContent = new HashMap<>(); msgContent.put("Title", title); msgContent.put("Desc", desc); - + // 构建消息体 Map msgBody = new HashMap<>(); msgBody.put("MsgType", "TIMCustomElem"); msgBody.put("MsgContent", msgContent); - + // 如果有扩展字段,添加到消息内容中 if (ext != null && !ext.isEmpty()) { msgContent.put("Ext", ext); @@ -396,7 +471,7 @@ public class TencentIMServiceImpl implements ITencentIMService { requestBody.put("To_Account", userIds); requestBody.put("MsgRandom", Integer.parseInt(random)); requestBody.put("MsgBody", new Object[]{msgBody}); - + // 设置离线推送信息 if (offlinePush) { Map offlinePushInfo = new HashMap<>(); @@ -410,13 +485,13 @@ public class TencentIMServiceImpl implements ITencentIMService { // 设置请求头 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); - + // 发送请求 HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); - + log.info("指定用户推送消息结果: {}", response.getBody()); - + // 解析响应判断是否成功 JSONObject responseJson = JSONObject.parseObject(response.getBody()); return "OK".equals(responseJson.getString("ActionStatus")); @@ -425,23 +500,23 @@ public class TencentIMServiceImpl implements ITencentIMService { return false; } } - + @Override public String processMessageVariables(String content, Map variables) { if (content == null || variables == null || variables.isEmpty()) { return content; } - + Matcher matcher = VARIABLE_PATTERN.matcher(content); StringBuffer sb = new StringBuffer(); - + while (matcher.find()) { String variableName = matcher.group(1); String replacement = variables.getOrDefault(variableName, "$" + variableName); matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement)); } - + matcher.appendTail(sb); return sb.toString(); } -} \ No newline at end of file +}