@@ -171,7 +171,7 @@ type CatalogProductOutcome struct {
|
||||
func UpsertCatalogShopeeProduct(q Execer, in CatalogShopeeProductInput) (out CatalogProductOutcome, err error) {
|
||||
var oldObserved, oldShopID, imageURL, shopName, imageSource, imageObserved, shopSource, shopObserved sql.NullString
|
||||
var imageManual, shopManual int
|
||||
shopID, err := FindShopIDByAlias(q, "shopee", in.ShopName)
|
||||
shopID, err := FindShopIDByName(q, in.ShopName)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
@@ -232,7 +232,7 @@ func UpsertCatalogShopeeProduct(q Execer, in CatalogShopeeProductInput) (out Cat
|
||||
resolvedShopID := oldShopID.String
|
||||
shopAssociationChanged := false
|
||||
if shopChanged {
|
||||
resolvedShopID, err = FindShopIDByAlias(q, "shopee", newShop)
|
||||
resolvedShopID, err = FindShopIDByName(q, newShop)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"cmautobuy/admin/spec"
|
||||
)
|
||||
|
||||
const mysqlSchemaVersion = 18
|
||||
const mysqlSchemaVersion = 19
|
||||
|
||||
// OpenMySQL 打开生产 MySQL 8 数据库。错误信息绝不包含完整 DSN 或密码。
|
||||
func OpenMySQL(cfg config.DatabaseConfig) (*sql.DB, error) {
|
||||
@@ -640,6 +640,18 @@ func MigrateMySQL(db *sql.DB) error {
|
||||
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)
|
||||
}
|
||||
current = 18
|
||||
}
|
||||
if current < 19 {
|
||||
if err := migrateMySQLV19(db); err != nil {
|
||||
return fmt.Errorf("执行 MySQL schema v19 失败: %w", err)
|
||||
}
|
||||
if err := checkMySQLV19Shape(db); err != nil {
|
||||
return fmt.Errorf("MySQL schema v19 自检失败,未记录版本: %w", err)
|
||||
}
|
||||
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`, 19, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
|
||||
return fmt.Errorf("记录 MySQL schema v19 失败: %w", err)
|
||||
}
|
||||
}
|
||||
return CheckMySQLSchema(db)
|
||||
}
|
||||
@@ -795,6 +807,51 @@ func migrateMySQLV18(db *sql.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateMySQLV19 把 v18 的渠道别名收敛成唯一店铺名称。
|
||||
// shop_channel_aliases 暂时作为兼容表保留,其内容完全由 shops 派生,不再允许独立配置。
|
||||
func migrateMySQLV19(db *sql.DB) error {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
// v18 同时存在全局启停和 SYB 启停。收敛时任一侧已停用都保持停用,
|
||||
// 避免升级后意外把原本禁止同步的店铺重新放入同步范围。
|
||||
if _, err := tx.Exec(`UPDATE shops s JOIN (
|
||||
SELECT shop_id,MIN(enabled) AS enabled FROM shop_channel_aliases
|
||||
WHERE channel='syb' GROUP BY shop_id
|
||||
) a ON a.shop_id=s.shop_id
|
||||
SET s.enabled=IF(s.enabled=1 AND a.enabled=1,1,0)`); err != nil {
|
||||
return fmt.Errorf("收敛店铺启停状态失败: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(`DELETE FROM shop_channel_aliases`); err != nil {
|
||||
return fmt.Errorf("清理旧渠道店铺名称失败: %w", err)
|
||||
}
|
||||
for _, channel := range []string{"syb", "shopee"} {
|
||||
if _, err := tx.Exec(`INSERT INTO shop_channel_aliases
|
||||
(alias_id,shop_id,channel,alias_name,normalized_alias,enabled,created_at,updated_at)
|
||||
SELECT CONCAT('v19-',?,'-',SHA2(shop_id,256)),shop_id,?,display_name,normalized_name,enabled,created_at,updated_at
|
||||
FROM shops`, channel, channel); err != nil {
|
||||
return fmt.Errorf("生成 %s 店铺兼容数据失败: %w", channel, err)
|
||||
}
|
||||
}
|
||||
if _, err := tx.Exec(`UPDATE syb_orders SET shop_id=NULL`); err != nil {
|
||||
return fmt.Errorf("清理顺运宝旧店铺关联失败: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(`UPDATE syb_orders so JOIN shops s
|
||||
ON s.normalized_name=TRIM(so.shop_name) COLLATE utf8mb4_bin SET so.shop_id=s.shop_id`); err != nil {
|
||||
return fmt.Errorf("按唯一店铺名称重建顺运宝关联失败: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(`UPDATE shopee_products SET shop_id=NULL`); err != nil {
|
||||
return fmt.Errorf("清理蝦皮旧店铺关联失败: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(`UPDATE shopee_products sp JOIN shops s
|
||||
ON s.normalized_name=TRIM(sp.shopee_shop_name) COLLATE utf8mb4_bin SET sp.shop_id=s.shop_id`); err != nil {
|
||||
return fmt.Errorf("按唯一店铺名称重建蝦皮关联失败: %w", err)
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// migrateMySQLV14 增加顺运宝同步店铺准入表和审计统计。
|
||||
func migrateMySQLV14(db *sql.DB) error {
|
||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS syb_allowed_shops (
|
||||
@@ -1790,7 +1847,10 @@ func CheckMySQLSchema(db *sql.DB) error {
|
||||
if err := checkMySQLV17Shape(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkMySQLV18Shape(db)
|
||||
if err := checkMySQLV18Shape(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkMySQLV19Shape(db)
|
||||
}
|
||||
|
||||
func checkMySQLV15Shape(db *sql.DB) error {
|
||||
@@ -1869,6 +1929,22 @@ func checkMySQLV18Shape(db *sql.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkMySQLV19Shape(db *sql.DB) error {
|
||||
var invalid int
|
||||
err := db.QueryRow(`SELECT COUNT(*) FROM shops s WHERE
|
||||
(SELECT COUNT(*) FROM shop_channel_aliases a WHERE a.shop_id=s.shop_id AND a.channel='syb')<>1 OR
|
||||
(SELECT COUNT(*) FROM shop_channel_aliases a WHERE a.shop_id=s.shop_id AND a.channel='shopee')<>1 OR
|
||||
EXISTS (SELECT 1 FROM shop_channel_aliases a WHERE a.shop_id=s.shop_id AND
|
||||
(a.alias_name<>s.display_name OR a.normalized_alias<>s.normalized_name OR a.enabled<>s.enabled))`).Scan(&invalid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("检查单一店铺名称兼容数据失败: %w", err)
|
||||
}
|
||||
if invalid != 0 {
|
||||
return fmt.Errorf("有 %d 个店铺的渠道兼容数据与唯一店铺名称不一致", invalid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkMySQLV14Shape(db *sql.DB) error {
|
||||
if err := checkMySQLSchema(db, []string{"syb_allowed_shops"}); err != nil {
|
||||
return err
|
||||
|
||||
@@ -875,7 +875,7 @@ func TestMySQLMigrate_V14升级V17回填元数据规格并增加软删除(t *tes
|
||||
}
|
||||
}
|
||||
|
||||
func TestMySQLMigrate_V17升级V18迁移全局店铺并精确回填(t *testing.T) {
|
||||
func TestMySQLMigrate_V17升级V19迁移单一店铺名称并精确回填(t *testing.T) {
|
||||
db := openMySQLMigrationTestDB(t)
|
||||
defer db.Close()
|
||||
cleanMySQLTestSchema(t, db)
|
||||
@@ -891,26 +891,34 @@ func TestMySQLMigrate_V17升级V18迁移全局店铺并精确回填(t *testing.T
|
||||
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`)
|
||||
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)
|
||||
VALUES('V18-SHOP','精确店铺','精确店铺',1,'V18-USER',?,?),
|
||||
('V18-OFF','停用店铺','停用店铺',0,'V18-USER',?,?)`, now, now, 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)
|
||||
t.Fatalf("v17 升级 v19 失败: %v", err)
|
||||
}
|
||||
if err := MigrateMySQL(db); err != nil {
|
||||
t.Fatalf("v18 重放失败: %v", err)
|
||||
t.Fatalf("v19 重放失败: %v", err)
|
||||
}
|
||||
if err := checkMySQLV18Shape(db); err != nil {
|
||||
t.Fatalf("v18 结构错误: %v", err)
|
||||
}
|
||||
if err := checkMySQLV19Shape(db); err != nil {
|
||||
t.Fatalf("v19 单一店铺名称错误: %v", err)
|
||||
}
|
||||
var enabled int
|
||||
if err := db.QueryRow(`SELECT enabled FROM shops WHERE shop_id='V18-OFF'`).Scan(&enabled); err != nil || enabled != 0 {
|
||||
t.Fatalf("旧 SYB 停用状态不得在收敛时重新启用: enabled=%d err=%v", enabled, 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'`,
|
||||
|
||||
+18
-63
@@ -16,15 +16,11 @@ var (
|
||||
ErrShopHasReferences = errors.New("店铺仍有关联数据")
|
||||
)
|
||||
|
||||
// ListShops 返回管理页的一店一行汇总;首版每个渠道维护一个主别名。
|
||||
// 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 {
|
||||
@@ -32,34 +28,11 @@ func ListShops(q Execer) ([]model.Shop, error) {
|
||||
}
|
||||
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.ShopeeProductCount,
|
||||
&item.CreatedByUserID, &item.CreatedAt, &item.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -85,24 +58,9 @@ func GetShop(q Execer, shopID string) (model.Shop, bool, error) {
|
||||
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”。
|
||||
// ListEnabledSybShopMappings 返回“店铺精确名称 → 店铺 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`)
|
||||
rows, err := q.Query(`SELECT normalized_name,shop_id FROM shops WHERE enabled=1 ORDER BY normalized_name`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询启用的 SYB 店铺配置失败: %w", err)
|
||||
}
|
||||
@@ -118,19 +76,18 @@ func ListEnabledSybShopMappings(q Execer) (map[string]string, error) {
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func FindShopIDByAlias(q Execer, channel, rawName string) (string, error) {
|
||||
func FindShopIDByName(q Execer, 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)
|
||||
err := q.QueryRow(`SELECT shop_id FROM shops WHERE normalized_name=?`, name).Scan(&shopID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("按 %s 店铺名称查业务店铺失败: %w", channel, err)
|
||||
return "", fmt.Errorf("按店铺名称查询店铺失败: %w", err)
|
||||
}
|
||||
return shopID, nil
|
||||
}
|
||||
@@ -186,14 +143,12 @@ func SetShopEnabled(q Execer, shopID string, enabled bool, updatedAt string) (bo
|
||||
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)
|
||||
func SetShopCompatibilityAliasesEnabled(q Execer, shopID string, enabled bool, updatedAt string) error {
|
||||
_, err := q.Exec(`UPDATE shop_channel_aliases SET enabled=?,updated_at=? WHERE shop_id=?`, enabled, updatedAt, shopID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("更新 SYB 同步状态失败: %w", err)
|
||||
return fmt.Errorf("同步店铺兼容数据状态失败: %w", err)
|
||||
}
|
||||
n, err := result.RowsAffected()
|
||||
return n > 0, err
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReplaceShopAlias(q Execer, aliasID, shopID, channel, aliasName, updatedAt string, enabled bool) error {
|
||||
@@ -208,7 +163,7 @@ func ReplaceShopAlias(q Execer, aliasID, shopID, channel, aliasName, updatedAt s
|
||||
CreatedAt: updatedAt, UpdatedAt: updatedAt})
|
||||
}
|
||||
|
||||
// RebuildShopAssociations 按渠道原始名称精确重建一个店铺的派生关联。
|
||||
// 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
|
||||
@@ -216,14 +171,14 @@ func RebuildShopAssociations(q Execer, shopID string) error {
|
||||
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 {
|
||||
if _, err := q.Exec(`UPDATE syb_orders so JOIN shops s
|
||||
ON s.shop_id=? AND s.normalized_name=TRIM(so.shop_name) COLLATE utf8mb4_bin
|
||||
SET so.shop_id=s.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 {
|
||||
if _, err := q.Exec(`UPDATE shopee_products sp JOIN shops s
|
||||
ON s.shop_id=? AND s.normalized_name=TRIM(sp.shopee_shop_name) COLLATE utf8mb4_bin
|
||||
SET sp.shop_id=s.shop_id`, shopID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
+28
-46
@@ -111,16 +111,15 @@ func UpsertShopeeSKU(q Execer, skuID, goodsID, specRaw, color, size, advice stri
|
||||
// NULL 表示这个蝦皮商品还没填 PDD 链接(LEFT JOIN 没查到对应行)。
|
||||
type ShopeeProductRow struct {
|
||||
model.ShopeeProduct
|
||||
BusinessShopName string
|
||||
ColorCount int
|
||||
SizeCount int
|
||||
SKUCount int
|
||||
PendingCount int
|
||||
CollectStatus sql.NullString
|
||||
CollectMsg sql.NullString
|
||||
ColorCount int
|
||||
SizeCount int
|
||||
SKUCount int
|
||||
PendingCount int
|
||||
CollectStatus sql.NullString
|
||||
CollectMsg sql.NullString
|
||||
}
|
||||
|
||||
// ShopeeFilter 是蝦皮商品列表页支持的筛选条件,四项都可以为空。
|
||||
// ShopeeFilter 是蝦皮商品列表页支持的筛选和内部范围条件。
|
||||
//
|
||||
// Status 取值见 §「状态筛选的四个取值」(工单 #43):
|
||||
//
|
||||
@@ -132,16 +131,14 @@ type ShopeeProductRow struct {
|
||||
// 认不出的取值一律当作 ""(不筛选),由 service 层的 ParseShopeeStatus 兜底,
|
||||
// 这里不做校验——repository 只管拼 SQL。
|
||||
type ShopeeFilter struct {
|
||||
Keyword string
|
||||
ShopName string
|
||||
Status string
|
||||
Shop string // has / missing / 空(全部)
|
||||
Image string // has / missing / 空(全部)
|
||||
StoreID string // 业务店铺 ID / unlinked / 空(全部)
|
||||
Deleted bool // true 只看软删除数据;false 只看正常数据
|
||||
Keyword string
|
||||
SearchField string // goods_id / title / shop_name
|
||||
Status string
|
||||
Deleted bool // true 只看软删除数据;false 只看正常数据
|
||||
Unlinked bool // true 只看尚未登记到店铺管理的正常商品
|
||||
}
|
||||
|
||||
// shopeeFilterClause 把关键字、状态、店铺和图片筛选拼成 WHERE 子句,供 ListShopeeProducts
|
||||
// shopeeFilterClause 把分类关键词、状态和内部范围拼成 WHERE 子句,供 ListShopeeProducts
|
||||
// 和 CountShopeeProductsFiltered 共用——两处筛选逻辑必须完全一致,
|
||||
// 否则底部统计会跟表格对不上(#19 踩过一次,见工单 #43)。
|
||||
//
|
||||
@@ -156,13 +153,17 @@ func shopeeFilterClause(filter ShopeeFilter) (string, []any) {
|
||||
var args []any
|
||||
|
||||
if kw := strings.TrimSpace(filter.Keyword); kw != "" {
|
||||
like := "%" + escapeLike(kw) + "%"
|
||||
clauses = append(clauses, `(sp.goods_id LIKE ? ESCAPE '!' OR sp.title LIKE ? ESCAPE '!')`)
|
||||
args = append(args, like, like)
|
||||
}
|
||||
if shopName := strings.TrimSpace(filter.ShopName); shopName != "" {
|
||||
clauses = append(clauses, `sp.shopee_shop_name LIKE ? ESCAPE '!'`)
|
||||
args = append(args, "%"+escapeLike(shopName)+"%")
|
||||
switch filter.SearchField {
|
||||
case "title":
|
||||
clauses = append(clauses, `sp.title LIKE ? ESCAPE '!'`)
|
||||
args = append(args, "%"+escapeLike(kw)+"%")
|
||||
case "shop_name":
|
||||
clauses = append(clauses, `sp.shopee_shop_name LIKE ? ESCAPE '!'`)
|
||||
args = append(args, "%"+escapeLike(kw)+"%")
|
||||
default:
|
||||
clauses = append(clauses, `sp.goods_id = ?`)
|
||||
args = append(args, kw)
|
||||
}
|
||||
}
|
||||
|
||||
switch filter.Status {
|
||||
@@ -175,26 +176,8 @@ func shopeeFilterClause(filter ShopeeFilter) (string, []any) {
|
||||
clauses = append(clauses, `(sp.pdd_goods_url IS NOT NULL AND sp.pdd_goods_url <> '')`)
|
||||
}
|
||||
|
||||
switch filter.Shop {
|
||||
case "has":
|
||||
clauses = append(clauses, `(sp.shopee_shop_name IS NOT NULL AND TRIM(sp.shopee_shop_name) <> '')`)
|
||||
case "missing":
|
||||
clauses = append(clauses, `(sp.shopee_shop_name IS NULL OR TRIM(sp.shopee_shop_name) = '')`)
|
||||
}
|
||||
|
||||
switch filter.Image {
|
||||
case "has":
|
||||
clauses = append(clauses, `(sp.image_url IS NOT NULL AND TRIM(sp.image_url) <> '')`)
|
||||
case "missing":
|
||||
clauses = append(clauses, `(sp.image_url IS NULL OR TRIM(sp.image_url) = '')`)
|
||||
}
|
||||
switch strings.TrimSpace(filter.StoreID) {
|
||||
case "":
|
||||
case "unlinked":
|
||||
if filter.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
|
||||
@@ -202,7 +185,7 @@ func shopeeFilterClause(filter ShopeeFilter) (string, []any) {
|
||||
|
||||
// ListShopeeProducts 按筛选条件分页查蝦皮商品列表(商品级一行),联查规格聚合数和采集状态。
|
||||
//
|
||||
// keyword 匹配商品 ID 或商品名称,**不匹配颜色/尺码**——
|
||||
// keyword 只匹配 SearchField 指定的商品 ID、商品名称或店铺名,**不匹配颜色/尺码**——
|
||||
// 那是 SKU 级信息,商品级列表里搜出来没法定位到具体是哪个 SKU(见 #41)。
|
||||
//
|
||||
// `[必须]` 分页用 LIMIT/OFFSET 在数据库里做,不把全量查出来在 Go 里切片
|
||||
@@ -210,7 +193,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.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,
|
||||
SELECT sp.goods_id,sp.shop_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,
|
||||
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,
|
||||
@@ -218,7 +201,6 @@ 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` +
|
||||
@@ -237,7 +219,7 @@ func ListShopeeProducts(q Execer, filter ShopeeFilter, limit, offset int) ([]Sho
|
||||
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, &shopID, &r.BusinessShopName, &title, &shopeeStatus, &mainSKUCode, &imageURL, &shopName, &imageSource, &imageObserved, &imageManual, &shopSource, &shopObserved, &shopManual, &source,
|
||||
&r.GoodsID, &shopID, &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,
|
||||
|
||||
@@ -244,9 +244,9 @@ func UpsertSybOrder(q Execer, o model.SybOrder) (created bool, err error) {
|
||||
return false, fmt.Errorf("syb_id 不能为空")
|
||||
}
|
||||
if strings.TrimSpace(o.ShopID) == "" && strings.TrimSpace(o.ShopName) != "" {
|
||||
o.ShopID, err = FindShopIDByAlias(q, "syb", o.ShopName)
|
||||
o.ShopID, err = FindShopIDByName(q, o.ShopName)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("解析顺运宝明细 %s 的业务店铺失败: %w", o.SybID, err)
|
||||
return false, fmt.Errorf("解析顺运宝明细 %s 的店铺失败: %w", o.SybID, err)
|
||||
}
|
||||
}
|
||||
var specKey any
|
||||
@@ -411,7 +411,7 @@ func backfillSybProductMetadata(db *sql.DB) error {
|
||||
return fmt.Errorf("读取历史顺运宝图片失败: %w", err)
|
||||
}
|
||||
for goodsID, item := range items {
|
||||
shopID, err := FindShopIDByAlias(db, "syb", item.shopName)
|
||||
shopID, err := FindShopIDByName(db, item.shopName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解析历史顺运宝店铺失败: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user