fix: 确认回填PDD规格可用于采购 (#227)
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cmautobuy/admin/model"
|
||||
"cmautobuy/admin/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
XLSXAvailabilityEligible = "candidate"
|
||||
XLSXAvailabilityPDDMissing = "pdd_missing_or_deleted"
|
||||
XLSXAvailabilityStatusUnsupported = "status_not_collected"
|
||||
XLSXAvailabilityJSONInvalid = "pdd_json_invalid"
|
||||
XLSXAvailabilitySourceMismatch = "spec_source_not_shopee_backfill"
|
||||
XLSXAvailabilitySKUsMissing = "pdd_skus_missing"
|
||||
XLSXAvailabilitySpecsInvalid = "pdd_specs_invalid"
|
||||
XLSXAvailabilityPriceInvalid = "pdd_price_invalid"
|
||||
XLSXAvailabilityAlreadyDecided = "availability_already_decided"
|
||||
XLSXAvailabilityMarkerInvalid = "availability_marker_invalid"
|
||||
)
|
||||
|
||||
// XLSXAvailabilityCandidate 是一件可安全确认导入规格可用于采购的 PDD 商品。
|
||||
type XLSXAvailabilityCandidate struct {
|
||||
PDDGoodsID string
|
||||
SKUCount int
|
||||
Original repository.PDDPriceBackfillTarget
|
||||
NewSKUsJSON string
|
||||
}
|
||||
|
||||
// XLSXAvailabilityPlan 保存 dry-run 统计和事务更新需要的完整原值。
|
||||
type XLSXAvailabilityPlan struct {
|
||||
Shop string
|
||||
GeneratedAt string
|
||||
Sources XLSXPriceSources
|
||||
Summary map[string]int
|
||||
Candidates []XLSXAvailabilityCandidate
|
||||
}
|
||||
|
||||
// BuildXLSXAvailabilityPlan 仅在内存中生成三个固定 Excel 的可售确认计划。
|
||||
func BuildXLSXAvailabilityPlan(sources XLSXPriceSources,
|
||||
targets []repository.PDDPriceBackfillTarget, now time.Time) XLSXAvailabilityPlan {
|
||||
plan := XLSXAvailabilityPlan{
|
||||
Shop: XLSXPriceBackfillShop, GeneratedAt: now.UTC().Format(model.TimeLayout),
|
||||
Sources: sources, Summary: make(map[string]int),
|
||||
}
|
||||
targetByID := make(map[string]repository.PDDPriceBackfillTarget, len(targets))
|
||||
for _, target := range targets {
|
||||
targetByID[target.GoodsID] = target
|
||||
}
|
||||
goodsIDs := make([]string, 0, len(sources.PDDPrices))
|
||||
for goodsID := range sources.PDDPrices {
|
||||
goodsIDs = append(goodsIDs, goodsID)
|
||||
}
|
||||
sort.Strings(goodsIDs)
|
||||
for _, goodsID := range goodsIDs {
|
||||
target, exists := targetByID[goodsID]
|
||||
if !exists {
|
||||
plan.Summary[XLSXAvailabilityPDDMissing]++
|
||||
continue
|
||||
}
|
||||
if target.CollectStatus != string(model.CollectCollected) {
|
||||
plan.Summary[XLSXAvailabilityStatusUnsupported]++
|
||||
continue
|
||||
}
|
||||
newRaw, skuCount, status := confirmBackfilledPDDAvailability(target.SKUsJSON)
|
||||
if status != XLSXAvailabilityEligible {
|
||||
plan.Summary[status]++
|
||||
continue
|
||||
}
|
||||
plan.Summary[XLSXAvailabilityEligible]++
|
||||
plan.Candidates = append(plan.Candidates, XLSXAvailabilityCandidate{
|
||||
PDDGoodsID: goodsID, SKUCount: skuCount, Original: target, NewSKUsJSON: newRaw,
|
||||
})
|
||||
}
|
||||
return plan
|
||||
}
|
||||
|
||||
func confirmBackfilledPDDAvailability(raw string) (string, int, string) {
|
||||
var root map[string]json.RawMessage
|
||||
if err := json.Unmarshal([]byte(raw), &root); err != nil || root == nil {
|
||||
return "", 0, XLSXAvailabilityJSONInvalid
|
||||
}
|
||||
var source string
|
||||
if err := json.Unmarshal(root["spec_source"], &source); err != nil || source != "shopee_backfill" {
|
||||
return "", 0, XLSXAvailabilitySourceMismatch
|
||||
}
|
||||
var skus []map[string]json.RawMessage
|
||||
if err := json.Unmarshal(root["skus"], &skus); err != nil {
|
||||
return "", 0, XLSXAvailabilityJSONInvalid
|
||||
}
|
||||
if len(skus) == 0 {
|
||||
return "", 0, XLSXAvailabilitySKUsMissing
|
||||
}
|
||||
for _, sku := range skus {
|
||||
var options map[string]string
|
||||
if err := json.Unmarshal(sku["options"], &options); err != nil || len(options) == 0 {
|
||||
return "", 0, XLSXAvailabilitySpecsInvalid
|
||||
}
|
||||
for key, value := range options {
|
||||
if strings.TrimSpace(key) == "" || strings.TrimSpace(value) == "" {
|
||||
return "", 0, XLSXAvailabilitySpecsInvalid
|
||||
}
|
||||
}
|
||||
var price *int64
|
||||
priceRaw, hasPrice := sku["price_cent"]
|
||||
if !hasPrice || json.Unmarshal(priceRaw, &price) != nil || price == nil || *price <= 0 {
|
||||
return "", 0, XLSXAvailabilityPriceInvalid
|
||||
}
|
||||
if available, exists := sku["available"]; exists && strings.TrimSpace(string(available)) != "null" {
|
||||
return "", 0, XLSXAvailabilityAlreadyDecided
|
||||
}
|
||||
var marker string
|
||||
if err := json.Unmarshal(sku["availability_status"], &marker); err != nil || marker != "unknown" {
|
||||
return "", 0, XLSXAvailabilityMarkerInvalid
|
||||
}
|
||||
}
|
||||
for _, sku := range skus {
|
||||
sku["available"] = json.RawMessage("true")
|
||||
delete(sku, "availability_status")
|
||||
}
|
||||
encodedSKUs, err := json.Marshal(skus)
|
||||
if err != nil {
|
||||
return "", 0, XLSXAvailabilityJSONInvalid
|
||||
}
|
||||
root["skus"] = encodedSKUs
|
||||
encodedRoot, err := json.Marshal(root)
|
||||
if err != nil {
|
||||
return "", 0, XLSXAvailabilityJSONInvalid
|
||||
}
|
||||
return string(encodedRoot), len(skus), XLSXAvailabilityEligible
|
||||
}
|
||||
|
||||
// ApplyXLSXAvailabilityPlan 在一个事务中确认全部候选;任一原值变化则整批回滚。
|
||||
func ApplyXLSXAvailabilityPlan(db *sql.DB, plan XLSXAvailabilityPlan, now time.Time) (int, error) {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("开始确认 PDD 可售状态事务失败: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
updatedAt := now.UTC().Format(model.TimeLayout)
|
||||
updated := 0
|
||||
for _, candidate := range plan.Candidates {
|
||||
ok, err := repository.ReplaceBackfilledPDDAvailability(
|
||||
tx, candidate.Original, candidate.NewSKUsJSON, updatedAt)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("PDD 商品 %s 在 dry-run 后发生变化,已回滚整批可售确认", candidate.PDDGoodsID)
|
||||
}
|
||||
updated++
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return 0, fmt.Errorf("提交 PDD 可售状态确认失败: %w", err)
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
Reference in New Issue
Block a user