|
|
@@ -0,0 +1,126 @@
|
|
|
+import { FastifyInstance } from 'fastify'
|
|
|
+import {
|
|
|
+ CreatePaymentParams,
|
|
|
+ CreatePaymentResponse,
|
|
|
+ PaymentNotifyParams,
|
|
|
+ PaymentQueryParams,
|
|
|
+ PaymentQueryResponse
|
|
|
+} from '../dto/payment.dto'
|
|
|
+import axios from 'axios'
|
|
|
+import crypto from 'crypto'
|
|
|
+
|
|
|
+export class PaymentService {
|
|
|
+ private app: FastifyInstance
|
|
|
+ private url: string
|
|
|
+ private key: string
|
|
|
+ private pid: string
|
|
|
+
|
|
|
+ constructor(app: FastifyInstance) {
|
|
|
+ this.app = app
|
|
|
+ this.url = app.config.PAYMENT_URL
|
|
|
+ this.key = app.config.PAYMENT_KEY
|
|
|
+ this.pid = app.config.PAYMENT_PID
|
|
|
+ }
|
|
|
+
|
|
|
+ private generateSign(params: Record<string, any>, key: string): string {
|
|
|
+ const filtered = Object.keys(params)
|
|
|
+ .filter(k => params[k] !== undefined && params[k] !== '' && k !== 'sign' && k !== 'sign_type')
|
|
|
+ .sort()
|
|
|
+ .map(k => `${k}=${params[k]}`)
|
|
|
+ .join('&')
|
|
|
+
|
|
|
+ const strToSign = filtered + key
|
|
|
+
|
|
|
+ return crypto.createHash('md5').update(strToSign, 'utf8').digest('hex')
|
|
|
+ }
|
|
|
+
|
|
|
+ private verifyNotifySign(params: PaymentNotifyParams): boolean {
|
|
|
+ const { sign: receivedSign, ...rest } = params
|
|
|
+ const calculatedSign = this.generateSign(rest, this.key)
|
|
|
+ return calculatedSign === receivedSign
|
|
|
+ }
|
|
|
+
|
|
|
+ async createOrder(params: CreatePaymentParams): Promise<CreatePaymentResponse> {
|
|
|
+ try {
|
|
|
+ const sign = this.generateSign(params, this.key)
|
|
|
+
|
|
|
+ const requestData = {
|
|
|
+ ...params,
|
|
|
+ sign,
|
|
|
+ sign_type: 'MD5'
|
|
|
+ }
|
|
|
+
|
|
|
+ const { data } = await axios.post<CreatePaymentResponse>(this.url + '/qrcode', requestData, {
|
|
|
+ headers: { 'Content-Type': 'application/json' }
|
|
|
+ })
|
|
|
+
|
|
|
+ // {
|
|
|
+ // "code": 1,
|
|
|
+ // "msg": "创建订单成功。",
|
|
|
+ // "trade_no": "10000202509181600155",
|
|
|
+ // "out_trade_no": "202509181234567",
|
|
|
+ // "code_url": "https://dt-2025091816.front.mijieqi.cn/Pay/5NlZddZO/CheckOrder"
|
|
|
+ // }
|
|
|
+
|
|
|
+ return data
|
|
|
+ } catch (error) {
|
|
|
+ this.app.log.error('Failed to create payment order:', error)
|
|
|
+ throw error
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async handlePaymentNotify(params: PaymentNotifyParams): Promise<void> {
|
|
|
+ try {
|
|
|
+ if (!this.verifyNotifySign(params)) {
|
|
|
+ throw new Error('Verify notify sign failed')
|
|
|
+ }
|
|
|
+
|
|
|
+ if (params.trade_status === 'TRADE_SUCCESS') {
|
|
|
+ this.app.log.info('Payment success, out_trade_no: ' + params.out_trade_no)
|
|
|
+ } else {
|
|
|
+ this.app.log.warn('Payment failed, out_trade_no: ' + params.out_trade_no)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ this.app.log.error('Failed to handle payment notification:', error)
|
|
|
+ throw error
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async queryOrder(params: { trade_no?: string; out_trade_no?: string }): Promise<PaymentQueryResponse> {
|
|
|
+ try {
|
|
|
+ if (!params.trade_no && !params.out_trade_no) {
|
|
|
+ throw new Error('trade_no and out_trade_no are required')
|
|
|
+ }
|
|
|
+ const queryParams: PaymentQueryParams = {
|
|
|
+ act: 'order',
|
|
|
+ pid: this.pid,
|
|
|
+ key: this.key,
|
|
|
+ trade_no: params.trade_no,
|
|
|
+ out_trade_no: params.out_trade_no
|
|
|
+ }
|
|
|
+
|
|
|
+ const { data } = await axios.get<PaymentQueryResponse>(this.url + '/api.php', {
|
|
|
+ params: queryParams
|
|
|
+ })
|
|
|
+
|
|
|
+ // {
|
|
|
+ // "code": 1,
|
|
|
+ // "msg": "查询订单号成功!",
|
|
|
+ // "pid": 1000,
|
|
|
+ // "trade_no": "10000202509181600155",
|
|
|
+ // "out_trade_no": "202509181234567",
|
|
|
+ // "name": "测试商品",
|
|
|
+ // "addtime": "2025-09-18 16:48:59",
|
|
|
+ // "endtime": null,
|
|
|
+ // "status": 0,
|
|
|
+ // "money": "1.00",
|
|
|
+ // "type": "alipay"
|
|
|
+ // }
|
|
|
+
|
|
|
+ return data
|
|
|
+ } catch (error) {
|
|
|
+ this.app.log.error('Failed to query payment order:', error)
|
|
|
+ throw error
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|