主要产出:
- 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
67 lines
2.9 KiB
Go
67 lines
2.9 KiB
Go
// Package service 提供 meeting 模块的业务逻辑
|
||
package service
|
||
|
||
import (
|
||
"context"
|
||
|
||
authModel "github.com/echochat/backend/app/auth/model"
|
||
notifyService "github.com/echochat/backend/app/notify/service"
|
||
)
|
||
|
||
// NotifyPusher 通知推送接口
|
||
// 由 notify.service.NotifyService 隐式实现,供 meeting 模块注入使用
|
||
// 会议邀请、主持人变更、踢出会议等事件通过此接口落地通知中心
|
||
type NotifyPusher interface {
|
||
Push(ctx context.Context, payload *notifyService.PushPayload)
|
||
PushBatch(ctx context.Context, payloads []*notifyService.PushPayload)
|
||
}
|
||
|
||
// UserInfoResolver 查询用户昵称 / 头像的接口
|
||
// 由 contact.FriendshipDAO 隐式实现(已有 GetUsersByIDs 方法)
|
||
// 用于拉取会议邀请中主持人信息、成员列表头像等
|
||
type UserInfoResolver interface {
|
||
GetUsersByIDs(ctx context.Context, userIDs []int64) ([]authModel.User, error)
|
||
}
|
||
|
||
// OnlineChecker 查询用户在线状态的接口
|
||
// 由 ws.OnlineService 隐式实现(签名对齐现有实现:不返回 error,查询失败按离线处理)
|
||
// 用于会议邀请时判断收件人是否在线(离线则降级为仅通知入库,上线时通过未读补偿推送)
|
||
type OnlineChecker interface {
|
||
IsOnline(ctx context.Context, userID int64) bool
|
||
}
|
||
|
||
// MediaOrchestrator 媒体服务器编排接口
|
||
// Task 7 落地 Go → Node media-server HTTP Client 后由 node_client.NodeClient 实现
|
||
// Task 5 阶段使用 NoopMediaOrchestrator 占位,仅返回假的 RouterID,不产生真实媒体资源
|
||
// 语义:会议房间在 Go 侧落库后,通过该接口驱动 Node 端 mediasoup Router 创建与销毁
|
||
type MediaOrchestrator interface {
|
||
// CreateRouter 为会议房间创建 mediasoup Router
|
||
// 入参:room_code 作为 Node 端聚合键;返回 Router ID 与推荐编解码参数(Task 7 对接时补充)
|
||
CreateRouter(ctx context.Context, roomCode string) (routerID string, err error)
|
||
|
||
// CloseRouter 关闭会议房间对应的 mediasoup Router 及其下所有 transport/producer/consumer
|
||
// 幂等:重复关闭不返回错误
|
||
CloseRouter(ctx context.Context, roomCode string) error
|
||
}
|
||
|
||
// NoopMediaOrchestrator 占位实现:Task 5 完成生命周期接口时使用
|
||
// 返回伪造的 RouterID,所有操作仅写日志不调用 Node
|
||
// Task 7 完成后全局 wire 切换到真实 NodeClient 实现
|
||
type NoopMediaOrchestrator struct{}
|
||
|
||
// NewNoopMediaOrchestrator 构造占位的 MediaOrchestrator
|
||
func NewNoopMediaOrchestrator() *NoopMediaOrchestrator {
|
||
return &NoopMediaOrchestrator{}
|
||
}
|
||
|
||
// CreateRouter 返回以 "noop-router-" 为前缀的伪造 RouterID
|
||
// 调用方可据此区分真实 / 占位实现,便于调试与切换
|
||
func (n *NoopMediaOrchestrator) CreateRouter(_ context.Context, roomCode string) (string, error) {
|
||
return "noop-router-" + roomCode, nil
|
||
}
|
||
|
||
// CloseRouter 占位实现:直接返回 nil
|
||
func (n *NoopMediaOrchestrator) CloseRouter(_ context.Context, _ string) error {
|
||
return nil
|
||
}
|