feat: 支持按PDD商品ID反查蝦皮商品 (#297)
This commit is contained in:
@@ -50,6 +50,15 @@ func Test列表写操作跳转保留筛选和页码(t *testing.T) {
|
||||
"q": "店铺 A", "search_field": "shop_name", "status": "no_link", "unlinked": "1", "page": "2", "page_size": "50", "msg": "完成",
|
||||
})
|
||||
})
|
||||
t.Run("蝦皮PDD商品ID", func(t *testing.T) {
|
||||
context, recorder := listPostContext(t, "/shopee/save", url.Values{
|
||||
"q": {"PDD-123"}, "search_field": {"pdd_goods_id"}, "status": {"has_link"}, "page": {"3"}, "page_size": {"100"},
|
||||
})
|
||||
(&Handler{}).shopeeRedirect(context, "完成")
|
||||
assertRedirectQuery(t, recorder, "/shopee", map[string]string{
|
||||
"q": "PDD-123", "search_field": "pdd_goods_id", "status": "has_link", "page": "3", "page_size": "100", "msg": "完成",
|
||||
})
|
||||
})
|
||||
t.Run("顺运宝", func(t *testing.T) {
|
||||
context, recorder := listPostContext(t, "/syb/associate-pdd", url.Values{
|
||||
"order_no": {"SYB-1"}, "shop": {"测试店铺"}, "stage": {"pdd_missing"}, "page": {"3"}, "page_size": {"100"},
|
||||
@@ -106,14 +115,14 @@ func Test列表写操作跳转归一化非法每页条数(t *testing.T) {
|
||||
|
||||
func Test蝦皮分页详情和状态条保留分类搜索(t *testing.T) {
|
||||
values := url.Values{
|
||||
"q": {"店铺 A"}, "search_field": {"shop_name"}, "status": {"has_link"}, "unlinked": {"1"},
|
||||
"q": {"PDD-123"}, "search_field": {"pdd_goods_id"}, "status": {"has_link"}, "unlinked": {"1"},
|
||||
}
|
||||
detail, err := url.Parse("/shopee/detail?" + detailValuesForShopee(values, 3))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for key, want := range map[string]string{
|
||||
"q": "店铺 A", "search_field": "shop_name", "status": "has_link", "unlinked": "1", "page": "3",
|
||||
"q": "PDD-123", "search_field": "pdd_goods_id", "status": "has_link", "unlinked": "1", "page": "3",
|
||||
} {
|
||||
if got := detail.Query().Get(key); got != want {
|
||||
t.Errorf("详情参数 %s=%q,期望 %q", key, got, want)
|
||||
|
||||
@@ -132,7 +132,7 @@ type ShopeeProductRow struct {
|
||||
// 这里不做校验——repository 只管拼 SQL。
|
||||
type ShopeeFilter struct {
|
||||
Keyword string
|
||||
SearchField string // goods_id / title / shop_name
|
||||
SearchField string // goods_id / pdd_goods_id / title / shop_name
|
||||
Status string
|
||||
Deleted bool // true 只看软删除数据;false 只看正常数据
|
||||
Unlinked bool // true 只看尚未登记到店铺管理的正常商品
|
||||
@@ -154,6 +154,9 @@ func shopeeFilterClause(filter ShopeeFilter) (string, []any) {
|
||||
|
||||
if kw := strings.TrimSpace(filter.Keyword); kw != "" {
|
||||
switch filter.SearchField {
|
||||
case "pdd_goods_id":
|
||||
clauses = append(clauses, `sp.pdd_goods_id = ?`)
|
||||
args = append(args, kw)
|
||||
case "title":
|
||||
clauses = append(clauses, `sp.title LIKE ? ESCAPE '!'`)
|
||||
args = append(args, "%"+escapeLike(kw)+"%")
|
||||
@@ -185,7 +188,8 @@ func shopeeFilterClause(filter ShopeeFilter) (string, []any) {
|
||||
|
||||
// ListShopeeProducts 按筛选条件分页查蝦皮商品列表(商品级一行),联查规格聚合数和采集状态。
|
||||
//
|
||||
// keyword 只匹配 SearchField 指定的商品 ID、商品名称或店铺名,**不匹配颜色/尺码**——
|
||||
// keyword 只匹配 SearchField 指定的蝦皮商品 ID、PDD 商品 ID、商品名称或店铺名,
|
||||
// **不匹配颜色/尺码**——
|
||||
// 那是 SKU 级信息,商品级列表里搜出来没法定位到具体是哪个 SKU(见 #41)。
|
||||
//
|
||||
// `[必须]` 分页用 LIMIT/OFFSET 在数据库里做,不把全量查出来在 Go 里切片
|
||||
|
||||
@@ -198,10 +198,11 @@ func ShopeeStatusLabel(status string) string {
|
||||
return shopeeStatusTexts[status]
|
||||
}
|
||||
|
||||
// ShopeeSearchFieldOptions 返回关键词可以查询的三个字段。
|
||||
// ShopeeSearchFieldOptions 返回关键词可以查询的四个字段。
|
||||
func ShopeeSearchFieldOptions() []ShopeeStatusOption {
|
||||
return []ShopeeStatusOption{
|
||||
{"goods_id", "商品 ID"},
|
||||
{"pdd_goods_id", "PDD 商品 ID"},
|
||||
{"title", "商品名称"},
|
||||
{"shop_name", "店铺名"},
|
||||
}
|
||||
@@ -210,7 +211,7 @@ func ShopeeSearchFieldOptions() []ShopeeStatusOption {
|
||||
// ParseShopeeSearchField 使用固定白名单,避免把请求参数当成 SQL 列名。
|
||||
func ParseShopeeSearchField(s string) string {
|
||||
value := strings.TrimSpace(s)
|
||||
if value == "title" || value == "shop_name" {
|
||||
if value == "pdd_goods_id" || value == "title" || value == "shop_name" {
|
||||
return value
|
||||
}
|
||||
return "goods_id"
|
||||
|
||||
@@ -484,15 +484,16 @@ func TestParseShopeeStatus_认不出来当全部(t *testing.T) {
|
||||
func TestListShopeeProducts_分类搜索(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
for _, product := range []struct {
|
||||
id, title, shop, image string
|
||||
id, title, shop, image, pddGoodsID string
|
||||
}{
|
||||
{"A", "店铺有图", "店铺 A", "https://example.invalid/a.jpg"},
|
||||
{"B", "店铺无图", "店铺 B", ""},
|
||||
{"C", "无店有图", "", "https://example.invalid/c.jpg"},
|
||||
{"D", "资料为空白", " ", " "},
|
||||
{"A", "店铺有图", "店铺 A", "https://example.invalid/a.jpg", "PDD-1"},
|
||||
{"B", "店铺无图", "店铺 B", "", "PDD-1"},
|
||||
{"C", "无店有图", "", "https://example.invalid/c.jpg", "PDD-10"},
|
||||
{"D", "资料为空白", " ", " ", ""},
|
||||
} {
|
||||
seedShopeeProduct(t, db, product.id, product.title)
|
||||
if _, err := db.Exec(`UPDATE shopee_products SET shopee_shop_name=?,image_url=? WHERE goods_id=?`, product.shop, product.image, product.id); err != nil {
|
||||
if _, err := db.Exec(`UPDATE shopee_products SET shopee_shop_name=?,image_url=?,pdd_goods_id=? WHERE goods_id=?`,
|
||||
product.shop, product.image, product.pddGoodsID, product.id); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -503,6 +504,7 @@ func TestListShopeeProducts_分类搜索(t *testing.T) {
|
||||
want []string
|
||||
}{
|
||||
{"商品ID精确", repository.ShopeeFilter{Keyword: "A", SearchField: "goods_id"}, []string{"A"}},
|
||||
{"PDD商品ID精确且允许多条关联", repository.ShopeeFilter{Keyword: "PDD-1", SearchField: "pdd_goods_id"}, []string{"A", "B"}},
|
||||
{"商品名称包含", repository.ShopeeFilter{Keyword: "店铺有图", SearchField: "title"}, []string{"A"}},
|
||||
{"店铺名称包含", repository.ShopeeFilter{Keyword: "店铺", SearchField: "shop_name"}, []string{"A", "B"}},
|
||||
}
|
||||
@@ -578,7 +580,7 @@ func TestListShopeeProducts_未登记入口只显示未关联店铺(t *testing.T
|
||||
}
|
||||
|
||||
func TestParseShopeeSearchField_固定白名单(t *testing.T) {
|
||||
for _, value := range []string{"goods_id", "title", "shop_name"} {
|
||||
for _, value := range []string{"goods_id", "pdd_goods_id", "title", "shop_name"} {
|
||||
if got := ParseShopeeSearchField(value); got != value {
|
||||
t.Errorf("ParseShopeeSearchField(%q)=%q", value, got)
|
||||
}
|
||||
@@ -588,4 +590,9 @@ func TestParseShopeeSearchField_固定白名单(t *testing.T) {
|
||||
t.Errorf("ParseShopeeSearchField(%q)=%q,期望 goods_id", value, got)
|
||||
}
|
||||
}
|
||||
|
||||
options := ShopeeSearchFieldOptions()
|
||||
if len(options) != 4 || options[1].Value != "pdd_goods_id" || options[1].Text != "PDD 商品 ID" {
|
||||
t.Fatalf("搜索分类选项=%+v,期望包含 PDD 商品 ID", options)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,10 +180,11 @@
|
||||
- `[必须]` 筛选参数认不出来的一律当"全部",不报错——地址栏是用户可以
|
||||
随便改的。
|
||||
- `[必须]` 筛选与关键词搜索可以叠加,翻页时都要保留,见 §3.2。
|
||||
- **分类搜索**(工单 #213):分类固定为商品 ID、商品名称、店铺名。商品 ID 使用精确
|
||||
匹配,商品名称和店铺名使用转义后的包含匹配;后端必须用白名单分支,不能把请求参数
|
||||
拼成 SQL 列名。未知分类回退到商品 ID。状态、分类和关键词在翻页、详情弹窗和写操作
|
||||
返回时都要保留。
|
||||
- **分类搜索**(工单 #213、#297):分类固定为商品 ID、PDD 商品 ID、商品名称、店铺名。
|
||||
商品 ID 和 PDD 商品 ID 使用精确匹配,商品名称和店铺名使用转义后的包含匹配;同一个
|
||||
PDD 商品 ID 可以返回多条关联蝦皮商品。后端必须用白名单分支,不能把请求参数拼成 SQL
|
||||
列名。未知分类回退到商品 ID。状态、分类和关键词在翻页、详情弹窗和写操作返回时都要
|
||||
保留。
|
||||
- **回收站**(工单 #213):不再显示“数据范围”筛选。管理员通过独立回收站按钮查看
|
||||
软删除商品,并可返回正常商品;已删除列表不允许创建采集任务或打开处理详情。
|
||||
- **未登记商品**(工单 #212):不在常用筛选中增加额外下拉框。管理员从店铺管理页的
|
||||
|
||||
Reference in New Issue
Block a user