payment.mt.service.ts 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780
  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, RefundRequest, RefundResponse } from '../entities/payment.types.js';
  6. import { PaymentCreateResponse } from '../entities/payment.types.js';
  7. import { GenericCrudService } from '@d8d/shared-crud';
  8. import { SystemConfigServiceMt } from '@d8d/core-module-mt/system-config-module-mt';
  9. import { OrderMt } from '@d8d/orders-module-mt';
  10. import { PayStatus, PayType } from '@d8d/orders-module-mt';
  11. /**
  12. * 微信支付服务 - 多租户版本
  13. * 使用微信支付v3 SDK,支持小程序支付,支持租户数据隔离
  14. */
  15. export class PaymentMtService extends GenericCrudService<PaymentMtEntity> {
  16. private wxPay: WxPay;
  17. private readonly systemConfigService: SystemConfigServiceMt;
  18. constructor(
  19. dataSource: DataSource
  20. ) {
  21. super(dataSource, PaymentMtEntity, {
  22. tenantOptions: { enabled: true, tenantIdField: 'tenantId' }
  23. });
  24. this.systemConfigService = new SystemConfigServiceMt(dataSource);
  25. // 微信支付SDK将在需要时动态初始化
  26. this.wxPay = null as any;
  27. }
  28. /**
  29. * 初始化微信支付SDK实例
  30. */
  31. private async initializeWxPay(tenantId: number): Promise<void> {
  32. const config = await this.getPaymentConfig(tenantId);
  33. // 初始化微信支付SDK
  34. this.wxPay = new WxPay({
  35. appid: config.appId,
  36. mchid: config.merchantId,
  37. publicKey: Buffer.from(config.publicKey),
  38. privateKey: Buffer.from(config.privateKey),
  39. key: config.v3Key,
  40. serial_no: config.certSerialNo
  41. });
  42. }
  43. /**
  44. * 获取支付配置(优先从系统配置获取,回退到环境变量)
  45. */
  46. private async getPaymentConfig(tenantId: number): Promise<{
  47. merchantId: string;
  48. appId: string;
  49. v3Key: string;
  50. notifyUrl: string;
  51. certSerialNo: string;
  52. publicKey: string;
  53. privateKey: string;
  54. }> {
  55. // 配置键定义
  56. const configKeys = [
  57. 'wx.payment.merchant.id',
  58. 'wx.mini.app.id',
  59. 'wx.payment.v3.key',
  60. 'wx.payment.notify.url',
  61. 'wx.payment.cert.serial.no',
  62. 'wx.payment.public.key',
  63. 'wx.payment.private.key'
  64. ];
  65. // 优先从系统配置获取
  66. const configs = await this.systemConfigService.getConfigsByKeys(configKeys, tenantId);
  67. // 获取配置值,如果系统配置中没有则回退到环境变量
  68. const merchantId = configs['wx.payment.merchant.id'] || process.env.WECHAT_MERCHANT_ID || '';
  69. const appId = configs['wx.mini.app.id'] || process.env.WX_MINI_APP_ID || '';
  70. const v3Key = configs['wx.payment.v3.key'] || process.env.WECHAT_V3_KEY || '';
  71. const notifyUrl = configs['wx.payment.notify.url'] || process.env.WECHAT_PAY_NOTIFY_URL || '';
  72. const certSerialNo = configs['wx.payment.cert.serial.no'] || process.env.WECHAT_MERCHANT_CERT_SERIAL_NO || '';
  73. let publicKey = configs['wx.payment.public.key'] || process.env.WECHAT_PUBLIC_KEY || '';
  74. let privateKey = configs['wx.payment.private.key'] || process.env.WECHAT_PRIVATE_KEY || '';
  75. // 处理证书字符串,将 \n 转换为实际换行符
  76. publicKey = publicKey.replace(/\\n/g, '\n');
  77. privateKey = privateKey.replace(/\\n/g, '\n');
  78. // 验证配置完整性
  79. if (!merchantId || !appId || !v3Key || !certSerialNo || !publicKey || !privateKey) {
  80. throw new Error('微信支付配置不完整,请检查系统配置或环境变量');
  81. }
  82. return {
  83. merchantId,
  84. appId,
  85. v3Key,
  86. notifyUrl,
  87. certSerialNo,
  88. publicKey,
  89. privateKey
  90. };
  91. }
  92. /**
  93. * 创建微信支付订单
  94. * @param externalOrderId 外部订单ID
  95. * @param userId 用户ID
  96. * @param totalAmount 支付金额(分)
  97. * @param description 支付描述
  98. * @param openid 用户OpenID
  99. * @param tenantId 租户ID
  100. */
  101. async createPayment(
  102. externalOrderId: number,
  103. userId: number,
  104. totalAmount: number,
  105. description: string,
  106. openid: string,
  107. tenantId: number
  108. ): Promise<PaymentCreateResponse> {
  109. // 检查是否已存在相同外部订单ID的支付记录
  110. const paymentRepository = this.dataSource.getRepository(PaymentMtEntity);
  111. const existingPayment = await paymentRepository.findOne({
  112. where: { externalOrderId, tenantId }
  113. });
  114. if (existingPayment) {
  115. // 如果支付记录状态是PAID(已支付),不允许重新支付
  116. if (existingPayment.paymentStatus === PaymentStatus.PAID) {
  117. throw new Error('该订单已支付成功,请勿重复支付');
  118. }
  119. // 如果支付记录状态是REFUNDED(已退款),根据业务需求决定是否允许重新支付
  120. // 这里暂时允许重新支付,删除旧记录
  121. if (existingPayment.paymentStatus === PaymentStatus.REFUNDED) {
  122. console.log(`租户${tenantId}订单${externalOrderId}存在已退款的支付记录,允许重新支付`);
  123. }
  124. // 删除旧支付记录,创建新的支付记录
  125. await paymentRepository.remove(existingPayment);
  126. console.log(`删除租户${tenantId}订单${externalOrderId}的旧支付记录,状态: ${existingPayment.paymentStatus}`);
  127. }
  128. if (!openid) {
  129. throw new Error('用户OpenID不能为空');
  130. }
  131. try {
  132. // 重新初始化微信支付SDK
  133. await this.initializeWxPay(tenantId);
  134. // 获取支付配置
  135. const config = await this.getPaymentConfig(tenantId);
  136. // console.debug('微信支付配置:', config);
  137. // 创建商户订单号
  138. const outTradeNo = `PAYMENT_${externalOrderId}_${Date.now()}`;
  139. // 使用微信支付SDK创建JSAPI支付
  140. const result = await this.wxPay.transactions_jsapi({
  141. appid: config.appId,
  142. mchid: config.merchantId,
  143. description,
  144. out_trade_no: outTradeNo,
  145. notify_url: config.notifyUrl,
  146. amount: {
  147. total: totalAmount,
  148. },
  149. payer: {
  150. openid
  151. }
  152. });
  153. console.debug('微信支付SDK返回结果:', result);
  154. // 从 package 字段中提取 prepay_id
  155. const prepayId = result.package ? result.package.replace('prepay_id=', '') : undefined;
  156. // 创建支付记录
  157. const payment = new PaymentMtEntity();
  158. payment.externalOrderId = externalOrderId;
  159. payment.userId = userId;
  160. payment.totalAmount = totalAmount;
  161. payment.description = description;
  162. payment.paymentStatus = PaymentStatus.PROCESSING;
  163. payment.outTradeNo = outTradeNo;
  164. payment.openid = openid;
  165. payment.tenantId = tenantId;
  166. await paymentRepository.save(payment);
  167. // 直接返回微信支付SDK生成的参数
  168. return {
  169. paymentId: prepayId,
  170. timeStamp: result.timeStamp,
  171. nonceStr: result.nonceStr,
  172. package: result.package,
  173. signType: result.signType,
  174. paySign: result.paySign,
  175. totalAmount: totalAmount // 添加金额字段用于前端验证
  176. };
  177. } catch (error) {
  178. const errorMessage = error instanceof Error ? error.message : '未知错误';
  179. throw new Error(`微信支付创建失败: ${errorMessage}`);
  180. }
  181. }
  182. /**
  183. * 获取飞鹅API配置
  184. */
  185. private async getFeieApiConfig(tenantId: number): Promise<any> {
  186. try {
  187. console.debug(`[租户${tenantId}] 获取飞鹅API配置`);
  188. // 从 system_config_mt 表获取飞鹅API配置
  189. const configKeys = [
  190. 'feie.api.user', // 飞鹅API用户
  191. 'feie.api.ukey', // 飞鹅API密钥
  192. 'feie.api.base_url', // 飞鹅API基础URL
  193. 'feie.api.timeout', // API超时时间
  194. 'feie.api.max_retries' // 最大重试次数
  195. ];
  196. const configs = await this.systemConfigService.getConfigsByKeys(configKeys, tenantId);
  197. // 检查必要的配置项
  198. const user = configs['feie.api.user'];
  199. const ukey = configs['feie.api.ukey'];
  200. if (!user || !ukey) {
  201. console.warn(`[租户${tenantId}] 飞鹅API配置不完整,缺少user或ukey`);
  202. return null;
  203. }
  204. // 返回配置结构
  205. return {
  206. user,
  207. ukey,
  208. baseUrl: configs['feie.api.base_url'] || 'http://api.feieyun.cn/Api/Open/',
  209. timeout: parseInt(configs['feie.api.timeout'] || '10000', 10),
  210. maxRetries: parseInt(configs['feie.api.max_retries'] || '3', 10)
  211. };
  212. } catch (error) {
  213. console.warn(`[租户${tenantId}] 获取飞鹅API配置失败:`, error);
  214. return null;
  215. }
  216. }
  217. /**
  218. * 触发打印任务
  219. * 使用飞鹅打印模块创建延迟打印任务
  220. */
  221. private async triggerPrintTask(
  222. tenantId: number,
  223. orderId: number
  224. ): Promise<void> {
  225. try {
  226. // 动态导入飞鹅打印模块,避免循环依赖
  227. const { PrintTriggerService } = await import('@d8d/feie-printer-module-mt');
  228. // 获取飞鹅API配置
  229. const feieConfig = await this.getFeieApiConfig(tenantId);
  230. if (!feieConfig) {
  231. console.debug(`[租户${tenantId}] 飞鹅API配置未找到,跳过打印任务`);
  232. return;
  233. }
  234. // 创建打印触发服务并处理订单支付成功事件
  235. const printTriggerService = new PrintTriggerService(this.dataSource, feieConfig);
  236. await printTriggerService.handleOrderPaymentSuccess(tenantId, orderId);
  237. console.debug(`[租户${tenantId}] 打印任务触发成功,订单ID: ${orderId}`);
  238. } catch (error) {
  239. // 检查是否是模块未找到错误
  240. const errorMessage = error instanceof Error ? error.message : '未知错误';
  241. if (errorMessage.includes('Cannot find package') || errorMessage.includes('ERR_MODULE_NOT_FOUND')) {
  242. console.debug(`[租户${tenantId}] 飞鹅打印模块未安装,跳过打印任务,订单ID: ${orderId}`);
  243. } else {
  244. console.warn(`[租户${tenantId}] 触发打印任务失败,订单ID: ${orderId}:`, error);
  245. }
  246. // 不抛出错误,打印失败不影响支付流程
  247. }
  248. }
  249. /**
  250. * 取消打印任务
  251. * 使用飞鹅打印模块取消关联的打印任务
  252. */
  253. private async cancelPrintTasks(tenantId: number, orderId: number): Promise<void> {
  254. try {
  255. // 动态导入飞鹅打印模块,避免循环依赖
  256. const { PrintTriggerService } = await import('@d8d/feie-printer-module-mt');
  257. // 获取飞鹅API配置
  258. const feieConfig = await this.getFeieApiConfig(tenantId);
  259. if (!feieConfig) {
  260. console.debug(`[租户${tenantId}] 飞鹅API配置未找到,跳过取消打印任务`);
  261. return;
  262. }
  263. // 创建打印触发服务
  264. const printTriggerService = new PrintTriggerService(this.dataSource, feieConfig);
  265. // 处理订单退款事件
  266. await printTriggerService.handleOrderRefund(tenantId, orderId);
  267. console.debug(`[租户${tenantId}] 打印任务取消成功,订单ID: ${orderId}`);
  268. } catch (error) {
  269. // 检查是否是模块未找到错误
  270. const errorMessage = error instanceof Error ? error.message : '未知错误';
  271. if (errorMessage.includes('Cannot find package') || errorMessage.includes('ERR_MODULE_NOT_FOUND')) {
  272. console.debug(`[租户${tenantId}] 飞鹅打印模块未安装,跳过取消打印任务,订单ID: ${orderId}`);
  273. } else {
  274. console.warn(`[租户${tenantId}] 取消打印任务失败,订单ID: ${orderId}:`, error);
  275. }
  276. // 不抛出错误,打印取消失败不影响退款流程
  277. }
  278. }
  279. /**
  280. * 触发支付成功事件
  281. * 创建延迟打印任务
  282. * 注意:此方法主要用于测试,正常情况下支付回调会自动触发
  283. */
  284. async triggerPaymentSuccessEvent(tenantId: number, orderId: number): Promise<void> {
  285. try {
  286. console.debug(`[租户${tenantId}] 触发支付成功事件,订单ID: ${orderId}`);
  287. // 尝试触发打印(如果飞鹅打印模块可用)
  288. await this.triggerPrintTask(tenantId, orderId);
  289. } catch (error) {
  290. console.error(`[租户${tenantId}] 触发支付成功事件失败,订单ID: ${orderId}:`, error);
  291. // 为了调试,抛出错误以便在API响应中看到
  292. throw new Error(`触发支付成功事件失败: ${error instanceof Error ? error.message : '未知错误'}`);
  293. }
  294. }
  295. /**
  296. * 触发退款事件
  297. * 取消关联的打印任务
  298. */
  299. private async triggerRefundEvent(tenantId: number, orderId: number): Promise<void> {
  300. try {
  301. console.debug(`[租户${tenantId}] 触发退款事件,订单ID: ${orderId}`);
  302. // 尝试取消打印任务(如果飞鹅打印模块可用)
  303. await this.cancelPrintTasks(tenantId, orderId);
  304. } catch (error) {
  305. console.error(`[租户${tenantId}] 触发退款事件失败,订单ID: ${orderId}:`, error);
  306. // 不抛出错误,避免影响退款流程
  307. }
  308. }
  309. /**
  310. * 处理支付回调
  311. */
  312. async handlePaymentCallback(
  313. callbackData: any,
  314. headers: any,
  315. rawBody: string // 添加原始请求体参数
  316. ): Promise<void> {
  317. console.debug('收到支付回调请求:', {
  318. headers,
  319. callbackData,
  320. rawBody
  321. });
  322. // 重新初始化微信支付SDK(先初始化以进行签名验证和解密)
  323. console.debug(`开始初始化微信支付SDK`);
  324. await this.initializeWxPay(headers['X-Tenant-Id']); // 使用从域名代理中配置的header中的租户id
  325. console.debug(`微信支付SDK初始化完成`);
  326. // 验证回调签名
  327. console.debug(`开始验证回调签名`);
  328. const isValid = await this.wxPay.verifySign({
  329. timestamp: headers['wechatpay-timestamp'],
  330. nonce: headers['wechatpay-nonce'],
  331. body: rawBody, // 优先使用原始请求体
  332. serial: headers['wechatpay-serial'],
  333. signature: headers['wechatpay-signature']
  334. });
  335. console.debug(`回调签名验证结果:`, isValid);
  336. if (!isValid) {
  337. console.debug(`回调签名验证失败,headers:`, headers);
  338. throw new Error('回调签名验证失败');
  339. }
  340. console.debug(`回调签名验证成功`);
  341. // 解密回调数据
  342. console.debug(`开始解密回调数据`);
  343. const decryptedData = this.wxPay.decipher_gcm(
  344. callbackData.resource.ciphertext,
  345. callbackData.resource.associated_data || '',
  346. callbackData.resource.nonce
  347. );
  348. console.debug(`解密回调数据:`, decryptedData);
  349. console.debug(`解密回调数据类型:`, typeof decryptedData);
  350. // 处理解密后的数据,可能是字符串或对象
  351. let parsedData;
  352. if (typeof decryptedData === 'string') {
  353. parsedData = JSON.parse(decryptedData);
  354. } else {
  355. parsedData = decryptedData;
  356. }
  357. console.debug(`解析后的回调数据:`, parsedData);
  358. // 从解密后的数据中获取商户订单号
  359. const outTradeNo = parsedData.out_trade_no;
  360. if (!outTradeNo) {
  361. console.debug('解密后的回调数据中缺少商户订单号,回调数据:', parsedData);
  362. throw new Error('回调数据中缺少商户订单号');
  363. }
  364. console.debug(`开始处理支付回调,商户订单号: ${outTradeNo}`);
  365. // 从数据库中查找支付记录以获取租户ID
  366. const paymentRepository = this.dataSource.getRepository(PaymentMtEntity);
  367. const payment = await paymentRepository.findOne({
  368. where: { outTradeNo }
  369. });
  370. if (!payment) {
  371. console.debug(`支付记录不存在,商户订单号: ${outTradeNo}`);
  372. throw new Error('支付记录不存在');
  373. }
  374. console.debug(`[租户${payment.tenantId}] 找到支付记录,外部订单ID: ${payment.externalOrderId}, 当前支付状态: ${payment.paymentStatus}`);
  375. // 重新使用正确的租户ID初始化微信支付SDK
  376. console.debug(`[租户${payment.tenantId}] 重新初始化微信支付SDK`);
  377. await this.initializeWxPay(payment.tenantId);
  378. console.debug(`[租户${payment.tenantId}] 微信支付SDK重新初始化完成`);
  379. // 根据回调结果更新支付状态
  380. const originalPaymentStatus = payment.paymentStatus;
  381. if (parsedData.trade_state === 'SUCCESS') {
  382. payment.paymentStatus = PaymentStatus.PAID;
  383. payment.wechatTransactionId = parsedData.transaction_id;
  384. console.debug(`[租户${payment.tenantId}] 支付成功,微信交易流水号: ${parsedData.transaction_id}`);
  385. } else if (parsedData.trade_state === 'FAIL') {
  386. payment.paymentStatus = PaymentStatus.FAILED;
  387. console.debug(`[租户${payment.tenantId}] 支付失败`);
  388. } else if (parsedData.trade_state === 'REFUND') {
  389. payment.paymentStatus = PaymentStatus.REFUNDED;
  390. console.debug(`[租户${payment.tenantId}] 支付退款`);
  391. }
  392. console.debug(`[租户${payment.tenantId}] 支付状态从 ${originalPaymentStatus} 更新为 ${payment.paymentStatus}`);
  393. console.debug(`[租户${payment.tenantId}] 开始保存支付记录`);
  394. await paymentRepository.save(payment);
  395. console.debug(`[租户${payment.tenantId}] 支付记录保存成功`);
  396. // 根据支付状态更新订单状态
  397. console.debug(`[租户${payment.tenantId}] 开始更新订单状态,外部订单ID: ${payment.externalOrderId}, 支付状态: ${payment.paymentStatus}`);
  398. try {
  399. if (payment.paymentStatus === PaymentStatus.PAID) {
  400. // 支付成功,更新订单状态为已支付 (2),支付类型为微信支付 (4)
  401. await this.updateOrderPaymentStatus(payment.tenantId, payment.externalOrderId, PayStatus.SUCCESS, PayType.WECHAT);
  402. console.debug(`[租户${payment.tenantId}] 订单状态更新为已支付,订单ID: ${payment.externalOrderId}`);
  403. // 触发支付成功事件,创建延迟打印任务
  404. await this.triggerPaymentSuccessEvent(payment.tenantId, payment.externalOrderId);
  405. } else if (payment.paymentStatus === PaymentStatus.FAILED) {
  406. // 支付失败,更新订单状态为支付失败 (4),支付类型为微信支付 (4)
  407. await this.updateOrderPaymentStatus(payment.tenantId, payment.externalOrderId, PayStatus.FAILED, PayType.WECHAT);
  408. console.debug(`[租户${payment.tenantId}] 订单状态更新为支付失败,订单ID: ${payment.externalOrderId}`);
  409. } else if (payment.paymentStatus === PaymentStatus.REFUNDED) {
  410. // 退款,更新订单状态为已退款 (3),支付类型为微信支付 (4)
  411. await this.updateOrderPaymentStatus(payment.tenantId, payment.externalOrderId, PayStatus.REFUNDED, PayType.WECHAT);
  412. console.debug(`[租户${payment.tenantId}] 订单状态更新为已退款,订单ID: ${payment.externalOrderId}`);
  413. }
  414. } catch (error) {
  415. console.debug(`[租户${payment.tenantId}] 订单状态更新失败,订单ID: ${payment.externalOrderId}, 错误详情:`, {
  416. error: error instanceof Error ? error.message : '未知错误',
  417. stack: error instanceof Error ? error.stack : undefined
  418. });
  419. // 这里不抛出错误,因为支付记录已经保存,订单状态更新失败不影响支付回调的成功响应
  420. // 但记录详细的错误信息以便后续排查
  421. }
  422. }
  423. /**
  424. * 查询支付状态
  425. */
  426. async getPaymentStatus(externalOrderId: number, tenantId: number): Promise<PaymentStatus> {
  427. const paymentRepository = this.dataSource.getRepository(PaymentMtEntity);
  428. const payment = await paymentRepository.findOne({
  429. where: { externalOrderId, tenantId }
  430. });
  431. if (!payment) {
  432. throw new Error('支付记录不存在');
  433. }
  434. return payment.paymentStatus;
  435. }
  436. /**
  437. * 生成随机字符串
  438. */
  439. private generateNonceStr(length: number = 32): string {
  440. const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  441. let result = '';
  442. for (let i = 0; i < length; i++) {
  443. result += chars.charAt(Math.floor(Math.random() * chars.length));
  444. }
  445. return result;
  446. }
  447. /**
  448. * 生成回调签名(用于测试)
  449. */
  450. async generateCallbackSignature(
  451. timestamp: string,
  452. nonce: string,
  453. callbackData: any,
  454. tenantId: number
  455. ): Promise<string> {
  456. // 初始化微信支付SDK
  457. await this.initializeWxPay(tenantId);
  458. return this.wxPay.getSignature(
  459. 'POST',
  460. nonce,
  461. timestamp,
  462. '/v3/pay/transactions/jsapi',
  463. callbackData
  464. );
  465. }
  466. /**
  467. * 获取微信支付平台证书(用于测试)
  468. */
  469. async getPlatformCertificates(tenantId: number): Promise<any> {
  470. try {
  471. // 重新初始化微信支付SDK
  472. await this.initializeWxPay(tenantId);
  473. console.debug('开始获取微信支付平台证书...');
  474. const config = await this.getPaymentConfig(tenantId);
  475. const certificates = await this.wxPay.get_certificates(config.v3Key);
  476. console.debug('获取平台证书成功:', certificates);
  477. return certificates;
  478. } catch (error) {
  479. console.debug('获取平台证书失败:', error);
  480. throw error;
  481. }
  482. }
  483. /**
  484. * 更新订单支付状态
  485. * @param tenantId 租户ID
  486. * @param externalOrderId 外部订单ID
  487. * @param payState 支付状态 (0未支付、1支付中、2支付成功、3已退款、4支付失败、5订单关闭)
  488. * @param payType 支付类型 (1积分、2礼券、3额度支付、4微信支付),可选,默认为微信支付
  489. */
  490. async updateOrderPaymentStatus(tenantId: number, externalOrderId: number, payState: number, payType: number = PayType.WECHAT): Promise<void> {
  491. console.debug(`[租户${tenantId}] 开始更新订单支付状态,订单ID: ${externalOrderId}, 支付状态: ${payState}, 支付类型: ${payType}`);
  492. try {
  493. // 直接使用数据源更新订单支付状态
  494. const orderRepository = this.dataSource.getRepository(OrderMt);
  495. // 首先检查订单是否存在
  496. const order = await orderRepository.findOne({
  497. where: { id: externalOrderId, tenantId }
  498. });
  499. if (!order) {
  500. console.debug(`[租户${tenantId}] 订单不存在,订单ID: ${externalOrderId}`);
  501. throw new Error(`订单ID ${externalOrderId} 不存在`);
  502. }
  503. console.debug(`[租户${tenantId}] 找到订单,准备更新支付状态,当前支付状态: ${order.payState}`);
  504. const updateResult = await orderRepository.update(
  505. { id: externalOrderId, tenantId },
  506. {
  507. payState,
  508. payType,
  509. updatedAt: new Date()
  510. }
  511. );
  512. if (updateResult.affected === 0) {
  513. console.debug(`[租户${tenantId}] 订单更新失败,订单ID: ${externalOrderId}`);
  514. throw new Error(`订单ID ${externalOrderId} 更新失败`);
  515. }
  516. console.debug(`[租户${tenantId}] 订单支付状态更新成功,订单ID: ${externalOrderId}, 支付状态: ${payState}, 支付类型: ${payType}`);
  517. } catch (error) {
  518. console.debug(`[租户${tenantId}] 订单支付状态更新失败,订单ID: ${externalOrderId}, 错误详情:`, {
  519. error: error instanceof Error ? error.message : '未知错误',
  520. stack: error instanceof Error ? error.stack : undefined
  521. });
  522. throw error;
  523. }
  524. }
  525. /**
  526. * 执行微信支付退款
  527. * @param tenantId 租户ID
  528. * @param orderNo 订单号
  529. * @param refundAmount 退款金额(分)
  530. * @param refundReason 退款原因
  531. */
  532. async refund(
  533. tenantId: number,
  534. orderNo: string,
  535. refundAmount: number,
  536. refundReason?: string
  537. ): Promise<RefundResponse> {
  538. console.debug(`[租户${tenantId}] 开始处理退款,订单号: ${orderNo}, 退款金额: ${refundAmount}`);
  539. try {
  540. // 初始化微信支付SDK
  541. await this.initializeWxPay(tenantId);
  542. // 根据订单号查找支付记录
  543. const paymentRepository = this.dataSource.getRepository(PaymentMtEntity);
  544. // 首先通过订单号查找对应的订单ID
  545. const orderRepository = this.dataSource.getRepository(OrderMt);
  546. const order = await orderRepository.findOne({
  547. where: { orderNo, tenantId }
  548. });
  549. if (!order) {
  550. throw new Error(`订单不存在,订单号: ${orderNo}`);
  551. }
  552. // 通过订单ID查找支付记录
  553. const payment = await paymentRepository.findOne({
  554. where: { externalOrderId: order.id, tenantId }
  555. });
  556. if (!payment) {
  557. throw new Error(`支付记录不存在,订单号: ${orderNo}`);
  558. }
  559. // 验证支付状态
  560. if (payment.paymentStatus !== PaymentStatus.PAID) {
  561. throw new Error(`订单支付状态不正确,当前状态: ${payment.paymentStatus}`);
  562. }
  563. // 验证退款金额并转换为整数(分)
  564. const refundAmountInCents = Math.round(refundAmount * 100);
  565. if (refundAmountInCents <= 0 || refundAmountInCents > payment.totalAmount) {
  566. throw new Error(`退款金额无效,退款金额: ${refundAmount}, 支付金额: ${payment.totalAmount}`);
  567. }
  568. // 生成退款订单号
  569. const outRefundNo = `REFUND_${orderNo}_${Date.now()}`;
  570. // 验证outTradeNo存在
  571. if (!payment.outTradeNo) {
  572. throw new Error('支付记录缺少商户订单号');
  573. }
  574. // 调用微信支付退款API
  575. const result = await this.wxPay.refunds({
  576. out_trade_no: payment.outTradeNo,
  577. out_refund_no: outRefundNo,
  578. amount: {
  579. refund: refundAmountInCents,
  580. total: payment.totalAmount,
  581. currency: 'CNY'
  582. },
  583. reason: refundReason || '用户取消订单'
  584. });
  585. console.debug(`[租户${tenantId}] 微信支付退款API返回结果:`, result);
  586. // 更新支付记录的退款状态
  587. payment.refundStatus = PaymentStatus.REFUNDED;
  588. payment.refundTransactionId = outRefundNo; // 使用退款订单号作为临时退款流水号
  589. payment.refundAmount = refundAmountInCents;
  590. payment.refundTime = new Date();
  591. await paymentRepository.save(payment);
  592. console.debug(`[租户${tenantId}] 退款处理完成,退款订单号: ${outRefundNo}`);
  593. return {
  594. refundId: outRefundNo,
  595. outRefundNo: outRefundNo,
  596. refundStatus: 'SUCCESS',
  597. refundAmount: refundAmount,
  598. refundTime: new Date().toISOString()
  599. };
  600. } catch (error) {
  601. console.debug(`[租户${tenantId}] 退款处理失败,订单号: ${orderNo}, 错误:`, error);
  602. throw new Error(`退款处理失败: ${error instanceof Error ? error.message : '未知错误'}`);
  603. }
  604. }
  605. /**
  606. * 处理退款回调
  607. */
  608. async handleRefundCallback(
  609. callbackData: any,
  610. headers: any,
  611. rawBody: string
  612. ): Promise<void> {
  613. console.debug('收到退款回调请求:', {
  614. headers,
  615. callbackData,
  616. rawBody
  617. });
  618. // 重新初始化微信支付SDK(先初始化以进行签名验证和解密)
  619. await this.initializeWxPay(1); // 先使用默认租户ID进行初始化
  620. // 验证回调签名
  621. const isValid = await this.wxPay.verifySign({
  622. timestamp: headers['wechatpay-timestamp'],
  623. nonce: headers['wechatpay-nonce'],
  624. body: rawBody,
  625. serial: headers['wechatpay-serial'],
  626. signature: headers['wechatpay-signature']
  627. });
  628. if (!isValid) {
  629. console.debug('退款回调签名验证失败,headers:', headers);
  630. throw new Error('退款回调签名验证失败');
  631. }
  632. console.debug('退款回调签名验证成功');
  633. // 解密回调数据
  634. const decryptedData = this.wxPay.decipher_gcm(
  635. callbackData.resource.ciphertext,
  636. callbackData.resource.associated_data || '',
  637. callbackData.resource.nonce
  638. );
  639. console.debug('解密退款回调数据:', decryptedData);
  640. // 处理解密后的数据
  641. let parsedData;
  642. if (typeof decryptedData === 'string') {
  643. parsedData = JSON.parse(decryptedData);
  644. } else {
  645. parsedData = decryptedData;
  646. }
  647. console.debug('解析后的退款回调数据:', parsedData);
  648. // 从解密后的数据中获取退款订单号
  649. const outRefundNo = parsedData.out_refund_no;
  650. if (!outRefundNo) {
  651. console.debug('解密后的退款回调数据中缺少退款订单号,回调数据:', parsedData);
  652. throw new Error('退款回调数据中缺少退款订单号');
  653. }
  654. console.debug(`开始处理退款回调,退款订单号: ${outRefundNo}`);
  655. // 这里可以根据退款回调更新相关业务状态
  656. // 例如:更新退款记录状态、通知用户等
  657. console.debug(`退款回调处理完成,退款订单号: ${outRefundNo}, 退款状态: ${parsedData.refund_status}`);
  658. }
  659. }