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

20 KiB
Raw Blame History

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<T>(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<id,boolean>)替代原 x-data="{ fav: isFav(it.id) }" 的一次性捕获(后者服务端异步返回后不会刷新卡片)。
  • src/lib/favorites.ts:新增 loadServerFavIds(): Promise<string[]>(调 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 页 <script define:vars={{meta...}}> 被 Astro 包成内联 IIFE,原脚本顶部的 import 被塞进函数体内 → SyntaxError 整段不执行,故 ecordHistory/收藏绑定全失效。修复:en/cn item/[id].astro 去掉 define:vars,改由按钮 data-id/type/title/cover/brand/save-label/saved-label 属性透传,模块脚本内 tn.dataset 读取。dev server 已重编译,抓模块源码确认 import 已在顶层。清理了测试写入的 r000AhByP 记录。教训已写入 MEMORY.md Astro script 坑。

浏览历史设计纠正:仅图集记录,单张 item 不记(17:3x)✅

用户纠正:浏览历史应「进图集(RunwayLooks/StreetSnaps)才记」,单张 item 详情页不该记。此前我给 item 页加了 image 历史记录,属理解错误。修复:删 en/cn item/[id].astro 里的 ecordHistory({kind:'image',...}) 调用与导入(收藏逻辑不动);账户页 History 区移除无用的「图片」筛选 tab(仅留 全部/图集),卡片角标固定 gallery。图集页 gallery 记录逻辑(RunwayLooks/StreetSnaps 的 init() 调 recordHistory)保持不变,已抓打包脚本确认 ecordHistory/kind:gallery 仍在。MEMORY.md 同步更新:浏览历史仅记图集。

个人中心「没有」根因定位(17:4x)✅

用户报「个人中心还是没有」。排查:后端 history 全链路实测通过(Node 脚本 login admin → POST /auth/history ok → GET total=1,histories 表存在);前端 recordHistory/fetchHistoryPage/account 渲染逻辑均正确。根因在登录态:.env 当前 BASE_API=http://localhost:8090,本地库只有 admin、无 root,用户用 root/root 登录失败 → getUser() 为空 → 进 account 被踢登录页、图集不记历史。结论:本地验证一律用 admin/Studio#2026!Admin;要用 root 须改 .env 回线上域名并重启 dev server+硬刷新+重登录。MEMORY 账号体系段已补此坑。

纠正:user 表确有 root + 个人中心空的真因(18:4x)✅

实测 db_dev.users 经 scripts/seed_users 创建 admin(强密码 Studio#2026!Admin)+root/root(uid 8)。 oot/root 登录本地 :8090 返回 200;GET /auth/history 显示 root 账号下已有 street-snaps/runway-looks 两条真实图集历史(viewed_at≈当前)。此前'本地无 root、root 登录必失败'为误判,已纠正 MEMORY 账号体系段。 个人中心'看不到'两主因:(1) account.astro 默认 tab='saved'(收藏,常空),浏览历史在 'My History' tab,需点击;(2) 前端 dev server 若连线上而非本地 8090,则 root 登录线上、看不到本地这 2 条。已 stro dev stop+stro dev --background 重启使 .env 的 BASE_API=本地 8090 生效。验证:硬刷新→root/root 登录→点 My History→应见 2 条。

实锤根因:账户页历史空 = kind=all 被后端按字面过滤(18:5x)✅

带 root token 实测后端 /auth/history 三种查询:无 kind → total=2;kind=all → total=0(后端把 all 当字面 kind 值 WHERE kind='all');kind=gallery → total=2。根因:账户页 History「全部/默认」把 kind='all' 拼进 query,后端无该语义 → 恒空;数据其实一直在 root 下(2 条 gallery)。用户直接 curl 不带 token 返回 0 属正常未鉴权空返回。修复:src/lib/history.ts fetchHistoryPage 把 'all'/空 归一为不带 kind 参数(只传 image|gallery 这类具体值)。MEMORY 浏览历史条目已记坑。另:后端对无效 kind 值返回空而非全部,属脆弱设计(可选后续加固)。

浏览历史语义纠正与收敛:图集→文章级(19:4x-20:0x)✅

用户澄清:他说「进到图集里才有浏览记录」指的是点开一篇文章(详情页看整组图),不是进 RunwayLooks/StreetSnaps 列表页——此前按列表页 gallery 实现是理解反了。已按用户拍板重构:

  • 语义:浏览历史=点开过的文章;item 详情页 init ecordHistory(meta.id);列表页记录与 auth:login 补记全删(en/cn item + RunwayLooks + StreetSnaps)。
  • 表收敛:histories 改 (user_id,target_uid,viewed_at),DROP kind/target_type/title/cover/brand;唯一键(user_id,target_uid);保留 HISTORY_CAP=2000 FIFO(文章会积累)。后端 model/dto/handler/service/repository 同步删字段;dto.HistoryAdd 仅 target_uid。008 迁移脚本改新表结构,存量库跑 migrate_refresh ALTER(实测生效:GET items 仅 {id,viewed_at},POST {target_uid}→200)。
  • 展示:账户页先服务端分页取 id,再逐篇 getHistoryMeta(id) 回查公开详情拿 title/cover/brand(api.ts 新增,s= 前缀判 street 否则 runway);卡片跳文章详情;删 kind 筛选 tab/histHref/gallery 徽标;标题回查失败用 '…'。
  • 验证:POST/GET 全绿;/public/runway-looks/r0042h1mx 回查字段齐全;account、item 页面 SSR 200。dev server 已重启(PID 31792)、后端 server-bin 已重建。MEMORY 浏览历史条目已整体改写。

item 页脚本死引用 bug:loadServerFavIds 不存在 → 整页脚本挂(19:5x)✅

用户反馈打开文章详情不发 /auth/history。Console 报 Uncaught SyntaxError: ...'/src/lib/favorites.ts' does not provide an export named 'loadServerFavIds' → 模块级 import 失败整段脚本不执行(recordHistory 与收藏事件全失效)。根因:收藏服务端化重构时 favorites.ts 把 loadServerFavIds 改成 checkFavorited(ids),但 en/cn item/[id].astro import 与调用漏改。修复:item 页改用 checkFavorited([meta.id]) + favorites.ts 新增 hydrateFavorited(id,type,title,cover,brand)(服务端命中写回本地、不触发 POST),en/cn 同步。lint 0,实测脚本已无死引用、recordHistory 调用在位。教训:删除/改名 lib 导出后须全局 grep 残留 import;item 页脚本曾长期瘫痪而无人察觉(收藏/历史均静默失效)。

Alpine「t is not defined」修复 + dev 热更崩溃(20:0x)✅

账户页模板把 t() 写进客户端 Alpine 表达式(如 x-text 里 t(remove) 等共 5 处/语言),Alpine 作用域无 t,一旦有数据渲染就 Expression Error。修复:文案改为 frontmatter 预翻译常量(removeLabel/itemsLabel/clearHistoryLabel),模板用 Astro 插值输出,x-text 只保留数字;计数拆分数字 span 加静态文案。全项目 grep 客户端表达式内 t( 已为 0。约定:Alpine 表达式绝不调 t(),文案一律走 {t()} 服务端插值或预翻译常量。另:批量编辑后 dev server 增量编译状态损坏,全站 500(undefined is not a function at Array.map),重启 astro dev 即恢复,非代码问题。