Files
frontend_v2/.workbuddy/memory/MEMORY.md
toom1996 36f3ad039f
Some checks failed
deploy / deploy (push) Has been cancelled
update
2026-09-03 19:45:36 +08:00

9.7 KiB
Raw Blame History

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

架构总览

  • 前端 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,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 拿 PID。本地后端长时间空闲后报 invalid connection 先重启后端(连接池 conn_max_idle_time 已设,须 < MySQL wait_timeout)。
  • 前端 dev server 不热加载 .env:改 BASE_API 后需 astro dev stop+astro dev --background 重启;浏览器旧 bundle 需硬刷新、旧 token 失效需重登录。

关键约定

  • 视觉:黑白极简编辑风(monospace 小标签 + Georgia 衬线大标题 + 细线),HBX/时尚档案调性。纯黑白,禁用暖米色/柔和阴影;字体全走 Tailwind 体系。
  • 品牌名前端只显英文(display=nameEn);走秀标题/描述按 locale 选列。
  • API 命名:一个功能一个接口,跨资源/独立功能独立路径(如 brands/hot 而非 ?featured=1)。
  • CSS:能 @apply 就 @apply;<style> 顶部 @reference "tailwindcss";;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,module/defer 在 window.load 前执行无竞态),Raleway 字体链随组件 hoist 到 head。文案 Portfolio Studio + 副标 Loading,黑白配色。改加载屏只动这一个文件。

Astro <script> 坑

  • 作用域隔离:打包型 <script> 拿不到 frontmatter 的 import/变量,需脚本顶部重新 import。
  • define:vars + import = 致命(已踩坑):define:vars 把脚本包成 IIFE 注入 const meta=...,原顶部 import 被塞进函数体 → 非模块上下文非法 → 整段 SyntaxError 不执行。凡脚本要 import,改用 data-* 属性透传(Layout.astro 已用)。en/cn item/[id].astro 曾因此详情页历史/收藏全失效,已修。
  • 片段页(无 <html>/<head> 的 .astro)的 import '@/styles/global.css' build 时不注入 head → 整页无样式。login.astro 曾因此丢样式,已修。
  • Alpine x-show/x-on 等属性表达式访问不到模块 import 的符号:@astrojs/alpinejs 下,.astro <script> 里 import { getUser } from "@/lib/api" 的函数,在 x-show="!getUser() && preview" 这类属性表达式里是未定义自由变量(Alpine 只认组件 data 属性 / $magic / global),求值抛 ReferenceError → 绑定失效、元素卡在 x-cloak 的 display:none。已踩坑:详情页「剩余 N 张」占位遮罩与底部门禁用 x-show="!getUser()&&preview",SSR 静态 HTML 有遮罩但运行时全隐藏(误判成功只因验证看了静态 HTML)。修复:组件 data 增 isAuthed:false,init() 用 this.isAuthed = !!getUser() 初始化、auth:login 事件置 true,x-show 改用 !isAuthed && preview(列表分页器 x-show="getUser() && lastPage>1" 同病,已一并改 isAuthed)。铁律:Alpine 属性表达式里只写组件 data 属性或 $magic,绝不直接调模块 import 函数;要读外部状态先存进 data 属性。
  • 验证 Alpine 运行时务必真跑浏览器(或至少确认表达式可解析),只看 SSR 静态 HTML 会漏掉运行时绑定失效。

