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 }