diff --git a/pom.xml b/pom.xml index a9e017390..c8dd1f8a5 100644 --- a/pom.xml +++ b/pom.xml @@ -409,11 +409,6 @@ ruoyi-common-mq ${revision} - - org.springframework.boot - spring-boot-starter-amqp - 2.5.4 - org.dromara diff --git a/ruoyi-admin/src/main/java/org/dromara/app/AppDictController.java b/ruoyi-admin/src/main/java/org/dromara/app/AppDictController.java new file mode 100644 index 000000000..0d6b6af02 --- /dev/null +++ b/ruoyi-admin/src/main/java/org/dromara/app/AppDictController.java @@ -0,0 +1,45 @@ +package org.dromara.app; + +import cn.hutool.core.util.ObjectUtil; +import com.wzj.soopin.member.convert.FeedbackConvert; +import com.wzj.soopin.member.domain.bo.FeedbackBO; +import com.wzj.soopin.member.service.IFeedbackService; +import io.swagger.annotations.Api; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.domain.R; +import org.dromara.common.log.annotation.Log; +import org.dromara.common.log.enums.BusinessType; +import org.dromara.system.domain.vo.SysDictDataVo; +import org.dromara.system.service.ISysDictDataService; +import org.dromara.system.service.ISysDictTypeService; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.List; + + +@Slf4j +@Api(tags = "VlogController 短视频相关业务功能的接口") +@RequestMapping("/app/sys/dict") +@RestController +@AllArgsConstructor +public class AppDictController { + private final ISysDictDataService dictDataService; + private final ISysDictTypeService dictTypeService; + + /** + * 根据字典类型查询字典数据信息 + * + * @param dictType 字典类型 + */ + @GetMapping(value = "/type/{dictType}") + public R> dictType(@PathVariable String dictType) { + List data = dictTypeService.selectDictDataByType(dictType); + if (ObjectUtil.isNull(data)) { + data = new ArrayList<>(); + } + return R.ok(data); + } +} diff --git a/ruoyi-admin/src/main/java/org/dromara/app/AppFeedbackController.java b/ruoyi-admin/src/main/java/org/dromara/app/AppFeedbackController.java new file mode 100644 index 000000000..f82244c90 --- /dev/null +++ b/ruoyi-admin/src/main/java/org/dromara/app/AppFeedbackController.java @@ -0,0 +1,51 @@ +package org.dromara.app; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.wzj.soopin.content.domain.bo.*; +import com.wzj.soopin.content.domain.po.Vlog; +import com.wzj.soopin.content.domain.vo.IndexVlogVO; +import com.wzj.soopin.content.service.VlogService; +import com.wzj.soopin.content.service.VlogUploadService; +import com.wzj.soopin.content.utils.QcCloud; +import com.wzj.soopin.content.utils.RedisOperator; +import com.wzj.soopin.member.convert.FeedbackConvert; +import com.wzj.soopin.member.domain.bo.FeedbackBO; +import com.wzj.soopin.member.service.IFeedbackService; +import io.swagger.annotations.Api; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.dromara.common.core.domain.R; +import org.dromara.common.core.domain.model.LoginUser; +import org.dromara.common.core.exception.ServiceException; +import org.dromara.common.log.annotation.Log; +import org.dromara.common.log.enums.BusinessType; +import org.dromara.common.satoken.utils.LoginHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.wzj.soopin.content.domain.base.BaseInfoProperties.*; + + +@Slf4j +@Api(tags = "VlogController 短视频相关业务功能的接口") +@RequestMapping("/app/feedback") +@RestController +@AllArgsConstructor +public class AppFeedbackController { + private final IFeedbackService service; + private final FeedbackConvert convert; + + @Tag(name = "修改意见反馈备注信息") + @Log(title = "新增意见反馈", businessType = BusinessType.UPDATE) + @PostMapping("/add") + public R add(@RequestBody FeedbackBO feedback) { + return R.ok(service.save(convert.toPo(feedback))); + } +} diff --git a/ruoyi-admin/src/main/java/org/dromara/app/AppVlogController.java b/ruoyi-admin/src/main/java/org/dromara/app/AppVlogController.java index 962de1bd6..716f8f66a 100644 --- a/ruoyi-admin/src/main/java/org/dromara/app/AppVlogController.java +++ b/ruoyi-admin/src/main/java/org/dromara/app/AppVlogController.java @@ -18,11 +18,16 @@ import org.apache.commons.lang3.StringUtils; import org.dromara.common.core.domain.R; import org.dromara.common.core.domain.model.LoginUser; import org.dromara.common.core.exception.ServiceException; +import org.dromara.common.mq.domain.MQMessage; +import org.dromara.common.mq.enums.MQMessageType; +import org.dromara.common.mq.enums.MessageActionEnum; +import org.dromara.common.mq.utils.MqUtil; import org.dromara.common.satoken.utils.LoginHelper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -150,15 +155,14 @@ public class AppVlogController { @PostMapping("like") - public R like(@RequestBody Map params) { + public R like(@RequestBody VlogBO vlogBO) { LoginUser loginUser = LoginHelper.getLoginUser(); if (loginUser == null) { throw new ServiceException("用户未登录"); } String userId = String.valueOf(loginUser.getUserId()); - String vlogId = params.get("vlogId"); - + String vlogId = vlogBO.getId(); //获取vlog @@ -188,6 +192,22 @@ public class AppVlogController { vlogService.flushCounts(vlogId, counts); } } + if (userId != null && vlog.getMemberId() != null && !userId.equals(vlog.getMemberId())) { + // 新版:使用模板类型编号和参数 + Map params = new HashMap<>(); + params.put("userId", userId); + params.put("nickname", loginUser.getNickname()); + params.put("action", MessageActionEnum.INTERACTION_LIKE.name()); + params.put("toUserId",vlog.getMemberId()); + MQMessage message = MQMessage.builder() + .messageType(MQMessageType.IM.name()) + .data(params) + .source("member") + .build(); + // 关注消息 + MqUtil.sendIMMessage(message); + + } return R.ok(); } diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheConstants.java index 42c2467b7..536bf0934 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheConstants.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheConstants.java @@ -57,4 +57,6 @@ public interface CacheConstants { String CHAT="CHAT:"; + String IM_MSG_RECEIVE="im:msg:receive:"; + } diff --git a/ruoyi-common/ruoyi-common-mq/pom.xml b/ruoyi-common/ruoyi-common-mq/pom.xml index cd5f7ba10..97f1eb7d5 100644 --- a/ruoyi-common/ruoyi-common-mq/pom.xml +++ b/ruoyi-common/ruoyi-common-mq/pom.xml @@ -32,7 +32,7 @@ org.apache.rocketmq rocketmq-spring-boot-starter - 2.2.3 + 2.3.1 diff --git a/ruoyi-common/ruoyi-common-mq/src/main/java/org/dromara/common/mq/domain/MQMessage.java b/ruoyi-common/ruoyi-common-mq/src/main/java/org/dromara/common/mq/domain/MQMessage.java index 3a47e8a0d..519c1cf9f 100644 --- a/ruoyi-common/ruoyi-common-mq/src/main/java/org/dromara/common/mq/domain/MQMessage.java +++ b/ruoyi-common/ruoyi-common-mq/src/main/java/org/dromara/common/mq/domain/MQMessage.java @@ -11,6 +11,7 @@ import java.time.LocalDateTime; @Data @Builder(toBuilder = true) @AllArgsConstructor +@NoArgsConstructor public class MQMessage { /** * 消息ID diff --git a/ruoyi-common/ruoyi-common-mq/src/main/java/org/dromara/common/mq/enums/MQMessageType.java b/ruoyi-common/ruoyi-common-mq/src/main/java/org/dromara/common/mq/enums/MQMessageType.java new file mode 100644 index 000000000..48a7969f6 --- /dev/null +++ b/ruoyi-common/ruoyi-common-mq/src/main/java/org/dromara/common/mq/enums/MQMessageType.java @@ -0,0 +1,7 @@ +package org.dromara.common.mq.enums; + +public enum MQMessageType { + IM, + VLOG, + COMMENT; +} diff --git a/ruoyi-common/ruoyi-common-mq/src/main/java/org/dromara/common/mq/enums/MessageActionEnum.java b/ruoyi-common/ruoyi-common-mq/src/main/java/org/dromara/common/mq/enums/MessageActionEnum.java index 005ff2b19..62a2b14cf 100644 --- a/ruoyi-common/ruoyi-common-mq/src/main/java/org/dromara/common/mq/enums/MessageActionEnum.java +++ b/ruoyi-common/ruoyi-common-mq/src/main/java/org/dromara/common/mq/enums/MessageActionEnum.java @@ -2,39 +2,37 @@ package org.dromara.common.mq.enums; public enum MessageActionEnum { - NEW_FOUCS(1,"新的关注"), //新的关注 - SYSTEM_NOTIFY(2, "系统->通知"), // 系统->通知 - SYSTEM_REPORT(3, "系统->举报下架(视频,视频评论) 视频评论"), // 系统->举报下架(视频,视频评论) 视频评论 - SYSTEM_CHECK(4, "系统->审核结果(复审,驳回 ,通过)"), // 系统->审核结果(复审,驳回 ,通过) - SYSTEM_PUSH(5, "系统->推广类的"), //系统->推广类的 - INTERACTION_COMMENT(6, "互动->评论"), //互动->评论 - INTERACTION_AT(7, "互动->视频评论中的@"), //互动->视频评论中的@ - INTERACTION_LIKE(8, "互动->点赞"), //互动->点赞 - INTERACTION_REPLY(9, "互动->评论回复"), //互动->评论回复 - ORDER_RECHARGE(10, "订单->充值 online"), //订单->充值 online - ORDER_PAY(11, "订单->订单交易成功通知 online"), //订单->订单交易成功通知 online - ORDER_REFUND(12, "订单->退款结果通知"), //订单->退款结果通知 - GROUP_NOTIFY_CHECK(13, "群通知->进群申请 online"), //群通知->进群申请 online - GROUP_NOTIFY_ACCEPT(14, "群通知->进群审核审核通过 online"), // 群通知->进群审核审核通过 online - GROUP_NOTIFY_FAIL(15, "群通知->进群审核审核拒绝 online"), // 群通知->进群审核审核拒绝 online - GROUP_NOTIFY_LEAVE_UP(16, "群通知->群升级为达人群通知"), // 群通知->群升级为达人群通知 - GROUP_NOTIFY_LEAVE_DOWN(17, "群通知->群降级为普通群通知"); // 群通知->群降级为普通群通知 + NEW_FOUCS(1,"newFocus"), //新的关注 + SYSTEM_NOTIFY(2, "system"), // 系统->通知 + SYSTEM_REPORT(3, "system"), // 系统->举报下架(视频,视频评论) 视频评论 + SYSTEM_CHECK(4, "system"), // 系统->审核结果(复审,驳回 ,通过) + SYSTEM_PUSH(5, "system"), //系统->推广类的 + INTERACTION_COMMENT(6, "interaction"), //互动->评论 + INTERACTION_AT(7, "interaction"), //互动->视频评论中的@ + INTERACTION_LIKE(8, "interaction"), //互动->点赞 + INTERACTION_REPLY(9, "interaction"), //互动->评论回复 + ORDER_RECHARGE(10, "order"), //订单->充值 online + ORDER_PAY(11, "order"), //订单->订单交易成功通知 online + ORDER_REFUND(12, "order"), //订单->退款结果通知 + GROUP_NOTIFY_CHECK(13, "groupNotify"), //群通知->进群申请 online + GROUP_NOTIFY_ACCEPT(14, "groupNotify"), // 群通知->进群审核审核通过 online + GROUP_NOTIFY_FAIL(15, "groupNotify"), // 群通知->进群审核审核拒绝 online + GROUP_NOTIFY_LEAVE_UP(16, "groupNotify"), // 群通知->群升级为达人群通知 + GROUP_NOTIFY_LEAVE_DOWN(17, "groupNotify"); // 群通知->群降级为普通群通知 private int code; private String account; - private String desc; - MessageActionEnum(int code, String desc) { + MessageActionEnum(int code, String account) { this.code = code; - this.desc = desc; + this.account = account; } public int getCode() { return code; } - - public String getDesc() { - return desc; + public String getAccount() { + return account; } public static MessageActionEnum getByCode(int code) { diff --git a/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/consumer/MessageRocketMQConsumer.java b/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/consumer/MessageRocketMQConsumer.java index cde201d26..cbfa1f133 100644 --- a/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/consumer/MessageRocketMQConsumer.java +++ b/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/consumer/MessageRocketMQConsumer.java @@ -1,16 +1,14 @@ package com.wzj.soopin.im.consumer; +import com.wzj.soopin.im.service.IMQMessageHandleService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; 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 com.wzj.soopin.im.domain.vo.SysMessageVo; import org.dromara.common.mq.config.RocketMQConfig; -import org.dromara.system.websocket.MessageWebSocketServer; +import org.dromara.common.mq.domain.MQMessage; import org.springframework.stereotype.Component; /** @@ -24,66 +22,45 @@ import org.springframework.stereotype.Component; @RocketMQMessageListener( topic = RocketMQConfig.TOPIC_IM_MSG, consumerGroup = RocketMQConfig.CONSUMER_GROUP_SYS_MSG, - selectorExpression = RocketMQConfig.TAG_SYS_MSG + selectorExpression = "*" +// ackMode = AckMode.MANUAL ) -public class MessageRocketMQConsumer implements RocketMQListener { +public class MessageRocketMQConsumer implements RocketMQListener { - private final MessageWebSocketServer messageWebSocketServer; + private final IMQMessageHandleService messageHandleService; @Override - public void onMessage(String message) { - try { - log.info("接收到RocketMQ消息: {}", message); - // 解析消息格式,格式为: {"userId": "123", "message": {...}} - MessageWrapper wrapper = JsonUtils.parseObject(message, MessageWrapper.class); - if (wrapper != null && wrapper.getUserId() != null && wrapper.getMessage() != null) { - // 将String类型的userId转换为Long类型 - Long userIdLong = null; - try { - if (StringUtils.isNotBlank(wrapper.getUserId())) { - userIdLong = Long.parseLong(wrapper.getUserId()); - } - } catch (NumberFormatException e) { - log.error("用户ID转换失败: {}", wrapper.getUserId(), e); - return; - } + public void onMessage(MessageExt messageExt) { // 参数为MessageExt + String message = new String(messageExt.getBody()); + log.info("接收到RocketMQ消息: {}, msgId: {}", message, messageExt.getMsgId()); - if (userIdLong != null) { - // 发送WebSocket消息 - messageWebSocketServer.sendMessage(userIdLong, JsonUtils.toJsonString(wrapper.getMessage())); - log.info("通过WebSocket发送消息成功,userId: {}", userIdLong); + try { + MQMessage mqMessage = JsonUtils.parseObject(message, MQMessage.class); + if (mqMessage != null) { + boolean result = messageHandleService.handleMessage(mqMessage); + if (result) { + // 消息处理成功,手动确认 +// messageExt.acknowledge(); // 2.3.1版本支持该方法 + log.info("消息确认成功, msgId: {}", messageExt.getMsgId()); } else { - log.warn("用户ID为空或无效: {}", wrapper.getUserId()); + // 业务失败,抛出异常触发重试 + throw new RuntimeException("业务处理失败,将重试"); } } else { - log.warn("消息格式不正确: {}", message); + log.warn("消息格式错误,直接确认避免重试: {}", message); +// messageExt.acknowledge(); // 格式错误无需重试 } } catch (Exception e) { - log.error("处理RocketMQ消息失败", e); + log.error("消息处理异常, msgId: {}", messageExt.getMsgId(), e); + // 达到最大重试次数后确认消息 + if (messageExt.getReconsumeTimes() >= 3) { + log.error("达到最大重试次数,确认消息进入死信队列, msgId: {}", messageExt.getMsgId()); +// messageExt.acknowledge(); + } else { + // 未达重试上限,抛出异常触发重试 + throw new RuntimeException("消息处理异常,将重试", e); + } } } - /** - * 消息包装类 - */ - public static class MessageWrapper { - private String userId; - private SysMessageVo message; - - public String getUserId() { - return userId; - } - - public void setUserId(String userId) { - this.userId = userId; - } - - public SysMessageVo getMessage() { - return message; - } - - public void setMessage(SysMessageVo message) { - this.message = message; - } - } } diff --git a/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/domain/SysMessageTemplate.java b/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/domain/SysMessageTemplate.java index 077d69b84..466e625c7 100644 --- a/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/domain/SysMessageTemplate.java +++ b/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/domain/SysMessageTemplate.java @@ -64,4 +64,9 @@ public class SysMessageTemplate extends BaseAudit { * 备注 */ private String description; + + /** + * 模板参数 + */ + private String params; } diff --git a/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/event/MessageEventListener.java b/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/event/MessageEventListener.java index fb23706c9..a1b0f174c 100644 --- a/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/event/MessageEventListener.java +++ b/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/event/MessageEventListener.java @@ -52,9 +52,9 @@ public class MessageEventListener { String userId = event.getUserIdStr(); // 创建消息包装对象 - MessageRocketMQConsumer.MessageWrapper wrapper = new MessageRocketMQConsumer.MessageWrapper(); - wrapper.setUserId(userId); - wrapper.setMessage(event.getMessage()); +// MessageRocketMQConsumer.MessageWrapper wrapper = new MessageRocketMQConsumer.MessageWrapper(); +// wrapper.setUserId(userId); +// wrapper.setMessage(event.getMessage()); // 首先尝试发送消息到腾讯IM boolean tencentIMSendSuccess = false; @@ -129,35 +129,18 @@ public class MessageEventListener { } } else { // 默认为单用户推送 - TencentIMServiceImpl.TencentIMResult imResult = tencentIMService.sendMessageToTencentIM(fromUserId, toUserId, content,ext); - tencentIMSendSuccess = imResult.isSuccess(); - if (tencentIMSendSuccess) { - log.info("腾讯IM推送成功,userId={}, response={}", toUserId, imResult.getRawResponse()); - } else { - log.warn("腾讯IM推送失败,userId={}, errorCode={}, errorInfo={}, response={}", - toUserId, imResult.getErrorCode(), imResult.getErrorInfo(), imResult.getRawResponse()); - } + tencentIMSendSuccess = tencentIMService.sendMessageToTencentIM(fromUserId, toUserId, content,ext); + + } + if (tencentIMSendSuccess) { + log.info("腾讯IM推送成功,userId={}", toUserId); + } else { + log.warn("腾讯IM推送失败,userId={}", toUserId); } } catch (Exception e) { log.error("发送消息到腾讯IM异常,将尝试通过其他方式发送", e); } - // 无论腾讯IM是否发送成功,都继续尝试通过RocketMQ发送消息 - // 这样可以确保消息至少通过一种方式发送出去 - /* - boolean rocketMQSendSuccess = false; - try { - rocketMQService.sendMessage( - RocketMQConfig.TOPIC_SYS_MSG, - RocketMQConfig.TAG_SYS_MSG, - wrapper - ); - rocketMQSendSuccess = true; - log.info("消息事件已通过RocketMQ发送,userId: {}", userId); - } catch (Exception e) { - log.error("通过RocketMQ发送消息失败,将尝试直接通过WebSocket发送", e); - } - */ boolean rocketMQSendSuccess = false; // 直接设为false,表示不走MQ // 如果前两种方式都失败,则尝试直接通过WebSocket发送 diff --git a/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/service/IMQMessageHandleService.java b/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/service/IMQMessageHandleService.java new file mode 100644 index 000000000..118edd1d2 --- /dev/null +++ b/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/service/IMQMessageHandleService.java @@ -0,0 +1,7 @@ +package com.wzj.soopin.im.service; + +import org.dromara.common.mq.domain.MQMessage; + +public interface IMQMessageHandleService { + boolean handleMessage(MQMessage message); +} diff --git a/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/service/ISysMessageTemplateService.java b/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/service/ISysMessageTemplateService.java index 37f65341a..a38d5e32e 100644 --- a/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/service/ISysMessageTemplateService.java +++ b/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/service/ISysMessageTemplateService.java @@ -66,4 +66,12 @@ public interface ISysMessageTemplateService extends IService */ List selectTemplateListByName(String name); SysMessageTemplate selectByTemplateType(String templateType) ; + + /** + * 根据action获取消息模板 + * @param action + * @return + */ + SysMessageTemplate getTemplateByAction(Integer action) ; + } diff --git a/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/service/ITencentIMService.java b/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/service/ITencentIMService.java index b7c1fda87..144e79e54 100644 --- a/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/service/ITencentIMService.java +++ b/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/service/ITencentIMService.java @@ -19,7 +19,7 @@ public interface ITencentIMService { * @param content 消息内容 * @return 是否发送成功 */ - TencentIMServiceImpl.TencentIMResult sendMessageToTencentIM(String fromUserId, String toUserId, String content, String cloudCustomData); + boolean sendMessageToTencentIM(String fromUserId, String toUserId, String content, String cloudCustomData); /** * 推送消息给全体用户 diff --git a/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/service/impl/MQMessageHandleServiceImpl.java b/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/service/impl/MQMessageHandleServiceImpl.java new file mode 100644 index 000000000..2e3b7e420 --- /dev/null +++ b/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/service/impl/MQMessageHandleServiceImpl.java @@ -0,0 +1,165 @@ +package com.wzj.soopin.im.service.impl; + +import cn.hutool.json.JSONObject; +import com.wzj.soopin.im.consumer.MessageRocketMQConsumer; +import com.wzj.soopin.im.domain.SysMessageTemplate; +import com.wzj.soopin.im.service.IMQMessageHandleService; +import com.wzj.soopin.im.service.ISysMessageTemplateService; +import com.wzj.soopin.im.service.ITencentIMService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.constant.CacheConstants; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.json.utils.JsonUtils; +import org.dromara.common.mq.domain.MQMessage; +import org.dromara.common.mq.enums.MessageActionEnum; +import org.dromara.common.redis.redis.RedisCache; +import org.dromara.system.websocket.MessageWebSocketServer; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Service +@RequiredArgsConstructor +@Slf4j +public class MQMessageHandleServiceImpl implements IMQMessageHandleService { + // 变量替换的正则表达式模式 + private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\$\\{\\w+}"); + private final RedisCache redisCache; + private final ISysMessageTemplateService templateService; + private final ITencentIMService tencentIMService; + private final MessageWebSocketServer messageWebSocketServer; + + public boolean handleMessage(MQMessage message) { + // 消息幂等性检查 + String messageId = message.getMessageId(); + // 检查消息是否已处理 + if (redisCache.getCacheObject(CacheConstants.IM_MSG_RECEIVE + messageId) != null) { + log.info("消息已处理,忽略重复消息:{}", messageId); + return true; + } + //创建im消息 + //根据消息类型获取消息模板 + Map params = (Map) message.getData(); + String action = (String) params.get("action"); + //根据action获取消息模板 + MessageActionEnum actionEnum = MessageActionEnum.valueOf(action); + + SysMessageTemplate template = templateService.getTemplateByAction(actionEnum.getCode()); + + if (template == null) { + log.error("根据action获取消息模板失败,action:{}", action); + return true; + } + + // 首先尝试发送消息到腾讯IM + boolean tencentIMSendSuccess = false; + try { + // 消息发送者可能是系统或管理员,这里使用action对应的账号发送 + String fromUserId = actionEnum.getAccount(); + String toUserId = ""; // 接收者是事件中的用户ID + String content = template.getContent(); // 只取content字段 + + // 处理消息变量替换(如果有) + // 从消息中提取标题和描述 + String title = template.getTitle(); + String desc = template.getContent(); + // 扩展字段,可以在APP端获取 + String ext = template.getExt(); + // 处理变量替换 + if (params.size() > 0) { + title = processMessageVariables(title, params); + desc = processMessageVariables(desc, params); + content = processMessageVariables(content, params); + ext = processMessageVariables(ext, params); + toUserId = String.valueOf(params.get("toUserId")); + } + + String pushType = null; + // 根据消息类型决定推送方式 + String templateParams = template.getParams(); + if (templateParams != null) { + JSONObject param = new JSONObject(templateParams); + // 从消息参数中提取接收者 + if (param != null && param.containsKey("pushType")) { + pushType = String.valueOf(param.get("pushType")); + } + } + + if (pushType != null) { + switch (pushType) { + case "ALL": + // 全员推送 + tencentIMSendSuccess = tencentIMService.pushToAll(title, desc, true, ext); + break; + case "ATTRIBUTE": + // 属性推送 + Map attributes = new HashMap<>(); + // 从消息参数中提取属性条件 + if (params != null && params.containsKey("attributes")) { + Object attrObj = params.get("attributes"); + if (attrObj instanceof Map) { + attributes = (Map) attrObj; + } + } + tencentIMSendSuccess = tencentIMService.pushByAttributes(title, desc, attributes, true, ext); + break; + case "TAG": + // 标签推送 + if (params != null && params.containsKey("tags")) { + Object tagsObj = params.get("tags"); + if (tagsObj instanceof String[]) { + tencentIMSendSuccess = tencentIMService.pushByTags(title, desc, + Arrays.asList((String[]) tagsObj), true, ext); + } + } + break; + case "USER": + default: + // 单用户推送(默认) + tencentIMSendSuccess = tencentIMService.pushToUsers(title, desc, + Collections.singletonList(toUserId), true, ext); + break; + } + } else { + // 默认为单用户推送 + tencentIMSendSuccess = tencentIMService.sendMessageToTencentIM(fromUserId, toUserId, content, ext); + } + if (tencentIMSendSuccess) { + log.info("腾讯IM推送成功,userId={}", toUserId); + } else { + log.error("腾讯IM推送失败,userId={}", toUserId); + } + // 如果前两种方式都失败,则尝试直接通过WebSocket发送 + if (!tencentIMSendSuccess) { + try { + messageWebSocketServer.sendMessage( + Long.parseLong(toUserId), + JsonUtils.toJsonString(message) + ); + log.info("消息事件已直接通过WebSocket发送,userId: {}", toUserId); + } catch (Exception e) { + log.error("通过WebSocket发送消息失败", e); + } + } + } catch (Exception e) { + log.error("发送消息到腾讯IM异常,将尝试通过其他方式发送", e); + } + return true; + } + + public String processMessageVariables(String content, Map variables) { + if (content == null || variables == null || variables.isEmpty()) { + return content; + } + for (String key : variables.keySet()) { + content = content.replaceAll("\\$\\{" + key + "\\}", variables.get(key).toString()); + } + return content; + } +} diff --git a/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/service/impl/SysMessageTemplateServiceImpl.java b/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/service/impl/SysMessageTemplateServiceImpl.java index cf98d70ac..f5105e27a 100644 --- a/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/service/impl/SysMessageTemplateServiceImpl.java +++ b/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/service/impl/SysMessageTemplateServiceImpl.java @@ -78,4 +78,10 @@ public class SysMessageTemplateServiceImpl extends ServiceImpl lqw = new LambdaQueryWrapper<>(); + lqw.eq(SysMessageTemplate::getAction, action).eq(SysMessageTemplate::getStatus, 0); + return templateMapper.selectOne(lqw); + } } diff --git a/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/service/impl/TencentIMServiceImpl.java b/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/service/impl/TencentIMServiceImpl.java index 36f96e25f..8c511fd11 100644 --- a/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/service/impl/TencentIMServiceImpl.java +++ b/ruoyi-modules/ruoyi-im/src/main/java/com/wzj/soopin/im/service/impl/TencentIMServiceImpl.java @@ -171,7 +171,7 @@ public class TencentIMServiceImpl implements ITencentIMService { } @Override - public TencentIMResult sendMessageToTencentIM(String fromUserId, String toUserId, String msgBody,String cloudCustomData) { + public boolean sendMessageToTencentIM(String fromUserId, String toUserId, String msgBody,String cloudCustomData) { TencentIMResult result = new TencentIMResult(); try { String userSig = generateAdminUserSig(); @@ -206,10 +206,9 @@ public class TencentIMServiceImpl implements ITencentIMService { result.setSuccess("OK".equals(result.getActionStatus()) && result.getErrorCode() == 0); } catch (Exception e) { log.error("发送消息到腾讯IM异常", e); - result.setSuccess(false); - result.setErrorInfo(e.getMessage()); + return false; } - return result; + return true; } @Override diff --git a/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/controller/FansController.java b/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/controller/FansController.java index 38163a1d4..2c51086df 100644 --- a/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/controller/FansController.java +++ b/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/controller/FansController.java @@ -22,6 +22,9 @@ import org.dromara.common.core.constant.CacheConstants; import org.dromara.common.core.domain.R; import org.dromara.common.log.annotation.Log; import org.dromara.common.log.enums.BusinessType; +import org.dromara.common.mq.domain.MQMessage; +import org.dromara.common.mq.enums.MessageActionEnum; +import org.dromara.common.mq.utils.MqUtil; import org.dromara.common.redis.utils.RedisUtils; import org.springframework.web.bind.annotation.*; @@ -88,201 +91,7 @@ public class FansController { return R.ok(service.removeById(id)); } - // 提取IM关注处理逻辑为私有方法 - private void handleImFollow(Long myId, List vloggerIds) { - if (myId != null && vloggerIds != null) { - for (Long vloggerId : vloggerIds) { - service.doFollow(myId, vloggerId); - } - } - } - @PostMapping("follow") - public Object follow(@RequestBody(required = false) Map callbackData, - @RequestParam(required = false) Long myId, - @RequestParam(required = false) Long vloggerId) { - // 1. IM回调格式 - if (callbackData != null && callbackData.containsKey("CallbackCommand")) { - log.info("收到IM关注回调: {}", callbackData); - - // 1.1 兼容腾讯IM的 PairList 回调格式 - Object pairListObj = callbackData.get("PairList"); - if (pairListObj instanceof List) { - List pairList = (List) pairListObj; - for (Object pairObj : pairList) { - if (pairObj instanceof Map) { - Map pair = (Map) pairObj; - Object fromAccountObj = pair.get("From_Account"); - Object toAccountObj = pair.get("To_Account"); - if (fromAccountObj != null && toAccountObj != null) { - Long fromId = null; - Long toId = null; - try { - fromId = Long.valueOf(fromAccountObj.toString()); - toId = Long.valueOf(toAccountObj.toString()); - } catch (Exception e) { - log.warn("IM回调Pair参数转换失败: {}", pair); - continue; - } - service.doFollow(fromId, toId); - log.info("IM回调处理成功: fromId={}, vloggerId={}", fromId, toId); - } else { - log.warn("IM回调Pair参数不完整: {}", pair); - } - } - } - // 返回IM平台要求的应答 - Map resp = new HashMap<>(); - resp.put("ActionStatus", "OK"); - resp.put("ErrorCode", 0); - resp.put("ErrorInfo", ""); - return resp; - } - - // 1.2 兼容原有 FollowList 格式 - String fromAccount = (String) callbackData.get("From_Account"); - List> followList = (List>) callbackData.get("FollowList"); - if (fromAccount != null && followList != null) { - Long fromId = Long.valueOf(fromAccount.replace("UserID_", "")); - List vloggerIds = new java.util.ArrayList<>(); - for (Map follow : followList) { - String toAccount = follow.get("To_Account"); - vloggerIds.add(Long.valueOf(toAccount.replace("UserID_", ""))); - } - handleImFollow(fromId, vloggerIds); - log.info("IM回调处理成功: fromId={}, vloggerIds={}", fromId, vloggerIds); - } else { - log.warn("IM回调参数不完整: {}", callbackData); - } - Map resp = new HashMap<>(); - resp.put("ActionStatus", "OK"); - resp.put("ErrorCode", 0); - resp.put("ErrorInfo", ""); - return resp; - } - // 2. 兼容JSON方式传递myId和vloggerId - if (callbackData != null && callbackData.containsKey("myId") && callbackData.containsKey("vloggerId")) { - log.info("收到JSON关注回调: {}", callbackData); - Object myIdObj = callbackData.get("myId"); - Object vloggerIdObj = callbackData.get("vloggerId"); - if (myIdObj != null && vloggerIdObj != null) { - Long myIdVal = Long.valueOf(myIdObj.toString()); - Long vloggerIdVal = Long.valueOf(vloggerIdObj.toString()); - handleImFollow(myIdVal, java.util.Arrays.asList(vloggerIdVal)); - log.info("JSON回调处理成功: myId={}, vloggerId={}", myIdVal, vloggerIdVal); - return R.ok("关注成功"); - } else { - log.warn("JSON回调参数不完整: {}", callbackData); - } - } - // 3. 普通参数,自动组装成IM格式处理 - if (myId != null && vloggerId != null) { - log.info("收到参数关注回调: myId={}, vloggerId={}", myId, vloggerId); - handleImFollow(myId, java.util.Arrays.asList(vloggerId)); - log.info("参数回调处理成功: myId={}, vloggerId={}", myId, vloggerId); - return R.ok("关注成功"); - } - log.error("关注回调失败,参数为空: callbackData={}, myId={}, vloggerId={}", callbackData, myId, vloggerId); - return R.fail("id不能为空"); - } - - @PostMapping("cancel") - public Object cancel(@RequestBody(required = false) Map callbackData, - @RequestParam(required = false) Long myId, - @RequestParam(required = false) Long vloggerId) { - // 1. 判断是否为IM回调格式 - if (callbackData != null && callbackData.containsKey("CallbackCommand")) { - log.info("收到IM取消关注回调: {}", callbackData); - - // 1.1 兼容腾讯IM的 PairList 回调格式 - Object pairListObj = callbackData.get("PairList"); - log.info("PairList 类型: {}, 内容: {}", pairListObj != null ? pairListObj.getClass() : "null", pairListObj); - if (pairListObj instanceof List) { - List pairList = (List) pairListObj; - for (Object pairObj : pairList) { - if (pairObj instanceof Map) { - Map pair = (Map) pairObj; - Object fromAccountObj = pair.get("From_Account"); - Object toAccountObj = pair.get("To_Account"); - if (fromAccountObj != null && toAccountObj != null) { - Long fromId = null; - Long toId = null; - try { - fromId = Long.valueOf(fromAccountObj.toString()); - toId = Long.valueOf(toAccountObj.toString()); - } catch (Exception e) { - log.warn("IM回调Pair参数转换失败: {}", pair); - continue; - } - service.doCancel(fromId, toId); - log.info("IM取消关注回调处理成功: fromId={}, toId={}", fromId, toId); - } else { - log.warn("IM取消关注回调Pair参数不完整: {}", pair); - } - } else { - log.warn("PairList元素不是Map: {}", pairObj); - } - } - // 返回IM平台要求的应答 - Map resp = new HashMap<>(); - resp.put("ActionStatus", "OK"); - resp.put("ErrorCode", 0); - resp.put("ErrorInfo", ""); - return resp; - } else { - log.warn("PairList 不是 List 类型: {}", pairListObj); - } - - // 1.2 兼容原有 To_Account 逻辑 - String fromAccount = (String) callbackData.get("From_Account"); - List toAccounts = null; - // 兼容腾讯IM的 To_Account 可能是 List 或 List> - Object toAccountObj = callbackData.get("To_Account"); - if (toAccountObj instanceof List) { - List list = (List) toAccountObj; - if (!list.isEmpty() && list.get(0) instanceof String) { - toAccounts = (List) list; - } else if (!list.isEmpty() && list.get(0) instanceof Map) { - // 兼容部分IM平台格式 - toAccounts = new java.util.ArrayList<>(); - for (Object obj : list) { - Map map = (Map) obj; - Object acc = map.get("To_Account"); - if (acc != null) toAccounts.add(acc.toString()); - } - } - } - if (fromAccount != null && toAccounts != null) { - Long fromId = Long.valueOf(fromAccount.replace("UserID_", "")); - for (String toAccount : toAccounts) { - Long toId = Long.valueOf(toAccount.replace("UserID_", "")); - service.doCancel(fromId, toId); - } - log.info("IM取消关注回调处理成功: fromId={}, toAccounts={}", fromId, toAccounts); - } else { - log.warn("IM取消关注回调参数不完整: {}", callbackData); - } - // 返回IM平台要求的应答 - Map resp = new HashMap<>(); - resp.put("ActionStatus", "OK"); - resp.put("ErrorCode", 0); - resp.put("ErrorInfo", ""); - return resp; - } - // 2. 原有取关逻辑 - if (myId == null || vloggerId == null) { - return R.fail("id不能为空"); - } - boolean flow = service.queryDoIFollowVloger(myId, vloggerId); - if (!flow) { - return R.fail("没有关注信息"); - } - service.doCancel(myId, vloggerId); - // 博主的粉丝-1,我的关注-1 - RedisUtils.decrAtomicValue(CacheConstants.MEMBER_FANS + ":" + vloggerId); - RedisUtils.decrAtomicValue(CacheConstants.MEMBER_FOLLOW + ":" + myId); - return R.ok("取消成功"); - } @GetMapping("block/list") @MemberFillMethod diff --git a/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/controller/ImCallbackController.java b/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/controller/ImCallbackController.java index b3cd9d563..da1edf115 100644 --- a/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/controller/ImCallbackController.java +++ b/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/controller/ImCallbackController.java @@ -1,38 +1,25 @@ package com.wzj.soopin.member.controller; -import com.wzj.soopin.member.domain.po.Member; -import com.wzj.soopin.member.service.IMemberService; -import com.wzj.soopin.member.service.IFansService; +import com.wzj.soopin.member.domain.vo.IMCallbackVO; +import com.wzj.soopin.member.service.IMCallbackService; import org.apache.commons.codec.digest.DigestUtils; -import org.dromara.common.core.constant.CacheConstants; -import org.dromara.common.redis.utils.RedisUtils; +import org.dromara.common.core.domain.R; +import org.dromara.common.core.utils.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; -import java.util.HashMap; -import java.util.List; import java.util.Map; -import com.wzj.soopin.member.service.IMemberBlockService; -import org.dromara.common.core.domain.R; -import org.dromara.common.core.utils.StringUtils; - @RestController @RequestMapping("/callback/api") public class ImCallbackController { private static final Logger log = LoggerFactory.getLogger(ImCallbackController.class); -// private static final String CALLBACK_TOKEN = "your_auth_token"; // @Autowired - private IMemberService memberService; - @Autowired - private IFansService fansService; - @Autowired - private FansController fansController; - @Autowired - private IMemberBlockService memberBlockService; + private IMCallbackService imCallbackService; + private static final String CALLBACK_TOKEN = "your_auth_token"; // @PostMapping public R handleCallback( @@ -41,13 +28,13 @@ public class ImCallbackController { @RequestParam(value = "CallbackCommand", required = false) String callbackCommand, @RequestBody Map requestBody) { -// // 1. 签名验证 -// if (!verifySignature(CALLBACK_TOKEN, sign, requestTime)) { -// return Map.of("ActionStatus", "FAIL", "ErrorCode", 1001); -// } + // 1. 签名验证 + if (sign != null && !verifySignature(CALLBACK_TOKEN, sign, requestTime)) { + return R.fail(1001, "签名校验失败"); + } // 2. 快速响应 - R response = R.ok(); + IMCallbackVO callbackVO = new IMCallbackVO(); // 3. 分发业务逻辑 if (StringUtils.isBlank(callbackCommand)) { @@ -56,148 +43,33 @@ public class ImCallbackController { switch (callbackCommand) { case "Sns.CallbackFriendAdd"://好友添加 // 关注,直接调用 FansController 的 follow - fansController.follow(requestBody, null, null); + callbackVO = imCallbackService.handleImFollow(requestBody); break; case "Sns.CallbackFriendDelete"://好友删除 // 取关,直接调用 FansController 的 cancel - fansController.cancel(requestBody, null, null); + imCallbackService.handleImCancelFollow(requestBody); break; case "Profile.CallbackPortraitSet": // 资料修改 - handleProfileCallbackPortraitSet(requestBody); + imCallbackService.handleProfileCallbackPortraitSet(requestBody); break; case "Sns.CallbackBlackListAdd": - handleImAddBlock(requestBody); + imCallbackService.handleImAddBlock(requestBody); break; case "Sns.CallbackBlackListDelete": - handleImCancelBlock(requestBody); + imCallbackService.handleImCancelBlock(requestBody); break; case "Follow.CallbackAfterFollowAdd"://关注 - fansController.follow(requestBody, null, null); + imCallbackService.handleImFollow(requestBody); break; case "Follow.CallbackAfterFollowDelete"://取关 - fansController.cancel(requestBody, null, null); + imCallbackService.handleImCancelFollow(requestBody); break; default: log.info("收到未知事件类型: {}, 参数: {}", callbackCommand, requestBody); break; } - return response; - } - - // 处理IM关注事件 - private void handleImFollow(Map requestBody) { - Object pairListObj = requestBody.get("PairList"); - if (pairListObj instanceof List) { - List pairList = (List) pairListObj; - for (Object pairObj : pairList) { - if (pairObj instanceof Map) { - Map pair = (Map) pairObj; - Object fromAccountObj = pair.get("From_Account"); - Object toAccountObj = pair.get("To_Account"); - if (fromAccountObj != null && toAccountObj != null) { - Long fromId = Long.valueOf(fromAccountObj.toString()); - Long toId = Long.valueOf(toAccountObj.toString()); - fansService.doFollow(fromId, toId); - } - } - } - } - } - - // 处理IM取关事件 - private void handleImCancel(Map requestBody) { - Object pairListObj = requestBody.get("PairList"); - if (pairListObj instanceof List) { - List pairList = (List) pairListObj; - for (Object pairObj : pairList) { - if (pairObj instanceof Map) { - Map pair = (Map) pairObj; - Object fromAccountObj = pair.get("From_Account"); - Object toAccountObj = pair.get("To_Account"); - if (fromAccountObj != null && toAccountObj != null) { - Long fromId = Long.valueOf(fromAccountObj.toString()); - Long toId = Long.valueOf(toAccountObj.toString()); - fansService.doCancel(fromId, toId); - } - } - } - } - } - - // 处理IM黑名单添加事件 - private void handleImAddBlock(Map requestBody) { - Object pairListObj = requestBody.get("PairList"); - if (pairListObj instanceof List) { - List pairList = (List) pairListObj; - for (Object pairObj : pairList) { - if (pairObj instanceof Map) { - Map pair = (Map) pairObj; - Object fromAccountObj = pair.get("From_Account"); - Object toAccountObj = pair.get("To_Account"); - if (fromAccountObj != null && toAccountObj != null) { - try { - Long myId = Long.valueOf(fromAccountObj.toString()); - Long vloggerId = Long.valueOf(toAccountObj.toString()); - // 1. 判断两个id不能为空 - if (myId == null || vloggerId == null) { - log.warn("黑名单回调参数id为空: myId={}, vloggerId={}", myId, vloggerId); - continue; - } - // 2. 是否已经存在关注关系 - boolean flow = fansService.queryDoIFollowVloger(myId, vloggerId); - if (flow) { - // 删除关注 - fansService.doCancel(myId, vloggerId); - // 博主的粉丝-1,我的关注-1 - RedisUtils.decrAtomicValue(CacheConstants.MEMBER_FANS + ":" + vloggerId); - RedisUtils.decrAtomicValue(CacheConstants.MEMBER_FOLLOW + ":" + myId); - } - // 3. 检查是否已在黑名单中 - boolean hasblock = memberBlockService.hasBlocked(myId, vloggerId); - if (hasblock) { - log.info("用户{}已拉黑{}", myId, vloggerId); - continue; - } - // 4. 拉黑 - memberBlockService.addBlock(myId, vloggerId); - log.info("用户{}成功拉黑{}", myId, vloggerId); - } catch (Exception e) { - log.warn("黑名单回调参数转换失败: {}", pair); - } - } - } - } - } - } - - // 处理IM取消拉黑事件 - private void handleImCancelBlock(Map requestBody) { - Object pairListObj = requestBody.get("PairList"); - if (pairListObj instanceof List) { - List pairList = (List) pairListObj; - for (Object pairObj : pairList) { - if (pairObj instanceof Map) { - Map pair = (Map) pairObj; - Object fromAccountObj = pair.get("From_Account"); - Object toAccountObj = pair.get("To_Account"); - if (fromAccountObj != null && toAccountObj != null) { - try { - Long myId = Long.valueOf(fromAccountObj.toString()); - Long vloggerId = Long.valueOf(toAccountObj.toString()); - if (myId == null || vloggerId == null) { - log.warn("取消拉黑回调参数id为空: myId={}, vloggerId={}", myId, vloggerId); - continue; - } - memberBlockService.removeBlock(myId, vloggerId); - log.info("用户{}已取消拉黑{}", myId, vloggerId); - } catch (Exception e) { - log.warn("取消拉黑回调参数转换失败: {}", pair); - } - } - } - } - } + return R.ok(callbackVO); } // 签名校验方法 @@ -209,58 +81,5 @@ public class ImCallbackController { return localSign.equals(sign); } - // 资料修改回调处理逻辑 - private void handleProfileCallbackPortraitSet(Map callbackData) { - log.info("收到IM用户资料更新回调: {}", callbackData); - String fromAccount = (String) callbackData.get("From_Account"); - List> profileItems = (List>) callbackData.get("ProfileItem"); - if (fromAccount != null && profileItems != null) { - Long userId = null; - try { - userId = Long.valueOf(fromAccount.replace("UserID_", "")); - } catch (Exception e) { - log.warn("用户ID转换失败: {}", fromAccount); - } - if (userId != null) { - Member memberToUpdate = memberService.getById(userId); - if (memberToUpdate != null) { - for (Map item : profileItems) { - String tag = (String) item.get("Tag"); - Object value = item.get("Value"); - updateMemberProfile(memberToUpdate, tag, value); - } - memberService.updateById(memberToUpdate); - log.info("IM用户资料更新回调处理成功: userId={}, profileItems={}", userId, profileItems); - } else { - log.warn("IM用户资料更新回调中用户不存在: userId={}", userId); - } - } - } else { - log.warn("IM用户资料更新回调参数不完整: {}", callbackData); - } - } - // 只更新 nickname/gender 字段 - private void updateMemberProfile(Member member, String tag, Object value) { - if (tag == null || value == null) { - return; - } - switch (tag) { - case "Tag_Profile_IM_Nick": - member.setNickname(value.toString()); - break; - case "Tag_Profile_IM_Gender": - if ("Gender_Type_Male".equals(value)) { - member.setGender(1); - } else if ("Gender_Type_Female".equals(value)) { - member.setGender(2); - } else { - member.setGender(0); - } - break; - default: - log.debug("未处理的用户资料字段: tag={}, value={}", tag, value); - break; - } - } } diff --git a/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/domain/vo/IMCallbackVO.java b/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/domain/vo/IMCallbackVO.java new file mode 100644 index 000000000..c23179ea0 --- /dev/null +++ b/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/domain/vo/IMCallbackVO.java @@ -0,0 +1,34 @@ +package com.wzj.soopin.member.domain.vo; + +import lombok.Data; + +import java.util.HashMap; +import java.util.Map; + +@Data +public class IMCallbackVO { + + private String ActionStatus; + private Integer ErrorCode; + private String ErrorInfo; + + + public IMCallbackVO() { + this.ActionStatus = "OK"; + this.ErrorCode = 0; + this.ErrorInfo = ""; + } + public IMCallbackVO(String ActionStatus, Integer ErrorCode, String ErrorInfo) { + this.ActionStatus = ActionStatus; + this.ErrorCode = ErrorCode; + this.ErrorInfo = ErrorInfo; + } + + public static IMCallbackVO success() { + return new IMCallbackVO(); + } + public static IMCallbackVO fail() { + return new IMCallbackVO("FAIL", 400, ""); + } + +} diff --git a/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/service/IFansService.java b/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/service/IFansService.java index eccac0f63..ecfa3270a 100644 --- a/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/service/IFansService.java +++ b/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/service/IFansService.java @@ -11,12 +11,12 @@ public interface IFansService extends IService { /** * 关注 */ - public void doFollow(Long myId, Long vloggerId); + public boolean doFollow(Long myId, Long vloggerId); /** * 取关 */ - public void doCancel(Long myId, Long vloggerId); + public boolean doCancel(Long myId, Long vloggerId); /** * 查询用户是否关注博主 diff --git a/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/service/IMCallbackService.java b/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/service/IMCallbackService.java new file mode 100644 index 000000000..85a03d9fe --- /dev/null +++ b/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/service/IMCallbackService.java @@ -0,0 +1,17 @@ +package com.wzj.soopin.member.service; + +import com.wzj.soopin.member.domain.vo.IMCallbackVO; + +import java.util.Map; + +public interface IMCallbackService { + IMCallbackVO handleImFollow(Map params); + + IMCallbackVO handleImCancelFollow(Map params); + IMCallbackVO handleImAddBlock(Map params); + + IMCallbackVO handleImCancelBlock(Map params); + + IMCallbackVO handleProfileCallbackPortraitSet(Map params); + +} diff --git a/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/service/impl/FansServiceImpl.java b/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/service/impl/FansServiceImpl.java index b6f48818b..33b734cff 100644 --- a/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/service/impl/FansServiceImpl.java +++ b/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/service/impl/FansServiceImpl.java @@ -20,6 +20,7 @@ import org.dromara.common.mq.domain.MQMessage; import org.dromara.common.mq.enums.MessageActionEnum; import org.dromara.common.mq.utils.MqUtil; import org.dromara.common.redis.redis.RedisCache; +import org.dromara.common.redis.utils.RedisUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -39,16 +40,18 @@ public class FansServiceImpl extends ServiceImpl implements IF @Autowired private IMemberService memberService; + + + + @Transactional(rollbackFor = Exception.class) @Override - public void doFollow(Long myId, Long vloggerId) { + public boolean doFollow(Long myId, Long vloggerId) { // 幂等性校验:已存在则不再插入 - - Fans existing = queryFansRelationship(myId, vloggerId); if (existing != null) { - return; + return true; } Fans fans = new Fans(); @@ -73,9 +76,11 @@ public class FansServiceImpl extends ServiceImpl implements IF if (follower != null && vlogger != null && !myId.equals(vloggerId)) { // 新版:使用模板类型编号和参数 Map params = new HashMap<>(); - params.put("followerNickname", follower.getNickname() == null ? "" : follower.getNickname()); - params.put("vloggerNickname", vlogger.getNickname() == null ? "" : vlogger.getNickname()); + params.put("userId", follower.getId()); + params.put("nickname", follower.getNickname() == null ? "" : follower.getNickname()); + params.put("faceUrl", vlogger.getAvatar() == null ? "" : vlogger.getAvatar()); params.put("action", MessageActionEnum.NEW_FOUCS.name()); + params.put("toUserId",vloggerId); MQMessage message = MQMessage.builder() .messageType("follow") .data(params) @@ -85,6 +90,7 @@ public class FansServiceImpl extends ServiceImpl implements IF MqUtil.sendIMMessage(message); } + return true; } public Fans queryFansRelationship(Long fanId, Long vlogerId) { @@ -97,7 +103,7 @@ public class FansServiceImpl extends ServiceImpl implements IF @Transactional(rollbackFor = Exception.class) @Override - public void doCancel(Long myId, Long vloggerId) { + public boolean doCancel(Long myId, Long vloggerId) { // 判断我们是否朋友关系,如果是,则需要取消双方的关系 Fans fan = queryFansRelationship(myId, vloggerId); if (fan != null && YesOrNo.YES.type.equals(fan.getFriendFlag())) { @@ -117,23 +123,31 @@ public class FansServiceImpl extends ServiceImpl implements IF baseMapper.delete(deleteWrapper); } else { log.warn("取消关注时未找到自己的粉丝关系,myId={}, vloggerId={}", myId, vloggerId); + return true; } + // 博主的粉丝-1,我的关注-1 + RedisUtils.decrAtomicValue(CacheConstants.MEMBER_FANS + ":" + vloggerId); + RedisUtils.decrAtomicValue(CacheConstants.MEMBER_FOLLOW + ":" + myId); // 推送取关消息 Member follower = memberService.getById(myId); Member vlogger = memberService.getById(vloggerId); if (follower != null && vlogger != null && !myId.equals(vloggerId)) { - // 新版:使用模板类型编号和参数 -// Map params = new HashMap<>(); -// params.put("followerNickname", follower.getNickname() == null ? "" : follower.getNickname()); -// params.put("vloggerNickname", vlogger.getNickname() == null ? "" : vlogger.getNickname()); -// SysMessageBo messageBo = new SysMessageBo(); -// messageBo.setTemplateType(org.dromara.system.domain.MessageTemplateType.UNFOLLOW); // 取关类型编号 -// messageBo.setTemplateParams(params); -// messageBo.setSenderId(myId); -// // 补充:设置消息标题,防止title为null -// messageBo.setTitle("取消关注通知"); -// sysMessageService.sendMessageToUser(messageBo, vloggerId); +// 新版:使用模板类型编号和参数 + Map params = new HashMap<>(); + params.put("userId", follower.getUserId()); + params.put("nickname", follower.getNickname() == null ? "" : follower.getNickname()); + params.put("faceUrl", vlogger.getAvatar() == null ? "" : vlogger.getAvatar()); + params.put("action", MessageActionEnum.NEW_FOUCS.name()); + params.put("toUserId",vloggerId); + MQMessage message = MQMessage.builder() + .messageType("follow") + .data(params) + .source("member") + .build(); + // 关注消息 + MqUtil.sendIMMessage(message); } + return true; } @Override diff --git a/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/service/impl/IMCallbackServiceImpl.java b/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/service/impl/IMCallbackServiceImpl.java new file mode 100644 index 000000000..a34eac031 --- /dev/null +++ b/ruoyi-modules/ruoyi-member/src/main/java/com/wzj/soopin/member/service/impl/IMCallbackServiceImpl.java @@ -0,0 +1,324 @@ +package com.wzj.soopin.member.service.impl; + +import com.wzj.soopin.member.domain.po.Member; +import com.wzj.soopin.member.domain.vo.IMCallbackVO; +import com.wzj.soopin.member.service.IFansService; +import com.wzj.soopin.member.service.IMCallbackService; +import com.wzj.soopin.member.service.IMemberBlockService; +import com.wzj.soopin.member.service.IMemberService; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.digest.DigestUtils; +import org.dromara.common.core.constant.CacheConstants; +import org.dromara.common.core.domain.R; +import org.dromara.common.mq.domain.MQMessage; +import org.dromara.common.mq.enums.MessageActionEnum; +import org.dromara.common.mq.utils.MqUtil; +import org.dromara.common.redis.utils.RedisUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@AllArgsConstructor +@Slf4j +public class IMCallbackServiceImpl implements IMCallbackService { + private final IFansService fansService; + private final IMemberBlockService memberBlockService; + private final IMemberService memberService; + + @Transactional(rollbackFor = Exception.class) + @Override + public IMCallbackVO handleImFollow(Map callbackData) { + // 幂等性校验:已存在则不再插入 + // 1. IM回调格式 + if (callbackData != null && callbackData.containsKey("CallbackCommand")) { + log.info("收到IM关注回调: {}", callbackData); + + // 1.1 兼容腾讯IM的 PairList 回调格式 + Object pairListObj = callbackData.get("PairList"); + if (pairListObj instanceof List) { + List pairList = (List) pairListObj; + for (Object pairObj : pairList) { + if (pairObj instanceof Map) { + Map pair = (Map) pairObj; + Object fromAccountObj = pair.get("From_Account"); + Object toAccountObj = pair.get("To_Account"); + if (fromAccountObj != null && toAccountObj != null) { + Long fromId = null; + Long toId = null; + try { + fromId = Long.valueOf(fromAccountObj.toString()); + toId = Long.valueOf(toAccountObj.toString()); + } catch (Exception e) { + log.warn("IM回调Pair参数转换失败: {}", pair); + continue; + } + fansService.doFollow(fromId, toId); + log.info("IM回调处理成功: fromId={}, vloggerId={}", fromId, toId); + } else { + log.warn("IM回调Pair参数不完整: {}", pair); + } + } + } + // 返回IM平台要求的应答 + return IMCallbackVO.success(); + } + + // 1.2 兼容原有 FollowList 格式 + String fromAccount = (String) callbackData.get("From_Account"); + List> followList = (List>) callbackData.get("FollowList"); + if (fromAccount != null && followList != null) { + Long fromId = Long.valueOf(fromAccount.replace("UserID_", "")); + List vloggerIds = new java.util.ArrayList<>(); + for (Map follow : followList) { + String toAccount = follow.get("To_Account"); + fansService.doFollow(fromId, Long.valueOf(toAccount.replace("UserID_", ""))); + } + + log.info("IM回调处理成功: fromId={}, vloggerIds={}", fromId, vloggerIds); + } else { + log.warn("IM回调参数不完整: {}", callbackData); + } + return IMCallbackVO.success(); + } + // 2. 兼容JSON方式传递myId和vloggerId + if (callbackData != null && callbackData.containsKey("myId") && callbackData.containsKey("vloggerId")) { + log.info("收到JSON关注回调: {}", callbackData); + Object myIdObj = callbackData.get("myId"); + Object vloggerIdObj = callbackData.get("vloggerId"); + if (myIdObj != null && vloggerIdObj != null) { + Long myIdVal = Long.valueOf(myIdObj.toString()); + Long vloggerIdVal = Long.valueOf(vloggerIdObj.toString()); + fansService.doFollow(myIdVal, vloggerIdVal); + + log.info("JSON回调处理成功: myId={}, vloggerId={}", myIdVal, vloggerIdVal); + return IMCallbackVO.success(); + } else { + log.warn("JSON回调参数不完整: {}", callbackData); + } + } + return IMCallbackVO.success(); + } + + + @Override + public IMCallbackVO handleImCancelFollow(Map callbackData) { + // 1. 判断是否为IM回调格式 + if (callbackData != null && callbackData.containsKey("CallbackCommand")) { + log.info("收到IM取消关注回调: {}", callbackData); + + // 1.1 兼容腾讯IM的 PairList 回调格式 + Object pairListObj = callbackData.get("PairList"); + log.info("PairList 类型: {}, 内容: {}", pairListObj != null ? pairListObj.getClass() : "null", pairListObj); + if (pairListObj instanceof List) { + List pairList = (List) pairListObj; + for (Object pairObj : pairList) { + if (pairObj instanceof Map) { + Map pair = (Map) pairObj; + Object fromAccountObj = pair.get("From_Account"); + Object toAccountObj = pair.get("To_Account"); + if (fromAccountObj != null && toAccountObj != null) { + Long fromId = null; + Long toId = null; + try { + fromId = Long.valueOf(fromAccountObj.toString()); + toId = Long.valueOf(toAccountObj.toString()); + } catch (Exception e) { + log.warn("IM回调Pair参数转换失败: {}", pair); + continue; + } + fansService.doCancel(fromId, toId); + log.info("IM取消关注回调处理成功: fromId={}, toId={}", fromId, toId); + } else { + log.warn("IM取消关注回调Pair参数不完整: {}", pair); + } + } else { + log.warn("PairList元素不是Map: {}", pairObj); + } + } + // 返回IM平台要求的应答 + + return IMCallbackVO.success(); + } else { + log.warn("PairList 不是 List 类型: {}", pairListObj); + } + + // 1.2 兼容原有 To_Account 逻辑 + String fromAccount = (String) callbackData.get("From_Account"); + List toAccounts = null; + // 兼容腾讯IM的 To_Account 可能是 List 或 List> + Object toAccountObj = callbackData.get("To_Account"); + if (toAccountObj instanceof List) { + List list = (List) toAccountObj; + if (!list.isEmpty() && list.get(0) instanceof String) { + toAccounts = (List) list; + } else if (!list.isEmpty() && list.get(0) instanceof Map) { + // 兼容部分IM平台格式 + toAccounts = new java.util.ArrayList<>(); + for (Object obj : list) { + Map map = (Map) obj; + Object acc = map.get("To_Account"); + if (acc != null) toAccounts.add(acc.toString()); + } + } + } + if (fromAccount != null && toAccounts != null) { + Long fromId = Long.valueOf(fromAccount.replace("UserID_", "")); + for (String toAccount : toAccounts) { + Long toId = Long.valueOf(toAccount.replace("UserID_", "")); + fansService.doCancel(fromId, toId); + } + log.info("IM取消关注回调处理成功: fromId={}, toAccounts={}", fromId, toAccounts); + } else { + log.warn("IM取消关注回调参数不完整: {}", callbackData); + } + // 返回IM平台要求的应答 + return IMCallbackVO.success(); + } + return IMCallbackVO.success(); + + } + + + + // 资料修改回调处理逻辑 + @Override + public IMCallbackVO handleProfileCallbackPortraitSet(Map callbackData) { + log.info("收到IM用户资料更新回调: {}", callbackData); + String fromAccount = (String) callbackData.get("From_Account"); + List> profileItems = (List>) callbackData.get("ProfileItem"); + if (fromAccount != null && profileItems != null) { + Long userId = null; + try { + userId = Long.valueOf(fromAccount.replace("UserID_", "")); + } catch (Exception e) { + log.warn("用户ID转换失败: {}", fromAccount); + } + if (userId != null) { + Member memberToUpdate = memberService.getById(userId); + if (memberToUpdate != null) { + for (Map item : profileItems) { + String tag = (String) item.get("Tag"); + Object value = item.get("Value"); + updateMemberProfile(memberToUpdate, tag, value); + } + memberService.updateById(memberToUpdate); + log.info("IM用户资料更新回调处理成功: userId={}, profileItems={}", userId, profileItems); + } else { + log.warn("IM用户资料更新回调中用户不存在: userId={}", userId); + } + } + } else { + log.warn("IM用户资料更新回调参数不完整: {}", callbackData); + } + return IMCallbackVO.success(); + } + + // 只更新 nickname/gender 字段 + private void updateMemberProfile(Member member, String tag, Object value) { + if (tag == null || value == null) { + return; + } + switch (tag) { + case "Tag_Profile_IM_Nick": + member.setNickname(value.toString()); + break; + case "Tag_Profile_IM_Gender": + if ("Gender_Type_Male".equals(value)) { + member.setGender(1); + } else if ("Gender_Type_Female".equals(value)) { + member.setGender(2); + } else { + member.setGender(0); + } + break; + default: + log.debug("未处理的用户资料字段: tag={}, value={}", tag, value); + break; + } + } + + // 处理IM黑名单添加事件 + @Override + public IMCallbackVO handleImAddBlock(Map requestBody) { + Object pairListObj = requestBody.get("PairList"); + if (pairListObj instanceof List) { + List pairList = (List) pairListObj; + for (Object pairObj : pairList) { + if (pairObj instanceof Map) { + Map pair = (Map) pairObj; + Object fromAccountObj = pair.get("From_Account"); + Object toAccountObj = pair.get("To_Account"); + if (fromAccountObj != null && toAccountObj != null) { + try { + Long myId = Long.valueOf(fromAccountObj.toString()); + Long vloggerId = Long.valueOf(toAccountObj.toString()); + // 1. 判断两个id不能为空 + if (myId == null || vloggerId == null) { + log.warn("黑名单回调参数id为空: myId={}, vloggerId={}", myId, vloggerId); + continue; + } + // 2. 是否已经存在关注关系 + boolean flow = fansService.queryDoIFollowVloger(myId, vloggerId); + if (flow) { + // 删除关注 + fansService.doCancel(myId, vloggerId); + // 博主的粉丝-1,我的关注-1 + RedisUtils.decrAtomicValue(CacheConstants.MEMBER_FANS + ":" + vloggerId); + RedisUtils.decrAtomicValue(CacheConstants.MEMBER_FOLLOW + ":" + myId); + } + // 3. 检查是否已在黑名单中 + boolean hasblock = memberBlockService.hasBlocked(myId, vloggerId); + if (hasblock) { + log.info("用户{}已拉黑{}", myId, vloggerId); + continue; + } + // 4. 拉黑 + memberBlockService.addBlock(myId, vloggerId); + log.info("用户{}成功拉黑{}", myId, vloggerId); + } catch (Exception e) { + log.warn("黑名单回调参数转换失败: {}", pair); + } + } + } + } + } + return IMCallbackVO.success(); + } + + + // 处理IM取消拉黑事件 + @Override + public IMCallbackVO handleImCancelBlock(Map requestBody) { + Object pairListObj = requestBody.get("PairList"); + if (pairListObj instanceof List) { + List pairList = (List) pairListObj; + for (Object pairObj : pairList) { + if (pairObj instanceof Map) { + Map pair = (Map) pairObj; + Object fromAccountObj = pair.get("From_Account"); + Object toAccountObj = pair.get("To_Account"); + if (fromAccountObj != null && toAccountObj != null) { + try { + Long myId = Long.valueOf(fromAccountObj.toString()); + Long vloggerId = Long.valueOf(toAccountObj.toString()); + if (myId == null || vloggerId == null) { + log.warn("取消拉黑回调参数id为空: myId={}, vloggerId={}", myId, vloggerId); + continue; + } + memberBlockService.removeBlock(myId, vloggerId); + log.info("用户{}已取消拉黑{}", myId, vloggerId); + } catch (Exception e) { + log.warn("取消拉黑回调参数转换失败: {}", pair); + } + } + } + } + } + return IMCallbackVO.success(); + } +}