Files
hyapi-server/cmd/product_prune/main.go

238 lines
7.0 KiB
Go
Raw Normal View History

2026-06-10 20:32:24 +08:00
// 按产品导出 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
}
}