feat: 收敛店铺管理和蝦皮分类搜索 (#212 #213)

This commit is contained in:
chengma
2026-08-13 11:47:56 +08:00
parent 1abed648a5
commit 313e9f9151
22 changed files with 389 additions and 464 deletions
+28 -46
View File
@@ -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,