feat: 任务改用采集采购独立序号 (#172)
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"cmautobuy/admin/config"
|
||||
@@ -650,6 +651,154 @@ func TestMySQLMigrate_V12形状错误不记录版本(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMySQLMigrate_V12升级V13迁移任务主键和关联(t *testing.T) {
|
||||
db := openMySQLMigrationTestDB(t)
|
||||
defer db.Close()
|
||||
cleanMySQLTestSchema(t, db)
|
||||
defer cleanMySQLTestSchema(t, db)
|
||||
if err := MigrateMySQL(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 模拟生产 v12:移除 v13 表、版本和新增的级联更新外键。
|
||||
mustExec(t, db, `DELETE FROM schema_migrations WHERE version=13`)
|
||||
mustExec(t, db, `DROP TABLE task_sequences`)
|
||||
mustExec(t, db, `ALTER TABLE task_claims DROP FOREIGN KEY fk_task_claims_task`)
|
||||
mustExec(t, db, `ALTER TABLE task_syb_sources DROP FOREIGN KEY fk_task_syb_sources_task`)
|
||||
mustExec(t, db, `ALTER TABLE task_syb_sources ADD CONSTRAINT fk_task_syb_sources_task
|
||||
FOREIGN KEY(task_id) REFERENCES tasks(task_id) ON DELETE CASCADE`)
|
||||
|
||||
mustExec(t, db, `INSERT INTO syb_orders(syb_id,order_no,title,product_spec,quantity,syb_data,created_at,updated_at)
|
||||
VALUES('SYB-V13','ORDER-SOURCE','商品','黑色,M',1,'{}','2026-08-12T00:00:00Z','2026-08-12T00:00:00Z')`)
|
||||
mustExec(t, db, `INSERT INTO tasks(task_id,task_type,status,pdd_goods_url,pdd_goods_id,order_no,result_data,created_at,updated_at) VALUES
|
||||
('OLD-C-LATE','collect','succeeded','https://example.invalid/c2','C-LATE',NULL,NULL,'2026-08-12T02:00:00Z','2026-08-12T02:00:00Z'),
|
||||
('OLD-C-FIRST','collect','assigned','https://example.invalid/c1','C-FIRST',NULL,NULL,'2026-08-12T01:00:00Z','2026-08-12T01:00:00Z'),
|
||||
('OLD-P-LATE','purchase','succeeded','https://example.invalid/p2','P-LATE','ORDER-P-LATE','{\"purchase\":{\"order_no\":\"PDD-2\",\"ordered_at\":\"2026-08-12T02:00:00Z\"}}','2026-08-12T04:00:00Z','2026-08-12T04:00:00Z'),
|
||||
('OLD-P-FIRST','purchase','succeeded','https://example.invalid/p1','P-FIRST','ORDER-P-FIRST','{\"purchase\":{\"order_no\":\"PDD-1\",\"ordered_at\":\"2026-08-12T01:00:00Z\"}}','2026-08-12T03:00:00Z','2026-08-12T03:00:00Z')`)
|
||||
mustExec(t, db, `INSERT INTO task_claims(task_id,client_id,claimed_at) VALUES
|
||||
('OLD-C-FIRST','CLIENT-1','2026-08-12T01:10:00Z'),
|
||||
('OLD-P-FIRST','CLIENT-1','2026-08-12T03:10:00Z'),
|
||||
('DELETED-TASK','CLIENT-OLD','2026-08-01T00:00:00Z')`)
|
||||
mustExec(t, db, `INSERT INTO task_syb_sources(task_id,syb_id,created_at)
|
||||
VALUES('OLD-C-FIRST','SYB-V13','2026-08-12T01:00:00Z')`)
|
||||
|
||||
if err := MigrateMySQL(db); err != nil {
|
||||
t.Fatalf("v12 升级 v13 失败: %v", err)
|
||||
}
|
||||
wants := map[string]string{"C-FIRST": "cj1", "C-LATE": "cj2", "P-FIRST": "cg1", "P-LATE": "cg2"}
|
||||
for goodsID, wantID := range wants {
|
||||
var taskID string
|
||||
if err := db.QueryRow(`SELECT task_id FROM tasks WHERE pdd_goods_id=?`, goodsID).Scan(&taskID); err != nil || taskID != wantID {
|
||||
t.Fatalf("商品 %s 任务编号=%q err=%v,期望 %s", goodsID, taskID, err, wantID)
|
||||
}
|
||||
}
|
||||
var claimCount, orphanCount, sourceCount int
|
||||
if err := db.QueryRow(`SELECT COUNT(*) FROM task_claims WHERE task_id IN ('cj1','cg1')`).Scan(&claimCount); err != nil || claimCount != 2 {
|
||||
t.Fatalf("有效领取历史未迁移: count=%d err=%v", claimCount, err)
|
||||
}
|
||||
if err := db.QueryRow(`SELECT COUNT(*) FROM task_claims c LEFT JOIN tasks t ON t.task_id=c.task_id WHERE t.task_id IS NULL`).Scan(&orphanCount); err != nil || orphanCount != 0 {
|
||||
t.Fatalf("孤儿领取历史未清理: count=%d err=%v", orphanCount, err)
|
||||
}
|
||||
if err := db.QueryRow(`SELECT COUNT(*) FROM task_syb_sources WHERE task_id='cj1' AND syb_id='SYB-V13'`).Scan(&sourceCount); err != nil || sourceCount != 1 {
|
||||
t.Fatalf("顺运宝来源未迁移: count=%d err=%v", sourceCount, err)
|
||||
}
|
||||
var resultData string
|
||||
if err := db.QueryRow(`SELECT result_data FROM tasks WHERE task_id='cg1'`).Scan(&resultData); err != nil || !strings.Contains(resultData, `"order_no":"PDD-1"`) {
|
||||
t.Fatalf("采购结果不得变化: result=%q err=%v", resultData, err)
|
||||
}
|
||||
for taskType, want := range map[model.TaskType]int64{model.TaskCollect: 2, model.TaskPurchase: 2} {
|
||||
var last int64
|
||||
if err := db.QueryRow(`SELECT current_value FROM task_sequences WHERE task_type=?`, taskType).Scan(&last); err != nil || last != want {
|
||||
t.Fatalf("%s 序列=%d err=%v,期望 %d", taskType, last, err, want)
|
||||
}
|
||||
}
|
||||
if err := MigrateMySQL(db); err != nil {
|
||||
t.Fatalf("v13 重放失败: %v", err)
|
||||
}
|
||||
var stableID string
|
||||
if err := db.QueryRow(`SELECT task_id FROM tasks WHERE pdd_goods_id='C-FIRST'`).Scan(&stableID); err != nil || stableID != "cj1" {
|
||||
t.Fatalf("重放不得二次改号: id=%s err=%v", stableID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextTaskID_MySQL并发独立递增且回滚不耗号(t *testing.T) {
|
||||
db := openMySQLMigrationTestDB(t)
|
||||
defer db.Close()
|
||||
cleanMySQLTestSchema(t, db)
|
||||
defer cleanMySQLTestSchema(t, db)
|
||||
if err := MigrateMySQL(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
const workers = 16
|
||||
ids := make(map[string]bool, workers)
|
||||
var mu sync.Mutex
|
||||
errCh := make(chan error, workers)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < workers; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
id, err := NextTaskID(tx, model.TaskCollect)
|
||||
if err == nil {
|
||||
err = tx.Commit()
|
||||
} else {
|
||||
tx.Rollback()
|
||||
}
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
ids[id] = true
|
||||
mu.Unlock()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
close(errCh)
|
||||
for err := range errCh {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(ids) != workers {
|
||||
t.Fatalf("并发分配得到 %d 个唯一编号,期望 %d", len(ids), workers)
|
||||
}
|
||||
for i := 1; i <= workers; i++ {
|
||||
if !ids[fmt.Sprintf("cj%d", i)] {
|
||||
t.Fatalf("并发编号缺少 cj%d: %+v", i, ids)
|
||||
}
|
||||
}
|
||||
|
||||
rollbackTx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rolledBackID, err := NextTaskID(rollbackTx, model.TaskPurchase)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := rollbackTx.Rollback(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
commitTx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
committedID, err := NextTaskID(commitTx, model.TaskPurchase)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := commitTx.Commit(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rolledBackID != "cg1" || committedID != "cg1" {
|
||||
t.Fatalf("回滚不应消耗采购序号: rollback=%s commit=%s", rolledBackID, committedID)
|
||||
}
|
||||
}
|
||||
|
||||
func openMySQLMigrationTestDB(t *testing.T) *sql.DB {
|
||||
t.Helper()
|
||||
if os.Getenv("CMAUTOBUY_MYSQL_TEST") != "1" {
|
||||
|
||||
Reference in New Issue
Block a user