一、要解决什么问题企业会议和「点一下就打过去」的音视频通话不是一类产品会议可以先预定、后入会预定当下不必占用摄像头和 SFU 房间。入会方式要多会议号、链接、二维码不要求双方是好友。主持人要能邀请、全体静音、结束本场普通人只能离开最后一个人离开不能把整场会关掉。同一人可能同时是两场会的主持人信令必须按meetingId隔离否则会串会。端不一样H5 能直接跑livekit-clientApp 用 web-view 包一层 HTML微信小程序没有会议室。所以架构上必须坚持一件事LiveKit 只负责音视频转发不负责「这场会还在不在」。二、总体架构三层职责层干什么不干什么业务后端MeetingService预定、会议号、成员、密码、人数、到期结束、签发 JWT不传媒体IM 信令 type300–305邀请、进房、离房、结束、媒体状态不走好友校验LiveKit SFUroom.connect(url, token)后的发布 / 订阅不知道 OA 的 scheduled / ended房间名约定会议m_{meetingId}一场会固定一个房间可反复进出三、数据模型一场会、一群人两张表meeting会议号、标题、发起人、开始时间、时长、密码、状态、room_name、入会链接。状态机scheduled已预定截止前可进含主持人清场之后ongoing有人在房内ended超过startTime durationMinutes才进入不能再进cancelled后台取消meeting_member角色host / guest邀请状态麦 / 摄像头 / 共享以及最关键的in_room。产品规则写在字段上in_room表示「当前是否在 LiveKit 房间」不是「有没有被邀请过」。离开只改自己的in_roomfalse不改会议状态。主持人点「结束」会踢所有人但状态仍等到截止时间才变成ended。人数上限 100含主持人在join和invite两处卡住。四、预定生成凭证不进 LiveKit接口POST /api/v1/meeting/create。预定时做这些事生成不重复的 9 位会议号用配置模板拼 H5 入会链接例如https://www.minglina.com/oah5/subpages/meeting/join?code{code}先save()拿到雪花 id再写room_name m_{id}主持人写入meeting_memberin_roomfalse返回会议号、链接、二维码不带 LiveKit token会议「预定成功」只是落库。五、进房业务校验 签发 JWT接口POST /api/v1/meeting/join。服务端顺序LiveKit 配置必须完整url / api-key / api-secret会议仍有效未 ended / cancelled未过截止时间有密码则校验未加入过则插入成员已在房断线重连只刷新姓名和 token把in_roomtrue会议标成ongoing用户状态设为忙线IM 广播MEETING_JOIN用会议 id 纠正房间名防止历史数据里的m_null签发 LiveKit Access TokenToken 不依赖 LiveKit 官方 Java SDK用 Auth0 JWT 按 LiveKit 规范自己签返回给前端的凭证是另有POST /token已经在房的人只刷新凭证避免弱网重连再走一遍 join。心跳POST /heartbeat刷新忙线过期时间避免被当成空闲。六、客户端怎么连上 LiveKit1. 端差异2. 进房时序会议室页onLoad先调/token失败再/join从详情页进来可能已经 join 过connectLiveKitRoom({ url, token, video: false, audio: true })默认关麦、关摄像头需要时再开每 20 秒心跳页面卸载调/leave并disconnect连接封装里几件必须注意的事autoSubscribe: true远端发布后自动订阅音频订阅按identity sid去重避免同一轨挂两个audio视频每次开关都强制重新绑定反复开关摄像头后远端才能再看到画面屏幕共享轨和摄像头轨分开停共享不要清掉摄像头TrackMuted时如果是音频不要当成关摄像头否则全体静音后画面会黑七、信令IM 300–305且必须跳过好友检查会议不是通话扫码进来的人往往不是好友。IM 收包时要先按类型分流type含义300MEETING_INVITE邀请301MEETING_JOIN有人进房302MEETING_LEAVE有人离房303MEETING_ENDpermanenttrue截止结束false主持人清场304MEETING_MEDIA麦 / 摄像头 / 共享全体静音带audioOnly305MEETING_REJECT拒绝邀请后端广播时type 100的信令不做好友校验。App 会议室在 web-view 里原生 IM 收包后要再转一份*App事件用evalJS调handleMeetingEventH5 才能更新成员列表。八、控场离开、结束、全体静音离开只改自己inRoomfalse广播LEAVE。最后一个人走了会议还在截止前还能再进。主持人结束kickAllOut(..., permanentfalse)所有人被请出状态不变。客户端收到MEETING_END后断开 LiveKit 并退出页面。截止前别人仍可再 join。到期结束任意info / mine / join都会走expireIfDue。超过startTime duration才statusended并permanenttrue踢人之后不能再进。全体静音只改audiopayload 带audioOnly: true不要带 video/screen避免前端把视频也关了。主持人不静音自己。共享屏幕同一时刻只允许一人其他人的screen清掉。九、串会1. 房间名写成m_nullbuildMeetingRoomName(meeting.getId())若在save()之前调用id 为空LiveKit 房间就是m_null。所有会议进同一个 SFU 房间画面和声音会串。正确顺序先保存再用主键更新room_name。进房时ensureRoomName()若发现m_null/ 空按当前 id 改写。buildMeetingRoomName遇到空 id 直接抛错。2. IM 不带meetingId过滤一个人同时主持 A、B 两场会人在 A 里也会收到 B 的 JOIN。客户端必须会议室页忽略其它会的信令刷新成员时丢掉inRoomfalse的人。十、可以抄走的实现清单媒体与信令分离SFU 只转发状态机在自己库里。预定不进房join 才签发 JWT。会议房间名绑定业务主键且必须在主键生成之后。信令号段独立并跳过好友检查。每条会议信令带meetingId客户端过滤。离开 ≠ 结束主持人清场 ≠ 到期结束。全体静音只动音频轨。H5 用 CDN 加载 SDKApp 用 web-view 对齐同一版本。不要用beforeunload断开 LiveKit。入会链接用服务端模板不要用当前页 origin。媒体层可以换成别的 SFU上面这套业务边界仍然成立。