重构 API 为 8 领域 + 应用层架构

将后端模块拆分为 domains、applications、shared 三层,结算计算器移入 domain 纯函数目录,API 路径与测试保持不变。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-02 14:48:41 +08:00
parent 14e49374ac
commit 4c92157299
47 changed files with 169 additions and 138 deletions

View File

@@ -0,0 +1,108 @@
import { Injectable, BadRequestException } from '@nestjs/common';
import { PrismaService } from '../../../shared/prisma/prisma.service';
import { WalletService } from '../../ledger/wallet.service';
import { Decimal } from '@prisma/client/runtime/library';
import { generateBatchNo } from '../../../shared/common/decorators';
@Injectable()
export class CashbackService {
constructor(
private prisma: PrismaService,
private wallet: WalletService,
) {}
async previewBatch(periodStart: Date, periodEnd: Date) {
const settledBets = await this.prisma.bet.findMany({
where: {
status: { in: ['WON', 'LOST', 'SETTLED'] },
settledAt: { gte: periodStart, lte: periodEnd },
},
include: { user: { include: { agentProfile: true } } },
});
const playerStakes = new Map<string, { userId: bigint; stake: Decimal; rate: Decimal }>();
for (const bet of settledBets) {
if (bet.status === 'PUSH' || bet.status === 'VOID') continue;
const key = bet.userId.toString();
const existing = playerStakes.get(key) ?? {
userId: bet.userId,
stake: new Decimal(0),
rate: new Decimal(0.01),
};
existing.stake = existing.stake.add(bet.stake);
playerStakes.set(key, existing);
}
const items = Array.from(playerStakes.values()).map((p) => ({
userId: p.userId,
effectiveStake: p.stake,
rate: p.rate,
amount: p.stake.mul(p.rate),
}));
const totalAmount = items.reduce((s, i) => s.add(i.amount), new Decimal(0));
const batch = await this.prisma.cashbackBatch.create({
data: {
batchNo: generateBatchNo('CB'),
periodStart,
periodEnd,
status: 'PREVIEW',
totalAmount,
playerCount: items.length,
},
});
for (const item of items) {
await this.prisma.cashbackItem.create({
data: {
batchId: batch.id,
userId: item.userId,
effectiveStake: item.effectiveStake,
rate: item.rate,
amount: item.amount,
},
});
}
return { batch, items, totalAmount };
}
async confirmBatch(batchId: bigint, operatorId: bigint) {
const batch = await this.prisma.cashbackBatch.findUnique({
where: { id: batchId },
include: { items: true },
});
if (!batch) throw new BadRequestException('Batch not found');
if (batch.status !== 'PREVIEW') throw new BadRequestException('Already confirmed');
for (const item of batch.items) {
if (item.amount.gt(0)) {
await this.wallet.deposit(
item.userId,
item.amount,
operatorId,
`Cashback batch ${batch.batchNo}`,
batch.batchNo,
);
}
}
await this.prisma.cashbackBatch.update({
where: { id: batchId },
data: { status: 'CONFIRMED', confirmedAt: new Date(), operatorId },
});
return { success: true };
}
async getUserCashbacks(userId: bigint) {
return this.prisma.cashbackItem.findMany({
where: { userId },
include: { batch: true },
orderBy: { createdAt: 'desc' },
});
}
}