将 Cursor Plan 模式下的临时产物 task_8_会议生命周期状态机_3a0bc37b.plan.md
归档至 docs/plans/ 目录,统一命名风格为日期前缀 + 阶段/任务描述:
2026-04-21-phase2e-2-task8-lifecycle.plan.md
用途:
- 与 docs/plans/2026-04-21-phase2e-2-{design,implementation.plan}.md 共同构成
Phase 2e-2 Task 8 的完整设计 → 实施 → 归档三件套
- 保留 15 个 todo 的详细拆分 + 参考附录,作为后续复杂 Task 的实施样本
- 头部补充 archived_at / archived_note,指向实际交付结果与 commit
不改动文件主体内容,仅追加归档元数据。
Made-with: Cursor
11 KiB
11 KiB
name, overview, archived_at, archived_note, todos, isProject
| name | overview | archived_at | archived_note | todos | isProject | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Task 8 会议生命周期状态机 | 落地 Phase 2e-2 Task 8:host 掉线 2 分钟宽限期 + 自动转让、空房 5 分钟 TTL、后台定时清理兜底任务,同步修复 Task 7 遗留的 CreateRouter 重复调用。整体 0.5 人日。 | 2026-04-21 | 本文件原为 Cursor Plan 模式临时产物(.cursor/plans/task_8_*.plan.md), Task 8 实施完成后归档至 docs/plans/ 供后续审计追溯。 实施结果与实际偏差见 docs/plans/2026-04-21-phase2e-2-implementation.plan.md §Task 8 与 docs/progress/CURRENT_STATUS.md 的 Task 8 交付条目;E2E 验证 20/20 PASS (docs/verify/meeting_t8_verify.mjs);最终 commit: 3b83c79。 |
|
false |
一、核心决策(回答已确认)
- 普通成员 WS 断线(Q1=A):维持现状,仅清 WS 媒体资源,不动
meeting_participants;长期不活跃依赖 4h 兜底清理(Task 8 内实现) - Router 去重(Q2=C):业务层
JoinRoom不再调CreateRouter+HTTPMediaOrchestrator.CreateRouter加 sync.Map 幂等防御 - 过期检测:
time.AfterFunc为主(低延迟)+ 后台meeting_cleanup_task每 30s 扫描兜底(服务重启/机器迁移场景) - 并发保护:
HandleHostGraceExpired入口先DELhost_grace key,DEL 返回 0 视为已被重连清走,直接 return(天然互斥) - 可测性:
config.MeetingConfig{HostGraceSeconds, EmptyRoomTTLSeconds, CleanupIntervalSeconds},默认 120/300/30,E2E 测试可覆盖为 2/3/1
二、状态机流转
stateDiagram-v2
[*] --> Active: CreateRoom
Active --> HostGrace: OnHostDisconnect (host WS 掉线)
HostGrace --> Active: OnHostReconnect (120s 内重连,DEL grace key)
HostGrace --> Transferring: host_grace 过期 AfterFunc 触发
Transferring --> Active: 事务转让最早加入者 + 广播 host.changed
Transferring --> EmptyTTL: 无其他活跃成员
Active --> EmptyTTL: OnAllMembersLeft (全员 Leave)
EmptyTTL --> Active: JoinRoom 命中 empty_ttl key 复活
EmptyTTL --> Ended: 300s 过期 + CloseRouter + status=2 reason=empty_ttl
Active --> Ended: host EndRoom (status=2 reason=host_ended)
Ended --> [*]
三、文件变更清单
3.1 新建
backend/go-service/app/meeting/service/meeting_lifecycle_service.go(约 350 行)- 统一封装 5 个钩子:
OnHostDisconnect / OnHostReconnect / HandleHostGraceExpired / OnAllMembersLeft / HandleEmptyRoomExpired - 内部
sync.Map[roomCode]*time.Timer管理本地 AfterFunc - 暴露
RescheduleFromRedis(ctx)(服务启动时扫描已存在的 grace/empty_ttl key 补装 timer)
- 统一封装 5 个钩子:
backend/go-service/app/meeting/task/meeting_cleanup_task.go(约 180 行)- 启动时先
lifecycleSvc.RescheduleFromRedis一次 - 每 30s:扫
echo:meeting:host_grace:*找出TTL ≤ 0或 timer 已丢失的 key → 再次触发HandleHostGraceExpired;同理处理empty_ttlkey - 每 10 分钟:
roomDAO.ListStaleActive(hoursNoActivity=4)→EndRoom(..., reason=system_error)兜底 - 每 24 小时:
chatDAO.DeleteByRoomIDs(olderThan=24h)聊天清理
- 启动时先
docs/verify/meeting_t8_verify.mjs(约 250 行)E2E 脚本
3.2 修改(业务逻辑)
backend/go-service/app/meeting/service/meeting_service.goJoinRoomL307-316:移除mediaOrchestrator.CreateRouter调用,改为复用已存在 Router(查 Redisecho:meeting:room:{code}router_id 字段或直接从 HTTPMediaOrchestrator sync.Map 读)LeaveRoomL381-384:全员退出时从"立即 MarkEnded+CloseRouter"改为调lifecycleSvc.OnAllMembersLeft(ctx, code)JoinRoom新增 L260 后:检查echo:meeting:empty_ttl:{code}存在即lifecycleSvc.CancelEmptyTTL(ctx, code)恢复房间CreateRoomL213 保留(房间首创 Router)
backend/go-service/app/meeting/service/http_media_orchestrator.goCreateRouter前置查roomRouterCachesync.Map,命中即直接 return(底层幂等防御,对业务层误调零影响)
backend/go-service/app/meeting/service/meeting_signal_service.go- 新增公开方法
OnWSDisconnect(ctx context.Context, userID int64):participantDAO.FindActiveByUser→ 存在则调cleanupUserResources;若room.HostID == userID则额外lifecycleSvc.OnHostDisconnect OnRoomJoin新增 L112 前:若该用户是 room.HostID 则调lifecycleSvc.OnHostReconnect(支持主持人重连恢复)
- 新增公开方法
3.3 WS 断线钩子集成
backend/go-service/app/ws/handler.go- 新增字段
meetingDisconnectHook MeetingDisconnectHook(接口,可选,避免循环依赖) SetOnDisconnect回调 L115-123:在UserOffline之后追加hook.OnWSDisconnect(ctx, userID)
- 新增字段
app/ws/interfaces.go(新增 10 行)定义接口:type MeetingDisconnectHook interface { OnWSDisconnect(ctx context.Context, userID int64) }app/provider/wire.go:wire.Bind(new(ws.MeetingDisconnectHook), new(*service.MeetingSignalService))
3.4 配置 + 启动
backend/go-service/config/config.go新增MeetingConfig { HostGraceSeconds int; EmptyRoomTTLSeconds int; CleanupIntervalSeconds int; StaleRoomHours int }config.dev.yaml/config.docker.yaml默认值:120 / 300 / 30 / 4cmd/server/main.goL82 后追加:app.MeetingCleanupTask.Start()+defer Stop()app/provider/provider.go:App结构体新增MeetingCleanupTask *task.MeetingCleanupTask
3.5 DAO 扩展
backend/go-service/app/meeting/dao/meeting_room_dao.goListStaleActive(ctx, hoursNoActivity int, limit int) ([]int64, error):status != ended AND started_at < NOW() - hoursNoActivityFindActiveByCode(ctx, code) (*MeetingRoom, error)(若不存在直接复用已有 GetByCode)
3.6 Redis key 增补
| key | TTL | 值 | 作用 |
|---|---|---|---|
echo:meeting:host_grace:{code} |
120s | {host_id, grace_until, started_at} JSON |
已在设计 §5.4 定义,Task 8 落地 |
echo:meeting:empty_ttl:{code} |
300s | 1(占位) |
空房 TTL key(设计 §5.4 echo:meeting:room:{code} 的 TTL 语义,本 Task 改为独立 key 语义更清晰) |
echo:meeting:grace_lock:{code} |
5s | 1 |
SETNX 锁,防止多实例部署并发处理同一过期事件(可选,MVP 单实例不强依赖) |
四、并发/容错要点
- host 重连与过期竞争:
HandleHostGraceExpired入口先DEL host_grace+ 检查返回值;返回 0 即已被OnHostReconnect清走,跳过本次处理 - AfterFunc 取消:
lifecycleSvc的sync.Map存*time.Timer,OnHostReconnect/CancelEmptyTTL时调timer.Stop()+sync.Map.Delete - 服务重启兜底:
RescheduleFromRedis扫SCAN echo:meeting:host_grace:*和echo:meeting:empty_ttl:*,按PTTL剩余毫秒数重新 AfterFunc - TransferHost 事务幂等:DAO 层已是事务;外层在 DEL grace_key 成功后才调 TransferHost,天然串行
- 4 小时兜底 EndRoom:
cleanupTask用独立 context.Background + 5min timeout,失败仅日志不 panic
五、测试/验收
- 单元:psql 手工脚本验证
TransferHost事务在冲突时回滚(复用 Task 3 风格) - E2E 脚本
docs/verify/meeting_t8_verify.mjs(TEST 模式下HostGraceSeconds=2、EmptyRoomTTLSeconds=3、CleanupIntervalSeconds=1):- host 掉线 → 2s 内读
host_grace存在 → 第二成员 3s 后收meeting.host.changed,room.HostID 在 DB 已更新 - host 掉线 → 1s 内重连 →
host_grace被 DEL → host 身份保留 - 全员 leave →
empty_ttl存在 → 新用户 join → key 被 DEL + 房间 status 仍 Active - 全员 leave → 不 join → 3s 后 MarkEnded + CloseRouter(Node 日志印证)
- JoinRoom 两次不再产生两次
router createdNode 日志(Task 7 遗留修复验收)
- host 掉线 → 2s 内读
go build ./... && go vet ./...全绿,ReadLints 零问题
六、文档同步(commit 前)
docs/progress/CURRENT_STATUS.md新增 Task 8 章节,总进度切Task 0-8 ✅ / Task 9-16 待执行docs/plans/2026-04-21-phase2e-2-implementation.plan.mdTask 8 标记已完成 + 实际产出docs/plans/2026-04-21-phase2e-2-design.md§十六变更记录追加 Task 8 偏差(若有)docs/api/frontend/meeting.md补充 host 宽限期 UI 建议 + 空房复活行为.cursor/rules/project-context.mdc进度同步
七、风险与回退
- 本地 timer 失效(宿主时钟跳变)→ 30s 扫描兜底接管
- 多实例部署并发处理同一过期事件 → MVP 单实例可容忍,后续用
grace_lockSETNX 修复 - JoinRoom 移除 CreateRouter 后如果 Router 丢失(Node 重启)→ HTTPMediaOrchestrator 层可加"404 → 重建"兜底(作为 Task 8 可选增强或 Task 16 再议)