payment.service.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import { DataSource } from 'typeorm';
  2. import WxPay from 'wechatpay-node-v3';
  3. import { Buffer } from 'buffer';
  4. import { PaymentEntity } from '../entities/payment.entity.js';
  5. import { PaymentStatus } from '../entities/payment.types.js';
  6. import { PaymentCreateResponse } from '../entities/payment.types.js';
  7. /**
  8. * 微信支付服务
  9. * 使用微信支付v3 SDK,支持小程序支付
  10. */
  11. export class PaymentService {
  12. private readonly wxPay: WxPay;
  13. private readonly merchantId: string;
  14. private readonly appId: string;
  15. private readonly v3Key: string;
  16. private readonly notifyUrl: string;
  17. constructor(
  18. private readonly dataSource: DataSource
  19. ) {
  20. // 从环境变量获取支付配置
  21. this.merchantId = process.env.WECHAT_MERCHANT_ID || '';
  22. this.appId = process.env.WX_MINI_APP_ID || '';
  23. this.v3Key = process.env.WECHAT_V3_KEY || '';
  24. this.notifyUrl = process.env.WECHAT_PAY_NOTIFY_URL || '';
  25. const certSerialNo = process.env.WECHAT_MERCHANT_CERT_SERIAL_NO || '';
  26. if (!this.merchantId || !this.appId || !this.v3Key || !certSerialNo) {
  27. throw new Error('微信支付配置不完整,请检查环境变量');
  28. }
  29. // 处理证书字符串,将 \n 转换为实际换行符
  30. const publicKey = (process.env.WECHAT_PUBLIC_KEY || '').replace(/\\n/g, '\n');
  31. const privateKey = (process.env.WECHAT_PRIVATE_KEY || '').replace(/\\n/g, '\n');
  32. // 初始化微信支付SDK
  33. this.wxPay = new WxPay({
  34. appid: this.appId,
  35. mchid: this.merchantId,
  36. publicKey: Buffer.from(publicKey),
  37. privateKey: Buffer.from(privateKey),
  38. key: this.v3Key,
  39. serial_no: certSerialNo
  40. });
  41. }
  42. /**
  43. * 创建微信支付订单
  44. * @param externalOrderId 外部订单ID
  45. * @param userId 用户ID
  46. * @param totalAmount 支付金额(分)
  47. * @param description 支付描述
  48. * @param openid 用户OpenID
  49. */
  50. async createPayment(
  51. externalOrderId: number,
  52. userId: number,
  53. totalAmount: number,
  54. description: string,
  55. openid: string
  56. ): Promise<PaymentCreateResponse> {
  57. // 检查是否已存在相同外部订单ID的支付记录
  58. const paymentRepository = this.dataSource.getRepository(PaymentEntity);
  59. const existingPayment = await paymentRepository.findOne({
  60. where: { externalOrderId }
  61. });
  62. if (existingPayment) {
  63. if (existingPayment.paymentStatus !== PaymentStatus.PENDING) {
  64. throw new Error('该订单已存在支付记录且状态不正确');
  65. }
  66. // 如果存在待支付的记录,可以更新或重新创建,这里选择重新创建
  67. await paymentRepository.remove(existingPayment);
  68. }
  69. if (!openid) {
  70. throw new Error('用户OpenID不能为空');
  71. }
  72. try {
  73. // 创建商户订单号
  74. const outTradeNo = `PAYMENT_${externalOrderId}_${Date.now()}`;
  75. // 使用微信支付SDK创建JSAPI支付
  76. const result = await this.wxPay.transactions_jsapi({
  77. appid: this.appId,
  78. mchid: this.merchantId,
  79. description,
  80. out_trade_no: outTradeNo,
  81. notify_url: this.notifyUrl,
  82. amount: {
  83. total: totalAmount,
  84. },
  85. payer: {
  86. openid
  87. }
  88. });
  89. console.debug('微信支付SDK返回结果:', result);
  90. // 从 package 字段中提取 prepay_id
  91. const prepayId = result.package ? result.package.replace('prepay_id=', '') : undefined;
  92. // 创建支付记录
  93. const payment = new PaymentEntity();
  94. payment.externalOrderId = externalOrderId;
  95. payment.userId = userId;
  96. payment.totalAmount = totalAmount;
  97. payment.description = description;
  98. payment.paymentStatus = PaymentStatus.PROCESSING;
  99. payment.outTradeNo = outTradeNo;
  100. payment.openid = openid;
  101. await paymentRepository.save(payment);
  102. // 直接返回微信支付SDK生成的参数
  103. return {
  104. paymentId: prepayId,
  105. timeStamp: result.timeStamp,
  106. nonceStr: result.nonceStr,
  107. package: result.package,
  108. signType: result.signType,
  109. paySign: result.paySign,
  110. totalAmount: totalAmount // 添加金额字段用于前端验证
  111. };
  112. } catch (error) {
  113. const errorMessage = error instanceof Error ? error.message : '未知错误';
  114. throw new Error(`微信支付创建失败: ${errorMessage}`);
  115. }
  116. }
  117. /**
  118. * 处理支付回调
  119. */
  120. async handlePaymentCallback(
  121. callbackData: any,
  122. headers: any,
  123. rawBody: string // 添加原始请求体参数
  124. ): Promise<void> {
  125. console.debug('收到支付回调请求:', {
  126. headers,
  127. callbackData,
  128. rawBody
  129. });
  130. // 验证回调签名
  131. const isValid = await this.wxPay.verifySign({
  132. timestamp: headers['wechatpay-timestamp'],
  133. nonce: headers['wechatpay-nonce'],
  134. body: rawBody, // 优先使用原始请求体
  135. serial: headers['wechatpay-serial'],
  136. signature: headers['wechatpay-signature']
  137. });
  138. console.debug('回调签名验证结果:', isValid);
  139. if (!isValid) {
  140. throw new Error('回调签名验证失败');
  141. }
  142. // 解密回调数据
  143. const decryptedData = this.wxPay.decipher_gcm(
  144. callbackData.resource.ciphertext,
  145. callbackData.resource.associated_data || '',
  146. callbackData.resource.nonce
  147. );
  148. console.log('解密回调数据', decryptedData)
  149. console.log('解密回调数据类型:', typeof decryptedData)
  150. // 处理解密后的数据,可能是字符串或对象
  151. let parsedData;
  152. if (typeof decryptedData === 'string') {
  153. parsedData = JSON.parse(decryptedData);
  154. } else {
  155. parsedData = decryptedData;
  156. }
  157. const paymentRepository = this.dataSource.getRepository(PaymentEntity);
  158. const outTradeNo = parsedData.out_trade_no;
  159. const payment = await paymentRepository.findOne({
  160. where: { outTradeNo }
  161. });
  162. if (!payment) {
  163. throw new Error('支付记录不存在');
  164. }
  165. // 根据回调结果更新支付状态
  166. if (parsedData.trade_state === 'SUCCESS') {
  167. payment.paymentStatus = PaymentStatus.PAID;
  168. payment.wechatTransactionId = parsedData.transaction_id;
  169. } else if (parsedData.trade_state === 'FAIL') {
  170. payment.paymentStatus = PaymentStatus.FAILED;
  171. } else if (parsedData.trade_state === 'REFUND') {
  172. payment.paymentStatus = PaymentStatus.REFUNDED;
  173. }
  174. await paymentRepository.save(payment);
  175. }
  176. /**
  177. * 查询支付状态
  178. */
  179. async getPaymentStatus(externalOrderId: number): Promise<PaymentStatus> {
  180. const paymentRepository = this.dataSource.getRepository(PaymentEntity);
  181. const payment = await paymentRepository.findOne({
  182. where: { externalOrderId }
  183. });
  184. if (!payment) {
  185. throw new Error('支付记录不存在');
  186. }
  187. return payment.paymentStatus;
  188. }
  189. /**
  190. * 生成随机字符串
  191. */
  192. private generateNonceStr(length: number = 32): string {
  193. const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  194. let result = '';
  195. for (let i = 0; i < length; i++) {
  196. result += chars.charAt(Math.floor(Math.random() * chars.length));
  197. }
  198. return result;
  199. }
  200. /**
  201. * 生成回调签名(用于测试)
  202. */
  203. generateCallbackSignature(
  204. timestamp: string,
  205. nonce: string,
  206. callbackData: any
  207. ): string {
  208. return this.wxPay.getSignature(
  209. 'POST',
  210. nonce,
  211. timestamp,
  212. '/v3/pay/transactions/jsapi',
  213. callbackData
  214. );
  215. }
  216. /**
  217. * 获取微信支付平台证书(用于测试)
  218. */
  219. async getPlatformCertificates(): Promise<any> {
  220. try {
  221. console.debug('开始获取微信支付平台证书...');
  222. const certificates = await this.wxPay.get_certificates(this.v3Key);
  223. console.debug('获取平台证书成功:', certificates);
  224. return certificates;
  225. } catch (error) {
  226. console.debug('获取平台证书失败:', error);
  227. throw error;
  228. }
  229. }
  230. }