Files
urbanLifeline/docs/功能实现-会议通知消息.md
2025-12-26 18:55:54 +08:00

311 lines
9.7 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.

# 功能实现视频会议预约模式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 | 消息IDUUID |
| 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