Browse Source

新增支付功能,包括支付控制器、服务和路由,更新环境配置以支持支付相关参数,同时调整应用程序标题和描述。

wuyi 3 months ago
parent
commit
cf18c60366

+ 6 - 1
.env

@@ -19,4 +19,9 @@ OSS_SECRET=YTAgTr8lWX4IrtDBM2Efpqa0iD5FfE
 OSS_BUCKET=afjp282x4b
 OSS_REGION=oss-ap-southeast-3
 OSS_ENDPOINT=https://oss-ap-southeast-3.aliyuncs.com
-UPLOAD_FOLDER=base
+UPLOAD_FOLDER=base
+
+# PAY
+PAYMENT_URL=https://backend-new.mijieqi.cn
+PAYMENT_KEY=QOwfoFwMg8dMDM5CDqmkwFBHHcW3hF3C
+PAYMENT_PID=1000

+ 6 - 1
.env.production

@@ -19,4 +19,9 @@ OSS_SECRET=YTAgTr8lWX4IrtDBM2Efpqa0iD5FfE
 OSS_BUCKET=afjp282x4b
 OSS_REGION=oss-ap-southeast-3
 OSS_ENDPOINT=https://oss-ap-southeast-3.aliyuncs.com
-UPLOAD_FOLDER=base
+UPLOAD_FOLDER=base
+
+# PAY
+PAYMENT_URL=https://backend-new.mijieqi.cn
+PAYMENT_KEY=QOwfoFwMg8dMDM5CDqmkwFBHHcW3hF3C
+PAYMENT_PID=1000

+ 4 - 2
src/app.ts

@@ -16,6 +16,7 @@ import financeRoutes from './routes/finance.routes'
 import teamRoutes from './routes/team.routes'
 import teamMembersRoutes from './routes/team-members.routes'
 import promotionLinkRoutes from './routes/promotion-link.routes'
