feat: polish wallet recharge cards

This commit is contained in:
JetSprow
2026-05-01 03:41:30 +10:00
parent 035ac9266a
commit 0c8b402f3e
15 changed files with 774 additions and 103 deletions

View File

@@ -1,6 +1,7 @@
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";
@@ -185,6 +186,38 @@ function getPlanCardGenerationLimit(planType: "PROXY" | "STREAMING", availabilit
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)}`;
@@ -379,11 +412,12 @@ export async function createRechargeCards(input: {
});
}
export async function redeemRechargeCard(userId: string, rawCode: string) {
export async function redeemRechargeCard(userId: string, rawCode: string): Promise<RedeemRechargeCardResult> {
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: {
@@ -398,27 +432,45 @@ export async function redeemRechargeCard(userId: string, rawCode: string) {
});
if (!card) throw new Error("充值卡不存在");
if (card.status !== "UNUSED") throw new Error("这张充值卡已使用或已停用");
if (card.expiresAt && card.expiresAt < new Date()) {
await tx.rechargeCard.update({
where: { id: card.id },
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("这张充值卡已过期");
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("余额充值卡金额无效");
}
await creditWallet(tx, {
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({
@@ -445,17 +497,9 @@ export async function redeemRechargeCard(userId: string, rawCode: string) {
});
await provisionSubscriptionWithDb(order, tx);
result = { type: "PLAN", planName: card.plan.name };
}
await tx.rechargeCard.update({
where: { id: card.id },
data: {
status: "REDEEMED",
redeemedById: userId,
redeemedAt: new Date(),
},
});
await createNotification(
{
userId,
@@ -471,7 +515,7 @@ export async function redeemRechargeCard(userId: string, rawCode: string) {
tx,
);
return card.type;
return result;
});
}