package platform import ( "bytes" "context" "encoding/csv" "fmt" "sort" "strings" "time" ) type ReconciliationExportFile struct { Name string Content []byte RowCount int } func (s *Service) reconciliationStore() (ReconciliationStore, error) { store, ok := s.store.(ReconciliationStore) if !ok { return nil, fmt.Errorf("store does not provide reconciliation center") } return store, nil } func (s *Service) ReconciliationSummary(ctx context.Context, days int) (ReconciliationSummary, error) { store, err := s.reconciliationStore() if err != nil { return ReconciliationSummary{}, err } if days <= 0 || days > 90 { days = 30 } return store.ReconciliationSummary(ctx, days) } func (s *Service) ReconciliationIssues(ctx context.Context, query ReconciliationQuery) (Page[ReconciliationIssue], error) { store, err := s.reconciliationStore() if err != nil { return Page[ReconciliationIssue]{}, err } query.Keyword = strings.TrimSpace(query.Keyword) query.Scope = strings.ToLower(strings.TrimSpace(query.Scope)) if query.Scope != "archived" { query.Scope = "current" } query.RuleCode = strings.TrimSpace(query.RuleCode) query.Category = strings.TrimSpace(query.Category) query.Severity = strings.TrimSpace(query.Severity) query.Status = strings.TrimSpace(query.Status) query.Owner = strings.TrimSpace(query.Owner) query.SLA = strings.TrimSpace(query.SLA) if query.Limit <= 0 || query.Limit > 200 { query.Limit = 50 } if query.Offset < 0 { query.Offset = 0 } return store.ReconciliationIssues(ctx, query) } func (s *Service) ReconciliationAssignees(ctx context.Context, search string) ([]ReconciliationAssignee, error) { store, err := s.reconciliationStore() if err != nil { return nil, err } search = strings.TrimSpace(search) if len([]rune(search)) > 80 { return nil, clientError{Code: "RECONCILIATION_ASSIGNEE_SEARCH_TOO_LONG", Message: "负责人搜索不能超过 80 个字符"} } items, err := store.ReconciliationAssignees(ctx) if err != nil { return nil, err } byName := make(map[string]ReconciliationAssignee, len(items)+1) for _, item := range items { item.Name = strings.TrimSpace(item.Name) if item.Name != "" { byName[strings.ToLower(item.Name)] = item } } if principal, ok := PrincipalFromContext(ctx); ok { name := strings.TrimSpace(principal.Name) if name == "" { name = strings.TrimSpace(principal.Username) } if name != "" { key := strings.ToLower(name) item, exists := byName[key] if !exists { item = ReconciliationAssignee{Name: name, Source: "current"} } if item.Username == "" { item.Username = strings.TrimSpace(principal.Username) } item.Current = true byName[key] = item } } needle := strings.ToLower(search) result := make([]ReconciliationAssignee, 0, len(byName)) for _, item := range byName { if needle != "" && !strings.Contains(strings.ToLower(item.Name+" "+item.Username), needle) { continue } result = append(result, item) } sort.Slice(result, func(i, j int) bool { if result[i].Current != result[j].Current { return result[i].Current } if result[i].ActiveCount != result[j].ActiveCount { return result[i].ActiveCount > result[j].ActiveCount } return result[i].Name < result[j].Name }) if len(result) > 50 { result = result[:50] } return result, nil } func (s *Service) ExportReconciliationIssues(ctx context.Context, query ReconciliationQuery) (ReconciliationExportFile, error) { query.Offset = 0 query.Limit = 200 first, err := s.ReconciliationIssues(ctx, query) if err != nil { return ReconciliationExportFile{}, err } if first.Total > reconciliationFindingLimit { return ReconciliationExportFile{}, clientError{Code: "RECONCILIATION_EXPORT_TOO_LARGE", Message: "当前筛选超过 50,000 条,请缩小范围后导出"} } items := append([]ReconciliationIssue(nil), first.Items...) for offset := len(first.Items); offset < first.Total; offset += query.Limit { query.Offset = offset page, pageErr := s.ReconciliationIssues(ctx, query) if pageErr != nil { return ReconciliationExportFile{}, pageErr } items = append(items, page.Items...) if len(page.Items) == 0 { break } } var buffer bytes.Buffer buffer.WriteString("\xEF\xBB\xBF") writer := csv.NewWriter(&buffer) _ = writer.Write([]string{"差异编号", "等级", "状态", "规则", "问题", "VIN", "车牌", "来源 A", "来源 B", "负责人", "处理期限", "首次发现", "最近发现", "命中次数", "结论说明", "处置人", "归档时间", "归档人", "归档原因", "版本"}) for _, item := range items { _ = writer.Write([]string{ reconciliationCSVCell(item.ID), reconciliationCSVCell(reconciliationSeverityLabel(item.Severity)), reconciliationCSVCell(reconciliationStatusLabel(item.Status)), reconciliationCSVCell(item.RuleCode), reconciliationCSVCell(item.Title), reconciliationCSVCell(item.VIN), reconciliationCSVCell(item.Plate), reconciliationCSVCell(item.ProtocolA), reconciliationCSVCell(item.ProtocolB), reconciliationCSVCell(item.Assignee), reconciliationCSVCell(item.DueAt), reconciliationCSVCell(item.FirstSeenAt), reconciliationCSVCell(item.LastSeenAt), fmt.Sprintf("%d", item.OccurrenceCount), reconciliationCSVCell(item.ResolutionNote), reconciliationCSVCell(item.ResolvedBy), reconciliationCSVCell(item.ArchivedAt), reconciliationCSVCell(item.ArchivedBy), reconciliationCSVCell(item.ArchiveReason), fmt.Sprintf("%d", item.Version), }) } writer.Flush() if err := writer.Error(); err != nil { return ReconciliationExportFile{}, err } return ReconciliationExportFile{ Name: fmt.Sprintf("质量差异_%s.csv", time.Now().Format("20060102_150405")), Content: buffer.Bytes(), RowCount: len(items), }, nil } func reconciliationCSVCell(value string) string { value = strings.TrimSpace(value) if value != "" && strings.ContainsRune("=+-@", rune(value[0])) { return "'" + value } return value } func reconciliationSeverityLabel(value string) string { if label := map[string]string{"critical": "严重", "major": "重要", "minor": "一般"}[value]; label != "" { return label } return value } func reconciliationStatusLabel(value string) string { if label := map[string]string{"pending": "待处理", "confirmed_source_a": "确认来源 A", "confirmed_source_b": "确认来源 B", "no_action": "无需处理", "fixed": "已修复", "recovered": "已恢复"}[value]; label != "" { return label } return value } func normalizeReconciliationLifecycleRequest(ctx context.Context, request ReconciliationLifecycleRequest) (ReconciliationLifecycleRequest, error) { request.Reason = strings.TrimSpace(request.Reason) if request.Version <= 0 { return request, clientError{Code: "RECONCILIATION_VERSION_REQUIRED", Message: "差异记录版本不能为空"} } if len([]rune(request.Reason)) < 4 { return request, clientError{Code: "RECONCILIATION_ARCHIVE_REASON_REQUIRED", Message: "请填写至少 4 个字符的归档或恢复原因"} } if len([]rune(request.Reason)) > 500 { return request, clientError{Code: "RECONCILIATION_ARCHIVE_REASON_TOO_LONG", Message: "归档或恢复原因不能超过 500 字"} } if err := authorizeInternalOperations(ctx, true); err != nil { return request, clientError{Code: "PERMISSION_DENIED", Message: "只有管理员可以整理差异审计归档"} } request.Actor = ActorFromContext(ctx) return request, nil } func (s *Service) SetReconciliationIssueArchived(ctx context.Context, id string, archived bool, request ReconciliationLifecycleRequest) (ReconciliationIssue, error) { id = strings.TrimSpace(id) if id == "" || len(id) > 64 { return ReconciliationIssue{}, clientError{Code: "RECONCILIATION_ID_INVALID", Message: "差异记录编号无效"} } normalized, err := normalizeReconciliationLifecycleRequest(ctx, request) if err != nil { return ReconciliationIssue{}, err } store, err := s.reconciliationStore() if err != nil { return ReconciliationIssue{}, err } return store.SetReconciliationIssueArchived(ctx, id, archived, normalized) } func (s *Service) BatchSetReconciliationIssuesArchived(ctx context.Context, archived bool, request ReconciliationBatchLifecycleRequest) (ReconciliationBatchActionResult, error) { result := ReconciliationBatchActionResult{ Requested: len(request.Items), Succeeded: make([]ReconciliationIssue, 0, len(request.Items)), Skipped: make([]ReconciliationBatchActionFailure, 0), } if len(request.Items) == 0 { return result, clientError{Code: "RECONCILIATION_BATCH_EMPTY", Message: "请至少选择一条差异记录"} } if len(request.Items) > 20 { return result, clientError{Code: "RECONCILIATION_BATCH_TOO_LARGE", Message: "单次最多整理 20 条差异记录"} } seen := make(map[string]struct{}, len(request.Items)) for _, item := range request.Items { id := strings.TrimSpace(item.ID) if id == "" || len(id) > 64 { return result, clientError{Code: "RECONCILIATION_ID_INVALID", Message: "批量整理中包含无效的差异记录编号"} } if _, exists := seen[id]; exists { return result, clientError{Code: "RECONCILIATION_BATCH_DUPLICATE_ID", Message: "批量整理不能重复选择同一条差异记录"} } seen[id] = struct{}{} } for _, item := range request.Items { updated, err := s.SetReconciliationIssueArchived(ctx, item.ID, archived, ReconciliationLifecycleRequest{ Version: item.Version, Reason: request.Reason, }) if err == nil { result.Succeeded = append(result.Succeeded, updated) continue } failure := ReconciliationBatchActionFailure{ID: item.ID, Code: "RECONCILIATION_BATCH_ITEM_FAILED", Message: "整理失败,请刷新后重试"} if itemError, ok := asClientError(err); ok { failure.Code, failure.Message = itemError.Code, itemError.Message } result.Skipped = append(result.Skipped, failure) } return result, nil } func (s *Service) AssignReconciliationIssue(ctx context.Context, id string, request ReconciliationAssignmentRequest) (ReconciliationIssue, error) { id = strings.TrimSpace(id) if id == "" || len(id) > 64 { return ReconciliationIssue{}, clientError{Code: "RECONCILIATION_ID_INVALID", Message: "差异记录编号无效"} } request.Actor = ActorFromContext(ctx) store, err := s.reconciliationStore() if err != nil { return ReconciliationIssue{}, err } return store.AssignReconciliationIssue(ctx, id, request) } func (s *Service) BatchAssignReconciliationIssues(ctx context.Context, request ReconciliationBatchAssignmentRequest) (ReconciliationBatchActionResult, error) { result := ReconciliationBatchActionResult{Requested: len(request.Items), Succeeded: []ReconciliationIssue{}, Skipped: []ReconciliationBatchActionFailure{}} if len(request.Items) == 0 { return result, clientError{Code: "RECONCILIATION_BATCH_EMPTY", Message: "请至少选择一条差异记录"} } if len(request.Items) > 20 { return result, clientError{Code: "RECONCILIATION_BATCH_TOO_LARGE", Message: "单次最多交接 20 条差异记录"} } seen := map[string]bool{} for _, item := range request.Items { id := strings.TrimSpace(item.ID) if id == "" || len(id) > 64 || seen[id] { return result, clientError{Code: "RECONCILIATION_BATCH_ITEM_INVALID", Message: "批量交接包含无效或重复记录"} } seen[id] = true } for _, item := range request.Items { updated, err := s.AssignReconciliationIssue(ctx, item.ID, ReconciliationAssignmentRequest{Version: item.Version, Assignee: request.Assignee, DueAt: request.DueAt}) if err == nil { result.Succeeded = append(result.Succeeded, updated) continue } failure := ReconciliationBatchActionFailure{ID: item.ID, Code: "RECONCILIATION_BATCH_ITEM_FAILED", Message: "交接失败,请刷新后重试"} if itemError, ok := asClientError(err); ok { failure.Code, failure.Message = itemError.Code, itemError.Message } result.Skipped = append(result.Skipped, failure) } return result, nil } func (s *Service) ReconciliationIssue(ctx context.Context, id string) (ReconciliationIssue, error) { id = strings.TrimSpace(id) if id == "" || len(id) > 64 { return ReconciliationIssue{}, clientError{Code: "RECONCILIATION_ID_INVALID", Message: "差异记录编号无效"} } store, err := s.reconciliationStore() if err != nil { return ReconciliationIssue{}, err } return store.ReconciliationIssue(ctx, id) } func (s *Service) UpdateReconciliationIssue(ctx context.Context, id string, request ReconciliationActionRequest) (ReconciliationIssue, error) { id = strings.TrimSpace(id) if id == "" || len(id) > 64 { return ReconciliationIssue{}, clientError{Code: "RECONCILIATION_ID_INVALID", Message: "差异记录编号无效"} } request.Actor = ActorFromContext(ctx) store, err := s.reconciliationStore() if err != nil { return ReconciliationIssue{}, err } return store.UpdateReconciliationIssue(ctx, id, request) } func (s *Service) BatchUpdateReconciliationIssues(ctx context.Context, request ReconciliationBatchActionRequest) (ReconciliationBatchActionResult, error) { request.Status = strings.TrimSpace(request.Status) request.Note = strings.TrimSpace(request.Note) result := ReconciliationBatchActionResult{ Requested: len(request.Items), Succeeded: make([]ReconciliationIssue, 0, len(request.Items)), Skipped: make([]ReconciliationBatchActionFailure, 0), } if len(request.Items) == 0 { return result, clientError{Code: "RECONCILIATION_BATCH_EMPTY", Message: "请至少选择一条差异记录"} } if len(request.Items) > 20 { return result, clientError{Code: "RECONCILIATION_BATCH_TOO_LARGE", Message: "单次最多处置 20 条差异记录"} } allowed := map[string]bool{"pending": true, "no_action": true, "fixed": true} if !allowed[request.Status] { return result, clientError{Code: "RECONCILIATION_BATCH_STATUS_INVALID", Message: "批量处置仅支持退回待复核、无需处理或已修复"} } if request.Status != "pending" && request.Note == "" { return result, clientError{Code: "RECONCILIATION_NOTE_REQUIRED", Message: "批量处置必须填写说明"} } if len([]rune(request.Note)) > 500 { return result, clientError{Code: "RECONCILIATION_NOTE_TOO_LONG", Message: "处置说明不能超过 500 个字符"} } seen := make(map[string]struct{}, len(request.Items)) for _, item := range request.Items { id := strings.TrimSpace(item.ID) if id == "" || len(id) > 64 { return result, clientError{Code: "RECONCILIATION_ID_INVALID", Message: "批量处置中包含无效的差异记录编号"} } if _, exists := seen[id]; exists { return result, clientError{Code: "RECONCILIATION_BATCH_DUPLICATE_ID", Message: "批量处置不能重复选择同一条差异记录"} } seen[id] = struct{}{} } for _, item := range request.Items { updated, err := s.UpdateReconciliationIssue(ctx, strings.TrimSpace(item.ID), ReconciliationActionRequest{ Version: item.Version, Status: request.Status, Note: request.Note, }) if err == nil { result.Succeeded = append(result.Succeeded, updated) continue } failure := ReconciliationBatchActionFailure{ID: strings.TrimSpace(item.ID), Code: "RECONCILIATION_BATCH_ITEM_FAILED", Message: "处置失败,请刷新后重试"} if itemError, ok := asClientError(err); ok { failure.Code = itemError.Code failure.Message = itemError.Message } result.Skipped = append(result.Skipped, failure) } return result, nil } func (s *Service) EvaluateReconciliation(ctx context.Context) (ReconciliationEvaluationResult, error) { store, err := s.reconciliationStore() if err != nil { return ReconciliationEvaluationResult{}, err } return store.EvaluateReconciliation(ctx) }