feat: 迭代
This commit is contained in:
@@ -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('无权限访问');
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user