图搜采集成功并自动关联后,拉取该虾皮商品的完整颜色尺码清单合并进档案, 再触发一次匹配。此后同一虾皮商品的其它颜色 SYB 订单无需再采集、再点匹配。 - 新增 shopeespec 叶子包,凭据只从 GOAUTO_ERPGO_BASE_URL/GOAUTO_ERPGO_APIKEY 读取;错误文本不携带 URL,避免 apikey 流进日志。 - 写入档案前经 sybspec.StripAnnotations 剥离 【...】,与 SYB 明细同源, 否则 confirmedMappings 查不到、映射全部落空。 - 标记记录同步时对着哪个 PDD 商品(spec_sync_pdd_product_id),不是布尔值, 重新关联时自动失效。 - 已完整同步的商品再次图搜时返回 IMAGE_SEARCH_SPEC_SYNCED。 - 迁移只加列不回填:既有档案仍是从 SYB 明细增量累积的,不能假装已同步。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
120 lines
4.0 KiB
Go
120 lines
4.0 KiB
Go
// Package shopeespec fetches a Shopee product's complete colour and size list
|
|
// from the ERP-Go side service (#290).
|
|
//
|
|
// 存在的理由:虾皮档案里的颜色尺码是从 SYB 明细增量累积的(sybimport.mergeParsedSpec),
|
|
// SYB 送来什么才有什么,因此永远滞后于真实商品。新订单带来新组合时又变成“未匹配”,
|
|
// 采购员得反复去点 AI 匹配。拉取完整清单后可以一次性匹配完,之后新订单直接可采购。
|
|
package shopeespec
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// 返回体是颜色与尺码两个字符串数组,实测 462 字节;上限留足冗余即可。
|
|
maxResponseBytes = 1 << 20
|
|
requestTimeout = 10 * time.Second
|
|
)
|
|
|
|
// ErrNotConfigured means the base URL or API key is absent, so the caller
|
|
// should skip the sync instead of failing the surrounding operation.
|
|
var ErrNotConfigured = errors.New("shopee spec service is not configured")
|
|
|
|
// Spec is the complete colour and size list of one Shopee product, exactly as
|
|
// the platform spells them — 【...】 annotations included.
|
|
type Spec struct {
|
|
Colors []string
|
|
Sizes []string
|
|
}
|
|
|
|
// Client reads its endpoint and credential from the environment.
|
|
//
|
|
// `[必须]` 凭据只从环境变量读取,不接受参数传入、不落日志。本地由
|
|
// scripts/start-server.ps1 从 config.yaml 的 erpgo 段转换;线上在
|
|
// /etc/goauto/goauto.env 配置。
|
|
type Client struct {
|
|
HTTP *http.Client
|
|
}
|
|
|
|
func NewClient() *Client { return &Client{HTTP: &http.Client{Timeout: requestTimeout}} }
|
|
|
|
// Fetch returns the full spec list for one Shopee item id.
|
|
func (c *Client) Fetch(ctx context.Context, shopeeItemID string) (Spec, error) {
|
|
shopeeItemID = strings.TrimSpace(shopeeItemID)
|
|
if shopeeItemID == "" {
|
|
return Spec{}, errors.New("shopee item id is empty")
|
|
}
|
|
base := strings.TrimSpace(os.Getenv("GOAUTO_ERPGO_BASE_URL"))
|
|
key := strings.TrimSpace(os.Getenv("GOAUTO_ERPGO_APIKEY"))
|
|
if base == "" || key == "" {
|
|
return Spec{}, ErrNotConfigured
|
|
}
|
|
endpoint, err := url.Parse(strings.TrimRight(base, "/") + "/api/v1/shopee/product/spec/" + url.PathEscape(shopeeItemID))
|
|
if err != nil {
|
|
return Spec{}, err
|
|
}
|
|
query := endpoint.Query()
|
|
query.Set("apikey", key)
|
|
endpoint.RawQuery = query.Encode()
|
|
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
|
|
if err != nil {
|
|
return Spec{}, err
|
|
}
|
|
client := c.HTTP
|
|
if client == nil {
|
|
client = &http.Client{Timeout: requestTimeout}
|
|
}
|
|
response, err := client.Do(request)
|
|
if err != nil {
|
|
// `[必须]` 不要把 err 直接往外带:net/url 的错误会把完整 URL(含 apikey)
|
|
// 写进错误文本,那会让凭据流进日志和工单。
|
|
return Spec{}, fmt.Errorf("shopee spec request failed for item %s", shopeeItemID)
|
|
}
|
|
defer response.Body.Close()
|
|
if response.StatusCode != http.StatusOK {
|
|
return Spec{}, fmt.Errorf("shopee spec service returned %d for item %s", response.StatusCode, shopeeItemID)
|
|
}
|
|
body, err := io.ReadAll(io.LimitReader(response.Body, maxResponseBytes))
|
|
if err != nil {
|
|
return Spec{}, fmt.Errorf("shopee spec response unreadable for item %s", shopeeItemID)
|
|
}
|
|
var payload struct {
|
|
Data struct {
|
|
Color []string `json:"color"`
|
|
Size []string `json:"size"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
return Spec{}, fmt.Errorf("shopee spec response is not decodable for item %s", shopeeItemID)
|
|
}
|
|
spec := Spec{Colors: clean(payload.Data.Color), Sizes: clean(payload.Data.Size)}
|
|
if len(spec.Colors) == 0 && len(spec.Sizes) == 0 {
|
|
return Spec{}, fmt.Errorf("shopee spec service returned no specs for item %s", shopeeItemID)
|
|
}
|
|
return spec, nil
|
|
}
|
|
|
|
func clean(values []string) []string {
|
|
result := make([]string, 0, len(values))
|
|
seen := map[string]bool{}
|
|
for _, value := range values {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" || seen[value] {
|
|
continue
|
|
}
|
|
seen[value] = true
|
|
result = append(result, value)
|
|
}
|
|
return result
|
|
}
|