package usecase import ( "context" "strings" "unicode/utf8" "cmroubao/backend-api/internal/domain" ) const candidateReasonSchemaVersion = 1 type CandidateHumanReviewItemInput struct { CandidateOrdinal int `json:"candidate_ordinal"` Label string `json:"label"` PrimaryReasonCode string `json:"primary_reason_code"` ReasonCodes []string `json:"reason_codes"` Note string `json:"note"` } type StoreCandidateHumanReviewCommand struct { Identity ExecutionResultIdentity TaskContentSHA256 string ReasonSchemaVersion int Outcome string SelectedCandidateOrdinal *int PrimaryReasonCode string Note string SupersedesReviewID *string Items []CandidateHumanReviewItemInput } type StoreCandidateHumanReviewResult struct { Review domain.CandidateHumanReview Replayed bool } func (service *ExecutionResultService) StoreHumanReview( ctx context.Context, command StoreCandidateHumanReviewCommand, ) (StoreCandidateHumanReviewResult, error) { identity, err := normalizeExecutionIdentity(command.Identity) if err != nil { return StoreCandidateHumanReviewResult{}, err } command.Identity = identity if err := validateCandidateHumanReview(command); err != nil { return StoreCandidateHumanReviewResult{}, err } reviewID, err := service.ids.NewID() if err != nil { return StoreCandidateHumanReviewResult{}, internalExecutionResultFailure(err) } now := service.clock.Now().UTC() requestHash, err := executionResultHash(command) if err != nil { return StoreCandidateHumanReviewResult{}, internalExecutionResultFailure(err) } deviceID := identity.DeviceID review := domain.CandidateHumanReview{ ID: reviewID, TaskID: identity.TaskID, ExecutionID: identity.ExecutionID, TaskContentSHA256: command.TaskContentSHA256, ReasonSchemaVersion: command.ReasonSchemaVersion, Outcome: command.Outcome, SelectedCandidateOrdinal: command.SelectedCandidateOrdinal, PrimaryReasonCode: strings.TrimSpace(command.PrimaryReasonCode), Note: strings.TrimSpace(command.Note), SupersedesReviewID: trimmedOptional(command.SupersedesReviewID), ActorUserID: identity.UserID, ActorDeviceID: &deviceID, CreatedAt: now, Items: make([]domain.CandidateHumanReviewItem, 0, len(command.Items)), } for _, item := range command.Items { review.Items = append(review.Items, domain.CandidateHumanReviewItem{ CandidateOrdinal: item.CandidateOrdinal, Label: item.Label, PrimaryReasonCode: item.PrimaryReasonCode, ReasonCodes: append([]string(nil), item.ReasonCodes...), Note: strings.TrimSpace(item.Note), }) } stored, replayed, err := service.repository.StoreCandidateHumanReview( ctx, service.write( identity, executionResultHumanReviewOperation, requestHash, now, ), review, ) if err != nil { return StoreCandidateHumanReviewResult{}, wrapLifecycleRepositoryError(err) } return StoreCandidateHumanReviewResult{ Review: stored, Replayed: replayed, }, nil } func validateCandidateHumanReview( command StoreCandidateHumanReviewCommand, ) error { if !sha256Pattern.MatchString(command.TaskContentSHA256) { return executionResultInvalid( "task_content_sha256", "must be lowercase SHA-256", ) } if command.ReasonSchemaVersion != candidateReasonSchemaVersion { return executionResultInvalid( "reason_schema_version", "must be 1", ) } if !validOutcome(command.Outcome) { return executionResultInvalid("outcome", "is invalid") } primary := strings.TrimSpace(command.PrimaryReasonCode) if !validReviewPrimaryReason(command.Outcome, primary) || !validReviewNote(primary, command.Note) { return executionResultInvalid("primary_reason_code", "is invalid") } if command.SupersedesReviewID != nil && !isUUID(strings.TrimSpace(*command.SupersedesReviewID)) { return executionResultInvalid("supersedes_review_id", "must be a UUID") } if len(command.Items) > 5 { return executionResultInvalid("items", "must contain at most 5 items") } seen := make(map[int]struct{}, len(command.Items)) accepted := 0 for _, item := range command.Items { if item.CandidateOrdinal < 1 || item.CandidateOrdinal > 5 { return executionResultInvalid( "items", "candidate_ordinal must be between 1 and 5", ) } if _, found := seen[item.CandidateOrdinal]; found { return executionResultInvalid("items", "candidate_ordinal must be unique") } seen[item.CandidateOrdinal] = struct{}{} if item.Label != "ACCEPT" && item.Label != "REJECT" { return executionResultInvalid("items", "label must be ACCEPT or REJECT") } if item.Label == "ACCEPT" { accepted++ } if !validHumanReviewItem(item) { return executionResultInvalid("items", "contains invalid reasons") } } if command.Outcome == "CANDIDATE_ACCEPTED" { if command.SelectedCandidateOrdinal == nil || accepted != 1 { return executionResultInvalid( "selected_candidate_ordinal", "must identify the single accepted item", ) } _, found := seen[*command.SelectedCandidateOrdinal] if !found || !itemAccepted(command.Items, *command.SelectedCandidateOrdinal) { return executionResultInvalid( "selected_candidate_ordinal", "must identify the single accepted item", ) } } else { if command.SelectedCandidateOrdinal != nil || accepted != 0 { return executionResultInvalid( "items", "non-accepted outcomes may contain only rejected items", ) } if len(command.Items) == 0 && command.Outcome != "NO_MATCH" && command.Outcome != "MANUAL_REQUIRED" { return executionResultInvalid( "items", "empty reviews require NO_MATCH or MANUAL_REQUIRED", ) } } return nil } func validHumanReviewItem(item CandidateHumanReviewItemInput) bool { primary := strings.TrimSpace(item.PrimaryReasonCode) if len(item.ReasonCodes) < 1 || len(item.ReasonCodes) > 8 { return false } seen := map[string]struct{}{} containsPrimary := false for _, candidate := range item.ReasonCodes { code := strings.TrimSpace(candidate) if !validItemReason(item.Label, code) { return false } if _, duplicate := seen[code]; duplicate { return false } seen[code] = struct{}{} containsPrimary = containsPrimary || code == primary } return containsPrimary && validReviewNote(primary, item.Note) } func validReviewPrimaryReason(outcome string, code string) bool { switch outcome { case "CANDIDATE_ACCEPTED": return code == "SELECTED_BEST_MATCH" || code == "OTHER" case "CANDIDATE_REJECTED", "NO_MATCH": return code == "NO_ACCEPTABLE_CANDIDATE" || code == "OTHER" case "MANUAL_REQUIRED": return code == "INSUFFICIENT_EVIDENCE" || code == "OTHER" default: return false } } func validItemReason(label string, code string) bool { if label == "ACCEPT" { _, found := acceptReasonCodes[code] return found } _, found := rejectReasonCodes[code] return found } func validReviewNote(primaryReason string, value string) bool { value = strings.TrimSpace(value) if !utf8.ValidString(value) || utf8.RuneCountInString(value) > 200 || len([]byte(value)) > 800 { return false } if primaryReason == "OTHER" { return utf8.RuneCountInString(value) >= 4 } return true } func itemAccepted(items []CandidateHumanReviewItemInput, ordinal int) bool { for _, item := range items { if item.CandidateOrdinal == ordinal { return item.Label == "ACCEPT" } } return false } func trimmedOptional(value *string) *string { if value == nil { return nil } trimmed := strings.TrimSpace(*value) return &trimmed } var acceptReasonCodes = map[string]struct{}{ "SKU_MATCH": {}, "IMAGE_MATCH": {}, "PRICE_ACCEPTABLE": {}, "EVIDENCE_SUFFICIENT": {}, "OTHER": {}, } var rejectReasonCodes = map[string]struct{}{ "SKU_MISMATCH": {}, "IMAGE_MISMATCH": {}, "PRICE_TOO_HIGH": {}, "OUT_OF_STOCK": {}, "EVIDENCE_INSUFFICIENT": {}, "NOT_BEST_MATCH": {}, "OTHER": {}, }