feat: 蝦皮数据页改为商品级列表与规格弹窗 (#41)
原来是 SKU 级一行,问题不在行数(6092→5195 只少 15%),在语义错了: pdd_goods_url / pdd_goods_id 在 shopee_products 上,是商品级字段。 商品 24420648774 有 62 个 SKU,列表里 62 行显示同一个 PDD 链接, 将来双击任意一行编辑的也是同一个字段,操作员会以为改的是这一行。 加了两列,都是防止聚合后丢信息: 「SKU 数」—— 只显示颜色数和尺码数会严重误导。实测 602 个商品(11.6%) 的颜色数×尺码数大于实际 SKU 数,最悬殊的 26886533818 是 15×5=75 但 实际只有 28 个。蝦皮报表只含有销售成绩的 SKU,数据本来就不全。 「待补」—— 原来解析失败的行整行标黄,聚合成数量后这个信号会丢。 23 条失败分布在 6 个商品里,其中 4 个是「部分失败」:数字看着正常, 坏数据被吃掉,永远没人去补。现在待补>0 整行标黄。 颜色数/尺码数只统计 parse_ok=1,否则空值会被算进 DISTINCT 多出 一个「空颜色」。 弹窗只读,待补的行显示 spec_raw 原文——不显示的话人工不知道该填什么, 保留原文这个设计就白做了。复用 #18 的弹窗机制,app.js 未改动。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+131
-30
@@ -10,19 +10,26 @@ import (
|
||||
"cmautobuy/admin/repository"
|
||||
)
|
||||
|
||||
// ShopeeSKUView 是列表页一行要显示的全部内容,全部已经是字符串,
|
||||
// ShopeeProductView 是列表页一行要显示的全部内容,全部已经是字符串,
|
||||
// 模板里不做判断和格式化。
|
||||
type ShopeeSKUView struct {
|
||||
//
|
||||
// 一行对应一个**商品**,不是一个 SKU——见工单 #41。
|
||||
type ShopeeProductView struct {
|
||||
GoodsID string
|
||||
Title string
|
||||
SKUID string
|
||||
|
||||
Color string
|
||||
Size string
|
||||
Advice string
|
||||
// ParseFailed 为 true 时整行标黄,提示需要人工补颜色/尺码/建议,
|
||||
// 见 docs/admin/05-ui-specification.md §4.2。
|
||||
ParseFailed bool
|
||||
// ColorCount / SizeCount 只统计 parse_ok = 1 的 SKU(`[必须]`,见 #41)。
|
||||
ColorCount int
|
||||
SizeCount int
|
||||
// SKUCount 是这个商品报表里实际出现过的 SKU 数(不管解析成功与否)。
|
||||
// `[必须]` 必须单独展示:蝦皮报表只含有销售成绩的 SKU,
|
||||
// 颜色数 × 尺码数经常大于 SKUCount,只显示前两者会严重误导操作员,
|
||||
// 让人以为要匹配的规格比实际多得多(实测最悬殊相差 2.7 倍,见 #41)。
|
||||
SKUCount int
|
||||
// PendingCount 是 parse_ok = 0 的 SKU 数,即"待补"。
|
||||
// `[必须]` > 0 时整行标黄(沿用 .row-warn)——聚合成商品级之后,
|
||||
// "部分失败"的商品数字看起来完全正常,坏数据会被吃掉,见 #41。
|
||||
PendingCount int
|
||||
|
||||
// PddURL 为空且 PddMissing 为 true 时,模板显示"未填写"并标红——
|
||||
// 这是最需要操作员注意的状态,见 §4.2。
|
||||
@@ -30,26 +37,25 @@ type ShopeeSKUView struct {
|
||||
PddMissing bool
|
||||
|
||||
StatusText string
|
||||
IsManual bool
|
||||
UpdatedAt string
|
||||
}
|
||||
|
||||
// ShopeeListResult 是列表页要的全部数据。
|
||||
type ShopeeListResult struct {
|
||||
Rows []ShopeeSKUView
|
||||
Rows []ShopeeProductView
|
||||
Total int // shopee_products 的总商品数(不受筛选影响),用于判断"是否已导入过"
|
||||
IsFiltered bool
|
||||
}
|
||||
|
||||
// ListShopeeSKUs 查蝦皮 SKU 列表并把每一行翻成界面文字。
|
||||
// ListShopeeProducts 查蝦皮商品列表(商品级一行)并把每一行翻成界面文字。
|
||||
//
|
||||
// 采集状态来自 pdd_products(联查得到),不是蝦皮自己的字段,
|
||||
// 见 docs/admin/01-requirements.md §6.1:
|
||||
//
|
||||
// shopee_products.pdd_goods_id 为空 -> "未填链接"
|
||||
// pdd_products.collect_status -> 未采集 / 采集中 / 已采集 / 采集失败
|
||||
func ListShopeeSKUs(db *sql.DB, keyword string) (*ShopeeListResult, error) {
|
||||
rows, err := repository.ListShopeeSKUs(db, keyword)
|
||||
func ListShopeeProducts(db *sql.DB, keyword string) (*ShopeeListResult, error) {
|
||||
rows, err := repository.ListShopeeProducts(db, keyword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -59,26 +65,19 @@ func ListShopeeSKUs(db *sql.DB, keyword string) (*ShopeeListResult, error) {
|
||||
}
|
||||
|
||||
result := &ShopeeListResult{
|
||||
Rows: make([]ShopeeSKUView, 0, len(rows)),
|
||||
Rows: make([]ShopeeProductView, 0, len(rows)),
|
||||
Total: total,
|
||||
IsFiltered: keyword != "",
|
||||
}
|
||||
for _, r := range rows {
|
||||
v := ShopeeSKUView{
|
||||
GoodsID: r.GoodsID,
|
||||
Title: r.ProductTitle,
|
||||
SKUID: r.SKUID,
|
||||
IsManual: r.IsManual,
|
||||
UpdatedAt: formatLocalTime(r.UpdatedAt),
|
||||
}
|
||||
if r.ParseOK {
|
||||
v.Color, v.Size, v.Advice = r.Color, r.Size, r.Advice
|
||||
if v.Advice == "" {
|
||||
v.Advice = placeholder
|
||||
}
|
||||
} else {
|
||||
v.Color, v.Size, v.Advice = placeholder, placeholder, placeholder
|
||||
v.ParseFailed = true
|
||||
v := ShopeeProductView{
|
||||
GoodsID: r.GoodsID,
|
||||
Title: r.Title,
|
||||
ColorCount: r.ColorCount,
|
||||
SizeCount: r.SizeCount,
|
||||
SKUCount: r.SKUCount,
|
||||
PendingCount: r.PendingCount,
|
||||
UpdatedAt: formatLocalTime(r.UpdatedAt),
|
||||
}
|
||||
|
||||
if r.PddGoodsID == "" {
|
||||
@@ -100,3 +99,105 @@ func ListShopeeSKUs(db *sql.DB, keyword string) (*ShopeeListResult, error) {
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ---------- 弹窗 ----------
|
||||
|
||||
// ShopeeSpecView 是弹窗规格表的一行。
|
||||
type ShopeeSpecView struct {
|
||||
SKUID string
|
||||
Color string
|
||||
Size string
|
||||
Advice string
|
||||
// StatusText 是"✓"或"待补",`[必须]` 要有文字,不能只靠颜色区分(见 #41)。
|
||||
StatusText string
|
||||
// Pending 为 true 时模板整行标黄,并在下面加一行显示 SpecRaw 原文。
|
||||
Pending bool
|
||||
// SpecRaw 只在 Pending 为 true 时才有意义。
|
||||
// `[必须]` 待补的行必须显示规格原文——不显示的话人工不知道该填什么,
|
||||
// 这正是保留 spec_raw 这个设计的全部意义,见 #41。
|
||||
SpecRaw string
|
||||
}
|
||||
|
||||
// ShopeeProductDetail 是双击弹窗要显示的全部内容。
|
||||
//
|
||||
// `[必须]` 这个弹窗只读,不含可提交的表单——本工单不实现保存,
|
||||
// ShopeeSave 仍是 501,见 #41。
|
||||
type ShopeeProductDetail struct {
|
||||
GoodsID string
|
||||
Title string
|
||||
ShopeeStatus string
|
||||
MainSKUCode string
|
||||
|
||||
PddURL string
|
||||
PddMissing bool
|
||||
StatusText string
|
||||
|
||||
SKUs []ShopeeSpecView
|
||||
PendingCount int
|
||||
}
|
||||
|
||||
// GetShopeeProductDetail 读一个蝦皮商品的详情(商品信息 + 完整规格表)。
|
||||
// 商品不存在返回 (nil, nil)。
|
||||
func GetShopeeProductDetail(db *sql.DB, goodsID string) (*ShopeeProductDetail, error) {
|
||||
p, err := repository.GetShopeeProductByGoodsID(db, goodsID)
|
||||
if err != nil || p == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d := &ShopeeProductDetail{
|
||||
GoodsID: p.GoodsID,
|
||||
Title: p.Title,
|
||||
ShopeeStatus: p.ShopeeStatus,
|
||||
MainSKUCode: p.MainSKUCode,
|
||||
}
|
||||
if d.Title == "" {
|
||||
d.Title = placeholder
|
||||
}
|
||||
if d.ShopeeStatus == "" {
|
||||
d.ShopeeStatus = placeholder
|
||||
}
|
||||
if d.MainSKUCode == "" {
|
||||
d.MainSKUCode = placeholder
|
||||
}
|
||||
|
||||
if p.PddGoodsID == "" {
|
||||
d.PddMissing = true
|
||||
d.PddURL = "未填写"
|
||||
d.StatusText = "未填链接"
|
||||
} else {
|
||||
d.PddURL = p.PddGoodsURL
|
||||
status, _, err := repository.GetShopeeCollectStatus(db, p.PddGoodsID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if status.Valid {
|
||||
d.StatusText = collectStatusText(model.CollectStatus(status.String))
|
||||
} else {
|
||||
d.StatusText = "PDD 商品缺失"
|
||||
}
|
||||
}
|
||||
|
||||
skus, err := repository.ListShopeeSKUsByGoodsID(db, goodsID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.SKUs = make([]ShopeeSpecView, 0, len(skus))
|
||||
for _, sk := range skus {
|
||||
row := ShopeeSpecView{SKUID: sk.SKUID}
|
||||
if sk.ParseOK {
|
||||
row.Color, row.Size, row.Advice = sk.Color, sk.Size, sk.Advice
|
||||
if row.Advice == "" {
|
||||
row.Advice = placeholder
|
||||
}
|
||||
row.StatusText = "✓"
|
||||
} else {
|
||||
row.Color, row.Size, row.Advice = placeholder, placeholder, placeholder
|
||||
row.StatusText = "待补"
|
||||
row.Pending = true
|
||||
row.SpecRaw = sk.SpecRaw
|
||||
d.PendingCount++
|
||||
}
|
||||
d.SKUs = append(d.SKUs, row)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"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 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, "")
|
||||
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, "")
|
||||
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, "")
|
||||
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, "")
|
||||
if err != nil {
|
||||
t.Fatalf("查询失败: %v", err)
|
||||
}
|
||||
row := result.Rows[0]
|
||||
if !row.PddMissing || row.PddURL != "未填写" {
|
||||
t.Errorf("PddMissing/PddURL = %v/%q,想要 true/\"未填写\"", row.PddMissing, row.PddURL)
|
||||
}
|
||||
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, "")
|
||||
if err != nil {
|
||||
t.Fatalf("查询失败: %v", err)
|
||||
}
|
||||
row := result.Rows[0]
|
||||
if row.PddMissing {
|
||||
t.Error("已填链接,PddMissing 应为 false")
|
||||
}
|
||||
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, "1001")
|
||||
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, "背心")
|
||||
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, "黑色")
|
||||
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.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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user