9.4 KiB
9.4 KiB
项目长期记忆(Project MEMORY.md)
维护约定:本文件只放跨会话长期有效的事实与铁律;当日细节写
.workbuddy/memory/YYYY-MM-DD.md。 历次合并去重:2026-09-15(大幅压缩;保留架构/约定/脚本铁律/类型坑/lib分层/验证手段/门禁/账号/i18n;删冗长示例与已过时待办)。
架构总览
- 前端
d:/project/frontend_v2:Astro(adapter: nodestandalone,SSR 取数)+ Tailwind v4 + Alpine.js。构建npm run build;类型检查npm run check(=astro check)。 - 后端
d:/project/backend_v2:Go + Gin + GORM(PostgreSQL + pgvector)。本地库见scripts/pgvector(Docker 一键,PG16+,容器名 pgvector,库 fashion,账号 fashion/fashion_dev_2026;initdb自动CREATE EXTENSION vector)。公开引擎 :8090(/api/v1/public/*、/api/v1/auth/*);SSG 引擎 :8091(仅 127.0.0.1,nginx 不反代)。 - 接口 i18n:后端按
?locale=cn|en选列;语言以 URL 前缀为准;prefixDefaultLocale: true(/en、/cn 都带前缀;根/由服务端 302 到 /en)。 - base 地址全在
src/lib/config/:config.baseApi(浏览器) /config.baseApiSsr(SSR 绝对) /config.baseApiSsg(构建期内部端口)。密钥类(SSG_TOKEN)直读 env。 - 后端 Windows 编译:
go build -o server-bin.exe(非server-bin)。前端 dev 不热加载.env,改 base 后astro dev stop+--background+浏览器硬刷新。 504 (Outdated Optimize Dep):删node_modules/.vite后重启 dev。
关键约定
- 视觉:黑白极简编辑风,纯黑白禁用暖米色/柔和阴影;字体走 Tailwind 体系(系统字体栈,不引外部字体)。
- 跨组件复用原子样式放
global.css(Astro 组件<style>有作用域哈希,跨组件命中不了)。现含.fav-btn/.locate-flash/@media(hover:none)/.no-scrollbar/[x-cloak]。 - Tailwind v4:
<style>顶部@reference "tailwindcss";(编译期指令,不进 HTML);无零值类;能 @apply 就 @apply。 - Header 下拉
x-show一律带x-cloak防 FOUC。Layout<slot/>包flex-1 flex flex-col;页内根容器禁用m-auto(用w-full+max-w-[x] mx-auto)。 - 未启用 View Transitions。
Astro <script> 铁律(踩坑汇总)
- 打包型
<script>拿不到 frontmatter 变量,需脚本顶部重新 import。 define:vars+import= 致命(包成 IIFE 后 import 进函数体 → SyntaxError)。要 import 改用data-*透传。- 片段页(无
<html>)import '@/styles/global.css'build 不注入 head → 整页无样式。 - Alpine 表达式只认组件 data /
$magic/ global,外部函数先存进 data 属性(如isAuthed、toAbsUrl看似空包装但不能删)。 - 验证 Alpine 运行时务必真跑浏览器,只看 SSR HTML 会漏掉绑定失效。
- 共享组件
Alpine.data('name', () => ({...}))+x-data="name()";由工厂展开须显式标注返回类型。 - Swiper:
initSwipers()先destroySwipers();setupSwipers()rAF 延后 +DOMContentLoaded;Tab 用 document 事件委托。 - IntersectionObserver 用模块级单例(如 gallery 的
thumbObserver),重建前?.disconnect()(别放 Alpine data 属性被代理包一层)。
类型检查与 Alpine 类型推断(2026-09-14 引入 npm run check,已修到 0)
- 坑 A:
@types/alpinejs自引用约束使 data 属性退化unknown/{}→ 属性显式标注(user: null as AuthUser|null、favIds: [] as string[]);Alpine.store先const s: Store = {...}再注册。 - 坑 B:对象展开让 T 退化
{}→ 给回调显式标注返回类型(): ReturnType<typeof createGalleryView> & X => ({...})。 - 排查铁律:「Property does not exist on type '{}'」/「Object of type 'unknown'」先怀疑这两类,别怀疑业务代码。
astro check的 stdout 会被吞 →npx astro check 2>&1 | Out-File -FilePath x.txt -Encoding utf8再读;Get-Content须带-Encoding utf8。
lib 模块分层(2026-09-14 核对磁盘)
i18n/index.ts(配置+工具+API语言透传:原 config/utils/lib/locale 三合一)+i18n/dictionary.ts(纯数据,独立保留)。auth.ts仅会话存储原语;api.ts业务取数+鉴权(login/logout/fetchMe/enforceIdleLogout有意留 api.ts,避免 auth↔request 循环);ssg.ts构建期、ssr.ts运行期、gallery.ts灯箱工厂、crypto.ts签名、config/base 址、endpoints.ts后端路径唯一来源、routes.ts前端跳转 URL(带前缀,href用 astro:i18n、clientHref浏览器内自拼)、http.ts调试日志(开关DEV||DEBUG_API)、favorites.ts/history.ts。- 依赖无环:api→{request,crypto,auth,i18n,endpoints};ssr→{request,endpoints,i18n,api(type)};ssg→{request,api(type),endpoints};gallery→{request,auth,favorites};request→{crypto,auth,i18n,config,endpoints}。
验证手段
- 改完必跑
npm run build(全链路 import + 预渲染)+npm run check。 - 预渲染页:
dist/client/{en,cn}/<page>/index.html归一化对比(去astro-cid-*与_astro/<asset>逐字节比)。 prerender=false页:临时建 prerender 验证页喂假数据,或 dev :4321 时 curl 真实 SSR(curl :8090/api/v1/public/runway-looks的data直接是数组)。HTML 单行用[regex]::Matches($h,'...').Count统计关键标记。
未登录预览门禁(详情页)
- 后端硬截断:
/runway-looks/:id、/street-snaps/:id匿名且图>5 时截断前 5 张 + 回preview:true+image_total;已登录(Bearer)返回完整。列表接口全量。 - 前端:SSR 前 5 张 + 剩余空图遮罩(
x-show="!isAuthed && preview"+x-cloak,"login to view" + "+N photos");登录后取全量追加extraImages。getUser()读 localStoragefa_user。
账号体系
- 仅预置内部账号(admin/root)。双令牌 JWT(access 7d 无状态 + refresh 30d 仅存 SHA256 落库);登录即吊销该用户全部 refresh。
- 收藏服务端化
favorites表(按 user_id 隔离,编码串 r=/s=、i=/j=);浏览历史histories表(唯一键 user_id+target_uid,FIFO 上限 2000)。
i18n 字典约定
dict: Record<string,{cn:string}>,key 即英文原文(en 返 key),value 只存非默认语言(仅 cn)。translate(key,locale)对 keytoLowerCase()查(大小写不敏感,t('Home')命中home);缺翻译 en 回退 + dev 告警。中文绝不可当 key。
记住语言偏好(2026-09-15 新增)
- 需求:选过语言后下次进首页直接用上次语言。
i18n/index.ts新增STORED_LOCALE_KEY='preferred_locale'、storeLocale(locale)、getStoredLocale()。- Layout
<head>顶部is:inline+define:vars脚本:进首页(path==='/' 或 '/'+defaultLocale)且记住非默认语言时location.replace('/'+stored)。 - 语言切换器
x-data="langSwitch()";<a>@click="open=false; pick('<code>')";footer 注册langSwitch(pick(code){ storeLocale(code) })。 - 铁律:只在显式点击切换器时写 localStorage,绝不按页面加载 URL 写——否则与首页自动跳转冲突(点了英文又被 URL 的 en 覆盖,首页永远跳不回去)。
目录约定(用户 2026-09-17 确认)
src/components/views/必须保留,勿并入src/pages/,也勿把内容提取摊平。 原因:本项目prefixDefaultLocale: true→src/pages/{en,cn}/是两套 locale 页面树,每个页面文件只是薄壳(路由 +export const prerender+ 引入共享视图 + 渲染);views/*是 en/cn 两棵页面树共用的页面体实现(语言靠Astro.currentLocale运行时区分)。并进 pages 会迫使 en/cn 各抄一份,制造重复。views/命名与 pages 略撞语义,但目录本身职责明确(跨 locale 共享页面体单一事实源),仅属 cosmetic,非必要不改名。- 现状
views/成员:Account / Home / Login / RunwayLooks / StreetSnaps / RunwayLooksDetail / StreetSnapsDetail(RunwayLooksDetail/StreetSnapsDetail非独立路由,是被pages/{en,cn}/{runway-looks,street-snaps}/[id].astro复用的详情体;2026-09-17 由 ItemPage 按type拆成两个,共享脚本抽到lib/detailFav.ts的initDetailFav())。
图片去重(backend_v2,2026-09-17 落地)
- 三档全上:Tier1 精确(sha1 内容寻址 +
content_sha1部分唯一索引,命中整行跳过不插);Tier2 近重复(phash存vector(64)二进制串,bit_hamming_opsHNSW,<~> <= 10命中标IsDuplicate/DupOf,只标不拦);Tier3 语义(image_embeddings表embedding vector(512)+vector_cosine_opsHNSW,待 CLIP/DINOv2 接入,当前不写)。 - 关键文件:
internal/pkg/phash/phash.go(dHash 纯标准库,ToVectorBits→vector(64)串,webp 解码失败→0→NULL)、internal/database/postgres.go(ensureDedupSchema幂等 DDL +CREATE EXTENSION IF NOT EXISTS vector)、internal/service/ingest_service.go(dedupImage)、internal/repository/ingest_repository.go(ImageExistsBySha1/FindNearDuplicateImage)。 phash走sql.NullString+type:vector(64)text 扫描,规避 pgx 原生类型坑;空串→NULL 不参与近邻检索。- 索引查取代全表扫(用户"图片量会很大"痛点):原
ListImagePHashes全表扫 O(N) 已移除。 - 待办:pgvector Docker 实连烟测 GORM 读
vector(64)进sql.NullString;汉明阈值 10 可按样本微调。