| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- import { FastifyInstance } from 'fastify'
- import {
- CreatePaymentParams,
- CreatePaymentResponse,
- PaymentNotifyParams,
- PaymentQueryParams,
- PaymentQueryResponse,
- CreatePaymentOrderParams,
- CreatePaymentOrderResponse,
- UserQueryOrderResponse
- } 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
- private static readonly ORDER_TYPE_TO_VIP_LEVEL_MAP: Record<OrderType, VipLevel> = {
- [OrderType.SINGLE_TIP]: VipLevel.FREE,
- [OrderType.HOURLY]: VipLevel.HOURLY,
- [OrderType.DAILY]: VipLevel.DAILY,
- [OrderType.WEEKLY]: VipLevel.WEEKLY,
- [OrderType.MONTHLY]: VipLevel.MONTHLY,
- [OrderType.QUARTERLY]: VipLevel.QUARTERLY,
- [OrderType.YEARLY]: VipLevel.YEARLY,
- [OrderType.LIFETIME]: VipLevel.LIFETIME
- } as const
- private static readonly STRING_TO_ORDER_TYPE_MAP: Record<string, OrderType> = {
- single: OrderType.SINGLE_TIP,
- hourly: OrderType.HOURLY,
- daily: OrderType.DAILY,
- weekly: OrderType.WEEKLY,
- monthly: OrderType.MONTHLY,
- quarterly: OrderType.QUARTERLY,
- yearly: OrderType.YEARLY,
- lifetime: OrderType.LIFETIME
- } as const
- 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 {
- 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> {
- if (params.trade_status === 'TRADE_SUCCESS') {
- this.app.log.info('Payment success, start processing')
- const incomeRecord = await this.incomeRecordsService.findByOrderNo(params.out_trade_no)
- if (!incomeRecord) {
- this.app.log.error('Failed to find corresponding income record')
- throw new Error('Failed to find corresponding income record')
- }
- await this.incomeRecordsService.update({
- id: incomeRecord.id,
- status: true
- })
- this.app.log.info('Income record status updated successfully')
- // 更新会员等级
- const vipLevel = this.getVipLevelByOrderType(incomeRecord.orderType)
- const member = await this.memberService.findByUserId(incomeRecord.userId)
- if (!member) {
- this.app.log.error('Failed to find corresponding member record')
- throw new Error('Failed to find corresponding member record')
- }
- await this.memberService.updateVipLevel(member.id, vipLevel)
- this.app.log.info('Member level updated successfully')
- this.app.log.info('Payment processing completed')
- } else {
- this.app.log.warn('Payment failed', {
- out_trade_no: params.out_trade_no,
- trade_status: params.trade_status
- })
- throw new Error('Payment failed')
- }
- }
- 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: `https://9g15.vip/api/payment/notify`,
- return_url: `https://yz1df.cc/account?pay=true`,
- name: `${params.type} 订单`,
- money: price,
- client_ip: params.ip
- }
- console.log('paymentParams:', paymentParams)
- const result = await this.createOrder(paymentParams)
- console.log('result:', 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('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 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) {
- 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
- }
- }
- async userQueryOrder(userId: number, orderNo?: string): Promise<UserQueryOrderResponse> {
- const responseData: UserQueryOrderResponse = {
- orderNo: orderNo || '',
- msg: '',
- status: 0
- }
- try {
- let targetOrderNo = orderNo
- let incomeRecord
- if (!orderNo) {
- // 查询用户最近12小时内的订单
- const twelveHoursAgo = new Date(Date.now() - 12 * 60 * 60 * 1000)
- const recentRecord = await this.incomeRecordsService.findRecentByUserId(userId, twelveHoursAgo)
- if (!recentRecord) {
- responseData.msg = '没有找到最近的订单'
- return responseData
- }
- targetOrderNo = recentRecord.orderNo
- incomeRecord = recentRecord
- responseData.orderNo = targetOrderNo
- } else {
- incomeRecord = await this.incomeRecordsService.findByOrderNo(orderNo, userId)
- if (!incomeRecord) {
- responseData.msg = '没有找到订单'
- return responseData
- }
- }
- if (!targetOrderNo) {
- responseData.msg = '订单号无效'
- return responseData
- }
- const result = await this.queryOrder({ out_trade_no: targetOrderNo })
- if (result.code !== 1) {
- responseData.msg = '没有找到支付订单'
- return responseData
- }
- if (result.status !== 1) {
- responseData.msg = '订单未支付'
- return responseData
- } else {
- if (incomeRecord.status === false) {
- await this.incomeRecordsService.update({
- id: incomeRecord.id,
- status: true
- })
- responseData.msg = '订单支付成功'
- responseData.status = 1
- } else {
- responseData.msg = '订单已支付'
- responseData.status = 1
- }
- }
- } catch (error) {
- this.app.log.error('Failed to query user order:', error)
- responseData.msg = '查询订单失败'
- }
- return responseData
- }
- private getVipLevelByOrderType(orderType: OrderType): VipLevel {
- return PaymentService.ORDER_TYPE_TO_VIP_LEVEL_MAP[orderType] ?? VipLevel.FREE
- }
- private getOrderTypeByType(type: string): OrderType {
- if (!type || typeof type !== 'string') {
- this.app.log.warn(`Invalid type parameter: ${type}, using default SINGLE_TIP`)
- return OrderType.SINGLE_TIP
- }
- const normalizedType = type.toLowerCase().trim()
- return PaymentService.STRING_TO_ORDER_TYPE_MAP[normalizedType] ?? OrderType.SINGLE_TIP
- }
- }
|