10 KiB
10 KiB
项目长期记忆(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/cnitem/[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)。卡片标记在 Alpinex-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存前端编码串(runwayr=/ streets=)。前端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=2000FIFO(文章会积累,有存在意义)。接口/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 列表接口。
- 收藏已服务端化(2026-09-02):后端表
- 视觉:登录/个人中心沿用站点黑白极简编辑风(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。