payment.service.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. import { FastifyInstance } from 'fastify'
  2. import {
  3. CreatePaymentParams,
  4. CreatePaymentResponse,
  5. PaymentNotifyParams,
  6. PaymentQueryParams,
  7. PaymentQueryResponse,
  8. CreatePaymentOrderParams,
  9. CreatePaymentOrderResponse,
  10. UserQueryOrderResponse
  11. } from '../dto/payment.dto'
  12. import axios from 'axios'
  13. import crypto from 'crypto'
  14. import { randomInt } from 'crypto'
  15. import Decimal from 'decimal.js'
  16. import { IncomeRecordsService } from './income-records.service'
  17. import { IncomeType, OrderType } from '../entities/income-records.entity'
  18. import { MemberService } from './member.service'
  19. import { UserService } from './user.service'
  20. import { TeamService } from './team.service'
  21. import { VipLevel } from '../entities/member.entity'
  22. export class PaymentService {
  23. private app: FastifyInstance
  24. private url: string
  25. private key: string
  26. private pid: string
  27. private incomeRecordsService: IncomeRecordsService
  28. private memberService: MemberService
  29. private userService: UserService
  30. private teamService: TeamService
  31. private static readonly ORDER_TYPE_TO_VIP_LEVEL_MAP: Record<OrderType, VipLevel> = {
  32. [OrderType.SINGLE_TIP]: VipLevel.FREE,
  33. [OrderType.HOURLY]: VipLevel.HOURLY,
  34. [OrderType.DAILY]: VipLevel.DAILY,
  35. [OrderType.WEEKLY]: VipLevel.WEEKLY,
  36. [OrderType.MONTHLY]: VipLevel.MONTHLY,
  37. [OrderType.QUARTERLY]: VipLevel.QUARTERLY,
  38. [OrderType.YEARLY]: VipLevel.YEARLY,
  39. [OrderType.LIFETIME]: VipLevel.LIFETIME
  40. } as const
  41. private static readonly STRING_TO_ORDER_TYPE_MAP: Record<string, OrderType> = {
  42. single: OrderType.SINGLE_TIP,
  43. hourly: OrderType.HOURLY,
  44. daily: OrderType.DAILY,
  45. weekly: OrderType.WEEKLY,
  46. monthly: OrderType.MONTHLY,
  47. quarterly: OrderType.QUARTERLY,
  48. yearly: OrderType.YEARLY,
  49. lifetime: OrderType.LIFETIME
  50. } as const
  51. constructor(app: FastifyInstance) {
  52. this.app = app
  53. this.url = app.config.PAYMENT_URL
  54. this.key = app.config.PAYMENT_KEY
  55. this.pid = app.config.PAYMENT_PID
  56. this.incomeRecordsService = new IncomeRecordsService(app)
  57. this.memberService = new MemberService(app)
  58. this.userService = new UserService(app)
  59. this.teamService = new TeamService(app)
  60. }
  61. private generateSign(params: Record<string, any>, key: string): string {
  62. const filtered = Object.keys(params)
  63. .filter(k => params[k] !== undefined && params[k] !== '' && k !== 'sign' && k !== 'sign_type')
  64. .sort()
  65. .map(k => `${k}=${params[k]}`)
  66. .join('&')
  67. const strToSign = filtered + key
  68. return crypto.createHash('md5').update(strToSign, 'utf8').digest('hex')
  69. }
  70. private verifyNotifySign(params: PaymentNotifyParams): boolean {
  71. const { sign: receivedSign, ...rest } = params
  72. const calculatedSign = this.generateSign(rest, this.key)
  73. return calculatedSign === receivedSign
  74. }
  75. async createOrder(params: CreatePaymentParams): Promise<CreatePaymentResponse> {
  76. try {
  77. const sign = this.generateSign(params, this.key)
  78. const requestData = {
  79. ...params,
  80. sign,
  81. sign_type: 'MD5'
  82. }
  83. const { data } = await axios.post<CreatePaymentResponse>(this.url + '/qrcode', requestData, {
  84. headers: { 'Content-Type': 'application/json' }
  85. })
  86. // {
  87. // "code": 1,
  88. // "msg": "创建订单成功。",
  89. // "trade_no": "10000202509181600155",
  90. // "out_trade_no": "202509181234567",
  91. // "code_url": "https://dt-2025091816.front.mijieqi.cn/Pay/5NlZddZO/CheckOrder"
  92. // }
  93. return data
  94. } catch (error) {
  95. this.app.log.error('Failed to create payment order:', error)
  96. throw error
  97. }
  98. }
  99. async handlePaymentNotify(params: PaymentNotifyParams): Promise<void> {
  100. if (params.trade_status === 'TRADE_SUCCESS') {
  101. this.app.log.info('Payment success, start processing')
  102. const incomeRecord = await this.incomeRecordsService.findByOrderNo(params.out_trade_no)
  103. if (!incomeRecord) {
  104. this.app.log.error('Failed to find corresponding income record')
  105. throw new Error('Failed to find corresponding income record')
  106. }
  107. await this.incomeRecordsService.update({
  108. id: incomeRecord.id,
  109. status: true
  110. })
  111. this.app.log.info('Income record status updated successfully')
  112. // 更新会员等级
  113. const vipLevel = this.getVipLevelByOrderType(incomeRecord.orderType)
  114. const member = await this.memberService.findByUserId(incomeRecord.userId)
  115. if (!member) {
  116. this.app.log.error('Failed to find corresponding member record')
  117. throw new Error('Failed to find corresponding member record')
  118. }
  119. await this.memberService.updateVipLevel(member.id, vipLevel)
  120. this.app.log.info('Member level updated successfully')
  121. this.app.log.info('Payment processing completed')
  122. } else {
  123. this.app.log.warn('Payment failed', {
  124. out_trade_no: params.out_trade_no,
  125. trade_status: params.trade_status
  126. })
  127. throw new Error('Payment failed')
  128. }
  129. }
  130. async createPaymentOrder(params: CreatePaymentOrderParams): Promise<CreatePaymentOrderResponse> {
  131. try {
  132. const user = await this.userService.findById(params.userId)
  133. if (!user) {
  134. throw new Error('用户不存在')
  135. }
  136. const vipPrices: Record<string, string> = {
  137. single: '0.10',
  138. hourly: '1.00',
  139. daily: '5.00',
  140. weekly: '20.00',
  141. monthly: '50.00',
  142. quarterly: '120.00',
  143. yearly: '400.00',
  144. lifetime: '1000.00'
  145. }
  146. const price = vipPrices[params.type]
  147. if (!price) {
  148. throw new Error('不支持的订单类型')
  149. }
  150. const out_trade_no = `${Date.now()}${params.userId}${randomInt(0, 1e4).toString().padStart(4, '0')}`
  151. const paymentParams: CreatePaymentParams = {
  152. pid: Number(this.pid),
  153. type: 'alipay',
  154. out_trade_no,
  155. notify_url: `https://9g15.vip/api/payment/notify`,
  156. return_url: `https://yz1df.cc/account?pay=true`,
  157. name: `${params.type} 订单`,
  158. money: price,
  159. client_ip: params.ip
  160. }
  161. console.log('paymentParams:', paymentParams)
  162. const result = await this.createOrder(paymentParams)
  163. console.log('result:', result)
  164. // 创建收入记录
  165. try {
  166. // 计算佣金
  167. let agentId = user.parentId
  168. let incomeAmount = new Decimal(price)
  169. if (user.parentId && user.parentId > 0) {
  170. try {
  171. const team = await this.teamService.findByUserId(user.parentId)
  172. if (team && team.commissionRate && team.commissionRate > 0) {
  173. const commissionRate = new Decimal(team.commissionRate)
  174. const hundred = new Decimal(100)
  175. incomeAmount = new Decimal(price).mul(commissionRate).div(hundred)
  176. }
  177. } catch (teamError) {
  178. this.app.log.warn('Failed to find team for user:', user.parentId, teamError)
  179. agentId = 0
  180. incomeAmount = new Decimal(0)
  181. }
  182. } else {
  183. agentId = 0
  184. incomeAmount = new Decimal(0)
  185. }
  186. await this.incomeRecordsService.create({
  187. agentId,
  188. userId: user.id,
  189. incomeAmount: incomeAmount.toNumber(),
  190. incomeType: IncomeType.COMMISSION,
  191. orderType: this.getOrderTypeByType(params.type),
  192. orderPrice: new Decimal(price).toNumber(),
  193. orderNo: out_trade_no,
  194. payChannel: 'alipay',
  195. payNo: result.trade_no || ''
  196. })
  197. } catch (incomeError) {
  198. this.app.log.error('Failed to create income record:', incomeError)
  199. throw new Error('Failed to create income record')
  200. }
  201. return {
  202. code: result.code,
  203. msg: result.msg,
  204. code_url: result.code_url,
  205. out_trade_no: result.out_trade_no
  206. }
  207. } catch (error) {
  208. this.app.log.error('Failed to purchase member:', error)
  209. throw error
  210. }
  211. }
  212. async queryOrder(params: { trade_no?: string; out_trade_no?: string }): Promise<PaymentQueryResponse> {
  213. try {
  214. if (!params.trade_no && !params.out_trade_no) {
  215. throw new Error('trade_no and out_trade_no are required')
  216. }
  217. const queryParams: PaymentQueryParams = {
  218. act: 'order',
  219. pid: this.pid,
  220. key: this.key,
  221. trade_no: params.trade_no,
  222. out_trade_no: params.out_trade_no
  223. }
  224. const { data } = await axios.get<PaymentQueryResponse>(this.url + '/api.php', {
  225. params: queryParams
  226. })
  227. // {
  228. // "code": 1,
  229. // "msg": "查询订单号成功!",
  230. // "pid": 1000,
  231. // "trade_no": "10000202509181600155",
  232. // "out_trade_no": "202509181234567",
  233. // "name": "测试商品",
  234. // "addtime": "2025-09-18 16:48:59",
  235. // "endtime": null,
  236. // "status": 0,
  237. // "money": "1.00",
  238. // "type": "alipay"
  239. // }
  240. return data
  241. } catch (error) {
  242. this.app.log.error('Failed to query payment order:', error)
  243. throw error
  244. }
  245. }
  246. async userQueryOrder(userId: number, orderNo?: string): Promise<UserQueryOrderResponse> {
  247. const responseData: UserQueryOrderResponse = {
  248. orderNo: orderNo || '',
  249. msg: '',
  250. status: 0
  251. }
  252. try {
  253. let targetOrderNo = orderNo
  254. let incomeRecord
  255. if (!orderNo) {
  256. // 查询用户最近12小时内的订单
  257. const twelveHoursAgo = new Date(Date.now() - 12 * 60 * 60 * 1000)
  258. const recentRecord = await this.incomeRecordsService.findRecentByUserId(userId, twelveHoursAgo)
  259. if (!recentRecord) {
  260. responseData.msg = '没有找到最近的订单'
  261. return responseData
  262. }
  263. targetOrderNo = recentRecord.orderNo
  264. incomeRecord = recentRecord
  265. responseData.orderNo = targetOrderNo
  266. } else {
  267. incomeRecord = await this.incomeRecordsService.findByOrderNo(orderNo, userId)
  268. if (!incomeRecord) {
  269. responseData.msg = '没有找到订单'
  270. return responseData
  271. }
  272. }
  273. if (!targetOrderNo) {
  274. responseData.msg = '订单号无效'
  275. return responseData
  276. }
  277. const result = await this.queryOrder({ out_trade_no: targetOrderNo })
  278. if (result.code !== 1) {
  279. responseData.msg = '没有找到支付订单'
  280. return responseData
  281. }
  282. if (result.status !== 1) {
  283. responseData.msg = '订单未支付'
  284. return responseData
  285. } else {
  286. if (incomeRecord.status === false) {
  287. await this.incomeRecordsService.update({
  288. id: incomeRecord.id,
  289. status: true
  290. })
  291. responseData.msg = '订单支付成功'
  292. responseData.status = 1
  293. } else {
  294. responseData.msg = '订单已支付'
  295. responseData.status = 1
  296. }
  297. }
  298. } catch (error) {
  299. this.app.log.error('Failed to query user order:', error)
  300. responseData.msg = '查询订单失败'
  301. }
  302. return responseData
  303. }
  304. private getVipLevelByOrderType(orderType: OrderType): VipLevel {
  305. return PaymentService.ORDER_TYPE_TO_VIP_LEVEL_MAP[orderType] ?? VipLevel.FREE
  306. }
  307. private getOrderTypeByType(type: string): OrderType {
  308. if (!type || typeof type !== 'string') {
  309. this.app.log.warn(`Invalid type parameter: ${type}, using default SINGLE_TIP`)
  310. return OrderType.SINGLE_TIP
  311. }
  312. const normalizedType = type.toLowerCase().trim()
  313. return PaymentService.STRING_TO_ORDER_TYPE_MAP[normalizedType] ?? OrderType.SINGLE_TIP
  314. }
  315. }