feature: 新增扫码登录接口,调整在线人员返回状态

This commit is contained in:
乾乾
2025-08-27 19:36:18 +08:00
parent b34b84b57d
commit e2c19eaa2b
143 changed files with 2247 additions and 3003 deletions

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -79,64 +79,6 @@
<artifactId>mybatis-plus-extension</artifactId>
</dependency>
<!-- 第三方文件存储:华为云存储 -->
<dependency>
<groupId>com.huaweicloud</groupId>
<artifactId>esdk-obs-java</artifactId>
<exclusions>
<exclusion>
<artifactId>log4j-core</artifactId>
<groupId>org.apache.logging.log4j</groupId>
</exclusion>
<exclusion>
<artifactId>okhttp</artifactId>
<groupId>com.squareup.okhttp3</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- 第三方文件存储阿里云oss -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<exclusions>
<exclusion>
<artifactId>jersey-core</artifactId>
<groupId>com.sun.jersey</groupId>
</exclusion>
<exclusion>
<artifactId>jakarta.activation-api</artifactId>
<groupId>jakarta.activation</groupId>
</exclusion>
</exclusions>
</dependency>
<!--第三方文件存储: 七牛oss -->
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<exclusions>
<exclusion>
<artifactId>okhttp</artifactId>
<groupId>com.squareup.okhttp3</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- 第三方文件存储: FastDFS -->
<dependency>
<groupId>com.luohuo.basic</groupId>
<artifactId>fastdfs-client</artifactId>
</dependency>
<!-- 第三方文件存储minio -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<exclusions>
<exclusion>
<artifactId>okhttp</artifactId>
<groupId>com.squareup.okhttp3</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-email</artifactId>

View File

