Files
frontend_v2/.workbuddy/memory/MEMORY.md
toom1996 97b023bbbd
Some checks failed
deploy / deploy (push) Has been cancelled
update
2026-08-31 19:59:36 +08:00

12 KiB
Raw Blame History

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

模块策略(重要)

  • 站点按模块逐个完善。当前上线「发布会模块」:首页 index + 走秀列表/详情(RunwayLooks = 走秀档案页,articles/shows 页面已在 i18n 重构中重组)。
  • 品牌索引页已下线(2026-08-26 确认):src/pages/brands.astro 及孤儿组件 src/components/pages/brands.astro 均已删除;品牌数据现在只通过走秀页侧栏 filter + 品牌弹窗呈现(热门 30 条走 SSG 接口,搜索/字母走运行时 /api/v1/public/brands)。
  • 占位模块组件已清理(2026-08-29):components/pages/ 下 About/Contact/LatestProjects/Portfolio/Login/Register/AuthForm/article 等占位/旧详情组件查无引用、也不是真实路由,已删除并移除空目录。
  • 导航死链清理(2026-08-30):用户决定移除 /latest-projects、/about、/contact 三个死链——已从 Layout.astro 主导航(早已不在)、routes.ts(latestProjects/about/contact 键)、dictionary.ts(about/contact/latest projects 翻译键)三处清除,全 src 扫描零残留引用。剩余死链 /portfolio、/login、/register 保留未动(Portfolio 导航项、Login/Signup 链接仍展示,用户未要求移除)。
  • 「header 别动」解读(2026-08-30 明确):用户「header 别动」指不要擅自改导航结构与视觉;但用户主动批准的导航调整(Runway 下拉改 5 个 collection 分类、Street Style 下拉减为 3 城、删除死链项)均属于已授权变更,不算违规。/en/login、/en/register 实际并非真实页面(旧 MEMORY 记录有误),AuthForm 组件已随清理删除。

