fix: route wallet card redemption through api

This commit is contained in:
JetSprow
2026-05-01 03:50:05 +10:00
parent 0c8b402f3e
commit b2a50514a4
2 changed files with 78 additions and 2 deletions

View File

@@ -4,7 +4,7 @@ import { useTransition } from "react";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { CreditCard, Gift } from "lucide-react";
import { createWalletRecharge, redeemWalletCard } from "@/actions/user/wallet";
import { createWalletRecharge } from "@/actions/user/wallet";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -15,6 +15,32 @@ function money(value: number) {
return `¥${value.toFixed(2)}`;
}
type RedeemCardResult =
| {
type: "BALANCE";
amount: number;
balanceAfter: number;
}
| {
type: "PLAN";
planName: string;
};
async function redeemWalletCardByApi(formData: FormData): Promise<RedeemCardResult> {
const response = await fetch("/api/wallet/redeem-card", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code: String(formData.get("code") ?? "") }),
});
const payload = await response.json().catch(() => null) as { error?: string } | RedeemCardResult | null;
if (!response.ok) {
throw new Error(payload && "error" in payload && payload.error ? payload.error : "充值卡兑换失败");
}
return payload as RedeemCardResult;
}
export function WalletActions() {
const router = useRouter();
const [rechargePending, startRecharge] = useTransition();
@@ -64,7 +90,7 @@ export function WalletActions() {
const formData = new FormData(form);
startRedeem(async () => {
try {
const result = await redeemWalletCard(formData);
const result = await redeemWalletCardByApi(formData);
form.reset();
if (result.type === "BALANCE") {
window.dispatchEvent(new CustomEvent(WALLET_BALANCE_UPDATED_EVENT, {

View File

@@ -0,0 +1,50 @@
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { jsonError, jsonOk } from "@/lib/api-response";
import { rateLimit } from "@/lib/rate-limit";
import { getActiveSession } from "@/lib/require-auth";
import { getActiveSubscriptionRiskRestriction } from "@/services/subscription-risk-review";
import { redeemRechargeCard } from "@/services/wallet";
const redeemCardSchema = z.object({
code: z.string().trim().min(4, "请输入充值卡卡密"),
});
export async function POST(req: Request) {
try {
const session = await getActiveSession();
if (!session) {
return jsonError("未登录", { status: 401 });
}
if (session.user.role !== "ADMIN") {
const restriction = await getActiveSubscriptionRiskRestriction(session.user.id);
if (restriction) {
return jsonError("账户存在未处理的订阅风控限制,请先新建工单联系客服", { status: 403 });
}
}
const { success, remaining } = await rateLimit(
`ratelimit:redeem-card:${session.user.id}`,
10,
60,
);
if (!success) {
return jsonError("兑换请求过于频繁,请稍后再试", {
status: 429,
headers: { "X-RateLimit-Remaining": String(remaining) },
});
}
const payload = redeemCardSchema.parse(await req.json());
const result = await redeemRechargeCard(session.user.id, payload.code);
revalidatePath("/wallet");
revalidatePath("/subscriptions");
revalidatePath("/dashboard");
return jsonOk(result);
} catch (error) {
return jsonError(error, { fallback: "充值卡兑换失败" });
}
}