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

56 lines
9.7 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)
## 架构总览
- 前端 `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 组件。