diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/JitsiProperties.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/JitsiProperties.java new file mode 100644 index 00000000..5e92cbd6 --- /dev/null +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/JitsiProperties.java @@ -0,0 +1,122 @@ +package org.xyzh.workcase.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * @description Jitsi Meet 配置属性类 + * @filename JitsiProperties.java + * @author claude + * @copyright xyzh + * @since 2025-12-27 + */ +@Configuration +@ConfigurationProperties(prefix = "jitsi") +public class JitsiProperties { + + /** + * 应用配置 + */ + private App app = new App(); + + /** + * 服务器配置 + */ + private Server server = new Server(); + + /** + * Token配置 + */ + private Token token = new Token(); + + public App getApp() { + return app; + } + + public void setApp(App app) { + this.app = app; + } + + public Server getServer() { + return server; + } + + public void setServer(Server server) { + this.server = server; + } + + public Token getToken() { + return token; + } + + public void setToken(Token token) { + this.token = token; + } + + /** + * 应用配置 + */ + public static class App { + /** + * 应用ID(必须与Docker配置中的JWT_APP_ID一致) + */ + private String id = "urbanLifeline"; + + /** + * JWT密钥(必须与Docker配置中的JWT_APP_SECRET一致) + */ + private String secret = "your-secret-key-change-in-production"; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getSecret() { + return secret; + } + + public void setSecret(String secret) { + this.secret = secret; + } + } + + /** + * 服务器配置 + */ + public static class Server { + /** + * Jitsi Meet服务器地址 + */ + private String url = "https://meet.jit.si"; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + } + + /** + * Token配置 + */ + public static class Token { + /** + * JWT Token有效期(毫秒)- 默认2小时 + */ + private Long expiration = 7200000L; + + public Long getExpiration() { + return expiration; + } + + public void setExpiration(Long expiration) { + this.expiration = expiration; + } + } +} 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 index dcb1ddce..7ffce9c9 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/JitsiTokenServiceImpl.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/JitsiTokenServiceImpl.java @@ -7,8 +7,9 @@ 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.springframework.beans.factory.annotation.Autowired; import org.xyzh.api.workcase.service.JitsiTokenService; +import org.xyzh.workcase.config.JitsiProperties; import java.util.Date; import java.util.HashMap; @@ -25,17 +26,8 @@ import java.util.Map; 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小时 + @Autowired + private JitsiProperties jitsiProperties; @Override public String generateJwtToken(String roomName, String userId, String userName, boolean isModerator) { @@ -44,7 +36,7 @@ public class JitsiTokenServiceImpl implements JitsiTokenService { try { long now = System.currentTimeMillis(); - long exp = now + tokenExpiration; + long exp = now + jitsiProperties.getToken().getExpiration(); // 构建用户上下文 Map userContext = new HashMap<>(); @@ -56,9 +48,9 @@ public class JitsiTokenServiceImpl implements JitsiTokenService { Map claims = new HashMap<>(); claims.put("context", Map.of("user", userContext)); claims.put("room", roomName); - claims.put("iss", jitsiAppId); + claims.put("iss", jitsiProperties.getApp().getId()); claims.put("aud", "jitsi"); - claims.put("sub", jitsiServerUrl); + claims.put("sub", jitsiProperties.getServer().getUrl()); claims.put("exp", exp / 1000); // 秒级时间戳 claims.put("nbf", now / 1000); @@ -73,7 +65,7 @@ public class JitsiTokenServiceImpl implements JitsiTokenService { .setClaims(claims) .setIssuedAt(new Date(now)) .setExpiration(new Date(exp)) - .signWith(SignatureAlgorithm.HS256, jitsiAppSecret.getBytes()) + .signWith(SignatureAlgorithm.HS256, jitsiProperties.getApp().getSecret().getBytes()) .compact(); logger.info("JWT Token生成成功: roomName={}", roomName); @@ -88,7 +80,7 @@ public class JitsiTokenServiceImpl implements JitsiTokenService { public boolean validateJwtToken(String token) { try { Claims claims = Jwts.parser() - .setSigningKey(jitsiAppSecret.getBytes()) + .setSigningKey(jitsiProperties.getApp().getSecret().getBytes()) .parseClaimsJws(token) .getBody(); @@ -106,7 +98,7 @@ public class JitsiTokenServiceImpl implements JitsiTokenService { logger.info("构建Jitsi iframe URL: roomName={}", roomName); StringBuilder url = new StringBuilder(); - url.append(jitsiServerUrl).append("/").append(roomName); + url.append(jitsiProperties.getServer().getUrl()).append("/").append(roomName); // 添加JWT Token url.append("?jwt=").append(jwtToken); 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 index 4d7c5cd3..2c4a9f93 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/VideoMeetingServiceImpl.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/VideoMeetingServiceImpl.java @@ -21,6 +21,7 @@ import org.xyzh.common.auth.utils.LoginUtil; import org.xyzh.common.core.domain.LoginDomain; import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.utils.id.IdUtil; +import org.xyzh.workcase.config.JitsiProperties; import org.xyzh.workcase.mapper.TbChatRoomMemberMapper; import org.xyzh.workcase.mapper.TbVideoMeetingMapper; @@ -53,6 +54,9 @@ public class VideoMeetingServiceImpl implements VideoMeetingService { @Autowired private ChatRoomService chatRoomService; + @Autowired + private JitsiProperties jitsiProperties; + @DubboReference(version = "1.0.0", group = "auth", timeout = 30000, retries = 0) private AuthService authService; @@ -132,7 +136,7 @@ public class VideoMeetingServiceImpl implements VideoMeetingService { if (meetingDTO.getAdvance() == null) { meetingDTO.setAdvance(5); // 默认提前5分钟可入会 } - + meetingDTO.setJitsiServerUrl(jitsiProperties.getServer().getUrl()); // 6. 插入数据库 int rows = videoMeetingMapper.insertVideoMeeting(meetingDTO); if (rows > 0) { diff --git a/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/ChatRoomView.vue b/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/ChatRoomView.vue index 96699d69..96389972 100644 --- a/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/ChatRoomView.vue +++ b/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/ChatRoomView.vue @@ -683,8 +683,9 @@ const subscribeToRoom = (roomId: string) => { roomSubscription = stompClient.subscribe(`/topic/chat/${roomId}`, (message: any) => { const chatMessage = JSON.parse(message.body) as ChatRoomMessageVO - // 避免重复添加自己发送的消息 - if (chatMessage.senderId !== loginDomain.user.userId) { + // 避免重复添加自己发送的普通消息 + // 但会议消息(meet类型)始终添加,因为它是系统生成的通知 + if (chatMessage.messageType === 'meet' || chatMessage.senderId !== loginDomain.user.userId) { messages.value.push(chatMessage) scrollToBottom() } diff --git a/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/MeetingCard/MeetingCard.vue b/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/MeetingCard/MeetingCard.vue index 05bd99e7..b3265f05 100644 --- a/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/MeetingCard/MeetingCard.vue +++ b/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/MeetingCard/MeetingCard.vue @@ -1,6 +1,9 @@ \ No newline at end of file diff --git a/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/chatRoom/ChatRoom.vue b/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/chatRoom/ChatRoom.vue index b8c47552..61d0b49c 100644 --- a/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/chatRoom/ChatRoom.vue +++ b/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/chatRoom/ChatRoom.vue @@ -36,7 +36,7 @@
@@ -186,7 +186,9 @@ const showMeetingCreate = ref(false) // 打开创建会议对话框或直接emit事件给父组件处理 const handleStartMeeting = () => { // emit事件给父组件,让父组件处理会议逻辑 - emit('start-meeting') + // emit('start-meeting') + showMeetingCreate.value = true + } // 会议创建成功回调 @@ -322,11 +324,13 @@ const handleJoinMeeting = async (meetingId: string) => { } // 获取会议数据(将contentExtra转换为VideoMeetingVO) -function getMeetingData(contentExtra: Record | undefined): VideoMeetingVO { - if (!contentExtra) { - return {} as VideoMeetingVO +// 从消息extra中提取meetingId +function getMeetingId(contentExtra: Record | undefined): string { + if (!contentExtra || !contentExtra.meetingId) { + console.warn('[ChatRoom] contentExtra中没有meetingId:', contentExtra) + return '' } - return contentExtra as VideoMeetingVO + return contentExtra.meetingId as string } // Markdown渲染函数 diff --git a/urbanLifelineWeb/packages/workcase_wechat/App.uvue b/urbanLifelineWeb/packages/workcase_wechat/App.uvue index 36b13375..f15c6008 100644 --- a/urbanLifelineWeb/packages/workcase_wechat/App.uvue +++ b/urbanLifelineWeb/packages/workcase_wechat/App.uvue @@ -46,7 +46,7 @@ // 显示模式选择器 showModeSelector() { uni.showActionSheet({ - itemList: ['员工模式 (17857100375)', '访客模式 (17857100376)'], + itemList: ['员工模式 (17857100375)', '访客模式 (17857100377)'], success: (res) => { let wechatId = '' let userMode = '' @@ -56,8 +56,8 @@ phone = '17857100375' userMode = 'staff' } else { - wechatId = '17857100376' - phone = '17857100376' + wechatId = '17857100377' + phone = '17857100377' userMode = 'guest' } // 存储选择 @@ -73,7 +73,7 @@ fail: () => { // 用户取消,默认使用访客模式 uni.setStorageSync('userMode', 'guest') - uni.setStorageSync('wechatId', '17857100376') + uni.setStorageSync('wechatId', '17857100377') console.log('默认使用访客模式') } }) diff --git a/urbanLifelineWeb/packages/workcase_wechat/pages.json b/urbanLifelineWeb/packages/workcase_wechat/pages.json index 8a0711bf..e6fd28a2 100644 --- a/urbanLifelineWeb/packages/workcase_wechat/pages.json +++ b/urbanLifelineWeb/packages/workcase_wechat/pages.json @@ -41,7 +41,7 @@ } }, { - "path": "pages/meeting/MeetingCreate", + "path": "pages/meeting/meetingCreate/MeetingCreate", "style": { "navigationStyle": "custom", "navigationBarTitleText": "创建会议" diff --git a/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoom/chatRoom.uvue b/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoom/chatRoom.uvue index ab2b76f7..c0ed09da 100644 --- a/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoom/chatRoom.uvue +++ b/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoom/chatRoom.uvue @@ -66,7 +66,7 @@ - + {{ formatTime(msg.sendTime) }} @@ -81,7 +81,7 @@ - + {{ formatTime(msg.sendTime) }} @@ -410,11 +410,13 @@ function formatTime(time?: string): string { } // 获取会议数据(将contentExtra转换为VideoMeetingVO) -function getMeetingData(contentExtra: Record | undefined): VideoMeetingVO { - if (!contentExtra) { - return {} as VideoMeetingVO +// 从消息extra中提取meetingId +function getMeetingId(contentExtra: Record | undefined): string { + if (!contentExtra || !contentExtra.meetingId) { + console.warn('[chatRoom] contentExtra中没有meetingId:', contentExtra) + return '' } - return contentExtra as VideoMeetingVO + return contentExtra.meetingId as string } // Markdown渲染函数(返回富文本HTML) @@ -553,119 +555,20 @@ function handleWorkcaseAction() { } // 发起会议 - 跳转到会议创建页面 -async function startMeeting() { - // 先检查是否有活跃会议 - try { - const res = await workcaseChatAPI.getActiveMeeting(roomId.value) - if (res.success && res.data) { - // 已有活跃会议,直接加入 - const meetingPageUrl = res.data.iframeUrl - const meetingId = res.data.meetingId - const meetingName = res.data.meetingName || '视频会议' - - // 构建完整的会议URL(包含域名和workcase路径) - const protocol = window.location.protocol - const host = window.location.host - const fullPath = meetingPageUrl.startsWith('/workcase') - ? meetingPageUrl - : '/workcase' + meetingPageUrl - // 附加roomId参数,用于离开会议后返回聊天室 - const fullMeetingUrl = `${protocol}//${host}${fullPath}&roomId=${roomId.value}` - - // 小程序环境:显示提示,引导用户复制链接在浏览器打开 - uni.showModal({ - title: '视频会议', - content: '微信小程序暂不支持视频会议,请复制链接在浏览器中打开', - confirmText: '复制链接', - cancelText: '取消', - success: (res) => { - if (res.confirm) { - uni.setClipboardData({ - data: fullMeetingUrl, - success: () => { - uni.showToast({ - title: '链接已复制,请在浏览器中打开', - icon: 'none', - duration: 3000 - }) - } - }) - } - } - }) - return - } - } catch (e) { - console.log('[chatRoom] 无活跃会议') - } - - // 没有活跃会议,创建新会议 - try { - const createRes = await workcaseChatAPI.createMeeting({ - roomId: roomId.value, - meetingName: roomName.value || '视频会议' - }) - - if (createRes.success && createRes.data) { - const meetingId = createRes.data.meetingId - - // 开始会议 - await workcaseChatAPI.startMeeting(meetingId) - - // 加入会议获取会议页面URL - const joinRes = await workcaseChatAPI.joinMeeting(meetingId) - if (joinRes.success && joinRes.data && joinRes.data.iframeUrl) { - const meetingPageUrl = joinRes.data.iframeUrl - - // 构建完整的会议URL(包含域名和workcase路径) - const protocol = window.location.protocol - const host = window.location.host - const fullPath = meetingPageUrl.startsWith('/workcase') - ? meetingPageUrl - : '/workcase' + meetingPageUrl - // 附加roomId参数,用于离开会议后返回聊天室 - const fullMeetingUrl = `${protocol}//${host}${fullPath}&roomId=${roomId.value}` - - // 小程序环境:显示提示,引导用户复制链接在浏览器打开 - uni.showModal({ - title: '会议已创建', - content: '微信小程序暂不支持视频会议,请复制链接在浏览器中打开', - confirmText: '复制链接', - cancelText: '取消', - success: (res) => { - if (res.confirm) { - uni.setClipboardData({ - data: fullMeetingUrl, - success: () => { - uni.showToast({ - title: '链接已复制,请在浏览器中打开', - icon: 'none', - duration: 3000 - }) - } - }) - } - } - }) - } else { - uni.showToast({ - title: '获取会议链接失败', - icon: 'none' - }) - } - } else { +function startMeeting() { + // 跳转到会议创建页面 + const url = `/pages/meeting/meetingCreate/MeetingCreate?roomId=${roomId.value}${workcaseId.value ? '&workcaseId=' + workcaseId.value : ''}` + console.log('[chatRoom] 跳转会议创建页面:', url) + uni.navigateTo({ + url: url, + fail: (err) => { + console.error('[chatRoom] 跳转会议创建页面失败:', err) uni.showToast({ - title: createRes.message || '创建会议失败', + title: '跳转失败', icon: 'none' }) } - } catch (error) { - console.error('[chatRoom] 创建会议失败:', error) - uni.showToast({ - title: '创建会议失败', - icon: 'none' - }) - } + }) } // 加入会议(从MeetingCard点击加入) @@ -793,24 +696,25 @@ function disconnectWebSocket() { // 处理接收到的新消息 function handleNewMessage(message: ChatRoomMessageVO) { console.log('[chatRoom] 收到新消息:', message) - - // 避免重复添加自己发送的消息(自己发送的消息已经通过sendMessage添加到列表) - if (message.senderId === currentUserId.value) { - console.log('[chatRoom] 跳过自己发送的消息') + + // 避免重复添加自己发送的普通消息(自己发送的消息已经通过sendMessage添加到列表) + // 但会议消息(meet类型)始终添加,因为它是系统生成的通知 + if (message.messageType !== 'meet' && message.senderId === currentUserId.value) { + console.log('[chatRoom] 跳过自己发送的普通消息') return } - + // 检查消息是否已存在(避免重复) const exists = messages.some(m => m.messageId === message.messageId) if (exists) { console.log('[chatRoom] 消息已存在,跳过') return } - + // 添加新消息到列表 messages.push(message) nextTick(() => scrollToBottom()) - + // 可以添加消息提示音或震动 // uni.vibrateShort() } diff --git a/urbanLifelineWeb/packages/workcase_wechat/pages/meeting/meetingCard/MeetingCard.uvue b/urbanLifelineWeb/packages/workcase_wechat/pages/meeting/meetingCard/MeetingCard.uvue index dcb1921f..459b4376 100644 --- a/urbanLifelineWeb/packages/workcase_wechat/pages/meeting/meetingCard/MeetingCard.uvue +++ b/urbanLifelineWeb/packages/workcase_wechat/pages/meeting/meetingCard/MeetingCard.uvue @@ -1,6 +1,9 @@