Files
EchoChat/backend/go-service/app/admin/dao/meeting_manage_dao.go
2026-05-18 19:42:55 +08:00

287 lines
10 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 提供 admin 模块的数据库访问操作(会议管理)
package dao
import (
"context"
"errors"
"fmt"
"time"
"github.com/echochat/backend/app/dto"
meetingModel "github.com/echochat/backend/app/meeting/model"
"github.com/echochat/backend/pkg/logs"
"go.uber.org/zap"
"gorm.io/gorm"
)
// MeetingManageDAO 管理端会议数据访问对象
//
// 设计要点:
// - 列表查询走聚合 SQL一次取 meeting_rooms + host 昵称 + 参与者数 + 录制数,避免 N+1
// - 仅做"查询/统计"层操作,写操作(结束会议、删除录制)放在 service 层并复用既有路径
// - 与 MessageManageDAO 保持一致的 ListXxx + 统计方法风格
type MeetingManageDAO struct {
db *gorm.DB
}
// NewMeetingManageDAO 创建 MeetingManageDAO 实例
func NewMeetingManageDAO(db *gorm.DB) *MeetingManageDAO {
return &MeetingManageDAO{db: db}
}
// MeetingListRow 列表行的聚合结果DAO 内部结构,不外泄)
//
// 通过一次带 LEFT JOIN + GROUP BY 的查询拿齐:
// - 主表 meeting_rooms 全部字段
// - 主持人昵称 / 头像auth_users
// - 参与者数meeting_participants 去重 user_id
// - 录制数meeting_recordings 总条目)
//
// 注意:使用 GORM Raw + Scan绕开 model 主键自动展开,方便聚合
type MeetingListRow struct {
ID int64 `gorm:"column:id"`
RoomCode string `gorm:"column:room_code"`
Title string `gorm:"column:title"`
Type int `gorm:"column:type"`
Status int `gorm:"column:status"`
HostID int64 `gorm:"column:host_id"`
HostNickname string `gorm:"column:host_nickname"`
HostAvatar string `gorm:"column:host_avatar"`
MaxMembers int `gorm:"column:max_members"`
ParticipantCount int `gorm:"column:participant_count"`
RecordingCount int `gorm:"column:recording_count"`
ScheduledAt *time.Time `gorm:"column:scheduled_at"`
StartedAt *time.Time `gorm:"column:started_at"`
EndedAt *time.Time `gorm:"column:ended_at"`
EndedReason *string `gorm:"column:ended_reason"`
CreatedAt time.Time `gorm:"column:created_at"`
}
// buildListWhere 把 AdminMeetingListRequest 拼成 WHERE 条件,返回 SQL 片段与参数
// 复用给 ListMeetings + CountMeetings 两个查询,保证条件一致
func (d *MeetingManageDAO) buildListWhere(req *dto.AdminMeetingListRequest) (string, []any) {
conditions := []string{"1=1"}
args := []any{}
if req.Keyword != "" {
conditions = append(conditions, "(mr.title ILIKE ? OR mr.room_code ILIKE ?)")
kw := fmt.Sprintf("%%%s%%", req.Keyword)
args = append(args, kw, kw)
}
if req.Status != nil {
conditions = append(conditions, "mr.status = ?")
args = append(args, *req.Status)
}
if req.HostID != nil {
conditions = append(conditions, "mr.host_id = ?")
args = append(args, *req.HostID)
}
if req.StartTime != "" {
if t, err := time.Parse("2006-01-02", req.StartTime); err == nil {
conditions = append(conditions, "mr.created_at >= ?")
args = append(args, t)
}
}
if req.EndTime != "" {
if t, err := time.Parse("2006-01-02", req.EndTime); err == nil {
conditions = append(conditions, "mr.created_at < ?")
args = append(args, t.AddDate(0, 0, 1))
}
}
// HasRecording 通过 HAVING 在外层处理,这里不拼接
where := ""
for i, c := range conditions {
if i == 0 {
where = c
} else {
where = where + " AND " + c
}
}
return where, args
}
// ListMeetings 分页查询会议列表
//
// 聚合 SQL 设计:
// - LEFT JOIN auth_users拿主持人昵称host 可能已删除时为空,不影响行)
// - LEFT JOIN meeting_participants聚合参与者数COUNT DISTINCT user_id
// - LEFT JOIN meeting_recordings聚合录制数COUNT recording.id
// - HasRecording 通过 HAVING recording_count > 0 二次过滤
//
// 排序created_at DESC最新会议在前
func (d *MeetingManageDAO) ListMeetings(ctx context.Context, req *dto.AdminMeetingListRequest) ([]MeetingListRow, int64, error) {
funcName := "dao.meeting_manage_dao.ListMeetings"
logs.Debug(ctx, funcName, "查询会议列表")
where, args := d.buildListWhere(req)
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
if pageSize > 100 {
pageSize = 100
}
// 子查询聚合 participant_count / recording_count避免笛卡尔积放大 COUNT
// 这里用相关子查询而非 JOIN+GROUP BY便于 HAVING 与 COUNT(*) 分页对齐
baseSelect := `
SELECT
mr.id, mr.room_code, mr.title, mr.type, mr.status, mr.host_id,
COALESCE(u.nickname, '') AS host_nickname,
COALESCE(u.avatar, '') AS host_avatar,
mr.max_members,
COALESCE((SELECT COUNT(DISTINCT mp.user_id) FROM meeting_participants mp WHERE mp.room_id = mr.id), 0) AS participant_count,
COALESCE((SELECT COUNT(*) FROM meeting_recordings mrec WHERE mrec.room_id = mr.id), 0) AS recording_count,
mr.scheduled_at, mr.started_at, mr.ended_at, mr.ended_reason, mr.created_at
FROM meeting_rooms mr
LEFT JOIN auth_users u ON u.id = mr.host_id
WHERE ` + where
// HasRecording 过滤:包成外层 SELECT * FROM (...) WHERE recording_count > 0
listSQL := baseSelect
countSQL := `SELECT COUNT(*) FROM (` + baseSelect + `) AS sub`
if req.HasRecording != nil && *req.HasRecording {
listSQL = `SELECT * FROM (` + baseSelect + `) AS sub WHERE recording_count > 0`
countSQL = `SELECT COUNT(*) FROM (` + listSQL + `) AS sub2`
}
listSQL += ` ORDER BY created_at DESC OFFSET ? LIMIT ?`
var total int64
if err := d.db.WithContext(ctx).Raw(countSQL, args...).Scan(&total).Error; err != nil {
logs.Error(ctx, funcName, "统计会议总数失败", zap.Error(err))
return nil, 0, err
}
pageArgs := append([]any{}, args...)
pageArgs = append(pageArgs, (page-1)*pageSize, pageSize)
var rows []MeetingListRow
if err := d.db.WithContext(ctx).Raw(listSQL, pageArgs...).Scan(&rows).Error; err != nil {
logs.Error(ctx, funcName, "查询会议列表失败", zap.Error(err))
return nil, 0, err
}
return rows, total, nil
}
// GetMeetingByID 取单条会议聚合行(含主持人昵称 + 聚合计数)
// 供详情接口复用 MeetingListRow 投影,避免重复定义结构
func (d *MeetingManageDAO) GetMeetingByID(ctx context.Context, id int64) (*MeetingListRow, error) {
funcName := "dao.meeting_manage_dao.GetMeetingByID"
sql := `
SELECT
mr.id, mr.room_code, mr.title, mr.type, mr.status, mr.host_id,
COALESCE(u.nickname, '') AS host_nickname,
COALESCE(u.avatar, '') AS host_avatar,
mr.max_members,
COALESCE((SELECT COUNT(DISTINCT mp.user_id) FROM meeting_participants mp WHERE mp.room_id = mr.id), 0) AS participant_count,
COALESCE((SELECT COUNT(*) FROM meeting_recordings mrec WHERE mrec.room_id = mr.id), 0) AS recording_count,
mr.scheduled_at, mr.started_at, mr.ended_at, mr.ended_reason, mr.created_at
FROM meeting_rooms mr
LEFT JOIN auth_users u ON u.id = mr.host_id
WHERE mr.id = ?
LIMIT 1
`
var row MeetingListRow
err := d.db.WithContext(ctx).Raw(sql, id).Scan(&row).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
logs.Error(ctx, funcName, "查询会议详情失败", zap.Int64("id", id), zap.Error(err))
return nil, err
}
if row.ID == 0 {
return nil, nil
}
return &row, nil
}
// ListParticipantsDetail 拉取会议的全部参与者(带用户昵称/头像)
//
// 按 joined_at ASC 排序,主持人先入会后退出时也按时间顺序展示,便于核对入会轨迹
func (d *MeetingManageDAO) ListParticipantsDetail(ctx context.Context, roomID int64) ([]ParticipantDetailRow, error) {
funcName := "dao.meeting_manage_dao.ListParticipantsDetail"
sql := `
SELECT
mp.user_id, mp.role, mp.joined_at, mp.left_at, mp.left_reason, mp.duration,
COALESCE(u.nickname, '') AS nickname,
COALESCE(u.avatar_url, '') AS avatar
FROM meeting_participants mp
LEFT JOIN auth_users u ON u.id = mp.user_id
WHERE mp.room_id = ?
ORDER BY mp.joined_at ASC, mp.id ASC
`
var rows []ParticipantDetailRow
if err := d.db.WithContext(ctx).Raw(sql, roomID).Scan(&rows).Error; err != nil {
logs.Error(ctx, funcName, "查询会议参与者失败", zap.Int64("room_id", roomID), zap.Error(err))
return nil, err
}
return rows, nil
}
// ParticipantDetailRow 参与者明细投影
type ParticipantDetailRow struct {
UserID int64 `gorm:"column:user_id"`
Role int `gorm:"column:role"`
JoinedAt time.Time `gorm:"column:joined_at"`
LeftAt *time.Time `gorm:"column:left_at"`
LeftReason *string `gorm:"column:left_reason"`
Duration int `gorm:"column:duration"`
Nickname string `gorm:"column:nickname"`
Avatar string `gorm:"column:avatar"`
}
// ListRecordingsByRoom 拉取会议下的全部录制admin 视角,按 started_at DESC
//
// 复用 meeting/model.MeetingRecording不另起 DTOservice 层再投影成 AdminMeetingRecordingDTO
func (d *MeetingManageDAO) ListRecordingsByRoom(ctx context.Context, roomID int64) ([]meetingModel.MeetingRecording, error) {
var list []meetingModel.MeetingRecording
err := d.db.WithContext(ctx).
Where("room_id = ?", roomID).
Order("started_at DESC, id DESC").
Find(&list).Error
return list, err
}
// CountTotal 历史会议总数
func (d *MeetingManageDAO) CountTotal(ctx context.Context) (int64, error) {
var count int64
err := d.db.WithContext(ctx).Model(&meetingModel.MeetingRoom{}).Count(&count).Error
return count, err
}
// CountActive 当前进行中status=1的会议数
func (d *MeetingManageDAO) CountActive(ctx context.Context) (int64, error) {
var count int64
err := d.db.WithContext(ctx).
Model(&meetingModel.MeetingRoom{}).
Where("status = ?", 1).
Count(&count).Error
return count, err
}
// CountToday 今日创建的会议数(按 created_at
func (d *MeetingManageDAO) CountToday(ctx context.Context) (int64, error) {
var count int64
today := time.Now().Truncate(24 * time.Hour)
err := d.db.WithContext(ctx).
Model(&meetingModel.MeetingRoom{}).
Where("created_at >= ?", today).
Count(&count).Error
return count, err
}
// CountRecordings 录制总数
func (d *MeetingManageDAO) CountRecordings(ctx context.Context) (int64, error) {
var count int64
err := d.db.WithContext(ctx).Model(&meetingModel.MeetingRecording{}).Count(&count).Error
return count, err
}