feat(phase2e-2): Go meeting 模块 DDL + Model + DAO + service/controller/router 骨架(Task 3+4)

Task 3 - 数据库与持久化层:
- init.sql 追加 3 张表(meeting_rooms / meeting_participants / meeting_chats)+ 9 索引 + 全量 COMMENT
- 新增 phase2e2_migration.sql 增量升级脚本(IF NOT EXISTS 幂等)
- app/constants/meeting.go 统一会议常量(类型/状态/角色/结束原因/离会原因/默认配置/WS 事件 8 组)
- app/meeting/model 3 个 model + GORM tag(TIMESTAMP(0) 对齐现有表风格)
- app/meeting/dao 3 个 DAO 共 24 个方法:
  * meeting_room_dao:Create/Get/Exists/MarkStarted/MarkEnded(乐观锁)/UpdateHost/UpdateSettings/ListByHost/ListExpiredForCleanup
  * meeting_participant_dao:JoinRoom(事务内复用旧记录)/LeaveRoom(EXTRACT(EPOCH) DB 时间)/LeaveAllActive/TransferHost(事务)/FindActiveByUser(JOIN 单点校验)/列表计数更新
  * meeting_chat_dao:Create/ListByRoom(游标)/DeleteByRoomIDs/CountByRoom
- psql 真库跑通 CRUD + UNIQUE + CASCADE + 主持人转让事务 + duration=10s 精确匹配 8 场景

