Files
rent/apps/server/src/modules/finance/finance.service.ts
T
2026-04-24 00:28:52 +08:00

450 lines
15 KiB
TypeScript

import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource, Between, FindOptionsWhere } from 'typeorm';
import { Settlement } from '@/entities/settlement.entity';
import { SettlementItem } from '@/entities/settlement-item.entity';
import { Withdrawal } from '@/entities/withdrawal.entity';
import { Merchant } from '@/entities/merchant.entity';
import { Order } from '@/entities/order.entity';
import {
QuerySettlementDto,
CreateWithdrawalDto,
UpdateBankInfoDto,
QueryWithdrawalDto,
AdminQuerySettlementDto,
RejectSettlementDto,
AdminQueryWithdrawalDto,
RejectWithdrawalDto,
QueryEarningsDto,
} from './dto/finance.dto';
@Injectable()
export class FinanceService {
constructor(
@InjectRepository(Settlement)
private settlementRepo: Repository<Settlement>,
@InjectRepository(SettlementItem)
private settlementItemRepo: Repository<SettlementItem>,
@InjectRepository(Withdrawal)
private withdrawalRepo: Repository<Withdrawal>,
@InjectRepository(Merchant)
private merchantRepo: Repository<Merchant>,
@InjectRepository(Order)
private orderRepo: Repository<Order>,
private dataSource: DataSource,
) {}
// ==================== 商家端 ====================
// 对账单列表
async getSettlements(merchantId: number, dto: QuerySettlementDto) {
const where: FindOptionsWhere<Settlement> = { merchantId };
if (dto.status) where.status = dto.status;
if (dto.periodStart && dto.periodEnd) {
where.periodStart = Between(dto.periodStart, dto.periodEnd);
}
const [list, total] = await this.settlementRepo.findAndCount({
where,
order: { createdAt: 'DESC' },
skip: ((dto.page ?? 1) - 1) * (dto.pageSize ?? 10),
take: dto.pageSize ?? 10,
});
return { list, total, page: dto.page ?? 1, pageSize: dto.pageSize ?? 10 };
}
// 对账单详情(含明细)
async getSettlementDetail(merchantId: number, id: number) {
const settlement = await this.settlementRepo.findOne({
where: { id, merchantId },
relations: ['items'],
});
if (!settlement) throw new NotFoundException('对账单不存在');
return settlement;
}
// 钱包信息
async getWallet(merchantId: number) {
const merchant = await this.merchantRepo.findOne({
where: { id: merchantId },
select: ['id', 'walletBalance', 'bankName', 'bankAccount', 'accountName'],
});
if (!merchant) throw new NotFoundException('商家不存在');
const pendingAmount = await this.withdrawalRepo
.createQueryBuilder('w')
.where('w.merchant_id = :merchantId AND w.status IN (:...statuses)', {
merchantId,
statuses: ['pending', 'approved'],
})
.select('COALESCE(SUM(w.amount), 0)', 'total')
.getRawOne();
return {
walletBalance: merchant.walletBalance,
bankName: merchant.bankName,
bankAccount: merchant.bankAccount,
accountName: merchant.accountName,
pendingWithdrawAmount: Number(pendingAmount?.total || 0),
};
}
// 更新银行卡信息
async updateBankInfo(merchantId: number, dto: UpdateBankInfoDto) {
await this.merchantRepo.update({ id: merchantId }, {
bankName: dto.bankName,
bankAccount: dto.bankAccount,
accountName: dto.accountName,
});
return { success: true };
}
// 申请提现
async createWithdrawal(merchantId: number, dto: CreateWithdrawalDto) {
const merchant = await this.merchantRepo.findOne({ where: { id: merchantId } });
if (!merchant) throw new NotFoundException('商家不存在');
if (Number(merchant.walletBalance) < dto.amount) {
throw new BadRequestException('余额不足');
}
if (dto.amount < 100) {
throw new BadRequestException('最低提现金额为100元');
}
// 查找已审核通过的对账单
const approvedSettlements = await this.settlementRepo.find({
where: { merchantId, status: 'approved' },
order: { createdAt: 'ASC' },
});
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 扣减商家余额
await queryRunner.manager.decrement(Merchant, { id: merchantId }, 'walletBalance', dto.amount);
const withdrawal = queryRunner.manager.create(Withdrawal, {
merchantId,
settlementIds: approvedSettlements.map(s => s.id),
amount: dto.amount,
fee: 0,
commissionAmount: 0,
actualAmount: dto.amount,
bankName: dto.bankName,
bankAccount: dto.bankAccount,
accountName: dto.accountName,
status: 'pending',
});
await queryRunner.manager.save(withdrawal);
await queryRunner.commitTransaction();
return withdrawal;
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
// 提现记录列表
async getWithdrawals(merchantId: number, dto: QueryWithdrawalDto) {
const where: FindOptionsWhere<Withdrawal> = { merchantId };
if (dto.status) where.status = dto.status;
const [list, total] = await this.withdrawalRepo.findAndCount({
where,
order: { createdAt: 'DESC' },
skip: ((dto.page ?? 1) - 1) * (dto.pageSize ?? 10),
take: dto.pageSize ?? 10,
});
return { list, total, page: dto.page ?? 1, pageSize: dto.pageSize ?? 10 };
}
// ==================== 平台管理端 ====================
// 管理端 - 对账单列表
async adminGetSettlements(dto: AdminQuerySettlementDto) {
const where: FindOptionsWhere<Settlement> = {};
if (dto.status) where.status = dto.status;
if (dto.merchantId) where.merchantId = dto.merchantId;
if (dto.periodStart && dto.periodEnd) {
where.periodStart = Between(dto.periodStart, dto.periodEnd);
}
const [list, total] = await this.settlementRepo.findAndCount({
where,
relations: ['merchant'],
order: { createdAt: 'DESC' },
skip: ((dto.page ?? 1) - 1) * (dto.pageSize ?? 10),
take: dto.pageSize ?? 10,
});
return { list, total, page: dto.page ?? 1, pageSize: dto.pageSize ?? 10 };
}
// 管理端 - 对账单详情
async adminGetSettlementDetail(id: number) {
const settlement = await this.settlementRepo.findOne({
where: { id },
relations: ['merchant', 'items'],
});
if (!settlement) throw new NotFoundException('对账单不存在');
return settlement;
}
// 审核通过对账单
async approveSettlement(id: number, reviewerId: number) {
const settlement = await this.settlementRepo.findOne({ where: { id } });
if (!settlement) throw new NotFoundException('对账单不存在');
if (settlement.status !== 'pending') throw new BadRequestException('只能审核待审核的对账单');
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await queryRunner.manager.update(Settlement, { id }, {
status: 'approved',
reviewerId,
reviewedAt: new Date(),
});
// 对账单金额进入商家待提现余额
await queryRunner.manager.increment(
Merchant,
{ id: settlement.merchantId },
'walletBalance',
Number(settlement.settlementAmount),
);
await queryRunner.commitTransaction();
return { success: true };
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
// 拒绝对账单
async rejectSettlement(id: number, reviewerId: number, dto: RejectSettlementDto) {
const settlement = await this.settlementRepo.findOne({ where: { id } });
if (!settlement) throw new NotFoundException('对账单不存在');
if (settlement.status !== 'pending') throw new BadRequestException('只能审核待审核的对账单');
await this.settlementRepo.update({ id }, {
status: 'rejected',
reviewerId,
reviewedAt: new Date(),
rejectReason: dto.rejectReason,
});
return { success: true };
}
// 管理端 - 提现列表
async adminGetWithdrawals(dto: AdminQueryWithdrawalDto) {
const where: FindOptionsWhere<Withdrawal> = {};
if (dto.status) where.status = dto.status;
if (dto.merchantId) where.merchantId = dto.merchantId;
const [list, total] = await this.withdrawalRepo.findAndCount({
where,
relations: ['merchant'],
order: { createdAt: 'DESC' },
skip: ((dto.page ?? 1) - 1) * (dto.pageSize ?? 10),
take: dto.pageSize ?? 10,
});
return { list, total, page: dto.page ?? 1, pageSize: dto.pageSize ?? 10 };
}
// 审核通过提现
async approveWithdrawal(id: number, reviewerId: number) {
const withdrawal = await this.withdrawalRepo.findOne({ where: { id } });
if (!withdrawal) throw new NotFoundException('提现记录不存在');
if (withdrawal.status !== 'pending') throw new BadRequestException('只能审核待审核的提现');
await this.withdrawalRepo.update({ id }, {
status: 'approved',
reviewerId,
reviewedAt: new Date(),
});
return { success: true };
}
// 拒绝提现(退还余额)
async rejectWithdrawal(id: number, reviewerId: number, dto: RejectWithdrawalDto) {
const withdrawal = await this.withdrawalRepo.findOne({ where: { id } });
if (!withdrawal) throw new NotFoundException('提现记录不存在');
if (withdrawal.status !== 'pending') throw new BadRequestException('只能审核待审核的提现');
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await queryRunner.manager.update(Withdrawal, { id }, {
status: 'rejected',
reviewerId,
reviewedAt: new Date(),
rejectReason: dto.rejectReason,
});
// 退还商家余额
await queryRunner.manager.increment(
Merchant,
{ id: withdrawal.merchantId },
'walletBalance',
Number(withdrawal.amount),
);
await queryRunner.commitTransaction();
return { success: true };
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
// 确认打款
async payWithdrawal(id: number, reviewerId: number) {
const withdrawal = await this.withdrawalRepo.findOne({ where: { id } });
if (!withdrawal) throw new NotFoundException('提现记录不存在');
if (withdrawal.status !== 'approved') throw new BadRequestException('只能对已审核通过的提现确认打款');
await this.withdrawalRepo.update({ id }, {
status: 'paid',
paidAt: new Date(),
});
return { success: true };
}
// 平台收益统计
async getEarnings(dto: QueryEarningsDto) {
const qb = this.settlementRepo.createQueryBuilder('s');
if (dto.startDate && dto.endDate) {
qb.where('s.period_start BETWEEN :startDate AND :endDate', {
startDate: dto.startDate,
endDate: dto.endDate,
});
}
const settlementStats = await qb
.select('COUNT(*)', 'totalSettlements')
.addSelect('COALESCE(SUM(s.order_amount), 0)', 'totalOrderAmount')
.addSelect('COALESCE(SUM(s.commission_amount), 0)', 'totalCommission')
.addSelect('COALESCE(SUM(s.settlement_amount), 0)', 'totalSettlementAmount')
.getRawOne();
// 提现统计
const wQb = this.withdrawalRepo.createQueryBuilder('w');
const withdrawalStats = await wQb
.select('COUNT(*)', 'totalWithdrawals')
.addSelect('COALESCE(SUM(CASE WHEN w.status = \'paid\' THEN w.amount ELSE 0 END), 0)', 'paidAmount')
.getRawOne();
// 服务费统计
const serviceFeeStats = await this.orderRepo
.createQueryBuilder('o')
.where('o.status IN (:...statuses)', { statuses: ['completed', 'checked_in'] })
.select('COALESCE(SUM(o.service_fee), 0)', 'totalServiceFee')
.getRawOne();
return {
orderAmount: Number(settlementStats?.totalOrderAmount || 0),
commission: Number(settlementStats?.totalCommission || 0),
settlementAmount: Number(settlementStats?.totalSettlementAmount || 0),
totalSettlements: Number(settlementStats?.totalSettlements || 0),
paidAmount: Number(withdrawalStats?.paidAmount || 0),
totalWithdrawals: Number(withdrawalStats?.totalWithdrawals || 0),
totalServiceFee: Number(serviceFeeStats?.totalServiceFee || 0),
};
}
// ==================== 定时任务调用 ====================
// 生成周期对账单
async generateSettlements(periodStart: string, periodEnd: string) {
// 查找所有已审核通过的商家
const merchants = await this.merchantRepo.find({ where: { status: 'approved' } });
const results: { merchantId: number; settlementId: number; orderCount: number }[] = [];
for (const merchant of merchants) {
// 检查是否已生成过该周期的对账单
const existing = await this.settlementRepo.findOne({
where: { merchantId: merchant.id, periodStart, periodEnd },
});
if (existing) continue;
// 查询该周期内已完成的订单
const orders = await this.orderRepo
.createQueryBuilder('o')
.where('o.merchant_id = :merchantId', { merchantId: merchant.id })
.andWhere('o.status = :status', { status: 'completed' })
.andWhere('o.check_in_date >= :periodStart', { periodStart })
.andWhere('o.check_in_date <= :periodEnd', { periodEnd })
.getMany();
if (orders.length === 0) continue;
const orderAmount = orders.reduce((sum, o) => sum + Number(o.payAmount), 0);
const commissionRate = 0.10;
const commissionAmount = Math.round(orderAmount * commissionRate * 100) / 100;
const settlementAmount = Math.round((orderAmount - commissionAmount) * 100) / 100;
const settlementNo = `ST${Date.now()}${merchant.id}`;
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const settlement = queryRunner.manager.create(Settlement, {
merchantId: merchant.id,
settlementNo,
periodStart,
periodEnd,
orderCount: orders.length,
orderAmount,
commissionRate,
commissionAmount,
settlementAmount,
status: 'pending',
});
await queryRunner.manager.save(settlement);
// 保存对账单明细
const items = orders.map(o => queryRunner.manager.create(SettlementItem, {
settlementId: settlement.id,
orderId: o.id,
orderNo: o.orderNo,
orderAmount: Number(o.payAmount),
}));
await queryRunner.manager.save(items);
await queryRunner.commitTransaction();
results.push({ merchantId: merchant.id, settlementId: settlement.id, orderCount: orders.length });
} catch (err) {
await queryRunner.rollbackTransaction();
console.error(`生成对账单失败 merchantId=${merchant.id}:`, err.message);
} finally {
await queryRunner.release();
}
}
return results;
}
}