450 lines
15 KiB
TypeScript
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;
|
|
}
|
|
}
|