Files
frontend_v2/.workbuddy/memory/MEMORY.md
toom1996 d6cb616337 update
2026-09-13 21:39:50 +08:00

11 KiB
Raw Blame History

项目长期记忆(Project MEMORY.md)

架构总览

  • 前端 d:/project/frontend_v2:Astro(adapter: node standalone,SSR 运行时取数)+ 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,nginx 不反代)。
  • 接口 i18n:后端按 ?locale=cn|en 选列;前端取数复用带 locale 原语(request/ssgRequest),绝不裸写 fetch;语言以 URL 前缀为准。
  • 后端编译产物坑(Windows):go build -o server-bin ./cmd/server 实际产出无扩展名 server-bin,但运行的是 server-bin.exe。必须 go build -o server-bin.exe 显式覆盖 .exe,再 Start-Process server-bin.exe;杀进程 (Get-NetTCPConnection -LocalPort 8090).OwningProcess。本地后端长空闲报 invalid connection 先重启后端(连接池 conn_max_idle_time 须 < MySQL wait_timeout)。
  • 前端 dev server 不热加载 .env:改 BASE_API 后 astro dev stop+astro dev --background;浏览器旧 bundle 硬刷新、旧 token 失效重登录。

关键约定

  • 视觉:黑白极简编辑风(monospace 小标签 + Georgia 衬线大标题 + 细线),纯黑白,禁用暖米色/柔和阴影;字体全走 Tailwind 体系。
  • 品牌名前端只显英文(display=nameEn);走秀标题/描述按 locale 选列。
  • API 命名:一个功能一个接口,跨资源/独立功能独立路径(如 brands/hot 而非 ?featured=1)。
  • CSS:能 @apply 就 @apply;<style> 顶部 @reference "tailwindcss";(Tailwind v4 编译期指令,最终 HTML 不出现,样式已生效);Tailwind v4 无零值类。
  • Layout <slot/> 包 flex-1 flex flex-col;页内根容器禁用 m-auto(shrink-to-fit 抖动),用 w-full+max-w-[x] mx-auto。
  • Header/下拉 FOUC:所有 x-show 下拉(Runway/Street/语言/账号/未登录 Login)一律带 x-cloak(global.css 已 [x-cloak]{display:none!important})。
  • 加载屏组件化:src/components/LoadingScreen.astro 封装标记+样式+打包 <script>(无 is:inline),Raleway 字体链随组件 hoist 到 head。改加载屏只动这一个文件。

Astro <script> 铁律(踩坑汇总)

  1. 作用域隔离:打包型 <script> 拿不到 frontmatter 的 import/变量,需脚本顶部重新 import。
  2. define:vars + import = 致命:define:vars 把脚本包成 IIFE 注入 const meta=...,原顶部 import 被塞进函数体 → SyntaxError 整段不执行。凡脚本要 import,改用 data-* 属性透传(item/[id].astro 的 fav-fab 已用此套路)。
  3. 片段页(无 <html>/<head>)的 import '@/styles/global.css' build 时不注入 head → 整页无样式(login.astro 曾踩)。
  4. Alpine 属性表达式访问不到模块 import 符号:x-show="!getUser() && preview" 中 getUser 是 import 来的自由变量,Alpine 只认组件 data 属性 / $magic / global → 求值抛 ReferenceError、元素卡 x-cloak 隐藏。铁律:表达式里只写组件 data 属性或 $magic,外部状态先存进 data 属性(isAuthed 即为此而生)。
  5. 验证 Alpine 运行时务必真跑浏览器(或至少确认表达式可解析),只看 SSR 静态 HTML 会漏掉运行时绑定失效。
  6. 共享 Alpine 组件:document.addEventListener('alpine:init', () => Alpine.data('name', () => ({...}))) 注册,x-data="name()" 引用(与 articleView/streetSnapView/imgLoader/showsPage 同款)。
  7. Swiper 生命周期(首页 Index.astro):initSwipers() 开头先 destroySwipers() 防重复/泄漏;统一入口 setupSwipers() 用 rAF 延后一帧,挂 DOMContentLoaded + astro:page-load(View Transitions 重入);swiper.update() 用 updateScheduled + rAF 合并防多图同帧狂 update;Tab 点击 document 事件委托(e.target.closest('.tab'))兼容 VT 新 DOM。

