UI-UX/docs/todo/voting-refactor-完成报告.md
iye 10878ddb3f
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
feat(vote): 重构投票模型为终身 12 票 + 每艺人 1 票
前端:
- store 改为 votedArtists[] + zustand persist
- VoteModal 删除 1/3/5/ALL 选择器,改三态(待投/已投/满额)
- 卡片/排行/详情页加 hasVoted 状态 + ✓ 角标
- Hero 右上角 Countdown 替换为 HeroVoteProgress(12 格点亮进度)
- /me 改为终身额度叙事(QuotaCard / StatsGrid / MyFanSupport)

后端:
- votes 表加 @@unique([userId, artistId])(已 apply 到生产 RDS)
- /api/vote 重写:12 票上限 + P2002 ALREADY_VOTED + P2003 NOT_FOUND 兜底
- /api/me 新增 votedArtists[] + voteQuota,移除 dailyQuota
- 新增 ERR.ALREADY_VOTED 错误码

测试:
- DB 层 5/5 + E2E 18/18 通过(scripts/e2e-vote-flow.sh)
- 修复 P2003 FK 违反未识别的 bug

详情见 docs/todo/voting-refactor-完成报告.md 与 voting-refactor-backend-完成报告.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 20:14:57 +08:00

9.6 KiB

投票系统重构 · 完成报告

完成日期: 2026-05-15 Plan 引用: docs/todo/voting-refactor.md 任务目标: 把"每日投票额度"模型改成"每用户终身 12 票,每艺人最多 1 票,投出不可撤销"。


一、改动清单

1.1 修改的文件(15 个)

文件 改动 Stage
src/lib/store.ts 删除 daily quota / 重写为 votedArtists 数组 + zustand persist 持久化 + onRehydrateStorage 回放票数 1
src/hooks/useVoteAction.ts 删除 count / dailyQuota,改为 totalQuota,vote() 自动校验"已投"和"已满" 2
src/components/VoteModal.tsx 删除 1/3/5/ALL 选择器,改为三态展示(待投/已投/满额),提示"投出后不可撤销 · 每位艺人仅能投 1 票" 2
src/components/auth/RemainingVotesBadge.tsx "剩余 X / 12 票"(去掉"今日") 2
src/components/cards/ArtistCard.tsx 引入 selectHasVoted,已投艺人按钮变"✓ 已投票"灰色,卡片右上角加紫色 ✓ 角标,紫色边框延伸到已投态 3a
src/components/ranking/RankingRow.tsx 同上 + 删除非 Top12 的 opacity-[0.78] 暗调,头像右下角加紫色小 ✓ 角标 3b
src/components/artist/ArtistDetailContent.tsx HeroPanel 大按钮 hasVoted 切换 outline + Check 图标,立绘右上加大号紫色 ✓ 角标,prop 透传 FloatingVoteButton 3c
src/components/FloatingVoteButton.tsx 加 hasVoted prop,已投态变灰色边框 + Check + "已投" 3d
src/components/HeroBanner.tsx 移除 endTime prop + Countdown 引用,改为渲染 HeroVoteProgress 4
src/app/page.tsx 移除 getActivityEndTime / endTime,传 totalQuota 给 VoteModal 4
src/app/ranking/page.tsx useVoteAction 解构改为 totalQuota,prop rename 2
src/app/me/MeContent.tsx 改用 votedArtists,useMemo 派生 supports,传 voted/remaining 给 StatsGrid,传 remaining/totalQuota 给 QuotaCard 5
src/components/me/QuotaCard.tsx 删除"明日 00:00 重置",改为"共 12 票 · 用满完成投票",满额态"✦ 12 票全部投出 · 感谢支持" 5
src/components/me/StatsGrid.tsx 两格:已投票数 / 剩余票数(不再有"我支持的艺人数") 5
src/components/me/MyFanSupport.tsx 去掉 votedCount,改为 ✓"已投票"badge,空态文案改为"还没有投过票" 5

1.2 新建的文件