+import paymentRoutes from './routes/payment.routes'
 
 const options: FastifyEnvOptions = {
   schema: schema,
@@ -62,8 +63,8 @@ export const createApp = async () => {
   app.register(swagger, {
     swagger: {
       info: {
-        title: 'Robin API',
-        description: 'Robin API documentation',
+        title: 'Wuma API',
+        description: 'Wuma API documentation',
         version: '1.0.0'
       },
       host: 'localhost',
@@ -87,6 +88,7 @@ export const createApp = async () => {
   app.register(teamRoutes, { prefix: '/api/teams' })
   app.register(teamMembersRoutes, { prefix: '/api/members' })
   app.register(promotionLinkRoutes, { prefix: '/api/links' })
+  app.register(paymentRoutes, { prefix: '/api/payment' })
 
   const dataSource = createDataSource(app)
   await dataSource.initialize()

+ 13 - 1
src/config/env.ts

@@ -16,7 +16,10 @@ export const schema = {
     'OSS_BUCKET',
     'OSS_REGION',
     'OSS_ENDPOINT',
-    'UPLOAD_FOLDER'
+    'UPLOAD_FOLDER',
+    'PAYMENT_URL',
+    'PAYMENT_KEY',
+    'PAYMENT_PID'
   ],
   properties: {
     PORT: {
@@ -67,6 +70,15 @@ export const schema = {
     },
     UPLOAD_FOLDER: {
       type: 'string'
+    },
+    PAYMENT_URL: {
+      type: 'string'
+    },
+    PAYMENT_KEY: {
+      type: 'string'
+    },
+    PAYMENT_PID: {
+      type: 'string'
     }
   }
 }

+ 54 - 0
src/controllers/payment.controller.ts

@@ -0,0 +1,54 @@
+import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
+import { PaymentService } from '../services/payment.service'
+import { CreatePaymentParams, PaymentNotifyParams } from '../dto/payment.dto'
+
+export class PaymentController {
+  private paymentService: PaymentService
+
+  constructor(app: FastifyInstance) {
+    this.paymentService = new PaymentService(app)
+  }
+
+  async createOrder(request: FastifyRequest<{ Body: CreatePaymentParams }>, reply: FastifyReply) {
+    try {
+      const result = await this.paymentService.createOrder(request.body)
+      return reply.code(200).send(result)
+    } catch (error) {
+      return reply.code(500).send('Failed to create payment order')
+    }
+  }
+
+  async notifyOrder(request: FastifyRequest<{ Body: PaymentNotifyParams }>, reply: FastifyReply) {
+    try {
+      await this.paymentService.handlePaymentNotify(request.body)
+      return reply.send('success')
+    } catch (error) {
+      return reply.code(500).send('Failed to handle payment notification')
+    }
+  }
+
+  async queryOrder(
+    request: FastifyRequest<{
+      Querystring: {
+        trade_no?: string
+        out_trade_no?: string
+      }
+    }>,
+    reply: FastifyReply
+  ) {
+    try {
+      const { trade_no, out_trade_no } = request.query
+
+      if (!trade_no && !out_trade_no) {
+        return reply.code(400).send({
+          message: 'trade_no and out_trade_no are required'
+        })
+      }
+
+      const result = await this.paymentService.queryOrder({ trade_no, out_trade_no })
+      return reply.send(result)
+    } catch (error) {
+      return reply.code(500).send('Failed to query payment order')
+    }
+  }
+}

+ 52 - 0
src/dto/payment.dto.ts

@@ -0,0 +1,52 @@
+export interface CreatePaymentParams {
+  pid: number
+  type?: string | 'alipay'
+  out_trade_no: string
+  notify_url: string
+  return_url: string
+  name: string
+  money: string
+  client_ip: string
+}
+
+export interface CreatePaymentResponse {
+  code: number
+  msg: string
+  trade_no?: string
+  out_trade_no?: string
+  code_url?: string
+}
+
+export interface PaymentNotifyParams {
+  pid: number
+  out_trade_no: string
+  trade_no: string
+  name: string
+  type: 'wxpay' | 'alipay'
+  money: string
+  trade_status: 'TRADE_SUCCESS' | 'TRADE_FAIL'
+  sign: string
+  sign_type: 'MD5'
+}
+
+export interface PaymentQueryParams {
+  act: 'order'
+  pid: string | number
+  key: string
+  trade_no?: string
+  out_trade_no?: string
+}
+
+export interface PaymentQueryResponse {
+  code: number
+  msg: string
+  pid: number
+  trade_no: string
+  out_trade_no: string
+  name: string
+  addtime: string
+  endtime: string
+  status: 0 | 1
+  money: string
+  type: 'alipay' | 'wxpay'
+}

+ 25 - 0
src/routes/payment.routes.ts

@@ -0,0 +1,25 @@
+import { FastifyInstance } from 'fastify'
+import { PaymentController } from '../controllers/payment.controller'
+import { CreatePaymentParams, PaymentNotifyParams } from '../dto/payment.dto'
+
+export default async function paymentRoutes(fastify: FastifyInstance) {
+  const paymentController = new PaymentController(fastify)
+
+  // 创建
+  fastify.post<{ Body: CreatePaymentParams }>(
+    '/create',
+    paymentController.createOrder.bind(paymentController)
+  )
+
+  // 支付结果通知
+  fastify.post<{ Body: PaymentNotifyParams }>(
+    '/notify',
+    paymentController.notifyOrder.bind(paymentController)
+  )
+
+  // 查询订单
+  fastify.get(
+    '/query',
+    paymentController.queryOrder.bind(paymentController)
+  )
+}

+ 126 - 0
src/services/payment.service.ts

@@ -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
+    }
+  }
+}

+ 5 - 0
src/types/fastify.d.ts

@@ -25,6 +25,11 @@ declare module 'fastify' {
       
       // 文件上传配置
       UPLOAD_FOLDER: string
+
+      // 支付配置
+      PAYMENT_URL: string
+      PAYMENT_KEY: string
+      PAYMENT_PID: string
     }
     dataSource: DataSource
   }