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

269 lines
7.1 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 提供 admin 模块的业务服务(会议管理)
package service
import (
"context"
"errors"
"time"
adminDAO "github.com/echochat/backend/app/admin/dao"
"github.com/echochat/backend/app/dto"
meetingModel "github.com/echochat/backend/app/meeting/model"
"github.com/echochat/backend/pkg/logs"
"go.uber.org/zap"
)
// ErrMeetingNotFound 会议记录不存在
var ErrMeetingNotFound = errors.New("会议记录不存在")
// MeetingManageService 管理端会议查询服务
//
// 仅做"读"路径:列表 / 详情 / 统计。
// "强制结束会议" 这种写操作直接复用 meeting/service.MeetingService.EndRoom 即可admin_force 路径),
// 暂不在本服务里重复包装,避免与现有 RBAC/中间件冲突。
type MeetingManageService struct {
dao *adminDAO.MeetingManageDAO
}
// NewMeetingManageService Wire Provider
func NewMeetingManageService(dao *adminDAO.MeetingManageDAO) *MeetingManageService {
return &MeetingManageService{dao: dao}
}
// ---------- public methods ----------
// GetMeetingList 查询会议列表(分页 + 多条件)
func (s *MeetingManageService) GetMeetingList(ctx context.Context, req *dto.AdminMeetingListRequest) (*dto.AdminMeetingListResponse, error) {
funcName := "service.meeting_manage_service.GetMeetingList"
logs.Debug(ctx, funcName, "获取会议列表")
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 {
req.PageSize = 20
}
rows, total, err := s.dao.ListMeetings(ctx, req)
if err != nil {
return nil, err
}
list := make([]dto.AdminMeetingDTO, 0, len(rows))
for i := range rows {
list = append(list, rowToDTO(&rows[i]))
}
return &dto.AdminMeetingListResponse{
Total: total,
List: list,
Page: req.Page,
PageSize: req.PageSize,
}, nil
}
// GetMeetingDetail 查询会议详情(含参与者 + 录制)
func (s *MeetingManageService) GetMeetingDetail(ctx context.Context, id int64) (*dto.AdminMeetingDetailResponse, error) {
funcName := "service.meeting_manage_service.GetMeetingDetail"
logs.Debug(ctx, funcName, "获取会议详情", zap.Int64("id", id))
row, err := s.dao.GetMeetingByID(ctx, id)
if err != nil {
return nil, err
}
if row == nil {
return nil, ErrMeetingNotFound
}
participants, err := s.dao.ListParticipantsDetail(ctx, row.ID)
if err != nil {
logs.Warn(ctx, funcName, "拉取参与者明细失败(继续返回详情)",
zap.Int64("room_id", row.ID), zap.Error(err))
participants = nil
}
recordings, err := s.dao.ListRecordingsByRoom(ctx, row.ID)
if err != nil {
logs.Warn(ctx, funcName, "拉取录制列表失败(继续返回详情)",
zap.Int64("room_id", row.ID), zap.Error(err))
recordings = nil
}
pDTOs := make([]dto.AdminMeetingParticipantDTO, 0, len(participants))
for i := range participants {
pDTOs = append(pDTOs, participantToDTO(&participants[i]))
}
rDTOs := make([]dto.AdminMeetingRecordingDTO, 0, len(recordings))
for i := range recordings {
rDTOs = append(rDTOs, recordingToDTO(&recordings[i]))
}
return &dto.AdminMeetingDetailResponse{
AdminMeetingDTO: rowToDTO(row),
Participants: pDTOs,
Recordings: rDTOs,
}, nil
}
// GetStats 获取会议管理摘要统计(卡片)
func (s *MeetingManageService) GetStats(ctx context.Context) (*dto.AdminMeetingStatsResponse, error) {
funcName := "service.meeting_manage_service.GetStats"
logs.Debug(ctx, funcName, "获取会议统计")
total, err := s.dao.CountTotal(ctx)
if err != nil {
return nil, err
}
active, err := s.dao.CountActive(ctx)
if err != nil {
return nil, err
}
today, err := s.dao.CountToday(ctx)
if err != nil {
return nil, err
}
recordings, err := s.dao.CountRecordings(ctx)
if err != nil {
return nil, err
}
return &dto.AdminMeetingStatsResponse{
TotalCount: total,
ActiveCount: active,
TodayCount: today,
RecordingCount: recordings,
}, nil
}
// ---------- DTO 投影辅助 ----------
const datetimeLayout = "2006-01-02 15:04:05"
// formatTimePtr 把 *time.Time 转成展示字符串nil 返回空串
func formatTimePtr(t *time.Time) string {
if t == nil {
return ""
}
return t.Format(datetimeLayout)
}
// formatStrPtr 把 *string 转成普通 stringnil 返回空串
func formatStrPtr(s *string) string {
if s == nil {
return ""
}
return *s
}
// typeLabel 1=即时 2=预约
func typeLabel(t int) string {
switch t {
case 1:
return "即时会议"
case 2:
return "预约会议"
default:
return "未知"
}
}
// statusLabel 0=未开始 1=进行中 2=已结束(与 meeting_rooms.status 对齐)
func statusLabel(s int) string {
switch s {
case 0:
return "未开始"
case 1:
return "进行中"
case 2:
return "已结束"
default:
return "未知"
}
}
// roleLabel 0=普通 1=主持人 2=联合主持人
func roleLabel(r int) string {
switch r {
case 1:
return "主持人"
case 2:
return "联合主持人"
default:
return "参会人"
}
}
// computeDuration 计算会议时长(秒)
// - 已结束ended_at - started_at
// - 进行中now - started_at
// - 未开始0
func computeDuration(row *adminDAO.MeetingListRow) int {
if row.StartedAt == nil {
return 0
}
end := time.Now()
if row.EndedAt != nil {
end = *row.EndedAt
}
d := end.Sub(*row.StartedAt)
if d < 0 {
return 0
}
return int(d.Seconds())
}
func rowToDTO(row *adminDAO.MeetingListRow) dto.AdminMeetingDTO {
return dto.AdminMeetingDTO{
ID: row.ID,
RoomCode: row.RoomCode,
Title: row.Title,
Type: row.Type,
TypeLabel: typeLabel(row.Type),
Status: row.Status,
StatusLabel: statusLabel(row.Status),
HostID: row.HostID,
HostNickname: row.HostNickname,
HostAvatar: row.HostAvatar,
MaxMembers: row.MaxMembers,
ParticipantCount: row.ParticipantCount,
RecordingCount: row.RecordingCount,
DurationSec: computeDuration(row),
ScheduledAt: formatTimePtr(row.ScheduledAt),
StartedAt: formatTimePtr(row.StartedAt),
EndedAt: formatTimePtr(row.EndedAt),
EndedReason: formatStrPtr(row.EndedReason),
CreatedAt: row.CreatedAt.Format(datetimeLayout),
}
}
func participantToDTO(p *adminDAO.ParticipantDetailRow) dto.AdminMeetingParticipantDTO {
return dto.AdminMeetingParticipantDTO{
UserID: p.UserID,
Nickname: p.Nickname,
Avatar: p.Avatar,
Role: p.Role,
RoleLabel: roleLabel(p.Role),
JoinedAt: p.JoinedAt.Format(datetimeLayout),
LeftAt: formatTimePtr(p.LeftAt),
LeftReason: formatStrPtr(p.LeftReason),
Duration: p.Duration,
}
}
func recordingToDTO(r *meetingModel.MeetingRecording) dto.AdminMeetingRecordingDTO {
out := dto.AdminMeetingRecordingDTO{
ID: r.ID,
StartedBy: r.StartedBy,
Status: r.Status,
FileURL: r.FileURL,
SizeBytes: r.SizeBytes,
DurationSec: r.DurationSec,
StartedAt: r.StartedAt.Format(datetimeLayout),
StoppedAt: formatTimePtr(r.StoppedAt),
}
// 仅 failed 暴露 failure_reasonrecording 状态下 FailureReason 字段被复用为本地路径,不应外泄
if r.Status == meetingModel.MeetingRecordingStatusFailed {
out.FailureReason = r.FailureReason
}
return out
}