jisti-meet服务开启
This commit is contained in:
310
docs/功能实现-会议通知消息.md
Normal file
310
docs/功能实现-会议通知消息.md
Normal file
@@ -0,0 +1,310 @@
|
||||
# 功能实现:视频会议预约模式(Reservation Model)
|
||||
|
||||
## 实现日期
|
||||
2025-12-26
|
||||
|
||||
## 功能概述
|
||||
实现视频会议"预约+按需创建"架构:用户创建会议预约时不立即创建Jitsi会议室,仅在首个用户在允许入会时间窗口内加入时,通过Redis双检锁机制创建Jitsi会议室。
|
||||
|
||||
## 架构模型
|
||||
|
||||
### 预约模式 (Reservation Model)
|
||||
```
|
||||
用户创建会议 → 保存预约信息(scheduled)→ 发送会议通知消息(meetingId)
|
||||
↓
|
||||
首个用户加入 → 时间窗口校验 → Redis分布式锁 → 创建Jitsi会议室 → 更新状态(ongoing)
|
||||
↓
|
||||
后续用户加入 → 直接获取已创建的会议室URL
|
||||
```
|
||||
|
||||
### 时间窗口规则
|
||||
- **提前入会时间**: `start_time - advance` 分钟
|
||||
- **允许入会窗口**: `[提前入会时间, end_time]`
|
||||
- **默认advance**: 5分钟
|
||||
|
||||
## 实现文件
|
||||
|
||||
### 后端修改
|
||||
|
||||
**VideoMeetingServiceImpl.java**
|
||||
- 位置:`urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/`
|
||||
- 修改内容:
|
||||
1. 新增导入:`TbChatRoomMessageDTO`、`ChatRoomService`、`JSONObject`
|
||||
2. 注入依赖:`ChatRoomService`
|
||||
3. 修改 `createMeeting()` 方法:插入数据库成功后调用 `sendMeetingNotification()`
|
||||
4. 新增私有方法:`sendMeetingNotification()`
|
||||
|
||||
## 详细实现
|
||||
|
||||
### 1. 依赖注入
|
||||
|
||||
```java
|
||||
@Autowired
|
||||
private ChatRoomService chatRoomService;
|
||||
```
|
||||
|
||||
### 2. 调用时机
|
||||
|
||||
在 `createMeeting()` 方法中,会议记录插入数据库成功后:
|
||||
|
||||
```java
|
||||
// 8. 插入数据库
|
||||
int rows = videoMeetingMapper.insertVideoMeeting(meetingDTO);
|
||||
if (rows > 0) {
|
||||
logger.info("视频会议创建成功: meetingId={}, jitsiRoomName={}",
|
||||
meetingId, jitsiRoomName);
|
||||
|
||||
// 9. 发送会议通知消息到聊天室
|
||||
sendMeetingNotification(meetingDTO, userName);
|
||||
|
||||
// 10. 返回VO
|
||||
VideoMeetingVO meetingVO = new VideoMeetingVO();
|
||||
BeanUtils.copyProperties(meetingDTO, meetingVO);
|
||||
return ResultDomain.success("创建会议成功", meetingVO);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 消息发送实现
|
||||
|
||||
**sendMeetingNotification() 方法 (VideoMeetingServiceImpl.java:396-442)**
|
||||
|
||||
```java
|
||||
/**
|
||||
* 发送会议通知消息到聊天室
|
||||
* @param meetingDTO 会议信息
|
||||
* @param creatorName 创建者名称
|
||||
*/
|
||||
private void sendMeetingNotification(TbVideoMeetingDTO meetingDTO, String creatorName) {
|
||||
try {
|
||||
logger.info("发送会议通知消息: roomId={}, meetingId={}",
|
||||
meetingDTO.getRoomId(), meetingDTO.getMeetingId());
|
||||
|
||||
// 构建消息内容
|
||||
TbChatRoomMessageDTO message = new TbChatRoomMessageDTO();
|
||||
message.setMessageId(IdUtil.generateUUID());
|
||||
message.setRoomId(meetingDTO.getRoomId());
|
||||
message.setSenderId(meetingDTO.getCreator());
|
||||
message.setSenderType(meetingDTO.getCreatorType());
|
||||
message.setSenderName(creatorName);
|
||||
message.setMessageType("meet"); // 会议类型消息
|
||||
message.setContent(meetingDTO.getIframeUrl()); // 会议URL作为内容
|
||||
message.setStatus("sent");
|
||||
message.setReadCount(0);
|
||||
message.setSendTime(new Date());
|
||||
|
||||
// 构建扩展信息(会议详情)
|
||||
JSONObject contentExtra = new JSONObject();
|
||||
contentExtra.put("meetingId", meetingDTO.getMeetingId());
|
||||
contentExtra.put("meetingName", meetingDTO.getMeetingName());
|
||||
contentExtra.put("jitsiRoomName", meetingDTO.getJitsiRoomName());
|
||||
contentExtra.put("iframeUrl", meetingDTO.getIframeUrl());
|
||||
contentExtra.put("maxParticipants", meetingDTO.getMaxParticipants());
|
||||
contentExtra.put("creatorName", creatorName);
|
||||
contentExtra.put("workcaseId", meetingDTO.getWorkcaseId());
|
||||
message.setContentExtra(contentExtra);
|
||||
|
||||
// 发送消息
|
||||
ResultDomain<TbChatRoomMessageDTO> sendResult = chatRoomService.sendMessage(message);
|
||||
if (sendResult.getSuccess()) {
|
||||
logger.info("会议通知消息发送成功: messageId={}", message.getMessageId());
|
||||
} else {
|
||||
logger.warn("会议通知消息发送失败: {}", sendResult.getMessage());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 消息发送失败不影响会议创建
|
||||
logger.error("发送会议通知消息异常: roomId={}, error={}",
|
||||
meetingDTO.getRoomId(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 消息数据结构
|
||||
|
||||
### 消息字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|-----|------|------|
|
||||
| messageId | String | 消息ID(UUID) |
|
||||
| roomId | String | 聊天室ID |
|
||||
| senderId | String | 发送者ID(会议创建者) |
|
||||
| senderType | String | 发送者类型(guest/agent) |
|
||||
| senderName | String | 发送者名称 |
|
||||
| **messageType** | **String** | **"meet"(会议消息类型)** |
|
||||
| **content** | **String** | **会议iframe URL** |
|
||||
| contentExtra | JSONObject | 会议详细信息(见下表) |
|
||||
| status | String | "sent" |
|
||||
| readCount | Integer | 0 |
|
||||
| sendTime | Date | 发送时间 |
|
||||
|
||||
### contentExtra 详细信息
|
||||
|
||||
```json
|
||||
{
|
||||
"meetingId": "会议ID",
|
||||
"meetingName": "会议名称",
|
||||
"jitsiRoomName": "Jitsi房间名",
|
||||
"iframeUrl": "会议URL",
|
||||
"maxParticipants": 10,
|
||||
"creatorName": "创建者名称",
|
||||
"workcaseId": "工单ID"
|
||||
}
|
||||
```
|
||||
|
||||
## 前端渲染建议
|
||||
|
||||
### Vue Web 端
|
||||
|
||||
在 `ChatRoom.vue` 中添加会议消息卡片渲染:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div v-if="message.messageType === 'meet'" class="meeting-card">
|
||||
<div class="meeting-card-header">
|
||||
<Video :size="20" />
|
||||
<span>{{ message.contentExtra.meetingName }}</span>
|
||||
</div>
|
||||
<div class="meeting-card-body">
|
||||
<p>发起人:{{ message.contentExtra.creatorName }}</p>
|
||||
<p>最多参与人数:{{ message.contentExtra.maxParticipants }}</p>
|
||||
</div>
|
||||
<div class="meeting-card-footer">
|
||||
<button @click="joinMeeting(message.contentExtra.meetingId)">
|
||||
加入会议
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Video } from 'lucide-vue-next'
|
||||
|
||||
const joinMeeting = async (meetingId) => {
|
||||
// 调用加入会议API
|
||||
const res = await workcaseChatAPI.joinVideoMeeting(meetingId)
|
||||
if (res.code === 0) {
|
||||
// 打开会议iframe
|
||||
meetingUrl.value = res.data.iframeUrl
|
||||
showMeeting.value = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### UniApp 小程序端
|
||||
|
||||
在 `chatRoom.uvue` 中添加会议消息卡片:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view v-if="message.messageType === 'meet'" class="meeting-card">
|
||||
<view class="meeting-card-header">
|
||||
<text class="icon-video">📹</text>
|
||||
<text>{{ message.contentExtra.meetingName }}</text>
|
||||
</view>
|
||||
<view class="meeting-card-body">
|
||||
<text>发起人:{{ message.contentExtra.creatorName }}</text>
|
||||
<text>最多 {{ message.contentExtra.maxParticipants }} 人</text>
|
||||
</view>
|
||||
<button @tap="joinMeeting(message.contentExtra.meetingId)" class="join-btn">
|
||||
加入会议
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
function joinMeeting(meetingId: string) {
|
||||
// 调用加入会议API
|
||||
workcaseChatAPI.joinVideoMeeting(meetingId).then(res => {
|
||||
if (res.success && res.data) {
|
||||
// 跳转到会议页面
|
||||
uni.navigateTo({
|
||||
url: `/pages/meeting/Meeting?meetingUrl=${encodeURIComponent(res.data.iframeUrl)}&meetingId=${meetingId}`
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 设计考虑
|
||||
|
||||
### 1. 异步发送
|
||||
消息发送在独立的 try-catch 块中,失败不影响会议创建流程
|
||||
|
||||
### 2. 完整信息
|
||||
contentExtra 包含会议所有关键信息,前端可灵活使用
|
||||
|
||||
### 3. 类型明确
|
||||
messageType 使用 "meet" 标识会议消息,方便前端过滤和渲染
|
||||
|
||||
### 4. URL 即内容
|
||||
content 字段直接存储会议URL,方便快速访问
|
||||
|
||||
### 5. 日志追踪
|
||||
完整的日志记录,便于问题排查
|
||||
|
||||
## 测试要点
|
||||
|
||||
### 1. 会议创建测试
|
||||
```bash
|
||||
POST /urban-lifeline/workcase/chat/meeting/create
|
||||
{
|
||||
"roomId": "test-room-123",
|
||||
"workcaseId": "WC001",
|
||||
"meetingName": "技术支持会议",
|
||||
"maxParticipants": 10
|
||||
}
|
||||
```
|
||||
|
||||
**预期结果**:
|
||||
- ✅ 返回会议创建成功
|
||||
- ✅ 数据库 tb_video_meeting 表插入会议记录
|
||||
- ✅ 数据库 tb_chat_room_message 表插入类型为 "meet" 的消息
|
||||
- ✅ 消息的 content 字段包含会议URL
|
||||
- ✅ 消息的 contentExtra 包含会议详细信息
|
||||
|
||||
### 2. 前端卡片渲染测试
|
||||
- ✅ 聊天消息列表中显示会议卡片
|
||||
- ✅ 卡片展示会议名称、发起人、参与人数等信息
|
||||
- ✅ 点击"加入会议"按钮能正确跳转
|
||||
|
||||
### 3. 多人加入测试
|
||||
- ✅ 创建者加入会议(主持人权限)
|
||||
- ✅ 其他成员通过卡片加入会议(普通权限)
|
||||
- ✅ 非聊天室成员无法加入
|
||||
|
||||
### 4. 异常情况测试
|
||||
- ✅ 消息发送失败不影响会议创建
|
||||
- ✅ 会议创建失败不会发送消息
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **消息类型**:messageType 为 "meet",而非 "meeting"(根据用户需求)
|
||||
|
||||
2. **权限控制**:
|
||||
- 只有聊天室成员才能创建会议
|
||||
- 只有聊天室成员才能加入会议
|
||||
|
||||
3. **事务处理**:
|
||||
- 会议创建在事务中(@Transactional)
|
||||
- 消息发送在独立的 try-catch,失败不回滚会议创建
|
||||
|
||||
4. **前端适配**:
|
||||
- Web端和小程序端需分别实现会议卡片渲染
|
||||
- 建议使用统一的样式和交互
|
||||
|
||||
5. **扩展性**:
|
||||
- contentExtra 可根据需要添加更多字段
|
||||
- 建议前端做好字段缺失的容错处理
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [Jitsi Meet 视频会议集成总结](./项目总结-Jitsi-Meet视频会议功能.md)
|
||||
- [Docker 部署指南](./Jitsi-Meet-Docker部署指南.md)
|
||||
- [代码重构 - 视频会议API规范化](./代码重构-视频会议API规范化.md)
|
||||
|
||||
---
|
||||
|
||||
**实现人员**:Claude Code
|
||||
**审核状态**:待审核
|
||||
**版本**:v1.0
|
||||
Reference in New Issue
Block a user