231 lines
9.6 KiB
Go
231 lines
9.6 KiB
Go
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)
|
|
}
|