mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
feat: add admin wallet recharge order actions
This commit is contained in:
@@ -384,7 +384,7 @@ SMTP 配置在后台“系统设置”中完成,密码会加密保存在数据
|
|||||||
|
|
||||||
## 钱包、充值卡与套餐 Push
|
## 钱包、充值卡与套餐 Push
|
||||||
|
|
||||||
钱包在用户端 `/wallet` 使用。用户可以创建余额充值订单,选择除余额支付外的外部支付方式完成充值;充值订单可取消,管理员可在订单页的“充值订单”标签查看。普通商品订单支持余额支付,余额不足时会给出可读错误。
|
钱包在用户端 `/wallet` 使用。用户可以创建余额充值订单,选择除余额支付外的外部支付方式完成充值;充值订单可取消,管理员可在订单页的“充值订单”标签查看、手动确认入账、取消或删除记录。普通商品订单支持余额支付,余额不足时会给出可读错误。
|
||||||
|
|
||||||
充值卡在后台“商业配置”中生成:
|
充值卡在后台“商业配置”中生成:
|
||||||
|
|
||||||
|
|||||||
@@ -367,6 +367,12 @@ Server Actions 是后台和用户端写操作的主要入口。它们不是公
|
|||||||
- `updateOrderReview(...)`:更新风控/复核状态。
|
- `updateOrderReview(...)`:更新风控/复核状态。
|
||||||
- `batchOrderOperation(formData)`:批量操作订单。
|
- `batchOrderOperation(formData)`:批量操作订单。
|
||||||
|
|
||||||
|
#### 充值订单:`src/actions/admin/recharge-orders.ts`
|
||||||
|
|
||||||
|
- `confirmAdminWalletRecharge(id)`:手动确认待支付充值订单,立即给用户钱包入账。
|
||||||
|
- `cancelAdminWalletRecharge(id)`:取消待支付充值订单,不改变钱包余额。
|
||||||
|
- `deleteAdminWalletRecharge(id)`:删除充值订单记录;已入账订单只删除记录,不回滚余额,钱包流水保留。
|
||||||
|
|
||||||
#### 其他管理动作
|
#### 其他管理动作
|
||||||
|
|
||||||
- 用户:`src/actions/admin/users.ts`
|
- 用户:`src/actions/admin/users.ts`
|
||||||
|
|||||||
153
src/actions/admin/recharge-orders.ts
Normal file
153
src/actions/admin/recharge-orders.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { requireAdmin } from "@/lib/require-auth";
|
||||||
|
import { actorFromSession, recordAuditLog } from "@/services/audit";
|
||||||
|
import { processWalletRechargeOrderSuccess } from "@/services/wallet";
|
||||||
|
|
||||||
|
const rechargeOrderIdSchema = z.string().trim().min(1, "充值订单不存在");
|
||||||
|
|
||||||
|
function manualTradeNo(orderId: string) {
|
||||||
|
return `WR-MANUAL-${Date.now()}-${orderId.slice(0, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function revalidateRechargeOrderViews(orderId: string) {
|
||||||
|
revalidatePath("/admin/orders");
|
||||||
|
revalidatePath("/wallet");
|
||||||
|
revalidatePath(`/wallet/recharge/${orderId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function confirmAdminWalletRecharge(rechargeOrderId: string) {
|
||||||
|
const session = await requireAdmin();
|
||||||
|
const id = rechargeOrderIdSchema.parse(rechargeOrderId);
|
||||||
|
const recharge = await prisma.walletRechargeOrder.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: { user: { select: { email: true, name: true } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!recharge) {
|
||||||
|
throw new Error("充值订单不存在或已被删除");
|
||||||
|
}
|
||||||
|
if (recharge.status !== "PENDING") {
|
||||||
|
throw new Error("只能确认待支付充值订单");
|
||||||
|
}
|
||||||
|
|
||||||
|
const tradeNo = recharge.tradeNo ?? manualTradeNo(recharge.id);
|
||||||
|
const paymentRef = recharge.paymentRef ?? `manual:${session.user.id}:${Date.now()}`;
|
||||||
|
const result = await processWalletRechargeOrderSuccess({
|
||||||
|
rechargeOrderId: recharge.id,
|
||||||
|
paidAmount: Number(recharge.amount),
|
||||||
|
paymentMethod: recharge.paymentMethod ?? "manual",
|
||||||
|
paymentRef,
|
||||||
|
tradeNo,
|
||||||
|
description: "管理员手动确认余额充值",
|
||||||
|
metadata: {
|
||||||
|
manual: true,
|
||||||
|
adminUserId: session.user.id,
|
||||||
|
adminEmail: session.user.email ?? null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.finalStatus !== "PAID") {
|
||||||
|
throw new Error("充值订单状态已变化,请刷新后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
await recordAuditLog({
|
||||||
|
actor: actorFromSession(session),
|
||||||
|
action: "wallet_recharge.confirm",
|
||||||
|
targetType: "WalletRechargeOrder",
|
||||||
|
targetId: recharge.id,
|
||||||
|
targetLabel: recharge.user.email,
|
||||||
|
message: `手动确认充值订单 ${recharge.id},入账 ¥${Number(recharge.amount).toFixed(2)}`,
|
||||||
|
metadata: {
|
||||||
|
userEmail: recharge.user.email,
|
||||||
|
amount: Number(recharge.amount),
|
||||||
|
tradeNo,
|
||||||
|
paymentRef,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
revalidateRechargeOrderViews(recharge.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cancelAdminWalletRecharge(rechargeOrderId: string) {
|
||||||
|
const session = await requireAdmin();
|
||||||
|
const id = rechargeOrderIdSchema.parse(rechargeOrderId);
|
||||||
|
const recharge = await prisma.walletRechargeOrder.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: { user: { select: { email: true, name: true } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!recharge) {
|
||||||
|
throw new Error("充值订单不存在或已被删除");
|
||||||
|
}
|
||||||
|
if (recharge.status !== "PENDING") {
|
||||||
|
throw new Error("只能取消待支付充值订单");
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.walletRechargeOrder.update({
|
||||||
|
where: { id: recharge.id },
|
||||||
|
data: {
|
||||||
|
status: "CANCELLED",
|
||||||
|
paymentUrl: null,
|
||||||
|
expireAt: null,
|
||||||
|
note: "管理员手动取消",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await recordAuditLog({
|
||||||
|
actor: actorFromSession(session),
|
||||||
|
action: "wallet_recharge.cancel",
|
||||||
|
targetType: "WalletRechargeOrder",
|
||||||
|
targetId: recharge.id,
|
||||||
|
targetLabel: recharge.user.email,
|
||||||
|
message: `取消充值订单 ${recharge.id}`,
|
||||||
|
metadata: {
|
||||||
|
userEmail: recharge.user.email,
|
||||||
|
amount: Number(recharge.amount),
|
||||||
|
tradeNo: recharge.tradeNo,
|
||||||
|
paymentMethod: recharge.paymentMethod,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
revalidateRechargeOrderViews(recharge.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAdminWalletRecharge(rechargeOrderId: string) {
|
||||||
|
const session = await requireAdmin();
|
||||||
|
const id = rechargeOrderIdSchema.parse(rechargeOrderId);
|
||||||
|
const recharge = await prisma.walletRechargeOrder.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
user: { select: { email: true, name: true } },
|
||||||
|
transactions: { select: { id: true }, take: 5 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!recharge) {
|
||||||
|
throw new Error("充值订单不存在或已被删除");
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.walletRechargeOrder.delete({ where: { id: recharge.id } });
|
||||||
|
|
||||||
|
await recordAuditLog({
|
||||||
|
actor: actorFromSession(session),
|
||||||
|
action: "wallet_recharge.delete",
|
||||||
|
targetType: "WalletRechargeOrder",
|
||||||
|
targetId: recharge.id,
|
||||||
|
targetLabel: recharge.user.email,
|
||||||
|
message: `删除充值订单 ${recharge.id}`,
|
||||||
|
metadata: {
|
||||||
|
userEmail: recharge.user.email,
|
||||||
|
amount: Number(recharge.amount),
|
||||||
|
status: recharge.status,
|
||||||
|
tradeNo: recharge.tradeNo,
|
||||||
|
paymentMethod: recharge.paymentMethod,
|
||||||
|
keptWalletTransactions: recharge.transactions.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
revalidateRechargeOrderViews(recharge.id);
|
||||||
|
}
|
||||||
@@ -8,9 +8,10 @@ import {
|
|||||||
DataTableHeaderRow,
|
DataTableHeaderRow,
|
||||||
DataTableRow,
|
DataTableRow,
|
||||||
} from "@/components/shared/data-table";
|
} from "@/components/shared/data-table";
|
||||||
import { OrderStatusBadge } from "@/components/shared/domain-badges";
|
import { StatusBadge, type StatusTone } from "@/components/shared/status-badge";
|
||||||
import { getPaymentProviderName } from "@/services/payment/catalog";
|
import { getPaymentProviderName } from "@/services/payment/catalog";
|
||||||
import { formatDateShort } from "@/lib/utils";
|
import { formatDateShort } from "@/lib/utils";
|
||||||
|
import { RechargeOrderActions } from "../recharge-order-actions";
|
||||||
import type { AdminRechargeOrderRow } from "../orders-data";
|
import type { AdminRechargeOrderRow } from "../orders-data";
|
||||||
|
|
||||||
interface RechargeOrdersTableProps {
|
interface RechargeOrdersTableProps {
|
||||||
@@ -22,9 +23,28 @@ function formatAmount(amount: { toString(): string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPaymentLabel(provider: string | null) {
|
function getPaymentLabel(provider: string | null) {
|
||||||
|
if (provider === "manual") return "手动确认";
|
||||||
return provider ? getPaymentProviderName(provider) : "未选择支付";
|
return provider ? getPaymentProviderName(provider) : "未选择支付";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rechargeStatusLabels: Record<AdminRechargeOrderRow["status"], string> = {
|
||||||
|
PENDING: "待支付",
|
||||||
|
PAID: "已入账",
|
||||||
|
CANCELLED: "已取消",
|
||||||
|
REFUNDED: "已退款",
|
||||||
|
};
|
||||||
|
|
||||||
|
function getRechargeStatusTone(status: AdminRechargeOrderRow["status"]): StatusTone {
|
||||||
|
if (status === "PAID") return "success";
|
||||||
|
if (status === "PENDING") return "warning";
|
||||||
|
if (status === "CANCELLED") return "neutral";
|
||||||
|
return "danger";
|
||||||
|
}
|
||||||
|
|
||||||
|
function RechargeStatusBadge({ status }: { status: AdminRechargeOrderRow["status"] }) {
|
||||||
|
return <StatusBadge tone={getRechargeStatusTone(status)}>{rechargeStatusLabels[status]}</StatusBadge>;
|
||||||
|
}
|
||||||
|
|
||||||
export function RechargeOrdersTable({ rechargeOrders }: RechargeOrdersTableProps) {
|
export function RechargeOrdersTable({ rechargeOrders }: RechargeOrdersTableProps) {
|
||||||
return (
|
return (
|
||||||
<DataTableShell
|
<DataTableShell
|
||||||
@@ -38,7 +58,7 @@ export function RechargeOrdersTable({ rechargeOrders }: RechargeOrdersTableProps
|
|||||||
<p className="break-all text-sm font-semibold">{order.user.email}</p>
|
<p className="break-all text-sm font-semibold">{order.user.email}</p>
|
||||||
<p className="mt-1 text-xs text-muted-foreground">{order.user.name || "未设置昵称"}</p>
|
<p className="mt-1 text-xs text-muted-foreground">{order.user.name || "未设置昵称"}</p>
|
||||||
</div>
|
</div>
|
||||||
<OrderStatusBadge status={order.status} />
|
<RechargeStatusBadge status={order.status} />
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg bg-muted/25 p-3">
|
<div className="rounded-lg bg-muted/25 p-3">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
@@ -50,10 +70,11 @@ export function RechargeOrdersTable({ rechargeOrders }: RechargeOrdersTableProps
|
|||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-xs text-muted-foreground">{formatDateShort(order.createdAt)}</p>
|
<p className="mt-1 text-xs text-muted-foreground">{formatDateShort(order.createdAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<RechargeOrderActions orderId={order.id} status={order.status} />
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
>
|
>
|
||||||
<DataTable aria-label="充值订单列表" className="min-w-[980px]">
|
<DataTable aria-label="充值订单列表" className="min-w-[1120px]">
|
||||||
<DataTableHead>
|
<DataTableHead>
|
||||||
<DataTableHeaderRow>
|
<DataTableHeaderRow>
|
||||||
<DataTableHeadCell>用户</DataTableHeadCell>
|
<DataTableHeadCell>用户</DataTableHeadCell>
|
||||||
@@ -62,6 +83,7 @@ export function RechargeOrdersTable({ rechargeOrders }: RechargeOrdersTableProps
|
|||||||
<DataTableHeadCell>状态</DataTableHeadCell>
|
<DataTableHeadCell>状态</DataTableHeadCell>
|
||||||
<DataTableHeadCell>备注</DataTableHeadCell>
|
<DataTableHeadCell>备注</DataTableHeadCell>
|
||||||
<DataTableHeadCell>时间</DataTableHeadCell>
|
<DataTableHeadCell>时间</DataTableHeadCell>
|
||||||
|
<DataTableHeadCell className="text-right">操作</DataTableHeadCell>
|
||||||
</DataTableHeaderRow>
|
</DataTableHeaderRow>
|
||||||
</DataTableHead>
|
</DataTableHead>
|
||||||
<DataTableBody>
|
<DataTableBody>
|
||||||
@@ -81,7 +103,7 @@ export function RechargeOrdersTable({ rechargeOrders }: RechargeOrdersTableProps
|
|||||||
</div>
|
</div>
|
||||||
</DataTableCell>
|
</DataTableCell>
|
||||||
<DataTableCell>
|
<DataTableCell>
|
||||||
<OrderStatusBadge status={order.status} />
|
<RechargeStatusBadge status={order.status} />
|
||||||
</DataTableCell>
|
</DataTableCell>
|
||||||
<DataTableCell className="max-w-64 whitespace-normal break-words text-xs text-muted-foreground">
|
<DataTableCell className="max-w-64 whitespace-normal break-words text-xs text-muted-foreground">
|
||||||
{order.note || order.paymentRef || "—"}
|
{order.note || order.paymentRef || "—"}
|
||||||
@@ -89,6 +111,9 @@ export function RechargeOrdersTable({ rechargeOrders }: RechargeOrdersTableProps
|
|||||||
<DataTableCell className="whitespace-nowrap text-muted-foreground">
|
<DataTableCell className="whitespace-nowrap text-muted-foreground">
|
||||||
{formatDateShort(order.createdAt)}
|
{formatDateShort(order.createdAt)}
|
||||||
</DataTableCell>
|
</DataTableCell>
|
||||||
|
<DataTableCell>
|
||||||
|
<RechargeOrderActions orderId={order.id} status={order.status} />
|
||||||
|
</DataTableCell>
|
||||||
</DataTableRow>
|
</DataTableRow>
|
||||||
))}
|
))}
|
||||||
</DataTableBody>
|
</DataTableBody>
|
||||||
|
|||||||
84
src/app/(admin)/admin/orders/recharge-order-actions.tsx
Normal file
84
src/app/(admin)/admin/orders/recharge-order-actions.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { CheckCircle2, Trash2, XCircle } from "lucide-react";
|
||||||
|
import {
|
||||||
|
cancelAdminWalletRecharge,
|
||||||
|
confirmAdminWalletRecharge,
|
||||||
|
deleteAdminWalletRecharge,
|
||||||
|
} from "@/actions/admin/recharge-orders";
|
||||||
|
import { ConfirmActionButton } from "@/components/shared/confirm-action-button";
|
||||||
|
|
||||||
|
type RechargeOrderStatus = "PENDING" | "PAID" | "CANCELLED" | "REFUNDED";
|
||||||
|
|
||||||
|
export function RechargeOrderActions({
|
||||||
|
orderId,
|
||||||
|
status,
|
||||||
|
}: {
|
||||||
|
orderId: string;
|
||||||
|
status: RechargeOrderStatus;
|
||||||
|
}) {
|
||||||
|
const router = useRouter();
|
||||||
|
const isPending = status === "PENDING";
|
||||||
|
const isPaid = status === "PAID";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap justify-start gap-2 lg:justify-end">
|
||||||
|
{isPending && (
|
||||||
|
<>
|
||||||
|
<ConfirmActionButton
|
||||||
|
title="确认这笔充值?"
|
||||||
|
description="会立即把充值金额入账到用户钱包,并将订单标记为已入账。"
|
||||||
|
confirmLabel="确认入账"
|
||||||
|
successMessage="充值订单已确认入账"
|
||||||
|
errorMessage="确认充值订单失败"
|
||||||
|
size="sm"
|
||||||
|
onConfirm={async () => {
|
||||||
|
await confirmAdminWalletRecharge(orderId);
|
||||||
|
}}
|
||||||
|
onSuccess={() => router.refresh()}
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="size-3.5" />
|
||||||
|
确认
|
||||||
|
</ConfirmActionButton>
|
||||||
|
<ConfirmActionButton
|
||||||
|
title="取消这笔充值?"
|
||||||
|
description="取消后用户不能继续支付这笔充值订单,钱包余额不会变化。"
|
||||||
|
confirmLabel="取消充值"
|
||||||
|
successMessage="充值订单已取消"
|
||||||
|
errorMessage="取消充值订单失败"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onConfirm={async () => {
|
||||||
|
await cancelAdminWalletRecharge(orderId);
|
||||||
|
}}
|
||||||
|
onSuccess={() => router.refresh()}
|
||||||
|
>
|
||||||
|
<XCircle className="size-3.5" />
|
||||||
|
取消
|
||||||
|
</ConfirmActionButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<ConfirmActionButton
|
||||||
|
title="删除这条充值记录?"
|
||||||
|
description={
|
||||||
|
isPaid
|
||||||
|
? "只删除充值订单记录,不回滚已入账余额,钱包流水会保留。"
|
||||||
|
: "删除后这条充值记录不可恢复,不会改变用户钱包余额。"
|
||||||
|
}
|
||||||
|
confirmLabel="删除记录"
|
||||||
|
successMessage="充值记录已删除"
|
||||||
|
errorMessage="删除充值记录失败"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onConfirm={async () => {
|
||||||
|
await deleteAdminWalletRecharge(orderId);
|
||||||
|
}}
|
||||||
|
onSuccess={() => router.refresh()}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-3.5" />
|
||||||
|
删除
|
||||||
|
</ConfirmActionButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ export const auditActionFilterOptions = [
|
|||||||
{ label: "风控操作", value: "risk." },
|
{ label: "风控操作", value: "risk." },
|
||||||
{ label: "系统设置", value: "settings." },
|
{ label: "系统设置", value: "settings." },
|
||||||
{ label: "支付配置", value: "payment." },
|
{ label: "支付配置", value: "payment." },
|
||||||
|
{ label: "钱包充值", value: "wallet_recharge." },
|
||||||
{ label: "公告操作", value: "announcement." },
|
{ label: "公告操作", value: "announcement." },
|
||||||
{ label: "工单操作", value: "support." },
|
{ label: "工单操作", value: "support." },
|
||||||
{ label: "优惠规则", value: "coupon." },
|
{ label: "优惠规则", value: "coupon." },
|
||||||
@@ -112,6 +113,9 @@ const auditActionLabels: Record<string, string> = {
|
|||||||
"user.force_delete": "强制删除用户",
|
"user.force_delete": "强制删除用户",
|
||||||
"user.status": "更新用户状态",
|
"user.status": "更新用户状态",
|
||||||
"user.update": "更新用户",
|
"user.update": "更新用户",
|
||||||
|
"wallet_recharge.cancel": "取消充值订单",
|
||||||
|
"wallet_recharge.confirm": "确认充值入账",
|
||||||
|
"wallet_recharge.delete": "删除充值记录",
|
||||||
};
|
};
|
||||||
|
|
||||||
const auditTargetTypeLabels: Record<string, string> = {
|
const auditTargetTypeLabels: Record<string, string> = {
|
||||||
@@ -134,6 +138,7 @@ const auditTargetTypeLabels: Record<string, string> = {
|
|||||||
TrafficSync: "流量同步",
|
TrafficSync: "流量同步",
|
||||||
User: "用户",
|
User: "用户",
|
||||||
UserSubscription: "订阅",
|
UserSubscription: "订阅",
|
||||||
|
WalletRechargeOrder: "充值订单",
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenLabels: Record<string, string> = {
|
const tokenLabels: Record<string, string> = {
|
||||||
|
|||||||
@@ -117,9 +117,11 @@ export const nodeStatusLabels: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const paymentProviderLabels: Record<string, string> = {
|
export const paymentProviderLabels: Record<string, string> = {
|
||||||
|
balance: "余额支付",
|
||||||
epay: "易支付",
|
epay: "易支付",
|
||||||
alipay_f2f: "支付宝当面付",
|
alipay_f2f: "支付宝当面付",
|
||||||
usdt_trc20: "USDT (TRC20)",
|
usdt_trc20: "USDT (TRC20)",
|
||||||
|
manual: "手动确认",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const paymentChannelLabels: Record<string, string> = {
|
export const paymentChannelLabels: Record<string, string> = {
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ export function getPaymentProviderDefinition(provider: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getPaymentProviderName(provider: string): string {
|
export function getPaymentProviderName(provider: string): string {
|
||||||
|
if (provider === "manual") return "手动确认";
|
||||||
return getPaymentProviderDefinition(provider)?.name ?? provider;
|
return getPaymentProviderDefinition(provider)?.name ?? provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma, type OrderStatus } from "@prisma/client";
|
||||||
import { prisma, type DbClient } from "@/lib/prisma";
|
import { prisma, type DbClient } from "@/lib/prisma";
|
||||||
import { formatDate } from "@/lib/utils";
|
import { formatDate } from "@/lib/utils";
|
||||||
import { createNotification } from "@/services/notifications";
|
import { createNotification } from "@/services/notifications";
|
||||||
@@ -298,33 +298,43 @@ export async function createWalletRechargeOrder(userId: string, amountValue: num
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processWalletRechargeSuccess(
|
export async function processWalletRechargeOrderSuccess(input: {
|
||||||
tradeNo: string,
|
rechargeOrderId: string;
|
||||||
paidAmount: number,
|
paidAmount?: number;
|
||||||
paymentRef?: string,
|
paymentRef?: string | null;
|
||||||
) {
|
paymentMethod?: string | null;
|
||||||
if (!Number.isFinite(paidAmount) || paidAmount <= 0) {
|
tradeNo?: string | null;
|
||||||
return { processed: false, finalStatus: null as null | "PENDING" | "PAID" };
|
description?: string;
|
||||||
}
|
metadata?: Record<string, unknown>;
|
||||||
|
}) {
|
||||||
const rechargeOrder = await prisma.walletRechargeOrder.findUnique({
|
const rechargeOrder = await prisma.walletRechargeOrder.findUnique({
|
||||||
where: { tradeNo },
|
where: { id: input.rechargeOrderId },
|
||||||
});
|
});
|
||||||
if (!rechargeOrder) {
|
if (!rechargeOrder) {
|
||||||
return { processed: false, finalStatus: null as null | "PENDING" | "PAID" };
|
return { processed: false, finalStatus: null as OrderStatus | null };
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectedAmount = Number(rechargeOrder.amount);
|
const expectedAmount = Number(rechargeOrder.amount);
|
||||||
if (Math.abs(expectedAmount - paidAmount) > 0.01) {
|
if (
|
||||||
|
input.paidAmount !== undefined
|
||||||
|
&& (!Number.isFinite(input.paidAmount) || Math.abs(expectedAmount - input.paidAmount) > 0.01)
|
||||||
|
) {
|
||||||
throw new Error("支付金额与充值金额不一致");
|
throw new Error("支付金额与充值金额不一致");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tradeNo = input.tradeNo ?? rechargeOrder.tradeNo;
|
||||||
|
const paymentMethod = input.paymentMethod ?? rechargeOrder.paymentMethod;
|
||||||
|
|
||||||
return prisma.$transaction(async (tx) => {
|
return prisma.$transaction(async (tx) => {
|
||||||
const claimed = await tx.walletRechargeOrder.updateMany({
|
const claimed = await tx.walletRechargeOrder.updateMany({
|
||||||
where: { id: rechargeOrder.id, status: "PENDING" },
|
where: { id: rechargeOrder.id, status: "PENDING" },
|
||||||
data: {
|
data: {
|
||||||
status: "PAID",
|
status: "PAID",
|
||||||
paymentRef: paymentRef ?? rechargeOrder.paymentRef,
|
paymentRef: input.paymentRef ?? rechargeOrder.paymentRef,
|
||||||
|
paymentMethod,
|
||||||
|
tradeNo,
|
||||||
|
paymentUrl: null,
|
||||||
|
expireAt: null,
|
||||||
note: null,
|
note: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -341,9 +351,11 @@ export async function processWalletRechargeSuccess(
|
|||||||
userId: rechargeOrder.userId,
|
userId: rechargeOrder.userId,
|
||||||
amount: rechargeOrder.amount,
|
amount: rechargeOrder.amount,
|
||||||
type: "BALANCE_RECHARGE",
|
type: "BALANCE_RECHARGE",
|
||||||
description: `${getPaymentProviderName(rechargeOrder.paymentMethod ?? "")} 余额充值`,
|
description:
|
||||||
|
input.description
|
||||||
|
?? `${paymentMethod ? getPaymentProviderName(paymentMethod) : "余额"} 余额充值`,
|
||||||
rechargeOrderId: rechargeOrder.id,
|
rechargeOrderId: rechargeOrder.id,
|
||||||
metadata: { tradeNo },
|
metadata: { tradeNo, ...(input.metadata ?? {}) },
|
||||||
});
|
});
|
||||||
|
|
||||||
await createNotification(
|
await createNotification(
|
||||||
@@ -363,6 +375,30 @@ export async function processWalletRechargeSuccess(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function processWalletRechargeSuccess(
|
||||||
|
tradeNo: string,
|
||||||
|
paidAmount: number,
|
||||||
|
paymentRef?: string,
|
||||||
|
) {
|
||||||
|
if (!Number.isFinite(paidAmount) || paidAmount <= 0) {
|
||||||
|
return { processed: false, finalStatus: null as OrderStatus | null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const rechargeOrder = await prisma.walletRechargeOrder.findUnique({
|
||||||
|
where: { tradeNo },
|
||||||
|
});
|
||||||
|
if (!rechargeOrder) {
|
||||||
|
return { processed: false, finalStatus: null as OrderStatus | null };
|
||||||
|
}
|
||||||
|
|
||||||
|
return processWalletRechargeOrderSuccess({
|
||||||
|
rechargeOrderId: rechargeOrder.id,
|
||||||
|
paidAmount,
|
||||||
|
paymentRef,
|
||||||
|
tradeNo,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function createRechargeCards(input: {
|
export async function createRechargeCards(input: {
|
||||||
createdById: string;
|
createdById: string;
|
||||||
type: "BALANCE" | "PLAN";
|
type: "BALANCE" | "PLAN";
|
||||||
|
|||||||
Reference in New Issue
Block a user