package repository import ( "database/sql" "fmt" "strings" "cmautobuy/admin/model" ) // claimCandidateLimit 是一次最多尝试抢多少条。 // 抢不到说明被别的客户端拿走了,再试下一条;都抢不到就当作没任务。 const claimCandidateLimit = 10 // ClaimNextTask 为指定客户端领取一个任务。 // // 没有可领的任务时返回 (nil, nil) —— 调用方据此返回 204。 // // 防并发的做法是**条件更新 + 检查影响行数**:先查出候选, // 再用 `WHERE task_id = ? AND status = 'assigned'` 去更新, // 影响行数为 0 就说明被别人抢先了,换下一条。 // 不用 SELECT ... FOR UPDATE,SQLite 没有那个。 func ClaimNextTask(db *sql.DB, clientID string, supportedTypes []string) (*model.Task, error) { if clientID == "" { return nil, fmt.Errorf("client_id 不能为空") } query := `SELECT task_id FROM tasks WHERE assigned_client = ? AND status = 'assigned'` args := []any{clientID} // 客户端只声明支持某些类型时,不要给它别的类型 if len(supportedTypes) > 0 { placeholders := strings.TrimSuffix(strings.Repeat("?,", len(supportedTypes)), ",") query += ` AND task_type IN (` + placeholders + `)` for _, t := range supportedTypes { args = append(args, t) } } query += ` ORDER BY priority DESC, created_at LIMIT ?` args = append(args, claimCandidateLimit) rows, err := db.Query(query, args...) if err != nil { return nil, fmt.Errorf("查询可领任务失败: %w", err) } var candidates []string for rows.Next() { var id string if err := rows.Scan(&id); err != nil { rows.Close() return nil, fmt.Errorf("读取候选任务失败: %w", err) } candidates = append(candidates, id) } rows.Close() if err := rows.Err(); err != nil { return nil, err } now := model.NowISO() for _, taskID := range candidates { res, err := db.Exec(` UPDATE tasks SET status = 'claimed', claimed_at = ?, updated_at = ? WHERE task_id = ? AND status = 'assigned'`, now, now, taskID) if err != nil { return nil, fmt.Errorf("领取任务 %s 失败: %w", taskID, err) } n, err := res.RowsAffected() if err != nil { return nil, err } if n == 0 { continue // 被别的客户端抢先了,换下一条 } return GetTask(db, taskID) } return nil, nil // 没有可领的任务 } // GetTask 按编号读一条任务。 func GetTask(db *sql.DB, taskID string) (*model.Task, error) { var t model.Task var assigned, claimedAt, sybID, orderNo, goodsID, skuID sql.NullString var pddGoodsID, pddOptions, resultData, errCode, errMsg, finishedAt sql.NullString var quantity, maxPrice sql.NullInt64 err := db.QueryRow(` SELECT task_id, task_type, status, version, priority, assigned_client, claimed_at, syb_id, order_no, goods_id, shopee_sku_id, pdd_goods_url, pdd_goods_id, pdd_options, quantity, max_price_cent, result_data, error_code, error_message, finished_at, created_at, updated_at FROM tasks WHERE task_id = ?`, taskID).Scan( &t.TaskID, &t.TaskType, &t.Status, &t.Version, &t.Priority, &assigned, &claimedAt, &sybID, &orderNo, &goodsID, &skuID, &t.PddGoodsURL, &pddGoodsID, &pddOptions, &quantity, &maxPrice, &resultData, &errCode, &errMsg, &finishedAt, &t.CreatedAt, &t.UpdatedAt) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, fmt.Errorf("读取任务 %s 失败: %w", taskID, err) } t.AssignedClient = assigned.String t.ClaimedAt = claimedAt.String t.SybID = sybID.String t.OrderNo = orderNo.String t.GoodsID = goodsID.String t.ShopeeSKUID = skuID.String t.PddGoodsID = pddGoodsID.String t.PddOptions = pddOptions.String t.Quantity = int(quantity.Int64) t.MaxPriceCent = maxPrice.Int64 t.ResultData = resultData.String t.ErrorCode = errCode.String t.ErrorMessage = errMsg.String t.FinishedAt = finishedAt.String return &t, nil }