2026-05-29 19:56:14 +08:00

275 lines
7.1 KiB
Go

package report
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"strconv"
"strings"
"hyapp-admin-server/internal/integration/roomclient"
)
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 + "%"
conditions = append(conditions, `(r.report_id LIKE ? OR r.room_id LIKE ? OR CAST(r.target_user_id AS CHAR) LIKE ? OR CAST(r.reporter_user_id AS CHAR) LIKE ? OR u.current_display_user_id LIKE ? OR u.default_display_user_id LIKE ? OR u.username LIKE ?)`)
args = append(args, like, like, like, like, like, like, like)
}
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
}