Files
cmautobuy/admin/service/shopee_list.go
T

292 lines
8.8 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"
"strings"
"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
SourceText string
// PddURL 为空且 PddMissing 为 true 时,模板显示"未填写"并标红——
// 这是最需要操作员注意的状态,见 §4.2。
PddURL string
PddMissing bool
StatusText string
UpdatedAt string
}
// ShopeeListResult 是列表页要的全部数据。
type ShopeeListResult struct {
Rows []ShopeeProductView
// Total 是**当前筛选条件下**的商品总数,不是本页行数。
// `[必须]` 底部状态条必须显示这个,不能显示本页行数(20),
// 否则操作员会以为总共就这么多,见工单 #43。
Total int
// HasAnyProducts 是 shopee_products 的全库总数是否大于 0,不受筛选影响,
// 只用于区分空状态文案:"从没导入过" vs "筛选没结果"。
HasAnyProducts bool
IsFiltered bool
Page int
PageSize int
TotalPages int
}
// ListShopeeProducts 按筛选条件分页查蝦皮商品列表(商品级一行)并把每一行翻成界面文字。
//
// 采集状态来自 pdd_products(联查得到),不是蝦皮自己的字段,
// 见 docs/admin/01-requirements.md §6.1:
//
// shopee_products.pdd_goods_id 为空 -> "未填链接"
// pdd_products.collect_status -> 未采集 / 采集中 / 已采集 / 采集失败
//
// `[必须]` Total 用和 ListShopeeProducts 同一套筛选条件的 COUNT
// (repository.CountShopeeProductsFiltered),分页页数据此算出,
// page 越界时兜到最后一页,见 service/pagination.go(工单 #43)。
func ListShopeeProducts(db *sql.DB, filter repository.ShopeeFilter, page int) (*ShopeeListResult, error) {
total, err := repository.CountShopeeProductsFiltered(db, filter)
if err != nil {
return nil, err
}
totalPages := TotalPages(total)
page = ClampPage(page, totalPages)
offset := (page - 1) * PageSize
rows, err := repository.ListShopeeProducts(db, filter, PageSize, offset)
if err != nil {
return nil, err
}
hasAny, err := repository.CountShopeeProducts(db)
if err != nil {
return nil, err
}
result := &ShopeeListResult{
Rows: make([]ShopeeProductView, 0, len(rows)),
Total: total,
HasAnyProducts: hasAny > 0,
IsFiltered: strings.TrimSpace(filter.Keyword) != "" || filter.Status != "",
Page: page,
PageSize: PageSize,
TotalPages: totalPages,
}
for _, r := range rows {
v := ShopeeProductView{
GoodsID: r.GoodsID,
Title: r.Title,
ColorCount: r.ColorCount,
SizeCount: r.SizeCount,
SKUCount: r.SKUCount,
PendingCount: r.PendingCount,
SourceText: shopeeSourceText(r.Source),
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
}
func shopeeSourceText(source string) string {
if source == "syb" {
return "顺运宝骨架"
}
return "蝦皮报表"
}
// ---------- 状态筛选 ----------
// shopeeStatusTexts 是状态筛选下拉框每个取值对应的中文文字,
// 也用于底部状态条筛选后的前缀("待补规格:6 个商品"),见工单 #43。
var shopeeStatusTexts = map[string]string{
"pending_spec": "待补规格",
"no_link": "未填 PDD 链接",
"has_link": "已填链接",
}
// ShopeeStatusOption 是筛选下拉框的一项。
type ShopeeStatusOption struct {
Value string // 空串表示"全部"
Text string
}
// ShopeeStatusOptions 返回状态筛选下拉框的全部选项:全部 + 三个状态。
func ShopeeStatusOptions() []ShopeeStatusOption {
return []ShopeeStatusOption{
{"", "全部"},
{"pending_spec", shopeeStatusTexts["pending_spec"]},
{"no_link", shopeeStatusTexts["no_link"]},
{"has_link", shopeeStatusTexts["has_link"]},
}
}
// ParseShopeeStatus 校验筛选参数。认不出的一律当"全部",不报错——
// 地址栏参数是用户可以随便改的,不值得为它弹错误页(工单 #43)。
func ParseShopeeStatus(s string) string {
status := strings.TrimSpace(s)
if _, ok := shopeeStatusTexts[status]; ok {
return status
}
return ""
}
// ShopeeStatusLabel 返回状态筛选取值对应的中文文字,取值为空或认不出时
// 返回空串——调用方(handler 层组装状态条文案)据此判断要不要用作前缀。
func ShopeeStatusLabel(status string) string {
return shopeeStatusTexts[status]
}
// ---------- 弹窗 ----------
// 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 是双击弹窗要显示的全部内容。
type ShopeeProductDetail struct {
GoodsID string
Title string
ShopeeStatus string
MainSKUCode string
PddURL string
PddGoodsID string
PddMissing bool
StatusText string
CollectStatus string
CollectMsg string
CanCollect bool
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,
PddGoodsID: p.PddGoodsID,
}
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, msg, err := repository.GetShopeeCollectStatus(db, p.PddGoodsID)
if err != nil {
return nil, err
}
if status.Valid {
d.CollectStatus = status.String
d.CollectMsg = msg.String
d.StatusText = collectStatusText(model.CollectStatus(status.String))
d.CanCollect = status.String == string(model.CollectPending) ||
status.String == string(model.CollectFailed)
} 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
}