Files
hyapi-server/cmd/product_prune/main.go
2026-06-10 20:32:24 +08:00

238 lines
7.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 按产品导出 JSON 清理数据库:软删除「导出中已下线」或「不在有效列表」的产品及其目录关联数据。
//
// go run ./cmd/product_prune -dry-run
// go run ./cmd/product_prune -export ../public_product_export_2026-06-10_163520.json
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"sort"
"strings"
"hyapi-server/internal/config"
"hyapi-server/internal/domains/product/entities"
"hyapi-server/internal/infrastructure/database"
"go.uber.org/zap"
"gorm.io/gorm"
)
type exportProduct struct {
ID string `json:"id"`
Code string `json:"code"`
DeletedAt *string `json:"deleted_at"`
}
func main() {
exportPath := flag.String("export", "../public_product_export_2026-06-10_163520.json", "产品导出 JSON 路径")
dryRun := flag.Bool("dry-run", false, "仅统计,不写库")
pruneSubscriptions := flag.Bool("prune-subscriptions", true, "同时软删除指向已清理产品的订阅")
flag.Parse()
activeCodes, err := loadActiveCodes(*exportPath)
if err != nil {
log.Fatalf("读取导出文件失败: %v", err)
}
log.Printf("导出中有效产品deleted_at 为空)共 %d 个", len(activeCodes))
cfg, err := config.LoadConfig()
if err != nil {
log.Fatalf("加载配置失败: %v", err)
}
logger, _ := zap.NewProduction()
defer logger.Sync()
dbCfg := database.Config{
Host: cfg.Database.Host,
Port: cfg.Database.Port,
User: cfg.Database.User,
Password: cfg.Database.Password,
Name: cfg.Database.Name,
SSLMode: cfg.Database.SSLMode,
Timezone: cfg.Database.Timezone,
MaxOpenConns: cfg.Database.MaxOpenConns,
MaxIdleConns: cfg.Database.MaxIdleConns,
ConnMaxLifetime: cfg.Database.ConnMaxLifetime,
}
dbWrapper, err := database.NewConnection(dbCfg)
if err != nil {
log.Fatalf("连接数据库失败: %v", err)
}
db := dbWrapper.DB
var products []entities.Product
if err := db.Unscoped().Select("id", "code", "deleted_at").Find(&products).Error; err != nil {
log.Fatalf("查询产品失败: %v", err)
}
var removeIDs []string
var removeCodes []string
for _, p := range products {
code := strings.TrimSpace(strings.ToUpper(p.Code))
if code == "" {
removeIDs = append(removeIDs, p.ID)
removeCodes = append(removeCodes, p.Code)
continue
}
if _, ok := activeCodes[code]; !ok {
removeIDs = append(removeIDs, p.ID)
removeCodes = append(removeCodes, p.Code)
}
}
sort.Strings(removeCodes)
log.Printf("数据库产品总数 %d待清理 %d", len(products), len(removeIDs))
if len(removeCodes) > 0 {
limit := len(removeCodes)
if limit > 30 {
limit = 30
}
log.Printf("待清理产品编号示例(前 %d 个): %s", limit, strings.Join(removeCodes[:limit], ", "))
}
if len(removeIDs) == 0 {
log.Println("无需清理")
return
}
if *dryRun {
log.Println("dry-run 模式,未修改数据库")
printRelatedCounts(db, removeIDs, *pruneSubscriptions)
return
}
err = db.Transaction(func(tx *gorm.DB) error {
if err := softDeleteByProductOrPackage(tx, &entities.ProductPackageItem{}, removeIDs); err != nil {
return fmt.Errorf("product_package_item: %w", err)
}
if err := softDeleteByProductID(tx, &entities.ProductDocumentation{}, removeIDs); err != nil {
return fmt.Errorf("product_documentation: %w", err)
}
if err := softDeleteByProductID(tx, &entities.ProductApiConfig{}, removeIDs); err != nil {
return fmt.Errorf("product_api_config: %w", err)
}
if err := softDeleteByProductID(tx, &entities.ProductParameter{}, removeIDs); err != nil {
return fmt.Errorf("product_parameter: %w", err)
}
if err := softDeleteByProductID(tx, &entities.ProductUIComponent{}, removeIDs); err != nil {
return fmt.Errorf("product_ui_component: %w", err)
}
if err := softDeleteByProductID(tx, &entities.ComponentReportDownload{}, removeIDs); err != nil {
return fmt.Errorf("component_report_download: %w", err)
}
if *pruneSubscriptions {
if err := softDeleteByProductID(tx, &entities.Subscription{}, removeIDs); err != nil {
return fmt.Errorf("subscription: %w", err)
}
}
res := tx.Where("id IN ?", removeIDs).Delete(&entities.Product{})
if res.Error != nil {
return fmt.Errorf("product: %w", res.Error)
}
log.Printf("已软删除 product %d 行", res.RowsAffected)
return nil
})
if err != nil {
log.Fatalf("清理失败: %v", err)
}
log.Println("清理完成")
}
func loadActiveCodes(path string) (map[string]struct{}, error) {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var rows []exportProduct
if err := json.Unmarshal(raw, &rows); err != nil {
return nil, err
}
active := make(map[string]struct{})
for _, r := range rows {
if !isExportActive(r.DeletedAt) {
continue
}
code := strings.TrimSpace(strings.ToUpper(r.Code))
if code != "" {
active[code] = struct{}{}
}
}
return active, nil
}
func isExportActive(deletedAt *string) bool {
if deletedAt == nil {
return true
}
s := strings.TrimSpace(*deletedAt)
return s == "" || strings.EqualFold(s, "null")
}
func softDeleteByProductID(tx *gorm.DB, model interface{}, productIDs []string) error {
if len(productIDs) == 0 {
return nil
}
res := tx.Where("product_id IN ?", productIDs).Delete(model)
if res.Error != nil {
return res.Error
}
if res.RowsAffected > 0 {
log.Printf("已软删除 %T %d 行", model, res.RowsAffected)
}
return nil
}
func softDeleteByProductOrPackage(tx *gorm.DB, model interface{}, productIDs []string) error {
if len(productIDs) == 0 {
return nil
}
res := tx.Where("product_id IN ? OR package_id IN ?", productIDs, productIDs).Delete(model)
if res.Error != nil {
return res.Error
}
if res.RowsAffected > 0 {
log.Printf("已软删除 %T %d 行", model, res.RowsAffected)
}
return nil
}
func printRelatedCounts(db *gorm.DB, productIDs []string, subs bool) {
checks := []struct {
name string
fn func() int64
}{
{"product_package_item", func() int64 {
var n int64
db.Model(&entities.ProductPackageItem{}).Where("product_id IN ? OR package_id IN ?", productIDs, productIDs).Count(&n)
return n
}},
{"product_documentation", countByProduct(db, &entities.ProductDocumentation{}, productIDs)},
{"product_api_config", countByProduct(db, &entities.ProductApiConfig{}, productIDs)},
{"product_parameter", countByProduct(db, &entities.ProductParameter{}, productIDs)},
{"product_ui_component", countByProduct(db, &entities.ProductUIComponent{}, productIDs)},
{"component_report_download", countByProduct(db, &entities.ComponentReportDownload{}, productIDs)},
}
if subs {
checks = append(checks, struct {
name string
fn func() int64
}{"subscription", countByProduct(db, &entities.Subscription{}, productIDs)})
}
for _, c := range checks {
log.Printf("[dry-run] 关联 %s 约 %d 行", c.name, c.fn())
}
}
func countByProduct(db *gorm.DB, model interface{}, productIDs []string) func() int64 {
return func() int64 {
var n int64
db.Model(model).Where("product_id IN ?", productIDs).Count(&n)
return n
}
}