# iMeeting Web Client - 技术设计文档 ## 一、项目概述 ### 1.1 项目定位 iMeeting Web Client 是一个基于 React 的纯前端轻量级会议录音客户端,专注于快速录音和会议管理功能。 ### 1.2 核心特性 - 🎤 **一键录音**:点击即录,实时流式上传 - 📚 **会议列表**:快速查看历史会议,生成分享二维码 - 📱 **响应式设计**:支持桌面端、平板、移动端 - 🚀 **纯前端实现**:无需额外服务器,直接调用现有API --- ## 二、界面设计分析 ### 2.1 主界面布局(参考 design.png) 采用 2x2 网格卡片布局: ``` ┌─────────────┬─────────────┐ │ 麦克风 │ AI │ │ (橙色) │ (灰色) │ │ [激活] │ [预留] │ ├─────────────┼─────────────┤ │ 大脑 │ 知识库 │ │ (灰色) │ (橙色) │ │ [预留] │ [激活] │ └─────────────┴─────────────┘ ``` **功能状态:** - ✅ **麦克风(左上,橙色)**:一键录音功能 - **已激活** - ⏸ **AI(右上,灰色)**:AI分析功能 - 预留 - ⏸ **大脑(左下,灰色)**:智能功能 - 预留 - ✅ **知识库(右下,橙色)**:会议列表 - **已激活** ### 2.2 页面结构 ``` / - 主页(4个功能卡片) ├── /record - 录音页面(点击麦克风进入) └── /meetings - 会议列表页(点击知识库进入) └── /:id - 会议详情(简介+二维码) ``` ### 2.3 响应式断点 ```css /* 移动端 */ @media (max-width: 768px) { /* 单列布局,卡片堆叠 */ } /* 平板 */ @media (min-width: 768px) and (max-width: 1024px) { /* 2x2布局,卡片适中 */ } /* 桌面 */ @media (min-width: 1024px) { /* 2x2布局,卡片标准 */ } ``` --- ## 三、技术架构 ### 3.1 技术栈选型 #### 核心框架 - **React 18**:UI框架 - **React Router v6**:路由管理 - **Vite**:构建工具(快速、轻量) #### 样式方案 - **CSS Modules**:模块化样式 - **响应式布局**:CSS Grid + Flexbox #### 状态管理 - **React Context API**:全局状态(用户、录音状态) - **useState/useReducer**:局部状态 #### 核心库 ```json { "react": "^18.2.0", "react-router-dom": "^6.20.0", "axios": "^1.6.0", "qrcode.react": "^3.1.0", "lucide-react": "^0.292.0" } ``` ### 3.2 项目结构 ``` client/ ├── public/ │ └── index.html ├── src/ │ ├── components/ # 公共组件 │ │ ├── Layout/ │ │ │ └── MainLayout.jsx │ │ ├── FeatureCard/ # 功能卡片 │ │ │ ├── FeatureCard.jsx │ │ │ └── FeatureCard.module.css │ │ ├── RecordButton/ # 录音按钮 │ │ └── QRCodeModal/ # 二维码弹窗 │ ├── pages/ │ │ ├── Home/ # 主页(2x2卡片) │ │ │ ├── Home.jsx │ │ │ └── Home.module.css │ │ ├── Record/ # 录音页 │ │ │ ├── Record.jsx │ │ │ └── Record.module.css │ │ └── Meetings/ # 会议列表 │ │ ├── MeetingList.jsx │ │ ├── MeetingDetail.jsx │ │ └── Meetings.module.css │ ├── hooks/ # 自定义Hooks │ │ ├── useAudioRecorder.js │ │ ├── useStreamUpload.js │ │ └── useAuth.js │ ├── services/ # API服务 │ │ ├── api.js # Axios封装 │ │ ├── auth.js │ │ ├── meeting.js │ │ └── upload.js │ ├── utils/ │ │ ├── audio.js # 音频工具 │ │ └── format.js │ ├── styles/ │ │ └── global.css │ ├── App.jsx │ └── main.jsx ├── package.json ├── vite.config.js └── README.md ``` --- ## 四、核心功能实现 ### 4.1 一键录音功能 #### 4.1.1 录音流程 ``` 用户点击 → 请求麦克风权限 → 开始录音 → 分片收集 → 实时上传 → 停止录音 → 创建会议记录 → 跳转到会议详情 ``` #### 4.1.2 技术实现 **(1)使用 MediaRecorder API** ```javascript // hooks/useAudioRecorder.js import { useState, useRef, useCallback } from 'react'; export const useAudioRecorder = () => { const [isRecording, setIsRecording] = useState(false); const [duration, setDuration] = useState(0); const mediaRecorderRef = useRef(null); const streamRef = useRef(null); const chunksRef = useRef([]); const startRecording = useCallback(async () => { try { // 请求麦克风权限 const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, // 回声消除 noiseSuppression: true, // 降噪 sampleRate: 44100 // 采样率 } }); streamRef.current = stream; // 选择合适的MIME类型 const mimeType = getSupportedMimeType(); const mediaRecorder = new MediaRecorder(stream, { mimeType, audioBitsPerSecond: 128000 }); // 每秒收集一次数据(用于流式上传) mediaRecorder.start(1000); mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { chunksRef.current.push(event.data); // 触发流式上传 onChunkAvailable(event.data); } }; mediaRecorderRef.current = mediaRecorder; setIsRecording(true); } catch (error) { handleRecordingError(error); } }, []); const stopRecording = useCallback(() => { if (mediaRecorderRef.current) { mediaRecorderRef.current.stop(); streamRef.current?.getTracks().forEach(track => track.stop()); setIsRecording(false); } }, []); return { isRecording, duration, startRecording, stopRecording }; }; // 获取浏览器支持的MIME类型 const getSupportedMimeType = () => { const types = [ 'audio/webm;codecs=opus', 'audio/webm', 'audio/ogg;codecs=opus', 'audio/mp4' ]; return types.find(type => MediaRecorder.isTypeSupported(type)) || ''; }; ``` **(2)流式上传实现** ##### 方案一:分片上传(推荐) **优点:** - ✅ HTTP协议,简单稳定 - ✅ 可断点续传 - ✅ 兼容现有后端 **实现:** ```javascript // hooks/useStreamUpload.js import { useState, useCallback } from 'react'; import { uploadAudioChunk, completeUpload } from '../services/upload'; export const useStreamUpload = () => { const [uploadProgress, setUploadProgress] = useState(0); const [sessionId, setSessionId] = useState(null); const chunkIndexRef = useRef(0); const startSession = useCallback(async (meetingId) => { // 初始化上传会话 const session = await initUploadSession(meetingId); setSessionId(session.session_id); return session; }, []); const uploadChunk = useCallback(async (chunk) => { if (!sessionId) return; const formData = new FormData(); formData.append('chunk', chunk); formData.append('chunk_index', chunkIndexRef.current); formData.append('session_id', sessionId); try { await uploadAudioChunk(formData); chunkIndexRef.current++; setUploadProgress(prev => prev + 1); } catch (error) { console.error('Upload chunk failed:', error); // 可以实现重试逻辑 } }, [sessionId]); const finishUpload = useCallback(async (meetingId) => { if (!sessionId) return; return await completeUpload({ session_id: sessionId, meeting_id: meetingId, total_chunks: chunkIndexRef.current }); }, [sessionId]); return { startSession, uploadChunk, finishUpload, uploadProgress }; }; ``` **后端需要新增的接口:** ```javascript // 需要后端提供这些接口 POST /api/audio/upload-init // 初始化上传会话 POST /api/audio/upload-chunk // 上传分片 POST /api/audio/upload-complete // 完成上传,合并分片 ``` ##### 方案二:简化方案(先录完再传) 如果后端暂时不支持分片,可以先实现简化版本: ```javascript const stopAndUpload = useCallback(async () => { mediaRecorder.stop(); // 等待所有数据收集完毕 mediaRecorder.onstop = async () => { const audioBlob = new Blob(chunksRef.current, { type: mimeType }); // 一次性上传完整音频 const formData = new FormData(); formData.append('audio_file', audioBlob, 'recording.webm'); formData.append('meeting_id', meetingId); await uploadAudio(formData); }; }, []); ``` **推荐:先实现方案二(简单快速),后续优化为方案一** #### 4.1.3 录音界面设计 ``` ┌─────────────────────────────────┐ │ 正在录音... │ │ │ │ ⏺ [动画波形] │ │ │ │ 00:03:24 │ │ │ │ ━━━━━━━━━━━━━━━━━━━━ │ │ │ │ [暂停] [停止并保存] │ └─────────────────────────────────┘ ``` **关键元素:** - 录音状态指示(红点闪烁) - 实时时长显示 - 简单的音量可视化(可选) - 暂停/继续按钮 - 停止按钮 **错误处理:** ```javascript const handleRecordingError = (error) => { switch(error.name) { case 'NotAllowedError': showError('需要麦克风权限,请在浏览器设置中允许'); break; case 'NotFoundError': showError('未检测到麦克风设备'); break; case 'NotReadableError': showError('麦克风被其他应用占用'); break; default: showError('录音失败,请重试'); } }; ``` --- ### 4.2 会议列表功能 #### 4.2.1 列表页实现 ```javascript // pages/Meetings/MeetingList.jsx import { useState, useEffect } from 'react'; import { getMeetings } from '../../services/meeting'; import QRCodeModal from '../../components/QRCodeModal'; const MeetingList = () => { const [meetings, setMeetings] = useState([]); const [selectedMeeting, setSelectedMeeting] = useState(null); const [showQR, setShowQR] = useState(false); useEffect(() => { loadMeetings(); }, []); const loadMeetings = async () => { const response = await getMeetings({ user_id: currentUser.user_id, page: 1, page_size: 50 }); setMeetings(response.data.meetings); }; const handleShowQR = (meeting) => { setSelectedMeeting(meeting); setShowQR(true); }; return (
{meetings.map(meeting => (

{meeting.title}

{formatDateTime(meeting.meeting_time)}

{meeting.summary?.substring(0, 100)}...

))} {showQR && ( setShowQR(false)} /> )}
); }; ``` #### 4.2.2 二维码组件 ```javascript // components/QRCodeModal/QRCodeModal.jsx import { QRCodeSVG } from 'qrcode.react'; const QRCodeModal = ({ meeting, onClose }) => { const qrUrl = `${window.location.origin}/meetings/preview/${meeting.meeting_id}`; return (
e.stopPropagation()}>

