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
115 lines
4.7 KiB
Go
115 lines
4.7 KiB
Go
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 // 空房销毁 TTL(5 分钟)
|
||
MeetingInviteTokenTTL = 600 // 邀请链接 Token TTL(10 分钟)
|
||
MeetingChatRetentionHours = 24 // 会议聊天保留时长(会议结束后)
|
||
)
|
||
|
||
// 会议 WS 事件常量(设计文档 §6.3,11 个事件)
|
||
// 命名格式: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" // 会议内文字聊天
|
||
)
|