20 KiB
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/Favoritehandler。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_refreshMIG_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 buildEXIT=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 buildEXIT=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;serviceList→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/serverEXIT=0;本地后端重启新二进制 HEALTH_OK;PowerShell 端到端:加3收藏 → P1(total3,last2,count2)/P2(count1) 分页正确 → check 返回3命中 → 删除后 total0。前端astro buildEXIT=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_uidrunway-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 buildEXIT=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.goSetConnMaxIdleTime,默认 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 即恢复,非代码问题。