# 工单服务 + 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
