11 KiB
11 KiB
项目长期记忆(Project MEMORY.md)
架构总览
- 前端
d:/project/frontend_v2:Astro(adapter: nodestandalone,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须 < MySQLwait_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> 铁律(踩坑汇总)
- 作用域隔离:打包型
<script>拿不到 frontmatter 的 import/变量,需脚本顶部重新 import。 define:vars+import= 致命:define:vars把脚本包成 IIFE 注入const meta=...,原顶部import被塞进函数体 → SyntaxError 整段不执行。凡脚本要 import,改用data-*属性透传(item/[id].astro 的 fav-fab 已用此套路)。- 片段页(无
<html>/<head>)的import '@/styles/global.css'build 时不注入 head → 整页无样式(login.astro 曾踩)。 - Alpine 属性表达式访问不到模块 import 符号:
x-show="!getUser() && preview"中getUser是 import 来的自由变量,Alpine 只认组件 data 属性 /$magic/ global → 求值抛 ReferenceError、元素卡x-cloak隐藏。铁律:表达式里只写组件 data 属性或$magic,外部状态先存进 data 属性(isAuthed即为此而生)。 - 验证 Alpine 运行时务必真跑浏览器(或至少确认表达式可解析),只看 SSR 静态 HTML 会漏掉运行时绑定失效。
- 共享 Alpine 组件:
document.addEventListener('alpine:init', () => Alpine.data('name', () => ({...})))注册,x-data="name()"引用(与articleView/streetSnapView/imgLoader/showsPage同款)。 - 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) 由 SSRgetSsr*透传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()读 localStoragefa_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)。前端.envBASE_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编码串(图集 runwayr=/streets=,图片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.goDetail 在UserIDFrom命中时一次性favSvc.Check回写detail.Favorited+Images[].Favorited(匿名不动)。item 页已登录收藏校验 3→0 次。列表页RunwayLooks.astro/StreetSnaps.astro仍用/me/favorites/checks批量校正(保留);dead 单数 legacy/auth/favorites/check路由已删。铁律:收藏态只能合并进「带 Bearer 的 authed 详情接口」,匿名公开/列表接口绝不合并。
- item 页收藏态已合并进 authed 详情接口(2026-09-13 落地):原
- 浏览历史(2026-09-02):记录时机在 item 详情页
recordHistory(meta.id)(编码串带类型,无需 kind);列表页不记。后端histories表唯一键uniq(user_id,target_uid),HISTORY_CAP=2000FIFO。前端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,运行时 SSRgetSsrArticle→getSsrStreetSnap),id 类型前缀 r=/s= 直接查对应表无回落。ROUTES.article/streetSnap指向/item/${id}。 - 列表组件同构,抽
src/styles/look-grid.css+src/lib/looks-grid.ts;品牌筛选弹窗BrandModal.astro(仅 RunwayLooks)。卡片标记在 Alpinex-for内客户端渲染,不能提成 Astro 组件。
i18n 字典约定(src/i18n/dictionary.ts)
- 结构
export const dict: Record<string, {cn:string}>。key 即英文原文(en 直接返 key),value 只存非默认语言(仅 cn)。translate(key,locale)对 keytoLowerCase()后查,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 条。