update
This commit is contained in:
@ -1,206 +0,0 @@
|
||||
// Command backfill_city 给 street_snap 加 city 列并从 title(首选)/ source_url(兜底)回填。
|
||||
//
|
||||
// 项目已移除 AutoMigrate,表结构由外部 SQL 托管(见 scripts/sql/004_add_streetsnap_city.sql)。
|
||||
// 本程序是「没有 mysql 客户端时」的执行器:
|
||||
// - ALTER 走 ensureColumn:先查 information_schema,缺才加,可安全重跑(幂等)。
|
||||
// - 回填:优先从 title 第一段(location,\t 分隔)按城市关键词匹配;
|
||||
// 命中不到再退到 source_url / cover 的 URL slug 解析。幂等(已填过的不会覆盖)。
|
||||
//
|
||||
// 存储的 city 值即「规范英文名首字母大写」(Paris / New York …),
|
||||
// 与前端街拍列表 City 筛选项 value、header 下拉 ?city= 参数完全一致(精确匹配)。
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const dsn = "root:root@tcp(127.0.0.1:3306)/db_dev?charset=utf8mb4&parseTime=True&loc=Local"
|
||||
|
||||
// cityKeywords 城市关键词 → 规范存储值(首字母大写)。
|
||||
// 顺序无关;extractCityFromTitle 用「包含」匹配,可容忍 "Paris Fashion Week" / "New York FW25" 等形式。
|
||||
var cityKeywords = []struct {
|
||||
kw string
|
||||
city string
|
||||
}{
|
||||
{"paris", "Paris"},
|
||||
{"milan", "Milan"},
|
||||
{"london", "London"},
|
||||
{"new york", "New York"},
|
||||
{"nyfw", "New York"},
|
||||
{"tokyo", "Tokyo"},
|
||||
{"seoul", "Seoul"},
|
||||
{"florence", "Florence"},
|
||||
{"shanghai", "Shanghai"},
|
||||
{"copenhagen", "Copenhagen"},
|
||||
{"berlin", "Berlin"},
|
||||
{"rome", "Rome"},
|
||||
{"madrid", "Madrid"},
|
||||
{"amsterdam", "Amsterdam"},
|
||||
{"antwerp", "Antwerp"},
|
||||
{"los angeles", "Los Angeles"},
|
||||
{"moscow", "Moscow"},
|
||||
{"mumbai", "Mumbai"},
|
||||
{"beijing", "Beijing"},
|
||||
}
|
||||
|
||||
// citySlug(URL 兜底用):slug → 规范存储值。多词城市(new-york)必须排在单词组之前被 HasPrefix 命中。
|
||||
var citySlug = map[string]string{
|
||||
"paris": "Paris",
|
||||
"milan": "Milan",
|
||||
"london": "London",
|
||||
"new-york": "New York",
|
||||
"tokyo": "Tokyo",
|
||||
"seoul": "Seoul",
|
||||
"copenhagen": "Copenhagen",
|
||||
"florence": "Florence",
|
||||
"berlin": "Berlin",
|
||||
"shanghai": "Shanghai",
|
||||
"beijing": "Beijing",
|
||||
"moscow": "Moscow",
|
||||
"los-angeles": "Los Angeles",
|
||||
"rome": "Rome",
|
||||
"madrid": "Madrid",
|
||||
"amsterdam": "Amsterdam",
|
||||
"antwerp": "Antwerp",
|
||||
"mumbai": "Mumbai",
|
||||
}
|
||||
|
||||
// extractCityFromTitle 从 title 第一段(location,\t 分隔)按关键词匹配规范城市名。
|
||||
func extractCityFromTitle(title string) string {
|
||||
seg := title
|
||||
if i := strings.Index(title, "\t"); i >= 0 {
|
||||
seg = title[:i]
|
||||
}
|
||||
segL := strings.ToLower(strings.TrimSpace(seg))
|
||||
if segL == "" {
|
||||
return ""
|
||||
}
|
||||
for _, c := range cityKeywords {
|
||||
if strings.Contains(segL, c.kw) {
|
||||
return c.city
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractCityFromURL 兜底:从 source_url / cover 的 URL 第一段 slug 解析城市。
|
||||
func extractCityFromURL(raws ...string) string {
|
||||
for _, raw := range raws {
|
||||
u := strings.TrimSpace(raw)
|
||||
if u == "" {
|
||||
continue
|
||||
}
|
||||
if i := strings.Index(u, "://"); i >= 0 {
|
||||
u = u[i+3:]
|
||||
}
|
||||
if i := strings.Index(u, "/"); i >= 0 {
|
||||
u = u[i+1:]
|
||||
}
|
||||
u = strings.Trim(u, "/")
|
||||
if u == "" {
|
||||
continue
|
||||
}
|
||||
seg := strings.SplitN(u, "/", 2)[0]
|
||||
segL := strings.ToLower(seg)
|
||||
for slug, city := range citySlug {
|
||||
if segL == slug || strings.HasPrefix(segL, slug+"-") {
|
||||
return city
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatal("open:", err)
|
||||
}
|
||||
|
||||
// 1) 幂等加列(与 004_add_streetsnap_city.sql 对齐)
|
||||
ensureColumn(db, "street_snap", "city", "VARCHAR(128) NOT NULL DEFAULT '' AFTER year")
|
||||
|
||||
// 2) 逐行回填:优先 title 第一段,兜底 URL slug
|
||||
type row struct {
|
||||
ID uint32
|
||||
Title string
|
||||
SourceURL string
|
||||
Cover string
|
||||
}
|
||||
var rows []row
|
||||
if r := db.Raw("SELECT id, title, source_url, cover FROM street_snap WHERE is_deleted=0 AND (city IS NULL OR city='')").Scan(&rows); r.Error != nil {
|
||||
log.Fatal("select:", r.Error)
|
||||
}
|
||||
var updated int64
|
||||
for _, r := range rows {
|
||||
city := extractCityFromTitle(r.Title)
|
||||
if city == "" {
|
||||
city = extractCityFromURL(r.SourceURL, r.Cover)
|
||||
}
|
||||
if city == "" {
|
||||
continue
|
||||
}
|
||||
if r2 := db.Exec("UPDATE street_snap SET city=? WHERE id=?", city, r.ID); r2.Error != nil {
|
||||
log.Printf("update id=%d failed: %v", r.ID, r2.Error)
|
||||
continue
|
||||
}
|
||||
updated++
|
||||
}
|
||||
fmt.Printf(" updated city for %d rows\n", updated)
|
||||
|
||||
// 3) 校验:输出 distinct city 分布 + 未归类样本
|
||||
printCities(db)
|
||||
printUnclassifiedSamples(db)
|
||||
fmt.Println("backfill city done.")
|
||||
}
|
||||
|
||||
func ensureColumn(db *gorm.DB, table, col, def string) {
|
||||
var n int64
|
||||
db.Raw("SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='db_dev' AND TABLE_NAME=? AND COLUMN_NAME=?", table, col).Scan(&n)
|
||||
if n > 0 {
|
||||
fmt.Printf(" skip %s.%s (exists)\n", table, col)
|
||||
return
|
||||
}
|
||||
stmt := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s", table, col, def)
|
||||
fmt.Println(" +", stmt)
|
||||
if r := db.Exec(stmt); r.Error != nil {
|
||||
log.Fatalf("alter %s.%s failed: %v", table, col, r.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func printCities(db *gorm.DB) {
|
||||
type row struct {
|
||||
City string
|
||||
Cnt int64
|
||||
}
|
||||
var rows []row
|
||||
db.Raw(`SELECT city, COUNT(*) cnt FROM street_snap WHERE is_deleted=0 AND city<>'' GROUP BY city ORDER BY cnt DESC`).Scan(&rows)
|
||||
fmt.Printf(" distinct cities: %d\n", len(rows))
|
||||
for _, r := range rows {
|
||||
fmt.Printf(" %-14q %d\n", r.City, r.Cnt)
|
||||
}
|
||||
}
|
||||
|
||||
// printUnclassifiedSamples 输出仍未归类行的 title 第一段样本,便于后续补充城市词典。
|
||||
func printUnclassifiedSamples(db *gorm.DB) {
|
||||
type row struct {
|
||||
Title string
|
||||
}
|
||||
var rows []row
|
||||
db.Raw(`SELECT title FROM street_snap WHERE is_deleted=0 AND (city IS NULL OR city='') LIMIT 25`).Scan(&rows)
|
||||
var total int64
|
||||
db.Raw(`SELECT COUNT(*) FROM street_snap WHERE is_deleted=0 AND (city IS NULL OR city='')`).Scan(&total)
|
||||
fmt.Printf(" unclassified rows: %d (sample locations below)\n", total)
|
||||
for _, r := range rows {
|
||||
loc := r.Title
|
||||
if i := strings.Index(loc, "\t"); i >= 0 {
|
||||
loc = loc[:i]
|
||||
}
|
||||
fmt.Printf(" loc=%-30q title=%-50q\n", loc, r.Title)
|
||||
}
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
// Command backfill_title_cn 把 brand_runway.title_cn 由已解析的结构化分类字段
|
||||
// (year / collection_type / season)规则生成中文走秀标题。
|
||||
//
|
||||
// 设计:
|
||||
// - title 字段本质就是「季节/品类描述」(Vogue 格式:Spring 2026 Ready-to-Wear、
|
||||
// Resort 2026、Pre-Fall 2023、Fall 2023 Couture、Spring 2024 Menswear),
|
||||
// 品牌名在 JOIN 出的 brand_name 里单独给,不混进 title。
|
||||
// - collection_type / season 早已由历史迁移解析回填,因此 title_cn 可纯规则生成,
|
||||
// 完全不依赖外部翻译服务,且结果稳定可重跑。
|
||||
//
|
||||
// 幂等:每次都按分类字段重新计算并整表覆盖,重跑无害。
|
||||
// 不写任何业务逻辑,仅做一次性数据回填。
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const dsn = "root:root@tcp(127.0.0.1:3306)/db?charset=utf8mb4&parseTime=True&loc=Local"
|
||||
|
||||
// titleCnSQL 单条 UPDATE 用 CASE 把分类字段映射为中文标题。
|
||||
// year 参与拼接(CONCAT),season 为 NULL 时 COALESCE 成 ''。
|
||||
const titleCnSQL = `
|
||||
UPDATE brand_runway
|
||||
SET title_cn = CASE
|
||||
WHEN collection_type = 'resort' THEN CONCAT(year, ' 度假系列')
|
||||
WHEN collection_type = 'pre_fall' THEN CONCAT(year, ' 早秋系列')
|
||||
WHEN collection_type = 'rtw' AND season = 'spring' THEN CONCAT(year, ' 春夏成衣')
|
||||
WHEN collection_type = 'rtw' AND season = 'fall' THEN CONCAT(year, ' 秋冬成衣')
|
||||
WHEN collection_type = 'rtw' THEN CONCAT(year, ' 成衣')
|
||||
WHEN collection_type = 'menswear' AND season = 'spring' THEN CONCAT(year, ' 春夏男装')
|
||||
WHEN collection_type = 'menswear' AND season = 'fall' THEN CONCAT(year, ' 秋冬男装')
|
||||
WHEN collection_type = 'couture' AND season = 'spring' THEN CONCAT(year, ' 春夏高定')
|
||||
WHEN collection_type = 'couture' AND season = 'fall' THEN CONCAT(year, ' 秋冬高定')
|
||||
ELSE CONCAT(year, ' 系列')
|
||||
END
|
||||
WHERE is_deleted = 0`
|
||||
|
||||
func main() {
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatal("open:", err)
|
||||
}
|
||||
|
||||
var before int64
|
||||
db.Raw(`SELECT COUNT(*) FROM brand_runway WHERE is_deleted=0 AND title_cn <> ''`).Scan(&before)
|
||||
fmt.Printf("before: title_cn 非空 = %d\n", before)
|
||||
|
||||
if r := db.Exec(titleCnSQL); r.Error != nil {
|
||||
log.Fatalf("backfill title_cn failed: %v", r.Error)
|
||||
} else {
|
||||
fmt.Printf("updated rows = %d\n", r.RowsAffected)
|
||||
}
|
||||
|
||||
var after int64
|
||||
db.Raw(`SELECT COUNT(*) FROM brand_runway WHERE is_deleted=0 AND title_cn <> ''`).Scan(&after)
|
||||
fmt.Printf("after: title_cn 非空 = %d\n", after)
|
||||
|
||||
// 抽样核对:原始 title -> 生成 title_cn
|
||||
type row struct {
|
||||
Title string
|
||||
TitleCn string
|
||||
}
|
||||
var rows []row
|
||||
db.Raw(`SELECT title, title_cn FROM brand_runway WHERE is_deleted=0 ORDER BY id LIMIT 12`).Scan(&rows)
|
||||
fmt.Println("samples:")
|
||||
for _, r := range rows {
|
||||
fmt.Printf(" %-32q -> %q\n", r.Title, r.TitleCn)
|
||||
}
|
||||
fmt.Println("title_cn backfill done.")
|
||||
}
|
||||
@ -1,106 +0,0 @@
|
||||
// Command i18n_migrate 落地方案 A 的 i18n 列(加列 + 回填)。
|
||||
//
|
||||
// 项目已移除 AutoMigrate,表结构由外部 SQL 托管(见 scripts/sql/002_i18n_columns.sql,
|
||||
// 那是给人 review 的规范底稿)。本程序是「没有 mysql 客户端时」的执行器:
|
||||
// - ALTER 走 ensureColumn:先查 information_schema,缺才加,可安全重跑(幂等)。
|
||||
// - 回填 UPDATE 内联且幂等,重跑无害。
|
||||
//
|
||||
// 不写任何业务逻辑。
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const dsn = "root:root@tcp(127.0.0.1:3306)/db?charset=utf8mb4&parseTime=True&loc=Local"
|
||||
const han = "[一-鿿]"
|
||||
|
||||
func main() {
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatal("open:", err)
|
||||
}
|
||||
|
||||
// 1) 幂等加列(与 002_i18n_columns.sql 对齐)
|
||||
ensureColumn(db, "brand", "name_en", "VARCHAR(255) NOT NULL DEFAULT '' AFTER name")
|
||||
ensureColumn(db, "brand", "name_cn", "VARCHAR(255) NOT NULL DEFAULT '' AFTER name_en")
|
||||
ensureColumn(db, "brand_runway", "title_en", "VARCHAR(255) NOT NULL DEFAULT '' AFTER title")
|
||||
ensureColumn(db, "brand_runway", "title_cn", "VARCHAR(255) NOT NULL DEFAULT '' AFTER title_en")
|
||||
ensureColumn(db, "brand_runway", "description_en", "VARCHAR(1024) NOT NULL DEFAULT '' AFTER description")
|
||||
ensureColumn(db, "brand_runway", "description_cn", "VARCHAR(1024) NOT NULL DEFAULT '' AFTER description_en")
|
||||
|
||||
// 2) 回填品牌名:从 name / show_name 拆拉丁与中文(幂等)
|
||||
db.Exec(fmt.Sprintf(`
|
||||
UPDATE brand SET
|
||||
name_en = CASE
|
||||
WHEN TRIM(REGEXP_REPLACE(name, '%s', '')) <> '' THEN TRIM(REGEXP_REPLACE(name, '%s', ''))
|
||||
WHEN show_name LIKE '%%/%%' THEN TRIM(SUBSTRING_INDEX(show_name, '/', 1))
|
||||
ELSE ''
|
||||
END,
|
||||
name_cn = CASE
|
||||
WHEN show_name LIKE '%%/%%' AND SUBSTRING_INDEX(show_name, '/', -1) REGEXP '%s'
|
||||
THEN TRIM(SUBSTRING_INDEX(show_name, '/', -1))
|
||||
WHEN show_name REGEXP '%s' THEN TRIM(show_name)
|
||||
WHEN name REGEXP '%s' THEN TRIM(name)
|
||||
ELSE ''
|
||||
END`, han, han, han, han, han))
|
||||
|
||||
// 3) 回填走秀:title / description 当前全英文,直接进 *_en(幂等)
|
||||
db.Exec(`UPDATE brand_runway SET title_en = title, description_en = description, title_cn = '', description_cn = ''`)
|
||||
|
||||
// 4) 校验
|
||||
printCounts(db)
|
||||
fmt.Println("migration done.")
|
||||
}
|
||||
|
||||
func ensureColumn(db *gorm.DB, table, col, def string) {
|
||||
var n int64
|
||||
db.Raw("SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='db' AND TABLE_NAME=? AND COLUMN_NAME=?", table, col).Scan(&n)
|
||||
if n > 0 {
|
||||
fmt.Printf(" skip %s.%s (exists)\n", table, col)
|
||||
return
|
||||
}
|
||||
stmt := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s", table, col, def)
|
||||
fmt.Println(" +", stmt)
|
||||
if r := db.Exec(stmt); r.Error != nil {
|
||||
log.Fatalf("alter %s.%s failed: %v", table, col, r.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func printCounts(db *gorm.DB) {
|
||||
type c struct {
|
||||
Tot, En, Cn, Both, Dcn int64
|
||||
}
|
||||
var b c
|
||||
db.Raw(`SELECT COUNT(*) tot,
|
||||
SUM(CASE WHEN name_en<>'' THEN 1 ELSE 0 END) en,
|
||||
SUM(CASE WHEN name_cn<>'' THEN 1 ELSE 0 END) cn,
|
||||
SUM(CASE WHEN name_en<>'' AND name_cn<>'' THEN 1 ELSE 0 END) both_cnt
|
||||
FROM brand`).Scan(&b)
|
||||
fmt.Printf(" brand: total=%d name_en=%d name_cn=%d both=%d\n", b.Tot, b.En, b.Cn, b.Both)
|
||||
|
||||
var r c
|
||||
db.Raw(`SELECT COUNT(*) tot,
|
||||
SUM(CASE WHEN title_en<>'' THEN 1 ELSE 0 END) en,
|
||||
SUM(CASE WHEN title_cn<>'' THEN 1 ELSE 0 END) cn,
|
||||
SUM(CASE WHEN description_cn<>'' THEN 1 ELSE 0 END) dcn
|
||||
FROM brand_runway WHERE is_deleted=0`).Scan(&r)
|
||||
fmt.Printf(" runway: total=%d title_en=%d title_cn=%d desc_cn=%d\n", r.Tot, r.En, r.Cn, r.Dcn)
|
||||
|
||||
type s struct {
|
||||
Name string
|
||||
ShowName string
|
||||
NameEn string
|
||||
NameCn string
|
||||
}
|
||||
var samples []s
|
||||
db.Raw("SELECT name, show_name, name_en, name_cn FROM brand WHERE name REGEXP ? OR show_name REGEXP ? ORDER BY id LIMIT 8", han, han).Scan(&samples)
|
||||
fmt.Println(" brand samples:")
|
||||
for _, x := range samples {
|
||||
fmt.Printf(" name=%-18q show=%-26q -> en=%-18q cn=%-18q\n", x.Name, x.ShowName, x.NameEn, x.NameCn)
|
||||
}
|
||||
}
|
||||
@ -1,98 +0,0 @@
|
||||
// Command migrate_refresh 一次性建表脚本(与 scripts/sql/*.sql 同源,手写 DDL 以保持与项目"SQL 托管表结构"约定一致)。
|
||||
//
|
||||
// 运行:go run ./scripts/migrate_refresh
|
||||
// 可用环境变量 DB_DSN 覆盖连接串(默认 root:root@127.0.0.1:3306/db_dev)。
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
dsn := os.Getenv("DB_DSN")
|
||||
if dsn == "" {
|
||||
dsn = "root:root@tcp(127.0.0.1:3306)/db_dev?charset=utf8mb4&parseTime=True&loc=Local"
|
||||
}
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatalf("open db: %v", err)
|
||||
}
|
||||
stmt := `CREATE TABLE IF NOT EXISTS refresh_tokens (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
user_id INT UNSIGNED NOT NULL,
|
||||
token_hash VARCHAR(64) NOT NULL,
|
||||
expires_at INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
revoked TINYINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
user_agent VARCHAR(255) NOT NULL DEFAULT '',
|
||||
ip VARCHAR(64) NOT NULL DEFAULT '',
|
||||
created_at INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_token_hash (token_hash),
|
||||
KEY idx_user_id (user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`
|
||||
if err := db.Exec(stmt).Error; err != nil {
|
||||
log.Fatalf("create refresh_tokens: %v", err)
|
||||
}
|
||||
log.Println("✓ refresh_tokens ready")
|
||||
|
||||
stmtFav := `CREATE TABLE IF NOT EXISTS favorites (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
created_at INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
user_id INT UNSIGNED NOT NULL,
|
||||
target_type VARCHAR(16) NOT NULL DEFAULT '',
|
||||
target_uid VARCHAR(64) NOT NULL DEFAULT '',
|
||||
title VARCHAR(255) NOT NULL DEFAULT '',
|
||||
cover VARCHAR(512) NOT NULL DEFAULT '',
|
||||
brand VARCHAR(255) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uniq_user_target (user_id, target_uid),
|
||||
KEY idx_user (user_id),
|
||||
KEY idx_user_created (user_id, created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`
|
||||
if err := db.Exec(stmtFav).Error; err != nil {
|
||||
log.Fatalf("create favorites: %v", err)
|
||||
}
|
||||
// 复合索引(user_id, created_at):优化收藏分页深翻页(10万收藏场景);已存在则忽略报错
|
||||
if err := db.Exec("ALTER TABLE favorites ADD INDEX idx_user_created (user_id, created_at)").Error; err != nil {
|
||||
log.Printf("ℹ favorites idx_user_created 已存在,可忽略: %v", err)
|
||||
}
|
||||
log.Println("✓ favorites ready")
|
||||
|
||||
// histories:文章级浏览历史(2026-09-02 收敛)。
|
||||
// 仅存 user_id + target_uid(带 r=/s= 前缀的对外编码串,前缀即类型)+ viewed_at;
|
||||
// kind/target_type/title/cover/brand 快照已弃用(账户页展示时回查公开详情接口)。
|
||||
stmtHist := `CREATE TABLE IF NOT EXISTS histories (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
viewed_at INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
user_id INT UNSIGNED NOT NULL,
|
||||
target_uid VARCHAR(64) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uniq_user_target (user_id, target_uid),
|
||||
KEY idx_user_viewed (user_id, viewed_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`
|
||||
if err := db.Exec(stmtHist).Error; err != nil {
|
||||
log.Fatalf("create histories: %v", err)
|
||||
}
|
||||
// —— 存量库收敛(幂等):清掉旧固定串行(runway-looks/street-snaps),卸掉旧列与旧唯一键,换上新的 ——
|
||||
// 旧结构列/键可能不存在(新库)或已收敛(二次执行),每条失败仅提示、不中断。
|
||||
trimHist := []string{
|
||||
"DELETE FROM histories WHERE target_uid NOT LIKE 'r=%' AND target_uid NOT LIKE 's=%'",
|
||||
"ALTER TABLE histories DROP KEY uniq_user_kind_target",
|
||||
"ALTER TABLE histories DROP COLUMN kind",
|
||||
"ALTER TABLE histories DROP COLUMN target_type",
|
||||
"ALTER TABLE histories DROP COLUMN title",
|
||||
"ALTER TABLE histories DROP COLUMN cover",
|
||||
"ALTER TABLE histories DROP COLUMN brand",
|
||||
"ALTER TABLE histories ADD UNIQUE KEY uniq_user_target (user_id, target_uid)",
|
||||
}
|
||||
for _, stmt := range trimHist {
|
||||
if err := db.Exec(stmt).Error; err != nil {
|
||||
log.Printf("ℹ histories 收敛步骤可忽略(可能已收敛/新库): %v", err)
|
||||
}
|
||||
}
|
||||
log.Println("✓ histories ready (article-level: user_id+target_uid+viewed_at)")
|
||||
}
|
||||
Reference in New Issue
Block a user