Files
frontend_v2/.workbuddy/memory/MEMORY.md
toom1996 97b023bbbd
Some checks failed
deploy / deploy (push) Has been cancelled
update
2026-08-31 19:59:36 +08:00

70 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 项目长期记忆(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 组件已随清理删除。
## 架构总览
- 前端:**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` 为空白属预期,中文页回落英文。
## 关键约定
- 视觉语言:黑白极简编辑风(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`)。
- 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 陷阱(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>` 里拿不到(运行时 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/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 组件。
## 账号体系(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)。
## 部署(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`。