{meeting.title}

扫描二维码查看会议详情

会议时间:{formatDateTime(meeting.meeting_time)}

创建人:{meeting.creator_username}

参会人数:{meeting.attendees?.length || 0}

); }; ``` **二维码跳转:** - URL: `/meetings/preview/{meetingId}` - 这个页面已经在后端实现(无需登录即可查看) --- ## 五、API接口清单 ### 5.1 复用现有接口 #### 用户认证 ```javascript POST /api/auth/login // 登录 GET /api/auth/me // 获取当前用户信息 ``` #### 会议管理 ```javascript POST /api/meetings // 创建会议 GET /api/meetings // 获取会议列表 ?user_id=xxx&page=1&page_size=20 GET /api/meetings/{id} // 获取会议详情 DELETE /api/meetings/{id} // 删除会议 POST /api/meetings/upload-audio // 上传音频(完整文件) - meeting_id - audio_file - auto_summarize (false) ``` ### 5.2 需要新增的接口(分片上传,可选) ```javascript POST /api/audio/upload-init // 初始化分片上传 Request: { meeting_id: number, estimated_duration: number, // 预计时长(秒) mime_type: string } Response: { session_id: string } POST /api/audio/upload-chunk // 上传音频分片 Request: FormData { session_id: string, chunk_index: number, chunk: Blob } Response: { success: boolean } POST /api/audio/upload-complete // 完成上传 Request: { session_id: string, meeting_id: number, total_chunks: number } Response: { file_path: string, task_id: string // 转录任务ID } ``` --- ## 六、关键技术实现 ### 6.1 音频格式兼容性 | 浏览器 | 推荐格式 | 备选格式 | |--------|---------|---------| | Chrome | webm(opus) | webm | | Safari | mp4(aac) | - | | Firefox | webm(opus) | ogg | | Edge | webm(opus) | webm | **检测代码:** ```javascript const getSupportedMimeType = () => { const types = [ 'audio/webm;codecs=opus', // Chrome, Firefox, Edge 'audio/mp4', // Safari 'audio/webm', // 通用webm 'audio/ogg;codecs=opus' // Firefox fallback ]; for (const type of types) { if (MediaRecorder.isTypeSupported(type)) { console.log('Using MIME type:', type); return type; } } throw new Error('No supported audio MIME type found'); }; ``` ### 6.2 权限管理 ```javascript // utils/permissions.js export const requestMicrophonePermission = async () => { try { // 尝试获取麦克风流 const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); // 立即停止流(仅用于权限检查) stream.getTracks().forEach(track => track.stop()); return { granted: true }; } catch (error) { if (error.name === 'NotAllowedError') { return { granted: false, error: '用户拒绝了麦克风权限' }; } else if (error.name === 'NotFoundError') { return { granted: false, error: '未找到麦克风设备' }; } else { return { granted: false, error: '获取麦克风权限失败' }; } } }; ``` ### 6.3 状态管理(Context) ```javascript // context/AppContext.jsx import { createContext, useContext, useState } from 'react'; const AppContext = createContext(); export const AppProvider = ({ children }) => { const [user, setUser] = useState(null); const [isRecording, setIsRecording] = useState(false); const value = { user, setUser, isRecording, setIsRecording }; return ( {children} ); }; export const useApp = () => { const context = useContext(AppContext); if (!context) { throw new Error('useApp must be used within AppProvider'); } return context; }; ``` ### 6.4 响应式布局实现 ```css /* pages/Home/Home.module.css */ .home { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; } .grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; max-width: 600px; width: 100%; } /* 移动端 */ @media (max-width: 768px) { .grid { grid-template-columns: 1fr; max-width: 400px; } } /* 功能卡片 */ .card { aspect-ratio: 1; border-radius: 20px; display: flex; flex-direction: column; align-items: center; justify-content: center; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; } .card:hover { transform: translateY(-5px); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); } .card.active { background: linear-gradient(135deg, #FF8C42 0%, #FF6B35 100%); color: white; } .card.inactive { background: #E0E0E0; color: #999; cursor: not-allowed; } .card.inactive:hover { transform: none; } ``` --- ## 七、用户体验优化 ### 7.1 录音反馈 ```javascript // 视觉反馈:录音按钮呼吸动画 @keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } } .recording { animation: pulse 1.5s ease-in-out infinite; } // 红点闪烁 @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } } .recording-indicator { animation: blink 1s infinite; } ``` ### 7.2 加载状态 ```javascript const [loading, setLoading] = useState(false); // 上传时显示进度 {loading && (

