854 lines
22 KiB
Markdown
854 lines
22 KiB
Markdown
# 工单服务 + Jitsi Meet 视频会议 技术方案
|
||
|
||
## 📋 目录
|
||
1. [业务流程分析](#业务流程分析)
|
||
2. [数据表结构设计](#数据表结构设计)
|
||
3. [核心服务功能](#核心服务功能)
|
||
4. [API接口设计](#api接口设计)
|
||
5. [前端集成方案](#前端集成方案)
|
||
6. [安全方案](#安全方案)
|
||
|
||
---
|
||
|
||
## 业务流程分析
|
||
|
||
### 完整业务流程
|
||
|
||
```
|
||
用户进入小程序
|
||
↓
|
||
AI客服对话(默认,存储在 ai.tb_chat 表)
|
||
↓
|
||
连续3次AI对话后询问是否转人工
|
||
↓
|
||
用户触发转人工(可能一开始就手动触发,没有聊天记录)
|
||
↓
|
||
【核心变更】创建IM聊天室(取代微信客服)
|
||
↓
|
||
同步 ai.tb_chat 对话记录到聊天室
|
||
↓
|
||
客服人员加入聊天室(AI退出)
|
||
↓
|
||
客服与客户IM对话(员工续接AI对话)
|
||
↓
|
||
【可选】发起Jitsi Meet视频会议
|
||
↓
|
||
【可选】用户/员工在聊天室内创建工单(也可在web管理端)
|
||
↓
|
||
工单处理和状态更新
|
||
↓
|
||
工单完成/撤销,生成总结和词云
|
||
```
|
||
|
||
### 关键改动点
|
||
|
||
**AI对话 → 转人工 → 创建聊天室**
|
||
- AI对话存储在 `ai.tb_chat` 表(通过WorkcaseChatService调用AI接口)
|
||
- 转人工时创建IM聊天室,同步AI对话记录
|
||
- 工单和会议没有前置关系,可在聊天室内随时创建
|
||
|
||
**取消微信客服 → 使用自建IM + Jitsi Meet**
|
||
- IM聊天室:文字、图片、文件、语音消息
|
||
- 视频会议:通过iframe嵌入Jitsi Meet,最简实现
|
||
- 权限控制:只有聊天室成员才能创建/加入会议
|
||
|
||
---
|
||
|
||
## 数据表结构设计
|
||
|
||
### 核心表结构(6+1张表)
|
||
|
||
#### 1. **tb_chat_room** - 聊天室表 ⭐核心表
|
||
|
||
**用途**:转人工时创建的聊天室,可关联工单
|
||
|
||
```sql
|
||
room_id -- 聊天室ID(主键)
|
||
workcase_id -- 关联工单ID(可选,可后续绑定)
|
||
room_name -- 聊天室名称
|
||
status -- 状态:active-活跃 closed-已关闭 archived-已归档
|
||
guest_id -- 来客ID
|
||
guest_name -- 来客姓名
|
||
ai_session_id -- AI对话会话ID(从ai.tb_chat同步)
|
||
current_agent_id -- 当前负责客服ID
|
||
agent_count -- 已加入客服人数
|
||
message_count -- 消息总数
|
||
unread_count -- 未读消息数(客服端)
|
||
last_message_time -- 最后消息时间
|
||
last_message -- 最后消息内容(列表展示用)
|
||
```
|
||
|
||
**业务规则**:
|
||
- 转人工时创建聊天室,同步 ai.tb_chat 对话记录
|
||
- 用户/员工可在聊天室内创建工单,绑定 workcase_id
|
||
- 聊天室可独立存在,无需绑定工单
|
||
- 工单完成后聊天室归档
|
||
|
||
---
|
||
|
||
#### 2. **tb_chat_room_member** - 聊天室成员表
|
||
|
||
**用途**:记录聊天室内的所有成员
|
||
|
||
```sql
|
||
member_id -- 成员记录ID(主键)
|
||
room_id -- 聊天室ID
|
||
user_id -- 用户ID(来客ID或员工ID)
|
||
user_type -- 用户类型:guest-来客 agent-客服 ai-AI助手
|
||
user_name -- 用户名称
|
||
role -- 角色:owner-创建者 admin-管理员 member-普通成员
|
||
status -- 状态:active-活跃 left-已离开 removed-被移除
|
||
unread_count -- 该成员的未读消息数
|
||
last_read_time -- 最后阅读时间
|
||
last_read_msg_id -- 最后阅读的消息ID
|
||
join_time -- 加入时间
|
||
leave_time -- 离开时间
|
||
```
|
||
|
||
**业务规则**:
|
||
- 来客自动加入(创建者)
|
||
- 客服手动加入或系统分配
|
||
- 用于权限校验:只有成员能发消息和发起会议
|
||
|
||
---
|
||
|
||
#### 3. **tb_chat_message** - 聊天室消息表
|
||
|
||
**用途**:存储所有聊天消息(AI对话+人工客服对话)
|
||
|
||
```sql
|
||
message_id -- 消息ID(主键)
|
||
room_id -- 聊天室ID
|
||
sender_id -- 发送者ID
|
||
sender_type -- 发送者类型:guest-来客 agent-客服 ai-AI助手 system-系统消息
|
||
sender_name -- 发送者名称
|
||
message_type -- 消息类型:text image file voice video system meeting
|
||
content -- 消息内容
|
||
content_extra -- 扩展内容(JSONB:图片URL、文件信息、会议链接等)
|
||
reply_to_msg_id -- 回复的消息ID(引用回复)
|
||
is_ai_message -- 是否AI消息
|
||
ai_message_id -- AI原始消息ID(追溯用)
|
||
status -- 状态:sending sent delivered read failed recalled
|
||
read_count -- 已读人数
|
||
send_time -- 发送时间
|
||
```
|
||
|
||
**业务规则**:
|
||
- 从ai.tb_chat同步AI对话时设置 `is_ai_message=true`
|
||
- 会议通知作为系统消息 `message_type=meeting`
|
||
- 支持引用回复和消息撤回
|
||
|
||
---
|
||
|
||
#### 4. **tb_video_meeting** - 视频会议表 ⭐Jitsi Meet
|
||
|
||
**用途**:记录聊天室内创建的视频会议
|
||
|
||
```sql
|
||
meeting_id -- 会议ID(主键,也是Jitsi房间名)
|
||
room_id -- 关联聊天室ID
|
||
workcase_id -- 关联工单ID
|
||
meeting_name -- 会议名称
|
||
meeting_password -- 会议密码(可选)
|
||
jwt_token -- JWT Token(身份验证)
|
||
jitsi_room_name -- Jitsi房间名(格式:workcase_{workcase_id}_{timestamp})
|
||
jitsi_server_url -- Jitsi服务器地址(默认:https://meet.jit.si)
|
||
status -- 状态:scheduled ongoing ended cancelled
|
||
creator_type -- 创建者类型:guest-来客 agent-客服
|
||
creator_name -- 创建者名称
|
||
participant_count -- 参与人数
|
||
max_participants -- 最大参与人数
|
||
start_time -- 实际开始时间
|
||
end_time -- 实际结束时间
|
||
duration_seconds -- 会议时长(秒)
|
||
iframe_url -- iframe嵌入URL(生成后存储)
|
||
config -- Jitsi配置项(JSONB自定义配置)
|
||
```
|
||
|
||
**业务规则**:
|
||
- 只有聊天室成员能创建会议
|
||
- `jitsi_room_name`唯一,格式:`workcase_{workcaseId}_{timestamp}`
|
||
- `iframe_url`在创建时生成,前端直接使用
|
||
|
||
---
|
||
|
||
#### 5. **tb_meeting_participant** - 会议参与记录表(可选)
|
||
|
||
**用途**:用于审计和统计
|
||
|
||
```sql
|
||
participant_id -- 参与记录ID(主键)
|
||
meeting_id -- 会议ID
|
||
user_id -- 用户ID
|
||
user_type -- 用户类型:guest-来客 agent-客服
|
||
user_name -- 用户名称
|
||
join_time -- 加入时间
|
||
leave_time -- 离开时间
|
||
duration_seconds -- 参与时长(秒)
|
||
is_moderator -- 是否主持人
|
||
join_method -- 加入方式:web mobile desktop
|
||
device_info -- 设备信息
|
||
```
|
||
|
||
**业务规则**:
|
||
- 记录每个参与者的加入和离开时间
|
||
- 用于统计会议时长和参与情况
|
||
- 可用于生成会议报告
|
||
|
||
---
|
||
|
||
#### 6. **tb_customer_service** - 客服人员配置表(可选)
|
||
|
||
**用途**:管理有客服权限的员工
|
||
|
||
```sql
|
||
user_id
|
||
username
|
||
---
|
||
|
||
#### 7. **tb_word_cloud** - 词云统计表(已有)
|
||
|
||
**用途**:记录聊天和工单中的关键词
|
||
|
||
```sql
|
||
word_id -- 词条ID(主键)
|
||
word -- 词语
|
||
frequency -- 词频
|
||
source_type -- 来源类型:chat workcase global
|
||
source_id -- 来源ID(room_id/workcase_id)
|
||
category -- 分类:fault device emotion等
|
||
stat_date -- 统计日期(按天聚合)
|
||
```
|
||
|
||
---
|
||
|
||
## 核心服务功能
|
||
|
||
### Service层设计
|
||
|
||
#### 1. **ChatRoomService** - 聊天室服务
|
||
|
||
```java
|
||
// 创建聊天室(工单创建时调用)
|
||
ChatRoomVO createChatRoom(CreateChatRoomDTO dto);
|
||
|
||
// 关闭聊天室(工单完成时调用)
|
||
void closeChatRoom(String roomId, String closedBy);
|
||
|
||
// 获取聊天室详情
|
||
ChatRoomVO getChatRoomByWorkcaseId(String workcaseId);
|
||
|
||
// 同步AI对话记录到聊天室
|
||
void syncAiMessages(String roomId, String aiSessionId);
|
||
|
||
// 更新聊天室统计信息
|
||
void updateChatRoomStats(String roomId);
|
||
```
|
||
|
||
---
|
||
|
||
#### 2. **ChatMemberService** - 聊天室成员服务
|
||
|
||
```java
|
||
// 添加成员到聊天室
|
||
void addMember(String roomId, String userId, String userType);
|
||
|
||
// 移除成员
|
||
void removeMember(String roomId, String userId);
|
||
|
||
// 检查用户是否是聊天室成员(权限校验)
|
||
boolean isMemberOfRoom(String roomId, String userId);
|
||
|
||
// 获取聊天室成员列表
|
||
List<ChatMemberVO> getRoomMembers(String roomId);
|
||
|
||
// 更新成员未读数
|
||
void updateMemberUnreadCount(String roomId, String userId);
|
||
```
|
||
|
||
---
|
||
|
||
#### 3. **ChatMessageService** - 聊天消息服务
|
||
|
||
```java
|
||
// 发送消息
|
||
ChatMessageVO sendMessage(SendMessageDTO dto);
|
||
|
||
// 获取聊天历史
|
||
PageResult<ChatMessageVO> getChatHistory(String roomId, PageParam pageParam);
|
||
|
||
// 标记消息已读
|
||
void markMessagesAsRead(String roomId, String userId, List<String> messageIds);
|
||
|
||
// 撤回消息
|
||
void recallMessage(String messageId, String userId);
|
||
|
||
// 同步AI消息(从ai.tb_chat)
|
||
void syncAiMessages(String roomId, String aiSessionId);
|
||
```
|
||
|
||
---
|
||
|
||
#### 4. **VideoMeetingService** - 视频会议服务 ⭐核心
|
||
|
||
```java
|
||
// 创建会议
|
||
VideoMeetingVO createMeeting(CreateMeetingDTO dto);
|
||
|
||
// 验证加入会议权限
|
||
boolean validateMeetingAccess(String meetingId, String userId);
|
||
|
||
// 生成会议iframe URL
|
||
String generateMeetingIframeUrl(String meetingId, String userId);
|
||
|
||
// 开始会议
|
||
void startMeeting(String meetingId);
|
||
|
||
// 结束会议
|
||
void endMeeting(String meetingId);
|
||
|
||
// 获取会议详情
|
||
VideoMeetingVO getMeetingInfo(String meetingId);
|
||
|
||
// 记录参与者加入
|
||
void recordParticipantJoin(String meetingId, String userId);
|
||
|
||
// 记录参与者离开
|
||
void recordParticipantLeave(String meetingId, String userId);
|
||
```
|
||
|
||
---
|
||
|
||
#### 5. **JitsiTokenService** - Jitsi JWT Token服务
|
||
|
||
```java
|
||
// 生成JWT Token(用于身份验证)
|
||
String generateJwtToken(String roomName, String userId, String userName, boolean isModerator);
|
||
|
||
// 验证JWT Token
|
||
boolean validateJwtToken(String token);
|
||
|
||
// 生成iframe嵌入URL
|
||
String buildIframeUrl(String roomName, String jwtToken, JitsiConfig config);
|
||
```
|
||
|
||
---
|
||
|
||
## API接口设计
|
||
|
||
### 聊天室相关接口
|
||
|
||
#### 1. 创建聊天室
|
||
```
|
||
POST /api/workcase/chat-room/create
|
||
请求体:{
|
||
"workcaseId": "WC20231220001",
|
||
"guestId": "GUEST001",
|
||
"guestName": "张三",
|
||
"aiSessionId": "AI_SESSION_123"
|
||
}
|
||
响应:{
|
||
"code": 0,
|
||
"data": {
|
||
"roomId": "ROOM001",
|
||
"roomName": "工单#WC20231220001的客服支持",
|
||
"status": "active"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 2. 发送消息
|
||
```
|
||
POST /api/workcase/chat-room/send-message
|
||
请求体:{
|
||
"roomId": "ROOM001",
|
||
"senderId": "USER001",
|
||
"senderType": "agent",
|
||
"messageType": "text",
|
||
"content": "您好,我是客服小李,请问有什么可以帮到您?"
|
||
}
|
||
响应:{
|
||
"code": 0,
|
||
"data": {
|
||
"messageId": "MSG001",
|
||
"sendTime": "2023-12-20T10:00:00Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 3. 获取聊天历史
|
||
```
|
||
GET /api/workcase/chat-room/messages?roomId=ROOM001&page=1&size=50
|
||
响应:{
|
||
"code": 0,
|
||
"data": {
|
||
"total": 120,
|
||
"list": [
|
||
{
|
||
"messageId": "MSG001",
|
||
"senderId": "USER001",
|
||
"senderName": "客服小李",
|
||
"senderType": "agent",
|
||
"messageType": "text",
|
||
"content": "您好,我是客服小李",
|
||
"sendTime": "2023-12-20T10:00:00Z",
|
||
"status": "read"
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 视频会议相关接口 ⭐
|
||
|
||
#### 1. 创建视频会议
|
||
```
|
||
POST /api/workcase/meeting/create
|
||
请求头:Authorization: Bearer <token>
|
||
请求体:{
|
||
"roomId": "ROOM001",
|
||
"workcaseId": "WC20231220001",
|
||
"meetingName": "工单技术支持会议",
|
||
"maxParticipants": 10
|
||
}
|
||
响应:{
|
||
"code": 0,
|
||
"data": {
|
||
"meetingId": "MEET001",
|
||
"jitsiRoomName": "workcase_WC20231220001_1703059200",
|
||
"iframeUrl": "https://meet.jit.si/workcase_WC20231220001_1703059200?jwt=eyJhbGc...",
|
||
"status": "scheduled"
|
||
}
|
||
}
|
||
```
|
||
|
||
**业务逻辑**:
|
||
1. 验证请求用户是否是聊天室成员
|
||
2. 生成唯一的 `jitsi_room_name`
|
||
3. 生成JWT Token(包含用户身份和权限)
|
||
4. 构建iframe URL
|
||
5. 发送系统消息到聊天室通知会议创建
|
||
|
||
---
|
||
|
||
#### 2. 获取会议信息(用于加入会议)
|
||
```
|
||
GET /api/workcase/meeting/info/{meetingId}
|
||
请求头:Authorization: Bearer <token>
|
||
响应:{
|
||
"code": 0,
|
||
"data": {
|
||
"meetingId": "MEET001",
|
||
"meetingName": "工单技术支持会议",
|
||
"jitsiRoomName": "workcase_WC20231220001_1703059200",
|
||
"iframeUrl": "https://meet.jit.si/workcase_WC20231220001_1703059200?jwt=eyJhbGc...",
|
||
"status": "ongoing",
|
||
"participantCount": 2,
|
||
"maxParticipants": 10,
|
||
"canJoin": true
|
||
}
|
||
}
|
||
```
|
||
|
||
**业务逻辑**:
|
||
1. 验证请求用户是否是聊天室成员
|
||
2. 生成用户专属的JWT Token
|
||
3. 返回带Token的iframe URL
|
||
4. 记录参与者加入时间
|
||
|
||
---
|
||
|
||
#### 3. 开始会议
|
||
```
|
||
POST /api/workcase/meeting/start/{meetingId}
|
||
响应:{
|
||
"code": 0,
|
||
"message": "会议已开始"
|
||
}
|
||
```
|
||
|
||
#### 4. 结束会议
|
||
```
|
||
POST /api/workcase/meeting/end/{meetingId}
|
||
响应:{
|
||
"code": 0,
|
||
"data": {
|
||
"durationSeconds": 1800,
|
||
"participantCount": 3
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 前端集成方案
|
||
|
||
### 最简iframe嵌入实现
|
||
|
||
#### Vue 3 组件示例
|
||
|
||
```vue
|
||
<template>
|
||
<div class="video-meeting-container">
|
||
<!-- 会议控制按钮 -->
|
||
<div v-if="!meetingStarted" class="meeting-controls">
|
||
<Button @click="createMeeting" variant="gradient-blue">
|
||
<Video class="w-4 h-4 mr-2" />
|
||
发起视频会议
|
||
</Button>
|
||
</div>
|
||
|
||
<!-- Jitsi Meet iframe -->
|
||
<div v-if="meetingStarted" class="meeting-iframe-wrapper">
|
||
<iframe
|
||
:src="iframeUrl"
|
||
allow="camera; microphone; fullscreen; display-capture"
|
||
allowfullscreen
|
||
class="meeting-iframe"
|
||
></iframe>
|
||
|
||
<Button @click="endMeeting" variant="danger" class="end-meeting-btn">
|
||
结束会议
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue';
|
||
import { Video } from 'lucide-vue-next';
|
||
import { Button } from '@/components/ui';
|
||
import { createVideoMeeting, endVideoMeeting } from '@/api/workcase';
|
||
|
||
const props = defineProps<{
|
||
roomId: string;
|
||
workcaseId: string;
|
||
}>();
|
||
|
||
const meetingStarted = ref(false);
|
||
const iframeUrl = ref('');
|
||
const meetingId = ref('');
|
||
|
||
// 创建会议
|
||
const createMeeting = async () => {
|
||
try {
|
||
const res = await createVideoMeeting({
|
||
roomId: props.roomId,
|
||
workcaseId: props.workcaseId,
|
||
meetingName: `工单 ${props.workcaseId} 技术支持`,
|
||
maxParticipants: 10
|
||
});
|
||
|
||
if (res.code === 0) {
|
||
meetingId.value = res.data.meetingId;
|
||
iframeUrl.value = res.data.iframeUrl;
|
||
meetingStarted.value = true;
|
||
}
|
||
} catch (error) {
|
||
console.error('创建会议失败:', error);
|
||
}
|
||
};
|
||
|
||
// 结束会议
|
||
const endMeeting = async () => {
|
||
try {
|
||
await endVideoMeeting(meetingId.value);
|
||
meetingStarted.value = false;
|
||
iframeUrl.value = '';
|
||
} catch (error) {
|
||
console.error('结束会议失败:', error);
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.video-meeting-container {
|
||
width: 100%;
|
||
height: 100%;
|
||
position: relative;
|
||
}
|
||
|
||
.meeting-iframe-wrapper {
|
||
width: 100%;
|
||
height: 100%;
|
||
position: relative;
|
||
}
|
||
|
||
.meeting-iframe {
|
||
width: 100%;
|
||
height: 100%;
|
||
border: none;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.end-meeting-btn {
|
||
position: absolute;
|
||
bottom: 20px;
|
||
right: 20px;
|
||
z-index: 1000;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
---
|
||
|
||
### API请求封装
|
||
|
||
```typescript
|
||
// src/api/workcase/meeting.ts
|
||
import { http } from '@/utils/http';
|
||
|
||
export interface CreateMeetingParams {
|
||
roomId: string;
|
||
workcaseId: string;
|
||
meetingName: string;
|
||
maxParticipants?: number;
|
||
}
|
||
|
||
export interface VideoMeetingVO {
|
||
meetingId: string;
|
||
meetingName: string;
|
||
jitsiRoomName: string;
|
||
iframeUrl: string;
|
||
status: string;
|
||
participantCount: number;
|
||
maxParticipants: number;
|
||
}
|
||
|
||
// 创建视频会议
|
||
export const createVideoMeeting = (params: CreateMeetingParams) => {
|
||
return http.post<VideoMeetingVO>('/api/workcase/meeting/create', params);
|
||
};
|
||
|
||
// 获取会议信息
|
||
export const getMeetingInfo = (meetingId: string) => {
|
||
return http.get<VideoMeetingVO>(`/api/workcase/meeting/info/${meetingId}`);
|
||
};
|
||
|
||
// 结束会议
|
||
export const endVideoMeeting = (meetingId: string) => {
|
||
return http.post(`/api/workcase/meeting/end/${meetingId}`);
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
### 移动端适配
|
||
|
||
```vue
|
||
<template>
|
||
<div class="mobile-meeting" v-if="isMobile">
|
||
<!-- 移动端全屏展示 -->
|
||
<div class="mobile-iframe-container">
|
||
<iframe
|
||
:src="iframeUrl"
|
||
allow="camera; microphone; fullscreen"
|
||
allowfullscreen
|
||
class="mobile-iframe"
|
||
></iframe>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
.mobile-meeting {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 9999;
|
||
background: #000;
|
||
}
|
||
|
||
.mobile-iframe {
|
||
width: 100vw;
|
||
height: 100vh;
|
||
border: none;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
---
|
||
|
||
## 安全方案
|
||
|
||
### 1. **会议权限控制**
|
||
|
||
#### 后端验证逻辑
|
||
```java
|
||
public boolean validateMeetingAccess(String meetingId, String userId) {
|
||
// 1. 获取会议信息
|
||
VideoMeetingDO meeting = meetingMapper.selectById(meetingId);
|
||
if (meeting == null) {
|
||
throw new BusinessException("会议不存在");
|
||
}
|
||
|
||
// 2. 检查用户是否是聊天室成员
|
||
ChatRoomMemberDO member = memberMapper.selectByRoomAndUser(
|
||
meeting.getRoomId(), userId
|
||
);
|
||
|
||
if (member == null || !"active".equals(member.getStatus())) {
|
||
throw new BusinessException("您不是聊天室成员,无法加入会议");
|
||
}
|
||
|
||
return true;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 2. **JWT Token生成**
|
||
|
||
```java
|
||
public String generateJwtToken(String roomName, String userId, String userName, boolean isModerator) {
|
||
long now = System.currentTimeMillis();
|
||
long exp = now + (2 * 60 * 60 * 1000); // 2小时有效期
|
||
|
||
return Jwts.builder()
|
||
.setIssuer("urbanLifeline")
|
||
.setSubject(roomName)
|
||
.setAudience("jitsi")
|
||
.claim("context", Map.of(
|
||
"user", Map.of(
|
||
"id", userId,
|
||
"name", userName,
|
||
"moderator", isModerator
|
||
)
|
||
))
|
||
.claim("room", roomName)
|
||
.setIssuedAt(new Date(now))
|
||
.setExpiration(new Date(exp))
|
||
.signWith(SignatureAlgorithm.HS256, jitsiSecretKey)
|
||
.compact();
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3. **iframe URL构建**
|
||
|
||
```java
|
||
public String buildIframeUrl(String roomName, String jwtToken, JitsiConfig config) {
|
||
StringBuilder url = new StringBuilder();
|
||
url.append(jitsiServerUrl).append("/").append(roomName);
|
||
|
||
// JWT认证
|
||
url.append("?jwt=").append(jwtToken);
|
||
|
||
// Jitsi配置项
|
||
url.append("&config.startWithAudioMuted=").append(config.isStartWithAudioMuted());
|
||
url.append("&config.startWithVideoMuted=").append(config.isStartWithVideoMuted());
|
||
url.append("&config.enableWelcomePage=false");
|
||
url.append("&config.prejoinPageEnabled=false");
|
||
url.append("&config.disableDeepLinking=true");
|
||
|
||
// 界面配置
|
||
url.append("&interfaceConfig.SHOW_JITSI_WATERMARK=false");
|
||
url.append("&interfaceConfig.SHOW_WATERMARK_FOR_GUESTS=false");
|
||
url.append("&interfaceConfig.DISABLE_JOIN_LEAVE_NOTIFICATIONS=true");
|
||
|
||
return url.toString();
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4. **防止未授权访问**
|
||
|
||
```java
|
||
@PostMapping("/create")
|
||
@PreAuthorize("hasAuthority('workcase:meeting:create')")
|
||
public ResultDomain<VideoMeetingVO> createMeeting(
|
||
@RequestBody @Valid CreateMeetingDTO dto,
|
||
@RequestHeader("Authorization") String token
|
||
) {
|
||
// 1. 从token解析用户信息
|
||
String userId = JwtUtil.getUserIdFromToken(token);
|
||
|
||
// 2. 验证是否是聊天室成员
|
||
if (!chatMemberService.isMemberOfRoom(dto.getRoomId(), userId)) {
|
||
return ResultDomain.failure("您不是聊天室成员,无法创建会议");
|
||
}
|
||
|
||
// 3. 创建会议
|
||
VideoMeetingVO meeting = videoMeetingService.createMeeting(dto, userId);
|
||
|
||
return ResultDomain.success(meeting);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 技术要点总结
|
||
|
||
### ✅ 优势
|
||
|
||
1. **最简实现**:iframe嵌入,无需深度集成Jitsi服务端
|
||
2. **权限安全**:JWT Token + 聊天室成员校验
|
||
3. **无状态**:Jitsi Meet本身无状态,只记录必要的会议元数据
|
||
4. **可扩展**:可后续升级为自建Jitsi服务器
|
||
5. **成本低**:使用官方meet.jit.si服务器,免费
|
||
|
||
### ⚠️ 注意事项
|
||
|
||
1. **会议记录**:使用官方服务器无法录制,需自建服务器
|
||
2. **并发限制**:官方服务器有并发限制,建议后续自建
|
||
3. **网络要求**:需要良好的网络环境,可能需要科学上网
|
||
4. **数据隐私**:敏感场景建议自建Jitsi服务器
|
||
|
||
---
|
||
|
||
## 下一步工作
|
||
|
||
1. ✅ 数据表已创建(createTableWorkcase.sql)
|
||
2. ⏳ 实现Service层业务逻辑
|
||
3. ⏳ 实现Controller层API接口
|
||
4. ⏳ 实现前端IM聊天室组件
|
||
5. ⏳ 实现前端视频会议组件
|
||
6. ⏳ 测试和调优
|
||
|
||
---
|
||
|
||
## 附录:Jitsi Meet配置参考
|
||
|
||
### 推荐配置项
|
||
|
||
```javascript
|
||
const jitsiConfig = {
|
||
// 音视频设置
|
||
startWithAudioMuted: false,
|
||
startWithVideoMuted: false,
|
||
|
||
// 功能开关
|
||
enableWelcomePage: false,
|
||
prejoinPageEnabled: false,
|
||
disableDeepLinking: true,
|
||
|
||
// 录制和直播
|
||
recordingEnabled: false,
|
||
liveStreamingEnabled: false,
|
||
|
||
// 聊天和屏幕共享
|
||
enableChat: true,
|
||
enableScreenSharing: true,
|
||
|
||
// 界面定制
|
||
hideConferenceSubject: false,
|
||
hideConferenceTimer: false,
|
||
|
||
// 安全设置
|
||
enableE2EE: false, // 端到端加密
|
||
requireDisplayName: true
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
**文档版本**:v1.0
|
||
**最后更新**:2023-12-20
|
||
**作者**:Cascade AI Assistant
|