feat: 建立商品颜色映射数据模型 (#293)
This commit is contained in:
@@ -0,0 +1,409 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"cmautobuy/admin/model"
|
||||
"cmautobuy/admin/repository"
|
||||
"cmautobuy/admin/spec"
|
||||
)
|
||||
|
||||
// ProductColorMappingRulesVersion 进入上下文版本,规则变化会让旧页面整体失效。
|
||||
const ProductColorMappingRulesVersion = "color-rules-v1"
|
||||
|
||||
// ProductColorSource 汇总同一颜色在两类可靠数据源中的出现次数。
|
||||
type ProductColorSource struct {
|
||||
Key, Raw string
|
||||
FormalCount, SybCount int
|
||||
}
|
||||
|
||||
func (s ProductColorSource) SourceText() string {
|
||||
parts := make([]string, 0, 2)
|
||||
if s.FormalCount > 0 {
|
||||
parts = append(parts, fmt.Sprintf("蝦皮 SKU %d", s.FormalCount))
|
||||
}
|
||||
if s.SybCount > 0 {
|
||||
parts = append(parts, fmt.Sprintf("顺运宝 %d", s.SybCount))
|
||||
}
|
||||
return strings.Join(parts, " · ")
|
||||
}
|
||||
|
||||
// PddColorCandidate 是当前可购买完整规格组合按颜色聚合后的候选。
|
||||
// 金额始终使用整数分;没有可靠价格时 HasPrice=false。
|
||||
type PddColorCandidate struct {
|
||||
DimensionKey string
|
||||
Value string
|
||||
MinPriceCent int64
|
||||
MaxPriceCent int64
|
||||
HasPrice bool
|
||||
SKUCount int
|
||||
}
|
||||
|
||||
// ProductColorMappingRow 把颜色来源、当前映射及目标有效性放在同一行。
|
||||
type ProductColorMappingRow struct {
|
||||
Color ProductColorSource
|
||||
Mapping *model.ProductColorMapping
|
||||
MappingValid bool
|
||||
}
|
||||
|
||||
// ProductColorMappingContext 是页面和后续规则复用共同依赖的只读快照。
|
||||
type ProductColorMappingContext struct {
|
||||
ShopeeGoodsID, ShopeeTitle string
|
||||
PddGoodsID, PddTitle string
|
||||
PddDimensionKey string
|
||||
Rows []ProductColorMappingRow
|
||||
Candidates []PddColorCandidate
|
||||
ContextVersion string
|
||||
UnavailableReason string
|
||||
}
|
||||
|
||||
// ProductColorMappingUpdate 是一次局部保存。TargetValue 为空表示清除当前映射。
|
||||
type ProductColorMappingUpdate struct {
|
||||
ShopeeColorKey string
|
||||
TargetValue string
|
||||
}
|
||||
|
||||
// NormalizeProductColorKey 只折叠首尾及连续空白,不做同义词、繁简或大小写推断。
|
||||
func NormalizeProductColorKey(raw string) string {
|
||||
return strings.Join(strings.Fields(raw), " ")
|
||||
}
|
||||
|
||||
// GetProductColorMappingContext 返回一个商品的当前颜色映射上下文。
|
||||
// 商品不存在返回 (nil, nil);未关联、未采集或维度不明确通过 UnavailableReason 表达。
|
||||
func GetProductColorMappingContext(db *sql.DB, goodsID string) (*ProductColorMappingContext, error) {
|
||||
return loadProductColorMappingContext(db, strings.TrimSpace(goodsID))
|
||||
}
|
||||
|
||||
func loadProductColorMappingContext(q repository.Execer, goodsID string) (*ProductColorMappingContext, error) {
|
||||
product, err := repository.GetShopeeProductByGoodsID(q, goodsID)
|
||||
if err != nil || product == nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &ProductColorMappingContext{
|
||||
ShopeeGoodsID: product.GoodsID, ShopeeTitle: product.Title, PddGoodsID: product.PddGoodsID,
|
||||
}
|
||||
colors, err := collectProductColors(q, goodsID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Rows = make([]ProductColorMappingRow, len(colors))
|
||||
for i, color := range colors {
|
||||
result.Rows[i].Color = color
|
||||
}
|
||||
if product.PddGoodsID == "" {
|
||||
result.UnavailableReason = "当前蝦皮商品尚未关联 PDD 商品"
|
||||
result.ContextVersion = colorMappingContextVersion(result, "", "")
|
||||
return result, nil
|
||||
}
|
||||
pdd, err := repository.GetPddProductByGoodsID(q, product.PddGoodsID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pdd == nil || pdd.IsDeleted() {
|
||||
result.UnavailableReason = "当前关联的 PDD 商品不存在或已删除"
|
||||
result.ContextVersion = colorMappingContextVersion(result, "", "")
|
||||
return result, nil
|
||||
}
|
||||
result.PddTitle = pdd.Title
|
||||
if pdd.CollectStatus != model.CollectCollected || strings.TrimSpace(pdd.SkusJSON) == "" {
|
||||
result.UnavailableReason = "当前 PDD 商品尚未完成采集"
|
||||
result.ContextVersion = colorMappingContextVersion(result, pdd.UpdatedAt, pdd.SkusJSON)
|
||||
return result, nil
|
||||
}
|
||||
dimensionKey, candidates, err := aggregatePddColorCandidates(pdd.SkusJSON)
|
||||
if err != nil {
|
||||
result.UnavailableReason = err.Error()
|
||||
result.ContextVersion = colorMappingContextVersion(result, pdd.UpdatedAt, pdd.SkusJSON)
|
||||
return result, nil
|
||||
}
|
||||
result.PddDimensionKey, result.Candidates = dimensionKey, candidates
|
||||
mappings, err := repository.ListProductColorMappings(q, goodsID, product.PddGoodsID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mappingByColor := make(map[string]model.ProductColorMapping, len(mappings))
|
||||
for _, mapping := range mappings {
|
||||
mappingByColor[mapping.ShopeeColorKey] = mapping
|
||||
}
|
||||
validTargets := make(map[string]bool, len(candidates))
|
||||
for _, candidate := range candidates {
|
||||
validTargets[candidate.Value] = true
|
||||
}
|
||||
for i := range result.Rows {
|
||||
mapping, ok := mappingByColor[result.Rows[i].Color.Key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
copy := mapping
|
||||
result.Rows[i].Mapping = ©
|
||||
result.Rows[i].MappingValid = mapping.PddDimensionKey == dimensionKey && validTargets[mapping.PddColorValue]
|
||||
}
|
||||
result.ContextVersion = colorMappingContextVersion(result, pdd.UpdatedAt, pdd.SkusJSON)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func collectProductColors(q repository.Execer, goodsID string) ([]ProductColorSource, error) {
|
||||
type accumulator struct {
|
||||
raw string
|
||||
formalCount, sybCount int
|
||||
}
|
||||
byKey := map[string]*accumulator{}
|
||||
add := func(raw string, formal bool) {
|
||||
key := NormalizeProductColorKey(raw)
|
||||
if key == "" {
|
||||
return
|
||||
}
|
||||
item := byKey[key]
|
||||
if item == nil {
|
||||
item = &accumulator{raw: strings.TrimSpace(raw)}
|
||||
byKey[key] = item
|
||||
} else if candidate := strings.TrimSpace(raw); candidate < item.raw {
|
||||
item.raw = candidate
|
||||
}
|
||||
if formal {
|
||||
item.formalCount++
|
||||
} else {
|
||||
item.sybCount++
|
||||
}
|
||||
}
|
||||
skus, err := repository.ListShopeeSKUsByGoodsID(q, goodsID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, sku := range skus {
|
||||
add(sku.Color, true)
|
||||
}
|
||||
observations, err := repository.ListSybSpecObservations(q, goodsID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, observation := range observations {
|
||||
if parsed, ok := spec.ParseShopeeSpec(observation.SpecRaw); ok {
|
||||
add(parsed.Color, false)
|
||||
}
|
||||
}
|
||||
result := make([]ProductColorSource, 0, len(byKey))
|
||||
for key, item := range byKey {
|
||||
result = append(result, ProductColorSource{
|
||||
Key: key, Raw: item.raw, FormalCount: item.formalCount, SybCount: item.sybCount,
|
||||
})
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool { return result[i].Key < result[j].Key })
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func aggregatePddColorCandidates(raw string) (string, []PddColorCandidate, error) {
|
||||
collected, err := parseCollected(raw)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("PDD 采集规格无法解析:%w", err)
|
||||
}
|
||||
colorKeys := map[string]bool{}
|
||||
for _, dimension := range collected.Dimensions {
|
||||
if isExplicitPddColorDimension(dimension.Key) || isExplicitPddColorDimension(dimension.Name) {
|
||||
colorKeys[dimension.Key] = true
|
||||
}
|
||||
}
|
||||
if len(colorKeys) == 0 {
|
||||
for _, sku := range collected.SKUs {
|
||||
for key := range sku.Options {
|
||||
if isExplicitPddColorDimension(key) {
|
||||
colorKeys[key] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(colorKeys) != 1 {
|
||||
return "", nil, fmt.Errorf("PDD 颜色维度不明确:识别到 %d 个候选维度", len(colorKeys))
|
||||
}
|
||||
var dimensionKey string
|
||||
for key := range colorKeys {
|
||||
dimensionKey = key
|
||||
}
|
||||
byValue := map[string]*PddColorCandidate{}
|
||||
for _, sku := range collected.SKUs {
|
||||
if !sku.Available || len(sku.Options) == 0 {
|
||||
continue
|
||||
}
|
||||
value := strings.TrimSpace(sku.Options[dimensionKey])
|
||||
if value == "" {
|
||||
return "", nil, fmt.Errorf("可购买规格组合缺少颜色维度 %s", dimensionKey)
|
||||
}
|
||||
item := byValue[value]
|
||||
if item == nil {
|
||||
item = &PddColorCandidate{DimensionKey: dimensionKey, Value: value}
|
||||
byValue[value] = item
|
||||
}
|
||||
item.SKUCount++
|
||||
if sku.PriceCent != nil && *sku.PriceCent > 0 {
|
||||
if !item.HasPrice || *sku.PriceCent < item.MinPriceCent {
|
||||
item.MinPriceCent = *sku.PriceCent
|
||||
}
|
||||
if !item.HasPrice || *sku.PriceCent > item.MaxPriceCent {
|
||||
item.MaxPriceCent = *sku.PriceCent
|
||||
}
|
||||
item.HasPrice = true
|
||||
}
|
||||
}
|
||||
if len(byValue) == 0 {
|
||||
return "", nil, fmt.Errorf("PDD 最新采集结果没有可购买的颜色候选")
|
||||
}
|
||||
result := make([]PddColorCandidate, 0, len(byValue))
|
||||
for _, item := range byValue {
|
||||
result = append(result, *item)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool { return result[i].Value < result[j].Value })
|
||||
return dimensionKey, result, nil
|
||||
}
|
||||
|
||||
func isExplicitPddColorDimension(raw string) bool {
|
||||
value := strings.ToLower(strings.Join(strings.Fields(strings.TrimSpace(raw)), ""))
|
||||
switch value {
|
||||
case "color", "colour", "color_family", "颜色", "颜色分类", "颜色選擇", "颜色选择",
|
||||
"顏色", "顏色分類", "顏色選擇":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func colorMappingContextVersion(context *ProductColorMappingContext, pddUpdatedAt, pddSKUsJSON string) string {
|
||||
type contextColor struct {
|
||||
Key, Raw string
|
||||
FormalCount, SybCount int
|
||||
}
|
||||
colors := make([]contextColor, 0, len(context.Rows))
|
||||
for _, row := range context.Rows {
|
||||
colors = append(colors, contextColor{row.Color.Key, row.Color.Raw, row.Color.FormalCount, row.Color.SybCount})
|
||||
}
|
||||
pddHash := sha256.Sum256([]byte(pddSKUsJSON))
|
||||
payload := struct {
|
||||
RulesVersion string
|
||||
ShopeeID string
|
||||
PddID string
|
||||
PddUpdatedAt string
|
||||
PddHash string
|
||||
Colors []contextColor
|
||||
}{ProductColorMappingRulesVersion, context.ShopeeGoodsID, context.PddGoodsID, pddUpdatedAt, hex.EncodeToString(pddHash[:]), colors}
|
||||
raw, _ := json.Marshal(payload)
|
||||
sum := sha256.Sum256(raw)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// SaveProductColorMappings 在一个事务中校验完整上下文,并保存本次变化的颜色。
|
||||
// 任何一行过期或无效都会使整批回滚。
|
||||
func SaveProductColorMappings(db *sql.DB, actor *model.User, goodsID, expectedContextVersion string, updates []ProductColorMappingUpdate) (int, error) {
|
||||
if actor == nil {
|
||||
return 0, ErrUnauthenticated
|
||||
}
|
||||
if actor.Status != model.UserActive {
|
||||
return 0, invalidInput("当前账号不是正常状态,不能保存颜色映射")
|
||||
}
|
||||
if len(updates) == 0 {
|
||||
return 0, invalidInput("没有需要保存的颜色变化")
|
||||
}
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("开始保存颜色映射事务失败: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
context, err := loadProductColorMappingContext(tx, strings.TrimSpace(goodsID))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if context == nil {
|
||||
return 0, invalidInput("蝦皮商品不存在或已删除")
|
||||
}
|
||||
if context.UnavailableReason != "" {
|
||||
return 0, invalidInput(context.UnavailableReason)
|
||||
}
|
||||
if expectedContextVersion == "" || context.ContextVersion != strings.TrimSpace(expectedContextVersion) {
|
||||
return 0, invalidInput("商品关联、规格或颜色来源已变化,请刷新后重新核对")
|
||||
}
|
||||
colors := make(map[string]ProductColorSource, len(context.Rows))
|
||||
for _, row := range context.Rows {
|
||||
colors[row.Color.Key] = row.Color
|
||||
}
|
||||
targets := make(map[string]bool, len(context.Candidates))
|
||||
for _, candidate := range context.Candidates {
|
||||
targets[candidate.Value] = true
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
changed, now := 0, model.NowISO()
|
||||
for _, update := range updates {
|
||||
colorKey := NormalizeProductColorKey(update.ShopeeColorKey)
|
||||
if colorKey == "" || seen[colorKey] {
|
||||
return 0, invalidInput("颜色变化中包含空值或重复项")
|
||||
}
|
||||
seen[colorKey] = true
|
||||
color, ok := colors[colorKey]
|
||||
if !ok {
|
||||
return 0, invalidInput("蝦皮颜色已不存在,请刷新后重试")
|
||||
}
|
||||
old, err := repository.GetProductColorMapping(tx, context.ShopeeGoodsID, colorKey, context.PddGoodsID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
target := strings.TrimSpace(update.TargetValue)
|
||||
if target == "" {
|
||||
if old == nil {
|
||||
continue
|
||||
}
|
||||
if _, err := repository.DeleteProductColorMapping(tx, context.ShopeeGoodsID, colorKey, context.PddGoodsID); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := repository.InsertProductColorMappingAudit(tx, model.ProductColorMappingAudit{
|
||||
ShopeeGoodsID: context.ShopeeGoodsID, ShopeeColorKey: colorKey, ShopeeColorRaw: color.Raw,
|
||||
PddGoodsID: context.PddGoodsID, Action: "clear", OldDimensionKey: old.PddDimensionKey,
|
||||
OldColorValue: old.PddColorValue, ContextVersion: context.ContextVersion,
|
||||
ActorUserID: actor.UserID, CreatedAt: now,
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
changed++
|
||||
continue
|
||||
}
|
||||
if !targets[target] {
|
||||
return 0, invalidInput("所选 PDD 颜色已不存在或不可购买,请刷新后重试")
|
||||
}
|
||||
if utf8.RuneCountInString(colorKey) > 191 || utf8.RuneCountInString(color.Raw) > 500 ||
|
||||
utf8.RuneCountInString(context.PddDimensionKey) > 191 || utf8.RuneCountInString(target) > 500 {
|
||||
return 0, invalidInput("颜色名称超过数据库允许长度,未保存")
|
||||
}
|
||||
if old != nil && old.PddDimensionKey == context.PddDimensionKey && old.PddColorValue == target {
|
||||
continue
|
||||
}
|
||||
item := model.ProductColorMapping{
|
||||
ShopeeGoodsID: context.ShopeeGoodsID, ShopeeColorKey: colorKey, ShopeeColorRaw: color.Raw,
|
||||
PddGoodsID: context.PddGoodsID, PddDimensionKey: context.PddDimensionKey,
|
||||
PddColorValue: target, ContextVersion: context.ContextVersion,
|
||||
MappedBy: actor.UserID, MappedAt: now,
|
||||
}
|
||||
if err := repository.UpsertProductColorMapping(tx, item); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
audit := model.ProductColorMappingAudit{
|
||||
ShopeeGoodsID: context.ShopeeGoodsID, ShopeeColorKey: colorKey, ShopeeColorRaw: color.Raw,
|
||||
PddGoodsID: context.PddGoodsID, Action: "upsert", NewDimensionKey: item.PddDimensionKey,
|
||||
NewColorValue: item.PddColorValue, ContextVersion: context.ContextVersion,
|
||||
ActorUserID: actor.UserID, CreatedAt: now,
|
||||
}
|
||||
if old != nil {
|
||||
audit.OldDimensionKey, audit.OldColorValue = old.PddDimensionKey, old.PddColorValue
|
||||
}
|
||||
if err := repository.InsertProductColorMappingAudit(tx, audit); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
changed++
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return 0, fmt.Errorf("提交颜色映射事务失败: %w", err)
|
||||
}
|
||||
return changed, nil
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"cmautobuy/admin/model"
|
||||
"cmautobuy/admin/repository"
|
||||
)
|
||||
|
||||
func TestNormalizeProductColorKey_只折叠空白(t *testing.T) {
|
||||
if got := NormalizeProductColorKey(" 寵粉\t誘惑-E01 "); got != "寵粉 誘惑-E01" {
|
||||
t.Fatalf("折叠空白结果=%q", got)
|
||||
}
|
||||
if got := NormalizeProductColorKey("寵粉誘惑-E01"); got != "寵粉誘惑-E01" {
|
||||
t.Fatalf("不应做繁简或同义词转换,实际=%q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAggregatePddColorCandidates_只统计可购买组合和整数价格区间(t *testing.T) {
|
||||
raw := `{"dimensions":[{"key":"color","name":"颜色分类"},{"key":"size","name":"尺码"}],"skus":[` +
|
||||
`{"options":{"color":"黑色","size":"M"},"price_cent":1180,"available":true},` +
|
||||
`{"options":{"color":"黑色","size":"L"},"price_cent":1280,"available":true},` +
|
||||
`{"options":{"color":"白色","size":"M"},"price_cent":999,"available":false}]}`
|
||||
key, candidates, err := aggregatePddColorCandidates(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if key != "color" || len(candidates) != 1 {
|
||||
t.Fatalf("颜色维度或候选错误 key=%q candidates=%+v", key, candidates)
|
||||
}
|
||||
got := candidates[0]
|
||||
if got.Value != "黑色" || got.SKUCount != 2 || !got.HasPrice || got.MinPriceCent != 1180 || got.MaxPriceCent != 1280 {
|
||||
t.Fatalf("颜色聚合错误:%+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAggregatePddColorCandidates_拒绝多个显式颜色维度(t *testing.T) {
|
||||
raw := `{"dimensions":[{"key":"color","name":"颜色"},{"key":"style","name":"顏色分類"}],` +
|
||||
`"skus":[{"options":{"color":"黑色","style":"标准"},"available":true}]}`
|
||||
if _, _, err := aggregatePddColorCandidates(raw); err == nil {
|
||||
t.Fatal("多个显式颜色维度必须阻止自动选择")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveProductColorMappings_保存审计并用上下文版本防过期(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
seedShopeeProduct(t, db, "S-COLOR", "颜色映射商品")
|
||||
seedShopeeSKU(t, db, "SKU-COLOR", "S-COLOR", "黑色,M", "黑色", "M", "", true)
|
||||
if _, err := repository.EnsurePddProduct(db, "P-COLOR", "https://mobile.yangkeduo.com/goods.html?goods_id=P-COLOR"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw := `{"dimensions":[{"key":"color","name":"颜色分类"},{"key":"size","name":"尺码"}],` +
|
||||
`"skus":[{"options":{"color":"曜石黑","size":"M"},"price_cent":1180,"available":true}]}`
|
||||
if _, err := db.Exec(`UPDATE pdd_products SET collect_status='collected',skus_json=?,updated_at=? WHERE goods_id='P-COLOR'`, raw, "2026-08-23T01:00:00Z"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
setShopeePddLink(t, db, "S-COLOR", "P-COLOR", "https://mobile.yangkeduo.com/goods.html?goods_id=P-COLOR")
|
||||
|
||||
context, err := GetProductColorMappingContext(db, "S-COLOR")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if context.UnavailableReason != "" || len(context.Rows) != 1 || len(context.Candidates) != 1 {
|
||||
t.Fatalf("颜色上下文不完整:%+v", context)
|
||||
}
|
||||
actor := &model.User{UserID: "U-COLOR", Status: model.UserActive}
|
||||
changed, err := SaveProductColorMappings(db, actor, "S-COLOR", context.ContextVersion,
|
||||
[]ProductColorMappingUpdate{{ShopeeColorKey: "黑色", TargetValue: "曜石黑"}})
|
||||
if err != nil || changed != 1 {
|
||||
t.Fatalf("保存颜色映射 changed=%d err=%v", changed, err)
|
||||
}
|
||||
mapping, err := repository.GetProductColorMapping(db, "S-COLOR", "黑色", "P-COLOR")
|
||||
if err != nil || mapping == nil || mapping.PddColorValue != "曜石黑" || mapping.MappedBy != actor.UserID {
|
||||
t.Fatalf("颜色映射读取错误 mapping=%+v err=%v", mapping, err)
|
||||
}
|
||||
var audits int
|
||||
if err := db.QueryRow(`SELECT COUNT(*) FROM product_color_mapping_audits WHERE shopee_goods_id='S-COLOR'`).Scan(&audits); err != nil || audits != 1 {
|
||||
t.Fatalf("审计记录=%d err=%v", audits, err)
|
||||
}
|
||||
if _, err := db.Exec(`UPDATE pdd_products SET updated_at=? WHERE goods_id='P-COLOR'`, "2026-08-23T02:00:00Z"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := SaveProductColorMappings(db, actor, "S-COLOR", context.ContextVersion,
|
||||
[]ProductColorMappingUpdate{{ShopeeColorKey: "黑色"}}); err == nil {
|
||||
t.Fatal("旧上下文版本必须拒绝整批保存")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user