package user import ( "context" "net/url" "strings" "unicode/utf8" "hyapp/pkg/idgen" "hyapp/pkg/roomid" "hyapp/pkg/xerr" userdomain "hyapp/services/user-service/internal/domain/user" ) const ( maxReportReasonRunes = 512 maxReportImages = 9 maxReportImageURLLen = 1024 ) var validReportTypes = map[string]struct{}{ "politics": {}, "pornography": {}, "graphic_violence": {}, "verbal_abuse": {}, "fraud": {}, "illegal_content": {}, "minors": {}, "in_person_transactions": {}, "other": {}, } // SubmitReport 校验 App 举报目标和证据 URL,并把不可变举报事实交给 user repository 持久化。 func (s *Service) SubmitReport(ctx context.Context, reporterUserID int64, userID int64, roomID string, reportType string, reason string, imageURLs []string, requestID string) (userdomain.Report, error) { if s.userRepository == nil { return userdomain.Report{}, xerr.New(xerr.Unavailable, "user repository is not configured") } reporterUserID, userID, roomID, reportType, reason, imageURLs, targetType, err := normalizeReportInput(reporterUserID, userID, roomID, reportType, reason, imageURLs) if err != nil { return userdomain.Report{}, err } nowMs := s.now().UnixMilli() report := userdomain.Report{ ReportID: idgen.New("rpt"), ReporterUserID: reporterUserID, TargetType: targetType, UserID: userID, RoomID: roomID, ReportType: reportType, Reason: reason, ImageURLs: imageURLs, Status: userdomain.ReportStatusSubmitted, RequestID: strings.TrimSpace(requestID), CreatedAtMs: nowMs, UpdatedAtMs: nowMs, } return s.userRepository.SubmitReport(ctx, report) } func normalizeReportInput(reporterUserID int64, userID int64, roomID string, reportType string, reason string, imageURLs []string) (int64, int64, string, string, string, []string, string, error) { roomID = strings.TrimSpace(roomID) reportType = strings.ToLower(strings.TrimSpace(reportType)) reason = strings.TrimSpace(reason) if reporterUserID <= 0 { return 0, 0, "", "", "", nil, "", xerr.New(xerr.InvalidArgument, "reporter_user_id is required") } hasUser := userID > 0 hasRoom := roomID != "" if hasUser == hasRoom { return 0, 0, "", "", "", nil, "", xerr.New(xerr.InvalidArgument, "user_id and room_id must be exactly one") } if hasUser && userID == reporterUserID { return 0, 0, "", "", "", nil, "", xerr.New(xerr.InvalidArgument, "cannot report self") } targetType := userdomain.ReportTargetUser if hasRoom { if !roomid.ValidStringID(roomID) { return 0, 0, "", "", "", nil, "", xerr.New(xerr.InvalidArgument, "room_id is invalid") } targetType = userdomain.ReportTargetRoom } if _, ok := validReportTypes[reportType]; !ok { return 0, 0, "", "", "", nil, "", xerr.New(xerr.InvalidArgument, "report_type is invalid") } if utf8.RuneCountInString(reason) > maxReportReasonRunes { return 0, 0, "", "", "", nil, "", xerr.New(xerr.InvalidArgument, "reason is too long") } normalizedImages, err := normalizeReportImageURLs(imageURLs) if err != nil { return 0, 0, "", "", "", nil, "", err } return reporterUserID, userID, roomID, reportType, reason, normalizedImages, targetType, nil } func normalizeReportImageURLs(imageURLs []string) ([]string, error) { if len(imageURLs) > maxReportImages { return nil, xerr.New(xerr.InvalidArgument, "too many report images") } normalized := make([]string, 0, len(imageURLs)) for _, raw := range imageURLs { value := strings.TrimSpace(raw) if value == "" { continue } if len(value) > maxReportImageURLLen { return nil, xerr.New(xerr.InvalidArgument, "image url is too long") } parsed, err := url.Parse(value) if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") { return nil, xerr.New(xerr.InvalidArgument, "image url is invalid") } normalized = append(normalized, value) } return normalized, nil }