9.7 KiB
9.7 KiB
功能实现:视频会议预约模式(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/ - 修改内容:
- 新增导入:
TbChatRoomMessageDTO、ChatRoomService、JSONObject - 注入依赖:
ChatRoomService - 修改
createMeeting()方法:插入数据库成功后调用sendMeetingNotification() - 新增私有方法:
sendMeetingNotification()
- 新增导入:
详细实现
1. 依赖注入
@Autowired
private ChatRoomService chatRoomService;
2. 调用时机
在 createMeeting() 方法中,会议记录插入数据库成功后:
// 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)
/**
* 发送会议通知消息到聊天室
* @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 详细信息
{
"meetingId": "会议ID",
"meetingName": "会议名称",
"jitsiRoomName": "Jitsi房间名",
"iframeUrl": "会议URL",
"maxParticipants": 10,
"creatorName": "创建者名称",
"workcaseId": "工单ID"
}
前端渲染建议
Vue Web 端
在 ChatRoom.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 中添加会议消息卡片:
<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. 会议创建测试
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. 异常情况测试
- ✅ 消息发送失败不影响会议创建
- ✅ 会议创建失败不会发送消息
注意事项
-
消息类型:messageType 为 "meet",而非 "meeting"(根据用户需求)
-
权限控制:
- 只有聊天室成员才能创建会议
- 只有聊天室成员才能加入会议
-
事务处理:
- 会议创建在事务中(@Transactional)
- 消息发送在独立的 try-catch,失败不回滚会议创建
-
前端适配:
- Web端和小程序端需分别实现会议卡片渲染
- 建议使用统一的样式和交互
-
扩展性:
- contentExtra 可根据需要添加更多字段
- 建议前端做好字段缺失的容错处理
相关文档
实现人员:Claude Code 审核状态:待审核 版本:v1.0