99 lines
4.1 KiB
Go
99 lines
4.1 KiB
Go
// 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)")
|
||
}
|