Prechádzať zdrojové kódy

新增单片购买订单创建功能,更新支付控制器和服务逻辑以支持用户单片购买,同时扩展相关DTO和路由,优化收入记录查询和创建逻辑,确保支持资源ID的过滤。

wuyi 3 mesiacov pred
rodič
commit
34510169b4

+ 50 - 4
src/controllers/payment.controller.ts

@@ -4,7 +4,10 @@ import {
   CreatePaymentParams,
   PaymentNotifyParams,
   CreatePaymentOrderParams,
-  UserQueryOrderParams
+  UserQueryOrderParams,
+  CreateSinglePaymentOrderParams,
+  SingleQueryParams,
+  SingleListQueryParams
 } from '../dto/payment.dto'
 
 export class PaymentController {
@@ -39,6 +42,27 @@ export class PaymentController {
     }
   }
 
+  async createSinglePaymentOrder(
+    request: FastifyRequest<{ Body: CreateSinglePaymentOrderParams }>,
+    reply: FastifyReply
+  ) {
+    try {
+      const { resourceId, userId } = request.body
+      const ip = request.ip
+
+      if (!userId || !resourceId) {
+        return reply.code(400).send({
+          message: '缺少字段'
+        })
+      }
+
+      const result = await this.paymentService.createSinglePaymentOrder({ resourceId, userId, ip })
+      return reply.send(result)
+    } catch (error) {
+      return reply.code(500).send({ message: '创建单片购买订单失败', error })
+    }
+  }
+
   async notifyOrder(request: FastifyRequest<{ Querystring: PaymentNotifyParams }>, reply: FastifyReply) {
     try {
       await this.paymentService.handlePaymentNotify(request.query)
@@ -76,11 +100,33 @@ export class PaymentController {
   async userQueryOrder(request: FastifyRequest<{ Querystring: UserQueryOrderParams }>, reply: FastifyReply) {
     try {
       const user = request.user
-      const { orderNo } = request.query
-      const result = await this.paymentService.userQueryOrder(user.id, orderNo)
+      const { orderNo, resourceId } = request.query
+      const result = await this.paymentService.userQueryOrder(user.id, orderNo, resourceId)
+      return reply.send(result)
+    } catch (error) {
+      return reply.code(500).send('Failed to query user order')
+    }
+  }
+
+  async userSingleQuery(request: FastifyRequest<{ Querystring: SingleQueryParams }>, reply: FastifyReply) {
+    try {
+      const user = request.user
+      const { resourceId } = request.query
+      const result = await this.paymentService.userSingleQuery(user.id, resourceId)
+      return reply.send(result)
+    } catch (error) {
+      return reply.code(500).send('Failed to query user single query')
+    }
+  }
+
+  async userSingleList(request: FastifyRequest<{ Querystring: SingleListQueryParams }>, reply: FastifyReply) {
+    try {
+      const user = request.user
+      const { page = 0, size = 20, resourceId } = request.query
+      const result = await this.paymentService.userSingleList(user.id, { page, size, resourceId })
       return reply.send(result)
     } catch (error) {
-      return reply.code(500).send(error)
+      return reply.code(500).send('Failed to query user single list')
     }
   }
 }

+ 5 - 0
src/dto/income-records.dto.ts

@@ -11,6 +11,7 @@ export interface CreateIncomeRecordBody {
   orderNo: string
   payChannel: string
   payNo: string
+  resourceId?: string
 }
 
 export interface UpdateIncomeRecordBody {
@@ -23,6 +24,7 @@ export interface UpdateIncomeRecordBody {
   orderNo?: string
   payChannel?: string
   payNo?: string
+  resourceId?: string
   status?: boolean
   delFlag?: boolean
 }
@@ -35,9 +37,12 @@ export interface ListIncomeRecordsQuery extends Pagination {
   orderNo?: string
   payChannel?: string
   payNo?: string
+  resourceId?: string
   status?: boolean
   startDate?: string
   endDate?: string
+  select?: string[]
+  delFlag?: boolean
 }
 
 export interface IncomeRecordParams {

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

@@ -23,6 +23,12 @@ export interface CreatePaymentOrderParams {
   ip: string
 }
 
+export interface CreateSinglePaymentOrderParams {
+  resourceId: number
+  userId: number
+  ip: string
+}
+
 export interface CreatePaymentOrderResponse {
   code: number
   msg: string
@@ -66,6 +72,7 @@ export interface PaymentQueryResponse {
 
 export interface UserQueryOrderParams {
   orderNo?: string
+  resourceId?: string
 }
 
 export interface UserQueryOrderResponse {
@@ -73,3 +80,32 @@ export interface UserQueryOrderResponse {
   msg: string
   status: 0 | 1
 }
+
+export interface SingleQueryParams {
+  resourceId: string
+}
+
+export interface SingleQueryResponse {
+  hasAccess: boolean
+}
+
+export interface SingleListQueryParams {
+  page?: number
+  size?: number
+  resourceId?: string
+}
+
+export interface SingleListResponse {
+  content: Array<{
+    id: number
+    userId: number
+    resourceId: string
+    status: boolean
+    createdAt: string
+  }>
+  metadata: {
+    total: number
+    page: number
+    size: number
+  }
+}

+ 3 - 0
src/entities/income-records.entity.ts

@@ -45,6 +45,9 @@ export class IncomeRecords {
   })
   incomeType: IncomeType
 
+  @Column({ nullable: true })
+  resourceId: string
+
   @Column({
     type: 'enum',
     enum: OrderType,

+ 30 - 3
src/routes/payment.routes.ts

@@ -1,17 +1,30 @@
 import { FastifyInstance } from 'fastify'
 import { PaymentController } from '../controllers/payment.controller'
-import { PaymentNotifyParams, CreatePaymentOrderParams, UserQueryOrderParams } from '../dto/payment.dto'
+import {
+  PaymentNotifyParams,
+  CreatePaymentOrderParams,
+  UserQueryOrderParams,
+  CreateSinglePaymentOrderParams,
+  SingleQueryParams,
+  SingleListQueryParams
+} from '../dto/payment.dto'
 import { authenticate } from '../middlewares/auth.middleware'
 
 export default async function paymentRoutes(fastify: FastifyInstance) {
   const paymentController = new PaymentController(fastify)
 
-  // 创建支付订单
+  // 创建会员购买订单
   fastify.post<{ Body: CreatePaymentOrderParams }>(
-    '/create',
+    '/vip/create',
     paymentController.createPaymentOrder.bind(paymentController)
   )
 
+  // 创建单片购买订单
+  fastify.post<{ Body: CreateSinglePaymentOrderParams }>(
+    '/single/create',
+    paymentController.createSinglePaymentOrder.bind(paymentController)
+  )
+
   // 支付结果通知
   fastify.get<{ Querystring: PaymentNotifyParams }>('/notify', paymentController.notifyOrder.bind(paymentController))
 
@@ -24,4 +37,18 @@ export default async function paymentRoutes(fastify: FastifyInstance) {
     { onRequest: [authenticate] },
     paymentController.userQueryOrder.bind(paymentController)
   )
+
+  // 用户是否购买单片
+  fastify.get<{ Querystring: SingleQueryParams }>(
+    '/single/query',
+    { onRequest: [authenticate] },
+    paymentController.userSingleQuery.bind(paymentController)
+  )
+
+  // 用户购买列表
+  fastify.get<{ Querystring: SingleListQueryParams }>(
+    '/single/list',
+    { onRequest: [authenticate] },
+    paymentController.userSingleList.bind(paymentController)
+  )
 }

+ 39 - 5
src/services/income-records.service.ts

@@ -21,7 +21,8 @@ export class IncomeRecordsService {
       orderPrice: data.orderPrice,
       orderNo: data.orderNo,
       payChannel: data.payChannel,
-      payNo: data.payNo
+      payNo: data.payNo,
+      resourceId: data.resourceId
     })
     return this.incomeRecordsRepository.save(incomeRecord)
   }
@@ -41,9 +42,12 @@ export class IncomeRecordsService {
       orderNo,
       payChannel,
       payNo,
+      resourceId,
       status,
       startDate,
-      endDate
+      endDate,
+      select,
+      delFlag
     } = query
 
     const where: any = {}
@@ -76,6 +80,10 @@ export class IncomeRecordsService {
       where.payNo = Like(`%${payNo}%`)
     }
 
+    if (resourceId) {
+      where.resourceId = resourceId
+    }
+
     if (status !== undefined) {
       where.status = status
     }
@@ -83,13 +91,24 @@ export class IncomeRecordsService {
     if (startDate && endDate) {
       where.createdAt = Between(new Date(startDate), new Date(endDate))
     }
+    
+    if (delFlag !== undefined) {
+      where.delFlag = delFlag
+    }
 
-    const [records, total] = await this.incomeRecordsRepository.findAndCount({
+    const queryOptions: any = {
       where,
       skip: (Number(page) || 0) * (Number(size) || 20),
       take: Number(size) || 20,
       order: { createdAt: 'DESC' }
-    })
+    }
+
+    // 如果有指定字段,则只查询这些字段
+    if (select && select.length > 0) {
+      queryOptions.select = select
+    }
+
+    const [records, total] = await this.incomeRecordsRepository.findAndCount(queryOptions)
 
     return {
       content: records,
@@ -101,11 +120,14 @@ export class IncomeRecordsService {
     }
   }
 
-  async findByOrderNo(orderNo: string, userId?: number): Promise<IncomeRecords | null> {
+  async findByOrderNo(orderNo: string, userId?: number, resourceId?: string): Promise<IncomeRecords | null> {
     const where: any = { orderNo }
     if (userId) {
       where.userId = userId
     }
+    if (resourceId) {
+      where.resourceId = resourceId
+    }
     const record = await this.incomeRecordsRepository.findOne({ where })
     if (!record) {
       return null
@@ -126,6 +148,18 @@ export class IncomeRecordsService {
     return record
   }
 
+  async findSingleByResourceId(resourceId: string, userId?: number): Promise<IncomeRecords | null> {
+    const where: any = { resourceId, status: true, delFlag: false }
+    if (userId) {
+      where.userId = userId
+    }
+    const record = await this.incomeRecordsRepository.findOne({ where })
+    if (!record) {
+      return null
+    }
+    return record
+  }
+
   async update(data: UpdateIncomeRecordBody): Promise<IncomeRecords> {
     const { id, ...updateData } = data
     await this.incomeRecordsRepository.update(id, updateData)

+ 105 - 4
src/services/payment.service.ts

@@ -7,14 +7,18 @@ import {
   PaymentQueryResponse,
   CreatePaymentOrderParams,
   CreatePaymentOrderResponse,
-  UserQueryOrderResponse
+  CreateSinglePaymentOrderParams,
+  UserQueryOrderResponse,
+  SingleListQueryParams,
+  SingleListResponse
 } from '../dto/payment.dto'
 import axios from 'axios'
 import crypto from 'crypto'
 import { randomInt } from 'crypto'
 import Decimal from 'decimal.js'
 import { IncomeRecordsService } from './income-records.service'
-import { IncomeType, OrderType } from '../entities/income-records.entity'
+import { IncomeRecords, IncomeType, OrderType } from '../entities/income-records.entity'
+import { PaginationResponse } from '../dto/common.dto'
 import { MemberService } from './member.service'
 import { UserService } from './user.service'
 import { TeamService } from './team.service'
@@ -237,6 +241,67 @@ export class PaymentService {
     }
   }
 
+  async createSinglePaymentOrder(params: CreateSinglePaymentOrderParams): Promise<CreatePaymentOrderResponse> {
+    try {
+      const user = await this.userService.findById(params.userId)
+      if (!user) {
+        throw new Error('用户不存在')
+      }
+
+      // 单片购买固定价格
+      const price = '10.00'
+      const out_trade_no = `${Date.now()}${params.userId}${params.resourceId}${randomInt(0, 1e4)
+        .toString()
+        .padStart(4, '0')}`
+
+      const paymentParams: CreatePaymentParams = {
+        pid: Number(this.pid),
+        type: 'alipay',
+        out_trade_no,
+        notify_url: `https://9g15.vip/api/payment/notify`,
+        return_url: `https://yz1df.cc/video/${params.resourceId}?pay=true`,
+        name: `单片购买 - 资源ID: ${params.resourceId}`,
+        money: price,
+        client_ip: params.ip
+      }
+
+      const result = await this.createOrder(paymentParams)
+
+      // 创建收入记录
+      try {
+        // 计算佣金
+        let agentId = 0
+        let incomeAmount = new Decimal(price)
+
+        await this.incomeRecordsService.create({
+          agentId,
+          userId: user.id,
+          incomeAmount: incomeAmount.toNumber(),
+          incomeType: IncomeType.TIP,
+          orderType: OrderType.SINGLE_TIP,
+          orderPrice: new Decimal(price).toNumber(),
+          orderNo: out_trade_no,
+          payChannel: 'alipay',
+          payNo: result.trade_no || '',
+          resourceId: params.resourceId.toString()
+        })
+      } catch (incomeError) {
+        this.app.log.error('Failed to create income record:', incomeError)
+        throw new Error('Failed to create income record')
+      }
+
+      return {
+        code: result.code,
+        msg: result.msg,
+        code_url: result.code_url,
+        out_trade_no: result.out_trade_no
+      }
+    } catch (error) {
+      this.app.log.error('Failed to create single payment order:', error)
+      throw error
+    }
+  }
+
   async queryOrder(params: { trade_no?: string; out_trade_no?: string }): Promise<PaymentQueryResponse> {
     try {
       if (!params.trade_no && !params.out_trade_no) {
@@ -275,7 +340,7 @@ export class PaymentService {
     }
   }
 
-  async userQueryOrder(userId: number, orderNo?: string): Promise<UserQueryOrderResponse> {
+  async userQueryOrder(userId: number, orderNo?: string, resourceId?: string): Promise<UserQueryOrderResponse> {
     const responseData: UserQueryOrderResponse = {
       orderNo: orderNo || '',
       msg: '',
@@ -300,7 +365,7 @@ export class PaymentService {
         incomeRecord = recentRecord
         responseData.orderNo = targetOrderNo
       } else {
-        incomeRecord = await this.incomeRecordsService.findByOrderNo(orderNo, userId)
+        incomeRecord = await this.incomeRecordsService.findByOrderNo(orderNo, userId, resourceId)
         if (!incomeRecord) {
           responseData.msg = '没有找到订单'
           return responseData
@@ -355,4 +420,40 @@ export class PaymentService {
     const normalizedType = type.toLowerCase().trim()
     return PaymentService.STRING_TO_ORDER_TYPE_MAP[normalizedType] ?? OrderType.SINGLE_TIP
   }
+
+  async userSingleQuery(userId: number, resourceId: string): Promise<boolean> {
+    const incomeRecord = await this.incomeRecordsService.findSingleByResourceId(resourceId, userId)
+    if (!incomeRecord) {
+      return false
+    }
+    return true
+  }
+
+  async userSingleList(userId: number, query: SingleListQueryParams): Promise<SingleListResponse> {
+    const { page = 0, size = 20, resourceId } = query
+
+    const records = await this.incomeRecordsService.findAll({
+      page,
+      size,
+      userId,
+      orderType: OrderType.SINGLE_TIP,
+      status: true,
+      delFlag: false,
+      ...(resourceId && { resourceId }),
+      select: ['id', 'userId', 'resourceId', 'status', 'createdAt']
+    })
+
+    const content = records.content.map(record => ({
+      id: record.id,
+      userId: record.userId,
+      resourceId: record.resourceId,
+      status: record.status,
+      createdAt: record.createdAt.toISOString()
+    }))
+
+    return {
+      content,
+      metadata: records.metadata
+    }
+  }
 }