154 lines
5.5 KiB
Go
154 lines
5.5 KiB
Go
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
|
|||
|
|
}
|