Files
toom1996 b04a511b26 update
2026-09-17 22:06:11 +08:00

76 lines
9.4 KiB
Markdown
Raw Permalink 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)
> 维护约定:本文件只放**跨会话长期有效**的事实与铁律;当日细节写 `.workbuddy/memory/YYYY-MM-DD.md`。
> 历次合并去重:2026-09-15(大幅压缩;保留架构/约定/脚本铁律/类型坑/lib分层/验证手段/门禁/账号/i18n;删冗长示例与已过时待办)。
## 架构总览
- 前端 `d:/project/frontend_v2`:Astro(`adapter: node` standalone,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>` 铁律(踩坑汇总)
1. 打包型 `<script>` 拿不到 frontmatter 变量,需脚本顶部重新 import。
2. **`define:vars` + `import` = 致命**(包成 IIFE 后 import 进函数体 → SyntaxError)。要 import 改用 `data-*` 透传。
3. 片段页(无 `<html>`)`import '@/styles/global.css'` build 不注入 head → 整页无样式。
4. **Alpine 表达式只认组件 data / `$magic` / global**,外部函数先存进 data 属性(如 `isAuthed`、`toAbsUrl` 看似空包装但不能删)。
5. 验证 Alpine 运行时务必真跑浏览器,只看 SSR HTML 会漏掉绑定失效。
6. 共享组件 `Alpine.data('name', () => ({...}))` + `x-data="name()"`;由工厂展开须显式标注返回类型。
7. Swiper:`initSwipers()` 先 `destroySwipers()`;`setupSwipers()` rAF 延后 + `DOMContentLoaded`;Tab 用 document 事件委托。
8. 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()` 读 localStorage `fa_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)` 对 key `toLowerCase()` 查(大小写不敏感,`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_ops` HNSW,`<~> <= 10` 命中标 `IsDuplicate`/`DupOf`,只标不拦);Tier3 语义(`image_embeddings` 表 `embedding vector(512)` + `vector_cosine_ops` HNSW,待 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 可按样本微调。