主要产出:
- DTO 层:app/dto/meeting_dto.go 完整定义 13 个 DTO(请求/响应/基础共用)
- Service 层:MeetingService 12 业务方法 + 11 个 sentinel 错误 + 4 辅助
- CreateRoom/JoinRoom/LeaveRoom/EndRoom 核心生命周期
- KickMember/TransferHost/GetRoom/ListMyMeetings 会议管理
- InviteUsers(+ NotifyPusher)/ RedeemInviteToken 邀请链路
- SendChat/ListChats 会议内聊天
- Controller 层:12 handler + handleError 领域错误 → HTTP 映射
- 工具层:pkg/utils/meeting_code.go(XXX-XXX-XXX + invite token)
- Stub 接口:MediaOrchestrator(Task 7 替换)+ NoopMediaOrchestrator
- 路径修正:/rooms → /rooms/mine、/invites → /invite-tokens 对齐设计
- DAO 契约修复:GetByID/GetByCode/GetByRoomAndUser/FindActiveByUser
将 gorm.ErrRecordNotFound 转为 (nil, nil),service 统一 nil 判定
- 安全强化:密码 bcrypt + 5 次错误锁 10 分钟;邀请 token 仅通过
NotifyPusher.Extra 定向下发,响应不回传
- host 自动转让:host 离会时将最早加入者提升为 host
- 单点参会:用户同一时间仅能在一个活跃会议
- API 文档:docs/api/frontend/meeting.md 重写为 12 接口完整规范
- Wire 依赖注入:MediaOrchestrator + NoopMediaOrchestrator provider
验证:
- go build / go vet / wire 零告警
- 端到端 3 用户场景:12 happy path + 5 错误路径 PASS=19 / FAIL=0
覆盖密码错/房间不存在/单点冲突/越权/邀请失效
文档同步:
- docs/progress/CURRENT_STATUS.md 增补 Task 5 章节
- .cursor/rules/project-context.mdc 更新阶段状态
- docs/plans/2026-04-21-phase2e-2-implementation.plan.md 标记 T5 ✅
下一步:Task 6(WS 信令协议 + BroadcastToMeeting 替换 PublishToUser 循环)
Made-with: Cursor
42 lines
1.5 KiB
Go
42 lines
1.5 KiB
Go
package utils
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"encoding/hex"
|
||
"fmt"
|
||
"math/big"
|
||
)
|
||
|
||
// meetingCodeDigitGroups 会议号的 3 段 × 3 位数字布局:XXX-XXX-XXX
|
||
// 每段由 [0, 1000) 的 crypto/rand 数字零填充为 3 位构成
|
||
// 去除连字符后总长度与 constants.MeetingRoomCodeLength (=9) 对齐
|
||
const meetingCodeDigitGroups = 3
|
||
const meetingCodeDigitPerGroup = 3
|
||
|
||
// GenerateMeetingRoomCode 生成 9 位随机会议号,格式为 XXX-XXX-XXX
|
||
// 使用 crypto/rand 保证足够熵,服务端持久化时可按需去除连字符
|
||
// 返回示例:329-471-086
|
||
func GenerateMeetingRoomCode() (string, error) {
|
||
parts := make([]string, meetingCodeDigitGroups)
|
||
maxPerGroup := big.NewInt(1000) // 每段 [0, 999]
|
||
for i := 0; i < meetingCodeDigitGroups; i++ {
|
||
n, err := rand.Int(rand.Reader, maxPerGroup)
|
||
if err != nil {
|
||
return "", fmt.Errorf("生成会议号随机数失败: %w", err)
|
||
}
|
||
parts[i] = fmt.Sprintf("%0*d", meetingCodeDigitPerGroup, n.Int64())
|
||
}
|
||
return parts[0] + "-" + parts[1] + "-" + parts[2], nil
|
||
}
|
||
|
||
// GenerateMeetingInviteToken 生成邀请链接用的高熵 Token(32 hex 字符,16 字节随机)
|
||
// 用于 Redis key echo:meeting:invite:{token} 的兑换凭证
|
||
// Token 不包含会议号等敏感信息,仅作为索引,查询时回查 Redis 值获取完整 payload
|
||
func GenerateMeetingInviteToken() (string, error) {
|
||
buf := make([]byte, 16)
|
||
if _, err := rand.Read(buf); err != nil {
|
||
return "", fmt.Errorf("生成邀请 Token 随机数失败: %w", err)
|
||
}
|
||
return hex.EncodeToString(buf), nil
|
||
}
|