24 KiB
P5 收藏/评分 实现计划
面向 AI 代理的工作者: 必需子技能:使用 superpowers:subagent-driven-development 逐任务实现。步骤用
- [ ]复选框跟踪。spec:docs/specs/2026-09-30-favorites-ratings-design.md(需求唯一真源)。
目标: 作品收藏(布尔幂等)+ 五星评分(upsert/可撤)贯通 server→web,目录新增「热门」排序,账号页显示我的反应。
架构: 两新表 favorites/ratings(PK user_id+work_id);读侧把聚合嵌入 GameSummary 并把反应水位并入 ETag;「热门」= 贝叶斯均分 + 收藏对数,纯前端函数;deploy 零改动。
技术栈: Go 1.24 + gin + pgx / Vue 3 + vitest。
全局约束(所有任务遵守)
- 分支:crearte-server 与 crearte 各自
feat/favorites-ratings(从 master 新开)。两仓 AGENTS 红线:master 上禁 commit;合并必须--no-ff。wrapper 可直提 master。 - 基线:crearte-server master =
a8ea755(P0 后未动过);crearte master =63bb96d(P1 后)。开工前各自git pull --ff-only origin master确认。 - 宿主 go 是 1.18,模块要求 1.24:一切 go 命令走 docker——
cd crearte-server/src docker run --rm -v "$PWD:/src" -w /src golang:1.24-alpine sh -c 'gofmt -l . && go vet ./... && go test ./...'gofmt -l须输出空;首次拉镜像慢属正常。宿主机严禁直接 go build/test。 - web:
cd crearte/src && npm ci(node_modules 不在仓内,首次慢);验证npm test、npm run typecheck、npm run build全绿。 - 不动的文件:
.env、compose、nginx——本计划 deploy 零 diff。schemaVersion维持 2。 - JSON 字段名逐字:
ratingAvg(1 位小数)/ratingCount/favoriteCount;ReactionView:favoriteCount/ratingCount/ratingAvg/favorited/rated/score。 - 每个测试步骤先 RED 后 GREEN;commit message 用计划给定文本。
任务 1:server 仓库层(迁移 + ReactionRepository + store 接线)
文件:
-
创建:
src/internal/repository/migrations/0009_favorites_ratings.sql -
创建:
src/internal/model/reaction.go -
创建:
src/internal/repository/reaction.go(Postgres 实现) -
修改:
src/internal/repository/content.go(接口 + PostgresContentStore + txContent) -
修改:
src/internal/repository/memory_content.go(MemoryContentStore) -
测试:
src/internal/repository/reaction_test.go(memory 全量 + pg-gated 冒烟) -
步骤 1.1 开分支:
cd crearte-server && git checkout -b feat/favorites-ratings -
步骤 1.2 写迁移
0009_favorites_ratings.sql(spec §3.1 逐字):
CREATE TABLE favorites (
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
work_id text NOT NULL REFERENCES works(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, work_id)
);
CREATE TABLE ratings (
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
work_id text NOT NULL REFERENCES works(id) ON DELETE CASCADE,
score smallint NOT NULL CHECK (score BETWEEN 1 AND 5),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, work_id)
);
CREATE INDEX favorites_work_idx ON favorites(work_id);
CREATE INDEX ratings_work_idx ON ratings(work_id);
- 步骤 1.3
model/reaction.go:
package model
type WorkReaction struct {
FavoriteCount int
RatingCount int
RatingSum int
}
func (r WorkReaction) Avg() float64 {
if r.RatingCount == 0 {
return 0
}
return float64(r.RatingSum) / float64(r.RatingCount)
}
type UserReactions struct {
Favorites []string
Ratings map[string]int
}
type ReactionWatermark struct {
FavoriteCount int
RatingCount int
RatingSum int
}
- 步骤 1.4
repository/reaction.go——接口 + Postgres 实现(DBTX 模式同PostgresWorkRepository):
type ReactionRepository interface {
SetFavorite(ctx context.Context, userID, workID string) error
DeleteFavorite(ctx context.Context, userID, workID string) error
UpsertRating(ctx context.Context, userID, workID string, score int) error
DeleteRating(ctx context.Context, userID, workID string) error
HasFavorite(ctx context.Context, userID, workID string) (bool, error)
GetRating(ctx context.Context, userID, workID string) (int, bool, error)
AggregateFor(ctx context.Context, workID string) (model.WorkReaction, error)
AggregateAll(ctx context.Context) (map[string]model.WorkReaction, error)
ListByUser(ctx context.Context, userID string) (model.UserReactions, error)
Watermark(ctx context.Context) (model.ReactionWatermark, error)
}
SQL 要点(全部 mapPgError 包装):
-
SetFavorite:INSERT INTO favorites(user_id, work_id) VALUES($1,$2) ON CONFLICT DO NOTHING -
UpsertRating:INSERT INTO ratings(user_id, work_id, score) VALUES($1,$2,$3) ON CONFLICT (user_id, work_id) DO UPDATE SET score=EXCLUDED.score, updated_at=now() -
Delete*:无条件DELETE FROM ... WHERE user_id=$1 AND work_id=$2(删不存在也算成功) -
HasFavorite:SELECT EXISTS(...);GetRating:SELECT score ...+pgx.ErrNoRows → (0,false,nil) -
AggregateFor:两条count(*)/COALESCE(sum(score),0)查询合成 -
AggregateAll:两表各GROUP BY work_id,合并进 map(仅出现过的 work 有条目) -
ListByUser:favorites 按 created_at 排序取[]string;ratings 扫成 map -
Watermark:四条标量——SELECT count(*), COALESCE(sum(score),0) FROM ratings、SELECT count(*) FROM favorites -
步骤 1.5 接线三处 store:
content.go:ContentRepository接口加Reactions() ReactionRepository;PostgresContentStore加字段reactions *PostgresReactionRepository(构造里NewPostgresReactionRepository(pool)),accessor 一行;txContent同样加Reactions()(返回&PostgresReactionRepository{q: tx})。memory_content.go:MemoryContentStore加字段favorites map[[2]string]struct{}、ratings map[[2]string]int(构造函数初始化);accessorfunc (m *MemoryContentStore) Reactions() ReactionRepository { return memoryReactions{m} };WithTx的快照 clone 与回滚补这两个 map(与 works 同款)。- 新
memoryReactions实现同一接口(锁m.mu;键[2]string{userID, workID})。Watermark遍历两 map 累加。
- 步骤 1.6 写
reaction_test.go——先失败测试(memory): TestMemoryReactionRoundtrip:SetFavorite 幂等(二次 count 仍 1)→ DeleteFavorite→count 0;UpsertRating 3→count1 sum3→改 5→sum5→Delete→count0。TestMemoryAggregateAll:两作品多用户 → map 正确、缺席作品无条目。TestMemoryListByUser:favorites 顺序 = 插入序(用SetNow推进时间),ratings map 完整。TestMemoryWatermarkChanges:加收藏/加分/分从 3→5(Watermark.RatingSum 变)各触发水位变。- pg-gated:
func TestPostgresReactionGated(t)——os.Getenv("TEST_DATABASE_URL")==""时t.Skip;非空时复用content_migrate_test.go的建库方式跑 Roundtrip 同断言。 运行:docker run --rm -v "$PWD:/src" -w /src golang:1.24-alpine go test ./internal/repository/ -run Reaction→ RED(接口不存在/编译失败也算 RED)。 - 步骤 1.7 实现至 GREEN:同命令 +
go vet ./...+gofmt -l .(空输出)。 - 步骤 1.8 Commit:
git add -A && git commit -m "feat(repository): favorites/ratings tables + ReactionRepository (pg + memory + store wiring)
- migration 0009 two tables PK(user_id,work_id), work_id indexes, FK cascade
- AggregateFor/All, ListByUser, Watermark for ETag invalidation
- ContentRepository gains Reactions(); tx + memory WithTx snapshot updated"
任务 2:server 服务 + DTO + 路由 + 装配
文件:
-
修改:
src/internal/service/content.go(ListPublished/GetPublishedDetail 签名 + 写侧方法 + 水位并入 ETag) -
修改:
src/internal/handler/games.go(summary/detail 填聚合) -
修改:
src/internal/api/dto/content.go(GameSummary 三字段) -
创建:
src/internal/api/dto/reaction.go -
创建:
src/internal/handler/reactions.go -
修改:
src/internal/api/router.go(常量 + Deps + 注册)、src/internal/api/ratelimit.go(DefaultReactionRateLimit = 30) -
修改:
src/cmd/serve.go(装配Reactions: handler.NewReactions(contentSvc)) -
测试:
src/internal/api/reactions_test.go、更新src/internal/service/content_test.go调用点(grep -rn 'ListPublished(\|GetPublishedDetail(' src找全) -
修改:
docs/CHANGELOG.md(0.11.0 双语置顶) -
步骤 2.1 先写失败测试
reactions_test.go(helpernewReactionEngine照抄uploads_test.go的 newWriteEngine 骨架,Deps 加Reactions: handler.NewReactions(contentSvc),种子作品用demoExternalWork()+ publish)。断言清单: -
匿名 PUT favorite → 401。
-
u1 PUT favorite → 200
{favoriteCount:1, favorited:true, ratingCount:0};再 PUT → favoriteCount 仍 1;DELETE → 0,可再 DELETE(幂等 200)。 -
u1 PUT rating
{"score":4}→ 200 avg4 count1;u25→ avg 4.5;u1 改2→ avg 3.5;u1 DELETE → avg 5 count1;GET detail:ratingAvg:5,ratingCount:1,favoriteCount:1。 -
score为 0/6/字符串/缺省 → 400invalid_request;不存在作品 → 404not_found;下架作品(SetDelisted true)→ 404。 -
GET
/api/games:反应发生前后 ETag 头必须不同;条目含favoriteCount/ratingCount/ratingAvg,无反应作品三字段缺省(omitempty)。 -
GET
/api/me/reactions(u2)→{"favorites":[],"ratings":{"alice/2048":5}};u1 收藏后 favorites 含 id。 -
运行 docker go test ./internal/api → RED。
-
步骤 2.2 service 改造(GREEN 核心):
// 签名变更(调用点同步更新)
func (s *ContentService) ListPublished(ctx context.Context) ([]model.WorkWithVersion, map[string]model.WorkReaction, string, error)
func (s *ContentService) GetPublishedDetail(ctx context.Context, id string) (model.WorkWithVersion, model.WorkReaction, string, error)
- 列表:取
agg, _ := s.store.Reactions().AggregateAll(ctx)、wm, _ := Watermark(ctx);哈希 basis 由stamp|count扩为stamp|count|<wm.FavoriteCount>|<wm.RatingCount>|<wm.RatingSum>。 - 详情:
reaction := AggregateFor(ctx,id);basis 追加|<fav>|<cnt>|<sum>。 - 写侧(每个先
GetByID+Visible()校验 →ErrNotFound/不可见映射ErrContentNotFound):
func (s *ContentService) SetFavorite(ctx context.Context, userID, workID string, on bool) (model.WorkReaction, error) // on→SetFavorite off→DeleteFavorite
func (s *ContentService) RateWork(ctx context.Context, userID, workID string, score int) (model.WorkReaction, error) // UpsertRating
func (s *ContentService) UnrateWork(ctx context.Context, userID, workID string) (model.WorkReaction, error) // DeleteRating
func (s *ContentService) UserReaction(ctx context.Context, userID, workID string) (favorited bool, score int, rated bool, err error)
func (s *ContentService) MyReactions(ctx context.Context, userID string) (model.UserReactions, error)
- 步骤 2.3 dto:
GameSummary尾部加
RatingAvg *float64 `json:"ratingAvg,omitempty"`
RatingCount int `json:"ratingCount,omitempty"`
FavoriteCount int `json:"favoriteCount,omitempty"`
dto/reaction.go:
type ReactionView struct {
FavoriteCount int `json:"favoriteCount"`
RatingCount int `json:"ratingCount"`
RatingAvg *float64 `json:"ratingAvg,omitempty"`
Favorited bool `json:"favorited"`
Rated bool `json:"rated"`
Score int `json:"score,omitempty"`
}
func NewReactionView(r model.WorkReaction, favorited bool, score int, rated bool) ReactionView // Avg 保留 1 位小数:math.Round(a*10)/10,count==0 时 RatingAvg=nil
type MeReactionsResponse struct {
Favorites []string `json:"favorites"`
Ratings map[string]int `json:"ratings"`
}
- 步骤 2.4 handler:
games.go的summary(item)→summary(item, reaction model.WorkReaction)(无反应传零值,仅 >0 时填字段;float 指针同样只在 RatingCount>0 时给)。List/Detail 用新签名;Detail 的h.detail(item, reaction)同步。新handler/reactions.go:
type Reactions struct{ svc *service.ContentService }
func NewReactions(svc *service.ContentService) *Reactions
// workID(c):c.Param("user")/("slug") 过 service.UsernamePattern/SlugPattern,不合→400 invalid_request;service.JoinWorkID 拼 id
// SetFavorite/DeleteFavorite/SetRating/DeleteRating:CurrentUser(c).ID → svc 写侧 → svc.UserReaction 组装 ReactionView → 200 JSON
// SetRating body:struct{ Score int `json:"score"` };越界(1-5 之外)/非法 JSON → 400 invalid_request
// ErrContentNotFound → 404 not_found;其他 err → 500 internal(log.Printf 同 games.go 风格)
// Me:svc.MyReactions → MeReactionsResponse(空 favorites 输出 [],不输出 null)
- 步骤 2.5 router:常量三条 +
DefaultReactionRateLimit = 30+ 注册块 + Deps 字段Reactions *handler.Reactions+ cmd/serve.go 装配(无 storage 也注册——反应不依赖对象存储):
RouteGameFavorite = "/api/games/:user/:slug/favorite"
RouteGameRating = "/api/games/:user/:slug/rating"
RouteMeReactions = "/api/me/reactions"
PUT 两条挂 RequireUser + reactionLimiter;DELETE 两条与 GET 只挂 RequireUser。
- 步骤 2.6 全绿验证:docker
gofmt -l . && go vet ./... && go test ./...(content_test.go 调用点已按新签名改)。 - 步骤 2.7 CHANGELOG 置顶
## [0.11.0] - 2026-09-30,双语:Added——favorites/ratings 两表 + 六端点 + 目录/详情聚合字段 + 反应水位并入 ETag(英文行紧接中文行)。 - 步骤 2.8 Commit:
git add -A && git commit -m "feat(api): favorites/ratings user-space endpoints + reaction aggregates in content read side
- PUT/DELETE /api/games/:u/:s/favorite & /rating (RequireUser, 30/min), GET /api/me/reactions
- GameSummary gains ratingAvg/ratingCount/favoriteCount (omitempty, schemaVersion stays 2)
- list/detail ETag folds reaction watermark (counts+sum) — aggregates never served stale
- changelog 0.11.0"
任务 3:web 数据契约 + 热门排序
文件:
-
修改:
src/app/data/types.ts(GameSummary 可选字段)、src/app/lib/filter.ts、src/app/lib/filter.test.ts、src/app/components/ResultMeta.vue -
步骤 3.1
cd crearte && git checkout -b feat/favorites-ratings && cd src && npm ci -
步骤 3.2 失败测试(vitest):
-
types.test.ts/既有断言不因可选字段破坏; -
filter.test.ts新增 describe('sort hot'):- 构造 g1(n=1,sum=5,fav=0)、g2(n=100,sum=430,fav=0):m=435/101≈4.307 → g1 分 (5·4.307+5)/(105)=0.2527… vs g2 (5·4.307+430)/(105)=4.152… → g2 先;
- 压平用例:单人 5 星(n=1,fav=0)输给 n=3 sum=12(均 4)+fav=1 的作品;
- 收藏对数:两作品同均分同 n,fav 10 vs 1 → fav10 先;
- 静态缺省:三字段 undefined 的作品全获同分 → 按 addedAt 倒序(退化为 new);
- URL 往返:
stateToQuery({sort:'hot'})出?sort=hot,parseQuery认hot,未知值回退new。
-
步骤 3.3 实现:
-
types.tsGameSummary 加ratingAvg?: number; ratingCount?: number; favoriteCount?: number。 -
filter.ts:SortKey = 'new' | 'name' | 'duration' | 'hot';parseFilterState白名单加'hot';新导出:
export function hotScore(game: GameSummary, priorC: number, priorMean: number): number {
const n = game.ratingCount ?? 0
const sum = (game.ratingAvg ?? 0) * n
return (priorC * priorMean + sum) / (priorC + n) + 0.5 * Math.log(1 + (game.favoriteCount ?? 0))
}
filterGames 的 hot 分支:先对过滤后集合求 nAll=ΣratingCount、sumAll=Σ(ratingAvg×ratingCount),m = nAll>0 ? sumAll/nAll : 4;scores = new Map(...)(按 id 缓存一次计算,sort 比较器禁止重复算分);比较 b-a,|Δ|<=1e-9 时 addedAt 倒序 → id 正序。
ResultMeta.vue:<option value="new">最新收录</option>之后插<option value="hot">热门</option>。- 步骤 3.4
npm test全绿 +npm run typecheck。 - 步骤 3.5 Commit
git add -A && git commit -m "feat(catalog): hot sort — bayesian prior mean + favorite log weight, pure fn with tie-breaks"
任务 4:web 反应 UI + 账号页
文件:
-
创建:
src/app/lib/reactions.ts(+ test)、src/app/components/GameReactions.vue(+ test) -
修改:
src/app/views/GameView.vue、src/app/components/GameCard.vue(+ 其 test)、src/app/views/AccountView.vue(+ test) -
修改:
docs/CHANGELOG.md(0.16.0) -
步骤 4.1 失败测试(列出断言,实现者按此写):
-
reactions.test.ts:注入假 fetch——PUT favorite 带Authorization: Bearer <token>、URL 拼VITE_API_BASE_URL去尾斜杠;401 → 抛AuthRequiredError;base 为空时reactionsEnabled=false、调用抛错不发请求。 -
GameReactions.test.ts(mount,mock lib):显示4.5 · 2 人评分 · 3 收藏;favorited=true 时 ♥ aria-pressed;点星 4 → setRating('u','s',4);当前分 4 再点 4 → unrate;匿名(session.getToken()=null)点按钮 → router.push('/login?next=…');乐观锁:在途禁再点(busy)。 -
GameCard.test.ts增:ratingCount>0 显★ 4.5 · 2 人;favoriteCount>0 显♥ 3;两者 0/缺省不显徽标。 -
AccountView.test.ts增:fetchMine → favorites 命中 repo 列表渲染链接行、取消收藏调 setFavorite(on=false);空数据显「还没有收藏或评分。」 -
步骤 4.2
lib/reactions.ts:
import { session } from '@/auth'
const rawBase = (import.meta.env.VITE_API_BASE_URL ?? '').trim()
export const reactionsEnabled = rawBase !== ''
const base = rawBase.replace(/\/+$/, '')
export interface ReactionView { favoriteCount: number; ratingCount: number; ratingAvg?: number; favorited: boolean; rated: boolean; score?: number }
async function call(path: string, init: RequestInit): Promise<ReactionView | unknown> {
if (!reactionsEnabled) throw new Error('未配置后端')
const token = session.getToken()
if (!token) throw new AuthRequiredError()
const res = await fetch(`${base}${path}`, { ...init, headers: { Authorization: `Bearer ${token}`, ...(init.body ? { 'Content-Type': 'application/json' } : {}) } })
if (res.status === 401) throw new AuthRequiredError()
if (!res.ok) throw new Error(`请求失败 ${res.status}`)
return res.json()
}
export const setFavorite = (user, slug, on): Promise<ReactionView> => call(`/api/games/${user}/${slug}/favorite`, { method: on ? 'PUT' : 'DELETE' })
export const setRating = (user, slug, score) => ...PUT body JSON.stringify({score})...
export const unrate = (user, slug) => ...DELETE...
export async function fetchMine(): Promise<{ favorites: string[]; ratings: Record<string, number> }>
class AuthRequiredError extends Error {} // export
- 步骤 4.3
GameReactions.vue(propsgame: Game;script:favorited/rated/score/countsref,onMounted若有 token 调fetchMine()初始化个人态;toggleFav()、pickStar(i)(i===score→unrate 否则 setRating)、reset()(已评且点击当前星);每次成功后用 ReactionView 全量替换本地态;失败显示一行错误文本aria-live;busy在途禁用全部按钮)。模板:♥ 按钮(data-testid="fav-btn",aria-pressed,样式沿用 border-2 border-ink bg-surface lift 族)+ 五星条五颗data-testid="star-<i>"(实心=floor(avg);已评显个人分)+ 文案{{ ratingCount>0 ?${ratingAvg} · ${ratingCount} 人评分: '暂无评分' }} · {{ favoriteCount }} 收藏。挂载:GameView<GameReactions v-if="game" :game="game" />插在操作链接(开始体验/GameHost)之后、intro 分隔线之前;authEnabled(VITE_API_BASE_URL 非空,import 自@/auth)为假时组件内部直接不渲染(noauth 零请求)。 - 步骤 4.4
GameCard.vue:作者行之后、tags 行之前加徽标<p v-if="hasReaction" data-testid="reaction-badge">…(computed 拼★ avg · n 人与♥ f,aria-hidden星形用文本即可)。 - 步骤 4.5
AccountView.vue:会话 section 前插「我的收藏」——onMounted && session.state.status==='authenticated'时fetchMine()+repo.listGames()join(GameSummary.id 匹配);渲染复用 GameCard 链接行样式(标题→/games/u/s + 取消收藏小钮);评分列表紧凑行作品名 ★<score>;空态文案;reactionsEnabled假时整节不渲染。 - 步骤 4.6 全绿:
npm test && npm run typecheck && npm run build。 - 步骤 4.7 CHANGELOG 0.16.0 双语置顶(Added:详情收藏/评分控件、目录热门排序与徽标、账号页我的反应)。
- 步骤 4.8 Commit
git add -A && git commit -m "feat(ux): favorite & 5-star rating UI on game page, reaction badges on cards, my-reactions section on account, catalog hot sort option, changelog 0.16.0"
任务 5:live 冒烟(prod 栈实跑,零代码改动)
- 步骤 5.1
cd crearte-deploy && docker compose --profile prod build api-prod web-prod && docker compose --profile prod up -d,等 api-prod healthy(新代码进镜像——先 build 再 up;web-prod 用最新前端)。 - 步骤 5.2 冒烟链(bash 脚本 /tmp/p5smoke.sh,
set -euo pipefail;B=http://localhost:8080):
ETAG0=$(curl -sD- -o /dev/null $B/api/games | grep -i '^etag' | tr -d '\r' | cut -d' ' -f2)- 注册 u1@p5.example/u2@p5.example(curl register ×2,各 login 拿 T1/T2)。
- u1 PUT
$B/api/games/p1admin/p1-smoke/favorite(Bearer T1)→ jq.favoriteCount==1 and .favorited==true;再 PUT → 仍 1。 - u1 rating
{"score":4}→.ratingAvg==4;u2{"score":5}→4.5;u1 改{"score":2}→3.5;u1 DELETE →.ratingAvg==5 and .ratingCount==1。 curl -sD- -o /dev/null $B/api/games取 ETAG1 ≠ ETAG0;带If-None-Match: <ETAG1>→ 304。curl -s $B/api/gamesjq 断言 p1admin/p1-smoke 条目favoriteCount==1, ratingCount==1, ratingAvg==5;curl -s $B/api/games/p1admin/p1-smoke详情同样。curl -s $B/api/me/reactions -H "Authorization: Bearer ***")→.favorites|index("p1admin/p1-smoke")!=null;u2 →.ratings["p1admin/p1-smoke"]==5。- 负路:匿名 PUT favorite→401;
{"score":7}→400;不存在作品→404。 - 徽标渲染抽查:
curl -s http://localhost:8080/ | head -c 300200 即可(SPA,DOM 断言归 vitest;浏览器级验收 manual-deferred 如常)。
- 步骤 5.3 收尾:
docker compose --profile prod down(保留卷);删 /tmp/p5*。 - 步骤 5.4 复跑静态验证:docker go 全套、
npm test/typecheck/build(若任务 2/4 后无改动可引用其报告,重跑一次留快照更稳)。 报告写.superpowers/sdd/p5-task-5-report.md。
任务 6:两仓合并 + 推送
- 步骤 6.1 crearte-server:
git checkout master && git pull --ff-only origin master && git merge --no-ff feat/favorites-ratings -m "Merge branch 'feat/favorites-ratings' — favorites & 5-star ratings, hot sort server side (P5)" && git push origin master && git branch -d feat/favorites-ratings && git push origin --delete feat/favorites-ratings 2>/dev/null; git log --oneline -1 - 步骤 6.2 crearte:同款 merge message 尾缀换
(P5),push、删分支。 - 步骤 6.3
--ff-only若拉来新提交且冲突:git merge --abort上报 BLOCKED,禁止强推。
任务 7:wrapper 收尾(控制者直接执行)
ROADMAP P5 行 → **完成**(server \<6.1 hash>` / crearte `<6.2 hash>`,2026-09-30 合并推送;范围修订 deploy 零改动,见 spec);索引表加 P5 两行文档;CHANGELOG 0.2.3 双语;commit+push;终态核验 git -C crearte-server pull --ff-only origin master等三条 +./clone_all.sh`。
自检记录(已内联修复)
- 规格覆盖:spec §3.1→任务 1;§3.2→任务 2;§3.3→任务 3/4;§4→任务 2/3/4/5 测试 + 任务 5 live;§2 ETag→1.4 Watermark/2.2 并入。✔
- 占位符扫描:无 TODO/待定;hash 占位为运行时事实。✔
- 命名一致:
ReactionRepository/Watermark/AggregateAll/UserReactions/ReactionView(任务 1→2);hotScore/setFavorite/unrate/fetchMine/AuthRequiredError(任务 3→4);JSON 名三处(2.3、4.2、5.2)逐字一致。✔