UI-UX/src/lib/date-utils.ts
iye a9f4799f71
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m26s
feat(db): wire real persistence for votes / users / quota / supports
数据正式落库, 不再仅靠浏览器内存:

prisma/schema.prisma:
- Artist 模型对齐当前前端数据形态:
  * 旧字段 slogan / birthday / cv / themeColor 改为可选 (前端早不用, 但保留兼容历史 seed)
  * 新增 age / gender / motto / personality / catchphrase / skills / track (来自人物小传)
- 注释从 "001 ~ 035" 改 "001 ~ 036"

prisma/seed.ts:
- 整体重写: 从 src/lib/artist-bios.ts 的 ARTIST_SEEDS 灌真实 36 人
- 不再写假数据 (AURORA / LUMI / NEBULA...)
- portrait / videoUrl 不入库 (前端 NEXT_PUBLIC_TOS_DOMAIN 拼接, 换桶不用 reseed)
- ActivityConfig 默认 dailyQuota=10, perArtistLimit=0, voteEnabled=true, 活动期 30 天

src/lib/date-utils.ts (新增):
- startOfUtcDay(): 修复"今日"在 MySQL @db.Date 列与 JS Date 之间的 TZ 漂移
- isSameUtcDay(): 共享给签到判断

修复 P2025 bug (vote / me / signin):
- 用 startOfUtcDay 替代 startOfDay (后者用 setHours 取本地午夜,
  对 @db.Date 列会因 TZ 漂移导致 upsert 后再用 userId_date 复合键查找失败)
- /api/vote 的扣额度从 userId_date 改用 dq.id 主键 update, 双保险
- 三个路由的 startOfDay 重复实现合并到 lib/date-utils

E2E 验证 (curl):
  登录 → 投 5 给 002 → 余 5 ✓
  投 3 给 003 → 余 2 / totalVotes 8 ✓
  /api/me supports 反映 002+003 真实 voteTotal ✓
  超额 (5 票 余 2) → 409 QUOTA_EXHAUSTED ✓
  /api/ranking 票数实时反映 DB ✓

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 17:32:38 +08:00

28 lines
1.1 KiB
TypeScript

/**
* 日期工具 · 跨 API 共享, 避免日期 / 时区 相关 bug。
*
* 为什么用 UTC 而不是本地时区?
* MySQL @db.Date 列存储时只保留日期部分。如果用 setHours(0,0,0,0) 取本地午夜,
* JS Date 对应的 UTC 时刻在 +8 区是当日 16:00 UTC, 经过 Prisma 序列化 + MySQL
* TZ 转换后, 存的"日期"和读的"日期"可能差一天, 导致 dailyQuota 复合键 (userId, date)
* 找不到刚 upsert 的行 (P2025)。
*
* 用 setUTCHours(0,0,0,0) 拿到的是 "今天的 UTC 0 点", 无论服务器 / 客户端 TZ,
* 存取一致。代价: 中国用户在 08:00 之前(UTC 0 点之前)算"昨天"。对于按日额度场景
* 足够好(短期可接受, 长期需要按"运营日"显式划分)。
*/
export function startOfUtcDay(d = new Date()): Date {
const x = new Date(d);
x.setUTCHours(0, 0, 0, 0);
return x;
}
/** 判断两个 Date 是否同一个 UTC 日期 */
export function isSameUtcDay(a: Date, b: Date): boolean {
return (
a.getUTCFullYear() === b.getUTCFullYear() &&
a.getUTCMonth() === b.getUTCMonth() &&
a.getUTCDate() === b.getUTCDate()
);
}