|
|
@@ -4,22 +4,40 @@ import {
|
|
|
CreatePaymentResponse,
|
|
|
PaymentNotifyParams,
|
|
|
PaymentQueryParams,
|
|
|
- PaymentQueryResponse
|
|
|
+ PaymentQueryResponse,
|
|
|
+ CreatePaymentOrderParams,
|
|
|
+ CreatePaymentOrderResponse
|
|
|
} 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 { MemberService } from './member.service'
|
|
|
+import { UserService } from './user.service'
|
|
|
+import { TeamService } from './team.service'
|
|
|
+import { VipLevel } from '../entities/member.entity'
|
|
|
|
|
|
export class PaymentService {
|
|
|
private app: FastifyInstance
|
|
|
private url: string
|
|
|
private key: string
|
|
|
private pid: string
|
|
|
+ private incomeRecordsService: IncomeRecordsService
|
|
|
+ private memberService: MemberService
|
|
|
+ private userService: UserService
|
|
|
+ private teamService: TeamService
|
|
|
|
|
|
constructor(app: FastifyInstance) {
|
|
|
this.app = app
|
|
|
this.url = app.config.PAYMENT_URL
|
|
|
this.key = app.config.PAYMENT_KEY
|
|
|
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 {
|
|
|
@@ -77,8 +95,29 @@ export class PaymentService {
|
|
|
|
|
|
if (params.trade_status === 'TRADE_SUCCESS') {
|
|
|
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 {
|
|
|
- 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) {
|
|
|
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> {
|
|
|
try {
|
|
|
if (!params.trade_no && !params.out_trade_no) {
|
|
|
@@ -123,4 +252,34 @@ export class PaymentService {
|
|
|
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
|
|
|
+ }
|
|
|
}
|