Files
cmautobuy/admin/service/xlsx_availability_confirmation.go
T

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
}