feat: 增加商品颜色匹配页面 (#294)
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ProductColorMappingOption 是原生 select 中的一个可购买 PDD 颜色。
|
||||
type ProductColorMappingOption struct {
|
||||
Value, Label string
|
||||
Selected bool
|
||||
}
|
||||
|
||||
// ProductColorMappingOptionGroup 用 optgroup 表达当前、未使用和已匹配三组候选。
|
||||
type ProductColorMappingOptionGroup struct {
|
||||
Label string
|
||||
Options []ProductColorMappingOption
|
||||
}
|
||||
|
||||
// ProductColorMappingPageRow 是正式页面的一行。
|
||||
type ProductColorMappingPageRow struct {
|
||||
ColorKey, ColorRaw, SourceText string
|
||||
OriginalValue, SelectedValue string
|
||||
Status, StatusText string
|
||||
MappingText string
|
||||
SelectedInvalid bool
|
||||
Groups []ProductColorMappingOptionGroup
|
||||
}
|
||||
|
||||
// ProductColorMappingPageView 只负责把领域上下文翻译成模板可直接显示的数据。
|
||||
type ProductColorMappingPageView struct {
|
||||
ShopeeGoodsID, ShopeeTitle, PddGoodsID, PddTitle string
|
||||
PddDimensionKey, ContextVersion string
|
||||
UnavailableReason string
|
||||
Rows []ProductColorMappingPageRow
|
||||
MappedCount, ValidCount, InvalidCount int
|
||||
}
|
||||
|
||||
// BuildProductColorMappingPageView 生成分组候选。overrides 只用于 POST 失败后回显,
|
||||
// 不会改变服务端候选或有效性判断。
|
||||
func BuildProductColorMappingPageView(context *ProductColorMappingContext, overrides map[string]string) *ProductColorMappingPageView {
|
||||
view := &ProductColorMappingPageView{
|
||||
ShopeeGoodsID: context.ShopeeGoodsID, ShopeeTitle: context.ShopeeTitle,
|
||||
PddGoodsID: context.PddGoodsID, PddTitle: context.PddTitle,
|
||||
PddDimensionKey: context.PddDimensionKey, ContextVersion: context.ContextVersion,
|
||||
UnavailableReason: context.UnavailableReason,
|
||||
}
|
||||
occupiedBy := map[string][]string{}
|
||||
for _, row := range context.Rows {
|
||||
if row.Mapping != nil && row.MappingValid {
|
||||
occupiedBy[row.Mapping.PddColorValue] = append(occupiedBy[row.Mapping.PddColorValue], row.Color.Raw)
|
||||
}
|
||||
}
|
||||
for value := range occupiedBy {
|
||||
sort.Strings(occupiedBy[value])
|
||||
}
|
||||
candidateByValue := make(map[string]PddColorCandidate, len(context.Candidates))
|
||||
for _, candidate := range context.Candidates {
|
||||
candidateByValue[candidate.Value] = candidate
|
||||
}
|
||||
for _, sourceRow := range context.Rows {
|
||||
row := ProductColorMappingPageRow{
|
||||
ColorKey: sourceRow.Color.Key, ColorRaw: sourceRow.Color.Raw,
|
||||
SourceText: sourceRow.Color.SourceText(), Status: "pending", StatusText: "待匹配",
|
||||
}
|
||||
if sourceRow.Mapping != nil {
|
||||
row.OriginalValue = sourceRow.Mapping.PddColorValue
|
||||
row.SelectedValue = sourceRow.Mapping.PddColorValue
|
||||
row.MappingText = "人工维护"
|
||||
if strings.TrimSpace(sourceRow.Mapping.MappedBy) != "" {
|
||||
row.MappingText += " · " + sourceRow.Mapping.MappedBy
|
||||
}
|
||||
if sourceRow.MappingValid {
|
||||
row.Status, row.StatusText = "mapped", "已匹配"
|
||||
view.ValidCount++
|
||||
} else {
|
||||
row.Status, row.StatusText, row.SelectedInvalid = "invalid", "映射已失效", true
|
||||
view.InvalidCount++
|
||||
}
|
||||
view.MappedCount++
|
||||
}
|
||||
if selected, ok := overrides[sourceRow.Color.Key]; ok {
|
||||
row.SelectedValue = strings.TrimSpace(selected)
|
||||
_, targetExists := candidateByValue[row.SelectedValue]
|
||||
row.SelectedInvalid = row.SelectedValue != "" && !targetExists
|
||||
}
|
||||
row.Groups = buildColorMappingOptionGroups(context.Candidates, occupiedBy, row.SelectedValue)
|
||||
view.Rows = append(view.Rows, row)
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
func buildColorMappingOptionGroups(candidates []PddColorCandidate, occupiedBy map[string][]string, selected string) []ProductColorMappingOptionGroup {
|
||||
var current, unused, used []ProductColorMappingOption
|
||||
for _, candidate := range candidates {
|
||||
label := pddColorCandidateLabel(candidate)
|
||||
option := ProductColorMappingOption{Value: candidate.Value, Label: label, Selected: candidate.Value == selected}
|
||||
if candidate.Value == selected {
|
||||
current = append(current, option)
|
||||
continue
|
||||
}
|
||||
if colors := occupiedBy[candidate.Value]; len(colors) > 0 {
|
||||
option.Label += "(已匹配:" + strings.Join(colors, " / ") + ")"
|
||||
used = append(used, option)
|
||||
} else {
|
||||
unused = append(unused, option)
|
||||
}
|
||||
}
|
||||
groups := make([]ProductColorMappingOptionGroup, 0, 3)
|
||||
if len(current) > 0 {
|
||||
groups = append(groups, ProductColorMappingOptionGroup{Label: "当前选择", Options: current})
|
||||
}
|
||||
if len(unused) > 0 {
|
||||
groups = append(groups, ProductColorMappingOptionGroup{Label: "未使用的颜色", Options: unused})
|
||||
}
|
||||
if len(used) > 0 {
|
||||
groups = append(groups, ProductColorMappingOptionGroup{Label: "已经匹配(仍可选择)", Options: used})
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
func pddColorCandidateLabel(candidate PddColorCandidate) string {
|
||||
price := "价格未采到"
|
||||
if candidate.HasPrice {
|
||||
if candidate.MinPriceCent == candidate.MaxPriceCent {
|
||||
price = fmt.Sprintf("¥%.2f", float64(candidate.MinPriceCent)/100)
|
||||
} else {
|
||||
price = fmt.Sprintf("¥%.2f–%.2f", float64(candidate.MinPriceCent)/100, float64(candidate.MaxPriceCent)/100)
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%s · %s · %d 个可购买规格", candidate.Value, price, candidate.SKUCount)
|
||||
}
|
||||
|
||||
// ProductColorMappingStatusLine 返回页面底部的短状态。
|
||||
func ProductColorMappingStatusLine(view *ProductColorMappingPageView) string {
|
||||
if view.UnavailableReason != "" {
|
||||
return view.UnavailableReason
|
||||
}
|
||||
return fmt.Sprintf("共 %d 个蝦皮颜色 · 已匹配 %d · 待匹配 %d · 已失效 %d",
|
||||
len(view.Rows), view.ValidCount, len(view.Rows)-view.MappedCount, view.InvalidCount)
|
||||
}
|
||||
|
||||
// ProductColorMappingHasChanges 供测试明确局部保存的比较规则。
|
||||
func ProductColorMappingHasChanges(row ProductColorMappingPageRow) bool {
|
||||
return row.OriginalValue != row.SelectedValue
|
||||
}
|
||||
@@ -42,6 +42,30 @@ func TestAggregatePddColorCandidates_拒绝多个显式颜色维度(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildProductColorMappingPageView_未使用优先且已使用仍可多对一(t *testing.T) {
|
||||
context := &ProductColorMappingContext{
|
||||
ShopeeGoodsID: "S-1", PddGoodsID: "P-1", PddDimensionKey: "color", ContextVersion: "ctx",
|
||||
Candidates: []PddColorCandidate{
|
||||
{DimensionKey: "color", Value: "曜石黑", HasPrice: true, MinPriceCent: 1180, MaxPriceCent: 1280, SKUCount: 2},
|
||||
{DimensionKey: "color", Value: "奶油白", SKUCount: 1},
|
||||
},
|
||||
Rows: []ProductColorMappingRow{
|
||||
{Color: ProductColorSource{Key: "黑色", Raw: "黑色", FormalCount: 1}, MappingValid: true,
|
||||
Mapping: &model.ProductColorMapping{PddColorValue: "曜石黑", PddDimensionKey: "color", MappedBy: "U-1"}},
|
||||
{Color: ProductColorSource{Key: "深黑", Raw: "深黑", SybCount: 2}},
|
||||
},
|
||||
}
|
||||
view := BuildProductColorMappingPageView(context, map[string]string{"深黑": "曜石黑"})
|
||||
if view.ValidCount != 1 || len(view.Rows) != 2 || !ProductColorMappingHasChanges(view.Rows[1]) {
|
||||
t.Fatalf("页面汇总或失败回显不正确:%+v", view)
|
||||
}
|
||||
groups := view.Rows[1].Groups
|
||||
if len(groups) != 2 || groups[0].Label != "当前选择" || groups[0].Options[0].Value != "曜石黑" ||
|
||||
groups[1].Label != "未使用的颜色" || groups[1].Options[0].Value != "奶油白" {
|
||||
t.Fatalf("候选分组错误:%+v", groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveProductColorMappings_保存审计并用上下文版本防过期(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
seedShopeeProduct(t, db, "S-COLOR", "颜色映射商品")
|
||||
|
||||
Reference in New Issue
Block a user