Files
frontend_v2/.workbuddy/memory/MEMORY.md
toom1996 64af75c0a9
Some checks failed
deploy / deploy (push) Has been cancelled
update
2026-09-02 21:50:30 +08:00

54 lines
10 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)
## 模块策略
- 站点按模块逐个完善,当前主线为「发布会模块」:首页 index + 走秀档案(RunwayLooks)+ 街拍(StreetSnaps)。
- 品牌索引页已下线(2026-08-26):`brands.astro` 与孤儿组件已删;品牌仅经走秀页侧栏 filter + 品牌弹窗呈现(热门 30 走 SSG,搜索/字母走运行时 `/api/v1/public/brands`)。
- 导航死链清理(2026-08-30):删 `/latest-projects`、`/about`、`/contact` 三处(Layout/routes/dictionary 三处同步清)。**保留未动的死链**:`/portfolio`、`/login`、`/register`(用户未要求动)。
- 「header 别动」= 不擅自改导航结构/视觉;但用户主动批准的调整(Runway 下拉分类、Street Style 减城、删死链)均属已授权,不算违规。
## 架构总览
- 前端:`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 端点仅剩:`/api/v1/ssg/index/runway`(首页 runway 区块)与 `/api/v1/ssg/brands/hot`(走秀页热门品牌,固定 30 条)。
- 接口 i18n:后端按 `?locale=cn|en` 选列(pickLocale 缺翻译回落另一语言);前端取数统一复用带 locale 原语(request/ssgRequest),**绝不裸写 fetch**;语言以 URL 前缀为准。
- 走秀标题 `title_cn` 已规则回填(幂等);`description_cn` 空白属预期(中文页回落英文)。
- 后端连接池:已设 `conn_max_idle_time`(config.yml 默认 60s,须 < MySQL wait_timeout)主动回收空闲连接,避免空闲连接被 MySQL 回收后池子持死连接报 `invalid connection`。**本地后端若长时间空闲后接口报 invalid connection,先重启后端**(`cd d:/project/backend_v2 && go build -o server-bin ./cmd/server` 后杀旧进程重启)。
- 前端 dev server **不热加载 `.env`**:改 `BASE_API` 后必须 `astro dev stop` + `astro dev --background` 重启才生效。浏览器旧 bundle 仍用旧 `BASE_API`,需**硬刷新**(Ctrl+Shift+R);旧 token 对新后端无效,需**重新登录**。改 BASE_API 指向本地/线上后端后,若历史/收藏/登录"没反应",先查 dev server 是否重启、浏览器是否硬刷新+重登录。
## 关键约定
- 视觉语言:黑白极简编辑风(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`**(cross-axis margin:auto 触发 shrink-to-fit → 抖动),用 `w-full` + `max-w-[x] mx-auto`。
## Astro `<script>` 坑
- 作用域隔离:frontmatter 的 import/变量在打包型 `<script>` 拿不到,需脚本顶部重新 import。
- 不做模板插值:打包型 `<script>` 内 `{t('...')}` 不执行 → `t is not defined`;预翻译用 `define:vars` 或 data 属性注入,模板区 `{t()}` 正常。
- **`define:vars` + `import` = 致命组合(已踩坑)**:`define:vars` 会把脚本包成内联 IIFE 并注入 `const meta=...`,而原本脚本顶部的 `import` 语句被塞进函数体内 → `import` 在 IIFE/非模块上下文非法 → 整段脚本 SyntaxError 完全不执行(连带 `recordHistory`/事件绑定全失效)。**凡脚本里要 `import`,绝不能用 `define:vars`**。改用 `data-*` 属性在模板里透传变量,脚本内 `btn.dataset.xxx` 读取(Layout.astro 已用此模式)。en/cn `item/[id].astro` 曾因此导致详情页浏览历史/收藏全不工作,已修复。
- 片段页(无 `<html>/<head>` 的 .astro)的 `import '@/styles/global.css'` 在 build 时不会被注入 head → 产物缺 `<link rel=stylesheet>` 整页无样式。login.astro 曾因此丢样式,已补完整文档结构修复。
## 数据分类
- 走秀 `brand_runway`:`collection_type`(rtw/menswear/couture/resort/pre_fall) + `season`(spring/fall/null) + `season_code`(SS26…),title 规则回填(25,206 行,幂等)。公开列表 `?collection_type=&season=&season_code=&year=` 筛选。地理维度暂不做。
- 街拍 `street_snap.city`(2026-08-30 落地):DB 列 VARCHAR(128) + 精确匹配 `WHERE city = ?`;前端 header 下拉 `?city=` + 侧栏 City pills + 深链选中。规范值存「首字母大写英文」(Paris/New York/Copenhagen…),前后端大小写敏感须一致;dev 库仅 12 行样本(Paris 8 / Copenhagen 4),其余城市本地为空属预期。
## 前端复用
- 详情页统一 `/item`:`src/pages/{locale}/item/[id].astro`,服务端 getSsrArticle→getSsrStreetSnap 回落;`ROUTES.article`/`streetSnap` 均指向 `/item/${id}`。列表回链指向 `/runway-looks`、`/street-snaps`。
- 列表组件同构,抽 `src/styles/look-grid.css` + `src/lib/looks-grid.ts`(SPIN_SVG / makeCardSlots / buildPageList);品牌筛选弹窗 `BrandModal.astro`(仅 RunwayLooks)。卡片标记在 Alpine `x-for` 内客户端渲染,不能提成 Astro 组件。
## 账号体系(2026-08-31)
- 仅预置内部账号,不开放注册(前端无注册入口)。`seed_users/main.go` 创建两个账号:`admin`(密码取 `SEED_ADMIN_PASSWORD`,缺省 `Studio#2026!Admin`)+ `root`/`root`(测试弱密码,uid 通常为 8)。**本地库 `db_dev` 确有 root/root**;实测 `POST /api/v1/auth/login {account:"root",password:"root"}` 打到 `http://localhost:8090` 返回 200,`GET /auth/history` 显示 root 账号下已有 `street-snaps`/`runway-looks` 两条真实图集历史。**不要再说"本地无 root / root 登录必失败"——那是误判**(2026-09-02 因一次 401 错判,已纠正)。前端 `.env` 当前 `BASE_API=http://localhost:8090`。
- 双令牌 JWT:access 默认 **7 天(168h)**(无状态,前端请求用)+ refresh 默认 **30d(720h)**(随机串仅存 SHA256 哈希落库 `refresh_tokens`)。登录即吊销该用户全部既有 refresh(互斥登录)。
- 接口(`/api/v1/auth/`,除 /me 外公开):`POST /login`→`{access_token,refresh_token,expires_in,user}`;`POST /refresh`→`{access_token,expires_in}`(refresh 复用不轮换);`POST /logout`(单设备);`POST /logout-all`→`{revoked:N}`(踢下线);`GET /me`(Bearer)。
- 前端 `api.ts`:`saveSession/clearSession/refreshSession/fetchMe(authedFetch 遇 401 静默 refresh 一次)/logout(吊销当前 refresh 再清本地)/logoutAll`。
- 未做 `token_version` 即时吊销(access 在 7 天窗口内仍有效),如需即时失效再加。
- 硬门禁:RunwayLooks/StreetSnaps 筛选与翻页入口调 `requireLogin()`,未登录弹内嵌登录表单,登录成功 `loadPage(1)` 刷新;首屏 `init()` 的 `loadPage(1)` 不拦。
- 个人中心 `account.astro`:报头(方块字母头像+用户名大标题) + 两 tab(**Saved looks / My History**),已移除 Overview/Sessions & Security/Settings/退出入口/偏好设置/编辑资料(2026-09-01 精简)。
- **收藏已服务端化(2026-09-02)**:后端表 `favorites` + `/api/v1/auth/favorites`(GET 分页 `?page&per_page` / POST 新增幂等 / DELETE /:target_uid / POST `/check` 批量校验当前页收藏态),按 `user_id` 隔离,`target_uid` 存前端编码串(runway `r=` / street `s=`)。前端 `src/lib/favorites.ts`:账户页用 `fetchFavoritesPage()` 服务端分页;列表卡片用 `checkFavorited(ids)` 仅把当前页可见 id 发给后端打勾(与收藏总量解耦,10 万级也常数级);收藏/取消本地乐观 + 后台同步(失败不回滚本地)。favorites 表含复合索引 `idx_user_created(user_id,created_at)` 支撑深翻页。
- **浏览历史(2026-09-02 晚收敛为「文章级」,用户澄清纠正)**:语义=「点开过哪篇文章」,记录时机在 **item 详情页**(`recordHistory(meta.id)`,meta.id 是文章对外编码串 `r=`走秀 / `s=`街拍,编码串自带类型、无需 kind);**RunwayLooks/StreetSnaps 列表页进页不再记**(此前按"图集=列表页"实现是理解错,已纠正)。后端 `histories` 表收敛为 `(user_id, target_uid, viewed_at)`:`kind/target_type/title/cover/brand` 五列已 DROP(008 建表脚本同步改 + `migrate_refresh` 对存量库 ALTER,实测 GET items 仅 `{id,viewed_at}`);唯一键 `uniq(user_id,target_uid)`;保留 `HISTORY_CAP=2000` FIFO(文章会积累,有存在意义)。接口 `/api/v1/auth/history`:GET 分页 / POST `{target_uid}` upsert / DELETE /:target_uid / DELETE 清空,按 `user_id` 隔离;kind 参数与校验已随收敛移除(kind=all 字面过滤坑随之消失)。账户页 My History:服务端分页拿 id 列表后**逐篇回查公开详情**(`api.ts` 的 `getHistoryMeta(id)` → `/public/runway-looks|street-snaps/:id` 取 title/cover/brand;用户选"只存 id、展示回查"),卡片跳回文章详情,无 kind 筛选 tab。前端 `src/lib/history.ts`:`recordHistory(id)` / `fetchHistoryPage(page,perPage)`(服务端分页+回查,单条回查失败容忍回落本地)。
- **待办服务端化**:users 加 display_name/bio、refresh_tokens 列表接口。
- 视觉:登录/个人中心沿用站点黑白极简编辑风(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`。