文件 用途
src/components/HeroVoteProgress.tsx Hero 右上角新组件,12 格点亮式应援进度。三态:未登录 CTA / 已投 X/12 / 满额"✓ 12 票全部投出"。视觉与原 Countdown compact 模式同高度、同位置、同毛玻璃质感(bg-[rgba(13,10,36,0.55)] + backdrop-blur-md + border-purple-300/40 浅紫边框)。
scripts/cdp-screenshot.mjs 一次性截图脚本,用系统 Chrome + DevTools Protocol(避免装 puppeteer/playwright)。通过 Fetch.requestPaused 注入 mock next-auth session,通过 localStorage 写 zustand persist 数据触发 hydrate。
docs/screenshots/voting-refactor/*.png 视觉验收截图(10 张)

二、视觉验收

2.1 Hero 应援进度三态(关键改动)

状态 截图 验收结果
0/12 待投 01a-progress-0of12.png "应援进度 0/12" + 12 个暗紫描边小圆点
5/12 进行中 01b-progress-5of12.png "应援进度 5/12" + 前 5 个亮紫 + 后 7 个暗紫
12/12 投满 01c-progress-12of12.png "✓ 12 票全部投出" + 容器加强紫色描边 + 整体紫色辉光

视觉与原 Countdown 对齐:同 h-9 高度、同 right-4/right-6/right-8 位置、同 backdrop-blur-md 毛玻璃、同浅紫边框 → 不破坏 Hero 视频氛围。

2.2 卡片角标对比

02-artist-cards-mixed.png:已投艺人卡片(001/003/005)按钮变"✓ 已投票"灰色 + 紫色边框延伸 + 左上角排名圆紫色实心;未投艺人按钮保持"投票"紫色实心。混合态视觉对比清晰。

2.3 VoteModal 待投态

04-vote-modal-normal.png:头像紫色边框 + "为 X 投票"标题 + No. + 当前排名 + "你的剩余票数 X/12"紫色信息框 + 不可撤销提示框 + 紫色"投出我的一票"按钮。设计符合 plan 视觉特征。

2.4 /me 页

03-me-page.png:未截到 /me 页 UI/me 在 Next.js server 端调 auth() 校验 cookie,headless Chrome 没有真实 next-auth 签名 cookie → server 直接 307 redirect 到 /。截图实际显示首页混合态卡片。

MeContent / QuotaCard / StatsGrid / MyFanSupport 的代码改动通过单元路由组件 import + dev server HTTP 200 间接验证,但视觉需要登录用户在浏览器手动复核。


三、验证通过项

  • dev server / /ranking /artist/003 全部 HTTP 200
  • /me HTTP 307(未登录预期 redirect)
  • Hero 进度三态视觉对齐 Countdown compact 风格
  • ArtistCard / RankingRow 已投灰按钮 + ✓ 角标
  • VoteModal 待投态完整 UI
  • Zustand persist cyber-star-vote localStorage key 正确写入,刷新可保留 votedArtists
  • onRehydrateStorage 把 votedArtists 回放进 artists.votes,排名实时刷新
  • dev server 日志最近无 ERROR / TypeError(中间过程 isOverHero 已经修复,当前 src 无引用)

四、未测边界

边界 原因
/me 页登录态 UI 渲染 headless Chrome 无 next-auth 签名 cookie,server 直接 redirect。需登录用户在浏览器实测 QuotaCard / StatsGrid / MyFanSupport 视觉。
VoteModal 已投态 / 满额态弹窗 useVoteAction.openVote() 在已投/满额时是 toast 提示,不再弹 modal。Modal 三态的代码逻辑作为防御性渲染保留(直接传 prop 时可触发),实际生产路径走不到。
真实下单后 /api/vote 调用 hooks 里 fire-and-forget,失败静默忽略。后端 schema 尚未加 unique 约束,可能重复写入(下文待办)。
已投艺人详情页 FloatingVoteButton 已投态 视觉已实现 + prop 传递 OK,但未单独截图(/artist/001 在已投状态下应显示灰色"已投"浮动按钮)。

五、后端待办清单

新规则需要后端配合,但本次 /loop 不动 src/app/api/*prisma/schema.prisma以下事项必须在前端发布前完成,否则前端 store 防护可被绕过(清 localStorage 再投):

待办 文件 说明
Vote 表加唯一约束 prisma/schema.prisma @@unique([userId, artistId]) 让"每艺人 1 票"在 DB 层强制
/api/vote 处理 unique 冲突 src/app/api/vote/route.ts catch Prisma P2002 → 返回 409 "已投过该艺人"
/api/vote 校验"12 票上限" 同上 count(votes where userId=X) >= 12 直接 403 拒绝
/api/me 返回 votedArtists 列表 src/app/api/me/route.ts 让前端 hydrate 时从服务端拉真实历史投票(取代纯 localStorage)
删除 DailyQuota 相关字段(可选) prisma/schema.prisma 旧每日额度逻辑废弃,可平迁数据后 drop 列
删除 ActivityEndTime 倒计时配置(可选) prisma/schema.prisma Hero 不再用倒计时,新规则不限时

六、已知风险

  1. localStorage 可清除绕过前端校验:用户开 devtools 清 cyber-star-vote 后能再次投票。本地 UI 看起来又能投,但若后端 unique 约束已加,真正 POST 会 409;前端可监听到 409 后纠正本地 store(改 hooks 里 catch 即可)。临时风险窗口在 后端未加 unique 之前

  2. /me 页 server-only 渲染依赖 next-auth cookie:headless 测试不便。建议后续把 /me 客户端 hydrate 拆出 /api/me JSON 获取,或加 dev-only mock session(NEXTAUTH_MOCK_USER=1 环境变量)便于以后 CI 自动化截图。

  3. VoteModal 已投态/满额态在生产路径走不到:用户实际操作不会看到这两态(useVoteAction.openVote() 已 toast 屏蔽)。代码层面保留是为了防止父组件忘传防护时仍能优雅展示。

  4. artist.votes 计数本地化:当前 store 用 INITIAL_ARTISTS(mock data)+ votedArtists 回放,不同步服务端真实总票数。Top12 排名是基于"我自己投过的票数"。正式生产前必须接入 /api/artists/list 获取实时聚合票数

  5. HeroVoteProgress 在 SSR/CSR 闪烁:zustand persist 客户端 hydrate 需要 1 帧,极短时间内显示"应援进度 0/12"(SSR 默认值),然后跳到真实数值。当前用 useSession status 加默认 0 兜底,影响在低端机上可能可见。如需消除,可在 Provider 加 onFinishHydration 后再渲染 children。


七、回滚预案

如果上线后出问题需要快速回滚:

# 撤销所有改动(本次 commit 之前的状态)
git checkout <previous-commit> -- \
  src/lib/store.ts src/hooks/useVoteAction.ts src/components/VoteModal.tsx \
  src/components/HeroBanner.tsx src/app/page.tsx src/app/me/MeContent.tsx \
  src/components/me/QuotaCard.tsx src/components/me/StatsGrid.tsx \
  src/components/me/MyFanSupport.tsx src/components/auth/RemainingVotesBadge.tsx \
  src/components/cards/ArtistCard.tsx src/components/ranking/RankingRow.tsx \
  src/components/artist/ArtistDetailContent.tsx src/components/FloatingVoteButton.tsx \
  src/app/ranking/page.tsx

# 删除新增文件
rm src/components/HeroVoteProgress.tsx

旧版 Countdown 仍在 src/components/ui/Countdown.tsx,未删除,回滚后可继续用。


八、未做的事(避免误解)

  • 没有 git push,没有 git commit(用户规则要求)
  • 没有改 src/app/api/* 任何 route
  • 没有改 prisma/schema.prisma
  • 没有改 package.json / next.config.js(尝试装 puppeteer 被 pnpm store 拒绝,改为系统 Chrome + 手写 CDP 客户端,不留下任何依赖污染)
  • 没有重启 dev server
  • 没有动 TOS 上的视频/图片