feat: 展示蝦皮主图和店铺 (#142)

This commit is contained in:
chengma
2026-08-11 11:18:51 +08:00
parent a441dfef87
commit 38d6a0a04b
22 changed files with 555 additions and 101 deletions
+69 -16
View File
@@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"unicode/utf8"
@@ -28,6 +29,8 @@ type CatalogShopeeProduct struct {
Title string `json:"title"`
Status string `json:"status"`
MainSKUCode string `json:"main_sku_code"`
ImageURL string `json:"image_url"`
ShopName string `json:"shop_name"`
}
type CatalogShopeeSKU struct {
SKUID string `json:"sku_id"`
@@ -72,19 +75,23 @@ type CatalogBatchRequest struct {
Associations []CatalogAssociation `json:"associations"`
}
type CatalogCounts struct {
ShopeeCreated int `json:"shopee_created"`
ShopeeUpdated int `json:"shopee_updated"`
SKUCreated int `json:"sku_created"`
SKUUpdated int `json:"sku_updated"`
SKUFilled int `json:"sku_filled"`
SKUSameSourceUpdated int `json:"sku_same_source_updated"`
SKUSkipped int `json:"sku_skipped"`
SKUManualSkipped int `json:"sku_manual_skipped"`
SKUStaleSkipped int `json:"sku_stale_skipped"`
PddCreated int `json:"pdd_created"`
PddUpdated int `json:"pdd_updated"`
AssociationCreated int `json:"association_created"`
AssociationUnchanged int `json:"association_unchanged"`
ShopeeCreated int `json:"shopee_created"`
ShopeeUpdated int `json:"shopee_updated"`
ShopeeFieldsFilled int `json:"shopee_fields_filled"`
ShopeeFieldsSameSourceUpdated int `json:"shopee_fields_same_source_updated"`
ShopeeFieldsManualSkipped int `json:"shopee_fields_manual_skipped"`
ShopeeFieldsStaleSkipped int `json:"shopee_fields_stale_skipped"`
SKUCreated int `json:"sku_created"`
SKUUpdated int `json:"sku_updated"`
SKUFilled int `json:"sku_filled"`
SKUSameSourceUpdated int `json:"sku_same_source_updated"`
SKUSkipped int `json:"sku_skipped"`
SKUManualSkipped int `json:"sku_manual_skipped"`
SKUStaleSkipped int `json:"sku_stale_skipped"`
PddCreated int `json:"pdd_created"`
PddUpdated int `json:"pdd_updated"`
AssociationCreated int `json:"association_created"`
AssociationUnchanged int `json:"association_unchanged"`
}
type CatalogBatchResponse struct {
BatchID string `json:"batch_id"`
@@ -132,6 +139,12 @@ func ValidateCatalogBatch(req CatalogBatchRequest) error {
return catalogInvalid("INVALID_SHOPEE_PRODUCT", "蝦皮商品 ID/标题不能为空且批内不能重复", map[string]any{"index": i, "goods_id": p.GoodsID})
}
seenProducts[p.GoodsID] = true
if utf8.RuneCountInString(strings.TrimSpace(p.ShopName)) > 500 {
return catalogInvalid("INVALID_SHOPEE_SHOP_NAME", "蝦皮店铺名不能超过 500 个字符", map[string]any{"index": i})
}
if err := validateCatalogImageURL(p.ImageURL); err != nil {
return catalogInvalid("INVALID_SHOPEE_IMAGE_URL", err.Error(), map[string]any{"index": i})
}
}
for i, s := range req.ShopeeSKUs {
key, keyErr := spec.SpecKey(s.SpecRaw)
@@ -205,16 +218,20 @@ func ImportCatalogBatch(db *sql.DB, source string, req CatalogBatchRequest, raw
return CatalogBatchResponse{}, importErr
}
for _, p := range req.ShopeeProducts {
c, u, e := repository.UpsertCatalogShopeeProduct(tx, p.GoodsID, p.Title, p.Status, p.MainSKUCode, req.ObservedAt, now)
outcome, e := repository.UpsertCatalogShopeeProduct(tx, repository.CatalogShopeeProductInput{GoodsID: p.GoodsID, Title: p.Title, Status: p.Status, MainSKU: p.MainSKUCode, ImageURL: strings.TrimSpace(p.ImageURL), ShopName: strings.TrimSpace(p.ShopName), Source: source, ObservedAt: req.ObservedAt, Now: now, UpdatePolicy: policy})
if e != nil {
return fail(e)
}
if c {
if outcome.Created {
counts.ShopeeCreated++
}
if u {
if outcome.Updated {
counts.ShopeeUpdated++
}
counts.ShopeeFieldsFilled += outcome.FieldsFilled
counts.ShopeeFieldsSameSourceUpdated += outcome.FieldsSameSourceUpdated
counts.ShopeeFieldsManualSkipped += outcome.FieldsManualSkipped
counts.ShopeeFieldsStaleSkipped += outcome.FieldsStaleSkipped
}
for _, p := range req.PddProducts {
var skus string
@@ -278,6 +295,10 @@ func ImportCatalogBatch(db *sql.DB, source string, req CatalogBatchRequest, raw
run.ResponseBody = string(body)
run.ShopeeCreated = counts.ShopeeCreated
run.ShopeeUpdated = counts.ShopeeUpdated
run.ShopeeFieldsFilled = counts.ShopeeFieldsFilled
run.ShopeeFieldsSameSourceUpdated = counts.ShopeeFieldsSameSourceUpdated
run.ShopeeFieldsManualSkipped = counts.ShopeeFieldsManualSkipped
run.ShopeeFieldsStaleSkipped = counts.ShopeeFieldsStaleSkipped
run.SKUCreated = counts.SKUCreated
run.SKUUpdated = counts.SKUUpdated
run.SKUFilled = counts.SKUFilled
@@ -314,6 +335,38 @@ func catalogSKURecordID(goodsID, specKey string) string {
return "catalog:" + hex.EncodeToString(sum[:])
}
func validateCatalogImageURL(raw string) error {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil
}
if len(raw) > 2048 {
return fmt.Errorf("蝦皮主图片 URL 不能超过 2048 字节")
}
u, err := url.ParseRequestURI(raw)
if err != nil || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") {
return fmt.Errorf("蝦皮主图片 URL 必须是完整的 HTTP/HTTPS 地址")
}
if u.User != nil {
return fmt.Errorf("蝦皮主图片 URL 不能包含用户名或密码")
}
parameters := u.Query()
if fragmentParameters, fragmentErr := url.ParseQuery(u.Fragment); fragmentErr == nil {
for key, values := range fragmentParameters {
parameters[key] = append(parameters[key], values...)
}
}
for key := range parameters {
lower := strings.ToLower(key)
for _, risky := range []string{"token", "cookie", "password", "passwd", "authorization", "credential", "secret", "session"} {
if strings.Contains(lower, risky) {
return fmt.Errorf("蝦皮主图片 URL 不能包含凭据参数")
}
}
}
return nil
}
func replayCatalogBatch(db *sql.DB, source, batchID, hash, now string) (CatalogBatchResponse, error) {
run, err := repository.GetCatalogImportRun(db, source, batchID)
if err != nil {