diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/docs/代码修改日志 b/jeecg-boot/jeecg-module-system/jeecg-system-biz/docs/代码修改日志 index 339cf31..7d368e8 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/docs/代码修改日志 +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/docs/代码修改日志 @@ -495,9 +495,19 @@ jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestStd/components/MesXslRubber jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestStd/components/MesXslRubberQuickTestMethodSelectModal.vue jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestStd/components/MesXslRubberQuickTestStdMixerPsSelectModal.vue --- author:jiangxh---date:20260525--for: 【MES】原材料检验标准密炼PS批准时关联胶料快检实验标准置已批准(反审核不回退,触发由审核改为批准)--- -jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/common/XslMesBizConstants.java -jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRubberQuickTestStdService.java -jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRubberQuickTestStdServiceImpl.java jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerPsCompileServiceImpl.java jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRubberQuickTestStdController.java + +-- author:xsl---date:20260528--for: 【IM聊天-OA】IM群聊:创建群聊、群列表、群消息收发与未读 --- +jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_110__sys_im_group_chat.sql +jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/dto/SysImCreateGroupDTO.java +jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/entity/SysImConversation.java +jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/vo/SysImConversationVO.java +jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/mapper/SysImConversationMapper.java +jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/mapper/xml/SysImConversationMapper.xml +jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/ISysImChatService.java +jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/impl/SysImChatServiceImpl.java +jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/controller/SysImChatController.java +jeecgboot-vue3/src/views/system/im/im.api.ts +jeecgboot-vue3/src/views/system/im/ImCreateGroupModal.vue +jeecgboot-vue3/src/views/system/im/ImChat.vue diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/controller/SysImChatController.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/controller/SysImChatController.java index 5606159..9d63655 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/controller/SysImChatController.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/controller/SysImChatController.java @@ -11,6 +11,7 @@ import org.jeecg.common.api.vo.Result; import org.jeecg.common.system.vo.LoginUser; import org.jeecg.common.util.TokenUtils; import org.jeecg.common.util.oConvertUtils; +import org.jeecg.modules.im.dto.SysImCreateGroupDTO; import org.jeecg.modules.im.dto.SysImSendMessageDTO; import org.jeecg.modules.im.service.ISysImChatService; import org.jeecg.modules.im.vo.SysImContactVO; @@ -136,4 +137,22 @@ public class SysImChatController { return Result.OK(imChatService.listDeptMembers(user.getId(), resolveTenantId(user, request), user.getOrgCode(), keyword)); } //update-end---author:cursor ---date:20260528 for:【IM聊天-OA】联系人接口(兼容保留,同本部门)----------- + + //update-begin---author:xsl ---date:20260528 for:【IM聊天-OA】群聊接口----------- + @Operation(summary = "IM聊天-群聊列表") + @RequiresPermissions("sys:im:chat:list") + @GetMapping("/groups") + public Result> groups(HttpServletRequest request) { + LoginUser user = currentUser(); + return Result.OK(imChatService.listGroupConversations(user.getId(), resolveTenantId(user, request))); + } + + @Operation(summary = "IM聊天-创建群聊") + @RequiresPermissions("sys:im:chat:group") + @PostMapping("/group/create") + public Result createGroup(@RequestBody SysImCreateGroupDTO dto, HttpServletRequest request) { + LoginUser user = currentUser(); + return Result.OK(imChatService.createGroupConversation(user.getId(), resolveTenantId(user, request), user.getOrgCode(), dto)); + } + //update-end---author:xsl ---date:20260528 for:【IM聊天-OA】群聊接口----------- } diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/dto/SysImCreateGroupDTO.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/dto/SysImCreateGroupDTO.java new file mode 100644 index 0000000..33ffe96 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/dto/SysImCreateGroupDTO.java @@ -0,0 +1,20 @@ +package org.jeecg.modules.im.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * 创建 IM 群聊 + */ +@Data +@Schema(description = "创建IM群聊") +public class SysImCreateGroupDTO { + + @Schema(description = "群名称") + private String groupName; + + @Schema(description = "群成员用户ID(不含本人时可自动加入创建人)") + private List memberUserIds; +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/entity/SysImConversation.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/entity/SysImConversation.java index 14ed062..3f5865b 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/entity/SysImConversation.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/entity/SysImConversation.java @@ -20,10 +20,14 @@ public class SysImConversation implements Serializable { @TableId(type = IdType.ASSIGN_ID) private String id; - /** 会话类型 single单聊 */ + /** 会话类型 single单聊 group群聊 */ private String convType; /** 单聊唯一键 */ private String userPairKey; + /** 群名称 */ + private String groupName; + /** 群主用户ID */ + private String ownerId; /** 租户ID */ private Integer tenantId; /** 最后一条消息摘要 */ diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/mapper/SysImConversationMapper.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/mapper/SysImConversationMapper.java index 09d4b73..54c246c 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/mapper/SysImConversationMapper.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/mapper/SysImConversationMapper.java @@ -16,4 +16,9 @@ public interface SysImConversationMapper extends BaseMapper { * 查询当前用户的会话列表 */ List listMyConversations(@Param("userId") String userId, @Param("tenantId") Integer tenantId); + + /** + * 查询当前用户的群聊列表 + */ + List listMyGroupConversations(@Param("userId") String userId, @Param("tenantId") Integer tenantId); } diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/mapper/xml/SysImConversationMapper.xml b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/mapper/xml/SysImConversationMapper.xml index 92eed95..5af297d 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/mapper/xml/SysImConversationMapper.xml +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/mapper/xml/SysImConversationMapper.xml @@ -23,4 +23,26 @@ ORDER BY c.last_time DESC + + diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/ISysImChatService.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/ISysImChatService.java index 3440ec7..0e1c580 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/ISysImChatService.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/ISysImChatService.java @@ -4,6 +4,7 @@ package org.jeecg.modules.im.service; import com.baomidou.mybatisplus.core.metadata.IPage; +import org.jeecg.modules.im.dto.SysImCreateGroupDTO; import org.jeecg.modules.im.dto.SysImSendMessageDTO; import org.jeecg.modules.im.vo.SysImContactVO; @@ -34,7 +35,9 @@ public interface ISysImChatService { SysImConversationVO openSingleConversation(String userId, Integer tenantId, String orgCode, String targetUserId); + List listGroupConversations(String userId, Integer tenantId); + SysImConversationVO createGroupConversation(String userId, Integer tenantId, String orgCode, SysImCreateGroupDTO dto); IPage listMessages(String userId, String conversationId, Integer pageNo, Integer pageSize, String startTime, String beforeTime); diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/impl/SysImChatServiceImpl.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/impl/SysImChatServiceImpl.java index c9eeba9..a7d761f 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/impl/SysImChatServiceImpl.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/impl/SysImChatServiceImpl.java @@ -9,6 +9,7 @@ import org.jeecg.common.constant.WebsocketConst; import org.jeecg.common.exception.JeecgBootException; import org.jeecg.common.util.DateUtils; import org.jeecg.common.util.oConvertUtils; +import org.jeecg.modules.im.dto.SysImCreateGroupDTO; import org.jeecg.modules.im.dto.SysImSendMessageDTO; import org.jeecg.modules.im.entity.SysImConversation; import org.jeecg.modules.im.entity.SysImConversationMember; @@ -41,6 +42,8 @@ import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.LinkedHashSet; +import java.util.Set; import java.util.stream.Collectors; /** @@ -51,6 +54,7 @@ import java.util.stream.Collectors; public class SysImChatServiceImpl implements ISysImChatService { private static final String CONV_TYPE_SINGLE = "single"; + private static final String CONV_TYPE_GROUP = "group"; private static final String MSG_TYPE_TEXT = "text"; private static final String MSG_TYPE_IMAGE = "image"; private static final String MSG_TYPE_BIZ_RECORD = "biz_record"; @@ -116,6 +120,84 @@ public class SysImChatServiceImpl implements ISysImChatService { } //update-end---author:cursor ---date:20260528 for:【IM聊天-OA】打开单聊会话----------- + //update-begin---author:xsl ---date:20260528 for:【IM聊天-OA】群聊会话列表与创建----------- + @Override + public List listGroupConversations(String userId, Integer tenantId) { + if (oConvertUtils.isEmpty(userId) || tenantId == null || tenantId <= 0) { + return Collections.emptyList(); + } + return conversationMapper.listMyGroupConversations(userId, tenantId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public SysImConversationVO createGroupConversation(String userId, Integer tenantId, String orgCode, SysImCreateGroupDTO dto) { + if (dto == null || oConvertUtils.isEmpty(dto.getGroupName())) { + throw new JeecgBootException("群名称不能为空"); + } + String groupName = dto.getGroupName().trim(); + if (groupName.length() > 30) { + throw new JeecgBootException("群名称不能超过30字"); + } + List memberIds = normalizeGroupMemberIds(userId, dto.getMemberUserIds()); + if (memberIds.size() < 2) { + throw new JeecgBootException("群聊至少需要2名成员"); + } + if (memberIds.size() > 50) { + throw new JeecgBootException("群成员不能超过50人"); + } + for (String memberId : memberIds) { + if (userId.equals(memberId)) { + continue; + } + validateTenantChat(userId, tenantId, orgCode, memberId); + } + Date now = new Date(); + SysImConversation conversation = new SysImConversation(); + conversation.setConvType(CONV_TYPE_GROUP); + conversation.setGroupName(groupName); + conversation.setOwnerId(userId); + conversation.setTenantId(tenantId); + conversation.setCreateBy(userId); + conversation.setCreateTime(now); + conversation.setUpdateTime(now); + conversationMapper.insert(conversation); + for (String memberId : memberIds) { + createMember(conversation.getId(), memberId, now); + } + return buildGroupConversationVo(conversation, userId); + } + + private List normalizeGroupMemberIds(String creatorId, List memberUserIds) { + Set memberIds = new LinkedHashSet<>(); + memberIds.add(creatorId); + if (memberUserIds != null) { + for (String memberId : memberUserIds) { + if (oConvertUtils.isNotEmpty(memberId)) { + memberIds.add(memberId); + } + } + } + return new ArrayList<>(memberIds); + } + + private SysImConversationVO buildGroupConversationVo(SysImConversation conversation, String userId) { + SysImConversationMember member = getMember(userId, conversation.getId()); + SysImConversationVO vo = new SysImConversationVO(); + vo.setConversationId(conversation.getId()); + vo.setConvType(CONV_TYPE_GROUP); + vo.setGroupName(conversation.getGroupName()); + vo.setOwnerId(conversation.getOwnerId()); + vo.setLastContent(conversation.getLastContent()); + vo.setLastTime(conversation.getLastTime()); + vo.setUnreadCount(member == null ? 0 : member.getUnreadCount()); + Long memberCount = memberMapper.selectCount(new LambdaQueryWrapper() + .eq(SysImConversationMember::getConversationId, conversation.getId())); + vo.setMemberCount(memberCount == null ? 0 : memberCount.intValue()); + return vo; + } + //update-end---author:xsl ---date:20260528 for:【IM聊天-OA】群聊会话列表与创建----------- + //update-begin---author:cursor ---date:20260528 for:【IM聊天-OA】消息分页----------- @Override public IPage listMessages(String userId, String conversationId, Integer pageNo, Integer pageSize, String startTime, String beforeTime) { @@ -179,7 +261,15 @@ public class SysImChatServiceImpl implements ISysImChatService { SysImConversation conversation = conversationMapper.selectById(dto.getConversationId()); //update-begin---author:cursor ---date:20260528 for:【IM聊天-OA】图片消息会话摘要----------- - conversation.setLastContent(truncate(resolveLastContent(message.getMsgType(), message.getContent()), 200)); + String lastContent = resolveLastContent(message.getMsgType(), message.getContent()); + if (CONV_TYPE_GROUP.equals(conversation.getConvType())) { + SysUser sender = userMapper.selectById(userId); + String senderName = sender != null ? oConvertUtils.getString(sender.getRealname(), sender.getUsername()) : ""; + if (oConvertUtils.isNotEmpty(senderName)) { + lastContent = senderName + ": " + lastContent; + } + } + conversation.setLastContent(truncate(lastContent, 200)); //update-end---author:cursor ---date:20260528 for:【IM聊天-OA】图片消息会话摘要----------- conversation.setLastTime(now); conversation.setUpdateTime(now); @@ -188,7 +278,7 @@ public class SysImChatServiceImpl implements ISysImChatService { memberMapper.incrementUnreadExceptSender(dto.getConversationId(), userId); SysImMessageVO messageVo = toMessageVo(message, userId); fillBizRecordReceiverPermission(messageVo, message, userId, new HashMap<>(4)); - pushChatMessage(dto.getConversationId(), userId, messageVo); + pushChatMessage(dto.getConversationId(), userId, messageVo, conversation.getConvType()); return messageVo; } //update-end---author:cursor ---date:20260528 for:【IM聊天-OA】发送消息----------- @@ -422,6 +512,10 @@ public class SysImChatServiceImpl implements ISysImChatService { if (!Boolean.TRUE.equals(vo.getMine()) || !MSG_TYPE_BIZ_RECORD.equals(vo.getMsgType())) { return; } + SysImConversation conversation = conversationMapper.selectById(message.getConversationId()); + if (conversation == null || !CONV_TYPE_SINGLE.equals(conversation.getConvType())) { + return; + } String pagePath = extractBizRecordPagePath(vo.getContent()); if (oConvertUtils.isEmpty(pagePath)) { return; @@ -509,7 +603,7 @@ public class SysImChatServiceImpl implements ISysImChatService { //update-end---author:xsl ---date:20260528 for:【IM聊天-OA】发送方提示接收方无功能权限----------- //update-end---author:cursor ---date:20260528 for:【IM聊天-OA】消息列表批量填充发送人----------- - private void pushChatMessage(String conversationId, String senderId, SysImMessageVO messageVo) { + private void pushChatMessage(String conversationId, String senderId, SysImMessageVO messageVo, String convType) { List members = memberMapper.selectList(new LambdaQueryWrapper() .eq(SysImConversationMember::getConversationId, conversationId)); for (SysImConversationMember member : members) { @@ -520,6 +614,9 @@ public class SysImChatServiceImpl implements ISysImChatService { obj.put(WebsocketConst.MSG_CMD, WebsocketConst.MSG_CHAT); obj.put(WebsocketConst.MSG_USER_ID, member.getUserId()); obj.put("conversationId", conversationId); + //update-begin---author:xsl ---date:20260528 for:【IM聊天-OA】WebSocket推送会话类型区分群聊----------- + obj.put("convType", convType); + //update-end---author:xsl ---date:20260528 for:【IM聊天-OA】WebSocket推送会话类型区分群聊----------- obj.put("messageId", messageVo.getId()); obj.put("senderId", messageVo.getSenderId()); //update-begin---author:cursor ---date:20260528 for:【IM聊天-OA】WebSocket推送补全头像字段----------- diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/vo/SysImConversationVO.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/vo/SysImConversationVO.java index 96997df..1596d44 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/vo/SysImConversationVO.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/vo/SysImConversationVO.java @@ -34,4 +34,10 @@ public class SysImConversationVO { private String targetUsername; @Schema(description = "对方头像") private String targetAvatar; + @Schema(description = "群名称") + private String groupName; + @Schema(description = "群成员数") + private Integer memberCount; + @Schema(description = "群主用户ID") + private String ownerId; } diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_110__sys_im_group_chat.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_110__sys_im_group_chat.sql new file mode 100644 index 0000000..cc15bca --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_110__sys_im_group_chat.sql @@ -0,0 +1,22 @@ +-- IM 群聊:扩展会话表字段 +SET NAMES utf8mb4; + +ALTER TABLE `sys_im_conversation` + ADD COLUMN `group_name` varchar(100) DEFAULT NULL COMMENT '群名称' AFTER `user_pair_key`, + ADD COLUMN `owner_id` varchar(32) DEFAULT NULL COMMENT '群主用户ID' AFTER `group_name`; + +ALTER TABLE `sys_im_conversation` + MODIFY COLUMN `conv_type` varchar(10) NOT NULL DEFAULT 'single' COMMENT '会话类型 single单聊 group群聊'; + +INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`) +VALUES ('1995000000000000113', '1995000000000000110', '创建群聊', 2, 'sys:im:chat:group', '1', 3.00, 0, 1, 0, '1', 0, 'admin', NOW()); + +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) +SELECT REPLACE(UUID(), '-', ''), r.id, p.id, NULL, NOW(), '127.0.0.1' +FROM `sys_role` r +CROSS JOIN `sys_permission` p +WHERE r.`role_code` = 'admin' + AND p.`id` = '1995000000000000113' + AND NOT EXISTS ( + SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id` + ); diff --git a/jeecgboot-vue3/src/hooks/web/useWebSocket.ts b/jeecgboot-vue3/src/hooks/web/useWebSocket.ts index 1b31be6..91c6cd6 100644 --- a/jeecgboot-vue3/src/hooks/web/useWebSocket.ts +++ b/jeecgboot-vue3/src/hooks/web/useWebSocket.ts @@ -10,6 +10,15 @@ import { useUserStore } from '/@/store/modules/user'; let result: WebSocketResult; const listeners = new Map(); let connectedUrl = ''; +//update-begin---author:xsl ---date:20260528 for:【IM聊天】WS 重连后通知监听方重新拉取消息----------- +let wsConnectionCount = 0; +const reconnectListeners = new Set<() => void>(); + +export function onWebSocketReconnect(callback: () => void) { + reconnectListeners.add(callback); + return () => reconnectListeners.delete(callback); +} +//update-end---author:xsl ---date:20260528 for:【IM聊天】WS 重连后通知监听方重新拉取消息----------- /** * 构建系统 WebSocket 地址(含 context-path,如 /jeecg-boot) @@ -67,7 +76,15 @@ export function connectWebSocket(url: string) { protocols: [token], // 代码逻辑说明: [issues/6662] 演示系统socket总断,换一个写法 onConnected: function (ws) { + wsConnectionCount++; console.log('[WebSocket] 连接成功', ws); + //update-begin---author:xsl ---date:20260528 for:【IM聊天】WS 重连后通知监听方重新拉取消息----------- + if (wsConnectionCount > 1) { + reconnectListeners.forEach((cb) => { + try { cb(); } catch (err) { console.error(err); } + }); + } + //update-end---author:xsl ---date:20260528 for:【IM聊天】WS 重连后通知监听方重新拉取消息----------- }, onDisconnected: function (ws, event) { console.log('[WebSocket] 连接断开:', ws, event); diff --git a/jeecgboot-vue3/src/views/system/im/ImChat.vue b/jeecgboot-vue3/src/views/system/im/ImChat.vue index c990296..b4a958a 100644 --- a/jeecgboot-vue3/src/views/system/im/ImChat.vue +++ b/jeecgboot-vue3/src/views/system/im/ImChat.vue @@ -13,7 +13,23 @@ -
+ +
+ + +
+ +
- +
+ + + 发起群聊 + +
+
+ +
@@ -70,7 +123,7 @@
- {{ activeMember ? activeMember.realname || activeMember.username : 'IM聊天' }} + {{ activeChatTitle }}
- @@ -37,11 +42,13 @@ import dayjs from 'dayjs'; import { useModal } from '/@/components/Modal'; import { getFileAccessHttpUrl } from '/@/utils/common/compUtils'; - import { fetchDeptMembers } from '/@/views/system/im/im.api'; + import { fetchDeptMembers, fetchGroups } from '/@/views/system/im/im.api'; import { formatImMessagePreview } from '/@/views/system/im/imMessageUtil'; import { syncImUnreadFromMembers } from '/@/views/system/im/useImUnread'; + import { getCachedGroupUnreadItems, initGroupUnreadFromList } from '/@/views/system/im/imCache'; import ImChatModal from '/@/views/system/im/ImChatModal.vue'; import { openImChat } from '/@/views/system/im/imSession'; + import { Icon } from '/@/components/Icon'; defineOptions({ name: 'SysImChatMessageList' }); @@ -49,26 +56,59 @@ (e: 'closeModal'): void; }>(); - interface ChatMemberItem { + //update-begin---author:xsl ---date:20260528 for:【IM聊天】统一单聊与群聊展示类型----------- + type ItemType = 'single' | 'group'; + + interface UnreadItem { + type: ItemType; + /** 单聊:userId;群聊:conversationId */ id: string; - username: string; - realname?: string; + displayName: string; avatar?: string; conversationId?: string; lastContent?: string; lastTime?: string; unreadCount?: number; } + //update-end---author:xsl ---date:20260528 for:【IM聊天】统一单聊与群聊展示类型----------- const loading = ref(false); - const members = ref([]); + const members = ref([]); + const groups = ref([]); const [registerImChatModal, { openModal: openImChatModal }] = useModal(); - const unreadList = computed(() => - members.value + //update-begin---author:xsl ---date:20260528 for:【IM聊天】合并单聊和群聊未读列表----------- + const unreadList = computed(() => { + const singleItems: UnreadItem[] = members.value .filter((item) => (item.unreadCount || 0) > 0) - .sort((a, b) => dayjs(b.lastTime || 0).valueOf() - dayjs(a.lastTime || 0).valueOf()), - ); + .map((item) => ({ + type: 'single', + id: item.id, + displayName: item.realname || item.username || '', + avatar: item.avatar, + conversationId: item.conversationId, + lastContent: item.lastContent, + lastTime: item.lastTime, + unreadCount: item.unreadCount, + })); + + const groupItems: UnreadItem[] = groups.value + .filter((item) => (item.unreadCount || 0) > 0) + .map((item) => ({ + type: 'group', + id: item.conversationId, + displayName: item.groupName || '群聊', + conversationId: item.conversationId, + lastContent: item.lastContent, + lastTime: item.lastTime, + unreadCount: item.unreadCount, + })); + + return [...singleItems, ...groupItems].sort( + (a, b) => dayjs(b.lastTime || 0).valueOf() - dayjs(a.lastTime || 0).valueOf(), + ); + }); + //update-end---author:xsl ---date:20260528 for:【IM聊天】合并单聊和群聊未读列表----------- const locale = computed(() => ({ emptyText: loading.value ? ' ' : '暂无未读聊天消息', @@ -96,36 +136,52 @@ return d.format('MM-DD HH:mm'); } + //update-begin---author:xsl ---date:20260528 for:【IM聊天】同时拉取单聊成员和群聊列表,修复 syncImUnreadFromMembers 丢失群聊未读----------- async function reload(silent = false) { - //update-begin---author:cursor ---date:20250528 for:【IM聊天-OA】聊天消息列表静默刷新,避免 spin 遮罩闪烁----------- - const showLoading = !silent && members.value.length === 0; + const showLoading = !silent && members.value.length === 0 && groups.value.length === 0; if (showLoading) { loading.value = true; } try { - members.value = ((await fetchDeptMembers()) || []) as ChatMemberItem[]; - syncImUnreadFromMembers(members.value); + const [fetchedMembers, fetchedGroups] = await Promise.all([ + fetchDeptMembers().catch(() => []), + fetchGroups().catch(() => []), + ]); + members.value = (fetchedMembers || []) as any[]; + groups.value = (fetchedGroups || []) as any[]; + // 同步群聊未读到全局缓存 + initGroupUnreadFromList(groups.value); + // 合并单聊 + 群聊未读数,防止 syncImUnreadFromMembers 只传成员数据导致群聊角标清零 + syncImUnreadFromMembers([...members.value, ...getCachedGroupUnreadItems()]); } catch { if (!silent) { members.value = []; + groups.value = []; } } finally { if (showLoading) { loading.value = false; } } - //update-end---author:cursor ---date:20250528 for:【IM聊天-OA】聊天消息列表静默刷新,避免 spin 遮罩闪烁----------- } + //update-end---author:xsl ---date:20260528 for:【IM聊天】同时拉取单聊成员和群聊列表,修复 syncImUnreadFromMembers 丢失群聊未读----------- - async function handleOpenChat(item: ChatMemberItem) { - //update-begin---author:xsl ---date:20260528 for:【IM聊天-OA】统一 IM 打开入口,全页优先----------- - const mode = await openImChat({ targetUserId: item.id, pageContext: null }); - if (mode === 'modal') { - openImChatModal(true, { targetUserId: item.id, pageContext: null }); + //update-begin---author:xsl ---date:20260528 for:【IM聊天】点击群聊条目直接打开群聊会话----------- + async function handleOpenChat(item: UnreadItem) { + if (item.type === 'group') { + const mode = await openImChat({ conversationId: item.conversationId }); + if (mode === 'modal') { + openImChatModal(true, { conversationId: item.conversationId }); + } + } else { + const mode = await openImChat({ targetUserId: item.id, pageContext: null }); + if (mode === 'modal') { + openImChatModal(true, { targetUserId: item.id, pageContext: null }); + } } - //update-end---author:xsl ---date:20260528 for:【IM聊天-OA】统一 IM 打开入口,全页优先----------- emit('closeModal'); } + //update-end---author:xsl ---date:20260528 for:【IM聊天】点击群聊条目直接打开群聊会话----------- defineExpose({ reload }); diff --git a/jeecgboot-vue3/src/views/system/message/components/SysMessageModal.vue b/jeecgboot-vue3/src/views/system/message/components/SysMessageModal.vue index a494a02..e002cd2 100644 --- a/jeecgboot-vue3/src/views/system/message/components/SysMessageModal.vue +++ b/jeecgboot-vue3/src/views/system/message/components/SysMessageModal.vue @@ -154,7 +154,9 @@ import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; import { useImUnread } from '/@/views/system/im/useImUnread'; import { onWebSocket, offWebSocket, ensureWebSocketConnected } from '/@/hooks/web/useWebSocket'; - import { handleImChatSocket, isImChatUiOpen } from '/@/views/system/im/imCache'; + //update-begin---author:xsl ---date:20260528 for:【IM聊天】只保留 isImChatUiOpen,handleImChatSocket 已移除(由 notify 统一调用)----------- + import { isImChatUiOpen } from '/@/views/system/im/imCache'; + //update-end---author:xsl ---date:20260528 for:【IM聊天】只保留 isImChatUiOpen,handleImChatSocket 已移除(由 notify 统一调用)----------- import calendar from '/@/assets/icons/calendarNotice.png'; import folder from '/@/assets/icons/folderNotice.png'; import system from '/@/assets/icons/systemNotice.png'; @@ -439,8 +441,12 @@ function onModalWebSocket(data) { if (data.cmd === 'chat') { - handleImChatSocket(data); + //update-begin---author:xsl ---date:20260528 for:【IM聊天】SysMessageModal 与 notify/index.vue 同时监听 WS,handleImChatSocket 已由 notify 处理,此处只做 UI 刷新----------- + // handleImChatSocket 已在 notify/index.vue 的 onWebSocketMessage 中调用 + // 如果在此重复调用,incrementGroupUnread 和 dispatchImChatSocketUi 会触发两次 + // 导致 patchGroup(unreadIncrement:1) 执行两次,群聊 tab 角标变为 2(实际只有 1 条消息) refreshImUnread(false); + //update-end---author:xsl ---date:20260528 for:【IM聊天】SysMessageModal 与 notify/index.vue 同时监听 WS,handleImChatSocket 已由 notify 处理,此处只做 UI 刷新----------- if (isImChatUiOpen()) { return; }