update
This commit is contained in:
@ -70,6 +70,7 @@ import (
|
||||
"github.com/jackc/pgx/v5/stdlib"
|
||||
|
||||
"fashionapi/internal/config"
|
||||
"fashionapi/internal/service"
|
||||
)
|
||||
|
||||
// column 是单列的结构信息,用于生成 CREATE TABLE、INSERT 列清单与序列重置语句。
|
||||
@ -110,6 +111,8 @@ func main() {
|
||||
runImport(os.Args[2:])
|
||||
case "fixdup":
|
||||
runFixDup(os.Args[2:])
|
||||
case "filltitlecn":
|
||||
runFillTitleCn(os.Args[2:])
|
||||
case "purge-rejected":
|
||||
runPurgeRejected(os.Args[2:])
|
||||
case "migrate":
|
||||
@ -128,8 +131,13 @@ func usage() {
|
||||
导出「结构 + 主键 / 索引 / 视图 + 数据」为单个纯 SQL 文件
|
||||
dbtool import [-config <yml>] [-in <file>] [-clean]
|
||||
把 dump 出来的 SQL 文件灌入目标库
|
||||
dbtool fixdup [-config <yml>]
|
||||
dbtool fixdup [-config <yml>] [-dry-run]
|
||||
清理「悬空」重复标记(is_duplicate=1 但其源图已删/软删),修编辑页误显「重复」
|
||||
-dry-run 只统计/列出将受影响的行,不实际改写(推荐先跑一次确认)
|
||||
dbtool filltitlecn [-config <yml>] [-dry-run]
|
||||
历史回填中文标题:扫描 brand_runways / street_snaps 里 title_cn 为空且英文标题
|
||||
非空的存活记录,用与入库同源的规则(记忆优先、规则兜底)补全;规则无法完整翻译
|
||||
的行跳过(不写脏数据)。-dry-run 只统计/列出,不实际改写(推荐先跑一次确认)
|
||||
dbtool purge-rejected [-config <yml>]
|
||||
把现存「已驳回(rejected)」且未下架的记录转删除:级联软删其全部图片、记录置
|
||||
is_deleted=1(后台即消失),并把图片 key 入队 media_cleanup,由运行中的 worker
|
||||
@ -304,9 +312,17 @@ func runImport(args []string) {
|
||||
// (is_deleted=1)。这类陈旧标记会让编辑页误显「重复」且源图打不开。仅清悬空项,真实近重复
|
||||
// (源图仍在且存活)不受影响。等价于 DeleteImage 触发 ReresolveRunwayDedup 的重算效果之一,
|
||||
// 但可一次性全局修历史遗留数据,无需逐张删图触发。
|
||||
// runFixDup 清理「悬空」重复标记:is_duplicate=1 但 dup_of 指向的源图行已不存在或已被软删
|
||||
// (is_deleted=1)。这类陈旧标记会让编辑页误显「重复」且源图打不开。仅清悬空项,真实近重复
|
||||
// (源图仍在且存活)不受影响。等价于 DeleteImage 触发 ReresolveRunwayDedup 的重算效果之一,
|
||||
// 但可一次性全局修历史遗留数据,无需逐张删图触发。
|
||||
//
|
||||
// -dry-run 只统计并打印将受影响的行(含其 dup_of,便于确认源确已消失),不实际改写;
|
||||
// 不带该参数则直接 UPDATE。
|
||||
func runFixDup(args []string) {
|
||||
fs := flag.NewFlagSet("fixdup", flag.ExitOnError)
|
||||
cfgPath := fs.String("config", "", "配置文件路径(默认 configs/config.yml)")
|
||||
dryRun := fs.Bool("dry-run", false, "只统计并打印将受影响的行,不实际改写")
|
||||
_ = fs.Parse(args)
|
||||
|
||||
db, _, err := openDB(*cfgPath)
|
||||
@ -316,12 +332,53 @@ func runFixDup(args []string) {
|
||||
defer db.Close()
|
||||
|
||||
for _, t := range []string{"brand_runway_images", "street_snap_images"} {
|
||||
// 悬空判定:标了重复,但其 dup_of 为空 / 为 NULL / 不在「存活源」集合里。
|
||||
// 子查询只取 is_deleted=0 的 id,因此「源已软删」与「源行已不存在」两种情况都被覆盖。
|
||||
where := `is_duplicate = 1
|
||||
AND (dup_of IS NULL OR dup_of = '' OR dup_of NOT IN (
|
||||
SELECT CAST(id AS text) FROM public.` + qi(t) + ` WHERE is_deleted = 0))`
|
||||
|
||||
if *dryRun {
|
||||
var cnt int
|
||||
if err := db.QueryRow(`SELECT count(*) FROM public.` + qi(t) + ` WHERE ` + where).Scan(&cnt); err != nil {
|
||||
log.Fatalf("✗ 统计 %s 失败: %v", t, err)
|
||||
}
|
||||
log.Printf("→ %s: 将清理悬空重复标记 %d 行(dry-run,未改写)", t, cnt)
|
||||
const sampleLimit = 50
|
||||
if cnt > 0 {
|
||||
rows, qerr := db.Query(fmt.Sprintf(
|
||||
`SELECT id, dup_of FROM public.%s WHERE %s ORDER BY id LIMIT %d`, qi(t), where, sampleLimit))
|
||||
if qerr != nil {
|
||||
log.Fatalf("✗ 列举 %s 失败: %v", t, qerr)
|
||||
}
|
||||
type aff struct {
|
||||
ID uint32
|
||||
DupOf string
|
||||
}
|
||||
var list []aff
|
||||
for rows.Next() {
|
||||
var a aff
|
||||
if err := rows.Scan(&a.ID, &a.DupOf); err != nil {
|
||||
rows.Close()
|
||||
log.Fatalf("✗ 扫描 %s 失败: %v", t, err)
|
||||
}
|
||||
list = append(list, a)
|
||||
}
|
||||
rows.Close()
|
||||
for _, a := range list {
|
||||
log.Printf(" id=%d dup_of=%q", a.ID, a.DupOf)
|
||||
}
|
||||
if cnt > sampleLimit {
|
||||
log.Printf(" …(仅显示前 %d 行,共 %d 行)", sampleLimit, cnt)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`
|
||||
UPDATE public.%[1]s
|
||||
SET is_duplicate = 0, dup_of = ''
|
||||
WHERE is_duplicate = 1
|
||||
AND (dup_of IS NULL OR dup_of = '' OR dup_of NOT IN (
|
||||
SELECT CAST(id AS text) FROM public.%[1]s WHERE is_deleted = 0));`, qi(t))
|
||||
WHERE %[2]s;`, qi(t), where)
|
||||
res, err := db.Exec(q)
|
||||
if err != nil {
|
||||
log.Fatalf("✗ 清理 %s 失败: %v", t, err)
|
||||
@ -337,6 +394,128 @@ WHERE is_duplicate = 1
|
||||
//
|
||||
// 与后台点「删除(清空 S4)」完全等价(后者走 articleService/streetSnapService.SetDeleted →
|
||||
// PurgeGallery),但本命令一次性全局处理历史遗留的 rejected 数据,无需逐条手动操作。
|
||||
// runFillTitleCn 历史回填中文标题:扫描两张表里 title_cn 为空且英文标题非空的存活记录,
|
||||
// 用与入库同源的规则补全(记忆优先:先查 title_cn_memory;否则规则兜底)。
|
||||
// 规则无法完整翻译的行跳过(不写脏数据)。-dry-run 只统计/列出,不实际改写。
|
||||
func runFillTitleCn(args []string) {
|
||||
fs := flag.NewFlagSet("filltitlecn", flag.ExitOnError)
|
||||
cfgPath := fs.String("config", "", "配置文件路径(默认 configs/config.yml)")
|
||||
dryRun := fs.Bool("dry-run", false, "只统计/列出将受影响的行,不实际改写")
|
||||
_ = fs.Parse(args)
|
||||
|
||||
db, _, err := openDB(*cfgPath)
|
||||
if err != nil {
|
||||
log.Fatalf("✗ %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 加载品牌映射与翻译记忆(与入库/编辑同源)。
|
||||
brandCN, err := loadBrandCNMap(db)
|
||||
if err != nil {
|
||||
log.Fatalf("✗ 加载品牌映射失败: %v", err)
|
||||
}
|
||||
mem, err := loadTitleCnMemory(db)
|
||||
if err != nil {
|
||||
log.Fatalf("✗ 加载翻译记忆失败: %v", err)
|
||||
}
|
||||
|
||||
tables := []struct {
|
||||
table, enCol, cnCol, idCol string
|
||||
}{
|
||||
{"brand_runways", "title_en", "title_cn", "id"},
|
||||
{"street_snaps", "title", "title_cn", "id"},
|
||||
}
|
||||
for _, t := range tables {
|
||||
rows, qerr := db.QueryContext(context.Background(), fmt.Sprintf(
|
||||
`SELECT %[1]s, %[2]s FROM public.%[3]s WHERE is_deleted = 0 AND (%[4]s IS NULL OR %[4]s = '') AND %[2]s IS NOT NULL AND %[2]s <> ''`,
|
||||
qi(t.idCol), qi(t.enCol), qi(t.table), qi(t.cnCol)))
|
||||
if qerr != nil {
|
||||
log.Fatalf("✗ 扫描 %s 失败: %v", t.table, qerr)
|
||||
}
|
||||
type aff struct{ ID uint32; En, Cn string }
|
||||
var list []aff
|
||||
for rows.Next() {
|
||||
var id uint32
|
||||
var en string
|
||||
if err := rows.Scan(&id, &en); err != nil {
|
||||
rows.Close()
|
||||
log.Fatalf("✗ 扫描 %s 失败: %v", t.table, err)
|
||||
}
|
||||
if cn, ok := mem[en]; ok && cn != "" {
|
||||
list = append(list, aff{id, en, cn})
|
||||
continue
|
||||
}
|
||||
if cn, ok := service.RuleTranslate(en, brandCN); ok && cn != "" {
|
||||
list = append(list, aff{id, en, cn})
|
||||
}
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
if *dryRun {
|
||||
log.Printf("→ %s: 将补全中文标题 %d 行(dry-run,未改写)", t.table, len(list))
|
||||
const limit = 50
|
||||
for i, a := range list {
|
||||
if i >= limit {
|
||||
log.Printf(" …(仅显示前 %d 行,共 %d 行)", limit, len(list))
|
||||
break
|
||||
}
|
||||
log.Printf(" id=%d %q → %q", a.ID, a.En, a.Cn)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var updated int
|
||||
for _, a := range list {
|
||||
res, uerr := db.ExecContext(context.Background(), fmt.Sprintf(
|
||||
`UPDATE public.%s SET %s = $1 WHERE %s = $2`, qi(t.table), qi(t.cnCol), qi(t.idCol)),
|
||||
a.Cn, a.ID)
|
||||
if uerr != nil {
|
||||
log.Fatalf("✗ 更新 %s id=%d 失败: %v", t.table, a.ID, uerr)
|
||||
}
|
||||
if n, _ := res.RowsAffected(); n > 0 {
|
||||
updated++
|
||||
}
|
||||
}
|
||||
log.Printf("✓ %s: 补全中文标题 %d 行", t.table, updated)
|
||||
}
|
||||
}
|
||||
|
||||
// loadBrandCNMap 取全部存活品牌的 name_en(小写)→name_cn 映射(与入库翻译共用)。
|
||||
func loadBrandCNMap(db *sql.DB) (map[string]string, error) {
|
||||
rows, err := db.QueryContext(context.Background(), `SELECT LOWER(name_en), name_cn FROM public.brands WHERE is_deleted = 0 AND name_en <> ''`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
m := map[string]string{}
|
||||
for rows.Next() {
|
||||
var en, cn string
|
||||
if err := rows.Scan(&en, &cn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m[en] = strings.TrimSpace(cn)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// loadTitleCnMemory 取人工翻译记忆(英文标题→中文标题),回填时优先命中。
|
||||
func loadTitleCnMemory(db *sql.DB) (map[string]string, error) {
|
||||
rows, err := db.QueryContext(context.Background(), `SELECT title_en, title_cn FROM public.title_cn_memory`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
m := map[string]string{}
|
||||
for rows.Next() {
|
||||
var en, cn string
|
||||
if err := rows.Scan(&en, &cn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m[en] = cn
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func runPurgeRejected(args []string) {
|
||||
fs := flag.NewFlagSet("purge-rejected", flag.ExitOnError)
|
||||
cfgPath := fs.String("config", "", "配置文件路径(默认 configs/config.yml)")
|
||||
|
||||
Reference in New Issue
Block a user