[fix]修改消息的逻辑,将消息通过mq放出来,然后再由消费者发送给腾讯

This commit is contained in:
wangqx 2025-08-27 10:49:02 +08:00
parent 8859977761
commit 03a9967da4
25 changed files with 818 additions and 532 deletions

View File

@ -409,11 +409,6 @@
<artifactId>ruoyi-common-mq</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>org.dromara</groupId>

View File

@ -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<List<SysDictDataVo>> dictType(@PathVariable String dictType) {
List<SysDictDataVo> data = dictTypeService.selectDictDataByType(dictType);
if (ObjectUtil.isNull(data)) {
data = new ArrayList<>();
}
return R.ok(data);
}
}

View File

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

View File

@ -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<Void> like(@RequestBody Map<String, String> params) {
public R<Void> 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<String, Object> 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();
}

View File

@ -57,4 +57,6 @@ public interface CacheConstants {
String CHAT="CHAT:";
String IM_MSG_RECEIVE="im:msg:receive:";
}

View File

@ -32,7 +32,7 @@
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.3</version>
<version>2.3.1</version>
</dependency>
<!-- Spring Messaging for RocketMQ -->

View File

@ -11,6 +11,7 @@ import java.time.LocalDateTime;
@Data
@Builder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
public class MQMessage {
/**
* 消息ID

View File

@ -0,0 +1,7 @@
package org.dromara.common.mq.enums;
public enum MQMessageType {
IM,
VLOG,
COMMENT;
}

View File

@ -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) {

View File

@ -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<String> {
public class MessageRocketMQConsumer implements RocketMQListener<MessageExt> {
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;
}
}
}

View File

@ -64,4 +64,9 @@ public class SysMessageTemplate extends BaseAudit {
* 备注
*/
private String description;
/**
* 模板参数
*/
private String params;
}

View File

@ -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发送

View File

@ -0,0 +1,7 @@
package com.wzj.soopin.im.service;
import org.dromara.common.mq.domain.MQMessage;
public interface IMQMessageHandleService {
boolean handleMessage(MQMessage message);
}

View File

@ -66,4 +66,12 @@ public interface ISysMessageTemplateService extends IService<SysMessageTemplate>
*/
List<SysMessageTemplateVo> selectTemplateListByName(String name);
SysMessageTemplate selectByTemplateType(String templateType) ;
/**
* 根据action获取消息模板
* @param action
* @return
*/
SysMessageTemplate getTemplateByAction(Integer action) ;
}

View File

@ -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);
/**
* 推送消息给全体用户

View File

@ -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<String, Object> params = (Map<String, Object>) 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<String, Object> attributes = new HashMap<>();
// 从消息参数中提取属性条件
if (params != null && params.containsKey("attributes")) {
Object attrObj = params.get("attributes");
if (attrObj instanceof Map) {
attributes = (Map<String, Object>) 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<String, Object> 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;
}
}

View File

@ -78,4 +78,10 @@ public class SysMessageTemplateServiceImpl extends ServiceImpl<SysMessageTemplat
lqw.eq(SysMessageTemplate::getType, templateType).eq(SysMessageTemplate::getStatus, 0);
return templateMapper.selectOne(lqw);
}
@Override
public SysMessageTemplate getTemplateByAction(Integer action) {
LambdaQueryWrapper<SysMessageTemplate> lqw = new LambdaQueryWrapper<>();
lqw.eq(SysMessageTemplate::getAction, action).eq(SysMessageTemplate::getStatus, 0);
return templateMapper.selectOne(lqw);
}
}

View File

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

View File

@ -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<Long> vloggerIds) {
if (myId != null && vloggerIds != null) {
for (Long vloggerId : vloggerIds) {
service.doFollow(myId, vloggerId);
}
}
}
@PostMapping("follow")
public Object follow(@RequestBody(required = false) Map<String, Object> 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<String, Object> 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<Map<String, String>> followList = (List<Map<String, String>>) callbackData.get("FollowList");
if (fromAccount != null && followList != null) {
Long fromId = Long.valueOf(fromAccount.replace("UserID_", ""));
List<Long> vloggerIds = new java.util.ArrayList<>();
for (Map<String, String> 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<String, Object> 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<String, Object> 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<String, Object> 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<String> toAccounts = null;
// 兼容腾讯IM的 To_Account 可能是 List<String> List<Map<String, String>>
Object toAccountObj = callbackData.get("To_Account");
if (toAccountObj instanceof List) {
List<?> list = (List<?>) toAccountObj;
if (!list.isEmpty() && list.get(0) instanceof String) {
toAccounts = (List<String>) 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<String, Object> 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

View File

@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> callbackData) {
log.info("收到IM用户资料更新回调: {}", callbackData);
String fromAccount = (String) callbackData.get("From_Account");
List<Map<String, Object>> profileItems = (List<Map<String, Object>>) 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<String, Object> 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;
}
}
}

View File

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

View File

@ -11,12 +11,12 @@ public interface IFansService extends IService<Fans> {
/**
* 关注
*/
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);
/**
* 查询用户是否关注博主

View File

@ -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<String, Object> params);
IMCallbackVO handleImCancelFollow(Map<String, Object> params);
IMCallbackVO handleImAddBlock(Map<String, Object> params);
IMCallbackVO handleImCancelBlock(Map<String, Object> params);
IMCallbackVO handleProfileCallbackPortraitSet(Map<String, Object> params);
}

View File

@ -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<FansMapper, Fans> 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<FansMapper, Fans> implements IF
if (follower != null && vlogger != null && !myId.equals(vloggerId)) {
// 新版使用模板类型编号和参数
Map<String, Object> 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<FansMapper, Fans> implements IF
MqUtil.sendIMMessage(message);
}
return true;
}
public Fans queryFansRelationship(Long fanId, Long vlogerId) {
@ -97,7 +103,7 @@ public class FansServiceImpl extends ServiceImpl<FansMapper, Fans> 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<FansMapper, Fans> 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<String, Object> 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<String, Object> 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

View File

@ -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<String, Object> 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<Map<String, String>> followList = (List<Map<String, String>>) callbackData.get("FollowList");
if (fromAccount != null && followList != null) {
Long fromId = Long.valueOf(fromAccount.replace("UserID_", ""));
List<Long> vloggerIds = new java.util.ArrayList<>();
for (Map<String, String> 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<String, Object> 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<String> toAccounts = null;
// 兼容腾讯IM的 To_Account 可能是 List<String> List<Map<String, String>>
Object toAccountObj = callbackData.get("To_Account");
if (toAccountObj instanceof List) {
List<?> list = (List<?>) toAccountObj;
if (!list.isEmpty() && list.get(0) instanceof String) {
toAccounts = (List<String>) 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<String, Object> callbackData) {
log.info("收到IM用户资料更新回调: {}", callbackData);
String fromAccount = (String) callbackData.get("From_Account");
List<Map<String, Object>> profileItems = (List<Map<String, Object>>) 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<String, Object> 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<String, Object> 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<String, Object> 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();
}
}