Files
EchoChat/backend/go-service/app/meeting/dao/meeting_chat_dao.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

88 lines
2.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 dao
import (
"context"
"github.com/echochat/backend/app/meeting/model"
"github.com/echochat/backend/pkg/logs"
"go.uber.org/zap"
"gorm.io/gorm"
)
// MeetingChatDAO 会议内文字聊天数据访问对象
type MeetingChatDAO struct {
db *gorm.DB
}
// NewMeetingChatDAO 创建实例
func NewMeetingChatDAO(db *gorm.DB) *MeetingChatDAO {
return &MeetingChatDAO{db: db}
}
// Create 写入一条会议聊天消息
// ID / CreatedAt 由数据库自增 + autoCreateTime 填充
func (d *MeetingChatDAO) Create(ctx context.Context, chat *model.MeetingChat) error {
funcName := "dao.meeting_chat_dao.Create"
err := d.db.WithContext(ctx).Create(chat).Error
if err != nil {
logs.Error(ctx, funcName, "写入会议聊天消息失败",
zap.Int64("room_id", chat.RoomID),
zap.Int64("user_id", chat.UserID),
zap.Error(err))
}
return err
}
// ListByRoom 按房间正序返回聊天历史(供会议室打开时加载)
// afterID 为游标id > afterID 的记录0 表示从头开始
// limit 默认 50上限 200
func (d *MeetingChatDAO) ListByRoom(ctx context.Context, roomID int64, afterID int64, limit int) ([]model.MeetingChat, error) {
funcName := "dao.meeting_chat_dao.ListByRoom"
if limit <= 0 {
limit = 50
}
if limit > 200 {
limit = 200
}
q := d.db.WithContext(ctx).Where("room_id = ?", roomID)
if afterID > 0 {
q = q.Where("id > ?", afterID)
}
var list []model.MeetingChat
err := q.Order("id ASC").Limit(limit).Find(&list).Error
if err != nil {
logs.Error(ctx, funcName, "查询会议聊天失败",
zap.Int64("room_id", roomID), zap.Error(err))
}
return list, err
}
// DeleteByRoomIDs 按房间 ID 批量删除聊天消息(会议结束 24 小时后清理)
// 外层已有 meeting_rooms ON DELETE CASCADE此方法用于主动定时清理不删除 room 本身)
func (d *MeetingChatDAO) DeleteByRoomIDs(ctx context.Context, roomIDs []int64) (int64, error) {
funcName := "dao.meeting_chat_dao.DeleteByRoomIDs"
if len(roomIDs) == 0 {
return 0, nil
}
res := d.db.WithContext(ctx).
Where("room_id IN ?", roomIDs).
Delete(&model.MeetingChat{})
if res.Error != nil {
logs.Error(ctx, funcName, "批量清理聊天消息失败",
zap.Int("room_count", len(roomIDs)), zap.Error(res.Error))
}
return res.RowsAffected, res.Error
}
// CountByRoom 统计房间内聊天条数(供管理端观测)
func (d *MeetingChatDAO) CountByRoom(ctx context.Context, roomID int64) (int64, error) {
var count int64
err := d.db.WithContext(ctx).
Model(&model.MeetingChat{}).
Where("room_id = ?", roomID).
Count(&count).Error
return count, err
}