/** * 钱包服务 - 余额查询、扣费(原子事务)、充值、入账 */ import { Injectable, Logger } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { InsufficientBalanceException } from './exceptions/insufficient-balance.exception'; @Injectable() export class WalletService { private readonly logger = new Logger(WalletService.name); constructor(private readonly prisma: PrismaService) {} async getWallet(userId: string) { const user = await this.prisma.user.findUnique({ where: { id: userId }, select: { balanceCents: true, currency: true }, }); if (!user) { throw new Error('用户不存在'); } const transactions = await this.prisma.balanceTransaction.findMany({ where: { userId }, orderBy: { createdAt: 'desc' }, take: 20, select: { id: true, amountCents: true, type: true, refId: true, createdAt: true, }, }); return { balanceCents: Number(user.balanceCents), currency: user.currency, transactions: transactions.map((t) => ({ ...t, amountCents: Number(t.amountCents), refId: t.refId ?? null, createdAt: t.createdAt.toISOString(), })), }; } /** * 原子扣费 - 使用 UPDATE WHERE 保证余额充足 * 返回值 { count: 0 } 表示余额不足,抛出 InsufficientBalanceException * 插入 BalanceTransaction type='usage' */ async deduct( userId: string, amountCents: bigint, refId: string, _model?: string, ): Promise<{ newBalance: bigint }> { const result = await this.prisma.$transaction(async (tx) => { // 原子扣减:仅当 balanceCents >= amountCents 时更新 const updateResult = await tx.user.updateMany({ where: { id: userId, balanceCents: { gte: amountCents }, }, data: { balanceCents: { decrement: amountCents }, }, }); if (updateResult.count === 0) { const user = await tx.user.findUnique({ where: { id: userId }, select: { balanceCents: true }, }); throw new InsufficientBalanceException( '余额不足,无法完成扣费', amountCents, user?.balanceCents ?? 0n, ); } // 记录交易 await tx.balanceTransaction.create({ data: { userId, amountCents: -amountCents, type: 'usage', refId, }, }); // 读取新余额 const updated = await tx.user.findUniqueOrThrow({ where: { id: userId }, select: { balanceCents: true }, }); return { newBalance: updated.balanceCents }; }); this.logger.debug(`Deducted user=${userId} amount=${amountCents} ref=${refId}`); return result; } /** * 入账(充值/退款)- 直接增加余额并记录交易 */ async credit( userId: string, amountCents: bigint, refId: string, type: string, ): Promise<{ newBalance: bigint }> { const result = await this.prisma.$transaction(async (tx) => { const updated = await tx.user.update({ where: { id: userId }, data: { balanceCents: { increment: amountCents }, }, select: { balanceCents: true }, }); await tx.balanceTransaction.create({ data: { userId, amountCents, type, refId, }, }); return { newBalance: updated.balanceCents }; }); this.logger.debug(`Credited user=${userId} amount=${amountCents} ref=${refId} type=${type}`); return result; } }