65 lines
6.4 KiB
Markdown
65 lines
6.4 KiB
Markdown
# 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。
|