Files
EchoChat/backend/go-service/app/meeting/service/meeting_service.go
bujinyuan d22f61fb9b 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
2026-04-21 16:04:57 +08:00

145 lines
5.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"context"
"errors"
"github.com/echochat/backend/app/meeting/dao"
"github.com/echochat/backend/app/meeting/model"
"github.com/echochat/backend/pkg/ws"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
// Task 5 会继续填充具体业务逻辑时使用的哨兵错误,此处先集中定义占位
// 命名与 group/notify 模块的 Err* 风格一致,控制器可直接 errors.Is 识别
var (
ErrMeetingNotFound = errors.New("会议不存在")
ErrMeetingEnded = errors.New("会议已结束")
ErrMeetingFull = errors.New("会议已满员")
ErrMeetingPasswordWrong = errors.New("会议密码错误")
ErrNotInMeeting = errors.New("你不在此会议中")
ErrAlreadyInOtherMeeting = errors.New("你当前已在其他会议中")
ErrNotMeetingHost = errors.New("仅主持人可执行此操作")
ErrInviteTokenInvalid = errors.New("邀请链接已失效")
ErrRoomCodeConflict = errors.New("会议号生成冲突,请重试")
ErrNotImplemented = errors.New("功能尚未实现")
)
// MeetingService 会议业务服务
// Task 4 骨架阶段:仅完成依赖组装和方法签名占位,具体业务逻辑留待 Task 5/6/7 填充
// 方法返回 ErrNotImplementedController 将其映射为 501 响应,便于 Postman 与前端联调前观察路由完整性
type MeetingService struct {
roomDAO *dao.MeetingRoomDAO
participantDAO *dao.MeetingParticipantDAO
chatDAO *dao.MeetingChatDAO
db *gorm.DB
redis *redis.Client
pubsub *ws.PubSub
notifyPusher NotifyPusher
userResolver UserInfoResolver
onlineChecker OnlineChecker
}
// NewMeetingService 创建 MeetingService 实例
// 依赖通过构造函数注入,接口依赖由上游 Wire 绑定到具体实现
func NewMeetingService(
roomDAO *dao.MeetingRoomDAO,
participantDAO *dao.MeetingParticipantDAO,
chatDAO *dao.MeetingChatDAO,
db *gorm.DB,
redis *redis.Client,
pubsub *ws.PubSub,
notifyPusher NotifyPusher,
userResolver UserInfoResolver,
onlineChecker OnlineChecker,
) *MeetingService {
return &MeetingService{
roomDAO: roomDAO,
participantDAO: participantDAO,
chatDAO: chatDAO,
db: db,
redis: redis,
pubsub: pubsub,
notifyPusher: notifyPusher,
userResolver: userResolver,
onlineChecker: onlineChecker,
}
}
// ====== 会议生命周期Task 5 实现) ======
// CreateRoom 创建会议房间
// Task 5生成 room_code、bcrypt 密码、持久化 + 写 Redis room 快照
func (s *MeetingService) CreateRoom(ctx context.Context, hostID int64, title, password string, maxMembers int, settings map[string]interface{}) (*model.MeetingRoom, error) {
return nil, ErrNotImplemented
}
// GetRoomByCode 通过会议号查询房间详情(含当前成员列表)
func (s *MeetingService) GetRoomByCode(ctx context.Context, code string) (*model.MeetingRoom, error) {
return nil, ErrNotImplemented
}
// JoinRoom 用户加入会议(校验密码 / 容量 / 单点参会)
func (s *MeetingService) JoinRoom(ctx context.Context, userID int64, code, password string) (*model.MeetingParticipant, error) {
return nil, ErrNotImplemented
}
// LeaveRoom 用户主动离会
// 若离开者是 host 且房间内还有其他成员,触发主持人转让(最早加入者接任)
// 若离开后房间空,设置 Redis room TTL=300s 等待重入复用
func (s *MeetingService) LeaveRoom(ctx context.Context, userID int64, code string) error {
return ErrNotImplemented
}
// EndRoom 主持人主动结束会议status=ended + 全员 left_at + 触发 mediasoup 清理)
func (s *MeetingService) EndRoom(ctx context.Context, userID int64, code string) error {
return ErrNotImplemented
}
// TransferHost 主持人转让(仅当前 host 可调用)
func (s *MeetingService) TransferHost(ctx context.Context, operatorID int64, code string, newHostID int64) error {
return ErrNotImplemented
}
// KickMember 主持人踢出成员
func (s *MeetingService) KickMember(ctx context.Context, operatorID int64, code string, targetUserID int64) error {
return ErrNotImplemented
}
// ListMyMeetings 我主持的 / 我参与的会议列表(含分页)
func (s *MeetingService) ListMyMeetings(ctx context.Context, userID int64, role string, offset, limit int) ([]model.MeetingRoom, int64, error) {
return nil, 0, ErrNotImplemented
}
// ====== 邀请链接Task 5 ======
// CreateInviteToken 生成邀请链接 Token 并写 RedisTTL 600s
func (s *MeetingService) CreateInviteToken(ctx context.Context, inviterID int64, code string, inviteeID int64) (string, error) {
return "", ErrNotImplemented
}
// RedeemInviteToken 点击邀请链接时兑换 Token校验 + 删除)
func (s *MeetingService) RedeemInviteToken(ctx context.Context, userID int64, token string) (string, error) {
return "", ErrNotImplemented
}
// InviteUsers 主持人邀请用户(批量,走 notify.Push 发送 meeting_invite 通知)
func (s *MeetingService) InviteUsers(ctx context.Context, inviterID int64, code string, inviteeIDs []int64, groupIDs []int64) error {
return ErrNotImplemented
}
// ====== 会议内聊天Task 6 ======
// SendChatMessage 写入会议聊天 + 向房间内所有成员广播 WS meeting.chat.message
func (s *MeetingService) SendChatMessage(ctx context.Context, userID int64, code, content string) (*model.MeetingChat, error) {
return nil, ErrNotImplemented
}
// ListChatMessages 加载会议聊天历史(游标分页)
func (s *MeetingService) ListChatMessages(ctx context.Context, userID int64, code string, afterID int64, limit int) ([]model.MeetingChat, error) {
return nil, ErrNotImplemented
}