feat: 迭代
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览周结算数据(不实际执行)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user