2026-08-08 09:57:12 +08:00
|
|
|
|
// 蝦皮数据列表页的查询和界面文字组装。
|
|
|
|
|
|
//
|
2026-08-11 10:16:48 +08:00
|
|
|
|
// 分成单独文件是因为它只负责蝦皮列表查询视图,不承担商品目录批量写入。
|
2026-08-08 09:57:12 +08:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"database/sql"
|
2026-08-11 10:46:11 +08:00
|
|
|
|
"fmt"
|
2026-08-08 11:40:07 +08:00
|
|
|
|
"strings"
|
2026-08-08 09:57:12 +08:00
|
|
|
|
|
|
|
|
|
|
"cmautobuy/admin/model"
|
|
|
|
|
|
"cmautobuy/admin/repository"
|
2026-08-11 10:46:11 +08:00
|
|
|
|
"cmautobuy/admin/spec"
|
2026-08-08 09:57:12 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-08 11:12:25 +08:00
|
|
|
|
// ShopeeProductView 是列表页一行要显示的全部内容,全部已经是字符串,
|
2026-08-08 09:57:12 +08:00
|
|
|
|
// 模板里不做判断和格式化。
|
2026-08-08 11:12:25 +08:00
|
|
|
|
//
|
|
|
|
|
|
// 一行对应一个**商品**,不是一个 SKU——见工单 #41。
|
|
|
|
|
|
type ShopeeProductView struct {
|
2026-08-13 11:47:56 +08:00
|
|
|
|
GoodsID string
|
|
|
|
|
|
Title string
|
|
|
|
|
|
ImageURL string
|
|
|
|
|
|
ShopName string
|
2026-08-08 09:57:12 +08:00
|
|
|
|
|
2026-08-08 11:12:25 +08:00
|
|
|
|
// 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
|
2026-08-10 12:24:23 +08:00
|
|
|
|
SourceText string
|
2026-08-08 09:57:12 +08:00
|
|
|
|
|
2026-08-13 12:07:27 +08:00
|
|
|
|
// 列表正文只显示稳定、短小的 PDD 商品 ID,完整 URL 留在 title 中。
|
|
|
|
|
|
// PddMissing 为 true 时模板显示"未填写"并标红——这是最需要操作员
|
|
|
|
|
|
// 注意的状态,见 §4.2。
|
|
|
|
|
|
PddGoodsID string
|
2026-08-08 09:57:12 +08:00
|
|
|
|
PddURL string
|
|
|
|
|
|
PddMissing bool
|
|
|
|
|
|
|
|
|
|
|
|
StatusText string
|
|
|
|
|
|
UpdatedAt string
|
2026-08-13 10:39:21 +08:00
|
|
|
|
Deleted bool
|
2026-08-08 09:57:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ShopeeListResult 是列表页要的全部数据。
|
|
|
|
|
|
type ShopeeListResult struct {
|
2026-08-08 11:40:07 +08:00
|
|
|
|
Rows []ShopeeProductView
|
|
|
|
|
|
|
|
|
|
|
|
// Total 是**当前筛选条件下**的商品总数,不是本页行数。
|
|
|
|
|
|
// `[必须]` 底部状态条必须显示这个,不能显示本页行数(20),
|
|
|
|
|
|
// 否则操作员会以为总共就这么多,见工单 #43。
|
|
|
|
|
|
Total int
|
|
|
|
|
|
|
|
|
|
|
|
// HasAnyProducts 是 shopee_products 的全库总数是否大于 0,不受筛选影响,
|
|
|
|
|
|
// 只用于区分空状态文案:"从没导入过" vs "筛选没结果"。
|
|
|
|
|
|
HasAnyProducts bool
|
|
|
|
|
|
|
2026-08-08 09:57:12 +08:00
|
|
|
|
IsFiltered bool
|
2026-08-08 11:40:07 +08:00
|
|
|
|
|
|
|
|
|
|
Page int
|
|
|
|
|
|
PageSize int
|
|
|
|
|
|
TotalPages int
|
2026-08-08 09:57:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-08 11:40:07 +08:00
|
|
|
|
// ListShopeeProducts 按筛选条件分页查蝦皮商品列表(商品级一行)并把每一行翻成界面文字。
|
2026-08-08 09:57:12 +08:00
|
|
|
|
//
|
|
|
|
|
|
// 采集状态来自 pdd_products(联查得到),不是蝦皮自己的字段,
|
|
|
|
|
|
// 见 docs/admin/01-requirements.md §6.1:
|
|
|
|
|
|
//
|
|
|
|
|
|
// shopee_products.pdd_goods_id 为空 -> "未填链接"
|
|
|
|
|
|
// pdd_products.collect_status -> 未采集 / 采集中 / 已采集 / 采集失败
|
2026-08-08 11:40:07 +08:00
|
|
|
|
//
|
|
|
|
|
|
// `[必须]` 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)
|
2026-08-08 09:57:12 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2026-08-08 11:40:07 +08:00
|
|
|
|
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)
|
2026-08-08 09:57:12 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result := &ShopeeListResult{
|
2026-08-08 11:40:07 +08:00
|
|
|
|
Rows: make([]ShopeeProductView, 0, len(rows)),
|
|
|
|
|
|
Total: total,
|
|
|
|
|
|
HasAnyProducts: hasAny > 0,
|
2026-08-13 11:47:56 +08:00
|
|
|
|
IsFiltered: strings.TrimSpace(filter.Keyword) != "" || filter.Status != "" || filter.Deleted || filter.Unlinked,
|
2026-08-08 11:40:07 +08:00
|
|
|
|
Page: page,
|
|
|
|
|
|
PageSize: PageSize,
|
|
|
|
|
|
TotalPages: totalPages,
|
2026-08-08 09:57:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
for _, r := range rows {
|
2026-08-08 11:12:25 +08:00
|
|
|
|
v := ShopeeProductView{
|
2026-08-13 11:47:56 +08:00
|
|
|
|
GoodsID: r.GoodsID,
|
|
|
|
|
|
Title: r.Title,
|
|
|
|
|
|
ImageURL: r.ImageURL,
|
|
|
|
|
|
ShopName: r.ShopeeShopName,
|
|
|
|
|
|
ColorCount: r.ColorCount,
|
|
|
|
|
|
SizeCount: r.SizeCount,
|
|
|
|
|
|
SKUCount: r.SKUCount,
|
|
|
|
|
|
PendingCount: r.PendingCount,
|
|
|
|
|
|
SourceText: shopeeSourceText(r.Source),
|
|
|
|
|
|
UpdatedAt: formatLocalTime(r.UpdatedAt),
|
|
|
|
|
|
Deleted: r.IsDeleted(),
|
2026-08-08 09:57:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if r.PddGoodsID == "" {
|
|
|
|
|
|
v.PddMissing = true
|
|
|
|
|
|
v.StatusText = "未填链接"
|
|
|
|
|
|
} else {
|
2026-08-13 12:07:27 +08:00
|
|
|
|
v.PddGoodsID = r.PddGoodsID
|
2026-08-08 09:57:12 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-08-08 11:12:25 +08:00
|
|
|
|
|
2026-08-10 12:24:23 +08:00
|
|
|
|
func shopeeSourceText(source string) string {
|
|
|
|
|
|
if source == "syb" {
|
|
|
|
|
|
return "顺运宝骨架"
|
|
|
|
|
|
}
|
|
|
|
|
|
return "蝦皮报表"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-08 11:40:07 +08:00
|
|
|
|
// ---------- 状态筛选 ----------
|
|
|
|
|
|
|
|
|
|
|
|
// 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]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 11:47:56 +08:00
|
|
|
|
// ShopeeSearchFieldOptions 返回关键词可以查询的三个字段。
|
|
|
|
|
|
func ShopeeSearchFieldOptions() []ShopeeStatusOption {
|
2026-08-11 12:04:38 +08:00
|
|
|
|
return []ShopeeStatusOption{
|
2026-08-13 11:47:56 +08:00
|
|
|
|
{"goods_id", "商品 ID"},
|
|
|
|
|
|
{"title", "商品名称"},
|
|
|
|
|
|
{"shop_name", "店铺名"},
|
2026-08-11 12:04:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 11:47:56 +08:00
|
|
|
|
// ParseShopeeSearchField 使用固定白名单,避免把请求参数当成 SQL 列名。
|
|
|
|
|
|
func ParseShopeeSearchField(s string) string {
|
2026-08-11 12:04:38 +08:00
|
|
|
|
value := strings.TrimSpace(s)
|
2026-08-13 11:47:56 +08:00
|
|
|
|
if value == "title" || value == "shop_name" {
|
2026-08-11 12:04:38 +08:00
|
|
|
|
return value
|
|
|
|
|
|
}
|
2026-08-13 11:47:56 +08:00
|
|
|
|
return "goods_id"
|
2026-08-11 12:04:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-08 11:12:25 +08:00
|
|
|
|
// ---------- 弹窗 ----------
|
|
|
|
|
|
|
|
|
|
|
|
// 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 {
|
2026-08-11 11:18:51 +08:00
|
|
|
|
GoodsID string
|
|
|
|
|
|
Title string
|
|
|
|
|
|
ShopeeStatus string
|
|
|
|
|
|
MainSKUCode string
|
|
|
|
|
|
ImageURL string
|
|
|
|
|
|
ShopName string
|
|
|
|
|
|
ImageSourceText, ImageObservedAt string
|
|
|
|
|
|
ShopNameSourceText, ShopNameObservedAt string
|
2026-08-08 11:12:25 +08:00
|
|
|
|
|
2026-08-09 22:40:58 +08:00
|
|
|
|
PddURL string
|
|
|
|
|
|
PddGoodsID string
|
|
|
|
|
|
PddMissing bool
|
|
|
|
|
|
StatusText string
|
|
|
|
|
|
CollectStatus string
|
|
|
|
|
|
CollectMsg string
|
|
|
|
|
|
CanCollect bool
|
2026-08-08 11:12:25 +08:00
|
|
|
|
|
2026-08-11 10:46:11 +08:00
|
|
|
|
SKUs []ShopeeSpecView
|
|
|
|
|
|
PendingCount int
|
|
|
|
|
|
SybObservations []SybObservationView
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SybObservationView 是详情页只读的顺运宝历史观测,不是正式 SKU。
|
|
|
|
|
|
type SybObservationView struct {
|
|
|
|
|
|
SpecRaw, PriceText, ImageURL, LastObservedAt string
|
|
|
|
|
|
OrderCount, TotalQuantity int
|
|
|
|
|
|
MatchedSKUID, Color, Size, MatchText string
|
2026-08-08 11:12:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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{
|
2026-08-11 11:18:51 +08:00
|
|
|
|
GoodsID: p.GoodsID,
|
|
|
|
|
|
Title: p.Title,
|
|
|
|
|
|
ShopeeStatus: p.ShopeeStatus,
|
|
|
|
|
|
MainSKUCode: p.MainSKUCode,
|
|
|
|
|
|
ImageURL: p.ImageURL,
|
|
|
|
|
|
ShopName: p.ShopeeShopName,
|
|
|
|
|
|
ImageSourceText: catalogFieldSourceText(p.ImageSource, p.ImageIsManual),
|
|
|
|
|
|
ImageObservedAt: formatLocalTime(p.ImageObservedAt),
|
|
|
|
|
|
ShopNameSourceText: catalogFieldSourceText(p.ShopNameSource, p.ShopNameIsManual),
|
|
|
|
|
|
ShopNameObservedAt: formatLocalTime(p.ShopNameObservedAt),
|
|
|
|
|
|
PddGoodsID: p.PddGoodsID,
|
2026-08-08 11:12:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
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
|
2026-08-09 22:40:58 +08:00
|
|
|
|
status, msg, err := repository.GetShopeeCollectStatus(db, p.PddGoodsID)
|
2026-08-08 11:12:25 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if status.Valid {
|
2026-08-09 22:40:58 +08:00
|
|
|
|
d.CollectStatus = status.String
|
|
|
|
|
|
d.CollectMsg = msg.String
|
2026-08-08 11:12:25 +08:00
|
|
|
|
d.StatusText = collectStatusText(model.CollectStatus(status.String))
|
2026-08-09 22:40:58 +08:00
|
|
|
|
d.CanCollect = status.String == string(model.CollectPending) ||
|
|
|
|
|
|
status.String == string(model.CollectFailed)
|
2026-08-08 11:12:25 +08:00
|
|
|
|
} 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)
|
|
|
|
|
|
}
|
2026-08-11 10:46:11 +08:00
|
|
|
|
observations, err := repository.ListSybSpecObservations(db, goodsID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
formalByKey := map[string][]model.ShopeeSKU{}
|
|
|
|
|
|
for _, sk := range skus {
|
|
|
|
|
|
if key, keyErr := spec.SpecKey(sk.SpecRaw); keyErr == nil {
|
|
|
|
|
|
formalByKey[key] = append(formalByKey[key], sk)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, observation := range observations {
|
|
|
|
|
|
view := SybObservationView{SpecRaw: observation.SpecRaw, PriceText: fmt.Sprintf("NT$%.2f", float64(observation.LatestPriceCent)/100), ImageURL: observation.LatestImageURL, OrderCount: observation.OrderCount, TotalQuantity: observation.TotalQuantity, LastObservedAt: formatLocalTime(observation.LastObservedAt), MatchText: "待目录补全"}
|
|
|
|
|
|
if matches := formalByKey[observation.SpecKey]; len(matches) == 1 {
|
|
|
|
|
|
view.MatchedSKUID = matches[0].SKUID
|
|
|
|
|
|
view.Color = matches[0].Color
|
|
|
|
|
|
view.Size = matches[0].Size
|
|
|
|
|
|
view.MatchText = "唯一匹配"
|
|
|
|
|
|
} else if len(matches) > 1 {
|
|
|
|
|
|
view.MatchText = "多条匹配,待人工确认"
|
|
|
|
|
|
}
|
|
|
|
|
|
d.SybObservations = append(d.SybObservations, view)
|
|
|
|
|
|
}
|
2026-08-08 11:12:25 +08:00
|
|
|
|
return d, nil
|
|
|
|
|
|
}
|
2026-08-11 11:18:51 +08:00
|
|
|
|
|
|
|
|
|
|
func catalogFieldSourceText(source string, manual bool) string {
|
|
|
|
|
|
if manual {
|
|
|
|
|
|
return "人工维护"
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.TrimSpace(source) == "" {
|
|
|
|
|
|
return "—"
|
|
|
|
|
|
}
|
|
|
|
|
|
return source
|
|
|
|
|
|
}
|