269 lines
7.1 KiB
Go
269 lines
7.1 KiB
Go
// 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 转成普通 string;nil 返回空串
|
||
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_reason;recording 状态下 FailureReason 字段被复用为本地路径,不应外泄
|
||
if r.Status == meetingModel.MeetingRecordingStatusFailed {
|
||
out.FailureReason = r.FailureReason
|
||
}
|
||
return out
|
||
}
|