592 lines
22 KiB
Go
592 lines
22 KiB
Go
package service
|
||
|
||
import (
|
||
"database/sql"
|
||
"fmt"
|
||
"testing"
|
||
|
||
"cmautobuy/admin/repository"
|
||
)
|
||
|
||
// seedShopeeProduct 建一个蝦皮商品,goods_id / title 之外不填别的。
|
||
func seedShopeeProduct(t *testing.T, db *sql.DB, goodsID, title string) {
|
||
t.Helper()
|
||
if err := repository.UpsertShopeeProduct(db, goodsID, title, "正常", "货号"+goodsID); err != nil {
|
||
t.Fatalf("建蝦皮商品 %s 失败: %v", goodsID, err)
|
||
}
|
||
}
|
||
|
||
// seedShopeeSKU 建一条蝦皮 SKU。specRaw 只在 parseOK=false 时用于校验回显。
|
||
func seedShopeeSKU(t *testing.T, db *sql.DB, skuID, goodsID, specRaw, color, size, advice string, parseOK bool) {
|
||
t.Helper()
|
||
if err := repository.UpsertShopeeSKU(db, skuID, goodsID, specRaw, color, size, advice, parseOK, "sku-"+skuID); err != nil {
|
||
t.Fatalf("建蝦皮 SKU %s 失败: %v", skuID, err)
|
||
}
|
||
}
|
||
|
||
// setShopeePddLink 直接写 shopee_products.pdd_goods_url / pdd_goods_id,
|
||
// 模拟"已经填过 PDD 链接"。ShopeeSave 本工单不实现,测试直接写库代替。
|
||
func setShopeePddLink(t *testing.T, db *sql.DB, goodsID, pddGoodsID, pddURL string) {
|
||
t.Helper()
|
||
_, err := db.Exec(
|
||
`UPDATE shopee_products SET pdd_goods_id = ?, pdd_goods_url = ? WHERE goods_id = ?`,
|
||
pddGoodsID, pddURL, goodsID)
|
||
if err != nil {
|
||
t.Fatalf("写蝦皮商品 %s 的 PDD 链接失败: %v", goodsID, err)
|
||
}
|
||
}
|
||
|
||
func TestGetShopeeProductDetail_汇总顺运宝观测且唯一匹配正式SKU(t *testing.T) {
|
||
db := newTestDB(t)
|
||
seedShopeeProduct(t, db, "S-OBS", "观测商品")
|
||
seedShopeeSKU(t, db, "SKU-OBS", "S-OBS", "黑色,M", "黑色", "M", "", true)
|
||
now1, now2 := "2026-08-10T00:00:00.000000000Z", "2026-08-11T00:00:00.000000000Z"
|
||
for _, row := range []struct {
|
||
id string
|
||
qty int
|
||
price int64
|
||
image, at string
|
||
}{
|
||
{"OBS-1", 2, 23900, "https://example.invalid/old.jpg", now1},
|
||
{"OBS-2", 3, 25900, "https://example.invalid/new.jpg", now2},
|
||
} {
|
||
_, err := db.Exec(`INSERT INTO syb_orders(syb_id,order_no,title,product_spec,spec_key,shopee_goods_id,quantity,price_twd_cent,image_url,syb_data,created_at,updated_at)
|
||
VALUES(?,?,?,?,?,?,?,?,?,'{}',?,?)`, row.id, "ORDER-"+row.id, "观测商品", "黑色,M", "黑色,M", "S-OBS", row.qty, row.price, row.image, row.at, row.at)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
detail, err := GetShopeeProductDetail(db, "S-OBS")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(detail.SybObservations) != 1 {
|
||
t.Fatalf("观测规格数=%d", len(detail.SybObservations))
|
||
}
|
||
got := detail.SybObservations[0]
|
||
if got.OrderCount != 2 || got.TotalQuantity != 5 || got.PriceText != "NT$259.00" || got.ImageURL != "https://example.invalid/new.jpg" {
|
||
t.Fatalf("汇总错误:%+v", got)
|
||
}
|
||
if got.MatchedSKUID != "SKU-OBS" || got.MatchText != "唯一匹配" {
|
||
t.Fatalf("正式 SKU 匹配错误:%+v", got)
|
||
}
|
||
// 同一 syb_id 只是更新,不会产生第三个观测订单。
|
||
if _, err := db.Exec(`UPDATE syb_orders SET quantity=4 WHERE syb_id='OBS-2'`); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
detail, _ = GetShopeeProductDetail(db, "S-OBS")
|
||
if detail.SybObservations[0].OrderCount != 2 || detail.SybObservations[0].TotalQuantity != 6 {
|
||
t.Fatalf("重放后统计错误:%+v", detail.SybObservations[0])
|
||
}
|
||
}
|
||
|
||
func TestGetShopeeProductDetail_多条正式SKU不猜测(t *testing.T) {
|
||
db := newTestDB(t)
|
||
seedShopeeProduct(t, db, "S-MULTI", "多匹配")
|
||
seedShopeeSKU(t, db, "SKU-1", "S-MULTI", "黑色,M", "黑色", "M", "", true)
|
||
seedShopeeSKU(t, db, "SKU-2", "S-MULTI", "黑色,M", "黑色", "M", "", true)
|
||
now := "2026-08-11T00:00:00.000000000Z"
|
||
_, err := db.Exec(`INSERT INTO syb_orders(syb_id,order_no,title,product_spec,spec_key,shopee_goods_id,quantity,price_twd_cent,syb_data,created_at,updated_at) VALUES('O-1','ORDER','多匹配','黑色,M','黑色,M','S-MULTI',1,100,'{}',?,?)`, now, now)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
detail, err := GetShopeeProductDetail(db, "S-MULTI")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got := detail.SybObservations[0]
|
||
if got.MatchedSKUID != "" || got.MatchText != "多条匹配,待人工确认" {
|
||
t.Fatalf("不应猜测:%+v", got)
|
||
}
|
||
}
|
||
|
||
// ── 列表:商品级聚合 ──────────────────────────────────
|
||
|
||
func TestListShopeeProducts_商品级一行(t *testing.T) {
|
||
db := newTestDB(t)
|
||
|
||
// 商品 24420648774 在真实样本里有 62 个 SKU,这里用 3 个模拟同样的
|
||
// "一个商品多个 SKU 但只应该出现一行"的场景,见工单 #41。
|
||
seedShopeeProduct(t, db, "24420648774", "吊帶背心")
|
||
seedShopeeSKU(t, db, "sku-1", "24420648774", "", "黑色", "M", "40-50公斤", true)
|
||
seedShopeeSKU(t, db, "sku-2", "24420648774", "", "黑色", "L", "50-60公斤", true)
|
||
seedShopeeSKU(t, db, "sku-3", "24420648774", "", "白色", "M", "40-50公斤", true)
|
||
|
||
result, err := ListShopeeProducts(db, repository.ShopeeFilter{}, 1)
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if len(result.Rows) != 1 {
|
||
t.Fatalf("行数 = %d,想要 1(商品级一行,不是 SKU 级)", len(result.Rows))
|
||
}
|
||
row := result.Rows[0]
|
||
if row.SKUCount != 3 {
|
||
t.Errorf("SKUCount = %d,想要 3", row.SKUCount)
|
||
}
|
||
if row.ColorCount != 2 {
|
||
t.Errorf("ColorCount = %d,想要 2(黑/白)", row.ColorCount)
|
||
}
|
||
if row.SizeCount != 2 {
|
||
t.Errorf("SizeCount = %d,想要 2(M/L)", row.SizeCount)
|
||
}
|
||
if row.PendingCount != 0 {
|
||
t.Errorf("PendingCount = %d,想要 0", row.PendingCount)
|
||
}
|
||
}
|
||
|
||
func TestListShopeeProducts_颜色尺码只统计parse_ok(t *testing.T) {
|
||
db := newTestDB(t)
|
||
|
||
// 模拟工单里 26886533818 的情形:颜色数 × 尺码数会大于实际 SKU 数,
|
||
// 而且有一部分 SKU 解析失败——它们不能被算进颜色/尺码的 DISTINCT。
|
||
seedShopeeProduct(t, db, "26886533818", "測試商品")
|
||
seedShopeeSKU(t, db, "s1", "26886533818", "", "咖啡色", "2XL", "", true)
|
||
seedShopeeSKU(t, db, "s2", "26886533818", "", "咖啡色", "3XL", "", true)
|
||
seedShopeeSKU(t, db, "s3", "26886533818", "", "紅色", "M", "", true)
|
||
// 解析失败的两条:spec_raw 保留,color/size 为空,不该被 DISTINCT 算进去。
|
||
seedShopeeSKU(t, db, "s4", "26886533818", "紅色,3XL建議80-90公斤】", "", "", "", false)
|
||
seedShopeeSKU(t, db, "s5", "26886533818", "粉色,3XL【寬鬆版 82.5-92.5kg", "", "", "", false)
|
||
|
||
result, err := ListShopeeProducts(db, repository.ShopeeFilter{}, 1)
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if len(result.Rows) != 1 {
|
||
t.Fatalf("行数 = %d,想要 1", len(result.Rows))
|
||
}
|
||
row := result.Rows[0]
|
||
if row.SKUCount != 5 {
|
||
t.Errorf("SKUCount = %d,想要 5(含 2 条解析失败的)", row.SKUCount)
|
||
}
|
||
if row.ColorCount != 2 {
|
||
t.Errorf("ColorCount = %d,想要 2(咖啡色/紅色,不含解析失败的空颜色)", row.ColorCount)
|
||
}
|
||
if row.SizeCount != 3 {
|
||
t.Errorf("SizeCount = %d,想要 3(2XL/3XL/M,不含解析失败的空尺码)", row.SizeCount)
|
||
}
|
||
if row.PendingCount != 2 {
|
||
t.Errorf("PendingCount = %d,想要 2", row.PendingCount)
|
||
}
|
||
}
|
||
|
||
func TestListShopeeProducts_待补大于0整行标黄(t *testing.T) {
|
||
db := newTestDB(t)
|
||
seedShopeeProduct(t, db, "1001", "商品A")
|
||
seedShopeeSKU(t, db, "s1", "1001", "坏数据", "", "", "", false)
|
||
|
||
result, err := ListShopeeProducts(db, repository.ShopeeFilter{}, 1)
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if result.Rows[0].PendingCount != 1 {
|
||
t.Fatalf("PendingCount = %d,想要 1", result.Rows[0].PendingCount)
|
||
}
|
||
// 整行标黄由模板按 PendingCount > 0 判断,这里只验证数据源正确。
|
||
}
|
||
|
||
func TestListShopeeProducts_PDD链接未填写(t *testing.T) {
|
||
db := newTestDB(t)
|
||
seedShopeeProduct(t, db, "1001", "商品A")
|
||
|
||
result, err := ListShopeeProducts(db, repository.ShopeeFilter{}, 1)
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
row := result.Rows[0]
|
||
if !row.PddMissing || row.PddGoodsID != "" || row.PddURL != "" {
|
||
t.Errorf("未关联 PDD 视图错误:%+v", row)
|
||
}
|
||
if row.StatusText != "未填链接" {
|
||
t.Errorf("StatusText = %q,想要 \"未填链接\"", row.StatusText)
|
||
}
|
||
}
|
||
|
||
func TestListShopeeProducts_采集状态联查(t *testing.T) {
|
||
db := newTestDB(t)
|
||
seedShopeeProduct(t, db, "1001", "商品A")
|
||
|
||
if _, err := repository.EnsurePddProduct(db, "9001", "https://mobile.yangkeduo.com/goods.html?goods_id=9001"); err != nil {
|
||
t.Fatalf("建 PDD 商品失败: %v", err)
|
||
}
|
||
if err := repository.SetCollectResult(db, "9001", "PDD标题", "店铺", "{}"); err != nil {
|
||
t.Fatalf("设置采集结果失败: %v", err)
|
||
}
|
||
setShopeePddLink(t, db, "1001", "9001", "https://mobile.yangkeduo.com/goods.html?goods_id=9001")
|
||
|
||
result, err := ListShopeeProducts(db, repository.ShopeeFilter{}, 1)
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
row := result.Rows[0]
|
||
if row.PddMissing {
|
||
t.Error("已填链接,PddMissing 应为 false")
|
||
}
|
||
if row.PddGoodsID != "9001" || row.PddURL != "https://mobile.yangkeduo.com/goods.html?goods_id=9001" {
|
||
t.Errorf("PDD 商品 ID/完整链接错误:%q/%q", row.PddGoodsID, row.PddURL)
|
||
}
|
||
if row.StatusText != "已采集" {
|
||
t.Errorf("StatusText = %q,想要 \"已采集\"", row.StatusText)
|
||
}
|
||
}
|
||
|
||
func TestListShopeeProducts_关键字匹配商品ID和名称_不匹配颜色尺码(t *testing.T) {
|
||
db := newTestDB(t)
|
||
seedShopeeProduct(t, db, "1001", "吊帶背心")
|
||
seedShopeeProduct(t, db, "1002", "短袖T恤")
|
||
seedShopeeSKU(t, db, "s1", "1001", "", "黑色", "M", "", true)
|
||
|
||
// 按商品 ID 匹配
|
||
result, err := ListShopeeProducts(db, repository.ShopeeFilter{Keyword: "1001"}, 1)
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if len(result.Rows) != 1 || result.Rows[0].GoodsID != "1001" {
|
||
t.Fatalf("按商品 ID 搜索结果不对: %+v", result.Rows)
|
||
}
|
||
|
||
// 按商品名称匹配
|
||
result, err = ListShopeeProducts(db, repository.ShopeeFilter{Keyword: "背心", SearchField: "title"}, 1)
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if len(result.Rows) != 1 || result.Rows[0].GoodsID != "1001" {
|
||
t.Fatalf("按商品名称搜索结果不对: %+v", result.Rows)
|
||
}
|
||
|
||
// 颜色/尺码不参与匹配:搜"黑色"应该搜不到任何商品。
|
||
result, err = ListShopeeProducts(db, repository.ShopeeFilter{Keyword: "黑色", SearchField: "title"}, 1)
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if len(result.Rows) != 0 {
|
||
t.Fatalf("关键字不应匹配颜色,得到 %+v", result.Rows)
|
||
}
|
||
}
|
||
|
||
// ── 弹窗详情 ──────────────────────────────────
|
||
|
||
func TestGetShopeeProductDetail_待补行显示原文(t *testing.T) {
|
||
db := newTestDB(t)
|
||
seedShopeeProduct(t, db, "1001", "商品A")
|
||
seedShopeeSKU(t, db, "s1", "1001", "", "黑色", "M", "40-50公斤", true)
|
||
seedShopeeSKU(t, db, "s2", "1001", "紅色,3XL建議80-90公斤】", "", "", "", false)
|
||
|
||
detail, err := GetShopeeProductDetail(db, "1001")
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if detail == nil {
|
||
t.Fatal("详情为 nil")
|
||
}
|
||
if len(detail.SKUs) != 2 {
|
||
t.Fatalf("SKUs 数量 = %d,想要 2(含解析失败的)", len(detail.SKUs))
|
||
}
|
||
if detail.PendingCount != 1 {
|
||
t.Errorf("PendingCount = %d,想要 1", detail.PendingCount)
|
||
}
|
||
|
||
var pending *ShopeeSpecView
|
||
for i := range detail.SKUs {
|
||
if detail.SKUs[i].Pending {
|
||
pending = &detail.SKUs[i]
|
||
}
|
||
}
|
||
if pending == nil {
|
||
t.Fatal("没有找到待补的那一行")
|
||
}
|
||
if pending.SpecRaw != "紅色,3XL建議80-90公斤】" {
|
||
t.Errorf("SpecRaw = %q,想要显示原文", pending.SpecRaw)
|
||
}
|
||
if pending.RecordID != "s2" || pending.Color != "" || pending.Size != "" {
|
||
t.Errorf("待补规格的编辑值或内部定位错误:%+v", pending)
|
||
}
|
||
if pending.StatusText != "待补" {
|
||
t.Errorf("StatusText = %q,想要 \"待补\"", pending.StatusText)
|
||
}
|
||
|
||
for _, s := range detail.SKUs {
|
||
if !s.Pending && s.StatusText != "✓" {
|
||
t.Errorf("解析成功的行 StatusText = %q,想要 \"✓\"", s.StatusText)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestGetShopeeProductDetail_商品不存在返回nil(t *testing.T) {
|
||
db := newTestDB(t)
|
||
detail, err := GetShopeeProductDetail(db, "not-exist")
|
||
if err != nil {
|
||
t.Fatalf("不应该报错: %v", err)
|
||
}
|
||
if detail != nil {
|
||
t.Fatalf("不存在的商品应该返回 nil,得到 %+v", detail)
|
||
}
|
||
}
|
||
|
||
// ── 分页(工单 #43) ──────────────────────────────────
|
||
|
||
func seedShopeeProducts(t *testing.T, db *sql.DB, n int) {
|
||
t.Helper()
|
||
for i := 0; i < n; i++ {
|
||
seedShopeeProduct(t, db, fmt.Sprintf("g%04d", i), fmt.Sprintf("商品%d", i))
|
||
}
|
||
}
|
||
|
||
func TestListShopeeProducts_每页固定20条(t *testing.T) {
|
||
db := newTestDB(t)
|
||
seedShopeeProducts(t, db, 25)
|
||
|
||
result, err := ListShopeeProducts(db, repository.ShopeeFilter{}, 1)
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if len(result.Rows) != 20 {
|
||
t.Errorf("第 1 页行数 = %d,想要 20", len(result.Rows))
|
||
}
|
||
if result.Total != 25 {
|
||
t.Errorf("Total = %d,想要 25(全量,不是本页数)", result.Total)
|
||
}
|
||
if result.TotalPages != 2 {
|
||
t.Errorf("TotalPages = %d,想要 2", result.TotalPages)
|
||
}
|
||
|
||
result2, err := ListShopeeProducts(db, repository.ShopeeFilter{}, 2)
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if len(result2.Rows) != 5 {
|
||
t.Errorf("第 2 页行数 = %d,想要 5(25 条剩下的)", len(result2.Rows))
|
||
}
|
||
}
|
||
|
||
func TestListShopeeProducts_page越界兜到最后一页(t *testing.T) {
|
||
db := newTestDB(t)
|
||
seedShopeeProducts(t, db, 25)
|
||
|
||
result, err := ListShopeeProducts(db, repository.ShopeeFilter{}, 9999)
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if result.Page != 2 {
|
||
t.Errorf("Page = %d,想要兜到最后一页 2", result.Page)
|
||
}
|
||
if len(result.Rows) != 5 {
|
||
t.Errorf("越界后应显示最后一页的数据,行数 = %d,想要 5,不是空表格", len(result.Rows))
|
||
}
|
||
}
|
||
|
||
func TestListShopeeProducts_page小于1或非数字当作第1页(t *testing.T) {
|
||
db := newTestDB(t)
|
||
seedShopeeProducts(t, db, 3)
|
||
|
||
for _, p := range []int{0, -1, -100} {
|
||
result, err := ListShopeeProducts(db, repository.ShopeeFilter{}, p)
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if result.Page != 1 {
|
||
t.Errorf("page=%d 时 Page = %d,想要 1", p, result.Page)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestListShopeeProducts_总数为0时第1页共1页(t *testing.T) {
|
||
db := newTestDB(t)
|
||
|
||
result, err := ListShopeeProducts(db, repository.ShopeeFilter{}, 1)
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if result.TotalPages != 1 {
|
||
t.Errorf("总数为 0 时 TotalPages = %d,想要 1(不能出现『第 1/0 页』)", result.TotalPages)
|
||
}
|
||
if result.Page != 1 {
|
||
t.Errorf("Page = %d,想要 1", result.Page)
|
||
}
|
||
}
|
||
|
||
// ── 状态筛选(工单 #43) ──────────────────────────────
|
||
|
||
func TestListShopeeProducts_待补规格筛选_一个商品多个失败SKU只出现一行(t *testing.T) {
|
||
db := newTestDB(t)
|
||
// 同一商品两条解析失败的 SKU:用 JOIN + DISTINCT 会因为多行匹配 EXISTS
|
||
// 子查询而出重复,这里验证过滤用 EXISTS 时该商品只出现一次。
|
||
seedShopeeProduct(t, db, "1001", "待补商品")
|
||
seedShopeeSKU(t, db, "s1", "1001", "坏数据1", "", "", "", false)
|
||
seedShopeeSKU(t, db, "s2", "1001", "坏数据2", "", "", "", false)
|
||
seedShopeeProduct(t, db, "1002", "正常商品")
|
||
seedShopeeSKU(t, db, "s3", "1002", "", "黑色", "M", "", true)
|
||
|
||
result, err := ListShopeeProducts(db, repository.ShopeeFilter{Status: "pending_spec"}, 1)
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if result.Total != 1 {
|
||
t.Fatalf("Total = %d,想要 1(不因为两条失败 SKU 出重复行)", result.Total)
|
||
}
|
||
if len(result.Rows) != 1 || result.Rows[0].GoodsID != "1001" {
|
||
t.Fatalf("筛选结果不对: %+v", result.Rows)
|
||
}
|
||
}
|
||
|
||
func TestListShopeeProducts_未填链接和已填链接筛选(t *testing.T) {
|
||
db := newTestDB(t)
|
||
seedShopeeProduct(t, db, "1001", "未填链接商品")
|
||
seedShopeeProduct(t, db, "1002", "已填链接商品")
|
||
setShopeePddLink(t, db, "1002", "9001", "https://mobile.yangkeduo.com/goods.html?goods_id=9001")
|
||
|
||
noLink, err := ListShopeeProducts(db, repository.ShopeeFilter{Status: "no_link"}, 1)
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if noLink.Total != 1 || noLink.Rows[0].GoodsID != "1001" {
|
||
t.Fatalf("未填链接筛选结果不对: %+v", noLink.Rows)
|
||
}
|
||
|
||
hasLink, err := ListShopeeProducts(db, repository.ShopeeFilter{Status: "has_link"}, 1)
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
if hasLink.Total != 1 || hasLink.Rows[0].GoodsID != "1002" {
|
||
t.Fatalf("已填链接筛选结果不对: %+v", hasLink.Rows)
|
||
}
|
||
}
|
||
|
||
func TestListShopeeProducts_筛选参数认不出来当全部(t *testing.T) {
|
||
db := newTestDB(t)
|
||
seedShopeeProducts(t, db, 3)
|
||
|
||
result, err := ListShopeeProducts(db, repository.ShopeeFilter{Status: "not_a_real_status"}, 1)
|
||
if err != nil {
|
||
t.Fatalf("查询失败: %v", err)
|
||
}
|
||
// repository 层不做校验,直接透传的话这个取值走不进 switch 的任何分支,
|
||
// 等价于不筛选——校验(认不出来当全部)在 service.ParseShopeeStatus,
|
||
// 这里验证 repository 端不会因为陌生取值报错或漏数据。
|
||
if result.Total != 3 {
|
||
t.Errorf("Total = %d,想要 3(陌生取值不应影响结果)", result.Total)
|
||
}
|
||
}
|
||
|
||
func TestParseShopeeStatus_认不出来当全部(t *testing.T) {
|
||
cases := []string{"", "not_a_real_status", " ", "PENDING_SPEC"}
|
||
for _, c := range cases {
|
||
if got := ParseShopeeStatus(c); got != "" {
|
||
t.Errorf("ParseShopeeStatus(%q) = %q,想要空串(当全部)", c, got)
|
||
}
|
||
}
|
||
for _, c := range []string{"pending_spec", "no_link", "has_link"} {
|
||
if got := ParseShopeeStatus(c); got != c {
|
||
t.Errorf("ParseShopeeStatus(%q) = %q,想要原样返回", c, got)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestListShopeeProducts_分类搜索(t *testing.T) {
|
||
db := newTestDB(t)
|
||
for _, product := range []struct {
|
||
id, title, shop, image string
|
||
}{
|
||
{"A", "店铺有图", "店铺 A", "https://example.invalid/a.jpg"},
|
||
{"B", "店铺无图", "店铺 B", ""},
|
||
{"C", "无店有图", "", "https://example.invalid/c.jpg"},
|
||
{"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 {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
cases := []struct {
|
||
name string
|
||
filter repository.ShopeeFilter
|
||
want []string
|
||
}{
|
||
{"商品ID精确", repository.ShopeeFilter{Keyword: "A", SearchField: "goods_id"}, []string{"A"}},
|
||
{"商品名称包含", repository.ShopeeFilter{Keyword: "店铺有图", SearchField: "title"}, []string{"A"}},
|
||
{"店铺名称包含", repository.ShopeeFilter{Keyword: "店铺", SearchField: "shop_name"}, []string{"A", "B"}},
|
||
}
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
result, err := ListShopeeProducts(db, tc.filter, 1)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if result.Total != len(tc.want) || len(result.Rows) != len(tc.want) {
|
||
t.Fatalf("Total=%d rows=%d,期望 %v", result.Total, len(result.Rows), tc.want)
|
||
}
|
||
for index, want := range tc.want {
|
||
if result.Rows[index].GoodsID != want {
|
||
t.Fatalf("第 %d 行=%s,期望 %s", index, result.Rows[index].GoodsID, want)
|
||
}
|
||
}
|
||
if !result.IsFiltered {
|
||
t.Fatal("使用分类搜索时 IsFiltered 应为 true")
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestListShopeeProducts_店铺名称LIKE特殊字符按原文匹配(t *testing.T) {
|
||
db := newTestDB(t)
|
||
for _, product := range []struct{ id, shop string }{
|
||
{"SPECIAL", "测试%店_铺!"},
|
||
{"PLAIN", "测试普通店铺"},
|
||
} {
|
||
seedShopeeProduct(t, db, product.id, product.id)
|
||
if _, err := db.Exec(`UPDATE shopee_products SET shopee_shop_name=? WHERE goods_id=?`, product.shop, product.id); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
for _, keyword := range []string{"%", "_", "!", "%店_"} {
|
||
result, err := ListShopeeProducts(db, repository.ShopeeFilter{Keyword: keyword, SearchField: "shop_name"}, 1)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if result.Total != 1 || len(result.Rows) != 1 || result.Rows[0].GoodsID != "SPECIAL" {
|
||
t.Fatalf("店铺搜索 %q 得到 %+v,总数 %d,期望只匹配 SPECIAL", keyword, result.Rows, result.Total)
|
||
}
|
||
if !result.IsFiltered {
|
||
t.Fatal("只使用店铺名称时 IsFiltered 应为 true")
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestListShopeeProducts_未登记入口只显示未关联店铺(t *testing.T) {
|
||
db := newTestDB(t)
|
||
seedShopeeProduct(t, db, "UNLINKED", "未登记商品")
|
||
seedShopeeProduct(t, db, "LINKED", "已登记商品")
|
||
if _, err := db.Exec(`INSERT INTO users(user_id,username,password_hash,role,status,password_changed_at,created_at,updated_at)
|
||
VALUES('SHOP-USER','shop-user','x','admin','active',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)`); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := db.Exec(`INSERT INTO shops(shop_id,display_name,normalized_name,enabled,created_by_user_id,created_at,updated_at)
|
||
VALUES('SHOP-1','店铺一','店铺一',1,'SHOP-USER',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)`); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := db.Exec(`UPDATE shopee_products SET shop_id='SHOP-1' WHERE goods_id='LINKED'`); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
result, err := ListShopeeProducts(db, repository.ShopeeFilter{Unlinked: true}, 1)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if result.Total != 1 || len(result.Rows) != 1 || result.Rows[0].GoodsID != "UNLINKED" {
|
||
t.Fatalf("未登记结果错误:total=%d rows=%+v", result.Total, result.Rows)
|
||
}
|
||
}
|
||
|
||
func TestParseShopeeSearchField_固定白名单(t *testing.T) {
|
||
for _, value := range []string{"goods_id", "title", "shop_name"} {
|
||
if got := ParseShopeeSearchField(value); got != value {
|
||
t.Errorf("ParseShopeeSearchField(%q)=%q", value, got)
|
||
}
|
||
}
|
||
for _, value := range []string{"", "unknown", "title desc", " "} {
|
||
if got := ParseShopeeSearchField(value); got != "goods_id" {
|
||
t.Errorf("ParseShopeeSearchField(%q)=%q,期望 goods_id", value, got)
|
||
}
|
||
}
|
||
}
|