feat: 迭代

This commit is contained in:
2026-06-01 09:36:52 +08:00
parent e8bce5e924
commit f021b43f05
38 changed files with 1785 additions and 88 deletions
+9 -5
View File
@@ -30,11 +30,15 @@ WECHAT_APPID=wx6b2d69c900f8f93a
WECHAT_SECRET=
# 微信支付配置
WECHAT_MCHID=
WECHAT_SERIAL_NO=
WECHAT_APIV3_KEY=
WECHAT_PRIVATE_KEY=
WECHAT_REFUND_NOTIFY_URL=https://your-domain.com/api/payment/wechat/refund-notify
WECHAT_MCHID=1234567890
WECHAT_SERIAL_NO=your_certificate_serial_number
WECHAT_APIV3_KEY=your_32_character_apiv3_key_here
WECHAT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY_CONTENT_HERE\n-----END PRIVATE KEY-----"
WECHAT_PAY_NOTIFY_URL=https://yourdomain.com/api/app/payment/wechat/notify
WECHAT_REFUND_NOTIFY_URL=https://yourdomain.com/api/app/payment/wechat/refund-notify
# API基础地址
API_BASE_URL=https://yourdomain.com
# 支付宝小程序
ALIPAY_APPID=
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { WechatPayService } from './wechat-pay.service';
@Module({
imports: [ConfigModule],
providers: [WechatPayService],
exports: [WechatPayService],
})
export class PaymentModule {}
@@ -0,0 +1,199 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Wechatpay, Payment } from 'wechatpay-node-v3';
@Injectable()
export class WechatPayService {
private readonly logger = new Logger(WechatPayService.name);
private pay: Wechatpay;
constructor(private configService: ConfigService) {
const appid = this.configService.get<string>('WECHAT_APPID');
const mchid = this.configService.get<string>('WECHAT_MCHID');
const privateKey = this.configService.get<string>('WECHAT_PRIVATE_KEY');
const serialNo = this.configService.get<string>('WECHAT_SERIAL_NO');
const apiv3Key = this.configService.get<string>('WECHAT_APIV3_KEY');
if (!appid || !mchid || !privateKey || !serialNo || !apiv3Key) {
this.logger.warn('微信支付配置不完整,支付功能将不可用');
return;
}
this.pay = new Wechatpay({
appid,
mchid,
privateKey: Buffer.from(privateKey.replace(/\\n/g, '\n')),
serialNo,
apiv3Key,
});
}
/**
* JSAPI下单(小程序支付)
*/
async createJsapiOrder(params: {
orderNo: string;
description: string;
amount: number;
openid: string;
notifyUrl: string;
}) {
if (!this.pay) {
throw new Error('微信支付未配置');
}
try {
const result = await this.pay.transactions_jsapi({
appid: this.configService.get<string>('WECHAT_APPID'),
mchid: this.configService.get<string>('WECHAT_MCHID'),
description: params.description,
out_trade_no: params.orderNo,
notify_url: params.notifyUrl,
amount: {
total: Math.round(params.amount * 100), // 转换为分
currency: 'CNY',
},
payer: {
openid: params.openid,
},
});
this.logger.log(`微信支付下单成功: ${params.orderNo}`);
return result;
} catch (error) {
this.logger.error(`微信支付下单失败: ${error.message}`, error.stack);
throw new Error(`微信支付下单失败: ${error.message}`);
}
}
/**
* 查询订单
*/
async queryOrder(orderNo: string) {
if (!this.pay) {
throw new Error('微信支付未配置');
}
try {
const result = await this.pay.query({
out_trade_no: orderNo,
});
return result;
} catch (error) {
this.logger.error(`查询微信支付订单失败: ${error.message}`, error.stack);
throw new Error(`查询订单失败: ${error.message}`);
}
}
/**
* 关闭订单
*/
async closeOrder(orderNo: string) {
if (!this.pay) {
throw new Error('微信支付未配置');
}
try {
await this.pay.close({
out_trade_no: orderNo,
});
this.logger.log(`关闭微信支付订单: ${orderNo}`);
} catch (error) {
this.logger.error(`关闭微信支付订单失败: ${error.message}`, error.stack);
throw new Error(`关闭订单失败: ${error.message}`);
}
}
/**
* 申请退款
*/
async refund(params: {
orderNo: string;
refundNo: string;
totalAmount: number;
refundAmount: number;
reason: string;
notifyUrl?: string;
}) {
if (!this.pay) {
throw new Error('微信支付未配置');
}
try {
const result = await this.pay.refunds({
out_trade_no: params.orderNo,
out_refund_no: params.refundNo,
reason: params.reason,
notify_url: params.notifyUrl,
amount: {
refund: Math.round(params.refundAmount * 100),
total: Math.round(params.totalAmount * 100),
currency: 'CNY',
},
});
this.logger.log(`微信支付退款成功: ${params.refundNo}`);
return result;
} catch (error) {
this.logger.error(`微信支付退款失败: ${error.message}`, error.stack);
throw new Error(`退款失败: ${error.message}`);
}
}
/**
* 查询退款
*/
async queryRefund(refundNo: string) {
if (!this.pay) {
throw new Error('微信支付未配置');
}
try {
const result = await this.pay.find_refunds({
out_refund_no: refundNo,
});
return result;
} catch (error) {
this.logger.error(`查询微信退款失败: ${error.message}`, error.stack);
throw new Error(`查询退款失败: ${error.message}`);
}
}
/**
* 验证回调签名
*/
verifySignature(params: {
timestamp: string;
nonce: string;
body: string;
signature: string;
serial: string;
}): boolean {
if (!this.pay) {
throw new Error('微信支付未配置');
}
try {
return this.pay.verifySign(params);
} catch (error) {
this.logger.error(`验证微信支付签名失败: ${error.message}`, error.stack);
return false;
}
}
/**
* 解密回调数据
*/
decryptData(ciphertext: string, nonce: string, associatedData: string): any {
if (!this.pay) {
throw new Error('微信支付未配置');
}
try {
return this.pay.decipher_gcm(ciphertext, associatedData, nonce);
} catch (error) {
this.logger.error(`解密微信支付数据失败: ${error.message}`, error.stack);
throw new Error(`解密数据失败: ${error.message}`);
}
}
}
@@ -0,0 +1,21 @@
export enum AdminRole {
SUPER_ADMIN = 'super_admin',
ADMIN = 'admin',
OPERATOR = 'operator',
}
export enum AdminStatus {
ACTIVE = 'active',
FROZEN = 'frozen',
}
export const ADMIN_ROLE_LABELS = {
[AdminRole.SUPER_ADMIN]: '超级管理员',
[AdminRole.ADMIN]: '管理员',
[AdminRole.OPERATOR]: '运营人员',
};
export const ADMIN_STATUS_LABELS = {
[AdminStatus.ACTIVE]: '正常',
[AdminStatus.FROZEN]: '冻结',
};
@@ -1,6 +1,7 @@
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';
import { AdminRole } from '../constants/admin.constant';
@Injectable()
export class RolesGuard implements CanActivate {
@@ -17,6 +18,12 @@ export class RolesGuard implements CanActivate {
}
const { user } = context.switchToHttp().getRequest();
// 超级管理员拥有所有权限
if (user?.role === AdminRole.SUPER_ADMIN) {
return true;
}
if (!user || !requiredRoles.includes(user.role)) {
throw new ForbiddenException('无权限访问');
}
+2 -1
View File
@@ -5,4 +5,5 @@ export * from './guards/seller-jwt-auth.guard';
export * from './guards/roles.guard';
export * from './decorators/roles.decorator';
export * from './decorators/current-user.decorator';
export * from './decorators/current-seller.decorator';
export * from './decorators/current-seller.decorator';
export * from './constants/admin.constant';
+5 -4
View File
@@ -1,4 +1,5 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm';
import { AdminRole, AdminStatus } from '@/common/constants/admin.constant';
@Entity('admins')
export class Admin {
@@ -22,12 +23,12 @@ export class Admin {
email: string;
@Index()
@Column({ type: 'enum', enum: ['super_admin', 'admin', 'operator'], default: 'admin', comment: '角色' })
role: 'super_admin' | 'admin' | 'operator';
@Column({ type: 'enum', enum: AdminRole, default: AdminRole.ADMIN, comment: '角色' })
role: AdminRole;
@Index()
@Column({ type: 'enum', enum: ['active', 'frozen'], default: 'active', comment: '状态' })
status: 'active' | 'frozen';
@Column({ type: 'enum', enum: AdminStatus, default: AdminStatus.ACTIVE, comment: '状态' })
status: AdminStatus;
@Column({ name: 'last_login_at', type: 'datetime', nullable: true, comment: '最后登录时间' })
lastLoginAt: Date;
@@ -0,0 +1,61 @@
import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam } from '@nestjs/swagger';
import { AdminManageService } from './admin-manage.service';
import { CreateAdminDto, UpdateAdminDto, UpdateAdminPasswordDto, QueryAdminDto } from './dto/admin.dto';
import { JwtAuthGuard, RolesGuard, Roles, AdminRole } from '@/common';
@ApiTags('管理端-管理员管理')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(AdminRole.SUPER_ADMIN)
@Controller('admin/admins')
export class AdminManageController {
constructor(private readonly adminManageService: AdminManageService) {}
@Get()
@ApiOperation({ summary: '获取管理员列表' })
async getAdminList(@Query() query: QueryAdminDto) {
return this.adminManageService.getAdminList(query);
}
@Get(':id')
@ApiOperation({ summary: '获取管理员详情' })
@ApiParam({ name: 'id', description: '管理员ID' })
async getAdminById(@Param('id') id: number) {
return this.adminManageService.getAdminById(id);
}
@Post()
@ApiOperation({ summary: '创建管理员' })
async createAdmin(@Body() dto: CreateAdminDto) {
return this.adminManageService.createAdmin(dto);
}
@Put(':id')
@ApiOperation({ summary: '更新管理员信息' })
@ApiParam({ name: 'id', description: '管理员ID' })
async updateAdmin(@Param('id') id: number, @Body() dto: UpdateAdminDto) {
return this.adminManageService.updateAdmin(id, dto);
}
@Put(':id/password')
@ApiOperation({ summary: '重置管理员密码' })
@ApiParam({ name: 'id', description: '管理员ID' })
async updateAdminPassword(@Param('id') id: number, @Body() dto: UpdateAdminPasswordDto) {
return this.adminManageService.updateAdminPassword(id, dto);
}
@Put(':id/toggle-status')
@ApiOperation({ summary: '切换管理员状态' })
@ApiParam({ name: 'id', description: '管理员ID' })
async toggleAdminStatus(@Param('id') id: number) {
return this.adminManageService.toggleAdminStatus(id);
}
@Delete(':id')
@ApiOperation({ summary: '删除管理员' })
@ApiParam({ name: 'id', description: '管理员ID' })
async deleteAdmin(@Param('id') id: number) {
return this.adminManageService.deleteAdmin(id);
}
}
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Admin } from '@/entities/admin.entity';
import { AdminManageController } from './admin-manage.controller';
import { AdminManageService } from './admin-manage.service';
@Module({
imports: [TypeOrmModule.forFeature([Admin])],
controllers: [AdminManageController],
providers: [AdminManageService],
exports: [AdminManageService],
})
export class AdminManageModule {}
@@ -0,0 +1,119 @@
import { Injectable, NotFoundException, BadRequestException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Like } from 'typeorm';
import { Admin } from '@/entities/admin.entity';
import { CreateAdminDto, UpdateAdminDto, UpdateAdminPasswordDto, QueryAdminDto } from './dto/admin.dto';
import { AdminRole, AdminStatus } from '@/common/constants/admin.constant';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AdminManageService {
constructor(
@InjectRepository(Admin)
private adminRepo: Repository<Admin>,
) {}
async getAdminList(query: QueryAdminDto) {
const { username, name, role, status, page = 1, pageSize = 20 } = query;
const queryBuilder = this.adminRepo.createQueryBuilder('admin');
if (username) {
queryBuilder.andWhere('admin.username LIKE :username', { username: `%${username}%` });
}
if (name) {
queryBuilder.andWhere('admin.name LIKE :name', { name: `%${name}%` });
}
if (role) {
queryBuilder.andWhere('admin.role = :role', { role });
}
if (status) {
queryBuilder.andWhere('admin.status = :status', { status });
}
queryBuilder.orderBy('admin.createdAt', 'DESC');
const skip = (page - 1) * pageSize;
queryBuilder.skip(skip).take(pageSize);
const [items, total] = await queryBuilder.getManyAndCount();
return {
items,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
};
}
async getAdminById(id: number) {
const admin = await this.adminRepo.findOne({ where: { id } });
if (!admin) {
throw new NotFoundException('管理员不存在');
}
return admin;
}
async createAdmin(dto: CreateAdminDto) {
const existingAdmin = await this.adminRepo.findOne({ where: { username: dto.username } });
if (existingAdmin) {
throw new ConflictException('用户名已存在');
}
const hashedPassword = await bcrypt.hash(dto.password, 10);
const admin = this.adminRepo.create({
...dto,
password: hashedPassword,
});
return this.adminRepo.save(admin);
}
async updateAdmin(id: number, dto: UpdateAdminDto) {
const admin = await this.getAdminById(id);
Object.assign(admin, dto);
return this.adminRepo.save(admin);
}
async updateAdminPassword(id: number, dto: UpdateAdminPasswordDto) {
const admin = await this.getAdminById(id);
const hashedPassword = await bcrypt.hash(dto.password, 10);
admin.password = hashedPassword;
await this.adminRepo.save(admin);
return { message: '密码修改成功' };
}
async deleteAdmin(id: number) {
const admin = await this.getAdminById(id);
if (admin.role === AdminRole.SUPER_ADMIN) {
throw new BadRequestException('不能删除超级管理员');
}
await this.adminRepo.remove(admin);
return { message: '删除成功' };
}
async toggleAdminStatus(id: number) {
const admin = await this.getAdminById(id);
if (admin.role === AdminRole.SUPER_ADMIN) {
throw new BadRequestException('不能冻结超级管理员');
}
admin.status = admin.status === AdminStatus.ACTIVE ? AdminStatus.FROZEN : AdminStatus.ACTIVE;
return this.adminRepo.save(admin);
}
}
@@ -0,0 +1,106 @@
import { IsString, IsEmail, IsEnum, IsOptional, MinLength, MaxLength } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { AdminRole, AdminStatus } from '@/common/constants/admin.constant';
export class CreateAdminDto {
@ApiProperty({ description: '用户名', example: 'admin001' })
@IsString()
@MinLength(3)
@MaxLength(50)
username: string;
@ApiProperty({ description: '密码', example: 'Admin@123' })
@IsString()
@MinLength(6)
@MaxLength(50)
password: string;
@ApiProperty({ description: '姓名', example: '张三' })
@IsString()
@MaxLength(50)
name: string;
@ApiPropertyOptional({ description: '手机号', example: '13800138000' })
@IsOptional()
@IsString()
@MaxLength(20)
phone?: string;
@ApiPropertyOptional({ description: '邮箱', example: 'admin@example.com' })
@IsOptional()
@IsEmail()
@MaxLength(100)
email?: string;
@ApiProperty({ description: '角色', enum: AdminRole, example: AdminRole.ADMIN })
@IsEnum(AdminRole)
role: AdminRole;
}
export class UpdateAdminDto {
@ApiPropertyOptional({ description: '姓名', example: '张三' })
@IsOptional()
@IsString()
@MaxLength(50)
name?: string;
@ApiPropertyOptional({ description: '手机号', example: '13800138000' })
@IsOptional()
@IsString()
@MaxLength(20)
phone?: string;
@ApiPropertyOptional({ description: '邮箱', example: 'admin@example.com' })
@IsOptional()
@IsEmail()
@MaxLength(100)
email?: string;
@ApiPropertyOptional({ description: '角色', enum: AdminRole, example: AdminRole.ADMIN })
@IsOptional()
@IsEnum(AdminRole)
role?: AdminRole;
@ApiPropertyOptional({ description: '状态', enum: AdminStatus, example: AdminStatus.ACTIVE })
@IsOptional()
@IsEnum(AdminStatus)
status?: AdminStatus;
}
export class UpdateAdminPasswordDto {
@ApiProperty({ description: '新密码', example: 'NewPass@123' })
@IsString()
@MinLength(6)
@MaxLength(50)
password: string;
}
export class QueryAdminDto {
@ApiPropertyOptional({ description: '用户名', example: 'admin' })
@IsOptional()
@IsString()
username?: string;
@ApiPropertyOptional({ description: '姓名', example: '张三' })
@IsOptional()
@IsString()
name?: string;
@ApiPropertyOptional({ description: '角色', enum: AdminRole })
@IsOptional()
@IsEnum(AdminRole)
role?: AdminRole;
@ApiPropertyOptional({ description: '状态', enum: AdminStatus })
@IsOptional()
@IsEnum(AdminStatus)
status?: AdminStatus;
@ApiPropertyOptional({ description: '页码', example: 1 })
@IsOptional()
page?: number;
@ApiPropertyOptional({ description: '每页数量', example: 20 })
@IsOptional()
pageSize?: number;
}
@@ -10,6 +10,7 @@ import { AdminActivityModule } from './activity/activity.module';
import { AdminConfigModule } from './config/config.module';
import { AdminFinanceModule } from './finance/finance.module';
import { AdminWebsiteModule } from './website/website.module';
import { AdminManageModule } from './admin-manage/admin-manage.module';
@Module({
imports: [
@@ -24,6 +25,7 @@ import { AdminWebsiteModule } from './website/website.module';
AdminConfigModule,
AdminFinanceModule,
AdminWebsiteModule,
AdminManageModule,
],
})
export class AdminModule {}
@@ -11,6 +11,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { Admin } from '@/entities/admin.entity';
import { AdminStatus } from '@/common/constants/admin.constant';
import {
AdminLoginDto,
CreateAdminDto,
@@ -131,12 +132,12 @@ export class AdminAuthService {
}
async freeze(id: number) {
await this.adminRepo.update(id, { status: 'frozen' });
await this.adminRepo.update(id, { status: AdminStatus.FROZEN });
return { message: '已冻结' };
}
async unfreeze(id: number) {
await this.adminRepo.update(id, { status: 'active' });
await this.adminRepo.update(id, { status: AdminStatus.ACTIVE });
return { message: '已解冻' };
}
@@ -144,8 +145,7 @@ export class AdminAuthService {
const payload = {
sub: admin.id,
username: admin.username,
role: 'admin',
adminRole: admin.role,
role: admin.role,
type: 'admin',
};
const accessToken = await this.jwtService.signAsync(payload);
@@ -40,4 +40,31 @@ export class AdminConfigController {
await this.uploadService.updateStorageConfig(body);
return this.uploadService.getStorageConfig();
}
@Get('withdraw')
@ApiOperation({ summary: '获取提现配置' })
async getWithdrawConfig() {
const [merchantMin, platformMin] = await Promise.all([
this.configService.getMerchantMinWithdrawAmount(),
this.configService.getPlatformMinWithdrawAmount(),
]);
return {
merchantMinWithdrawAmount: merchantMin,
platformMinWithdrawAmount: platformMin,
};
}
@Put('withdraw/merchant-min')
@ApiOperation({ summary: '设置商家提现最低金额' })
async setMerchantMinWithdrawAmount(@Body() body: { amount: number }) {
await this.configService.setMerchantMinWithdrawAmount(body.amount);
return { amount: await this.configService.getMerchantMinWithdrawAmount() };
}
@Put('withdraw/platform-min')
@ApiOperation({ summary: '设置平台提现最低金额' })
async setPlatformMinWithdrawAmount(@Body() body: { amount: number }) {
await this.configService.setPlatformMinWithdrawAmount(body.amount);
return { amount: await this.configService.getPlatformMinWithdrawAmount() };
}
}
@@ -12,6 +12,7 @@ import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { WithdrawalService } from '@/modules/shared/finance/withdrawal.service';
import { JwtAuthGuard, RolesGuard } from '@/common';
import { Roles } from '@/common/decorators/roles.decorator';
import { AdminRole } from '@/common/constants/admin.constant';
import { CurrentUser } from '@/common/decorators/current-user.decorator';
import {
CreatePlatformWithdrawalDto,
@@ -25,7 +26,7 @@ import {
@ApiTags('提现管理(管理员)')
@Controller('admin/finance/withdrawals')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@Roles(AdminRole.SUPER_ADMIN)
@ApiBearerAuth()
export class WithdrawalAdminController {
constructor(private readonly withdrawalService: WithdrawalService) {}
@@ -15,6 +15,7 @@ import { UserFinanceModule } from './finance/finance.module';
import { UserActivityModule } from './activity/activity.module';
import { RoomModule } from './room/room.module';
import { LocationModule } from './location/location.module';
import { PaymentModule } from './payment/payment.module';
import { MerchantController } from './merchant/merchant.controller';
import { MerchantService } from '@/modules/merchant/merchant.service';
@@ -31,6 +32,7 @@ import { MerchantService } from '@/modules/merchant/merchant.service';
UserActivityModule,
RoomModule,
LocationModule,
PaymentModule,
],
controllers: [MerchantController],
providers: [MerchantService],
@@ -10,6 +10,7 @@ import { UserActivityModule } from '@/modules/app/activity/activity.module';
import { ConfigModule } from '@/modules/shared/config/config.module';
import { FinanceModule } from '@/modules/shared/finance/finance.module';
import { UserCouponModule } from '@/modules/app/coupon/coupon.module';
import { PaymentModule } from '@/modules/shared/payment/payment.module';
@Module({
imports: [
@@ -18,6 +19,7 @@ import { UserCouponModule } from '@/modules/app/coupon/coupon.module';
ConfigModule,
FinanceModule,
UserCouponModule,
PaymentModule,
],
controllers: [OrderController],
providers: [OrderService],
@@ -11,6 +11,7 @@ import { ConfigService } from '@/modules/shared/config/config.service';
import { RefundService } from '@/modules/shared/finance/refund.service';
import { AccountService } from '@/modules/shared/finance/account.service';
import { CouponService } from '@/modules/app/coupon/coupon.service';
import { WechatPayService } from '@/modules/shared/payment/wechat-pay.service';
@Injectable()
export class OrderService {
@@ -28,6 +29,7 @@ export class OrderService {
private readonly refundService: RefundService,
private readonly accountService: AccountService,
private readonly couponService: CouponService,
private readonly wechatPayService: WechatPayService,
) {}
/**
@@ -320,58 +322,47 @@ export class OrderService {
throw new BadRequestException('当前订单状态不可支付');
}
// 使用事务确保所有操作原子性
const queryRunner = this.orderRepo.manager.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
// 仅支持微信支付
if (paymentMethod !== 'wechat') {
throw new BadRequestException('当前仅支持微信支付');
}
// 获取用户openid
const user = await this.orderRepo.manager.findOne('User', {
where: { id: userId },
select: ['id', 'openid'],
});
if (!user || !user.openid) {
throw new BadRequestException('用户未绑定微信,无法使用微信支付');
}
// 调用微信支付统一下单
const notifyUrl = this.configService.get<string>('WECHAT_PAY_NOTIFY_URL') ||
`${this.configService.get<string>('API_BASE_URL')}/api/app/payment/wechat/notify`;
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(),
const payResult = await this.wechatPayService.createJsapiOrder({
orderNo: order.orderNo,
description: `${order.room?.name || '房间'}预订`,
amount: order.payAmount,
openid: user.openid,
notifyUrl,
});
// 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}`,
);
// 更新订单支付方式
await this.orderRepo.update(order.id, {
paymentMethod: 'wechat',
});
// 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,
});
}
// 提交事务
await queryRunner.commitTransaction();
return { message: '支付成功', paymentNo };
// 返回小程序支付参数
return {
orderNo: order.orderNo,
payAmount: order.payAmount,
payParams: payResult,
};
} catch (error) {
// 回滚事务
await queryRunner.rollbackTransaction();
throw error;
} finally {
// 释放连接
await queryRunner.release();
throw new BadRequestException(`发起支付失败: ${error.message}`);
}
}
@@ -0,0 +1,91 @@
import { Controller, Post, Body, Headers, HttpCode, HttpStatus, Logger, RawBodyRequest, Req } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiExcludeEndpoint } from '@nestjs/swagger';
import { PaymentService } from './payment.service';
import { Request } from 'express';
@ApiTags('支付回调')
@Controller('app/payment')
export class PaymentController {
private readonly logger = new Logger(PaymentController.name);
constructor(private readonly paymentService: PaymentService) {}
@Post('wechat/notify')
@HttpCode(HttpStatus.OK)
@ApiExcludeEndpoint()
async wechatNotify(
@Req() req: RawBodyRequest<Request>,
@Headers('wechatpay-signature') signature: string,
@Headers('wechatpay-timestamp') timestamp: string,
@Headers('wechatpay-nonce') nonce: string,
@Headers('wechatpay-serial') serial: string,
@Body() body: any,
) {
this.logger.log('收到微信支付回调');
try {
// 获取原始请求体
const rawBody = req.rawBody ? req.rawBody.toString('utf8') : JSON.stringify(body);
// 验证签名
const isValid = await this.paymentService.verifyWechatSignature({
timestamp,
nonce,
body: rawBody,
signature,
serial,
});
if (!isValid) {
this.logger.error('微信支付回调签名验证失败');
return { code: 'FAIL', message: '签名验证失败' };
}
// 处理回调
await this.paymentService.handleWechatNotify(body);
return { code: 'SUCCESS', message: '成功' };
} catch (error) {
this.logger.error(`处理微信支付回调失败: ${error.message}`, error.stack);
return { code: 'FAIL', message: error.message };
}
}
@Post('wechat/refund-notify')
@HttpCode(HttpStatus.OK)
@ApiExcludeEndpoint()
async wechatRefundNotify(
@Req() req: RawBodyRequest<Request>,
@Headers('wechatpay-signature') signature: string,
@Headers('wechatpay-timestamp') timestamp: string,
@Headers('wechatpay-nonce') nonce: string,
@Headers('wechatpay-serial') serial: string,
@Body() body: any,
) {
this.logger.log('收到微信退款回调');
try {
const rawBody = req.rawBody ? req.rawBody.toString('utf8') : JSON.stringify(body);
const isValid = await this.paymentService.verifyWechatSignature({
timestamp,
nonce,
body: rawBody,
signature,
serial,
});
if (!isValid) {
this.logger.error('微信退款回调签名验证失败');
return { code: 'FAIL', message: '签名验证失败' };
}
await this.paymentService.handleWechatRefundNotify(body);
return { code: 'SUCCESS', message: '成功' };
} catch (error) {
this.logger.error(`处理微信退款回调失败: ${error.message}`, error.stack);
return { code: 'FAIL', message: error.message };
}
}
}
@@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PaymentController } from './payment.controller';
import { PaymentService } from './payment.service';
import { Order } from '@/entities/order.entity';
import { RoomCalendar } from '@/entities/room-calendar.entity';
import { PaymentModule as SharedPaymentModule } from '@/modules/shared/payment/payment.module';
import { FinanceModule } from '@/modules/shared/finance/finance.module';
@Module({
imports: [
TypeOrmModule.forFeature([Order, RoomCalendar]),
SharedPaymentModule,
FinanceModule,
],
controllers: [PaymentController],
providers: [PaymentService],
exports: [PaymentService],
})
export class PaymentModule {}
@@ -0,0 +1,172 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Order } from '@/entities/order.entity';
import { RoomCalendar } from '@/entities/room-calendar.entity';
import { WechatPayService } from '@/modules/shared/payment/wechat-pay.service';
import { AccountService } from '@/modules/shared/finance/account.service';
@Injectable()
export class PaymentService {
private readonly logger = new Logger(PaymentService.name);
constructor(
@InjectRepository(Order)
private orderRepo: Repository<Order>,
@InjectRepository(RoomCalendar)
private calendarRepo: Repository<RoomCalendar>,
private readonly wechatPayService: WechatPayService,
private readonly accountService: AccountService,
) {}
/**
* 验证微信支付签名
*/
async verifyWechatSignature(params: {
timestamp: string;
nonce: string;
body: string;
signature: string;
serial: string;
}): Promise<boolean> {
return this.wechatPayService.verifySignature(params);
}
/**
* 处理微信支付回调
*/
async handleWechatNotify(body: any) {
const { resource } = body;
if (!resource) {
throw new Error('回调数据格式错误');
}
// 解密数据
const decryptedData = this.wechatPayService.decryptData(
resource.ciphertext,
resource.nonce,
resource.associated_data,
);
const {
out_trade_no: orderNo,
transaction_id: transactionId,
trade_state: tradeState,
trade_state_desc: tradeStateDesc,
} = decryptedData;
this.logger.log(`微信支付回调: 订单号=${orderNo}, 交易状态=${tradeState}`);
// 查询订单
const order = await this.orderRepo.findOne({
where: { orderNo },
relations: ['room'],
});
if (!order) {
throw new Error(`订单不存在: ${orderNo}`);
}
// 如果订单已支付,直接返回成功
if (order.status !== 'pending_pay') {
this.logger.warn(`订单已处理: ${orderNo}, 当前状态=${order.status}`);
return;
}
// 只处理支付成功的回调
if (tradeState !== 'SUCCESS') {
this.logger.warn(`支付未成功: ${orderNo}, 状态=${tradeState}, 描述=${tradeStateDesc}`);
return;
}
// 使用事务处理支付成功逻辑
const queryRunner = this.orderRepo.manager.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 1. 更新订单状态为已支付
const paymentNo = `PAY${Date.now()}${Math.floor(Math.random() * 10000)}`;
await queryRunner.manager.update(Order, order.id, {
status: 'pending_confirm',
paymentNo,
transactionId,
paidAt: new Date(),
});
// 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. 扣减房态库存(注意:创建订单时已经扣减了sold,这里不需要再扣减)
// 如果创建订单时没有扣减库存,则需要在这里扣减
// 根据之前的代码,创建订单时已经扣减了库存,所以这里不需要再次扣减
// 提交事务
await queryRunner.commitTransaction();
this.logger.log(`订单支付成功: ${orderNo}`);
} catch (error) {
// 回滚事务
await queryRunner.rollbackTransaction();
this.logger.error(`处理支付回调失败: ${error.message}`, error.stack);
throw error;
} finally {
// 释放连接
await queryRunner.release();
}
}
/**
* 处理微信退款回调
*/
async handleWechatRefundNotify(body: any) {
const { resource } = body;
if (!resource) {
throw new Error('回调数据格式错误');
}
// 解密数据
const decryptedData = this.wechatPayService.decryptData(
resource.ciphertext,
resource.nonce,
resource.associated_data,
);
const {
out_trade_no: orderNo,
out_refund_no: refundNo,
refund_status: refundStatus,
} = decryptedData;
this.logger.log(`微信退款回调: 订单号=${orderNo}, 退款单号=${refundNo}, 状态=${refundStatus}`);
// 查询订单
const order = await this.orderRepo.findOne({
where: { orderNo },
});
if (!order) {
throw new Error(`订单不存在: ${orderNo}`);
}
// 只处理退款成功的回调
if (refundStatus === 'SUCCESS') {
await this.orderRepo.update(order.id, {
status: 'refunded',
refundAt: new Date(),
});
this.logger.log(`订单退款成功: ${orderNo}`);
} else if (refundStatus === 'ABNORMAL') {
this.logger.error(`订单退款异常: ${orderNo}`);
}
}
}
@@ -54,4 +54,50 @@ export class ConfigService {
}
await this.setConfig('service_fee_rate', rate.toString(), '软件服务费比例');
}
async getAllConfigs() {
return this.configRepo.find({ order: { createdAt: 'DESC' } });
}
async deleteConfig(key: string): Promise<void> {
await this.configRepo.delete({ configKey: key });
}
/**
* 获取商家提现最低金额
*/
async getMerchantMinWithdrawAmount(): Promise<number> {
const value = await this.getConfig('merchant_min_withdraw_amount');
const amount = parseFloat(value || '100');
return isNaN(amount) ? 100 : amount;
}
/**
* 获取平台提现最低金额
*/
async getPlatformMinWithdrawAmount(): Promise<number> {
const value = await this.getConfig('platform_min_withdraw_amount');
const amount = parseFloat(value || '10');
return isNaN(amount) ? 10 : amount;
}
/**
* 设置商家提现最低金额
*/
async setMerchantMinWithdrawAmount(amount: number): Promise<void> {
if (amount < 0) {
throw new Error('提现最低金额不能为负数');
}
await this.setConfig('merchant_min_withdraw_amount', amount.toString(), '商家提现最低金额(元)');
}
/**
* 设置平台提现最低金额
*/
async setPlatformMinWithdrawAmount(amount: number): Promise<void> {
if (amount < 0) {
throw new Error('提现最低金额不能为负数');
}
await this.setConfig('platform_min_withdraw_amount', amount.toString(), '平台提现最低金额(元)');
}
}
@@ -27,6 +27,7 @@ import { ReportService } from './report.service';
import { RefundService } from './refund.service';
import { BankCardService } from './bank-card.service';
import { MerchantModule } from '@/modules/merchant/merchant.module';
import { ConfigModule } from '../config/config.module';
@Module({
imports: [
@@ -51,6 +52,7 @@ import { MerchantModule } from '@/modules/merchant/merchant.module';
Order,
]),
forwardRef(() => MerchantModule),
ConfigModule,
],
providers: [
SettlementService,
@@ -9,6 +9,7 @@ import { PlatformAccount } from '@/entities/platform-account.entity';
import { MerchantTransaction } from '@/entities/merchant-transaction.entity';
import { AccountService } from './account.service';
import { TransactionService } from './transaction.service';
import { ConfigService } from '../config/config.service';
@Injectable()
export class WithdrawalService {
@@ -23,6 +24,7 @@ export class WithdrawalService {
private merchantTransactionRepo: Repository<MerchantTransaction>,
private accountService: AccountService,
private transactionService: TransactionService,
private configService: ConfigService,
private dataSource: DataSource,
) {}
@@ -35,9 +37,8 @@ export class WithdrawalService {
}) {
const { amount, paymentChannel } = dto;
if (amount < 10) {
throw new BadRequestException('最低提现金额为10元');
}
// 用户提现最低金额由邀请活动配置中的 withdrawThreshold 控制
// 这里不再检查最低金额,由调用方(邀请活动模块)负责验证
const account = await this.accountService.getUserAccount(userId);
@@ -86,8 +87,9 @@ export class WithdrawalService {
}) {
const { amount, bankName, bankAccount, accountName } = dto;
if (amount < 100) {
throw new BadRequestException('最低提现金额为100元');
const minAmount = await this.configService.getMerchantMinWithdrawAmount();
if (amount < minAmount) {
throw new BadRequestException(`最低提现金额为${minAmount}`);
}
const account = await this.accountService.getMerchantAccount(merchantId);
@@ -152,8 +154,9 @@ export class WithdrawalService {
}) {
const { amount, bankName, bankAccount, accountName } = dto;
if (amount < 10) {
throw new BadRequestException('最低提现金额为10元');
const minAmount = await this.configService.getPlatformMinWithdrawAmount();
if (amount < minAmount) {
throw new BadRequestException(`最低提现金额为${minAmount}`);
}
const account = await this.accountService.getPlatformAccount();