164 lines
5.4 KiB
Go
164 lines
5.4 KiB
Go
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
|
|
}
|