package compliance import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "regexp" "sort" "strings" "sync" "time" "unicode/utf8" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgxpool" "go.uber.org/zap" sharedcompliance "github.com/geo-platform/tenant-api/internal/shared/compliance" "github.com/geo-platform/tenant-api/internal/shared/config" sharedllm "github.com/geo-platform/tenant-api/internal/shared/llm" "github.com/geo-platform/tenant-api/internal/shared/messaging/rabbitmq" "github.com/geo-platform/tenant-api/internal/shared/response" tenantrepo "github.com/geo-platform/tenant-api/internal/tenant/repository" ) const ( triggerEditorManual = "editor_manual" triggerPublishGate = "publish_gate" triggerAPI = "api" triggerSchedulerRecheck = "scheduler_recheck" triggerManualReviewBypass = "manual_review_bypass" globalPolicyCacheKey = int64(0) sourceDict = "dict" sourceRegex = "regex" aiPointsQuotaType = "ai_points" aiUsageTypeComplianceLLMJudge = "compliance_llm_judge" aiResourceTypeComplianceReview = "compliance_review" maxStoredViolations = 200 maxMatchedTextRunes = 80 ) type Service struct { pool *pgxpool.Pool mq *rabbitmq.Client cfg config.Provider llm sharedllm.Client logger *zap.Logger policyMu sync.Mutex policies map[int64]cachedPolicy reviewCache sync.Map } type cachedPolicy struct { policy *policySnapshot expires time.Time } func NewService(pool *pgxpool.Pool, cfg config.Provider, logger *zap.Logger) *Service { return &Service{pool: pool, cfg: cfg, logger: logger, policies: make(map[int64]cachedPolicy)} } func (s *Service) WithRabbitMQ(mq *rabbitmq.Client) *Service { if s != nil { s.mq = mq } return s } func (s *Service) WithLLM(client sharedllm.Client) *Service { if s != nil { s.llm = client } return s } type GateForPublishRequest struct { TenantID int64 ArticleID int64 ArticleVersionID int64 ActorID int64 TargetPlatforms []string AckRecordID *int64 TriggerSource string } type CheckArticleRequest struct { TenantID int64 ArticleID int64 ArticleVersionID *int64 ActorID int64 Title *string Plaintext *string TargetPlatforms []string TriggerSource string } type CheckPlainRequest struct { TenantID int64 ActorID int64 Title string Plaintext string TargetPlatforms []string } type ConsumeAckQuery struct { TenantID int64 ArticleID int64 ArticleVersionID int64 CheckRecordID int64 ContentHash string PolicyFingerprint string ConsumedByJobID uuid.UUID } type CreateManualReviewRequest struct { TenantID int64 ArticleID int64 ActorID int64 CheckRecordID int64 Note string } type CancelManualReviewRequest struct { TenantID int64 ArticleID int64 ActorID int64 ReviewID int64 Reason string } type ReviewJobMessage struct { JobID int64 `json:"job_id"` MessageID string `json:"message_id"` TenantID int64 `json:"tenant_id"` ArticleID int64 `json:"article_id"` ArticleVersionID int64 `json:"article_version_id"` CheckRecordID int64 `json:"check_record_id"` } type GateOutcome struct { Decision sharedcompliance.GateDecision Result *sharedcompliance.CheckResult } type articleVersionSnapshot struct { ArticleID int64 ArticleVersionID int64 Title string Plaintext string ManualReviewStatus sharedcompliance.ManualReviewStatus ManualReviewID *int64 ManualReviewReason *string ManualReviewDecidedAt *time.Time } type policySnapshot struct { LoadedAt time.Time MasterEnabled bool EnforcementMode string LLMJudgeEnabled bool Revision int64 DictionaryVersionSum int64 EnabledDictionaries []string DictVersions map[string]int Terms []termRule } type termRule struct { DictionaryID int64 DictionaryCode string DictionaryName string DictionaryVersion int PlatformScope string ApplicablePlatforms []string MatchType string Pattern string Level string Hint *string ReferenceLaw *string compiledRegex *regexp.Regexp compiledRegexPattern string } type checkRecordRow struct { ID int64 TenantID int64 ArticleID int64 ArticleVersionID int64 TargetPlatforms []string ContentHash string PolicyFingerprint string EnforcementMode string HighestLevel *string Passed bool HitCount int StoredHitCount int Truncated bool ManualReviewStatus sharedcompliance.ManualReviewStatus ManualReviewID *int64 CheckedAt time.Time DurationMS int } func (s *Service) RuntimeStatus(ctx context.Context, tenantID int64) (*sharedcompliance.RuntimeStatus, error) { cfg := s.currentComplianceConfig() if !cfg.Enabled { return &sharedcompliance.RuntimeStatus{ Enabled: false, ConfigEnabled: false, MasterEnabled: false, EnforcementMode: "disabled", LLMJudgeEnabled: false, SnapshotReloadInterval: cfg.SnapshotReloadInterval.String(), PublishGateTimeout: cfg.PublishGateTimeout.String(), }, nil } policy, err := s.resolvePolicy(ctx, tenantID) if err != nil { return nil, err } return &sharedcompliance.RuntimeStatus{ Enabled: policy.MasterEnabled && policy.EnforcementMode != "disabled", ConfigEnabled: true, MasterEnabled: policy.MasterEnabled, EnforcementMode: policy.EnforcementMode, LLMJudgeEnabled: policy.LLMJudgeEnabled, SnapshotReloadInterval: cfg.SnapshotReloadInterval.String(), PublishGateTimeout: cfg.PublishGateTimeout.String(), DictionaryVersion: policy.DictionaryVersionSum, EnabledDictionaries: append([]string(nil), policy.EnabledDictionaries...), }, nil } func (s *Service) CheckPlain(ctx context.Context, req CheckPlainRequest) (*sharedcompliance.CheckResult, error) { cfg := s.currentComplianceConfig() if !cfg.Enabled { return disabledCheckResult(req.TargetPlatforms), nil } policy, err := s.resolvePolicy(ctx, req.TenantID) if err != nil { return nil, err } return s.runAndPersist(ctx, runInput{ TenantID: req.TenantID, ActorID: req.ActorID, Title: req.Title, Plaintext: req.Plaintext, TargetPlatforms: req.TargetPlatforms, TriggerSource: triggerAPI, Policy: policy, }) } func (s *Service) CheckArticle(ctx context.Context, req CheckArticleRequest) (*sharedcompliance.CheckResult, error) { cfg := s.currentComplianceConfig() targets := normalizePlatforms(req.TargetPlatforms) trigger := normalizeArticleCheckTrigger(req.TriggerSource) if !cfg.Enabled && trigger != triggerEditorManual { return disabledCheckResult(targets), nil } versionID, err := s.ResolvePublishableVersion(ctx, req.TenantID, req.ArticleID, req.ArticleVersionID) if err != nil { return nil, err } snapshot, err := s.loadArticleVersionSnapshot(ctx, req.TenantID, req.ArticleID, versionID) if err != nil { return nil, err } if snapshot.ManualReviewStatus == sharedcompliance.ManualReviewStatusPending || snapshot.ManualReviewStatus == sharedcompliance.ManualReviewStatusApproved || snapshot.ManualReviewStatus == sharedcompliance.ManualReviewStatusRejected { if trigger != triggerEditorManual { return manualReviewOnlyResult(snapshot, targets), nil } } policy, err := s.resolvePolicy(ctx, req.TenantID) if err != nil { return nil, err } title := snapshot.Title if req.Title != nil { title = strings.TrimSpace(*req.Title) } plaintext := snapshot.Plaintext if req.Plaintext != nil { plaintext = strings.TrimSpace(*req.Plaintext) } if trigger == triggerEditorManual { return s.runPreview(ctx, runInput{ TenantID: req.TenantID, ArticleID: req.ArticleID, ArticleVersionID: versionID, ActorID: req.ActorID, Title: title, Plaintext: plaintext, TargetPlatforms: targets, TriggerSource: trigger, Policy: editorPreviewPolicy(policy), ManualStatus: snapshot.ManualReviewStatus, ManualReviewID: snapshot.ManualReviewID, }) } return s.runAndPersist(ctx, runInput{ TenantID: req.TenantID, ArticleID: req.ArticleID, ArticleVersionID: versionID, ActorID: req.ActorID, Title: title, Plaintext: plaintext, TargetPlatforms: targets, TriggerSource: trigger, Policy: policy, ManualStatus: snapshot.ManualReviewStatus, ManualReviewID: snapshot.ManualReviewID, }) } func (s *Service) GateForPublish(ctx context.Context, req GateForPublishRequest) (*GateOutcome, error) { cfg := s.currentComplianceConfig() if !cfg.Enabled { result := disabledCheckResult(req.TargetPlatforms) result.Decision = sharedcompliance.GateDecisionPass result.Passed = true return &GateOutcome{Decision: sharedcompliance.GateDecisionPass, Result: result}, nil } req.TargetPlatforms = normalizePlatforms(req.TargetPlatforms) trigger := strings.TrimSpace(req.TriggerSource) if trigger == "" { trigger = triggerPublishGate } req.TriggerSource = trigger snapshot, err := s.loadArticleVersionSnapshot(ctx, req.TenantID, req.ArticleID, req.ArticleVersionID) if err != nil { if cached, ok := s.cachedManualReview(req.ArticleVersionID); ok && cached.ManualReviewStatus != sharedcompliance.ManualReviewStatusNone { result := manualReviewOnlyResult(cached, req.TargetPlatforms) return &GateOutcome{Decision: result.Decision, Result: result}, manualReviewGateError(cached) } return nil, response.ErrServiceUnavailable(41004, "compliance_engine_unavailable", "manual review status is unavailable") } s.cacheManualReview(snapshot) switch snapshot.ManualReviewStatus { case sharedcompliance.ManualReviewStatusPending: result := manualReviewOnlyResult(snapshot, req.TargetPlatforms) return &GateOutcome{Decision: sharedcompliance.GateDecisionBlock, Result: result}, response.ErrConflict(41007, "compliance_manual_review_pending", "current article version is waiting for manual review") case sharedcompliance.ManualReviewStatusApproved: result, err := s.createManualReviewBypassRecord(ctx, req, snapshot) if err != nil { return nil, err } return &GateOutcome{Decision: sharedcompliance.GateDecisionPass, Result: result}, nil case sharedcompliance.ManualReviewStatusRejected: result := manualReviewOnlyResult(snapshot, req.TargetPlatforms) return &GateOutcome{Decision: sharedcompliance.GateDecisionBlock, Result: result}, response.ErrConflict(41006, "compliance_manual_review_rejected", "current article version was rejected by manual review") } policy, err := s.resolvePolicy(ctx, req.TenantID) if err != nil { return nil, err } if !policy.MasterEnabled || policy.EnforcementMode == "disabled" { result := disabledCheckResult(req.TargetPlatforms) result.EnforcementMode = policy.EnforcementMode result.Passed = true result.Decision = sharedcompliance.GateDecisionPass return &GateOutcome{Decision: sharedcompliance.GateDecisionPass, Result: result}, nil } if req.AckRecordID != nil { result, err := s.validateAckForPublish(ctx, req, policy) if err != nil { return nil, err } return &GateOutcome{Decision: sharedcompliance.GateDecisionPass, Result: result}, nil } result, err := s.runAndPersist(ctx, runInput{ TenantID: req.TenantID, ArticleID: req.ArticleID, ArticleVersionID: req.ArticleVersionID, ActorID: req.ActorID, Title: snapshot.Title, Plaintext: snapshot.Plaintext, TargetPlatforms: req.TargetPlatforms, TriggerSource: trigger, Policy: policy, ManualStatus: snapshot.ManualReviewStatus, ManualReviewID: snapshot.ManualReviewID, }) if err != nil { if policy.EnforcementMode == "mandatory" { return nil, response.ErrServiceUnavailable(41004, "compliance_engine_unavailable", "compliance gate failed closed") } return &GateOutcome{Decision: sharedcompliance.GateDecisionPass, Result: disabledCheckResult(req.TargetPlatforms)}, nil } return &GateOutcome{Decision: result.Decision, Result: result}, nil } func (s *Service) ResolvePublishableVersion(ctx context.Context, tenantID, articleID int64, requested *int64) (int64, error) { if requested != nil && *requested > 0 { var id int64 err := s.pool.QueryRow(ctx, ` SELECT av.id FROM article_versions av JOIN articles a ON a.id = av.article_id WHERE av.id = $1 AND av.article_id = $2 AND a.tenant_id = $3 AND a.deleted_at IS NULL `, *requested, articleID, tenantID).Scan(&id) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return 0, response.ErrBadRequest(41005, "invalid_article_version", "article_version_id does not belong to this article") } return 0, response.ErrInternal(50010, "query_failed", "failed to validate article version") } return id, nil } var versionValue pgtype.Int8 err := s.pool.QueryRow(ctx, ` SELECT current_version_id FROM articles WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL `, articleID, tenantID).Scan(&versionValue) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return 0, response.ErrNotFound(40411, "article_not_found", "article not found") } return 0, response.ErrInternal(50010, "query_failed", "failed to resolve current article version") } if !versionValue.Valid || versionValue.Int64 <= 0 { return 0, response.ErrBadRequest(41005, "invalid_article_version", "article has no publishable version") } return versionValue.Int64, nil } type runInput struct { TenantID int64 ArticleID int64 ArticleVersionID int64 ActorID int64 Title string Plaintext string TargetPlatforms []string TriggerSource string Policy *policySnapshot ManualStatus sharedcompliance.ManualReviewStatus ManualReviewID *int64 } func (s *Service) runAndPersist(ctx context.Context, input runInput) (*sharedcompliance.CheckResult, error) { checkResult, violations, err := s.runCheck(input) if err != nil { return nil, err } persistedResult, err := s.insertCheckRecord(ctx, input, checkRecordRow{ TenantID: input.TenantID, ArticleID: input.ArticleID, ArticleVersionID: input.ArticleVersionID, TargetPlatforms: checkResult.TargetPlatforms, ContentHash: checkResult.ContentHash, PolicyFingerprint: checkResult.PolicyFingerprint, EnforcementMode: checkResult.EnforcementMode, HighestLevel: checkResult.HighestLevel, Passed: checkResult.Passed, HitCount: checkResult.HitCount, StoredHitCount: checkResult.StoredHitCount, Truncated: checkResult.Truncated, ManualReviewStatus: checkResult.ManualReviewStatus, ManualReviewID: checkResult.ManualReviewID, CheckedAt: checkResult.CheckedAt, DurationMS: checkResult.DurationMS, }, violations, checkResult.Decision) if err != nil { return nil, err } if input.Policy.LLMJudgeEnabled && input.ArticleID > 0 && input.ArticleVersionID > 0 && persistedResult.RecordID > 0 { s.enqueueLLMReview(ctx, input, persistedResult.RecordID) } return persistedResult, nil } func (s *Service) runPreview(ctx context.Context, input runInput) (*sharedcompliance.CheckResult, error) { result, _, err := s.runCheck(input) return result, err } func (s *Service) runCheck(input runInput) (*sharedcompliance.CheckResult, []sharedcompliance.Violation, error) { start := time.Now() if input.Policy == nil { return nil, nil, response.ErrServiceUnavailable(41004, "compliance_engine_unavailable", "compliance policy is unavailable") } input.TargetPlatforms = normalizePlatforms(input.TargetPlatforms) plaintext := strings.TrimSpace(input.Plaintext) title := strings.TrimSpace(input.Title) contentHash := contentHash(title, plaintext) fingerprint := policyFingerprint(input.Policy, input.TargetPlatforms) violations := matchViolations(input.Policy, input.TenantID, input.TargetPlatforms, title+"\n"+plaintext) highest := highestViolationLevel(violations) hitCount := len(violations) truncated := false if len(violations) > maxStoredViolations { violations = violations[:maxStoredViolations] truncated = true } decision := decide(input.Policy.EnforcementMode, highest, hitCount) passed := decision == sharedcompliance.GateDecisionPass durationMS := int(time.Since(start).Milliseconds()) if input.ManualStatus == "" { input.ManualStatus = sharedcompliance.ManualReviewStatusNone } result := &sharedcompliance.CheckResult{ Decision: decision, Passed: passed, HighestLevel: highest, Violations: violations, HitCount: hitCount, StoredHitCount: len(violations), Truncated: truncated, EnforcementMode: input.Policy.EnforcementMode, ContentHash: contentHash, PolicyFingerprint: fingerprint, TargetPlatforms: input.TargetPlatforms, ManualReviewStatus: input.ManualStatus, ManualReviewID: input.ManualReviewID, CheckedAt: time.Now().UTC(), DurationMS: durationMS, } if input.TriggerSource == triggerEditorManual { result.StoredHitCount = 0 } return result, violations, nil } func (s *Service) insertCheckRecord(ctx context.Context, input runInput, record checkRecordRow, violations []sharedcompliance.Violation, decision sharedcompliance.GateDecision) (*sharedcompliance.CheckResult, error) { dictVersions, _ := json.Marshal(recordDictVersions(input.Policy)) var articleID any if record.ArticleID > 0 { articleID = &record.ArticleID } var articleVersionID any if record.ArticleVersionID > 0 { articleVersionID = &record.ArticleVersionID } var checkedBy any if input.ActorID > 0 { checkedBy = &input.ActorID } var highest any if record.HighestLevel != nil { highest = *record.HighestLevel } err := s.pool.QueryRow(ctx, ` INSERT INTO compliance_check_records ( tenant_id, article_id, article_version_id, target_platforms, content_hash, policy_fingerprint, trigger_source, enforcement_mode, highest_level, passed, hit_count, stored_hit_count, truncated, dict_versions, llm_used, duration_ms, checked_by, manual_review_status, manual_review_id ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,FALSE,$15,$16,$17,$18) RETURNING id, checked_at `, record.TenantID, articleID, articleVersionID, record.TargetPlatforms, record.ContentHash, record.PolicyFingerprint, input.TriggerSource, record.EnforcementMode, highest, record.Passed, record.HitCount, record.StoredHitCount, record.Truncated, dictVersions, record.DurationMS, checkedBy, string(record.ManualReviewStatus), int64PtrAny(record.ManualReviewID), ).Scan(&record.ID, &record.CheckedAt) if err != nil { return nil, response.ErrInternal(51001, "compliance_check_record_create_failed", "failed to create compliance check record") } for i := range violations { violations[i].RecordID = record.ID id, err := s.insertViolation(ctx, record.ID, record.TenantID, violations[i]) if err != nil { return nil, err } violations[i].ID = id } return checkResultFromRecord(record, violations, decision), nil } func (s *Service) insertViolation(ctx context.Context, recordID, tenantID int64, v sharedcompliance.Violation) (int64, error) { var id int64 err := s.pool.QueryRow(ctx, ` INSERT INTO compliance_violations ( record_id, tenant_id, platform_code, source, dictionary_code, dictionary_name, term_pattern, matched_text, start_offset, end_offset, dom_path, level, hint, reference_law ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING id `, recordID, tenantID, v.PlatformCode, v.Source, v.DictionaryCode, v.DictionaryName, v.TermPattern, truncateRunes(v.MatchedText, maxMatchedTextRunes), v.StartOffset, v.EndOffset, v.DomPath, v.Level, v.Hint, v.ReferenceLaw, ).Scan(&id) if err != nil { return 0, response.ErrInternal(51002, "compliance_violation_create_failed", "failed to create compliance violation") } return id, nil } func (s *Service) CreateAck(ctx context.Context, tenantID, checkRecordID, actorID int64, acknowledgedViolationIDs []int64) (*sharedcompliance.AckRecord, error) { record, err := s.loadCheckRecord(ctx, tenantID, checkRecordID) if err != nil { return nil, err } if record.ArticleID <= 0 || record.ArticleVersionID <= 0 { return nil, response.ErrBadRequest(41003, "compliance_ack_mismatch", "check record is not bound to an article version") } violations, err := s.loadViolations(ctx, tenantID, checkRecordID) if err != nil { return nil, err } required := make([]int64, 0, len(violations)) for _, violation := range violations { if record.EnforcementMode == "mandatory" && violation.Level == "block" { return nil, response.ErrConflict(41001, "compliance_blocked", "blocked violations cannot be acknowledged") } required = append(required, violation.ID) } if !sameInt64Set(required, acknowledgedViolationIDs) { return nil, response.ErrBadRequest(41003, "compliance_ack_mismatch", "acknowledged_violation_ids must cover every acknowledged violation") } if len(required) == 0 { return nil, response.ErrBadRequest(41003, "compliance_ack_mismatch", "there are no violations to acknowledge") } tx, err := s.pool.Begin(ctx) if err != nil { return nil, response.ErrInternal(51003, "compliance_ack_create_failed", "failed to begin compliance ack transaction") } defer rollbackTx(tx) expiresAt := time.Now().UTC().Add(24 * time.Hour) var ack sharedcompliance.AckRecord err = tx.QueryRow(ctx, ` INSERT INTO compliance_ack_records ( tenant_id, article_id, article_version_id, check_record_id, content_hash, policy_fingerprint, acknowledged_by, ack_violation_ids, expires_at ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING id, check_record_id, article_id, article_version_id, content_hash, policy_fingerprint, expires_at, created_at `, tenantID, record.ArticleID, record.ArticleVersionID, record.ID, record.ContentHash, record.PolicyFingerprint, actorID, required, expiresAt, ).Scan(&ack.ID, &ack.CheckRecordID, &ack.ArticleID, &ack.ArticleVersionID, &ack.ContentHash, &ack.PolicyFingerprint, &ack.ExpiresAt, &ack.CreatedAt) if err != nil { return nil, response.ErrInternal(51003, "compliance_ack_create_failed", "failed to create compliance ack") } if _, err := tx.Exec(ctx, ` UPDATE compliance_violations SET acknowledged_in_ack_id = $1 WHERE record_id = $2 AND tenant_id = $3 AND id = ANY($4) `, ack.ID, record.ID, tenantID, required); err != nil { return nil, response.ErrInternal(51003, "compliance_ack_create_failed", "failed to mark acknowledged violations") } if err := tx.Commit(ctx); err != nil { return nil, response.ErrInternal(51003, "compliance_ack_create_failed", "failed to commit compliance ack") } return &ack, nil } func (s *Service) ConsumeAckTx(ctx context.Context, tx pgx.Tx, ackRecordID int64, q ConsumeAckQuery) (bool, error) { if tx == nil { return false, errors.New("tx is nil") } tag, err := tx.Exec(ctx, ` UPDATE compliance_ack_records SET consumed_at = NOW(), consumed_by_job_id = $1 WHERE id = $2 AND tenant_id = $3 AND article_id = $4 AND article_version_id = $5 AND check_record_id = $6 AND content_hash = $7 AND policy_fingerprint = $8 AND consumed_at IS NULL AND expires_at > NOW() `, q.ConsumedByJobID, ackRecordID, q.TenantID, q.ArticleID, q.ArticleVersionID, q.CheckRecordID, q.ContentHash, q.PolicyFingerprint, ) if err != nil { return false, err } return tag.RowsAffected() == 1, nil } func (s *Service) validateAckForPublish(ctx context.Context, req GateForPublishRequest, policy *policySnapshot) (*sharedcompliance.CheckResult, error) { var recordID int64 err := s.pool.QueryRow(ctx, ` SELECT check_record_id FROM compliance_ack_records WHERE id = $1 AND tenant_id = $2 AND article_id = $3 AND article_version_id = $4 AND consumed_at IS NULL AND expires_at > NOW() `, *req.AckRecordID, req.TenantID, req.ArticleID, req.ArticleVersionID).Scan(&recordID) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, response.ErrBadRequest(41003, "compliance_ack_mismatch", "ack record is expired, consumed, or not bound to this article version") } return nil, response.ErrInternal(51004, "compliance_ack_lookup_failed", "failed to validate compliance ack") } record, err := s.loadCheckRecord(ctx, req.TenantID, recordID) if err != nil { return nil, err } expectedFingerprint := policyFingerprint(policy, req.TargetPlatforms) if record.ArticleID != req.ArticleID || record.ArticleVersionID != req.ArticleVersionID || record.PolicyFingerprint != expectedFingerprint { return nil, response.ErrBadRequest(41003, "compliance_ack_mismatch", "ack record no longer matches current content or policy") } violations, err := s.loadViolations(ctx, req.TenantID, record.ID) if err != nil { return nil, err } for _, violation := range violations { if record.EnforcementMode == "mandatory" && violation.Level == "block" { return nil, response.ErrConflict(41001, "compliance_blocked", "blocked violations cannot be acknowledged") } } result := checkResultFromRecord(record, violations, sharedcompliance.GateDecisionPass) result.Decision = sharedcompliance.GateDecisionPass result.Passed = true return result, nil } func (s *Service) LatestCheck(ctx context.Context, tenantID, articleID int64) (*sharedcompliance.CheckResult, error) { var recordID int64 err := s.pool.QueryRow(ctx, ` SELECT id FROM compliance_check_records WHERE tenant_id = $1 AND article_id = $2 ORDER BY checked_at DESC, id DESC LIMIT 1 `, tenantID, articleID).Scan(&recordID) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, response.ErrNotFound(40461, "compliance_check_not_found", "no compliance check record found") } return nil, response.ErrInternal(51005, "compliance_check_lookup_failed", "failed to load latest compliance check") } record, err := s.loadCheckRecord(ctx, tenantID, recordID) if err != nil { return nil, err } violations, err := s.loadViolations(ctx, tenantID, recordID) if err != nil { return nil, err } return checkResultFromRecord(record, violations, decide(record.EnforcementMode, record.HighestLevel, record.HitCount)), nil } func (s *Service) MarkPublishJobBlockedByCompliance(ctx context.Context, jobID uuid.UUID, result *sharedcompliance.CheckResult) error { if s == nil || s.pool == nil || jobID == uuid.Nil { return nil } var recordID any var reason any if result != nil && result.RecordID > 0 { recordID = result.RecordID reasonBytes, _ := json.Marshal(result) reason = string(reasonBytes) } tx, err := s.pool.Begin(ctx) if err != nil { return response.ErrInternal(51012, "compliance_publish_job_block_failed", "failed to begin compliance block transaction") } defer rollbackTx(tx) if _, err := tx.Exec(ctx, ` UPDATE desktop_publish_jobs SET status = 'blocked_by_compliance', compliance_blocked_record_id = $2, compliance_blocked_at = NOW(), compliance_blocked_reason = $3, updated_at = NOW() WHERE desktop_id = $1 AND status <> 'blocked_by_compliance' `, jobID, recordID, reason); err != nil { return response.ErrInternal(51012, "compliance_publish_job_block_failed", "failed to mark publish job blocked") } if _, err := tx.Exec(ctx, ` UPDATE desktop_tasks SET status = 'aborted', error = jsonb_build_object( 'message', 'compliance_blocked', 'record_id', $2::bigint, 'detail', $3::text ), active_attempt_id = NULL, lease_token_hash = NULL, lease_expires_at = NULL, updated_at = NOW() WHERE job_id = $1 AND kind = 'publish' AND status = 'queued' `, jobID, recordID, reason); err != nil { return response.ErrInternal(51012, "compliance_publish_job_block_failed", "failed to abort blocked publish tasks") } if err := tx.Commit(ctx); err != nil { return response.ErrInternal(51012, "compliance_publish_job_block_failed", "failed to commit compliance block") } return nil } func (s *Service) SubmitManualReview(ctx context.Context, req CreateManualReviewRequest) (*sharedcompliance.ManualReview, error) { note := strings.TrimSpace(req.Note) if note == "" { return nil, response.ErrBadRequest(40001, "invalid_params", "note is required") } if utf8.RuneCountInString(note) > 500 { return nil, response.ErrBadRequest(40001, "invalid_params", "note must be 500 characters or less") } record, err := s.loadCheckRecord(ctx, req.TenantID, req.CheckRecordID) if err != nil { return nil, err } if record.ArticleID != req.ArticleID || record.ArticleVersionID <= 0 { return nil, response.ErrBadRequest(41009, "compliance_manual_review_invalid_check_record", "check record does not belong to this article version") } violations, err := s.loadViolations(ctx, req.TenantID, record.ID) if err != nil { return nil, err } hasBlock := false for _, violation := range violations { if violation.Level == "block" { hasBlock = true break } } if !hasBlock { return nil, response.ErrBadRequest(41009, "compliance_manual_review_invalid_check_record", "manual review requires at least one block violation") } summaryJSON, err := json.Marshal(violations) if err != nil { return nil, response.ErrInternal(51006, "manual_review_create_failed", "failed to encode manual review summary") } tx, err := s.pool.Begin(ctx) if err != nil { return nil, response.ErrInternal(51006, "manual_review_create_failed", "failed to begin manual review transaction") } defer rollbackTx(tx) var review sharedcompliance.ManualReview err = tx.QueryRow(ctx, ` INSERT INTO compliance_manual_reviews ( tenant_id, article_id, article_version_id, original_check_record_id, original_content_hash, original_policy_fingerprint, original_target_platforms, original_violation_summary, requested_by, request_note, status ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,'pending') RETURNING id, tenant_id, article_id, article_version_id, original_check_record_id, requested_by, requested_at, request_note, status `, req.TenantID, req.ArticleID, record.ArticleVersionID, record.ID, record.ContentHash, record.PolicyFingerprint, record.TargetPlatforms, summaryJSON, req.ActorID, note, ).Scan(&review.ID, &review.TenantID, &review.ArticleID, &review.ArticleVersionID, &review.CheckRecordID, &review.RequestedBy, &review.RequestedAt, &review.RequestNote, &review.Status) if err != nil { if isUniqueViolation(err) { _ = tx.Rollback(ctx) status, lookupErr := s.lookupManualReviewStatus(ctx, req.TenantID, record.ArticleVersionID) if lookupErr == nil && status == sharedcompliance.ManualReviewStatusPending { return nil, response.ErrConflict(41007, "compliance_manual_review_pending", "manual review is already pending") } return nil, response.ErrConflict(41008, "compliance_manual_review_terminal", "manual review for this version is already terminal") } return nil, response.ErrInternal(51006, "manual_review_create_failed", "failed to create manual review") } if _, err := tx.Exec(ctx, ` UPDATE article_versions SET manual_review_status = 'pending', manual_review_id = $1, manual_review_updated_at = NOW() WHERE id = $2 AND article_id = $3 `, review.ID, record.ArticleVersionID, req.ArticleID); err != nil { return nil, response.ErrInternal(51006, "manual_review_create_failed", "failed to stamp article version") } if err := tx.Commit(ctx); err != nil { return nil, response.ErrInternal(51006, "manual_review_create_failed", "failed to commit manual review") } review.ViolationSummary = violations review.OriginalPlatforms = record.TargetPlatforms s.reviewCache.Delete(record.ArticleVersionID) return &review, nil } func (s *Service) LatestManualReview(ctx context.Context, tenantID, articleID int64, requestedVersionID *int64) (*sharedcompliance.ManualReview, error) { versionID, err := s.ResolvePublishableVersion(ctx, tenantID, articleID, requestedVersionID) if err != nil { return nil, err } review, err := s.loadLatestManualReviewByVersion(ctx, tenantID, versionID) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return &sharedcompliance.ManualReview{ TenantID: tenantID, ArticleID: articleID, ArticleVersionID: versionID, Status: sharedcompliance.ManualReviewStatusNone, }, nil } return nil, err } return review, nil } func (s *Service) CancelManualReview(ctx context.Context, req CancelManualReviewRequest) (*sharedcompliance.ManualReview, error) { reason := strings.TrimSpace(req.Reason) if utf8.RuneCountInString(reason) > 500 { return nil, response.ErrBadRequest(40001, "invalid_params", "reason must be 500 characters or less") } tx, err := s.pool.Begin(ctx) if err != nil { return nil, response.ErrInternal(51007, "manual_review_cancel_failed", "failed to begin manual review cancellation") } defer rollbackTx(tx) var review sharedcompliance.ManualReview err = tx.QueryRow(ctx, ` UPDATE compliance_manual_reviews SET status = 'cancelled', cancelled_by = $1, cancelled_at = NOW(), cancel_reason = $2 WHERE id = $3 AND tenant_id = $4 AND article_id = $5 AND requested_by = $1 AND status = 'pending' RETURNING id, tenant_id, article_id, article_version_id, original_check_record_id, requested_by, requested_at, request_note, status, cancelled_by, cancelled_at, cancel_reason `, req.ActorID, nullableString(reason), req.ReviewID, req.TenantID, req.ArticleID).Scan( &review.ID, &review.TenantID, &review.ArticleID, &review.ArticleVersionID, &review.CheckRecordID, &review.RequestedBy, &review.RequestedAt, &review.RequestNote, &review.Status, &review.CancelledBy, &review.CancelledAt, &review.CancelReason, ) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, response.ErrConflict(41010, "compliance_manual_review_not_cancellable", "manual review cannot be cancelled") } return nil, response.ErrInternal(51007, "manual_review_cancel_failed", "failed to cancel manual review") } if _, err := tx.Exec(ctx, ` UPDATE article_versions SET manual_review_status = 'none', manual_review_id = NULL, manual_review_updated_at = NOW() WHERE id = $1 AND article_id = $2 `, review.ArticleVersionID, req.ArticleID); err != nil { return nil, response.ErrInternal(51007, "manual_review_cancel_failed", "failed to clear article version stamp") } if err := tx.Commit(ctx); err != nil { return nil, response.ErrInternal(51007, "manual_review_cancel_failed", "failed to commit manual review cancellation") } s.reviewCache.Delete(review.ArticleVersionID) return &review, nil } func DecodeReviewJobMessage(body []byte) (ReviewJobMessage, error) { var msg ReviewJobMessage if err := json.Unmarshal(body, &msg); err != nil { return msg, err } if msg.JobID <= 0 || msg.TenantID <= 0 || msg.ArticleID <= 0 || msg.ArticleVersionID <= 0 || msg.CheckRecordID <= 0 { return msg, fmt.Errorf("invalid compliance review job message") } return msg, nil } func (s *Service) ProcessReviewJob(ctx context.Context, msg ReviewJobMessage) error { if s == nil || s.pool == nil { return response.ErrServiceUnavailable(41004, "compliance_engine_unavailable", "compliance service is unavailable") } cfg := s.currentComplianceConfig() if !cfg.Enabled || !cfg.LLMJudgeEnabled { return s.markReviewJobDone(ctx, msg.JobID) } if s.llm == nil || s.llm.Validate() != nil { return s.failReviewJob(ctx, msg.JobID, cfg.ReviewMaxAttempts, sharedllm.ErrNotConfigured) } job, claimed, err := s.claimReviewJob(ctx, msg) if err != nil || !claimed { return err } prompt, err := s.buildReviewPrompt(ctx, job) if err != nil { return s.failReviewJob(ctx, job.ID, cfg.ReviewMaxAttempts, err) } if strings.TrimSpace(prompt) == "" { return s.markReviewJobDone(ctx, job.ID) } reservation, billingErr := s.reserveComplianceLLMJudgePoints(ctx, job, prompt) if billingErr != nil { if s.logger != nil { s.logger.Warn("compliance llm billing skipped review", zap.Error(billingErr), zap.Int64("tenant_id", job.TenantID), zap.Int64("job_id", job.ID), zap.Int64("check_record_id", job.CheckRecordID), ) } return s.markReviewJobDone(ctx, job.ID) } result, err := s.llm.Generate(ctx, sharedllm.GenerateRequest{ Prompt: prompt, Timeout: cfg.ReviewWorkerTimeout, MaxOutputTokens: 900, ResponseFormat: &sharedllm.ResponseFormat{ Type: sharedllm.ResponseFormatTypeJSONObject, Name: "compliance_review", Description: "Semantic compliance review result for a publish check record.", }, }, nil) if err != nil { _ = s.refundComplianceLLMJudgePoints(context.Background(), job, reservation, err) return s.failReviewJob(ctx, job.ID, cfg.ReviewMaxAttempts, err) } summary := decodeLLMReviewResult(result.Content) if err := s.persistLLMReviewResult(ctx, job, summary); err != nil { _ = s.refundComplianceLLMJudgePoints(context.Background(), job, reservation, err) return s.failReviewJob(ctx, job.ID, cfg.ReviewMaxAttempts, err) } if err := s.completeComplianceLLMJudgePoints(ctx, job, reservation, result.Model); err != nil && s.logger != nil { s.logger.Warn("compliance llm billing completion failed", zap.Error(err), zap.Int64("tenant_id", job.TenantID), zap.Int64("job_id", job.ID), zap.Int64("check_record_id", job.CheckRecordID), ) } return s.markReviewJobDone(ctx, job.ID) } type reviewJob struct { ID int64 TenantID int64 ArticleID int64 ArticleVersionID int64 CheckRecordID int64 CheckedBy int64 Attempts int } func (s *Service) claimReviewJob(ctx context.Context, msg ReviewJobMessage) (reviewJob, bool, error) { var job reviewJob err := s.pool.QueryRow(ctx, ` UPDATE compliance_review_jobs SET status = 'running', attempts = attempts + 1, locked_at = NOW(), locked_by = $2, updated_at = NOW() WHERE id = $1 AND status IN ('pending','failed') AND available_at <= NOW() RETURNING id, tenant_id, article_id, article_version_id, check_record_id, COALESCE(( SELECT checked_by FROM compliance_check_records cr WHERE cr.id = compliance_review_jobs.check_record_id ), 0), attempts `, msg.JobID, strings.TrimSpace(msg.MessageID)).Scan( &job.ID, &job.TenantID, &job.ArticleID, &job.ArticleVersionID, &job.CheckRecordID, &job.CheckedBy, &job.Attempts, ) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return job, false, nil } return job, false, err } return job, true, nil } func (s *Service) buildReviewPrompt(ctx context.Context, job reviewJob) (string, error) { var title string var plaintext pgtype.Text err := s.pool.QueryRow(ctx, ` SELECT av.title, av.plaintext_snapshot FROM article_versions av JOIN articles a ON a.id = av.article_id WHERE av.id = $1 AND a.id = $2 AND a.tenant_id = $3 `, job.ArticleVersionID, job.ArticleID, job.TenantID).Scan(&title, &plaintext) if err != nil { return "", err } text := strings.TrimSpace(plaintext.String) if text == "" { snapshot, err := s.loadArticleVersionSnapshot(ctx, job.TenantID, job.ArticleID, job.ArticleVersionID) if err != nil { return "", err } text = snapshot.Plaintext title = snapshot.Title } return strings.TrimSpace(`你是内容合规复核助手。请只基于给定标题和正文判断是否存在广告法极限词、政治敏感、涉黄涉暴、行业违禁或明显平台高风险表达。 返回 JSON,不要输出 Markdown: { "risk": true, "level": "high", "summary": "一句话说明风险", "evidence": "命中的短句或关键词", "hint": "给作者的修改建议" } 若没有高风险,返回 {"risk": false, "level": "info", "summary": "", "evidence": "", "hint": ""}。 标题:` + title + ` 正文: ` + truncateRunes(text, 6000)), nil } type llmReviewSummary struct { Risk bool `json:"risk"` Level string `json:"level"` Summary string `json:"summary"` Evidence string `json:"evidence"` Hint string `json:"hint"` } func decodeLLMReviewResult(content string) llmReviewSummary { var summary llmReviewSummary _ = json.Unmarshal([]byte(strings.TrimSpace(content)), &summary) summary.Level = normalizeLevel(summary.Level) if summary.Level == "" { summary.Level = "info" } summary.Summary = strings.TrimSpace(summary.Summary) summary.Evidence = strings.TrimSpace(summary.Evidence) summary.Hint = strings.TrimSpace(summary.Hint) return summary } func (s *Service) persistLLMReviewResult(ctx context.Context, job reviewJob, summary llmReviewSummary) error { if !summary.Risk { _, err := s.pool.Exec(ctx, ` UPDATE compliance_check_records SET llm_used = TRUE WHERE id = $1 AND tenant_id = $2 `, job.CheckRecordID, job.TenantID) return err } matchedText := summary.Evidence if matchedText == "" { matchedText = summary.Summary } hint := summary.Hint if hint == "" { hint = summary.Summary } tx, err := s.pool.Begin(ctx) if err != nil { return err } defer rollbackTx(tx) if _, err := tx.Exec(ctx, ` INSERT INTO compliance_violations ( record_id, tenant_id, source, matched_text, start_offset, end_offset, level, hint ) VALUES ($1,$2,'llm',$3,0,0,$4,$5) `, job.CheckRecordID, job.TenantID, truncateRunes(matchedText, maxMatchedTextRunes), summary.Level, nullableString(hint)); err != nil { return err } if _, err := tx.Exec(ctx, ` UPDATE compliance_check_records SET llm_used = TRUE, hit_count = hit_count + 1, stored_hit_count = stored_hit_count + 1, highest_level = CASE WHEN highest_level IS NULL OR $3 > CASE highest_level WHEN 'block' THEN 4 WHEN 'high' THEN 3 WHEN 'medium' THEN 2 WHEN 'info' THEN 1 ELSE 0 END THEN $2 ELSE highest_level END WHERE id = $1 AND tenant_id = $4 `, job.CheckRecordID, summary.Level, levelRank(summary.Level), job.TenantID); err != nil { return err } return tx.Commit(ctx) } type aiPointReservation struct { ReservationID int64 UsageLogID int64 Points int RequestChars int BaseChars int } func (s *Service) reserveComplianceLLMJudgePoints(ctx context.Context, job reviewJob, meteredText string) (*aiPointReservation, error) { if s == nil || s.pool == nil { return nil, response.ErrServiceUnavailable(50344, "ai_points_unavailable", "ai points store is unavailable") } tx, err := s.pool.Begin(ctx) if err != nil { return nil, err } defer rollbackTx(tx) if err := lockTenantAIPoints(ctx, tx, job.TenantID); err != nil { return nil, err } now := time.Now().UTC() quotaRepo := tenantrepo.NewQuotaRepository(tx) status, err := quotaRepo.GetAIQuotaStatus(ctx, job.TenantID, now) if err != nil { return nil, err } requestChars := countAIPointChars(meteredText) points := calculateAIPointCost(requestChars, status.BaseChars) if status.Total <= 0 || status.Balance < points { return nil, response.ErrForbidden(40381, "ai_points_insufficient", "AI points are insufficient") } resourceType := aiResourceTypeComplianceReview resourceID := job.CheckRecordID reservationID, err := quotaRepo.CreateQuotaReservation(ctx, tenantrepo.QuotaReservationInput{ TenantID: job.TenantID, OperatorID: job.CheckedBy, QuotaType: aiPointsQuotaType, ResourceType: resourceType, ResourceID: &resourceID, ReservedAmount: points, ExpireAt: now.Add(time.Hour), }) if err != nil { return nil, err } metadataJSON, err := json.Marshal(map[string]any{ "article_id": job.ArticleID, "article_version_id": job.ArticleVersionID, "check_record_id": job.CheckRecordID, "review_job_id": job.ID, }) if err != nil { return nil, err } usageID, err := tenantrepo.NewAIPointUsageRepository(tx).Create(ctx, tenantrepo.CreateAIPointUsageLogInput{ TenantID: job.TenantID, OperatorID: job.CheckedBy, QuotaReservationID: reservationID, UsageType: aiUsageTypeComplianceLLMJudge, ResourceType: &resourceType, ResourceID: &resourceID, RequestChars: requestChars, BaseChars: status.BaseChars, Points: points, MetadataJSON: metadataJSON, }) if err != nil { return nil, err } reason := "ai_points_reserve" referenceType := "ai_point_usage" if _, err := quotaRepo.InsertQuotaLedger(ctx, tenantrepo.QuotaLedgerInput{ TenantID: job.TenantID, OperatorID: job.CheckedBy, QuotaType: aiPointsQuotaType, Delta: -points, BalanceAfter: status.Balance - points, Reason: &reason, ReferenceType: &referenceType, ReferenceID: &usageID, }); err != nil { return nil, err } if err := tx.Commit(ctx); err != nil { return nil, err } return &aiPointReservation{ ReservationID: reservationID, UsageLogID: usageID, Points: points, RequestChars: requestChars, BaseChars: status.BaseChars, }, nil } func (s *Service) completeComplianceLLMJudgePoints(ctx context.Context, job reviewJob, reservation *aiPointReservation, model string) error { if s == nil || s.pool == nil || reservation == nil || reservation.ReservationID <= 0 || reservation.UsageLogID <= 0 { return nil } tx, err := s.pool.Begin(ctx) if err != nil { return err } defer rollbackTx(tx) pending, err := lockPendingAIPointUsage(ctx, tx, job.TenantID, reservation.UsageLogID) if err != nil || !pending { return err } if err := confirmAIPointReservationIfPending(ctx, tx, job.TenantID, reservation.ReservationID); err != nil { return err } var modelPtr *string if strings.TrimSpace(model) != "" { normalized := strings.TrimSpace(model) modelPtr = &normalized } if err := tenantrepo.NewAIPointUsageRepository(tx).MarkCompleted(ctx, job.TenantID, reservation.UsageLogID, modelPtr); err != nil { return err } return tx.Commit(ctx) } func (s *Service) refundComplianceLLMJudgePoints(ctx context.Context, job reviewJob, reservation *aiPointReservation, jobErr error) error { if s == nil || s.pool == nil || reservation == nil || reservation.ReservationID <= 0 || reservation.UsageLogID <= 0 || reservation.Points <= 0 { return nil } tx, err := s.pool.Begin(ctx) if err != nil { return err } defer rollbackTx(tx) if err := lockTenantAIPoints(ctx, tx, job.TenantID); err != nil { return err } pending, err := lockPendingAIPointUsage(ctx, tx, job.TenantID, reservation.UsageLogID) if err != nil || !pending { return err } refunded, err := refundAIPointReservationIfPending(ctx, tx, job.TenantID, reservation.ReservationID) if err != nil { return err } if refunded { status, err := tenantrepo.NewQuotaRepository(tx).GetAIQuotaStatus(ctx, job.TenantID, time.Now().UTC()) if err != nil { return err } quotaRepo := tenantrepo.NewQuotaRepository(tx) reason := "ai_points_refund" referenceType := "ai_point_usage" usageID := reservation.UsageLogID if _, err := quotaRepo.InsertQuotaLedger(ctx, tenantrepo.QuotaLedgerInput{ TenantID: job.TenantID, OperatorID: job.CheckedBy, QuotaType: aiPointsQuotaType, Delta: reservation.Points, BalanceAfter: status.Balance + reservation.Points, Reason: &reason, ReferenceType: &referenceType, ReferenceID: &usageID, }); err != nil { return err } } if err := tenantrepo.NewAIPointUsageRepository(tx).MarkRefunded(ctx, job.TenantID, reservation.UsageLogID, truncateRunes(errorString(jobErr), 2000)); err != nil { return err } return tx.Commit(ctx) } func countAIPointChars(text string) int { return utf8.RuneCountInString(strings.TrimSpace(text)) } func calculateAIPointCost(chars, baseChars int) int { if baseChars <= 0 { baseChars = 1000 } if chars <= 0 { return 1 } return chars/baseChars + 1 } func lockTenantAIPoints(ctx context.Context, tx pgx.Tx, tenantID int64) error { _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(20260428, hashint8($1)::int)`, tenantID) return err } func lockPendingAIPointUsage(ctx context.Context, tx pgx.Tx, tenantID, usageLogID int64) (bool, error) { var status string err := tx.QueryRow(ctx, ` SELECT status FROM ai_point_usage_logs WHERE id = $1 AND tenant_id = $2 FOR UPDATE `, usageLogID, tenantID).Scan(&status) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return false, nil } return false, err } return status == "pending", nil } func confirmAIPointReservationIfPending(ctx context.Context, tx pgx.Tx, tenantID, reservationID int64) error { tag, err := tx.Exec(ctx, ` UPDATE quota_reservations SET status = 'confirmed', consumed_amount = reserved_amount, updated_at = NOW() WHERE id = $1 AND tenant_id = $2 AND quota_type = $3 AND status = 'pending' `, reservationID, tenantID, aiPointsQuotaType) if err != nil { return err } return ignoreAIPointReservationReset(ctx, tx, tag.RowsAffected() > 0, tenantID, reservationID) } func refundAIPointReservationIfPending(ctx context.Context, tx pgx.Tx, tenantID, reservationID int64) (bool, error) { tag, err := tx.Exec(ctx, ` UPDATE quota_reservations SET status = 'refunded', refunded_amount = reserved_amount, updated_at = NOW() WHERE id = $1 AND tenant_id = $2 AND quota_type = $3 AND status = 'pending' `, reservationID, tenantID, aiPointsQuotaType) if err != nil { return false, err } if tag.RowsAffected() > 0 { return true, nil } return false, ignoreAIPointReservationReset(ctx, tx, false, tenantID, reservationID) } func ignoreAIPointReservationReset(ctx context.Context, tx pgx.Tx, updated bool, tenantID, reservationID int64) error { if updated { return nil } var status string err := tx.QueryRow(ctx, ` SELECT status FROM quota_reservations WHERE id = $1 AND tenant_id = $2 AND quota_type = $3 `, reservationID, tenantID, aiPointsQuotaType).Scan(&status) if err != nil { return err } if status == "reset" { return nil } return errors.New("ai points reservation is not pending") } func (s *Service) markReviewJobDone(ctx context.Context, jobID int64) error { _, err := s.pool.Exec(ctx, ` UPDATE compliance_review_jobs SET status = 'done', locked_at = NULL, locked_by = NULL, updated_at = NOW() WHERE id = $1 `, jobID) return err } func (s *Service) failReviewJob(ctx context.Context, jobID int64, maxAttempts int, jobErr error) error { if maxAttempts <= 0 { maxAttempts = 3 } _, err := s.pool.Exec(ctx, ` UPDATE compliance_review_jobs SET status = CASE WHEN attempts >= $2 THEN 'failed' ELSE 'pending' END, available_at = CASE WHEN attempts >= $2 THEN available_at ELSE NOW() + interval '5 minutes' END, locked_at = NULL, locked_by = NULL, last_error = $3, updated_at = NOW() WHERE id = $1 `, jobID, maxAttempts, truncateRunes(errorString(jobErr), 2000)) if err != nil { return err } return jobErr } func (s *Service) enqueueLLMReview(ctx context.Context, input runInput, checkRecordID int64) { if s == nil || s.pool == nil || s.mq == nil || checkRecordID <= 0 { return } messageID := uuid.New() var jobID int64 err := s.pool.QueryRow(ctx, ` INSERT INTO compliance_review_jobs ( message_id, tenant_id, article_id, article_version_id, check_record_id, status ) VALUES ($1,$2,$3,$4,$5,'pending') ON CONFLICT (check_record_id) DO UPDATE SET available_at = LEAST(compliance_review_jobs.available_at, NOW()), updated_at = NOW() RETURNING id `, messageID, input.TenantID, input.ArticleID, input.ArticleVersionID, checkRecordID).Scan(&jobID) if err != nil { if s.logger != nil { s.logger.Warn("compliance review job enqueue failed", zap.Error(err), zap.Int64("tenant_id", input.TenantID), zap.Int64("article_id", input.ArticleID), zap.Int64("check_record_id", checkRecordID), ) } return } message := ReviewJobMessage{ JobID: jobID, MessageID: messageID.String(), TenantID: input.TenantID, ArticleID: input.ArticleID, ArticleVersionID: input.ArticleVersionID, CheckRecordID: checkRecordID, } body, err := json.Marshal(message) if err != nil { return } if err := s.mq.PublishComplianceReviewTask(ctx, body); err != nil && s.logger != nil { s.logger.Warn("compliance review message publish failed", zap.Error(err), zap.Int64("job_id", jobID), zap.Int64("check_record_id", checkRecordID), ) } } func (s *Service) createManualReviewBypassRecord(ctx context.Context, req GateForPublishRequest, snapshot articleVersionSnapshot) (*sharedcompliance.CheckResult, error) { policy := &policySnapshot{ MasterEnabled: true, EnforcementMode: "disabled", DictVersions: map[string]int{}, } return s.runAndPersist(ctx, runInput{ TenantID: req.TenantID, ArticleID: req.ArticleID, ArticleVersionID: req.ArticleVersionID, ActorID: req.ActorID, Title: snapshot.Title, Plaintext: "", TargetPlatforms: req.TargetPlatforms, TriggerSource: triggerManualReviewBypass, Policy: policy, ManualStatus: snapshot.ManualReviewStatus, ManualReviewID: snapshot.ManualReviewID, }) } func (s *Service) loadArticleVersionSnapshot(ctx context.Context, tenantID, articleID, versionID int64) (articleVersionSnapshot, error) { var snapshot articleVersionSnapshot var title pgtype.Text var plaintext pgtype.Text var manualStatus string var manualReviewID pgtype.Int8 var decisionReason pgtype.Text var reviewedAt pgtype.Timestamptz err := s.pool.QueryRow(ctx, ` SELECT a.id, av.id, av.title, av.plaintext_snapshot, av.manual_review_status, av.manual_review_id, mr.decision_reason, mr.reviewed_at FROM article_versions av JOIN articles a ON a.id = av.article_id LEFT JOIN compliance_manual_reviews mr ON mr.id = av.manual_review_id WHERE a.tenant_id = $1 AND a.id = $2 AND av.id = $3 AND a.deleted_at IS NULL `, tenantID, articleID, versionID).Scan( &snapshot.ArticleID, &snapshot.ArticleVersionID, &title, &plaintext, &manualStatus, &manualReviewID, &decisionReason, &reviewedAt, ) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return snapshot, response.ErrBadRequest(41005, "invalid_article_version", "article version does not belong to this article") } return snapshot, err } snapshot.Title = strings.TrimSpace(title.String) snapshot.Plaintext = strings.TrimSpace(plaintext.String) if snapshot.Plaintext == "" { content, err := tenantrepo.LoadArticleVersionContent(ctx, s.pool, articleID, versionID) if err == nil && content != nil { snapshot.Plaintext = strings.TrimSpace(snapshot.Title + "\n" + tenantrepo.ExtractArticlePlaintext(stringValue(content.MarkdownContent))) _, _ = s.pool.Exec(ctx, ` UPDATE article_versions SET plaintext_snapshot = $1 WHERE id = $2 AND plaintext_snapshot IS NULL `, snapshot.Plaintext, versionID) } } snapshot.ManualReviewStatus = sharedcompliance.ManualReviewStatus(strings.TrimSpace(manualStatus)) if snapshot.ManualReviewStatus == "" { snapshot.ManualReviewStatus = sharedcompliance.ManualReviewStatusNone } if manualReviewID.Valid { value := manualReviewID.Int64 snapshot.ManualReviewID = &value } if decisionReason.Valid { value := decisionReason.String snapshot.ManualReviewReason = &value } if reviewedAt.Valid { value := reviewedAt.Time snapshot.ManualReviewDecidedAt = &value } return snapshot, nil } func (s *Service) resolvePolicy(ctx context.Context, _ int64) (*policySnapshot, error) { cfg := s.currentComplianceConfig() now := time.Now() s.policyMu.Lock() if cached, ok := s.policies[globalPolicyCacheKey]; ok && cached.policy != nil && now.Before(cached.expires) { policy := cached.policy s.policyMu.Unlock() return policy, nil } s.policyMu.Unlock() policy, err := s.loadPolicy(ctx) if err != nil { s.policyMu.Lock() cached := s.policies[globalPolicyCacheKey] s.policyMu.Unlock() if cached.policy != nil && now.Sub(cached.policy.LoadedAt) < 24*time.Hour { if s.logger != nil { s.logger.Warn("using stale compliance policy", zap.Error(err)) } return cached.policy, nil } return nil, response.ErrServiceUnavailable(41004, "compliance_engine_unavailable", "compliance policy is unavailable") } s.policyMu.Lock() if s.policies == nil { s.policies = make(map[int64]cachedPolicy) } s.policies[globalPolicyCacheKey] = cachedPolicy{policy: policy, expires: now.Add(cfg.SnapshotReloadInterval)} s.policyMu.Unlock() return policy, nil } func (s *Service) loadPolicy(ctx context.Context) (*policySnapshot, error) { var ( globalMaster bool globalMode string globalEnabledRawText string globalLLM bool globalRevision int64 ) err := s.pool.QueryRow(ctx, ` SELECT master_enabled, enforcement_mode, enabled_dictionaries::text, llm_judge_enabled, revision FROM ops.compliance_policies WHERE id = 1 LIMIT 1 `).Scan( &globalMaster, &globalMode, &globalEnabledRawText, &globalLLM, &globalRevision, ) if err != nil { return nil, err } mode := strings.TrimSpace(globalMode) enabledRaw := []byte(globalEnabledRawText) llmEnabled := globalLLM revision := globalRevision enabledIDs := decodeJSONInt64Slice(enabledRaw) terms, dictNames, dictVersions, versionSum, err := s.loadTerms(ctx, enabledIDs) if err != nil { return nil, err } return &policySnapshot{ LoadedAt: time.Now(), MasterEnabled: globalMaster, EnforcementMode: normalizeMode(mode), LLMJudgeEnabled: llmEnabled && s.currentComplianceConfig().LLMJudgeEnabled, Revision: revision, DictionaryVersionSum: versionSum, EnabledDictionaries: dictNames, DictVersions: dictVersions, Terms: terms, }, nil } func editorPreviewPolicy(policy *policySnapshot) *policySnapshot { if policy == nil { return &policySnapshot{ LoadedAt: time.Now(), MasterEnabled: true, EnforcementMode: "advisory", DictVersions: map[string]int{}, } } copyPolicy := *policy if !copyPolicy.MasterEnabled { copyPolicy.MasterEnabled = true } if copyPolicy.EnforcementMode == "disabled" || strings.TrimSpace(copyPolicy.EnforcementMode) == "" { copyPolicy.EnforcementMode = "advisory" } copyPolicy.LLMJudgeEnabled = false return ©Policy } func normalizeArticleCheckTrigger(trigger string) string { switch strings.TrimSpace(trigger) { case triggerPublishGate: return triggerPublishGate default: return triggerEditorManual } } func (s *Service) loadTerms(ctx context.Context, enabledIDs []int64) ([]termRule, []string, map[string]int, int64, error) { filterEnabled := len(enabledIDs) > 0 rows, err := s.pool.Query(ctx, ` SELECT d.id, d.code, d.name, d.version, d.platform_scope, COALESCE(d.applicable_platforms, '{}') AS applicable_platforms, t.match_type, t.pattern, COALESCE(t.level_override, d.default_level) AS level, t.hint, t.reference_law FROM ops.compliance_dictionaries d JOIN ops.compliance_dictionary_terms t ON t.dictionary_id = d.id WHERE d.enabled = TRUE AND t.enabled = TRUE AND (NOT $1::boolean OR d.id = ANY($2::bigint[])) ORDER BY d.id, t.id `, filterEnabled, enabledIDs) if err != nil { return nil, nil, nil, 0, err } defer rows.Close() terms := make([]termRule, 0) dictSeen := map[string]struct{}{} dictNames := make([]string, 0) dictVersions := map[string]int{} var versionSum int64 for rows.Next() { var term termRule var hint pgtype.Text var ref pgtype.Text if err := rows.Scan( &term.DictionaryID, &term.DictionaryCode, &term.DictionaryName, &term.DictionaryVersion, &term.PlatformScope, &term.ApplicablePlatforms, &term.MatchType, &term.Pattern, &term.Level, &hint, &ref, ); err != nil { return nil, nil, nil, 0, err } term.Pattern = strings.TrimSpace(term.Pattern) if term.Pattern == "" { continue } if hint.Valid { value := hint.String term.Hint = &value } if ref.Valid { value := ref.String term.ReferenceLaw = &value } if term.MatchType == "regex" { compiled, err := regexp.Compile(term.Pattern) if err != nil { if s.logger != nil { s.logger.Warn("skip invalid compliance regex", zap.String("pattern", term.Pattern), zap.Error(err)) } continue } term.compiledRegex = compiled term.compiledRegexPattern = term.Pattern } terms = append(terms, term) if _, ok := dictSeen[term.DictionaryCode]; !ok { dictSeen[term.DictionaryCode] = struct{}{} dictNames = append(dictNames, term.DictionaryName) dictVersions[term.DictionaryCode] = term.DictionaryVersion versionSum += int64(term.DictionaryVersion) } } return terms, dictNames, dictVersions, versionSum, rows.Err() } func matchViolations(policy *policySnapshot, tenantID int64, targetPlatforms []string, content string) []sharedcompliance.Violation { if policy == nil || !policy.MasterEnabled || policy.EnforcementMode == "disabled" { return nil } content = strings.TrimSpace(content) if content == "" { return nil } violations := make([]sharedcompliance.Violation, 0) for _, term := range policy.Terms { platforms := applicableViolationPlatforms(term, targetPlatforms) if platforms == nil && term.PlatformScope == "specific" { continue } switch term.MatchType { case "regex": if term.compiledRegex == nil { continue } matches := term.compiledRegex.FindAllStringIndex(content, -1) for _, match := range matches { violations = append(violations, buildViolationsForTerm(term, platforms, content, match[0], match[1])...) } case "exact": searchFrom := 0 for { idx := strings.Index(content[searchFrom:], term.Pattern) if idx < 0 { break } start := searchFrom + idx end := start + len(term.Pattern) violations = append(violations, buildViolationsForTerm(term, platforms, content, start, end)...) searchFrom = end if searchFrom >= len(content) { break } } } } return violations } func applicableViolationPlatforms(term termRule, targetPlatforms []string) []string { if term.PlatformScope != "specific" { return nil } targetSet := make(map[string]struct{}, len(targetPlatforms)) for _, platform := range targetPlatforms { targetSet[platform] = struct{}{} } out := make([]string, 0) for _, platform := range term.ApplicablePlatforms { platform = strings.TrimSpace(platform) if platform == "" { continue } if len(targetSet) == 0 { out = append(out, platform) continue } if _, ok := targetSet[platform]; ok { out = append(out, platform) } } sort.Strings(out) return out } func buildViolationsForTerm(term termRule, platforms []string, content string, start, end int) []sharedcompliance.Violation { matched := content[start:end] base := sharedcompliance.Violation{ Source: sourceDict, DictionaryCode: stringPtr(term.DictionaryCode), DictionaryName: stringPtr(term.DictionaryName), TermPattern: stringPtr(term.Pattern), MatchedText: truncateRunes(matched, maxMatchedTextRunes), StartOffset: start, EndOffset: end, Level: normalizeLevel(term.Level), Hint: term.Hint, ReferenceLaw: term.ReferenceLaw, } if len(platforms) == 0 { return []sharedcompliance.Violation{base} } out := make([]sharedcompliance.Violation, 0, len(platforms)) for _, platform := range platforms { next := base next.PlatformCode = stringPtr(platform) out = append(out, next) } return out } func (s *Service) loadCheckRecord(ctx context.Context, tenantID, recordID int64) (checkRecordRow, error) { var record checkRecordRow var articleID pgtype.Int8 var articleVersionID pgtype.Int8 var highest pgtype.Text var manualReviewID pgtype.Int8 var manualStatus string err := s.pool.QueryRow(ctx, ` SELECT id, tenant_id, article_id, article_version_id, target_platforms, content_hash, policy_fingerprint, enforcement_mode, highest_level, passed, hit_count, stored_hit_count, truncated, manual_review_status, manual_review_id, checked_at, duration_ms FROM compliance_check_records WHERE id = $1 AND tenant_id = $2 `, recordID, tenantID).Scan( &record.ID, &record.TenantID, &articleID, &articleVersionID, &record.TargetPlatforms, &record.ContentHash, &record.PolicyFingerprint, &record.EnforcementMode, &highest, &record.Passed, &record.HitCount, &record.StoredHitCount, &record.Truncated, &manualStatus, &manualReviewID, &record.CheckedAt, &record.DurationMS, ) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return record, response.ErrNotFound(40461, "compliance_check_not_found", "compliance check record not found") } return record, response.ErrInternal(51005, "compliance_check_lookup_failed", "failed to load compliance check record") } if articleID.Valid { record.ArticleID = articleID.Int64 } if articleVersionID.Valid { record.ArticleVersionID = articleVersionID.Int64 } if highest.Valid { record.HighestLevel = &highest.String } if manualReviewID.Valid { record.ManualReviewID = &manualReviewID.Int64 } record.ManualReviewStatus = sharedcompliance.ManualReviewStatus(manualStatus) return record, nil } func (s *Service) loadViolations(ctx context.Context, tenantID, recordID int64) ([]sharedcompliance.Violation, error) { rows, err := s.pool.Query(ctx, ` SELECT id, record_id, platform_code, source, dictionary_code, dictionary_name, term_pattern, matched_text, start_offset, end_offset, dom_path, level, hint, reference_law FROM compliance_violations WHERE tenant_id = $1 AND record_id = $2 ORDER BY id `, tenantID, recordID) if err != nil { return nil, response.ErrInternal(51008, "compliance_violation_lookup_failed", "failed to load compliance violations") } defer rows.Close() out := make([]sharedcompliance.Violation, 0) for rows.Next() { var v sharedcompliance.Violation var platform pgtype.Text var dictCode pgtype.Text var dictName pgtype.Text var termPattern pgtype.Text var domPath pgtype.Text var hint pgtype.Text var ref pgtype.Text if err := rows.Scan( &v.ID, &v.RecordID, &platform, &v.Source, &dictCode, &dictName, &termPattern, &v.MatchedText, &v.StartOffset, &v.EndOffset, &domPath, &v.Level, &hint, &ref, ); err != nil { return nil, response.ErrInternal(51008, "compliance_violation_lookup_failed", "failed to scan compliance violations") } if platform.Valid { v.PlatformCode = &platform.String } if dictCode.Valid { v.DictionaryCode = &dictCode.String } if dictName.Valid { v.DictionaryName = &dictName.String } if termPattern.Valid { v.TermPattern = &termPattern.String } if domPath.Valid { v.DomPath = &domPath.String } if hint.Valid { v.Hint = &hint.String } if ref.Valid { v.ReferenceLaw = &ref.String } out = append(out, v) } return out, rows.Err() } func (s *Service) loadLatestManualReviewByVersion(ctx context.Context, tenantID, versionID int64) (*sharedcompliance.ManualReview, error) { var review sharedcompliance.ManualReview var checkID pgtype.Int8 var requestNote pgtype.Text var reviewedBy pgtype.Int8 var reviewedAt pgtype.Timestamptz var decisionReason pgtype.Text var cancelledBy pgtype.Int8 var cancelledAt pgtype.Timestamptz var cancelReason pgtype.Text var summaryRaw []byte var platforms []string err := s.pool.QueryRow(ctx, ` SELECT mr.id, mr.tenant_id, mr.article_id, mr.article_version_id, mr.original_check_record_id, mr.requested_by, mr.requested_at, mr.request_note, mr.status, mr.cancelled_by, mr.cancelled_at, mr.cancel_reason, mr.reviewed_by, mr.reviewed_at, mr.decision_reason, mr.original_violation_summary, mr.original_target_platforms FROM compliance_manual_reviews mr WHERE mr.tenant_id = $1 AND mr.article_version_id = $2 AND mr.status <> 'cancelled' ORDER BY mr.requested_at DESC, mr.id DESC LIMIT 1 `, tenantID, versionID).Scan( &review.ID, &review.TenantID, &review.ArticleID, &review.ArticleVersionID, &checkID, &review.RequestedBy, &review.RequestedAt, &requestNote, &review.Status, &cancelledBy, &cancelledAt, &cancelReason, &reviewedBy, &reviewedAt, &decisionReason, &summaryRaw, &platforms, ) if err != nil { return nil, err } if checkID.Valid { review.CheckRecordID = &checkID.Int64 } if requestNote.Valid { review.RequestNote = &requestNote.String } if reviewedBy.Valid { review.ReviewedBy = &reviewedBy.Int64 } if reviewedAt.Valid { review.ReviewedAt = &reviewedAt.Time } if decisionReason.Valid { review.DecisionReason = &decisionReason.String } if cancelledBy.Valid { review.CancelledBy = &cancelledBy.Int64 } if cancelledAt.Valid { review.CancelledAt = &cancelledAt.Time } if cancelReason.Valid { review.CancelReason = &cancelReason.String } _ = json.Unmarshal(summaryRaw, &review.ViolationSummary) review.OriginalPlatforms = platforms return &review, nil } func (s *Service) lookupManualReviewStatus(ctx context.Context, tenantID, versionID int64) (sharedcompliance.ManualReviewStatus, error) { var status string err := s.pool.QueryRow(ctx, ` SELECT status FROM compliance_manual_reviews WHERE tenant_id = $1 AND article_version_id = $2 AND status <> 'cancelled' ORDER BY requested_at DESC, id DESC LIMIT 1 `, tenantID, versionID).Scan(&status) if err != nil { return "", err } return sharedcompliance.ManualReviewStatus(status), nil } func checkResultFromRecord(record checkRecordRow, violations []sharedcompliance.Violation, decision sharedcompliance.GateDecision) *sharedcompliance.CheckResult { return &sharedcompliance.CheckResult{ RecordID: record.ID, Decision: decision, Passed: record.Passed, HighestLevel: record.HighestLevel, Violations: violations, HitCount: record.HitCount, StoredHitCount: record.StoredHitCount, Truncated: record.Truncated, EnforcementMode: record.EnforcementMode, ContentHash: record.ContentHash, PolicyFingerprint: record.PolicyFingerprint, TargetPlatforms: record.TargetPlatforms, ManualReviewStatus: record.ManualReviewStatus, ManualReviewID: record.ManualReviewID, CheckedAt: record.CheckedAt, DurationMS: record.DurationMS, } } func manualReviewOnlyResult(snapshot articleVersionSnapshot, targets []string) *sharedcompliance.CheckResult { decision := sharedcompliance.GateDecisionBlock passed := false if snapshot.ManualReviewStatus == sharedcompliance.ManualReviewStatusApproved { decision = sharedcompliance.GateDecisionPass passed = true } return &sharedcompliance.CheckResult{ Decision: decision, Passed: passed, Violations: []sharedcompliance.Violation{}, EnforcementMode: "disabled", ContentHash: contentHash(snapshot.Title, snapshot.Plaintext), PolicyFingerprint: "", TargetPlatforms: normalizePlatforms(targets), ManualReviewStatus: snapshot.ManualReviewStatus, ManualReviewID: snapshot.ManualReviewID, ManualReviewReason: snapshot.ManualReviewReason, ManualReviewDecidedAt: snapshot.ManualReviewDecidedAt, CheckedAt: time.Now().UTC(), } } func disabledCheckResult(targets []string) *sharedcompliance.CheckResult { return &sharedcompliance.CheckResult{ Decision: sharedcompliance.GateDecisionPass, Passed: true, Violations: []sharedcompliance.Violation{}, EnforcementMode: "disabled", TargetPlatforms: normalizePlatforms(targets), ManualReviewStatus: sharedcompliance.ManualReviewStatusNone, CheckedAt: time.Now().UTC(), } } func (s *Service) currentComplianceConfig() config.ComplianceConfig { if s != nil && s.cfg != nil { if cfg := s.cfg.Current(); cfg != nil { out := cfg.Compliance config.NormalizeComplianceConfig(&out) return out } } out := config.ComplianceConfig{Enabled: true} config.NormalizeComplianceConfig(&out) return out } func decide(mode string, highest *string, hitCount int) sharedcompliance.GateDecision { if hitCount <= 0 || highest == nil { return sharedcompliance.GateDecisionPass } if mode == "mandatory" { return sharedcompliance.GateDecisionBlock } return sharedcompliance.GateDecisionNeedsAck } func highestViolationLevel(violations []sharedcompliance.Violation) *string { if len(violations) == 0 { return nil } best := "info" for _, violation := range violations { if levelRank(violation.Level) > levelRank(best) { best = violation.Level } } return &best } func levelRank(level string) int { switch strings.TrimSpace(level) { case "block": return 4 case "high": return 3 case "medium": return 2 case "info": return 1 default: return 0 } } func normalizeLevel(level string) string { level = strings.TrimSpace(level) switch level { case "block", "high", "medium", "info": return level default: return "high" } } func normalizeMode(mode string) string { mode = strings.TrimSpace(mode) switch mode { case "mandatory", "advisory", "disabled": return mode default: return "mandatory" } } func contentHash(title, plaintext string) string { sum := sha256.Sum256([]byte(strings.TrimSpace(title) + "\n" + strings.TrimSpace(plaintext))) return hex.EncodeToString(sum[:]) } func policyFingerprint(policy *policySnapshot, targets []string) string { targets = normalizePlatforms(targets) parts := []string{ fmt.Sprintf("rev:%d", policy.Revision), "mode:" + policy.EnforcementMode, fmt.Sprintf("llm:%t", policy.LLMJudgeEnabled), "targets:" + strings.Join(targets, ","), } keys := make([]string, 0, len(policy.DictVersions)) for key := range policy.DictVersions { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { parts = append(parts, fmt.Sprintf("%s:%d", key, policy.DictVersions[key])) } sum := sha256.Sum256([]byte(strings.Join(parts, "|"))) return hex.EncodeToString(sum[:]) } func recordDictVersions(policy *policySnapshot) map[string]int { if policy == nil || policy.DictVersions == nil { return map[string]int{} } out := make(map[string]int, len(policy.DictVersions)) for key, value := range policy.DictVersions { out[key] = value } return out } func normalizePlatforms(platforms []string) []string { seen := make(map[string]struct{}, len(platforms)) out := make([]string, 0, len(platforms)) for _, platform := range platforms { platform = strings.TrimSpace(platform) if platform == "" { continue } if _, ok := seen[platform]; ok { continue } seen[platform] = struct{}{} out = append(out, platform) } sort.Strings(out) return out } func decodeJSONInt64Slice(raw []byte) []int64 { if len(raw) == 0 { return nil } var nums []int64 if err := json.Unmarshal(raw, &nums); err == nil { return nums } var floats []float64 if err := json.Unmarshal(raw, &floats); err != nil { return nil } out := make([]int64, 0, len(floats)) for _, n := range floats { out = append(out, int64(n)) } return out } func sameInt64Set(a, b []int64) bool { if len(a) != len(b) { return false } a = append([]int64(nil), a...) b = append([]int64(nil), b...) sort.Slice(a, func(i, j int) bool { return a[i] < a[j] }) sort.Slice(b, func(i, j int) bool { return b[i] < b[j] }) for i := range a { if a[i] != b[i] { return false } } return true } func truncateRunes(value string, maxRunes int) string { value = strings.TrimSpace(value) if maxRunes <= 0 { return "" } runes := []rune(value) if len(runes) <= maxRunes { return value } return string(runes[:maxRunes]) } func stringPtr(value string) *string { return &value } func stringValue(value *string) string { if value == nil { return "" } return *value } func errorString(err error) string { if err == nil { return "" } return err.Error() } func nullableString(value string) *string { value = strings.TrimSpace(value) if value == "" { return nil } return &value } func int64PtrAny(value *int64) any { if value == nil { return nil } return *value } func rollbackTx(tx pgx.Tx) { if tx == nil { return } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _ = tx.Rollback(ctx) } func isUniqueViolation(err error) bool { var pgErr *pgconn.PgError return errors.As(err, &pgErr) && pgErr.Code == "23505" } func (s *Service) cacheManualReview(snapshot articleVersionSnapshot) { if snapshot.ArticleVersionID <= 0 { return } s.reviewCache.Store(snapshot.ArticleVersionID, struct { snapshot articleVersionSnapshot expires time.Time }{snapshot: snapshot, expires: time.Now().Add(60 * time.Second)}) } func (s *Service) cachedManualReview(versionID int64) (articleVersionSnapshot, bool) { value, ok := s.reviewCache.Load(versionID) if !ok { return articleVersionSnapshot{}, false } typed, ok := value.(struct { snapshot articleVersionSnapshot expires time.Time }) if !ok || time.Now().After(typed.expires) { s.reviewCache.Delete(versionID) return articleVersionSnapshot{}, false } return typed.snapshot, true } func manualReviewGateError(snapshot articleVersionSnapshot) error { switch snapshot.ManualReviewStatus { case sharedcompliance.ManualReviewStatusPending: return response.ErrConflict(41007, "compliance_manual_review_pending", "current article version is waiting for manual review") case sharedcompliance.ManualReviewStatusRejected: return response.ErrConflict(41006, "compliance_manual_review_rejected", "current article version was rejected by manual review") default: return nil } }