This commit is contained in:
oneao committed 2025-12-30 22:25:54 +08:00
1 parent a6dbe24dd4
commit 52b16ed2b0
21 files changed
+442 -105

No files matched your search

@@ -89,4 +89,18 @@ public class ImController {
return Result.success(); return Result.success();
} }
@GetMapping("/friend/{friendId}")
public Result<FriendProfileVO> getFriendProfile(@PathVariable("friendId") Long friendId){
FriendProfileVO friendProfileVO = imFriendService.getFriendProfile(friendId);
return Result.success(friendProfileVO);
}
@GetMapping("/group/{groupId}")
public Result<GroupProfileVO> getGroupProfile(@PathVariable("groupId") Long groupId){
GroupProfileVO groupProfileVO = imGroupService.getGroupProfile(groupId);
return Result.success(groupProfileVO);
}
} }
@@ -13,8 +13,7 @@ public enum MessageTypeEnums {
OFFLINE(6), OFFLINE(6),
GROUP_OFFLINE(7), GROUP_OFFLINE(7),
PRIVATE(8), PRIVATE(8),
GROUP(9), GROUP(9);
FRIEND_UPDATE(10);
private final int code; private final int code;
@@ -12,12 +12,12 @@ import cn.link56.im.message.MessageResponse;
import cn.link56.im.model.dto.GroupDTO; import cn.link56.im.model.dto.GroupDTO;
import cn.link56.im.model.dto.LoginDTO; import cn.link56.im.model.dto.LoginDTO;
import cn.link56.im.model.dto.PrivateDTO; import cn.link56.im.model.dto.PrivateDTO;
import cn.link56.im.model.po.ImGroupMessage;
import cn.link56.im.model.po.ImPrivateMessage; import cn.link56.im.model.po.ImPrivateMessage;
import cn.link56.im.model.vo.GroupMessageVO;
import cn.link56.im.model.vo.PrivateMessageVO; import cn.link56.im.model.vo.PrivateMessageVO;
import cn.link56.im.model.vo.QueryFriendVO; import cn.link56.im.model.vo.QueryFriendVO;
import cn.link56.im.service.ImFriendService; import cn.link56.im.service.*;
import cn.link56.im.service.ImGroupUserService;
import cn.link56.im.service.ImPrivateMessageService;
import cn.link56.im.storage.SessionManager; import cn.link56.im.storage.SessionManager;
import cn.link56.im.utils.MessageDataConverter; import cn.link56.im.utils.MessageDataConverter;
import cn.link56.im.utils.MessageWriter; import cn.link56.im.utils.MessageWriter;
@@ -40,6 +40,10 @@ public class MessageHandler {
private ImPrivateMessageService imPrivateMessageService; private ImPrivateMessageService imPrivateMessageService;
@Resource @Resource
private CompanyUserService companyUserService; private CompanyUserService companyUserService;
@Resource
private ImGroupMessageService imGroupMessageService;
@Resource
private ImGroupService imGroupService;
// 处理消息 // 处理消息
public void handleMessage(Channel channel, MessageRequest request) { public void handleMessage(Channel channel, MessageRequest request) {
@@ -104,7 +108,7 @@ public class MessageHandler {
MessageWriter.broadcastToUsers(onlineFriends,MessageResponse.success(MessageTypeEnums.ONLINE).setFrom(from)); MessageWriter.broadcastToUsers(onlineFriends,MessageResponse.success(MessageTypeEnums.ONLINE).setFrom(from));
// 通知群组 // 通知群组
List<Long> onlineGroupUsers = imGroupUserService.getOnlineGroupUsers(from); List<Long> onlineGroupUsers = imGroupUserService.getAllOnlineGroupUsers(from);
MessageWriter.broadcastToUsers(onlineGroupUsers,MessageResponse.success(MessageTypeEnums.GROUP_ONLINE).setFrom(from)); MessageWriter.broadcastToUsers(onlineGroupUsers,MessageResponse.success(MessageTypeEnums.GROUP_ONLINE).setFrom(from));
} }
@@ -115,7 +119,7 @@ public class MessageHandler {
MessageWriter.broadcastToUsers(onlineFriends,MessageResponse.success(MessageTypeEnums.OFFLINE).setFrom(from)); MessageWriter.broadcastToUsers(onlineFriends,MessageResponse.success(MessageTypeEnums.OFFLINE).setFrom(from));
// 通知群组 // 通知群组
List<Long> onlineGroupUsers = imGroupUserService.getOnlineGroupUsers(from); List<Long> onlineGroupUsers = imGroupUserService.getAllOnlineGroupUsers(from);
MessageWriter.broadcastToUsers(onlineGroupUsers,MessageResponse.success(MessageTypeEnums.GROUP_OFFLINE).setFrom(from)); MessageWriter.broadcastToUsers(onlineGroupUsers,MessageResponse.success(MessageTypeEnums.GROUP_OFFLINE).setFrom(from));
} }
@@ -132,22 +136,6 @@ public class MessageHandler {
// 好友关系建立 // 好友关系建立
imFriendService.createFriend(to,from); imFriendService.createFriend(to,from);
// 通知建立了好友关系
if (SessionManager.isOnline(to)) {
CompanyUser companyUser = companyUserService.getById(from);
if (ObjectUtils.isNotEmpty(companyUser)){
QueryFriendVO queryFriendVO = new QueryFriendVO();
queryFriendVO.setFriendId(from);
queryFriendVO.setFriendName(companyUser.getName());
queryFriendVO.setFriendAvatar(companyUser.getAvatar());
queryFriendVO.setIsOnline(true);
queryFriendVO.setUnreadCount(0);
MessageWriter.writeToUser(to,MessageResponse.success(MessageTypeEnums.FRIEND_UPDATE,from).setData(queryFriendVO));
}
}
// 保存 // 保存
Long privateMessageID = IdGenerator.nextId(); Long privateMessageID = IdGenerator.nextId();
Long sendTime = TimeUtils.getCurrentTimestampMillis(); Long sendTime = TimeUtils.getCurrentTimestampMillis();
@@ -188,11 +176,51 @@ public class MessageHandler {
// 群聊逻辑 // 群聊逻辑
private void handleGroup(Channel channel, MessageRequest request) { private void handleGroup(Channel channel, MessageRequest request) {
Long from = request.getFrom(); Long from = request.getFrom();
Long to = request.getTo(); Long groupId = request.getTo();
GroupDTO dto = MessageDataConverter.convert(request.getData(), GroupDTO.class); GroupDTO dto = MessageDataConverter.convert(request.getData(), GroupDTO.class);
String content = dto.getContent(); String content = dto.getContent();
Integer type = dto.getType(); Integer type = dto.getType();
String tempId = dto.getTempId(); String tempId = dto.getTempId();
String senderName = dto.getSenderName();
String senderAvatar = dto.getSenderAvatar();
// 将消息插入到数据库中
long nextId = IdGenerator.nextId();
long timestampMillis = TimeUtils.getCurrentTimestampMillis();
ImGroupMessage imGroupMessage = new ImGroupMessage();
imGroupMessage.setId(nextId);
imGroupMessage.setContent(content);
imGroupMessage.setType(type);
imGroupMessage.setSenderId(from);
imGroupMessage.setSendTime(timestampMillis);
imGroupMessage.setGroupId(groupId);
imGroupMessageService.save(imGroupMessage);
// 更新未读状态
imGroupUserService.updateGroupUnreadCount(groupId,from);
// 更新lastMessageId
imGroupService.updateLaseMessageId(groupId,nextId);
// 构建回复消息
GroupMessageVO groupMessageVO = new GroupMessageVO();
groupMessageVO.setTempId(tempId);
groupMessageVO.setId(nextId);
groupMessageVO.setSenderId(from);
groupMessageVO.setContent(content);
groupMessageVO.setType(type);
groupMessageVO.setSendTime(timestampMillis);
groupMessageVO.setGroupId(groupId);
groupMessageVO.setSenderName(senderName);
groupMessageVO.setSenderAvatar(senderAvatar);
// 通知建立群组关系
// 查询该群在线的用户
List<Long> onlineGroupUsers = imGroupUserService.getGroupOnlineUserIds(groupId);
MessageWriter.broadcastToUsers(onlineGroupUsers,MessageResponse.success(MessageTypeEnums.GROUP,groupId).setData(groupMessageVO));
} }
} }
@@ -1,6 +1,7 @@
package cn.link56.im.mapper; package cn.link56.im.mapper;
import cn.link56.im.model.po.ImFriend; import cn.link56.im.model.po.ImFriend;
import cn.link56.im.model.vo.FriendProfileVO;
import cn.link56.im.model.vo.QueryFriendVO; import cn.link56.im.model.vo.QueryFriendVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
@@ -34,4 +35,10 @@ public interface ImFriendMapper extends BaseMapper<ImFriend> {
@Param("userId") Long userId, @Param("userId") Long userId,
@Param("friendId") Long friendId @Param("friendId") Long friendId
); );
FriendProfileVO selectFriendProfile(
@Param("userId") Long userId,
@Param("friendId") Long friendId
);
} }
@@ -1,7 +1,7 @@
package cn.link56.im.mapper; package cn.link56.im.mapper;
import cn.link56.im.model.po.ImGroup; import cn.link56.im.model.po.ImGroup;
import cn.link56.im.model.vo.QueryFriendVO; import cn.link56.im.model.vo.GroupProfileVO;
import cn.link56.im.model.vo.QueryGroupVO; import cn.link56.im.model.vo.QueryGroupVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
@@ -21,4 +21,8 @@ public interface ImGroupMapper extends BaseMapper<ImGroup> {
Long selectGroupTotal(@Param("userId") Long userId, Long selectGroupTotal(@Param("userId") Long userId,
@Param("name") String name); @Param("name") String name);
GroupProfileVO selectGroupProfile(
@Param("groupId") Long groupId,
@Param("userId") Long userId);
} }
@@ -11,4 +11,7 @@ public class GroupDTO {
private String content; private String content;
private Integer type; private Integer type;
private String tempId; private String tempId;
private String senderAvatar;
private String senderName;
} }
@@ -0,0 +1,15 @@
package cn.link56.im.model.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class FriendProfileVO {
private Long friendId;
private String friendName;
private String friendAvatar;
private Integer unreadCount;
}
@@ -0,0 +1,14 @@
package cn.link56.im.model.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GroupMessageVO extends QueryGroupMessageVO {
private String tempId;
}
@@ -0,0 +1,16 @@
package cn.link56.im.model.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GroupProfileVO {
private Long groupId;
private String groupName;
private String groupAvatar;
private Integer unreadCount;
}
@@ -16,7 +16,6 @@ public class QueryFriendVO {
private Integer unreadCount; private Integer unreadCount;
private Long lastMessageId;
private Long lastMessageTime; private Long lastMessageTime;
private Integer lastMessageType; private Integer lastMessageType;
private String lastMessageContent; private String lastMessageContent;
@@ -14,8 +14,9 @@ public class QueryGroupVO {
private Integer unreadCount; private Integer unreadCount;
private Long lastMessageId;
private Long lastMessageTime; private Long lastMessageTime;
private Integer lastMessageType; private Integer lastMessageType;
private String lastMessageContent; private String lastMessageContent;
private String lastMessageSenderName;
} }
@@ -6,6 +6,7 @@ import cn.link56.common.utils.snowflake.idgen.IdGenerator;
import cn.link56.im.mapper.ImFriendMapper; import cn.link56.im.mapper.ImFriendMapper;
import cn.link56.im.model.dto.QueryFriendDTO; import cn.link56.im.model.dto.QueryFriendDTO;
import cn.link56.im.model.po.ImFriend; import cn.link56.im.model.po.ImFriend;
import cn.link56.im.model.vo.FriendProfileVO;
import cn.link56.im.model.vo.QueryFriendVO; import cn.link56.im.model.vo.QueryFriendVO;
import cn.link56.im.storage.SessionManager; import cn.link56.im.storage.SessionManager;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -103,4 +104,10 @@ public class ImFriendService extends ServiceImpl<ImFriendMapper, ImFriend> {
public void decreaseUnreadCount(Long userId,Long friendId){ public void decreaseUnreadCount(Long userId,Long friendId){
imFriendMapper.decreaseUnreadCount(userId, friendId); imFriendMapper.decreaseUnreadCount(userId, friendId);
} }
public FriendProfileVO getFriendProfile(Long friendId) {
Long userId = UserContext.getUserId();
return imFriendMapper.selectFriendProfile(userId, friendId);
}
} }
@@ -5,13 +5,17 @@ import cn.link56.common.context.UserContext;
import cn.link56.im.mapper.ImGroupMapper; import cn.link56.im.mapper.ImGroupMapper;
import cn.link56.im.model.dto.QueryGroupDTO; import cn.link56.im.model.dto.QueryGroupDTO;
import cn.link56.im.model.po.ImGroup; import cn.link56.im.model.po.ImGroup;
import cn.link56.im.model.vo.GroupProfileVO;
import cn.link56.im.model.vo.QueryFriendVO; import cn.link56.im.model.vo.QueryFriendVO;
import cn.link56.im.model.vo.QueryGroupVO; import cn.link56.im.model.vo.QueryGroupVO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.List; import java.util.List;
import java.util.Objects;
@Service @Service
public class ImGroupService extends ServiceImpl<ImGroupMapper, ImGroup>{ public class ImGroupService extends ServiceImpl<ImGroupMapper, ImGroup>{
@@ -32,4 +36,18 @@ public class ImGroupService extends ServiceImpl<ImGroupMapper, ImGroup>{
return new BasePageVO<>(total,queryFriendVOS); return new BasePageVO<>(total,queryFriendVOS);
} }
public void updateLaseMessageId(Long groupId,Long lastMessageId){
LambdaUpdateWrapper<ImGroup> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(ImGroup::getId,groupId)
.set(ImGroup::getLastMessageId,lastMessageId);
imGroupMapper.update(updateWrapper);
}
public GroupProfileVO getGroupProfile(Long groupId) {
Long userId = UserContext.getUserId();
return imGroupMapper.selectGroupProfile(groupId,userId);
}
} }
@@ -21,7 +21,7 @@ public class ImGroupUserService extends ServiceImpl<ImGroupUserMapper, ImGroupUs
private ImGroupUserMapper imGroupUserMapper; private ImGroupUserMapper imGroupUserMapper;
// 获取群组在线用户列表 // 获取群组在线用户列表
public List<Long> getOnlineGroupUsers(Long userId) { public List<Long> getAllOnlineGroupUsers(Long userId) {
// 1️⃣ 查询用户所在的群组ID列表 // 1️⃣ 查询用户所在的群组ID列表
LambdaQueryWrapper<ImGroupUser> groupWrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<ImGroupUser> groupWrapper = new LambdaQueryWrapper<>();
groupWrapper.eq(ImGroupUser::getUserId, userId) groupWrapper.eq(ImGroupUser::getUserId, userId)
@@ -48,6 +48,19 @@ public class ImGroupUserService extends ServiceImpl<ImGroupUserMapper, ImGroupUs
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
public List<Long> getGroupOnlineUserIds(Long groupId) {
LambdaQueryWrapper<ImGroupUser> groupWrapper = new LambdaQueryWrapper<>();
groupWrapper.eq(ImGroupUser::getGroupId, groupId)
.select(ImGroupUser::getUserId);
List<ImGroupUser> groupMembers = imGroupUserMapper.selectList(groupWrapper);
return groupMembers.stream()
.map(ImGroupUser::getUserId)
.filter(SessionManager::isOnline)
.distinct() // 去重
.collect(Collectors.toList());
}
public void decreaseUnreadCount(Long groupId,Long userId){ public void decreaseUnreadCount(Long groupId,Long userId){
LambdaUpdateWrapper<ImGroupUser> updateWrapper = new LambdaUpdateWrapper<>(); LambdaUpdateWrapper<ImGroupUser> updateWrapper = new LambdaUpdateWrapper<>();
@@ -70,4 +83,15 @@ public class ImGroupUserService extends ServiceImpl<ImGroupUserMapper, ImGroupUs
return queryGroupUserVOS; return queryGroupUserVOS;
} }
public void updateGroupUnreadCount(Long groupId, Long excludeUserId) {
LambdaUpdateWrapper<ImGroupUser> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper
.eq(ImGroupUser::getGroupId, groupId)
.ne(ImGroupUser::getUserId, excludeUserId)
.setSql("b_unread_count = b_unread_count + 1");
imGroupUserMapper.update(updateWrapper);
}
} }
@@ -9,12 +9,10 @@
SELECT * SELECT *
FROM ( FROM (
SELECT SELECT
f.b_id AS id,
f.b_friend_id AS friendId, f.b_friend_id AS friendId,
u.b_name AS friendName, u.b_name AS friendName,
u.b_avatar AS friendAvatar, u.b_avatar AS friendAvatar,
f.b_unread_count AS unreadCount, f.b_unread_count AS unreadCount,
m.b_id AS lastMessageId,
m.b_send_time AS lastMessageTime, m.b_send_time AS lastMessageTime,
m.b_type AS lastMessageType, m.b_type AS lastMessageType,
m.b_content AS lastMessageContent, m.b_content AS lastMessageContent,
@@ -63,4 +61,15 @@
WHERE b_user_id = #{userId} and b_friend_id = #{friendId} WHERE b_user_id = #{userId} and b_friend_id = #{friendId}
</update> </update>
<select id="selectFriendProfile" resultType="cn.link56.im.model.vo.FriendProfileVO">
SELECT
f.b_friend_id AS friendId,
u.b_name AS friendName,
u.b_avatar AS friendAvatar,
f.b_unread_count AS unreadCount
FROM b_im_friend f
INNER JOIN b_company_user u ON f.b_friend_id = u.b_id
WHERE f.b_user_id = #{userId} AND f.b_friend_id = #{friendId}
</select>
</mapper> </mapper>
@@ -14,12 +14,10 @@
g.b_avatar AS groupAvatar, g.b_avatar AS groupAvatar,
gu.b_unread_count AS unreadCount, gu.b_unread_count AS unreadCount,
m.b_id AS lastMessageId,
m.b_send_time AS lastMessageTime, m.b_send_time AS lastMessageTime,
m.b_type AS lastMessageType, m.b_type AS lastMessageType,
m.b_content AS lastMessageContent, m.b_content AS lastMessageContent,
u.b_id AS lastMessageSenderId,
u.b_name AS lastMessageSenderName, u.b_name AS lastMessageSenderName,
ROW_NUMBER() OVER ( ROW_NUMBER() OVER (
@@ -29,7 +27,7 @@
) AS rn ) AS rn
FROM b_im_group g FROM b_im_group g
INNER JOIN b_im_group_user gu ON g.b_id = gu.b_group_id INNER JOIN b_im_group_user gu ON g.b_id = gu.b_group_id
LEFT JOIN b_im_group_message m ON gu.b_last_message_id = m.b_id LEFT JOIN b_im_group_message m ON g.b_last_message_id = m.b_id
LEFT JOIN b_company_user u ON m.b_sender_id = u.b_id LEFT JOIN b_company_user u ON m.b_sender_id = u.b_id
WHERE gu.b_user_id = #{userId} WHERE gu.b_user_id = #{userId}
<if test="name != null and name != ''"> <if test="name != null and name != ''">
@@ -51,4 +49,18 @@
</if> </if>
</select> </select>
<select id="selectGroupProfile"
resultType="cn.link56.im.model.vo.GroupProfileVO">
SELECT
g.b_id AS groupId,
g.b_name AS groupName,
g.b_avatar AS groupAvatar,
gu.b_unread_count AS unreadCount
FROM b_im_group g
INNER JOIN b_im_group_user gu ON g.b_id = gu.b_group_id
WHERE gu.b_user_id = #{userId}
AND g.b_id = #{groupId}
</select>
</mapper> </mapper>
@@ -1,5 +1,5 @@
import type { CreateFriendParams, ImFriend, ImGroup, ImGroupMessage, ImGroupUser, ImPrivateMessage, QueryFriendParams, QueryGroupMessageParams, QueryGroupParams, QueryGroupUserParams, QueryPrivateMessageParams, UpdateGroupUnreadParams, UpdatePrivateUnreadParams } from '@/types/im' import type { CreateFriendParams, ImFriend, ImFriendProfile, ImGroup, ImGroupMessage, ImGroupProfile, ImGroupUser, ImPrivateMessage, QueryFriendParams, QueryGroupMessageParams, QueryGroupParams, QueryGroupUserParams, QueryPrivateMessageParams, UpdateGroupUnreadParams, UpdatePrivateUnreadParams } from '@/types/im'
import { httpPostJson } from '@/request/utils' import { httpGet, httpPostJson } from '@/request/utils'
export function createFriendApi(params: CreateFriendParams) { export function createFriendApi(params: CreateFriendParams) {
return httpPostJson<void>(`/im/friend/create`, params) return httpPostJson<void>(`/im/friend/create`, params)
@@ -31,3 +31,11 @@ export function updatePrivateUnreadApi(params: UpdatePrivateUnreadParams) {
export function queryGroupUserApi(params: QueryGroupUserParams) { export function queryGroupUserApi(params: QueryGroupUserParams) {
return httpPostJson<ImGroupUser[]>('/im/group/user/query', params) return httpPostJson<ImGroupUser[]>('/im/group/user/query', params)
} }
export function getFriendProfileApi(friendId: number) {
return httpGet<ImFriendProfile>(`/im/friend/${friendId}`)
}
export function getGroupProfileApi(groupId: number) {
return httpGet<ImGroupProfile>(`/im/group/${groupId}`)
}
@@ -256,7 +256,7 @@ function createWebSocket(options?: Partial<UseWebSocketOptions>) {
} }
function sendGroup(groupId: number, content: any) { function sendGroup(groupId: number, content: any) {
send(MessageType.GROUP, { groupId, content }) send(MessageType.GROUP, content, groupId)
} }
/* ================== 初始化 ================== */ /* ================== 初始化 ================== */
@@ -13,7 +13,6 @@ export enum MessageType {
PRIVATE = 8, PRIVATE = 8,
GROUP = 9, GROUP = 9,
FRIEND_UPDATE = 10,
} }
export interface MessageRequest { export interface MessageRequest {
@@ -48,15 +47,12 @@ export interface QueryFriendParams {
} }
export interface ImFriend { export interface ImFriend {
id: number
friendId: number friendId: number
friendName: string friendName: string
friendAvatar: string friendAvatar: string
unreadCount: number unreadCount: number
lastMessageId: number
lastMessageTime: number lastMessageTime: number
lastMessageType: number lastMessageType: number
lastMessageContent: string lastMessageContent: string
@@ -79,12 +75,10 @@ export interface ImGroup {
unreadCount: number unreadCount: number
lastMessageId: number
lastMessageTime: number lastMessageTime: number
lastMessageType: number lastMessageType: number
lastMessageContent: string lastMessageContent: string
lastMessageSenderId: number
lastMessageSenderName: string lastMessageSenderName: string
} }
@@ -152,3 +146,17 @@ export interface ImGroupUser {
userName: string userName: string
isOnline: boolean isOnline: boolean
} }
export interface ImFriendProfile {
friendId: number
friendName: string
friendAvatar: string
unreadCount: number
}
export interface ImGroupProfile {
groupId: number
groupName: string
groupAvatar: string
unreadCount: number
}
@@ -7,6 +7,7 @@ import { useScrollWatcher } from '@/hooks/useScrollWatcher'
import { queryGroupMessageApi, queryGroupUserApi } from '@/request/api/im' import { queryGroupMessageApi, queryGroupUserApi } from '@/request/api/im'
import { useUserStore } from '@/store/user' import { useUserStore } from '@/store/user'
import { useWebSocket } from '@/store/websocket' import { useWebSocket } from '@/store/websocket'
import { MessageType } from '@/types/im'
import { formatImTime } from '@/utils/time' import { formatImTime } from '@/utils/time'
import { generateUUID } from '@/utils/uuid' import { generateUUID } from '@/utils/uuid'
@@ -22,6 +23,40 @@ onMounted(() => {
websocket.onMessage(async (msg) => { websocket.onMessage(async (msg) => {
const { from, type, data } = msg const { from, type, data } = msg
if (type === MessageType.GROUP) {
// 自己发的
if (userId === data.senderId) {
messages.value.forEach((item) => {
if (item._tempId === data.tempId) {
item._status = 'sent'
item._tempId = ''
item.id = data.id
}
})
}
else {
if (props.group.groupId === from) {
const newData: ImGroupMessage = {
id: data.id,
groupId: data.groupId,
senderId: data.senderId,
senderName: data.senderName,
senderAvatar: data.senderAvatar,
content: data.content,
type: data.type,
sendTime: data.sendTime,
}
messages.value.push(newData)
// 更新未读数量,交给外部
// 等待 DOM 更新再滚动到底部
await nextTick()
scrollToLatestMessage()
}
}
}
}) })
}) })
@@ -52,6 +87,7 @@ watch(
pagination.value = { pageSize: 10, loading: false, hasMore: true } pagination.value = { pageSize: 10, loading: false, hasMore: true }
loadMessages('down') loadMessages('down')
groupUsers.value = []
loadGroupUsers() loadGroupUsers()
} }
}, },
@@ -106,8 +142,6 @@ async function loadMessages(direction = 'down') {
pagination.value.loading = true pagination.value.loading = true
const userId = userStore.user.id
let beforeTimestamp: number | null = null let beforeTimestamp: number | null = null
let beforeId: number | null = null let beforeId: number | null = null
@@ -197,39 +231,44 @@ async function sendMessage() {
ElMessage.warning('发送消息不能为空') ElMessage.warning('发送消息不能为空')
} }
// const userId = userStore.user.id const userId = userStore.user.id
// const _tempId = generateUUID() const groupId = props.group.groupId
const _tempId = generateUUID()
// const localMessage: ImPrivateMessage = { const localMessage: ImGroupMessage = {
// id: -Date.now(), id: -Date.now(),
// senderId: userId, groupId,
// receiverId: props.friend.friendId, senderId: userId,
// content, senderAvatar: userStore.user.avatar,
// type: 0, senderName: userStore.user.name,
// sendTime: Date.now(), content,
// _tempId, type: 0,
// _status: 'sending', sendTime: Date.now(),
// } _tempId,
_status: 'sending',
}
// messages.value.push(localMessage) messages.value.push(localMessage)
// // 清空输入框 // 清空输入框
// if (chatInputRef.value) { if (chatInputRef.value) {
// chatInputRef.value.innerHTML = '' chatInputRef.value.innerHTML = ''
// } }
// const privateData: any = { const sentData: any = {
// content, content,
// type: 0, type: 0,
// tempId: _tempId, tempId: _tempId,
// } senderAvatar: userStore.user.avatar,
senderName: userStore.user.name,
}
// websocket.sendPrivate(props.friend.friendId, privateData) websocket.sendGroup(groupId, sentData)
// chatInputValue.value = '' chatInputValue.value = ''
// // 等待 DOM 更新再滚动到底部 // 等待 DOM 更新再滚动到底部
// await nextTick() await nextTick()
// scrollToLatestMessage() scrollToLatestMessage()
} }
</script> </script>
@@ -3,7 +3,7 @@ import type { ImFriend, ImGroup } from '@/types/im'
import { Search } from '@element-plus/icons-vue' import { Search } from '@element-plus/icons-vue'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { queryFriendApi, queryGroupApi, updateGroupUnreadApi, updatePrivateUnreadApi } from '@/request/api/im' import { getFriendProfileApi, getGroupProfileApi, queryFriendApi, queryGroupApi, updateGroupUnreadApi, updatePrivateUnreadApi } from '@/request/api/im'
import { useUserStore } from '@/store/user' import { useUserStore } from '@/store/user'
import { useWebSocket } from '@/store/websocket' import { useWebSocket } from '@/store/websocket'
import { MessageType } from '@/types/im' import { MessageType } from '@/types/im'
@@ -78,7 +78,7 @@ onMounted(() => {
loadGroups() loadGroups()
loadFriends() loadFriends()
websocket.onMessage((msg) => { websocket.onMessage(async (msg) => {
const { type, from, data } = msg const { type, from, data } = msg
// 更新在线状态 // 更新在线状态
@@ -94,55 +94,164 @@ onMounted(() => {
else if (type === MessageType.OFFLINE) { else if (type === MessageType.OFFLINE) {
updateOnlineStatus(false) updateOnlineStatus(false)
} }
else if (type === MessageType.FRIEND_UPDATE) {
const updatedFriend = data as ImFriend
// 检查是否已经存在
const exists = friendState.value.list.some(
item => item.friendId === updatedFriend.friendId,
)
// 如果不存在,放到第一个
if (!exists) {
friendState.value.list.unshift(updatedFriend)
friendState.value.total += 1
}
}
else if (type === MessageType.PRIVATE) { else if (type === MessageType.PRIVATE) {
const chatFriendId = currentFriendChat.value?.friendId const chatFriendId = currentFriendChat.value?.friendId
const { senderId, receiverId, type: contentType, content, sendTime } = data const { senderId, receiverId, type: contentType, content, sendTime } = data
// 遍历好友列表更新最近消息和未读 // 私聊中,对方一定是 sender 或 receiver
friendState.value.list.forEach((item) => { const targetFriendId = senderId === userId ? receiverId : senderId
if (item.friendId === senderId || item.friendId === receiverId) {
// 先找本地好友
const friend = friendState.value.list.find(
item => item.friendId === targetFriendId,
)
// 如果本地不存在,按需拉取
if (!friend) {
try {
const res = await getFriendProfileApi(targetFriendId)
const newFriend: ImFriend = {
friendId: res.data.friendId,
friendAvatar: res.data.friendAvatar,
friendName: res.data.friendName,
unreadCount: res.data.unreadCount,
lastMessageTime: sendTime,
lastMessageContent: content,
lastMessageType: contentType,
isOnline: true,
}
friendState.value.list.unshift(newFriend)
}
catch (e) {
console.error('getFriendProfileApi failed', e)
return
}
}
if (!friend) {
return
}
// 更新最近消息 // 更新最近消息
item.lastMessageTime = sendTime friend.lastMessageTime = sendTime
item.lastMessageContent = content friend.lastMessageContent = content
item.lastMessageType = contentType friend.lastMessageType = contentType
// 如果是别人发的消息并且不是当前聊天好友,增加未读 // 未读数处理
if (userId !== senderId && senderId !== chatFriendId && item.friendId === senderId) { if (userId !== senderId && targetFriendId !== chatFriendId) {
item.unreadCount = (item.unreadCount || 0) + 1 friend.unreadCount = (friend.unreadCount || 0) + 1
} }
}
})
// 更新当前聊天对象最近消息 // 当前聊天窗口
if (currentFriendChat.value && (senderId === chatFriendId || receiverId === chatFriendId)) { if (currentFriendChat.value && chatFriendId === targetFriendId) {
currentFriendChat.value.lastMessageTime = sendTime currentFriendChat.value.lastMessageTime = sendTime
currentFriendChat.value.lastMessageContent = content currentFriendChat.value.lastMessageContent = content
currentFriendChat.value.lastMessageType = contentType currentFriendChat.value.lastMessageType = contentType
// 重置未读数量,如果是别人发的消息且当前聊天窗口打开
if (userId !== senderId) { if (userId !== senderId) {
currentFriendChat.value.unreadCount = 0 currentFriendChat.value.unreadCount = 0
// 调用接口同步重置
updatePrivateUnreadApi({ updatePrivateUnreadApi({
userId, userId,
friendId: chatFriendId!, friendId: chatFriendId!,
}) })
} }
} }
// 永远置顶
const index = friendState.value.list.findIndex(
item => item.friendId === targetFriendId,
)
if (index > -1) {
const [f] = friendState.value.list.splice(index, 1)
friendState.value.list.unshift(f)
}
}
else if (type === MessageType.GROUP) {
const {
groupId,
senderId,
senderName,
type: contentType,
content,
sendTime,
} = data
const currentGroupId = currentGroupChat.value?.groupId
// 1️⃣ 查找本地群
const group = groupState.value.list.find(
item => item.groupId === groupId,
)
// 2️⃣ 本地没有 → 懒加载群 Profile
if (!group) {
try {
const res = await getGroupProfileApi(groupId)
const newGroup: ImGroup = {
groupId: res.data.groupId,
groupAvatar: res.data.groupAvatar,
groupName: res.data.groupName,
unreadCount: res.data.unreadCount,
lastMessageTime: sendTime,
lastMessageContent: content,
lastMessageType: contentType,
lastMessageSenderName: senderName,
}
groupState.value.list.unshift(newGroup)
}
catch (e) {
console.error('getGroupProfileApi failed', e)
return
}
}
if (!group) {
return
}
// 3️⃣ 更新最近消息
group.lastMessageTime = sendTime
group.lastMessageContent = content
group.lastMessageType = contentType
group.lastMessageSenderName = senderName
// 4️⃣ 未读处理(不是自己 & 当前群未打开)
if (senderId !== userId && groupId !== currentGroupId) {
group.unreadCount = (group.unreadCount || 0) + 1
}
// 5️⃣ 当前群窗口处理
if (currentGroupChat.value && currentGroupId === groupId) {
currentGroupChat.value.lastMessageTime = sendTime
currentGroupChat.value.lastMessageContent = content
currentGroupChat.value.lastMessageType = contentType
currentGroupChat.value.lastMessageSenderName = senderName
if (senderId !== userId) {
currentGroupChat.value.unreadCount = 0
updateGroupUnreadApi({
userId,
groupId,
})
}
}
// 6️⃣ 永远置顶
const index = groupState.value.list.findIndex(
item => item.groupId === groupId,
)
if (index > -1) {
const [g] = groupState.value.list.splice(index, 1)
groupState.value.list.unshift(g)
}
} }
}) })
}) })
@@ -342,7 +451,7 @@ function onGroupChat(group: ImGroup) {
</div> </div>
<div class="im__list-body"> <div class="im__list-body">
<div <div
v-for="item in friendState.list" :key="item.id" class="im__chat-item" v-for="item in friendState.list" :key="item.friendId" class="im__chat-item"
:class="{ :class="{
'im__chat-item--selected': 'im__chat-item--selected':
isPrivateChat && item.friendId === currentFriendChat?.friendId, isPrivateChat && item.friendId === currentFriendChat?.friendId,
@@ -411,8 +520,11 @@ function onGroupChat(group: ImGroup) {
</div> </div>
<div class="im__chat-item-message"> <div class="im__chat-item-message">
<div v-if="item.lastMessageId" class="im__chat-item-last-message"> <div class="im__chat-item-last-message">
{{ item.lastMessageSenderName }}:{{ item.lastMessageContent }} <template v-if="item.lastMessageSenderName">
{{ item.lastMessageSenderName }}:
</template>
{{ item.lastMessageContent }}
</div> </div>
<el-badge :value="item.unreadCount || 0" :show-zero="false" :max="99" class="im__chat-item-unread-count" /> <el-badge :value="item.unreadCount || 0" :show-zero="false" :max="99" class="im__chat-item-unread-count" />
</div> </div>