feat: 建立全局店铺跨渠道管理 (#208 #209 #210 #211)

This commit is contained in:
chengma
2026-08-13 11:15:54 +08:00
parent 901b37a8cb
commit 8f707722ba
28 changed files with 1160 additions and 500 deletions
+21 -5
View File
@@ -169,11 +169,15 @@ type CatalogProductOutcome struct {
// UpsertCatalogShopeeProduct 独立保护主图和店铺字段,绝不覆盖人工 PDD 关联。
func UpsertCatalogShopeeProduct(q Execer, in CatalogShopeeProductInput) (out CatalogProductOutcome, err error) {
var oldObserved, imageURL, shopName, imageSource, imageObserved, shopSource, shopObserved sql.NullString
var oldObserved, oldShopID, imageURL, shopName, imageSource, imageObserved, shopSource, shopObserved sql.NullString
var imageManual, shopManual int
err = q.QueryRow(`SELECT source_observed_at,image_url,shopee_shop_name,image_source,image_observed_at,image_is_manual,shop_name_source,shop_name_observed_at,shop_name_is_manual FROM shopee_products WHERE goods_id=?`, in.GoodsID).Scan(&oldObserved, &imageURL, &shopName, &imageSource, &imageObserved, &imageManual, &shopSource, &shopObserved, &shopManual)
shopID, err := FindShopIDByAlias(q, "shopee", in.ShopName)
if err != nil {
return out, err
}
err = q.QueryRow(`SELECT source_observed_at,shop_id,image_url,shopee_shop_name,image_source,image_observed_at,image_is_manual,shop_name_source,shop_name_observed_at,shop_name_is_manual FROM shopee_products WHERE goods_id=?`, in.GoodsID).Scan(&oldObserved, &oldShopID, &imageURL, &shopName, &imageSource, &imageObserved, &imageManual, &shopSource, &shopObserved, &shopManual)
if errors.Is(err, sql.ErrNoRows) {
_, err = q.Exec(`INSERT INTO shopee_products(goods_id,title,shopee_status,main_sku_code,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(?,''),NULLIF(?,''),NULLIF(?,''),NULLIF(?,''),CASE WHEN TRIM(?)='' THEN NULL ELSE ? END,CASE WHEN TRIM(?)='' THEN NULL ELSE ? END,0,CASE WHEN TRIM(?)='' THEN NULL ELSE ? END,CASE WHEN TRIM(?)='' THEN NULL ELSE ? END,0,'api',?,?,?)`, in.GoodsID, in.Title, in.Status, in.MainSKU, in.ImageURL, in.ShopName, in.ImageURL, in.Source, in.ImageURL, in.ObservedAt, in.ShopName, in.Source, in.ShopName, in.ObservedAt, in.ObservedAt, in.Now, in.Now)
_, err = q.Exec(`INSERT INTO shopee_products(goods_id,shop_id,title,shopee_status,main_sku_code,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(?,''),?,NULLIF(?,''),NULLIF(?,''),NULLIF(?,''),NULLIF(?,''),CASE WHEN TRIM(?)='' THEN NULL ELSE ? END,CASE WHEN TRIM(?)='' THEN NULL ELSE ? END,0,CASE WHEN TRIM(?)='' THEN NULL ELSE ? END,CASE WHEN TRIM(?)='' THEN NULL ELSE ? END,0,'api',?,?,?)`, in.GoodsID, shopID, in.Title, in.Status, in.MainSKU, in.ImageURL, in.ShopName, in.ImageURL, in.Source, in.ImageURL, in.ObservedAt, in.ShopName, in.Source, in.ShopName, in.ObservedAt, in.ObservedAt, in.Now, in.Now)
out.Created = err == nil
return out, err
}
@@ -225,10 +229,22 @@ func UpsertCatalogShopeeProduct(q Execer, in CatalogShopeeProductInput) (out Cat
}
imageChanged = apply(in.ImageURL, &newImage, imageSource, imageObserved, imageManual)
shopChanged = apply(in.ShopName, &newShop, shopSource, shopObserved, shopManual)
if !baseUpdate && !imageChanged && !shopChanged {
resolvedShopID := oldShopID.String
shopAssociationChanged := false
if shopChanged {
resolvedShopID, err = FindShopIDByAlias(q, "shopee", newShop)
if err != nil {
return out, err
}
shopAssociationChanged = resolvedShopID != oldShopID.String
} else if resolvedShopID == "" && shopID != "" && strings.TrimSpace(newShop) == strings.TrimSpace(in.ShopName) {
resolvedShopID = shopID
shopAssociationChanged = true
}
if !baseUpdate && !imageChanged && !shopChanged && !shopAssociationChanged {
return out, nil
}
_, err = q.Exec(`UPDATE shopee_products SET title=CASE WHEN ? THEN ? ELSE title END,shopee_status=CASE WHEN ? THEN NULLIF(?,'') ELSE shopee_status END,main_sku_code=CASE WHEN ? THEN NULLIF(?,'') ELSE main_sku_code END,source=CASE WHEN ? THEN 'api' ELSE source END,source_observed_at=CASE WHEN ? THEN ? ELSE source_observed_at END,image_url=NULLIF(?,''),shopee_shop_name=NULLIF(?,''),image_source=CASE WHEN ? THEN ? ELSE image_source END,image_observed_at=CASE WHEN ? THEN ? ELSE image_observed_at END,shop_name_source=CASE WHEN ? THEN ? ELSE shop_name_source END,shop_name_observed_at=CASE WHEN ? THEN ? ELSE shop_name_observed_at END,updated_at=? WHERE goods_id=?`, baseUpdate, in.Title, baseUpdate, in.Status, baseUpdate, in.MainSKU, baseUpdate, baseUpdate, in.ObservedAt, newImage, newShop, imageChanged, in.Source, imageChanged, in.ObservedAt, shopChanged, in.Source, shopChanged, in.ObservedAt, in.Now, in.GoodsID)
_, err = q.Exec(`UPDATE shopee_products SET title=CASE WHEN ? THEN ? ELSE title END,shopee_status=CASE WHEN ? THEN NULLIF(?,'') ELSE shopee_status END,main_sku_code=CASE WHEN ? THEN NULLIF(?,'') ELSE main_sku_code END,source=CASE WHEN ? THEN 'api' ELSE source END,source_observed_at=CASE WHEN ? THEN ? ELSE source_observed_at END,shop_id=CASE WHEN ? THEN NULLIF(?,'') ELSE shop_id END,image_url=NULLIF(?,''),shopee_shop_name=NULLIF(?,''),image_source=CASE WHEN ? THEN ? ELSE image_source END,image_observed_at=CASE WHEN ? THEN ? ELSE image_observed_at END,shop_name_source=CASE WHEN ? THEN ? ELSE shop_name_source END,shop_name_observed_at=CASE WHEN ? THEN ? ELSE shop_name_observed_at END,updated_at=? WHERE goods_id=?`, baseUpdate, in.Title, baseUpdate, in.Status, baseUpdate, in.MainSKU, baseUpdate, baseUpdate, in.ObservedAt, shopAssociationChanged, resolvedShopID, newImage, newShop, imageChanged, in.Source, imageChanged, in.ObservedAt, shopChanged, in.Source, shopChanged, in.ObservedAt, in.Now, in.GoodsID)
out.Updated = err == nil
return out, err
}
+158 -2
View File
@@ -20,7 +20,7 @@ import (
"cmautobuy/admin/spec"
)
const mysqlSchemaVersion = 17
const mysqlSchemaVersion = 18
// OpenMySQL 打开生产 MySQL 8 数据库。错误信息绝不包含完整 DSN 或密码。
func OpenMySQL(cfg config.DatabaseConfig) (*sql.DB, error) {
@@ -628,6 +628,18 @@ func MigrateMySQL(db *sql.DB) error {
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`, 17, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
return fmt.Errorf("记录 MySQL schema v17 失败: %w", err)
}
current = 17
}
if current < 18 {
if err := migrateMySQLV18(db); err != nil {
return fmt.Errorf("执行 MySQL schema v18 失败: %w", err)
}
if err := checkMySQLV18Shape(db); err != nil {
return fmt.Errorf("MySQL schema v18 自检失败,未记录版本: %w", err)
}
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`, 18, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
return fmt.Errorf("记录 MySQL schema v18 失败: %w", err)
}
}
return CheckMySQLSchema(db)
}
@@ -675,6 +687,114 @@ func migrateMySQLV17(db *sql.DB) error {
return nil
}
// migrateMySQLV18 建立跨渠道店铺身份,保留旧 SYB 准入表作为回退依据。
func migrateMySQLV18(db *sql.DB) error {
statements := []string{
`CREATE TABLE IF NOT EXISTS shops (
shop_id VARCHAR(191) COLLATE utf8mb4_bin PRIMARY KEY,
display_name VARCHAR(500) NOT NULL,
normalized_name VARCHAR(500) COLLATE utf8mb4_bin NOT NULL,
enabled TINYINT NOT NULL DEFAULT 1,
created_by_user_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
created_at VARCHAR(35) NOT NULL,
updated_at VARCHAR(35) NOT NULL,
UNIQUE KEY uq_shops_name (normalized_name),
KEY idx_shops_enabled (enabled,normalized_name),
CONSTRAINT fk_shops_user FOREIGN KEY (created_by_user_id) REFERENCES users(user_id),
CONSTRAINT chk_shops_enabled CHECK (enabled IN (0,1))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
`CREATE TABLE IF NOT EXISTS shop_channel_aliases (
alias_id VARCHAR(191) COLLATE utf8mb4_bin PRIMARY KEY,
shop_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
channel VARCHAR(16) COLLATE utf8mb4_bin NOT NULL,
alias_name VARCHAR(500) NOT NULL,
normalized_alias VARCHAR(500) COLLATE utf8mb4_bin NOT NULL,
enabled TINYINT NOT NULL DEFAULT 1,
created_at VARCHAR(35) NOT NULL,
updated_at VARCHAR(35) NOT NULL,
UNIQUE KEY uq_shop_channel_alias (channel,normalized_alias),
KEY idx_shop_alias_shop (shop_id,channel,enabled),
CONSTRAINT fk_shop_alias_shop FOREIGN KEY (shop_id) REFERENCES shops(shop_id) ON DELETE CASCADE,
CONSTRAINT chk_shop_alias_channel CHECK (channel IN ('syb','shopee')),
CONSTRAINT chk_shop_alias_enabled CHECK (enabled IN (0,1))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
}
for _, statement := range statements {
if _, err := db.Exec(statement); err != nil {
return err
}
}
for _, column := range []struct{ table, name, ddl string }{
{"syb_orders", "shop_id", `ALTER TABLE syb_orders ADD COLUMN shop_id VARCHAR(191) COLLATE utf8mb4_bin NULL AFTER order_no`},
{"shopee_products", "shop_id", `ALTER TABLE shopee_products ADD COLUMN shop_id VARCHAR(191) COLLATE utf8mb4_bin NULL AFTER goods_id`},
} {
exists, err := mysqlColumnExists(db, column.table, column.name)
if err != nil {
return err
}
if !exists {
if _, err := db.Exec(column.ddl); err != nil {
return fmt.Errorf("增加 %s.%s 失败: %w", column.table, column.name, err)
}
}
}
for _, index := range []struct{ table, name, ddl string }{
{"syb_orders", "idx_syb_orders_shop_id", `ALTER TABLE syb_orders ADD INDEX idx_syb_orders_shop_id (shop_id)`},
{"shopee_products", "idx_shopee_products_shop_id", `ALTER TABLE shopee_products ADD INDEX idx_shopee_products_shop_id (shop_id)`},
} {
exists, err := mysqlIndexExists(db, index.table, index.name)
if err != nil {
return err
}
if !exists {
if _, err := db.Exec(index.ddl); err != nil {
return err
}
}
}
for _, fk := range []struct{ table, name, ddl string }{
{"syb_orders", "fk_syb_orders_shop", `ALTER TABLE syb_orders ADD CONSTRAINT fk_syb_orders_shop FOREIGN KEY (shop_id) REFERENCES shops(shop_id) ON DELETE SET NULL`},
{"shopee_products", "fk_shopee_products_shop", `ALTER TABLE shopee_products ADD CONSTRAINT fk_shopee_products_shop FOREIGN KEY (shop_id) REFERENCES shops(shop_id) ON DELETE SET NULL`},
} {
exists, err := mysqlConstraintExists(db, fk.table, fk.name)
if err != nil {
return err
}
if !exists {
if _, err := db.Exec(fk.ddl); err != nil {
return err
}
}
}
// 旧准入项一对一迁移成业务店铺;旧 enabled 只控制 SYB 渠道,不停用业务店铺。
if _, err := db.Exec(`INSERT INTO shops(shop_id,display_name,normalized_name,enabled,created_by_user_id,created_at,updated_at)
SELECT shop_id,shop_name,normalized_name,1,created_by_user_id,created_at,updated_at FROM syb_allowed_shops
ON DUPLICATE KEY UPDATE display_name=VALUES(display_name),updated_at=VALUES(updated_at)`); err != nil {
return fmt.Errorf("迁移旧同步店铺失败: %w", err)
}
if _, err := db.Exec(`INSERT INTO shop_channel_aliases(alias_id,shop_id,channel,alias_name,normalized_alias,enabled,created_at,updated_at)
SELECT CONCAT('legacy-syb-',shop_id),shop_id,'syb',shop_name,normalized_name,enabled,created_at,updated_at FROM syb_allowed_shops
ON DUPLICATE KEY UPDATE shop_id=VALUES(shop_id),alias_name=VALUES(alias_name),enabled=VALUES(enabled),updated_at=VALUES(updated_at)`); err != nil {
return fmt.Errorf("迁移 SYB 店铺别名失败: %w", err)
}
if _, err := db.Exec(`INSERT INTO shop_channel_aliases(alias_id,shop_id,channel,alias_name,normalized_alias,enabled,created_at,updated_at)
SELECT CONCAT('legacy-shopee-',shop_id),shop_id,'shopee',shop_name,normalized_name,1,created_at,updated_at FROM syb_allowed_shops
ON DUPLICATE KEY UPDATE shop_id=VALUES(shop_id),alias_name=VALUES(alias_name),updated_at=VALUES(updated_at)`); err != nil {
return fmt.Errorf("迁移蝦皮店铺别名失败: %w", err)
}
if _, err := db.Exec(`UPDATE syb_orders so JOIN shop_channel_aliases a
ON a.channel='syb' AND a.normalized_alias=TRIM(so.shop_name) COLLATE utf8mb4_bin
SET so.shop_id=a.shop_id WHERE so.shop_id IS NULL`); err != nil {
return fmt.Errorf("回填顺运宝业务店铺失败: %w", err)
}
if _, err := db.Exec(`UPDATE shopee_products sp JOIN shop_channel_aliases a
ON a.channel='shopee' AND a.normalized_alias=TRIM(sp.shopee_shop_name) COLLATE utf8mb4_bin
SET sp.shop_id=a.shop_id WHERE sp.shop_id IS NULL`); err != nil {
return fmt.Errorf("回填蝦皮业务店铺失败: %w", err)
}
return nil
}
// migrateMySQLV14 增加顺运宝同步店铺准入表和审计统计。
func migrateMySQLV14(db *sql.DB) error {
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS syb_allowed_shops (
@@ -1667,7 +1787,10 @@ func CheckMySQLSchema(db *sql.DB) error {
if err := checkMySQLV16Shape(db); err != nil {
return err
}
return checkMySQLV17Shape(db)
if err := checkMySQLV17Shape(db); err != nil {
return err
}
return checkMySQLV18Shape(db)
}
func checkMySQLV15Shape(db *sql.DB) error {
@@ -1713,6 +1836,39 @@ func checkMySQLV17Shape(db *sql.DB) error {
return nil
}
func checkMySQLV18Shape(db *sql.DB) error {
if err := checkMySQLSchema(db, []string{"shops", "shop_channel_aliases"}); err != nil {
return err
}
for _, column := range []struct{ table, name string }{{"syb_orders", "shop_id"}, {"shopee_products", "shop_id"}} {
if err := checkMySQLVarcharColumn(db, column.table, column.name, 191, true, "utf8mb4_bin", ""); err != nil {
return err
}
}
for _, item := range []struct{ table, name string }{
{"shops", "uq_shops_name"}, {"shops", "idx_shops_enabled"},
{"shop_channel_aliases", "uq_shop_channel_alias"}, {"shop_channel_aliases", "idx_shop_alias_shop"},
{"syb_orders", "idx_syb_orders_shop_id"}, {"shopee_products", "idx_shopee_products_shop_id"},
} {
exists, err := mysqlIndexExists(db, item.table, item.name)
if err != nil || !exists {
return fmt.Errorf("店铺索引 %s.%s 缺失: %v", item.table, item.name, err)
}
}
for _, item := range []struct{ table, name string }{
{"shops", "fk_shops_user"}, {"shops", "chk_shops_enabled"},
{"shop_channel_aliases", "fk_shop_alias_shop"}, {"shop_channel_aliases", "chk_shop_alias_channel"},
{"shop_channel_aliases", "chk_shop_alias_enabled"}, {"syb_orders", "fk_syb_orders_shop"},
{"shopee_products", "fk_shopee_products_shop"},
} {
exists, err := mysqlConstraintExists(db, item.table, item.name)
if err != nil || !exists {
return fmt.Errorf("店铺约束 %s.%s 缺失: %v", item.table, item.name, err)
}
}
return nil
}
func checkMySQLV14Shape(db *sql.DB) error {
if err := checkMySQLSchema(db, []string{"syb_allowed_shops"}); err != nil {
return err
@@ -875,6 +875,57 @@ func TestMySQLMigrate_V14升级V17回填元数据规格并增加软删除(t *tes
}
}
func TestMySQLMigrate_V17升级V18迁移全局店铺并精确回填(t *testing.T) {
db := openMySQLMigrationTestDB(t)
defer db.Close()
cleanMySQLTestSchema(t, db)
defer cleanMySQLTestSchema(t, db)
if err := MigrateMySQL(db); err != nil {
t.Fatal(err)
}
// 退回真实 v17 形状,再放入旧准入配置和两侧历史数据。
mustExec(t, db, `ALTER TABLE shopee_products DROP FOREIGN KEY fk_shopee_products_shop`)
mustExec(t, db, `ALTER TABLE syb_orders DROP FOREIGN KEY fk_syb_orders_shop`)
mustExec(t, db, `ALTER TABLE shopee_products DROP COLUMN shop_id`)
mustExec(t, db, `ALTER TABLE syb_orders DROP COLUMN shop_id`)
mustExec(t, db, `DROP TABLE shop_channel_aliases`)
mustExec(t, db, `DROP TABLE shops`)
mustExec(t, db, `DELETE FROM schema_migrations WHERE version=18`)
now := "2026-08-13T02:00:00Z"
mustExec(t, db, `INSERT INTO users(user_id,username,password_hash,role,status,password_changed_at,created_at,updated_at)
VALUES('V18-USER','v18-user','x','admin','active',?,?,?)`, now, now, now)
mustExec(t, db, `INSERT INTO syb_allowed_shops(shop_id,shop_name,normalized_name,enabled,created_by_user_id,created_at,updated_at)
VALUES('V18-SHOP','精确店铺','精确店铺',1,'V18-USER',?,?)`, now, now)
mustExec(t, db, `INSERT INTO syb_orders(syb_id,order_no,shop_name,title,quantity,syb_data,created_at,updated_at)
VALUES('V18-ORDER','O-V18','精确店铺','商品',1,'{}',?,?)`, now, now)
mustExec(t, db, `INSERT INTO shopee_products(goods_id,title,shopee_shop_name,source,created_at,updated_at)
VALUES('V18-PRODUCT','商品','精确店铺','api',?,?),('V18-UNLINKED','商品','其他店铺','api',?,?)`, now, now, now, now)
if err := MigrateMySQL(db); err != nil {
t.Fatalf("v17 升级 v18 失败: %v", err)
}
if err := MigrateMySQL(db); err != nil {
t.Fatalf("v18 重放失败: %v", err)
}
if err := checkMySQLV18Shape(db); err != nil {
t.Fatalf("v18 结构错误: %v", err)
}
for _, query := range []string{
`SELECT shop_id FROM syb_orders WHERE syb_id='V18-ORDER'`,
`SELECT shop_id FROM shopee_products WHERE goods_id='V18-PRODUCT'`,
} {
var shopID string
if err := db.QueryRow(query).Scan(&shopID); err != nil || shopID != "V18-SHOP" {
t.Fatalf("历史精确回填错误: shop=%q err=%v", shopID, err)
}
}
var unlinked sql.NullString
if err := db.QueryRow(`SELECT shop_id FROM shopee_products WHERE goods_id='V18-UNLINKED'`).Scan(&unlinked); err != nil || unlinked.Valid {
t.Fatalf("无法确认的店铺不应猜测关联: %+v err=%v", unlinked, err)
}
}
func openMySQLMigrationTestDB(t *testing.T) *sql.DB {
t.Helper()
if os.Getenv("CMAUTOBUY_MYSQL_TEST") != "1" {
+253
View File
@@ -0,0 +1,253 @@
package repository
import (
"database/sql"
"errors"
"fmt"
"strings"
"cmautobuy/admin/model"
"github.com/go-sql-driver/mysql"
)
var (
ErrShopNameExists = errors.New("店铺名称已经存在")
ErrShopAliasExists = errors.New("渠道店铺名称已经关联")
ErrShopHasReferences = errors.New("店铺仍有关联数据")
)
// ListShops 返回管理页的一店一行汇总;首版每个渠道维护一个主别名。
func ListShops(q Execer) ([]model.Shop, error) {
rows, err := q.Query(`SELECT s.shop_id,s.display_name,s.normalized_name,s.enabled,
COALESCE(MAX(CASE WHEN a.channel='syb' THEN a.alias_name END),''),
COALESCE(MAX(CASE WHEN a.channel='syb' THEN a.enabled END),0),
COALESCE(MAX(CASE WHEN a.channel='shopee' THEN a.alias_name END),''),
COUNT(DISTINCT sp.goods_id),s.created_by_user_id,s.created_at,s.updated_at
FROM shops s
LEFT JOIN shop_channel_aliases a ON a.shop_id=s.shop_id
LEFT JOIN shopee_products sp ON sp.shop_id=s.shop_id AND sp.deleted_at IS NULL
GROUP BY s.shop_id ORDER BY s.enabled DESC,s.normalized_name`)
if err != nil {
return nil, fmt.Errorf("查询店铺列表失败: %w", err)
}
defer rows.Close()
var result []model.Shop
for rows.Next() {
var item model.Shop
var enabled, sybEnabled int
if err := rows.Scan(&item.ShopID, &item.DisplayName, &item.NormalizedName, &enabled,
&item.SybAlias, &sybEnabled, &item.ShopeeAlias, &item.ShopeeProductCount,
&item.CreatedByUserID, &item.CreatedAt, &item.UpdatedAt); err != nil {
return nil, err
}
item.Enabled = enabled == 1
item.SybSyncEnabled = sybEnabled == 1
result = append(result, item)
}
return result, rows.Err()
}
// ListShopOptions 返回筛选器使用的全部业务店铺,停用项仍保留以便查历史数据。
func ListShopOptions(q Execer) ([]model.Shop, error) {
rows, err := q.Query(`SELECT shop_id,display_name,normalized_name,enabled,created_by_user_id,created_at,updated_at
FROM shops ORDER BY enabled DESC,normalized_name`)
if err != nil {
return nil, fmt.Errorf("查询店铺选项失败: %w", err)
}
defer rows.Close()
var result []model.Shop
for rows.Next() {
var item model.Shop
var enabled int
if err := rows.Scan(&item.ShopID, &item.DisplayName, &item.NormalizedName, &enabled,
&item.CreatedByUserID, &item.CreatedAt, &item.UpdatedAt); err != nil {
return nil, err
}
item.Enabled = enabled == 1
result = append(result, item)
}
return result, rows.Err()
}
func GetShop(q Execer, shopID string) (model.Shop, bool, error) {
var item model.Shop
var enabled int
err := q.QueryRow(`SELECT shop_id,display_name,normalized_name,enabled,created_by_user_id,created_at,updated_at
FROM shops WHERE shop_id=?`, shopID).Scan(&item.ShopID, &item.DisplayName,
&item.NormalizedName, &enabled, &item.CreatedByUserID, &item.CreatedAt, &item.UpdatedAt)
if errors.Is(err, sql.ErrNoRows) {
return model.Shop{}, false, nil
}
if err != nil {
return model.Shop{}, false, fmt.Errorf("查询店铺失败: %w", err)
}
item.Enabled = enabled == 1
return item, true, nil
}
func GetShopAliasEnabled(q Execer, shopID, channel string) (bool, bool, error) {
var enabled int
err := q.QueryRow(`SELECT enabled FROM shop_channel_aliases WHERE shop_id=? AND channel=? LIMIT 1`,
shopID, channel).Scan(&enabled)
if errors.Is(err, sql.ErrNoRows) {
return false, false, nil
}
if err != nil {
return false, false, fmt.Errorf("查询渠道店铺状态失败: %w", err)
}
return enabled == 1, true, nil
}
// ListEnabledSybShopMappings 返回“SYB 原始精确名称 → 业务店铺 ID”。
func ListEnabledSybShopMappings(q Execer) (map[string]string, error) {
rows, err := q.Query(`SELECT a.normalized_alias,a.shop_id FROM shop_channel_aliases a
JOIN shops s ON s.shop_id=a.shop_id WHERE a.channel='syb' AND a.enabled=1 AND s.enabled=1
ORDER BY a.normalized_alias`)
if err != nil {
return nil, fmt.Errorf("查询启用的 SYB 店铺配置失败: %w", err)
}
defer rows.Close()
result := map[string]string{}
for rows.Next() {
var name, shopID string
if err := rows.Scan(&name, &shopID); err != nil {
return nil, err
}
result[name] = shopID
}
return result, rows.Err()
}
func FindShopIDByAlias(q Execer, channel, rawName string) (string, error) {
name := strings.TrimSpace(rawName)
if name == "" {
return "", nil
}
var shopID string
err := q.QueryRow(`SELECT shop_id FROM shop_channel_aliases
WHERE channel=? AND normalized_alias=?`, channel, name).Scan(&shopID)
if errors.Is(err, sql.ErrNoRows) {
return "", nil
}
if err != nil {
return "", fmt.Errorf("按 %s 店铺名称查业务店铺失败: %w", channel, err)
}
return shopID, nil
}
func InsertShop(q Execer, item model.Shop) error {
_, err := q.Exec(`INSERT INTO shops(shop_id,display_name,normalized_name,enabled,created_by_user_id,created_at,updated_at)
VALUES(?,?,?,?,?,?,?)`, item.ShopID, item.DisplayName, item.NormalizedName, item.Enabled,
item.CreatedByUserID, item.CreatedAt, item.UpdatedAt)
if err != nil {
var mysqlErr *mysql.MySQLError
if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
return ErrShopNameExists
}
return fmt.Errorf("新增店铺失败: %w", err)
}
return nil
}
func InsertShopAlias(q Execer, item model.ShopChannelAlias) error {
_, err := q.Exec(`INSERT INTO shop_channel_aliases(alias_id,shop_id,channel,alias_name,normalized_alias,enabled,created_at,updated_at)
VALUES(?,?,?,?,?,?,?,?)`, item.AliasID, item.ShopID, item.Channel, item.AliasName,
item.NormalizedAlias, item.Enabled, item.CreatedAt, item.UpdatedAt)
if err != nil {
var mysqlErr *mysql.MySQLError
if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
return ErrShopAliasExists
}
return fmt.Errorf("新增渠道店铺名称失败: %w", err)
}
return nil
}
func UpdateShopName(q Execer, shopID, displayName, normalizedName, updatedAt string) (bool, error) {
result, err := q.Exec(`UPDATE shops SET display_name=?,normalized_name=?,updated_at=? WHERE shop_id=?`,
displayName, normalizedName, updatedAt, shopID)
if err != nil {
var mysqlErr *mysql.MySQLError
if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
return false, ErrShopNameExists
}
return false, fmt.Errorf("更新店铺名称失败: %w", err)
}
n, err := result.RowsAffected()
return n == 1, err
}
func SetShopEnabled(q Execer, shopID string, enabled bool, updatedAt string) (bool, error) {
result, err := q.Exec(`UPDATE shops SET enabled=?,updated_at=? WHERE shop_id=?`, enabled, updatedAt, shopID)
if err != nil {
return false, fmt.Errorf("更新店铺状态失败: %w", err)
}
n, err := result.RowsAffected()
return n == 1, err
}
func SetShopSybEnabled(q Execer, shopID string, enabled bool, updatedAt string) (bool, error) {
result, err := q.Exec(`UPDATE shop_channel_aliases SET enabled=?,updated_at=?
WHERE shop_id=? AND channel='syb'`, enabled, updatedAt, shopID)
if err != nil {
return false, fmt.Errorf("更新 SYB 同步状态失败: %w", err)
}
n, err := result.RowsAffected()
return n > 0, err
}
func ReplaceShopAlias(q Execer, aliasID, shopID, channel, aliasName, updatedAt string, enabled bool) error {
if _, err := q.Exec(`DELETE FROM shop_channel_aliases WHERE shop_id=? AND channel=?`, shopID, channel); err != nil {
return fmt.Errorf("移除旧渠道店铺名称失败: %w", err)
}
if strings.TrimSpace(aliasName) == "" {
return nil
}
return InsertShopAlias(q, model.ShopChannelAlias{AliasID: aliasID, ShopID: shopID, Channel: channel,
AliasName: aliasName, NormalizedAlias: strings.TrimSpace(aliasName), Enabled: enabled,
CreatedAt: updatedAt, UpdatedAt: updatedAt})
}
// RebuildShopAssociations 按渠道原始名称精确重建一个店铺的派生关联。
func RebuildShopAssociations(q Execer, shopID string) error {
if _, err := q.Exec(`UPDATE syb_orders SET shop_id=NULL WHERE shop_id=?`, shopID); err != nil {
return err
}
if _, err := q.Exec(`UPDATE shopee_products SET shop_id=NULL WHERE shop_id=?`, shopID); err != nil {
return err
}
if _, err := q.Exec(`UPDATE syb_orders so JOIN shop_channel_aliases a
ON a.shop_id=? AND a.channel='syb' AND a.normalized_alias=TRIM(so.shop_name) COLLATE utf8mb4_bin
SET so.shop_id=a.shop_id`, shopID); err != nil {
return err
}
if _, err := q.Exec(`UPDATE shopee_products sp JOIN shop_channel_aliases a
ON a.shop_id=? AND a.channel='shopee' AND a.normalized_alias=TRIM(sp.shopee_shop_name) COLLATE utf8mb4_bin
SET sp.shop_id=a.shop_id`, shopID); err != nil {
return err
}
return nil
}
func DeleteDisabledShop(q Execer, shopID string) (bool, error) {
var references int
if err := q.QueryRow(`SELECT (SELECT COUNT(*) FROM syb_orders WHERE shop_id=?)+
(SELECT COUNT(*) FROM shopee_products WHERE shop_id=?)`, shopID, shopID).Scan(&references); err != nil {
return false, err
}
if references > 0 {
return false, ErrShopHasReferences
}
result, err := q.Exec(`DELETE FROM shops WHERE shop_id=? AND enabled=0`, shopID)
if err != nil {
return false, fmt.Errorf("删除停用店铺失败: %w", err)
}
n, err := result.RowsAffected()
return n == 1, err
}
func CountUnlinkedShopeeShops(q Execer) (int, error) {
var count int
err := q.QueryRow(`SELECT COUNT(*) FROM shopee_products WHERE deleted_at IS NULL AND shop_id IS NULL`).Scan(&count)
return count, err
}
+25 -12
View File
@@ -111,12 +111,13 @@ func UpsertShopeeSKU(q Execer, skuID, goodsID, specRaw, color, size, advice stri
// NULL 表示这个蝦皮商品还没填 PDD 链接(LEFT JOIN 没查到对应行)。
type ShopeeProductRow struct {
model.ShopeeProduct
ColorCount int
SizeCount int
SKUCount int
PendingCount int
CollectStatus sql.NullString
CollectMsg sql.NullString
BusinessShopName string
ColorCount int
SizeCount int
SKUCount int
PendingCount int
CollectStatus sql.NullString
CollectMsg sql.NullString
}
// ShopeeFilter 是蝦皮商品列表页支持的筛选条件,四项都可以为空。
@@ -136,6 +137,7 @@ type ShopeeFilter struct {
Status string
Shop string // has / missing / 空(全部)
Image string // has / missing / 空(全部)
StoreID string // 业务店铺 ID / unlinked / 空(全部)
Deleted bool // true 只看软删除数据;false 只看正常数据
}
@@ -186,6 +188,14 @@ func shopeeFilterClause(filter ShopeeFilter) (string, []any) {
case "missing":
clauses = append(clauses, `(sp.image_url IS NULL OR TRIM(sp.image_url) = '')`)
}
switch strings.TrimSpace(filter.StoreID) {
case "":
case "unlinked":
clauses = append(clauses, `sp.shop_id IS NULL`)
default:
clauses = append(clauses, `sp.shop_id=?`)
args = append(args, strings.TrimSpace(filter.StoreID))
}
return " WHERE " + strings.Join(clauses, " AND "), args
}
@@ -200,7 +210,7 @@ func shopeeFilterClause(filter ShopeeFilter) (string, []any) {
func ListShopeeProducts(q Execer, filter ShopeeFilter, limit, offset int) ([]ShopeeProductRow, error) {
where, args := shopeeFilterClause(filter)
sqlText := `
SELECT sp.goods_id, sp.title, sp.shopee_status, sp.main_sku_code, sp.image_url,sp.shopee_shop_name,sp.image_source,sp.image_observed_at,sp.image_is_manual,sp.shop_name_source,sp.shop_name_observed_at,sp.shop_name_is_manual,sp.source,
SELECT sp.goods_id,sp.shop_id,COALESCE(MAX(bs.display_name),''), sp.title, sp.shopee_status, sp.main_sku_code, sp.image_url,sp.shopee_shop_name,sp.image_source,sp.image_observed_at,sp.image_is_manual,sp.shop_name_source,sp.shop_name_observed_at,sp.shop_name_is_manual,sp.source,
sp.pdd_goods_url, sp.pdd_goods_id,sp.deleted_at,sp.deleted_by_user_id, sp.created_at, sp.updated_at,
COUNT(DISTINCT CASE WHEN sk.parse_ok = 1 THEN sk.color END) AS color_count,
COUNT(DISTINCT CASE WHEN sk.parse_ok = 1 THEN sk.size END) AS size_count,
@@ -208,6 +218,7 @@ func ListShopeeProducts(q Execer, filter ShopeeFilter, limit, offset int) ([]Sho
COUNT(CASE WHEN sk.parse_ok = 0 THEN 1 END) AS pending_count,
pp.collect_status, pp.collect_msg
FROM shopee_products sp
LEFT JOIN shops bs ON bs.shop_id=sp.shop_id
LEFT JOIN shopee_skus sk ON sk.goods_id = sp.goods_id
LEFT JOIN pdd_products pp
ON pp.goods_id = sp.pdd_goods_id AND pp.deleted_at IS NULL` +
@@ -223,10 +234,10 @@ func ListShopeeProducts(q Execer, filter ShopeeFilter, limit, offset int) ([]Sho
var list []ShopeeProductRow
for rows.Next() {
var r ShopeeProductRow
var title, shopeeStatus, mainSKUCode, imageURL, shopName, imageSource, imageObserved, shopSource, shopObserved, source, pddGoodsURL, pddGoodsID, deletedAt, deletedBy sql.NullString
var shopID, title, shopeeStatus, mainSKUCode, imageURL, shopName, imageSource, imageObserved, shopSource, shopObserved, source, pddGoodsURL, pddGoodsID, deletedAt, deletedBy sql.NullString
var imageManual, shopManual int
if err := rows.Scan(
&r.GoodsID, &title, &shopeeStatus, &mainSKUCode, &imageURL, &shopName, &imageSource, &imageObserved, &imageManual, &shopSource, &shopObserved, &shopManual, &source,
&r.GoodsID, &shopID, &r.BusinessShopName, &title, &shopeeStatus, &mainSKUCode, &imageURL, &shopName, &imageSource, &imageObserved, &imageManual, &shopSource, &shopObserved, &shopManual, &source,
&pddGoodsURL, &pddGoodsID, &deletedAt, &deletedBy, &r.CreatedAt, &r.UpdatedAt,
&r.ColorCount, &r.SizeCount, &r.SKUCount, &r.PendingCount,
&r.CollectStatus, &r.CollectMsg,
@@ -234,6 +245,7 @@ func ListShopeeProducts(q Execer, filter ShopeeFilter, limit, offset int) ([]Sho
return nil, fmt.Errorf("读取蝦皮商品列表失败: %w", err)
}
r.Title = title.String
r.ShopID = shopID.String
r.ShopeeStatus = shopeeStatus.String
r.MainSKUCode = mainSKUCode.String
r.ImageURL = imageURL.String
@@ -273,13 +285,13 @@ func CountShopeeProductsFiltered(q Execer, filter ShopeeFilter) (int, error) {
// 弹窗组装商品信息时用。查不到返回 (nil, nil)。
func GetShopeeProductByGoodsID(q Execer, goodsID string) (*model.ShopeeProduct, error) {
var p model.ShopeeProduct
var title, shopeeStatus, mainSKUCode, imageURL, shopName, imageSource, imageObserved, shopSource, shopObserved, source, pddGoodsURL, pddGoodsID, deletedAt, deletedBy sql.NullString
var shopID, title, shopeeStatus, mainSKUCode, imageURL, shopName, imageSource, imageObserved, shopSource, shopObserved, source, pddGoodsURL, pddGoodsID, deletedAt, deletedBy sql.NullString
var imageManual, shopManual int
err := q.QueryRow(`
SELECT goods_id, title, shopee_status, main_sku_code,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,
SELECT goods_id,shop_id, title, shopee_status, main_sku_code,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,
pdd_goods_url, pdd_goods_id,deleted_at,deleted_by_user_id, created_at, updated_at
FROM shopee_products WHERE goods_id = ? AND deleted_at IS NULL`, goodsID).Scan(
&p.GoodsID, &title, &shopeeStatus, &mainSKUCode, &imageURL, &shopName, &imageSource, &imageObserved, &imageManual, &shopSource, &shopObserved, &shopManual, &source,
&p.GoodsID, &shopID, &title, &shopeeStatus, &mainSKUCode, &imageURL, &shopName, &imageSource, &imageObserved, &imageManual, &shopSource, &shopObserved, &shopManual, &source,
&pddGoodsURL, &pddGoodsID, &deletedAt, &deletedBy, &p.CreatedAt, &p.UpdatedAt)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
@@ -288,6 +300,7 @@ func GetShopeeProductByGoodsID(q Execer, goodsID string) (*model.ShopeeProduct,
return nil, fmt.Errorf("查询蝦皮商品 %s 失败: %w", goodsID, err)
}
p.Title = title.String
p.ShopID = shopID.String
p.ShopeeStatus = shopeeStatus.String
p.MainSKUCode = mainSKUCode.String
p.ImageURL = imageURL.String
+24 -99
View File
@@ -12,100 +12,10 @@ import (
"fmt"
"strings"
"github.com/go-sql-driver/mysql"
"cmautobuy/admin/model"
"cmautobuy/admin/spec"
)
var ErrSybAllowedShopExists = errors.New("顺运宝允许店铺已经存在")
// ListSybAllowedShops 返回全部准入项;管理页需要同时看到已停用项。
func ListSybAllowedShops(q Execer) ([]model.SybAllowedShop, error) {
rows, err := q.Query(`SELECT shop_id,shop_name,normalized_name,enabled,created_by_user_id,created_at,updated_at
FROM syb_allowed_shops ORDER BY enabled DESC,normalized_name`)
if err != nil {
return nil, fmt.Errorf("查询顺运宝允许店铺失败: %w", err)
}
defer rows.Close()
var result []model.SybAllowedShop
for rows.Next() {
var item model.SybAllowedShop
var enabled int
if err := rows.Scan(&item.ShopID, &item.ShopName, &item.NormalizedName, &enabled,
&item.CreatedByUserID, &item.CreatedAt, &item.UpdatedAt); err != nil {
return nil, fmt.Errorf("读取顺运宝允许店铺失败: %w", err)
}
item.Enabled = enabled == 1
result = append(result, item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("读取顺运宝允许店铺失败: %w", err)
}
return result, nil
}
func ListEnabledSybShopNames(q Execer) ([]string, error) {
rows, err := q.Query(`SELECT normalized_name FROM syb_allowed_shops WHERE enabled=1 ORDER BY normalized_name`)
if err != nil {
return nil, fmt.Errorf("查询启用的顺运宝店铺失败: %w", err)
}
defer rows.Close()
var names []string
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, fmt.Errorf("读取启用的顺运宝店铺失败: %w", err)
}
names = append(names, name)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("读取启用的顺运宝店铺失败: %w", err)
}
return names, nil
}
func InsertSybAllowedShop(q Execer, item model.SybAllowedShop) error {
_, err := q.Exec(`INSERT INTO syb_allowed_shops
(shop_id,shop_name,normalized_name,enabled,created_by_user_id,created_at,updated_at)
VALUES(?,?,?,?,?,?,?)`, item.ShopID, item.ShopName, item.NormalizedName, item.Enabled,
item.CreatedByUserID, item.CreatedAt, item.UpdatedAt)
if err != nil {
var mysqlErr *mysql.MySQLError
if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
return ErrSybAllowedShopExists
}
return fmt.Errorf("新增顺运宝允许店铺失败: %w", err)
}
return nil
}
func SetSybAllowedShopEnabled(q Execer, shopID string, enabled bool, updatedAt string) (bool, error) {
result, err := q.Exec(`UPDATE syb_allowed_shops SET enabled=?,updated_at=? WHERE shop_id=?`, enabled, updatedAt, shopID)
if err != nil {
return false, fmt.Errorf("更新顺运宝允许店铺失败: %w", err)
}
affected, err := result.RowsAffected()
if err != nil {
return false, fmt.Errorf("确认顺运宝允许店铺更新结果失败: %w", err)
}
return affected == 1, nil
}
// DeleteDisabledSybAllowedShop 只删除提交瞬间仍处于停用状态的配置项。
// enabled 条件是最终并发保护,不能只依赖管理页隐藏启用项的删除按钮。
func DeleteDisabledSybAllowedShop(q Execer, shopID string) (bool, error) {
result, err := q.Exec(`DELETE FROM syb_allowed_shops WHERE shop_id=? AND enabled=0`, shopID)
if err != nil {
return false, fmt.Errorf("删除已停用的顺运宝店铺失败: %w", err)
}
affected, err := result.RowsAffected()
if err != nil {
return false, fmt.Errorf("确认顺运宝店铺删除结果失败: %w", err)
}
return affected == 1, nil
}
// ---------- 会话缓存 ----------
// SaveSybSession 写入或更新顺运宝登录会话缓存(按用户名 upsert)。
@@ -333,6 +243,12 @@ func UpsertSybOrder(q Execer, o model.SybOrder) (created bool, err error) {
if o.SybID == "" {
return false, fmt.Errorf("syb_id 不能为空")
}
if strings.TrimSpace(o.ShopID) == "" && strings.TrimSpace(o.ShopName) != "" {
o.ShopID, err = FindShopIDByAlias(q, "syb", o.ShopName)
if err != nil {
return false, fmt.Errorf("解析顺运宝明细 %s 的业务店铺失败: %w", o.SybID, err)
}
}
var specKey any
if strings.TrimSpace(o.ProductSpec) != "" {
key, keyErr := spec.SpecKey(o.ProductSpec)
@@ -353,17 +269,18 @@ func UpsertSybOrder(q Execer, o model.SybOrder) (created bool, err error) {
now := model.NowISO()
if strings.TrimSpace(o.ShopeeGoodsID) != "" {
if err := upsertSybShopeeProduct(q, o.ShopeeGoodsID, o.Title, o.ShopName, o.ImageURL, now); err != nil {
if err := upsertSybShopeeProduct(q, o.ShopeeGoodsID, o.ShopID, o.Title, o.ShopName, o.ImageURL, now); err != nil {
return false, fmt.Errorf("为顺运宝明细 %s 补建蝦皮商品骨架失败: %w", o.SybID, err)
}
}
_, err = q.Exec(`
INSERT INTO syb_orders
(syb_id, order_no, shop_name, title, product_spec, spec_key, shopee_goods_id,
(syb_id, order_no, shop_id, shop_name, title, product_spec, spec_key, shopee_goods_id,
quantity, price_twd_cent, image_url, syb_data, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, NULLIF(?,''), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
order_no = VALUES(order_no),
shop_id = VALUES(shop_id),
shop_name = VALUES(shop_name),
title = VALUES(title),
product_spec = VALUES(product_spec),
@@ -374,7 +291,7 @@ func UpsertSybOrder(q Execer, o model.SybOrder) (created bool, err error) {
image_url = VALUES(image_url),
syb_data = VALUES(syb_data),
updated_at = VALUES(updated_at)`,
o.SybID, o.OrderNo, nullableText(o.ShopName), o.Title, nullableText(o.ProductSpec), specKey, nullableText(o.ShopeeGoodsID),
o.SybID, o.OrderNo, strings.TrimSpace(o.ShopID), nullableText(o.ShopName), o.Title, nullableText(o.ProductSpec), specKey, nullableText(o.ShopeeGoodsID),
o.Quantity, o.PriceTwdCent, nullableText(o.ImageURL),
o.SybData, now, now)
if err != nil {
@@ -387,20 +304,24 @@ func UpsertSybOrder(q Execer, o model.SybOrder) (created bool, err error) {
//
// 店铺和图片是字段级低优先级数据:只能补空值,或更新原本同样来自 syb 的值;
// 人工字段和商品目录等权威来源永远不被顺运宝覆盖。空值也不能清除已有内容。
func upsertSybShopeeProduct(q Execer, goodsID, title, shopName, imageURL, observedAt string) error {
func upsertSybShopeeProduct(q Execer, goodsID, shopID, title, shopName, imageURL, observedAt string) error {
_, err := q.Exec(`
INSERT INTO shopee_products
(goods_id,title,image_url,shopee_shop_name,
(goods_id,shop_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(?),''),
VALUES (?, NULLIF(TRIM(?),''), ?, 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
shop_id = CASE
WHEN VALUES(shop_id) IS NOT NULL AND (shop_id IS NULL OR shop_name_source='syb') THEN VALUES(shop_id)
ELSE shop_id
END,
title = CASE
WHEN source='syb' AND TRIM(VALUES(title))<>'' THEN VALUES(title)
ELSE title
@@ -445,7 +366,7 @@ func upsertSybShopeeProduct(q Execer, goodsID, title, shopName, imageURL, observ
OR (shop_name_source='syb' AND TRIM(VALUES(shopee_shop_name))<>'') THEN VALUES(updated_at)
ELSE updated_at
END`,
goodsID, title, imageURL, shopName,
goodsID, shopID, title, imageURL, shopName,
imageURL, imageURL, observedAt,
shopName, shopName, observedAt,
observedAt, observedAt, observedAt)
@@ -490,7 +411,11 @@ func backfillSybProductMetadata(db *sql.DB) error {
return fmt.Errorf("读取历史顺运宝图片失败: %w", err)
}
for goodsID, item := range items {
if err := upsertSybShopeeProduct(db, goodsID, "", item.shopName, item.imageURL, item.observedAt); err != nil {
shopID, err := FindShopIDByAlias(db, "syb", item.shopName)
if err != nil {
return fmt.Errorf("解析历史顺运宝店铺失败: %w", err)
}
if err := upsertSybShopeeProduct(db, goodsID, shopID, "", item.shopName, item.imageURL, item.observedAt); err != nil {
return fmt.Errorf("回填蝦皮商品 %s 的顺运宝元数据失败: %w", goodsID, err)
}
}