Files
cmautobuy/admin/repository/shopee.go
T

154 lines
5.5 KiB
Go
Raw Normal View History

2026-08-08 09:57:12 +08:00
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
}