更新处理器
This commit is contained in:
237
cmd/product_prune/main.go
Normal file
237
cmd/product_prune/main.go
Normal file
@@ -0,0 +1,237 @@
|
||||
// 按产品导出 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user