正在上传录音...

)} ``` ### 7.3 错误提示 ```javascript // components/Toast.jsx const Toast = ({ message, type = 'info' }) => (
{message}
); // 使用 showToast('录音已保存', 'success'); showToast('上传失败,请重试', 'error'); ``` --- ## 八、安全性考虑 ### 8.1 Token管理 ```javascript // services/api.js import axios from 'axios'; const api = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL }); // 请求拦截器:添加Token api.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // 响应拦截器:处理401 api.interceptors.response.use( response => response, error => { if (error.response?.status === 401) { localStorage.removeItem('token'); window.location.href = '/login'; } return Promise.reject(error); } ); export default api; ``` ### 8.2 数据加密 - HTTPS传输(生产环境必须) - 录音数据不在本地长期存储 - Token使用JWT,设置合理过期时间 --- ## 九、部署方案 ### 9.1 构建配置 ```javascript // vite.config.js import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], server: { port: 3002, proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true } } }, build: { outDir: 'dist', sourcemap: false, minify: 'terser' } }); ``` ### 9.2 环境变量 ```bash # .env.development VITE_API_BASE_URL=http://localhost:8000 # .env.production VITE_API_BASE_URL=https://api.yourdomain.com ``` ### 9.3 部署方式 **方案1:独立部署(推荐)** ```bash npm run build # 将 dist/ 部署到 Nginx/CDN ``` **Nginx配置:** ```nginx server { listen 80; server_name client.yourdomain.com; root /var/www/client/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api { proxy_pass http://backend:8000; } } ``` **方案2:集成到现有frontend** ``` frontend/ ├── src/ # 原有管理端 └── client/ # 新客户端 └── build后集成 ``` --- ## 十、后端接口规划 ### 10.1 用户认证接口 #### 1. 用户登录 ```javascript POST /api/auth/login Request: { "username": "string", "password": "string" } Response: { "code": "200", "message": "登录成功", "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "user": { "user_id": 1, "username": "testuser", "caption": "测试用户" } } } Error Response: { "code": "401", "message": "用户名或密码错误", "data": null } ``` #### 2. 获取当前用户信息 ```javascript GET /api/auth/me Headers: { "Authorization": "Bearer {token}" } Response: { "code": "200", "message": "获取成功", "data": { "user_id": 1, "username": "testuser", "caption": "测试用户", "created_at": "2025-01-20T10:00:00" } } ``` --- ### 10.2 会议管理接口 #### 1. 创建会议 ```javascript POST /api/meetings Headers: { "Authorization": "Bearer {token}" } Request: { "title": "临时会议", // 可选,默认"临时会议-时间戳" "meeting_time": "2025-01-25T14:30:00", // 可选,默认当前时间 "attendee_ids": [], // 可选,参会人员ID数组 "tags": "" // 可选,标签 } Response: { "code": "200", "message": "会议创建成功", "data": { "meeting_id": 123, "title": "临时会议-20250125143000", "meeting_time": "2025-01-25T14:30:00", "user_id": 1, "created_at": "2025-01-25T14:30:00" } } 说明: - Web客户端录音前先调用此接口创建会议记录 - 录音完成后,使用meeting_id上传音频 ``` #### 2. 获取会议列表 ```javascript GET /api/meetings Headers: { "Authorization": "Bearer {token}" } Query Parameters: { "user_id": 1, // 必填:当前用户ID "page": 1, // 可选:页码,默认1 "page_size": 20, // 可选:每页数量,默认20 "filter_type": "created" // 可选:筛选类型 (created/attended/all),默认created } Response: { "code": "200", "message": "获取会议列表成功", "data": { "meetings": [ { "meeting_id": 123, "title": "产品需求讨论会", "meeting_time": "2025-01-25T14:30:00", "summary": "会议讨论了新版本的产品需求...", "created_at": "2025-01-25T14:25:00", "audio_file_path": "/uploads/audio/123/xxx.webm", "creator_id": 1, "creator_username": "张三", "attendees": [ { "user_id": 2, "caption": "李四" }, { "user_id": 3, "caption": "王五" } ], "tags": [ { "id": 1, "name": "产品", "color": "#FF5733" } ] } ], "total": 45, "page": 1, "page_size": 20, "total_pages": 3, "has_more": true } } ``` --- ### 10.3 流式音频上传接口(新增) #### 1. 初始化上传会话 ```javascript POST /api/audio/stream/init Headers: { "Authorization": "Bearer {token}", "Content-Type": "application/json" } Request: { "meeting_id": 123, "mime_type": "audio/webm;codecs=opus", // 前端录音格式 "estimated_duration": 300 // 预计时长(秒),可选 } Response: { "code": "200", "message": "上传会话初始化成功", "data": { "session_id": "sess_1737804123456_abc123", "chunk_size": 1024000, // 建议分片大小(字节),1MB "max_chunks": 1000 // 最大分片数量 } } 说明: - 后端创建临时目录存储分片 - session_id用于后续上传分片 - 返回建议的分片大小 ``` #### 2. 上传音频分片 ```javascript POST /api/audio/stream/chunk Headers: { "Authorization": "Bearer {token}", "Content-Type": "multipart/form-data" } Request: FormData { "session_id": "sess_1737804123456_abc123", "chunk_index": 0, // 分片序号,从0开始 "chunk": , // 音频分片数据 "is_last": false // 是否为最后一片 } Response: { "code": "200", "message": "分片上传成功", "data": { "session_id": "sess_1737804123456_abc123", "chunk_index": 0, "received": true, "total_received": 1 // 已接收的分片总数 } } Error Response (需要重传): { "code": "500", "message": "分片上传失败", "data": { "session_id": "sess_1737804123456_abc123", "chunk_index": 0, "should_retry": true } } 说明: - 前端每1秒收集一次音频数据(MediaRecorder.start(1000)) - 实时上传每个分片,不等待录音结束 - 支持失败重传 - chunk_index连续递增 ``` #### 3. 完成上传并合并 ```javascript POST /api/audio/stream/complete Headers: { "Authorization": "Bearer {token}", "Content-Type": "application/json" } Request: { "session_id": "sess_1737804123456_abc123", "meeting_id": 123, "total_chunks": 180, // 总分片数 "mime_type": "audio/webm;codecs=opus", "auto_transcribe": true // 是否自动启动转录 } Response: { "code": "200", "message": "音频上传完成", "data": { "meeting_id": 123, "file_path": "/uploads/audio/123/abc123.webm", "file_size": 184320000, // 字节 "duration": 180, // 秒 "task_id": "task_transcription_123", // 转录任务ID(如果auto_transcribe=true) "task_status": "pending" } } Error Response (合并失败): { "code": "500", "message": "音频合并失败:部分分片丢失", "data": { "missing_chunks": [5, 12, 34], // 缺失的分片序号 "should_retry": true } } 说明: - 后端按序合并所有分片 - 验证分片完整性 - 保存最终音频文件 - 可选自动启动转录任务 - 清理临时分片文件 ``` #### 4. 取消上传会话 ```javascript DELETE /api/audio/stream/cancel Headers: { "Authorization": "Bearer {token}", "Content-Type": "application/json" } Request: { "session_id": "sess_1737804123456_abc123" } Response: { "code": "200", "message": "上传会话已取消", "data": { "session_id": "sess_1737804123456_abc123", "cleaned": true } } 说明: - 用户中途停止录音或取消上传时调用 - 后端清理临时分片文件 - 释放会话资源 ``` --- ### 10.4 接口调用流程 #### 完整录音上传流程 ``` 1. 用户点击录音按钮 ↓ 2. 前端:创建会议 POST /api/meetings → 获得 meeting_id ↓ 3. 前端:初始化上传会话 POST /api/audio/stream/init → 获得 session_id ↓ 4. 前端:开始录音(MediaRecorder) ↓ 5. 每1秒收集音频数据 ↓ 6. 实时上传分片 POST /api/audio/stream/chunk (chunk_index: 0, 1, 2, ...) ↓ 7. 用户点击停止 ↓ 8. 前端:完成上传 POST /api/audio/stream/complete (is_last=true, total_chunks=N) ↓ 9. 后端:合并分片 → 启动转录 ↓ 10. 前端:跳转到会议详情页 /meetings/{meeting_id} ``` #### 错误恢复流程 ``` 分片上传失败 ↓ 前端:重试该分片(最多3次) ↓ 成功 → 继续下一分片 ↓ 失败 → 提示用户,取消上传 DELETE /api/audio/stream/cancel ``` --- ### 10.5 后端实现要点 #### 1. 分片存储策略 ```python # 后端临时目录结构 /tmp/audio_uploads/ └── sess_1737804123456_abc123/ ├── chunk_0000.webm ├── chunk_0001.webm ├── chunk_0002.webm └── metadata.json # metadata.json { "session_id": "sess_1737804123456_abc123", "meeting_id": 123, "mime_type": "audio/webm;codecs=opus", "total_chunks": null, # 初始为null,完成时填入 "received_chunks": [0, 1, 2, ...], "created_at": "2025-01-25T14:30:00", "expires_at": "2025-01-25T15:30:00" # 1小时后过期 } ``` #### 2. 分片合并逻辑 ```python def merge_audio_chunks(session_id, total_chunks): """合并音频分片""" session_dir = f"/tmp/audio_uploads/{session_id}" # 1. 验证分片完整性 missing = [] for i in range(total_chunks): if not os.path.exists(f"{session_dir}/chunk_{i:04d}.webm"): missing.append(i) if missing: raise ValueError(f"Missing chunks: {missing}") # 2. 按序合并 output_path = f"/app/uploads/audio/{meeting_id}/{uuid.uuid4()}.webm" with open(output_path, 'wb') as outfile: for i in range(total_chunks): chunk_path = f"{session_dir}/chunk_{i:04d}.webm" with open(chunk_path, 'rb') as infile: outfile.write(infile.read()) # 3. 清理临时文件 shutil.rmtree(session_dir) return output_path ``` #### 3. 会话过期清理 ```python # 定时任务:清理1小时前的过期会话 @scheduler.scheduled_job('interval', hours=1) def cleanup_expired_sessions(): """清理过期的上传会话""" now = datetime.now() upload_dir = "/tmp/audio_uploads" for session_dir in os.listdir(upload_dir): metadata_path = f"{upload_dir}/{session_dir}/metadata.json" if os.path.exists(metadata_path): with open(metadata_path) as f: metadata = json.load(f) expires_at = datetime.fromisoformat(metadata['expires_at']) if now > expires_at: shutil.rmtree(f"{upload_dir}/{session_dir}") print(f"Cleaned up expired session: {session_dir}") ``` #### 4. 音频格式处理 ```python # 支持的音频格式 SUPPORTED_MIME_TYPES = { 'audio/webm;codecs=opus': '.webm', 'audio/webm': '.webm', 'audio/ogg;codecs=opus': '.ogg', 'audio/mp4': '.m4a' } def validate_mime_type(mime_type): """验证MIME类型""" if mime_type not in SUPPORTED_MIME_TYPES: raise ValueError(f"Unsupported MIME type: {mime_type}") return SUPPORTED_MIME_TYPES[mime_type] ``` --- ### 10.6 性能优化建议 #### 1. 并发控制 ```python # 限制每个用户的并发上传会话数 MAX_CONCURRENT_SESSIONS_PER_USER = 2 # 使用Redis记录用户的活跃会话 redis.sadd(f"user:{user_id}:sessions", session_id) if redis.scard(f"user:{user_id}:sessions") > MAX_CONCURRENT_SESSIONS_PER_USER: raise TooManyConcurrentSessions() ``` #### 2. 分片上传队列 ```python # 使用Celery异步处理分片保存 @celery.task def save_audio_chunk(session_id, chunk_index, chunk_data): """异步保存音频分片""" session_dir = f"/tmp/audio_uploads/{session_id}" chunk_path = f"{session_dir}/chunk_{chunk_index:04d}.webm" with open(chunk_path, 'wb') as f: f.write(chunk_data) # 更新metadata update_session_metadata(session_id, chunk_index) ``` #### 3. 带宽优化 ```python # 配置Nginx限速(可选) location /api/audio/stream/chunk { limit_req zone=upload_rate burst=5; limit_rate 2m; # 限制每个连接2MB/s } ``` --- ### 10.7 安全性措施 #### 1. 文件大小限制 ```python MAX_CHUNK_SIZE = 2 * 1024 * 1024 # 2MB per chunk MAX_TOTAL_SIZE = 500 * 1024 * 1024 # 500MB total MAX_DURATION = 3600 # 1 hour max recording @app.post("/api/audio/stream/chunk") async def upload_chunk( chunk: UploadFile = File(...), session_id: str = Form(...), chunk_index: int = Form(...) ): # 验证分片大小 if chunk.size > MAX_CHUNK_SIZE: raise HTTPException(400, "Chunk too large") # 验证总大小 session_total = get_session_total_size(session_id) if session_total + chunk.size > MAX_TOTAL_SIZE: raise HTTPException(400, "Total size exceeds limit") ``` #### 2. 防止目录遍历攻击 ```python def validate_session_id(session_id: str): """验证session_id格式,防止路径注入""" if not re.match(r'^sess_\d+_[a-zA-Z0-9]+$', session_id): raise ValueError("Invalid session_id format") return session_id ``` #### 3. 权限验证 ```python @app.post("/api/audio/stream/complete") async def complete_upload( request: CompleteUploadRequest, current_user: dict = Depends(get_current_user) ): # 验证会议所有权 meeting = get_meeting(request.meeting_id) if meeting.user_id != current_user['user_id']: raise HTTPException(403, "Permission denied") ``` --- ### 10.8 监控与日志 #### 关键指标 ```python # 记录以下指标 - 上传会话数(总数、活跃数) - 分片上传成功率 - 平均上传速度 - 合并耗时 - 失败原因统计 # 日志格式 { "event": "chunk_upload", "session_id": "sess_xxx", "chunk_index": 10, "chunk_size": 1024000, "upload_time_ms": 234, "user_id": 1, "timestamp": "2025-01-25T14:30:00" } ``` --- ## 十一、快速开始 ```bash # 1. 安装依赖 npm install # 2. 启动开发服务器 npm run dev # 3. 构建生产版本 npm run build # 4. 预览生产构建 npm run preview ``` --- **文档版本**:v1.0 **最后更新**:2025-01-25