feat: 展示蝦皮主图和店铺 (#142)
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user