diff --git a/urbanLifelineServ/.vscode/launch.json b/urbanLifelineServ/.vscode/launch.json index 0fc9038e..2fee27d0 100644 --- a/urbanLifelineServ/.vscode/launch.json +++ b/urbanLifelineServ/.vscode/launch.json @@ -1,6 +1,20 @@ { "version": "0.2.0", "configurations": [ + { + "type": "java", + "name": "URLQRCodeParseTest", + "request": "launch", + "mainClass": "org.xyzh.workcase.test.URLQRCodeParseTest", + "projectName": "workcase" + }, + { + "type": "java", + "name": "QRCodeTest", + "request": "launch", + "mainClass": "org.xyzh.workcase.test.QRCodeTest", + "projectName": "workcase" + }, { "type": "java", "name": "AesEncryptUtil", diff --git a/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/JitsiTokenService.java b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/JitsiTokenService.java new file mode 100644 index 00000000..a54e56cd --- /dev/null +++ b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/JitsiTokenService.java @@ -0,0 +1,54 @@ +package org.xyzh.api.workcase.service; + +import com.alibaba.fastjson2.JSONObject; + +/** + * @description Jitsi Meet JWT Token服务接口 + * @filename JitsiTokenService.java + * @author claude + * @copyright xyzh + * @since 2025-12-25 + */ +public interface JitsiTokenService { + + /** + * @description 生成Jitsi Meet JWT Token + * @param roomName Jitsi房间名 + * @param userId 用户ID + * @param userName 用户名称 + * @param isModerator 是否为主持人 + * @return JWT Token字符串 + * @author claude + * @since 2025-12-25 + */ + String generateJwtToken(String roomName, String userId, String userName, boolean isModerator); + + /** + * @description 验证JWT Token是否有效 + * @param token JWT Token + * @return boolean + * @author claude + * @since 2025-12-25 + */ + boolean validateJwtToken(String token); + + /** + * @description 构建Jitsi Meet iframe URL + * @param roomName Jitsi房间名 + * @param jwtToken JWT Token + * @param config Jitsi配置项(可选) + * @return iframe URL字符串 + * @author claude + * @since 2025-12-25 + */ + String buildIframeUrl(String roomName, String jwtToken, JSONObject config); + + /** + * @description 生成唯一的Jitsi房间名 + * @param workcaseId 工单ID + * @return 房间名字符串 + * @author claude + * @since 2025-12-25 + */ + String generateRoomName(String workcaseId); +} diff --git a/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/VideoMeetingService.java b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/VideoMeetingService.java new file mode 100644 index 00000000..4946c4e9 --- /dev/null +++ b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/VideoMeetingService.java @@ -0,0 +1,92 @@ +package org.xyzh.api.workcase.service; + +import org.xyzh.api.workcase.dto.TbVideoMeetingDTO; +import org.xyzh.api.workcase.vo.VideoMeetingVO; +import org.xyzh.common.core.domain.ResultDomain; + +/** + * @description 视频会议服务接口,管理Jitsi Meet视频会议 + * @filename VideoMeetingService.java + * @author claude + * @copyright xyzh + * @since 2025-12-25 + */ +public interface VideoMeetingService { + + /** + * @description 创建视频会议 + * @param meetingDTO 会议信息 + * @param userId 创建者用户ID + * @return ResultDomain + * @author claude + * @since 2025-12-25 + */ + ResultDomain createMeeting(TbVideoMeetingDTO meetingDTO); + + /** + * @description 获取会议信息 + * @param meetingId 会议ID + * @param userId 请求用户ID(用于权限验证) + * @return ResultDomain + * @author claude + * @since 2025-12-25 + */ + ResultDomain getMeetingInfo(String meetingId, String userId); + + /** + * @description 验证用户是否有权访问会议 + * @param meetingId 会议ID + * @param userId 用户ID + * @return ResultDomain + * @author claude + * @since 2025-12-25 + */ + ResultDomain validateMeetingAccess(String meetingId, String userId); + + /** + * @description 生成用户专属的会议访问URL(包含用户专属JWT Token) + * @param meetingId 会议ID + * @param userId 用户ID + * @return ResultDomain + * @author claude + * @since 2025-12-25 + */ + ResultDomain generateUserMeetingUrl(String meetingId, String userId); + + /** + * @description 开始会议(更新状态为ongoing) + * @param meetingId 会议ID + * @return ResultDomain + * @author claude + * @since 2025-12-25 + */ + ResultDomain startMeeting(String meetingId); + + /** + * @description 结束会议(更新状态为ended) + * @param meetingId 会议ID + * @return ResultDomain + * @author claude + * @since 2025-12-25 + */ + ResultDomain endMeeting(String meetingId); + + /** + * @description 获取聊天室当前活跃的会议 + * @param roomId 聊天室ID + * @return ResultDomain + * @author claude + * @since 2025-12-25 + */ + ResultDomain getActiveMeetingByRoom(String roomId); + + /** + * @description 检查用户是否为聊天室成员(内部方法) + * @param roomId 聊天室ID + * @param userId 用户ID + * @return boolean + * @author claude + * @since 2025-12-25 + */ + boolean isMemberOfRoom(String roomId, String userId); +} diff --git a/urbanLifelineServ/workcase/pom.xml b/urbanLifelineServ/workcase/pom.xml index 4f0f278e..c7dbd88e 100644 --- a/urbanLifelineServ/workcase/pom.xml +++ b/urbanLifelineServ/workcase/pom.xml @@ -30,6 +30,10 @@ org.xyzh.apis api-auth + + org.xyzh.apis + api-file + + + com.google.zxing + core + 3.5.3 + + + com.google.zxing + javase + 3.5.3 + + + + + org.apache.httpcomponents.client5 + httpclient5 + + + + + org.jsoup + jsoup + 1.17.2 + \ No newline at end of file diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatContorller.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatContorller.java index ef1026d3..e5a540d2 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatContorller.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatContorller.java @@ -268,6 +268,100 @@ public class WorkcaseChatContorller { return chatRoomService.assignCustomerService(roomId); } + // ========================= 视频会议管理(Jitsi Meet) ========================= + + @Autowired + private org.xyzh.api.workcase.service.VideoMeetingService videoMeetingService; + + @Operation(summary = "创建视频会议") + @PreAuthorize("hasAuthority('workcase:room:meeting')") + @PostMapping("/meeting/create") + public ResultDomain createVideoMeeting( + @RequestBody org.xyzh.api.workcase.dto.TbVideoMeetingDTO meetingDTO) { + ValidationResult vr = ValidationUtils.validate(meetingDTO, Arrays.asList( + ValidationUtils.requiredString("roomId", "聊天室ID") + )); + if (!vr.isValid()) { + return ResultDomain.failure(vr.getAllErrors()); + } + + try { + return videoMeetingService.createMeeting(meetingDTO); + } catch (Exception e) { + return ResultDomain.failure(e.getMessage()); + } + } + + @Operation(summary = "获取会议信息") + @PreAuthorize("hasAuthority('workcase:room:meeting')") + @GetMapping("/meeting/{meetingId}") + public ResultDomain getMeetingInfo( + @PathVariable(value = "meetingId") String meetingId) { + String userId = LoginUtil.getCurrentUserId(); + + try { + return videoMeetingService.getMeetingInfo(meetingId, userId); + } catch (Exception e) { + return ResultDomain.failure(e.getMessage()); + } + } + + @Operation(summary = "加入会议(生成用户专属JWT)") + @PreAuthorize("hasAuthority('workcase:room:meeting')") + @PostMapping("/meeting/{meetingId}/join") + public ResultDomain joinMeeting( + @PathVariable(value = "meetingId") String meetingId) { + String userId = LoginUtil.getCurrentUserId(); + + // 验证加入权限 + ResultDomain accessCheck = videoMeetingService.validateMeetingAccess(meetingId, userId); + if (!accessCheck.getSuccess() || !accessCheck.getData()) { + return ResultDomain.failure("您无权加入此会议"); + } + + // 生成用户专属的iframe URL(包含JWT Token) + try { + return videoMeetingService.generateUserMeetingUrl(meetingId, userId); + } catch (Exception e) { + return ResultDomain.failure(e.getMessage()); + } + } + + @Operation(summary = "开始会议") + @PreAuthorize("hasAuthority('workcase:room:meeting')") + @PostMapping("/meeting/{meetingId}/start") + public ResultDomain startMeeting(@PathVariable(value = "meetingId") String meetingId) { + try { + return videoMeetingService.startMeeting(meetingId); + } catch (Exception e) { + return ResultDomain.failure(e.getMessage()); + } + } + + @Operation(summary = "结束会议") + @PreAuthorize("hasAuthority('workcase:room:meeting')") + @PostMapping("/meeting/{meetingId}/end") + public ResultDomain endMeeting( + @PathVariable(value = "meetingId") String meetingId) { + try { + return videoMeetingService.endMeeting(meetingId); + } catch (Exception e) { + return ResultDomain.failure(e.getMessage()); + } + } + + @Operation(summary = "获取聊天室当前活跃会议") + @PreAuthorize("hasAuthority('workcase:room:meeting')") + @GetMapping("/meeting/room/{roomId}/active") + public ResultDomain getActiveMeetingByRoom( + @PathVariable(value = "roomId") String roomId) { + try { + return videoMeetingService.getActiveMeetingByRoom(roomId); + } catch (Exception e) { + return ResultDomain.failure(e.getMessage()); + } + } + // ========================= 微信客服消息回调 ========================= // @Operation(summary = "微信客服消息回调验证(GET)") diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/JitsiTokenServiceImpl.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/JitsiTokenServiceImpl.java new file mode 100644 index 00000000..65ed63ab --- /dev/null +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/JitsiTokenServiceImpl.java @@ -0,0 +1,145 @@ +package org.xyzh.workcase.service; + +import com.alibaba.fastjson2.JSONObject; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.apache.dubbo.config.annotation.DubboService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.xyzh.api.workcase.service.JitsiTokenService; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * @description Jitsi Meet JWT Token服务实现类 + * @filename JitsiTokenServiceImpl.java + * @author claude + * @copyright xyzh + * @since 2025-12-25 + */ +@DubboService(version = "1.0.0", group = "workcase", timeout = 30000, retries = 0) +public class JitsiTokenServiceImpl implements JitsiTokenService { + private static final Logger logger = LoggerFactory.getLogger(JitsiTokenServiceImpl.class); + + @Value("${jitsi.app.id:urbanLifeline}") + private String jitsiAppId; + + @Value("${jitsi.app.secret:your-secret-key-change-in-production}") + private String jitsiAppSecret; + + @Value("${jitsi.server.url:https://meet.jit.si}") + private String jitsiServerUrl; + + @Value("${jitsi.token.expiration:7200000}") + private Long tokenExpiration; // 默认2小时 + + @Override + public String generateJwtToken(String roomName, String userId, String userName, boolean isModerator) { + logger.info("生成Jitsi JWT Token: roomName={}, userId={}, userName={}, isModerator={}", + roomName, userId, userName, isModerator); + + try { + long now = System.currentTimeMillis(); + long exp = now + tokenExpiration; + + // 构建用户上下文 + Map userContext = new HashMap<>(); + userContext.put("id", userId); + userContext.put("name", userName); + userContext.put("moderator", isModerator); + + // 构建JWT claims + Map claims = new HashMap<>(); + claims.put("context", Map.of("user", userContext)); + claims.put("room", roomName); + claims.put("iss", jitsiAppId); + claims.put("aud", "jitsi"); + claims.put("sub", jitsiServerUrl); + claims.put("exp", exp / 1000); // 秒级时间戳 + claims.put("nbf", now / 1000); + + // 生成JWT Token + String token = Jwts.builder() + .setClaims(claims) + .setIssuedAt(new Date(now)) + .setExpiration(new Date(exp)) + .signWith(SignatureAlgorithm.HS256, jitsiAppSecret.getBytes()) + .compact(); + + logger.info("JWT Token生成成功: roomName={}", roomName); + return token; + } catch (Exception e) { + logger.error("生成JWT Token失败: roomName={}, error={}", roomName, e.getMessage(), e); + throw new RuntimeException("生成JWT Token失败: " + e.getMessage()); + } + } + + @Override + public boolean validateJwtToken(String token) { + try { + Claims claims = Jwts.parser() + .setSigningKey(jitsiAppSecret.getBytes()) + .parseClaimsJws(token) + .getBody(); + + // 检查过期时间 + Date expiration = claims.getExpiration(); + return expiration.after(new Date()); + } catch (Exception e) { + logger.warn("JWT Token验证失败: error={}", e.getMessage()); + return false; + } + } + + @Override + public String buildIframeUrl(String roomName, String jwtToken, JSONObject config) { + logger.info("构建Jitsi iframe URL: roomName={}", roomName); + + StringBuilder url = new StringBuilder(); + url.append(jitsiServerUrl).append("/").append(roomName); + + // 添加JWT Token + url.append("?jwt=").append(jwtToken); + + // 添加默认配置 + url.append("&config.startWithAudioMuted=false"); + url.append("&config.startWithVideoMuted=false"); + url.append("&config.enableWelcomePage=false"); + url.append("&config.prejoinPageEnabled=false"); + url.append("&config.disableDeepLinking=true"); + url.append("&config.enableChat=true"); + url.append("&config.enableScreenSharing=true"); + + // 界面配置 + url.append("&interfaceConfig.SHOW_JITSI_WATERMARK=false"); + url.append("&interfaceConfig.SHOW_WATERMARK_FOR_GUESTS=false"); + url.append("&interfaceConfig.DISABLE_JOIN_LEAVE_NOTIFICATIONS=false"); + url.append("&interfaceConfig.DEFAULT_BACKGROUND=#474747"); + + // 添加自定义配置 + if (config != null && !config.isEmpty()) { + config.forEach((key, value) -> { + url.append("&config.").append(key).append("=").append(value); + }); + } + + String iframeUrl = url.toString(); + logger.info("iframe URL构建成功: url={}", iframeUrl); + return iframeUrl; + } + + @Override + public String generateRoomName(String workcaseId) { + // 格式: workcase_{workcaseId}_{timestamp} + String roomName = String.format("workcase_%s_%d", + workcaseId != null ? workcaseId : "default", + System.currentTimeMillis()); + + logger.info("生成Jitsi房间名: roomName={}", roomName); + return roomName; + } +} diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/VideoMeetingServiceImpl.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/VideoMeetingServiceImpl.java new file mode 100644 index 00000000..39860acd --- /dev/null +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/VideoMeetingServiceImpl.java @@ -0,0 +1,386 @@ +package org.xyzh.workcase.service; + +import org.apache.dubbo.config.annotation.DubboService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import org.xyzh.api.workcase.dto.TbChatRoomMemberDTO; +import org.xyzh.api.workcase.dto.TbVideoMeetingDTO; +import org.xyzh.api.workcase.service.JitsiTokenService; +import org.xyzh.api.workcase.service.VideoMeetingService; +import org.xyzh.api.workcase.vo.ChatMemberVO; +import org.xyzh.api.workcase.vo.VideoMeetingVO; +import org.xyzh.common.auth.utils.LoginUtil; +import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.utils.id.IdUtil; +import org.xyzh.workcase.mapper.TbChatRoomMemberMapper; +import org.xyzh.workcase.mapper.TbVideoMeetingMapper; + +import java.util.Date; +import java.util.List; + +/** + * @description 视频会议服务实现类 + * @filename VideoMeetingServiceImpl.java + * @author claude + * @copyright xyzh + * @since 2025-12-25 + */ +@DubboService(version = "1.0.0", group = "workcase", timeout = 30000, retries = 0) +public class VideoMeetingServiceImpl implements VideoMeetingService { + private static final Logger logger = LoggerFactory.getLogger(VideoMeetingServiceImpl.class); + + @Autowired + private TbVideoMeetingMapper videoMeetingMapper; + + @Autowired + private TbChatRoomMemberMapper chatRoomMemberMapper; + + @Autowired + private JitsiTokenService jitsiTokenService; + + @Override + @Transactional + public ResultDomain createMeeting(TbVideoMeetingDTO meetingDTO) { + // 获取当前用户ID + String userId = LoginUtil.getCurrentUserId(); + logger.info("创建视频会议: roomId={}, workcaseId={}, userId={}", + meetingDTO.getRoomId(), meetingDTO.getWorkcaseId(), userId); + + try { + // 1. 验证用户是否为聊天室成员 + if (!isMemberOfRoom(meetingDTO.getRoomId(), userId)) { + logger.warn("用户不是聊天室成员,无法创建会议: roomId={}, userId={}", + meetingDTO.getRoomId(), userId); + return ResultDomain.failure("您不是聊天室成员,无法创建会议"); + } + + // 2. 检查聊天室是否已有进行中的会议 + TbVideoMeetingDTO existingMeetingFilter = new TbVideoMeetingDTO(); + existingMeetingFilter.setRoomId(meetingDTO.getRoomId()); + existingMeetingFilter.setStatus("ongoing"); + List existingMeetings = videoMeetingMapper.selectVideoMeetingList(existingMeetingFilter); + + if (existingMeetings != null && !existingMeetings.isEmpty()) { + logger.warn("聊天室已有进行中的会议: roomId={}", meetingDTO.getRoomId()); + return ResultDomain.failure("聊天室已有进行中的会议,请稍后再试"); + } + + // 3. 生成会议ID和房间名 + String meetingId = IdUtil.generateUUID(); + String jitsiRoomName = jitsiTokenService.generateRoomName(meetingDTO.getWorkcaseId()); + + // 4. 获取用户信息(从聊天室成员表) + TbChatRoomMemberDTO memberFilter = new TbChatRoomMemberDTO(); + memberFilter.setRoomId(meetingDTO.getRoomId()); + memberFilter.setUserId(userId); + List members = chatRoomMemberMapper.selectChatRoomMemberList(memberFilter); + + String userName = "用户"; + String userType = "guest"; + if (members != null && !members.isEmpty()) { + ChatMemberVO member = members.get(0); + userName = member.getUserName(); + userType = member.getUserType(); + } + + // 5. 生成创建者的JWT Token(创建者默认为主持人) + String jwtToken = jitsiTokenService.generateJwtToken( + jitsiRoomName, + userId, + userName, + true // 创建者为主持人 + ); + + // 6. 构建iframe URL + String iframeUrl = jitsiTokenService.buildIframeUrl(jitsiRoomName, jwtToken, meetingDTO.getConfig()); + + // 7. 填充会议信息 + meetingDTO.setMeetingId(meetingId); + meetingDTO.setJitsiRoomName(jitsiRoomName); + meetingDTO.setJwtToken(jwtToken); // 存储创建者的token(可选) + meetingDTO.setIframeUrl(iframeUrl); + meetingDTO.setStatus("scheduled"); + meetingDTO.setCreatorId(userId); + meetingDTO.setCreatorType(userType); + meetingDTO.setCreatorName(userName); + meetingDTO.setParticipantCount(0); + meetingDTO.setOptsn(IdUtil.getOptsn()); + + if (meetingDTO.getMaxParticipants() == null) { + meetingDTO.setMaxParticipants(10); + } + + // 8. 插入数据库 + int rows = videoMeetingMapper.insertVideoMeeting(meetingDTO); + if (rows > 0) { + logger.info("视频会议创建成功: meetingId={}, jitsiRoomName={}", + meetingId, jitsiRoomName); + + // 9. 返回VO + VideoMeetingVO meetingVO = new VideoMeetingVO(); + BeanUtils.copyProperties(meetingDTO, meetingVO); + return ResultDomain.success("创建会议成功", meetingVO); + } else { + logger.error("插入会议记录失败: meetingId={}", meetingId); + return ResultDomain.failure("创建会议失败"); + } + } catch (Exception e) { + logger.error("创建视频会议异常: roomId={}, error={}", + meetingDTO.getRoomId(), e.getMessage(), e); + return ResultDomain.failure("创建会议失败: " + e.getMessage()); + } + } + + @Override + public ResultDomain getMeetingInfo(String meetingId, String userId) { + logger.info("获取会议信息: meetingId={}, userId={}", meetingId, userId); + + try { + TbVideoMeetingDTO filter = new TbVideoMeetingDTO(); + filter.setMeetingId(meetingId); + List meetings = videoMeetingMapper.selectVideoMeetingList(filter); + + if (meetings == null || meetings.isEmpty()) { + logger.warn("会议不存在: meetingId={}", meetingId); + return ResultDomain.failure("会议不存在"); + } + + VideoMeetingVO meeting = meetings.get(0); + + // 验证访问权限 + ResultDomain accessCheck = validateMeetingAccess(meetingId, userId); + if (!accessCheck.getSuccess() || !accessCheck.getData()) { + logger.warn("用户无权访问会议: meetingId={}, userId={}", meetingId, userId); + return ResultDomain.failure("您无权访问此会议"); + } + + logger.info("获取会议信息成功: meetingId={}", meetingId); + return ResultDomain.success("获取会议信息成功", meeting); + } catch (Exception e) { + logger.error("获取会议信息异常: meetingId={}, error={}", meetingId, e.getMessage(), e); + return ResultDomain.failure("获取会议信息失败: " + e.getMessage()); + } + } + + @Override + public ResultDomain validateMeetingAccess(String meetingId, String userId) { + logger.info("验证会议访问权限: meetingId={}, userId={}", meetingId, userId); + + try { + // 1. 获取会议信息 + TbVideoMeetingDTO filter = new TbVideoMeetingDTO(); + filter.setMeetingId(meetingId); + List meetings = videoMeetingMapper.selectVideoMeetingList(filter); + + if (meetings == null || meetings.isEmpty()) { + logger.warn("会议不存在: meetingId={}", meetingId); + return ResultDomain.success("会议不存在", false); + } + + VideoMeetingVO meeting = meetings.get(0); + + // 2. 检查用户是否为聊天室成员 + boolean isMember = isMemberOfRoom(meeting.getRoomId(), userId); + + logger.info("会议访问权限验证结果: meetingId={}, userId={}, hasAccess={}", + meetingId, userId, isMember); + return ResultDomain.success("会议访问权限验证成功", isMember); + } catch (Exception e) { + logger.error("验证会议访问权限异常: meetingId={}, error={}", meetingId, e.getMessage(), e); + return ResultDomain.failure("验证访问权限失败: " + e.getMessage()); + } + } + + @Override + public ResultDomain generateUserMeetingUrl(String meetingId, String userId) { + logger.info("生成用户专属会议URL: meetingId={}, userId={}", meetingId, userId); + + try { + // 1. 获取会议信息 + TbVideoMeetingDTO filter = new TbVideoMeetingDTO(); + filter.setMeetingId(meetingId); + List meetings = videoMeetingMapper.selectVideoMeetingList(filter); + + if (meetings == null || meetings.isEmpty()) { + logger.warn("会议不存在: meetingId={}", meetingId); + return ResultDomain.failure("会议不存在"); + } + + VideoMeetingVO meeting = meetings.get(0); + + // 2. 验证访问权限 + ResultDomain accessCheck = validateMeetingAccess(meetingId, userId); + if (!accessCheck.getSuccess() || !accessCheck.getData()) { + logger.warn("用户无权访问会议: meetingId={}, userId={}", meetingId, userId); + return ResultDomain.failure("您无权访问此会议"); + } + + // 3. 获取用户信息 + TbChatRoomMemberDTO memberFilter = new TbChatRoomMemberDTO(); + memberFilter.setRoomId(meeting.getRoomId()); + memberFilter.setUserId(userId); + List members = chatRoomMemberMapper.selectChatRoomMemberList(memberFilter); + + String userName = "用户"; + boolean isModerator = false; + + if (members != null && !members.isEmpty()) { + ChatMemberVO member = members.get(0); + userName = member.getUserName(); + // 客服人员设为主持人 + isModerator = "agent".equals(member.getUserType()); + } + + // 4. 生成用户专属JWT Token + String userJwtToken = jitsiTokenService.generateJwtToken( + meeting.getJitsiRoomName(), + userId, + userName, + isModerator + ); + + // 5. 构建用户专属iframe URL + String userIframeUrl = jitsiTokenService.buildIframeUrl( + meeting.getJitsiRoomName(), + userJwtToken, + meeting.getConfig() + ); + + // 6. 更新VO + meeting.setJwtToken(userJwtToken); + meeting.setIframeUrl(userIframeUrl); + + logger.info("生成用户专属会议URL成功: meetingId={}, userId={}", meetingId, userId); + return ResultDomain.success("生成用户专属会议URL成功", meeting); + } catch (Exception e) { + logger.error("生成用户专属会议URL异常: meetingId={}, error={}", meetingId, e.getMessage(), e); + return ResultDomain.failure("生成会议URL失败: " + e.getMessage()); + } + } + + @Override + @Transactional + public ResultDomain startMeeting(String meetingId) { + logger.info("开始会议: meetingId={}", meetingId); + + try { + TbVideoMeetingDTO updateDTO = new TbVideoMeetingDTO(); + updateDTO.setMeetingId(meetingId); + updateDTO.setStatus("ongoing"); + updateDTO.setActualStartTime(new Date()); + + int rows = videoMeetingMapper.updateVideoMeeting(updateDTO); + if (rows > 0) { + logger.info("会议开始成功: meetingId={}", meetingId); + return ResultDomain.success("会议开始成功", true); + } else { + logger.warn("会议开始失败(可能不存在): meetingId={}", meetingId); + return ResultDomain.failure("会议不存在或已开始"); + } + } catch (Exception e) { + logger.error("开始会议异常: meetingId={}, error={}", meetingId, e.getMessage(), e); + return ResultDomain.failure("开始会议失败: " + e.getMessage()); + } + } + + @Override + @Transactional + public ResultDomain endMeeting(String meetingId) { + logger.info("结束会议: meetingId={}", meetingId); + + try { + // 1. 获取会议信息 + TbVideoMeetingDTO filter = new TbVideoMeetingDTO(); + filter.setMeetingId(meetingId); + List meetings = videoMeetingMapper.selectVideoMeetingList(filter); + + if (meetings == null || meetings.isEmpty()) { + logger.warn("会议不存在: meetingId={}", meetingId); + return ResultDomain.failure("会议不存在"); + } + + VideoMeetingVO meeting = meetings.get(0); + + // 2. 计算会议时长 + Integer durationSeconds = null; + if (meeting.getActualStartTime() != null) { + long duration = (System.currentTimeMillis() - meeting.getActualStartTime().getTime()) / 1000; + durationSeconds = (int) duration; + } + + // 3. 更新会议状态 + TbVideoMeetingDTO updateDTO = new TbVideoMeetingDTO(); + updateDTO.setMeetingId(meetingId); + updateDTO.setStatus("ended"); + updateDTO.setActualEndTime(new Date()); + updateDTO.setDurationSeconds(durationSeconds); + + int rows = videoMeetingMapper.updateVideoMeeting(updateDTO); + if (rows > 0) { + // 4. 更新VO + meeting.setStatus("ended"); + meeting.setActualEndTime(new Date()); + meeting.setDurationSeconds(durationSeconds); + + logger.info("会议结束成功: meetingId={}, duration={}秒", meetingId, durationSeconds); + return ResultDomain.success("会议结束成功", meeting); + } else { + logger.warn("会议结束失败: meetingId={}", meetingId); + return ResultDomain.failure("结束会议失败"); + } + } catch (Exception e) { + logger.error("结束会议异常: meetingId={}, error={}", meetingId, e.getMessage(), e); + return ResultDomain.failure("结束会议失败: " + e.getMessage()); + } + } + + @Override + public ResultDomain getActiveMeetingByRoom(String roomId) { + logger.info("获取聊天室活跃会议: roomId={}", roomId); + + try { + TbVideoMeetingDTO filter = new TbVideoMeetingDTO(); + filter.setRoomId(roomId); + filter.setStatus("ongoing"); + List meetings = videoMeetingMapper.selectVideoMeetingList(filter); + + if (meetings == null || meetings.isEmpty()) { + logger.info("聊天室无活跃会议: roomId={}", roomId); + return ResultDomain.failure("无活跃会议"); + } + + VideoMeetingVO meeting = meetings.get(0); + logger.info("找到活跃会议: roomId={}, meetingId={}", roomId, meeting.getMeetingId()); + return ResultDomain.success("找到活跃会议", meeting); + } catch (Exception e) { + logger.error("获取活跃会议异常: roomId={}, error={}", roomId, e.getMessage(), e); + return ResultDomain.failure("获取活跃会议失败: " + e.getMessage()); + } + } + + @Override + public boolean isMemberOfRoom(String roomId, String userId) { + logger.debug("检查用户是否为聊天室成员: roomId={}, userId={}", roomId, userId); + + try { + TbChatRoomMemberDTO filter = new TbChatRoomMemberDTO(); + filter.setRoomId(roomId); + filter.setUserId(userId); + filter.setStatus("active"); + + List members = chatRoomMemberMapper.selectChatRoomMemberList(filter); + boolean isMember = members != null && !members.isEmpty(); + + logger.debug("用户成员检查结果: roomId={}, userId={}, isMember={}", + roomId, userId, isMember); + return isMember; + } catch (Exception e) { + logger.error("检查用户成员身份异常: roomId={}, userId={}, error={}", + roomId, userId, e.getMessage(), e); + return false; + } + } +} diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseServiceImpl.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseServiceImpl.java index 78f10239..758a1814 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseServiceImpl.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseServiceImpl.java @@ -1,11 +1,26 @@ package org.xyzh.workcase.service; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.ArrayList; import java.util.List; +import javax.imageio.ImageIO; + +import org.apache.dubbo.config.annotation.DubboReference; import org.apache.dubbo.config.annotation.DubboService; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.io.entity.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.xyzh.api.file.dto.TbSysFileDTO; import org.xyzh.api.workcase.dto.TbWorkcaseDTO; import org.xyzh.api.workcase.dto.TbWorkcaseDeviceDTO; import org.xyzh.api.workcase.dto.TbWorkcaseProcessDTO; @@ -20,10 +35,23 @@ import org.xyzh.workcase.enums.WorkcaseProcessAction; import org.xyzh.workcase.mapper.TbWorkcaseDeviceMapper; import org.xyzh.workcase.mapper.TbWorkcaseMapper; import org.xyzh.workcase.mapper.TbWorkcaseProcessMapper; +import org.xyzh.api.file.service.FileService; import org.xyzh.api.workcase.dto.TbChatRoomDTO; import org.xyzh.api.workcase.service.ChatRoomService; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.DecodeHintType; +import com.google.zxing.MultiFormatReader; +import com.google.zxing.Result; +import com.google.zxing.client.j2se.BufferedImageLuminanceSource; +import com.google.zxing.common.HybridBinarizer; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; @DubboService(version = "1.0.0",group = "workcase",timeout = 30000,retries = 0) public class WorkcaseServiceImpl implements WorkcaseService { @@ -41,6 +69,9 @@ public class WorkcaseServiceImpl implements WorkcaseService { @Autowired private ChatRoomService chatRoomService; + @DubboReference(version = "1.0.0",group = "file",timeout = 30000,retries = 0) + private FileService fileService; + // ====================== 工单管理 ====================== @Override @@ -68,7 +99,10 @@ public class WorkcaseServiceImpl implements WorkcaseService { } // 统一由后端从登录态设置 creator,避免前端传入不可信 workcase.setCreator(LoginUtil.getCurrentUserId()); - + + // 解析设备铭牌二维码 + // ResultDomain deviceResult = anylizeQrCode(workcase.getDeviceNamePlateImg()); + int rows = workcaseMapper.insertWorkcase(workcase); if (rows > 0) { // 创建工单处理记录 @@ -80,7 +114,7 @@ public class WorkcaseServiceImpl implements WorkcaseService { process.setMessage("工单创建"); process.setCreator(workcase.getCreator()); workcaseProcessMapper.insertWorkcaseProcess(process); - + // 如果是新创建的聊天室,更新聊天室的 workcaseId logger.info("更新聊天室的工单ID: roomId={}, workcaseId={}", workcase.getRoomId(), workcase.getWorkcaseId()); @@ -89,9 +123,24 @@ public class WorkcaseServiceImpl implements WorkcaseService { updateRoom.setWorkcaseId(workcase.getWorkcaseId()); chatRoomService.updateChatRoom(updateRoom); - - syncWorkcaseToCrm(workcase); + // 插入设备文件记录到数据库 + // if (deviceResult.getSuccess() && deviceResult.getData() != null) { + // List deviceList = (List) deviceResult.getData(); + // for (TbWorkcaseDeviceDTO deviceDTO : deviceList) { + // deviceDTO.setWorkcaseId(workcase.getWorkcaseId()); + // try { + // workcaseDeviceMapper.insertWorkcaseDevice(deviceDTO); + // logger.info("设备文件记录插入成功: workcaseId={}, fileName={}", + // workcase.getWorkcaseId(), deviceDTO.getFileName()); + // } catch (Exception e) { + // logger.error("设备文件记录插入失败: " + deviceDTO.getFileName(), e); + // } + // } + // } + + syncWorkcaseToCrm(workcase); return ResultDomain.success("创建成功", workcase); + // return ResultDomain.success(deviceResult.getSuccess() ? "创建成功" : "设备铭牌二维码解析失败", workcase); } return ResultDomain.failure("创建失败"); } @@ -445,4 +494,349 @@ public class WorkcaseServiceImpl implements WorkcaseService { return ResultDomain.success("查询成功", pageDomain); } + /** + * @description 扫描设备铭牌二维码,并下载其中的文件,构建WorkDeviceDTO列表 + * @param qrcodeFileId 二维码文件id + * @return WorkDeviceDTO列表 + * @author yslg + * @since 2025-12-25 + */ + private ResultDomain anylizeQrCode(String qrcodeFileId){ + List workDeviceList = new ArrayList<>(); + + try { + logger.info("开始解析设备铭牌二维码: qrcodeFileId={}", qrcodeFileId); + + // 1. 从 FileService 获取二维码图片 + ResultDomain downloadResult = fileService.downloadFile(qrcodeFileId); + if (!downloadResult.getSuccess()) { + logger.error("下载二维码图片失败: {}", downloadResult.getMessage()); + return ResultDomain.failure("下载二维码图片失败: " + downloadResult.getMessage()); + } + + byte[] qrcodeImageBytes = downloadResult.getData(); + logger.info("二维码图片下载成功,大小: {} bytes", qrcodeImageBytes.length); + + // 2. 解析二维码内容 + String qrcodeContent = decodeQRCode(qrcodeImageBytes); + if (qrcodeContent == null || qrcodeContent.isEmpty()) { + logger.error("二维码解析失败或内容为空"); + return ResultDomain.failure("二维码解析失败或内容为空"); + } + + logger.info("二维码解析成功,内容: {}", qrcodeContent); + + // 3. 判断二维码内容类型并解析 + String deviceCode = null; + String device = null; + JSONArray files = null; + + // 尝试判断是 URL 还是 JSON + if (qrcodeContent.startsWith("http://") || qrcodeContent.startsWith("https://")) { + // 二维码包含 URL,需要访问 H5 页面解析 + logger.info("检测到二维码包含URL,开始访问页面: {}", qrcodeContent); + JSONObject pageData = parseDevicePageFromUrl(qrcodeContent); + + if (pageData == null) { + logger.error("访问URL并解析页面失败"); + return ResultDomain.failure("访问设备信息页面失败"); + } + + deviceCode = pageData.getString("deviceCode"); + device = pageData.getString("device"); + files = pageData.getJSONArray("files"); + } else { + // 二维码直接包含 JSON 数据 + logger.info("检测到二维码包含JSON数据,直接解析"); + JSONObject qrcodeData = JSON.parseObject(qrcodeContent); + deviceCode = qrcodeData.getString("deviceCode"); + device = qrcodeData.getString("device"); + files = qrcodeData.getJSONArray("files"); + } + + if (files == null || files.isEmpty()) { + logger.warn("二维码中没有文件信息"); + return ResultDomain.success("二维码中没有文件信息", workDeviceList); + } + + logger.info("设备编号: {}, 设备名称: {}, 文件数量: {}", deviceCode, device, files.size()); + + // 4. 下载并存储每个文件 + for (int i = 0; i < files.size(); i++) { + JSONObject fileInfo = files.getJSONObject(i); + String fileName = fileInfo.getString("fileName"); + String fileUrl = fileInfo.getString("url"); + + logger.info("开始下载文件 [{}/{}]: {}", i + 1, files.size(), fileName); + + try { + // 下载文件 + byte[] fileBytes = downloadFileFromUrl(fileUrl); + if (fileBytes == null || fileBytes.length == 0) { + logger.error("文件下载失败: {}", fileName); + continue; + } + + logger.info("文件下载成功: {}, 大小: {} bytes", fileName, fileBytes.length); + + // 推断文件类型 + String contentType = getContentTypeFromFileName(fileName); + + // 通过 FileService 存储文件 + ResultDomain uploadResult = fileService.uploadFileBytes( + fileBytes, + fileName, + contentType, + "workcase", + deviceCode + ); + + if (uploadResult.getSuccess()) { + TbSysFileDTO uploadedFile = uploadResult.getData(); + logger.info("文件上传成功: fileId={}, fileName={}", uploadedFile.getFileId(), fileName); + + // 创建 TbWorkcaseDeviceDTO 对象 + TbWorkcaseDeviceDTO deviceDTO = new TbWorkcaseDeviceDTO(); + deviceDTO.setDevice(device); + deviceDTO.setDeviceCode(deviceCode); + deviceDTO.setFileId(uploadedFile.getFileId()); + deviceDTO.setFileName(fileName); + deviceDTO.setFileRootId(uploadedFile.getFileRootId()); + deviceDTO.setOptsn(IdUtil.getOptsn()); + + workDeviceList.add(deviceDTO); + } else { + logger.error("文件上传失败: {}, 原因: {}", fileName, uploadResult.getMessage()); + } + + } catch (Exception e) { + logger.error("处理文件失败: " + fileName, e); + } + } + + logger.info("二维码解析完成,成功处理 {} 个文件", workDeviceList.size()); + return ResultDomain.success("解析成功,处理了 " + workDeviceList.size() + " 个文件", workDeviceList); + + } catch (Exception e) { + logger.error("解析设备铭牌二维码失败", e); + return ResultDomain.failure("解析设备铭牌二维码失败: " + e.getMessage()); + } + } + + /** + * 从 URL 解析设备页面信息 + */ + private JSONObject parseDevicePageFromUrl(String url) { + try { + logger.info("开始访问设备信息页面: {}", url); + + // 访问 URL 并获取 HTML 内容 + Document doc = Jsoup.connect(url) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .timeout(10000) + .followRedirects(true) + .get(); + + logger.info("页面访问成功,开始解析内容"); + + // 解析设备编号和型号(从页面标题或特定元素中) + String deviceInfo = doc.select("body").text(); + + // 尝试匹配设备编号模式:数字格式如 202508012 + String deviceCode = null; + String device = null; + + // 从页面内容中提取设备编号和型号 + if (deviceInfo.contains("(") && deviceInfo.contains(")")) { + // 例如: "202508012 (THHM1800PL)" + int start = deviceInfo.indexOf("("); + int end = deviceInfo.indexOf(")", start); + if (start > 0 && end > start) { + // 提取括号前的设备编号 + String beforeBracket = deviceInfo.substring(Math.max(0, start - 20), start).trim(); + String[] parts = beforeBracket.split("\\s+"); + if (parts.length > 0) { + deviceCode = parts[parts.length - 1].trim(); + } + + // 提取括号内的型号 + device = deviceInfo.substring(start + 1, end).trim(); + } + } + + logger.info("提取的设备信息 - 编号: {}, 型号: {}", deviceCode, device); + + // 解析文件列表 + JSONArray files = new JSONArray(); + + // 查找所有下载链接(通常是 PDF、图片等文件) + Elements links = doc.select("a[href]"); + for (Element link : links) { + String href = link.attr("abs:href"); // 获取绝对 URL + String text = link.text().trim(); + + // 过滤出文件下载链接(包含文件扩展名) + if (isFileLink(href) && !text.isEmpty()) { + JSONObject fileInfo = new JSONObject(); + fileInfo.put("fileName", text); + fileInfo.put("url", href); + files.add(fileInfo); + logger.info("发现文件: {} -> {}", text, href); + } + } + + // 如果没找到链接,尝试从 script 标签中查找 JSON 数据 + if (files.isEmpty()) { + Elements scripts = doc.select("script"); + for (Element script : scripts) { + String scriptContent = script.html(); + // 尝试查找 JSON 数据 + if (scriptContent.contains("files") || scriptContent.contains("fileName")) { + try { + // 尝试解析嵌入的 JSON + int jsonStart = scriptContent.indexOf("{"); + int jsonEnd = scriptContent.lastIndexOf("}") + 1; + if (jsonStart >= 0 && jsonEnd > jsonStart) { + String jsonStr = scriptContent.substring(jsonStart, jsonEnd); + JSONObject embedded = JSON.parseObject(jsonStr); + if (embedded.containsKey("files")) { + files = embedded.getJSONArray("files"); + if (deviceCode == null) { + deviceCode = embedded.getString("deviceCode"); + } + if (device == null) { + device = embedded.getString("device"); + } + break; + } + } + } catch (Exception e) { + // 忽略解析错误,继续下一个 script + } + } + } + } + + if (deviceCode == null || files.isEmpty()) { + logger.error("无法从页面中提取完整的设备信息"); + return null; + } + + JSONObject result = new JSONObject(); + result.put("deviceCode", deviceCode); + result.put("device", device != null ? device : "Unknown"); + result.put("files", files); + + logger.info("页面解析成功,设备编号: {}, 文件数量: {}", deviceCode, files.size()); + return result; + + } catch (Exception e) { + logger.error("解析设备页面失败: " + url, e); + return null; + } + } + + /** + * 判断是否为文件下载链接 + */ + private boolean isFileLink(String url) { + if (url == null || url.isEmpty()) { + return false; + } + String lowerUrl = url.toLowerCase(); + return lowerUrl.endsWith(".pdf") || + lowerUrl.endsWith(".jpg") || + lowerUrl.endsWith(".jpeg") || + lowerUrl.endsWith(".png") || + lowerUrl.endsWith(".doc") || + lowerUrl.endsWith(".docx") || + lowerUrl.endsWith(".xls") || + lowerUrl.endsWith(".xlsx") || + lowerUrl.contains("/download") || + lowerUrl.contains("/file/") || + lowerUrl.contains(".pdf?") || + lowerUrl.contains(".jpg?"); + } + + /** + * 解码二维码图片 + */ + private String decodeQRCode(byte[] imageBytes) { + try { + InputStream inputStream = new ByteArrayInputStream(imageBytes); + BufferedImage bufferedImage = ImageIO.read(inputStream); + + if (bufferedImage == null) { + logger.error("无法读取图片"); + return null; + } + + BinaryBitmap binaryBitmap = new BinaryBitmap( + new HybridBinarizer( + new BufferedImageLuminanceSource(bufferedImage) + ) + ); + + MultiFormatReader reader = new MultiFormatReader(); + java.util.Map hints = new java.util.HashMap<>(); + hints.put(DecodeHintType.CHARACTER_SET, "UTF-8"); + + Result result = reader.decode(binaryBitmap, hints); + return result.getText(); + + } catch (Exception e) { + logger.error("解码二维码失败", e); + return null; + } + } + + /** + * 从URL下载文件 + */ + private byte[] downloadFileFromUrl(String fileUrl) { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpGet httpGet = new HttpGet(URI.create(fileUrl)); + + try (CloseableHttpResponse response = httpClient.execute(httpGet)) { + if (response.getCode() == 200) { + return EntityUtils.toByteArray(response.getEntity()); + } else { + logger.error("下载文件失败,HTTP状态码: {}", response.getCode()); + return null; + } + } + } catch (Exception e) { + logger.error("下载文件失败: " + fileUrl, e); + return null; + } + } + + /** + * 根据文件名推断 Content-Type + */ + private String getContentTypeFromFileName(String fileName) { + if (fileName == null) { + return "application/octet-stream"; + } + + String lowerFileName = fileName.toLowerCase(); + if (lowerFileName.endsWith(".pdf")) { + return "application/pdf"; + } else if (lowerFileName.endsWith(".jpg") || lowerFileName.endsWith(".jpeg")) { + return "image/jpeg"; + } else if (lowerFileName.endsWith(".png")) { + return "image/png"; + } else if (lowerFileName.endsWith(".doc")) { + return "application/msword"; + } else if (lowerFileName.endsWith(".docx")) { + return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + } else if (lowerFileName.endsWith(".xls")) { + return "application/vnd.ms-excel"; + } else if (lowerFileName.endsWith(".xlsx")) { + return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + } else { + return "application/octet-stream"; + } + } + } diff --git a/urbanLifelineWeb/packages/workcase/src/api/workcase/meeting.ts b/urbanLifelineWeb/packages/workcase/src/api/workcase/meeting.ts new file mode 100644 index 00000000..c207277d --- /dev/null +++ b/urbanLifelineWeb/packages/workcase/src/api/workcase/meeting.ts @@ -0,0 +1,79 @@ +import { http } from '@/utils/http' + +/** + * 创建视频会议参数 + */ +export interface CreateMeetingParams { + roomId: string + workcaseId?: string + meetingName: string + maxParticipants?: number +} + +/** + * 视频会议VO + */ +export interface VideoMeetingVO { + meetingId: string + roomId: string + workcaseId?: string + meetingName: string + meetingPassword?: string + jwtToken: string + jitsiRoomName: string + jitsiServerUrl: string + status: string + creatorId: string + creatorType: string + creatorName: string + participantCount: number + maxParticipants: number + actualStartTime?: string + actualEndTime?: string + durationSeconds?: number + durationFormatted?: string + iframeUrl: string + config?: any +} + +/** + * 创建视频会议 + */ +export const createVideoMeeting = (params: CreateMeetingParams) => { + return http.post('/workcase/chat/meeting/create', params) +} + +/** + * 获取会议信息 + */ +export const getMeetingInfo = (meetingId: string) => { + return http.get(`/workcase/chat/meeting/${meetingId}`) +} + +/** + * 获取聊天室活跃会议 + */ +export const getActiveMeeting = (roomId: string) => { + return http.get(`/workcase/chat/meeting/room/${roomId}/active`) +} + +/** + * 加入会议(生成用户专属JWT) + */ +export const joinMeeting = (meetingId: string) => { + return http.post(`/workcase/chat/meeting/${meetingId}/join`) +} + +/** + * 开始会议 + */ +export const startVideoMeeting = (meetingId: string) => { + return http.post(`/workcase/chat/meeting/${meetingId}/start`) +} + +/** + * 结束会议 + */ +export const endVideoMeeting = (meetingId: string) => { + return http.post(`/workcase/chat/meeting/${meetingId}/end`) +} diff --git a/urbanLifelineWeb/packages/workcase/src/components/chatRoom/chatRoom/ChatRoom.scss b/urbanLifelineWeb/packages/workcase/src/components/chatRoom/chatRoom/ChatRoom.scss index add89690..7b761249 100644 --- a/urbanLifelineWeb/packages/workcase/src/components/chatRoom/chatRoom/ChatRoom.scss +++ b/urbanLifelineWeb/packages/workcase/src/components/chatRoom/chatRoom/ChatRoom.scss @@ -47,18 +47,59 @@ $brand-color-hover: #004488; // ==================== Jitsi Meet会议容器 ==================== .meeting-container { - position: sticky; - top: 0; - z-index: 10; - height: 400px; - background: #000; - border-bottom: 2px solid $brand-color; + width: 100%; + height: 500px; margin-bottom: 16px; + border-radius: 8px; + overflow: hidden; + border: 1px solid #e5e7eb; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - iframe { - width: 100%; - height: 100%; + .meeting-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + font-weight: 500; + font-size: 14px; + } + + .close-meeting-btn { + display: flex; + align-items: center; + gap: 4px; + padding: 6px 12px; + background: rgba(255, 255, 255, 0.2); border: none; + border-radius: 6px; + color: white; + cursor: pointer; + font-size: 14px; + transition: all 0.2s; + + &:hover { + background: rgba(255, 255, 255, 0.3); + } + } + + .meeting-iframe { + width: 100%; + height: calc(100% - 48px); + border: none; + } +} + +// 按钮禁用状态 +.action-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + + &:hover { + border-color: #e2e8f0; + color: #64748b; + background: transparent; } } diff --git a/urbanLifelineWeb/packages/workcase/src/components/chatRoom/chatRoom/ChatRoom.vue b/urbanLifelineWeb/packages/workcase/src/components/chatRoom/chatRoom/ChatRoom.vue index 1cd20ce0..7f65cb91 100644 --- a/urbanLifelineWeb/packages/workcase/src/components/chatRoom/chatRoom/ChatRoom.vue +++ b/urbanLifelineWeb/packages/workcase/src/components/chatRoom/chatRoom/ChatRoom.vue @@ -17,7 +17,14 @@
- +
+ 视频会议进行中 + +
+
@@ -72,12 +79,16 @@