fix: 对接ai模块

This commit is contained in:
乾乾
2025-10-30 10:07:46 +08:00
committed by Dawn
parent da0f814bc2
commit 9f269d64d7
52 changed files with 1057 additions and 666 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -34,7 +34,14 @@
<artifactId>luohuo-database-mode</artifactId>
<version>${luohuo-project.version}</version>
</dependency>
<dependency>
<groupId>com.luohuo.flex</groupId>
<artifactId>luohuo-sa-token-ext</artifactId>
</dependency>
<dependency>
<groupId>com.luohuo.basic</groupId>
<artifactId>luohuo-tenant</artifactId>
</dependency>
<dependency>
<groupId>com.luohuo.basic</groupId>
<artifactId>luohuo-all</artifactId>

View File

@@ -2,6 +2,13 @@ package com.luohuo.flex.ai.config;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.json.JsonReadFeature;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.luohuo.basic.jackson.LuohuoJacksonModule;
import com.luohuo.basic.utils.SpringUtils;
import com.luohuo.flex.ai.core.AiModelFactory;
import com.luohuo.flex.ai.core.AiModelFactoryImpl;
import com.luohuo.flex.ai.core.model.BaiChuanChatModel;
@@ -26,10 +33,22 @@ import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
import org.springframework.ai.tokenizer.TokenCountEstimator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import java.text.SimpleDateFormat;
import java.time.ZoneId;
import java.util.Locale;
import java.util.TimeZone;
import static com.luohuo.basic.utils.DateUtils.DEFAULT_DATE_TIME_FORMAT;
@Configuration
@EnableConfigurationProperties({HulaAiProperties.class,
@@ -242,4 +261,47 @@ public class AiAutoConfiguration {
private static ToolCallingManager getToolCallingManager() {
return SpringUtil.getBean(ToolCallingManager.class);
}
/**
* Spring 工具类
*
* @param applicationContext 上下文
*/
@Bean
public SpringUtils getSpringUtils(ApplicationContext applicationContext) {
SpringUtils instance = SpringUtils.getInstance();
SpringUtils.setApplicationContext(applicationContext);
return instance;
}
@Bean
@Primary
@ConditionalOnClass(ObjectMapper.class)
@ConditionalOnMissingBean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
objectMapper
.setLocale(Locale.CHINA)
//去掉默认的时间戳格式
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
// 时区
.setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault()))
//Date参数日期格式
.setDateFormat(new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT, Locale.CHINA))
//该特性决定parser是否允许JSON字符串包含非引号控制字符值小于32的ASCII字符包含制表符和换行符。 如果该属性关闭则如果遇到这些字符则会抛出异常。JSON标准说明书要求所有控制符必须使用引号因此这是一个非标准的特性
.configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), true)
// 忽略不能转义的字符
.configure(JsonReadFeature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER.mappedFeature(), true)
//在使用spring boot + jpa/hibernate如果实体字段上加有FetchType.LAZY并使用jackson序列化为json串时会遇到SerializationFeature.FAIL_ON_EMPTY_BEANS异常
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
//忽略未知字段
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
//单引号处理
.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
// 注册自定义模块
objectMapper.registerModule(new LuohuoJacksonModule()).findAndRegisterModules();
return objectMapper;
}
}

View File

