271 lines
7.4 KiB
Go
271 lines
7.4 KiB
Go
// Package service 提供 admin 模块的核心业务逻辑
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
adminDAO "github.com/echochat/backend/app/admin/dao"
|
|
authDAO "github.com/echochat/backend/app/auth/dao"
|
|
imDAO "github.com/echochat/backend/app/im/dao"
|
|
"github.com/echochat/backend/app/constants"
|
|
"github.com/echochat/backend/app/dto"
|
|
"github.com/echochat/backend/pkg/logs"
|
|
"github.com/echochat/backend/pkg/ws"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
var (
|
|
ErrMessageNotFound = errors.New("消息不存在")
|
|
ErrMessageRecalled = errors.New("消息已被撤回")
|
|
ErrMessageDeleted = errors.New("消息已被删除")
|
|
)
|
|
|
|
// MessageManageService 管理端消息管理服务
|
|
type MessageManageService struct {
|
|
msgDAO *adminDAO.MessageManageDAO
|
|
userDAO *authDAO.UserDAO
|
|
convDAO *imDAO.ConversationDAO
|
|
pubSub *ws.PubSub
|
|
}
|
|
|
|
// NewMessageManageService 创建 MessageManageService 实例
|
|
func NewMessageManageService(msgDAO *adminDAO.MessageManageDAO, userDAO *authDAO.UserDAO, convDAO *imDAO.ConversationDAO, pubSub *ws.PubSub) *MessageManageService {
|
|
return &MessageManageService{
|
|
msgDAO: msgDAO,
|
|
userDAO: userDAO,
|
|
convDAO: convDAO,
|
|
pubSub: pubSub,
|
|
}
|
|
}
|
|
|
|
// GetMessageList 获取消息列表(分页+筛选)
|
|
func (s *MessageManageService) GetMessageList(ctx context.Context, req *dto.AdminMessageListRequest) (*dto.AdminMessageListResponse, error) {
|
|
funcName := "service.message_manage_service.GetMessageList"
|
|
logs.Debug(ctx, funcName, "获取消息列表")
|
|
|
|
if req.Page <= 0 {
|
|
req.Page = 1
|
|
}
|
|
if req.PageSize <= 0 {
|
|
req.PageSize = 20
|
|
}
|
|
|
|
messages, total, err := s.msgDAO.ListMessages(ctx, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
senderIDs := make([]int64, 0, len(messages))
|
|
senderSet := make(map[int64]bool)
|
|
for _, m := range messages {
|
|
if m.SenderID > 0 && !senderSet[m.SenderID] {
|
|
senderIDs = append(senderIDs, m.SenderID)
|
|
senderSet[m.SenderID] = true
|
|
}
|
|
}
|
|
|
|
userMap := make(map[int64]struct {
|
|
Nickname string
|
|
Avatar string
|
|
})
|
|
if len(senderIDs) > 0 {
|
|
users, uErr := s.userDAO.FindByIDs(ctx, senderIDs)
|
|
if uErr != nil {
|
|
logs.Error(ctx, funcName, "批量查询用户信息失败", zap.Error(uErr))
|
|
} else {
|
|
for _, u := range users {
|
|
userMap[u.ID] = struct {
|
|
Nickname string
|
|
Avatar string
|
|
}{Nickname: u.Nickname, Avatar: u.Avatar}
|
|
}
|
|
}
|
|
}
|
|
|
|
list := make([]dto.AdminMessageDTO, 0, len(messages))
|
|
for _, m := range messages {
|
|
item := dto.AdminMessageDTO{
|
|
ID: m.ID,
|
|
ConversationID: m.ConversationID,
|
|
SenderID: m.SenderID,
|
|
Type: m.Type,
|
|
TypeLabel: constants.MessageTypeMap[m.Type],
|
|
Content: m.Content,
|
|
Extra: m.Extra,
|
|
Status: m.Status,
|
|
StatusLabel: constants.MessageStatusMap[m.Status],
|
|
CreatedAt: m.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
}
|
|
if info, ok := userMap[m.SenderID]; ok {
|
|
item.SenderNickname = info.Nickname
|
|
item.SenderAvatar = info.Avatar
|
|
}
|
|
list = append(list, item)
|
|
}
|
|
|
|
return &dto.AdminMessageListResponse{
|
|
Total: total,
|
|
List: list,
|
|
Page: req.Page,
|
|
PageSize: req.PageSize,
|
|
}, nil
|
|
}
|
|
|
|
// GetMessageDetail 获取消息详情
|
|
func (s *MessageManageService) GetMessageDetail(ctx context.Context, id int64) (*dto.AdminMessageDTO, error) {
|
|
funcName := "service.message_manage_service.GetMessageDetail"
|
|
logs.Debug(ctx, funcName, "获取消息详情", zap.Int64("id", id))
|
|
|
|
msg, err := s.msgDAO.GetMessageByID(ctx, id)
|
|
if err != nil {
|
|
return nil, ErrMessageNotFound
|
|
}
|
|
|
|
item := &dto.AdminMessageDTO{
|
|
ID: msg.ID,
|
|
ConversationID: msg.ConversationID,
|
|
SenderID: msg.SenderID,
|
|
Type: msg.Type,
|
|
TypeLabel: constants.MessageTypeMap[msg.Type],
|
|
Content: msg.Content,
|
|
Extra: msg.Extra,
|
|
Status: msg.Status,
|
|
StatusLabel: constants.MessageStatusMap[msg.Status],
|
|
CreatedAt: msg.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
}
|
|
|
|
if msg.SenderID > 0 {
|
|
users, uErr := s.userDAO.FindByIDs(ctx, []int64{msg.SenderID})
|
|
if uErr == nil && len(users) > 0 {
|
|
item.SenderNickname = users[0].Nickname
|
|
item.SenderAvatar = users[0].Avatar
|
|
}
|
|
}
|
|
|
|
return item, nil
|
|
}
|
|
|
|
// DeleteMessage 删除消息(软删除,状态改为已删除)
|
|
func (s *MessageManageService) DeleteMessage(ctx context.Context, id int64) error {
|
|
funcName := "service.message_manage_service.DeleteMessage"
|
|
logs.Info(ctx, funcName, "删除消息", zap.Int64("id", id))
|
|
|
|
msg, err := s.msgDAO.GetMessageByID(ctx, id)
|
|
if err != nil {
|
|
return ErrMessageNotFound
|
|
}
|
|
if msg.Status == constants.MessageStatusDeleted {
|
|
return ErrMessageDeleted
|
|
}
|
|
|
|
return s.msgDAO.UpdateMessageStatus(ctx, id, constants.MessageStatusDeleted)
|
|
}
|
|
|
|
// RecallMessage 管理员撤回消息(更新状态 + 推送 WS 通知给会话成员)
|
|
func (s *MessageManageService) RecallMessage(ctx context.Context, id int64) error {
|
|
funcName := "service.message_manage_service.RecallMessage"
|
|
logs.Info(ctx, funcName, "撤回消息", zap.Int64("id", id))
|
|
|
|
msg, err := s.msgDAO.GetMessageByID(ctx, id)
|
|
if err != nil {
|
|
return ErrMessageNotFound
|
|
}
|
|
if msg.Status == constants.MessageStatusRecalled {
|
|
return ErrMessageRecalled
|
|
}
|
|
if msg.Status == constants.MessageStatusDeleted {
|
|
return ErrMessageDeleted
|
|
}
|
|
|
|
if err := s.msgDAO.UpdateMessageStatus(ctx, id, constants.MessageStatusRecalled); err != nil {
|
|
return err
|
|
}
|
|
|
|
go s.pushRecallNotification(ctx, msg.ID, msg.ConversationID, msg.SenderID)
|
|
|
|
return nil
|
|
}
|
|
|
|
// pushRecallNotification 向会话成员推送撤回通知
|
|
func (s *MessageManageService) pushRecallNotification(ctx context.Context, messageID, conversationID, senderID int64) {
|
|
funcName := "service.message_manage_service.pushRecallNotification"
|
|
|
|
memberIDs, err := s.convDAO.GetConversationMemberIDs(ctx, conversationID)
|
|
if err != nil {
|
|
logs.Error(ctx, funcName, "获取会话成员失败", zap.Error(err))
|
|
return
|
|
}
|
|
|
|
pushData := map[string]interface{}{
|
|
"message_id": messageID,
|
|
"conversation_id": conversationID,
|
|
"operator_id": int64(0),
|
|
"sender_id": senderID,
|
|
"recall_text": "管理员撤回了一条消息",
|
|
}
|
|
|
|
for _, uid := range memberIDs {
|
|
pushMsg := ws.NewPushMessage("im.message.recalled", pushData)
|
|
if err := s.pubSub.PublishToUser(ctx, uid, pushMsg); err != nil {
|
|
logs.Error(ctx, funcName, "推送撤回通知失败", zap.Int64("user_id", uid), zap.Error(err))
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetStats 获取消息统计数据
|
|
func (s *MessageManageService) GetStats(ctx context.Context, req *dto.AdminMessageStatsRequest) (*dto.AdminMessageStatsResponse, error) {
|
|
funcName := "service.message_manage_service.GetStats"
|
|
logs.Debug(ctx, funcName, "获取消息统计")
|
|
|
|
days := req.Days
|
|
if days <= 0 {
|
|
days = 7
|
|
}
|
|
if days > 90 {
|
|
days = 90
|
|
}
|
|
|
|
totalCount, err := s.msgDAO.GetTotalCount(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
todayCount, err := s.msgDAO.GetTodayCount(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
typeDist, err := s.msgDAO.GetTypeDistribution(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range typeDist {
|
|
typeDist[i].Label = constants.MessageTypeMap[typeDist[i].Type]
|
|
}
|
|
|
|
dailyTrend, err := s.msgDAO.GetDailyTrend(ctx, days)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
activeUsers, err := s.msgDAO.GetActiveUsers(ctx, days)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
activeGroups, err := s.msgDAO.GetActiveGroups(ctx, days)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &dto.AdminMessageStatsResponse{
|
|
TotalCount: totalCount,
|
|
TodayCount: todayCount,
|
|
TypeDistribution: typeDist,
|
|
DailyTrend: dailyTrend,
|
|
ActiveUsers: activeUsers,
|
|
ActiveGroups: activeGroups,
|
|
}, nil
|
|
}
|