update
This commit is contained in:
@ -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)")
|
||||
}
|
||||
|
||||
@ -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 为弱密码测试账号,仅本地使用")
|
||||
}
|
||||
|
||||
17
scripts/sql/007_create_favorites.sql
Normal file
17
scripts/sql/007_create_favorites.sql
Normal 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;
|
||||
21
scripts/sql/008_create_histories.sql
Normal file
21
scripts/sql/008_create_histories.sql
Normal 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;
|
||||
Reference in New Issue
Block a user