mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
fix: route wallet card redemption through api
This commit is contained in:
@@ -4,7 +4,7 @@ import { useTransition } from "react";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { CreditCard, Gift } from "lucide-react";
|
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 { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -15,6 +15,32 @@ function money(value: number) {
|
|||||||
return `¥${value.toFixed(2)}`;
|
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() {
|
export function WalletActions() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [rechargePending, startRecharge] = useTransition();
|
const [rechargePending, startRecharge] = useTransition();
|
||||||
@@ -64,7 +90,7 @@ export function WalletActions() {
|
|||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
startRedeem(async () => {
|
startRedeem(async () => {
|
||||||
try {
|
try {
|
||||||
const result = await redeemWalletCard(formData);
|
const result = await redeemWalletCardByApi(formData);
|
||||||
form.reset();
|
form.reset();
|
||||||
if (result.type === "BALANCE") {
|
if (result.type === "BALANCE") {
|
||||||
window.dispatchEvent(new CustomEvent(WALLET_BALANCE_UPDATED_EVENT, {
|
window.dispatchEvent(new CustomEvent(WALLET_BALANCE_UPDATED_EVENT, {
|
||||||
|
|||||||
50
src/app/api/wallet/redeem-card/route.ts
Normal file
50
src/app/api/wallet/redeem-card/route.ts
Normal 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: "充值卡兑换失败" });
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user