架构总览

  • 前端: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,/api/v1/ssg/*,SSGToken 中间件,nginx 不反代)。
  • SSG 分组现状(2026-08-26):只剩两个端点——/api/v1/ssg/index/runway(首页 runway 区块,IndexService.Runway,hotBrand 20 条)+ /api/v1/ssg/brands/hot(走秀页热门品牌,BrandService.Hot,固定 30 条、按图片总数热度顺序,零参数)。/ssg/brands(全量 A-Z)与 /ssg/articles/ids 均已删除。
  • 接口 i18n 约定:后端按 ?locale=cn|en 选列返回(pickLocale 缺翻译回落另一语言);前端取数统一复用带 locale 的原语(request/ssgRequest 等),绝不裸写 fetch;语言以 URL 前缀为准。
  • 走秀标题 title_cn 已规则回填(backend_v2 scripts/backfill_title_cn/main.go,幂等);description_cn 为空白属预期,中文页回落英文。

关键约定

  • 视觉语言:黑白极简编辑风(monospace + Georgia 衬线大标题 + 细线),HBX / 时尚档案调性。字体全走 Tailwind 体系,禁止自写字体栈。
  • 品牌名前端一律只显示英文(用户偏好 2026-08-26:「前端品牌不要展示中文名称,标题可以留着」):api.ts 映射 display = nameEn;走秀标题/描述仍按 locale 选列。
  • API 命名哲学(用户原则):「每一个地方尽可能不要通过参数来复用接口,尽量一个功能一个接口」。查询参数仅限「同一资源的列表筛选/分页」;跨资源/独立功能一律独立路径(如 brands/hot 而非 ?featured=1)。待办:公开侧 /api/v1/public/brands 的 featured/keyword/letter 参数拆分、/articles 的 brand_id 拆 brands/:id/shows、资源命名 shows vs articles、popular vs featured(目标清单见 artifact api_endpoints_catalog.md)。
  • CSS:能 @apply 就 @apply;<style> 顶部 @reference "tailwindcss";。Tailwind v4 无零值类。
  • 导航(Layout.astro):「header 别动」指不擅自改结构/视觉,但用户主动批准的导航调整有效(Runway 下拉改 4 个 collection 分类 rtw/menswear/couture/resort、Street Style 下拉减为「全部/Paris/New York/Milan」4 项、移除 latest-projects/about/contact 死链)。剩余死链 /portfolio、/login、/register 用户未要求动,保留。

Layout 陷阱(2026-08-12)

  • Layout 把 <slot /> 包在 flex-1 flex flex-col 容器里;页内根容器禁用 m-auto(cross-axis margin:auto 触发 shrink-to-fit → 内容多少决定宽度、页面抖动)。用 w-full(可配 max-w-[xxx] + mx-auto)。

Astro <script> 两大坑

  • 作用域隔离:frontmatter 的 import/变量在打包型 <script> 里拿不到(运行时 ReferenceError);脚本里要用什么就在脚本顶部重新 import。
  • 不做模板插值:打包型 <script> 里 {t('...')} 不会被执行 → t is not defined。预翻译用 define:vars={{ hiLabel }} 注入;模板区 {t()} 正常。
  • 打包后脚本在 _astro/*.js 而非内联;查 dist 产物用 search_content(ripgrep),PowerShell 无 Select-String -Recurse。

走秀数据分类(brand_runway)

  • 三维分类字段:collection_type(rtw/menswear/couture/resort/pre_fall)、season(spring/fall/null)、season_code(SS26/FW25…派生)。靠 title 规则回填(幂等,25,206 行)。
  • 公开列表 ?collection_type=&season=&season_code=&year= 筛选;前端走秀页有系列/季节/年份 chips。
  • 地理/城市维度(runway):用户明确暂不做。
  • 分布:rtw 15203 / menswear 4061 / couture 672 / resort 2894 / pre_fall 2376。

街拍地区分类(street_snap.city,2026-08-30)

  • 全链路已落地:DB 加 city 列(VARCHAR(128),脚本 scripts/sql/004_add_streetsnap_city.sql + 幂等回填 scripts/backfill_city/main.go,无 AutoMigrate)+ 后端 StreetSnapQuery.City 精确匹配 WHERE city = ? + 前端 header 下拉 ?city= + 列表侧栏 City pills + 深链自动选中。
  • 规范值约定(重要):city 列存「首字母大写英文规范名」(Paris / New York / Copenhagen …),前端 header 下拉与列表 cityOptions 的 value 用同一大写值,后端做精确匹配、大小写敏感,必须前后一致。backfill 首选从 title 第一段(location,\t 分隔)按城市关键词匹配,兜底从 source_url/cover 的 URL slug 解析。
  • 前端 t('Paris') 经 translate() 自动 key.toLowerCase() 查字典,中文回落「巴黎」,i18n 正常。
  • dev 库 db_dev.street_snap 仅 12 行样本(Paris 8 / Copenhagen 4),其余下拉城市(London/Milan…)本地预览为空属预期;生产真实爬取数据才有完整覆盖。
  • 验证:重启后端 :8090 后,?city=Paris→8、?city=Copenhagen→4、?city=London→0、无参→12,全链路通过。client_sign 本地 enabled:false,可直接 curl/Node 验证。

前端组件复用(2026-08-29)

  • 详情页统一 /item:走秀 lookbook 与街拍详情全部收口到 src/pages/{locale}/item/[id].astro,服务端按 getSsrArticle(id) → getSsrStreetSnap(id) 回落渲染(先 runway 后 street)。ROUTES.article 与 ROUTES.streetSnap 均指向 /item/${id}(runway-looks/[id]、street-snaps/[id]、article.astro(?id=) 等旧详情页均已删除)。列表页回链仍指向列表 /runway-looks、/street-snaps。
  • 列表页共享资源:两个列表组件(RunwayLooks / StreetSnaps)同构,抽成 src/styles/look-grid.css(卡片/网格/分页/侧栏/spinner/骨架/动画)+ src/lib/looks-grid.ts(SPIN_SVG、makeCardSlots(images,count,opts)、buildPageList(total,cur));品牌筛选弹窗 src/components/BrandModal.astro(仅 RunwayLooks 用,openBrandModal/confirmBrandModal 等由页面脚本驱动)。卡片标记因在 Alpine x-for 内客户端渲染,不能提成 Astro 组件。

账号体系(2026-08-31)

  • 决策:仅预置内部账号,不开放注册(前端无注册入口,避免垃圾账号/撞库)。账号由 seed 脚本预置。
  • 默认内部账号:admin / admin@studio.local / 密码 Studio#2026!Admin(env SEED_ADMIN_PASSWORD 可覆盖;务必改默认密码)。
  • 双令牌 JWT(2026-08-31 落地):access token 短命(默认 2h,无状态 JWT,仅前端请求用)+ refresh token 长命(默认 30d,随机串只存 SHA256 哈希落库 refresh_tokens)。纯无状态 JWT 无法吊销,故 refresh 必须落库才能做踢下线。
    • 接口(均在 /api/v1/auth/,除 /me 外均公开、无需 Bearer):
      • POST /login → {access_token, refresh_token, expires_in(秒), user}(login 返回结构已从 {token,user} 改为双 token,前端已同步)。登录即顶旧会话:签发前先吊销该用户全部既有 refresh,保证同账号同一时刻只有一条活性会话(用户明确诉求:不希望同一账号被不同人同时登录)。
      • POST /refresh → {access_token, expires_in}(用 refresh_token 换 access;refresh 复用、不轮换)。
      • POST /logout → {ok:true}(吊销当前 refresh,单设备登出)。
      • POST /logout-all → {revoked:N}(按 refresh 反查用户,吊销其全部 refresh = 踢下线/全设备登出)。
      • GET /me(Bearer 校验)保留。
    • 后端改动:config.JWT 新增 RefreshExpireHours(env JWT_REFRESH_EXPIRE_HOURS);AuthService 加 Refresh/Logout/RevokeAllByRefresh;RefreshTokenRepository(Create/FindByHash/Revoke/RevokeAllByUser/DeleteExpired);model.RefreshToken(gorm 表 refresh_tokens)。
    • 建表:scripts/sql/006_create_refresh_tokens.sql;一键建表脚本 scripts/migrate_refresh/main.go(go run ./scripts/migrate_refresh,DSN 默认 root:root@127.0.0.1:3306/db_dev,可由 DB_DSN 覆盖)。
    • 前端 api.ts(2026-08-31 改):saveSession(access,refresh,user) 存 fa_token/fa_refresh/fa_user;新增 refreshSession()(POST /auth/refresh,刷新 access、复用 refresh);fetchMe() 走 authedFetch(遇 401 静默 refresh 一次再重试);logout()(吊销当前 refresh 再清本地)、logoutAll()(踢下线再清本地)。clearSession() 一并清三键。
    • 踢下线效果:普通踢=吊销 refresh,已签发 access 在 2h 窗口内仍有效,到期后才被弹回登录(非即时,内部工具足够);未做 token_version 即时吊销方案(如需访问令牌即时失效再加 users.token_version 并在 middleware.Auth 比对)。
    • 验证(2026-08-31):go build -o bin/server.exe ./cmd/server EXIT=0;端到端 Node 脚本确认 login→refresh→logout-all(revoked:1)→再 refresh=401(ALL_OK);同账号二次 login 后首次 refresh 返回 401、二次 refresh 返回 200(MUTEX_OK,互斥登录生效)。前端 npm run build EXIT=0。
  • 硬门禁(用户拍板):RunwayLooks / StreetSnaps 的筛选与翻页入口调 requireLogin(),未登录则弹出内嵌登录表单(showLoginModal);登录成功后 loadPage(1) 按当前筛选刷新。首屏 init() 的 loadPage(1) 不拦(保证未登录能看到第一页)。
  • 页面:src/pages/{en,cn}/login.astro(黑白极简风,Alpine 表单,登录后跳 ?redirect= 或 /account)、account.astro(资料+登出+收藏/历史占位,未登录跳 /login?redirect=/account)。
  • 视觉:登录/个人中心沿用站点黑白极简编辑风(monospace + 细线下划线输入框 + Georgia 衬线大标题 + hover:opacity-60)。

部署(Docker + Gitea Actions)

  • 交付文件:前端 Dockerfile / nginx.conf / docker-compose.yml / deploy.sh / .gitea/workflows/deploy.yml / .env.example / DEPLOY.md;后端 Dockerfile。nginx 拦截 /api/v1/ssg/ 404;健康检查 /api/v1/public/brands。