Files
cmautobuy/admin/service/pdd_page_test.go
T
chengmaandClaude Opus 5 eb8d357f5a feat: PDD 商品页面 (#18)
打通 Client↔Admin 闭环的一环:录入 PDD 链接 → 创建采集任务 →
客户端领走去采 → 规格和价格显示在页面上。#16 写的 MarkCollecting 和
SoftDeletePddProduct 至此才有生产调用方。

链接解析严格、不做容错兜底:goods_id 上有 UNIQUE 约束,防重全靠它。
猜一个的话同一商品会存成好几行、采好几遍,规格映射还说不清指向哪一行。
短链一律拒绝,卡域名是为了拦"粘了淘宝链接"这种失误。

创建采集任务先占状态再建任务:MarkCollecting 只在 pending/failed 时成功,
同时充当"有没有人已经在采"的判断,与建任务在同一事务里,
所以并发点多次只会建出一个任务(实测 4 并发 → 1 条)。

submit.go 的 collectedData 增加 Dimensions —— 没有它就只能按 Go 的 map
遍历,而 map 无序,同一商品每次刷新"颜色/尺码"的先后都可能变。

顺带修 #16 一处缺陷:EnsurePddProduct 复活分支清空了 skus_json 却漏了
title,导致复活后状态显示"未采集"但标题还留着旧值。

审查打回一次:清理"PDD 链接唯一的录入口"这一过期说法(全库 5 处),
以及 shopee.go 里"空 → no_link"的过期 TODO —— no_link 已在 #16
从 CHECK 约束删除,照写会直接撞约束。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 11:27:06 +08:00

662 lines
23 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"database/sql"
"errors"
"strings"
"testing"
"cmautobuy/admin/model"
"cmautobuy/admin/repository"
)
// ── 链接解析 ───────────────────────────────────────────
func TestParsePddGoodsID_认得的写法(t *testing.T) {
cases := []struct {
name string
url string
want string
}{
{"标准商品页", "https://mobile.yangkeduo.com/goods.html?goods_id=737116531267", "737116531267"},
{"goods2 带一堆参数", "https://mobile.yangkeduo.com/goods2.html?_x_org=2&goods_id=737116531267&refer_page=1", "737116531267"},
{"优惠券落地页用的是 _x_goods_id", "https://yangkeduo.com/duo_coupon_landing.html?_x_goods_id=737116531267", "737116531267"},
{"pinduoduo.com 域名", "https://mobile.pinduoduo.com/goods.html?goods_id=737116531267", "737116531267"},
{"http 也认", "http://mobile.yangkeduo.com/goods.html?goods_id=737116531267", "737116531267"},
{"前后有空格", " https://mobile.yangkeduo.com/goods.html?goods_id=737116531267 ", "737116531267"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := ParsePddGoodsID(tc.url)
if err != nil {
t.Fatalf("应该解析成功: %v", err)
}
if got != tc.want {
t.Errorf("goods_id = %q,期望 %q", got, tc.want)
}
})
}
}
// 解析不出来必须报错。容错兜底会让同一个商品存成好几行,
// 采好几遍,规格映射还说不清指向哪一行。
func TestParsePddGoodsID_认不出的一律报错(t *testing.T) {
cases := []struct{ name, url string }{
{"空串", ""},
{"只有空格", " "},
{"短链没有 goods_id", "https://p.pinduoduo.com/AbCdEfGh"},
{"不是拼多多", "https://item.taobao.com/item.htm?id=737116531267"},
{"不带协议", "mobile.yangkeduo.com/goods.html?goods_id=737116531267"},
{"goods_id 不是数字", "https://mobile.yangkeduo.com/goods.html?goods_id=abc123456"},
{"goods_id 太短像是被截断", "https://mobile.yangkeduo.com/goods.html?goods_id=123"},
{"goods_id 为空", "https://mobile.yangkeduo.com/goods.html?goods_id="},
{"域名只是长得像", "https://yangkeduo.com.evil.example/goods.html?goods_id=737116531267"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := ParsePddGoodsID(tc.url)
if !errors.Is(err, ErrBadPddURL) {
t.Fatalf("期望 ErrBadPddURL,实际 err=%v got=%q", err, got)
}
// 报错要说清下一步该怎么办,光说"无效"操作员不知道改什么
if len(err.Error()) < len("PDD 链接无效")+4 {
t.Errorf("错误信息太笼统: %q", err.Error())
}
})
}
}
// ── 价格显示 ───────────────────────────────────────────
// price_cent 为 null 显示"未采到",**不能显示 ¥0.00**。
// 0 元和采不到价格是两回事,而这个数是要参与价格保护比对的。
func TestFormatPriceCent(t *testing.T) {
cent := func(v int64) *int64 { return &v }
cases := []struct {
in *int64
want string
}{
{nil, "未采到"},
{cent(1256), "¥12.56"},
{cent(0), "¥0.00"},
{cent(5), "¥0.05"},
{cent(100), "¥1.00"},
{cent(123456), "¥1234.56"},
}
for _, tc := range cases {
if got := formatPriceCent(tc.in); got != tc.want {
t.Errorf("formatPriceCent(%v) = %q,期望 %q", tc.in, got, tc.want)
}
}
}
// ── 创建 ───────────────────────────────────────────────
func TestCreatePddProduct_只填链接即可(t *testing.T) {
db := newTestDB(t)
goodsID, err := CreatePddProduct(db, "https://mobile.yangkeduo.com/goods.html?goods_id=737116531267")
if err != nil {
t.Fatalf("创建失败: %v", err)
}
if goodsID != "737116531267" {
t.Errorf("goods_id = %q", goodsID)
}
p, _ := repository.GetPddProductByGoodsID(db, goodsID)
if p == nil {
t.Fatal("应该建出一行")
}
if p.CollectStatus != model.CollectPending {
t.Errorf("新建的状态应为 pending,实际 %s", p.CollectStatus)
}
if p.Title != "" || p.SkusJSON != "" {
t.Error("标题和规格应该留空,等采集回填")
}
}
func TestCreatePddProduct_链接解析不出就不写库(t *testing.T) {
db := newTestDB(t)
if _, err := CreatePddProduct(db, "https://p.pinduoduo.com/AbCdEfGh"); !errors.Is(err, ErrBadPddURL) {
t.Fatalf("期望 ErrBadPddURL,实际 %v", err)
}
var n int
db.QueryRow(`SELECT COUNT(*) FROM pdd_products`).Scan(&n)
if n != 0 {
t.Errorf("解析失败时不该写库,实际有 %d 行", n)
}
}
func TestCreatePddProduct_重复创建不产生第二行(t *testing.T) {
db := newTestDB(t)
url := "https://mobile.yangkeduo.com/goods.html?goods_id=737116531267"
for i := 0; i < 3; i++ {
if _, err := CreatePddProduct(db, url); err != nil {
t.Fatalf("第 %d 次创建失败: %v", i+1, err)
}
}
var n int
db.QueryRow(`SELECT COUNT(*) FROM pdd_products`).Scan(&n)
if n != 1 {
t.Errorf("同一个商品应该只有 1 行,实际 %d 行", n)
}
}
// 软删除后重新创建同一链接要复活原行,并且**旧采集结果被清空**。
// 记录被删过一次,旧数据不该再当有效的用。
func TestCreatePddProduct_删除后重新创建复活且清空采集结果(t *testing.T) {
db := newTestDB(t)
url := "https://mobile.yangkeduo.com/goods.html?goods_id=737116531267"
goodsID, _ := CreatePddProduct(db, url)
if err := repository.SetCollectResult(db, goodsID, "旧标题", sampleSkusJSON); err != nil {
t.Fatalf("写采集结果失败: %v", err)
}
before, _ := repository.GetPddProductByGoodsID(db, goodsID)
if _, err := DeletePddProducts(db, []string{goodsID}); err != nil {
t.Fatalf("删除失败: %v", err)
}
if _, err := CreatePddProduct(db, url); err != nil {
t.Fatalf("重新创建失败: %v", err)
}
after, _ := repository.GetPddProductByGoodsID(db, goodsID)
if after.ID != before.ID {
t.Errorf("应该复活原行(id %d),实际是新行 id %d", before.ID, after.ID)
}
if after.IsDeleted() {
t.Error("复活后 deleted_at 应该清掉")
}
if after.SkusJSON != "" || after.Title != "" {
t.Errorf("复活后采集结果应清空,实际 title=%q skus=%q", after.Title, after.SkusJSON)
}
if after.CollectStatus != model.CollectPending {
t.Errorf("复活后状态应回到 pending,实际 %s", after.CollectStatus)
}
}
// ── 编辑链接 ───────────────────────────────────────────
func TestUpdatePddProductURL_同一个商品可以改写法(t *testing.T) {
db := newTestDB(t)
goodsID, _ := CreatePddProduct(db, "https://mobile.yangkeduo.com/goods.html?goods_id=737116531267")
p, _ := repository.GetPddProductByGoodsID(db, goodsID)
newURL := "https://mobile.yangkeduo.com/goods2.html?goods_id=737116531267&refer_page=1"
if err := UpdatePddProductURL(db, p.ID, newURL); err != nil {
t.Fatalf("保存失败: %v", err)
}
after, _ := repository.GetPddProductByID(db, p.ID)
if after.URL != newURL {
t.Errorf("链接没保存上: %q", after.URL)
}
}
// 换成另一个商品必须拒绝:这一行上挂着采集结果和 sku_mappings,
// goods_id 一换那些数据就全指到错的商品上,之后按它下单就是买错东西。
func TestUpdatePddProductURL_换成别的商品要拒绝(t *testing.T) {
db := newTestDB(t)
goodsID, _ := CreatePddProduct(db, "https://mobile.yangkeduo.com/goods.html?goods_id=737116531267")
p, _ := repository.GetPddProductByGoodsID(db, goodsID)
err := UpdatePddProductURL(db, p.ID,
"https://mobile.yangkeduo.com/goods.html?goods_id=999888777666")
if !errors.Is(err, ErrPddGoodsIDChanged) {
t.Fatalf("期望 ErrPddGoodsIDChanged,实际 %v", err)
}
after, _ := repository.GetPddProductByID(db, p.ID)
if after.GoodsID != goodsID {
t.Error("拒绝之后 goods_id 不该变")
}
if !strings.Contains(err.Error(), "创建") {
t.Errorf("错误信息要告诉操作员改用「创建」,实际 %q", err.Error())
}
}
// ── 列表 ───────────────────────────────────────────────
// sampleSkusJSON 是一份采集结果样本,两个维度三个规格,其中一个采不到价格。
const sampleSkusJSON = `{
"schema_version": 1,
"goods_id": "737116531267",
"title": "西装外套三件套",
"dimensions": [
{"key": "color", "name": "颜色分类"},
{"key": "size", "name": "尺码"}
],
"skus": [
{"options": {"color": "黑色", "size": "M"}, "price_cent": 1256, "available": true, "raw_price": "¥12.56"},
{"options": {"color": "白色", "size": "M"}, "price_cent": 1256, "available": false, "raw_price": "¥12.56"},
{"options": {"color": "红色", "size": "L"}, "price_cent": null, "available": true, "raw_price": ""}
]
}`
func createProduct(t *testing.T, db *sql.DB, goodsID string) {
t.Helper()
if _, err := CreatePddProduct(db,
"https://mobile.yangkeduo.com/goods.html?goods_id="+goodsID); err != nil {
t.Fatalf("创建商品 %s 失败: %v", goodsID, err)
}
}
// 规格数:未采集显示 —,采到几个就显示几个。
// **采到 0 个要如实显示 0**,那说明采集出了问题,显示成 — 就看不出来了。
func TestListPddProducts_规格数(t *testing.T) {
db := newTestDB(t)
createProduct(t, db, "100000000001") // 没采过
createProduct(t, db, "100000000002") // 采到 3 个
createProduct(t, db, "100000000003") // 采到 0 个
repository.SetCollectResult(db, "100000000002", "有规格的", sampleSkusJSON)
repository.SetCollectResult(db, "100000000003", "没规格的", `{"skus": []}`)
result, err := ListPddProducts(db, "", "")
if err != nil {
t.Fatalf("查列表失败: %v", err)
}
want := map[string]string{
"100000000001": placeholder,
"100000000002": "3",
"100000000003": "0",
}
for _, row := range result.Rows {
if got := row.SkuCountText; got != want[row.GoodsID] {
t.Errorf("商品 %s 规格数 = %q,期望 %q", row.GoodsID, got, want[row.GoodsID])
}
}
}
// skus_json 存进了坏数据时,列表页必须还能打开。
// json_array_length 碰到非法 JSON 会让整条查询报错,那样页面直接白屏。
func TestListPddProducts_坏掉的采集结果不影响列表打开(t *testing.T) {
db := newTestDB(t)
createProduct(t, db, "100000000001")
if _, err := db.Exec(
`UPDATE pdd_products SET skus_json = '这不是JSON' WHERE goods_id = ?`,
"100000000001"); err != nil {
t.Fatalf("造坏数据失败: %v", err)
}
result, err := ListPddProducts(db, "", "")
if err != nil {
t.Fatalf("列表页应该照样打得开: %v", err)
}
if len(result.Rows) != 1 || result.Rows[0].SkuCountText != placeholder {
t.Errorf("坏数据的规格数应显示占位符,实际 %+v", result.Rows)
}
}
func TestListPddProducts_按采集状态筛选(t *testing.T) {
db := newTestDB(t)
createProduct(t, db, "100000000001")
createProduct(t, db, "100000000002")
repository.SetCollectResult(db, "100000000002", "已采的", sampleSkusJSON)
collected, err := ListPddProducts(db, "", model.CollectCollected)
if err != nil {
t.Fatalf("筛选失败: %v", err)
}
if len(collected.Rows) != 1 || collected.Rows[0].GoodsID != "100000000002" {
t.Errorf("按「已采集」筛选应只剩 1 条,实际 %d 条", len(collected.Rows))
}
if !collected.IsFiltered {
t.Error("IsFiltered 应为 true,空状态文案要靠它分情况")
}
// 统计不受筛选影响:状态条是全局概览
if collected.Total != 2 {
t.Errorf("Total 应是全部 2 条,不该跟着筛选变,实际 %d", collected.Total)
}
}
func TestListPddProducts_按商品ID和链接搜索(t *testing.T) {
db := newTestDB(t)
createProduct(t, db, "737116531267")
createProduct(t, db, "999888777666")
byID, _ := ListPddProducts(db, "7371165", "")
if len(byID.Rows) != 1 || byID.Rows[0].GoodsID != "737116531267" {
t.Errorf("按 ID 片段搜索失败,命中 %d 条", len(byID.Rows))
}
byURL, _ := ListPddProducts(db, "yangkeduo", "")
if len(byURL.Rows) != 2 {
t.Errorf("按链接搜索应命中 2 条,实际 %d 条", len(byURL.Rows))
}
}
// LIKE 的通配符要转义,否则搜 "7_7" 会把 "737…" 也捞出来,看着像搜索坏了。
//
// 注意别拿单个 "_" 当测试词:链接里的 goods_id= 本来就带下划线,
// 那种情况命中是对的,测不出问题。
func TestListPddProducts_搜索词里的通配符不当通配符用(t *testing.T) {
db := newTestDB(t)
createProduct(t, db, "737116531267")
for _, kw := range []string{"7_7", "%"} {
result, err := ListPddProducts(db, kw, "")
if err != nil {
t.Fatalf("搜 %q 出错: %v", kw, err)
}
if len(result.Rows) != 0 {
t.Errorf("搜 %q 应该一条都搜不到,实际 %d 条", kw, len(result.Rows))
}
}
}
func TestListPddProducts_删除的查不到(t *testing.T) {
db := newTestDB(t)
createProduct(t, db, "737116531267")
DeletePddProducts(db, []string{"737116531267"})
result, _ := ListPddProducts(db, "", "")
if len(result.Rows) != 0 {
t.Errorf("软删除的不该出现在列表里,实际 %d 条", len(result.Rows))
}
if result.Total != 0 {
t.Errorf("统计也不该算上删掉的,实际 %d", result.Total)
}
}
func TestStatusLine_四个状态都列出来(t *testing.T) {
db := newTestDB(t)
createProduct(t, db, "100000000001")
createProduct(t, db, "100000000002")
repository.SetCollectResult(db, "100000000002", "已采的", sampleSkusJSON)
result, _ := ListPddProducts(db, "", "")
line := result.StatusLine()
for _, want := range []string{"共 2 条", "已采集 1", "未采集 1", "采集中 0", "采集失败 0"} {
if !strings.Contains(line, want) {
t.Errorf("状态条缺少 %q:%s", want, line)
}
}
}
// ── 弹窗详情 ───────────────────────────────────────────
func TestGetPddProductDetail_规格表按dimensions排列(t *testing.T) {
db := newTestDB(t)
createProduct(t, db, "737116531267")
repository.SetCollectResult(db, "737116531267", "西装外套三件套", sampleSkusJSON)
p, _ := repository.GetPddProductByGoodsID(db, "737116531267")
d, err := GetPddProductDetail(db, p.ID)
if err != nil {
t.Fatalf("读详情失败: %v", err)
}
if !d.Collected {
t.Fatal("已采集的应标为 Collected")
}
// 表头按 dimensions 里的顺序,用的是 name 不是 key
if len(d.DimensionNames) != 2 ||
d.DimensionNames[0] != "颜色分类" || d.DimensionNames[1] != "尺码" {
t.Errorf("维度顺序不对: %v", d.DimensionNames)
}
if len(d.SKUs) != 3 {
t.Fatalf("应有 3 个规格,实际 %d", len(d.SKUs))
}
first := d.SKUs[0]
if len(first.Options) != 2 || first.Options[0] != "黑色" || first.Options[1] != "M" {
t.Errorf("第一行规格值顺序不对: %v", first.Options)
}
if first.PriceText != "¥12.56" || first.Available != "是" {
t.Errorf("第一行价格/有货不对: %q %q", first.PriceText, first.Available)
}
if d.SKUs[1].Available != "否" {
t.Errorf("缺货的应显示「否」,实际 %q", d.SKUs[1].Available)
}
// price_cent 为 null 的那一行
if d.SKUs[2].PriceText != "未采到" {
t.Errorf("采不到价格应显示「未采到」而不是 ¥0.00,实际 %q", d.SKUs[2].PriceText)
}
}
// dimensions 缺失时退回按 key 排序。不排的话 Go 的 map 是随机顺序,
// 同一个商品每次刷新页面列的顺序都不一样。
func TestGetPddProductDetail_没有dimensions时按key排序且稳定(t *testing.T) {
db := newTestDB(t)
createProduct(t, db, "737116531267")
repository.SetCollectResult(db, "737116531267", "无维度信息", `{
"skus": [{"options": {"size": "M", "color": "黑色", "style": "A"},
"price_cent": 100, "available": true}]
}`)
p, _ := repository.GetPddProductByGoodsID(db, "737116531267")
for i := 0; i < 5; i++ {
d, err := GetPddProductDetail(db, p.ID)
if err != nil {
t.Fatalf("读详情失败: %v", err)
}
want := []string{"color", "size", "style"}
for j, name := range want {
if d.DimensionNames[j] != name {
t.Fatalf("第 %d 次读,维度顺序应稳定为 %v,实际 %v", i+1, want, d.DimensionNames)
}
}
if d.SKUs[0].Options[0] != "黑色" {
t.Fatalf("规格值应跟着表头一起排,实际 %v", d.SKUs[0].Options)
}
}
}
func TestGetPddProductDetail_未采集时不算已采集(t *testing.T) {
db := newTestDB(t)
createProduct(t, db, "737116531267")
p, _ := repository.GetPddProductByGoodsID(db, "737116531267")
d, err := GetPddProductDetail(db, p.ID)
if err != nil {
t.Fatalf("读详情失败: %v", err)
}
if d.Collected {
t.Error("没采过的不该标为已采集——界面要显示「尚未采集」而不是空表")
}
if d.StatusText != "未采集" {
t.Errorf("状态文字 = %q", d.StatusText)
}
if d.SkusError != "" {
t.Errorf("没采过不是解析出错,SkusError 应为空,实际 %q", d.SkusError)
}
}
// 采集结果坏掉要如实说出来,不能装作"没有规格"——
// 前者是数据坏了要重采,后者是商品本身没规格,处理方式不一样。
func TestGetPddProductDetail_采集结果坏掉时说清楚(t *testing.T) {
db := newTestDB(t)
createProduct(t, db, "737116531267")
db.Exec(`UPDATE pdd_products SET skus_json = '坏数据' WHERE goods_id = ?`, "737116531267")
p, _ := repository.GetPddProductByGoodsID(db, "737116531267")
d, err := GetPddProductDetail(db, p.ID)
if err != nil {
t.Fatalf("不该整个失败,要能打开弹窗: %v", err)
}
if d.SkusError == "" {
t.Error("应该给出解析失败的说明")
}
if d.Collected {
t.Error("解析不了就不能当成已采集")
}
}
func TestGetPddProductDetail_删除的读不到(t *testing.T) {
db := newTestDB(t)
createProduct(t, db, "737116531267")
p, _ := repository.GetPddProductByGoodsID(db, "737116531267")
DeletePddProducts(db, []string{"737116531267"})
d, err := GetPddProductDetail(db, p.ID)
if err != nil {
t.Fatalf("不该报错: %v", err)
}
if d != nil {
t.Error("已删除的应返回 nil,让界面提示「已被删除,请刷新」")
}
}
// ── 创建采集任务 ───────────────────────────────────────
// collectTaskOf 读出某个商品对应的采集任务。
func collectTaskOf(t *testing.T, db *sql.DB, goodsID string) (status, assigned, url string, ok bool) {
t.Helper()
var a sql.NullString
err := db.QueryRow(`
SELECT status, assigned_client, pdd_goods_url
FROM tasks WHERE task_type = 'collect' AND pdd_goods_id = ?`, goodsID,
).Scan(&status, &a, &url)
if err == sql.ErrNoRows {
return "", "", "", false
}
if err != nil {
t.Fatalf("查采集任务失败: %v", err)
}
return status, a.String, url, true
}
// `[必须]` 采集任务不指定客户端。采集是纯读取,哪台机器跑都一样,
// 指定了反而会在那台机器关着的时候干等。
func TestCreatePddCollectTasks_不指定客户端(t *testing.T) {
db := newTestDB(t)
createProduct(t, db, "737116531267")
created, skipped, err := CreatePddCollectTasks(db, []string{"737116531267"})
if err != nil {
t.Fatalf("建任务失败: %v", err)
}
if created != 1 || skipped != 0 {
t.Fatalf("created=%d skipped=%d,期望 1/0", created, skipped)
}
status, assigned, url, ok := collectTaskOf(t, db, "737116531267")
if !ok {
t.Fatal("应该建出一条采集任务")
}
if assigned != "" {
t.Errorf("assigned_client 必须为空,实际 %q", assigned)
}
if status != "pending" {
t.Errorf("状态应为 pending(无主待领),实际 %q", status)
}
// Client 契约里 pdd_goods_url 必填,没有它客户端拿到任务也不知道去哪采
if url == "" {
t.Error("pdd_goods_url 必须有值")
}
// 建完任务商品状态要变成采集中
p, _ := repository.GetPddProductByGoodsID(db, "737116531267")
if p.CollectStatus != model.CollectCollecting {
t.Errorf("商品状态应为 collecting,实际 %s", p.CollectStatus)
}
}
// 建出来的任务必须真的能被领走,否则页面上永远卡在"采集中"。
func TestCreatePddCollectTasks_建出的任务能被任意客户端领走(t *testing.T) {
db := newTestDB(t)
createProduct(t, db, "737116531267")
CreatePddCollectTasks(db, []string{"737116531267"})
task, err := ClaimNextTask(db, "client-随便哪台", []string{"collect"})
if err != nil {
t.Fatalf("领取失败: %v", err)
}
if task == nil {
t.Fatal("采集任务应该能被任意客户端领到")
}
if task.PddGoodsID != "737116531267" {
t.Errorf("领到的任务商品不对: %q", task.PddGoodsID)
}
}
func TestCreatePddCollectTasks_按商品去重(t *testing.T) {
db := newTestDB(t)
createProduct(t, db, "737116531267")
created, _, err := CreatePddCollectTasks(db,
[]string{"737116531267", "737116531267", " 737116531267 ", ""})
if err != nil {
t.Fatalf("建任务失败: %v", err)
}
if created != 1 {
t.Errorf("重复勾选只该建 1 个任务,实际 %d", created)
}
var n int
db.QueryRow(`SELECT COUNT(*) FROM tasks WHERE task_type = 'collect'`).Scan(&n)
if n != 1 {
t.Errorf("库里应只有 1 条采集任务,实际 %d", n)
}
}
// 已经在采的跳过:再建一个就是让两台机器采同一个商品,白费一趟。
// 而且跳过了几个必须报出来,静默跳过会让操作员等半天不知道为什么没动静。
func TestCreatePddCollectTasks_采集中的跳过并报数(t *testing.T) {
db := newTestDB(t)
createProduct(t, db, "100000000001")
createProduct(t, db, "100000000002")
// 第一个先建一次,让它进入 collecting
if _, _, err := CreatePddCollectTasks(db, []string{"100000000001"}); err != nil {
t.Fatalf("第一次建任务失败: %v", err)
}
created, skipped, err := CreatePddCollectTasks(db,
[]string{"100000000001", "100000000002"})
if err != nil {
t.Fatalf("第二次建任务失败: %v", err)
}
if created != 1 {
t.Errorf("只该给没在采的那个建任务,实际建了 %d 个", created)
}
if skipped != 1 {
t.Errorf("采集中的那个应计入 skipped,实际 %d", skipped)
}
}
// 采集失败的可以重新采集,不能一直卡在失败状态。
func TestCreatePddCollectTasks_失败的可以重新采集(t *testing.T) {
db := newTestDB(t)
createProduct(t, db, "737116531267")
repository.SetCollectFailed(db, "737116531267", "页面打不开", "")
created, skipped, err := CreatePddCollectTasks(db, []string{"737116531267"})
if err != nil {
t.Fatalf("建任务失败: %v", err)
}
if created != 1 || skipped != 0 {
t.Errorf("失败的应该能重采,created=%d skipped=%d", created, skipped)
}
}
func TestCreatePddCollectTasks_已删除的跳过(t *testing.T) {
db := newTestDB(t)
createProduct(t, db, "737116531267")
DeletePddProducts(db, []string{"737116531267"})
created, skipped, err := CreatePddCollectTasks(db, []string{"737116531267"})
if err != nil {
t.Fatalf("不该报错: %v", err)
}
if created != 0 || skipped != 1 {
t.Errorf("已删除的应被跳过,created=%d skipped=%d", created, skipped)
}
}
func TestCreatePddCollectTasks_什么都没勾时不建任务(t *testing.T) {
db := newTestDB(t)
created, skipped, err := CreatePddCollectTasks(db, nil)
if err != nil || created != 0 || skipped != 0 {
t.Errorf("空输入应安静返回,得到 %d/%d/%v", created, skipped, err)
}
}