199 lines
6.2 KiB
Go
199 lines
6.2 KiB
Go
package service
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"database/sql"
|
||
|
|
"sync"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"cmautobuy/admin/model"
|
||
|
|
)
|
||
|
|
|
||
|
|
// insertUnassignedTask 插一条**无主**任务:没有指定客户端,状态是 pending。
|
||
|
|
// 采集任务就长这样——创建时不指定客户端,谁领到算谁的。
|
||
|
|
func insertUnassignedTask(t *testing.T, db *sql.DB, taskID, taskType string) {
|
||
|
|
t.Helper()
|
||
|
|
now := model.NowISO()
|
||
|
|
_, err := db.Exec(`
|
||
|
|
INSERT INTO tasks (task_id, task_type, status, assigned_client,
|
||
|
|
pdd_goods_url, pdd_goods_id, created_at, updated_at)
|
||
|
|
VALUES (?, ?, 'pending', NULL, 'https://x/1', '1', ?, ?)`,
|
||
|
|
taskID, taskType, now, now)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("插入无主任务失败: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func taskAssignee(t *testing.T, db *sql.DB, taskID string) (client, status string) {
|
||
|
|
t.Helper()
|
||
|
|
var c sql.NullString
|
||
|
|
if err := db.QueryRow(
|
||
|
|
`SELECT assigned_client, status FROM tasks WHERE task_id = ?`, taskID,
|
||
|
|
).Scan(&c, &status); err != nil {
|
||
|
|
t.Fatalf("查询任务失败: %v", err)
|
||
|
|
}
|
||
|
|
return c.String, status
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── 无主任务能被领到 ───────────────────────────────────
|
||
|
|
|
||
|
|
func TestClaim_无主任务能被领到并标记领取者(t *testing.T) {
|
||
|
|
db := newTestDB(t)
|
||
|
|
insertUnassignedTask(t, db, "T-FREE", "collect")
|
||
|
|
|
||
|
|
task, err := ClaimNextTask(db, "client-001", []string{"collect"})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("领取失败: %v", err)
|
||
|
|
}
|
||
|
|
if task == nil {
|
||
|
|
t.Fatal("无主任务应该能被领到——不然采集任务建出来就是死的")
|
||
|
|
}
|
||
|
|
|
||
|
|
client, status := taskAssignee(t, db, "T-FREE")
|
||
|
|
if client != "client-001" {
|
||
|
|
t.Errorf("领取后应把领取者写进 assigned_client,实际 %q", client)
|
||
|
|
}
|
||
|
|
if status != "claimed" {
|
||
|
|
t.Errorf("状态应为 claimed,实际 %s", status)
|
||
|
|
}
|
||
|
|
if task.ClaimedAt == "" {
|
||
|
|
t.Error("claimed_at 应该有值")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestClaim_无主任务只能被领一次(t *testing.T) {
|
||
|
|
db := newTestDB(t)
|
||
|
|
insertUnassignedTask(t, db, "T-FREE", "collect")
|
||
|
|
|
||
|
|
if first, _ := ClaimNextTask(db, "client-001", []string{"collect"}); first == nil {
|
||
|
|
t.Fatal("第一次应该领到")
|
||
|
|
}
|
||
|
|
second, err := ClaimNextTask(db, "client-002", []string{"collect"})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("第二次领取报错: %v", err)
|
||
|
|
}
|
||
|
|
if second != nil {
|
||
|
|
t.Errorf("已被领走的任务不该再被领到:%s", second.TaskID)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── 优先级:指定给我的优先于无主的 ─────────────────────
|
||
|
|
|
||
|
|
func TestClaim_指定给本机的优先于无主的(t *testing.T) {
|
||
|
|
db := newTestDB(t)
|
||
|
|
// 先插无主的,让它 created_at 更早——如果没有优先级规则,
|
||
|
|
// 按 created_at 排序会先拿到它,测试就能发现问题
|
||
|
|
insertUnassignedTask(t, db, "T-FREE", "purchase")
|
||
|
|
insertTask(t, db, "T-MINE", "client-001")
|
||
|
|
|
||
|
|
task, err := ClaimNextTask(db, "client-001", []string{"purchase"})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("领取失败: %v", err)
|
||
|
|
}
|
||
|
|
if task == nil {
|
||
|
|
t.Fatal("应该领到任务")
|
||
|
|
}
|
||
|
|
if task.TaskID != "T-MINE" {
|
||
|
|
t.Errorf("应优先领取指定给本机的 T-MINE,实际领到 %s —— "+
|
||
|
|
"显式分配是人为决定,应当先兑现", task.TaskID)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestClaim_指定给别人的仍然领不到(t *testing.T) {
|
||
|
|
db := newTestDB(t)
|
||
|
|
insertTask(t, db, "T-OTHER", "client-999")
|
||
|
|
|
||
|
|
task, err := ClaimNextTask(db, "client-001", []string{"purchase"})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("领取失败: %v", err)
|
||
|
|
}
|
||
|
|
if task != nil {
|
||
|
|
t.Errorf("不该领到指定给别的客户端的任务:%s", task.TaskID)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── 类型过滤仍然生效 ───────────────────────────────────
|
||
|
|
|
||
|
|
func TestClaim_无主任务也受supported_types约束(t *testing.T) {
|
||
|
|
db := newTestDB(t)
|
||
|
|
insertUnassignedTask(t, db, "T-FREE", "purchase")
|
||
|
|
|
||
|
|
task, err := ClaimNextTask(db, "client-001", []string{"collect"})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("领取失败: %v", err)
|
||
|
|
}
|
||
|
|
if task != nil {
|
||
|
|
t.Errorf("只声明 collect 的客户端不该拿到 purchase 任务:%s", task.TaskID)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── 并发抢占 ───────────────────────────────────────────
|
||
|
|
|
||
|
|
// 多个客户端同时抢同一条无主任务,只能有一个拿到。
|
||
|
|
// 这是"谁领到算谁的"这个模式的安全底线——两台都拿到就会重复采集/重复下单。
|
||
|
|
func TestClaim_并发抢无主任务只有一个拿到(t *testing.T) {
|
||
|
|
db := newTestDB(t)
|
||
|
|
insertUnassignedTask(t, db, "T-ONLY-ONE", "collect")
|
||
|
|
|
||
|
|
const workers = 8
|
||
|
|
var (
|
||
|
|
wg sync.WaitGroup
|
||
|
|
mu sync.Mutex
|
||
|
|
winners []string
|
||
|
|
lastErr error
|
||
|
|
)
|
||
|
|
for i := 0; i < workers; i++ {
|
||
|
|
clientID := "client-" + string(rune('A'+i))
|
||
|
|
wg.Add(1)
|
||
|
|
go func() {
|
||
|
|
defer wg.Done()
|
||
|
|
task, err := ClaimNextTask(db, clientID, []string{"collect"})
|
||
|
|
mu.Lock()
|
||
|
|
defer mu.Unlock()
|
||
|
|
if err != nil {
|
||
|
|
lastErr = err
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if task != nil {
|
||
|
|
winners = append(winners, clientID)
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
}
|
||
|
|
wg.Wait()
|
||
|
|
|
||
|
|
if lastErr != nil {
|
||
|
|
t.Fatalf("并发领取出错: %v", lastErr)
|
||
|
|
}
|
||
|
|
if len(winners) != 1 {
|
||
|
|
t.Fatalf("同一条无主任务被 %d 个客户端领到,期望正好 1 个:%v",
|
||
|
|
len(winners), winners)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 而且库里记的领取者必须就是那个赢家
|
||
|
|
client, _ := taskAssignee(t, db, "T-ONLY-ONE")
|
||
|
|
if client != winners[0] {
|
||
|
|
t.Errorf("assigned_client 记的是 %q,但实际领到的是 %q", client, winners[0])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── 领取历史仍然被记录 ─────────────────────────────────
|
||
|
|
|
||
|
|
// 提交结果时的权限判断依赖 task_claims(只有从没领过的才 403),
|
||
|
|
// 无主任务这条路径也必须记。
|
||
|
|
func TestClaim_无主任务领取后也记领取历史(t *testing.T) {
|
||
|
|
db := newTestDB(t)
|
||
|
|
insertUnassignedTask(t, db, "T-FREE", "collect")
|
||
|
|
RegisterClient(db, model.Client{ClientID: "client-001"}, true)
|
||
|
|
|
||
|
|
if _, err := ClaimNextTask(db, "client-001", []string{"collect"}); err != nil {
|
||
|
|
t.Fatalf("领取失败: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
var n int
|
||
|
|
db.QueryRow(`SELECT COUNT(*) FROM task_claims
|
||
|
|
WHERE task_id = 'T-FREE' AND client_id = 'client-001'`).Scan(&n)
|
||
|
|
if n != 1 {
|
||
|
|
t.Errorf("领取历史应有 1 条,实际 %d —— "+
|
||
|
|
"没有它的话提交结果会被误判成 403", n)
|
||
|
|
}
|
||
|
|
}
|