mockpay功能调试
CI / verify (pull_request) Has been cancelled

This commit is contained in:
2026-08-20 00:51:18 +08:00
parent 2f633b307c
commit 4db21cf1a1
11 changed files with 320 additions and 531 deletions
@@ -0,0 +1,73 @@
import { config } from 'dotenv';
import { resolve } from 'path';
import { PrismaClient } from '@prisma/client';
config({ path: resolve('.env'), override: true });
function mockPayEnabled() {
return process.env.MOCK_PAY !== 'false';
}
function wechatPayEnabled() {
const e = process.env;
const appId = e.WX_MINI_APP_ID?.trim() || e.WX_APP_ID?.trim();
return (
!mockPayEnabled() &&
!!(
appId &&
e.WX_MCH_ID?.trim() &&
e.WX_MCH_SERIAL_NO?.trim() &&
e.WX_MCH_PRIVATE_KEY?.trim() &&
e.WX_API_V3_KEY?.trim()
)
);
}
const prisma = new PrismaClient();
console.log('=== .env only ===');
console.log('MOCK_PAY:', process.env.MOCK_PAY);
console.log('mockPayEnabled:', mockPayEnabled());
const rows = await prisma.systemConfig.findMany({
where: { configKey: { in: ['MOCK_PAY', 'MOCK_SMS', 'MOCK_WECHAT', 'WX_MCH_ID'] } },
});
console.log('\n=== system_config (DB) ===');
for (const r of rows) {
console.log(`${r.configKey} = ${r.configKey.includes('KEY') ? '(redacted)' : r.value}`);
process.env[r.configKey] = r.value;
}
console.log('\n=== After DB overlay (API startup) ===');
console.log('MOCK_PAY:', process.env.MOCK_PAY);
console.log('mockPayEnabled:', mockPayEnabled());
console.log('wechatPayEnabled:', wechatPayEnabled());
const orders = await prisma.order.findMany({
where: { orderType: 'PROXY' },
orderBy: { id: 'desc' },
take: 5,
select: {
id: true,
orderNo: true,
status: true,
payStatus: true,
payExternalNo: true,
channelSource: true,
createdAt: true,
},
});
console.log('\n=== Recent PROXY orders ===');
for (const o of orders) {
console.log({
id: o.id.toString(),
orderNo: o.orderNo,
status: o.status,
payStatus: o.payStatus,
payExternalNo: o.payExternalNo,
channelSource: o.channelSource,
createdAt: o.createdAt.toISOString(),
});
}
await prisma.$disconnect();
@@ -1,30 +1,19 @@
import { Injectable } from '@nestjs/common';
import { loadAppConfig } from '@dukang/shared-types';
import { PrismaService } from '../../common/prisma/prisma.module';
import type { IPayProvider, PayMethod, PayOrderResult, RefundOrderResult } from './pay.interface';
@Injectable()
export class PayMockProvider implements IPayProvider {
constructor(private readonly prisma: PrismaService) {}
async payOrder(
orderId: bigint,
_openId?: string,
_platform?: 'h5' | 'mini',
payMethod: PayMethod = 'JSAPI',
_payMethod: PayMethod = 'JSAPI',
): Promise<PayOrderResult> {
if (!loadAppConfig().mockPay) {
throw new Error('Real WeChat pay requires PayWechatProvider');
}
if (payMethod === 'NATIVE') {
const order = await this.prisma.order.findUnique({ where: { id: orderId } });
const orderNo = order?.orderNo ?? orderId.toString();
return {
mode: 'native',
codeUrl: `mock://wechat-pay/native?orderNo=${encodeURIComponent(orderNo)}`,
externalNo: `MOCK-NATIVE-${Date.now()}`,
};
}
return { mode: 'mock', externalNo: `MOCK-${Date.now()}` };
}
@@ -21,15 +21,6 @@ export class PayWechatProvider implements IPayProvider {
payMethod: PayMethod = 'JSAPI',
): Promise<PayOrderResult> {
if (loadAppConfig().mockPay) {
if (payMethod === 'NATIVE') {
const order = await this.prisma.order.findUnique({ where: { id: orderId } });
const orderNo = order?.orderNo ?? orderId.toString();
return {
mode: 'native',
codeUrl: `mock://wechat-pay/native?orderNo=${encodeURIComponent(orderNo)}`,
externalNo: `MOCK-NATIVE-${Date.now()}`,
};
}
return { mode: 'mock', externalNo: `MOCK-${Date.now()}` };
}
if (!this.wechat.isPayEnabled()) {
@@ -25,15 +25,15 @@ export class AdminOrdersService {
private readonly adminRedeemService: AdminRedeemService,
) {}
/** v3.5.1 #1:发布会大屏,返回全部订单(按时间倒序,上限 2000) */
/** v3.5.1 #1:发布会大屏,仅已付款订单;按付款时间倒序 */
async listBigScreen(limit?: string) {
const take = Math.min(Math.max(Number(limit) || 2000, 1), 2000);
const orders = await this.prisma.order.findMany({
where: {
isTest: false,
payAmount: { gte: 100 },
payStatus: 'PAID',
},
orderBy: { createdAt: 'desc' },
orderBy: [{ paidAt: 'desc' }, { createdAt: 'desc' }],
take,
include: { user: { select: { phone: true } } },
});
@@ -41,12 +41,14 @@ export class AdminOrdersService {
items: orders.map((o) => {
const spec = o.productSpec ? ` ${o.productSpec}` : '';
const phone = o.user?.phone || o.receiverPhone;
const paidAt = o.paidAt?.toISOString() ?? o.createdAt.toISOString();
return {
id: o.id.toString(),
orderNo: o.orderNo,
payAmount: Number(o.payAmount),
items: `${o.productName}${spec} × ${o.quantity}`,
createdAt: o.createdAt.toISOString(),
createdAt: paidAt,
paidAt,
userPhoneMasked: phone ? maskContactPhone(phone) : null,
};
}),
@@ -24,7 +24,7 @@ import { AuthService } from '../iam/auth.service';
import { PromoCodeService } from '../promo/promo-code.service';
import { TicketService } from '../common/ticket.service';
import { PAY_PROVIDER, DELIVERY_PROVIDER } from '../../integrations/integrations.constants';
import { IPayProvider } from '../../integrations/pay/pay.interface';
import { IPayProvider, type PayOrderResult } from '../../integrations/pay/pay.interface';
import { IDeliveryProvider } from '../../integrations/delivery/delivery.interface';
import { IpGeoService } from '../../common/geo/ip-geo.service';
import { buildOrderClientLocationSnapshot } from '../../common/geo/client-location.util';
@@ -2173,6 +2173,9 @@ export class TradeService {
throw new BadRequestException('订单已超时未支付');
}
const mockPaid = await this.tryMockPayProxyOrder(order, 'PARTNER_PROXY_MOCK_PAY');
if (mockPaid) return mockPaid;
const lockedMethod = this.parseProxyPayMethod(order.channelSource);
if (lockedMethod && lockedMethod !== payMethod) {
throw new BadRequestException(
@@ -2217,33 +2220,7 @@ export class TradeService {
throw e;
}
if (payResult.mode === 'native') {
return {
mode: 'native' as const,
orderId: order.id.toString(),
orderNo: order.orderNo,
codeUrl: payResult.codeUrl,
payExpireAt: order.payExpireAt?.toISOString() ?? null,
};
}
if (payResult.mode === 'jsapi') {
return {
mode: 'jsapi' as const,
orderId: order.id.toString(),
orderNo: order.orderNo,
prepay: payResult.prepay,
payExpireAt: order.payExpireAt?.toISOString() ?? null,
};
}
await this.markProxyOrderPaid(order.id, payResult.externalNo, 'PARTNER_PROXY_MOCK_PAY');
return {
mode: 'mock' as const,
orderId: order.id.toString(),
orderNo: order.orderNo,
payExpireAt: order.payExpireAt?.toISOString() ?? null,
};
return this.finishProxyPayResult(order, payResult, 'PARTNER_PROXY_MOCK_PAY');
}
/** 总部代下单支付:仅 Native 收款码 */
@@ -2262,6 +2239,9 @@ export class TradeService {
throw new BadRequestException('订单已超时未支付');
}
const mockPaid = await this.tryMockPayProxyOrder(order, 'HQ_PROXY_MOCK_PAY');
if (mockPaid) return mockPaid;
this.payRedeemAnomaly.onPayAttempt(Number(order.payAmount), {
orderNo: order.orderNo,
userId: order.userId,
@@ -2277,20 +2257,15 @@ export class TradeService {
});
throw e;
}
if (payResult.mode !== 'native') {
const result = await this.finishProxyPayResult(order, payResult, 'HQ_PROXY_MOCK_PAY');
if (result.mode !== 'native' && result.mode !== 'mock') {
this.payRedeemAnomaly.onPayFail('无法生成收款码', {
orderNo: order.orderNo,
userId: order.userId,
});
throw new BadRequestException('无法生成收款码');
}
return {
mode: 'native' as const,
orderId: order.id.toString(),
orderNo: order.orderNo,
codeUrl: payResult.codeUrl,
payExpireAt: order.payExpireAt?.toISOString() ?? null,
};
return result;
}
/** Mock 环境确认代下单支付 */
@@ -2375,6 +2350,64 @@ export class TradeService {
return this.fulfillmentService.getOrderTrack(orderId);
}
private async tryMockPayProxyOrder(
order: { id: bigint; orderNo: string; payExpireAt: Date | null },
operator: string,
) {
if (!loadAppConfig().mockPay) return null;
await this.markProxyOrderPaid(order.id, `MOCK-${Date.now()}`, operator);
return {
mode: 'mock' as const,
orderId: order.id.toString(),
orderNo: order.orderNo,
payExpireAt: order.payExpireAt?.toISOString() ?? null,
};
}
/** 代下单支付结果:Mock 环境直接落单,真实环境返回收款码/JSAPI */
private async finishProxyPayResult(
order: { id: bigint; orderNo: string; payExpireAt: Date | null },
payResult: PayOrderResult,
operator: string,
) {
const mockPay = loadAppConfig().mockPay;
const isMockNative =
payResult.mode === 'native' &&
(mockPay || payResult.codeUrl.startsWith('mock://'));
if (payResult.mode === 'mock' || isMockNative) {
await this.markProxyOrderPaid(order.id, payResult.externalNo, operator);
return {
mode: 'mock' as const,
orderId: order.id.toString(),
orderNo: order.orderNo,
payExpireAt: order.payExpireAt?.toISOString() ?? null,
};
}
if (payResult.mode === 'native') {
return {
mode: 'native' as const,
orderId: order.id.toString(),
orderNo: order.orderNo,
codeUrl: payResult.codeUrl,
payExpireAt: order.payExpireAt?.toISOString() ?? null,
};
}
if (payResult.mode === 'jsapi') {
return {
mode: 'jsapi' as const,
orderId: order.id.toString(),
orderNo: order.orderNo,
prepay: payResult.prepay,
payExpireAt: order.payExpireAt?.toISOString() ?? null,
};
}
throw new BadRequestException('支付发起失败');
}
private async markProxyOrderPaid(orderId: bigint, externalNo: string, operator: string) {
const order = await this.prisma.order.findUnique({ where: { id: orderId } });
if (!order) throw new NotFoundException('订单不存在');