update
Some checks failed
deploy / deploy (push) Has been cancelled

This commit is contained in:
toom1996
2026-09-02 21:50:30 +08:00
parent 50e5775dc8
commit 64af75c0a9
23 changed files with 1730 additions and 285 deletions

View File

@ -0,0 +1,87 @@
# 2026-09-01 工作日志
## 登录页样式丢失(17:30 修复)
- 现象:用户反馈 login 页样式没了。排查发现 `src/pages/{en,cn}/login.astro` 是**裸 `<body>` 片段页**(没有 `<html>/<head>`),仅靠前端 `import '@/styles/global.css'` 引入样式。Astro 在构建时对片段页不会把 import 的 CSS 注入 head,导致构建产物 `dist/.../login/index.html` 完全没有 `<link rel=stylesheet>`,整页无样式(dev 模式下 Vite HMR 偶尔能补上,故时有时无)。
- 修复:给两个 login 页补上完整 `<!doctype html><html><head>(charset/viewport/title)</head><body>...`,前端 `import '@/styles/global.css` 随即被注入 head;`<script>` 移入 body 内。复用 Layout 同款写法。
- 验证:`npx astro build` EXIT=0,`dist/.../en/login/index.html` 头部出现 `<link rel="stylesheet" href="/_astro/...css">`,确认样式回归。其它页面走 Layout 不受影响;全仓仅 login 两页存在此片段页 import CSS 问题。
## 新增 root 测试账号
- 用户要求加一个账号/密码都是 root 的测试账号。改 `scripts/seed_users/main.go`:把单账号 upsert 重构为账号列表循环,`admin`(强密码) + `root`(密码 root,明文弱密码,仅本地联调)。
- 运行 `go run ./scripts/seed_users`(EXIT=0):`✓ 内部账号已存在,已刷新密码哈希: root`;`ℹ 账号 root 登录方式: root / 密码: root`。
- 实测登录:`Invoke-RestMethod POST /api/v1/auth/login {account:root,password:root}` → `STATUS_OK user=root`,access_len=177、refresh_len=64。登录链路通,用户可直接用 root/root 测试。
## 重要纠正:access token 实际是 7 天,不是 2h
- 之前记忆/口述误记 access=2h。核对 `configs/config.yml` 第 40 行 `expire_hours: 168`(=7 天,注释"与原项目一致");`internal/config/config.go` 默认回落也是 168h;refresh 720h=30 天。
- 实测 root 登录 `expires_in=604800`(7 天)佐证。
## 个人中心页面重设计(企业级,设计优先)
- 用户反馈现 account.astro「根本不像企业级页面」,要求先把视觉/结构做好,功能可先不实现。
- 重写 `src/pages/{en,cn}/account.astro`:黑白极简编辑风(与站点一致)。结构 = ① 黑底 Hero(字母头像 monogram + 用户名/邮箱 + 角色 chip「Internal · Portfolio Studio」+ 加入于 2026 + Sign out);② 四宫格数据概览(Saved looks / Collections / History / Active sessions,占位 0/0/0/1);③ 左栏 tab 导航 + 右栏内容面板,tab = Overview / My Collections / My History / Sessions & Security / Settings;④ 收藏/历史为空状态(细线 SVG + CTA 去浏览档案);⑤ Sessions 面板含「当前设备」会话卡 + 单会话互斥说明 + 「退出所有设备」按钮(占位,带 Soon 标);⑥ Settings 偏好行(语言/外观/通知,均 Soon 占位)。
- 功能约定:仅 `logout()`(调 api.logout 吊销 refresh)与未登录重定向真实可用;编辑资料/收藏/历史/会话管理/偏好均为占位(disabled 或 Soon 标),待后续实现。
- 词典:新增一批账户中心键(overview / sessions & security / settings / saved looks / active sessions / member since / role / internal member / account overview / account details / username / edit profile / view archive / no collections yet / no history yet / browse the archive / current session / this device / last active / sign out all devices / single session note / preferences / appearance / notifications / soon / quick stats / now),en 即键本身、cn 提供翻译。
- 验证:`npm run build` EXIT=0,`/en/account`、`/cn/account` 均预渲染成功;read_lints 0 错误。dev server 仍在 4321,需用 root/root 登录后访问 /en/account 看完整效果(未登录仅见登录引导卡)。
- 影响:互斥登录"踢人"只吊销 refresh,而**已签发的 access 还能活 7 天**——即被顶掉的会话最多要等 7 天才失效,2h 窗口的说法是错的。要让同账号互斥真正即时生效,必须加 `users.token_version`(签发时嵌版本、middleware.Auth 每次比对),或把 access TTL 调短(如 2h)。已修正 MEMORY.md。
## 账户中心 v1:做成真实可用的客户端版(16:30 续)
- 用户要求把个人中心做成"像 B 站/论坛那种有功能的",不要占位。结合"一点一点完善",本轮交付**客户端存储版**(数据存浏览器、按登录用户名隔离),零后端风险,登录即可用;函数签名预留替换服务端。
- 新增前端库:`src/lib/favorites.ts`(收藏:getFavorites/isFavorited/toggleFavorite/removeFavorite/clearFavorites)、`src/lib/history.ts`(浏览历史:recordHistory/getHistory/clearHistory,去重+上限60)、`src/lib/profile.ts`(昵称/简介:getProfile/saveProfile)。均按 `fa_fav_<user>`/`fa_hist_<user>`/`fa_profile_<user>` 隔离。
- 详情页 `src/pages/{en,cn}/item/[id].astro`:新增右下角浮动「收藏」按钮(fab,点击 toggle 收藏、已收藏反白),并在加载时 `recordHistory` 记录浏览历史(仅登录态);meta 由服务端 frontmatter 透传,define:vars 注入脚本。
- 重写 `src/pages/{en,cn}/account.astro`:Hero(昵称可编辑、取 profile 存储)、四宫格真实计数(收藏数/历史数)、Tab = Overview / Saved looks / History / Sessions & Security / Settings 全部接真实数据:
- Saved looks:真实收藏列表(卡片+封面+移除+跳详情),空状态引导。
- History:真实浏览历史列表+清空按钮。
- Sessions:解析 UA 显示「当前设备」(浏览器·OS),「退出所有设备」接 `logoutAll()`(真实)。
- Settings:语言切换(EN/中文 跳对应 locale 账户页,真实)、外观(浅色/深色 toggle,theme-dark 切内容面板与统计卡,真实)、通知(开/关 本地存储)。
- 编辑资料弹窗:修改昵称/简介,存 profile 本地,Hero 即时反映。
- 词典 `dictionary.ts` 补 cn 翻译:member profile/save/saved/remove/cancel/save changes/display name/bio/clear history/no saved looks yet/light/dark/on/off。
- 验证:`npm run build` EXIT=0,`/en/account`、`/cn/account` 预渲染成功;read_lints 0 错误。dev server :4321 运行中,用 root/root 登录后访问 /en/account 可见完整功能(未登录自动跳 /en/login)。
- **待办(下一轮,服务端化)**:收藏/历史/资料/会话列表需后端化以支持跨设备——需新增 `favorites` 表+接口、`users` 加 display_name/bio 列、`refresh_tokens` 列表接口(登录时补 user_agent/ip)、`/me/profile` 更新接口。现前端 lib 函数签名已设计为可平滑替换实现。
## 个人中心视觉重构(18:00)
- 用户反馈 account 页「太难看了」。排查:页面样式其实正常(Layout 注入 CSS 正常),丑点是**设计语言不统一**——原稿用暖米色 `bg-[#f7f5f1]` 面板 + 大柔影 `shadow-[0_18px_50px_rgba(0,0,0,.04)]` + `rounded-[30px]` 大圆角,与站点纯黑白极简编辑风(thin border、`rounded-none`、mono 标签、serif 大标题、`shadow-sm`)冲突。
- 重写 `src/pages/{en,cn}/account.astro`(与 v1 功能/Alpine 逻辑完全一致,仅重排版面):报头改白底细线(去掉黑底大卡 + 方块 monogram 头像);统计卡改 `border-black/15` 网格、无柔影无大圆角;Tab 导航激活态改「黑底白字」实心(原 border-b-2 下划线);内容面板 `border border-black/15 bg-white`、`rounded-none`;收藏/历史卡片改 `border-black/15` + `hover:border-black` + `bg-[#f1f1f1]` 占位(对齐 look-grid.css);编辑弹窗去圆角去阴影;新增 `js-surface/js-line/js-chip/accent-btn/ghost-btn` class 钩子支撑深色模式反相(`.account-root.theme-dark` 全局 `border-color` 反白 + 主按钮反白)。cn/en 两文件共用同一套 `t()` 文案,新增词典键 `items`(cn:条)。
- 验证:`npx astro build` EXIT=0;dist 产物确认 stylesheet 链接 + 新 class + 深色 `<style>` 均注入;read_lints 0 错误。
## 个人中心大幅精简(18:47)
- 用户要求:去掉 Active Sessions / My Collections(认为无用,或换功能)、去掉 Sign out、section 内容简洁点"有用户名就够了"。
- 改动 `src/pages/{en,cn}/account.astro`:
- 报头:删邮箱行、角色 chip、Sign out 按钮、编辑资料入口;仅留「方块字母头像 + 用户名」大标题(member profile 小标签保留)。
- 删整块「四宫格数据概览」(含 Saved looks / My Collections / My History / Active Sessions 占位卡)。
- Tab 导航:删 Overview、Sessions & Security 两项;保留 Saved looks / My History / Settings 三个。默认 tab 改为 `saved`。
- 删编辑资料弹窗及 displayName/bio 相关逻辑(profile.ts 不再接入)。
- Sign out:报头按钮移除,仅在 Settings 底部保留一个**低调文字链接**(underline hover)`logout()`,避免彻底无法登出(切账号卡死)。已告知用户若想彻底删除可再提。
- 脚本同步精简:删 `editing/displayName/bio/device/parseUA`、`saveProfileEdit/logoutAllDevices`、移除 `logoutAll/getProfile/saveProfile` 导入;保留 favorites/history/settings 逻辑。
- 验证:`npx astro build` EXIT=0;dev server :4321 运行中,root/root 登录访问 /en/account 看效果。
- 遗留:dictionary.ts 中部分键(my collections / active sessions / sessions & security / sign out all devices / account overview / username / email / display name / bio / role / internal member / member since / edit profile / view archive / save changes / cancel 等)已无人引用,属数据冗余,未删(不影响构建)。
## 个人中心再精简:去掉偏好设置(19:10)
- 用户:「偏好设置也不要了」。结合上轮已去掉 sign out 报头按钮,现整体移除 Settings tab。
- 改 `src/pages/{en,cn}/account.astro`:删导航里的 Settings 按钮、删整个 Settings 面板(语言/外观/通知/退出登录全部移除);脚本移除 `themeDark/notif` 状态、`toggleTheme/toggleNotif/switchLang/logout` 方法,及 `logout` 导入;移除 `<main>` 上的 `:class="theme-dark"` 绑定与深色模式 `<style>` 块(js-surface/accent-btn 等 class 留作空操作,无害)。
- 结果:个人中心现仅 = 报头(方块头像+用户名)+ 两个 tab(Saved looks / My History),无偏好、无退出登录入口。
- 影响:登录后页面无任何「退出/切换账号」入口,需清理浏览器 localStorage 才能换号。已向用户说明,如需保留可再加。
- 验证:`npm run build` EXIT=0;read_lints 0 错误。
## 个人中心:加宽容器 + 收藏假数据(19:15)
- 用户:① 收藏搞个假数据看效果;② 觉得 main 容器太窄,加宽。
- 改动 `src/pages/{en,cn}/account.astro`:
- 主容器 `max-w-5xl` → `max-w-7xl`(1280px),收藏/历史卡片网格 `grid-cols-2 sm:grid-cols-3` → `grid-cols-2 sm:grid-cols-3 lg:grid-cols-4`(间距 gap-4→gap-5)。
- 收藏假数据:脚本新增 `demoFavorites`(8 条,含 Maison Margiela/Prada/Bottega/Loewe/Saint Laurent/Celine/Balenciaga/Jacquemus 的 FW25/SS26 RTW,标 `demo:true`)。`init()` 中 `getFavorites()` 为空时回退到 demoFavorites,保证有内容可预览;一旦用户存入真实收藏即不再显示 demo。
- 占位图:新增 `PLACEHOLDER`(内联 SVG data URI,灰底 "LOOK" 字样),`coverUrl(f)` 方法在 `f.cover` 为空时返回占位(真实收藏无封面也不再裂图)。
- demo 项处理:点击 demo 卡片 `itemHref` 跳走秀档案(不进 404 详情);点 REMOVE 仅从内存 `favorites` 移除 demo 项、不写 localStorage。
- 验证:`npm run build` EXIT=0;read_lints 0 错误。
- 注:若账号之前已存真实收藏,收藏 tab 显示真实数据而非 demo(布局同样可见);要看纯 demo 需先清空该用户收藏的 localStorage。
## 列表卡片收藏按钮 + 压黑底白字(19:24)
- 用户三点:① 图片应可收藏,且抱怨找不到收藏/取消入口;② 整体偏无背景色,尽量少用黑底白字组件。
- 改动:
- `src/components/RunwayLooks.astro` / `StreetSnaps.astro`:卡片 `<a>` 外包 `relative` 容器并加 `x-data="{ fav: isFav(it.id) }"`,右上角加白底细边心形按钮 `.fav-toggle`:`@click.prevent="fav = toggleFav(it)"`;未登录点按由 `toggleFav` 弹登录框(requireLogin)。脚本引入 `isFavorited/toggleFavorite`,新增 `isFav(id)`、`toggleFav(it)`(runway→type:'runway'/brand=brand_name;street→type:'street'/brand=''),`toggleFavorite` 返回布尔驱动 `fav` 视觉。
- `src/styles/look-grid.css`:新增 `.fav-toggle`(白底 `border-black/15` 圆钮,`hover:scale-110`)与 `.fav-toggle.is-on svg { fill:#000 }`(已收藏仅实心黑心,不出现黑底块)。
- `src/pages/{en,cn}/item/[id].astro`:详情页浮窗收藏按钮由黑底白字改为白底描边(`hover:bg-black` 反白),图标改心形 + `#fav-fab.is-active .fav-heart{fill:currentColor}`;脚本去掉 `btn.style.background/color` 内联黑底,改用 `label.textContent` + `is-active` 类切换。
- `src/pages/{en,cn}/account.astro`:tab 激活态由「黑底块 bg-black text-white」改为编辑风细线(黑字 + `border-b-2 border-black` 移动端下划线 / `lg:border-l-2 lg:border-l-black` 桌面左竖线);两个 `browse the archive` 按钮由 `bg-black text-white` 改为 `bg-white text-black`(保留 hover 反白)。cn 版 tab 含 `font-mono`(en 无),已分别适配。
- 验证:`npm run build` EXIT=0;read_lints 0 错误。
- 遗留黑底白字(用户说"尽可能少"而非全删,保留作小强调):分页 `.pg.is-current`、卡片 `.showsbadge`(RTW 角标)、导航 `2026` 小药丸、登录提交按钮 hover 反白——均为功能性/极小强调,未改。若用户要更彻底再动。
## 收藏爱心图标修正(19:39)
- 用户:"你现在看下,收藏按钮的logo、"(话未说完,但意图是检查/修图标)。
- 发现:列表页(RunwayLooks / StreetSnaps)与详情页(item/[id] en+cn)四个位置的收藏按钮共用同一段**手写爱心 SVG path**,坐标不对称——叶瓣中心 x=5.5/14.5(均=10),底部尖端却在 x=12,导致整颗心向左偏、视觉是斜的。
- 修复:四处理所当然统一替换为严格对称爱心 path `M12 21C12 21 4 14.5 4 8.8C4 6.1 6.1 4 8.7 4C10.3 4 11.5 4.8 12 6C12.5 4.8 13.7 4 15.3 4C17.9 4 20 6.1 20 8.8C20 14.5 12 21 12 21Z`(叶瓣中心 x=8.7/15.3、尖端 x=12、凹点 x=12,左右镜像)。
- 验证:`npm run build` EXIT=0;read_lints 0 错误。

