import crypto from "crypto"; import { Prisma } from "@prisma/client"; import { prisma, type DbClient } from "@/lib/prisma"; import { formatDate } from "@/lib/utils"; import { createNotification } from "@/services/notifications"; import { provisionSubscriptionWithDb } from "@/services/provision"; import { getPaymentProviderName } from "@/services/payment/catalog"; import { getPlanAvailability, type PlanAvailability } from "@/services/plan-availability"; const MONEY_SCALE = 100; function toMoneyDecimal(value: number | string | Prisma.Decimal) { const amount = value instanceof Prisma.Decimal ? value.toNumber() : Number(value); if (!Number.isFinite(amount) || amount <= 0) { throw new Error("金额必须大于 0"); } return new Prisma.Decimal(Math.round(amount * MONEY_SCALE) / MONEY_SCALE); } function generateRedeemCode(prefix = "JB") { const random = crypto.randomUUID().replace(/-/g, "").slice(0, 18).toUpperCase(); return `${prefix}-${random.slice(0, 6)}-${random.slice(6, 12)}-${random.slice(12, 18)}`; } async function createUniqueRedeemCode(db: DbClient, prefix?: string) { for (let index = 0; index < 8; index += 1) { const code = generateRedeemCode(prefix); const existing = await db.rechargeCard.findUnique({ where: { code }, select: { id: true }, }); if (!existing) return code; } throw new Error("充值卡卡密生成失败,请重试"); } export async function getOrCreateWallet(db: DbClient, userId: string) { return db.walletAccount.upsert({ where: { userId }, update: {}, create: { userId, balance: 0 }, }); } export async function getWalletBalance(userId: string) { const wallet = await getOrCreateWallet(prisma, userId); return Number(wallet.balance); } export async function creditWallet( db: DbClient, input: { userId: string; amount: number | string | Prisma.Decimal; type: "BALANCE_RECHARGE" | "CARD_REDEEM" | "ADMIN_ADJUST" | "REFUND" | "SUBSCRIPTION_TRANSFER_REFUND"; description?: string; orderId?: string | null; rechargeOrderId?: string | null; rechargeCardId?: string | null; metadata?: Prisma.InputJsonValue; }, ) { const amount = toMoneyDecimal(input.amount); await getOrCreateWallet(db, input.userId); const wallet = await db.walletAccount.update({ where: { userId: input.userId }, data: { balance: { increment: amount } }, }); await db.walletTransaction.create({ data: { walletId: wallet.id, userId: input.userId, type: input.type, amount, balanceAfter: wallet.balance, description: input.description ?? null, orderId: input.orderId ?? null, rechargeOrderId: input.rechargeOrderId ?? null, rechargeCardId: input.rechargeCardId ?? null, metadata: input.metadata ?? undefined, }, }); return wallet; } export async function debitWallet( db: DbClient, input: { userId: string; amount: number | string | Prisma.Decimal; type: "BALANCE_PAYMENT" | "SUBSCRIPTION_TRANSFER_FEE"; description?: string; orderId?: string | null; metadata?: Prisma.InputJsonValue; }, ) { const amount = toMoneyDecimal(input.amount); await getOrCreateWallet(db, input.userId); const claimed = await db.walletAccount.updateMany({ where: { userId: input.userId, balance: { gte: amount }, }, data: { balance: { decrement: amount } }, }); if (claimed.count === 0) { throw new Error("余额不足,请先充值后再操作"); } const wallet = await db.walletAccount.findUniqueOrThrow({ where: { userId: input.userId }, }); await db.walletTransaction.create({ data: { walletId: wallet.id, userId: input.userId, type: input.type, amount: amount.negated(), balanceAfter: wallet.balance, description: input.description ?? "余额扣费", orderId: input.orderId ?? null, metadata: input.metadata ?? undefined, }, }); return wallet; } export async function debitWalletForOrder( db: DbClient, input: { userId: string; orderId: string; amount: number | string | Prisma.Decimal; description?: string; }, ) { const amount = toMoneyDecimal(input.amount); return debitWallet(db, { userId: input.userId, orderId: input.orderId, amount, type: "BALANCE_PAYMENT", description: input.description ?? "余额支付订单", }); } type PlanCardSnapshot = { type: "PROXY" | "STREAMING"; nodeId: string | null; inboundId: string | null; fixedTrafficGb: number | null; minTrafficGb: number | null; totalTrafficGb: number | null; inboundOptions?: Array<{ inboundId: string; inbound: { isActive: boolean; serverId: string } }>; }; function resolvePlanCardConfig(card: { selectedInboundId: string | null; trafficGb: number | null; plan: PlanCardSnapshot; }) { const plan = card.plan; if (plan.type === "STREAMING") { return { selectedInboundId: null, trafficGb: null }; } const selectedInboundId = card.selectedInboundId ?? plan.inboundId ?? card.plan.inboundOptions?.find( (item) => item.inbound.isActive && (!plan.nodeId || item.inbound.serverId === plan.nodeId), )?.inboundId ?? null; const trafficGb = card.trafficGb ?? plan.fixedTrafficGb ?? plan.minTrafficGb ?? plan.totalTrafficGb ?? null; if (!selectedInboundId) { throw new Error("套餐充值卡缺少可用线路,无法自动开通代理套餐"); } if (!trafficGb || trafficGb <= 0) { throw new Error("套餐充值卡缺少可用流量配置"); } return { selectedInboundId, trafficGb }; } function getPlanCardGenerationLimit(planType: "PROXY" | "STREAMING", availability: PlanAvailability) { const limits: number[] = []; if (availability.remainingByPlanLimit != null) { limits.push(availability.remainingByPlanLimit); } if (planType === "STREAMING" && availability.remainingByServiceCapacity != null) { limits.push(availability.remainingByServiceCapacity); } return limits.length > 0 ? Math.min(...limits) : null; } function getUnavailableRechargeCardMessage(card: { status: "UNUSED" | "REDEEMED" | "EXPIRED" | "DISABLED"; redeemedAt: Date | null; expiresAt: Date | null; }) { if (card.status === "REDEEMED") { return card.redeemedAt ? `这张充值卡已在 ${formatDate(card.redeemedAt)} 兑换,不能重复使用。` : "这张充值卡已兑换,不能重复使用。"; } if (card.status === "EXPIRED") { return card.expiresAt ? `这张充值卡已于 ${formatDate(card.expiresAt)} 过期。` : "这张充值卡已过期。"; } if (card.status === "DISABLED") { return "这张充值卡已停用,请联系管理员处理。"; } return "这张充值卡当前不可兑换。"; } export type RedeemRechargeCardResult = | { type: "BALANCE"; amount: number; balanceAfter: number; } | { type: "PLAN"; planName: string; }; export async function payOrderWithWallet(orderId: string, userId: string) { const tradeNo = `BAL-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`; return prisma.$transaction(async (tx) => { const config = await tx.paymentConfig.findUnique({ where: { provider: "balance" }, select: { enabled: true }, }); if (config && !config.enabled) { throw new Error("余额支付当前已关闭"); } const order = await tx.order.findUnique({ where: { id: orderId }, include: { plan: true, user: true }, }); if (!order || order.userId !== userId) { throw new Error("订单不存在"); } if (order.status !== "PENDING") { throw new Error("这笔订单已经不在待支付状态"); } const amount = toMoneyDecimal(order.amount); await debitWalletForOrder(tx, { userId, orderId, amount, description: `余额支付 ${order.plan.name}`, }); const paidOrder = await tx.order.update({ where: { id: orderId }, data: { status: "PAID", paymentMethod: "balance", paymentRef: tradeNo, paymentUrl: null, tradeNo, expireAt: null, note: null, }, include: { plan: true, user: true }, }); const affectedNodeIds = await provisionSubscriptionWithDb(paidOrder, tx); return { tradeNo, affectedNodeIds }; }); } export async function createWalletRechargeOrder(userId: string, amountValue: number) { const amount = toMoneyDecimal(amountValue); return prisma.walletRechargeOrder.create({ data: { userId, amount }, }); } export async function processWalletRechargeSuccess( tradeNo: string, paidAmount: number, paymentRef?: string, ) { if (!Number.isFinite(paidAmount) || paidAmount <= 0) { return { processed: false, finalStatus: null as null | "PENDING" | "PAID" }; } const rechargeOrder = await prisma.walletRechargeOrder.findUnique({ where: { tradeNo }, }); if (!rechargeOrder) { return { processed: false, finalStatus: null as null | "PENDING" | "PAID" }; } const expectedAmount = Number(rechargeOrder.amount); if (Math.abs(expectedAmount - paidAmount) > 0.01) { throw new Error("支付金额与充值金额不一致"); } return prisma.$transaction(async (tx) => { const claimed = await tx.walletRechargeOrder.updateMany({ where: { id: rechargeOrder.id, status: "PENDING" }, data: { status: "PAID", paymentRef: paymentRef ?? rechargeOrder.paymentRef, note: null, }, }); if (claimed.count === 0) { const current = await tx.walletRechargeOrder.findUnique({ where: { id: rechargeOrder.id }, select: { status: true }, }); return { processed: false, finalStatus: current?.status ?? null }; } await creditWallet(tx, { userId: rechargeOrder.userId, amount: rechargeOrder.amount, type: "BALANCE_RECHARGE", description: `${getPaymentProviderName(rechargeOrder.paymentMethod ?? "")} 余额充值`, rechargeOrderId: rechargeOrder.id, metadata: { tradeNo }, }); await createNotification( { userId: rechargeOrder.userId, type: "ORDER", level: "SUCCESS", title: "余额充值成功", body: `已充值 ¥${expectedAmount.toFixed(2)} 到账户余额。`, link: "/wallet", dedupeKey: `wallet-recharge:${rechargeOrder.id}`, }, tx, ); return { processed: true, finalStatus: "PAID" as const }; }); } export async function createRechargeCards(input: { createdById: string; type: "BALANCE" | "PLAN"; quantity: number; balanceAmount?: number; planId?: string; batchName?: string | null; expiresAt?: Date | null; }) { const quantity = Math.min(Math.max(input.quantity, 1), 200); return prisma.$transaction(async (tx) => { let plan: | (NonNullable>> & { inboundOptions: Array<{ inboundId: string; inbound: { isActive: boolean; serverId: string } }>; }) | null = null; if (input.type === "PLAN") { if (!input.planId) throw new Error("请选择套餐"); plan = await tx.subscriptionPlan.findUnique({ where: { id: input.planId }, include: { inboundOptions: { include: { inbound: { select: { isActive: true, serverId: true } } }, }, }, }); if (!plan) throw new Error("套餐不存在"); if (!plan.isActive) throw new Error("只能为上架中的套餐生成兑换卡"); const availability = await getPlanAvailability(plan, { db: tx }); const remaining = getPlanCardGenerationLimit(plan.type, availability); if (remaining != null && quantity > remaining) { throw new Error(`套餐剩余库存不足,当前最多可生成 ${remaining} 张`); } } const cards = []; for (let index = 0; index < quantity; index += 1) { const code = await createUniqueRedeemCode(tx, input.type === "PLAN" ? "JP" : "JB"); cards.push( await tx.rechargeCard.create({ data: { code, type: input.type, balanceAmount: input.type === "BALANCE" ? toMoneyDecimal(input.balanceAmount ?? 0) : null, planId: input.type === "PLAN" ? input.planId! : null, trafficGb: input.type === "PLAN" && plan?.type === "PROXY" ? plan.fixedTrafficGb ?? plan.minTrafficGb ?? plan.totalTrafficGb : null, selectedInboundId: input.type === "PLAN" && plan?.type === "PROXY" ? plan.inboundId ?? plan.inboundOptions.find( (item) => item.inbound.isActive && (!plan?.nodeId || item.inbound.serverId === plan.nodeId), )?.inboundId ?? null : null, batchName: input.batchName || null, expiresAt: input.expiresAt ?? null, createdById: input.createdById, }, }), ); } return cards; }); } export async function redeemRechargeCard(userId: string, rawCode: string): Promise { const code = rawCode.trim().toUpperCase(); if (!code) throw new Error("请输入充值卡卡密"); return prisma.$transaction(async (tx) => { const redeemedAt = new Date(); const card = await tx.rechargeCard.findUnique({ where: { code }, include: { plan: { include: { inboundOptions: { include: { inbound: { select: { isActive: true, serverId: true } } }, }, }, }, }, }); if (!card) throw new Error("充值卡不存在"); if (card.status !== "UNUSED") throw new Error(getUnavailableRechargeCardMessage(card)); if (card.expiresAt && card.expiresAt < redeemedAt) { await tx.rechargeCard.updateMany({ where: { id: card.id, status: "UNUSED" }, data: { status: "EXPIRED" }, }); throw new Error(getUnavailableRechargeCardMessage({ ...card, status: "EXPIRED" })); } const claimed = await tx.rechargeCard.updateMany({ where: { id: card.id, status: "UNUSED" }, data: { status: "REDEEMED", redeemedById: userId, redeemedAt, }, }); if (claimed.count === 0) { throw new Error("这张充值卡刚刚已被兑换,请刷新后查看最新状态。"); } let result: RedeemRechargeCardResult; if (card.type === "BALANCE") { if (!card.balanceAmount || Number(card.balanceAmount) <= 0) { throw new Error("余额充值卡金额无效"); } const wallet = await creditWallet(tx, { userId, amount: card.balanceAmount, type: "CARD_REDEEM", description: "兑换余额充值卡", rechargeCardId: card.id, }); result = { type: "BALANCE", amount: Number(card.balanceAmount), balanceAfter: Number(wallet.balance), }; } else { if (!card.plan) throw new Error("套餐充值卡绑定的套餐不存在"); const { selectedInboundId, trafficGb } = resolvePlanCardConfig({ selectedInboundId: card.selectedInboundId, trafficGb: card.trafficGb, plan: card.plan, }); const order = await tx.order.create({ data: { userId, planId: card.plan.id, kind: "NEW_PURCHASE", selectedInboundId, trafficGb, amount: 0, subtotalAmount: 0, discountAmount: 0, status: "PAID", paymentMethod: "recharge_card", paymentRef: card.code, tradeNo: `CARD-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`, }, include: { plan: true, user: true }, }); await provisionSubscriptionWithDb(order, tx); result = { type: "PLAN", planName: card.plan.name }; } await createNotification( { userId, type: "ORDER", level: "SUCCESS", title: card.type === "BALANCE" ? "余额充值卡兑换成功" : "套餐充值卡兑换成功", body: card.type === "BALANCE" ? `已充值 ¥${Number(card.balanceAmount).toFixed(2)} 到账户余额。` : `${card.plan?.name ?? "套餐"} 已激活。`, link: card.type === "BALANCE" ? "/wallet" : "/subscriptions", dedupeKey: `recharge-card-redeemed:${card.id}`, }, tx, ); return result; }); } export async function expireOldRechargeCards() { await prisma.rechargeCard.updateMany({ where: { status: "UNUSED", expiresAt: { lt: new Date() }, }, data: { status: "EXPIRED" }, }); }