@@ -1,5 +1,6 @@
package com.luohuo.flex.msg.controller;
import com.luohuo.basic.tenant.core.aop.TenantIgnore;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -62,11 +63,18 @@ public class ExtendMsgController extends SuperController<ExtendMsgService, Long,
@Operation(summary = "根据模板发送消息", description = "根据模板发送消息")
@PostMapping("/sendByTemplate")
@WebLog("发送消息")
public R<Boolean> sendByTemplate(@RequestBody @Validated(SuperEntity.Update.class) ExtendMsgSendVO data
, @Parameter(hidden = true) @LoginUser(isEmployee = true) SysUser sysUser) {
public R<Boolean> sendByTemplate(@RequestBody @Validated(SuperEntity.Update.class) ExtendMsgSendVO data, @Parameter(hidden = true) @LoginUser(isEmployee = true) SysUser sysUser) {
return R.success(msgBiz.sendByTemplate(data, sysUser));
}
@Operation(summary = "根据模板发送消息", description = "根据模板发送消息")
@PostMapping("/anyUser/sendByTemplate")
@WebLog("发送消息")
@TenantIgnore
public R<Boolean> anyUserSendByTemplate(@RequestBody @Validated(SuperEntity.Update.class) ExtendMsgSendVO data, @Parameter(hidden = true) @LoginUser(isEmployee = true) SysUser sysUser) {
return R.success(msgBiz.sendByTemplate(data, sysUser));
}
@Operation(summary = "发布站内信", description = "发布站内信")
@PostMapping("/publish")
@WebLog("发布站内信")

View File

@@ -1,52 +0,0 @@
package com.luohuo.flex.msg.controller;
import com.luohuo.basic.annotation.log.WebLog;
import com.luohuo.basic.annotation.user.LoginUser;
import com.luohuo.basic.base.R;
import com.luohuo.basic.base.entity.SuperEntity;
import com.luohuo.basic.tenant.core.aop.TenantIgnore;
import com.luohuo.flex.model.entity.system.SysUser;
import com.luohuo.flex.msg.biz.MsgBiz;
import com.luohuo.flex.msg.vo.update.ExtendMsgSendVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 前端控制器
* 消息
* </p>
*
* @author zuihou
* @date 2022-07-10 11:41:17
* @create [2022-07-10 11:41:17] [zuihou] [代码生成器生成]
*/
@Slf4j
@RequiredArgsConstructor
@Validated
@RestController
@RequestMapping("/anyUser")
@Tag(name = "消息模版")
public class MsgController {
private final MsgBiz msgBiz;
@Operation(summary = "根据模板发送消息", description = "根据模板发送消息")
@PostMapping("/extendMsg/sendByTemplate")
@WebLog("发送消息")
@TenantIgnore
public R<Boolean> sendByTemplate(@RequestBody @Validated(SuperEntity.Update.class) ExtendMsgSendVO data
, @Parameter(hidden = true) @LoginUser(isEmployee = true) SysUser sysUser) {
return R.success(msgBiz.sendByTemplate(data, sysUser));
}
}

View File

@@ -1,6 +1,5 @@
package com.luohuo.flex.msg.api;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
@@ -25,6 +24,6 @@ public interface MsgApi {
* @return
*/
@Operation(summary = "根据模板发送消息", description = "根据模板发送消息")
@PostMapping("/anyUser/extendMsg/sendByTemplate")
@PostMapping("/extendMsg/anyUser/sendByTemplate")
R<Boolean> sendByTemplate(@RequestBody ExtendMsgSendVO data);
}

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

@@ -31,7 +31,6 @@ import com.luohuo.basic.exception.BizException;
import com.luohuo.basic.exception.UnauthorizedException;
import com.luohuo.basic.utils.StrPool;
import com.luohuo.flex.common.properties.IgnoreProperties;
import com.luohuo.flex.common.utils.Base64Util;
import static com.luohuo.basic.context.ContextConstants.*;
@@ -100,17 +99,13 @@ public class TokenContextFilter implements WebFilter, Ordered {
ContextUtil.setGrayVersion(getHeader(ContextConstants.GRAY_VERSION, request));
try {
// 2,解码 Authorization
parseClient(request, mutate);
// 3, 获取 应用id
// 1 获取 应用信息
parseApplication(request, mutate);
Mono<Void> token = parseToken(exchange, chain, mutate);
if (token != null) {
return token;
}
} catch (UnauthorizedException e) {
return errorResponse(response, e.getMessage(), e.getCode());
} catch (BizException e) {
@@ -170,15 +165,6 @@ public class TokenContextFilter implements WebFilter, Ordered {
return null;
}
private void parseClient(ServerHttpRequest request, ServerHttpRequest.Builder mutate) {
String base64Authorization = getHeader(CLIENT_KEY, request);
if (StrUtil.isNotEmpty(base64Authorization)) {
String[] client = Base64Util.getClient(base64Authorization);
ContextUtil.setClientId(client[0]);
addHeader(mutate, CLIENT_ID_HEADER, ContextUtil.getClientId());
}
}
private void parseApplication(ServerHttpRequest request, ServerHttpRequest.Builder mutate) {
String applicationIdStr = getHeader(APPLICATION_ID_KEY, request);
if (StrUtil.isNotEmpty(applicationIdStr)) {

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@}
@@ -52,6 +53,7 @@ spring:
password: ${luohuo.nacos.password}
server-addr: ${luohuo.nacos.ip}:${luohuo.nacos.port}
namespace: ${luohuo.nacos.namespace}
ip: ${luohuo.nacos.local-ip}
metadata: # 元数据,用于权限服务实时获取各个服务的所有接口
management.context-path: ${server.servlet.context-path:}${spring.mvc.servlet.path:}${management.endpoints.web.base-path:}
gray_version: qianqian

View File

@@ -13,13 +13,15 @@ public class GroupMemberAddEvent extends ApplicationEvent {
// 变动的成员
private final List<Long> memberList;
private final Long roomId;
private final Long cUid;
public GroupMemberAddEvent(Object source, Long roomId, List<Long> memberList, Long cUid) {
// 消息接收人
private final Long uid;
public GroupMemberAddEvent(Object source, Long roomId, List<Long> memberList, Long uid) {
super(source);
this.memberList = memberList;
this.roomId = roomId;
this.cUid = cUid;
this.roomId = roomId;
this.uid = uid;
}
}

View File

@@ -1,20 +0,0 @@
package com.luohuo.flex.im.common.event;
import com.luohuo.flex.model.entity.ws.OffLineResp;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;
/**
* token过期事件
* @author ZOL
*/
@Getter
public class TokenExpireEvent extends ApplicationEvent {
private final OffLineResp offLine;
public TokenExpireEvent(Object source, OffLineResp offLine) {
super(source);
this.offLine = offLine;
}
}

View File

@@ -2,6 +2,7 @@ package com.luohuo.flex.im.common.event.listener;
import com.luohuo.flex.im.api.PresenceApi;
import com.luohuo.flex.im.common.event.GroupMemberAddEvent;
import com.luohuo.flex.im.core.chat.dao.GroupMemberDao;
import com.luohuo.flex.im.domain.vo.request.ChatMessageReq;
import com.luohuo.flex.im.core.chat.service.ChatService;
import com.luohuo.flex.im.core.chat.service.adapter.MemberAdapter;
@@ -10,6 +11,7 @@ import com.luohuo.flex.im.core.chat.service.cache.GroupMemberCache;
import com.luohuo.flex.im.domain.entity.User;
import com.luohuo.flex.im.core.user.service.cache.UserInfoCache;
import com.luohuo.flex.im.core.user.service.impl.PushService;
import com.luohuo.flex.model.entity.ws.ChatMember;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
@@ -33,6 +35,7 @@ public class GroupMemberAddListener {
private ChatService chatService;
private UserInfoCache userInfoCache;
private GroupMemberDao groupMemberDao;
private GroupMemberCache groupMemberCache;
private PresenceApi presenceApi;
private PushService pushService;
@@ -46,7 +49,7 @@ public class GroupMemberAddListener {
public void sendAddMsg(GroupMemberAddEvent event) {
List<Long> uidList = event.getMemberList();
Long roomId = event.getRoomId();
User user = userInfoCache.get(event.getCUid());
User user = userInfoCache.get(event.getUid());
ChatMessageReq chatMessageReq = RoomAdapter.buildGroupAddMessage(roomId, user, userInfoCache.getBatch(uidList));
chatService.sendMsg(chatMessageReq, user.getId());
}
@@ -64,7 +67,9 @@ public class GroupMemberAddListener {
// 在线的用户
List<Long> onlineUids = presenceApi.getGroupOnlineMembers(roomId).getData();
Map<Long, User> map = userInfoCache.getBatch(event.getMemberList());
pushService.sendPushMsg(MemberAdapter.buildMemberAddWS(roomId, onlineUids, map), memberUidList, event.getCUid());
List<ChatMember> memberResps = groupMemberDao.getMemberListByUid(event.getMemberList());
pushService.sendPushMsg(MemberAdapter.buildMemberAddWS(roomId, onlineUids, memberResps, map), memberUidList, event.getUid());
// 移除缓存
groupMemberCache.evictMemberUidList(roomId);

View File

@@ -32,8 +32,8 @@ public class UserApplyListener {
@TransactionalEventListener(classes = UserApplyEvent.class, fallbackExecution = true)
public void notifyFriend(UserApplyEvent event) {
UserApply userApply = event.getUserApply();
Integer unReadCount = userApplyDao.getUnReadCount(userApply.getTargetId());
pushService.sendPushMsg(WsAdapter.buildApplySend(new WSFriendApply(userApply.getUid(), unReadCount)), userApply.getTargetId(), userApply.getUid());
WSFriendApply resp = userApplyDao.getUnReadCount(userApply.getUid(), userApply.getTargetId());
pushService.sendPushMsg(WsAdapter.buildApplySend(resp), userApply.getTargetId(), userApply.getUid());
}
}

View File

@@ -28,7 +28,9 @@ import java.util.stream.Collectors;
@Slf4j
public abstract class AbstractUrlDiscover implements UrlDiscover {
//链接识别的正则
private static final Pattern PATTERN = Pattern.compile("https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()!@:%_\\+.~#?&\\/\\/=]*)", Pattern.CASE_INSENSITIVE);
private static final Pattern PATTERN = Pattern.compile(
"((https?:\\/\\/|www\\.)[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()!@:%_\\+.~#?&\\/\\/=]*))", Pattern.CASE_INSENSITIVE
);
private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(
Math.max(4, Runtime.getRuntime().availableProcessors() * 2),
@@ -104,7 +106,6 @@ public abstract class AbstractUrlDiscover implements UrlDiscover {
private String assemble(String url) {
if (!StrUtil.startWith(url, "http")) {
return "http://" + url;
}

View File

@@ -0,0 +1,55 @@
package com.luohuo.flex.im.core.chat.cache;
import com.luohuo.basic.base.entity.SuperEntity;
import com.luohuo.basic.model.cache.CacheHashKey;
import com.luohuo.basic.model.cache.CacheKeyBuilder;
import com.luohuo.flex.common.cache.CacheKeyModular;
import com.luohuo.flex.common.cache.CacheKeyTable;
import java.time.Duration;
/**
* 用户所有会话信息的缓存
* @author 乾乾
* @date 2025-08-27 10:45 上午
*/
public class UserContactCacheKeyBuilder implements CacheKeyBuilder {
public static CacheHashKey build(Long id) {
return new UserContactCacheKeyBuilder().hashFieldKey("concat", id);
}
@Override
public String getTenant() {
return null;
}
@Override
public String getTable() {
return CacheKeyTable.Chat.USER_CONTACT;
}
@Override
public String getPrefix() {
return CacheKeyModular.PREFIX;
}
@Override
public String getModular() {
return CacheKeyModular.CHAT;
}
@Override
public String getField() {
return SuperEntity.ID_FIELD;
}
@Override
public ValueType getValueType() {
return ValueType.obj;
}
@Override
public Duration getExpire() {
return Duration.ofDays(1L);
}
}

View File

@@ -0,0 +1,12 @@
package com.luohuo.flex.im.core.chat.consumer;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class DebounceInfo {
private Long lastMessageId;
private Long lastUpdateTime;
private int pendingCount;
}

View File

@@ -1,6 +1,7 @@
package com.luohuo.flex.im.core.chat.consumer;
import com.luohuo.basic.context.ContextUtil;
import com.luohuo.basic.utils.TimeUtils;
import com.luohuo.flex.common.constant.MqConstant;
import com.luohuo.flex.im.core.chat.dao.ContactDao;
import com.luohuo.flex.im.core.chat.dao.MessageDao;
@@ -31,8 +32,10 @@ import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* 发送消息更新房间收信箱,并同步给房间成员信箱
@@ -75,7 +78,7 @@ public class MsgSendConsumer implements RocketMQListener<MsgSendMessageDTO> {
}
// 2. 更新所有群成员的会话时间, 并推送房间成员
contactDao.refreshOrCreateActiveTime(room.getId(), memberUidList, message.getId(), message.getCreateTime());
refreshContactActiveTimeEnhanced(room.getId(), memberUidList, message.getId(), TimeUtils.getTime(message.getCreateTime()));
// 3. 与在线人员交集并进行路由
switch (MessageTypeEnum.of(message.getType())) {
@@ -117,4 +120,35 @@ public class MsgSendConsumer implements RocketMQListener<MsgSendMessageDTO> {
}
}
}
// 增强版防抖控制
private final Map<Long, DebounceInfo> roomDebounceInfo = new ConcurrentHashMap<>();
private static final long MAX_DEBOUNCE_TIME = 10000;
private static final int MAX_DEBOUNCE_COUNT = 1000;
private void refreshContactActiveTimeEnhanced(Long roomId, List<Long> memberUidList, Long messageId, Long createTime) {
Long currentTime = System.currentTimeMillis();
DebounceInfo info = roomDebounceInfo.get(roomId);
if (info == null) {
// 首次更新
contactDao.refreshOrCreateActiveTime(roomId, memberUidList, messageId, TimeUtils.timestampToLocalDateTime(createTime));
roomDebounceInfo.put(roomId, new DebounceInfo(messageId, currentTime, 0));
return;
}
// 检查是否需要立即更新
boolean shouldUpdate = (currentTime - info.getLastUpdateTime()) >= MAX_DEBOUNCE_TIME || info.getPendingCount() >= MAX_DEBOUNCE_COUNT;
if (shouldUpdate) {
// 执行更新并使用最新消息ID
contactDao.refreshOrCreateActiveTime(roomId, memberUidList, messageId, TimeUtils.timestampToLocalDateTime(createTime));
roomDebounceInfo.put(roomId, new DebounceInfo(messageId, currentTime, 0));
log.debug("强制更新会话时间 roomId: {}, 消息ID: {}", roomId, messageId);
} else {
// 累积防抖计数
roomDebounceInfo.put(roomId, new DebounceInfo(messageId, info.getLastUpdateTime(), info.getPendingCount() + 1));
log.debug("防抖中 roomId: {}, 累积消息: {}", roomId, info.getPendingCount() + 1);
}
}
}

View File

@@ -6,8 +6,10 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.extension.toolkit.SimpleQuery;
import com.luohuo.basic.cache.repository.CachePlusOps;
import com.luohuo.basic.tenant.core.aop.TenantIgnore;
import com.luohuo.flex.im.core.chat.cache.UserContactCacheKeyBuilder;
import jakarta.annotation.Resource;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import com.luohuo.flex.im.domain.vo.req.CursorPageBaseReq;
@@ -18,10 +20,8 @@ import com.luohuo.flex.im.domain.entity.Message;
import com.luohuo.flex.im.core.chat.mapper.ContactMapper;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
@@ -34,6 +34,9 @@ import java.util.stream.Collectors;
@Service
public class ContactDao extends ServiceImpl<ContactMapper, Contact> {
@Resource
private CachePlusOps cachePlusOps;
public Contact get(Long uid, Long roomId) {
return lambdaQuery()
.eq(Contact::getUid, uid)
@@ -158,21 +161,7 @@ public class ContactDao extends ServiceImpl<ContactMapper, Contact> {
* @return
*/
public List<Contact> getAllContactsByUid(Long uid) {
return lambdaQuery().eq(Contact::getUid, uid).list();
return cachePlusOps.hGet(UserContactCacheKeyBuilder.build(uid), x -> lambdaQuery().eq(Contact::getUid, uid).list(), true).getValue();
}
public Map<Long, Long> getLastMsgIds(List<Long> roomIds) {
LambdaQueryWrapper<Contact> wrapper = new LambdaQueryWrapper<Contact>()
.in(Contact::getRoomId, roomIds);
return SimpleQuery
.group(wrapper, Contact::getRoomId,
Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparing(
Contact::getLastMsgId,
Comparator.nullsLast(Comparator.naturalOrder())
)),
opt -> opt.map(Contact::getLastMsgId).orElse(null)
));
}
}

View File

@@ -8,10 +8,14 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luohuo.flex.im.core.chat.service.cache.RoomGroupCache;
import com.luohuo.flex.im.domain.entity.GroupMember;
import com.luohuo.flex.im.domain.entity.RoomGroup;
import com.luohuo.flex.im.domain.enums.GroupRoleEnum;
import com.luohuo.flex.im.core.chat.mapper.GroupMemberMapper;
import com.luohuo.flex.im.core.chat.service.cache.GroupMemberCache;
import com.luohuo.flex.model.entity.ws.ChatMember;
import com.luohuo.flex.model.entity.ws.ChatMemberResp;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@@ -19,7 +23,6 @@ import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static com.luohuo.flex.im.domain.enums.GroupRoleEnum.ROLE_LIST;
@@ -38,6 +41,8 @@ public class GroupMemberDao extends ServiceImpl<GroupMemberMapper, GroupMember>
@Resource
@Lazy
private GroupMemberCache groupMemberCache;
@Resource
private RoomGroupCache roomGroupCache;
/**
* 查询群成员
@@ -97,27 +102,29 @@ public class GroupMemberDao extends ServiceImpl<GroupMemberMapper, GroupMember>
/**
* 查询人员在群里的角色
* @param groupId
* @param uid
* @param roomId 房间id
* @param uid 用户id
* @return
*/
public GroupMember getMember(Long groupId, Long uid) {
return lambdaQuery()
.eq(GroupMember::getGroupId, groupId)
public GroupMember getMember(Long roomId, Long uid) {
RoomGroup roomGroup = roomGroupCache.getByRoomId(roomId);
return lambdaQuery()
.eq(GroupMember::getGroupId, roomGroup.getId())
.eq(GroupMember::getUid, uid)
.one();
}
/**
* 用户群组关系构建器
* TODO 加上缓存,事件驱动用户加群、退群都需要清空
* @param uid
* @return
*/
public Set<Long> getSelfAllGroup(Long uid) {
return lambdaQuery().eq(GroupMember::getUid, uid).list().stream().map(GroupMember::getGroupId).collect(Collectors.toSet());
public GroupMember getMemberByGroupId(Long groupId, Long uid) {
return lambdaQuery()
.eq(GroupMember::getGroupId, groupId)
.eq(GroupMember::getUid, uid)
.one();
}
/**
* 查询自己创建的群聊数量
* @param uid 当前用户
*/
public List<GroupMember> getSelfGroup(Long uid) {
return lambdaQuery()
.eq(GroupMember::getUid, uid)
@@ -230,18 +237,22 @@ public class GroupMemberDao extends ServiceImpl<GroupMemberMapper, GroupMember>
/**
* 将群员设置为屏蔽该群
* @param roomId
* @param uid
* @param roomId 房间id
* @param uid 屏蔽的人
*/
public void setMemberDeFriend(Long roomId, Long uid, Boolean deFriend) {
GroupMember member = getMember(roomId, uid);
member.setDeFriend(deFriend);
updateById(member);
groupMemberCache.evictMemberUidList(roomId);
groupMemberCache.evictExceptMemberList(roomId);
groupMemberCache.evictMemberDetail(roomId, uid);
}
public List<GroupMember> getMemberListByGroupId(Long groupId) {
return list(new LambdaQueryWrapper<GroupMember>()
.eq(GroupMember::getGroupId, groupId));
public List<ChatMemberResp> getMemberListByGroupId(Long groupId) {
return baseMapper.getMemberListByGroupId(groupId);
}
public List<ChatMember> getMemberListByUid(List<Long> memberList) {
return baseMapper.getMemberListByUid(memberList);
}
}

View File

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luohuo.basic.tenant.core.aop.TenantIgnore;
import com.luohuo.flex.im.domain.entity.Contact;
import com.luohuo.flex.im.domain.vo.req.CursorPageBaseReq;
import com.luohuo.flex.im.domain.vo.res.CursorPageBaseResp;
import com.luohuo.flex.im.common.utils.CursorUtils;
@@ -14,7 +15,9 @@ import com.luohuo.flex.im.core.chat.mapper.MessageMapper;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
@@ -61,15 +64,6 @@ public class MessageDao extends ServiceImpl<MessageMapper, Message> {
.update();
}
@TenantIgnore
public Integer getUnReadCount(Long roomId, LocalDateTime readTime, Long uid) {
return Math.toIntExact(lambdaQuery()
.eq(Message::getRoomId, roomId)
.gt(Objects.nonNull(readTime), Message::getCreateTime, readTime)
.ne(Message::getFromUid, uid)
.count());
}
/**
* 根据房间ID逻辑删除消息
*
@@ -87,4 +81,17 @@ public class MessageDao extends ServiceImpl<MessageMapper, Message> {
}
return this.update(wrapper);
}
@TenantIgnore
public Integer getUnReadCount(Long roomId, LocalDateTime readTime, Long uid) {
return Math.toIntExact(lambdaQuery()
.eq(Message::getRoomId, roomId)
.gt(Objects.nonNull(readTime), Message::getCreateTime, readTime)
.ne(Message::getFromUid, uid)
.count());
}
public Map<Long, Integer> batchGetUnReadCount(Collection<Contact> contactList) {
return baseMapper.batchGetUnReadCount(contactList);
}
}

View File

@@ -1,17 +0,0 @@
package com.luohuo.flex.im.core.chat.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.luohuo.flex.im.domain.entity.Message2;
import org.springframework.stereotype.Repository;
/**
* <p>
* 会话列表 Mapper 接口
* </p>
*
* @author nyh
*/
@Repository
public interface Contact222Mapper extends BaseMapper<Message2> {
}

View File

@@ -1,9 +1,13 @@
package com.luohuo.flex.im.core.chat.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.luohuo.flex.model.entity.ws.ChatMember;
import com.luohuo.flex.model.entity.ws.ChatMemberResp;
import org.springframework.stereotype.Repository;
import com.luohuo.flex.im.domain.entity.GroupMember;
import java.util.List;
/**
* <p>
* 群成员表 Mapper 接口
@@ -14,4 +18,7 @@ import com.luohuo.flex.im.domain.entity.GroupMember;
@Repository
public interface GroupMemberMapper extends BaseMapper<GroupMember> {
List<ChatMemberResp> getMemberListByGroupId(Long groupId);
List<ChatMember> getMemberListByUid(List<Long> memberList);
}

View File

@@ -1,9 +1,14 @@
package com.luohuo.flex.im.core.chat.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.luohuo.flex.im.domain.entity.Contact;
import org.apache.ibatis.annotations.MapKey;
import org.springframework.stereotype.Repository;
import com.luohuo.flex.im.domain.entity.Message;
import java.util.Collection;
import java.util.Map;
/**
* <p>
* 消息表 Mapper 接口
@@ -14,4 +19,6 @@ import com.luohuo.flex.im.domain.entity.Message;
@Repository
public interface MessageMapper extends BaseMapper<Message> {
@MapKey("room_id")
Map<Long, Integer> batchGetUnReadCount(Collection<Contact> contactList);
}

View File

@@ -25,6 +25,4 @@ public interface ContactService {
Integer getMsgUnReadCount(Message message);
Map<Long, MsgReadInfoDTO> getMsgReadInfo(List<Message> messages);
Map<Long, Long> getLastMsgIds(List<Long> roomIds);
}

View File

@@ -2,6 +2,7 @@ package com.luohuo.flex.im.core.chat.service.adapter;
import com.luohuo.flex.im.domain.entity.IpDetail;
import com.luohuo.flex.im.domain.entity.IpInfo;
import com.luohuo.flex.model.entity.ws.ChatMember;
import com.luohuo.flex.model.enums.ChatActiveStatusEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
@@ -20,10 +21,10 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static com.luohuo.flex.model.entity.ws.WSMemberChange.CHANGE_TYPE_ADD;
import static com.luohuo.flex.model.entity.ws.WSMemberChange.CHANGE_TYPE_REMOVE;
/**
@@ -36,10 +37,10 @@ public class MemberAdapter {
/**
* 将User对象转换为ChatMemberResp对象并注入实时在线状态
* @param list 用户列表
* @param onlineStatusMap 实时在线状态映射表 (uid -> 是否在线)
* @param onlineUids 用户在线状态表
* @return 转换后的群成员响应对象列表
*/
public static List<ChatMemberResp> buildMember(List<User> list, Map<Long, Boolean> onlineStatusMap) {
public static List<ChatMemberResp> buildMember(List<User> list, Set<Long> onlineUids) {
return list.stream().map(user -> {
ChatMemberResp resp = new ChatMemberResp();
resp.setUid(String.valueOf(user.getId()));
@@ -48,7 +49,7 @@ public class MemberAdapter {
resp.setAccount(user.getAccount());
resp.setLocPlace(Optional.ofNullable(user.getIpInfo()).map(IpInfo::getUpdateIpDetail).map(IpDetail::getCity).orElse(null));
resp.setUserStateId(user.getUserStateId());
Boolean isOnline = onlineStatusMap.get(user.getId());
Boolean isOnline = onlineUids.contains(user.getId());
resp.setActiveStatus(isOnline? ChatActiveStatusEnum.ONLINE.getStatus(): ChatActiveStatusEnum.OFFLINE.getStatus());
// 最后活跃时间(离线用户显示)
if (!isOnline && user.getLastOptTime() != null) {
@@ -87,38 +88,45 @@ public class MemberAdapter {
* @param map 用户的基础信息
* @return
*/
public static WsBaseResp<WSMemberChange> buildMemberAddWS(Long roomId, List<Long> onlineUids, Map<Long, User> map) {
WsBaseResp<WSMemberChange> wsBaseResp = new WsBaseResp<>();
wsBaseResp.setType(WSRespTypeEnum.NEW_FRIEND_SESSION.getType());
WSMemberChange wsMemberChange = new WSMemberChange();
public static WsBaseResp<WSMemberChange> buildMemberAddWS(Long roomId, List<Long> onlineUids, List<ChatMember> memberResps, Map<Long, User> map) {
WsBaseResp<WSMemberChange> wsBaseResp = new WsBaseResp<>();
wsBaseResp.setType(WSRespTypeEnum.memberChange.getType());
WSMemberChange wsMemberChange = new WSMemberChange();
memberResps.forEach(item -> {
Long uid = Long.parseLong(item.getUid());
User user = map.get(uid);
if (user != null) {
item.setActiveStatus(onlineUids.contains(user.getId()) ? ChatActiveStatusEnum.ONLINE.getStatus() : ChatActiveStatusEnum.OFFLINE.getStatus());
item.setLastOptTime(user.getLastOptTime());
item.setName(user.getName());
item.setAvatar(user.getAvatar());
item.setAccount(user.getAccount());
item.setUserStateId(user.getUserStateId()+"");
}
});
List<WSMemberChange.UserState> states = map.values().stream().map(user -> {
WSMemberChange.UserState userState = new WSMemberChange.UserState();
userState.setActiveStatus(onlineUids.contains(user.getId()) ? ChatActiveStatusEnum.ONLINE.getStatus() : ChatActiveStatusEnum.OFFLINE.getStatus());
userState.setLastOptTime(user.getLastOptTime());
userState.setUid(user.getId());
return userState;
}).collect(Collectors.toList());
wsMemberChange.setChangeType(CHANGE_TYPE_ADD);
wsMemberChange.setUserList(states);
wsMemberChange.setRoomId(roomId);
wsBaseResp.setData(wsMemberChange);
return wsBaseResp;
}
wsMemberChange.setUserList(memberResps);
wsMemberChange.setRoomId(roomId+"");
wsBaseResp.setData(wsMemberChange);
return wsBaseResp;
}
public static WsBaseResp<WSMemberChange> buildMemberRemoveWS(Long roomId, List<Long> uidList) {
public static WsBaseResp<WSMemberChange> buildMemberRemoveWS(Long roomId, List<Long> uidList, Integer type) {
WsBaseResp<WSMemberChange> wsBaseResp = new WsBaseResp<>();
wsBaseResp.setType(WSRespTypeEnum.NEW_FRIEND_SESSION.getType());
wsBaseResp.setType(WSRespTypeEnum.memberChange.getType());
WSMemberChange wsMemberChange = new WSMemberChange();
List<WSMemberChange.UserState> states = uidList.stream().map(uid -> {
WSMemberChange.UserState userState = new WSMemberChange.UserState();
userState.setUid(uid);
return userState;
List<ChatMember> states = uidList.stream().map(uid -> {
ChatMember chatMember = new ChatMember();
chatMember.setUid(uid+"");
return chatMember;
}).collect(Collectors.toList());
wsMemberChange.setUserList(states);
wsMemberChange.setRoomId(roomId);
wsMemberChange.setChangeType(CHANGE_TYPE_REMOVE);
wsMemberChange.setRoomId(roomId+"");
wsMemberChange.setChangeType(type);
wsBaseResp.setData(wsMemberChange);
return wsBaseResp;
}

View File

@@ -11,8 +11,11 @@ import com.luohuo.flex.im.domain.entity.msg.VideoCallMsgDTO;
import com.luohuo.flex.im.domain.entity.msg.MergeMsg;
import com.luohuo.flex.im.domain.entity.msg.MergeMsgDTO;
import com.luohuo.flex.im.domain.entity.msg.NoticeMsgDTO;
import com.luohuo.flex.im.domain.enums.ApplyReadStatusEnum;
import com.luohuo.flex.im.domain.enums.ApplyStatusEnum;
import com.luohuo.flex.im.domain.enums.RoomTypeEnum;
import com.luohuo.flex.im.domain.vo.req.room.UserApplyResp;
import com.luohuo.flex.model.entity.ws.PendingInviteWs;
import com.luohuo.flex.model.entity.ws.WSFriendApply;
import com.luohuo.flex.model.enums.MessageMarkTypeEnum;
import com.luohuo.flex.im.domain.enums.MessageStatusEnum;
import com.luohuo.flex.im.domain.enums.MessageTypeEnum;
@@ -185,7 +188,7 @@ public class MessageAdapter {
noticeMsgDTO.setTop(announcements.getTop());
noticeMsgDTO.setRoomId(announcements.getRoomId());
noticeMsgDTO.setContent(announcements.getContent());
noticeMsgDTO.setCreateTime(TimeUtils.getTime(announcements.getCreateTime()));
noticeMsgDTO.setCreateTime(TimeUtils.getTime(announcements.getUpdateTime()));
chatMessageReq.setBody(noticeMsgDTO);
return chatMessageReq;
}
@@ -196,17 +199,18 @@ public class MessageAdapter {
public static WsBaseResp<UserApplyResp> buildRoomGroupMessage(Long uid, Long roomId, Long groupAdminId, String msg) {
WsBaseResp<UserApplyResp> wsBaseResp = new WsBaseResp<>();
wsBaseResp.setType(WSRespTypeEnum.ROOM_GROUP_MSG.getType());
wsBaseResp.setData(new UserApplyResp(uid, 2, roomId, groupAdminId, msg, 1, 1, LocalDateTime.now()));
wsBaseResp.setData(new UserApplyResp(uid, RoomTypeEnum.GROUP.getType(), roomId, groupAdminId, msg, ApplyStatusEnum.WAIT_APPROVAL.getCode(), ApplyReadStatusEnum.UNREAD.getCode(), false, LocalDateTime.now()));
return wsBaseResp;
}
/**
* 邀请用户进群通知
* @param resp 通知数据
*/
public static WsBaseResp<PendingInviteWs> buildInviteeUserAddGroupMessage(Long uid, Long inviteId, Long groupId) {
WsBaseResp<PendingInviteWs> wsBaseResp = new WsBaseResp<>();
wsBaseResp.setType(WSRespTypeEnum.INVITEE_USER_ADD_GROUP.getType());
wsBaseResp.setData(new PendingInviteWs(uid.toString(), groupId.toString(), inviteId.toString(), LocalDateTime.now()));
public static WsBaseResp<WSFriendApply> buildInviteeUserAddGroupMessage(WSFriendApply resp) {
WsBaseResp<WSFriendApply> wsBaseResp = new WsBaseResp<>();
wsBaseResp.setType(WSRespTypeEnum.NEW_APPLY.getType());
wsBaseResp.setData(resp);
return wsBaseResp;
}

View File

@@ -5,7 +5,6 @@ import cn.hutool.core.util.StrUtil;
import com.luohuo.flex.im.domain.entity.Contact;
import com.luohuo.flex.im.domain.entity.GroupMember;
import com.luohuo.flex.im.domain.entity.Room;
import com.luohuo.flex.im.domain.entity.RoomGroup;
import com.luohuo.flex.im.domain.enums.GroupRoleEnum;
import com.luohuo.flex.im.domain.enums.MessageTypeEnum;
import com.luohuo.flex.im.domain.vo.request.ChatMessageReq;

View File

@@ -1,5 +1,6 @@
package com.luohuo.flex.im.core.chat.service.cache;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.luohuo.flex.im.core.chat.dao.GroupMemberDao;
import com.luohuo.flex.im.core.chat.dao.RoomGroupDao;
@@ -10,6 +11,7 @@ import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@@ -31,7 +33,7 @@ public class GroupMemberCache {
* @param roomId
* @return
*/
@Cacheable(cacheNames = "luohuo:member", key = "'all:'+#roomId")
@Cacheable(cacheNames = "luohuo:member:all", key = "#roomId")
public List<Long> getMemberUidList(Long roomId) {
RoomGroup roomGroup = roomGroupDao.getByRoomId(roomId);
if (Objects.isNull(roomGroup)) {
@@ -45,7 +47,7 @@ public class GroupMemberCache {
* @param roomId
* @return
*/
@Cacheable(cacheNames = "luohuo:member", key = "'except:'+#roomId")
@Cacheable(cacheNames = "luohuo:member:except", key = "#roomId")
public List<Long> getMemberExceptUidList(Long roomId) {
RoomGroup roomGroup = roomGroupDao.getByRoomId(roomId);
if (Objects.isNull(roomGroup)) {
@@ -56,22 +58,17 @@ public class GroupMemberCache {
/**
* 获取指定成员在群中的详细信息
* @param roomId 聊天室ID
* @param roomId 房间id
* @param memberUid 成员用户ID
* @return 成员详细信息
*/
@Cacheable(cacheNames = "luohuo:member:info", key = "#roomId + ':' + #memberUid")
public GroupMember getMemberDetail(Long roomId, Long memberUid) {
return groupMemberDao.getBaseMapper().selectOne(new QueryWrapper<GroupMember>()
.eq("group_id", roomId)
.eq("uid", memberUid)
.last("LIMIT 1")
);
return groupMemberDao.getMember(roomId, memberUid);
}
@CacheEvict(cacheNames = "luohuo:member:info", key = "#roomId + ':' + #memberUid")
public void evictMemberDetail(Long roomId, Long memberUid) {
// 清理单个成员缓存
}
@CacheEvict(cacheNames = "luohuo:member:info", allEntries = true)
@@ -89,14 +86,12 @@ public class GroupMemberCache {
}
@CacheEvict(cacheNames = "luohuo:member", key = "'except:'+#roomId")
public List<Long> evictExceptMemberList(Long roomId) {
return null;
@CacheEvict(cacheNames = "luohuo:member:except", key = "#roomId")
public void evictExceptMemberList(Long roomId) {
}
@CacheEvict(cacheNames = "luohuo:member", key = "'all:'+#roomId")
public List<Long> evictMemberList(Long roomId) {
return null;
@CacheEvict(cacheNames = "luohuo:member:all", key = "#roomId")
public void evictMemberList(Long roomId) {
}
/**
@@ -105,8 +100,11 @@ public class GroupMemberCache {
* @return
*/
public List<Long> getJoinedRoomIds(Long uid) {
List<Long> groupIds = groupMemberDao.getBaseMapper().selectList(new QueryWrapper<GroupMember>().eq("uid", uid)).stream().map(GroupMember::getGroupId).collect(Collectors.toList());
List<Long> groupIds = groupMemberDao.getBaseMapper().selectList(new QueryWrapper<GroupMember>().eq("uid", uid).eq("de_friend", 0)).stream().map(GroupMember::getGroupId).collect(Collectors.toList());
return roomGroupDao.getRoomIdByGroupId(groupIds);
if(CollUtil.isNotEmpty(groupIds)){
return roomGroupDao.getRoomIdByGroupId(groupIds);
}
return new ArrayList<>();
}
}

View File

@@ -8,6 +8,8 @@ import cn.hutool.core.util.ObjectUtil;
import com.alibaba.nacos.client.naming.utils.CollectionUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.common.collect.Lists;
import com.luohuo.basic.base.R;
import com.luohuo.basic.utils.SpringUtils;
import com.luohuo.basic.utils.TimeUtils;
import com.luohuo.flex.im.api.PresenceApi;
import com.luohuo.flex.im.core.chat.dao.*;
@@ -20,8 +22,6 @@ import com.luohuo.flex.model.enums.ChatActiveStatusEnum;
import jakarta.annotation.Nullable;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -64,7 +64,6 @@ import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -74,14 +73,11 @@ import static com.luohuo.flex.im.common.config.ThreadPoolConfig.LUOHUO_EXECUTOR;
@Slf4j
@AllArgsConstructor
public class ChatServiceImpl implements ChatService {
private final RoomDao roomDao;
private final UserFriendDao userFriendDao;
private final RoomGroupDao roomGroupDao;
private final GroupMemberCache groupMemberCache;
private MsgCache msgCache;
private MessageDao messageDao;
private UserDao userDao;
private ApplicationEventPublisher applicationEventPublisher;
private MessageMarkDao messageMarkDao;
private RoomFriendDao roomFriendDao;
private RoleService roleService;
@@ -116,7 +112,7 @@ public class ChatServiceImpl implements ChatService {
}
// 发布消息发送事件
applicationEventPublisher.publishEvent(new MessageSendEvent(this, new ChatMsgSendDto(msgId, uid)));
SpringUtils.publishEvent(new MessageSendEvent(this, new ChatMsgSendDto(msgId, uid)));
return msgId;
}
@@ -124,10 +120,13 @@ public class ChatServiceImpl implements ChatService {
Room room = roomCache.get(roomId);
Assert.notNull(room, "房间不存在!");
if (room.isRoomGroup()) {
RoomGroup roomGroup = roomGroupCache.get(roomId);
GroupMember member = groupMemberDao.getMember(roomGroup.getId(), uid);
// GroupMember member = groupMemberCache.getMemberDetail(roomId, uid);
GroupMember member = groupMemberDao.getMember(roomId, uid);
Assert.notNull(member, "您已经被移除该群");
Assert.isFalse(!isSend && member.getDeFriend(), "您已经屏蔽群聊!");
if (member.getDeFriend()) {
throw new BizException("你已屏蔽群聊,无法发送消息");
}
} else {
RoomFriend roomFriend = roomFriendDao.getByRoomId(roomId);
boolean u1State = uid.equals(roomFriend.getUid1());
@@ -182,7 +181,7 @@ public class ChatServiceImpl implements ChatService {
@Override
public ChatMessageResp getMsgResp(Message message, Long receiveUid) {
return CollUtil.getFirst(getMsgRespBatch(message.getRoomId(), Collections.singletonList(message), receiveUid));
return CollUtil.getFirst(getMsgRespBatch(Collections.singletonList(message), receiveUid));
}
@Override
@@ -191,14 +190,6 @@ public class ChatServiceImpl implements ChatService {
return getMsgResp(msg, receiveUid);
}
public Map<Long, Boolean> batchGetOnlineStatus(List<Long> uidList) {
if (CollectionUtils.isEmpty(uidList)) return Collections.emptyMap();
// 分页批量查询
return Lists.partition(uidList, 500).stream()
.map(chunk -> presenceApi.getUsersOnlineStatus(chunk.stream().collect(Collectors.toList())).getData())
.collect(HashMap::new, Map::putAll, Map::putAll);
}
private String generateCursor(ChatActiveStatusEnum type, String innerCursor) {
return type.name() + "_" + innerCursor;
}
@@ -209,12 +200,9 @@ public class ChatServiceImpl implements ChatService {
ChatActiveStatusEnum activeStatusEnum = pair.getKey();
String timeCursor = pair.getValue();
// 1. 批量获取所有成员在线状态(本地缓存优化)
Map<Long, Boolean> onlineStatusMap = batchGetOnlineStatus(memberUidList);
// 2. 分离在线用户与离线用户
List<Long> onlineUids = memberUidList.stream().filter(uid -> onlineStatusMap.get(uid)).collect(Collectors.toList());
List<Long> offlineUids = memberUidList.stream().filter(uid -> !onlineUids.contains(uid)).collect(Collectors.toList());
// 1. 批量获取所有成员在线状态、分离在线用户与离线用户
Set<Long> onlineUids = presenceApi.getOnlineUsersList(memberUidList).getData();
Set<Long> offlineUids = memberUidList.stream().filter(uid -> !onlineUids.contains(uid)).collect(Collectors.toSet());
// 3. 动态分页组装
List<ChatMemberResp> resultList = new ArrayList<>();
@@ -223,12 +211,12 @@ public class ChatServiceImpl implements ChatService {
// 在线列表
CursorPageBaseResp<User> onlinePage = userDao.getCursorPage(onlineUids, new CursorPageBaseReq(request.getPageSize(), timeCursor));
// 添加在线列表
resultList.addAll(MemberAdapter.buildMember(onlinePage.getList(), onlineStatusMap));
resultList.addAll(MemberAdapter.buildMember(onlinePage.getList(), onlineUids));
if (onlinePage.getIsLast()) {
// 如果是最后一页,从离线列表再补点数据
CursorPageBaseResp<User> offlinePage = userDao.getCursorPage(offlineUids, new CursorPageBaseReq(request.getPageSize() - onlinePage.getList().size(), null));
resultList.addAll(MemberAdapter.buildMember(offlinePage.getList(), onlineStatusMap));
resultList.addAll(MemberAdapter.buildMember(offlinePage.getList(), onlineUids));
timeCursor = generateCursor(ChatActiveStatusEnum.OFFLINE, offlinePage.getCursor());
isLast = offlinePage.getIsLast();
} else {
@@ -239,7 +227,7 @@ public class ChatServiceImpl implements ChatService {
// 离线列表
CursorPageBaseResp<User> cursorPage = userDao.getCursorPage(offlineUids, new CursorPageBaseReq(request.getPageSize(), timeCursor));
// 添加离线线列表
resultList.addAll(MemberAdapter.buildMember(cursorPage.getList(), onlineStatusMap));
resultList.addAll(MemberAdapter.buildMember(cursorPage.getList(), onlineUids));
timeCursor = cursorPage.getCursor();
isLast = cursorPage.getIsLast();
}
@@ -270,7 +258,7 @@ public class ChatServiceImpl implements ChatService {
if (cursorPage.isEmpty()) {
return CursorPageBaseResp.empty();
}
return CursorPageBaseResp.init(cursorPage, getMsgRespBatch(request.getRoomId(), cursorPage.getList(), receiveUid), cursorPage.getTotal());
return CursorPageBaseResp.init(cursorPage, getMsgRespBatch(cursorPage.getList(), receiveUid), cursorPage.getTotal());
}
// @Cacheable(value = "userRooms", key = "#uid", unless = "#result == null")
@@ -291,7 +279,7 @@ public class ChatServiceImpl implements ChatService {
}
// 2. 批量查询所有房间的最后一条消息ID用于权限过滤
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<Message>().in(Message::getRoomId, roomIds);
if(ObjectUtil.isNotNull(lastOptTime) && lastOptTime > 0) {
wrapper.ge(Message::getCreateTime, TimeUtils.getDateTimeOfTimestamp(lastOptTime)).le(Message::getCreateTime, LocalDateTime.now());
}else {
@@ -304,7 +292,7 @@ public class ChatServiceImpl implements ChatService {
// 4. 转换为响应对象并返回
List<ChatMessageResp> baseMessages = new ArrayList<>();
for (Long roomId : groupedMessages.keySet()) {
baseMessages.addAll(getMsgRespBatch(roomId, groupedMessages.get(roomId), receiveUid));
baseMessages.addAll(getMsgRespBatch(groupedMessages.get(roomId), receiveUid));
}
return baseMessages;
}
@@ -416,7 +404,7 @@ public class ChatServiceImpl implements ChatService {
return messageDao.listByIds(msgIds);
}
private void checkRecall(Long uid, Message message) {
private void checkRecall(Long uid, Message message) {
AssertUtil.isNotEmpty(message, "消息有误");
AssertUtil.notEqual(message.getType(), MessageTypeEnum.RECALL.getType(), "消息无法撤回");
boolean isChatManager = roleService.hasRole(uid, RoleTypeEnum.CHAT_MANAGER);
@@ -429,54 +417,13 @@ public class ChatServiceImpl implements ChatService {
AssertUtil.isTrue(between < 2, "超过2分钟的消息不能撤回");
}
public List<ChatMessageResp> getMsgRespBatch(Long roomId, List<Message> messages, Long receiveUid) {
public List<ChatMessageResp> getMsgRespBatch(List<Message> messages, Long receiveUid) {
if (CollectionUtil.isEmpty(messages)) {
return new ArrayList<>();
}
// 查询消息标志
List<MessageMark> msgMark = messageMarkDao.getValidMarkByMsgIdBatch(messages.stream().map(Message::getId).collect(Collectors.toList()));
List<ChatMessageResp> chatMessageResp = MessageAdapter.buildMsgResp(messages, msgMark, receiveUid);
Room room = roomDao.getById(roomId);
// 如果是群聊,设置消息的发送人的名称显示
try {
if (Objects.equals(room.getType(), RoomTypeEnum.GROUP.getType())) {
Set<Long> uidSet = messages.stream().map(Message::getFromUid).collect(Collectors.toSet());
RoomGroup roomGroup = roomGroupDao.getByRoomId(roomId);
Map<Long, GroupMember> groupMemberMap = groupMemberDao.getMemberBatch(roomGroup.getId(), uidSet).stream().collect(Collectors.toMap(GroupMember::getUid, Function.identity()));
Map<Long, User> userMap = userDao.getByIds(uidSet).stream().collect(Collectors.toMap(User::getId, Function.identity()));
Map<Long, String> userFriendMapByFriendUid = userFriendDao.getByFriends(receiveUid, uidSet)
.stream()
.filter(item ->StringUtils.isNotEmpty(item.getRemark()))
.collect(Collectors.toMap(UserFriend::getFriendUid, UserFriend::getRemark));
// 设置 user 名称
chatMessageResp.forEach(item -> {
ChatMessageResp.UserInfo fromUser = item.getFromUser();
long uid = Long.parseLong(fromUser.getUid());
GroupMember groupMember = groupMemberMap.get(uid);
String friendRemark = userFriendMapByFriendUid.get(uid);
// 优先显示好友备注
if (StringUtils.isNotEmpty(friendRemark)){
fromUser.setNickname(friendRemark);
}
else if (groupMember != null){
String myName = groupMember.getMyName();
if (StringUtils.isNotEmpty(myName)){
fromUser.setNickname(myName);
}else {
User user = userMap.get(uid);
fromUser.setNickname(user.getName());
}
}
});
}
} catch (Exception e) {
log.error(e.getMessage());
}
return chatMessageResp;
return MessageAdapter.buildMsgResp(messages, msgMark, receiveUid);
}
}

View File

@@ -62,8 +62,4 @@ public class ContactServiceImpl implements ContactService {
}).collect(Collectors.toMap(MsgReadInfoDTO::getMsgId, Function.identity()));
}
@Override
public Map<Long, Long> getLastMsgIds(List<Long> roomIds) {
return contactDao.getLastMsgIds(roomIds);
}
}

View File

@@ -1,15 +1,17 @@
package com.luohuo.flex.im.core.chat.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.Pair;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.luohuo.basic.cache.repository.CachePlusOps;
import com.luohuo.basic.exception.code.ResponseEnum;
import com.luohuo.basic.model.cache.CacheKey;
import com.luohuo.basic.utils.SpringUtils;
import com.luohuo.basic.utils.TimeUtils;
import com.luohuo.flex.common.cache.PresenceCacheKeyBuilder;
import com.luohuo.flex.im.api.PresenceApi;
import com.luohuo.flex.im.core.chat.dao.RoomFriendDao;
@@ -19,7 +21,6 @@ import com.luohuo.flex.im.core.user.dao.UserBackpackDao;
import com.luohuo.flex.im.core.user.dao.UserFriendDao;
import com.luohuo.flex.im.core.user.dao.UserPrivacyDao;
import com.luohuo.flex.im.domain.entity.*;
import com.luohuo.flex.im.domain.enums.ApplyEnum;
import com.luohuo.flex.im.domain.enums.ApplyStatusEnum;
import com.luohuo.flex.im.domain.vo.req.room.UserApplyResp;
import com.luohuo.flex.im.domain.vo.request.admin.AdminAddReq;
@@ -34,11 +35,11 @@ import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.CollectionUtils;
import com.luohuo.basic.exception.BizException;
import com.luohuo.basic.exception.code.GroupErrorEnum;
@@ -112,6 +113,7 @@ import java.util.stream.Collectors;
import static com.luohuo.flex.im.common.config.ThreadPoolConfig.LUOHUO_EXECUTOR;
import static com.luohuo.flex.im.core.chat.constant.GroupConst.MAX_MANAGE_COUNT;
import static com.luohuo.flex.im.domain.enums.ApplyReadStatusEnum.UNREAD;
@Slf4j
@Service
@@ -138,12 +140,12 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
private UserDao userDao;
private ChatService chatService;
private RoleService roleService;
private ApplicationEventPublisher applicationEventPublisher;
private RoomService roomService;
private GroupMemberCache groupMemberCache;
private PushService pushService;
private FriendService friendService;
private PresenceApi presenceApi;
private TransactionTemplate transactionTemplate;
private void warmUpGroupMemberCache(Long roomId) {
// 1. 查询房间中所有用户
@@ -346,14 +348,13 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
@Override
public CursorPageBaseResp<UserApplyResp> queryApplyPage(Long uid, MemberReq req) {
// 1. 校验权限
RoomGroup group = roomGroupCache.get(req.getRoomId());
GroupMember member = groupMemberDao.getMember(group.getId(), uid);
GroupMember member = groupMemberDao.getMember(req.getRoomId(), uid);
if (member == null || member.getRoleId() == GroupRoleEnum.MEMBER.getType()) {
throw new BizException("无权限查看申请列表");
}
// 2. 游标查询申请进群记录
CursorPageBaseResp<UserApply> userApplyPage = userApplyDao.getApplyPage(uid, 2, req);
CursorPageBaseResp<UserApply> userApplyPage = userApplyDao.getApplyPage(uid, RoomTypeEnum.GROUP.getType(), req);
return CursorPageBaseResp.init(userApplyPage, userApplyPage.getList().stream().map(apply -> {
UserApplyResp applyResp = new UserApplyResp();
@@ -377,12 +378,8 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
*/
@Async(LUOHUO_EXECUTOR)
public void asyncOnline(List<Long> uidList, Long roomId, boolean online) {
Map<Long, Boolean> map = presenceApi.getUsersOnlineStatus(uidList).getData();
uidList = map.entrySet().stream()
.filter(entry -> entry.getValue())
.map(Map.Entry::getKey)
.collect(Collectors.toList());
if(uidList.isEmpty()){
Set<Long> onlineList = presenceApi.getOnlineUsersList(uidList).getData();
if(CollUtil.isEmpty(onlineList)){
return;
}
@@ -392,7 +389,7 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
if(online) {
// 处理在线的状态
cachePlusOps.sAdd(ogmKey, uid.toString());
cachePlusOps.sAdd(ogmKey, uid);
cachePlusOps.sAdd(ougKey, roomId);
} else {
// 处理离线的状态
@@ -412,7 +409,7 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
throw new RuntimeException("群聊不存在!");
}
GroupMember groupMember = groupMemberDao.getMember(roomGroup.getId(), uid);
GroupMember groupMember = groupMemberDao.getMemberByGroupId(roomGroup.getId(), uid);
if(ObjectUtil.isNull(groupMember)){
throw new RuntimeException(StrUtil.format("您不是{}的群成员!", roomGroup.getName()));
}
@@ -498,6 +495,7 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
announcements.setUid(uid);
announcements.setTop(param.getTop());
announcements.setCreateTime(now);
announcements.setUpdateTime(now);
roomService.saveAnnouncements(announcements);
// 创建已读的信息
@@ -535,7 +533,7 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
announcements.setRoomId(param.getRoomId());
announcements.setContent(param.getContent());
announcements.setTop(param.getTop());
announcements.setCreateTime(announcement.getCreateTime());
announcements.setUpdateTime(TimeUtils.now());
Boolean edit = roomService.updateAnnouncement(announcements);
if(edit){
chatService.sendMsg(MessageAdapter.buildAnnouncementsMsg(param.getRoomId(), announcements), uid);
@@ -634,9 +632,8 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
Room room = roomCache.get(request.getRoomId());
if(room.getType().equals(RoomTypeEnum.GROUP.getType())){
// 1. 把群成员的信息设置为禁止
RoomGroup roomGroup = roomGroupCache.get(request.getRoomId());
name = roomGroup.getName();
groupMemberDao.setMemberDeFriend(roomGroup.getId(), uid, request.getState());
name = roomGroupCache.get(request.getRoomId()).getName();
groupMemberDao.setMemberDeFriend(request.getRoomId(), uid, request.getState());
} else {
// 2. 把两个人的房间全部设置为禁止
RoomFriend roomFriend = roomFriendCache.get(request.getRoomId());
@@ -695,7 +692,7 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
// 获取群成员数、在线人员、备注、我的群名称
Long onlineNum = map.get(room.getId());
Long memberNum = (long) groupMemberCache.getMemberUidList(roomId).size();
GroupMember member = groupMemberDao.getMember(roomGroup.getId(), uid);
GroupMember member = groupMemberDao.getMemberByGroupId(roomGroup.getId(), uid);
return MemberResp.builder()
.avatar(roomGroup.getAvatar())
@@ -735,26 +732,22 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
}
// 2. 获取群组和成员数据
RoomGroup roomGroup = roomGroupCache.get(request.getRoomId());
List<GroupMember> groupMembers = groupMemberDao.getMemberListByGroupId(roomGroup.getId());
List<ChatMemberResp> chatMemberResps = groupMemberDao.getMemberListByGroupId(roomGroupCache.get(request.getRoomId()).getId());
// 3. 转换为响应对象
List<ChatMemberResp> chatMemberResps = BeanUtil.copyToList(groupMembers, ChatMemberResp.class);
// 5. 批量获取用户信息
// 3. 批量获取用户信息
Set<Long> uids = chatMemberResps.stream().map(ChatMemberResp::getUid).map(Long::parseLong).collect(Collectors.toSet());
Map<Long, User> userInfoBatch = userCache.getUserInfoBatch(uids);
// 6. 批量获取在线状态
Map<Long, Boolean> onlineStatusMap = presenceApi.getUsersOnlineStatus(new ArrayList<>(uids)).getData();
// 5. 批量获取在线状态
Set<Long> onlineList = presenceApi.getOnlineUsersList(new ArrayList<>(uids)).getData();
// 7. 填充用户信息和在线状态
// 6. 填充用户信息和在线状态
chatMemberResps.forEach(item -> {
Long uid = Long.parseLong(item.getUid());
User user = userInfoBatch.get(uid);
if (user != null) {
item.setActiveStatus(onlineStatusMap.getOrDefault(uid, false) ? ChatActiveStatusEnum.ONLINE.getStatus() : ChatActiveStatusEnum.OFFLINE.getStatus());
item.setActiveStatus(onlineList.contains(uid) ? ChatActiveStatusEnum.ONLINE.getStatus() : ChatActiveStatusEnum.OFFLINE.getStatus());
item.setLastOptTime(user.getLastOptTime());
item.setName(user.getName());
item.setAvatar(user.getAvatar());
@@ -764,18 +757,19 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
});
// 7. 群主、管理员永远在前面
return chatMemberResps.stream()
.sorted((m1, m2) -> {
// 群主 > 管理员> 普通成员
int roleCompare = Integer.compare(m1.getRoleId(), m2.getRoleId());
// 如果是相同角色
if (roleCompare == 0) {
return Integer.compare(m1.getActiveStatus(), m2.getActiveStatus());
}
// 不同角色:管理组始终排在普通组前面
return roleCompare;
}).collect(Collectors.toList());
return chatMemberResps;
// return chatMemberResps.stream()
// .sorted((m1, m2) -> {
// // 群主 > 管理员> 普通成员
// int roleCompare = Integer.compare(m1.getRoleId(), m2.getRoleId());
//
// // 如果是相同角色
// if (roleCompare == 0) {
// return Integer.compare(m1.getActiveStatus(), m2.getActiveStatus());
// }
// // 不同角色:管理组始终排在普通组前面
// return roleCompare;
// }).collect(Collectors.toList());
}
@Override
@@ -795,19 +789,19 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
}
@Override
@Transactional(rollbackFor = Exception.class)
@RedissonLock(prefixKey = "delMember:", key = "#request.roomId")
public void delMember(Long uid, MemberDelReq request) {
Room room = roomCache.get(request.getRoomId());
AssertUtil.isNotEmpty(room, "房间号有误");
RoomGroup roomGroup = roomGroupCache.get(request.getRoomId());
AssertUtil.isNotEmpty(roomGroup, "房间号有误");
GroupMember self = groupMemberDao.getMember(roomGroup.getId(), uid);
GroupMember self = groupMemberDao.getMemberByGroupId(roomGroup.getId(), uid);
AssertUtil.isNotEmpty(self, GroupErrorEnum.USER_NOT_IN_GROUP, "groupMember");
// 如果房间人员小于3人 那么直接解散群聊
Long count = cachePlusOps.sCard(PresenceCacheKeyBuilder.groupMembersKey(request.getRoomId()));
if(count < 3){
CacheKey membersKey = PresenceCacheKeyBuilder.groupMembersKey(request.getRoomId());
Long count = cachePlusOps.sCard(membersKey);
if(count < 3 && self.getRoleId().equals(GroupRoleEnum.LEADER.getType())) {
MemberExitReq exitReq = new MemberExitReq();
exitReq.setRoomId(request.getRoomId());
exitReq.setAccount(roomGroup.getAccount());
@@ -826,54 +820,69 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
}
// 1.3 普通成员 判断是否有权限操作
AssertUtil.isTrue(hasPower(self), GroupErrorEnum.NOT_ALLOWED_FOR_REMOVE);
GroupMember member = groupMemberDao.getMember(roomGroup.getId(), removedUid);
GroupMember member = groupMemberDao.getMemberByGroupId(roomGroup.getId(), removedUid);
AssertUtil.isNotEmpty(member, "用户已经移除");
groupMemberDao.removeById(member.getId());
// 发送移除事件告知群成员
List<Long> memberUidList = groupMemberCache.getMemberExceptUidList(roomGroup.getRoomId());
if(!memberUidList.contains(request.getUid())){
memberUidList.add(request.getUid());
}
WsBaseResp<WSMemberChange> ws = MemberAdapter.buildMemberRemoveWS(roomGroup.getRoomId(), Arrays.asList(member.getUid()));
pushService.sendPushMsg(ws, memberUidList, uid);
groupMemberCache.evictMemberUidList(room.getId());
groupMemberCache.evictMemberDetail(room.getId(), removedUid);
// 新版移除群聊
CacheKey uKey = PresenceCacheKeyBuilder.userGroupsKey(uid);
CacheKey gKey = PresenceCacheKeyBuilder.groupMembersKey(room.getId());
cachePlusOps.sRem(gKey, uid);
cachePlusOps.sRem(uKey, room.getId());
asyncOnline(Arrays.asList(uid), room.getId(), false);
// 发送移除事件告知群成员
if(transactionTemplate.execute(e -> {
groupMemberDao.removeById(member.getId());
// 1.5 移除会话
contactDao.removeByRoomId(room.getId(), Collections.singletonList(request.getUid()));
return true;
})){
List<Long> memberUidList = groupMemberCache.getMemberExceptUidList(roomGroup.getRoomId());
if(!memberUidList.contains(request.getUid())){
memberUidList.add(request.getUid());
}
WsBaseResp<WSMemberChange> ws = MemberAdapter.buildMemberRemoveWS(roomGroup.getRoomId(), Arrays.asList(member.getUid()), WSMemberChange.CHANGE_TYPE_REMOVE);
pushService.sendPushMsg(ws, memberUidList, uid);
groupMemberCache.evictMemberUidList(room.getId());
groupMemberCache.evictMemberDetail(room.getId(), removedUid);
// 移除群聊缓存
CacheKey uKey = PresenceCacheKeyBuilder.userGroupsKey(request.getUid());
cachePlusOps.sRem(membersKey, request.getUid());
cachePlusOps.sRem(uKey, room.getId());
asyncOnline(Arrays.asList(request.getUid()), room.getId(), false);
}
}
@Override
@RedissonLock(key = "#request.roomId")
@Transactional(rollbackFor = Exception.class)
public void addMember(Long uid, MemberAddReq request) {
// 1. 校验数据
Room room = roomCache.get(request.getRoomId());
AssertUtil.isNotEmpty(room, "房间号有误");
RoomGroup roomGroup = roomGroupCache.get(request.getRoomId());
AssertUtil.isNotEmpty(roomGroup, "房间号有误");
GroupMember self = groupMemberDao.getMember(roomGroup.getId(), uid);
GroupMember self = groupMemberDao.getMemberByGroupId(roomGroup.getId(), uid);
AssertUtil.isNotEmpty(self, "您不是群成员");
// 已经进群了的
List<Long> memberBatch = groupMemberDao.getMemberBatch(roomGroup.getId(), request.getUidList()).stream().map(GroupMember::getUid).toList();
Set<Long> existUid = new HashSet<>(memberBatch);
List<Long> validUids = request.getUidList().stream().filter(a -> !existUid.contains(a)).distinct().collect(Collectors.toList());
// 已经邀请过的数据
List<Long> existingUsers = userApplyDao.getExistingUsers(request.getRoomId(), request.getUidList());
HashSet<Long> validUidSet = request.getUidList();
validUidSet.removeAll(memberBatch);
validUidSet.removeAll(existingUsers);
List<Long> validUids = new ArrayList<>(validUidSet);
if (CollectionUtils.isEmpty(validUids)) {
return;
}
// 2. 创建邀请记录
List<UserApply> invites = validUids.stream().map(inviteeUid -> new UserApply(uid, ApplyEnum.GROUP.getCode(), roomGroup.getRoomId(), inviteeUid, StrUtil.format("{}邀请你加入{}", userCache.getUserInfo(uid).getName(), roomGroup.getName()), ApplyStatusEnum.WAIT_APPROVAL.getCode(), 1, 0)).collect(Collectors.toList());
userApplyDao.saveBatch(invites);
transactionTemplate.execute(e -> {
List<UserApply> invites = validUids.stream().map(inviteeUid -> new UserApply(uid, RoomTypeEnum.GROUP.getType(), roomGroup.getRoomId(), inviteeUid, StrUtil.format("{}邀请你加入{}", userCache.getUserInfo(uid).getName(), roomGroup.getName()), ApplyStatusEnum.WAIT_APPROVAL.getCode(), UNREAD.getCode(), 0, false)).collect(Collectors.toList());
userApplyDao.saveBatch(invites);
return true;
});
// 3. 通知被邀请的人进群
validUids.forEach(inviteId -> {
User invite = userCache.getUserInfo(inviteId);
if(Objects.isNull(invite)){
pushService.sendPushMsg(MessageAdapter.buildInviteeUserAddGroupMessage(uid, inviteId, roomGroup.getId()), inviteId, uid);
User user = userCache.getUserInfo(inviteId);
if(ObjectUtil.isNotNull(user)){
pushService.sendPushMsg(MessageAdapter.buildInviteeUserAddGroupMessage(userApplyDao.getUnReadCount(inviteId, inviteId)), inviteId, uid);
}
});
}
@@ -885,7 +894,6 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
* @param request 请求信息
*/
@Override
@Transactional(rollbackFor = Exception.class)
@RedissonLock(prefixKey = "exitGroup:", key = "#request.roomId")
public void exitGroup(Long uid, MemberExitReq request) {
Long roomId = request.getRoomId();
@@ -901,27 +909,36 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
Boolean isGroupShip = groupMemberDao.isGroupShip(roomGroup.getRoomId(), Collections.singletonList(uid));
AssertUtil.isTrue(isGroupShip, GroupErrorEnum.USER_NOT_IN_GROUP);
// 4. 判断该用户是否是群主
// 5. 获取要移除的群成员
Boolean isLord = groupMemberDao.isLord(roomGroup.getId(), uid);
List<Long> memberUidList;
if (isLord) {
memberUidList = groupMemberDao.getMemberUidList(roomGroup.getId(), null);
} else {
memberUidList = groupMemberCache.getMemberExceptUidList(roomGroup.getRoomId());
}
if (isLord) {
// 4.1 删除房间和群并清除缓存
boolean isDelRoom = roomService.removeById(roomId);
roomGroupCache.removeById(roomGroup.getId());
roomGroupCache.evictGroup(roomGroup.getAccount());
if(StrUtil.isNotEmpty(request.getAccount())){
roomGroupCache.evictGroup(request.getAccount());
}
AssertUtil.isTrue(isDelRoom, ResponseEnum.SYSTEM_BUSY.getMsg());
// 4.2 删除会话
Boolean isDelContact = contactDao.removeByRoomId(roomId, Collections.EMPTY_LIST);
AssertUtil.isTrue(isDelContact, ResponseEnum.SYSTEM_BUSY.getMsg());
// 4.3 获取并删除群成员
List<Long> memberUidList = groupMemberDao.getMemberUidList(roomGroup.getId(), null);
Boolean isDelGroupMember = groupMemberDao.removeByGroupId(roomGroup.getId(), Collections.EMPTY_LIST);
AssertUtil.isTrue(isDelGroupMember, ResponseEnum.SYSTEM_BUSY.getMsg());
// 4.4 删除消息记录 (逻辑删除)
Boolean isDelMessage = messageDao.removeByRoomId(roomId, Collections.EMPTY_LIST);
AssertUtil.isTrue(isDelMessage, ResponseEnum.SYSTEM_BUSY.getMsg());
transactionTemplate.execute(e -> {
boolean isDelRoom = roomService.removeById(roomId);
roomGroupCache.removeById(roomGroup.getId());
roomGroupCache.evictGroup(roomGroup.getAccount());
if(StrUtil.isNotEmpty(request.getAccount())){
roomGroupCache.evictGroup(request.getAccount());
}
AssertUtil.isTrue(isDelRoom, ResponseEnum.SYSTEM_BUSY.getMsg());
// 4.2 删除会话
Boolean isDelContact = contactDao.removeByRoomId(roomId, Collections.EMPTY_LIST);
AssertUtil.isTrue(isDelContact, "会话移除异常");
// 4.3 删除群成员
Boolean isDelGroupMember = groupMemberDao.removeByGroupId(roomGroup.getId(), Collections.EMPTY_LIST);
AssertUtil.isTrue(isDelGroupMember, "群成员移除失败");
// 4.4 删除消息记录 (逻辑删除)
Boolean isDelMessage = messageDao.removeByRoomId(roomId, Collections.EMPTY_LIST);
AssertUtil.isTrue(isDelMessage, ResponseEnum.SYSTEM_BUSY.getMsg());
return true;
});
// 4.5 告知所有人群已经被解散, 这里要走groupMemberDao查询缓存中可能没有屏蔽群的用户
groupMemberCache.evictMemberUidList(room.getId());
groupMemberCache.evictAllMemberDetails();
@@ -932,15 +949,17 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
asyncOnline(memberUidList, room.getId(), false);
pushService.sendPushMsg(RoomAdapter.buildGroupDissolution(roomGroup.getName()), memberUidList, uid);
} else {
// 4.6 删除会话
Boolean isDelContact = contactDao.removeByRoomId(roomId, Collections.singletonList(uid));
AssertUtil.isTrue(isDelContact, ResponseEnum.SYSTEM_BUSY.getMsg());
// 4.7 删除群成员
Boolean isDelGroupMember = groupMemberDao.removeByGroupId(roomGroup.getId(), Collections.singletonList(uid));
AssertUtil.isTrue(isDelGroupMember, ResponseEnum.SYSTEM_BUSY.getMsg());
transactionTemplate.execute(e -> {
// 4.6 删除会话
Boolean isDelContact = contactDao.removeByRoomId(roomId, Collections.singletonList(uid));
AssertUtil.isTrue(isDelContact, "会话移除异常");
// 4.7 删除群成员
Boolean isDelGroupMember = groupMemberDao.removeByGroupId(roomGroup.getId(), Collections.singletonList(uid));
AssertUtil.isTrue(isDelGroupMember, "群成员移除失败");
return true;
});
// 4.8 发送移除事件告知群成员
List<Long> memberUidList = groupMemberCache.getMemberExceptUidList(roomGroup.getRoomId());
WsBaseResp<WSMemberChange> ws = MemberAdapter.buildMemberRemoveWS(roomGroup.getRoomId(), Arrays.asList(uid));
WsBaseResp<WSMemberChange> ws = MemberAdapter.buildMemberRemoveWS(roomGroup.getRoomId(), Arrays.asList(uid), WSMemberChange.CHANGE_TYPE_QUIT);
pushService.sendPushMsg(ws, memberUidList, uid);
groupMemberCache.evictMemberUidList(room.getId());
groupMemberCache.evictMemberDetail(room.getId(), uid);
@@ -972,6 +991,7 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
// 处理新房间里面所有在线人员
List<Long> uidList = new ArrayList<>(request.getUidList());
uidList.add(uid);
groupMemberCache.evictMemberUidList(roomGroup.getRoomId());
CacheKey gKey = PresenceCacheKeyBuilder.groupMembersKey(roomGroup.getRoomId());
uidList.forEach(id -> {
@@ -980,7 +1000,7 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
});
asyncOnline(uidList, roomGroup.getRoomId(), true);
applicationEventPublisher.publishEvent(new GroupMemberAddEvent(this, roomGroup.getRoomId(), request.getUidList(), uid));
SpringUtils.publishEvent(new GroupMemberAddEvent(this, roomGroup.getRoomId(), request.getUidList(), uid));
return roomGroup.getRoomId();
}
@@ -994,7 +1014,7 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
* 获取群角色
*/
private Integer getGroupRole(Long uid, RoomGroup roomGroup, Room room) {
GroupMember member = Objects.isNull(uid) ? null : groupMemberDao.getMember(roomGroup.getId(), uid);
GroupMember member = Objects.isNull(uid) ? null : groupMemberDao.getMemberByGroupId(roomGroup.getId(), uid);
if (Objects.nonNull(member)) {
return GroupRoleAPPEnum.of(member.getRoleId()).getType();
} else if (isHotGroup(room)) {
@@ -1031,8 +1051,7 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
Map<Long, Message> msgMap = messages.stream().collect(Collectors.toMap(Message::getId, Function.identity()));
Map<Long, User> lastMsgUidMap = userInfoCache.getBatch(messages.stream().map(Message::getFromUid).collect(Collectors.toList()));
// 消息未读数
Map<Long, Integer> unReadCountMap = getUnReadCountMap(uid, roomIds);
Map<Long, RoomGroup> roomGroupMapByRoomId = roomGroupDao.listByRoomIds(roomIds).stream().collect(Collectors.toMap(RoomGroup::getRoomId, Function.identity()));
Map<Long, Integer> unReadCountMap = getUnReadCountMap(contactMap.values());
return roomBaseInfoMap.values().stream().map(room -> {
ChatRoomResp resp = new ChatRoomResp();
@@ -1063,32 +1082,29 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
resp.setRemark(roomBaseInfo.getRemark());
resp.setMyName(roomBaseInfo.getMyName());
Message message = msgMap.get(room.getLastMsgId());
if (Objects.nonNull(message)) {
AbstractMsgHandler strategyNoNull = MsgHandlerFactory.getStrategyNoNull(message.getType());
// 判断是群聊还是单聊
if (Objects.equals(roomBaseInfo.getType(), RoomTypeEnum.GROUP.getType())) {
RoomGroup roomGroup = roomGroupMapByRoomId.get(roomId);
GroupMember messageUser = null;
try {
messageUser = groupMemberCache.getMemberDetail(roomGroup.getId(), message.getFromUid());
} catch (Exception e) {
log.error("群聊成员获取失败-->", e.getMessage());
}
if(resp.getShield()){
resp.setText("您已屏蔽该会话");
} else {
if (Objects.nonNull(message)) {
AbstractMsgHandler strategyNoNull = MsgHandlerFactory.getStrategyNoNull(message.getType());
// 判断是群聊还是单聊
if (Objects.equals(roomBaseInfo.getType(), RoomTypeEnum.GROUP.getType())) {
GroupMember messageUser = groupMemberCache.getMemberDetail(roomId, message.getFromUid());
if (ObjectUtil.isNotNull(messageUser)) {
if (StrUtil.isNotEmpty(messageUser.getMyName())) {
resp.setText(messageUser.getMyName() + ":" + strategyNoNull.showContactMsg(message));
}
if (ObjectUtil.isNotNull(messageUser)) {
if (StrUtil.isNotEmpty(messageUser.getMyName())){
resp.setText(messageUser.getMyName() + ":" + strategyNoNull.showContactMsg(message));
}
// 当自己查看时,且最后一条消息是自己发送的,那么显示群备注
if (uid.equals(message.getFromUid()) && StrUtil.isNotEmpty(messageUser.getRemark())){
resp.setRemark(messageUser.getRemark());
// 当自己查看时,且最后一条消息是自己发送的,那么显示群备注
if (uid.equals(message.getFromUid()) && StrUtil.isNotEmpty(messageUser.getRemark())){
resp.setRemark(messageUser.getRemark());
}
} else {
resp.setText((lastMsgUidMap.get(message.getFromUid()).getName()) + ":" + strategyNoNull.showContactMsg(message));
}
} else {
resp.setText((lastMsgUidMap.get(message.getFromUid()).getName()) + ":" + strategyNoNull.showContactMsg(message));
resp.setText(strategyNoNull.showContactMsg(message));
}
} else {
resp.setText(strategyNoNull.showContactMsg(message));
}
}
resp.setUnreadCount(unReadCountMap.getOrDefault(roomId, 0));
@@ -1100,16 +1116,20 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
/**
* 获取未读数
*/
private Map<Long, Integer> getUnReadCountMap(Long uid, List<Long> roomIds) {
if (Objects.isNull(uid)) {
public Map<Long, Integer> getUnReadCountMap(Collection<Contact> contactList) {
if (CollUtil.isEmpty(contactList)) {
return new HashMap<>();
}
List<Contact> contacts = contactDao.getByRoomIds(roomIds, uid);
return contacts.parallelStream()
.map(contact -> Pair.of(contact.getRoomId(), messageDao.getUnReadCount(contact.getRoomId(), contact.getReadTime(), uid)))
.collect(Collectors.toMap(Pair::getKey, Pair::getValue));
return messageDao.batchGetUnReadCount(contactList);
}
/**
* 返回房间id与好友的映射
* @param roomIds
* @param uid
* @return
*/
private Map<Long, User> getFriendRoomMap(List<Long> roomIds, Long uid) {
if (CollectionUtil.isEmpty(roomIds)) {
return new HashMap<>();
@@ -1135,9 +1155,6 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
// 获取好友信息
List<Long> friendRoomId = groupRoomIdMap.get(RoomTypeEnum.FRIEND.getType());
Map<Long, User> friendRoomMap = getFriendRoomMap(friendRoomId, uid);
Map<Long, Long> remove = new HashMap<>();
Map<Long, RoomBaseInfo> collect = roomMap.values().stream().filter(Objects::nonNull).map(room -> {
RoomBaseInfo roomBaseInfo = new RoomBaseInfo();
roomBaseInfo.setRoomId(room.getId());
@@ -1148,19 +1165,18 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
if (RoomTypeEnum.of(room.getType()) == RoomTypeEnum.GROUP) {
RoomGroup roomGroup = roomInfoBatch.get(room.getId());
roomBaseInfo.setId(roomGroup.getId());
roomBaseInfo.setName(roomGroup.getName());
roomBaseInfo.setAvatar(roomGroup.getAvatar());
roomBaseInfo.setAccount(roomGroup.getAccount());
GroupMember member = null;
try {
member = groupMemberCache.getMemberDetail(roomBaseInfo.getId(), uid);
} catch (Exception e) {
remove.put(roomBaseInfo.getId(), uid);
}
GroupMember member = groupMemberCache.getMemberDetail(room.getId(), uid);
// todo 稳定了这里可以不用判空理论上100% 在群里
if (ObjectUtil.isNotNull(member)) {
roomBaseInfo.setMyName(member.getMyName());
roomBaseInfo.setRemark(member.getRemark());
if(StrUtil.isNotEmpty(member.getRemark())){
roomBaseInfo.setName(member.getRemark());
} else {
roomBaseInfo.setName(roomGroup.getName());
}
roomBaseInfo.setRoleId(member.getRoleId());
}else {
roomBaseInfo.setMyName("会话异常");
@@ -1178,9 +1194,6 @@ public class RoomAppServiceImpl implements RoomAppService, InitializingBean {
return roomBaseInfo;
}).collect(Collectors.toMap(RoomBaseInfo::getRoomId, Function.identity()));
if (CollectionUtil.isNotEmpty(remove)) {
log.info("需要删除的数据", remove);
}
return collect;
}
}

View File

@@ -86,7 +86,7 @@ public class RoomServiceImpl implements RoomService {
@Transactional(rollbackFor = Exception.class)
public RoomGroup createGroupRoom(Long uid, GroupAddReq groupAddReq) {
List<GroupMember> selfGroup = groupMemberDao.getSelfGroup(uid);
AssertUtil.isTrue(selfGroup.size() < 60, "每个人只能创建五个群");
AssertUtil.isTrue(selfGroup.size() < 5, "每个人只能创建五个群");
User user = userInfoCache.get(uid);
Room room = createRoom(RoomTypeEnum.GROUP);
// 插入群

View File

@@ -7,14 +7,18 @@ import com.luohuo.flex.im.common.utils.CursorUtils;
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.ApplyTypeEnum;
import com.luohuo.flex.im.core.user.mapper.UserApplyMapper;
import com.luohuo.flex.im.domain.enums.RoomTypeEnum;
import com.luohuo.flex.im.domain.vo.req.CursorPageBaseReq;
import com.luohuo.flex.im.domain.vo.res.CursorPageBaseResp;
import com.luohuo.flex.im.domain.vo.resp.friend.FriendUnreadDto;
import com.luohuo.flex.model.entity.ws.WSFriendApply;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.stream.Collectors;
import static com.luohuo.flex.im.domain.enums.ApplyReadStatusEnum.READ;
import static com.luohuo.flex.im.domain.enums.ApplyReadStatusEnum.UNREAD;
@@ -34,7 +38,7 @@ public class UserApplyDao extends ServiceImpl<UserApplyMapper, UserApply> {
* 获取用户进群审批列表
*/
public CursorPageBaseResp<UserApply> getApplyPage(Long uid, Integer type, CursorPageBaseReq request) {
return CursorUtils.getCursorPageByMysql(this, request, wrapper -> wrapper.eq(UserApply::getTargetId, uid).eq(UserApply::getType, type), UserApply::getCreateTime);
return CursorUtils.getCursorPageByMysql(this, request, wrapper -> wrapper.eq(UserApply::getTargetId, uid).eq(UserApply::getType, type).eq(UserApply::getApplyFor, true), UserApply::getCreateTime);
}
/**
@@ -48,17 +52,31 @@ public class UserApplyDao extends ServiceImpl<UserApplyMapper, UserApply> {
return lambdaQuery().eq(UserApply::getUid, uid)
.eq(UserApply::getTargetId, targetUid)
.eq(UserApply::getStatus, ApplyStatusEnum.WAIT_APPROVAL.getCode())
.eq(UserApply::getType, ApplyTypeEnum.ADD_FRIEND.getCode())
.eq(UserApply::getType, RoomTypeEnum.FRIEND.getType())
.notIn(initiator,UserApply::getDeleted,ApplyDeletedEnum.applyDeleted())
.notIn(!initiator,UserApply::getDeleted,ApplyDeletedEnum.targetDeleted())
.one();
}
public Integer getUnReadCount(Long targetId) {
return Math.toIntExact(lambdaQuery().eq(UserApply::getTargetId, targetId)
.eq(UserApply::getReadStatus, UNREAD.getCode())
.eq(UserApply::getDeleted,ApplyDeletedEnum.NORMAL.getCode())
.count());
/**
* 返回好友、群聊申请的未读数量
* @param targetId
* @return
*/
public WSFriendApply getUnReadCount(Long uid, Long targetId) {
List<FriendUnreadDto> unReadCountByTypeMap = baseMapper.getUnReadCountByType(targetId, UNREAD.getCode(), ApplyDeletedEnum.NORMAL.getCode());
WSFriendApply wsFriendApply = new WSFriendApply();
wsFriendApply.setUid(uid);
// 构造需要的数据
for (FriendUnreadDto friendUnreadDto : unReadCountByTypeMap) {
if(friendUnreadDto.getType().equals(RoomTypeEnum.FRIEND.getType())){
wsFriendApply.setUnReadCount4Friend(friendUnreadDto.getCount());
} else {
wsFriendApply.setUnReadCount4Group(friendUnreadDto.getCount());
}
}
return wsFriendApply;
}
public IPage<UserApply> friendApplyPage(Long uid, Page<UserApply> page) {
@@ -86,7 +104,7 @@ public class UserApplyDao extends ServiceImpl<UserApplyMapper, UserApply> {
.update();
}
public void updateStatus(Long applyId,ApplyStatusEnum statusEnum) {
public void updateStatus(Long applyId, ApplyStatusEnum statusEnum) {
lambdaUpdate().set(UserApply::getStatus, statusEnum.getCode())
.set(UserApply::getUpdateTime, LocalDateTime.now())
.eq(UserApply::getId,applyId)
@@ -99,4 +117,12 @@ public class UserApplyDao extends ServiceImpl<UserApplyMapper, UserApply> {
.eq(UserApply::getId, applyId)
.update();
}
public List<Long> getExistingUsers(Long roomId, HashSet<Long> uidList) {
return lambdaQuery()
.eq(UserApply::getRoomId, roomId)
.eq(UserApply::getStatus, ApplyStatusEnum.WAIT_APPROVAL.getCode())
.in(UserApply::getTargetId, uidList)
.list().stream().map(UserApply::getTargetId).collect(Collectors.toList());
}
}

View File

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luohuo.flex.im.common.enums.NormalOrNoEnum;
import com.luohuo.flex.im.domain.vo.req.CursorPageBaseReq;
import com.luohuo.flex.im.domain.vo.req.user.ModifyNameReq;
import com.luohuo.flex.im.domain.vo.res.CursorPageBaseResp;
import com.luohuo.flex.im.common.utils.CursorUtils;
import com.luohuo.flex.im.domain.vo.response.ChatMemberListResp;
@@ -31,10 +32,11 @@ public class UserDao extends ServiceImpl<UserMapper, User> {
return getOne(wrapper);
}
public void modifyName(Long uid, String name) {
public void modifyName(Long uid, ModifyNameReq req) {
User update = new User();
update.setId(uid);
update.setName(name);
update.setName(req.getName());
update.setResume(req.getResume());
updateById(update);
}
@@ -68,7 +70,7 @@ public class UserDao extends ServiceImpl<UserMapper, User> {
/**
* @param memberUidList 在线或离线的群成员id
*/
public CursorPageBaseResp<User> getCursorPage(List<Long> memberUidList, CursorPageBaseReq request) {
public CursorPageBaseResp<User> getCursorPage(Set<Long> memberUidList, CursorPageBaseReq request) {
if(memberUidList == null || memberUidList.size() == 0){
return new CursorPageBaseResp<>();
}

View File

@@ -1,9 +1,13 @@
package com.luohuo.flex.im.core.user.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.luohuo.flex.im.domain.vo.resp.friend.FriendUnreadDto;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import com.luohuo.flex.im.domain.entity.UserApply;
import java.util.List;
/**
* <p>
* 用户申请表 Mapper 接口
@@ -14,4 +18,5 @@ import com.luohuo.flex.im.domain.entity.UserApply;
@Repository
public interface UserApplyMapper extends BaseMapper<UserApply> {
List<FriendUnreadDto> getUnReadCountByType(@Param("targetId") Long targetId, @Param("readStatus") Integer readStatus, @Param("normal") Integer normal);
}

View File

@@ -3,13 +3,12 @@ package com.luohuo.flex.im.core.user.service;
import com.luohuo.flex.im.domain.entity.UserApply;
import com.luohuo.flex.im.domain.vo.req.PageBaseReq;
import com.luohuo.flex.im.domain.vo.req.friend.FriendApplyReq;
import com.luohuo.flex.im.domain.vo.req.friend.FriendApproveReq;
import com.luohuo.flex.im.domain.vo.request.RoomApplyReq;
import com.luohuo.flex.im.domain.vo.request.member.ApplyReq;
import com.luohuo.flex.im.domain.vo.request.member.GroupApplyHandleReq;
import com.luohuo.flex.im.domain.vo.res.PageBaseResp;
import com.luohuo.flex.im.domain.vo.resp.friend.FriendApplyResp;
import com.luohuo.flex.im.domain.vo.resp.friend.FriendUnreadResp;
import com.luohuo.flex.model.entity.ws.WSFriendApply;
import jakarta.validation.Valid;
/**
@@ -28,7 +27,7 @@ public interface ApplyService {
* @param request 请求
* @param uid uid
*/
UserApply apply(Long uid, FriendApplyReq request);
UserApply handlerApply(Long uid, FriendApplyReq request);
/**
* 申请加群
@@ -49,7 +48,7 @@ public interface ApplyService {
* @param uid uid
* @param request 请求
*/
void acceptInvite(Long uid, @Valid ApplyReq request);
void handlerApply(Long uid, @Valid ApplyReq request);
/**
* 分页查询好友申请
@@ -62,25 +61,9 @@ public interface ApplyService {
/**
* 申请未读数
*
* @return {@link FriendUnreadResp}
* @return {@link WSFriendApply}
*/
FriendUnreadResp unread(Long uid);
/**
* 拒绝
*
* @param uid uid
* @param request 请求
*/
void reject(Long uid, FriendApproveReq request);
/**
* 忽略申请
*
* @param uid uid
* @param request 请求
*/
void ignore(Long uid, FriendApproveReq request);
WSFriendApply unread(Long uid);
/**
* 删除申请
@@ -88,5 +71,5 @@ public interface ApplyService {
* @param uid uid
* @param request 请求
*/
void deleteApprove(Long uid, FriendApproveReq request);
void deleteApprove(Long uid, ApplyReq request);
}

View File

@@ -9,7 +9,6 @@ import com.luohuo.flex.im.domain.vo.req.friend.FriendCheckReq;
import com.luohuo.flex.im.domain.vo.req.friend.FriendReq;
import com.luohuo.flex.im.domain.vo.resp.friend.FriendCheckResp;
import com.luohuo.flex.im.domain.vo.resp.friend.FriendResp;
import com.luohuo.flex.im.domain.vo.resp.friend.FriendUnreadResp;
import jakarta.validation.Valid;
import java.util.List;
@@ -47,13 +46,6 @@ public interface FriendService {
*/
void createUserApply(Long uid, Long roomId, Long targetId, String msg, Integer type);
/**
* 申请未读数
*
* @return {@link FriendUnreadResp}
*/
FriendUnreadResp unread(Long uid);
/**
* 与系统用户创建好友关系
* 与系统用户创建聊天框

View File

@@ -25,6 +25,12 @@ import java.util.List;
*/
public interface UserService {
/**
* 校验邮箱是否存在
* @param email 邮箱
*/
Boolean checkEmail(String email);
/**
* @param defUserId 主系统的userId
* @param tenantId
@@ -54,7 +60,7 @@ public interface UserService {
* @param uid
* @param req
*/
void modifyName(Long uid, ModifyNameReq req);
void modifyInfo(Long uid, ModifyNameReq req);
/**

View File

@@ -2,17 +2,18 @@ package com.luohuo.flex.im.core.user.service.adapter;
import com.luohuo.flex.im.domain.entity.UserApply;
import com.luohuo.flex.im.domain.entity.UserFriend;
import com.luohuo.flex.im.domain.enums.RoomTypeEnum;
import com.luohuo.flex.model.enums.ChatActiveStatusEnum;
import com.luohuo.flex.im.domain.vo.req.friend.FriendApplyReq;
import com.luohuo.flex.im.domain.vo.resp.friend.FriendApplyResp;
import com.luohuo.flex.im.domain.vo.resp.friend.FriendResp;
import java.util.List;
import java.util.Map;
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.ApplyTypeEnum.ADD_FRIEND;
/**
@@ -26,7 +27,7 @@ public class FriendAdapter {
userApplyNew.setUid(uid);
userApplyNew.setRoomId(0L);
userApplyNew.setMsg(request.getMsg());
userApplyNew.setType(ADD_FRIEND.getCode());
userApplyNew.setType(RoomTypeEnum.FRIEND.getType());
userApplyNew.setTargetId(request.getTargetUid());
userApplyNew.setStatus(WAIT_APPROVAL.getCode());
userApplyNew.setReadStatus(UNREAD.getCode());
@@ -49,10 +50,10 @@ public class FriendAdapter {
/**
* @param friendPage 好友列表
* @param userOnlineMap 用户在线状态映射
* @param onlineList 在线用户id
* @return
*/
public static List<FriendResp> buildFriend(List<UserFriend> friendPage, Map<Long, Boolean> userOnlineMap) {
public static List<FriendResp> buildFriend(List<UserFriend> friendPage, Set<Long> onlineList) {
Map<Long, UserFriend> friendHashMap = friendPage.stream().collect(Collectors.toMap(friend -> friend.getFriendUid(), Function.identity()));
return friendPage.stream().map(userFriend -> {
FriendResp resp = new FriendResp();
@@ -60,7 +61,7 @@ public class FriendAdapter {
UserFriend friend = friendHashMap.get(userFriend.getFriendUid());
resp.setHideMyPosts(friend.getHideMyPosts());
resp.setHideTheirPosts(friend.getHideTheirPosts());
resp.setActiveStatus(userOnlineMap.get(userFriend.getFriendUid())? ChatActiveStatusEnum.ONLINE.getStatus(): ChatActiveStatusEnum.OFFLINE.getStatus());
resp.setActiveStatus(onlineList.contains(userFriend.getFriendUid())? ChatActiveStatusEnum.ONLINE.getStatus(): ChatActiveStatusEnum.OFFLINE.getStatus());
return resp;
}).collect(Collectors.toList());
}

View File

@@ -1,6 +1,7 @@
package com.luohuo.flex.im.core.user.service.adapter;
import cn.hutool.core.util.StrUtil;
import com.luohuo.flex.im.domain.enums.RoomTypeEnum;
import com.luohuo.flex.im.domain.vo.req.room.UserApplyResp;
import com.luohuo.flex.model.entity.ws.*;
import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
@@ -56,7 +57,9 @@ public class WsAdapter {
WsBaseResp<WSMsgRecall> wsBaseResp = new WsBaseResp<>();
wsBaseResp.setType(WSRespTypeEnum.MSG_RECALL.getType());
WSMsgRecall recall = new WSMsgRecall();
BeanUtils.copyProperties(recallDTO, recall);
recall.setRecallUid(recallDTO.getRecallUid()+"");
recall.setMsgId(recallDTO.getMsgId()+"");
recall.setRoomId(recallDTO.getRoomId()+"");
wsBaseResp.setData(recall);
return wsBaseResp;
}
@@ -84,7 +87,7 @@ public class WsAdapter {
public static WsBaseResp<WSFriendApply> buildApplySend(WSFriendApply resp) {
WsBaseResp<WSFriendApply> wsBaseResp = new WsBaseResp<>();
wsBaseResp.setType(WSRespTypeEnum.REQUEST_NEW_FRIEND.getType());
wsBaseResp.setType(WSRespTypeEnum.NEW_APPLY.getType());
wsBaseResp.setData(resp);
return wsBaseResp;
}
@@ -116,7 +119,7 @@ public class WsAdapter {
public static WsBaseResp<UserApplyResp> buildApplyResultWS(Long uid, Long roomId, Long targetId, String msg, Integer status, Integer readStatus) {
WsBaseResp<UserApplyResp> wsBaseResp = new WsBaseResp<>();
wsBaseResp.setType(WSRespTypeEnum.GROUP_APPLY_NOTICE.getType());
wsBaseResp.setData(new UserApplyResp(uid, 2, roomId, targetId, msg, status, readStatus, LocalDateTime.now()));
wsBaseResp.setData(new UserApplyResp(uid, RoomTypeEnum.GROUP.getType(), roomId, targetId, msg, status, readStatus, true, LocalDateTime.now()));
return wsBaseResp;
}
}

View File

@@ -40,30 +40,33 @@ import com.luohuo.flex.im.domain.entity.User;
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.ApplyEnum;
import com.luohuo.flex.im.domain.enums.ApplyReadStatusEnum;
import com.luohuo.flex.im.domain.enums.ApplyStatusEnum;
import com.luohuo.flex.im.domain.enums.GroupRoleEnum;
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.friend.FriendApplyReq;
import com.luohuo.flex.im.domain.vo.req.friend.FriendApproveReq;
import com.luohuo.flex.im.domain.vo.request.RoomApplyReq;
import com.luohuo.flex.im.domain.vo.request.member.ApplyReq;
import com.luohuo.flex.im.domain.vo.request.member.GroupApplyHandleReq;
import com.luohuo.flex.im.domain.vo.res.PageBaseResp;
import com.luohuo.flex.im.domain.vo.resp.friend.FriendApplyResp;
import com.luohuo.flex.im.domain.vo.resp.friend.FriendUnreadResp;
import com.luohuo.flex.model.entity.ws.WSFriendApply;
import com.luohuo.flex.model.redis.annotation.RedissonLock;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static com.luohuo.flex.im.domain.enums.ApplyStatusEnum.AGREE;
import static com.luohuo.flex.im.domain.enums.ApplyStatusEnum.WAIT_APPROVAL;
/**
@@ -89,6 +92,7 @@ public class ApplyServiceImpl implements ApplyService {
private FriendService friendService;
private CachePlusOps cachePlusOps;
private RoomAppService roomAppService;
private TransactionTemplate transactionTemplate;
/**
* 申请好友
@@ -96,9 +100,8 @@ public class ApplyServiceImpl implements ApplyService {
* @param request 请求
*/
@Override
@RedissonLock(prefixKey = "friend:apply", key = "#uid")
@Transactional(rollbackFor = Exception.class)
public UserApply apply(Long uid, FriendApplyReq request) {
@RedissonLock(prefixKey = "friend:handlerApply", key = "#uid")
public UserApply handlerApply(Long uid, FriendApplyReq request) {
//是否有好友关系
UserFriend friend = userFriendDao.getByFriend(uid, request.getTargetUid());
AssertUtil.isEmpty(friend, "你们已经是好友了");
@@ -111,7 +114,7 @@ public class ApplyServiceImpl implements ApplyService {
// 是否有待审批的申请记录(别人请求自己的)
UserApply friendApproving = userApplyDao.getFriendApproving(request.getTargetUid(), uid, false);
if (Objects.nonNull(friendApproving)) {
acceptInvite(uid, new ApplyReq(friendApproving.getId(), ApplyStatusEnum.AGREE.getCode()));
handlerApply(uid, new ApplyReq(friendApproving.getId(), AGREE.getCode()));
return null;
}
// 申请入库
@@ -147,7 +150,7 @@ public class ApplyServiceImpl implements ApplyService {
String msg = StrUtil.format("用户{}申请加入群聊{}", userInfo.getName(), roomGroup.getName());
for (Long groupAdminId : groupAdminIds) {
friendService.createUserApply(uid, roomGroup.getRoomId(), groupAdminId, msg, 2);
friendService.createUserApply(uid, roomGroup.getRoomId(), groupAdminId, msg, RoomTypeEnum.GROUP.getType());
// 给群里的管理员发送申请
pushService.sendPushMsg(MessageAdapter.buildRoomGroupMessage(uid, roomGroup.getRoomId(), groupAdminId, msg), groupAdminId, uid);
@@ -156,75 +159,107 @@ public class ApplyServiceImpl implements ApplyService {
}
@Override
@RedissonLock(key = "#request.roomId")
@Transactional(rollbackFor = Exception.class)
public void acceptInvite(Long uid, ApplyReq request) {
@RedissonLock(key = "#request.applyId")
public void handlerApply(Long uid, ApplyReq request) {
// 1. 校验邀请记录
UserApply invite = userApplyDao.getById(request.getApplyId());
if (invite == null || !invite.getTargetId().equals(uid)) {
throw new BizException("无效的邀请");
}
if (!invite.getStatus().equals(ApplyStatusEnum.WAIT_APPROVAL.getCode())) {
if (request.getState().equals(WAIT_APPROVAL.getCode())) {
throw new BizException("无效的审批状态");
}
if (!invite.getStatus().equals(WAIT_APPROVAL.getCode())) {
throw new BizException("无效的审批");
}
// 处理加好友
invite.setStatus(request.getState());
if(invite.getType().equals(ApplyEnum.USER_FRIEND.getCode())){
AssertUtil.equal(invite.getStatus(), WAIT_APPROVAL.getCode(), "已同意好友申请");
// 同意申请
userApplyDao.agree(request.getApplyId());
switch (request.getState()){
case 0 -> userApplyDao.updateStatus(request.getApplyId(), ApplyStatusEnum.REJECT);
case 2 -> {
// 处理加好友
invite.setStatus(request.getState());
if(invite.getType().equals(RoomTypeEnum.FRIEND.getType())){
AssertUtil.equal(invite.getStatus(), AGREE.getCode(), "已同意好友申请");
// 同意申请
AtomicReference<Long> atomicRoomId = null;
AtomicReference<Boolean> atomicIsFromTempSession = null;
transactionTemplate.execute(e -> {
userApplyDao.agree(request.getApplyId());
UserFriend userFriend = userFriendDao.getByFriend(uid, invite.getUid());
atomicIsFromTempSession.set(userFriend != null && userFriend.getIsTemp());
// 如果是从临时会话升级,则修改会话状态;否则创建新会话
if (atomicIsFromTempSession.get()) {
userFriend.setIsTemp(false);
userFriendDao.updateById(userFriend);
} else {
// 创建一个聊天房间
RoomFriend roomFriend = roomService.createFriendRoom(Arrays.asList(uid, invite.getUid()));
// 检查是否是临时会话升级
UserFriend userFriend = userFriendDao.getByFriend(uid, invite.getUid());
boolean isFromTempSession = userFriend != null && userFriend.getIsTemp();
// 如果是从临时会话升级,则修改会话状态;否则创建新会话
if (isFromTempSession) {
userFriend.setIsTemp(false);
userFriendDao.updateById(userFriend);
} else {
// 创建一个聊天房间
RoomFriend roomFriend = roomService.createFriendRoom(Arrays.asList(uid, invite.getUid()));
// 创建双方好友关系
friendService.createFriend(roomFriend.getRoomId(), uid, invite.getUid());
atomicRoomId.set(roomFriend.getRoomId());
}
// 更新邀请状态
userApplyDao.updateById(invite);
return true;
});
// 创建双方好友关系
friendService.createFriend(roomFriend.getRoomId(), uid, invite.getUid());
// 如果是从临时会话升级,则修改会话状态;否则创建新会话
if (!atomicIsFromTempSession.get()) {
// 添加双方好友主动关系、被动关系
cachePlusOps.sAdd(FriendCacheKeyBuilder.userFriendsKey(uid), invite.getUid());
cachePlusOps.sAdd(FriendCacheKeyBuilder.reverseFriendsKey(invite.getUid()), uid);
cachePlusOps.sAdd(FriendCacheKeyBuilder.userFriendsKey(invite.getUid()), uid);
cachePlusOps.sAdd(FriendCacheKeyBuilder.reverseFriendsKey(uid), invite.getUid());
friendService.warmUpRoomMemberCache(Arrays.asList(atomicRoomId.get()));
// 添加双方好友主动关系、被动关系
cachePlusOps.sAdd(FriendCacheKeyBuilder.userFriendsKey(uid), invite.getUid());
cachePlusOps.sAdd(FriendCacheKeyBuilder.reverseFriendsKey(invite.getUid()), uid);
cachePlusOps.sAdd(FriendCacheKeyBuilder.userFriendsKey(invite.getUid()), uid);
cachePlusOps.sAdd(FriendCacheKeyBuilder.reverseFriendsKey(uid), invite.getUid());
friendService.warmUpRoomMemberCache(Arrays.asList(roomFriend.getRoomId()));
// 发送一条同意消息。。我们已经是好友了,开始聊天吧
chatService.sendMsg(MessageAdapter.buildAgreeMsg(atomicRoomId.get(), true), uid);
}
// 发送一条同意消息。。我们已经是好友了,开始聊天吧
chatService.sendMsg(MessageAdapter.buildAgreeMsg(roomFriend.getRoomId(), true), uid);
// 通知请求方已处理好友申请
SpringUtils.publishEvent(new UserApprovalEvent(this, RequestApprovalDto.builder().uid(uid).targetUid(invite.getUid()).build()));
} else {
// 处理加群
RoomGroup roomGroup = roomGroupCache.getByRoomId(invite.getRoomId());
Room room = roomCache.get(invite.getRoomId());
GroupMember member = groupMemberDao.getMemberByGroupId(roomGroup.getId(), uid);
if(ObjectUtil.isNotNull(member)){
throw new BizException(StrUtil.format("{}已经在{}里", userCache.getUserInfo(invite.getTargetId()).getName(), roomGroup.getName()));
}
transactionTemplate.execute(e -> {
groupMemberDao.save(MemberAdapter.buildMemberAdd(roomGroup.getId(), invite.getTargetId()));
// 3.2 系统下发加群信息
if(room.isHotRoom()){
friendService.createSystemFriend(request.getApplyId());
}
// 更新邀请状态
userApplyDao.updateById(invite);
return true;
});
// 3.3 写入缓存
groupMemberCache.evictMemberList(invite.getRoomId());
groupMemberCache.evictExceptMemberList(invite.getRoomId());
CacheKey uKey = PresenceCacheKeyBuilder.userGroupsKey(invite.getTargetId());
CacheKey gKey = PresenceCacheKeyBuilder.groupMembersKey(room.getId());
cachePlusOps.sAdd(uKey, room.getId());
cachePlusOps.sAdd(gKey, invite.getTargetId());
roomAppService.asyncOnline(Arrays.asList(invite.getTargetId()), room.getId(), true);
SpringUtils.publishEvent(new GroupMemberAddEvent(this, room.getId(), Arrays.asList(invite.getTargetId()), invite.getUid()));
}
}
// 通知请求方已处理好友申请
SpringUtils.publishEvent(new UserApprovalEvent(this, RequestApprovalDto.builder().uid(uid).targetUid(invite.getUid()).build()));
} else {
// 处理加群
groupMemberDao.save(MemberAdapter.buildMemberAdd(invite.getTargetId(), invite.getTargetId()));
// 3.2 系统下发加群信息
Room room = roomCache.get(invite.getRoomId());
if(room.isHotRoom()){
friendService.createSystemFriend(request.getApplyId());
case 3 -> {
checkRecord(request);
userApplyDao.updateStatus(request.getApplyId(), ApplyStatusEnum.IGNORE);
}
// 3.3 写入缓存
CacheKey uKey = PresenceCacheKeyBuilder.userGroupsKey(invite.getRoomId());
CacheKey gKey = PresenceCacheKeyBuilder.groupMembersKey(room.getId());
cachePlusOps.sAdd(uKey, room.getId());
cachePlusOps.sAdd(gKey, invite.getTargetId());
roomAppService.asyncOnline(Arrays.asList(invite.getTargetId()), room.getId(), true);
SpringUtils.publishEvent(new GroupMemberAddEvent(this, room.getId(), Arrays.asList(request.getApplyId()), uid));
}
// 2.1 更新邀请状态
userApplyDao.updateById(invite);
}
/**
@@ -238,35 +273,43 @@ public class ApplyServiceImpl implements ApplyService {
public void handleApply(Long uid, GroupApplyHandleReq req) {
// 1. 校验申请记录
UserApply apply = userApplyDao.getById(req.getApplyId());
if (apply == null || !apply.getType().equals(2)) {
if (apply == null || !apply.getType().equals(RoomTypeEnum.GROUP.getType())) {
throw new BizException("无效的申请记录");
}
// 2. 校验管理员权限
RoomGroup group = roomGroupCache.get(apply.getRoomId());
GroupMember member = groupMemberDao.getMember(group.getId(), uid);
if (member == null || (member.getRoleId().equals(GroupRoleEnum.LEADER.getType()) &&
!member.getRoleId().equals(GroupRoleEnum.MANAGER.getType()))) {
GroupMember member = groupMemberDao.getMemberByGroupId(group.getId(), uid);
if (member == null || (member.getRoleId().equals(GroupRoleEnum.LEADER.getType()) && !member.getRoleId().equals(GroupRoleEnum.MANAGER.getType()))) {
throw new BizException("无审批权限");
}
// 3. 更新申请状态
Room room = roomCache.get(group.getRoomId());
apply.setStatus(req.getStatus());
apply.setReadStatus(1);
userApplyDao.updateById(apply);
apply.setReadStatus(ApplyReadStatusEnum.READ.getCode());
// 5. 同意则加群
if (req.getStatus().equals(2)) {
// 5.1 加入群聊
groupMemberDao.save(MemberAdapter.buildMemberAdd(group.getId(), apply.getUid()));
// 3. 更新申请状态
transactionTemplate.execute(e -> {
userApplyDao.updateById(apply);
// 5.2 处理热点群机器人好友
Room room = roomCache.get(group.getRoomId());
if (room.isHotRoom()) {
friendService.createSystemFriend(apply.getUid());
// 5. 同意则加群
if (req.getStatus().equals(2)) {
// 5.1 加入群聊
groupMemberDao.save(MemberAdapter.buildMemberAdd(group.getId(), apply.getTargetId()));
// 5.2 处理热点群机器人好友
if (room.isHotRoom()) {
friendService.createSystemFriend(apply.getUid());
}
}
return true;
});
// 5.3 写入缓存
if (req.getStatus().equals(2)) {
groupMemberCache.evictMemberList(group.getRoomId());
groupMemberCache.evictExceptMemberList(group.getRoomId());
// 5.3 写入缓存
CacheKey uKey = PresenceCacheKeyBuilder.userGroupsKey(apply.getUid());
CacheKey gKey = PresenceCacheKeyBuilder.groupMembersKey(room.getId());
cachePlusOps.sAdd(uKey, room.getId());
@@ -274,7 +317,7 @@ public class ApplyServiceImpl implements ApplyService {
roomAppService.asyncOnline(Arrays.asList(apply.getUid()), room.getId(), true);
// 5.5 发布成员增加事件
SpringUtils.publishEvent(new GroupMemberAddEvent(this, room.getId(), Collections.singletonList(apply.getUid()), uid));
SpringUtils.publishEvent(new GroupMemberAddEvent(this, room.getId(), Collections.singletonList(apply.getUid()), apply.getUid()));
}
// 6. 通知申请人 [这条消息需要覆盖前端]
@@ -309,29 +352,16 @@ public class ApplyServiceImpl implements ApplyService {
/**
* 申请未读数
*
* @return {@link FriendUnreadResp}
* @return {@link WSFriendApply}
*/
@Override
public FriendUnreadResp unread(Long uid) {
Integer unReadCount = userApplyDao.getUnReadCount(uid);
return new FriendUnreadResp(unReadCount);
}
@Override
public void reject(Long uid, FriendApproveReq request) {
UserApply userApply = checkRecord(request);
userApplyDao.updateStatus(request.getApplyId(), ApplyStatusEnum.REJECT);
}
@Override
public void ignore(Long uid, FriendApproveReq request) {
checkRecord(request);
userApplyDao.updateStatus(request.getApplyId(), ApplyStatusEnum.IGNORE);
public WSFriendApply unread(Long uid) {
return userApplyDao.getUnReadCount(uid, uid);
}
@Override
@RedissonLock(prefixKey = "friend:deleteApprove", key = "#uid")
public void deleteApprove(Long uid, FriendApproveReq request) {
public void deleteApprove(Long uid, ApplyReq request) {
UserApply userApply = checkRecord(request);
ApplyDeletedEnum deletedEnum;
//判断谁删了这条记录
@@ -351,7 +381,7 @@ public class ApplyServiceImpl implements ApplyService {
userApplyDao.deleteApprove(request.getApplyId(), deletedEnum);
}
private UserApply checkRecord(FriendApproveReq request) {
private UserApply checkRecord(ApplyReq request) {
UserApply userApply = userApplyDao.getById(request.getApplyId());
AssertUtil.isNotEmpty(userApply, "不存在申请记录");
AssertUtil.equal(userApply.getStatus(), WAIT_APPROVAL.getCode(), "对方已是您的好友");

View File

@@ -8,6 +8,8 @@ import com.luohuo.basic.model.cache.CacheKey;
import com.luohuo.flex.common.cache.FriendCacheKeyBuilder;
import com.luohuo.flex.common.cache.PresenceCacheKeyBuilder;
import com.luohuo.flex.im.api.PresenceApi;
import com.luohuo.flex.im.domain.enums.ApplyReadStatusEnum;
import com.luohuo.flex.im.domain.enums.ApplyStatusEnum;
import jakarta.annotation.PostConstruct;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -36,7 +38,6 @@ import com.luohuo.flex.im.domain.vo.req.friend.FriendCheckReq;
import com.luohuo.flex.im.domain.vo.req.friend.FriendReq;
import com.luohuo.flex.im.domain.vo.resp.friend.FriendCheckResp;
import com.luohuo.flex.im.domain.vo.resp.friend.FriendResp;
import com.luohuo.flex.im.domain.vo.resp.friend.FriendUnreadResp;
import com.luohuo.flex.im.core.user.service.FriendService;
import com.luohuo.flex.im.core.user.service.adapter.FriendAdapter;
import com.luohuo.flex.im.core.user.service.cache.UserCache;
@@ -128,8 +129,9 @@ public class FriendServiceImpl implements FriendService, InitializingBean {
userApply.setType(type);
userApply.setRoomId(roomId);
userApply.setTargetId(targetId);
userApply.setStatus(1);
userApply.setReadStatus(1);
userApply.setStatus(ApplyStatusEnum.WAIT_APPROVAL.getCode());
userApply.setReadStatus(ApplyReadStatusEnum.UNREAD.getCode());
userApply.setApplyFor(true);
userApplyDao.save(userApply);
}
@@ -165,17 +167,6 @@ public class FriendServiceImpl implements FriendService, InitializingBean {
return userFriendDao.updateById(userFriend);
}
/**
* 申请未读数
*
* @return {@link FriendUnreadResp}
*/
@Override
public FriendUnreadResp unread(Long uid) {
Integer unReadCount = userApplyDao.getUnReadCount(uid);
return new FriendUnreadResp(unReadCount);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void createSystemFriend(Long uid){
@@ -221,8 +212,8 @@ public class FriendServiceImpl implements FriendService, InitializingBean {
}
List<Long> friendUids = friendPage.getList().stream().map(UserFriend::getFriendUid).collect(Collectors.toList());
Map<Long, Boolean> map = presenceApi.getUsersOnlineStatus(friendUids).getData();
return CursorPageBaseResp.init(friendPage, FriendAdapter.buildFriend(friendPage.getList(), map), 0L);
Set<Long> onlineList = presenceApi.getOnlineUsersList(friendUids).getData();
return CursorPageBaseResp.init(friendPage, FriendAdapter.buildFriend(friendPage.getList(), onlineList), 0L);
}
public void createFriend(Long roomId, Long uid, Long targetUid) {

View File

@@ -93,16 +93,7 @@ public class PushService {
* 直接发送, 单个用户直接推送
*/
public void sendPushMsg(WsBaseResp<?> msg, Long uid, Long cuid) {
// 1. 查询用户路由信息(节点 → 设备集合)
Map<String, Map<String, Long>> nodeDevices = routerService.findNodeDeviceUser(Arrays.asList(uid));
// 2. 按节点批量推送设备
nodeDevices.forEach((node, devices) ->
CompletableFuture.runAsync(
() -> mqProducer.sendMsg(MqConstant.PUSH_TOPIC + node, new NodePushDTO(msg, devices, cuid)),
getExecutorForNode(node)
)
);
sendPushMsg(msg, Arrays.asList(uid), cuid);
}
/**

View File

@@ -7,6 +7,8 @@ import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.luohuo.basic.context.ContextUtil;
import com.luohuo.basic.utils.SpringUtils;
import com.luohuo.basic.utils.TimeUtils;
import com.luohuo.flex.common.cache.PresenceCacheKeyBuilder;
import com.luohuo.flex.common.constant.DefValConstants;
import com.luohuo.flex.im.api.vo.UserRegisterVo;
import com.luohuo.flex.im.common.event.UserRegisterEvent;
import com.luohuo.flex.im.core.chat.service.ContactService;
@@ -14,7 +16,6 @@ import com.luohuo.flex.im.core.chat.service.RoomService;
import com.luohuo.flex.im.core.user.service.cache.DefUserCache;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.luohuo.basic.cache.redis2.CacheResult;
@@ -71,7 +72,6 @@ public class UserServiceImpl implements UserService {
public static final LocalDateTime MAX_DATE = LocalDateTime.of(2099, 12, 31, 00, 00, 00);
private final ContactService contactService;
private final RoomService roomService;
private final ApplicationEventPublisher applicationEventPublisher;
private UserCache userCache;
private DefUserCache defUserCache;
private UserBackpackDao userBackpackDao;
@@ -83,6 +83,12 @@ public class UserServiceImpl implements UserService {
private UserSummaryCache userSummaryCache;
private SensitiveWordBs sensitiveWordBs;
@Override
public Boolean checkEmail(String email) {
User user = userDao.getByEmail(email);
return user != null;
}
@Override
public Long getUIdByUserId(Long defUserId, Long tenantId) {
try {
@@ -107,7 +113,7 @@ public class UserServiceImpl implements UserService {
@Override
@Transactional
public void modifyName(Long uid, ModifyNameReq req) {
public void modifyInfo(Long uid, ModifyNameReq req) {
// 判断名字是不是重复
String newName = req.getName();
AssertUtil.isFalse(sensitiveWordBs.hasSensitiveWord(newName), "名字中包含敏感词,请重新输入"); // 判断名字中有没有敏感词
@@ -122,7 +128,7 @@ public class UserServiceImpl implements UserService {
// 用乐观锁,就不用分布式锁了
if (useSuccess) {
// 改名
userDao.modifyName(uid, req.getName());
userDao.modifyName(uid, req);
// 删除缓存
userSummaryCache.delete(uid);
userCache.userInfoChange(uid);
@@ -322,6 +328,7 @@ public class UserServiceImpl implements UserService {
.sex(userRegisterVo.getSex())
.userType(userRegisterVo.getUserType())
.name(userRegisterVo.getName())
.resume("这个人还没有填写个人简介呢")
.openId(userRegisterVo.getOpenId())
.tenantId(userRegisterVo.getTenantId())
.build();
@@ -330,11 +337,16 @@ public class UserServiceImpl implements UserService {
newUser.setCreateBy(1L);
userDao.save(newUser);
// 创建会话
contactService.createContact(newUser.getId(), 1L);
contactService.createContact(newUser.getId(), DefValConstants.DEF_ROOM_ID);
// 创建群成员
roomService.createGroupMember(1L, newUser.getId());
roomService.createGroupMember(DefValConstants.DEF_GROUP_ID, newUser.getId());
// 注入群组信息
cachePlusOps.sAdd(PresenceCacheKeyBuilder.groupMembersKey(DefValConstants.DEF_ROOM_ID), newUser.getId());
cachePlusOps.sAdd(PresenceCacheKeyBuilder.userGroupsKey(newUser.getId()), DefValConstants.DEF_ROOM_ID);
// 发布用户注册消息
applicationEventPublisher.publishEvent(new UserRegisterEvent(this, newUser));
SpringUtils.publishEvent(new UserRegisterEvent(this, newUser));
return true;
}
}

View File

@@ -3,14 +3,14 @@
<mapper namespace="com.luohuo.flex.im.core.chat.mapper.ContactMapper">
<insert id="refreshOrCreateActiveTime">
insert into im_contact(`room_id`,`uid`,`last_msg_id`,`active_time`)
values
INSERT INTO im_contact (room_id, uid, last_msg_id, active_time, is_del) VALUES
<foreach collection="memberUidList" item="uid" separator=",">
(#{roomId},#{uid},#{msgId},#{activeTime})
(#{roomId},#{uid},#{msgId},#{activeTime},0)
</foreach>
on DUPLICATE KEY UPDATE
`hide`=0,
`last_msg_id`=VALUES(last_msg_id),
`active_time`=VALUES(active_time)
ON DUPLICATE KEY UPDATE
is_del = 0,
hide = 0,
last_msg_id = VALUES(last_msg_id),
active_time = VALUES(active_time);
</insert>
</mapper>

View File

@@ -2,4 +2,13 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.luohuo.flex.im.core.chat.mapper.GroupMemberMapper">
<select id="getMemberListByUid" resultType="com.luohuo.flex.model.entity.ws.ChatMember">
select id, group_id, uid, role_id, remark, my_name, de_friend from im_group_member g WHERE is_del = 0 and uid IN
<foreach collection="memberList" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<select id="getMemberListByGroupId" resultType="com.luohuo.flex.model.entity.ws.ChatMemberResp">
select id, group_id, uid, role_id, remark, my_name, de_friend from im_group_member g WHERE is_del = 0 and group_id = #{groupId}
</select>
</mapper>

View File

@@ -2,4 +2,14 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.luohuo.flex.im.core.chat.mapper.MessageMapper">
<select id="batchGetUnReadCount" resultType="java.util.Map">
SELECT room_id, COUNT(*) AS unread_count
FROM im_message
WHERE (room_id, create_time) IN (
<foreach collection="contactList" item="contact" separator=",">
(#{contact.roomId}, #{contact.readTime})
</foreach>
)
GROUP BY room_id
</select>
</mapper>

View File

@@ -3,13 +3,14 @@
<mapper namespace="com.luohuo.flex.im.core.chat.mapper.RoomMapper">
<select id="groupList" resultType="com.luohuo.flex.im.domain.vo.res.GroupListVO">
select r.id as roomId,
select distinct r.id as roomId,
g.name as roomName,
g.avatar as avatar,
g.id as groupId
g.id as groupId,
r.update_time
from im_room_group g
inner join im_room r on g.room_id = r.id
inner join im_group_member m on g.id = m.group_id
inner join im_group_member m on g.id = m.group_id and m.is_del = 0
where m.uid = #{uid}
and g.is_del = 0
order by r.update_time,

View File

@@ -2,4 +2,10 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.luohuo.flex.im.core.user.mapper.UserApplyMapper">
<select id="getUnReadCountByType" resultType="com.luohuo.flex.im.domain.vo.resp.friend.FriendUnreadDto">
SELECT type, COUNT(*) as count
FROM im_user_apply
WHERE target_id = #{targetId} AND deleted = #{normal} AND read_status = #{readStatus}
GROUP BY type, `status` order by `status`
</select>
</mapper>

View File

@@ -6,13 +6,12 @@ import com.luohuo.flex.im.core.user.service.ApplyService;
import com.luohuo.flex.im.domain.entity.UserApply;
import com.luohuo.flex.im.domain.vo.req.PageBaseReq;
import com.luohuo.flex.im.domain.vo.req.friend.FriendApplyReq;
import com.luohuo.flex.im.domain.vo.req.friend.FriendApproveReq;
import com.luohuo.flex.im.domain.vo.request.RoomApplyReq;
import com.luohuo.flex.im.domain.vo.request.member.ApplyReq;
import com.luohuo.flex.im.domain.vo.request.member.GroupApplyHandleReq;
import com.luohuo.flex.im.domain.vo.res.PageBaseResp;
import com.luohuo.flex.im.domain.vo.resp.friend.FriendApplyResp;
import com.luohuo.flex.im.domain.vo.resp.friend.FriendUnreadResp;
import com.luohuo.flex.model.entity.ws.WSFriendApply;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
@@ -21,7 +20,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -48,20 +46,20 @@ public class ApplyController {
@PostMapping("/apply")
@Operation(summary = "好友申请")
public R<UserApply> apply(@Valid @RequestBody FriendApplyReq request) {
return R.success(applyService.apply(ContextUtil.getUid(), request));
return R.success(applyService.handlerApply(ContextUtil.getUid(), request));
}
@GetMapping("/unread")
@Operation(summary = "申请未读数")
public R<FriendUnreadResp> unread() {
public R<WSFriendApply> unread() {
Long uid = ContextUtil.getUid();
return R.success(applyService.unread(uid));
}
@Operation(summary ="审批别人邀请的进群、好友申请")
@PostMapping("/accept")
public R<Void> acceptInvite(@Valid @RequestBody ApplyReq request) {
applyService.acceptInvite(ContextUtil.getUid(), request);
@PostMapping("/handler/apply")
public R<Void> handlerApply(@Valid @RequestBody ApplyReq request) {
applyService.handlerApply(ContextUtil.getUid(), request);
return R.success();
}
@@ -71,30 +69,16 @@ public class ApplyController {
return R.success(applyService.applyGroup(ContextUtil.getUid(), request));
}
@PostMapping("/handle")
@PostMapping("/adminHandleApply")
@Operation(summary = "处理加群申请 [仅仅管理员、群主可调用]")
public R<Void> handleApply(@Valid @RequestBody GroupApplyHandleReq request) {
applyService.handleApply(ContextUtil.getUid(), request);
return R.success();
}
@PutMapping("/reject")
@Operation(summary = "审批拒绝")
public R<Boolean> reject(@Valid @RequestBody FriendApproveReq request) {
applyService.reject(ContextUtil.getUid(), request);
return R.success();
}
@PutMapping("/ignore")
@Operation(summary = "忽略审批")
public R<Boolean> ignore(@Valid @RequestBody FriendApproveReq request) {
applyService.ignore(ContextUtil.getUid(), request);
return R.success();
}
@DeleteMapping("/delete")
@Operation(summary = "删除好友申请")
public R<Boolean> deleteApprove(@Valid @RequestBody FriendApproveReq request) {
public R<Boolean> deleteApprove(@Valid @RequestBody ApplyReq request) {
applyService.deleteApprove(ContextUtil.getUid(), request);
return R.success();
}

View File

@@ -68,8 +68,8 @@ public class ChatController {
// @FrequencyControl(time = 120, count = 20, target = FrequencyControl.Target.IP)
public R<List<ChatMessageResp>> getMsgPage(@RequestParam(value = "lastOptTime", required = false) Long lastOptTime) {
List<ChatMessageResp> msgPage = chatService.getMsgList(lastOptTime, ContextUtil.getUid());
Set<String> blackMembers = getBlackUidSet();
msgPage.removeIf(a -> blackMembers.contains(a.getFromUser().getUid().toString()));
// Set<String> blackMembers = getBlackUidSet();
// msgPage.removeIf(a -> blackMembers.contains(a.getFromUser().getUid().toString()));
return R.success(msgPage);
}

View File

@@ -160,8 +160,8 @@ public class RoomController {
}
@Operation(summary = "公告列表")
@GetMapping("/announcement/list/{id}")
public R<IPage<Announcements>> announcementList(@PathVariable("id") Long roomId, @RequestParam("current") Long current,@RequestParam("size") Long size){
@GetMapping("/announcement/list")
public R<IPage<Announcements>> announcementList(@RequestParam("roomId") Long roomId, @RequestParam("current") Long current,@RequestParam("size") Long size){
IPage<Announcements> page = new Page<>(current,size);
return R.success(roomService.announcementList(roomId, page));
}
@@ -185,8 +185,8 @@ public class RoomController {
}
@Operation(summary = "删除公告")
@PostMapping("announcement/delete/{id}")
public R announcementDelete(@PathVariable("id") Long id){
@PostMapping("announcement/delete")
public R announcementDelete(@RequestParam("id") Long id){
return R.success(roomService.announcementDelete(ContextUtil.getUid(), id));
}

View File

@@ -13,7 +13,6 @@ import com.luohuo.flex.im.domain.vo.req.friend.FriendDeleteReq;
import com.luohuo.flex.im.domain.vo.req.friend.FriendReq;
import com.luohuo.flex.im.domain.vo.resp.friend.FriendCheckResp;
import com.luohuo.flex.im.domain.vo.resp.friend.FriendResp;
import com.luohuo.flex.im.domain.vo.resp.friend.FriendUnreadResp;
import com.luohuo.flex.im.core.user.service.FriendService;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
@@ -57,13 +56,6 @@ public class FriendController {
return R.success();
}
@GetMapping("/apply/unread")
@Operation(summary = "申请未读数")
public R<FriendUnreadResp> unread() {
Long uid = ContextUtil.getUid();
return R.success(friendService.unread(uid));
}
@GetMapping("/page")
@Operation(summary = "联系人列表")
public R<CursorPageBaseResp<FriendResp>> friendList(@Valid CursorPageBaseReq request) {

View File

@@ -41,6 +41,13 @@ public class UserController {
@Resource
private RoleService roleService;
@GetMapping("/checkEmail")
@Operation(summary ="绑定邮箱")
@TenantIgnore
public R<Boolean> checkEmail(@RequestParam("email") String email) {
return R.success(userService.checkEmail(email));
}
@GetMapping("/getById/{id}")
@Operation(summary ="用户详情 [仅远程接口调用]")
public R<UserInfoResp> getById(@PathVariable("id") Long id) {
@@ -93,8 +100,8 @@ public class UserController {
@PutMapping("/name")
@Operation(summary ="修改用户名")
public R<Void> modifyName(@Valid @RequestBody ModifyNameReq req) {
userService.modifyName(ContextUtil.getUid(), req);
public R<Void> modifyInfo(@Valid @RequestBody ModifyNameReq req) {
userService.modifyInfo(ContextUtil.getUid(), req);
return R.success();
}

View File

@@ -6,11 +6,7 @@ import com.luohuo.flex.im.domain.entity.UserState;
import com.luohuo.flex.im.core.user.service.UserStateService;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.annotation.Resource;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -24,9 +20,9 @@ public class UserStateController {
@Resource
private UserStateService userStateService;
@PostMapping("changeState/{id}")
@PostMapping("changeState")
@Operation(summary = "用户状态改变")
public R<Boolean> changeState(@PathVariable("id") Long id){
public R<Boolean> changeState(@RequestParam("id") Long id){
return R.success(userStateService.changeState(ContextUtil.getUid(), id));
}

View File

@@ -1,6 +1,5 @@
package com.luohuo.flex.im.domain.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.luohuo.basic.base.entity.Entity;
import lombok.AllArgsConstructor;
@@ -29,19 +28,16 @@ public class GroupMember extends Entity<Long> {
/**
* 群组id
*/
@TableField("group_id")
private Long groupId;
/**
* 成员uid
*/
@TableField("uid")
private Long uid;
/**
* 成员角色1群主(可撤回,可移除,可解散) 2管理员(可撤回,可移除) 3普通成员
*/
@TableField("role_id")
private Integer roleId;
/**

View File

@@ -1,78 +0,0 @@
package com.luohuo.flex.im.domain.entity;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler;
import com.luohuo.basic.base.entity.TenantEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* <p>
* 消息表
* </p>
*
* @author nyh
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName(value = "im_message", autoResultMap = true)
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Message2 extends TenantEntity<Long> {
private static final long serialVersionUID = 1L;
/**
* 会话表id
*/
@TableField("room_id")
private Long roomId;
/**
* 消息发送者uid
*/
@TableField("from_uid")
private Long fromUid;
/**
* 消息内容
*/
@TableField("content")
private String content;
/**
* 回复的消息内容
*/
@TableField("reply_msg_id")
private Long replyMsgId;
/**
* 消息状态 0正常 1删除
*/
@TableField("status")
private Integer status;
/**
* 与回复消息的间隔条数
*/
@TableField("gap_count")
private Integer gapCount;
/**
* 消息类型 1正常文本 2.撤回消息
*/
@TableField("type")
private Integer type;
/**
* 消息扩展字段
*/
@TableField(value = "extra", typeHandler = FastjsonTypeHandler.class)
private JSONObject extra;
}

View File

@@ -70,6 +70,12 @@ public class User extends Entity<Long> {
@TableField("open_id")
private String openId;
/**
* 个人简介
*/
@TableField("resume")
private String resume;
/**
* @see UserState
*/
@@ -121,12 +127,6 @@ public class User extends Entity<Long> {
@TableField(value = "ip_info", typeHandler = JacksonTypeHandler.class)
private IpInfo ipInfo;
/**
* 密码
*/
@TableField("password")
private String password;
public void refreshIp(String ip) {
if (ipInfo == null) {
ipInfo = new IpInfo();

View File

@@ -3,6 +3,8 @@ 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.RoomTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -31,7 +33,8 @@ public class UserApply extends Entity<Long> {
private Long uid;
/**
* 申请类型 1加好友 2群聊
* 申请类型
* @see RoomTypeEnum
*/
@TableField("type")
private Integer type;
@@ -55,13 +58,15 @@ public class UserApply extends Entity<Long> {
private String msg;
/**
* 申请状态 1待审批 2同意 3拒绝
* 申请状态
* @see ApplyStatusEnum
*/
@TableField("status")
private Integer status;
/**
* 阅读状态 1未读 2已读
* 阅读状态
* @see com.luohuo.flex.im.domain.enums.ApplyReadStatusEnum
*/
@TableField("read_status")
private Integer readStatus;
@@ -71,4 +76,10 @@ public class UserApply extends Entity<Long> {
*/
@TableField("deleted")
private Integer deleted;
/**
* 主动申请加群
*/
@TableField("apply_for")
private Boolean applyFor;
}

View File

@@ -1,20 +0,0 @@
package com.luohuo.flex.im.domain.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author 乾乾
* 申请类型枚举
*/
@Getter
@AllArgsConstructor
public enum ApplyEnum {
USER_FRIEND(1, "好友申请"),
GROUP(2, "群聊邀请");
private final Integer code;
private final String desc;
}

View File

@@ -1,19 +0,0 @@
package com.luohuo.flex.im.domain.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author nyh
*/
@Getter
@AllArgsConstructor
public enum ApplyTypeEnum {
ADD_FRIEND(1, "加好友");
private final Integer code;
private final String desc;
}

View File

@@ -1,27 +0,0 @@
package com.luohuo.flex.im.domain.vo.req.friend;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import jakarta.validation.constraints.NotNull;
import com.luohuo.basic.base.entity.BaseEntity;
/**
* 申请好友信息
* @author nyh
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class FriendApproveReq extends BaseEntity {
@NotNull
@Schema(description ="申请id")
private Long applyId;
}

View File

@@ -1,5 +1,7 @@
package com.luohuo.flex.im.domain.vo.req.room;
import com.luohuo.flex.im.domain.enums.ApplyStatusEnum;
import com.luohuo.flex.im.domain.enums.RoomTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -14,7 +16,10 @@ import java.time.LocalDateTime;
public class UserApplyResp implements Serializable {
@Schema(description ="申请人uid")
private Long uid;
@Schema(description ="申请类型 1加好友 2申请进群")
/**
* @see RoomTypeEnum
*/
@Schema(description ="申请类型 1 群 2加好友")
private Integer type;
@Schema(description ="type = 2 才需要房间id")
private Long roomId;
@@ -22,10 +27,15 @@ public class UserApplyResp implements Serializable {
private Long targetId;
@Schema(description ="申请消息")
private String msg;
@Schema(description ="申请状态 1待审批 2同意 3拒绝")
/**
* @see ApplyStatusEnum
*/
@Schema(description ="申请状态")
private Integer status;
@Schema(description ="阅读状态 1未读 2已读")
private Integer readStatus;
@Schema(description ="主动申请加群 true 是主动申请进群")
private Boolean applyFor;
@Schema(description ="申请时间")
private LocalDateTime createTime;
}

View File

@@ -1,6 +1,5 @@
package com.luohuo.flex.im.domain.vo.req.user;
import com.luohuo.basic.base.entity.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -10,6 +9,7 @@ import org.hibernate.validator.constraints.Length;
import jakarta.validation.constraints.NotNull;
import java.io.Serializable;
/**
@@ -20,11 +20,13 @@ import jakarta.validation.constraints.NotNull;
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ModifyNameReq extends BaseEntity {
public class ModifyNameReq implements Serializable {
@NotNull
@Length(max = 8, message = "用户名可别取太长,不然我记不住噢")
@Schema(description = "用户名")
private String name;
@Schema(description = "个人简介")
private String resume;
}

View File

@@ -21,6 +21,6 @@ public class ApplyReq {
private Long applyId;
@NotNull(message = "请选择邀请记录")
@Schema(description ="1 = 同意 2 = 拒绝")
@Schema(description ="0 = 拒绝 2 = 同意 3 = 忽略")
private Integer state;
}

View File

@@ -1,5 +1,7 @@
package com.luohuo.flex.im.domain.vo.request.member;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@@ -8,6 +10,8 @@ public class GroupApplyHandleReq {
@NotNull(message = "申请ID不能为空")
private Long applyId;
@NotNull(message = "审批状态不能为空 2=同意 3=拒绝")
@NotNull(message = "审批状态不能为空 0拒绝 2同意 3忽略")
@Min(value = 0, message = "状态值不能小于0")
@Max(value = 3, message = "状态值不能大于3")
private Integer status;
}

View File

@@ -8,7 +8,7 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.HashSet;
/**
* 移除群成员
@@ -26,5 +26,5 @@ public class MemberAddReq {
@NotNull
@Size(min = 1, max = 50)
@Schema(description ="邀请的uid")
private List<Long> uidList;
private HashSet<Long> uidList;
}

View File

@@ -1,5 +1,6 @@
package com.luohuo.flex.im.domain.vo.resp.friend;
import com.luohuo.flex.im.domain.enums.ApplyStatusEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -32,7 +33,11 @@ public class FriendApplyResp {
@Schema(description ="申请信息")
private String msg;
@Schema(description ="申请状态 1待审批 2同意")
/**
* 申请状态
* @see ApplyStatusEnum
*/
@Schema(description ="申请状态")
private Integer status;
@Schema(description ="申请时间")

View File

@@ -1,24 +1,26 @@
package com.luohuo.flex.im.domain.vo.resp.friend;
import com.luohuo.basic.base.entity.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 好友校验
* @author nyh
* @author 乾乾
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class FriendUnreadResp extends BaseEntity {
public class FriendUnreadDto implements Serializable {
@Schema(description ="申请列表的未读数")
private Integer unReadCount;
@Schema(description ="类型")
private Integer type;
@Schema(description ="数量")
private Integer count;
}

View File

@@ -4,8 +4,6 @@ import com.luohuo.basic.base.R;
import com.luohuo.flex.im.api.hystrix.ImUserApiFallback;
import com.luohuo.flex.im.api.vo.UserRegisterVo;
import com.luohuo.flex.im.domain.vo.resp.user.UserInfoResp;
import com.luohuo.flex.model.entity.system.SysUser;
import com.luohuo.flex.model.vo.result.UserQuery;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import org.springframework.cloud.openfeign.FeignClient;
@@ -15,10 +13,6 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import java.io.Serializable;
import java.util.Map;
import java.util.Set;
/**
* 用户
*
@@ -28,10 +22,14 @@ import java.util.Set;
@FeignClient(name = "luohuo-im-server", fallback = ImUserApiFallback.class)
public interface ImUserApi {
@GetMapping("/user/checkEmail")
@Operation(summary ="校验邮箱是否存在 [仅远程接口调用]")
R<Boolean> checkEmail(@RequestParam("email") String email);
/**
* 获取前端展示信息
*/
@GetMapping("/getById/{id}")
@GetMapping("/user/getById/{id}")
@Operation(summary ="用户详情 [仅远程接口调用]")
R<UserInfoResp> getById(@PathVariable("id") Long id);
@@ -42,25 +40,6 @@ public interface ImUserApi {
@GetMapping("/user/findById")
R<Long> findById(@RequestParam("id") Long id, @RequestParam("tenantId") Long tenantId);
/**
* 根据id查询实体
*
* @param ids 唯一键可能不是主键ID)
* @return
*/
@PostMapping("/echo/user/findByIds")
Map<Serializable, Object> findByIds(@RequestParam(value = "ids") Set<Serializable> ids);
/**
* 根据id 查询用户详情
*
* @param userQuery 查询条件
* @return 系统用户
*/
@PostMapping(value = "/anyone/getSysUserById")
R<SysUser> getById(@RequestBody UserQuery userQuery);
/**
* 注册用户
*

View File

@@ -6,15 +6,9 @@ import com.luohuo.flex.im.domain.vo.resp.user.UserInfoResp;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import com.luohuo.basic.base.R;
import com.luohuo.flex.model.entity.system.SysUser;
import com.luohuo.flex.model.vo.result.UserQuery;
import com.luohuo.flex.im.api.ImUserApi;
import org.springframework.web.bind.annotation.RequestParam;
import java.io.Serializable;
import java.util.Map;
import java.util.Set;
/**
* 用户API熔断
*
@@ -25,6 +19,11 @@ import java.util.Set;
@RequiredArgsConstructor
public class ImUserApiFallback implements ImUserApi {
@Override
public R<Boolean> checkEmail(String email) {
return R.success(true);
}
@Override
public R<UserInfoResp> getById(Long id) {
return R.success(new UserInfoResp());
@@ -35,16 +34,6 @@ public class ImUserApiFallback implements ImUserApi {
throw BizException.wrap("ID: " + id + "用户不存在");
}
@Override
public Map<Serializable, Object> findByIds(Set<Serializable> ids) {
return Map.of();
}
@Override
public R<SysUser> getById(UserQuery userQuery) {
return R.timeout();
}
@Override
public R<Boolean> register(UserRegisterVo userRegisterVo) {
throw BizException.wrap("注册失败");

View File

@@ -62,7 +62,6 @@
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -1,116 +0,0 @@
package com.luohuo.flex.areatest;
import cn.hutool.crypto.SecureUtil;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.luohuo.basic.context.ContextUtil;
import com.luohuo.flex.base.entity.tenant.DefUser;
import com.luohuo.flex.base.service.tenant.DefUserService;
import com.luohuo.flex.im.core.chat.dao.MessageDao;
import com.luohuo.flex.im.core.chat.mapper.Contact222Mapper;
import com.luohuo.flex.im.core.user.dao.UserDao;
import com.luohuo.flex.im.domain.entity.Message;
import com.luohuo.flex.im.domain.entity.Message2;
import com.luohuo.flex.im.domain.entity.User;
import com.luohuo.flex.im.domain.entity.msg.MessageExtra;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.List;
@SpringBootTest
@ExtendWith(SpringExtension.class)
@Slf4j
public class CopyUserTest {
@Resource
DefUserService defUserService;
@Resource
UserDao userDao;
@Resource
Contact222Mapper contact222Mapper;
@Test
public void test2() {
ContextUtil.setTenantId(1L);
QueryWrapper<Message2> queryWrapper = new QueryWrapper<Message2>().ge("type", 12);
List<Message2> messages = contact222Mapper.selectList(queryWrapper);
for (Message2 message : messages) {
JSONObject extra = message.getExtra();
if (extra.containsKey("audioCallMsgDTO")) {
JSONObject audioCallMsg = extra.getJSONObject("audioCallMsgDTO");
if(audioCallMsg != null){
// 删除creator字段
audioCallMsg.remove("creator");
// 将修改后的对象放回extra中
extra.put("audioCallMsgDTO", audioCallMsg);
}
}
if (extra.containsKey("videoCallMsgDTO")) {
JSONObject audioCallMsg = extra.getJSONObject("videoCallMsgDTO");
if(audioCallMsg != null){
// 删除creator字段
audioCallMsg.remove("creator");
// 将修改后的对象放回extra中
extra.put("audioCallMsgDTO", audioCallMsg);
}
}
Message2 message2 = new Message2();
message2.setId(message.getId());
message2.setExtra(extra);
contact222Mapper.updateById(message2);
}
}
/**
* 同步用户数据
* 需要吧def_user 复制一份到luohuo_im_01 库里面
*/
@Test
public void test() {
ContextUtil.setTenantId(1L);
List<User> list = userDao.list();
for (User user : list) {
DefUser defUser = new DefUser();
defUser.setUsername(user.getAccount());
defUser.setNickName(user.getName());
defUser.setWxOpenId(user.getOpenId());
defUser.setTenantId(1L);
defUser.setLastLoginTime(user.getLastOptTime());
defUser.setEmail(user.getEmail());
defUser.setAvatar(user.getAvatar());
defUser.setPasswordErrorNum(0);
if(user.getIpInfo() != null){
com.luohuo.flex.model.entity.base.IpInfo ipInfo1 = new com.luohuo.flex.model.entity.base.IpInfo();
BeanUtils.copyProperties(user.getIpInfo(), ipInfo1);
defUser.setIpInfo(ipInfo1);
}
defUser.setReadonly(false);
defUser.setSalt(user.getName());
defUser.setState(true);
defUser.setSex(user.getSex());
defUser.setSystemType(2);
defUser.setCreateTime(user.getCreateTime());
defUser.setUpdateTime(user.getUpdateTime());
defUser.setCreateBy(1L);
defUser.setPassword(SecureUtil.sha256("123456" + defUser.getSalt()));
defUserService.getSuperManager().save(defUser);
User user1 = new User();
user1.setId(user.getId());
user1.setUserId(defUser.getId());
userDao.updateById(user1);
}
}
}

View File

@@ -0,0 +1,57 @@
package com.luohuo.flex.oauth;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.util.Base64;
public class AesUtil {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final byte[] SECRET_KEY = "Lu0Huo@32ByteKey!!1234567890ABCD".getBytes();
public static String encrypt(String plainText) {
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY, "AES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] iv = cipher.getIV(); // GCM需要IV
byte[] encrypted = cipher.doFinal(plainText.getBytes());
// 组合IV+密文便于存储
return Base64.getEncoder().encodeToString(
ByteBuffer.allocate(iv.length + encrypted.length)
.put(iv)
.put(encrypted)
.array()
);
} catch (Exception e) {
throw new RuntimeException("加密失败", e);
}
}
public static String decrypt(String encryptedText) {
try {
byte[] decoded = Base64.getDecoder().decode(encryptedText);
ByteBuffer buffer = ByteBuffer.wrap(decoded);
// 提取IV
byte[] iv = new byte[12];
buffer.get(iv);
// 提取密文
byte[] cipherText = new byte[buffer.remaining()];
buffer.get(cipherText);
// 解密
Cipher cipher = Cipher.getInstance(ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY, "AES");
cipher.init(Cipher.DECRYPT_MODE, keySpec, new GCMParameterSpec(128, iv));
return new String(cipher.doFinal(cipherText));
} catch (Exception e) {
return null;
}
}
}

View File

@@ -0,0 +1,29 @@
package com.luohuo.flex.oauth.cache;
import com.luohuo.basic.model.cache.CacheKey;
import com.luohuo.basic.model.cache.CacheKeyBuilder;
import com.luohuo.flex.common.cache.CacheKeyModular;
import com.luohuo.flex.common.cache.CacheKeyTable;
import java.time.Duration;
/**
* 二维码状态键示例
*/
public class QrCacheKeyBuilder implements CacheKeyBuilder {
public static CacheKey builder(String qrId) {
return new QrCacheKeyBuilder().key(qrId);
}
@Override public String getPrefix() { return CacheKeyModular.PREFIX; }
@Override public String getModular() { return CacheKeyModular.OAUTH; }
@Override public String getTable() { return CacheKeyTable.OAUTH.QR; }
@Override public String getField() { return "id"; }
@Override public ValueType getValueType() { return ValueType.string; }
@Override
public Duration getExpire() {
return Duration.ofSeconds(30);
}
}

View File

@@ -0,0 +1,54 @@
package com.luohuo.flex.oauth.emuns;
public enum QrLoginState {
/**
* 待扫描(默认状态)
* - 有效期30秒半分钟
* - 描述:二维码生成后等待用户扫描
*/
PENDING("待扫描", "PENDING", 30_000),
/**
* 已扫描待确认
* - 有效期60秒1分钟
* - 描述:用户已扫码但未确认登录
*/
SCANNED("已扫描待确认", "SCANNED", 60_000),
/**
* 已确认可换取Token
* - 有效期10秒
* - 描述用户确认登录等待PC端换取Token
*/
CONFIRMED("已确认", "CONFIRMED", 10_000);
private final String description; // 状态描述
private final String value; // 状态值
private final int millis; // 状态有效期(毫秒)
// 枚举构造函数
QrLoginState(String description, String value, int millis) {
this.description = description;
this.value = value;
this.millis = millis;
}
// 获取状态描述
public String getDescription() {
return description;
}
public String getValue() {
return value;
}
// 获取状态超时时间
public int getMillis() {
return millis;
}
// 获取状态过期时间点
public Long getExpireTime() {
return System.currentTimeMillis() + millis;
}
}

View File

@@ -96,7 +96,7 @@ public abstract class AbstractTokenGranter implements TokenGranter {
if (!result.getsuccess()) {
return result;
}
result = checkClient();
result = checkAuthorization();
if (!result.getsuccess()) {
return result;
}
@@ -132,7 +132,7 @@ public abstract class AbstractTokenGranter implements TokenGranter {
defUser.refreshIp(ContextUtil.getIP());
// 10. 封装token
LoginResultVO loginResultVO = buildResult(uid, defUser, org, loginParam.getDeviceType());
LoginResultVO loginResultVO = buildResult(uid, defUser, org, loginParam.getDeviceType(), loginParam.getClientId());
LoginStatusDTO loginStatus = LoginStatusDTO.success(defUser.getId(), uid, defUser.getSystemType(), loginParam.getDeviceType());
SpringUtils.publishEvent(new UserOnlineEvent(this, ContextUtil.getTenantId(), uid, defUser.getId(), defUser.getLastLoginTime(), defUser.getIpInfo()));
SpringUtils.publishEvent(new LoginEvent(loginStatus));
@@ -172,16 +172,16 @@ public abstract class AbstractTokenGranter implements TokenGranter {
* @date 2022/10/5 12:38 PM
* @create [2022/10/5 12:38 PM ] [tangyh] [初始创建]
*/
protected R<LoginResultVO> checkClient() {
String basicHeader = JakartaServletUtil.getHeader(WebUtils.request(), CLIENT_KEY, StrPool.UTF_8);
String[] client = Base64Util.getClient(basicHeader);
DefClient defClient = defClientService.getClient(client[0], client[1]);
protected R<LoginResultVO> checkAuthorization() {
String basicHeader = JakartaServletUtil.getHeader(WebUtils.request(), AUTHORIZATION_KEY, StrPool.UTF_8);
String[] authorization = Base64Util.getAuthorization(basicHeader);
DefClient defAuthorization = defClientService.getClient(authorization[0], authorization[1]);
if (defClient == null) {
if (defAuthorization == null) {
return R.fail("请在.env文件中配置正确的客户端ID或者客户端秘钥");
}
if (!defClient.getState()) {
return R.fail("客户端[%s]已被禁用", defClient.getClientId());
if (!defAuthorization.getState()) {
return R.fail("客户端[%s]已被禁用", defAuthorization.getClientId());
}
return R.success(null);
}
@@ -370,23 +370,24 @@ public abstract class AbstractTokenGranter implements TokenGranter {
* @param userInfo 员工信息
* @param org 机构信息
* @param deviceType 登录设备
* @param clientId 设备指纹
* @return com.luohuo.flex.oauth.vo.result.LoginResultVO
* @author 乾乾
*/
protected LoginResultVO buildResult(Long uid, DefUser userInfo, Org org, String deviceType) {
protected LoginResultVO buildResult(Long uid, DefUser userInfo, Org org, String deviceType, String clientId) {
// 0. 处理同设备登录用户
String combinedDeviceType = kickout(uid, userInfo, deviceType);
// 1. 拿到登录用户的id
String loginId = userInfo.getId().toString();
StpUtil.login(loginId, combinedDeviceType);
// StpUtil.login(loginId, deviceType);
// 2. 配置登录设备、租户信息等等
SaSession tokenSession = StpUtil.getTokenSession();
tokenSession.setLoginId(userInfo.getId());
tokenSession.set(JWT_KEY_SYSTEM_TYPE, userInfo.getSystemType());
tokenSession.set(JWT_KEY_DEVICE, deviceType);
tokenSession.set(CLIENT_HEADER, clientId);
if (org.getCurrentTopCompanyId() != null) {
tokenSession.set(JWT_KEY_TOP_COMPANY_ID, org.getCurrentTopCompanyId());
} else {
@@ -472,10 +473,11 @@ public abstract class AbstractTokenGranter implements TokenGranter {
if (CollUtil.isNotEmpty(sameDeviceTokens)) {
for (String token : sameDeviceTokens) {
try {
String clientId = StpUtil.getTokenSessionByToken(token).getString(CLIENT_HEADER);
StpUtil.kickout(token);
log.info("已踢出会话: token={}", token);
SpringUtils.publishEvent(new TokenExpireEvent(this, new OffLineResp(uid, deviceType, ContextUtil.getIP(), token)));
SpringUtils.publishEvent(new TokenExpireEvent(this, new OffLineResp(uid, deviceType, clientId, ContextUtil.getIP(), token)));
} catch (Exception e) {
log.error("踢出会话失败: token={}", token, e);
}
@@ -525,7 +527,7 @@ public abstract class AbstractTokenGranter implements TokenGranter {
}
@Override
public LoginResultVO switchOrg(Long orgId) {
public LoginResultVO switchOrg(Long orgId, String clientId) {
StpUtil.checkLogin();
Long userId = ContextUtil.getUserId();
DefUser defUser = defUserService.getByIdCache(userId);
@@ -588,7 +590,7 @@ public abstract class AbstractTokenGranter implements TokenGranter {
.currentDeptId(deptId)
.build();
LoginResultVO loginResultVO = buildResult(ContextUtil.getUid(), defUser, org, StpUtil.getLoginType());
LoginResultVO loginResultVO = buildResult(ContextUtil.getUid(), defUser, org, StpUtil.getLoginType(), clientId);
LoginStatusDTO loginStatus = LoginStatusDTO.switchOrg(defUser.getId(), employee.getId());
SpringUtils.publishEvent(new LoginEvent(loginStatus));

View File

@@ -0,0 +1,34 @@
package com.luohuo.flex.oauth.granter;
import com.luohuo.basic.context.ContextUtil;
import com.luohuo.flex.oauth.AesUtil;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class DeviceFingerprintValidator {
/**
* 验证设备指纹的完整性与一致性 [目前仅仅判断了ip是否一致]
* @param currentFingerprint 当前设备指纹
* @param storedFingerprint 服务端存储的原始指纹
* @return 验证结果true=合法设备false=可疑设备)
*/
public static boolean validateDeviceFingerprint(String currentFingerprint, String storedFingerprint) {
// 1. 基础格式校验
String decryptedHash = AesUtil.decrypt(storedFingerprint);
if (decryptedHash == null) {
log.warn("设备指纹解密失败");
return false;
}
// 2. 解析动态盐值
String[] parts = decryptedHash.split("\\|");
if (parts.length < 2) return false;
String storedFullIp = parts[0];
long timestamp = Long.parseLong(parts[1]);
return storedFullIp.equals(ContextUtil.getIP()) && (System.currentTimeMillis() - timestamp < 3600_000);
}
}

View File

@@ -0,0 +1,171 @@
package com.luohuo.flex.oauth.granter;
import cn.dev33.satoken.config.SaTokenConfig;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ObjectUtil;
import com.luohuo.basic.base.R;
import com.luohuo.basic.cache.redis2.CacheResult;
import com.luohuo.basic.cache.repository.CachePlusOps;
import com.luohuo.basic.context.ContextUtil;
import com.luohuo.basic.exception.BizException;
import com.luohuo.basic.model.cache.CacheKey;
import com.luohuo.flex.base.entity.tenant.DefUser;
import com.luohuo.flex.base.service.system.DefClientService;
import com.luohuo.flex.base.service.tenant.DefUserService;
import com.luohuo.flex.base.service.user.BaseEmployeeService;
import com.luohuo.flex.base.service.user.BaseOrgService;
import com.luohuo.flex.common.properties.SystemProperties;
import com.luohuo.flex.im.api.ImUserApi;
import com.luohuo.flex.model.redis.annotation.RedissonLock;
import com.luohuo.flex.oauth.AesUtil;
import com.luohuo.flex.oauth.cache.QrCacheKeyBuilder;
import com.luohuo.flex.oauth.emuns.QrLoginState;
import com.luohuo.flex.oauth.vo.param.ConfirmReq;
import com.luohuo.flex.oauth.vo.param.LoginParamVO;
import com.luohuo.flex.oauth.vo.param.QueryStatusReq;
import com.luohuo.flex.oauth.vo.param.ScanReq;
import com.luohuo.flex.oauth.vo.result.LoginResultVO;
import com.luohuo.flex.oauth.vo.result.LoginUserInfo;
import com.luohuo.flex.oauth.vo.result.QrCodeResp;
import com.luohuo.flex.oauth.vo.result.QrCodeStatus;
import com.luohuo.flex.oauth.vo.result.ScanResp;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.Duration;
/**
* 二维码扫码登录
*
* @author 乾乾
* @date 2025年08月08日10:23:53
*/
@Component
@Slf4j
public class QrCodeGranter extends AbstractTokenGranter {
@Resource
protected CachePlusOps cachePlusOps;
public QrCodeGranter(SystemProperties systemProperties, DefClientService defClientService, DefUserService defUserService, BaseEmployeeService baseEmployeeService, BaseOrgService baseOrgService, SaTokenConfig saTokenConfig, ImUserApi imUserApi) {
super(systemProperties, defClientService, defUserService, baseEmployeeService, baseOrgService, saTokenConfig, imUserApi);
}
public QrCodeResp generateQRCode() {
QrLoginState pending = QrLoginState.PENDING;
String qrId = UUID.randomUUID().toString();
CacheKey cacheKey = QrCacheKeyBuilder.builder(qrId);
cacheKey.setExpire(Duration.ofMillis(pending.getMillis()));
// 2. 生成设备指纹
String ip = ContextUtil.getIP();
String deviceHash = makeRawDeviceHash(ip);
// 存储初始状态(含设备指纹防劫持)
QrCodeStatus state = new QrCodeStatus(pending.getValue(), deviceHash);
cachePlusOps.set(cacheKey, state, true);
return new QrCodeResp(qrId, deviceHash, ip, pending.getExpireTime());
}
/**
* 动态生成加密指纹 => IP+时间盐
* @return
*/
private static String makeRawDeviceHash(String ip) {
String rawDeviceHash = ip + "|" + System.currentTimeMillis();
return AesUtil.encrypt(rawDeviceHash);
}
/**
* 用户扫码
* @param req
* return 返回操作的过期时间
*/
@RedissonLock(prefixKey ="luohuo:handleScan:", key = "#req.qrId")
public ScanResp handleScan(ScanReq req) {
CacheKey cacheKey = QrCacheKeyBuilder.builder(req.getQrId());
// 1. 校验二维码状态
CacheResult<QrCodeStatus> cacheResult = cachePlusOps.get(cacheKey);
QrCodeStatus qrCodeStatus = cacheResult.getValue();
if (ObjectUtil.isNull(qrCodeStatus) || !QrLoginState.PENDING.getValue().equals(qrCodeStatus.getStatus())) {
throw new BizException("二维码已失效");
}
// 2. 更新为已扫描状态
String ip = ContextUtil.getIP();
if (!DeviceFingerprintValidator.validateDeviceFingerprint(makeRawDeviceHash(ip), qrCodeStatus.getDeviceHash())) {
throw new BizException("二维码已扫描");
}
// 3. 更新redis 状态
QrLoginState scanned = QrLoginState.SCANNED;
cacheKey.setExpire(Duration.ofMillis(scanned.getMillis()));
qrCodeStatus.setStatus(scanned.getValue());
cachePlusOps.set(cacheKey, qrCodeStatus);
return new ScanResp(ip, scanned.getExpireTime());
}
/**
* 用户确认
* @param req
* return 返回操作的过期时间
*/
@RedissonLock(prefixKey ="luohuo:confirmLogin:", key = "#req.qrId")
public Long confirmLogin(ConfirmReq req) {
CacheKey statusKey = QrCacheKeyBuilder.builder(req.getQrId());
// 状态校验仅允许SCANNED→CONFIRMED
CacheResult<QrCodeStatus> cacheResult = cachePlusOps.get(statusKey);
QrCodeStatus qrLoginState = cacheResult.getValue();
if (ObjectUtil.isNull(qrLoginState) || !QrLoginState.SCANNED.getValue().equals(qrLoginState.getStatus())) {
throw new BizException("二维码状态异常");
}
// 存储用户ID并更新状态
QrLoginState confirmed = QrLoginState.CONFIRMED;
statusKey.setExpire(Duration.ofMillis(confirmed.getMillis()));
cachePlusOps.set(statusKey, new LoginUserInfo(confirmed.getValue(), qrLoginState.getDeviceHash(), ContextUtil.getUserId(), ContextUtil.getUid()));
return confirmed.getExpireTime();
}
@RedissonLock(prefixKey ="luohuo:checkStatus:", key = "#req.deviceHash")
public R checkStatus(QueryStatusReq req) {
CacheKey statusKey = QrCacheKeyBuilder.builder(req.getQrId());
CacheResult<LoginUserInfo> result = cachePlusOps.get(statusKey);
LoginUserInfo userInfo = result.getValue();
// 1. 校验数据
if (userInfo == null) {
throw new BizException("二维码状态异常");
}
if (!req.getDeviceHash().equals(userInfo.getDeviceHash())) {
throw new BizException("设备状态异常");
}
String status = userInfo.getStatus();
if (QrLoginState.CONFIRMED.getValue().equals(status)) {
// 2. 查询用户信息
Long userId = userInfo.getUserId();
Long uid = userInfo.getUid();
DefUser defUser = defUserService.getByIdCache(userId);
// 3. 生成token
return R.success(buildResult(uid, defUser, findOrg(defUser), req.getDeviceType(), req.getClientId()));
} else if (QrLoginState.SCANNED.getValue().equals(status)) {
return R.fail().setData("请在手机上完成确认");
} else {
return R.fail().setData("请扫描二维码");
}
}
@Override
protected R<LoginResultVO> checkParam(LoginParamVO loginParam) {
return null;
}
@Override
protected DefUser getUser(LoginParamVO loginParam) {
return null;
}
}

View File

@@ -53,6 +53,6 @@ public interface TokenGranter {
* @date 2022/9/16 1:14 PM
* @create [2022/9/16 1:14 PM ] [tangyh] [初始创建]
*/
LoginResultVO switchOrg(Long orgId);
LoginResultVO switchOrg(Long orgId, String clientId);
}

View File

@@ -53,4 +53,10 @@ public interface UserInfoService {
* @return
*/
String registerByEmail(SysUser sysUser, RegisterByEmailVO register);
/**
* 校验邮箱是否存在
* @param email 邮箱信息
*/
Boolean checkEmail(String email);
}

View File

@@ -1,6 +1,5 @@
package com.luohuo.flex.oauth.service.impl;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
@@ -33,7 +32,6 @@ import com.luohuo.flex.oauth.properties.CaptchaProperties;
import com.luohuo.flex.oauth.service.CaptchaService;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.HashMap;
import static com.luohuo.basic.exception.code.ResponseEnum.CAPTCHA_ERROR;
@@ -140,7 +138,11 @@ public class CaptchaServiceImpl implements CaptchaService {
ArgumentAssert.isFalse(flag, "该邮箱已经被他人使用");
}
String code = RandomUtil.randomString(6);
// CacheKey imgKey = CaptchaCacheKeyBuilder.build(bindEmailReq.getClientId(), CaptchaTokenGranter.GRANT_TYPE);
// CacheResult<String> result = cacheOps.get(imgKey);
// ArgumentAssert.isFalse(!bindEmailReq.getCode().equals(result.getValue()), "图片验证码错误");
String code = RandomUtil.randomNumbers(6);
CacheKey cacheKey = CaptchaCacheKeyBuilder.build(bindEmailReq.getEmail(), bindEmailReq.getTemplateCode());
cacheOps.set(cacheKey, code);

View File

@@ -95,7 +95,6 @@ public class UserInfoServiceImpl implements UserInfoService {
public String registerByEmail(SysUser sysUser, RegisterByEmailVO register) {
// 1. 校验数据保存defUser
if (systemProperties.getVerifyCaptcha()) {
// 短信验证码
CacheKey cacheKey = new CaptchaCacheKeyBuilder().key(register.getEmail(), register.getKey());
CacheResult<String> code = cacheOps.get(cacheKey);
ArgumentAssert.equals(code.getValue(), register.getCode(), "验证码不正确");
@@ -158,4 +157,14 @@ public class UserInfoServiceImpl implements UserInfoService {
}
};
}
@Override
public Boolean checkEmail(String email) {
// 1. 判断系统邮箱是否存在
boolean systemEmail = defUserService.checkEmail(email, null);
// 2. 判断Im邮箱是否存在
boolean imEmail = imUserApi.checkEmail(email).getData();
return systemEmail || imEmail;
}
}

View File

@@ -2,16 +2,22 @@ package com.luohuo.flex.oauth.controller;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.StpUtil;
import com.alibaba.fastjson.JSONObject;
import com.luohuo.basic.annotation.log.WebLog;
import com.luohuo.basic.annotation.user.LoginUser;
import com.luohuo.basic.tenant.core.aop.TenantIgnore;
import com.luohuo.flex.base.vo.update.tenant.DefUserPasswordUpdateVO;
import com.luohuo.flex.model.entity.system.SysUser;
import com.luohuo.flex.oauth.granter.QrCodeGranter;
import com.luohuo.flex.oauth.vo.param.ConfirmReq;
import com.luohuo.flex.oauth.vo.param.QueryStatusReq;
import com.luohuo.flex.oauth.vo.param.RefreshTokenVO;
import com.luohuo.flex.oauth.vo.param.ScanReq;
import com.luohuo.flex.oauth.vo.result.QrCodeResp;
import com.luohuo.flex.oauth.vo.result.ScanResp;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
@@ -29,7 +35,6 @@ import com.luohuo.flex.oauth.enumeration.GrantType;
import com.luohuo.flex.oauth.granter.RefreshTokenGranter;
import com.luohuo.flex.oauth.granter.TokenGranterBuilder;
import com.luohuo.flex.oauth.service.UserInfoService;
import com.luohuo.flex.oauth.service.storage.StorageDriver;
import com.luohuo.flex.oauth.vo.param.LoginParamVO;
import com.luohuo.flex.oauth.vo.param.RegisterByEmailVO;
import com.luohuo.flex.oauth.vo.param.RegisterByMobileVO;
@@ -48,11 +53,11 @@ import com.luohuo.flex.oauth.vo.result.LoginResultVO;
@Tag(name = "登录-退出-注册")
public class RootController {
private final QrCodeGranter qrCodeGranter;
private final TokenGranterBuilder tokenGranterBuilder;
private final RefreshTokenGranter refreshTokenGranter;
private final DefUserService defUserService;
private final UserInfoService userInfoService;
private final StorageDriver storageDriver;
/**
* 登录接口
@@ -82,8 +87,8 @@ public class RootController {
@Operation(summary = "切换部门")
@PutMapping("/anyone/switchTenantAndOrg")
public R<LoginResultVO> switchOrg(@RequestParam(required = false) Long orgId) {
return R.success(tokenGranterBuilder.getGranter(GrantType.PASSWORD).switchOrg(orgId));
public R<LoginResultVO> switchOrg(@RequestParam(required = false) Long orgId, @RequestParam String clientId) {
return R.success(tokenGranterBuilder.getGranter(GrantType.PASSWORD).switchOrg(orgId, clientId));
}
@Operation(summary = "退出", description = "退出")
@@ -110,15 +115,39 @@ public class RootController {
return R.success(userInfoService.registerByEmail(sysUser, register));
}
@Operation(summary = "检测手机号是否存在")
@Operation(summary = "检测邮箱是否存在")
@GetMapping("/anyTenant/checkEmail")
public R<Boolean> checkEmail(@RequestParam("email") String email) {
return R.success(userInfoService.checkEmail(email));
}
@Operation(summary = "检测手机号是否存在")
@GetMapping("/anyTenant/checkMobile")
public R<Boolean> checkMobile(@RequestParam String mobile) {
public R<Boolean> checkMobile(@RequestParam("mobile") String mobile) {
return R.success(defUserService.checkMobile(mobile, null));
}
@Operation(summary = "获取七牛云上传token")
@GetMapping("/anyTenant/ossToken")
public R<JSONObject> token() {
return R.success(storageDriver.getToken());
@GetMapping("/anyTenant/qr/generate")
@Operation(summary = "生成登录二维码")
public R<QrCodeResp> generateQRCode() {
return R.success(qrCodeGranter.generateQRCode());
}
@GetMapping("/anyTenant/qr/status/query")
@Operation(summary = "查询二维码状态")
public R checkStatus(@Valid QueryStatusReq req) {
return qrCodeGranter.checkStatus(req);
}
@PostMapping("/qrcode/scan")
@Operation(summary = "扫描登录二维码")
public R<ScanResp> handleScan(@Valid @RequestBody ScanReq req) {
return R.success(qrCodeGranter.handleScan(req));
}
@PostMapping("/qrcode/confirm")
@Operation(summary = "用户在手机上确认登录")
public R<Long> confirmLogin(@Valid @RequestBody ConfirmReq req) {
return R.success(qrCodeGranter.confirmLogin(req));
}
}

View File

@@ -0,0 +1,14 @@
package com.luohuo.flex.oauth.vo.param;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.io.Serializable;
@Data
@Schema(description = "二维码确认")
public class ConfirmReq implements Serializable {
@NotEmpty(message = "二维码参数不能为空")
private String qrId;
}

View File

@@ -43,6 +43,9 @@ public class LoginParamVO {
@Schema(description = "手机号")
private String mobile;
@Schema(description = "客户端指纹信息")
private String clientId = "";
/**
* @see com.luohuo.flex.oauth.emuns.LoginEnum
*/

View File

@@ -0,0 +1,27 @@
package com.luohuo.flex.oauth.vo.param;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import java.io.Serializable;
@Data
@Schema(description = "轮询二维码状态")
public class QueryStatusReq implements Serializable {
@NotEmpty(message = "二维码参数不能为空")
private String qrId;
@NotEmpty(message = "设备信息异常")
@Schema(description = "前端指纹")
private String clientId;
@NotEmpty(message = "设备异常")
private String deviceHash;
@Pattern(regexp = "^(PC)$", message = "登录方式只能是 PC")
@NotEmpty(message = "登录方式只能是 PC")
@Schema(description = "请选择登录方式 PC")
private String deviceType;
}

View File

@@ -0,0 +1,14 @@
package com.luohuo.flex.oauth.vo.param;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.io.Serializable;
@Data
@Schema(description = "二维码请求")
public class ScanReq implements Serializable {
@NotEmpty(message = "二维码参数不能为空")
private String qrId;
}

View File

@@ -0,0 +1,32 @@
package com.luohuo.flex.oauth.vo.result;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
@Schema(description = "用户扫码成功后的信息")
public class LoginUserInfo implements Serializable {
@Schema(description = "二维码唯一标识")
private String status;
@Schema(description = "系统id")
private Long userId;
@Schema(description = "用户id")
private Long uid;
@Schema(description = "用户指纹")
private String deviceHash;
public LoginUserInfo(String status, String deviceHash, Long userId, Long uid) {
this.status = status;
this.userId = userId;
this.uid = uid;
this.deviceHash = deviceHash;
}
public LoginUserInfo() {
}
}

View File

@@ -0,0 +1,28 @@
package com.luohuo.flex.oauth.vo.result;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
@Schema(description = "二维码生成响应体")
public class QrCodeResp implements Serializable {
@Schema(description = "二维码唯一标识(用于轮询状态)")
private String qrId;
@Schema(description = "设备指纹")
private String deviceHash;
@Schema(description = "二维码过期时间戳(毫秒)")
private Long expireTime;
@Schema(description = "登录端ip")
private String ip;
public QrCodeResp(String qrId, String deviceHash, String ip, Long expireTime) {
this.qrId = qrId;
this.deviceHash = deviceHash;
this.expireTime = expireTime;
this.ip = ip;
}
}

View File

@@ -0,0 +1,24 @@
package com.luohuo.flex.oauth.vo.result;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
@Schema(description = "二维码状态")
public class QrCodeStatus implements Serializable {
@Schema(description = "二维码唯一标识(用于轮询状态)")
private String status;
@Schema(description = "设备指纹")
private String deviceHash;
public QrCodeStatus(String status, String deviceHash) {
this.status = status;
this.deviceHash = deviceHash;
}
public QrCodeStatus() {
}
}

View File

@@ -0,0 +1,21 @@
package com.luohuo.flex.oauth.vo.result;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
@Schema(description = "扫码后展示在手机端的信息")
public class ScanResp implements Serializable {
@Schema(description = "二维码过期时间戳(毫秒)")
private Long expireTime;
@Schema(description = "登录端ip")
private String ip;
public ScanResp(String ip, Long expireTime) {
this.ip = ip;
this.expireTime = expireTime;
}
}

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:
password: ${luohuo.nacos.password}
server-addr: ${luohuo.nacos.ip}:${luohuo.nacos.port}
namespace: ${luohuo.nacos.namespace}
ip: ${luohuo.nacos.local-ip}
metadata: # 元数据,用于权限服务实时获取各个服务的所有接口
management.context-path: ${server.servlet.context-path:}${spring.mvc.servlet.path:}${management.endpoints.web.base-path:}
gray_version: qianqian

View File

@@ -1,5 +1,7 @@
package com.luohuo.flex;
import cn.hutool.core.collection.CollUtil;
import com.google.common.collect.Lists;
import com.luohuo.basic.base.R;
import com.luohuo.basic.cache.repository.CachePlusOps;
import com.luohuo.basic.model.cache.CacheKey;
@@ -16,9 +18,12 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
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;
/**
@@ -35,6 +40,36 @@ public class OnlineController {
private CachePlusOps cachePlusOps;
/**
* 查询在线用户ID集合
* @param uids 待查询的用户ID列表
* @return 在线的用户ID集合
*/
@PostMapping("/user/online-users-list")
public R<Set<Long>> getOnlineUsersList(@RequestBody List<Long> uids) {
if (CollUtil.isEmpty(uids)) {
return R.success(Collections.emptySet());
}
String onlineKey = PresenceCacheKeyBuilder.globalOnlineUsersKey().getKey();
Set<Long> onlineUsers = new HashSet<>();
// 分页批量查询
Lists.partition(uids, 500).forEach(batch -> {
// 1. 批量查询分数
List<Object> scores = cachePlusOps.getZSetScores(onlineKey, batch);
// 2. 过滤在线用户
for (int i = 0; i < batch.size(); i++) {
if (scores.get(i) != null) {
onlineUsers.add(batch.get(i));
}
}
});
return R.success(onlineUsers);
}
/**
* 查询在线的人员
* @param uids

View File

@@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 用户
@@ -21,6 +22,14 @@ import java.util.Map;
*/
@FeignClient(name = "${" + Constants.PROJECT_PREFIX + ".feign.tenant-server:luohuo-presence-server}", fallback = PresenceApiFallback.class)
public interface PresenceApi {
/**
* 查询用户的在线id集合
*
* @return 用户id
*/
@PostMapping("/online/user/online-users-list")
R<Set<Long>> getOnlineUsersList(@RequestBody List<Long> uids);
/**
* 查询用户的在线状态
*

View File

@@ -6,8 +6,10 @@ import com.luohuo.flex.im.api.PresenceApi;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 在线用户API熔断
@@ -17,6 +19,11 @@ import java.util.Map;
*/
@Component
public class PresenceApiFallback implements PresenceApi {
@Override
public R<Set<Long>> getOnlineUsersList(List<Long> uids) {
return R.success(new HashSet<>());
}
@Override
public R<Map<Long, Boolean>> getUsersOnlineStatus(List<Long> uids) {
return R.success(new HashMap<>());

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@}
@@ -54,6 +55,7 @@ spring:
password: ${luohuo.nacos.password}
server-addr: ${luohuo.nacos.ip}:${luohuo.nacos.port}
namespace: ${luohuo.nacos.namespace}
ip: ${luohuo.nacos.local-ip}
metadata: # 元数据,用于权限服务实时获取各个服务的所有接口
management.context-path: ${server.servlet.context-path:}${spring.mvc.servlet.path:}${management.endpoints.web.base-path:}
grayversion: luohuo

View File

@@ -263,8 +263,20 @@ public interface CacheKeyTable {
* 朋友圈权限
*/
String FEED_TARGET = "feedTarget";
/**
* 会话信息
*/
String USER_CONTACT = "user_contact";
}
interface OAUTH {
/**
* 租户自定义字典
*/
String QR = "qr_status";
}
/**
* 消息服务缓存 start

View File

@@ -91,6 +91,9 @@ public class PresenceCacheKeyBuilder implements CacheKeyBuilder {
return new GlobalOnlineDevicesKeyBuilder().key();
}
/**
* 系统在线的所有用户 uid:clientId 的映射方式
*/
private static class GlobalOnlineDevicesKeyBuilder implements CacheKeyBuilder {
@Override public String getPrefix() { return CacheKeyModular.PREFIX; }
@Override public String getTenant() { return StrPool.EMPTY; }

Some files were not shown because too many files have changed in this diff Show More