import { Injectable, Logger } from '@nestjs/common'; import { PrismaService } from '../../prisma/prisma.service'; import * as crypto from 'crypto'; interface GatewayCreateOrderParams { merchantOrderId: string; amount: number; paymentMethod: 'alipay' | 'wechat'; subject?: string; remark?: string; } interface GatewayCreateOrderResult { gatewayOrderId: string; merchantOrderId: string; amount: number; paymentMethod: string; payUrl?: string; qrcode?: string; status: string; } export interface GatewayQueryResult { gatewayOrderId: string; merchantOrderId: string; amount: number; paymentMethod: string; status: string; transactionId?: string; paidAt?: string; } @Injectable() export class GatewayPayService { private readonly logger = new Logger(GatewayPayService.name); private readonly apiKey: string; private readonly apiSecret: string; private readonly baseUrl: string; private readonly notifyUrl: string; private readonly mock: boolean; constructor(private prisma: PrismaService) { this.apiKey = process.env.GATEWAY_PAY_API_KEY || ''; this.apiSecret = process.env.GATEWAY_PAY_API_SECRET || ''; this.baseUrl = (process.env.GATEWAY_PAY_BASE_URL || 'http://localhost:8100').replace(/\/+$/, ''); this.notifyUrl = process.env.GATEWAY_PAY_NOTIFY_URL || 'https://yuzhiran.com/api/v1/payment/gateway/webhook'; this.mock = process.env.GATEWAY_PAY_MOCK === 'true' || !this.apiKey; if (this.mock) { this.logger.warn('GATEWAY_PAY_MOCK=true,使用模拟支付模式'); } } private signRequest(method: string, path: string, body: Record): string { const timestamp = String(Math.floor(Date.now() / 1000)); const bodySha256 = crypto.createHash('sha256').update(JSON.stringify(body)).digest('hex'); const signStr = `${method}\n${path}\n${timestamp}\n${bodySha256}`; const signature = crypto.createHmac('sha256', this.apiSecret).update(signStr).digest('hex'); return `PAY ${this.apiKey}:${timestamp}:${signature}`; } private signGetRequest(method: string, path: string): string { const timestamp = String(Math.floor(Date.now() / 1000)); const emptyHash = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; const signStr = `${method}\n${path}\n${timestamp}\n${emptyHash}`; const signature = crypto.createHmac('sha256', this.apiSecret).update(signStr).digest('hex'); return `PAY ${this.apiKey}:${timestamp}:${signature}`; } async createOrder(params: GatewayCreateOrderParams): Promise { if (this.mock) { this.logger.log(`模拟下单: ${params.merchantOrderId}, ${params.paymentMethod}`); return { gatewayOrderId: `mock_${Date.now()}`, merchantOrderId: params.merchantOrderId, amount: params.amount, paymentMethod: params.paymentMethod, payUrl: params.paymentMethod === 'alipay' ? 'mock://pay' : undefined, qrcode: params.paymentMethod === 'wechat' ? 'mock://pay' : undefined, status: 'pending', }; } const body: Record = { merchant_order_id: params.merchantOrderId, amount: params.amount, payment_method: params.paymentMethod, subject: params.subject || '宇之然AI会员充值', notify_url: this.notifyUrl, }; if (params.remark) { body.remark = params.remark; } const auth = this.signRequest('POST', '/v1/pay/orders', body); try { const res = await fetch(`${this.baseUrl}/v1/pay/orders`, { method: 'POST', headers: { Authorization: auth, 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const json = await res.json(); if (json.code !== 0) throw new Error(json.message || '网关下单失败'); this.logger.log(`网关下单成功: ${params.merchantOrderId}, gatewayId: ${json.data.gateway_order_id}`); return { gatewayOrderId: json.data.gateway_order_id, merchantOrderId: json.data.merchant_order_id, amount: json.data.amount, paymentMethod: json.data.payment_method, payUrl: json.data.pay_url, qrcode: json.data.qrcode, status: json.data.status, }; } catch (err: any) { this.logger.error(`网关下单失败: ${err.message},使用模拟模式`); return { gatewayOrderId: `mock_${Date.now()}`, merchantOrderId: params.merchantOrderId, amount: params.amount, paymentMethod: params.paymentMethod, payUrl: params.paymentMethod === 'alipay' ? 'mock://pay' : undefined, qrcode: params.paymentMethod === 'wechat' ? 'mock://pay' : undefined, status: 'pending', }; } } async queryOrder(gatewayOrderId: string): Promise { if (this.mock || gatewayOrderId.startsWith('mock_')) { const order = await this.prisma.order.findFirst({ where: { gatewayOrderId } }); return { gatewayOrderId, merchantOrderId: order?.orderNo || '', amount: order?.amount || 0, paymentMethod: order?.payChannel || 'alipay', status: order?.status === 'PAID' ? 'paid' : 'pending', transactionId: order?.transactionId || undefined, }; } const path = `/v1/pay/orders/${gatewayOrderId}`; const auth = this.signGetRequest('GET', path); try { const res = await fetch(`${this.baseUrl}${path}`, { headers: { Authorization: auth }, }); const json = await res.json(); if (json.code !== 0) throw new Error(json.message || '查询失败'); return { gatewayOrderId: json.data.gateway_order_id, merchantOrderId: json.data.merchant_order_id, amount: json.data.amount, paymentMethod: json.data.payment_method, status: json.data.status, transactionId: json.data.transaction_id, paidAt: json.data.paid_at, }; } catch (err: any) { this.logger.error(`网关查询失败: ${err.message}`); return { gatewayOrderId, merchantOrderId: '', amount: 0, paymentMethod: 'alipay', status: 'unknown', }; } } async closeOrder(gatewayOrderId: string): Promise { if (this.mock || gatewayOrderId.startsWith('mock_')) { this.logger.log(`模拟关闭订单: ${gatewayOrderId}`); return true; } const path = `/v1/pay/orders/${gatewayOrderId}/close`; const auth = this.signRequest('POST', path, {}); try { const res = await fetch(`${this.baseUrl}${path}`, { method: 'POST', headers: { Authorization: auth, 'Content-Type': 'application/json' }, body: '{}', }); const json = await res.json(); if (json.code !== 0) throw new Error(json.message || '关闭订单失败'); this.logger.log(`网关订单已关闭: ${gatewayOrderId}`); return true; } catch (err: any) { this.logger.error(`关闭网关订单失败: ${err.message}`); return false; } } async syncOrderStatus(orderNo: string): Promise<{ updated: boolean; status: string }> { const order = await this.prisma.order.findUnique({ where: { orderNo } }); if (!order) return { updated: false, status: 'not_found' }; if (order.status !== 'PENDING' || !order.gatewayOrderId) { return { updated: false, status: order.status }; } const gatewayResult = await this.queryOrder(order.gatewayOrderId); if (gatewayResult.status === 'paid' && order.status === 'PENDING') { await this.prisma.order.update({ where: { id: order.id }, data: { status: 'PAID', paidAt: gatewayResult.paidAt ? new Date(gatewayResult.paidAt) : new Date(), transactionId: gatewayResult.transactionId || order.transactionId, }, }); await this.activateSubscription(order.id); this.logger.log(`订单状态同步成功: ${orderNo} (PENDING→PAID)`); return { updated: true, status: 'PAID' }; } return { updated: false, status: gatewayResult.status }; } async refund(merchantOrderId: string, amount?: number, reason?: string) { if (this.mock) { this.logger.log(`模拟网关退款: ${merchantOrderId}`); await this.prisma.order.update({ where: { orderNo: merchantOrderId }, data: { status: 'REFUNDED' }, }).catch(err => this.logger.warn(`退款更新订单失败: ${err.message}`)); return { code: 0, message: '模拟退款成功' }; } const body: Record = { merchant_order_id: merchantOrderId, reason: reason || '用户申请退款', }; if (amount) body.amount = amount; const auth = this.signRequest('POST', '/v1/pay/refunds', body); try { const res = await fetch(`${this.baseUrl}/v1/pay/refunds`, { method: 'POST', headers: { Authorization: auth, 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const json = await res.json(); if (json.code === 0) { await this.prisma.order.update({ where: { orderNo: merchantOrderId }, data: { status: 'REFUNDED' }, }).catch(err => this.logger.warn(`退款更新订单失败: ${err.message}`)); } return json; } catch (err: any) { this.logger.error(`网关退款失败: ${err.message}`); throw err; } } async handleWebhook(body: any): Promise<{ code: string; message: string }> { const event = body?.event; const data = body?.data; if (!event || !data) { return { code: 'FAIL', message: '无效的webhook数据' }; } this.logger.log(`网关webhook事件: ${event}`); if (event === 'recharge.completed') { const merchantOrderId = data.merchant_order_id; const gatewayOrderId = data.order_id; const transactionId = data.transaction_id; if (!merchantOrderId) { return { code: 'FAIL', message: '缺少商户订单号' }; } const order = await this.prisma.order.findUnique({ where: { orderNo: merchantOrderId }, include: { user: true }, }); if (!order) { this.logger.warn(`webhook订单不存在: ${merchantOrderId}`); return { code: 'FAIL', message: '订单不存在' }; } if (order.status === 'PAID') { this.logger.log(`webhook订单已支付,跳过: ${merchantOrderId}`); return { code: 'SUCCESS', message: '订单已处理' }; } await this.prisma.order.update({ where: { id: order.id }, data: { status: 'PAID', paidAt: new Date(), transactionId: transactionId || order.transactionId, gatewayOrderId: gatewayOrderId || order.gatewayOrderId, payChannel: data.payment_method === 'alipay' ? 'alipay' : 'wxpay', }, }); await this.activateSubscription(order.id); return { code: 'SUCCESS', message: '支付成功' }; } if (event === 'order.refunded') { const merchantOrderId = data.merchant_order_id; if (merchantOrderId) { await this.prisma.order.update({ where: { orderNo: merchantOrderId }, data: { status: 'REFUNDED' }, }).catch(() => {}); } return { code: 'SUCCESS', message: '退款处理成功' }; } return { code: 'SUCCESS', message: '事件已接收' }; } private async activateSubscription(orderId: number) { const order = await this.prisma.order.findUnique({ where: { id: orderId }, include: { user: true }, }); if (!order) return; if (order.planType === 'SKILL' && order.metadata) { const meta = JSON.parse(order.metadata); if (meta.skillId) { await this.prisma.userSkill.upsert({ where: { userId_skillId: { userId: order.userId, skillId: meta.skillId } }, update: { orderId: order.id }, create: { userId: order.userId, skillId: meta.skillId, orderId: order.id }, }); this.logger.log(`网关支付激活技能: 用户${order.userId}, 技能${meta.skillId}`); } return; } if (order.planType !== 'MONTHLY' && order.planType !== 'YEARLY') return; const durationDays = order.planType === 'MONTHLY' ? 30 : 365; const now = new Date(); let subscriptionEndDate: Date; const existingSub = await this.prisma.subscription.findFirst({ where: { userId: order.userId, status: 'ACTIVE', endDate: { gt: now }, }, }); if (existingSub) { subscriptionEndDate = new Date(existingSub.endDate.getTime() + durationDays * 24 * 60 * 60 * 1000); await this.prisma.subscription.update({ where: { id: existingSub.id }, data: { endDate: subscriptionEndDate }, }); } else { subscriptionEndDate = new Date(now); subscriptionEndDate.setDate(subscriptionEndDate.getDate() + durationDays); await this.prisma.subscription.create({ data: { userId: order.userId, plan: order.planType as any, startDate: now, endDate: subscriptionEndDate, status: 'ACTIVE', }, }); } await this.prisma.user.update({ where: { id: order.userId }, data: { memberPlan: order.planType as any, memberExpire: subscriptionEndDate, }, }); this.logger.log(`网关支付激活订阅: 用户${order.userId}, 类型${order.planType}, 到期${subscriptionEndDate}`); } }