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
|
|||
|
|
}
|
|||
|
|
}
|