View File

@ -0,0 +1,125 @@
# 2026-09-02 工作记录
## 收藏功能后端化(服务端持久化)✅ 完成并端到端验证
用户指出:此前收藏只有前端 localStorage("纯前端自慰"),后端没有对应接口。本次补齐后端 + 前端接入。
### 后端(d:/project/backend_v2)
新增文件:
- `internal/model/favorite.go`:`Favorite` 模型(user_id / target_type / target_uid / title / cover / brand),表 `favorites`,唯一索引 `(user_id, target_uid)`。
- `internal/dto/favorite.go`:`FavoriteItem`(id 即 target_uid)、`FavoriteAdd`(`target_type`+`target_uid` 必填 + 可选快照)。
- `internal/repository/favorite_repository.go`:`ListByUser` / `Add`(OnConflict DoNothing 幂等)/ `Remove`。
- `internal/service/favorite_service.go`:`List` / `Add`(校验 type∈{runway,street})/ `Remove`。
- `internal/handler/favorite_handler.go`:`List`(GET) / `Add`(POST) / `Remove`(DELETE `/:target_uid`),均 `middleware.Auth` 鉴权,返回 `{data:...}`。
- `internal/router/router.go`:在 `api.Group("/auth")` 下注册三接口(含 middleware.Auth)。
- `internal/main.go`:装配 `favRepo` / `favSvc` / `Favorite` handler。
- `scripts/sql/007_create_favorites.sql` + `scripts/migrate_refresh/main.go`:建表(幂等)。
约定:沿用项目「一个功能一个接口」哲学,收藏独立路径 `/auth/favorites`,不挂查询参数。target_uid 直接用前端卡片编码串(r=/s=),避免解码。
### 前端(d:/project/frontend_v2)
- `src/lib/api.ts`:新增 `authJson<T>(method, path, body?)`(带 Bearer、401 静默 refresh、返回 `data`、抛出后端 message)。
- `src/lib/favorites.ts`:重写为 server 模式门面——`serverMode()`(getUser+access token);`toggleFavorite` 本地乐观更新 + 后台 POST/DELETE 同步(失败不回滚本地);`removeFavorite` 乐观清 serverList + 后台 DELETE;`fetchServerFavorites()` 拉服务端权威列表(account 页用)。对外签名(getFavorites/isFavorited/toggleFavorite/removeFavorite)保持不变。
- `src/pages/{en,cn}/account.astro`:移除假数据 demo(demoFavorites 整段删除);init 改调 `fetchServerFavorites()` 取服务端收藏;`itemHref/removeFav` 去掉 demo 分支;登录态展示真实收藏,空则显示空态。
- 列表卡片(RunwayLooks/StreetSnaps)无需改:仍用 `isFavorited/toggleFavorite`,新增的 server 同步在 lib 内部完成。
### 验证
- 后端 `go build ./...` EXIT=0;`go run ./scripts/migrate_refresh` MIG_EXIT=0(favorites 表建好)。
- 重启本地后端(杀旧 PID 11160,`go build -o bin/server.exe` 后启动,HEALTH=200)。
- 用 PowerShell `Invoke-RestMethod` 跑通全生命周期(curl.exe -d 在 Windows 下 body 发不出,改用 Invoke-RestMethod 才成功):
admin 登录 → POST /auth/favorites(返回新建项)→ GET(含该项)→ DELETE → GET 返回 `[]`。真实落库,无残留。
- 注意:本地后端种子账号是 `admin / Studio#2026!Admin`;用户预览用的 `root/root` 走线上后端,本地调试用 admin。
- 前端 `npx astro build` EXIT=0;read_lints 0 错误。
### 遗留 / 未做
- 跨设备列表卡片心形初始态:列表卡片初始仍读本地 localStorage(保证即时);登录态服务端同步在后台,换设备首次进列表可能短暂不一致(同设备因本地镜像一致)。如需严格一致,列表页 loadPage 后需 bootstrap serverList 并触发 Alpine 重算(当前未做)。
- users.display_name/bio、refresh_tokens 列表接口仍待办。
- 历史(history)仍为纯本地,未服务端化(用户未要求)。
## 收藏态跨浏览器/设备一致性修复(11:1x)✅ 完成 + 构建验证
用户提供担忧:换浏览器收藏列表是否不展示(数据绑账号应不丢,但发现列表卡片初始态只从本地读、列表页从不拉服务端,导致新浏览器进列表爱心全空);并问展示收藏态是否压垮数据库。
结论与改动:
- 性能:后端 `ListByUser` = `WHERE user_id=?` 索引查询,每页加载 1 次(非每卡片 1 次),千人并发仅几十 QPS 微查询,**不会崩**;唯一会崩的「每卡片 1 查询」写法未采用。
- 修复方案:列表/详情页进入时**先本地即时填充 + 异步拉服务端校正**,用响应式 `favMap`(`Record<id,boolean>`)替代原 `x-data="{ fav: isFav(it.id) }"` 的一次性捕获(后者服务端异步返回后不会刷新卡片)。
- `src/lib/favorites.ts`:新增 `loadServerFavIds(): Promise<string[]>`(调 `fetchServerFavorites` 回填 serverList 并返 id 数组)。
- `RunwayLooks.astro` / `StreetSnaps.astro`:data 增 `favMap`/`favTouched`;`init()` 调 `loadFavs()`(本地即时 → 登录态 `loadServerFavIds` 校正,`favTouched` 为真时跳过覆盖避免丢刚点的收藏);卡片按钮改 `:class="{ 'is-on': favState(it.id) }"` + `@click.prevent="toggleFav(it)"`(toggleFav 内乐观更新 favMap);删除原 `isFav` 方法。
- `src/pages/{en,cn}/item/[id].astro`:详情页 `sync()` 初始后,登录态 `loadServerFavIds().then(sync)` 异步校正(跨浏览器详情页爱心一致)。
- 验证:`npx astro build` EXIT=0;read_lints 0 错误。
- 注:预览 `:4321` 走线上后端(root/root,线上 favorites 表需部署 007 迁移才有);要本地直接验证跨浏览器,把前端 BASE_API 指向本地 `:8090` 用 admin 登录即可(用户未要求切,保持现状)。
## 收藏海量场景优化(10万级):服务端分页 + 批量校验(12:0x)✅ 完成并端到端验证
用户追问:若用户收藏极多(2000、甚至 10 万)怎么办。结论与改动:
- 性能论证:DB 层 `WHERE user_id=?` 索引查询,2000/10万行均毫秒级,不会崩;favMap(id→bool) 为 O(1)。真痛点在①账户页一次性 `x-for` 全量渲染(10万 DOM 爆炸)②列表标记此前拉全量完整对象(2000≈500KB/次)。
- 改动:
- 后端:新增 `POST /api/v1/auth/favorites/check`(body `{ids:[...]}` → 返回已收藏子集);`GET /auth/favorites` 改服务端分页(`?page&per_page`,默认24/上限100,返回 `{data:{items,total,last_page}}`)。repository 加 `ListPaged/CountByUser/FilterExisting`;service `List`→`ListPaged`+`Check`;dto 加 `FavoriteCheck`;router 注册 check;favorites 表加复合索引 `idx_user_created(user_id, created_at)`(007 sql + migrate_refresh 幂等 ALTER ADD INDEX)。
- 前端 `favorites.ts`:新增 `fetchFavoritesPage`(账户分页)+ `checkFavorited(ids)`(列表仅问当前页可见 id);删除 `loadServerFavIds`/`fetchServerFavorites`。
- 列表组件(RunwayLooks/StreetSnaps):`loadPage` 后调 `markFavs()` 用 check 校正当前页 hearts(与收藏总量解耦,常数级);删 `favTouched`。
- 账户页(en/cn):Saved looks 服务端分页 + 翻页器(`favPageList` 窗口化页码:首尾页+当前页±1+…折叠);`removeFav` 刷新当前页。
- 验证:后端 `go build ./cmd/server` EXIT=0;本地后端重启新二进制 HEALTH_OK;PowerShell 端到端:加3收藏 → P1(total3,last2,count2)/P2(count1) 分页正确 → check 返回3命中 → 删除后 total0。前端 `astro build` EXIT=0、lint 0。
- 注:预览 `:4321` 仍指向线上后端(root/root,需部署 007 迁移才有 favorites 表);本地验证用 admin/Studio#2026!Admin 走 :8090。
## 浏览历史服务端化(图集 + 图片)✅ 完成 + 端到端验证(15:1x)
用户拍板:历史也走**服务端**(跨设备一致,尽管每次浏览都写库写入量比收藏高一个数量级);图集 = **列表页整体**(进 Runway Looks / Street Snaps 整页记一条)。
### 后端
- `scripts/sql/008_create_histories.sql` + `migrate_refresh`:建 `histories` 表,`uniq(user_id,kind,target_uid)`;kind∈{image,gallery},target_type∈{runway,street,runway_looks,street_snaps}。
- `internal/model/history.go` / `dto/history.go`(`HistoryItem`/`HistoryAdd`)/ `repository/history_repository.go` / `service/history_service.go` / `handler/history_handler.go` / router 注册(GET/POST/DELETE `/auth/history` + DELETE `/auth/history/:target_uid`)/ main.go 装配。
- **关键膨胀控制(与收藏的本质差异)**:`Upsert` 用 `OnConflict DoUpdates`——同 (user_id,kind,target_uid) 反复浏览只刷 `viewed_at` 不新增行;每用户硬上限 `HISTORY_CAP=2000`,超量用子查询双嵌套(规避 MySQL 同表子查删除限制)FIFO 删最旧。
- **修了一个真 bug**:首版 `ListPaged` 的 `total` 调 `CountByUser(ctx,userID)` 忽略 kind,导致按类型过滤时分页计数错(items 过滤了但 total 算全量)。已改 `CountByUser(ctx,userID,kind)` 一并过滤。
### 前端
- `src/lib/history.ts` 重写为服务端优先:`recordHistory({kind,type,id,title,cover,brand})`(登录态本地+后台 POST upsert;未登录仅本地);`fetchHistoryPage(page,perPage,kind)` 服务端分页(kind 空=全部);`removeHistory`/`clearHistory`。
- 埋点:`src/pages/{en,cn}/item/[id].astro` 详情页 `recordHistory` 加 `kind:"image"`;`RunwayLooks.astro`/`StreetSnaps.astro` 的 `init()` 进页面记一条 `kind:"gallery"`(target_uid `runway-looks`/`street-snaps`,仅登录态;翻页走 loadPage 不触发,符合「进页面就记」)。
- 账户页(en/cn)History tab:`全部/图片/图集` 切换(setHistKind→重载)、服务端分页 + `histPageList` 窗口化页码、`histHref`(image→item 详情,gallery→对应列表页)、图集/图片徽标、单条删除/清空。字典加 `all/image/gallery/no history yet/viewed gallery`。
### 验证
- 后端 `go build ./...` EXIT=0;`migrate_refresh` 建表 OK;重启本地后端新二进制(taskkill 杀 7912 后启动,HEALTH=200)。
- PowerShell 端到端:登录 admin → 记 gallery+image → 重复记同 image **upsert 不新增(total 恒 2)** → `?kind=gallery`(total1)/`?kind=image`(total1)/全部(total2) 计数正确 → DELETE 单条 → CLEAR 归零。修复后复测 GAL_TOTAL=1 IMG_TOTAL=1 ALL=2 通过。
- 前端 `npx astro build` EXIT=0;read_lints 0 错误。
- 注:预览 `:4321` 走线上后端(root/root,需部署 008 迁移才有 histories 表);本地真实验证用 admin/Studio#2026!Admin 走 :8090。
## 后端连接池僵死导致「历史/收藏/登录全失效」(15:2x)✅ 修复
用户反馈浏览历史「没生效」。排查:前端代码与历史接口均正常,但本地后端 :8090 的 `/api/v1/public/brands` 返回 `{"error":"invalid connection"}`,`/health` 正常。
根因:后端(`server-bin`,当时 PID 18604)的 MySQL 连接池连接僵死——GORM 此前只设 `ConnMaxLifetime=3600`、**未设空闲回收时间**,空闲连接被 MySQL 回收后池子仍持有死连接,查询即 `invalid connection` / `bad connection`(server.err 见 14:45、14:52 已报 bad connection)。MySQL 自 8/26 起未重启,故非 MySQL 重启,而是空闲连接被回收。
修复:
- 后端加 `conn_max_idle_time`(config.yml + DatabaseConfig + mysql.go `SetConnMaxIdleTime`,默认 60s,须 < MySQL wait_timeout),空闲连接主动回收,从源头避免死连接。
- `go build -o server-bin ./cmd/server` 后杀旧 PID 18604、重启新二进制;重启后 `/public/brands` 恢复正常,历史接口端到端全过(ALL=2 / GALLERY=1 / IMAGE=1,upsert 不增总量,清空归零)。
- 临时验证脚本 `_tmp_e2e.mjs` 用完已删。
教训:本地后端长时间空闲后接口再访问报 invalid connection,**先重启后端**;已加空闲回收,复现概率大降。前端 `BASE_API=http://localhost:8090`(.env),故本地 dev 直接打本地后端,无需切线上。线上后端(root/root)仍需部署 007(favorites)/008(histories) 迁移才有这两张表。
## 浏览历史「看不到」排查 + 登录后补记修复(16:4x)✅
用户反馈浏览记录看不到,且「runway 文章数据库没新增」。排查结论:
- **历史写在 `histories` 表,不是 `brand_runway`**(走秀主数据表仅由爬虫/种子导入,浏览永不写入)。用户查错表,非 bug。
- 历史**按用户隔离**:local 8090 实测 `histories` 表 `root` 有 1 条(`KIND=gallery TYPE=street_snaps TITLE='Street Snaps'`),`admin` 有 0 条。账户页登录 root 才能看到 root 的记录。
- 已实证登录态写入正常(root 那条即用户用 root 登录进 Street Snaps 列表页所记)。
修复:未登录进列表页→登录后不补记图集的边界 bug。`src/lib/api.ts` 的 `login()` 成功后派发 `window` 事件 `auth:login`;`RunwayLooks.astro`/`StreetSnaps.astro` 的 `init()` 监听该事件补记对应 `gallery`(type=runway_looks / street_snaps)。dev server 已重编译,三页 HTTP=200。
## item 页浏览历史不记录:根因=define:vars+import 致命组合(17:0x)✅
用户访问 /en/item/r000AhByP 后个人中心与 histories 表都无记录。排查:/auth/history 后端经 curl 实测可写(root POST 成功落库),排除后端问题。抓渲染 HTML 发现 item 页 <script define:vars={{meta...}}> 被 Astro 包成内联 IIFE,原脚本顶部的 import 被塞进函数体内 → SyntaxError 整段不执行,故
ecordHistory/收藏绑定全失效。修复:en/cn item/[id].astro 去掉 define:vars,改由按钮 data-id/type/title/cover/brand/save-label/saved-label 属性透传,模块脚本内 tn.dataset 读取。dev server 已重编译,抓模块源码确认 import 已在顶层。清理了测试写入的 r000AhByP 记录。教训已写入 MEMORY.md Astro script 坑。
## 浏览历史设计纠正:仅图集记录,单张 item 不记(17:3x)✅
用户纠正:浏览历史应「进图集(RunwayLooks/StreetSnaps)才记」,单张 item 详情页不该记。此前我给 item 页加了 image 历史记录,属理解错误。修复:删 en/cn item/[id].astro 里的
ecordHistory({kind:'image',...}) 调用与导入(收藏逻辑不动);账户页 History 区移除无用的「图片」筛选 tab(仅留 全部/图集),卡片角标固定 gallery。图集页 gallery 记录逻辑(RunwayLooks/StreetSnaps 的 init() 调 recordHistory)保持不变,已抓打包脚本确认
ecordHistory/kind:gallery 仍在。MEMORY.md 同步更新:浏览历史仅记图集。
## 个人中心「没有」根因定位(17:4x)✅
用户报「个人中心还是没有」。排查:后端 history 全链路实测通过(Node 脚本 login admin → POST /auth/history ok → GET total=1,histories 表存在);前端 recordHistory/fetchHistoryPage/account 渲染逻辑均正确。根因在登录态:.env 当前 BASE_API=http://localhost:8090,本地库只有 admin、无 root,用户用 root/root 登录失败 → getUser() 为空 → 进 account 被踢登录页、图集不记历史。结论:本地验证一律用 admin/Studio#2026!Admin;要用 root 须改 .env 回线上域名并重启 dev server+硬刷新+重登录。MEMORY 账号体系段已补此坑。
## 纠正:user 表确有 root + 个人中心空的真因(18:4x)✅
实测 db_dev.users 经 scripts/seed_users 创建 admin(强密码 Studio#2026!Admin)+root/root(uid 8)。
oot/root 登录本地 :8090 返回 200;GET /auth/history 显示 root 账号下已有 street-snaps/runway-looks 两条真实图集历史(viewed_at≈当前)。此前'本地无 root、root 登录必失败'为误判,已纠正 MEMORY 账号体系段。
个人中心'看不到'两主因:(1) account.astro 默认 tab='saved'(收藏,常空),浏览历史在 'My History' tab,需点击;(2) 前端 dev server 若连线上而非本地 8090,则 root 登录线上、看不到本地这 2 条。已 stro dev stop+stro dev --background 重启使 .env 的 BASE_API=本地 8090 生效。验证:硬刷新→root/root 登录→点 My History→应见 2 条。
## 实锤根因:账户页历史空 = kind=all 被后端按字面过滤(18:5x)✅
带 root token 实测后端 /auth/history 三种查询:无 kind → total=2;kind=all → total=0(后端把 all 当字面 kind 值 WHERE kind='all');kind=gallery → total=2。根因:账户页 History「全部/默认」把 kind='all' 拼进 query,后端无该语义 → 恒空;数据其实一直在 root 下(2 条 gallery)。用户直接 curl 不带 token 返回 0 属正常未鉴权空返回。修复:src/lib/history.ts fetchHistoryPage 把 'all'/空 归一为不带 kind 参数(只传 image|gallery 这类具体值)。MEMORY 浏览历史条目已记坑。另:后端对无效 kind 值返回空而非全部,属脆弱设计(可选后续加固)。
## 浏览历史语义纠正与收敛:图集→文章级(19:4x-20:0x)✅
用户澄清:他说「进到图集里才有浏览记录」指的是点开一篇文章(详情页看整组图),不是进 RunwayLooks/StreetSnaps 列表页——此前按列表页 gallery 实现是理解反了。已按用户拍板重构:
- 语义:浏览历史=点开过的文章;item 详情页 init
ecordHistory(meta.id);列表页记录与 auth:login 补记全删(en/cn item + RunwayLooks + StreetSnaps)。
- 表收敛:histories 改 (user_id,target_uid,viewed_at),DROP kind/target_type/title/cover/brand;唯一键(user_id,target_uid);保留 HISTORY_CAP=2000 FIFO(文章会积累)。后端 model/dto/handler/service/repository 同步删字段;dto.HistoryAdd 仅 target_uid。008 迁移脚本改新表结构,存量库跑 migrate_refresh ALTER(实测生效:GET items 仅 {id,viewed_at},POST {target_uid}→200)。
- 展示:账户页先服务端分页取 id,再逐篇 getHistoryMeta(id) 回查公开详情拿 title/cover/brand(api.ts 新增,s= 前缀判 street 否则 runway);卡片跳文章详情;删 kind 筛选 tab/histHref/gallery 徽标;标题回查失败用 '…'。
- 验证:POST/GET 全绿;/public/runway-looks/r0042h1mx 回查字段齐全;account、item 页面 SSR 200。dev server 已重启(PID 31792)、后端 server-bin 已重建。MEMORY 浏览历史条目已整体改写。

View File

@ -1,69 +1,53 @@
# 项目长期记忆(Project MEMORY.md)
## 模块策略(重要)
- 站点按模块逐个完善。当前上线「发布会模块」:首页 index + 走秀列表/详情(RunwayLooks = 走秀档案页,articles/shows 页面已在 i18n 重构中重组)。
- **品牌索引页已下线**(2026-08-26 确认):`src/pages/brands.astro` 及孤儿组件 `src/components/pages/brands.astro` 均已删除;品牌数据现在只通过走秀页侧栏 filter + 品牌弹窗呈现(热门 30 条走 SSG 接口,搜索/字母走运行时 `/api/v1/public/brands`)。
- **占位模块组件已清理(2026-08-29)**:`components/pages/` 下 `About`/`Contact`/`LatestProjects`/`Portfolio`/`Login`/`Register`/`AuthForm`/`article` 等占位/旧详情组件查无引用、也不是真实路由,已删除并移除空目录。
- **导航死链清理(2026-08-30)**:用户决定移除 `/latest-projects`、`/about`、`/contact` 三个死链——已从 `Layout.astro` 主导航(早已不在)、`routes.ts`(`latestProjects`/`about`/`contact` 键)、`dictionary.ts`(about/contact/latest projects 翻译键)三处清除,全 src 扫描零残留引用。**剩余死链 `/portfolio`、`/login`、`/register` 保留未动**(Portfolio 导航项、Login/Signup 链接仍展示,用户未要求移除)。
- **「header 别动」解读(2026-08-30 明确)**:用户「header 别动」指不要擅自改导航结构与视觉;但用户**主动批准**的导航调整(Runway 下拉改 5 个 collection 分类、Street Style 下拉减为 3 城、删除死链项)均属于已授权变更,不算违规。`/en/login`、`/en/register` 实际并非真实页面(旧 MEMORY 记录有误),AuthForm 组件已随清理删除。
## 模块策略
- 站点按模块逐个完善,当前主线为「发布会模块」:首页 index + 走秀档案(RunwayLooks)+ 街拍(StreetSnaps)。
- 品牌索引页已下线(2026-08-26):`brands.astro` 与孤儿组件已删;品牌仅经走秀页侧栏 filter + 品牌弹窗呈现(热门 30 走 SSG,搜索/字母走运行时 `/api/v1/public/brands`)。
- 导航死链清理(2026-08-30):删 `/latest-projects`、`/about`、`/contact` 三处(Layout/routes/dictionary 三处同步清)。**保留未动的死链**:`/portfolio`、`/login`、`/register`(用户未要求动)。
- 「header 别动」= 不擅自改导航结构/视觉;但用户主动批准的调整(Runway 下拉分类、Street Style 减城、删死链)均属已授权,不算违规。
## 架构总览
- 前端:**d:/project/frontend_v2** — Astro SSG + Tailwind v4 + Alpine.js。`npm run build`(SSG 取数走 ssgRequest,失败回落空/假数据不影响构建)。
- 后端:**d:/project/backend_v2** — Go + Gin + GORM(MySQL),公开引擎 :8090(`/api/v1/public/*`、`/api/v1/auth/*`),SSG 内部引擎 :8091(仅 127.0.0.1,`/api/v1/ssg/*`,SSGToken 中间件,nginx 不反代)。
- **SSG 分组现状(2026-08-26)**:只剩两个端点——`/api/v1/ssg/index/runway`(首页 runway 区块,`IndexService.Runway`,hotBrand 20 条)+ `/api/v1/ssg/brands/hot`(走秀页热门品牌,`BrandService.Hot`,固定 30 条、按图片总数热度顺序,零参数)。`/ssg/brands`(全量 A-Z)与 `/ssg/articles/ids` 均已删除。
- **接口 i18n 约定**:后端按 `?locale=cn|en` 选列返回(pickLocale 缺翻译回落另一语言);前端取数统一复用带 locale 的原语(request/ssgRequest 等),**绝不裸写 fetch;语言以 URL 前缀为准**。
- 走秀标题 `title_cn` 已规则回填(backend_v2 `scripts/backfill_title_cn/main.go`,幂等);`description_cn` 为空白属预期,中文页回落英文。
- 前端:`d:/project/frontend_v2` — Astro SSG + Tailwind v4 + Alpine.js。构建 `npm run build`(SSG 取数走 ssgRequest,失败回落空/假数据不影响构建)。
- 后端:`d:/project/backend_v2` — Go + Gin + GORM(MySQL)。公开引擎 :8090(`/api/v1/public/*`、`/api/v1/auth/*`);SSG 引擎 :8091(仅 127.0.0.1,`/api/v1/ssg/*`,SSGToken 中间件,nginx 不反代)。
- SSG 端点仅剩:`/api/v1/ssg/index/runway`(首页 runway 区块)与 `/api/v1/ssg/brands/hot`(走秀页热门品牌,固定 30 条)。
- 接口 i18n:后端按 `?locale=cn|en` 选列(pickLocale 缺翻译回落另一语言);前端取数统一复用带 locale 原语(request/ssgRequest),**绝不裸写 fetch**;语言以 URL 前缀为准。
- 走秀标题 `title_cn` 已规则回填(幂等);`description_cn` 空白属预期(中文页回落英文)。
- 后端连接池:已设 `conn_max_idle_time`(config.yml 默认 60s,须 < MySQL wait_timeout)主动回收空闲连接,避免空闲连接被 MySQL 回收后池子持死连接报 `invalid connection`。**本地后端若长时间空闲后接口报 invalid connection,先重启后端**(`cd d:/project/backend_v2 && go build -o server-bin ./cmd/server` 后杀旧进程重启)。
- 前端 dev server **不热加载 `.env`**:改 `BASE_API` 后必须 `astro dev stop` + `astro dev --background` 重启才生效。浏览器旧 bundle 仍用旧 `BASE_API`,需**硬刷新**(Ctrl+Shift+R);旧 token 对新后端无效,需**重新登录**。改 BASE_API 指向本地/线上后端后,若历史/收藏/登录"没反应",先查 dev server 是否重启、浏览器是否硬刷新+重登录。
## 关键约定
- 视觉语言:黑白极简编辑风(monospace + Georgia 衬线大标题 + 细线),HBX / 时尚档案调性。字体全走 Tailwind 体系,禁止自写字体栈。
- **品牌名前端一律只显示英文**(用户偏好 2026-08-26:「前端品牌不要展示中文名称,标题可以留着」):api.ts 映射 `display = nameEn`;走秀标题/描述仍按 locale 选列。
- **API 命名哲学(用户原则)**:「每一个地方尽可能不要通过参数来复用接口,尽量一个功能一个接口」。查询参数仅限「同一资源的列表筛选/分页」;跨资源/独立功能一律独立路径(如 `brands/hot` 而非 `?featured=1`)。待办:公开侧 `/api/v1/public/brands` 的 featured/keyword/letter 参数拆分、`/articles` 的 `brand_id` 拆 `brands/:id/shows`、资源命名 `shows` vs `articles`、`popular` vs `featured`(目标清单见 artifact `api_endpoints_catalog.md`)。
- 视觉语言:黑白极简编辑风(monospace 小标签 + Georgia 衬线大标题 + 细线),HBX / 时尚档案调性。**纯黑白,禁用暖米色/柔和阴影**;字体全走 Tailwind 体系,禁止自写字体栈。
- 品牌名前端一律只显示英文(`display = nameEn`);走秀标题/描述仍按 locale 选列。
- API 命名哲学(用户原则):「一个功能一个接口」,查询参数仅限同资源列表筛选/分页;跨资源/独立功能一律独立路径(如 `brands/hot` 而非 `?featured=1`)。
- CSS:能 @apply 就 @apply;`<style>` 顶部 `@reference "tailwindcss";`。Tailwind v4 无零值类。
- 导航(Layout.astro):「header 别动」指不擅自改结构/视觉,但用户**主动批准**的导航调整有效(Runway 下拉改 4 个 collection 分类 rtw/menswear/couture/resort、Street Style 下拉减为「全部/Paris/New York/Milan」4 项、移除 latest-projects/about/contact 死链)。剩余死链 /portfolio、/login、/register 用户未要求动,保留。
- Layout 把 `<slot/>` 包在 `flex-1 flex flex-col`;页内根容器**禁用 `m-auto`**(cross-axis margin:auto 触发 shrink-to-fit → 抖动),用 `w-full` + `max-w-[x] mx-auto`。
## Layout 陷阱(2026-08-12)
- Layout 把 `<slot />` 包在 `flex-1 flex flex-col` 容器里;页内根容器**禁用 `m-auto`**(cross-axis margin:auto 触发 shrink-to-fit → 内容多少决定宽度、页面抖动)。用 `w-full`(可配 `max-w-[xxx]` + `mx-auto`)。
## Astro `<script>` 坑
- 作用域隔离:frontmatter 的 import/变量在打包型 `<script>` 拿不到,需脚本顶部重新 import。
- 不做模板插值:打包型 `<script>` 内 `{t('...')}` 不执行 → `t is not defined`;预翻译用 `define:vars` 或 data 属性注入,模板区 `{t()}` 正常。
- **`define:vars` + `import` = 致命组合(已踩坑)**:`define:vars` 会把脚本包成内联 IIFE 并注入 `const meta=...`,而原本脚本顶部的 `import` 语句被塞进函数体内 → `import` 在 IIFE/非模块上下文非法 → 整段脚本 SyntaxError 完全不执行(连带 `recordHistory`/事件绑定全失效)。**凡脚本里要 `import`,绝不能用 `define:vars`**。改用 `data-*` 属性在模板里透传变量,脚本内 `btn.dataset.xxx` 读取(Layout.astro 已用此模式)。en/cn `item/[id].astro` 曾因此导致详情页浏览历史/收藏全不工作,已修复。
- 片段页(无 `<html>/<head>` 的 .astro)的 `import '@/styles/global.css'` 在 build 时不会被注入 head → 产物缺 `<link rel=stylesheet>` 整页无样式。login.astro 曾因此丢样式,已补完整文档结构修复。
## Astro `<script>` 两大坑
- **作用域隔离**:frontmatter 的 import/变量在打包型 `<script>` 里拿不到(运行时 ReferenceError);脚本里要用什么就在脚本顶部重新 import。
- **不做模板插值**:打包型 `<script>` 里 `{t('...')}` 不会被执行 → `t is not defined`。预翻译用 `define:vars={{ hiLabel }}` 注入;模板区 `{t()}` 正常。
- 打包后脚本在 `_astro/*.js` 而非内联;查 dist 产物用 search_content(ripgrep),PowerShell 无 `Select-String -Recurse`。
## 数据分类
- 走秀 `brand_runway`:`collection_type`(rtw/menswear/couture/resort/pre_fall) + `season`(spring/fall/null) + `season_code`(SS26…),title 规则回填(25,206 行,幂等)。公开列表 `?collection_type=&season=&season_code=&year=` 筛选。地理维度暂不做。
- 街拍 `street_snap.city`(2026-08-30 落地):DB 列 VARCHAR(128) + 精确匹配 `WHERE city = ?`;前端 header 下拉 `?city=` + 侧栏 City pills + 深链选中。规范值存「首字母大写英文」(Paris/New York/Copenhagen…),前后端大小写敏感须一致;dev 库仅 12 行样本(Paris 8 / Copenhagen 4),其余城市本地为空属预期。
## 走秀数据分类(brand_runway)
- 三维分类字段:`collection_type`(rtw/menswear/couture/resort/pre_fall)、`season`(spring/fall/null)、`season_code`(SS26/FW25…派生)。靠 title 规则回填(幂等,25,206 行)。
- 公开列表 `?collection_type=&season=&season_code=&year=` 筛选;前端走秀页有系列/季节/年份 chips。
- 地理/城市维度(runway):用户明确暂不做。
- 分布:rtw 15203 / menswear 4061 / couture 672 / resort 2894 / pre_fall 2376。
## 街拍地区分类(street_snap.city,2026-08-30)
- 全链路已落地:DB 加 `city` 列(VARCHAR(128),脚本 `scripts/sql/004_add_streetsnap_city.sql` + 幂等回填 `scripts/backfill_city/main.go`,无 AutoMigrate)+ 后端 `StreetSnapQuery.City` 精确匹配 `WHERE city = ?` + 前端 header 下拉 `?city=` + 列表侧栏 City pills + 深链自动选中。
- **规范值约定(重要)**:city 列存「首字母大写英文规范名」(Paris / New York / Copenhagen …),前端 header 下拉与列表 `cityOptions` 的 value 用**同一大写值**,后端做精确匹配、大小写敏感,必须前后一致。backfill 首选从 title 第一段(location,`\t` 分隔)按城市关键词匹配,兜底从 source_url/cover 的 URL slug 解析。
- 前端 `t('Paris')` 经 `translate()` 自动 `key.toLowerCase()` 查字典,中文回落「巴黎」,i18n 正常。
- dev 库 db_dev.street_snap 仅 12 行样本(Paris 8 / Copenhagen 4),其余下拉城市(London/Milan…)本地预览为空属预期;生产真实爬取数据才有完整覆盖。
- 验证:重启后端 :8090 后,`?city=Paris`→8、`?city=Copenhagen`→4、`?city=London`→0、无参→12,全链路通过。client_sign 本地 `enabled:false`,可直接 curl/Node 验证。
## 前端组件复用(2026-08-29)
- **详情页统一 `/item`**:走秀 lookbook 与街拍详情全部收口到 `src/pages/{locale}/item/[id].astro`,服务端按 `getSsrArticle(id)` → `getSsrStreetSnap(id)` 回落渲染(先 runway 后 street)。`ROUTES.article` 与 `ROUTES.streetSnap` 均指向 `/item/${id}`(`runway-looks/[id]`、`street-snaps/[id]`、`article.astro(?id=)` 等旧详情页均已删除)。列表页回链仍指向列表 `/runway-looks`、`/street-snaps`。
- **列表页共享资源**:两个列表组件(RunwayLooks / StreetSnaps)同构,抽成 `src/styles/look-grid.css`(卡片/网格/分页/侧栏/spinner/骨架/动画)+ `src/lib/looks-grid.ts`(`SPIN_SVG`、`makeCardSlots(images,count,opts)`、`buildPageList(total,cur)`);品牌筛选弹窗 `src/components/BrandModal.astro`(仅 RunwayLooks 用,`openBrandModal/confirmBrandModal` 等由页面脚本驱动)。卡片标记因在 Alpine `x-for` 内客户端渲染,**不能**提成 Astro 组件。
## 前端复用
- 详情页统一 `/item`:`src/pages/{locale}/item/[id].astro`,服务端 getSsrArticle→getSsrStreetSnap 回落;`ROUTES.article`/`streetSnap` 均指向 `/item/${id}`。列表回链指向 `/runway-looks`、`/street-snaps`。
- 列表组件同构,抽 `src/styles/look-grid.css` + `src/lib/looks-grid.ts`(SPIN_SVG / makeCardSlots / buildPageList);品牌筛选弹窗 `BrandModal.astro`(仅 RunwayLooks)。卡片标记在 Alpine `x-for` 内客户端渲染,不能提成 Astro 组件。
## 账号体系(2026-08-31)
- 决策:仅预置内部账号,**不开放注册**(前端无注册入口,避免垃圾账号/撞库)。账号由 seed 脚本预置。
- 默认内部账号:`admin` / `admin@studio.local` / 密码 `Studio#2026!Admin`(env `SEED_ADMIN_PASSWORD` 可覆盖;**务必改默认密码**)。
- **双令牌 JWT(2026-08-31 落地)**:access token 短命(默认 **2h**,无状态 JWT,仅前端请求用)+ refresh token 长命(默认 **30d**,随机串只存 SHA256 哈希落库 `refresh_tokens`)。纯无状态 JWT 无法吊销,故 refresh 必须落库才能做踢下线。
- 接口(均在 `/api/v1/auth/`,除 /me 外均公开、无需 Bearer):
- `POST /login` → `{access_token, refresh_token, expires_in(秒), user}`(login 返回结构已从 `{token,user}` 改为双 token,前端已同步)。**登录即顶旧会话**:签发前先吊销该用户全部既有 refresh,保证同账号同一时刻只有一条活性会话(用户明确诉求:不希望同一账号被不同人同时登录)。
- `POST /refresh` → `{access_token, expires_in}`(用 refresh_token 换 access;**refresh 复用、不轮换**)。
- `POST /logout` → `{ok:true}`(吊销当前 refresh,单设备登出)。
- `POST /logout-all` → `{revoked:N}`(按 refresh 反查用户,吊销其全部 refresh = **踢下线/全设备登出**)。
- `GET /me`(Bearer 校验)保留。
- 后端改动:`config.JWT` 新增 `RefreshExpireHours`(env `JWT_REFRESH_EXPIRE_HOURS`);`AuthService` 加 `Refresh/Logout/RevokeAllByRefresh`;`RefreshTokenRepository`(Create/FindByHash/Revoke/RevokeAllByUser/DeleteExpired);`model.RefreshToken`(gorm 表 `refresh_tokens`)。
- 建表:`scripts/sql/006_create_refresh_tokens.sql`;一键建表脚本 `scripts/migrate_refresh/main.go`(`go run ./scripts/migrate_refresh`,DSN 默认 root:root@127.0.0.1:3306/db_dev,可由 `DB_DSN` 覆盖)。
- 前端 `api.ts`(2026-08-31 改):`saveSession(access,refresh,user)` 存 `fa_token`/`fa_refresh`/`fa_user`;新增 `refreshSession()`(POST /auth/refresh,刷新 access、复用 refresh);`fetchMe()` 走 `authedFetch`(遇 401 静默 refresh 一次再重试);`logout()`(吊销当前 refresh 再清本地)、`logoutAll()`(踢下线再清本地)。`clearSession()` 一并清三键。
- 踢下线效果:**普通踢**=吊销 refresh,已签发 access 在 2h 窗口内仍有效,到期后才被弹回登录(非即时,内部工具足够);未做 `token_version` 即时吊销方案(如需访问令牌即时失效再加 `users.token_version` 并在 `middleware.Auth` 比对)。
- 验证(2026-08-31):`go build -o bin/server.exe ./cmd/server` EXIT=0;端到端 Node 脚本确认 login→refresh→logout-all(revoked:1)→再 refresh=401(ALL_OK);同账号二次 login 后首次 refresh 返回 401、二次 refresh 返回 200(MUTEX_OK,互斥登录生效)。前端 `npm run build` EXIT=0。
- **硬门禁(用户拍板)**:RunwayLooks / StreetSnaps 的筛选与翻页入口调 `requireLogin()`,未登录则弹出内嵌登录表单(`showLoginModal`);登录成功后 `loadPage(1)` 按当前筛选刷新。首屏 `init()` 的 `loadPage(1)` 不拦(保证未登录能看到第一页)。
- 页面:`src/pages/{en,cn}/login.astro`(黑白极简风,Alpine 表单,登录后跳 `?redirect=` 或 /account)、`account.astro`(资料+登出+收藏/历史占位,未登录跳 /login?redirect=/account)。
- 视觉:登录/个人中心沿用站点黑白极简编辑风(monospace + 细线下划线输入框 + Georgia 衬线大标题 + hover:opacity-60)。
- 仅预置内部账号,不开放注册(前端无注册入口)。`seed_users/main.go` 创建两个账号:`admin`(密码取 `SEED_ADMIN_PASSWORD`,缺省 `Studio#2026!Admin`)+ `root`/`root`(测试弱密码,uid 通常为 8)。**本地库 `db_dev` 确有 root/root**;实测 `POST /api/v1/auth/login {account:"root",password:"root"}` 打到 `http://localhost:8090` 返回 200,`GET /auth/history` 显示 root 账号下已有 `street-snaps`/`runway-looks` 两条真实图集历史。**不要再说"本地无 root / root 登录必失败"——那是误判**(2026-09-02 因一次 401 错判,已纠正)。前端 `.env` 当前 `BASE_API=http://localhost:8090`。
- 双令牌 JWT:access 默认 **7 天(168h)**(无状态,前端请求用)+ refresh 默认 **30d(720h)**(随机串仅存 SHA256 哈希落库 `refresh_tokens`)。登录即吊销该用户全部既有 refresh(互斥登录)。
- 接口(`/api/v1/auth/`,除 /me 外公开):`POST /login`→`{access_token,refresh_token,expires_in,user}`;`POST /refresh`→`{access_token,expires_in}`(refresh 复用不轮换);`POST /logout`(单设备);`POST /logout-all`→`{revoked:N}`(踢下线);`GET /me`(Bearer)。
- 前端 `api.ts`:`saveSession/clearSession/refreshSession/fetchMe(authedFetch 遇 401 静默 refresh 一次)/logout(吊销当前 refresh 再清本地)/logoutAll`。
- 未做 `token_version` 即时吊销(access 在 7 天窗口内仍有效),如需即时失效再加。
- 硬门禁:RunwayLooks/StreetSnaps 筛选与翻页入口调 `requireLogin()`,未登录弹内嵌登录表单,登录成功 `loadPage(1)` 刷新;首屏 `init()` 的 `loadPage(1)` 不拦。
- 个人中心 `account.astro`:报头(方块字母头像+用户名大标题) + 两 tab(**Saved looks / My History**),已移除 Overview/Sessions & Security/Settings/退出入口/偏好设置/编辑资料(2026-09-01 精简)。
- **收藏已服务端化(2026-09-02)**:后端表 `favorites` + `/api/v1/auth/favorites`(GET 分页 `?page&per_page` / POST 新增幂等 / DELETE /:target_uid / POST `/check` 批量校验当前页收藏态),按 `user_id` 隔离,`target_uid` 存前端编码串(runway `r=` / street `s=`)。前端 `src/lib/favorites.ts`:账户页用 `fetchFavoritesPage()` 服务端分页;列表卡片用 `checkFavorited(ids)` 仅把当前页可见 id 发给后端打勾(与收藏总量解耦,10 万级也常数级);收藏/取消本地乐观 + 后台同步(失败不回滚本地)。favorites 表含复合索引 `idx_user_created(user_id,created_at)` 支撑深翻页。
- **浏览历史(2026-09-02 晚收敛为「文章级」,用户澄清纠正)**:语义=「点开过哪篇文章」,记录时机在 **item 详情页**(`recordHistory(meta.id)`,meta.id 是文章对外编码串 `r=`走秀 / `s=`街拍,编码串自带类型、无需 kind);**RunwayLooks/StreetSnaps 列表页进页不再记**(此前按"图集=列表页"实现是理解错,已纠正)。后端 `histories` 表收敛为 `(user_id, target_uid, viewed_at)`:`kind/target_type/title/cover/brand` 五列已 DROP(008 建表脚本同步改 + `migrate_refresh` 对存量库 ALTER,实测 GET items 仅 `{id,viewed_at}`);唯一键 `uniq(user_id,target_uid)`;保留 `HISTORY_CAP=2000` FIFO(文章会积累,有存在意义)。接口 `/api/v1/auth/history`:GET 分页 / POST `{target_uid}` upsert / DELETE /:target_uid / DELETE 清空,按 `user_id` 隔离;kind 参数与校验已随收敛移除(kind=all 字面过滤坑随之消失)。账户页 My History:服务端分页拿 id 列表后**逐篇回查公开详情**(`api.ts` 的 `getHistoryMeta(id)` → `/public/runway-looks|street-snaps/:id` 取 title/cover/brand;用户选"只存 id、展示回查"),卡片跳回文章详情,无 kind 筛选 tab。前端 `src/lib/history.ts`:`recordHistory(id)` / `fetchHistoryPage(page,perPage)`(服务端分页+回查,单条回查失败容忍回落本地)。
- **待办服务端化**:users 加 display_name/bio、refresh_tokens 列表接口。
- 视觉:登录/个人中心沿用站点黑白极简编辑风(monospace 小标签 + 细线下划线输入框 + Georgia 衬线大标题 + hover:opacity-60)。
## 部署(Docker + Gitea Actions)
- 交付文件:前端 Dockerfile / nginx.conf / docker-compose.yml / deploy.sh / .gitea/workflows/deploy.yml / .env.example / DEPLOY.md;后端 Dockerfile。nginx 拦截 `/api/v1/ssg/` 404;健康检查 `/api/v1/public/brands`。
## 部署
- Docker + Gitea Actions:前端 Dockerfile/nginx.conf/docker-compose.yml/deploy.sh/.gitea/workflows/deploy.yml/.env.example/DEPLOY.md + 后端 Dockerfile。nginx 拦截 `/api/v1/ssg/` 404;健康检查 `/api/v1/public/brands`。