重构 API 为 8 领域 + 应用层架构
将后端模块拆分为 domains、applications、shared 三层,结算计算器移入 domain 纯函数目录,API 路径与测试保持不变。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
8
apps/api/src/domains/ledger/wallet.module.ts
Normal file
8
apps/api/src/domains/ledger/wallet.module.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { WalletService } from './wallet.service';
|
||||
|
||||
@Module({
|
||||
providers: [WalletService],
|
||||
exports: [WalletService],
|
||||
})
|
||||
export class WalletModule {}
|
||||
222
apps/api/src/domains/ledger/wallet.service.ts
Normal file
222
apps/api/src/domains/ledger/wallet.service.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { PrismaService } from '../../shared/prisma/prisma.service';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { generateTransactionId } from '../../shared/common/decorators';
|
||||
|
||||
@Injectable()
|
||||
export class WalletService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async getWallet(userId: bigint) {
|
||||
const wallet = await this.prisma.wallet.findUnique({ where: { userId } });
|
||||
if (!wallet) throw new BadRequestException('Wallet not found');
|
||||
return wallet;
|
||||
}
|
||||
|
||||
async createWallet(userId: bigint, currency = 'USD') {
|
||||
return this.prisma.wallet.create({
|
||||
data: { userId, currency },
|
||||
});
|
||||
}
|
||||
|
||||
private async lockWallet(tx: Parameters<Parameters<PrismaService['$transaction']>[0]>[0], userId: bigint) {
|
||||
const wallets = await tx.$queryRaw<Array<{ id: bigint; available_balance: Decimal; frozen_balance: Decimal; version: number }>>`
|
||||
SELECT id, available_balance, frozen_balance, version FROM wallets WHERE user_id = ${userId} FOR UPDATE
|
||||
`;
|
||||
if (!wallets.length) throw new BadRequestException('Wallet not found');
|
||||
return wallets[0];
|
||||
}
|
||||
|
||||
async deposit(
|
||||
userId: bigint,
|
||||
amount: Decimal | number,
|
||||
operatorId: bigint,
|
||||
remark?: string,
|
||||
referenceId?: string,
|
||||
) {
|
||||
const amt = new Decimal(amount);
|
||||
if (amt.lte(0)) throw new BadRequestException('Amount must be positive');
|
||||
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const w = await this.lockWallet(tx, userId);
|
||||
const balanceBefore = new Decimal(w.available_balance);
|
||||
const balanceAfter = balanceBefore.add(amt);
|
||||
|
||||
await tx.wallet.update({
|
||||
where: { id: w.id },
|
||||
data: {
|
||||
availableBalance: balanceAfter,
|
||||
version: { increment: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
await tx.walletTransaction.create({
|
||||
data: {
|
||||
transactionId: generateTransactionId(),
|
||||
userId,
|
||||
walletId: w.id,
|
||||
transactionType: 'MANUAL_DEPOSIT',
|
||||
amount: amt,
|
||||
balanceBefore,
|
||||
balanceAfter,
|
||||
frozenBefore: w.frozen_balance,
|
||||
frozenAfter: w.frozen_balance,
|
||||
referenceType: 'DEPOSIT',
|
||||
referenceId,
|
||||
operatorId,
|
||||
remark,
|
||||
},
|
||||
});
|
||||
|
||||
return { balanceAfter };
|
||||
});
|
||||
}
|
||||
|
||||
async withdraw(
|
||||
userId: bigint,
|
||||
amount: Decimal | number,
|
||||
operatorId: bigint,
|
||||
remark?: string,
|
||||
referenceId?: string,
|
||||
) {
|
||||
const amt = new Decimal(amount);
|
||||
if (amt.lte(0)) throw new BadRequestException('Amount must be positive');
|
||||
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const w = await this.lockWallet(tx, userId);
|
||||
const balanceBefore = new Decimal(w.available_balance);
|
||||
if (balanceBefore.lt(amt)) throw new BadRequestException('Insufficient balance');
|
||||
const balanceAfter = balanceBefore.sub(amt);
|
||||
|
||||
await tx.wallet.update({
|
||||
where: { id: w.id },
|
||||
data: {
|
||||
availableBalance: balanceAfter,
|
||||
version: { increment: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
await tx.walletTransaction.create({
|
||||
data: {
|
||||
transactionId: generateTransactionId(),
|
||||
userId,
|
||||
walletId: w.id,
|
||||
transactionType: 'MANUAL_WITHDRAW',
|
||||
amount: amt.neg(),
|
||||
balanceBefore,
|
||||
balanceAfter,
|
||||
frozenBefore: w.frozen_balance,
|
||||
frozenAfter: w.frozen_balance,
|
||||
referenceType: 'WITHDRAW',
|
||||
referenceId,
|
||||
operatorId,
|
||||
remark,
|
||||
},
|
||||
});
|
||||
|
||||
return { balanceAfter };
|
||||
});
|
||||
}
|
||||
|
||||
async freezeForBet(userId: bigint, stake: Decimal | number, betId: string) {
|
||||
const amt = new Decimal(stake);
|
||||
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const w = await this.lockWallet(tx, userId);
|
||||
const avail = new Decimal(w.available_balance);
|
||||
if (avail.lt(amt)) throw new BadRequestException('Insufficient balance');
|
||||
|
||||
const balanceAfter = avail.sub(amt);
|
||||
const frozenAfter = new Decimal(w.frozen_balance).add(amt);
|
||||
|
||||
await tx.wallet.update({
|
||||
where: { id: w.id },
|
||||
data: {
|
||||
availableBalance: balanceAfter,
|
||||
frozenBalance: frozenAfter,
|
||||
version: { increment: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
await tx.walletTransaction.create({
|
||||
data: {
|
||||
transactionId: generateTransactionId(),
|
||||
userId,
|
||||
walletId: w.id,
|
||||
transactionType: 'BET_FREEZE',
|
||||
amount: amt.neg(),
|
||||
balanceBefore: avail,
|
||||
balanceAfter,
|
||||
frozenBefore: w.frozen_balance,
|
||||
frozenAfter,
|
||||
referenceType: 'BET',
|
||||
referenceId: betId,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async settleBet(
|
||||
userId: bigint,
|
||||
stake: Decimal,
|
||||
payout: Decimal,
|
||||
betId: string,
|
||||
result: 'WIN' | 'LOSE' | 'PUSH' | 'VOID' | 'HALF_WIN' | 'HALF_LOSE',
|
||||
) {
|
||||
const txTypeMap: Record<string, string> = {
|
||||
WIN: 'BET_SETTLE_WIN',
|
||||
LOSE: 'BET_SETTLE_LOSE',
|
||||
PUSH: 'BET_SETTLE_PUSH',
|
||||
VOID: 'BET_VOID_REFUND',
|
||||
HALF_WIN: 'BET_SETTLE_WIN',
|
||||
HALF_LOSE: 'BET_SETTLE_LOSE',
|
||||
};
|
||||
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const w = await this.lockWallet(tx, userId);
|
||||
const avail = new Decimal(w.available_balance);
|
||||
const frozen = new Decimal(w.frozen_balance);
|
||||
const frozenAfter = frozen.sub(stake);
|
||||
const balanceAfter = avail.add(payout);
|
||||
|
||||
await tx.wallet.update({
|
||||
where: { id: w.id },
|
||||
data: {
|
||||
availableBalance: balanceAfter,
|
||||
frozenBalance: frozenAfter.lt(0) ? new Decimal(0) : frozenAfter,
|
||||
version: { increment: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
await tx.walletTransaction.create({
|
||||
data: {
|
||||
transactionId: generateTransactionId(),
|
||||
userId,
|
||||
walletId: w.id,
|
||||
transactionType: txTypeMap[result] || 'BET_SETTLE_WIN',
|
||||
amount: payout,
|
||||
balanceBefore: avail,
|
||||
balanceAfter,
|
||||
frozenBefore: frozen,
|
||||
frozenAfter: frozenAfter.lt(0) ? new Decimal(0) : frozenAfter,
|
||||
referenceType: 'BET',
|
||||
referenceId: betId,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getTransactions(userId: bigint, page = 1, pageSize = 20) {
|
||||
const skip = (page - 1) * pageSize;
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.walletTransaction.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.walletTransaction.count({ where: { userId } }),
|
||||
]);
|
||||
return { items, total, page, pageSize };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user