63 lines
11 KiB
Markdown
63 lines
11 KiB
Markdown
# 项目长期记忆(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 条。
|