feat: 迭代

This commit is contained in:
2026-05-26 21:27:48 +08:00
parent 4c7a1e06a8
commit d1147713f8
43 changed files with 1137 additions and 883 deletions
@@ -0,0 +1,15 @@
/**
* 账户名称常量
* 数据库存储使用这些英文常量
*/
export const ACCOUNT_NAMES = {
/** 系统总账户 */
SYSTEM_MAIN: 'SYSTEM_MAIN',
/** 平台主账户 */
PLATFORM_MAIN: 'PLATFORM_MAIN',
/** 平台备用账户 */
PLATFORM_BACKUP: 'PLATFORM_BACKUP',
} as const;
export type AccountName = typeof ACCOUNT_NAMES[keyof typeof ACCOUNT_NAMES];
@@ -5,7 +5,7 @@ export class PlatformAccount {
@PrimaryGeneratedColumn({ type: 'bigint', unsigned: true })
id: number;
@Column({ type: 'varchar', length: 50, comment: '账户名称(如:主账户、备用账户' })
@Column({ type: 'varchar', length: 50, comment: '账户名称(如:PLATFORM_MAIN、PLATFORM_BACKUP' })
account_name: string;
@Column({ type: 'decimal', precision: 12, scale: 2, default: 0, comment: '可用余额(平台净收益 = total_income - total_expense' })
@@ -5,7 +5,7 @@ export class SystemAccount {
@PrimaryGeneratedColumn({ type: 'bigint', unsigned: true })
id: number;
@Column({ type: 'varchar', length: 50, comment: '账户名称(如:主账户' })
@Column({ type: 'varchar', length: 50, comment: '账户名称(如:SYSTEM_MAIN' })
account_name: string;
@Column({ type: 'decimal', precision: 12, scale: 2, default: 0, comment: '可用余额(total_income - total_refund - total_withdrawn' })
@@ -102,7 +102,7 @@ export class ActivityService {
firstOrderRate: 0.05,
secondOrderRate: 0.005,
minCashback: 0.01,
maxCashback: 50,
maxCashback: 1000,
withdrawThreshold: 10,
maxOrderIndex: 2,
},
@@ -67,4 +67,26 @@ export class ReportAdminController {
// TODO: 实现导出功能
return { message: '导出功能开发中' };
}
@Get('service-fees/statistics')
@ApiOperation({ summary: '服务费统计' })
async getServiceFeeStatistics() {
return this.reportService.getServiceFeeStatistics();
}
@Get('service-fees/list')
@ApiOperation({ summary: '服务费明细列表' })
async getServiceFeeList(
@Query('page') page: number = 1,
@Query('pageSize') pageSize: number = 20,
@Query('startDate') startDate?: string,
@Query('endDate') endDate?: string,
) {
return this.reportService.getServiceFeeList({
page: Number(page),
pageSize: Number(pageSize),
startDate,
endDate,
});
}
}
@@ -1,4 +1,4 @@
import { Module } from '@nestjs/common';
import { Module, forwardRef } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ActivityController } from './activity.controller';
import { ActivityService } from './activity.service';
@@ -22,7 +22,7 @@ import { FinanceModule } from '@/modules/shared/finance/finance.module';
Order,
User,
]),
FinanceModule,
forwardRef(() => FinanceModule),
],
controllers: [ActivityController],
providers: [ActivityService],
@@ -10,12 +10,10 @@ import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '@/common';
import { CurrentUser } from '@/common/decorators/current-user.decorator';
import { WithdrawalService } from '@/modules/shared/finance/withdrawal.service';
import { TransactionService } from '@/modules/shared/finance/transaction.service';
import { AccountService } from '@/modules/shared/finance/account.service';
import {
CreateUserWithdrawalDto,
QueryUserWithdrawalDto,
QueryTransactionDto,
} from '@/modules/shared/finance/dto/finance.dto';
@ApiTags('财务管理(用户)')
@@ -25,7 +23,6 @@ import {
export class FinanceUserController {
constructor(
private readonly withdrawalService: WithdrawalService,
private readonly transactionService: TransactionService,
private readonly accountService: AccountService,
) {}
@@ -63,23 +60,4 @@ export class FinanceUserController {
pageSize: dto.pageSize,
});
}
@Get('transactions')
@ApiOperation({ summary: '交易流水列表' })
async getTransactions(
@CurrentUser('sub') userId: number,
@Query() dto: QueryTransactionDto,
) {
const account = await this.accountService.getUserAccount(userId);
return this.transactionService.getUserTransactions({
accountId: account.id,
direction: dto.direction,
transactionType: dto.transactionType,
startDate: dto.startDate,
endDate: dto.endDate,
page: dto.page,
pageSize: dto.pageSize,
});
}
}
@@ -320,42 +320,59 @@ export class OrderService {
throw new BadRequestException('当前订单状态不可支付');
}
// 模拟支付成功
const paymentNo = `PAY${Date.now()}${Math.floor(Math.random() * 10000)}`;
await this.orderRepo.update(order.id, {
status: 'pending_confirm',
paymentMethod,
paymentNo,
paidAt: new Date(),
});
// 使用事务确保所有操作原子性
const queryRunner = this.orderRepo.manager.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
// 记录系统总账户收入(用户实付金额)
const transactionNo = `TXN${Date.now()}${Math.floor(Math.random() * 10000)}`;
await this.accountService.addSystemIncome(
order.payAmount,
transactionNo,
'order_payment',
order.id,
order.orderNo,
`用户支付订单:${order.orderNo}`,
);
// 扣减房态库存
const checkIn = new Date(order.checkInDate);
const checkOut = new Date(order.checkOutDate);
for (let d = new Date(checkIn); d < checkOut; d.setDate(d.getDate() + 1)) {
const dateStr = d.toISOString().split('T')[0];
const calendar = await this.calendarRepo.findOne({
where: { roomId: order.roomId, date: dateStr },
try {
// 1. 更新订单状态为已支付
const paymentNo = `PAY${Date.now()}${Math.floor(Math.random() * 10000)}`;
await queryRunner.manager.update(Order, order.id, {
status: 'pending_confirm',
paymentMethod,
paymentNo,
paidAt: new Date(),
});
if (calendar) {
await this.calendarRepo.update(calendar.id, {
// 2. 记录系统总账户收入(用户实付金额)
const transactionNo = `TXN${Date.now()}${Math.floor(Math.random() * 10000)}`;
await this.accountService.addSystemIncome(
order.payAmount,
transactionNo,
'order_payment',
order.id,
order.orderNo,
`用户支付订单:${order.orderNo}`,
);
// 3. 扣减房态库存
const checkIn = new Date(order.checkInDate);
const checkOut = new Date(order.checkOutDate);
for (let d = new Date(checkIn); d < checkOut; d.setDate(d.getDate() + 1)) {
const dateStr = d.toISOString().split('T')[0];
const calendar = await queryRunner.manager.findOne(RoomCalendar, {
where: { roomId: order.roomId, date: dateStr },
});
if (!calendar) {
throw new BadRequestException(`房态日历数据异常:${dateStr}`);
}
await queryRunner.manager.update(RoomCalendar, calendar.id, {
sold: calendar.sold + order.roomCount,
});
}
}
return { message: '支付成功', paymentNo };
// 提交事务
await queryRunner.commitTransaction();
return { message: '支付成功', paymentNo };
} catch (error) {
// 回滚事务
await queryRunner.rollbackTransaction();
throw error;
} finally {
// 释放连接
await queryRunner.release();
}
}
/**
@@ -368,42 +385,59 @@ export class OrderService {
throw new BadRequestException('当前订单状态不可支付');
}
// 模拟支付成功
const paymentNo = `PAY${Date.now()}${Math.floor(Math.random() * 10000)}`;
await this.orderRepo.update(id, {
status: 'pending_confirm',
paymentMethod,
paymentNo,
paidAt: new Date(),
});
// 使用事务确保所有操作原子性
const queryRunner = this.orderRepo.manager.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
// 记录系统总账户收入(用户实付金额)
const transactionNo = `TXN${Date.now()}${Math.floor(Math.random() * 10000)}`;
await this.accountService.addSystemIncome(
order.payAmount,
transactionNo,
'order_payment',
order.id,
order.orderNo,
`用户支付订单:${order.orderNo}`,
);
// 扣减房态库存
const checkIn = new Date(order.checkInDate);
const checkOut = new Date(order.checkOutDate);
for (let d = new Date(checkIn); d < checkOut; d.setDate(d.getDate() + 1)) {
const dateStr = d.toISOString().split('T')[0];
const calendar = await this.calendarRepo.findOne({
where: { roomId: order.roomId, date: dateStr },
try {
// 1. 更新订单状态为已支付
const paymentNo = `PAY${Date.now()}${Math.floor(Math.random() * 10000)}`;
await queryRunner.manager.update(Order, id, {
status: 'pending_confirm',
paymentMethod,
paymentNo,
paidAt: new Date(),
});
if (calendar) {
await this.calendarRepo.update(calendar.id, {
// 2. 记录系统总账户收入(用户实付金额)
const transactionNo = `TXN${Date.now()}${Math.floor(Math.random() * 10000)}`;
await this.accountService.addSystemIncome(
order.payAmount,
transactionNo,
'order_payment',
order.id,
order.orderNo,
`用户支付订单:${order.orderNo}`,
);
// 3. 扣减房态库存
const checkIn = new Date(order.checkInDate);
const checkOut = new Date(order.checkOutDate);
for (let d = new Date(checkIn); d < checkOut; d.setDate(d.getDate() + 1)) {
const dateStr = d.toISOString().split('T')[0];
const calendar = await queryRunner.manager.findOne(RoomCalendar, {
where: { roomId: order.roomId, date: dateStr },
});
if (!calendar) {
throw new BadRequestException(`房态日历数据异常:${dateStr}`);
}
await queryRunner.manager.update(RoomCalendar, calendar.id, {
sold: calendar.sold + order.roomCount,
});
}
}
return { message: '支付成功', paymentNo };
// 提交事务
await queryRunner.commitTransaction();
return { message: '支付成功', paymentNo };
} catch (error) {
// 回滚事务
await queryRunner.rollbackTransaction();
throw error;
} finally {
// 释放连接
await queryRunner.release();
}
}
/**
@@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { MerchantAccount } from '@/entities/merchant-account.entity';
import { MerchantTransaction } from '@/entities/merchant-transaction.entity';
import { SettlementService } from '@/modules/shared/finance/settlement.service';
@Injectable()
export class MerchantFinanceService {
@@ -11,6 +12,7 @@ export class MerchantFinanceService {
private merchantAccountRepo: Repository<MerchantAccount>,
@InjectRepository(MerchantTransaction)
private merchantTransactionRepo: Repository<MerchantTransaction>,
private settlementService: SettlementService,
) {}
async getMerchantAccount(merchantId: number) {
@@ -33,6 +35,9 @@ export class MerchantFinanceService {
await this.merchantAccountRepo.save(account);
}
// 动态计算待结算金额
const pendingSettlement = await this.settlementService.getPendingSettlementAmount(merchantId);
// 计算可用余额并返回格式化的数据
return {
id: account.id,
@@ -43,7 +48,7 @@ export class MerchantFinanceService {
totalExpense: Number(account.total_expense),
totalSettlement: Number(account.total_settlement),
totalWithdraw: Number(account.total_withdraw),
pendingSettlement: Number(account.pending_settlement),
pendingSettlement: Number(pendingSettlement),
debtAmount: Number(account.debt_amount),
status: account.status,
lastSettlementAt: account.last_settlement_at,
@@ -93,4 +93,24 @@ export class SettlementMerchantController {
pageSize
};
}
@Get('pending/orders')
@ApiOperation({ summary: '查询待结算订单列表' })
async getPendingSettlementOrders(
@CurrentSeller('sub') sellerId: number,
@Query('page') page: number = 1,
@Query('pageSize') pageSize: number = 20,
) {
const merchantId = await this.getMerchantId(sellerId);
return this.settlementService.getPendingSettlementOrders(merchantId, page, pageSize);
}
@Get('pending/summary')
@ApiOperation({ summary: '查询待结算订单统计' })
async getPendingSettlementSummary(
@CurrentSeller('sub') sellerId: number,
) {
const merchantId = await this.getMerchantId(sellerId);
return this.settlementService.getPendingSettlementSummary(merchantId);
}
}
@@ -1,4 +1,4 @@
import { Module } from '@nestjs/common';
import { Module, forwardRef } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MerchantOrderController } from './order.controller';
import { MerchantOrderService } from './order.service';
@@ -6,11 +6,15 @@ import { Order } from '@/entities/order.entity';
import { Room } from '@/entities/room.entity';
import { RoomCalendar } from '@/entities/room-calendar.entity';
import { MerchantProfileModule } from '../profile/profile.module';
import { UserActivityModule } from '@/modules/app/activity/activity.module';
import { FinanceModule } from '@/modules/shared/finance/finance.module';
@Module({
imports: [
TypeOrmModule.forFeature([Order, Room, RoomCalendar]),
MerchantProfileModule,
forwardRef(() => UserActivityModule),
forwardRef(() => FinanceModule),
],
controllers: [MerchantOrderController],
providers: [MerchantOrderService],
@@ -1,13 +1,18 @@
import { Injectable, NotFoundException, BadRequestException, ForbiddenException } from '@nestjs/common';
import { Injectable, NotFoundException, BadRequestException, ForbiddenException, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Order } from '@/entities/order.entity';
import { Room } from '@/entities/room.entity';
import { RoomCalendar } from '@/entities/room-calendar.entity';
import { QueryOrderDto } from './dto/order.dto';
import { ActivityService } from '@/modules/app/activity/activity.service';
import { AccountService } from '@/modules/shared/finance/account.service';
import { TransactionService } from '@/modules/shared/finance/transaction.service';
@Injectable()
export class MerchantOrderService {
private readonly logger = new Logger(MerchantOrderService.name);
constructor(
@InjectRepository(Order)
private orderRepo: Repository<Order>,
@@ -15,6 +20,9 @@ export class MerchantOrderService {
private roomRepo: Repository<Room>,
@InjectRepository(RoomCalendar)
private calendarRepo: Repository<RoomCalendar>,
private readonly activityService: ActivityService,
private readonly accountService: AccountService,
private readonly transactionService: TransactionService,
) {}
async findByMerchant(merchantId: number, query: QueryOrderDto) {
@@ -111,6 +119,51 @@ export class MerchantOrderService {
checkoutAt: new Date(),
});
// 订单完成后立即结算服务费给平台账户
try {
const transactionNo = this.transactionService.generateTransactionNo();
const serviceFee = Number(order.serviceFee || 0);
if (serviceFee > 0) {
await this.accountService.addPlatformServiceFee(
serviceFee,
transactionNo,
'order_complete',
order.id,
order.orderNo,
`订单完成服务费 - ${order.orderNo}`,
);
this.logger.log(`订单 ${orderNo} 完成,平台服务费 ${serviceFee} 元已入账`);
}
} catch (error) {
this.logger.error(`订单 ${orderNo} 服务费结算失败: ${error.message}`);
// 不影响订单完成流程,只记录错误
}
// 增加商家待结算金额
try {
const merchantIncome = Number(order.merchantIncome || 0);
if (merchantIncome > 0) {
await this.accountService.addMerchantPendingSettlement(
order.merchantId,
merchantIncome,
);
this.logger.log(`订单 ${orderNo} 完成,商家待结算金额 ${merchantIncome} 元已增加`);
}
} catch (error) {
this.logger.error(`订单 ${orderNo} 商家待结算金额更新失败: ${error.message}`);
// 不影响订单完成流程,只记录错误
}
// 触发邀请返现
try {
await this.activityService.handleOrderCompleted(order.id);
this.logger.log(`订单 ${orderNo} 完成,已触发邀请返现处理`);
} catch (error) {
this.logger.error(`订单 ${orderNo} 邀请返现处理失败: ${error.message}`);
// 不影响订单完成流程,只记录错误
}
return { message: '已确认离店,订单已完成' };
}
}
@@ -10,6 +10,7 @@ import { MerchantTransaction } from '@/entities/merchant-transaction.entity';
import { PlatformTransaction } from '@/entities/platform-transaction.entity';
import { SystemTransaction } from '@/entities/system-transaction.entity';
import { QueryUserAccountsDto, QueryMerchantAccountsDto } from './dto/account.dto';
import { ACCOUNT_NAMES } from '@/constants/account.constant';
@Injectable()
export class AccountService {
@@ -82,7 +83,7 @@ export class AccountService {
/**
* 获取平台账户
*/
async getPlatformAccount(accountName: string = '主账户'): Promise<PlatformAccount> {
async getPlatformAccount(accountName: string = ACCOUNT_NAMES.PLATFORM_MAIN): Promise<PlatformAccount> {
const account = await this.platformAccountRepo.findOne({ where: { account_name: accountName } });
if (!account) {
@@ -246,12 +247,15 @@ export class AccountService {
}
const balance = Number(account.balance);
if (balance < amount) {
throw new BadRequestException('账户余额不足');
const frozenBalance = Number(account.frozen_balance);
const availableBalance = balance - frozenBalance;
if (availableBalance < amount) {
throw new BadRequestException('可用余额不足');
}
account.balance = balance - amount;
account.frozen_balance = Number(account.frozen_balance) + amount;
// 只增加冻结金额,不减少余额
account.frozen_balance = frozenBalance + amount;
account.version += 1;
await queryRunner.manager.save(account);
@@ -287,7 +291,7 @@ export class AccountService {
throw new BadRequestException('冻结余额不足');
}
account.balance = Number(account.balance) + amount;
// 只减少冻结金额,不增加余额(因为余额本来就没减少)
account.frozen_balance = frozenBalance - amount;
account.version += 1;
@@ -303,6 +307,7 @@ export class AccountService {
/**
* 商家账户增加余额(结算)
* 同时减少待结算金额
*/
async addMerchantBalance(
merchantId: number,
@@ -336,6 +341,8 @@ export class AccountService {
account.balance = balanceAfter;
account.total_income = Number(account.total_income) + amount;
// 结算时减少待结算金额
account.pending_settlement = Math.max(0, Number(account.pending_settlement) - amount);
account.version += 1;
await queryRunner.manager.save(account);
@@ -366,6 +373,40 @@ export class AccountService {
}
}
/**
* 商家账户增加待结算金额(订单完成时)
*/
async addMerchantPendingSettlement(
merchantId: number,
amount: number,
): Promise<void> {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const account = await queryRunner.manager.findOne(MerchantAccount, {
where: { merchant_id: merchantId },
lock: { mode: 'pessimistic_write' },
});
if (!account) {
throw new NotFoundException('商家账户不存在');
}
account.pending_settlement = Number(account.pending_settlement) + amount;
account.version += 1;
await queryRunner.manager.save(account);
await queryRunner.commitTransaction();
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
/**
* 商家账户扣减余额(提现)
*/
@@ -451,7 +492,7 @@ export class AccountService {
try {
const account = await queryRunner.manager.findOne(PlatformAccount, {
where: { account_name: '主账户' },
where: { account_name: ACCOUNT_NAMES.PLATFORM_MAIN },
lock: { mode: 'pessimistic_write' },
});
@@ -510,7 +551,7 @@ export class AccountService {
try {
const account = await queryRunner.manager.findOne(PlatformAccount, {
where: { account_name: '主账户' },
where: { account_name: ACCOUNT_NAMES.PLATFORM_MAIN },
lock: { mode: 'pessimistic_write' },
});
@@ -569,7 +610,7 @@ export class AccountService {
try {
const account = await queryRunner.manager.findOne(PlatformAccount, {
where: { account_name: '主账户' },
where: { account_name: ACCOUNT_NAMES.PLATFORM_MAIN },
lock: { mode: 'pessimistic_write' },
});
@@ -773,7 +814,7 @@ export class AccountService {
/**
* 获取系统总账户
*/
async getSystemAccount(accountName: string = '主账户'): Promise<SystemAccount> {
async getSystemAccount(accountName: string = ACCOUNT_NAMES.SYSTEM_MAIN): Promise<SystemAccount> {
const account = await this.systemAccountRepo.findOne({ where: { account_name: accountName } });
if (!account) {
@@ -802,7 +843,7 @@ export class AccountService {
try {
const account = await queryRunner.manager.findOne(SystemAccount, {
where: { account_name: '主账户' },
where: { account_name: ACCOUNT_NAMES.SYSTEM_MAIN },
lock: { mode: 'pessimistic_write' },
});
@@ -861,7 +902,7 @@ export class AccountService {
try {
const account = await queryRunner.manager.findOne(SystemAccount, {
where: { account_name: '主账户' },
where: { account_name: ACCOUNT_NAMES.SYSTEM_MAIN },
lock: { mode: 'pessimistic_write' },
});
@@ -920,7 +961,7 @@ export class AccountService {
try {
const account = await queryRunner.manager.findOne(SystemAccount, {
where: { account_name: '主账户' },
where: { account_name: ACCOUNT_NAMES.SYSTEM_MAIN },
lock: { mode: 'pessimistic_write' },
});
@@ -1,4 +1,4 @@
import { Module } from '@nestjs/common';
import { Module, forwardRef } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Settlement } from '@/entities/settlement.entity';
import { SettlementItem } from '@/entities/settlement-item.entity';
@@ -50,7 +50,7 @@ import { MerchantModule } from '@/modules/merchant/merchant.module';
Merchant,
Order,
]),
MerchantModule,
forwardRef(() => MerchantModule),
],
providers: [
SettlementService,
@@ -14,6 +14,7 @@ import { Settlement } from '@/entities/settlement.entity';
import { UserWithdrawal } from '@/entities/user-withdrawal.entity';
import { MerchantWithdrawal } from '@/entities/merchant-withdrawal.entity';
import { PlatformWithdrawal } from '@/entities/platform-withdrawal.entity';
import { ACCOUNT_NAMES } from '@/constants/account.constant';
import dayjs from 'dayjs';
@Injectable()
@@ -68,7 +69,7 @@ export class ReconciliationService {
*/
async performReconciliation(date: string) {
const platformAccount = await this.platformAccountRepo.findOne({
where: { account_name: '主账户' }
where: { account_name: ACCOUNT_NAMES.PLATFORM_MAIN }
});
const platformBalanceStart = Number(platformAccount?.balance || 0);
@@ -221,7 +222,7 @@ export class ReconciliationService {
*/
async getAccountSummary() {
const platformAccount = await this.platformAccountRepo.findOne({
where: { account_name: '主账户' }
where: { account_name: ACCOUNT_NAMES.PLATFORM_MAIN }
});
const merchantStats = await this.merchantAccountRepo
@@ -12,6 +12,7 @@ import { MerchantWithdrawal } from '@/entities/merchant-withdrawal.entity';
import { UserWithdrawal } from '@/entities/user-withdrawal.entity';
import { PlatformWithdrawal } from '@/entities/platform-withdrawal.entity';
import { MktInviteWithdrawal } from '@/entities/mkt-invite-withdrawal.entity';
import { ACCOUNT_NAMES } from '@/constants/account.constant';
import dayjs from 'dayjs';
@Injectable()
@@ -47,17 +48,18 @@ export class ReportService {
async getOverview() {
// 获取系统总账户
const systemAccount = await this.systemAccountRepo.findOne({
where: { account_name: '系统总账户' },
where: { account_name: ACCOUNT_NAMES.SYSTEM_MAIN },
});
// 获取平台账户
const platformAccount = await this.platformAccountRepo.findOne({
where: { account_name: '主账户' },
where: { account_name: ACCOUNT_NAMES.PLATFORM_MAIN },
});
const merchantStats = await this.merchantAccountRepo
.createQueryBuilder('a')
.select('SUM(a.balance)', 'totalBalance')
.addSelect('SUM(a.pending_settlement)', 'totalPendingSettlement')
.addSelect('COUNT(*)', 'count')
.getRawOne();
@@ -88,24 +90,34 @@ export class ReportService {
.andWhere('t.direction = :direction', { direction: 'expense' })
.getRawOne();
// 系统总账户金额(直接从数据库读取)
const systemTotalAmount = Number(systemAccount?.balance || 0);
// 各层级账户余额
const totalMerchantBalance = Number(merchantStats?.totalBalance || 0);
const totalMerchantPendingSettlement = Number(merchantStats?.totalPendingSettlement || 0);
const totalUserBalance = Number(userStats?.totalBalance || 0);
const platformBalance = Number(platformAccount?.balance || 0);
return {
// 系统总账户金额(用户实付 - 退款 - 提现
systemTotalAmount: Number(systemAccount?.balance || 0),
// 系统总账户金额(从 system_accounts 表读取
systemTotalAmount,
totalUserPaid: Number(systemAccount?.total_income || 0),
totalRefund: Number(systemAccount?.total_refund || 0),
totalWithdrawn: Number(systemAccount?.total_withdrawn || 0),
// 平台净收益(服务费 - 邀请返现)
platformBalance: Number(platformAccount?.balance || 0),
platformBalance,
platformTotalIncome: Number(platformAccount?.total_income || 0),
platformTotalExpense: Number(platformAccount?.total_expense || 0),
// 商家账户统计
totalMerchantBalance: Number(merchantStats?.totalBalance || 0),
totalMerchantBalance,
totalMerchantPendingSettlement,
merchantCount: Number(merchantStats?.count || 0),
// 用户账户统计
totalUserBalance: Number(userStats?.totalBalance || 0),
totalUserBalance,
userCount: Number(userStats?.count || 0),
// 今日统计
@@ -314,4 +326,123 @@ export class ReportService {
netAmount: Number(income?.sum || 0) - Number(expense?.sum || 0),
};
}
/**
* 服务费统计
*/
async getServiceFeeStatistics() {
const today = dayjs().format('YYYY-MM-DD');
const monthStart = dayjs().startOf('month').format('YYYY-MM-DD');
const monthEnd = dayjs().endOf('month').format('YYYY-MM-DD');
// 今日服务费
const todayFee = await this.orderRepo
.createQueryBuilder('o')
.select('SUM(o.service_fee)', 'sum')
.where('DATE(o.created_at) = :date', { date: today })
.andWhere('o.status != :status', { status: 'cancelled' })
.getRawOne();
// 本月服务费
const monthFee = await this.orderRepo
.createQueryBuilder('o')
.select('SUM(o.service_fee)', 'sum')
.where('DATE(o.created_at) BETWEEN :start AND :end', {
start: monthStart,
end: monthEnd,
})
.andWhere('o.status != :status', { status: 'cancelled' })
.getRawOne();
// 累计服务费
const totalFee = await this.orderRepo
.createQueryBuilder('o')
.select('SUM(o.service_fee)', 'sum')
.where('o.status != :status', { status: 'cancelled' })
.getRawOne();
// 待结算服务费(已完成但未结算的订单)
const pendingFee = await this.orderRepo
.createQueryBuilder('o')
.leftJoin('settlement_items', 'si', 'si.order_id = o.id')
.select('SUM(o.service_fee)', 'sum')
.where('o.status = :status', { status: 'completed' })
.andWhere('si.id IS NULL')
.getRawOne();
return {
todayCommission: Number(todayFee?.sum || 0),
monthCommission: Number(monthFee?.sum || 0),
totalCommission: Number(totalFee?.sum || 0),
pendingCommission: Number(pendingFee?.sum || 0),
};
}
/**
* 服务费明细列表
*/
async getServiceFeeList(params: {
page: number;
pageSize: number;
startDate?: string;
endDate?: string;
}) {
const { page, pageSize, startDate, endDate } = params;
const query = this.orderRepo
.createQueryBuilder('o')
.leftJoinAndSelect('o.merchant', 'm')
.leftJoinAndSelect('o.user', 'u')
.leftJoin('settlement_items', 'si', 'si.order_id = o.id')
.leftJoin('settlements', 's', 's.id = si.settlement_id')
.select([
'o.id',
'o.orderNo',
'o.totalAmount',
'o.serviceFee',
'o.status',
'o.createdAt',
'm.id',
'm.name',
'u.id',
'u.nickname',
's.settledAt',
])
.where('o.status != :status', { status: 'cancelled' })
.andWhere('o.service_fee > 0');
if (startDate && endDate) {
query.andWhere('DATE(o.created_at) BETWEEN :startDate AND :endDate', {
startDate,
endDate,
});
}
const [list, total] = await query
.orderBy('o.created_at', 'DESC')
.skip((page - 1) * pageSize)
.take(pageSize)
.getManyAndCount();
const result = list.map((order) => ({
id: order.id,
orderId: order.id,
orderNo: order.orderNo,
merchantName: order.merchant?.shopName || '-',
userName: order.user?.nickname || '-',
orderAmount: Number(order.totalAmount),
commissionRate: 0.05, // 固定5%,如果需要动态获取可以从配置读取
commissionAmount: Number(order.serviceFee),
status: order['s_settledAt'] ? 'settled' : 'pending',
settledAt: order['s_settledAt'] || null,
createdAt: order.createdAt,
}));
return {
list: result,
total,
page,
pageSize,
};
}
}
@@ -51,33 +51,66 @@ export class SettlementService {
throw new BadRequestException(`该周期 ${lastWeekStart} ~ ${lastWeekEnd} 已经结算过,无法重复结算`);
}
const orders = await this.orderRepo
// 查询所有已完成且截止到上周末的订单
this.logger.log(`查询条件: status=completed, checkout_at <= ${lastWeekEnd} 23:59:59`);
const allOrders = await this.orderRepo
.createQueryBuilder('o')
.where('o.status = :status', { status: 'completed' })
.andWhere('o.merchant_id IS NOT NULL')
.andWhere('o.checkout_at BETWEEN :start AND :end', {
start: `${lastWeekStart} 00:00:00`,
.andWhere('o.checkout_at IS NOT NULL')
.andWhere('o.checkout_at <= :end', {
end: `${lastWeekEnd} 23:59:59`
})
.getMany();
if (orders.length === 0) {
this.logger.log(`查询到 ${allOrders.length} 个已完成订单`);
if (allOrders.length === 0) {
this.logger.log('没有需要结算的订单');
throw new BadRequestException('该周期内没有需要结算的订单');
}
const ordersByMerchant = orders.reduce((acc, order) => {
// 批量查询已结算的订单ID
const orderIds = allOrders.map(o => o.id);
const settledItems = await this.settlementItemRepo
.createQueryBuilder('si')
.select('si.order_id')
.where('si.order_id IN (:...orderIds)', { orderIds })
.getRawMany();
const settledOrderIds = new Set(settledItems.map(item => item.order_id));
// 过滤出未结算的订单,并按商家分组
const ordersByMerchant: Record<number, Order[]> = {};
let skippedCount = 0;
for (const order of allOrders) {
const merchantId = order.merchantId;
if (!merchantId || isNaN(merchantId)) {
this.logger.warn(`订单 ${order.id} 的 merchantId 无效: ${merchantId}`);
return acc;
continue;
}
if (!acc[merchantId]) {
acc[merchantId] = [];
// 检查该订单是否已经被结算过
if (settledOrderIds.has(order.id)) {
skippedCount++;
continue;
}
acc[merchantId].push(order);
return acc;
}, {} as Record<number, Order[]>);
if (!ordersByMerchant[merchantId]) {
ordersByMerchant[merchantId] = [];
}
ordersByMerchant[merchantId].push(order);
}
this.logger.log(`共查询到 ${allOrders.length} 个订单,已结算 ${skippedCount} 个,待结算 ${allOrders.length - skippedCount}`);
if (Object.keys(ordersByMerchant).length === 0) {
this.logger.log('没有需要结算的订单(所有订单都已结算)');
throw new BadRequestException('没有需要结算的订单');
}
let successCount = 0;
let failCount = 0;
@@ -96,7 +129,7 @@ export class SettlementService {
}
this.logger.log(`周结算任务执行完成,成功:${successCount},失败:${failCount}`);
return { successCount, failCount, totalOrders: orders.length };
return { successCount, failCount, totalOrders: allOrders.length - skippedCount };
} catch (error) {
this.logger.error(`周结算任务执行失败:${error.message}`);
throw error;
@@ -333,6 +366,93 @@ export class SettlementService {
return orderAmount - serviceFee;
}
/**
* 获取商家待结算订单列表
*/
async getPendingSettlementOrders(merchantId: number, page: number = 1, pageSize: number = 20) {
const lastSettlement = await this.settlementRepo.findOne({
where: { merchantId },
order: { settledAt: 'DESC' }
});
const startDate = lastSettlement
? dayjs(lastSettlement.periodEnd).add(1, 'day').format('YYYY-MM-DD')
: '2020-01-01';
const queryBuilder = this.orderRepo.createQueryBuilder('order')
.leftJoinAndSelect('order.room', 'room')
.leftJoinAndSelect('order.user', 'user')
.where('order.merchantId = :merchantId', { merchantId })
.andWhere('order.status = :status', { status: 'completed' })
.andWhere('order.checkoutAt IS NOT NULL')
.andWhere('order.checkoutAt >= :startDate', { startDate: `${startDate} 00:00:00` })
.andWhere('order.checkoutAt <= :endDate', { endDate: new Date() })
.orderBy('order.checkoutAt', 'DESC');
const skip = (page - 1) * pageSize;
queryBuilder.skip(skip).take(pageSize);
const [orders, total] = await queryBuilder.getManyAndCount();
return {
list: orders.map(order => ({
id: order.id,
orderNo: order.orderNo,
roomName: order.room?.name || '',
checkInDate: order.checkInDate,
checkOutDate: order.checkOutDate,
nights: order.nights,
payAmount: Number(order.payAmount),
serviceFee: Number(order.serviceFee || 0),
merchantIncome: Number(order.merchantIncome || 0),
checkoutAt: order.checkoutAt,
contactName: order.contactName,
contactPhone: order.contactPhone,
})),
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize)
};
}
/**
* 获取商家待结算订单统计
*/
async getPendingSettlementSummary(merchantId: number) {
const lastSettlement = await this.settlementRepo.findOne({
where: { merchantId },
order: { settledAt: 'DESC' }
});
const startDate = lastSettlement
? dayjs(lastSettlement.periodEnd).add(1, 'day').format('YYYY-MM-DD')
: '2020-01-01';
const orders = await this.orderRepo
.createQueryBuilder('o')
.where('o.merchant_id = :merchantId', { merchantId })
.andWhere('o.status = :status', { status: 'completed' })
.andWhere('o.checkout_at IS NOT NULL')
.andWhere('o.checkout_at >= :startDate', { startDate: `${startDate} 00:00:00` })
.andWhere('o.checkout_at <= :endDate', { endDate: new Date() })
.getMany();
const orderCount = orders.length;
const totalPayAmount = orders.reduce((sum, o) => sum + Number(o.payAmount), 0);
const totalServiceFee = orders.reduce((sum, o) => sum + Number(o.serviceFee || 0), 0);
const totalMerchantIncome = orders.reduce((sum, o) => sum + Number(o.merchantIncome || 0), 0);
return {
orderCount,
totalPayAmount,
totalServiceFee,
totalMerchantIncome,
startDate,
lastSettlementDate: lastSettlement?.settledAt || null,
};
}
/**
* 预览周结算数据(不实际执行)
*/