Explorar o código

更新收入记录和支付相关逻辑,重命名字段为agentId,调整必填字段验证,新增支付订单创建逻辑,优化收入记录创建和统计方法,同时更新相关DTO和路由以支持新功能。

wuyi hai 3 meses
pai
achega
00cdb43189

+ 13 - 17
src/controllers/income-records.controller.ts

@@ -21,42 +21,38 @@ export class IncomeRecordsController {
   async create(request: FastifyRequest<{ Body: CreateIncomeRecordBody }>, reply: FastifyReply) {
   async create(request: FastifyRequest<{ Body: CreateIncomeRecordBody }>, reply: FastifyReply) {
     try {
     try {
       const {
       const {
-        teamId,
+        agentId,
         userId,
         userId,
         incomeAmount,
         incomeAmount,
-        agentName,
         incomeType,
         incomeType,
         orderType,
         orderType,
-        video,
-        price,
-        tipOrderId,
+        orderPrice,
+        orderNo,
         payChannel,
         payChannel,
         payNo
         payNo
       } = request.body
       } = request.body
 
 
       // 验证必填字段
       // 验证必填字段
       if (
       if (
-        !teamId ||
+        !agentId ||
         !userId ||
         !userId ||
         !incomeAmount ||
         !incomeAmount ||
-        !agentName ||
         !incomeType ||
         !incomeType ||
         !orderType ||
         !orderType ||
-        !video ||
-        !price ||
-        !tipOrderId ||
+        !orderPrice ||
+        !orderNo ||
         !payChannel ||
         !payChannel ||
         !payNo
         !payNo
       ) {
       ) {
         return reply.code(400).send({
         return reply.code(400).send({
           message:
           message:
-            'teamId、userId、incomeAmount、agentName、incomeType、orderType、video、price、tipOrderId、payChannel、payNo 为必填字段'
+            'agentId、userId、incomeAmount、incomeType、orderType、orderPrice、orderNo、payChannel、payNo 为必填字段'
         })
         })
       }
       }
 
 
       // 验证金额为正数
       // 验证金额为正数
-      if (incomeAmount <= 0 || price <= 0) {
-        return reply.code(400).send({ message: 'incomeAmount 和 price 必须大于 0' })
+      if (incomeAmount <= 0 || orderPrice <= 0) {
+        return reply.code(400).send({ message: 'incomeAmount 和 orderPrice 必须大于 0' })
       }
       }
 
 
       const incomeRecord = await this.incomeRecordsService.create(request.body)
       const incomeRecord = await this.incomeRecordsService.create(request.body)
@@ -87,7 +83,7 @@ export class IncomeRecordsController {
       if (user.role === UserRole.TEAM) {
       if (user.role === UserRole.TEAM) {
         const team = await this.teamService.findByUserId(user.id)
         const team = await this.teamService.findByUserId(user.id)
         if (team) {
         if (team) {
-          request.query.teamId = team.id
+          request.query.agentId = team.id
         }
         }
       } else if (user.role === UserRole.PROMOTER) {
       } else if (user.role === UserRole.PROMOTER) {
         request.query.userId = user.id
         request.query.userId = user.id
@@ -166,14 +162,14 @@ export class IncomeRecordsController {
         return reply.code(403).send({ message: '用户未登录' })
         return reply.code(403).send({ message: '用户未登录' })
       }
       }
       let userId: number | undefined
       let userId: number | undefined
-      let teamId: number | undefined
+      let agentId: number | undefined
       if (user.role === UserRole.TEAM) {
       if (user.role === UserRole.TEAM) {
         const team = await this.teamService.findByUserId(user.id)
         const team = await this.teamService.findByUserId(user.id)
-        teamId = team.id
+        agentId = team.id
       } else if (user.role === UserRole.PROMOTER) {
       } else if (user.role === UserRole.PROMOTER) {
         userId = user.id
         userId = user.id
       }
       }
-      const statistics = await this.incomeRecordsService.getStatistics(startDate, endDate, teamId, userId)
+      const statistics = await this.incomeRecordsService.getStatistics(startDate, endDate, agentId, userId)
       return reply.send(statistics)
       return reply.send(statistics)
     } catch (error) {
     } catch (error) {
       return reply.code(500).send({ message: '获取统计数据失败', error })
       return reply.code(500).send({ message: '获取统计数据失败', error })

+ 21 - 5
src/controllers/payment.controller.ts

@@ -1,6 +1,6 @@
 import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
 import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
 import { PaymentService } from '../services/payment.service'
 import { PaymentService } from '../services/payment.service'
-import { CreatePaymentParams, PaymentNotifyParams } from '../dto/payment.dto'
+import { CreatePaymentParams, PaymentNotifyParams, CreatePaymentOrderParams } from '../dto/payment.dto'
 
 
 export class PaymentController {
 export class PaymentController {
   private paymentService: PaymentService
   private paymentService: PaymentService
@@ -9,12 +9,28 @@ export class PaymentController {
     this.paymentService = new PaymentService(app)
     this.paymentService = new PaymentService(app)
   }
   }
 
 
-  async createOrder(request: FastifyRequest<{ Body: CreatePaymentParams }>, reply: FastifyReply) {
+  async createPaymentOrder(request: FastifyRequest<{ Body: CreatePaymentOrderParams }>, reply: FastifyReply) {
     try {
     try {
-      const result = await this.paymentService.createOrder(request.body)
-      return reply.code(200).send(result)
+      const { userId, type } = request.body
+      const ip = request.ip
+
+      if (!userId || !type || !ip) {
+        return reply.code(400).send({
+          message: '缺少字段'
+        })
+      }
+
+      const validTypes = ['single', 'hourly', 'daily', 'weekly', 'monthly', 'quarterly', 'yearly', 'lifetime']
+      if (!validTypes.includes(type)) {
+        return reply.code(400).send({
+          message: '不支持的订单类型'
+        })
+      }
+
+      const result = await this.paymentService.createPaymentOrder({ userId, type, ip })
+      return reply.send(result)
     } catch (error) {
     } catch (error) {
-      return reply.code(500).send('Failed to create payment order')
+      return reply.code(500).send({ message: '创建支付订单失败', error })
     }
     }
   }
   }
 
 

+ 11 - 14
src/dto/income-records.dto.ts

@@ -1,44 +1,41 @@
-import { FastifyRequest } from 'fastify'
 import { IncomeType, OrderType } from '../entities/income-records.entity'
 import { IncomeType, OrderType } from '../entities/income-records.entity'
 import { Pagination } from './common.dto'
 import { Pagination } from './common.dto'
 
 
 export interface CreateIncomeRecordBody {
 export interface CreateIncomeRecordBody {
-  teamId: number
+  agentId: number
   userId: number
   userId: number
   incomeAmount: number
   incomeAmount: number
-  agentName: string
   incomeType: IncomeType
   incomeType: IncomeType
   orderType: OrderType
   orderType: OrderType
-  video: string
-  price: number
-  tipOrderId: string
+  orderPrice: number
+  orderNo: string
   payChannel: string
   payChannel: string
   payNo: string
   payNo: string
-  source?: string
 }
 }
 
 
 export interface UpdateIncomeRecordBody {
 export interface UpdateIncomeRecordBody {
   id: number
   id: number
+  agentId?: number
   incomeAmount?: number
   incomeAmount?: number
-  agentName?: string
   incomeType?: IncomeType
   incomeType?: IncomeType
   orderType?: OrderType
   orderType?: OrderType
-  video?: string
-  price?: number
-  tipOrderId?: string
+  orderPrice?: number
+  orderNo?: string
   payChannel?: string
   payChannel?: string
   payNo?: string
   payNo?: string
-  source?: string
+  status?: boolean
   delFlag?: boolean
   delFlag?: boolean
 }
 }
 
 
 export interface ListIncomeRecordsQuery extends Pagination {
 export interface ListIncomeRecordsQuery extends Pagination {
-  teamId?: number
+  agentId?: number
   userId?: number
   userId?: number
-  agentName?: string
   incomeType?: IncomeType
   incomeType?: IncomeType
   orderType?: OrderType
   orderType?: OrderType
+  orderNo?: string
   payChannel?: string
   payChannel?: string
+  payNo?: string
+  status?: boolean
   startDate?: string
   startDate?: string
   endDate?: string
   endDate?: string
 }
 }

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

@@ -17,6 +17,19 @@ export interface CreatePaymentResponse {
   code_url?: string
   code_url?: string
 }
 }
 
 
+export interface CreatePaymentOrderParams {
+  userId: number
+  type: 'single' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly' | 'lifetime'
+  ip: string
+}
+
+export interface CreatePaymentOrderResponse {
+  code: number
+  msg: string
+  code_url?: string
+  out_trade_no?: string
+}
+
 export interface PaymentNotifyParams {
 export interface PaymentNotifyParams {
   pid: number
   pid: number
   out_trade_no: string
   out_trade_no: string

+ 9 - 12
src/entities/income-records.entity.ts

@@ -8,21 +8,24 @@ export enum IncomeType {
 export enum OrderType {
 export enum OrderType {
   SINGLE_TIP = 'single_tip',
   SINGLE_TIP = 'single_tip',
   HOURLY_MEMBER = 'hourly_member',
   HOURLY_MEMBER = 'hourly_member',
+  DAILY_MEMBER = 'daily_member',
   WEEKLY_MEMBER = 'weekly_member',
   WEEKLY_MEMBER = 'weekly_member',
   MONTHLY_MEMBER = 'monthly_member',
   MONTHLY_MEMBER = 'monthly_member',
+  QUARTERLY_MEMBER = 'quarterly_member',
   YEARLY_MEMBER = 'yearly_member',
   YEARLY_MEMBER = 'yearly_member',
   LIFETIME_MEMBER = 'lifetime_member'
   LIFETIME_MEMBER = 'lifetime_member'
 }
 }
 
 
 @Entity()
 @Entity()
-@Index('idx_team_date_type', ['teamId', 'createdAt', 'incomeType'])
+@Index('idx_agent_date_type', ['agentId', 'createdAt', 'incomeType'])
 @Index('idx_del_flag', ['delFlag'])
 @Index('idx_del_flag', ['delFlag'])
+@Index('idx_status', ['status'])
 export class IncomeRecords {
 export class IncomeRecords {
   @PrimaryGeneratedColumn()
   @PrimaryGeneratedColumn()
   id: number
   id: number
 
 
   @Column({ nullable: true, default: 0 })
   @Column({ nullable: true, default: 0 })
-  teamId: number
+  agentId: number
 
 
   @Column({ nullable: true, default: 0 })
   @Column({ nullable: true, default: 0 })
   userId: number
   userId: number
@@ -34,9 +37,6 @@ export class IncomeRecords {
   })
   })
   incomeAmount: number
   incomeAmount: number
 
 
-  @Column()
-  agentName: string
-
   @Column({
   @Column({
     type: 'enum',
     type: 'enum',
     enum: IncomeType,
     enum: IncomeType,
@@ -51,18 +51,15 @@ export class IncomeRecords {
   })
   })
   orderType: OrderType
   orderType: OrderType
 
 
-  @Column()
-  video: string
-
   @Column({
   @Column({
     type: 'decimal',
     type: 'decimal',
     precision: 10,
     precision: 10,
     scale: 5
     scale: 5
   })
   })
-  price: number
+  orderPrice: number
 
 
   @Column()
   @Column()
-  tipOrderId: string
+  orderNo: string
 
 
   @Column()
   @Column()
   payChannel: string
   payChannel: string
@@ -70,8 +67,8 @@ export class IncomeRecords {
   @Column()
   @Column()
   payNo: string
   payNo: string
 
 
-  @Column({ nullable: true })
-  source: string
+  @Column({ default: false })
+  status: boolean
 
 
   @Column({ default: false })
   @Column({ default: false })
   delFlag: boolean
   delFlag: boolean

+ 2 - 0
src/entities/member.entity.ts

@@ -3,9 +3,11 @@ import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateCol
 export enum VipLevel {
 export enum VipLevel {
   GUEST = 'guest',
   GUEST = 'guest',
   FREE = 'free',
   FREE = 'free',
+  HOURLY = 'hourly',
   DAILY = 'daily',
   DAILY = 'daily',
   WEEKLY = 'weekly',
   WEEKLY = 'weekly',
   MONTHLY = 'monthly',
   MONTHLY = 'monthly',
+  QUARTERLY = 'quarterly',
   YEARLY = 'yearly',
   YEARLY = 'yearly',
   LIFETIME = 'lifetime'
   LIFETIME = 'lifetime'
 }
 }

+ 6 - 12
src/routes/payment.routes.ts

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

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

@@ -13,18 +13,15 @@ export class IncomeRecordsService {
 
 
   async create(data: CreateIncomeRecordBody): Promise<IncomeRecords> {
   async create(data: CreateIncomeRecordBody): Promise<IncomeRecords> {
     const incomeRecord = this.incomeRecordsRepository.create({
     const incomeRecord = this.incomeRecordsRepository.create({
-      teamId: data.teamId,
+      agentId: data.agentId,
       userId: data.userId,
       userId: data.userId,
       incomeAmount: data.incomeAmount,
       incomeAmount: data.incomeAmount,
-      agentName: data.agentName,
       incomeType: data.incomeType,
       incomeType: data.incomeType,
       orderType: data.orderType,
       orderType: data.orderType,
-      video: data.video,
-      price: data.price,
-      tipOrderId: data.tipOrderId,
+      orderPrice: data.orderPrice,
+      orderNo: data.orderNo,
       payChannel: data.payChannel,
       payChannel: data.payChannel,
-      payNo: data.payNo,
-      source: data.source
+      payNo: data.payNo
     })
     })
     return this.incomeRecordsRepository.save(incomeRecord)
     return this.incomeRecordsRepository.save(incomeRecord)
   }
   }
@@ -34,22 +31,31 @@ export class IncomeRecordsService {
   }
   }
 
 
   async findAll(query: ListIncomeRecordsQuery): Promise<PaginationResponse<IncomeRecords>> {
   async findAll(query: ListIncomeRecordsQuery): Promise<PaginationResponse<IncomeRecords>> {
-    const { page, size, teamId, userId, agentName, incomeType, orderType, payChannel, startDate, endDate } = query
+    const {
+      page,
+      size,
+      agentId,
+      userId,
+      incomeType,
+      orderType,
+      orderNo,
+      payChannel,
+      payNo,
+      status,
+      startDate,
+      endDate
+    } = query
 
 
     const where: any = {}
     const where: any = {}
 
 
-    if (teamId) {
-      where.teamId = teamId
+    if (agentId) {
+      where.agentId = agentId
     }
     }
 
 
     if (userId) {
     if (userId) {
       where.userId = userId
       where.userId = userId
     }
     }
 
 
-    if (agentName) {
-      where.agentName = Like(`%${agentName}%`)
-    }
-
     if (incomeType) {
     if (incomeType) {
       where.incomeType = incomeType
       where.incomeType = incomeType
     }
     }
@@ -58,10 +64,22 @@ export class IncomeRecordsService {
       where.orderType = orderType
       where.orderType = orderType
     }
     }
 
 
+    if (orderNo) {
+      where.orderNo = Like(`%${orderNo}%`)
+    }
+
     if (payChannel) {
     if (payChannel) {
       where.payChannel = Like(`%${payChannel}%`)
       where.payChannel = Like(`%${payChannel}%`)
     }
     }
 
 
+    if (payNo) {
+      where.payNo = Like(`%${payNo}%`)
+    }
+
+    if (status !== undefined) {
+      where.status = status
+    }
+
     if (startDate && endDate) {
     if (startDate && endDate) {
       where.createdAt = Between(new Date(startDate), new Date(endDate))
       where.createdAt = Between(new Date(startDate), new Date(endDate))
     }
     }
@@ -83,6 +101,10 @@ export class IncomeRecordsService {
     }
     }
   }
   }
 
 
+  async findByOrderNo(orderNo: string): Promise<IncomeRecords> {
+    return this.incomeRecordsRepository.findOneOrFail({ where: { orderNo } })
+  }
+
   async update(data: UpdateIncomeRecordBody): Promise<IncomeRecords> {
   async update(data: UpdateIncomeRecordBody): Promise<IncomeRecords> {
     const { id, ...updateData } = data
     const { id, ...updateData } = data
     await this.incomeRecordsRepository.update(id, updateData)
     await this.incomeRecordsRepository.update(id, updateData)
@@ -100,12 +122,12 @@ export class IncomeRecordsService {
   async getStatistics(
   async getStatistics(
     startDate?: string,
     startDate?: string,
     endDate?: string,
     endDate?: string,
-    teamId?: number,
+    agentId?: number,
     userId?: number
     userId?: number
   ): Promise<{
   ): Promise<{
     dates: string[]
     dates: string[]
-    teams: Array<{
-      teamId: number
+    agents: Array<{
+      agentId: number
       data: number[]
       data: number[]
       tip: number[]
       tip: number[]
       commission: number[]
       commission: number[]
@@ -133,10 +155,10 @@ export class IncomeRecordsService {
     start.setHours(0, 0, 0, 0)
     start.setHours(0, 0, 0, 0)
     end.setHours(23, 59, 59, 999)
     end.setHours(23, 59, 59, 999)
 
 
-    const teamStats = await this.incomeRecordsRepository
+    const agentStats = await this.incomeRecordsRepository
       .createQueryBuilder('record')
       .createQueryBuilder('record')
       .select([
       .select([
-        'record.teamId as teamId',
+        'record.agentId as agentId',
         "DATE_FORMAT(CONVERT_TZ(record.createdAt, '+00:00', '+08:00'), '%Y-%m-%d') as date",
         "DATE_FORMAT(CONVERT_TZ(record.createdAt, '+00:00', '+08:00'), '%Y-%m-%d') as date",
         'CAST(SUM(CASE WHEN record.incomeType = :tipType THEN CAST(record.incomeAmount AS DECIMAL(10,5)) ELSE 0 END) AS DECIMAL(10,5)) as tipAmount',
         'CAST(SUM(CASE WHEN record.incomeType = :tipType THEN CAST(record.incomeAmount AS DECIMAL(10,5)) ELSE 0 END) AS DECIMAL(10,5)) as tipAmount',
         'CAST(SUM(CASE WHEN record.incomeType = :commissionType THEN CAST(record.incomeAmount AS DECIMAL(10,5)) ELSE 0 END) AS DECIMAL(10,5)) as commissionAmount',
         'CAST(SUM(CASE WHEN record.incomeType = :commissionType THEN CAST(record.incomeAmount AS DECIMAL(10,5)) ELSE 0 END) AS DECIMAL(10,5)) as commissionAmount',
@@ -147,30 +169,30 @@ export class IncomeRecordsService {
       .andWhere('record.delFlag = :delFlag', { delFlag: false })
       .andWhere('record.delFlag = :delFlag', { delFlag: false })
       .setParameter('tipType', IncomeType.TIP)
       .setParameter('tipType', IncomeType.TIP)
       .setParameter('commissionType', IncomeType.COMMISSION)
       .setParameter('commissionType', IncomeType.COMMISSION)
-      .andWhere(teamId ? 'record.teamId = :teamId' : '1=1', { teamId })
+      .andWhere(agentId ? 'record.agentId = :agentId' : '1=1', { agentId })
       .andWhere(userId ? 'record.userId = :userId' : '1=1', { userId })
       .andWhere(userId ? 'record.userId = :userId' : '1=1', { userId })
-      .groupBy('record.teamId')
+      .groupBy('record.agentId')
       .addGroupBy("DATE_FORMAT(CONVERT_TZ(record.createdAt, '+00:00', '+08:00'), '%Y-%m-%d')")
       .addGroupBy("DATE_FORMAT(CONVERT_TZ(record.createdAt, '+00:00', '+08:00'), '%Y-%m-%d')")
-      .orderBy('record.teamId', 'ASC')
+      .orderBy('record.agentId', 'ASC')
       .addOrderBy('date', 'ASC')
       .addOrderBy('date', 'ASC')
       .getRawMany()
       .getRawMany()
 
 
-    // 按团队分组处理数据
-    const teamMap = new Map<
+    // 按代理商分组处理数据
+    const agentMap = new Map<
       number,
       number,
       {
       {
-        teamId: number
+        agentId: number
         data: number[]
         data: number[]
         tip: number[]
         tip: number[]
         commission: number[]
         commission: number[]
       }
       }
     >()
     >()
 
 
-    // 初始化每个团队的数据数组
-    teamStats.forEach(stat => {
-      if (!teamMap.has(stat.teamId)) {
-        teamMap.set(stat.teamId, {
-          teamId: stat.teamId,
+    // 初始化每个代理商的数据数组
+    agentStats.forEach(stat => {
+      if (!agentMap.has(stat.agentId)) {
+        agentMap.set(stat.agentId, {
+          agentId: stat.agentId,
           data: dates.map(() => 0),
           data: dates.map(() => 0),
           tip: dates.map(() => 0),
           tip: dates.map(() => 0),
           commission: dates.map(() => 0)
           commission: dates.map(() => 0)
@@ -179,31 +201,31 @@ export class IncomeRecordsService {
 
 
       const dateIndex = dates.indexOf(stat.date)
       const dateIndex = dates.indexOf(stat.date)
       if (dateIndex !== -1) {
       if (dateIndex !== -1) {
-        const team = teamMap.get(stat.teamId)!
-        team.data[dateIndex] = Number(stat.totalAmount)
-        team.tip[dateIndex] = Number(stat.tipAmount)
-        team.commission[dateIndex] = Number(stat.commissionAmount)
+        const agent = agentMap.get(stat.agentId)!
+        agent.data[dateIndex] = Number(stat.totalAmount)
+        agent.tip[dateIndex] = Number(stat.tipAmount)
+        agent.commission[dateIndex] = Number(stat.commissionAmount)
       }
       }
     })
     })
 
 
-    const teams = Array.from(teamMap.values())
+    const agents = Array.from(agentMap.values())
 
 
     // 计算每天的总收入
     // 计算每天的总收入
     const total = dates.map((_, index) => {
     const total = dates.map((_, index) => {
-      return teams.reduce((sum, team) => sum + team.data[index], 0)
+      return agents.reduce((sum, agent) => sum + agent.data[index], 0)
     })
     })
 
 
     const totalTip = dates.map((_, index) => {
     const totalTip = dates.map((_, index) => {
-      return teams.reduce((sum, team) => sum + team.tip[index], 0)
+      return agents.reduce((sum, agent) => sum + agent.tip[index], 0)
     })
     })
 
 
     const totalCommission = dates.map((_, index) => {
     const totalCommission = dates.map((_, index) => {
-      return teams.reduce((sum, team) => sum + team.commission[index], 0)
+      return agents.reduce((sum, agent) => sum + agent.commission[index], 0)
     })
     })
 
 
     return {
     return {
       dates,
       dates,
-      teams,
+      agents,
       total,
       total,
       totalTip,
       totalTip,
       totalCommission
       totalCommission

+ 18 - 1
src/services/member.service.ts

@@ -11,6 +11,18 @@ export class MemberService {
   private memberRepository: Repository<Member>
   private memberRepository: Repository<Member>
   private dataSource: DataSource
   private dataSource: DataSource
 
 
+  private vipDurations: Record<VipLevel, number | null> = {
+    [VipLevel.GUEST]: null,
+    [VipLevel.FREE]: null,
+    [VipLevel.HOURLY]: 60 * 60 * 1000,
+    [VipLevel.DAILY]: 24 * 60 * 60 * 1000,
+    [VipLevel.WEEKLY]: 7 * 24 * 60 * 60 * 1000,
+    [VipLevel.MONTHLY]: 30 * 24 * 60 * 60 * 1000,
+    [VipLevel.QUARTERLY]: 90 * 24 * 60 * 60 * 1000,
+    [VipLevel.YEARLY]: 365 * 24 * 60 * 60 * 1000,
+    [VipLevel.LIFETIME]: new Date('2099-12-31').getTime() - Date.now()
+  }
+
   constructor(app: FastifyInstance) {
   constructor(app: FastifyInstance) {
     this.memberRepository = app.dataSource.getRepository(Member)
     this.memberRepository = app.dataSource.getRepository(Member)
     this.dataSource = app.dataSource
     this.dataSource = app.dataSource
@@ -47,7 +59,7 @@ export class MemberService {
         ip: ip || 'unknown',
         ip: ip || 'unknown',
         lastLoginAt: new Date()
         lastLoginAt: new Date()
       })
       })
-      const savedMember = await manager.save(member)
+      await manager.save(member)
 
 
       return savedUser
       return savedUser
     })
     })
@@ -224,9 +236,14 @@ export class MemberService {
 
 
   async updateVipLevel(id: number, vipLevel: VipLevel, vipExpireTime?: Date): Promise<Member> {
   async updateVipLevel(id: number, vipLevel: VipLevel, vipExpireTime?: Date): Promise<Member> {
     const updateData: Partial<Member> = { vipLevel }
     const updateData: Partial<Member> = { vipLevel }
+
     if (vipExpireTime) {
     if (vipExpireTime) {
       updateData.vipExpireTime = vipExpireTime
       updateData.vipExpireTime = vipExpireTime
+    } else {
+      const duration = this.vipDurations[vipLevel]
+      updateData.vipExpireTime = duration ? new Date(Date.now() + duration) : undefined
     }
     }
+
     return this.update(id, updateData)
     return this.update(id, updateData)
   }
   }
 
 

+ 161 - 2
src/services/payment.service.ts

@@ -4,22 +4,40 @@ import {
   CreatePaymentResponse,
   CreatePaymentResponse,
   PaymentNotifyParams,
   PaymentNotifyParams,
   PaymentQueryParams,
   PaymentQueryParams,
-  PaymentQueryResponse
+  PaymentQueryResponse,
+  CreatePaymentOrderParams,
+  CreatePaymentOrderResponse
 } from '../dto/payment.dto'
 } from '../dto/payment.dto'
 import axios from 'axios'
 import axios from 'axios'
 import crypto from 'crypto'
 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 { MemberService } from './member.service'
+import { UserService } from './user.service'
+import { TeamService } from './team.service'
+import { VipLevel } from '../entities/member.entity'
 
 
 export class PaymentService {
 export class PaymentService {
   private app: FastifyInstance
   private app: FastifyInstance
   private url: string
   private url: string
   private key: string
   private key: string
   private pid: string
   private pid: string
+  private incomeRecordsService: IncomeRecordsService
+  private memberService: MemberService
+  private userService: UserService
+  private teamService: TeamService
 
 
   constructor(app: FastifyInstance) {
   constructor(app: FastifyInstance) {
     this.app = app
     this.app = app
     this.url = app.config.PAYMENT_URL
     this.url = app.config.PAYMENT_URL
     this.key = app.config.PAYMENT_KEY
     this.key = app.config.PAYMENT_KEY
     this.pid = app.config.PAYMENT_PID
     this.pid = app.config.PAYMENT_PID
+    this.incomeRecordsService = new IncomeRecordsService(app)
+    this.memberService = new MemberService(app)
+    this.userService = new UserService(app)
+    this.teamService = new TeamService(app)
   }
   }
 
 
   private generateSign(params: Record<string, any>, key: string): string {
   private generateSign(params: Record<string, any>, key: string): string {
@@ -77,8 +95,29 @@ export class PaymentService {
 
 
       if (params.trade_status === 'TRADE_SUCCESS') {
       if (params.trade_status === 'TRADE_SUCCESS') {
         this.app.log.info('Payment success, out_trade_no: ' + params.out_trade_no)
         this.app.log.info('Payment success, out_trade_no: ' + params.out_trade_no)
+
+        try {
+          const incomeRecord = await this.incomeRecordsService.findByOrderNo(params.out_trade_no)
+          if (incomeRecord) {
+            // 更新收入记录状态
+            await this.incomeRecordsService.update({
+              id: incomeRecord.id,
+              status: true
+            })
+            this.app.log.info('Updated income record status success', incomeRecord.id)
+
+            // 更新会员等级
+            await this.memberService.updateVipLevel(
+              incomeRecord.userId,
+              this.getVipLevelByOrderType(incomeRecord.orderType)
+            )
+            this.app.log.info('Updated member level success', incomeRecord.userId)
+          }
+        } catch (error) {
+          this.app.log.error('Failed to process payment success:', error)
+        }
       } else {
       } else {
-        this.app.log.warn('Payment failed, out_trade_no: ' + params.out_trade_no)
+        this.app.log.warn('Payment failed, out_trade_no: ' + params.out_trade_no + ', trade_no: ' + params.trade_no)
       }
       }
     } catch (error) {
     } catch (error) {
       this.app.log.error('Failed to handle payment notification:', error)
       this.app.log.error('Failed to handle payment notification:', error)
@@ -86,6 +125,96 @@ export class PaymentService {
     }
     }
   }
   }
 
 
+  async createPaymentOrder(params: CreatePaymentOrderParams): Promise<CreatePaymentOrderResponse> {
+    try {
+      const user = await this.userService.findById(params.userId)
+      if (!user) {
+        throw new Error('用户不存在')
+      }
+      const vipPrices: Record<string, string> = {
+        single: '0.10',
+        hourly: '1.00',
+        daily: '5.00',
+        weekly: '20.00',
+        monthly: '50.00',
+        quarterly: '120.00',
+        yearly: '400.00',
+        lifetime: '1000.00'
+      }
+
+      const price = vipPrices[params.type]
+      if (!price) {
+        throw new Error('不支持的订单类型')
+      }
+
+      const out_trade_no = `${Date.now()}${params.userId}${randomInt(0, 1e4).toString().padStart(4, '0')}`
+
+      const paymentParams: CreatePaymentParams = {
+        pid: Number(this.pid),
+        type: 'alipay',
+        out_trade_no,
+        notify_url: `http://${this.app.config.HOST}:${this.app.config.PORT}/payment/notify`,
+        return_url: `https://www.baidu.com/`,
+        name: `${params.type} 订单`,
+        money: price,
+        client_ip: params.ip
+      }
+
+      const result = await this.createOrder(paymentParams)
+      console.log(result)
+
+      // 创建收入记录
+      try {
+        // 计算佣金
+        let agentId = user.parentId
+        let incomeAmount = new Decimal(price)
+
+        if (user.parentId && user.parentId > 0) {
+          try {
+            const team = await this.teamService.findByUserId(user.parentId)
+            if (team && team.commissionRate && team.commissionRate > 0) {
+              const commissionRate = new Decimal(team.commissionRate)
+              const hundred = new Decimal(100)
+              incomeAmount = new Decimal(price).mul(commissionRate).div(hundred)
+            }
+          } catch (teamError) {
+            this.app.log.warn('Failed to find team for user:', user.parentId, teamError)
+            agentId = 0
+            incomeAmount = new Decimal(0)
+          }
+        } else {
+          agentId = 0
+          incomeAmount = new Decimal(0)
+        }
+
+        await this.incomeRecordsService.create({
+          agentId,
+          userId: user.id,
+          incomeAmount: incomeAmount.toNumber(),
+          incomeType: IncomeType.COMMISSION,
+          orderType: this.getOrderTypeByType(params.type),
+          orderPrice: new Decimal(price).toNumber(),
+          orderNo: out_trade_no,
+          payChannel: 'alipay',
+          payNo: result.trade_no || ''
+        })
+      } catch (incomeError) {
+        this.app.log.error('Failed to create income record:', incomeError)
+        throw new Error('创建记录失败')
+      }
+
+      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 purchase member:', error)
+      throw error
+    }
+  }
+
   async queryOrder(params: { trade_no?: string; out_trade_no?: string }): Promise<PaymentQueryResponse> {
   async queryOrder(params: { trade_no?: string; out_trade_no?: string }): Promise<PaymentQueryResponse> {
     try {
     try {
       if (!params.trade_no && !params.out_trade_no) {
       if (!params.trade_no && !params.out_trade_no) {
@@ -123,4 +252,34 @@ export class PaymentService {
       throw error
       throw error
     }
     }
   }
   }
+
+  private getVipLevelByOrderType(orderType: OrderType): VipLevel {
+    const orderTypeMap: Record<OrderType, VipLevel> = {
+      [OrderType.SINGLE_TIP]: VipLevel.FREE,
+      [OrderType.HOURLY_MEMBER]: VipLevel.HOURLY,
+      [OrderType.DAILY_MEMBER]: VipLevel.DAILY,
+      [OrderType.WEEKLY_MEMBER]: VipLevel.WEEKLY,
+      [OrderType.MONTHLY_MEMBER]: VipLevel.MONTHLY,
+      [OrderType.QUARTERLY_MEMBER]: VipLevel.QUARTERLY,
+      [OrderType.YEARLY_MEMBER]: VipLevel.YEARLY,
+      [OrderType.LIFETIME_MEMBER]: VipLevel.LIFETIME
+    }
+
+    return orderTypeMap[orderType] || VipLevel.FREE
+  }
+
+  private getOrderTypeByType(type: string): OrderType {
+    const orderTypeMap: Record<string, OrderType> = {
+      single: OrderType.SINGLE_TIP,
+      hourly: OrderType.HOURLY_MEMBER,
+      daily: OrderType.DAILY_MEMBER,
+      weekly: OrderType.WEEKLY_MEMBER,
+      monthly: OrderType.MONTHLY_MEMBER,
+      quarterly: OrderType.QUARTERLY_MEMBER,
+      yearly: OrderType.YEARLY_MEMBER,
+      lifetime: OrderType.LIFETIME_MEMBER
+    }
+
+    return orderTypeMap[type] || OrderType.SINGLE_TIP
+  }
 }
 }

+ 2 - 2
src/services/team.service.ts

@@ -160,7 +160,7 @@ export class TeamService {
     })
     })
   }
   }
 
 
-  async findByUserId(userId: number): Promise<{ id: number; name: string; userId: number }> {
-    return this.teamRepository.findOneOrFail({ select: ['id', 'name', 'userId'], where: { userId } })
+  async findByUserId(userId: number): Promise<Team> {
+    return this.teamRepository.findOneOrFail({ where: { userId } })
   }
   }
 }
 }