238 lines
7.0 KiB
Go
238 lines
7.0 KiB
Go
// 按产品导出 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
|
||
}
|
||
}
|