package repo import ( "database/sql" "encoding/json" "time" "git.gozeros.com/wsk/eightbf-analysis-go/internal/domain" ) // JobRepo 会商作业表读写(eba_analysis_job)。大字段整段 JSON 文本存。 type JobRepo struct { DB *sql.DB } func NewJobRepo(db *sql.DB) *JobRepo { return &JobRepo{DB: db} } // EnsureSchema 执行建表脚本(幂等;重复索引错误由调用方忽略亦可)。 func (r *JobRepo) EnsureSchema(schemaSQL string) error { _, err := r.DB.Exec(schemaSQL) return err } func (r *JobRepo) Insert(job *domain.AnalysisJob) error { now := time.Now() job.CreatedAt = &now job.UpdatedAt = &now _, err := r.DB.Exec(`INSERT INTO eba_analysis_job( id,request_id,task_id,draft_id,draft_key,status,phase,cycle,active_agent_id, consensus_percentage,pending_clarification,orchestration_mode,model,error_message, owner_account_id,agents_json,chain_json,plan_json,access_json,thread_ids_json, progress_json,result_json,cancel_flag,created_at,updated_at ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0,?,?)`, job.ID, nullStr(job.RequestID), nullStr(job.TaskID), nullStr(job.DraftID), nullStr(job.DraftKey), job.Status, nullStr(job.Phase), job.Cycle, nullStr(job.ActiveAgentID), job.ConsensusPercentage, nullStr(job.PendingClarification), nullStr(job.OrchestrationMode), nullStr(job.Model), nullStr(job.Error), nullStr(job.OwnerAccountID), mustJSON(job.Agents), mustJSON(job.Dialogues), mustJSON(job.Plan), mustJSON(job.Access), mustJSON(job.ThreadIDs), mustJSON(job.Progress), mustJSON(job.Result), now, now, ) return err } // UpdateProgress 全量刷新进度相关列;cancelled 时同步 cancel_flag=1。 func (r *JobRepo) UpdateProgress(job *domain.AnalysisJob) error { now := time.Now() job.UpdatedAt = &now cancel := 0 if job.Status == domain.StatusCancelled { cancel = 1 } _, err := r.DB.Exec(`UPDATE eba_analysis_job SET status=?, phase=?, cycle=?, active_agent_id=?, consensus_percentage=?, pending_clarification=?, model=?, error_message=?, owner_account_id=?, agents_json=?, chain_json=?, plan_json=?, access_json=?, thread_ids_json=?, progress_json=?, result_json=?, cancel_flag=?, updated_at=? WHERE id=?`, job.Status, nullStr(job.Phase), job.Cycle, nullStr(job.ActiveAgentID), job.ConsensusPercentage, nullStr(job.PendingClarification), nullStr(job.Model), nullStr(job.Error), nullStr(job.OwnerAccountID), mustJSON(job.Agents), mustJSON(job.Dialogues), mustJSON(job.Plan), mustJSON(job.Access), mustJSON(job.ThreadIDs), mustJSON(job.Progress), mustJSON(job.Result), cancel, now, job.ID, ) return err } func (r *JobRepo) FindByID(id string) (*domain.AnalysisJob, error) { rows, err := r.DB.Query(`SELECT * FROM eba_analysis_job WHERE id=?`, id) if err != nil { return nil, err } defer rows.Close() if !rows.Next() { return nil, nil } return scanJob(rows) } func (r *JobRepo) FindActiveByTaskID(taskID string) (*domain.AnalysisJob, error) { if taskID == "" { return nil, nil } rows, err := r.DB.Query(`SELECT * FROM eba_analysis_job WHERE task_id=? AND status IN (?,?,?) ORDER BY created_at DESC LIMIT 1`, taskID, domain.StatusQueued, domain.StatusRunning, domain.StatusAwaitingInput) if err != nil { return nil, err } defer rows.Close() if !rows.Next() { return nil, nil } return scanJob(rows) } func (r *JobRepo) FindLatestByTaskID(taskID string) (*domain.AnalysisJob, error) { if taskID == "" { return nil, nil } rows, err := r.DB.Query(`SELECT * FROM eba_analysis_job WHERE task_id=? ORDER BY created_at DESC LIMIT 1`, taskID) if err != nil { return nil, err } defer rows.Close() if !rows.Next() { return nil, nil } return scanJob(rows) } func (r *JobRepo) FindActiveJobs() ([]*domain.AnalysisJob, error) { rows, err := r.DB.Query(`SELECT * FROM eba_analysis_job WHERE status IN (?,?,?) ORDER BY created_at ASC`, domain.StatusQueued, domain.StatusRunning, domain.StatusAwaitingInput) if err != nil { return nil, err } defer rows.Close() return scanJobs(rows) } func (r *JobRepo) FindQueuedJobs() ([]*domain.AnalysisJob, error) { rows, err := r.DB.Query(`SELECT * FROM eba_analysis_job WHERE status=? ORDER BY created_at ASC`, domain.StatusQueued) if err != nil { return nil, err } defer rows.Close() return scanJobs(rows) } func (r *JobRepo) MarkCancel(id string) (bool, error) { res, err := r.DB.Exec(`UPDATE eba_analysis_job SET cancel_flag=1, updated_at=? WHERE id=? AND status IN (?,?,?)`, time.Now(), id, domain.StatusQueued, domain.StatusRunning, domain.StatusAwaitingInput) if err != nil { return false, err } n, _ := res.RowsAffected() return n > 0, nil } func (r *JobRepo) IsCancelFlag(id string) (bool, error) { var v sql.NullInt64 err := r.DB.QueryRow(`SELECT cancel_flag FROM eba_analysis_job WHERE id=?`, id).Scan(&v) if err == sql.ErrNoRows { return false, nil } if err != nil { return false, err } return v.Valid && v.Int64 == 1, nil } func scanJobs(rows *sql.Rows) ([]*domain.AnalysisJob, error) { var out []*domain.AnalysisJob for rows.Next() { j, err := scanJob(rows) if err != nil { return nil, err } out = append(out, j) } return out, rows.Err() } func scanJob(rows *sql.Rows) (*domain.AnalysisJob, error) { cols, err := rows.Columns() if err != nil { return nil, err } vals := make([]interface{}, len(cols)) ptrs := make([]interface{}, len(cols)) for i := range vals { ptrs[i] = &vals[i] } if err := rows.Scan(ptrs...); err != nil { return nil, err } m := map[string]interface{}{} for i, c := range cols { m[c] = vals[i] } j := &domain.AnalysisJob{} j.ID = asString(m["id"]) j.RequestID = asString(m["request_id"]) j.TaskID = asString(m["task_id"]) j.DraftID = asString(m["draft_id"]) j.DraftKey = asString(m["draft_key"]) j.Status = asString(m["status"]) j.Phase = asString(m["phase"]) j.Cycle = asInt(m["cycle"]) j.ActiveAgentID = asString(m["active_agent_id"]) j.ConsensusPercentage = asInt(m["consensus_percentage"]) j.PendingClarification = asString(m["pending_clarification"]) j.OrchestrationMode = asString(m["orchestration_mode"]) j.Model = asString(m["model"]) j.Error = asString(m["error_message"]) j.OwnerAccountID = asString(m["owner_account_id"]) j.Agents = readList(asString(m["agents_json"])) j.Dialogues = readList(asString(m["chain_json"])) j.Plan = readMap(asString(m["plan_json"])) j.Access = readMap(asString(m["access_json"])) j.ThreadIDs = readMap(asString(m["thread_ids_json"])) j.Progress = readMap(asString(m["progress_json"])) j.Result = readMap(asString(m["result_json"])) if t := asTime(m["created_at"]); t != nil { j.CreatedAt = t } if t := asTime(m["updated_at"]); t != nil { j.UpdatedAt = t } return j, nil } func mustJSON(v interface{}) interface{} { if v == nil { return nil } b, err := json.Marshal(v) if err != nil { return nil } return string(b) } func readMap(jsonStr string) map[string]interface{} { if jsonStr == "" { return nil } var m map[string]interface{} if err := json.Unmarshal([]byte(jsonStr), &m); err != nil { return nil } return m } func readList(jsonStr string) []map[string]interface{} { if jsonStr == "" { return []map[string]interface{}{} } var list []map[string]interface{} if err := json.Unmarshal([]byte(jsonStr), &list); err != nil { return []map[string]interface{}{} } return list } func nullStr(s string) interface{} { if s == "" { return nil } return s } func asString(v interface{}) string { if v == nil { return "" } switch t := v.(type) { case string: return t case []byte: return string(t) default: return "" } } func asInt(v interface{}) int { if v == nil { return 0 } switch t := v.(type) { case int64: return int(t) case int32: return int(t) case int: return t case []byte: var n int _ = json.Unmarshal(t, &n) return n default: return 0 } } func asTime(v interface{}) *time.Time { if v == nil { return nil } if t, ok := v.(time.Time); ok { tt := t return &tt } return nil }