feat: 任务改用采集采购独立序号 (#172)

This commit is contained in:
chengma
2026-08-12 09:21:21 +08:00
parent eca3a77084
commit 9ad067503e
15 changed files with 523 additions and 44 deletions
@@ -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" {