@@ -85,10 +85,12 @@ public class AiChatConversationController {
}
// ========== 对话管理 ==========
@GetMapping("/page")
@Operation(summary = "获得对话分页", description = "用于【对话管理】菜单")
public R<PageResult<AiChatConversationRespVO>> getChatConversationPage(AiChatConversationPageReqVO pageReqVO) {
if(ObjUtil.isNull(pageReqVO.getUid())){
pageReqVO.setUid(ContextUtil.getUid());
}
PageResult<AiChatConversationDO> pageResult = chatConversationService.getChatConversationPage(pageReqVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty());

View File

@@ -14,7 +14,7 @@ import static com.luohuo.basic.utils.TimeUtils.DEFAULT_YEAR_FORMAT;
public class AiChatConversationPageReqVO extends PageParam {
@Schema(description = "用户编号", example = "1024")
private Long userId;
private Long uid;
@Schema(description = "对话标题", example = "你好")
private String title;

View File

@@ -98,5 +98,8 @@ public class AiChatConversationDO extends BaseDO {
* 上下文的最大 Message 数量
*/
private Integer maxContexts;
/**
* 租户id
*/
private Long tenantId;
}

View File

@@ -103,4 +103,8 @@ public class AiChatMessageDO extends BaseDO {
@TableField(typeHandler = LongListTypeHandler.class)
private List<Long> segmentIds;
/**
* 租户id
*/
private Long tenantId;
}

View File

@@ -27,7 +27,7 @@ public interface AiChatConversationMapper extends BaseMapperX<AiChatConversation
default PageResult<AiChatConversationDO> selectChatConversationPage(AiChatConversationPageReqVO pageReqVO) {
return selectPage(pageReqVO, new LambdaQueryWrapperX<AiChatConversationDO>()
.eqIfPresent(AiChatConversationDO::getUserId, pageReqVO.getUserId())
.eq(AiChatConversationDO::getUserId, pageReqVO.getUid())
.likeIfPresent(AiChatConversationDO::getTitle, pageReqVO.getTitle())
.betweenIfPresent(AiChatConversationDO::getCreateTime, pageReqVO.getCreateTime())
.orderByDesc(AiChatConversationDO::getId));

View File

@@ -3,6 +3,8 @@ package com.luohuo.flex.ai.service.chat;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import com.luohuo.basic.context.ContextUtil;
import com.luohuo.basic.tenant.core.aop.TenantIgnore;
import com.luohuo.flex.ai.common.pojo.PageResult;
import com.luohuo.flex.ai.controller.chat.vo.message.AiChatMessagePageReqVO;
import com.luohuo.flex.ai.controller.chat.vo.message.AiChatMessageRespVO;
@@ -139,8 +141,9 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
}
@Override
public Flux<R<AiChatMessageSendRespVO>> sendChatMessageStream(AiChatMessageSendReqVO sendReqVO,
Long userId) {
public Flux<R<AiChatMessageSendRespVO>> sendChatMessageStream(AiChatMessageSendReqVO sendReqVO, Long userId) {
ContextUtil.setIgnore(true);
// 1.1 校验对话存在
AiChatConversationDO conversation = chatConversationService
.validateChatConversationExists(sendReqVO.getConversationId());
@@ -195,10 +198,15 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
// 忽略租户,因为 Flux 异步无法透传租户
String content = contentBuffer.toString();
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(content));
ContextUtil.setIgnore(false);
}).doOnError(throwable -> {
log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable);
// 忽略租户,因为 Flux 异步无法透传租户
}).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR)));
ContextUtil.setIgnore(false);
}).onErrorResume(error -> {
ContextUtil.setIgnore(false);
return Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR));
});
}
private List<AiKnowledgeSegmentSearchRespBO> recallKnowledgeSegment(String content,

View File

@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.luohuo.flex</groupId>
<artifactId>luohuo-ai</artifactId>
<version>3.0.0</version>
</parent>
<artifactId>luohuo-ai-controller</artifactId>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.luohuo.flex</groupId>
<artifactId>luohuo-ai-biz</artifactId>
<version>${luohuo-project.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -17,7 +17,7 @@
<dependencies>
<dependency>
<groupId>com.luohuo.flex</groupId>
<artifactId>luohuo-ai-controller</artifactId>
<artifactId>luohuo-ai-biz</artifactId>
<version>${luohuo-project.version}</version>
</dependency>

View File

@@ -8,6 +8,7 @@ luohuo:
username: ${NACOS_USERNAME:@nacos.username@}
password: ${NACOS_PASSWORD:@nacos.password@}
web-port: ${NACOS_WEB_PORT:@nacos.web-port@}
local-ip: ${NACOS_LOCAL_IP:@nacos.local-ip@}
seata:
ip: ${SEATA_IP:@seata.ip@}
port: ${SEATA_PORT:@seata.port@}
@@ -53,6 +54,7 @@ spring:
username: ${luohuo.nacos.username}
password: ${luohuo.nacos.password}
server-addr: ${luohuo.nacos.ip}:${luohuo.nacos.port}
ip: ${luohuo.nacos.local-ip}
namespace: ${luohuo.nacos.namespace}
metadata: # 元数据,用于权限服务实时获取各个服务的所有接口
management.context-path: ${server.servlet.context-path:}${spring.mvc.servlet.path:}${management.endpoints.web.base-path:}

View File

@@ -20,7 +20,6 @@
</properties>
<modules>
<module>luohuo-ai-controller</module>
<module>luohuo-ai-entity</module>
<module>luohuo-ai-facade</module>
<module>luohuo-ai-server</module>

View File

@@ -2,7 +2,6 @@ package com.luohuo.flex.base.service.tenant.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
@@ -216,10 +215,9 @@ public class DefUserServiceImpl extends SuperCacheServiceImpl<DefUserManager, Lo
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean updatePassword(DefUserPasswordUpdateVO data) {
// ArgumentAssert.notEmpty(data.getOldPassword(), "请输入旧密码");
DefUser user = superManager.getUserByEmail(2, data.getEmail());
ArgumentAssert.notNull(user, "用户不存在");
ArgumentAssert.equals(user.getId(), ContextUtil.getUid(), "只能修改自己的密码");
// ArgumentAssert.notEmpty(data.getOldPassword(), "请输入旧密码");
// String oldPassword = SecureUtil.sha256(data.getOldPassword() + user.getSalt());
// ArgumentAssert.equals(user.getPassword(), oldPassword, "旧密码错误");
CacheKey cacheKey = new CaptchaCacheKeyBuilder().key(data.getEmail(), data.getKey());

View File

@@ -26,16 +26,16 @@ public class RedisKey {
*/
public static final String FEED_MEDIA = BASE_KEY + "feedMedia";
/**
* 用户徽章
*/
public static final String USER_ITEM = BASE_KEY + "userItem";
/**
* 朋友圈权限
*/
public static final String FEED_TARGET = BASE_KEY + "feedTarget";
/**
* 用户徽章
*/
public static final String USER_ITEM = BASE_KEY + "userItem";
/**
* DefUser用户信息
*/

View File

@@ -3,6 +3,7 @@ package com.luohuo.flex.im.core.chat.dao;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luohuo.flex.im.domain.entity.RoomGroup;
import com.luohuo.flex.im.core.chat.mapper.RoomGroupMapper;
import com.luohuo.flex.im.domain.vo.response.GroupResp;
import org.springframework.stereotype.Service;
import java.util.List;
@@ -30,6 +31,10 @@ public class RoomGroupDao extends ServiceImpl<RoomGroupMapper, RoomGroup> {
.one();
}
public GroupResp getByRoomIdIgnoreDel(Long roomId) {
return baseMapper.getByRoomIdIgnoreDel(roomId);
}
public boolean checkUser(Long uid, Long roomId) {
return baseMapper.checkUser(uid,roomId);
}

View File

@@ -1,6 +1,7 @@
package com.luohuo.flex.im.core.chat.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.luohuo.flex.im.domain.vo.response.GroupResp;
import org.springframework.stereotype.Repository;
import com.luohuo.flex.im.domain.entity.RoomGroup;
@@ -15,4 +16,6 @@ import com.luohuo.flex.im.domain.entity.RoomGroup;
public interface RoomGroupMapper extends BaseMapper<RoomGroup> {
boolean checkUser(Long uid, Long roomId);
GroupResp getByRoomIdIgnoreDel(Long roomId);
}

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.luohuo.flex.im.domain.vo.request.admin.AdminSetReq;
import com.luohuo.flex.im.domain.vo.request.contact.ContactAddReq;
import com.luohuo.flex.im.domain.vo.request.member.MemberExitReq;
import com.luohuo.flex.im.domain.vo.response.GroupResp;
import jakarta.validation.Valid;
import com.luohuo.flex.im.domain.vo.req.CursorPageBaseReq;
import com.luohuo.flex.im.domain.vo.res.CursorPageBaseResp;
@@ -30,6 +31,7 @@ import com.luohuo.flex.im.domain.vo.response.ChatRoomResp;
import com.luohuo.flex.im.domain.vo.response.MemberResp;
import com.luohuo.flex.im.domain.vo.req.MergeMessageReq;
import com.luohuo.flex.model.entity.ws.ChatMemberResp;
import java.util.List;
/**
@@ -55,7 +57,12 @@ public interface RoomAppService {
/**
* 获取群组信息
*/
MemberResp getGroupDetail(Long uid, long roomId);
MemberResp getGroupDetail(Long uid, Long roomId);
/**
* 获取群组基础信息
*/
GroupResp getGroupInfo(Long uid, Long roomId);
/**
* 获取群成员

View File

@@ -239,7 +239,7 @@ public class ChatServiceImpl implements ChatService {
}
// 2. 如果开启同步功能那么把最近N天的消息拉回去, 否则获取所有未ack的消息 [要排除屏蔽的房间的消息]
if(msgReq.getAsync()){
if(true || msgReq.getAsync()){
LocalDateTime effectiveStartTime = calculateStartTime(TimeUtils.getTime(LocalDateTime.now().minusDays(14)));
messages = messageDao.list(new LambdaQueryWrapper<Message>().in(Message::getRoomId, roomIds)
.between(Message::getCreateTime, effectiveStartTime, LocalDateTime.now()));

View File

@@ -31,6 +31,7 @@ import com.luohuo.flex.im.domain.vo.request.ChatMessageReq;
import com.luohuo.flex.im.domain.vo.request.admin.AdminSetReq;
import com.luohuo.flex.im.domain.vo.request.member.MemberExitReq;
import com.luohuo.flex.im.domain.entity.msg.TextMsgReq;
import com.luohuo.flex.im.domain.vo.response.GroupResp;
import com.luohuo.flex.model.entity.ws.AdminChangeDTO;
import com.luohuo.flex.model.enums.ChatActiveStatusEnum;
import com.luohuo.flex.im.domain.vo.request.contact.ContactAddReq;
@@ -39,6 +40,7 @@ import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Triple;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;
@@ -104,6 +106,7 @@ import com.luohuo.flex.im.core.user.service.impl.PushService;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -453,24 +456,6 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
}
}
/**
* 校验用户在群里的权限
*
* @param uid
* @return
*/
public GroupMember verifyGroupPermissions(Long uid, RoomGroup roomGroup) {
if (ObjectUtil.isNull(roomGroup)) {
throw new RuntimeException("群聊不存在!");
}
GroupMember groupMember = groupMemberDao.getMemberByGroupId(roomGroup.getId(), uid);
if (ObjectUtil.isNull(groupMember)) {
throw new RuntimeException(StrUtil.format("您不是{}的群成员!", roomGroup.getName()));
}
return groupMember;
}
/**
* 群主,管理员才可以修改
*
@@ -481,34 +466,55 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
@Override
public Boolean updateRoomInfo(Long uid, RoomInfoReq request) {
// 1.校验修改权限
RoomGroup roomGroup = roomGroupCache.get(request.getId());
GroupMember groupMember = verifyGroupPermissions(uid, roomGroup);
if (GroupRoleEnum.MEMBER.getType().equals(groupMember.getRoleId())) {
Triple<RoomGroup, GroupMember, Boolean> permissionCheck = checkGroupPermission(uid, request.getId());
if (!permissionCheck.getRight()) {
log.warn("用户无权限修改群信息uid:{}, roomId:{}", uid, request.getId());
return false;
}
RoomGroup roomGroup = permissionCheck.getLeft();
// 2.修改群信息
roomGroup.setAvatar(request.getAvatar());
roomGroup.setName(request.getName());
roomGroup.setAllowScanEnter(request.getAllowScanEnter());
Boolean success = roomService.updateRoomInfo(roomGroup);
// 3.通知群里所有人群信息修改了
if (success) {
roomGroupCache.delete(roomGroup.getId());
roomGroupCache.evictGroup(roomGroup.getAccount());
List<Long> memberUidList = groupMemberCache.getMemberExceptUidList(roomGroup.getRoomId());
pushService.sendPushMsg(RoomAdapter.buildRoomGroupChangeWS(roomGroup.getRoomId(), roomGroup.getName(), roomGroup.getAvatar()), memberUidList, uid);
}
// 3. 通知群里所有人群信息修改了
Boolean success = transactionTemplate.execute(status -> {
try {
boolean updateResult = roomService.updateRoomInfo(roomGroup);
if (!updateResult) {
status.setRollbackOnly();
return false;
}
// 4. 异步清理缓存
CompletableFuture.runAsync(() -> {
roomGroupCache.delete(roomGroup.getId());
roomGroupCache.evictGroup(roomGroup.getAccount());
});
List<Long> memberUidList = groupMemberCache.getMemberExceptUidList(roomGroup.getRoomId());
pushService.sendPushMsg(RoomAdapter.buildRoomGroupChangeWS(roomGroup.getRoomId(), roomGroup.getName(), roomGroup.getAvatar()), memberUidList, uid);
return true;
} catch (Exception e) {
status.setRollbackOnly();
throw e;
}
});
return success;
}
@Override
public Boolean updateMyRoomInfo(Long uid, RoomMyInfoReq request) {
// 1.校验修改权限
RoomGroup roomGroup = roomGroupCache.get(request.getId());
GroupMember member = verifyGroupPermissions(uid, roomGroup);
Triple<RoomGroup, GroupMember, Boolean> permissionCheck = checkGroupPermission(uid, request.getId());
if (!permissionCheck.getRight()) {
return false;
}
RoomGroup roomGroup = permissionCheck.getLeft();
GroupMember member = permissionCheck.getMiddle();
// 2.修改我的信息
boolean equals = member.getMyName().equals(StrUtil.isEmpty(request.getMyName()) ? "" : request.getMyName());
@@ -540,11 +546,36 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
return contactDao.updateById(contact);
}
/**
* 统一权限校验方法
* @return 返回三元组(roomGroup, groupMember, hasPermission)
*/
private Triple<RoomGroup, GroupMember, Boolean> checkGroupPermission(Long uid, Long roomId) {
RoomGroup roomGroup = roomGroupCache.get(roomId);
if (roomGroup == null) {
return Triple.of(null, null, false);
}
GroupMember groupMember = groupMemberCache.getMemberDetail(roomId, uid);
if (groupMember == null) {
return Triple.of(roomGroup, null, false);
}
boolean hasPermission = !GroupRoleEnum.MEMBER.getType().equals(groupMember.getRoleId());
return Triple.of(roomGroup, groupMember, hasPermission);
}
@Override
@Transactional
public Boolean pushAnnouncement(Long uid, AnnouncementsParam param) {
RoomGroup roomGroup = roomGroupCache.get(param.getRoomId());
List<Long> uids = roomService.getGroupUsers(roomGroup.getId(), false);
// 1. 权限校验
Triple<RoomGroup, GroupMember, Boolean> permissionCheck = checkGroupPermission(uid, param.getRoomId());
if (!permissionCheck.getRight()) {
return false;
}
// 2. 保存公告
List<Long> uids = roomService.getGroupUsers(permissionCheck.getLeft().getId(), false);
if (CollUtil.isNotEmpty(uids)) {
LocalDateTime now = LocalDateTime.now();
Announcements announcements = new Announcements();
@@ -579,7 +610,14 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
@Override
public Boolean announcementEdit(Long uid, AnnouncementsParam param) {
RoomGroup roomGroup = roomGroupCache.get(param.getRoomId());
// 1. 鉴权
Triple<RoomGroup, GroupMember, Boolean> permissionCheck = checkGroupPermission(uid, param.getRoomId());
if (!permissionCheck.getRight()) {
return false;
}
RoomGroup roomGroup = permissionCheck.getLeft();
// 2. 置顶公告
List<Long> uids = roomService.getGroupUsers(roomGroup.getId(), false);
if (CollUtil.isNotEmpty(uids)) {
AnnouncementsResp announcement = roomService.getAnnouncement(param.getId());
@@ -642,15 +680,14 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
}
@Override
@RedissonLock(prefixKey = "announceDel:", key = "#id")
public Boolean announcementDelete(Long uid, Long id) {
// 1. 鉴权
AnnouncementsResp resp = roomService.getAnnouncement(id);
Triple<RoomGroup, GroupMember, Boolean> validation = checkGroupPermission(uid, resp.getRoomId());
GroupMember groupMember = validation.getMiddle();
RoomGroup roomGroup = roomGroupCache.get(resp.getRoomId());
GroupMember groupMember = verifyGroupPermissions(uid, roomGroup);
long count = userBackpackDao.countByUidAndItemId(uid, "HuLa项目贡献者专属徽章");
long count = userBackpackDao.countByUidAndItemId(uid, DefValConstants.CONTRIBUTOR_ID);
if (count == 0 && GroupRoleEnum.MEMBER.getType().equals(groupMember.getRoleId())) {
return false;
}
@@ -716,8 +753,10 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
}
if (room.getType().equals(RoomTypeEnum.GROUP.getType())) {
RoomGroup sourceRoomGroup = roomGroupCache.get(req.getFromRoomId());
verifyGroupPermissions(uid, sourceRoomGroup);
Triple<RoomGroup, GroupMember, Boolean> permissionCheck = checkGroupPermission(uid, req.getFromRoomId());
if(ObjectUtil.isNull(permissionCheck.getMiddle())) {
throw new BizException("您不是群成员");
}
} else {
RoomFriend roomFriend = roomFriendCache.get(req.getFromRoomId());
if (ObjectUtil.isNull(roomFriend) || !roomFriend.getUid1().equals(uid) && !roomFriend.getUid1().equals(uid)) {
@@ -781,7 +820,7 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
}
@Override
public MemberResp getGroupDetail(Long uid, long roomId) {
public MemberResp getGroupDetail(Long uid, Long roomId) {
RoomGroup roomGroup = roomGroupCache.get(roomId);
Room room = roomCache.get(roomId);
AssertUtil.isNotEmpty(roomGroup, "roomId有误");
@@ -807,6 +846,11 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
.build();
}
@Override
public GroupResp getGroupInfo(Long uid, Long roomId) {
return roomGroupDao.getByRoomIdIgnoreDel(roomId);
}
@Override
public List<ChatMemberResp> listMember(MemberReq request) {
// 1. 基础校验
@@ -870,6 +914,7 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
public void delMember(Long uid, MemberDelReq request) {
Room room = roomCache.get(request.getRoomId());
AssertUtil.isNotEmpty(room, "房间号有误");
AssertUtil.isFalse(DefValConstants.DEF_ROOM_ID.equals(request.getRoomId()), "官方群聊无法移除");
RoomGroup roomGroup = roomGroupCache.get(request.getRoomId());
AssertUtil.isNotEmpty(roomGroup, "房间号有误");
GroupMember self = groupMemberDao.getMemberByGroupId(roomGroup.getId(), uid);
@@ -887,74 +932,77 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
}
// 1. 判断被移除的人是否是群主或者管理员 (群主不可以被移除,管理员只能被群主移除)
Long removedUid = request.getUid();
// 1.1 群主 非法操作
AssertUtil.isFalse(groupMemberDao.isLord(roomGroup.getId(), removedUid), GroupErrorEnum.NOT_ALLOWED_FOR_REMOVE, "");
// 1.2 管理员 判断是否是群主操作
if (groupMemberDao.isManager(roomGroup.getId(), removedUid)) {
Boolean isLord = groupMemberDao.isLord(roomGroup.getId(), uid);
AssertUtil.isTrue(isLord, GroupErrorEnum.NOT_ALLOWED_FOR_REMOVE);
}
// 1.3 普通成员 判断是否有权限操作
AssertUtil.isTrue(hasPower(self), GroupErrorEnum.NOT_ALLOWED_FOR_REMOVE);
GroupMember member = groupMemberDao.getMemberByGroupId(roomGroup.getId(), removedUid);
AssertUtil.isNotEmpty(member, "用户已经移除");
// 发送移除事件告知群成员
if (transactionTemplate.execute(e -> {
groupMemberDao.removeById(member.getId());
// 1.5 移除会话
contactDao.removeByRoomId(room.getId(), Collections.singletonList(request.getUid()));
return true;
})) {
// 移除群聊缓存
CacheKey uKey = PresenceCacheKeyBuilder.userGroupsKey(request.getUid());
cachePlusOps.sRem(membersKey, request.getUid());
cachePlusOps.sRem(uKey, room.getId());
asyncOnline(Arrays.asList(request.getUid()), room.getId(), false);
// 推送状态到前端
List<Long> memberUidList = groupMemberCache.getMemberExceptUidList(roomGroup.getRoomId());
if (!memberUidList.contains(request.getUid())) {
memberUidList.add(request.getUid());
request.getUidList().forEach(removedUid -> {
// 1.1 群主 非法操作
AssertUtil.isFalse(groupMemberDao.isLord(roomGroup.getId(), removedUid), GroupErrorEnum.NOT_ALLOWED_FOR_REMOVE, "");
// 1.2 管理员 判断是否是群主操作
if (groupMemberDao.isManager(roomGroup.getId(), removedUid)) {
Boolean isLord = groupMemberDao.isLord(roomGroup.getId(), uid);
AssertUtil.isTrue(isLord, GroupErrorEnum.NOT_ALLOWED_FOR_REMOVE);
}
WsBaseResp<WSMemberChange> ws = MemberAdapter.buildMemberRemoveWS(roomGroup.getRoomId(), (int) (memberNum - 1), Math.toIntExact(cachePlusOps.sCard(PresenceCacheKeyBuilder.onlineGroupMembersKey(room.getId()))), Arrays.asList(member.getUid()), WSMemberChange.CHANGE_TYPE_REMOVE);
pushService.sendPushMsg(ws, memberUidList, uid);
groupMemberCache.evictMemberList(room.getId());
groupMemberCache.evictExceptMemberList(room.getId());
groupMemberCache.evictMemberDetail(room.getId(), removedUid);
// 1.3 普通成员 判断是否有权限操作
AssertUtil.isTrue(hasPower(self), GroupErrorEnum.NOT_ALLOWED_FOR_REMOVE);
GroupMember member = groupMemberDao.getMemberByGroupId(roomGroup.getId(), removedUid);
AssertUtil.isNotEmpty(member, "用户已经移除");
long uuid = uidGenerator.getUid();
// 保存被删除人的通知
noticeService.createNotice(
RoomTypeEnum.GROUP,
NoticeTypeEnum.GROUP_MEMBER_DELETE,
uid,
removedUid,
uuid,
removedUid,
roomGroup.getRoomId(),
roomGroup.getName()
);
// 发送移除事件告知群成员
if (transactionTemplate.execute(e -> {
groupMemberDao.removeById(member.getId());
// 1.5 移除会话
contactDao.removeByRoomId(room.getId(), Collections.singletonList(removedUid));
return true;
})) {
// 移除群聊缓存
CacheKey uKey = PresenceCacheKeyBuilder.userGroupsKey(removedUid);
cachePlusOps.sRem(membersKey, removedUid);
cachePlusOps.sRem(uKey, room.getId());
asyncOnline(Arrays.asList(removedUid), room.getId(), false);
// 获取所有管理员
List<Long> managerIds = groupMemberDao.getGroupUsers(roomGroup.getId(), true);
managerIds.forEach(managerId -> noticeService.createNotice(
RoomTypeEnum.GROUP,
NoticeTypeEnum.GROUP_MEMBER_DELETE,
uid,
managerId,
uuid,
removedUid,
roomGroup.getRoomId(),
roomGroup.getName()
));
}
// 推送状态到前端
List<Long> memberUidList = groupMemberCache.getMemberExceptUidList(roomGroup.getRoomId());
if (!memberUidList.contains(removedUid)) {
memberUidList.add(removedUid);
}
WsBaseResp<WSMemberChange> ws = MemberAdapter.buildMemberRemoveWS(roomGroup.getRoomId(), (int) (memberNum - 1), Math.toIntExact(cachePlusOps.sCard(PresenceCacheKeyBuilder.onlineGroupMembersKey(room.getId()))), Arrays.asList(member.getUid()), WSMemberChange.CHANGE_TYPE_REMOVE);
pushService.sendPushMsg(ws, memberUidList, uid);
groupMemberCache.evictMemberList(room.getId());
groupMemberCache.evictExceptMemberList(room.getId());
groupMemberCache.evictMemberDetail(room.getId(), removedUid);
long uuid = uidGenerator.getUid();
// 保存被删除人的通知
noticeService.createNotice(
RoomTypeEnum.GROUP,
NoticeTypeEnum.GROUP_MEMBER_DELETE,
uid,
removedUid,
uuid,
removedUid,
roomGroup.getRoomId(),
roomGroup.getName()
);
// 获取所有管理员
List<Long> managerIds = groupMemberDao.getGroupUsers(roomGroup.getId(),true);
managerIds.forEach(managerId -> noticeService.createNotice(
RoomTypeEnum.GROUP,
NoticeTypeEnum.GROUP_MEMBER_DELETE,
uid,
managerId,
uuid,
removedUid,
roomGroup.getRoomId(),
roomGroup.getName()
));
}
});
}
@Override
@RedissonLock(key = "#request.roomId")
public void addMember(Long uid, MemberAddReq request) {
HashSet<Long> inviteUidList = request.getUidList();
AssertUtil.isNotEmpty(inviteUidList.contains(DefValConstants.DEF_BOT_ID), "不能拉小管家进群!");
// 1. 校验数据
Room room = roomCache.get(request.getRoomId());
AssertUtil.isNotEmpty(room, "房间号有误");
@@ -963,20 +1011,19 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
GroupMember self = groupMemberDao.getMemberByGroupId(roomGroup.getId(), uid);
AssertUtil.isNotEmpty(self, "您不是群成员");
// 已经进群了的
List<Long> memberBatch = groupMemberDao.getMemberBatch(roomGroup.getId(), request.getUidList()).stream().map(GroupMember::getUid).toList();
List<Long> memberBatch = groupMemberDao.getMemberBatch(roomGroup.getId(), inviteUidList).stream().map(GroupMember::getUid).toList();
// 已经邀请过的数据
List<Long> existingUsers = userApplyDao.getExistingUsers(request.getRoomId(), request.getUidList());
HashSet<Long> validUidSet = request.getUidList();
validUidSet.removeAll(memberBatch);
validUidSet.removeAll(existingUsers);
List<Long> existingUsers = userApplyDao.getExistingUsers(request.getRoomId(), inviteUidList);
inviteUidList.removeAll(memberBatch);
inviteUidList.removeAll(existingUsers);
List<Long> validUids = new ArrayList<>(validUidSet);
List<Long> validUids = new ArrayList<>(inviteUidList);
if (CollectionUtils.isEmpty(validUids)) {
return;
}
// 2. 创建邀请记录
List<UserApply> invites = validUids.stream().map(inviteeUid -> new UserApply(uid, RoomTypeEnum.GROUP.getType(), roomGroup.getRoomId(), inviteeUid, StrUtil.format("{}邀请你加入{}", userSummaryCache.get(uid).getName(), roomGroup.getName()), ApplyStatusEnum.WAIT_APPROVAL.getCode(), UNREAD.getCode(), 0, false)).collect(Collectors.toList());
List<UserApply> invites = validUids.stream().map(inviteeUid -> new UserApply(uid, RoomTypeEnum.GROUP.getType(), roomGroup.getRoomId(), inviteeUid, StrUtil.format("{}邀请你加入{}", userSummaryCache.get(uid).getName(), roomGroup.getName()), NoticeStatusEnum.UNTREATED.getStatus(), UNREAD.getCode(), 0, false)).collect(Collectors.toList());
transactionTemplate.execute(e -> userApplyDao.saveBatch(invites));
// 3. 通知被邀请的人进群, 通知时绑定通知id
@@ -1122,23 +1169,23 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
@Override
@RedissonLock(prefixKey = "addGroup:", key = "#uid")
public Long addGroup(Long uid, GroupAddReq request) {
Map<Long, SummeryInfoDTO> userMap = userSummaryCache.getBatch(request.getUidList());
AssertUtil.isTrue(userMap.size() > 1, "群聊人数应大于2人");
Map<Long, SummeryInfoDTO> inviteUserMap = userSummaryCache.getBatch(request.getUidList());
inviteUserMap.remove(DefValConstants.DEF_BOT_ID);
AssertUtil.isTrue(inviteUserMap.size() > 1, "群聊人数应大于2人");
userMap.remove(DefValConstants.DEF_BOT_ID);
List<Long> uidList = new ArrayList<>(userMap.keySet());
List<Long> inviteUidList = new ArrayList<>(inviteUserMap.keySet());
AtomicReference<Long> roomIdAtomic = new AtomicReference(0L);
// 创建群组数据并推送数据到前端
if (transactionTemplate.execute(e -> {
RoomGroup roomGroup = roomService.createGroupRoom(uid, request);
// 批量保存群成员
List<GroupMember> groupMembers = RoomAdapter.buildGroupMemberBatch(uidList, roomGroup.getId());
List<GroupMember> groupMembers = RoomAdapter.buildGroupMemberBatch(inviteUidList, roomGroup.getId());
groupMemberDao.saveBatch(groupMembers);
// 添加所有人的会话
uidList.add(uid);
for (Long memberId : uidList) {
inviteUidList.add(uid);
for (Long memberId : inviteUidList) {
contactDao.refreshOrCreate(roomGroup.getRoomId(), memberId);
}
@@ -1154,11 +1201,11 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
// 更新在线缓存
CacheKey onlineGroupMembersKey = PresenceCacheKeyBuilder.onlineGroupMembersKey(roomIdAtomic.get());
CacheKey gKey = PresenceCacheKeyBuilder.groupMembersKey(roomIdAtomic.get());
uidList.forEach(id -> {
inviteUidList.forEach(id -> {
cachePlusOps.sAdd(gKey, id);
cachePlusOps.sAdd(PresenceCacheKeyBuilder.userGroupsKey(id), roomIdAtomic.get());
});
asyncOnline(uidList, roomIdAtomic.get(), true);
asyncOnline(inviteUidList, roomIdAtomic.get(), true);
SpringUtils.publishEvent(new GroupMemberAddEvent(this, roomIdAtomic.get(), Math.toIntExact(cachePlusOps.sCard(gKey)), Math.toIntExact(cachePlusOps.sCard(onlineGroupMembersKey)), request.getUidList(), uid));
}
return roomIdAtomic.get();

View File

@@ -131,9 +131,13 @@ public class RoomServiceImpl implements RoomService {
@Override
public AnnouncementsResp getAnnouncement(Long id) {
AnnouncementsResp resp = new AnnouncementsResp();
BeanUtils.copyProperties(announcementsDao.getById(id), resp);
User user = userCache.get(resp.getUid());
resp.setUName(user.getName());
Announcements announcements = announcementsDao.getById(id);
resp.setUid(announcements.getUid());
resp.setRoomId(announcements.getRoomId());
resp.setTop(announcements.getTop());
resp.setContent(announcements.getContent());
resp.setCreateTime(announcements.getCreateTime());
resp.setId(announcements.getId());
return resp;
}

View File

@@ -4,10 +4,13 @@ import com.luohuo.basic.context.ContextUtil;
import com.luohuo.basic.utils.SpringUtils;
import com.luohuo.basic.utils.TimeUtils;
import com.luohuo.flex.im.core.chat.service.cache.GroupMemberCache;
import com.luohuo.flex.im.core.chat.service.cache.RoomCache;
import com.luohuo.flex.im.core.user.service.cache.UserSummaryCache;
import com.luohuo.flex.im.domain.dto.SummeryInfoDTO;
import com.luohuo.flex.im.domain.entity.GroupMember;
import com.luohuo.flex.im.domain.entity.Room;
import com.luohuo.flex.im.domain.enums.GroupRoleEnum;
import com.luohuo.flex.im.domain.enums.RoomTypeEnum;
import com.luohuo.flex.im.vo.result.tenant.MsgRecallVo;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
@@ -31,6 +34,7 @@ import java.util.Objects;
@AllArgsConstructor
public class RecallMsgHandler extends AbstractMsgHandler<Object> {
private RoomCache roomCache;
private MessageDao messageDao;
private UserSummaryCache userSummaryCache;
private GroupMemberCache groupMemberCache;
@@ -52,8 +56,14 @@ public class RecallMsgHandler extends AbstractMsgHandler<Object> {
Long senderUid = msg.getFromUid();
Long currentUserUid = ContextUtil.getUid();
String roleName = "";
Room room = roomCache.get(msg.getRoomId());
if(room.getType().equals(RoomTypeEnum.GROUP.getType())){
GroupMember recallerMember = groupMemberCache.getMemberDetail(msg.getRoomId(), recallerUid);
roleName = GroupRoleEnum.get(recallerMember.getRoleId());
}
// 获取撤回者的群成员信息和用户信息
GroupMember recallerMember = groupMemberCache.getMemberDetail(msg.getRoomId(), recallerUid);
SummeryInfoDTO recallerUserInfo = userSummaryCache.get(recallerUid);
// 获取被撤回消息发送者的用户信息(用于显示成员名称)
@@ -63,7 +73,7 @@ public class RecallMsgHandler extends AbstractMsgHandler<Object> {
boolean isRecallerCurrentUser = Objects.equals(recallerUid, currentUserUid);
boolean isSenderCurrentUser = Objects.equals(senderUid, currentUserUid);
String roleName = GroupRoleEnum.get(recallerMember.getRoleId());
String messageText;
if (isRecallerCurrentUser) {

View File

@@ -18,13 +18,13 @@ public class FeedMediaDao extends ServiceImpl<FeedMediaMapper, FeedMedia> {
/**
* 批量添加朋友圈的资源的数据
* @param feedId 朋友圈id
* @param urls 素材地址
* @param images 素材地址
* @param type 0 纯文字 1 图片 2 视频
*/
public List<FeedMedia> batchSaveMedia(Long feedId, List<String> urls, Integer type){
public List<FeedMedia> batchSaveMedia(Long feedId, List<String> images, Integer type){
List<FeedMedia> feedMediaList = new ArrayList<>();
for (int i = 0; i < urls.size(); i++) {
String url = urls.get(i);
for (int i = 0; i < images.size(); i++) {
String url = images.get(i);
FeedMedia feedMedia = new FeedMedia();
feedMedia.setSort(i);
feedMedia.setFeedId(feedId);

View File

@@ -1,6 +1,5 @@
package com.luohuo.flex.im.core.user.dao;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luohuo.flex.im.domain.entity.ItemConfig;
import com.luohuo.flex.im.core.user.mapper.ItemConfigMapper;
@@ -23,9 +22,4 @@ public class ItemConfigDao extends ServiceImpl<ItemConfigMapper, ItemConfig> {
.eq(ItemConfig::getType, type)
.list();
}
public ItemConfig getByDesc(String desc) {
return baseMapper.selectOne(new LambdaQueryWrapper<ItemConfig>()
.eq(ItemConfig::getDescribe, desc));
}
}

View File

@@ -62,10 +62,11 @@ public class NoticeDao extends ServiceImpl<NoticeMapper, Notice> {
return wsNotice;
}
public void readNotices(Long uid, List<Long> notices) {
public void readNotices(Long uid, Integer type, List<Long> notices) {
lambdaUpdate()
.set(Notice::getIsRead, READ.getCode())
.eq(Notice::getIsRead, UNREAD.getCode())
.eq(Notice::getType, type)
.in(Notice::getId, notices)
.eq(Notice::getReceiverId, uid)
.update();

View File

@@ -3,7 +3,7 @@ package com.luohuo.flex.im.core.user.dao;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luohuo.flex.im.domain.entity.UserApply;
import com.luohuo.flex.im.domain.enums.ApplyDeletedEnum;
import com.luohuo.flex.im.domain.enums.ApplyStatusEnum;
import com.luohuo.flex.im.domain.enums.NoticeStatusEnum;
import com.luohuo.flex.im.core.user.mapper.UserApplyMapper;
import com.luohuo.flex.im.domain.enums.RoomTypeEnum;
import org.springframework.stereotype.Service;
@@ -13,7 +13,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.stream.Collectors;
import static com.luohuo.flex.im.domain.enums.ApplyStatusEnum.AGREE;
import static com.luohuo.flex.im.domain.enums.NoticeStatusEnum.ACCEPTED;
/**
* <p>
@@ -35,7 +35,7 @@ public class UserApplyDao extends ServiceImpl<UserApplyMapper, UserApply> {
public UserApply getFriendApproving(Long uid, Long targetUid, boolean initiator) {
return lambdaQuery().eq(UserApply::getUid, uid)
.eq(UserApply::getTargetId, targetUid)
.eq(UserApply::getStatus, ApplyStatusEnum.WAIT_APPROVAL.getCode())
.eq(UserApply::getStatus, NoticeStatusEnum.UNTREATED.getStatus())
.eq(UserApply::getType, RoomTypeEnum.FRIEND.getType())
.notIn(initiator,UserApply::getDeleted, ApplyDeletedEnum.applyDeleted())
.notIn(!initiator,UserApply::getDeleted, ApplyDeletedEnum.targetDeleted())
@@ -45,13 +45,13 @@ public class UserApplyDao extends ServiceImpl<UserApplyMapper, UserApply> {
public void agree(Long applyId) {
lambdaUpdate()
.eq(UserApply::getId, applyId)
.set(UserApply::getStatus, AGREE.getCode())
.set(UserApply::getStatus, ACCEPTED.getStatus())
.set(UserApply::getUpdateTime, LocalDateTime.now())
.update();
}
public void updateStatus(Long applyId, ApplyStatusEnum statusEnum) {
lambdaUpdate().set(UserApply::getStatus, statusEnum.getCode())
public void updateStatus(Long applyId, NoticeStatusEnum statusEnum) {
lambdaUpdate().set(UserApply::getStatus, statusEnum.getStatus())
.set(UserApply::getUpdateTime, LocalDateTime.now())
.eq(UserApply::getId,applyId)
.update();
@@ -67,7 +67,7 @@ public class UserApplyDao extends ServiceImpl<UserApplyMapper, UserApply> {
public List<Long> getExistingUsers(Long roomId, HashSet<Long> uidList) {
return lambdaQuery()
.eq(UserApply::getRoomId, roomId)
.eq(UserApply::getStatus, ApplyStatusEnum.WAIT_APPROVAL.getCode())
.eq(UserApply::getStatus, NoticeStatusEnum.UNTREATED.getStatus())
.in(UserApply::getTargetId, uidList)
.list().stream().map(UserApply::getTargetId).collect(Collectors.toList());
}

View File

@@ -3,7 +3,6 @@ package com.luohuo.flex.im.core.user.dao;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luohuo.flex.im.domain.entity.ItemConfig;
import org.springframework.stereotype.Service;
import com.luohuo.flex.im.common.enums.YesOrNoEnum;
import com.luohuo.flex.im.domain.entity.UserBackpack;
@@ -67,11 +66,10 @@ public class UserBackpackDao extends ServiceImpl<UserBackpackMapper, UserBackpac
return lambdaQuery().eq(UserBackpack::getIdempotent, idempotent).one();
}
public long countByUidAndItemId(Long uid, String desc) {
ItemConfig itemConfig = itemConfigDao.getByDesc(desc);
public long countByUidAndItemId(Long uid, Long itemId) {
return baseMapper.selectCount(new LambdaQueryWrapper<UserBackpack>()
.eq(UserBackpack::getUid, uid)
.eq(UserBackpack::getItemId, itemConfig.getId())
.eq(UserBackpack::getItemId, itemId)
);
}
}

View File

@@ -3,7 +3,7 @@ package com.luohuo.flex.im.core.user.service;
import com.luohuo.flex.im.domain.entity.Notice;
import com.luohuo.flex.im.domain.enums.NoticeTypeEnum;
import com.luohuo.flex.im.domain.enums.RoomTypeEnum;
import com.luohuo.flex.im.domain.vo.req.PageBaseReq;
import com.luohuo.flex.im.domain.vo.req.NoticeReq;
import com.luohuo.flex.im.domain.vo.res.NoticeVO;
import com.luohuo.flex.im.domain.vo.res.PageBaseResp;
import com.luohuo.flex.model.entity.ws.WSNotice;
@@ -44,7 +44,7 @@ public interface NoticeService {
/**
* 获取用户通知
*/
PageBaseResp<NoticeVO> getUserNotices(Long userId, PageBaseReq request);
PageBaseResp<NoticeVO> getUserNotices(Long userId, NoticeReq request);
/**
* 标记为已读

View File

@@ -12,7 +12,7 @@ import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import static com.luohuo.flex.im.domain.enums.ApplyReadStatusEnum.UNREAD;
import static com.luohuo.flex.im.domain.enums.ApplyStatusEnum.WAIT_APPROVAL;
import static com.luohuo.flex.im.domain.enums.NoticeStatusEnum.UNTREATED;
/**
@@ -28,7 +28,7 @@ public class FriendAdapter {
userApplyNew.setMsg(request.getMsg());
userApplyNew.setType(RoomTypeEnum.FRIEND.getType());
userApplyNew.setTargetId(request.getTargetUid());
userApplyNew.setStatus(WAIT_APPROVAL.getCode());
userApplyNew.setStatus(UNTREATED.getStatus());
userApplyNew.setReadStatus(UNREAD.getCode());
return userApplyNew;
}

View File

@@ -40,9 +40,8 @@ import com.luohuo.flex.im.domain.entity.RoomGroup;
import com.luohuo.flex.im.domain.entity.UserApply;
import com.luohuo.flex.im.domain.entity.UserFriend;
import com.luohuo.flex.im.domain.enums.ApplyDeletedEnum;
import com.luohuo.flex.im.domain.enums.ApplyStatusEnum;
import com.luohuo.flex.im.domain.enums.GroupRoleEnum;
import com.luohuo.flex.im.domain.enums.NoticeStatusEnum;
import com.luohuo.flex.im.domain.enums.GroupRoleEnum;
import com.luohuo.flex.im.domain.enums.NoticeTypeEnum;
import com.luohuo.flex.im.domain.enums.RoomTypeEnum;
import com.luohuo.flex.im.domain.vo.req.friend.FriendApplyReq;
@@ -59,8 +58,7 @@ import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import static com.luohuo.flex.im.domain.enums.ApplyStatusEnum.AGREE;
import static com.luohuo.flex.im.domain.enums.ApplyStatusEnum.WAIT_APPROVAL;
import static com.luohuo.flex.im.domain.enums.NoticeStatusEnum.*;
/**
* 好友申请、群聊邀请
@@ -107,7 +105,7 @@ public class ApplyServiceImpl implements ApplyService {
// 是否有待审批的申请记录(别人请求自己的)
UserApply friendApproving = userApplyDao.getFriendApproving(request.getTargetUid(), uid, false);
if (Objects.nonNull(friendApproving)) {
handlerApply(uid, new ApplyReq(friendApproving.getId(), AGREE.getCode()));
handlerApply(uid, new ApplyReq(friendApproving.getId(), ACCEPTED.getStatus()));
return null;
}
// 申请入库
@@ -204,11 +202,11 @@ public class ApplyServiceImpl implements ApplyService {
throw new BizException("无效的邀请");
}
if (request.getState().equals(WAIT_APPROVAL.getCode())) {
if (request.getState().equals(UNTREATED.getStatus())) {
throw new BizException("无效的审批状态");
}
if (!invite.getStatus().equals(WAIT_APPROVAL.getCode())) {
if (!invite.getStatus().equals(UNTREATED.getStatus())) {
throw new BizException("无效的审批");
}
@@ -217,18 +215,18 @@ public class ApplyServiceImpl implements ApplyService {
throw new BizException("通知无法处理");
}
switch (request.getState()){
case 0 -> {
userApplyDao.updateStatus(request.getApplyId(), ApplyStatusEnum.REJECT);
switch (NoticeStatusEnum.get(request.getState())){
case REJECTED -> {
userApplyDao.updateStatus(request.getApplyId(), NoticeStatusEnum.REJECTED);
// 发送通知
notice.setStatus(NoticeStatusEnum.REJECTED.getStatus());
noticeService.updateNotice(notice);
}
case 2 -> {
case ACCEPTED -> {
// 处理加好友
invite.setStatus(request.getState());
if(invite.getType().equals(RoomTypeEnum.FRIEND.getType())){
AssertUtil.equal(invite.getStatus(), AGREE.getCode(), "已同意好友申请");
AssertUtil.equal(invite.getStatus(), ACCEPTED.getStatus(), "已同意好友申请");
// 同意申请
AtomicReference<Long> atomicRoomId = new AtomicReference(0L);
AtomicReference<Boolean> atomicIsFromTempSession = new AtomicReference(false);
@@ -320,10 +318,10 @@ public class ApplyServiceImpl implements ApplyService {
SpringUtils.publishEvent(new GroupMemberAddEvent(this, room.getId(), Math.toIntExact(cachePlusOps.sCard(gKey)), Math.toIntExact(cachePlusOps.sCard(onlineGroupMembersKey)), Arrays.asList(infoUid), uid));
}
}
case 3 -> {
case IGNORE -> {
checkRecord(request);
userApplyDao.updateStatus(request.getApplyId(), ApplyStatusEnum.IGNORE);
notice.setStatus(NoticeStatusEnum.IGNORE.getStatus());
userApplyDao.updateStatus(request.getApplyId(), IGNORE);
notice.setStatus(IGNORE.getStatus());
noticeService.updateNotice(notice);
}
}
@@ -354,7 +352,7 @@ public class ApplyServiceImpl implements ApplyService {
private UserApply checkRecord(ApplyReq request) {
UserApply userApply = userApplyDao.getById(request.getApplyId());
AssertUtil.isNotEmpty(userApply, "不存在申请记录");
AssertUtil.equal(userApply.getStatus(), WAIT_APPROVAL.getCode(), "对方已是您的好友");
AssertUtil.equal(userApply.getStatus(), UNTREATED.getStatus(), "对方已是您的好友");
return userApply;
}
}

View File

@@ -8,7 +8,7 @@ import com.luohuo.basic.cache.repository.CachePlusOps;
import com.luohuo.basic.exception.BizException;
import com.luohuo.basic.model.cache.CacheHashKey;
import com.luohuo.flex.common.cache.common.FeedMediaRelCacheKeyBuilder;
import com.luohuo.flex.im.common.constant.RedisKey;
import com.luohuo.flex.common.cache.common.FeedTargetRelCacheKeyBuilder;
import com.luohuo.flex.im.domain.vo.req.CursorPageBaseReq;
import com.luohuo.flex.im.domain.vo.res.CursorPageBaseResp;
import com.luohuo.flex.im.core.chat.service.adapter.MemberAdapter;
@@ -68,12 +68,9 @@ public class FeedServiceImpl implements FeedService {
CacheHashKey hashKey = FeedMediaRelCacheKeyBuilder.build(feed.getId());
CacheResult<List<FeedMedia>> result = cachePlusOps.get(hashKey, t -> feedMediaDao.getMediaByFeedId(feed.getId()));
List<FeedMedia> mediaList = result.getValue();
if(CollUtil.isEmpty(mediaList)){
mediaList = feedMediaDao.getMediaByFeedId(feed.getId());
cachePlusOps.hSet(hashKey, mediaList);
if(CollUtil.isNotEmpty(mediaList)){
feedVo.setUrls(mediaList.stream().sorted(Comparator.comparingInt(FeedMedia::getSort)).map(FeedMedia::getUrl).collect(Collectors.toList()));
}
feedVo.setUrls(mediaList.stream().sorted(Comparator.comparingInt(FeedMedia::getSort)).map(FeedMedia::getUrl).collect(Collectors.toList()));
}
feedVos.add(feedVo);
}
@@ -122,7 +119,6 @@ public class FeedServiceImpl implements FeedService {
public void saveFeed(FeedParam param, Long uid, Feed feed) {
List<Long> pushList = new ArrayList<>();
List<FeedTarget> feedTargets = new ArrayList<>();
List<FeedMedia> mediaList = new ArrayList<>();
switch (FeedPermissionEnum.get(param.getPermission())){
case open -> {
// 1. 查询所有好友,排除【不让他看我, 他不看我】的好友
@@ -169,19 +165,17 @@ public class FeedServiceImpl implements FeedService {
switch (FeedEnum.get(param.getMediaType())){
case WORD -> log.info("发布了一条纯文字朋友圈~~");
case IMAGE, VIDEO -> {
List<String> urls = param.getUrls();
if (CollUtil.isEmpty(urls)){
List<String> images = param.getImages();
if (CollUtil.isEmpty(images)){
throw new RuntimeException("请至少上传一条素材!");
}
mediaList = feedMediaDao.batchSaveMedia(feed.getId(), urls, param.getMediaType());
feedMediaDao.batchSaveMedia(feed.getId(), images, param.getMediaType());
}
}
// 3. 缓存权限+素材 告知 pushList 我发布了朋友圈
cachePlusOps.hDel(RedisKey.FEED_MEDIA, feed.getId());
cachePlusOps.hDel(RedisKey.FEED_TARGET, feed.getId());
cachePlusOps.hDel(RedisKey.FEED_MEDIA, feed.getId().toString(), mediaList);
cachePlusOps.hDel(RedisKey.FEED_TARGET, feed.getId().toString(), feedTargets);
// cachePlusOps.hDel(FeedMediaRelCacheKeyBuilder.build(feed.getId()));
// cachePlusOps.hDel(FeedTargetRelCacheKeyBuilder.build(feed.getId()));
pushService.sendPushMsg(MemberAdapter.buildFeedPushWS(uid), pushList, uid);
}
@@ -236,8 +230,8 @@ public class FeedServiceImpl implements FeedService {
feedDao.removeById(feedId);
// 2. 清空缓存
cachePlusOps.hDel(RedisKey.FEED_TARGET, feedId);
cachePlusOps.hDel(RedisKey.FEED_MEDIA, feedId);
cachePlusOps.hDel(FeedTargetRelCacheKeyBuilder.build(feedId));
cachePlusOps.hDel(FeedMediaRelCacheKeyBuilder.build(feedId));
return true;
}
@@ -284,11 +278,8 @@ public class FeedServiceImpl implements FeedService {
// 处理朋友圈权限
if(feedVo.getPermission().equals(FeedPermissionEnum.partVisible.getType()) || feedVo.getPermission().equals(FeedPermissionEnum.notAnyone.getType())){
CacheResult<Object> cacheResult = cachePlusOps.hGet(FeedMediaRelCacheKeyBuilder.build(feedId));
CacheResult<Object> cacheResult = cachePlusOps.hGet(FeedTargetRelCacheKeyBuilder.build(feedId), t -> feedTargetDao.selectFeedTargets(feedId));
List<FeedTarget> feedTargets = cacheResult.asList();
if(CollUtil.isEmpty(feedTargets)){
feedTargets = feedTargetDao.selectFeedTargets(feedId);
}
List<Long> taggetList = feedTargets.stream().filter(item -> item.getType().equals(1)).map(FeedTarget::getTargetId).collect(Collectors.toUnmodifiableList());
List<Long> userList = feedTargets.stream().filter(item -> item.getType().equals(2)).map(FeedTarget::getTargetId).collect(Collectors.toUnmodifiableList());

View File

@@ -13,7 +13,7 @@ import com.luohuo.flex.common.constant.DefValConstants;
import com.luohuo.flex.im.core.user.service.cache.UserSummaryCache;
import com.luohuo.flex.im.domain.dto.SummeryInfoDTO;
import com.luohuo.flex.im.domain.enums.ApplyReadStatusEnum;
import com.luohuo.flex.im.domain.enums.ApplyStatusEnum;
import com.luohuo.flex.im.domain.enums.NoticeStatusEnum;
import com.luohuo.flex.model.entity.WSRespTypeEnum;
import com.luohuo.flex.model.entity.WsBaseResp;
import jakarta.annotation.PostConstruct;
@@ -137,7 +137,7 @@ public class FriendServiceImpl implements FriendService, InitializingBean {
userApply.setType(type);
userApply.setRoomId(roomId);
userApply.setTargetId(targetId);
userApply.setStatus(ApplyStatusEnum.WAIT_APPROVAL.getCode());
userApply.setStatus(NoticeStatusEnum.UNTREATED.getStatus());
userApply.setReadStatus(ApplyReadStatusEnum.UNREAD.getCode());
userApply.setApplyFor(true);
userApplyDao.save(userApply);

View File

@@ -12,7 +12,7 @@ import com.luohuo.flex.im.domain.entity.Notice;
import com.luohuo.flex.im.domain.enums.NoticeStatusEnum;
import com.luohuo.flex.im.domain.enums.NoticeTypeEnum;
import com.luohuo.flex.im.domain.enums.RoomTypeEnum;
import com.luohuo.flex.im.domain.vo.req.PageBaseReq;
import com.luohuo.flex.im.domain.vo.req.NoticeReq;
import com.luohuo.flex.im.domain.vo.res.NoticeVO;
import com.luohuo.flex.im.domain.vo.res.PageBaseResp;
import com.luohuo.flex.model.entity.WSRespTypeEnum;
@@ -89,21 +89,21 @@ public class NoticeServiceImpl implements NoticeService {
}
}
private void readNotices(Long uid, IPage<Notice> noticeIPage) {
private void readNotices(Long uid, String applyType, IPage<Notice> noticeIPage) {
List<Long> notices = noticeIPage.getRecords()
.stream().map(Notice::getId)
.collect(Collectors.toList());
if(CollUtil.isNotEmpty(notices)){
noticeDao.readNotices(uid, notices);
noticeDao.readNotices(uid, "friend".equals(applyType)? 2: 1, notices);
}
}
@Override
public PageBaseResp<NoticeVO> getUserNotices(Long uid, PageBaseReq request) {
public PageBaseResp<NoticeVO> getUserNotices(Long uid, NoticeReq request) {
IPage<Notice> noticeIPage = noticeDao.getUserNotices(uid, true, request.plusPage());
// 将这些通知设为已读
if(request.getClick()){
readNotices(uid, noticeIPage);
readNotices(uid, request.getApplyType(), noticeIPage);
}
return PageBaseResp.init(noticeIPage, noticeIPage.getRecords().stream().map(notice -> convertToVO(notice)).collect(Collectors.toList()));
}

View File

@@ -9,6 +9,10 @@
on g.id = m.group_id
and g.room_id = #{roomId}
and m.uid = #{uid}
limit 1
where g.is_del = 0 limit 1
</select>
<select id="getByRoomIdIgnoreDel" resultType="com.luohuo.flex.im.domain.vo.response.GroupResp">
select id as group_id, room_id, account, name, avatar from im_room_group where room_id = #{roomId} limit 1
</select>
</mapper>

View File

@@ -4,7 +4,7 @@ import com.luohuo.basic.base.R;
import com.luohuo.basic.context.ContextUtil;
import com.luohuo.flex.im.core.user.service.NoticeService;
import com.luohuo.flex.im.domain.vo.req.NoticeReadReq;
import com.luohuo.flex.im.domain.vo.req.PageBaseReq;
import com.luohuo.flex.im.domain.vo.req.NoticeReq;
import com.luohuo.flex.im.domain.vo.res.NoticeVO;
import com.luohuo.flex.im.domain.vo.res.PageBaseResp;
import com.luohuo.flex.model.entity.ws.WSNotice;
@@ -24,7 +24,7 @@ public class NoticeController {
private NoticeService noticeService;
@GetMapping("/page")
public R<PageBaseResp<NoticeVO>> getNotices(@Valid PageBaseReq request) {
public R<PageBaseResp<NoticeVO>> getNotices(@Valid NoticeReq request) {
Long uid = ContextUtil.getUid();
return R.success(noticeService.getUserNotices(uid, request));
}

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.luohuo.flex.im.domain.vo.req.IdReqVO;
import com.luohuo.flex.im.domain.vo.res.IdRespVO;
import com.luohuo.flex.im.domain.vo.response.GroupResp;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
@@ -47,13 +48,20 @@ public class RoomController {
@Resource
private RoomAppService roomService;
@GetMapping("/group")
@GetMapping("/group/detail")
@Operation(summary ="群组详情")
public R<MemberResp> groupDetail(@Valid IdReqVO request) {
Long uid = ContextUtil.getUid();
return R.success(roomService.getGroupDetail(uid, request.getId()));
}
@GetMapping("/group/info")
@Operation(summary ="群组基础信息[没加群的人也可以查]")
public R<GroupResp> groupInfo(@Valid IdReqVO request) {
Long uid = ContextUtil.getUid();
return R.success(roomService.getGroupInfo(uid, request.getId()));
}
@GetMapping("search")
@Operation(summary = "查找群聊")
public R<List<RoomGroup>> searchGroup(@Valid RoomGroupReq req) {

View File

@@ -1,5 +1,6 @@
package com.luohuo.flex.im.controller.user;
import com.luohuo.flex.im.domain.vo.req.feed.FeedReq;
import io.swagger.v3.oas.annotations.tags.Tag;
import com.luohuo.basic.base.R;
import com.luohuo.basic.context.ContextUtil;
@@ -16,7 +17,6 @@ import jakarta.validation.Valid;
import jakarta.validation.groups.Default;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -45,10 +45,10 @@ public class FeedController {
return R.success(feedService.pushFeed(ContextUtil.getUid(), param));
}
@GetMapping("getFeedPermission/{feedId}")
@GetMapping("getFeedPermission")
@Operation(summary = "查看朋友圈权限")
public R<FeedPermission> getFeedPermission(@PathVariable("feedId") Long feedId) {
return R.success(feedService.getFeedPermission(ContextUtil.getUid(), feedId));
public R<FeedPermission> getFeedPermission(FeedReq feedReq) {
return R.success(feedService.getFeedPermission(ContextUtil.getUid(), feedReq.getFeedId()));
}
@PostMapping("edit")
@@ -57,15 +57,15 @@ public class FeedController {
return R.success(feedService.editFeed(ContextUtil.getUid(), param));
}
@PostMapping("del/{feedId}")
@PostMapping("del")
@Operation(summary = "删除朋友圈")
public R<Boolean> delFeed(@PathVariable("feedId") Long feedId) {
return R.success(feedService.delFeed(feedId));
public R<Boolean> delFeed(@RequestBody FeedReq feedReq) {
return R.success(feedService.delFeed(feedReq.getFeedId()));
}
@GetMapping("detail/{feedId}")
@GetMapping("detail")
@Operation(summary = "用户查看详情")
public R<FeedVo> feedDetail(@PathVariable("feedId") Long feedId) {
return R.success(feedService.feedDetail(feedId));
public R<FeedVo> feedDetail(FeedReq feedReq) {
return R.success(feedService.feedDetail(feedReq.getFeedId()));
}
}

View File

@@ -3,7 +3,7 @@ package com.luohuo.flex.im.domain.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableField;
import com.luohuo.basic.base.entity.Entity;
import com.luohuo.flex.im.domain.enums.ApplyStatusEnum;
import com.luohuo.flex.im.domain.enums.NoticeStatusEnum;
import com.luohuo.flex.im.domain.enums.RoomTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -59,7 +59,7 @@ public class UserApply extends Entity<Long> {
/**
* 申请状态
* @see ApplyStatusEnum
* @see NoticeStatusEnum
*/
@TableField("status")
private Integer status;

View File

@@ -1,24 +0,0 @@
package com.luohuo.flex.im.domain.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author nyh
* 申请状态枚举
*/
@Getter
@AllArgsConstructor
public enum ApplyStatusEnum {
WAIT_APPROVAL(1, "待审批"),
AGREE(2, "同意"),
REJECT(3, "拒绝"),
IGNORE(4, "忽略");
private final Integer code;
private final String desc;
}

View File

@@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
@@ -34,7 +35,7 @@ public class FeedParam extends OperParam {
private Integer mediaType;
@Schema(description = "发布的图片的url")
private List<String> urls;
private List<String> images;
@Schema(description = "发布视频的url")
private String videoUrl;
@@ -43,7 +44,7 @@ public class FeedParam extends OperParam {
* @see FeedPermissionEnum
*/
@Schema(description = "privacy -> 私密 open -> 公开 partVisible -> 部分可见 notAnyone -> 不给谁看")
@Min(value = 0, message = "请选择正确的可见类型")
@Pattern(regexp = "^(privacy|open|partVisible|notAnyone)$", message = "请选择正确的可见类型privacy/open/partVisible/notAnyone")
private String permission;
@Schema(description = "permission 限制的用户id")

View File

@@ -1,5 +1,6 @@
package com.luohuo.flex.im.domain.vo.request.member;
import com.luohuo.flex.im.domain.enums.NoticeStatusEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
@@ -20,7 +21,9 @@ public class ApplyReq {
@Schema(description ="邀请的id")
private Long applyId;
@NotNull(message = "请选择邀请记录")
@Schema(description ="0 = 拒绝 2 = 同意 3 = 忽略")
/**
* @see NoticeStatusEnum
*/
@NotNull(message = "请选择审批类型")
private Integer state;
}

View File

@@ -7,6 +7,7 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import jakarta.validation.constraints.NotNull;
import java.util.List;
/**
* 移除群成员
@@ -23,5 +24,5 @@ public class MemberDelReq {
@NotNull
@Schema(description ="被移除的uid主动退群填自己")
private Long uid;
private List<Long> uidList;
}

View File

@@ -21,9 +21,6 @@ public class AnnouncementsResp implements Serializable {
@Schema(description = "公告发布人id")
private Long uid;
@Schema(description = "公告发布人")
private String uName;
@Schema(description = "发布内容")
private String content;

View File

@@ -6,6 +6,8 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 群成员列表的成员信息
* @author nyh
@@ -14,7 +16,7 @@ import lombok.NoArgsConstructor;
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ChatMemberListResp {
public class ChatMemberListResp implements Serializable {
@Schema(description ="uid")
private Long uid;
@Schema(description ="用户名称")

View File

@@ -6,6 +6,8 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author nyh
*/
@@ -13,7 +15,7 @@ import lombok.NoArgsConstructor;
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ChatMessageReadResp {
public class ChatMessageReadResp implements Serializable {
@Schema(description ="已读或者未读的用户uid")
private Long uid;
}

View File

@@ -6,6 +6,7 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
@@ -16,7 +17,7 @@ import java.time.LocalDateTime;
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ChatRoomResp {
public class ChatRoomResp implements Serializable {
@Schema(description ="会话id")
private Long id;
@Schema(description ="单聊时对方的id群聊是groupId")

View File

@@ -6,6 +6,8 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author nyh
*/
@@ -13,7 +15,7 @@ import lombok.NoArgsConstructor;
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MemberResp {
public class MemberResp implements Serializable {
@Schema(description ="群聊id")
private Long groupId;
@Schema(description ="房间id")

View File

@@ -145,7 +145,7 @@ public class CaptchaServiceImpl implements CaptchaService {
String code = RandomUtil.randomNumbers(6);
CacheKey cacheKey = CaptchaCacheKeyBuilder.build(bindEmailReq.getEmail(), bindEmailReq.getTemplateCode());
if(cachePlusOps.exists(cacheKey)){
return R.success(cachePlusOps.ttl(cacheKey));
ArgumentAssert.isFalse(true, "验证码还剩"+cachePlusOps.ttl(cacheKey)+"秒过期,可以继续使用");
}
cachePlusOps.set(cacheKey, code);

View File

@@ -17,6 +17,8 @@ public interface DefValConstants {
Long DEF_ROOM_ID = 1L;
/** 内置的群组id */
Long DEF_GROUP_ID = 1L;
/** 贡献者id */
Long CONTRIBUTOR_ID = 6L;
/**
* 默认的树节点 分隔符
*/

View File

@@ -365,8 +365,10 @@ public class SessionManager {
* 清空所有会话
*/
public void clean() {
// 1. 标记服务不可用状态
// 0. 标记服务不可用状态
setAcceptingNewConnections(false);
nacosSessionRegistry.deregisterNode();
// 1. 收集所有设备信息
Map<Long, Set<String>> offlineDevices = new HashMap<>();
USER_DEVICE_SESSION_MAP.forEach((uid, deviceMap) -> offlineDevices.put(uid, new HashSet<>(deviceMap.keySet())));
@@ -397,8 +399,7 @@ public class SessionManager {
USER_DEVICE_SESSION_MAP.clear();
// 6. 清理路由与节点
nacosSessionRegistry.cleanupNodeRoutes();
nacosSessionRegistry.deregisterNode();
nacosSessionRegistry.cleanupNodeRoutes("");
}
/**

View File

@@ -1,5 +1,6 @@
package com.luohuo.flex.ws.websocket.nacos;
import cn.hutool.core.util.StrUtil;
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.NacosServiceManager;
import com.alibaba.nacos.api.exception.NacosException;
@@ -9,6 +10,7 @@ import com.alibaba.nacos.client.naming.utils.CollectionUtils;
import com.luohuo.basic.cache.repository.CachePlusOps;
import com.luohuo.basic.model.cache.CacheHashKey;
import com.luohuo.basic.model.cache.CacheKey;
import com.luohuo.flex.common.cache.PresenceCacheKeyBuilder;
import com.luohuo.flex.router.RouterCacheKeyBuilder;
import com.luohuo.flex.ws.websocket.SessionManager;
import jakarta.annotation.PostConstruct;
@@ -17,17 +19,24 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.net.InetAddress;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Nacos会话注册中心
@@ -156,9 +165,10 @@ public class NacosSessionRegistry {
/**
* 清理节点在Redis中的所有路由信息
*/
public void cleanupNodeRoutes() {
public void cleanupNodeRoutes(String cleanNodeId) {
cleanNodeId = StrUtil.isEmpty(cleanNodeId) ? nodeId : cleanNodeId;
// 1. 清理节点→设备映射
CacheKey cacheKey = RouterCacheKeyBuilder.buildNodeDevices(nodeId);
CacheKey cacheKey = RouterCacheKeyBuilder.buildNodeDevices(cleanNodeId);
Set<Object> deviceFields = redisTemplate.opsForSet().members(cacheKey.getKey());
if (!CollectionUtils.isEmpty(deviceFields)) {
@@ -169,7 +179,7 @@ public class NacosSessionRegistry {
// 清理节点本地映射
redisTemplate.delete(cacheKey.getKey());
}
log.info("节点路由清理完成: nodeId={}, 清理设备数={}", nodeId, deviceFields != null ? deviceFields.size() : 0);
log.info("节点路由清理完成: nodeId={}, 清理设备数={}", cleanNodeId, deviceFields != null ? deviceFields.size() : 0);
}
public void deregisterNode() {
@@ -202,4 +212,92 @@ public class NacosSessionRegistry {
log.error("心跳更新失败", e);
}
}
public Set<String> getAllActiveNodeIds() {
try {
List<Instance> instances = namingService.getAllInstances("ws-cluster", "WS_GROUP");
return instances.stream()
.filter(Instance::isHealthy)
.map(instance -> instance.getMetadata().get("nodeId"))
.collect(Collectors.toSet());
} catch (NacosException e) {
log.error("获取活跃节点失败", e);
return Collections.emptySet();
}
}
public Set<String> getAllRedisNodeIds() {
// 1. 获取基础key模式
String baseKey = RouterCacheKeyBuilder.buildNodeDevices("").getKey();
String pattern = baseKey + "*";
// 2. 使用SCAN命令安全遍历
ScanOptions options = ScanOptions.scanOptions()
.match(pattern)
.count(100)
.build();
Set<String> nodeIds = new HashSet<>();
try (Cursor<String> cursor = redisTemplate.scan(options)) {
while (cursor.hasNext()) {
String key = cursor.next();
// 3. 提取纯节点ID
if(key.startsWith(baseKey)) {
String[] parts = key.split(":");
nodeIds.add(parts[parts.length - 1]);
}
}
}
return nodeIds;
}
@Scheduled(fixedDelay = 30000)
public void cleanStaleRoutes() {
// 1. 获取所有需要对比的ID集合
Set<String> activeNodes = getAllActiveNodeIds();
Set<String> redisNodes = getAllRedisNodeIds();
// 2. 计算需要清理的节点ID
Set<String> staleNodes = redisNodes.stream()
.filter(node -> !activeNodes.contains(node))
.collect(Collectors.toSet());
// 3. 批量清理
staleNodes.forEach(node -> {
log.info("发现残留节点数据,开始清理: {}", node);
try {
cleanupNodeRoutes(node);
cleanNodeCompletely(node);
} catch (Exception e) {
log.error("节点清理失败: {}", node, e);
}
});
}
private void cleanNodeCompletely(String nodeId) {
// 1. 获取节点所有设备
CacheKey nodeDevicesKey = RouterCacheKeyBuilder.buildNodeDevices(nodeId);
Set<String> deviceFields = redisTemplate.opsForSet()
.members(nodeDevicesKey.getKey())
.stream()
.map(Object::toString)
.collect(Collectors.toSet());
if (CollectionUtils.isEmpty(deviceFields)) {
return;
}
// 2. 准备在线状态清理数据
Map<Long, Set<String>> uidToClients = deviceFields.stream()
.map(field -> field.split(":"))
.filter(parts -> parts.length == 2)
.collect(Collectors.groupingBy(
parts -> Long.parseLong(parts[0]),
Collectors.mapping(parts -> parts[1], Collectors.toSet())
));
// 3. 执行原子化清理
uidToClients.forEach((uid, clients) -> clients.forEach(client -> sessionManager.syncOnline(uid, client, false)));
log.info("节点完全清理完成: node={}, 设备数={}", nodeId, deviceFields.size());
}
}