模块策略

  • 主线:首页 + 走秀档案(RunwayLooks)+ 街拍(StreetSnaps)。品牌索引页已下线(2026-08-26),品牌经走秀页侧栏 filter + 品牌弹窗(热门 30 SSG,搜索/字母运行时 /api/v1/public/brands)。
  • 导航死链清理(2026-08-30):删 /latest-projects//about//contact。保留未动死链 /portfolio//login//register。「header 别动」= 不擅改导航结构/视觉。

未登录预览门禁(详情页,2026-09-03 定稿 + 后续)

  • 用户意图:列表页全量展示,限制只发生在点进图集详情后——未登录仅看前 5 张图,其余锁图遮罩提示登录。
  • 后端硬截断(真防护):GET /api/v1/public/runway-looks/:id 与 /street-snaps/:id 按 middleware.UserIDFrom(c) 判匿名且 len(Images)>5 时 Images=Images[:5] 并回 preview:true + image_total(截断前总数)。已登录(带 Bearer)返回完整图片集、无 preview。列表接口全量,翻页 page>1 仍 401 由 PublicFirstPageAuth 兜底(始终解析 Bearer、仅 page>1 强制)。
  • 前端:详情组件 Item.astro(runway)/StreetSnap.astro(street) 由 SSR getSsr* 透传 preview/imageTotal;画廊 SSR 渲染前 5 张 + 剩余 (imageTotal-5) 张空图遮罩(锁图标 + "login to view" + "+N photos",x-show="!isAuthed && preview" + x-cloak,点 goLogin())。已登录/auth:login 触发 getArticleDetailAuthed/getStreetSnapDetailAuthed(带 Bearer 取全量)把第 6 张起追加进 extraImages,isAuthed 为真时遮罩自动隐藏。getUser() 读 localStorage fa_user。
  • 底部 Preview 大框门禁已于 2026-09-03 后续移除(用户认为意义不大),仅保留画廊内遮罩提示;对应 dict key login to view all photos 已删。

