feat: 档口入库码后台批量回写 (#246)
This commit is contained in:
@@ -24,11 +24,11 @@ func TestInterruptApplyingInnerCodes_只收敛回写中记录(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := db.Exec(`INSERT INTO syb_inner_code_records(id,status,updated_at) VALUES
|
||||
(1,'applying','old'),(2,'ready','old')`); err != nil {
|
||||
(1,'applying','old'),(2,'ready','old'),(3,'queued','old')`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
count, err := InterruptApplyingInnerCodes(db, "2026-08-15T01:00:00Z")
|
||||
if err != nil || count != 1 {
|
||||
if err != nil || count != 2 {
|
||||
t.Fatalf("count=%d err=%v", count, err)
|
||||
}
|
||||
var status, message, updatedAt string
|
||||
@@ -42,6 +42,9 @@ func TestInterruptApplyingInnerCodes_只收敛回写中记录(t *testing.T) {
|
||||
if err := db.QueryRow(`SELECT status FROM syb_inner_code_records WHERE id=2`).Scan(&status); err != nil || status != "ready" {
|
||||
t.Fatalf("ready 记录不应变化 status=%s err=%v", status, err)
|
||||
}
|
||||
if err := db.QueryRow(`SELECT status,result_message FROM syb_inner_code_records WHERE id=3`).Scan(&status, &message); err != nil || status != "ready" || message == "" {
|
||||
t.Fatalf("queued 记录应恢复可回写 status=%s message=%q err=%v", status, message, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInnerCodeImportWriteError_唯一冲突不泄漏索引细节(t *testing.T) {
|
||||
@@ -65,7 +68,7 @@ func TestSoftDeleteInnerCodeRecords_所有状态只写删除审计(t *testing.T)
|
||||
)`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
statuses := []string{"pending", "ready", "applying", "updated", "already_filled", "skipped", "failed", "needs_check"}
|
||||
statuses := []string{"pending", "ready", "queued", "applying", "updated", "already_filled", "skipped", "failed", "needs_check"}
|
||||
for index, status := range statuses {
|
||||
if _, err := db.Exec(`INSERT INTO syb_inner_code_records(id,status,updated_at) VALUES(?,?,?)`, index+1, status, "old"); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -120,6 +123,95 @@ func TestSoftDeleteInnerCodeRecords_有失效ID时整批回滚(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueueInnerCodeApplyBatch_状态冲突时整批回滚(t *testing.T) {
|
||||
db, err := sql.Open("sqlite", "file:inner_code_queue_conflict?mode=memory&cache=shared")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
if _, err := db.Exec(`CREATE TABLE syb_inner_code_records (
|
||||
id INTEGER PRIMARY KEY,status TEXT NOT NULL,apply_batch_id TEXT,apply_queued_at TEXT,
|
||||
applied_by_user_id TEXT,result_message TEXT,apply_started_at TEXT,updated_at TEXT,
|
||||
deleted_at TEXT
|
||||
); INSERT INTO syb_inner_code_records(id,status,updated_at) VALUES
|
||||
(1,'ready','old'),(2,'pending','old')`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := QueueInnerCodeApplyBatch(db, []int64{1, 2}, "ICB-1", "user-1", "2026-08-15T05:00:00Z"); !errors.Is(err, ErrInnerCodeApplyConflict) {
|
||||
t.Fatalf("期望整批排队冲突,实际 %v", err)
|
||||
}
|
||||
var status string
|
||||
var batchID sql.NullString
|
||||
if err := db.QueryRow(`SELECT status,apply_batch_id FROM syb_inner_code_records WHERE id=1`).Scan(&status, &batchID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if status != "ready" || batchID.Valid {
|
||||
t.Fatalf("冲突后不应部分入队 status=%s batch=%+v", status, batchID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInnerCodeApplyBatchProgress_聚合终态与恢复记录(t *testing.T) {
|
||||
db, err := sql.Open("sqlite", "file:inner_code_batch_progress?mode=memory&cache=shared")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
if _, err := db.Exec(`CREATE TABLE syb_inner_code_records (
|
||||
id INTEGER PRIMARY KEY,status TEXT NOT NULL,apply_batch_id TEXT
|
||||
); INSERT INTO syb_inner_code_records(id,status,apply_batch_id) VALUES
|
||||
(1,'queued','ICB-1'),(2,'applying','ICB-1'),(3,'updated','ICB-1'),
|
||||
(4,'failed','ICB-1'),(5,'needs_check','ICB-1'),(6,'ready','ICB-1')`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
progress, err := GetInnerCodeApplyBatchProgress(db, "ICB-1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if progress == nil || progress.Total != 6 || progress.Queued != 1 || progress.Applying != 1 ||
|
||||
progress.Updated != 1 || progress.Failed != 1 || progress.NeedsCheck != 1 ||
|
||||
progress.Ready != 1 || progress.Processed() != 3 {
|
||||
t.Fatalf("progress=%+v", progress)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterruptInnerCodeApplyBatch_区分未知结果和未开始记录(t *testing.T) {
|
||||
db, err := sql.Open("sqlite", "file:inner_code_batch_interrupt?mode=memory&cache=shared")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
if _, err := db.Exec(`CREATE TABLE syb_inner_code_records (
|
||||
id INTEGER PRIMARY KEY,status TEXT NOT NULL,apply_batch_id TEXT,result_message TEXT,updated_at TEXT
|
||||
); INSERT INTO syb_inner_code_records(id,status,apply_batch_id,updated_at) VALUES
|
||||
(1,'applying','ICB-1','old'),(2,'queued','ICB-1','old'),(3,'queued','ICB-2','old')`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
needsCheck, released, err := InterruptInnerCodeApplyBatch(db, "ICB-1", "2026-08-15T06:00:00Z")
|
||||
if err != nil || needsCheck != 1 || released != 1 {
|
||||
t.Fatalf("needs_check=%d released=%d err=%v", needsCheck, released, err)
|
||||
}
|
||||
rows, err := db.Query(`SELECT id,status,result_message,updated_at FROM syb_inner_code_records ORDER BY id`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
wants := []string{"needs_check", "ready", "queued"}
|
||||
index := 0
|
||||
for rows.Next() {
|
||||
var id int
|
||||
var status, message, updatedAt string
|
||||
var nullableMessage sql.NullString
|
||||
if err := rows.Scan(&id, &status, &nullableMessage, &updatedAt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
message = nullableMessage.String
|
||||
if status != wants[index] || (id < 3 && (message == "" || updatedAt != "2026-08-15T06:00:00Z")) {
|
||||
t.Fatalf("id=%d status=%s message=%q updated=%s", id, status, message, updatedAt)
|
||||
}
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinishInnerCodeApply_软删除后仍保存后台结果(t *testing.T) {
|
||||
db, err := sql.Open("sqlite", "file:inner_code_finish_deleted?mode=memory&cache=shared")
|
||||
if err != nil {
|
||||
@@ -153,7 +245,7 @@ func TestInnerCodeStatusPreservesImportResult(t *testing.T) {
|
||||
t.Errorf("%s 不应保留旧规划", status)
|
||||
}
|
||||
}
|
||||
for _, status := range []string{"applying", "updated", "already_filled", "needs_check"} {
|
||||
for _, status := range []string{"queued", "applying", "updated", "already_filled", "needs_check"} {
|
||||
if !innerCodeStatusPreservesImportResult(model.InnerCodeStatus(status)) {
|
||||
t.Errorf("%s 应保留远端结果和审计", status)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user