package report import ( "context" "database/sql" "encoding/json" "fmt" "strconv" "strings" "time" "hyapp-admin-server/internal/integration/roomclient" "hyapp-admin-server/internal/modules/shared" ) const ( targetTypeUser = "user" targetTypeRoom = "room" ) type Service struct { roomClient roomclient.Client userDB *sql.DB } func NewService(userDB *sql.DB, roomClient roomclient.Client) *Service { return &Service{userDB: userDB, roomClient: roomClient} } func (s *Service) ListReports(ctx context.Context, appCode string, query listQuery) ([]reportDTO, int64, error) { query = normalizeListQuery(query) if s == nil || s.userDB == nil { return nil, 0, fmt.Errorf("user mysql is not configured") } whereSQL, args := reportWhere(appCode, query) var total int64 if err := s.userDB.QueryRowContext(ctx, ` SELECT COUNT(*) FROM user_reports r LEFT JOIN users u ON u.app_code = r.app_code AND u.user_id = r.target_user_id `+whereSQL, args..., ).Scan(&total); err != nil { return nil, 0, err } rows, err := s.userDB.QueryContext(ctx, ` SELECT r.report_id, r.reporter_user_id, r.target_type, r.target_user_id, r.room_id, r.report_type, r.reason, CAST(r.image_urls_json AS CHAR), r.status, r.request_id, r.created_at_ms, r.updated_at_ms, COALESCE(u.current_display_user_id, ''), COALESCE(u.default_display_user_id, ''), COALESCE(u.username, ''), COALESCE(u.avatar, '') FROM user_reports r LEFT JOIN users u ON u.app_code = r.app_code AND u.user_id = r.target_user_id `+whereSQL+` ORDER BY r.created_at_ms DESC, r.report_id DESC LIMIT ? OFFSET ?`, append(args, query.PageSize, offset(query.Page, query.PageSize))..., ) if err != nil { return nil, 0, err } defer rows.Close() items := make([]reportDTO, 0, query.PageSize) for rows.Next() { var item reportDTO var reporterUserID int64 var targetUserID sql.NullInt64 var imageURLsJSON string var displayUserID string var defaultDisplayUserID string var username string var avatar string if err := rows.Scan( &item.ReportID, &reporterUserID, &item.TargetType, &targetUserID, &item.RoomID, &item.ReportType, &item.Reason, &imageURLsJSON, &item.Status, &item.RequestID, &item.CreatedAtMS, &item.UpdatedAtMS, &displayUserID, &defaultDisplayUserID, &username, &avatar, ); err != nil { return nil, 0, err } imageURLs, err := parseImageURLs(imageURLsJSON) if err != nil { return nil, 0, err } item.ReporterUserID = formatID(reporterUserID) item.ImageURLs = imageURLs item.Target = reportTargetDTO{Type: item.TargetType} if targetUserID.Valid && targetUserID.Int64 > 0 { item.UserID = formatID(targetUserID.Int64) item.Target.User = &reportUserDTO{ Avatar: avatar, DefaultDisplayUserID: defaultDisplayUserID, DisplayUserID: displayUserID, UserID: item.UserID, Username: username, } } if item.TargetType == targetTypeRoom { item.Target.Room = &reportRoomDTO{RoomID: item.RoomID} } items = append(items, item) } if err := rows.Err(); err != nil { return nil, 0, err } if err := s.fillRoomTargets(ctx, items); err != nil { return nil, 0, err } return items, total, nil } func normalizeListQuery(query listQuery) listQuery { if query.Page < 1 { query.Page = 1 } if query.PageSize < 1 { query.PageSize = 20 } if query.PageSize > 100 { query.PageSize = 100 } query.Keyword = strings.TrimSpace(query.Keyword) query.TargetType = normalizeTargetType(query.TargetType) query.ReportType = strings.ToLower(strings.TrimSpace(query.ReportType)) if query.StartAtMS < 0 { query.StartAtMS = 0 } if query.EndAtMS < 0 { query.EndAtMS = 0 } return query } func normalizeTargetType(value string) string { switch strings.ToLower(strings.TrimSpace(value)) { case targetTypeUser, "用户": return targetTypeUser case targetTypeRoom, "房间": return targetTypeRoom default: return "" } } func reportWhere(appCode string, query listQuery) (string, []any) { conditions := []string{"r.app_code = ?"} args := []any{appCode} if query.TargetType != "" { conditions = append(conditions, "r.target_type = ?") args = append(args, query.TargetType) } if query.ReportType != "" { conditions = append(conditions, "r.report_type = ?") args = append(args, query.ReportType) } if query.StartAtMS > 0 { conditions = append(conditions, "r.created_at_ms >= ?") args = append(args, query.StartAtMS) } if query.EndAtMS > 0 { conditions = append(conditions, "r.created_at_ms < ?") args = append(args, query.EndAtMS) } if query.Keyword != "" { like := "%" + query.Keyword + "%" targetSQL, targetArgs := shared.UserIdentityKeywordSQL("u", "r.target_user_id", query.Keyword, time.Now().UnixMilli()) conditions = append(conditions, `(r.report_id LIKE ? OR r.room_id LIKE ? OR CAST(r.reporter_user_id AS CHAR) LIKE ? OR `+targetSQL+`)`) args = append(args, like, like, like) args = append(args, targetArgs...) } return "WHERE " + strings.Join(conditions, " AND "), args } func parseImageURLs(raw string) ([]string, error) { if strings.TrimSpace(raw) == "" { return []string{}, nil } var urls []string if err := json.Unmarshal([]byte(raw), &urls); err != nil { return nil, err } if urls == nil { return []string{}, nil } return urls, nil } func (s *Service) fillRoomTargets(ctx context.Context, items []reportDTO) error { if s == nil || s.roomClient == nil || len(items) == 0 { return nil } roomIDs := make([]string, 0, len(items)) for _, item := range items { if item.TargetType == targetTypeRoom && strings.TrimSpace(item.RoomID) != "" { roomIDs = append(roomIDs, item.RoomID) } } rooms, err := s.queryRooms(ctx, uniqueStrings(roomIDs)) if err != nil { return err } for index := range items { if items[index].TargetType != targetTypeRoom { continue } room := reportRoomDTO{RoomID: items[index].RoomID} if found, ok := rooms[items[index].RoomID]; ok { room = found } items[index].Target.Room = &room } return nil } func (s *Service) queryRooms(ctx context.Context, roomIDs []string) (map[string]reportRoomDTO, error) { rooms := make(map[string]reportRoomDTO, len(roomIDs)) for _, roomID := range roomIDs { room, err := s.roomClient.GetRoom(ctx, roomclient.GetRoomRequest{RoomID: roomID}) if err != nil { // 举报列表是治理入口,房间被删或房间服务短暂不可读时仍保留举报事实本身。 if ctx.Err() != nil { return nil, ctx.Err() } continue } rooms[roomID] = reportRoomDTO{ CoverURL: room.CoverURL, RoomID: room.RoomID, RoomShortID: room.RoomShortID, Title: room.Title, } } return rooms, nil } func formatID(value int64) string { if value <= 0 { return "" } return strconv.FormatInt(value, 10) } func uniqueStrings(values []string) []string { seen := make(map[string]struct{}, len(values)) unique := make([]string, 0, len(values)) for _, value := range values { value = strings.TrimSpace(value) if value == "" { continue } if _, ok := seen[value]; ok { continue } seen[value] = struct{}{} unique = append(unique, value) } return unique } func offset(page int, pageSize int) int { return (page - 1) * pageSize }