Files
crearte-monorepo/docs/specs/2026-09-30-favorites-ratings-design.md

6.4 KiB
Raw Permalink Blame History

P5 收藏/评分 设计 / Favorites & Ratings Design

  • 状态:设计已批准(2026-09-30,§1-§3 逐节确认)。
  • 路线图:docs/ROADMAP.md P5(第二波)。依赖 P1(写侧完成,可并行 P2)。
  • 范围定性:新数据表(favorites + ratings)+ 用户态 API + 目录「热门」排序 + 详情/账号页 UI。crearte-deploy 零改动(nginx /api/ 反代既有,新路由自动透传)——路线图"三仓"措辞按本 spec 修订为两仓。

1. 背景与意图

读者目前对作品只能"看"。P5 加上两类反应:收藏(布尔、幂等)与 5 星评分(一人一分,可改可撤),并让目录支持 热门排序:贝叶斯均分(单人满分霸榜被先验压平)+ 收藏对数权重。成功标准:dev 栈冒烟里"注册→收藏→评分→列表聚合与 ETag 变化→第二用户评分均值变化→我的反应→取消/幂等→校验与鉴权"全链成立;前端 typecheck/build 全绿。

约束(与维护者确认):

  • 键是账号 uuid(users.id),不是 username;改用户名不影响既有反应。
  • 下架作品的反应行保留(不删、不级联到作品删除语义之外)。
  • 写端点全部 RequireUser + 按 IP 限流(30/min),作品必须存在且可见否则 404。
  • ratingCount==0 不显示均分;静态源(index.json 无反应字段)视为 0 优雅降级。
  • schemaVersion 维持 2:新增字段全部可选,老断言(assertGameSummary)天然容忍。

2. 方案选型

方案 内容 取舍
A. 两表 + 读侧嵌入 DTO(已选) favorites / ratings 各一张,PK (user_id, work_id);列表/详情返回服务端聚合;热门分前端纯函数计算 语义直白、可反查(谁收藏了/该作品几星);聚合是 O(表) 全量 map,千级作品无压力
B. 单表 interactions(kind) 一张表 kind∈{favorite,rating} 省一张表但 CHECK/语义纠缠,聚合查询反而绕
C. 服务端算热门分返回排序 /api/games?sort=hot 与现"前端纯函数过滤排序"架构冲突,且 ETag 会随排序参数碎片化

ETag 正确性是方案 A 的关键暗礁:反应一改,旧列表/详情 ETag 若不变,304 将回吐过期聚合。对策:读侧 ETag 输入并入反应水位(Postgres 用两表 count + hashtext 校验和 + 评分和;memory 用等价计数器摘要)。

3. 设计详述

3.1 server — 数据与仓库(crearte-server 0.11.0)

  • 0009_favorites_ratings.sql:两表 + work_id 索引(FK 指 users/works,ON DELETE CASCADE;users 无删除路径故 favorites 行为事实不可变)。
  • ReactionRepository:SetFavorite / SetRating / DeleteRating / ReactionFor(user,work) / AggregateFor(work) / AggregateAll() / ListByUser(user) / Watermark();Postgres(DBTX,双查询拼聚合)+ Memory(map[[2]string],WithTx 快照补 favorites/ratings)。
  • ContentRepository 接口加 Reactions();PostgresContentStore、txContent、MemoryContentStore 三处接线。

3.2 server — 服务与 API

  • ContentService:
    • ListPublished 返回值加 map[string]model.WorkReaction,ETag 摘要并入 Watermark();
    • GetPublishedDetail 返回加 model.WorkReaction,ETag basis 追加 favCnt|rateCnt|sum;
    • 写侧:SetFavorite(ctx,userID,workID,on) / RateWork(...,score) / UnrateWork —— 先查 GetByID + Visible()(不可见 → ErrContentNotFound),再落库,返回 dto.ReactionView。
  • dto:GameSummary 增 ratingAvg *float64 omitempty / ratingCount / favoriteCount(count 为 0 时 omitempty,与静态缺省同形);新 ReactionView {favoriteCount, ratingCount, ratingAvg?, favorited, rated, score?};MeReactionsResponse {favorites []string, ratings map[string]int}。均分保留 1 位小数(math.Round(x*10)/10)。
  • 路由(router.go 常量 + 注册):
    • PUT|DELETE /api/games/:user/:slug/favorite
    • PUT /api/games/:user/:slug/rating(body {"score":1..5},非法 400)/ DELETE 同径撤
    • GET /api/me/reactions
    • 全部 RequireUser;写路径共享 reactionLimiter = NewRateLimiter(30, time.Minute);handler Reactions 新文件,Deps.Reactions 装配(cmd/serve.go)。

3.3 web — 排序与反应 UI(crearte 0.16.0)

  • data/types.ts:GameSummary 三个可选字段;apiRepo 断言容忍(不加必选)。
  • lib/filter.ts:SortKey 增 'hot'(默认仍 new);查询白名单同步;纯函数 hotScore = (5·4 + Σ)/(5 + n) + 0.5·ln(1 + favoriteCount),Σ = ratingAvg×ratingCount;排序比较 hotScore(b)-hotScore(a) || addedAt desc || id asc,浮点容差 1e-9 回退。
  • components/ResultMeta.vue:下拉加 <option value="hot">热门</option>(在「最新收录」之后)。
  • 新 lib/reactions.ts:封装 fetch(Bearer 取 session.getToken();base 与 data/apiRepo 同源 VITE_API_BASE_URL),导出 toggleFavorite/rate/unrate/fetchMine,enabled 判空 base。
  • 新 components/GameReactions.vue:♥ 收藏钮(aria-pressed)+ 五星条(点亮=floor 均值;点击=打分,再点当前分=撤)+ 计数文案 {{avg}} · {{count}} 人评分 · {{fav}} 收藏;匿名点击跳 /login(带 ?next=);操作后即地用 ReactionView 更新。挂载于 GameView 标题 meta 区。
  • GameCard.vue:统计徽章行 ★ 4.3 · 12 人 · ♥ 30(仅显示存在的聚合);点击仍进详情(徽章非交互)。
  • AccountView.vue:新「我的收藏」section —— 列表(链接 + 取消收藏钮)+「我的收藏评分」紧凑列表;空态「还没有收藏或评分。」

3.4 明确不做

作品删除级联语义变更、username 级联、防刷(仅 IP 限流)、评分分布图、匿名半星、分页排序(客户端全量前提不变)。

4. 测试与验收

  • server:repository memory + pg-gated 测试(幂等、撤、聚合、Watermark 变);api httptest(401/404/400、PUT→DELETE 链、/api/me/reactions 形状、ETag 随反应变);gofmt/vet/build/test 经 docker golang:1.24 全绿。
  • web:vitest(hotScore 纯函数含压平/对数/容差用例、排序稳定性、GameReactions 交互 mock fetch、GameCard 徽章渲染);typecheck+build 绿。
  • live:dev 栈冒烟全链(见实现计划任务 7)。
  • 两仓 feat/favorites-ratings → 各自 master --no-ff 合并推送;wrapper 登记 ROADMAP P5 完成 + CHANGELOG 0.2.3。