package repository import ( "database/sql" "fmt" "strings" "cmautobuy/admin/model" ) // UpsertShopeeProduct 写入或更新一条蝦皮商品汇总行。 // // `[必须]` DO UPDATE SET 里绝不允许出现 pdd_goods_url / pdd_goods_id。 // 蝦皮报表里没有这两列,写进去就是写空值——操作员可能攒了几周的 // PDD 链接,导入一次就被静默洗掉,而且不报错,等到建采购任务时 // 才会发现,那时已经找不回来了。见工单 #38、admin/AGENTS.md // 「Excel 导入只做 upsert,绝不允许先清空再导入」。 func UpsertShopeeProduct(q Execer, goodsID, title, shopeeStatus, mainSKUCode string) error { if goodsID == "" { return fmt.Errorf("goods_id 不能为空") } now := model.NowISO() _, err := q.Exec(` INSERT INTO shopee_products (goods_id, title, shopee_status, main_sku_code, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(goods_id) DO UPDATE SET title = excluded.title, shopee_status = excluded.shopee_status, main_sku_code = excluded.main_sku_code, updated_at = excluded.updated_at`, // 注意:pdd_goods_url / pdd_goods_id 两列不在 INSERT 的列清单里, // 也不在 DO UPDATE SET 里——新建时它们是 NULL(未填链接), // 已存在时它们完全不受这条语句影响。 goodsID, title, shopeeStatus, mainSKUCode, now, now) if err != nil { return fmt.Errorf("写入蝦皮商品 %s 失败: %w", goodsID, err) } return nil } // UpsertShopeeSKU 写入或更新一条蝦皮 SKU 行。 // // `[必须]` is_manual 不在 DO UPDATE SET 里,已存在的行导入时保持原值不变。 // 人工新增的 SKU(is_manual=1)就是靠这个不被导入覆盖成 0—— // 本工单不做手动新增功能,但这个口子现在就要留好。 func UpsertShopeeSKU(q Execer, skuID, goodsID, specRaw, color, size, advice string, parseOK bool, skuCode string) error { if skuID == "" { return fmt.Errorf("sku_id 不能为空") } if goodsID == "" { return fmt.Errorf("goods_id 不能为空") } parseOKInt := 0 if parseOK { parseOKInt = 1 } now := model.NowISO() _, err := q.Exec(` INSERT INTO shopee_skus (sku_id, goods_id, spec_raw, color, size, advice, parse_ok, sku_code, is_manual, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?) ON CONFLICT(sku_id) DO UPDATE SET goods_id = excluded.goods_id, spec_raw = excluded.spec_raw, color = excluded.color, size = excluded.size, advice = excluded.advice, parse_ok = excluded.parse_ok, sku_code = excluded.sku_code, updated_at = excluded.updated_at`, // is_manual 不在上面的 SET 列表里,SQLite 对没提到的列保持原值不变。 skuID, goodsID, specRaw, color, size, advice, parseOKInt, skuCode, now, now) if err != nil { return fmt.Errorf("写入蝦皮 SKU %s 失败: %w", skuID, err) } return nil } // ShopeeSKURow 是列表页要的一行:SKU 本身,联查出商品标题和采集状态。 // // 采集状态挂在 pdd_products 上,不在蝦皮这边,见 // docs/admin/01-requirements.md §6.1。CollectStatus / CollectMsg 为 // NULL 表示这个蝦皮商品还没填 PDD 链接(LEFT JOIN 没查到对应行)。 type ShopeeSKURow struct { model.ShopeeSKU ProductTitle string PddGoodsURL string PddGoodsID string CollectStatus sql.NullString CollectMsg sql.NullString } // ListShopeeSKUs 按蝦皮商品 ID 关键字查 SKU 列表(联查商品标题和采集状态)。 // keyword 为空表示不筛选。 func ListShopeeSKUs(q Execer, keyword string) ([]ShopeeSKURow, error) { sqlText := ` SELECT sk.sku_id, sk.goods_id, sk.spec_raw, sk.color, sk.size, sk.advice, sk.parse_ok, sk.sku_code, sk.is_manual, sk.created_at, sk.updated_at, sp.title, sp.pdd_goods_url, sp.pdd_goods_id, pp.collect_status, pp.collect_msg FROM shopee_skus sk JOIN shopee_products sp ON sp.goods_id = sk.goods_id LEFT JOIN pdd_products pp ON pp.goods_id = sp.pdd_goods_id AND pp.deleted_at IS NULL` var args []any if keyword = strings.TrimSpace(keyword); keyword != "" { sqlText += ` WHERE sk.goods_id LIKE ? ESCAPE '\'` args = append(args, "%"+escapeLike(keyword)+"%") } sqlText += ` ORDER BY sk.goods_id, sk.sku_id` rows, err := q.Query(sqlText, args...) if err != nil { return nil, fmt.Errorf("查询蝦皮 SKU 列表失败: %w", err) } defer rows.Close() var list []ShopeeSKURow for rows.Next() { var r ShopeeSKURow var color, size, advice, skuCode sql.NullString var parseOK, isManual int var pddGoodsURL, pddGoodsID sql.NullString if err := rows.Scan( &r.SKUID, &r.GoodsID, &r.SpecRaw, &color, &size, &advice, &parseOK, &skuCode, &isManual, &r.CreatedAt, &r.UpdatedAt, &r.ProductTitle, &pddGoodsURL, &pddGoodsID, &r.CollectStatus, &r.CollectMsg, ); err != nil { return nil, fmt.Errorf("读取蝦皮 SKU 列表失败: %w", err) } r.Color = color.String r.Size = size.String r.Advice = advice.String r.SKUCode = skuCode.String r.ParseOK = parseOK != 0 r.IsManual = isManual != 0 r.PddGoodsURL = pddGoodsURL.String r.PddGoodsID = pddGoodsID.String list = append(list, r) } return list, rows.Err() } // CountShopeeProducts 统计蝦皮商品总数,供列表页判断"是否已导入过任何数据"。 func CountShopeeProducts(q Execer) (int, error) { var n int if err := q.QueryRow(`SELECT COUNT(*) FROM shopee_products`).Scan(&n); err != nil { return 0, fmt.Errorf("统计蝦皮商品数量失败: %w", err) } return n, nil }