feat: 优化抽取实现

This commit is contained in:
2026-01-09 11:10:15 +08:00
parent b505c48a0f
commit 25d3865a6c
3 changed files with 84 additions and 24 deletions

View File

@@ -6,6 +6,7 @@ import { redPacketDraws } from "@/app/server/schema/red-packet-draws"
import { eq, sql } from "drizzle-orm" import { eq, sql } from "drizzle-orm"
import { randomUUID } from "crypto" import { randomUUID } from "crypto"
import { formatAmount } from "@/lib/format-amount" import { formatAmount } from "@/lib/format-amount"
import { formatAmountFromInt, parseAmountToInt, randomBigInt } from "@/lib/utils"
type DrawResult = type DrawResult =
| { ok: true; amount: string } | { ok: true; amount: string }
@@ -65,26 +66,65 @@ export async function drawRedPacket(
const precision = packet.currencyPrecision; const precision = packet.currencyPrecision;
const rule = packet.rule const rule = packet.rule
let amount: string let amount: string
/* 5⃣ 金额计算(关键) */ /* 5⃣ 金额计算(关键) */
if (rule.type === "fixed") { if (rule.type === "fixed") {
amount = formatAmount(rule.singleAmount, precision) amount = formatAmount(rule.singleAmount, precision)
} else if (rule.type === "random") { } else if (rule.type === "random") {
const min = Number(rule.min) const minInt = parseAmountToInt(rule.min, precision)
const max = Number(rule.max) const maxInt = parseAmountToInt(rule.max, precision)
amount = randomByPrecision(min, 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 { } else {
// 拼手气 // 拼手气
const total = Number(rule.totalAmount) const totalInt = parseAmountToInt(rule.totalAmount, precision)
const remain = total - Number(usedAmount) 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) { if (remainingCount === 1) {
amount = remain.toFixed(precision) amountInt = remainInt
} else { } else {
const max = remain / remainingCount * 2 const count = BigInt(remainingCount)
amount = randomByPrecision(0.01, max, precision) 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⃣ 写入抽取记录 */ /* 6⃣ 写入抽取记录 */
@@ -97,18 +137,4 @@ export async function drawRedPacket(
return { ok: true, amount } 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)
}

View File

@@ -4,3 +4,37 @@ import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) 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
}

View File

@@ -1,6 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2017", "target": "ES2020",
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,