+171
-14
@@ -5,7 +5,9 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -351,19 +353,7 @@ func UpsertSybOrder(q Execer, o model.SybOrder) (created bool, err error) {
|
||||
|
||||
now := model.NowISO()
|
||||
if strings.TrimSpace(o.ShopeeGoodsID) != "" {
|
||||
if _, err := q.Exec(`
|
||||
INSERT INTO shopee_products (goods_id, title, source, created_at, updated_at)
|
||||
VALUES (?, ?, 'syb', ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
title = CASE
|
||||
WHEN source = 'syb' AND TRIM(VALUES(title)) <> '' THEN VALUES(title)
|
||||
ELSE title
|
||||
END,
|
||||
updated_at = CASE
|
||||
WHEN source = 'syb' AND TRIM(VALUES(title)) <> '' THEN VALUES(updated_at)
|
||||
ELSE updated_at
|
||||
END`,
|
||||
o.ShopeeGoodsID, o.Title, now, now); err != nil {
|
||||
if err := upsertSybShopeeProduct(q, o.ShopeeGoodsID, o.Title, o.ShopName, o.ImageURL, now); err != nil {
|
||||
return false, fmt.Errorf("为顺运宝明细 %s 补建蝦皮商品骨架失败: %w", o.SybID, err)
|
||||
}
|
||||
}
|
||||
@@ -393,6 +383,173 @@ func UpsertSybOrder(q Execer, o model.SybOrder) (created bool, err error) {
|
||||
return created, nil
|
||||
}
|
||||
|
||||
// upsertSybShopeeProduct 用顺运宝观测补建蝦皮商品骨架。
|
||||
//
|
||||
// 店铺和图片是字段级低优先级数据:只能补空值,或更新原本同样来自 syb 的值;
|
||||
// 人工字段和商品目录等权威来源永远不被顺运宝覆盖。空值也不能清除已有内容。
|
||||
func upsertSybShopeeProduct(q Execer, goodsID, title, shopName, imageURL, observedAt string) error {
|
||||
_, err := q.Exec(`
|
||||
INSERT INTO shopee_products
|
||||
(goods_id,title,image_url,shopee_shop_name,
|
||||
image_source,image_observed_at,image_is_manual,
|
||||
shop_name_source,shop_name_observed_at,shop_name_is_manual,
|
||||
source,source_observed_at,created_at,updated_at)
|
||||
VALUES (?, ?, NULLIF(TRIM(?),''), NULLIF(TRIM(?),''),
|
||||
CASE WHEN TRIM(?)='' THEN NULL ELSE 'syb' END,
|
||||
CASE WHEN TRIM(?)='' THEN NULL ELSE ? END, 0,
|
||||
CASE WHEN TRIM(?)='' THEN NULL ELSE 'syb' END,
|
||||
CASE WHEN TRIM(?)='' THEN NULL ELSE ? END, 0,
|
||||
'syb', ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
title = CASE
|
||||
WHEN source='syb' AND TRIM(VALUES(title))<>'' THEN VALUES(title)
|
||||
ELSE title
|
||||
END,
|
||||
source_observed_at = CASE
|
||||
WHEN source='syb' AND TRIM(VALUES(title))<>'' THEN VALUES(source_observed_at)
|
||||
ELSE source_observed_at
|
||||
END,
|
||||
image_url = CASE
|
||||
WHEN image_is_manual=0 AND TRIM(VALUES(image_url))<>''
|
||||
AND (image_url IS NULL OR TRIM(image_url)='' OR image_source='syb') THEN VALUES(image_url)
|
||||
ELSE image_url
|
||||
END,
|
||||
image_observed_at = CASE
|
||||
WHEN image_is_manual=0 AND TRIM(VALUES(image_url))<>''
|
||||
AND (image_url IS NULL OR TRIM(image_url)='' OR image_source='syb') THEN VALUES(image_observed_at)
|
||||
ELSE image_observed_at
|
||||
END,
|
||||
image_source = CASE
|
||||
WHEN image_is_manual=0 AND TRIM(VALUES(image_url))<>''
|
||||
AND (image_url IS NULL OR TRIM(image_url)='' OR image_source='syb') THEN 'syb'
|
||||
ELSE image_source
|
||||
END,
|
||||
shopee_shop_name = CASE
|
||||
WHEN shop_name_is_manual=0 AND TRIM(VALUES(shopee_shop_name))<>''
|
||||
AND (shopee_shop_name IS NULL OR TRIM(shopee_shop_name)='' OR shop_name_source='syb') THEN VALUES(shopee_shop_name)
|
||||
ELSE shopee_shop_name
|
||||
END,
|
||||
shop_name_observed_at = CASE
|
||||
WHEN shop_name_is_manual=0 AND TRIM(VALUES(shopee_shop_name))<>''
|
||||
AND (shopee_shop_name IS NULL OR TRIM(shopee_shop_name)='' OR shop_name_source='syb') THEN VALUES(shop_name_observed_at)
|
||||
ELSE shop_name_observed_at
|
||||
END,
|
||||
shop_name_source = CASE
|
||||
WHEN shop_name_is_manual=0 AND TRIM(VALUES(shopee_shop_name))<>''
|
||||
AND (shopee_shop_name IS NULL OR TRIM(shopee_shop_name)='' OR shop_name_source='syb') THEN 'syb'
|
||||
ELSE shop_name_source
|
||||
END,
|
||||
updated_at = CASE
|
||||
WHEN source='syb'
|
||||
OR (image_source='syb' AND TRIM(VALUES(image_url))<>'')
|
||||
OR (shop_name_source='syb' AND TRIM(VALUES(shopee_shop_name))<>'') THEN VALUES(updated_at)
|
||||
ELSE updated_at
|
||||
END`,
|
||||
goodsID, title, imageURL, shopName,
|
||||
imageURL, imageURL, observedAt,
|
||||
shopName, shopName, observedAt,
|
||||
observedAt, observedAt, observedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
// backfillSybProductMetadata 把历史货运单中每个商品最新的非空店铺、图片补入商品主表。
|
||||
// 重放是安全的:实际覆盖规则仍由 upsertSybShopeeProduct 统一执行。
|
||||
func backfillSybProductMetadata(db *sql.DB) error {
|
||||
type metadata struct{ shopName, imageURL, observedAt string }
|
||||
items := map[string]metadata{}
|
||||
readLatest := func(column string, assign func(*metadata, string)) error {
|
||||
query := fmt.Sprintf(`SELECT shopee_goods_id, value_text, updated_at FROM (
|
||||
SELECT shopee_goods_id, %s AS value_text, updated_at,
|
||||
ROW_NUMBER() OVER (PARTITION BY shopee_goods_id ORDER BY updated_at DESC, syb_id DESC) AS row_no
|
||||
FROM syb_orders
|
||||
WHERE TRIM(COALESCE(shopee_goods_id,''))<>'' AND TRIM(COALESCE(%s,''))<>''
|
||||
) ranked WHERE row_no=1`, column, column)
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var goodsID, value, observedAt string
|
||||
if err := rows.Scan(&goodsID, &value, &observedAt); err != nil {
|
||||
return err
|
||||
}
|
||||
item := items[goodsID]
|
||||
assign(&item, value)
|
||||
if observedAt > item.observedAt {
|
||||
item.observedAt = observedAt
|
||||
}
|
||||
items[goodsID] = item
|
||||
}
|
||||
return rows.Err()
|
||||
}
|
||||
if err := readLatest("shop_name", func(item *metadata, value string) { item.shopName = value }); err != nil {
|
||||
return fmt.Errorf("读取历史顺运宝店铺失败: %w", err)
|
||||
}
|
||||
if err := readLatest("image_url", func(item *metadata, value string) { item.imageURL = value }); err != nil {
|
||||
return fmt.Errorf("读取历史顺运宝图片失败: %w", err)
|
||||
}
|
||||
for goodsID, item := range items {
|
||||
if err := upsertSybShopeeProduct(db, goodsID, "", item.shopName, item.imageURL, item.observedAt); err != nil {
|
||||
return fmt.Errorf("回填蝦皮商品 %s 的顺运宝元数据失败: %w", goodsID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpsertSybShopeeSKU 把格式明确的 SYB 规格作为低优先级蝦皮 SKU 观测写入。
|
||||
// 返回 parsed=false 时不写库,避免把猜测结果污染商品主数据。
|
||||
func UpsertSybShopeeSKU(q Execer, goodsID, specRaw, observedAt string) (parsed bool, err error) {
|
||||
parsedSpec, ok := spec.ParseShopeeSpec(specRaw)
|
||||
if !ok || strings.TrimSpace(goodsID) == "" {
|
||||
return false, nil
|
||||
}
|
||||
specKey, err := spec.SpecKey(specRaw)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
sum := sha256.Sum256([]byte(goodsID + "\x00" + specKey))
|
||||
recordID := "syb:" + hex.EncodeToString(sum[:])
|
||||
now := model.NowISO()
|
||||
_, err = UpsertCatalogShopeeSKU(q, CatalogShopeeSKUInput{
|
||||
RecordID: recordID, GoodsID: goodsID, SpecRaw: specRaw, SpecKey: specKey,
|
||||
Color: parsedSpec.Color, Size: parsedSpec.Size, Advice: parsedSpec.Advice,
|
||||
ParseOK: true, Source: "syb", ObservedAt: observedAt, Now: now, UpdatePolicy: "fill_missing",
|
||||
})
|
||||
return true, err
|
||||
}
|
||||
|
||||
func backfillSybShopeeSKUs(db *sql.DB) error {
|
||||
rows, err := db.Query(`SELECT shopee_goods_id,product_spec,updated_at FROM (
|
||||
SELECT shopee_goods_id,product_spec,updated_at,
|
||||
ROW_NUMBER() OVER (PARTITION BY shopee_goods_id,product_spec ORDER BY updated_at DESC,syb_id DESC) row_no
|
||||
FROM syb_orders
|
||||
WHERE TRIM(COALESCE(shopee_goods_id,''))<>'' AND TRIM(COALESCE(product_spec,''))<>''
|
||||
) ranked WHERE row_no=1`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取历史顺运宝规格失败: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
type observation struct{ goodsID, raw, observedAt string }
|
||||
var observations []observation
|
||||
for rows.Next() {
|
||||
var item observation
|
||||
if err := rows.Scan(&item.goodsID, &item.raw, &item.observedAt); err != nil {
|
||||
return err
|
||||
}
|
||||
observations = append(observations, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range observations {
|
||||
if _, err := UpsertSybShopeeSKU(db, item.goodsID, item.raw, item.observedAt); err != nil {
|
||||
return fmt.Errorf("回填蝦皮商品 %s 的顺运宝规格失败: %w", item.goodsID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SybSpecObservation 是顺运宝对某个蝦皮商品规格的历史观测汇总。
|
||||
// 它不是正式蝦皮 SKU,不得写回 shopee_skus。
|
||||
type SybSpecObservation struct {
|
||||
@@ -482,7 +639,7 @@ func sybOrderFilterClause(filter SybOrderFilter) (string, []any) {
|
||||
|
||||
const sybOrderContextFrom = `
|
||||
FROM syb_orders so
|
||||
LEFT JOIN shopee_products sp ON sp.goods_id = so.shopee_goods_id
|
||||
LEFT JOIN shopee_products sp ON sp.goods_id = so.shopee_goods_id AND sp.deleted_at IS NULL
|
||||
LEFT JOIN pdd_products pp ON pp.goods_id = sp.pdd_goods_id AND pp.deleted_at IS NULL
|
||||
LEFT JOIN spec_mappings sm
|
||||
ON sm.shopee_goods_id = so.shopee_goods_id
|
||||
|
||||
Reference in New Issue
Block a user