From 2f76ff10cc5b2a12fa1dc47599103def55462cde Mon Sep 17 00:00:00 2001 From: chengma Date: Mon, 24 Aug 2026 16:01:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E6=8C=89PDD=E5=95=86?= =?UTF-8?q?=E5=93=81ID=E5=8F=8D=E6=9F=A5=E8=9D=A6=E7=9A=AE=E5=95=86?= =?UTF-8?q?=E5=93=81=20(#297)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/handler/web/pagination_test.go | 13 +++++++++++-- admin/repository/shopee.go | 8 ++++++-- admin/service/shopee_list.go | 5 +++-- admin/service/shopee_list_test.go | 21 ++++++++++++++------- docs/admin/05-ui-specification.md | 9 +++++---- 5 files changed, 39 insertions(+), 17 deletions(-) diff --git a/admin/handler/web/pagination_test.go b/admin/handler/web/pagination_test.go index c1e7364..1e5e0f9 100644 --- a/admin/handler/web/pagination_test.go +++ b/admin/handler/web/pagination_test.go @@ -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) diff --git a/admin/repository/shopee.go b/admin/repository/shopee.go index 0e03bbe..9d0a68b 100644 --- a/admin/repository/shopee.go +++ b/admin/repository/shopee.go @@ -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 里切片 diff --git a/admin/service/shopee_list.go b/admin/service/shopee_list.go index ea5527a..d849682 100644 --- a/admin/service/shopee_list.go +++ b/admin/service/shopee_list.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" diff --git a/admin/service/shopee_list_test.go b/admin/service/shopee_list_test.go index 3a89d1f..9aff1a7 100644 --- a/admin/service/shopee_list_test.go +++ b/admin/service/shopee_list_test.go @@ -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) + } } diff --git a/docs/admin/05-ui-specification.md b/docs/admin/05-ui-specification.md index 3bf3899..18d35ea 100644 --- a/docs/admin/05-ui-specification.md +++ b/docs/admin/05-ui-specification.md @@ -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):不在常用筛选中增加额外下拉框。管理员从店铺管理页的