Task 4 - Go 侧 service/controller/router/wire 骨架:
- app/meeting/service/interfaces.go:NotifyPusher / UserInfoResolver / OnlineChecker 三接口(解耦 notify/contact/ws)
- app/meeting/service/meeting_service.go:MeetingService + 8 sentinel error + 17 个空方法(返回 ErrNotImplemented)
- app/meeting/controller/meeting_controller.go:12 个 Gin handler + responseNotImplemented(501)
- app/meeting/router.go:12 条路由挂 /api/v1/meeting/* 并套 jwtAuth(扁平化 router.go,与 group/notify 一致)
- app/meeting/provider.go:MeetingSet = wire.NewSet(DAO×3, Service, Controller)
- 全局 wire.go 挂入 MeetingSet + 3 条 wire.Bind(Notify/UserInfo/Online);App struct/NewApp 加字段;router/router.go 挂载
- 存量修复:admin/provider.go 补齐 MessageManage{DAO,Service,Controller} 解决旧版 wire 重生成报 no provider found
- 验证:go build ./... / go vet ./... / wire ./app/provider 零错误;GIN_MODE=debug 启动打印全部 12 条路由;无 token curl 3 条代表性路由均返回 401(JWT 中间件生效)

关键风格修正(偏离草案):
- 常量归入 app/constants/meeting.go 单文件(与 group.go / notify.go 同构),非草案 app/meeting/constants/
- TIMESTAMP(0) 取代草案 TIMESTAMPTZ 对齐所有现有表
- 冗余 idx_meeting_rooms_code 移除(UNIQUE 已自动建索引)
- router/provider 目录扁平化(app/meeting/router.go / app/meeting/provider.go)
- 延续项目 Go 侧"零 _test.go"风格,用代码审查 + psql 真库验证 + Playwright E2E 三层守护

文档同步:
- docs/progress/CURRENT_STATUS.md 新增 Task 3 / Task 4 交付段落
- .cursor/rules/project-context.mdc Phase 2e-2 进度推进至 Task 0-4 
- docs/plans/2026-04-21-phase2e-2-implementation.plan.md Task 3 / Task 4 标记完成 + 实际产出 vs 计划差异

Made-with: Cursor
This commit is contained in:
bujinyuan
2026-04-21 16:04:57 +08:00
parent 6e2e792db0
commit d22f61fb9b
23 changed files with 1474 additions and 70 deletions

View File

@@ -0,0 +1,114 @@
package constants
// 会议相关常量Phase 2e-2
// 包含:会议类型、会议状态、参与者角色、结束原因、离会原因、默认配置
// 所有枚举值严禁直接用中文字符串,中文仅用于 *Map 展示
// 会议类型meeting_rooms.type
const (
MeetingTypeInstant = 1 // 即时会议MVP 仅此)
MeetingTypeScheduled = 2 // 预约会议Phase 2e-3
)
// MeetingTypeMap 会议类型中文映射
var MeetingTypeMap = map[int]string{
MeetingTypeInstant: "即时会议",
MeetingTypeScheduled: "预约会议",
}
// 会议状态meeting_rooms.status
const (
MeetingStatusPending = 0 // 未开始(仅预约会议使用)
MeetingStatusActive = 1 // 进行中
MeetingStatusEnded = 2 // 已结束
)
// MeetingStatusMap 会议状态中文映射
var MeetingStatusMap = map[int]string{
MeetingStatusPending: "未开始",
MeetingStatusActive: "进行中",
MeetingStatusEnded: "已结束",
}
// 会议参与者角色meeting_participants.role
// MVP 仅使用 Participant / Host 两档CoHost 保留至第二期
const (
MeetingRoleParticipant = 0 // 普通参会人
MeetingRoleHost = 1 // 主持人
MeetingRoleCoHost = 2 // 联合主持人保留MVP 不用)
)
// MeetingRoleMap 参与者角色中文映射
var MeetingRoleMap = map[int]string{
MeetingRoleParticipant: "参会人",
MeetingRoleHost: "主持人",
MeetingRoleCoHost: "联合主持人",
}
// 会议结束原因meeting_rooms.ended_reason
const (
MeetingEndedReasonHostEnded = "host_ended" // 主持人主动结束
MeetingEndedReasonEmptyTTL = "empty_ttl" // 空房超时销毁
MeetingEndedReasonAdminForce = "admin_force" // 管理员强制结束
MeetingEndedReasonSystemError = "system_error" // 系统异常(如 Node worker died
)
// MeetingEndedReasonMap 结束原因中文映射
var MeetingEndedReasonMap = map[string]string{
MeetingEndedReasonHostEnded: "主持人结束",
MeetingEndedReasonEmptyTTL: "空房超时",
MeetingEndedReasonAdminForce: "管理员强制结束",
MeetingEndedReasonSystemError: "系统异常",
}
// 离会原因meeting_participants.left_reason
const (
MeetingLeftReasonSelf = "self" // 主动离会
MeetingLeftReasonKicked = "kicked" // 被主持人移除
MeetingLeftReasonHostEnd = "host_end" // 主持人结束会议
MeetingLeftReasonEmptyTTL = "empty_ttl" // 空房 TTL 触发
MeetingLeftReasonDisconnect = "disconnect" // 网络断开(宽限期结束仍未重连)
)
// MeetingLeftReasonMap 离会原因中文映射
var MeetingLeftReasonMap = map[string]string{
MeetingLeftReasonSelf: "主动离开",
MeetingLeftReasonKicked: "被移除",
MeetingLeftReasonHostEnd: "会议结束",
MeetingLeftReasonEmptyTTL: "空房超时",
MeetingLeftReasonDisconnect: "网络断开",
}
// 会议默认配置(应用层强制约束)
const (
MeetingMVPMaxMembers = 8 // MVP 阶段单会议硬上限
MeetingRoomCodeLength = 9 // 会议号去连字符后的长度XXX-XXX-XXX
MeetingRoomCodeRetryMax = 3 // 生成会议号冲突重试上限
MeetingPasswordMaxAttempts = 5 // 密码连续错误锁定阈值
MeetingPasswordLockSeconds = 600 // 密码锁定时长10 分钟)
MeetingHostGraceSeconds = 120 // 主持人掉线宽限期2 分钟)
MeetingEmptyRoomTTLSeconds = 300 // 空房销毁 TTL5 分钟)
MeetingInviteTokenTTL = 600 // 邀请链接 Token TTL10 分钟)
MeetingChatRetentionHours = 24 // 会议聊天保留时长(会议结束后)
)
// 会议 WS 事件常量(设计文档 §6.311 个事件)
// 命名格式meeting.{domain}.{action}
const (
// 房间级事件
MeetingWSEventRoomEnded = "meeting.room.ended" // 会议已结束
MeetingWSEventRoomLocked = "meeting.room.locked" // 会议被锁定等候室Phase 2e-3
MeetingWSEventRoomHostChange = "meeting.room.host.changed" // 主持人变更
// 成员级事件
MeetingWSEventMemberJoined = "meeting.member.joined" // 新成员加入
MeetingWSEventMemberLeft = "meeting.member.left" // 成员离开
MeetingWSEventMemberStateChange = "meeting.member.state.changed" // 麦克风 / 摄像头状态变化
MeetingWSEventMemberKicked = "meeting.member.kicked" // 被移出
// 媒体级事件Go 发起,驱动客户端 mediasoup-client 订阅/取消)
MeetingWSEventProducerNew = "meeting.producer.new" // 有新 producer 可订阅
MeetingWSEventProducerClosed = "meeting.producer.closed" // producer 已关闭
MeetingWSEventConsumerResumed = "meeting.consumer.resumed" // consumer 已恢复(由服务端触发)
MeetingWSEventChatMessage = "meeting.chat.message" // 会议内文字聊天
)