账号体系

  • 仅预置内部账号不开放注册。seed_users 建 admin(SEED_ADMIN_PASSWORD 缺省 Studio#2026!Admin) + root/root(uid 通常 8,本地 db_dev 确有,登录 200)。前端 .env BASE_API=http://localhost:8090。
  • 双令牌 JWT:access 7d 无状态 + refresh 30d 仅存 SHA256 落库;登录即吊销该用户全部 refresh(互斥登录)。/api/v1/auth/:/login /refresh(复用不轮换) /logout /logout-all /me(Bearer)。前端 src/lib/auth.ts(2026-09-13 从 api.ts 拆出):saveSession/clearSession/refreshSession/fetchMe/authedFetch(401 静默 refresh 一次)/login/logout;getUser/getAccessToken/getRefreshToken 也在此。业务取数在 src/lib/api.ts,签名在 src/lib/crypto.ts(clientSign),请求语言透传在 src/lib/locale.ts(setApiLocale/currentLocale/withLocale)。依赖无环:api→{crypto,auth,locale}、auth→locale。
  • 硬门禁:RunwayLooks/StreetSnaps 筛选与翻页 requireLogin(),未登录弹内嵌登录表单,登录成功 loadPage(1);首屏 init() 的 loadPage(1) 不拦。
  • 收藏服务端化:表 favorites + /api/v1/auth/favorites(GET 分页/POST 幂等/DELETE//me/favorites/checks 批量校验)。按 user_id 隔离,target_uid 编码串(图集 runway r=/street s=,图片 i=/j=)。前端 src/lib/favorites.ts:fetchFavoritesPage() 服务端分页;checkFavorited(ids) 仅发当前可见 id(单发请求,无循环);收藏/取消本地乐观 + 后台同步。
    • item 页收藏态已合并进 authed 详情接口(2026-09-13 落地):原 checkFavorited 触发 3 次(①② Item.astro/StreetSnap.astro 图片级 init+loadFull、③ fav-fab 图集级)已消除——前端 Item.astro/StreetSnap.astro 的 loadFull() 改为 loadAuthedState(),登录态用一次 getArticleDetailAuthed/getStreetSnapDetailAuthed 拿回完整图片集+收藏态:图集级 fav 经 fav:set 事件广播给 fav-fab、图片级以 im.favorited 重建 favIds;fav-fab 改监听 fav:set 不再调 checkFavorited([meta.id])。后端 article_handler.go/street_snap_handler.go Detail 在 UserIDFrom 命中时一次性 favSvc.Check 回写 detail.Favorited + Images[].Favorited(匿名不动)。item 页已登录收藏校验 3→0 次。列表页 RunwayLooks.astro/StreetSnaps.astro 仍用 /me/favorites/checks 批量校正(保留);dead 单数 legacy /auth/favorites/check 路由已删。铁律:收藏态只能合并进「带 Bearer 的 authed 详情接口」,匿名公开/列表接口绝不合并。
  • 浏览历史(2026-09-02):记录时机在 item 详情页 recordHistory(meta.id)(编码串带类型,无需 kind);列表页不记。后端 histories 表唯一键 uniq(user_id,target_uid),HISTORY_CAP=2000 FIFO。前端 src/lib/history.ts:账户页服务端分页 + 逐篇回查 getHistoryMeta(id)。recordHistory 仅 getUser() 时调用。
  • 个人中心 account.astro:报头 + 两 tab(Saved looks / My History)。

部署

  • Docker + Gitea Actions:Dockerfile/nginx.conf/docker-compose.yml/deploy.sh;nginx 拦截 /api/v1/ssg/ 404,健康检查 /api/v1/public/brands。

数据分类

  • 走秀 brand_runway:collection_type(rtw/menswear/couture/resort/pre_fall)+season+season_code(SS26…),title 规则回填(幂等)。公开列表 ?collection_type=&season=&season_code=&year= 筛选。
  • 街拍 street_snap.city:VARCHAR(128) 精确匹配 WHERE city=?;前端 header 下拉 ?city= + 侧栏 City pills。规范值「首字母大写英文」(Paris/New York/Copenhagen…),大小写敏感须一致;dev 库仅 12 行样本属预期。

前端复用

  • 详情页统一 /item:src/pages/{locale}/item/[id].astro(prerender=false,运行时 SSR getSsrArticle→getSsrStreetSnap),id 类型前缀 r=/s= 直接查对应表无回落。ROUTES.article/streetSnap 指向 /item/${id}。
  • 列表组件同构,抽 src/styles/look-grid.css+src/lib/looks-grid.ts;品牌筛选弹窗 BrandModal.astro(仅 RunwayLooks)。卡片标记在 Alpine x-for 内客户端渲染,不能提成 Astro 组件。

i18n 字典约定(src/i18n/dictionary.ts)

  • 结构 export const dict: Record<string, {cn:string}>。key 即英文原文(en 直接返 key),value 只存非默认语言(仅 cn)。translate(key,locale) 对 key toLowerCase() 后查,key 大小写不敏感;缺翻译 en 回退 + dev 告警。
  • 调用面:组件用 t('English phrase');部分标题经变量 t(city) 传入(城市 18 个,勿因「无字面量 t()」误删)。
  • 中文绝不可当 key:t('中文') 在 en 态显示中文。硬编中文先改英文 key 并在 dict 补 {cn:'中文'}。
  • 2026-09-13 审计:补 6 缺失 key(brands/Most photos/Newest year/Oldest year/Year/Sort)、中文 key 全部的 runway 改 all runway looks、删 20 个零调用死条目;字典 92→78 条。