// backfill-shop-pdd-specs 把指定蝦皮店铺的正式规格补成关联 PDD 的空规格骨架。 // 默认只预览;只有显式传入 --apply 才写数据库。 package main import ( "encoding/json" "flag" "fmt" "log" "os" "strings" "time" "cmautobuy/admin/config" "cmautobuy/admin/repository" "cmautobuy/admin/service" ) type backupFile struct { Shop string `json:"shop"` GeneratedAt string `json:"generated_at"` Items []backupItem `json:"items"` } type backupItem struct { PDDGoodsID string `json:"pdd_goods_id"` SKUsJSON string `json:"skus_json"` } func main() { shop := flag.String("shop", "", "要处理的蝦皮店铺名(精确匹配,必填)") apply := flag.Bool("apply", false, "实际写入;不传时只做 dry-run") flag.Parse() if strings.TrimSpace(*shop) == "" { log.Fatal("必须通过 --shop 指定店铺名") } cfg, err := config.LoadDatabase() if err != nil { log.Fatalf("读取 MySQL 配置失败: %v", err) } db, err := repository.OpenMySQL(cfg) if err != nil { log.Fatalf("连接 MySQL 失败: %v", err) } defer db.Close() sources, err := repository.ListShopSpecSources(db, *shop) if err != nil { log.Fatal(err) } now := time.Now() plan := service.BuildSpecBackfillPlan(*shop, sources, now) if !*apply { printReport(plan, 0, "dry-run") return } backupPath, err := writeBackup(plan) if err != nil { log.Fatalf("创建本机回退备份失败,未写数据库: %v", err) } updated, err := service.ApplySpecBackfill(db, plan, now) if err != nil { log.Fatalf("规格回填失败: %v;本机备份位于 %s", err, backupPath) } printReport(plan, updated, "applied") fmt.Printf("本机回退备份:%s\n", backupPath) } func printReport(plan service.SpecBackfillPlan, updated int, mode string) { summary, _ := json.Marshal(plan.Summary) fmt.Printf("模式:%s\n店铺:%s\n蝦皮商品:%d\n候选 PDD:%d\n已回填 PDD:%d\n本次更新 PDD:%d\n分类:%s\n", mode, plan.Shop, plan.SourceProductCount, plan.CandidatePDDCount, plan.BackfilledPDDCount, updated, summary) } func writeBackup(plan service.SpecBackfillPlan) (string, error) { file, err := os.CreateTemp("", "cmautobuy_spec_backfill_backup_*.json") if err != nil { return "", err } path := file.Name() defer file.Close() if err := file.Chmod(0o600); err != nil { return "", err } backup := backupFile{Shop: plan.Shop, GeneratedAt: time.Now().UTC().Format(time.RFC3339)} for _, candidate := range plan.Candidates { backup.Items = append(backup.Items, backupItem{PDDGoodsID: candidate.PDDGoodsID, SKUsJSON: candidate.OriginalSKUsJSON}) } encoder := json.NewEncoder(file) encoder.SetIndent("", " ") if err := encoder.Encode(backup); err != nil { return "", err } return path, nil }