diff --git a/tonesc-red-packet/app/server/actions/draw-red-packet.ts b/tonesc-red-packet/app/server/actions/draw-red-packet.ts index 039a142..be05c58 100644 --- a/tonesc-red-packet/app/server/actions/draw-red-packet.ts +++ b/tonesc-red-packet/app/server/actions/draw-red-packet.ts @@ -6,6 +6,7 @@ import { redPacketDraws } from "@/app/server/schema/red-packet-draws" import { eq, sql } from "drizzle-orm" import { randomUUID } from "crypto" import { formatAmount } from "@/lib/format-amount" +import { formatAmountFromInt, parseAmountToInt, randomBigInt } from "@/lib/utils" type DrawResult = | { ok: true; amount: string } @@ -65,26 +66,65 @@ export async function drawRedPacket( const precision = packet.currencyPrecision; const rule = packet.rule + let amount: string /* 5️⃣ 金额计算(关键) */ if (rule.type === "fixed") { amount = formatAmount(rule.singleAmount, precision) } else if (rule.type === "random") { - const min = Number(rule.min) - const max = Number(rule.max) - amount = randomByPrecision(min, max, precision) + const minInt = parseAmountToInt(rule.min, precision) + const maxInt = parseAmountToInt(rule.max, precision) + + if (minInt < 0n || maxInt < 0n) { + throw new Error("Invalid min/max: negative amount") + } + + if (minInt > maxInt) { + throw new Error("Invalid min/max: min > max") + } + + const amountInt = randomBigInt(minInt, maxInt) + + amount = formatAmountFromInt(amountInt, precision) } else { // 拼手气 - const total = Number(rule.totalAmount) - const remain = total - Number(usedAmount) + const totalInt = parseAmountToInt(rule.totalAmount, precision) + const usedInt = parseAmountToInt(usedAmount, precision) + + const remainInt = totalInt - usedInt + if (remainInt < 0n) { + throw new Error("Invalid state: remaining amount < 0") + } + + let amountInt: bigint if (remainingCount === 1) { - amount = remain.toFixed(precision) + amountInt = remainInt } else { - const max = remain / remainingCount * 2 - amount = randomByPrecision(0.01, max, precision) + const count = BigInt(remainingCount) + const MIN_UNIT = 1n // 1 分 / 1 sat / 1 最小单位 + + // max = (remain / remainingCount) * 2 + const maxInt = (remainInt / count) * 2n + + if (maxInt < MIN_UNIT) { + // 剩余过小,防御性兜底 + amountInt = MIN_UNIT + } else { + amountInt = randomBigInt(MIN_UNIT, maxInt) + } } + + if (amountInt < 0n) { + throw new Error("Invalid amount: negative") + } + + if (amountInt > remainInt) { + throw new Error("Invalid amount: exceeds remaining") + } + + amount = formatAmountFromInt(amountInt, precision) } /* 6️⃣ 写入抽取记录 */ @@ -97,18 +137,4 @@ export async function drawRedPacket( return { ok: true, amount } }) -} - -function randomByPrecision( - min: number, - max: number, - precision: number -) { - const factor = Math.pow(10, precision) - const value = - Math.floor( - (Math.random() * (max - min) + min) * factor - ) / factor - - return value.toFixed(precision) -} +} \ No newline at end of file diff --git a/tonesc-red-packet/lib/utils.ts b/tonesc-red-packet/lib/utils.ts index bd0c391..8acf3b9 100644 --- a/tonesc-red-packet/lib/utils.ts +++ b/tonesc-red-packet/lib/utils.ts @@ -4,3 +4,37 @@ import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } + +export function parseAmountToInt(amountStr: string, precision: number): bigint { + // 例:"123.45", precision=2 → 12345n + // 例:"0.00001234", precision=8 → 1234n + + const [intPart, fracPart = ""] = amountStr.split(".") + + if (fracPart.length > precision) { + throw new Error(`Amount ${amountStr} exceeds precision ${precision}`) + } + + const paddedFrac = fracPart.padEnd(precision, "0") + const normalized = intPart + paddedFrac + + return BigInt(normalized) +} + +export function formatAmountFromInt(value: bigint, precision: number): string { + const s = value.toString().padStart(precision + 1, "0") + + if (precision === 0) return s + + const i = s.length - precision + return `${s.slice(0, i)}.${s.slice(i)}` +} + +export function randomBigInt(min: bigint, max: bigint): bigint { + if (max < min) throw new Error("max < min") + + const range = max - min + 1n + const rand = BigInt(Math.floor(Math.random() * Number(range))) + + return min + rand +} \ No newline at end of file diff --git a/tonesc-red-packet/tsconfig.json b/tonesc-red-packet/tsconfig.json index 3a13f90..15c7b97 100644 --- a/tonesc-red-packet/tsconfig.json +++ b/tonesc-red-packet/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2017", + "target": "ES2020", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true,