# 2026-09-02 工作记录 ## 收藏功能后端化(服务端持久化)✅ 完成并端到端验证 用户指出:此前收藏只有前端 localStorage("纯前端自慰"),后端没有对应接口。本次补齐后端 + 前端接入。 ### 后端(d:/project/backend_v2) 新增文件: - `internal/model/favorite.go`:`Favorite` 模型(user_id / target_type / target_uid / title / cover / brand),表 `favorites`,唯一索引 `(user_id, target_uid)`。 - `internal/dto/favorite.go`:`FavoriteItem`(id 即 target_uid)、`FavoriteAdd`(`target_type`+`target_uid` 必填 + 可选快照)。 - `internal/repository/favorite_repository.go`:`ListByUser` / `Add`(OnConflict DoNothing 幂等)/ `Remove`。 - `internal/service/favorite_service.go`:`List` / `Add`(校验 type∈{runway,street})/ `Remove`。 - `internal/handler/favorite_handler.go`:`List`(GET) / `Add`(POST) / `Remove`(DELETE `/:target_uid`),均 `middleware.Auth` 鉴权,返回 `{data:...}`。 - `internal/router/router.go`:在 `api.Group("/auth")` 下注册三接口(含 middleware.Auth)。 - `internal/main.go`:装配 `favRepo` / `favSvc` / `Favorite` handler。 - `scripts/sql/007_create_favorites.sql` + `scripts/migrate_refresh/main.go`:建表(幂等)。 约定:沿用项目「一个功能一个接口」哲学,收藏独立路径 `/auth/favorites`,不挂查询参数。target_uid 直接用前端卡片编码串(r=/s=),避免解码。 ### 前端(d:/project/frontend_v2) - `src/lib/api.ts`:新增 `authJson(method, path, body?)`(带 Bearer、401 静默 refresh、返回 `data`、抛出后端 message)。 - `src/lib/favorites.ts`:重写为 server 模式门面——`serverMode()`(getUser+access token);`toggleFavorite` 本地乐观更新 + 后台 POST/DELETE 同步(失败不回滚本地);`removeFavorite` 乐观清 serverList + 后台 DELETE;`fetchServerFavorites()` 拉服务端权威列表(account 页用)。对外签名(getFavorites/isFavorited/toggleFavorite/removeFavorite)保持不变。 - `src/pages/{en,cn}/account.astro`:移除假数据 demo(demoFavorites 整段删除);init 改调 `fetchServerFavorites()` 取服务端收藏;`itemHref/removeFav` 去掉 demo 分支;登录态展示真实收藏,空则显示空态。 - 列表卡片(RunwayLooks/StreetSnaps)无需改:仍用 `isFavorited/toggleFavorite`,新增的 server 同步在 lib 内部完成。 ### 验证 - 后端 `go build ./...` EXIT=0;`go run ./scripts/migrate_refresh` MIG_EXIT=0(favorites 表建好)。 - 重启本地后端(杀旧 PID 11160,`go build -o bin/server.exe` 后启动,HEALTH=200)。 - 用 PowerShell `Invoke-RestMethod` 跑通全生命周期(curl.exe -d 在 Windows 下 body 发不出,改用 Invoke-RestMethod 才成功): admin 登录 → POST /auth/favorites(返回新建项)→ GET(含该项)→ DELETE → GET 返回 `[]`。真实落库,无残留。 - 注意:本地后端种子账号是 `admin / Studio#2026!Admin`;用户预览用的 `root/root` 走线上后端,本地调试用 admin。 - 前端 `npx astro build` EXIT=0;read_lints 0 错误。 ### 遗留 / 未做 - 跨设备列表卡片心形初始态:列表卡片初始仍读本地 localStorage(保证即时);登录态服务端同步在后台,换设备首次进列表可能短暂不一致(同设备因本地镜像一致)。如需严格一致,列表页 loadPage 后需 bootstrap serverList 并触发 Alpine 重算(当前未做)。 - users.display_name/bio、refresh_tokens 列表接口仍待办。 - 历史(history)仍为纯本地,未服务端化(用户未要求)。 ## 收藏态跨浏览器/设备一致性修复(11:1x)✅ 完成 + 构建验证 用户提供担忧:换浏览器收藏列表是否不展示(数据绑账号应不丢,但发现列表卡片初始态只从本地读、列表页从不拉服务端,导致新浏览器进列表爱心全空);并问展示收藏态是否压垮数据库。 结论与改动: - 性能:后端 `ListByUser` = `WHERE user_id=?` 索引查询,每页加载 1 次(非每卡片 1 次),千人并发仅几十 QPS 微查询,**不会崩**;唯一会崩的「每卡片 1 查询」写法未采用。 - 修复方案:列表/详情页进入时**先本地即时填充 + 异步拉服务端校正**,用响应式 `favMap`(`Record`)替代原 `x-data="{ fav: isFav(it.id) }"` 的一次性捕获(后者服务端异步返回后不会刷新卡片)。 - `src/lib/favorites.ts`:新增 `loadServerFavIds(): Promise`(调 `fetchServerFavorites` 回填 serverList 并返 id 数组)。 - `RunwayLooks.astro` / `StreetSnaps.astro`:data 增 `favMap`/`favTouched`;`init()` 调 `loadFavs()`(本地即时 → 登录态 `loadServerFavIds` 校正,`favTouched` 为真时跳过覆盖避免丢刚点的收藏);卡片按钮改 `:class="{ 'is-on': favState(it.id) }"` + `@click.prevent="toggleFav(it)"`(toggleFav 内乐观更新 favMap);删除原 `isFav` 方法。 - `src/pages/{en,cn}/item/[id].astro`:详情页 `sync()` 初始后,登录态 `loadServerFavIds().then(sync)` 异步校正(跨浏览器详情页爱心一致)。 - 验证:`npx astro build` EXIT=0;read_lints 0 错误。 - 注:预览 `:4321` 走线上后端(root/root,线上 favorites 表需部署 007 迁移才有);要本地直接验证跨浏览器,把前端 BASE_API 指向本地 `:8090` 用 admin 登录即可(用户未要求切,保持现状)。 ## 收藏海量场景优化(10万级):服务端分页 + 批量校验(12:0x)✅ 完成并端到端验证 用户追问:若用户收藏极多(2000、甚至 10 万)怎么办。结论与改动: - 性能论证:DB 层 `WHERE user_id=?` 索引查询,2000/10万行均毫秒级,不会崩;favMap(id→bool) 为 O(1)。真痛点在①账户页一次性 `x-for` 全量渲染(10万 DOM 爆炸)②列表标记此前拉全量完整对象(2000≈500KB/次)。 - 改动: - 后端:新增 `POST /api/v1/auth/favorites/check`(body `{ids:[...]}` → 返回已收藏子集);`GET /auth/favorites` 改服务端分页(`?page&per_page`,默认24/上限100,返回 `{data:{items,total,last_page}}`)。repository 加 `ListPaged/CountByUser/FilterExisting`;service `List`→`ListPaged`+`Check`;dto 加 `FavoriteCheck`;router 注册 check;favorites 表加复合索引 `idx_user_created(user_id, created_at)`(007 sql + migrate_refresh 幂等 ALTER ADD INDEX)。 - 前端 `favorites.ts`:新增 `fetchFavoritesPage`(账户分页)+ `checkFavorited(ids)`(列表仅问当前页可见 id);删除 `loadServerFavIds`/`fetchServerFavorites`。 - 列表组件(RunwayLooks/StreetSnaps):`loadPage` 后调 `markFavs()` 用 check 校正当前页 hearts(与收藏总量解耦,常数级);删 `favTouched`。 - 账户页(en/cn):Saved looks 服务端分页 + 翻页器(`favPageList` 窗口化页码:首尾页+当前页±1+…折叠);`removeFav` 刷新当前页。 - 验证:后端 `go build ./cmd/server` EXIT=0;本地后端重启新二进制 HEALTH_OK;PowerShell 端到端:加3收藏 → P1(total3,last2,count2)/P2(count1) 分页正确 → check 返回3命中 → 删除后 total0。前端 `astro build` EXIT=0、lint 0。 - 注:预览 `:4321` 仍指向线上后端(root/root,需部署 007 迁移才有 favorites 表);本地验证用 admin/Studio#2026!Admin 走 :8090。 ## 浏览历史服务端化(图集 + 图片)✅ 完成 + 端到端验证(15:1x) 用户拍板:历史也走**服务端**(跨设备一致,尽管每次浏览都写库写入量比收藏高一个数量级);图集 = **列表页整体**(进 Runway Looks / Street Snaps 整页记一条)。 ### 后端 - `scripts/sql/008_create_histories.sql` + `migrate_refresh`:建 `histories` 表,`uniq(user_id,kind,target_uid)`;kind∈{image,gallery},target_type∈{runway,street,runway_looks,street_snaps}。 - `internal/model/history.go` / `dto/history.go`(`HistoryItem`/`HistoryAdd`)/ `repository/history_repository.go` / `service/history_service.go` / `handler/history_handler.go` / router 注册(GET/POST/DELETE `/auth/history` + DELETE `/auth/history/:target_uid`)/ main.go 装配。 - **关键膨胀控制(与收藏的本质差异)**:`Upsert` 用 `OnConflict DoUpdates`——同 (user_id,kind,target_uid) 反复浏览只刷 `viewed_at` 不新增行;每用户硬上限 `HISTORY_CAP=2000`,超量用子查询双嵌套(规避 MySQL 同表子查删除限制)FIFO 删最旧。 - **修了一个真 bug**:首版 `ListPaged` 的 `total` 调 `CountByUser(ctx,userID)` 忽略 kind,导致按类型过滤时分页计数错(items 过滤了但 total 算全量)。已改 `CountByUser(ctx,userID,kind)` 一并过滤。 ### 前端 - `src/lib/history.ts` 重写为服务端优先:`recordHistory({kind,type,id,title,cover,brand})`(登录态本地+后台 POST upsert;未登录仅本地);`fetchHistoryPage(page,perPage,kind)` 服务端分页(kind 空=全部);`removeHistory`/`clearHistory`。 - 埋点:`src/pages/{en,cn}/item/[id].astro` 详情页 `recordHistory` 加 `kind:"image"`;`RunwayLooks.astro`/`StreetSnaps.astro` 的 `init()` 进页面记一条 `kind:"gallery"`(target_uid `runway-looks`/`street-snaps`,仅登录态;翻页走 loadPage 不触发,符合「进页面就记」)。 - 账户页(en/cn)History tab:`全部/图片/图集` 切换(setHistKind→重载)、服务端分页 + `histPageList` 窗口化页码、`histHref`(image→item 详情,gallery→对应列表页)、图集/图片徽标、单条删除/清空。字典加 `all/image/gallery/no history yet/viewed gallery`。 ### 验证 - 后端 `go build ./...` EXIT=0;`migrate_refresh` 建表 OK;重启本地后端新二进制(taskkill 杀 7912 后启动,HEALTH=200)。 - PowerShell 端到端:登录 admin → 记 gallery+image → 重复记同 image **upsert 不新增(total 恒 2)** → `?kind=gallery`(total1)/`?kind=image`(total1)/全部(total2) 计数正确 → DELETE 单条 → CLEAR 归零。修复后复测 GAL_TOTAL=1 IMG_TOTAL=1 ALL=2 通过。 - 前端 `npx astro build` EXIT=0;read_lints 0 错误。 - 注:预览 `:4321` 走线上后端(root/root,需部署 008 迁移才有 histories 表);本地真实验证用 admin/Studio#2026!Admin 走 :8090。 ## 后端连接池僵死导致「历史/收藏/登录全失效」(15:2x)✅ 修复 用户反馈浏览历史「没生效」。排查:前端代码与历史接口均正常,但本地后端 :8090 的 `/api/v1/public/brands` 返回 `{"error":"invalid connection"}`,`/health` 正常。 根因:后端(`server-bin`,当时 PID 18604)的 MySQL 连接池连接僵死——GORM 此前只设 `ConnMaxLifetime=3600`、**未设空闲回收时间**,空闲连接被 MySQL 回收后池子仍持有死连接,查询即 `invalid connection` / `bad connection`(server.err 见 14:45、14:52 已报 bad connection)。MySQL 自 8/26 起未重启,故非 MySQL 重启,而是空闲连接被回收。 修复: - 后端加 `conn_max_idle_time`(config.yml + DatabaseConfig + mysql.go `SetConnMaxIdleTime`,默认 60s,须 < MySQL wait_timeout),空闲连接主动回收,从源头避免死连接。 - `go build -o server-bin ./cmd/server` 后杀旧 PID 18604、重启新二进制;重启后 `/public/brands` 恢复正常,历史接口端到端全过(ALL=2 / GALLERY=1 / IMAGE=1,upsert 不增总量,清空归零)。 - 临时验证脚本 `_tmp_e2e.mjs` 用完已删。 教训:本地后端长时间空闲后接口再访问报 invalid connection,**先重启后端**;已加空闲回收,复现概率大降。前端 `BASE_API=http://localhost:8090`(.env),故本地 dev 直接打本地后端,无需切线上。线上后端(root/root)仍需部署 007(favorites)/008(histories) 迁移才有这两张表。 ## 浏览历史「看不到」排查 + 登录后补记修复(16:4x)✅ 用户反馈浏览记录看不到,且「runway 文章数据库没新增」。排查结论: - **历史写在 `histories` 表,不是 `brand_runway`**(走秀主数据表仅由爬虫/种子导入,浏览永不写入)。用户查错表,非 bug。 - 历史**按用户隔离**:local 8090 实测 `histories` 表 `root` 有 1 条(`KIND=gallery TYPE=street_snaps TITLE='Street Snaps'`),`admin` 有 0 条。账户页登录 root 才能看到 root 的记录。 - 已实证登录态写入正常(root 那条即用户用 root 登录进 Street Snaps 列表页所记)。 修复:未登录进列表页→登录后不补记图集的边界 bug。`src/lib/api.ts` 的 `login()` 成功后派发 `window` 事件 `auth:login`;`RunwayLooks.astro`/`StreetSnaps.astro` 的 `init()` 监听该事件补记对应 `gallery`(type=runway_looks / street_snaps)。dev server 已重编译,三页 HTTP=200。 ## item 页浏览历史不记录:根因=define:vars+import 致命组合(17:0x)✅ 用户访问 /en/item/r000AhByP 后个人中心与 histories 表都无记录。排查:/auth/history 后端经 curl 实测可写(root POST 成功落库),排除后端问题。抓渲染 HTML 发现 item 页