// 后端投票规则验证(不污染生产数据,所有写操作都在事务里 rollback) import { PrismaClient, Prisma } from "@prisma/client"; const prisma = new PrismaClient({ log: ["error"] }); const PASS = "\x1b[32mPASS\x1b[0m"; const FAIL = "\x1b[31mFAIL\x1b[0m"; let results = []; function record(name, ok, detail = "") { results.push({ name, ok, detail }); console.log(` [${ok ? PASS : FAIL}] ${name}${detail ? " -- " + detail : ""}`); } async function main() { console.log("=== 后端投票规则验证(事务回滚,不留痕)===\n"); // 取一个真实用户(测试数据中的最大投票用户) const sample = await prisma.$queryRaw` SELECT user_id, COUNT(*) AS c FROM votes GROUP BY user_id ORDER BY c DESC LIMIT 1 `; if (sample.length === 0) { console.log("[note] votes 表为空,跳过有交互的测试"); return; } const sampleUserId = BigInt(sample[0].user_id); const sampleVotes = await prisma.vote.findMany({ where: { userId: sampleUserId }, select: { artistId: true }, }); const sampleArtist = sampleVotes[0].artistId; console.log( `测试样本:user=${sampleUserId} 已投艺人=[${sampleVotes .map((v) => v.artistId) .join(",")}]\n`, ); // 测试 1:DB unique 约束生效 —— 重复 (userId, artistId) INSERT 应失败 P2002 console.log("[测试 1] DB unique 约束:重复 (userId, artistId) 必被拒"); try { await prisma.$transaction( async (tx) => { await tx.vote.create({ data: { userId: sampleUserId, artistId: sampleArtist, count: 1, source: "QUOTA", }, }); throw new Error("ROLLBACK_AFTER_TEST"); // 强制回滚 }, { timeout: 10000 }, ); record("unique 约束阻挡重复投票", false, "INSERT 居然成功了"); } catch (e) { if ( e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2002" ) { record("unique 约束阻挡重复投票", true, "P2002 unique constraint"); } else if (e.message === "ROLLBACK_AFTER_TEST") { record( "unique 约束阻挡重复投票", false, "INSERT 居然成功 = 没有 unique 约束!", ); } else { record("unique 约束阻挡重复投票", false, `异常: ${e.message}`); } } // 测试 2:不同艺人 INSERT 应该成功(在事务内回滚,不留痕) console.log( "\n[测试 2] 不同艺人投票不会被 unique 阻挡(事务内验证后回滚)", ); const candidateArtist = await prisma.artist.findFirst({ where: { id: { notIn: sampleVotes.map((v) => v.artistId) }, status: "ACTIVE", }, select: { id: true }, }); if (!candidateArtist) { record("跨艺人投票", false, "找不到该用户未投过的艺人样本"); } else { try { await prisma.$transaction( async (tx) => { await tx.vote.create({ data: { userId: sampleUserId, artistId: candidateArtist.id, count: 1, source: "QUOTA", }, }); throw new Error("ROLLBACK_AFTER_TEST"); }, { timeout: 10000 }, ); record("跨艺人投票", false, "事务未回滚"); } catch (e) { if (e.message === "ROLLBACK_AFTER_TEST") { record( "跨艺人投票", true, `允许投给新艺人 ${candidateArtist.id},事务已回滚`, ); } else { record("跨艺人投票", false, `异常: ${e.message}`); } } } // 测试 3:终身额度 12 票上限(由 /api/vote 应用层校验,DB 不强制) // 验证:对所有用户跑 SELECT COUNT(*) < 12 是 true,确认目前没人超额 console.log("\n[测试 3] 终身额度上限 12 票 - 数据合规性"); const overflows = await prisma.$queryRaw` SELECT user_id, COUNT(*) AS c FROM votes GROUP BY user_id HAVING c > 12 `; if (overflows.length === 0) { record( "现存数据无超额用户", true, "12 票上限对所有现存用户都成立(应用层会兜底)", ); } else { record( "现存数据无超额用户", false, `${overflows.length} 个用户已超过 12 票:${overflows .map((r) => `user=${r.user_id} c=${r.c}`) .join("; ")}`, ); } // 测试 4:回归确认 /api/me 用的 vote 关键查询能返回 votedArtists 列表 console.log("\n[测试 4] /api/me votedArtists 查询正确性"); const votedList = await prisma.vote.findMany({ where: { userId: sampleUserId }, select: { artistId: true, createdAt: true }, orderBy: { createdAt: "asc" }, }); const isAsc = votedList.every( (v, i) => i === 0 || votedList[i - 1].createdAt.getTime() <= v.createdAt.getTime(), ); record( "votedArtists 按 createdAt 升序", isAsc, `共 ${votedList.length} 条`, ); // 测试 5:旧 daily_quota / fan_supports 数据仍可读(向后兼容,不报错) console.log("\n[测试 5] 旧数据兼容性"); try { const dqCount = await prisma.dailyQuota.count(); const fsCount = await prisma.fanSupport.count(); record("旧 DailyQuota / FanSupport 仍可读", true, `dq=${dqCount} fs=${fsCount}`); } catch (e) { record("旧 DailyQuota / FanSupport 仍可读", false, e.message); } // 汇总 console.log("\n=== 汇总 ==="); const passed = results.filter((r) => r.ok).length; const failed = results.length - passed; console.log(`通过 ${passed} / 共 ${results.length}${failed ? ` 失败 ${failed}` : ""}`); await prisma.$disconnect(); if (failed) process.exit(1); } main().catch(async (e) => { console.error("\n[ABORT]", e); await prisma.$disconnect(); process.exit(1); });