9.7 KiB
9.7 KiB
项目长期记忆(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/cnitem/[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) 由 SSRgetSsr*透传preview与imageTotalprop;画廊渲染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()读 localStoragefa_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存编码串(runwayr=/streets=)。前端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=2000FIFO。前端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,SSRgetSsrArticle→getSsrStreetSnap回落;ROUTES.article/streetSnap指向/item/${id}。 - 列表组件同构,抽
src/styles/look-grid.css+src/lib/looks-grid.ts;品牌筛选弹窗BrandModal.astro(仅 RunwayLooks)。卡片标记在 Alpinex-for内客户端渲染,不能提成 Astro 组件。