模块策略

  • 主线:首页 index + 走秀档案(RunwayLooks)+ 街拍(StreetSnaps)。品牌索引页已下线(2026-08-26):品牌仅经走秀页侧栏 filter + 品牌弹窗(热门 30 SSG,搜索/字母运行时 /api/v1/public/brands)。
  • 导航死链清理(2026-08-30):删 /latest-projects//about//contact(Layout/routes/dictionary 同步)。保留未动死链:/portfolio//login//register。「header 别动」= 不擅改导航结构/视觉;用户已批的调整(Runway 下拉分类、Street Style 减城、删死链)属授权。

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

  • 用户真实意图:列表页照常展示全部图集,限制只发生在点进图集详情后——未登录仅看前 5 张图,其余提示登录(此前误做成列表页限制已回退)。
  • 后端硬截断(真防护,落详情接口):GET /api/v1/public/runway-looks/:id 与 /street-snaps/:id 判断 middleware.UserIDFrom(c)——未登录且 len(Images)>dto.PreviewLimit(=5) 时 Images=Images[:5] 并回 preview:true。response.DataPreview 包新增带 image_total(截断前图片总数),供前端渲染剩余占位遮罩。已登录(带 Bearer)返回完整图片集、无 preview。列表接口 response.Page 全量,翻页 page>1 仍 401 由 PublicFirstPageAuth 兜底。
  • PublicFirstPageAuth 始终解析 Bearer(有效写 Context),仅 page>1 强制;详情路由无 page → 匿名放行、已登录写态。
  • 前端:详情组件 Item.astro(runway)/StreetSnap.astro(street) 由 SSR getSsr* 透传 preview 与 imageTotal prop;画廊渲染 previewImages=SSR.images.slice(0,5)(SSR 渲染前 5 张)+ 剩余 (imageTotal-5) 张空图遮罩(锁图标 + "login to view" + "+N photos",x-show="!isAuthed && preview"+x-cloak,点击 goLogin())+ 底部 Preview 门禁块。isAuthed 是组件 data 属性(init() 用 getUser() 初始化、auth:login 置真),因 getUser 是模块 import 符号、不能直接用于 Alpine 表达式(见 Astro <script> 坑)。已登录或 auth:login 触发 getArticleDetailAuthed/getStreetSnapDetailAuthed(带 Bearer 取全量)把第 6 张起追加进 extraImages,此时 isAuthed 为真、遮罩 x-show 为假自动隐藏。getUser() 读 localStorage fa_user。
  • 列表页已回退全量展示,仅保留筛选/翻页 requireLogin() 硬门禁。
  • i18n:login to view(cn 登录查看)/photos(cn 张照片)/login to view all photos(cn 登录查看全部图片,底部门禁用)。

账号体系

  • 仅预置内部账号不开放注册。seed_users/main.go 建 admin(SEED_ADMIN_PASSWORD,缺省 Studio#2026!Admin) + root/root(测试弱密码,uid 通常 8)。本地 db_dev 确有 root/root,打 http://localhost:8090 登录 200——勿再误判"本地无 root"。前端 .env 当前 BASE_API=http://localhost:8090。
  • 双令牌 JWT:access 默认 7d(168h) 无状态 + refresh 默认 30d(720h) 仅存 SHA256 落库;登录即吊销该用户全部 refresh(互斥登录)。接口 /api/v1/auth/:/login /refresh(复用不轮换) /logout /logout-all /me(Bearer)。前端 api.ts:saveSession/clearSession/refreshSession/fetchMe/authedFetch(401 静默 refresh 一次)/logout/logoutAll。
  • 硬门禁:RunwayLooks/StreetSnaps 筛选与翻页入口 requireLogin(),未登录弹内嵌登录表单,登录成功 loadPage(1) 刷新;首屏 init() 的 loadPage(1) 不拦。
  • 收藏服务端化(2026-09-02):表 favorites + /api/v1/auth/favorites(GET 分页/POST 幂等/DELETE//check 批量校验当前页收藏态),按 user_id 隔离,target_uid 存编码串(runway r=/street s=)。前端 src/lib/favorites.ts:账户页 fetchFavoritesPage() 服务端分页;列表卡片 checkFavorited(ids) 仅发当前页可见 id;收藏/取消本地乐观+后台同步。
  • 浏览历史文章级(2026-09-02 收敛):记录时机在 item 详情页 recordHistory(meta.id)(编码串自带类型,无需 kind);列表页进页不再记。后端 histories 表 (user_id,target_uid,viewed_at),唯一键 uniq(user_id,target_uid),HISTORY_CAP=2000 FIFO。前端 src/lib/history.ts:账户页 My History 服务端分页+逐篇回查 getHistoryMeta(id) 取 title/cover/brand。recordHistory 仅在 getUser() 时调用。
  • 个人中心 account.astro:报头 + 两 tab(Saved looks / My History),已精简移除 Overview/Sessions 等。

部署

  • 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(2026-08-30):VARCHAR(128) 精确匹配 WHERE city=?;前端 header 下拉 ?city= + 侧栏 City pills。规范值「首字母大写英文」(Paris/New York/Copenhagen…),大小写敏感须一致;dev 库仅 12 行样本属预期。

前端复用

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