payment.mt.service.ts 8.1 KB

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