feat: 删除假支付逻辑,全面接入微信支付
- 删除 order.service.ts 中的 pay() 假支付方法 - 修复支付模块结构,移动到正确路径 - 重构退款服务使用统一的 WechatPayService - 完善退款回调处理(恢复库存+记录财务) - 修复小程序支付调用,使用 wxPay() 方法 - WechatPayService 从数据库系统密钥读取配置 - 移除 .env.example 中的微信支付配置(已迁移到数据库) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -276,7 +276,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted } from 'vue';
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
import { getOrderDetail, cancelOrder, payOrder } from '@/api/user/order';
|
import { getOrderDetail, cancelOrder, wxPay } from '@/api/user/order';
|
||||||
import { createReview, checkOrderReviewed } from '@/api/user/review';
|
import { createReview, checkOrderReviewed } from '@/api/user/review';
|
||||||
import { formatDate, formatDateTime } from '@/utils/date';
|
import { formatDate, formatDateTime } from '@/utils/date';
|
||||||
|
|
||||||
@@ -457,7 +457,7 @@ function handlePay() {
|
|||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
uni.showLoading({ title: '支付中...' });
|
uni.showLoading({ title: '支付中...' });
|
||||||
try {
|
try {
|
||||||
await payOrder(orderNo.value, 'wechat');
|
await wxPay(orderNo.value);
|
||||||
uni.hideLoading();
|
uni.hideLoading();
|
||||||
uni.showToast({ title: '支付成功', icon: 'success' });
|
uni.showToast({ title: '支付成功', icon: 'success' });
|
||||||
fetchDetail();
|
fetchDetail();
|
||||||
|
|||||||
@@ -1,147 +0,0 @@
|
|||||||
# 周结算接口修复说明
|
|
||||||
|
|
||||||
## 问题描述
|
|
||||||
|
|
||||||
调用 `/api/admin/finance/settlements/execute-weekly` 接口时,即使没有实际执行结算,也会返回成功消息 `"周结算任务已执行完成"`,导致用户误以为结算成功。
|
|
||||||
|
|
||||||
## 问题原因
|
|
||||||
|
|
||||||
1. **Controller 层**:没有捕获 Service 层抛出的异常,也没有返回详细的执行结果
|
|
||||||
2. **Service 层**:在以下情况会抛出 `BadRequestException`:
|
|
||||||
- 该周期已经结算过
|
|
||||||
- 没有需要结算的订单
|
|
||||||
- 所有订单都已结算
|
|
||||||
|
|
||||||
这些异常导致接口返回 400 错误,但前端可能没有正确处理,或者异常被中间件捕获后返回了成功状态。
|
|
||||||
|
|
||||||
## 修复方案
|
|
||||||
|
|
||||||
### 1. Controller 层改进
|
|
||||||
|
|
||||||
**修改前:**
|
|
||||||
```typescript
|
|
||||||
@Post('execute-weekly')
|
|
||||||
async executeWeeklySettlement() {
|
|
||||||
await this.settlementService.handleWeeklySettlement();
|
|
||||||
return { message: '周结算任务已执行完成' };
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**修改后:**
|
|
||||||
```typescript
|
|
||||||
@Post('execute-weekly')
|
|
||||||
async executeWeeklySettlement() {
|
|
||||||
const result = await this.settlementService.handleWeeklySettlement();
|
|
||||||
return {
|
|
||||||
message: '周结算任务已执行完成',
|
|
||||||
...result
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Service 层改进
|
|
||||||
|
|
||||||
将抛出异常改为返回明确的结果对象:
|
|
||||||
|
|
||||||
**修改前:**
|
|
||||||
```typescript
|
|
||||||
if (existingSettlements > 0) {
|
|
||||||
throw new BadRequestException(`该周期已经结算过,无法重复结算`);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**修改后:**
|
|
||||||
```typescript
|
|
||||||
if (existingSettlements > 0) {
|
|
||||||
this.logger.warn(`该周期 ${lastWeekStart} ~ ${lastWeekEnd} 已经结算过,跳过`);
|
|
||||||
return {
|
|
||||||
successCount: 0,
|
|
||||||
failCount: 0,
|
|
||||||
totalOrders: 0,
|
|
||||||
skipped: true,
|
|
||||||
reason: `该周期 ${lastWeekStart} ~ ${lastWeekEnd} 已经结算过`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 返回值结构
|
|
||||||
|
|
||||||
### 成功执行结算
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "success",
|
|
||||||
"data": {
|
|
||||||
"message": "周结算任务已执行完成",
|
|
||||||
"successCount": 5,
|
|
||||||
"failCount": 0,
|
|
||||||
"totalOrders": 120,
|
|
||||||
"skipped": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 跳过结算(已结算过)
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "success",
|
|
||||||
"data": {
|
|
||||||
"message": "周结算任务已执行完成",
|
|
||||||
"successCount": 0,
|
|
||||||
"failCount": 0,
|
|
||||||
"totalOrders": 0,
|
|
||||||
"skipped": true,
|
|
||||||
"reason": "该周期 2026-05-19 ~ 2026-05-25 已经结算过"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 跳过结算(无订单)
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "success",
|
|
||||||
"data": {
|
|
||||||
"message": "周结算任务已执行完成",
|
|
||||||
"successCount": 0,
|
|
||||||
"failCount": 0,
|
|
||||||
"totalOrders": 0,
|
|
||||||
"skipped": true,
|
|
||||||
"reason": "该周期内没有需要结算的订单"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 前端处理建议
|
|
||||||
|
|
||||||
前端应该根据返回的 `skipped` 字段判断是否真正执行了结算:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const response = await executeWeeklySettlement();
|
|
||||||
|
|
||||||
if (response.skipped) {
|
|
||||||
// 显示警告信息
|
|
||||||
message.warning(response.reason);
|
|
||||||
} else if (response.successCount > 0) {
|
|
||||||
// 显示成功信息
|
|
||||||
message.success(`结算成功:${response.successCount} 个商家,共 ${response.totalOrders} 个订单`);
|
|
||||||
|
|
||||||
if (response.failCount > 0) {
|
|
||||||
message.warning(`${response.failCount} 个商家结算失败,请查看日志`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
message.info('没有需要结算的数据');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 测试步骤
|
|
||||||
|
|
||||||
1. **首次执行**:调用接口,应该返回 `skipped: false` 和实际的结算数据
|
|
||||||
2. **重复执行**:再次调用接口,应该返回 `skipped: true` 和原因说明
|
|
||||||
3. **无订单场景**:在没有已完成订单的情况下调用,应该返回相应的提示
|
|
||||||
|
|
||||||
## 相关文件
|
|
||||||
|
|
||||||
- [settlement-admin.controller.ts](../src/modules/admin/finance/settlement-admin.controller.ts)
|
|
||||||
- [settlement.service.ts](../src/modules/shared/finance/settlement.service.ts)
|
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Injectable, NotFoundException, BadRequestException, ForbiddenException } from '@nestjs/common';
|
import { Injectable, NotFoundException, BadRequestException, ForbiddenException } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository, Between } from 'typeorm';
|
import { Repository, Between } from 'typeorm';
|
||||||
|
import { ConfigService as NestConfigService } from '@nestjs/config';
|
||||||
import { Order } from '@/entities/order.entity';
|
import { Order } from '@/entities/order.entity';
|
||||||
import { Room } from '@/entities/room.entity';
|
import { Room } from '@/entities/room.entity';
|
||||||
import { RoomCalendar } from '@/entities/room-calendar.entity';
|
import { RoomCalendar } from '@/entities/room-calendar.entity';
|
||||||
import { Review } from '@/entities/review.entity';
|
import { Review } from '@/entities/review.entity';
|
||||||
|
import { User } from '@/entities/user.entity';
|
||||||
import { CreateOrderDto, QueryOrderDto } from './dto/order.dto';
|
import { CreateOrderDto, QueryOrderDto } from './dto/order.dto';
|
||||||
import { ActivityService } from '@/modules/app/activity/activity.service';
|
import { ActivityService } from '@/modules/app/activity/activity.service';
|
||||||
import { ConfigService } from '@/modules/shared/config/config.service';
|
import { ConfigService } from '@/modules/shared/config/config.service';
|
||||||
@@ -26,6 +28,7 @@ export class OrderService {
|
|||||||
private reviewRepo: Repository<Review>,
|
private reviewRepo: Repository<Review>,
|
||||||
private readonly activityService: ActivityService,
|
private readonly activityService: ActivityService,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
|
private readonly nestConfigService: NestConfigService,
|
||||||
private readonly refundService: RefundService,
|
private readonly refundService: RefundService,
|
||||||
private readonly accountService: AccountService,
|
private readonly accountService: AccountService,
|
||||||
private readonly couponService: CouponService,
|
private readonly couponService: CouponService,
|
||||||
@@ -328,25 +331,25 @@ export class OrderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户openid
|
// 获取用户openid
|
||||||
const user = await this.orderRepo.manager.findOne('User', {
|
const user = await this.orderRepo.manager.findOne(User, {
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
select: ['id', 'openid'],
|
select: ['id', 'wechatOpenid'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user || !user.openid) {
|
if (!user || !user.wechatOpenid) {
|
||||||
throw new BadRequestException('用户未绑定微信,无法使用微信支付');
|
throw new BadRequestException('用户未绑定微信,无法使用微信支付');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用微信支付统一下单
|
// 调用微信支付统一下单
|
||||||
const notifyUrl = this.configService.get<string>('WECHAT_PAY_NOTIFY_URL') ||
|
const notifyUrl = this.nestConfigService.get<string>('WECHAT_PAY_NOTIFY_URL') ||
|
||||||
`${this.configService.get<string>('API_BASE_URL')}/api/app/payment/wechat/notify`;
|
`${this.nestConfigService.get<string>('API_BASE_URL')}/api/app/payment/wechat/notify`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payResult = await this.wechatPayService.createJsapiOrder({
|
const payResult = await this.wechatPayService.createJsapiOrder({
|
||||||
orderNo: order.orderNo,
|
orderNo: order.orderNo,
|
||||||
description: `${order.room?.name || '房间'}预订`,
|
description: `${order.room?.name || '房间'}预订`,
|
||||||
amount: order.payAmount,
|
amount: order.payAmount,
|
||||||
openid: user.openid,
|
openid: user.wechatOpenid,
|
||||||
notifyUrl,
|
notifyUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -366,71 +369,6 @@ export class OrderService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户支付订单(通过ID,保留用于内部调用)
|
|
||||||
*/
|
|
||||||
async pay(userId: number, id: number, paymentMethod: 'wechat' | 'alipay' | 'balance') {
|
|
||||||
const order = await this.findById(id);
|
|
||||||
if (order.userId !== userId) throw new ForbiddenException('无权操作此订单');
|
|
||||||
if (order.status !== 'pending_pay') {
|
|
||||||
throw new BadRequestException('当前订单状态不可支付');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用事务确保所有操作原子性
|
|
||||||
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, id, {
|
|
||||||
status: 'pending_confirm',
|
|
||||||
paymentMethod,
|
|
||||||
paymentNo,
|
|
||||||
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. 扣减房态库存
|
|
||||||
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 };
|
|
||||||
} catch (error) {
|
|
||||||
// 回滚事务
|
|
||||||
await queryRunner.rollbackTransaction();
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
// 释放连接
|
|
||||||
await queryRunner.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户申请退款(通过订单号)
|
* 用户申请退款(通过订单号)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -160,11 +160,56 @@ export class PaymentService {
|
|||||||
|
|
||||||
// 只处理退款成功的回调
|
// 只处理退款成功的回调
|
||||||
if (refundStatus === 'SUCCESS') {
|
if (refundStatus === 'SUCCESS') {
|
||||||
await this.orderRepo.update(order.id, {
|
// 使用事务处理退款成功逻辑
|
||||||
status: 'refunded',
|
const queryRunner = this.orderRepo.manager.connection.createQueryRunner();
|
||||||
refundAt: new Date(),
|
await queryRunner.connect();
|
||||||
});
|
await queryRunner.startTransaction();
|
||||||
this.logger.log(`订单退款成功: ${orderNo}`);
|
|
||||||
|
try {
|
||||||
|
// 1. 更新订单状态为已退款
|
||||||
|
await queryRunner.manager.update(Order, order.id, {
|
||||||
|
status: 'refunded',
|
||||||
|
refundAmount: order.payAmount,
|
||||||
|
refundAt: new Date(),
|
||||||
|
cancelledAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 恢复房态库存
|
||||||
|
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];
|
||||||
|
await queryRunner.manager.decrement(
|
||||||
|
RoomCalendar,
|
||||||
|
{ roomId: order.roomId, date: dateStr },
|
||||||
|
'sold',
|
||||||
|
order.roomCount,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 记录系统账户退款支出
|
||||||
|
const transactionNo = `REFUND_TXN_${Date.now()}${Math.floor(Math.random() * 10000)}`;
|
||||||
|
await this.accountService.addSystemRefund(
|
||||||
|
order.payAmount,
|
||||||
|
transactionNo,
|
||||||
|
'order_refund',
|
||||||
|
order.id,
|
||||||
|
order.orderNo,
|
||||||
|
`订单退款:${order.orderNo}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 提交事务
|
||||||
|
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();
|
||||||
|
}
|
||||||
} else if (refundStatus === 'ABNORMAL') {
|
} else if (refundStatus === 'ABNORMAL') {
|
||||||
this.logger.error(`订单退款异常: ${orderNo}`);
|
this.logger.error(`订单退款异常: ${orderNo}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { RefundService } from './refund.service';
|
|||||||
import { BankCardService } from './bank-card.service';
|
import { BankCardService } from './bank-card.service';
|
||||||
import { MerchantModule } from '@/modules/merchant/merchant.module';
|
import { MerchantModule } from '@/modules/merchant/merchant.module';
|
||||||
import { ConfigModule } from '../config/config.module';
|
import { ConfigModule } from '../config/config.module';
|
||||||
|
import { PaymentModule as SharedPaymentModule } from '@/modules/shared/payment/payment.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -53,6 +54,7 @@ import { ConfigModule } from '../config/config.module';
|
|||||||
]),
|
]),
|
||||||
forwardRef(() => MerchantModule),
|
forwardRef(() => MerchantModule),
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
|
SharedPaymentModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
SettlementService,
|
SettlementService,
|
||||||
|
|||||||
@@ -5,12 +5,10 @@ import { ConfigService } from '@nestjs/config';
|
|||||||
import { Order } from '@/entities/order.entity';
|
import { Order } from '@/entities/order.entity';
|
||||||
import { AccountService } from './account.service';
|
import { AccountService } from './account.service';
|
||||||
import { TransactionService } from './transaction.service';
|
import { TransactionService } from './transaction.service';
|
||||||
import Wechatpay = require('wechatpay-node-v3');
|
import { WechatPayService } from '@/modules/shared/payment/wechat-pay.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RefundService {
|
export class RefundService {
|
||||||
private wechatpayClient: Wechatpay | null;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Order)
|
@InjectRepository(Order)
|
||||||
private orderRepo: Repository<Order>,
|
private orderRepo: Repository<Order>,
|
||||||
@@ -18,42 +16,8 @@ export class RefundService {
|
|||||||
private transactionService: TransactionService,
|
private transactionService: TransactionService,
|
||||||
private dataSource: DataSource,
|
private dataSource: DataSource,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
) {
|
private wechatPayService: WechatPayService,
|
||||||
// 初始化微信支付客户端
|
) {}
|
||||||
this.initWechatpayClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化微信支付客户端
|
|
||||||
*/
|
|
||||||
private initWechatpayClient() {
|
|
||||||
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) {
|
|
||||||
console.warn('[微信支付] 配置不完整,退款功能将使用模拟模式');
|
|
||||||
this.wechatpayClient = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.wechatpayClient = new Wechatpay({
|
|
||||||
appid,
|
|
||||||
mchid,
|
|
||||||
publicKey: Buffer.from(privateKey, 'utf-8'),
|
|
||||||
privateKey: Buffer.from(privateKey, 'utf-8'),
|
|
||||||
serial_no: serialNo,
|
|
||||||
key: apiv3Key,
|
|
||||||
});
|
|
||||||
console.log('[微信支付] 客户端初始化成功');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[微信支付] 客户端初始化失败:', error.message);
|
|
||||||
this.wechatpayClient = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理退款
|
* 处理退款
|
||||||
@@ -135,12 +99,6 @@ export class RefundService {
|
|||||||
* @param order 订单信息
|
* @param order 订单信息
|
||||||
*/
|
*/
|
||||||
private async wechatRefund(order: Order): Promise<void> {
|
private async wechatRefund(order: Order): Promise<void> {
|
||||||
// 如果没有配置微信支付客户端,使用模拟模式
|
|
||||||
if (!this.wechatpayClient) {
|
|
||||||
console.log(`[微信退款-模拟] 订单号: ${order.orderNo}, 金额: ${order.payAmount}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查必要字段
|
// 检查必要字段
|
||||||
if (!order.transactionId) {
|
if (!order.transactionId) {
|
||||||
throw new BadRequestException('订单缺少微信支付交易号,无法退款');
|
throw new BadRequestException('订单缺少微信支付交易号,无法退款');
|
||||||
@@ -148,28 +106,20 @@ export class RefundService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const refundNo = `REFUND_${order.orderNo}_${Date.now()}`;
|
const refundNo = `REFUND_${order.orderNo}_${Date.now()}`;
|
||||||
const refundAmount = Math.round(order.payAmount * 100); // 转换为分
|
const notifyUrl = this.configService.get<string>('WECHAT_REFUND_NOTIFY_URL') ||
|
||||||
const totalAmount = Math.round(order.payAmount * 100);
|
`${this.configService.get<string>('API_BASE_URL')}/api/app/payment/wechat/refund-notify`;
|
||||||
|
|
||||||
// 调用微信支付退款API
|
// 调用微信支付退款API
|
||||||
const result: any = await this.wechatpayClient.refunds({
|
const result = await this.wechatPayService.refund({
|
||||||
transaction_id: order.transactionId,
|
orderNo: order.orderNo,
|
||||||
out_refund_no: refundNo,
|
refundNo,
|
||||||
|
totalAmount: order.payAmount,
|
||||||
|
refundAmount: order.payAmount,
|
||||||
reason: '用户申请退款',
|
reason: '用户申请退款',
|
||||||
amount: {
|
notifyUrl,
|
||||||
refund: refundAmount,
|
|
||||||
total: totalAmount,
|
|
||||||
currency: 'CNY',
|
|
||||||
},
|
|
||||||
notify_url: this.configService.get<string>('WECHAT_REFUND_NOTIFY_URL'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 检查退款结果
|
console.log(`[微信退款成功] 订单号: ${order.orderNo}, 退款单号: ${refundNo}`);
|
||||||
if (result.status === 'SUCCESS' || result.status === 'PROCESSING') {
|
|
||||||
console.log(`[微信退款成功] 订单号: ${order.orderNo}, 退款单号: ${refundNo}, 状态: ${result.status}`);
|
|
||||||
} else {
|
|
||||||
throw new BadRequestException(`微信退款失败: ${result.status}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[微信退款失败] 订单号: ${order.orderNo}, 错误:`, error);
|
console.error(`[微信退款失败] 订单号: ${order.orderNo}, 错误:`, error);
|
||||||
throw new BadRequestException(`微信退款失败: ${error.message || '未知错误'}`);
|
throw new BadRequestException(`微信退款失败: ${error.message || '未知错误'}`);
|
||||||
|
|||||||
+2
-1
@@ -1,9 +1,10 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { WechatPayService } from './wechat-pay.service';
|
import { WechatPayService } from './wechat-pay.service';
|
||||||
|
import { SecretModule } from '@/modules/shared/secret/secret.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule, SecretModule],
|
||||||
providers: [WechatPayService],
|
providers: [WechatPayService],
|
||||||
exports: [WechatPayService],
|
exports: [WechatPayService],
|
||||||
})
|
})
|
||||||
+50
-34
@@ -1,31 +1,51 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { Wechatpay, Payment } from 'wechatpay-node-v3';
|
import { SecretService } from '@/modules/shared/secret/secret.service';
|
||||||
|
import Wechatpay = require('wechatpay-node-v3');
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WechatPayService {
|
export class WechatPayService {
|
||||||
private readonly logger = new Logger(WechatPayService.name);
|
private readonly logger = new Logger(WechatPayService.name);
|
||||||
private pay: Wechatpay;
|
private pay: Wechatpay;
|
||||||
|
private initialized = false;
|
||||||
|
|
||||||
constructor(private configService: ConfigService) {
|
constructor(
|
||||||
const appid = this.configService.get<string>('WECHAT_APPID');
|
private configService: ConfigService,
|
||||||
const mchid = this.configService.get<string>('WECHAT_MCHID');
|
private secretService: SecretService,
|
||||||
const privateKey = this.configService.get<string>('WECHAT_PRIVATE_KEY');
|
) {
|
||||||
const serialNo = this.configService.get<string>('WECHAT_SERIAL_NO');
|
this.initPayClient();
|
||||||
const apiv3Key = this.configService.get<string>('WECHAT_APIV3_KEY');
|
}
|
||||||
|
|
||||||
if (!appid || !mchid || !privateKey || !serialNo || !apiv3Key) {
|
/**
|
||||||
this.logger.warn('微信支付配置不完整,支付功能将不可用');
|
* 初始化微信支付客户端
|
||||||
return;
|
*/
|
||||||
|
private async initPayClient() {
|
||||||
|
try {
|
||||||
|
const appid = await this.secretService.getDecryptedValue('WECHAT_APPID');
|
||||||
|
const mchid = await this.secretService.getDecryptedValue('WECHAT_MCHID');
|
||||||
|
const privateKey = await this.secretService.getDecryptedValue('WECHAT_PRIVATE_KEY');
|
||||||
|
const serialNo = await this.secretService.getDecryptedValue('WECHAT_SERIAL_NO');
|
||||||
|
const apiv3Key = await this.secretService.getDecryptedValue('WECHAT_APIV3_KEY');
|
||||||
|
|
||||||
|
if (!appid || !mchid || !privateKey || !serialNo || !apiv3Key) {
|
||||||
|
this.logger.warn('微信支付配置不完整,支付功能将不可用');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pay = new Wechatpay({
|
||||||
|
appid,
|
||||||
|
mchid,
|
||||||
|
publicKey: Buffer.from(privateKey.replace(/\\n/g, '\n')),
|
||||||
|
privateKey: Buffer.from(privateKey.replace(/\\n/g, '\n')),
|
||||||
|
serial_no: serialNo,
|
||||||
|
key: apiv3Key,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
this.logger.log('微信支付客户端初始化成功');
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`微信支付客户端初始化失败: ${error.message}`, error.stack);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pay = new Wechatpay({
|
|
||||||
appid,
|
|
||||||
mchid,
|
|
||||||
privateKey: Buffer.from(privateKey.replace(/\\n/g, '\n')),
|
|
||||||
serialNo,
|
|
||||||
apiv3Key,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,7 +58,7 @@ export class WechatPayService {
|
|||||||
openid: string;
|
openid: string;
|
||||||
notifyUrl: string;
|
notifyUrl: string;
|
||||||
}) {
|
}) {
|
||||||
if (!this.pay) {
|
if (!this.initialized || !this.pay) {
|
||||||
throw new Error('微信支付未配置');
|
throw new Error('微信支付未配置');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +90,7 @@ export class WechatPayService {
|
|||||||
* 查询订单
|
* 查询订单
|
||||||
*/
|
*/
|
||||||
async queryOrder(orderNo: string) {
|
async queryOrder(orderNo: string) {
|
||||||
if (!this.pay) {
|
if (!this.initialized || !this.pay) {
|
||||||
throw new Error('微信支付未配置');
|
throw new Error('微信支付未配置');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,14 +109,12 @@ export class WechatPayService {
|
|||||||
* 关闭订单
|
* 关闭订单
|
||||||
*/
|
*/
|
||||||
async closeOrder(orderNo: string) {
|
async closeOrder(orderNo: string) {
|
||||||
if (!this.pay) {
|
if (!this.initialized || !this.pay) {
|
||||||
throw new Error('微信支付未配置');
|
throw new Error('微信支付未配置');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.pay.close({
|
await this.pay.close(orderNo);
|
||||||
out_trade_no: orderNo,
|
|
||||||
});
|
|
||||||
this.logger.log(`关闭微信支付订单: ${orderNo}`);
|
this.logger.log(`关闭微信支付订单: ${orderNo}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`关闭微信支付订单失败: ${error.message}`, error.stack);
|
this.logger.error(`关闭微信支付订单失败: ${error.message}`, error.stack);
|
||||||
@@ -115,7 +133,7 @@ export class WechatPayService {
|
|||||||
reason: string;
|
reason: string;
|
||||||
notifyUrl?: string;
|
notifyUrl?: string;
|
||||||
}) {
|
}) {
|
||||||
if (!this.pay) {
|
if (!this.initialized || !this.pay) {
|
||||||
throw new Error('微信支付未配置');
|
throw new Error('微信支付未配置');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,14 +162,12 @@ export class WechatPayService {
|
|||||||
* 查询退款
|
* 查询退款
|
||||||
*/
|
*/
|
||||||
async queryRefund(refundNo: string) {
|
async queryRefund(refundNo: string) {
|
||||||
if (!this.pay) {
|
if (!this.initialized || !this.pay) {
|
||||||
throw new Error('微信支付未配置');
|
throw new Error('微信支付未配置');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.pay.find_refunds({
|
const result = await this.pay.find_refunds(refundNo);
|
||||||
out_refund_no: refundNo,
|
|
||||||
});
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`查询微信退款失败: ${error.message}`, error.stack);
|
this.logger.error(`查询微信退款失败: ${error.message}`, error.stack);
|
||||||
@@ -162,19 +178,19 @@ export class WechatPayService {
|
|||||||
/**
|
/**
|
||||||
* 验证回调签名
|
* 验证回调签名
|
||||||
*/
|
*/
|
||||||
verifySignature(params: {
|
async verifySignature(params: {
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
nonce: string;
|
nonce: string;
|
||||||
body: string;
|
body: string;
|
||||||
signature: string;
|
signature: string;
|
||||||
serial: string;
|
serial: string;
|
||||||
}): boolean {
|
}): Promise<boolean> {
|
||||||
if (!this.pay) {
|
if (!this.initialized || !this.pay) {
|
||||||
throw new Error('微信支付未配置');
|
throw new Error('微信支付未配置');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return this.pay.verifySign(params);
|
return await this.pay.verifySign(params);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`验证微信支付签名失败: ${error.message}`, error.stack);
|
this.logger.error(`验证微信支付签名失败: ${error.message}`, error.stack);
|
||||||
return false;
|
return false;
|
||||||
@@ -185,7 +201,7 @@ export class WechatPayService {
|
|||||||
* 解密回调数据
|
* 解密回调数据
|
||||||
*/
|
*/
|
||||||
decryptData(ciphertext: string, nonce: string, associatedData: string): any {
|
decryptData(ciphertext: string, nonce: string, associatedData: string): any {
|
||||||
if (!this.pay) {
|
if (!this.initialized || !this.pay) {
|
||||||
throw new Error('微信支付未配置');
|
throw new Error('微信支付未配置');
|
||||||
}
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user