package repository import ( "database/sql" "errors" "fmt" "cmautobuy/admin/model" ) func InsertAIMatchBatch(q Execer, batch model.AIMatchBatch, items []model.AIMatchBatchItem) error { _, err := q.Exec(`INSERT INTO ai_match_batches(batch_id,status,created_by_user_id,total_count, provider_id,provider_name,provider_base_url,model,timeout_seconds,max_concurrency, confidence_threshold_bps,config_fingerprint,rules_version,prompt_version,created_at) VALUES(?,?,?, ?,?,?,?,?,?,?,?,?,?,?,?)`, batch.BatchID, batch.Status, batch.CreatedByUserID, batch.TotalCount, batch.ProviderID, batch.ProviderName, batch.ProviderBaseURL, batch.Model, batch.TimeoutSeconds, batch.MaxConcurrency, batch.ConfidenceThresholdBPS, batch.ConfigFingerprint, batch.RulesVersion, batch.PromptVersion, batch.CreatedAt) if err != nil { return fmt.Errorf("创建 AI 匹配批次失败: %w", err) } for _, item := range items { if _, err := q.Exec(`INSERT INTO ai_match_batch_items(item_id,batch_id,syb_id,identity_hash, leader_syb_id,context_version,position,status) VALUES(?,?,?,?,?,?,?,?)`, item.ItemID, item.BatchID, item.SybID, item.IdentityHash, item.LeaderSybID, item.ContextVersion, item.Position, item.Status); err != nil { return fmt.Errorf("创建 AI 匹配批次明细失败: %w", err) } } return nil } func MarkAIMatchBatchRunning(q Execer, batchID, startedAt string) error { result, err := q.Exec(`UPDATE ai_match_batches SET status='running',started_at=? WHERE batch_id=? AND status='queued'`, startedAt, batchID) if err != nil { return fmt.Errorf("启动 AI 匹配批次失败: %w", err) } affected, err := result.RowsAffected() if err != nil { return err } if affected != 1 { return fmt.Errorf("AI 匹配批次不是待运行状态") } return nil } func ListAIMatchLeaderItems(q Execer, batchID string) ([]model.AIMatchBatchItem, error) { rows, err := q.Query(`SELECT item_id,batch_id,syb_id,identity_hash,leader_syb_id, context_version,position,status,outcome,message,mapping_source,option_key,confidence_bps, model_called,started_at,finished_at FROM ai_match_batch_items WHERE batch_id=? AND syb_id=leader_syb_id AND status='queued' ORDER BY position`, batchID) if err != nil { return nil, fmt.Errorf("读取 AI 匹配批次工作项失败: %w", err) } defer rows.Close() return scanAIMatchItems(rows) } func ListAIMatchIdentityItems(q Execer, batchID, identityHash string) ([]model.AIMatchBatchItem, error) { rows, err := q.Query(`SELECT item_id,batch_id,syb_id,identity_hash,leader_syb_id, context_version,position,status,outcome,message,mapping_source,option_key,confidence_bps, model_called,started_at,finished_at FROM ai_match_batch_items WHERE batch_id=? AND identity_hash=? ORDER BY position`, batchID, identityHash) if err != nil { return nil, fmt.Errorf("读取 AI 匹配批次同规格明细失败: %w", err) } defer rows.Close() return scanAIMatchItems(rows) } func ListAIMatchBatchItems(q Execer, batchID string) ([]model.AIMatchBatchItem, error) { rows, err := q.Query(`SELECT item_id,batch_id,syb_id,identity_hash,leader_syb_id, context_version,position,status,outcome,message,mapping_source,option_key,confidence_bps, model_called,started_at,finished_at FROM ai_match_batch_items WHERE batch_id=? ORDER BY position`, batchID) if err != nil { return nil, fmt.Errorf("读取 AI 匹配批次明细失败: %w", err) } defer rows.Close() return scanAIMatchItems(rows) } func scanAIMatchItems(rows *sql.Rows) ([]model.AIMatchBatchItem, error) { var result []model.AIMatchBatchItem for rows.Next() { var item model.AIMatchBatchItem var outcome, message, source, optionKey, startedAt, finishedAt sql.NullString var confidence sql.NullInt64 var called int if err := rows.Scan(&item.ItemID, &item.BatchID, &item.SybID, &item.IdentityHash, &item.LeaderSybID, &item.ContextVersion, &item.Position, &item.Status, &outcome, &message, &source, &optionKey, &confidence, &called, &startedAt, &finishedAt); err != nil { return nil, err } item.Outcome, item.Message, item.MappingSource, item.OptionKey = outcome.String, message.String, source.String, optionKey.String item.ConfidenceBPS, item.ConfidenceSet = int(confidence.Int64), confidence.Valid item.ModelCalled, item.StartedAt, item.FinishedAt = called == 1, startedAt.String, finishedAt.String result = append(result, item) } return result, rows.Err() } func MarkAIMatchItemRunning(q Execer, itemID, startedAt string) error { _, err := q.Exec(`UPDATE ai_match_batch_items SET status='running',started_at=? WHERE item_id=? AND status='queued'`, startedAt, itemID) if err != nil { return fmt.Errorf("标记 AI 匹配明细运行失败: %w", err) } return nil } func FinishAIMatchItem(q Execer, item model.AIMatchBatchItem) error { _, err := q.Exec(`UPDATE ai_match_batch_items SET status=?,outcome=?,message=?,mapping_source=?, option_key=?,confidence_bps=?,model_called=?,finished_at=? WHERE item_id=? AND status IN ('queued','running')`, item.Status, nullableText(item.Outcome), nullableText(item.Message), nullableText(item.MappingSource), nullableText(item.OptionKey), nullableInt(item.ConfidenceBPS, item.ConfidenceSet), item.ModelCalled, item.FinishedAt, item.ItemID) if err != nil { return fmt.Errorf("完成 AI 匹配明细失败: %w", err) } return nil } type AIMatchBatchCounts struct{ Processed, Success, Reused, Manual, Failed int } func CountAIMatchBatchItems(q Execer, batchID string) (AIMatchBatchCounts, error) { var result AIMatchBatchCounts err := q.QueryRow(`SELECT SUM(status NOT IN ('queued','running')), SUM(status='succeeded'),SUM(status='reused'),SUM(status IN ('manual','stale')), SUM(status IN ('failed','interrupted')) FROM ai_match_batch_items WHERE batch_id=?`, batchID). Scan(&result.Processed, &result.Success, &result.Reused, &result.Manual, &result.Failed) if err != nil { return result, fmt.Errorf("统计 AI 匹配批次失败: %w", err) } return result, nil } func FinishAIMatchBatch(q Execer, batchID, status, errorMessage, finishedAt string, counts AIMatchBatchCounts) error { _, err := q.Exec(`UPDATE ai_match_batches SET status=?,processed_count=?,success_count=?,reused_count=?, manual_count=?,failed_count=?,error_message=?,finished_at=? WHERE batch_id=?`, status, counts.Processed, counts.Success, counts.Reused, counts.Manual, counts.Failed, nullableText(errorMessage), finishedAt, batchID) if err != nil { return fmt.Errorf("完成 AI 匹配批次失败: %w", err) } return nil } func UpdateAIMatchBatchCounts(q Execer, batchID string, counts AIMatchBatchCounts) error { _, err := q.Exec(`UPDATE ai_match_batches SET processed_count=?,success_count=?,reused_count=?, manual_count=?,failed_count=? WHERE batch_id=? AND status='running'`, counts.Processed, counts.Success, counts.Reused, counts.Manual, counts.Failed, batchID) if err != nil { return fmt.Errorf("更新 AI 匹配批次进度失败: %w", err) } return nil } func GetAIMatchBatch(q Execer, batchID string) (*model.AIMatchBatch, error) { var batch model.AIMatchBatch var errorMessage, startedAt, finishedAt sql.NullString err := q.QueryRow(`SELECT batch_id,status,created_by_user_id,total_count,processed_count, success_count,reused_count,manual_count,failed_count,provider_id,provider_name, provider_base_url,model,timeout_seconds,max_concurrency,confidence_threshold_bps, config_fingerprint,rules_version,prompt_version,error_message,created_at,started_at,finished_at FROM ai_match_batches WHERE batch_id=?`, batchID).Scan(&batch.BatchID, &batch.Status, &batch.CreatedByUserID, &batch.TotalCount, &batch.ProcessedCount, &batch.SuccessCount, &batch.ReusedCount, &batch.ManualCount, &batch.FailedCount, &batch.ProviderID, &batch.ProviderName, &batch.ProviderBaseURL, &batch.Model, &batch.TimeoutSeconds, &batch.MaxConcurrency, &batch.ConfidenceThresholdBPS, &batch.ConfigFingerprint, &batch.RulesVersion, &batch.PromptVersion, &errorMessage, &batch.CreatedAt, &startedAt, &finishedAt) if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { return nil, fmt.Errorf("读取 AI 匹配批次失败: %w", err) } batch.ErrorMessage, batch.StartedAt, batch.FinishedAt = errorMessage.String, startedAt.String, finishedAt.String return &batch, nil } func GetLatestAIMatchBatchID(q Execer, userID string, admin bool) (string, error) { query := `SELECT batch_id FROM ai_match_batches` var args []any if !admin { query += ` WHERE created_by_user_id=?` args = append(args, userID) } query += ` ORDER BY created_at DESC,batch_id DESC LIMIT 1` var batchID string err := q.QueryRow(query, args...).Scan(&batchID) if errors.Is(err, sql.ErrNoRows) { return "", nil } if err != nil { return "", fmt.Errorf("读取最近 AI 匹配批次失败: %w", err) } return batchID, nil } func ListUnfinishedAIMatchBatchIDs(q Execer) ([]string, error) { rows, err := q.Query(`SELECT batch_id FROM ai_match_batches WHERE status IN ('queued','running') ORDER BY created_at,batch_id`) if err != nil { return nil, err } defer rows.Close() var result []string for rows.Next() { var id string if err := rows.Scan(&id); err != nil { return nil, err } result = append(result, id) } return result, rows.Err() } func InterruptAIMatchBatch(q Execer, batchID, finishedAt string) error { if _, err := q.Exec(`UPDATE ai_match_batch_items SET status='interrupted',outcome='failed', message='Admin 在批次完成前退出,可重新勾选未成功条目安全重试',finished_at=? WHERE batch_id=? AND status IN ('queued','running')`, finishedAt, batchID); err != nil { return err } counts, err := CountAIMatchBatchItems(q, batchID) if err != nil { return err } return FinishAIMatchBatch(q, batchID, "interrupted", "Admin 在批次完成前退出", finishedAt, counts) }