ShopSystem 第二阶段最后一环「浏览历史」今日收尾。新建 browse_history 表,采用 UNIQUE(user_id, product_id) UPSERT 策略,客户端用 sessionStorage 做 5 分钟防抖,后端在每次记录时裁剪到最多 100 条,避免热门商品反复浏览导致表膨胀。浏览历史 Tab 挂在用户中心,展示倒序分页列表、单条删除、一键清空,并对已下架商品做置灰禁用链接处理。

记录策略选 UPSERT 而非每行新增。同一个用户重复浏览同一个商品,在数据库里永远只占一行,通过 view_count 字段累计次数,列表按 last_viewed_at 倒序展示。这种设计在百万级浏览数据下仍然保持每个用户每商品只占 1 行,索引 idx_user_last_viewed 直接覆盖 ORDER BY 查询,无需聚合,响应速度稳定。
防抖放在客户端 sessionStorage。详情页是 Server Component,渲染期直接 INSERT 会被每次 F5 触发 view_count 自增,体验差且浪费 DB 写入。前端用 key 为 bh:last:<slug>、间隔 300 秒的本地存储判断是否已记录,避免重复请求。后端 UPSERT 是幂等的,即便客户端伪造跳过或禁用 sessionStorage,view_count 仍正确累积,系统不会出错。
上限 100 条裁剪用 DELETE 配合子查询 LIMIT。每次记录后,后端执行 DELETE FROM browse_history WHERE user_id=? AND id NOT IN (SELECT id FROM (SELECT id FROM browse_history WHERE user_id=? ORDER BY last_viewed_at DESC, id DESC LIMIT 100) AS keep),超出部分按 last_viewed_at 删最旧。这种写法避免 TOCTOU 竞态,两个并发请求都看到 count=99 时不会都插入导致超 1 条,数据库单次扫描内完成排序裁剪。
未登录用户静默 200。record 接口内部用 getOptionalUser,未登录时直接返回 { ok: true, recorded: false, reason: "not_login" },不写库、不报错、不阻塞 UI。详情页挂的 BrowseRecorder 客户端组件统一调用,不区分登录态,UI 层无感。

后端分 4 个 API:GET /api/account/history 倒序分页 + 孤儿清理,POST /api/account/history/record 记录浏览(含 UPSERT 与上限裁剪),DELETE /api/account/history 清空当前用户全部,DELETE /api/account/history/[id] 单条删除(校验所有权防越权)。限流 record 用 loose scope(60 次/秒/IP),浏览是高频操作避免与注册限流共享,其余接口用 default scope。
修复两个 P1 UX 问题。第一个:INNER JOIN 改 LEFT JOIN,加 GET 触发孤儿清理。Qclaw 测试发现,在 user 有指向已不存在商品的浏览记录时,total=100 但 items.length=1,UI 长期显示不一致。原因是 INNER JOIN 过滤掉孤儿行,改为 LEFT JOIN 后孤儿行也返回(商品字段全部 NULL),前端展示「商品已下架」占位,并在 GET 时执行 DELETE FROM browse_history WHERE user_id=? AND NOT EXISTS (…) 一次性清理当前用户孤儿,total 与 items 永远一致。
第二个:archived 商品置灰禁用链接。下架商品原本在 history tab 还能点击,跳详情页 404 让用户困惑。选了方案 A:卡片置灰(opacity-60 cursor-not-allowed)+ 缩略图换 🚫 emoji + 标题显示「商品已下架」+ 灰色「已下架」徽章 + 隐藏价格 + 用 div 替换 Link 不再跳转。保留用户历史记忆但视觉提示现在不能买,比隐藏方案体验好。
Qclaw 浏览器用户视角 13 项测试全过 0 P0 Bug,初测发现的两个 P1 UX 问题当日修复并通过复测 2/2(回归倒序、防抖、分页全部通过)。
第二阶段「用户体验」全部完成。从 Day 11 的 SKU 多规格、Day 12 的优惠券系统、Day 13 的地址管理、Day 14 的物流跟踪、Day 15 的退款售后、Day 16 的商品对比、Day 17 的收藏夹分组到今天的浏览历史,八天时间把用户视角的功能闭环做完整。下一步 Day 19 营销+推荐,view_count 字段可直接作为商品热度输入给推荐系统…

OpenClaw—AI研究