This commit is contained in:
toom1996
2026-09-02 21:51:35 +08:00
parent 2562fc4aaa
commit 1fd9c48f58
22 changed files with 908 additions and 30 deletions

View File

@ -38,4 +38,61 @@ func main() {
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)")
}

View File

@ -62,34 +62,46 @@ func main() {
log.Println("✓ users 表就绪")
// 2. 预置内部账号
const username = "admin"
const email = "admin@studio.local"
password := os.Getenv("SEED_ADMIN_PASSWORD")
if password == "" {
password = "Studio#2026!Admin"
// admin: 正式内部账号,密码取环境变量 SEED_ADMIN_PASSWORD(缺省回落强密码常量)。
// root: 测试账号,固定弱密码 root,仅供本地联调使用,勿用于生产。
adminPassword := os.Getenv("SEED_ADMIN_PASSWORD")
if adminPassword == "" {
adminPassword = "Studio#2026!Admin"
}
hash, e := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if e != nil {
log.Fatalf("✗ 生成密码哈希失败: %v", e)
accounts := []struct {
username string
email string
password string
}{
{username: "admin", email: "admin@studio.local", password: adminPassword},
{username: "root", email: "root@studio.local", password: "root"},
}
var existing model.User
qerr := db.Where("username = ?", username).First(&existing).Error
switch {
case errors.Is(qerr, gorm.ErrRecordNotFound):
user := model.User{Username: username, Email: email, PasswordHash: string(hash)}
if cerr := db.Create(&user).Error; cerr != nil {
log.Fatalf("✗ 创建内部账号失败: %v", cerr)
for _, acc := range accounts {
hash, e := bcrypt.GenerateFromPassword([]byte(acc.password), bcrypt.DefaultCost)
if e != nil {
log.Fatalf("✗ 生成密码哈希失败(%s): %v", acc.username, e)
}
log.Printf("✓ 已创建内部账号: %s / %s", username, email)
case qerr != nil:
log.Fatalf("✗ 查询账号失败: %v", qerr)
default:
existing.PasswordHash = string(hash)
if uerr := db.Save(&existing).Error; uerr != nil {
log.Fatalf("✗ 更新账号密码失败: %v", uerr)
var existing model.User
qerr := db.Where("username = ?", acc.username).First(&existing).Error
switch {
case errors.Is(qerr, gorm.ErrRecordNotFound):
user := model.User{Username: acc.username, Email: acc.email, PasswordHash: string(hash)}
if cerr := db.Create(&user).Error; cerr != nil {
log.Fatalf("✗ 创建内部账号失败(%s): %v", acc.username, cerr)
}
log.Printf("✓ 已创建内部账号: %s / %s", acc.username, acc.email)
case qerr != nil:
log.Fatalf("✗ 查询账号失败(%s): %v", acc.username, qerr)
default:
existing.PasswordHash = string(hash)
if uerr := db.Save(&existing).Error; uerr != nil {
log.Fatalf("✗ 更新账号密码失败(%s): %v", acc.username, uerr)
}
log.Printf("✓ 内部账号已存在,已刷新密码哈希: %s", acc.username)
}
log.Printf("✓ 内部账号已存在,已刷新密码哈希: %s", username)
log.Printf("ℹ 账号 %s 登录方式: %s / 密码: %s", acc.username, acc.username, acc.password)
}
log.Printf("ℹ 登录账号: %s 密码: %s(请尽快修改默认密码)", username, password)
log.Println("⚠ admin 为强密码账号请尽快改默认密码;root 为弱密码测试账号,仅本地使用")
}

View File

@ -0,0 +1,17 @@
-- 007: favorites —— 用户收藏(服务端持久化,按用户隔离)
-- 与项目约定一致:表结构由 SQL 托管,不依赖 GORM AutoMigrate。
-- target_uid 存前端卡片的 it.id(编码串),与前端直接对齐;title/cover/brand 为收藏时快照。
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;

View File

@ -0,0 +1,21 @@
-- 008: histories —— 用户浏览历史(服务端持久化,按用户隔离,跨设备一致)
-- 与 favorites 同理:表结构由 SQL 托管,不依赖 GORM AutoMigrate。
--
-- 2026-09-02 收敛为「文章级」浏览历史:
-- 1) 触发点:点开某篇走秀/街拍详情(/item/xxx)时记一条;进入列表页不再记。
-- 2) 仅存 (user_id, target_uid, viewed_at):target_uid 为带类型前缀的对外编码串
-- (runway r=xxx / street s=xxx,见后端 hashid.EncodeTyped),前缀本身携带类型,
-- 无需 kind / target_type 列。
-- 3) title/cover/brand 快照已移除:账户页展示时按 target_uid 回查公开详情接口,
-- 保持表最简、展示数据新鲜。
-- 唯一键 (user_id, target_uid):同一篇文章反复打开只 upsert 刷新 viewed_at,绝不新增行。
-- 每用户上限 HISTORY_CAP(应用层维护,默认 2000),超出 FIFO 删最旧。
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;