Files
cmautobuy/admin/service/shopee_list.go
T
chengmaandClaude Opus 5 64d5e74d0d 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>
2026-08-08 11:12:25 +08:00

204 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 蝦皮数据列表页的查询和界面文字组装。
//
// 分成单独文件是因为它和 shopee_import.go 职责不同:这里只读,不写库。
package service
import (
"database/sql"
"cmautobuy/admin/model"
"cmautobuy/admin/repository"
)
// ShopeeProductView 是列表页一行要显示的全部内容,全部已经是字符串,
// 模板里不做判断和格式化。
//
// 一行对应一个**商品**,不是一个 SKU——见工单 #41。
type ShopeeProductView struct {
GoodsID string
Title string
// 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。
PddURL string
PddMissing bool
StatusText string
UpdatedAt string
}
// ShopeeListResult 是列表页要的全部数据。
type ShopeeListResult struct {
Rows []ShopeeProductView
Total int // shopee_products 的总商品数(不受筛选影响),用于判断"是否已导入过"
IsFiltered bool
}
// ListShopeeProducts 查蝦皮商品列表(商品级一行)并把每一行翻成界面文字。
//
// 采集状态来自 pdd_products(联查得到),不是蝦皮自己的字段,
// 见 docs/admin/01-requirements.md §6.1:
//
// shopee_products.pdd_goods_id 为空 -> "未填链接"
// pdd_products.collect_status -> 未采集 / 采集中 / 已采集 / 采集失败
func ListShopeeProducts(db *sql.DB, keyword string) (*ShopeeListResult, error) {
rows, err := repository.ListShopeeProducts(db, keyword)
if err != nil {
return nil, err
}
total, err := repository.CountShopeeProducts(db)
if err != nil {
return nil, err
}
result := &ShopeeListResult{
Rows: make([]ShopeeProductView, 0, len(rows)),
Total: total,
IsFiltered: keyword != "",
}
for _, r := range rows {
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 == "" {
v.PddMissing = true
v.PddURL = "未填写"
v.StatusText = "未填链接"
} else {
v.PddURL = r.PddGoodsURL
if r.CollectStatus.Valid {
v.StatusText = collectStatusText(model.CollectStatus(r.CollectStatus.String))
} else {
// pdd_goods_id 有值但 pdd_products 里查不到对应行
// (比如那条 PDD 商品被软删除了),如实说明,不要装作"未采集"。
v.StatusText = "PDD 商品缺失"
}
}
result.Rows = append(result.Rows, v)
}
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
}