Files
urbanLifeline/urbanLifelineServ/workcase/工单+Jitsi Meet技术方案.md
2025-12-22 19:16:53 +08:00

855 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 工单服务 + 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_id -- 创建者ID
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 -- 来源IDroom_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