diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/docs/代码修改日志 b/jeecg-boot/jeecg-module-system/jeecg-system-biz/docs/代码修改日志 index 5cd6a7b..01b0b7c 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/docs/代码修改日志 +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/docs/代码修改日志 @@ -495,13 +495,23 @@ 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 + -- author:jiangxh---date:20260525--for: 【MES】胶料快检记录主子表、质量管理菜单、胶料信息批量检验生成 --- jeecg-boot/db/mes-xsl-rubber-quick-test-record-menu-permission.sql jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_108__mes_xsl_rubber_quick_test_record.sql 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..a677cac 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,10 +11,13 @@ 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.SysImGroupMembersDTO; import org.jeecg.modules.im.dto.SysImSendMessageDTO; import org.jeecg.modules.im.service.ISysImChatService; import org.jeecg.modules.im.vo.SysImContactVO; import org.jeecg.modules.im.vo.SysImConversationVO; +import org.jeecg.modules.im.vo.SysImGroupDetailVO; import org.jeecg.modules.im.vo.SysImMessageVO; import org.jeecg.modules.system.entity.SysUser; import org.jeecg.modules.system.service.ISysUserService; @@ -136,4 +139,87 @@ 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】群聊接口----------- + + //update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群管理接口----------- + @Operation(summary = "IM聊天-群聊详情") + @RequiresPermissions("sys:im:chat:list") + @GetMapping("/group/detail") + public Result groupDetail(@RequestParam(name = "conversationId") String conversationId) { + LoginUser user = currentUser(); + return Result.OK(imChatService.getGroupDetail(user.getId(), conversationId)); + } + + @Operation(summary = "IM聊天-添加群成员") + @RequiresPermissions("sys:im:chat:list") + @PostMapping("/group/addMembers") + public Result addGroupMembers(@RequestBody SysImGroupMembersDTO dto, HttpServletRequest request) { + LoginUser user = currentUser(); + return Result.OK(imChatService.addGroupMembers(user.getId(), resolveTenantId(user, request), user.getOrgCode(), dto)); + } + + @Operation(summary = "IM聊天-移除群成员") + @RequiresPermissions("sys:im:chat:list") + @PostMapping("/group/removeMember") + public Result removeGroupMember(@RequestParam(name = "conversationId") String conversationId, + @RequestParam(name = "memberUserId") String memberUserId) { + LoginUser user = currentUser(); + imChatService.removeGroupMember(user.getId(), conversationId, memberUserId); + return Result.OK("移除成功"); + } + + @Operation(summary = "IM聊天-修改群名称") + @RequiresPermissions("sys:im:chat:list") + @PostMapping("/group/rename") + public Result renameGroup(@RequestParam(name = "conversationId") String conversationId, + @RequestParam(name = "groupName") String groupName) { + LoginUser user = currentUser(); + return Result.OK(imChatService.renameGroup(user.getId(), conversationId, groupName)); + } + + @Operation(summary = "IM聊天-转让群主") + @RequiresPermissions("sys:im:chat:list") + @PostMapping("/group/transfer") + public Result transferGroupOwner(@RequestParam(name = "conversationId") String conversationId, + @RequestParam(name = "newOwnerId") String newOwnerId) { + LoginUser user = currentUser(); + imChatService.transferGroupOwner(user.getId(), conversationId, newOwnerId); + return Result.OK("转让成功"); + } + + @Operation(summary = "IM聊天-退出群聊") + @RequiresPermissions("sys:im:chat:list") + @PostMapping("/group/quit") + public Result quitGroup(@RequestParam(name = "conversationId") String conversationId) { + LoginUser user = currentUser(); + imChatService.quitGroup(user.getId(), conversationId); + return Result.OK("已退出群聊"); + } + + @Operation(summary = "IM聊天-解散群聊") + @RequiresPermissions("sys:im:chat:list") + @PostMapping("/group/dismiss") + public Result dismissGroup(@RequestParam(name = "conversationId") String conversationId) { + LoginUser user = currentUser(); + imChatService.dismissGroup(user.getId(), conversationId); + return Result.OK("群聊已解散"); + } + //update-end---author:cursor ---date:20260529 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/dto/SysImGroupMembersDTO.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/dto/SysImGroupMembersDTO.java new file mode 100644 index 0000000..2804598 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/dto/SysImGroupMembersDTO.java @@ -0,0 +1,22 @@ +package org.jeecg.modules.im.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * IM 群聊添加成员 + */ +//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-添加群成员----------- +@Data +@Schema(description = "IM群聊添加成员") +public class SysImGroupMembersDTO { + + @Schema(description = "会话ID") + private String conversationId; + + @Schema(description = "新增成员用户ID") + private List memberUserIds; +} +//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-添加群成员----------- 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..0ceebee 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,12 +4,16 @@ 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.SysImGroupMembersDTO; import org.jeecg.modules.im.dto.SysImSendMessageDTO; import org.jeecg.modules.im.vo.SysImContactVO; import org.jeecg.modules.im.vo.SysImConversationVO; +import org.jeecg.modules.im.vo.SysImGroupDetailVO; + import org.jeecg.modules.im.vo.SysImMessageVO; @@ -34,7 +38,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); @@ -50,5 +56,28 @@ public interface ISysImChatService { List listDeptMembers(String userId, Integer tenantId, String orgCode, String keyword); + //update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群管理接口----------- + /** 群聊详情(含成员列表,区分群主) */ + SysImGroupDetailVO getGroupDetail(String userId, String conversationId); + + /** 添加群成员(所有群成员可操作) */ + SysImConversationVO addGroupMembers(String userId, Integer tenantId, String orgCode, SysImGroupMembersDTO dto); + + /** 移除群成员(仅群主) */ + void removeGroupMember(String userId, String conversationId, String memberUserId); + + /** 修改群名称(仅群主) */ + SysImConversationVO renameGroup(String userId, String conversationId, String groupName); + + /** 转让群主(仅群主) */ + void transferGroupOwner(String userId, String conversationId, String newOwnerId); + + /** 退出群聊(非群主成员) */ + void quitGroup(String userId, String conversationId); + + /** 解散群聊(仅群主) */ + void dismissGroup(String userId, String conversationId); + //update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群管理接口----------- + } 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..bec7fc7 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,8 @@ 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.SysImGroupMembersDTO; import org.jeecg.modules.im.dto.SysImSendMessageDTO; import org.jeecg.modules.im.entity.SysImConversation; import org.jeecg.modules.im.entity.SysImConversationMember; @@ -19,6 +21,8 @@ import org.jeecg.modules.im.mapper.SysImMessageMapper; import org.jeecg.modules.im.service.ISysImChatService; import org.jeecg.modules.im.vo.SysImContactVO; import org.jeecg.modules.im.vo.SysImConversationVO; +import org.jeecg.modules.im.vo.SysImGroupDetailVO; +import org.jeecg.modules.im.vo.SysImGroupMemberVO; import org.jeecg.modules.im.vo.SysImMessageVO; import org.jeecg.modules.message.websocket.WebSocket; import org.jeecg.modules.system.entity.SysDepart; @@ -41,6 +45,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 +57,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 +123,271 @@ 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:20260529 for:【IM聊天-OA】群设置-群管理----------- + private static final int GROUP_MEMBER_MAX = 50; + + @Override + public SysImGroupDetailVO getGroupDetail(String userId, String conversationId) { + SysImConversation conversation = assertGroupConversation(userId, conversationId); + List members = memberMapper.selectList(new LambdaQueryWrapper() + .eq(SysImConversationMember::getConversationId, conversationId) + .orderByAsc(SysImConversationMember::getCreateTime)); + List memberUserIds = members.stream() + .map(SysImConversationMember::getUserId) + .filter(oConvertUtils::isNotEmpty) + .distinct() + .collect(Collectors.toList()); + Map userMap = new HashMap<>(memberUserIds.size()); + if (!memberUserIds.isEmpty()) { + List users = userMapper.selectBatchIds(memberUserIds); + if (users != null) { + for (SysUser user : users) { + userMap.put(user.getId(), user); + } + } + } + String ownerId = conversation.getOwnerId(); + List memberVoList = new ArrayList<>(members.size()); + for (SysImConversationMember member : members) { + SysUser user = userMap.get(member.getUserId()); + SysImGroupMemberVO memberVo = new SysImGroupMemberVO(); + memberVo.setUserId(member.getUserId()); + memberVo.setOwner(member.getUserId() != null && member.getUserId().equals(ownerId)); + if (user != null) { + memberVo.setRealname(user.getRealname()); + memberVo.setUsername(user.getUsername()); + memberVo.setAvatar(user.getAvatar()); + } + memberVoList.add(memberVo); + } + // 群主排在最前 + memberVoList.sort(Comparator.comparingInt(item -> Boolean.TRUE.equals(item.getOwner()) ? 0 : 1)); + SysImGroupDetailVO detail = new SysImGroupDetailVO(); + detail.setConversationId(conversation.getId()); + detail.setGroupName(conversation.getGroupName()); + detail.setOwnerId(ownerId); + detail.setMemberCount(memberVoList.size()); + detail.setOwner(userId.equals(ownerId)); + detail.setMembers(memberVoList); + return detail; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public SysImConversationVO addGroupMembers(String userId, Integer tenantId, String orgCode, SysImGroupMembersDTO dto) { + if (dto == null || oConvertUtils.isEmpty(dto.getConversationId())) { + throw new JeecgBootException("会话不存在"); + } + SysImConversation conversation = assertGroupConversation(userId, dto.getConversationId()); + if (dto.getMemberUserIds() == null || dto.getMemberUserIds().isEmpty()) { + throw new JeecgBootException("请选择要添加的成员"); + } + // 已在群成员 + Set existIds = memberMapper.selectList(new LambdaQueryWrapper() + .eq(SysImConversationMember::getConversationId, conversation.getId())) + .stream().map(SysImConversationMember::getUserId).collect(Collectors.toSet()); + // 去重并过滤已存在成员 + List toAdd = new ArrayList<>(); + Set seen = new LinkedHashSet<>(); + for (String memberId : dto.getMemberUserIds()) { + if (oConvertUtils.isEmpty(memberId) || existIds.contains(memberId) || !seen.add(memberId)) { + continue; + } + toAdd.add(memberId); + } + if (toAdd.isEmpty()) { + throw new JeecgBootException("所选成员已在群内"); + } + if (existIds.size() + toAdd.size() > GROUP_MEMBER_MAX) { + throw new JeecgBootException("群成员不能超过" + GROUP_MEMBER_MAX + "人"); + } + // 校验同租户、同部门 + for (String memberId : toAdd) { + validateTenantChat(userId, tenantId, orgCode, memberId); + } + Date now = new Date(); + for (String memberId : toAdd) { + createMember(conversation.getId(), memberId, now); + } + return buildGroupConversationVo(conversation, userId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeGroupMember(String userId, String conversationId, String memberUserId) { + SysImConversation conversation = assertGroupConversation(userId, conversationId); + assertGroupOwner(userId, conversation); + if (oConvertUtils.isEmpty(memberUserId)) { + throw new JeecgBootException("请选择要移除的成员"); + } + if (memberUserId.equals(conversation.getOwnerId())) { + throw new JeecgBootException("群主不能被移除"); + } + memberMapper.delete(new LambdaQueryWrapper() + .eq(SysImConversationMember::getConversationId, conversationId) + .eq(SysImConversationMember::getUserId, memberUserId)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public SysImConversationVO renameGroup(String userId, String conversationId, String groupName) { + SysImConversation conversation = assertGroupConversation(userId, conversationId); + assertGroupOwner(userId, conversation); + if (oConvertUtils.isEmpty(groupName)) { + throw new JeecgBootException("群名称不能为空"); + } + String name = groupName.trim(); + if (name.length() > 30) { + throw new JeecgBootException("群名称不能超过30字"); + } + conversation.setGroupName(name); + conversation.setUpdateBy(userId); + conversation.setUpdateTime(new Date()); + conversationMapper.updateById(conversation); + return buildGroupConversationVo(conversation, userId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void transferGroupOwner(String userId, String conversationId, String newOwnerId) { + SysImConversation conversation = assertGroupConversation(userId, conversationId); + assertGroupOwner(userId, conversation); + if (oConvertUtils.isEmpty(newOwnerId)) { + throw new JeecgBootException("请选择新群主"); + } + if (newOwnerId.equals(userId)) { + throw new JeecgBootException("不能转让给自己"); + } + if (getMember(newOwnerId, conversationId) == null) { + throw new JeecgBootException("新群主必须是群成员"); + } + conversation.setOwnerId(newOwnerId); + conversation.setUpdateBy(userId); + conversation.setUpdateTime(new Date()); + conversationMapper.updateById(conversation); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void quitGroup(String userId, String conversationId) { + SysImConversation conversation = assertGroupConversation(userId, conversationId); + if (userId.equals(conversation.getOwnerId())) { + throw new JeecgBootException("群主不能退出群聊,请先转让群主或解散群聊"); + } + memberMapper.delete(new LambdaQueryWrapper() + .eq(SysImConversationMember::getConversationId, conversationId) + .eq(SysImConversationMember::getUserId, userId)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void dismissGroup(String userId, String conversationId) { + SysImConversation conversation = assertGroupConversation(userId, conversationId); + assertGroupOwner(userId, conversation); + memberMapper.delete(new LambdaQueryWrapper() + .eq(SysImConversationMember::getConversationId, conversationId)); + conversationMapper.deleteById(conversationId); + } + + /** 校验会话存在、为群聊且当前用户是群成员 */ + private SysImConversation assertGroupConversation(String userId, String conversationId) { + if (oConvertUtils.isEmpty(conversationId)) { + throw new JeecgBootException("会话不存在"); + } + SysImConversation conversation = conversationMapper.selectById(conversationId); + if (conversation == null || !CONV_TYPE_GROUP.equals(conversation.getConvType())) { + throw new JeecgBootException("群聊不存在"); + } + assertMember(userId, conversationId); + return conversation; + } + + /** 校验当前用户是群主 */ + private void assertGroupOwner(String userId, SysImConversation conversation) { + if (!userId.equals(conversation.getOwnerId())) { + throw new JeecgBootException("仅群主可执行该操作"); + } + } + //update-end---author:cursor ---date:20260529 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 +451,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 +468,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 +702,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 +793,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 +804,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-biz/src/main/java/org/jeecg/modules/im/vo/SysImGroupDetailVO.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/vo/SysImGroupDetailVO.java new file mode 100644 index 0000000..c418a7a --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/vo/SysImGroupDetailVO.java @@ -0,0 +1,29 @@ +package org.jeecg.modules.im.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * IM 群聊详情(群设置) + */ +//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群详情----------- +@Data +@Schema(description = "IM群聊详情") +public class SysImGroupDetailVO { + + @Schema(description = "会话ID") + private String conversationId; + @Schema(description = "群名称") + private String groupName; + @Schema(description = "群主用户ID") + private String ownerId; + @Schema(description = "群成员数") + private Integer memberCount; + @Schema(description = "当前用户是否群主") + private Boolean owner; + @Schema(description = "群成员列表") + private List members; +} +//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群详情----------- diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/vo/SysImGroupMemberVO.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/vo/SysImGroupMemberVO.java new file mode 100644 index 0000000..87fd84a --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/vo/SysImGroupMemberVO.java @@ -0,0 +1,25 @@ +package org.jeecg.modules.im.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * IM 群成员 + */ +//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群成员展示----------- +@Data +@Schema(description = "IM群成员") +public class SysImGroupMemberVO { + + @Schema(description = "用户ID") + private String userId; + @Schema(description = "姓名") + private String realname; + @Schema(description = "账号") + private String username; + @Schema(description = "头像") + private String avatar; + @Schema(description = "是否群主") + private Boolean owner; +} +//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群成员展示----------- 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..f1082c5 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/SysMessageList.vue b/jeecgboot-vue3/src/views/system/message/components/SysMessageList.vue index b8d3bc0..445af0b 100644 --- a/jeecgboot-vue3/src/views/system/message/components/SysMessageList.vue +++ b/jeecgboot-vue3/src/views/system/message/components/SysMessageList.vue @@ -223,10 +223,19 @@ async function showMessageDetail(record){ if (isImChatNotice(record)) { //update-begin---author:xsl ---date:20260528 for:【IM聊天-OA】统一 IM 打开入口,全页优先----------- - const mode = await openImChat({ targetUserId: record.imTargetUserId, pageContext: null }); - if (mode === 'modal') { - openImChatModal(true, { targetUserId: record.imTargetUserId, pageContext: null }); + //update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息支持打开群聊会话----------- + if (record.imConvType === 'group') { + const mode = await openImChat({ conversationId: record.imConversationId }); + if (mode === 'modal') { + openImChatModal(true, { conversationId: record.imConversationId }); + } + } else { + const mode = await openImChat({ targetUserId: record.imTargetUserId, pageContext: null }); + if (mode === 'modal') { + openImChatModal(true, { targetUserId: record.imTargetUserId, pageContext: null }); + } } + //update-end---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息支持打开群聊会话----------- //update-end---author:xsl ---date:20260528 for:【IM聊天-OA】统一 IM 打开入口,全页优先----------- emit('close-modal'); return; 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; } diff --git a/jeecgboot-vue3/src/views/system/message/components/imChatNoticeAdapter.ts b/jeecgboot-vue3/src/views/system/message/components/imChatNoticeAdapter.ts index aef704c..084cb72 100644 --- a/jeecgboot-vue3/src/views/system/message/components/imChatNoticeAdapter.ts +++ b/jeecgboot-vue3/src/views/system/message/components/imChatNoticeAdapter.ts @@ -1,5 +1,7 @@ import dayjs, { Dayjs } from 'dayjs'; -import { fetchDeptMembers } from '/@/views/system/im/im.api'; +//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息补充群聊提醒----------- +import { fetchDeptMembers, fetchGroups } from '/@/views/system/im/im.api'; +//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息补充群聊提醒----------- import { formatImMessagePreview } from '/@/views/system/im/imMessageUtil'; export const IM_CHAT_BUS_TYPE = 'im_chat'; @@ -27,6 +29,12 @@ export interface ImChatNoticeRecord { imTargetUsername?: string; imAvatar?: string; imUnreadCount?: number; + //update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息补充群聊提醒----------- + /** 会话类型:single 单聊 / group 群聊 */ + imConvType?: 'single' | 'group'; + /** 群聊会话 ID(群聊提醒打开会话时使用) */ + imConversationId?: string; + //update-end---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息补充群聊提醒----------- } interface ImContactLike { @@ -40,6 +48,16 @@ interface ImContactLike { unreadCount?: number; } +//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息补充群聊提醒----------- +interface ImGroupLike { + conversationId: string; + groupName?: string; + lastContent?: string; + lastTime?: string; + unreadCount?: number; +} +//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息补充群聊提醒----------- + /** 标星列表不包含 IM;全部消息或未指定类型、聊天类型时合并 IM */ export function shouldIncludeImChatInList(params: ImChatSearchParams) { if (params.starFlag === '1') { @@ -142,14 +160,49 @@ export function mapImContactToNotice(contact: ImContactLike): ImChatNoticeRecord imTargetUsername: contact.username, imAvatar: contact.avatar, imUnreadCount: contact.unreadCount || 0, + //update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息补充群聊提醒----------- + imConvType: 'single', + //update-end---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息补充群聊提醒----------- }; } -export function filterImChatNotices(contacts: ImContactLike[], params: ImChatSearchParams) { - let list = (contacts || []) +//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息补充群聊提醒----------- +/** 将群聊会话映射为聊天提醒记录 */ +export function mapImGroupToNotice(group: ImGroupLike): ImChatNoticeRecord | null { + if (!group?.conversationId || !group?.lastTime) { + return null; + } + const name = group.groupName || '群聊'; + const preview = formatImMessagePreview(group.lastContent) || '发来一条新消息'; + const timeText = dayjs(group.lastTime).isValid() ? dayjs(group.lastTime).format('YYYY-MM-DD HH:mm:ss') : String(group.lastTime); + return { + id: `im_chat_group_${group.conversationId}`, + busType: IM_CHAT_BUS_TYPE, + titile: `${name}:${preview}`, + msgContent: preview, + sendTime: timeText, + createTime: timeText, + readFlag: (group.unreadCount || 0) > 0 ? '0' : '1', + noticeType: IM_CHAT_NOTICE_TYPE, + starFlag: '0', + imTargetUserId: '', + imUnreadCount: group.unreadCount || 0, + imConvType: 'group', + imConversationId: group.conversationId, + }; +} + +export function filterImChatNotices(contacts: ImContactLike[], groups: ImGroupLike[], params: ImChatSearchParams) { + const singleList = (contacts || []) .map(mapImContactToNotice) - .filter((item): item is ImChatNoticeRecord => !!item) - .filter((item) => isInRange(item.sendTime, params.rangeDateKey, params.rangeDate)); + .filter((item): item is ImChatNoticeRecord => !!item); + + // 指定发件人筛选时,仅匹配单聊(群聊不属于单个发件人) + const groupList = params.fromUser + ? [] + : (groups || []).map(mapImGroupToNotice).filter((item): item is ImChatNoticeRecord => !!item); + + let list = [...singleList, ...groupList].filter((item) => isInRange(item.sendTime, params.rangeDateKey, params.rangeDate)); if (params.fromUser) { list = list.filter((item) => item.imTargetUsername === params.fromUser || item.imTargetUserId === params.fromUser); @@ -159,9 +212,13 @@ export function filterImChatNotices(contacts: ImContactLike[], params: ImChatSea } export async function fetchImChatNoticeList(params: ImChatSearchParams) { - const contacts = ((await fetchDeptMembers()) || []) as ImContactLike[]; - return filterImChatNotices(contacts, params); + const [contacts, groups] = (await Promise.all([ + Promise.resolve(fetchDeptMembers()).catch(() => []), + Promise.resolve(fetchGroups()).catch(() => []), + ])) as [ImContactLike[], ImGroupLike[]]; + return filterImChatNotices(contacts || [], groups || [], params); } +//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息补充群聊提醒----------- export function mergeMessageList(systemList: any[] = [], imList: ImChatNoticeRecord[] = []) { if (!imList.length) {