payment.service.ts 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995
  1. import { prisma } from '../../models';
  2. import crypto from 'crypto';
  3. import https from 'https';
  4. // 懒加载支付 SDK(避免 Node.js ESM 兼容性问题)
  5. let AlipaySdk: any = null;
  6. let WeChatPay: any = null;
  7. let sdkInitialized = false;
  8. // ==================== 微信 JSAPI 支付相关缓存 ====================
  9. let cachedAccessToken: string | null = null;
  10. let accessTokenExpireAt: number = 0;
  11. let cachedJsapiTicket: string | null = null;
  12. let jsapiTicketExpireAt: number = 0;
  13. // 初始化支付 SDK
  14. async function initPaymentSdks() {
  15. if (sdkInitialized) return;
  16. sdkInitialized = true;
  17. try {
  18. const sdk = await import('alipay-sdk');
  19. AlipaySdk = sdk.AlipaySdk;
  20. console.log('[Payment] 支付宝 SDK 加载成功');
  21. } catch (e) {
  22. console.warn('[Payment] 支付宝 SDK 加载失败:', e);
  23. }
  24. try {
  25. // 直接 require,因为 wechatpay-node-v3 是 CommonJS 模块
  26. WeChatPay = require('wechatpay-node-v3');
  27. console.log('[Payment] 微信支付 SDK 加载成功');
  28. } catch (e) {
  29. console.warn('[Payment] 微信支付 SDK 加载失败:', e);
  30. }
  31. }
  32. // 生成订单号
  33. export function generateOrderNo(): string {
  34. const now = new Date();
  35. const dateStr = now.toISOString().slice(0, 10).replace(/-/g, '');
  36. const random = Math.random().toString(36).substring(2, 8).toUpperCase();
  37. return `PAY${dateStr}${random}`;
  38. }
  39. // Token 包配置
  40. const TOKEN_PACK = {
  41. minutesPerPack: 100,
  42. packUnitPrice: 0.048,
  43. get packPrice() { return Math.round(this.minutesPerPack * this.packUnitPrice * 100) / 100; },
  44. get packQuotaValue() { return Math.round(this.minutesPerPack * 0.032 * 100) / 100; },
  45. };
  46. // 创建 Token 包支付订单
  47. export async function createTokenPackOrder(
  48. userId: number,
  49. quantity: number,
  50. paymentMethod: 'alipay' | 'wechat' | 'mock',
  51. returnUrl?: string
  52. ): Promise<{
  53. orderNo: string;
  54. amount: number;
  55. planName: string;
  56. paymentUrl?: string;
  57. qrcode?: string;
  58. }> {
  59. await initPaymentSdks();
  60. if (quantity < 1) {
  61. throw new Error('购买数量至少为1');
  62. }
  63. const amount = Math.round(TOKEN_PACK.packPrice * quantity * 100) / 100;
  64. const orderNo = generateOrderNo();
  65. const subject = `${quantity}包积分包(共${TOKEN_PACK.minutesPerPack * quantity}分钟)`;
  66. // 创建订单记录(productType='token-pack',无 planId)
  67. await prisma.order.create({
  68. data: {
  69. userId,
  70. orderNo,
  71. planId: null,
  72. productType: 'token-pack',
  73. amount,
  74. status: 'pending',
  75. paymentMethod,
  76. }
  77. });
  78. // 生成支付信息
  79. let paymentUrl: string | undefined;
  80. let qrcode: string | undefined;
  81. try {
  82. if (paymentMethod === 'alipay') {
  83. const alipayResult = await generateAlipayPayment(orderNo, amount, subject, returnUrl);
  84. paymentUrl = alipayResult.paymentUrl;
  85. qrcode = alipayResult.qrcode;
  86. } else if (paymentMethod === 'wechat') {
  87. const wechatResult = await generateWechatPayment(orderNo, amount, subject);
  88. qrcode = wechatResult.qrcode;
  89. paymentUrl = wechatResult.h5Url;
  90. }
  91. } catch (error) {
  92. console.error('[Payment] 支付链接生成失败:', error);
  93. throw error;
  94. }
  95. return {
  96. orderNo,
  97. amount,
  98. planName: subject,
  99. paymentUrl,
  100. qrcode
  101. };
  102. }
  103. // 初始化支付宝 SDK
  104. let alipaySdk: any | null = null;
  105. function getAlipaySdk(): any | null {
  106. if (!AlipaySdk) {
  107. console.warn('[Alipay] 支付宝 SDK 未加载');
  108. return null;
  109. }
  110. const appId = process.env.ALIPAY_APP_ID || '';
  111. const privateKey = process.env.ALIPAY_PRIVATE_KEY || '';
  112. const alipayPublicKey = process.env.ALIPAY_PUBLIC_KEY || '';
  113. if (!appId || !privateKey || !alipayPublicKey) {
  114. console.warn('[Alipay] 未配置支付宝参数', { appId: !!appId, hasPrivateKey: !!privateKey, hasPublicKey: !!alipayPublicKey });
  115. return null;
  116. }
  117. if (!alipaySdk) {
  118. // keyType 必须指定为 PKCS8,因为 ALIPAY_PRIVATE_KEY 是 PKCS#8 格式(以 MIIEvQIBADAN 开头)
  119. // 如果不指定 keyType,SDK 默认按 PKCS1 处理,会把 PEM 头写为 RSA PRIVATE KEY
  120. // 导致 Node.js crypto 模块解析 PKCS8 数据时出错,从而签名失败
  121. alipaySdk = new AlipaySdk({
  122. appId,
  123. privateKey,
  124. alipayPublicKey,
  125. gateway: 'https://openapi.alipay.com/gateway.do',
  126. signType: 'RSA2',
  127. keyType: 'PKCS8',
  128. });
  129. }
  130. return alipaySdk;
  131. }
  132. // 初始化微信支付 SDK
  133. let wechatPay: any | null = null;
  134. function getWechatPay(): any | null {
  135. if (!WeChatPay) {
  136. console.warn('[WeChatPay] 微信支付 SDK 未加载');
  137. return null;
  138. }
  139. const appId = process.env.WECHAT_APP_ID;
  140. const mchId = process.env.WECHAT_MCH_ID;
  141. const serialNo = process.env.WECHAT_SERIAL_NO;
  142. const apiv3Key = process.env.WECHAT_APIV3_KEY;
  143. // 读取私钥和公钥文件(dotenv 无法正确解析多行 PEM 密钥)
  144. const fs = require('fs');
  145. const path = require('path');
  146. const certDir = path.join(__dirname, '../../../cert/wx');
  147. const privateKeyPath = path.join(certDir, 'apiclient_key.pem');
  148. const publicKeyPath = path.join(certDir, 'pub_key.pem');
  149. let privateKey = '';
  150. let publicKey = '';
  151. try {
  152. privateKey = fs.readFileSync(privateKeyPath, 'utf8');
  153. publicKey = fs.readFileSync(publicKeyPath, 'utf8');
  154. } catch (e) {
  155. console.warn('[WeChatPay] 未找到证书文件:', e.message);
  156. }
  157. if (!appId || !mchId || !privateKey || !serialNo || !apiv3Key) {
  158. console.warn('[WeChatPay] 未配置微信支付参数');
  159. return null;
  160. }
  161. if (!wechatPay) {
  162. // 从私钥导出公钥(dotenv无法正确解析多行PEM,且提供的pub_key.pem与私钥不匹配)
  163. const privKeyObj = crypto.createPrivateKey(privateKey);
  164. const pubKeyFromPriv = crypto.createPublicKey(privKeyObj);
  165. const publicKeyDer = pubKeyFromPriv.export({ type: 'spki', format: 'der' });
  166. wechatPay = new WeChatPay({
  167. appid: appId,
  168. mchid: mchId,
  169. serial_no: serialNo,
  170. publicKey: publicKeyDer,
  171. privateKey: Buffer.from(privateKey),
  172. key: apiv3Key,
  173. });
  174. }
  175. return wechatPay;
  176. }
  177. // 创建支付订单
  178. export async function createPaymentOrder(
  179. userId: number,
  180. planId: number,
  181. paymentMethod: 'alipay' | 'wechat' | 'mock',
  182. period: 'monthly' | 'yearly' = 'monthly',
  183. returnUrl?: string
  184. ): Promise<{
  185. orderNo: string;
  186. amount: number;
  187. planName: string;
  188. paymentUrl?: string;
  189. qrcode?: string;
  190. }> {
  191. // 确保 SDK 已加载
  192. await initPaymentSdks();
  193. // 获取套餐信息
  194. const plan = await prisma.subscriptionPlan.findUnique({
  195. where: { id: planId }
  196. });
  197. if (!plan) {
  198. throw new Error('套餐不存在');
  199. }
  200. // 生成订单号
  201. const orderNo = generateOrderNo();
  202. // 根据周期选择价格
  203. const amount = period === 'yearly' ? Number(plan.priceYearly) : Number(plan.priceMonthly);
  204. // 创建订单记录
  205. await prisma.order.create({
  206. data: {
  207. userId,
  208. orderNo,
  209. planId,
  210. productType: period,
  211. amount,
  212. status: 'pending',
  213. paymentMethod,
  214. }
  215. });
  216. // 生成支付信息
  217. let paymentUrl: string | undefined;
  218. let qrcode: string | undefined;
  219. try {
  220. if (paymentMethod === 'alipay') {
  221. const alipayResult = await generateAlipayPayment(orderNo, amount, plan.name, returnUrl);
  222. paymentUrl = alipayResult.paymentUrl;
  223. qrcode = alipayResult.qrcode;
  224. } else if (paymentMethod === 'wechat') {
  225. const wechatResult = await generateWechatPayment(orderNo, amount, plan.name);
  226. qrcode = wechatResult.qrcode;
  227. paymentUrl = wechatResult.h5Url;
  228. }
  229. } catch (error) {
  230. console.error('[Payment] 支付链接生成失败:', error);
  231. throw error;
  232. }
  233. return {
  234. orderNo,
  235. amount,
  236. planName: plan.name,
  237. paymentUrl,
  238. qrcode
  239. };
  240. }
  241. // 生成支付宝支付链接
  242. async function generateAlipayPayment(
  243. orderNo: string,
  244. amount: number,
  245. subject: string,
  246. returnUrl?: string
  247. ): Promise<{ paymentUrl?: string; qrcode?: string }> {
  248. const sdk = getAlipaySdk();
  249. // 默认回调地址
  250. const defaultReturnUrl = process.env.ALIPAY_RETURN_URL || `${process.env.BASE_URL || 'https://your-domain.com'}/#/pages/payment-result/index`;
  251. const finalReturnUrl = returnUrl || defaultReturnUrl;
  252. if (!sdk) {
  253. console.warn('[Alipay] 未配置支付宝,返回模拟支付链接');
  254. return {
  255. paymentUrl: `https://openapi.alipay.com/gateway.do?out_trade_no=${orderNo}&total_amount=${amount}&subject=${encodeURIComponent(subject)}`
  256. };
  257. }
  258. // 使用手机网站支付 API(WAP支付)
  259. // sdkExecute 返回完整查询字符串(含 biz_content + sign),拼上网关地址即可
  260. // pageExec 生成的 POST 表单把 biz_content 放在隐藏 input 里,提取 action URL 会丢失它
  261. try {
  262. const notifyUrl = process.env.ALIPAY_NOTIFY_URL || '';
  263. const queryString = sdk.sdkExec('alipay.trade.wap.pay', {
  264. bizContent: {
  265. out_trade_no: orderNo,
  266. total_amount: String(amount),
  267. subject: subject,
  268. product_code: 'QUICK_WAP_WAY',
  269. },
  270. returnUrl: finalReturnUrl,
  271. notifyUrl,
  272. });
  273. const paymentUrl = `https://openapi.alipay.com/gateway.do?${queryString}`;
  274. return {
  275. paymentUrl,
  276. };
  277. } catch (error) {
  278. console.error('[Alipay] 支付链接生成失败:', error);
  279. throw new Error('支付宝支付链接生成失败');
  280. }
  281. }
  282. // 生成微信支付二维码链接
  283. async function generateWechatPayment(
  284. orderNo: string,
  285. amount: number,
  286. subject: string
  287. ): Promise<{ qrcode?: string; h5Url?: string }> {
  288. const wechat = getWechatPay();
  289. if (!wechat) {
  290. console.warn('[WeChatPay] 未配置微信支付,返回模拟支付链接');
  291. return {
  292. qrcode: `https://api.mch.weixin.qq.com/qrcode/${orderNo}`,
  293. h5Url: `weixin://wxpay/bizpayurl?pr=${encodeURIComponent(orderNo)}`
  294. };
  295. }
  296. try {
  297. console.log('[WeChatPay] 开始创建 NATIVE 支付订单:', { orderNo, amount, subject });
  298. // 使用 NATIVE 支付(扫码支付)
  299. const result: any = await wechat.transactions_native({
  300. description: subject,
  301. out_trade_no: orderNo,
  302. amount: {
  303. total: Math.round(amount * 100), // 转换为分
  304. currency: 'CNY',
  305. },
  306. notify_url: process.env.WECHAT_NOTIFY_URL || `${process.env.BASE_URL || 'https://your-domain.com'}/api/payment/wechat/notify`,
  307. });
  308. console.log('[WeChatPay] 支付接口返回, result:', JSON.stringify(result));
  309. // 检查返回状态(SDK 不会reject错误响应,而是返回包含error的对象)
  310. if (result.status !== 200 && result.status !== '200') {
  311. const errorMsg = JSON.stringify(result);
  312. console.error('[WeChatPay] 支付失败:', errorMsg);
  313. throw new Error(`微信支付接口返回错误: ${errorMsg}`);
  314. }
  315. console.log('[WeChatPay] 支付二维码生成成功, code_url:', result.data?.code_url || result.code_url);
  316. // 返回二维码链接(前端自行生成二维码图片)
  317. return {
  318. qrcode: result.data?.code_url || result.code_url,
  319. };
  320. } catch (error: any) {
  321. const detail = error.message || JSON.stringify(error);
  322. console.error('[WeChatPay] 支付二维码生成失败:', detail);
  323. throw new Error(`微信支付暂不可用(${detail}),请稍后重试或使用其他支付方式`);
  324. }
  325. }
  326. // 生成支付宝扫码支付二维码
  327. export async function generateAlipayQrcode(
  328. orderNo: string,
  329. amount: number,
  330. subject: string
  331. ): Promise<string> {
  332. const sdk = getAlipaySdk();
  333. if (!sdk) {
  334. console.warn('[Alipay] 未配置支付宝,返回模拟二维码链接');
  335. return `https://qr.alipay.com/${orderNo}`;
  336. }
  337. try {
  338. // 使用当面付预下单 API(v3 curl 替代已废弃的 exec)
  339. const result = await sdk.curl('POST', '/v3/alipay/trade/precreate', {
  340. body: {
  341. out_trade_no: orderNo,
  342. total_amount: String(amount),
  343. subject: subject,
  344. },
  345. timeout: 10000,
  346. });
  347. // 解析返回的二维码链接
  348. const response = result as any;
  349. if (response.qr_code) {
  350. return response.qr_code;
  351. }
  352. return result as string;
  353. } catch (error) {
  354. console.error('[Alipay] 扫码支付二维码生成失败:', error);
  355. throw new Error('支付宝扫码支付生成失败');
  356. }
  357. }
  358. // 验证支付宝回调签名
  359. export function verifyAlipaySign(params: Record<string, string>): boolean {
  360. const sdk = getAlipaySdk();
  361. if (!sdk) {
  362. console.warn('[Alipay] 未配置支付宝,跳过签名验证');
  363. return true;
  364. }
  365. try {
  366. // checkNotifySignV2 不会对参数值做 decode,避免特殊字符导致验签失败
  367. const signVerified = sdk.checkNotifySignV2(params);
  368. console.log('[Alipay] 签名验证结果:', signVerified);
  369. return signVerified;
  370. } catch (error) {
  371. console.error('[Alipay] 签名验证失败:', error);
  372. return false;
  373. }
  374. }
  375. // 处理支付回调
  376. export async function handlePaymentCallback(
  377. orderNo: string,
  378. paymentId: string,
  379. status: 'success' | 'failed'
  380. ) {
  381. const order = await prisma.order.findFirst({
  382. where: { orderNo }
  383. });
  384. if (!order) {
  385. throw new Error('订单不存在');
  386. }
  387. if (order.status === 'paid') {
  388. // 订单已经支付成功,直接返回
  389. return { success: true, message: '订单已支付' };
  390. }
  391. if (status === 'failed') {
  392. await prisma.order.update({
  393. where: { id: order.id },
  394. data: {
  395. status: 'failed',
  396. paymentId,
  397. updatedAt: new Date()
  398. }
  399. });
  400. return { success: false, message: '支付失败' };
  401. }
  402. // 支付成功
  403. await prisma.order.update({
  404. where: { id: order.id },
  405. data: {
  406. status: 'paid',
  407. paymentId,
  408. paidAt: new Date(),
  409. updatedAt: new Date()
  410. }
  411. });
  412. // 根据产品类型处理
  413. if (order.productType === 'token-pack') {
  414. // 积分包:增加用户 costLimit
  415. await processTokenPackOrder(order);
  416. } else if (order.planId) {
  417. // 套餐订阅:激活订阅
  418. await activateSubscription(order.userId, order.planId, order.productType);
  419. }
  420. return { success: true, message: '支付成功' };
  421. }
  422. // 处理积分包订单(支付成功后给用户加额度)
  423. async function processTokenPackOrder(order: any) {
  424. const quantity = Math.round(Number(order.amount) / TOKEN_PACK.packPrice);
  425. const addedQuota = Math.round(TOKEN_PACK.packQuotaValue * quantity * 100) / 100;
  426. const addedTokens = TOKEN_PACK.minutesPerPack * quantity * 150; // 150 tokens/分钟
  427. // 原子更新 costLimit
  428. await prisma.user.update({
  429. where: { id: order.userId },
  430. data: { costLimit: { increment: addedQuota } },
  431. });
  432. // 同步增加 TokenBalance(积分包部分)
  433. await prisma.tokenBalance.upsert({
  434. where: { userId: order.userId },
  435. create: { userId: order.userId, totalTokens: addedTokens, packTokens: addedTokens },
  436. update: { totalTokens: { increment: addedTokens }, packTokens: { increment: addedTokens } },
  437. });
  438. // 记录购买历史
  439. await prisma.tokenPackPurchase.create({
  440. data: {
  441. userId: order.userId,
  442. packMinutes: TOKEN_PACK.minutesPerPack,
  443. quantity,
  444. unitPrice: TOKEN_PACK.packUnitPrice,
  445. totalAmount: Number(order.amount),
  446. addedQuota,
  447. },
  448. });
  449. console.log(`[TokenPack] 用户#${order.userId} 支付成功,购买 ${quantity} 包,增加额度 ¥${addedQuota},增加积分 ${addedTokens}`);
  450. }
  451. // 激活订阅
  452. export async function activateSubscription(userId: number, planId: number, productType: string = 'monthly') {
  453. const plan = await prisma.subscriptionPlan.findUnique({
  454. where: { id: planId }
  455. });
  456. if (!plan) {
  457. throw new Error('套餐不存在');
  458. }
  459. const now = new Date();
  460. // 根据订单的产品类型确定有效天数
  461. const days = productType === 'yearly' ? 365 : 30;
  462. const endDate = new Date(now.getTime() + days * 24 * 60 * 60 * 1000);
  463. const existingSubscription = await prisma.subscription.findFirst({
  464. where: {
  465. userId,
  466. status: 'active',
  467. endDate: { gt: now }
  468. }
  469. });
  470. let subscriptionEndDate = endDate;
  471. if (existingSubscription) {
  472. const isSamePlan = existingSubscription.planId === planId;
  473. if (isSamePlan) {
  474. // 续费:同套餐叠加天数
  475. subscriptionEndDate = new Date(existingSubscription.endDate.getTime() + days * 24 * 60 * 60 * 1000);
  476. await prisma.subscription.update({
  477. where: { id: existingSubscription.id },
  478. data: {
  479. endDate: subscriptionEndDate,
  480. updatedAt: now
  481. }
  482. });
  483. } else {
  484. // 升级/降级:结束旧订阅,新订阅从今天起算
  485. await prisma.subscription.update({
  486. where: { id: existingSubscription.id },
  487. data: {
  488. status: 'upgraded',
  489. updatedAt: now
  490. }
  491. });
  492. await prisma.subscription.create({
  493. data: {
  494. userId,
  495. planId,
  496. startDate: now,
  497. endDate,
  498. status: 'active',
  499. autoRenew: false
  500. }
  501. });
  502. }
  503. } else {
  504. await prisma.subscription.create({
  505. data: {
  506. userId,
  507. planId,
  508. startDate: now,
  509. endDate: subscriptionEndDate,
  510. status: 'active',
  511. autoRenew: false
  512. }
  513. });
  514. }
  515. // 更新用户会员等级
  516. await prisma.user.update({
  517. where: { id: userId },
  518. data: {
  519. memberLevel: plan.level,
  520. memberExpireAt: subscriptionEndDate
  521. }
  522. });
  523. // 更新Token余额
  524. const monthlyTokens = plan.monthlyTokens;
  525. const currentBalance = await prisma.tokenBalance.findUnique({
  526. where: { userId }
  527. });
  528. if (monthlyTokens !== -1) {
  529. // 订阅激活:保留积分包数据,重置订阅部分
  530. const existingPackTokens = currentBalance?.packTokens || 0;
  531. const existingUsedPackTokens = currentBalance?.usedPackTokens || 0;
  532. if (currentBalance) {
  533. await prisma.tokenBalance.update({
  534. where: { userId },
  535. data: {
  536. totalTokens: monthlyTokens + existingPackTokens,
  537. usedTokens: existingUsedPackTokens, // 只保留积分包已用
  538. packTokens: existingPackTokens,
  539. usedPackTokens: existingUsedPackTokens,
  540. resetDate: new Date(now.getFullYear(), now.getMonth() + 1, 1)
  541. }
  542. });
  543. } else {
  544. await prisma.tokenBalance.create({
  545. data: {
  546. userId,
  547. totalTokens: monthlyTokens,
  548. usedTokens: 0,
  549. resetDate: new Date(now.getFullYear(), now.getMonth() + 1, 1)
  550. }
  551. });
  552. }
  553. } else {
  554. // VIP无限额度:保留积分包数据
  555. const existingPackTokens = currentBalance?.packTokens || 0;
  556. const existingUsedPackTokens = currentBalance?.usedPackTokens || 0;
  557. if (currentBalance) {
  558. await prisma.tokenBalance.update({
  559. where: { userId },
  560. data: {
  561. totalTokens: -1,
  562. usedTokens: 0,
  563. packTokens: existingPackTokens,
  564. usedPackTokens: existingUsedPackTokens,
  565. resetDate: null
  566. }
  567. });
  568. } else {
  569. await prisma.tokenBalance.create({
  570. data: {
  571. userId,
  572. totalTokens: -1,
  573. usedTokens: 0
  574. }
  575. });
  576. }
  577. }
  578. }
  579. // 模拟支付成功
  580. export async function mockPaymentSuccess(orderNo: string, userId: number) {
  581. return handlePaymentCallback(orderNo, 'MOCK_' + Date.now(), 'success');
  582. }
  583. // 获取订单列表
  584. export async function getOrderList(userId: number, page: number = 1, pageSize: number = 20) {
  585. const where = { userId };
  586. const total = await prisma.order.count({ where });
  587. const list = await prisma.order.findMany({
  588. where,
  589. include: { plan: true },
  590. orderBy: { createdAt: 'desc' },
  591. skip: (page - 1) * pageSize,
  592. take: pageSize
  593. });
  594. return {
  595. list: list.map(order => ({
  596. ...order,
  597. amount: Number(order.amount),
  598. planName: order.plan?.name || ''
  599. })),
  600. total,
  601. page,
  602. pageSize,
  603. totalPages: Math.ceil(total / pageSize)
  604. };
  605. }
  606. // 获取订单详情
  607. export async function getOrderDetail(orderNo: string, userId: number) {
  608. const order = await prisma.order.findFirst({
  609. where: { orderNo, userId },
  610. include: { plan: true }
  611. });
  612. if (!order) {
  613. throw new Error('订单不存在');
  614. }
  615. return {
  616. ...order,
  617. amount: Number(order.amount),
  618. planName: order.plan?.name || ''
  619. };
  620. }
  621. // 查询微信支付订单状态
  622. export async function queryWechatOrder(outTradeNo: string) {
  623. const wechat = getWechatPay();
  624. if (!wechat) {
  625. return null;
  626. }
  627. try {
  628. const result = await wechat.query({ out_trade_no: outTradeNo });
  629. return result;
  630. } catch (error) {
  631. console.error('[WeChatPay] 查询订单失败:', error);
  632. return null;
  633. }
  634. }
  635. // 解密 WeChat V3 回调通知
  636. export function decryptWechatNotify(body: any): any | null {
  637. const wechat = getWechatPay();
  638. if (!wechat) return null;
  639. const { resource } = body || {};
  640. if (!resource || !resource.ciphertext) {
  641. console.error('[WeChatPay] 回调格式无效,缺少 resource.ciphertext');
  642. return null;
  643. }
  644. try {
  645. const decrypted = wechat.decipher_gcm(
  646. resource.ciphertext,
  647. resource.associated_data || '',
  648. resource.nonce,
  649. wechat.key // APIv3 key
  650. );
  651. console.log('[WeChatPay] 回调解密成功:', JSON.stringify(decrypted));
  652. return decrypted;
  653. } catch (e: any) {
  654. console.error('[WeChatPay] 回调解密失败:', e.message);
  655. return null;
  656. }
  657. }
  658. // 查询订单并同步微信支付状态(主动查询 + 自动更新)
  659. export async function getOrderDetailWithSync(orderNo: string, userId: number) {
  660. const order = await prisma.order.findFirst({
  661. where: { orderNo, userId },
  662. include: { plan: true }
  663. });
  664. if (!order) {
  665. throw new Error('订单不存在');
  666. }
  667. // 如果订单是 pending 状态且为微信支付,主动查询微信确认状态
  668. if (order.status === 'pending' && order.paymentMethod === 'wechat') {
  669. console.log('[Payment] 订单 pending,主动查询微信支付状态:', orderNo);
  670. const wechatResult = await queryWechatOrder(orderNo);
  671. if (wechatResult?.data?.trade_state === 'SUCCESS') {
  672. console.log('[Payment] 微信支付已成功,自动更新订单状态:', orderNo);
  673. const transactionId = wechatResult.data.transaction_id || '';
  674. await handlePaymentCallback(orderNo, transactionId, 'success');
  675. // 重新查询更新后的订单
  676. const updatedOrder = await prisma.order.findFirst({
  677. where: { orderNo, userId },
  678. include: { plan: true }
  679. });
  680. if (updatedOrder) {
  681. return {
  682. ...updatedOrder,
  683. amount: Number(updatedOrder.amount),
  684. planName: updatedOrder.plan?.name || ''
  685. };
  686. }
  687. }
  688. }
  689. return {
  690. ...order,
  691. amount: Number(order.amount),
  692. planName: order.plan?.name || ''
  693. };
  694. }
  695. // ==================== 微信 JSAPI 支付(公众号支付) ====================
  696. // Node.js https 简单 GET 请求
  697. function httpsGet(url: string): Promise<any> {
  698. return new Promise((resolve, reject) => {
  699. https.get(url, (res) => {
  700. let data = '';
  701. res.on('data', (chunk) => data += chunk);
  702. res.on('end', () => {
  703. try {
  704. resolve(JSON.parse(data));
  705. } catch (e) {
  706. resolve(data);
  707. }
  708. });
  709. }).on('error', reject);
  710. });
  711. }
  712. // 获取微信公众号 access_token(带缓存)
  713. export async function getWechatAccessToken(): Promise<string | null> {
  714. const now = Date.now();
  715. if (cachedAccessToken && now < accessTokenExpireAt) {
  716. return cachedAccessToken;
  717. }
  718. const appId = process.env.WECHAT_APP_ID;
  719. const appSecret = process.env.WECHAT_APP_SECRET;
  720. if (!appId || !appSecret) {
  721. console.warn('[WeChat JSAPI] 未配置 WECHAT_APP_ID 或 WECHAT_APP_SECRET');
  722. return null;
  723. }
  724. try {
  725. const data = await httpsGet(
  726. `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}`
  727. );
  728. if (data.errcode) {
  729. console.error('[WeChat JSAPI] 获取 access_token 失败:', data);
  730. return null;
  731. }
  732. cachedAccessToken = data.access_token;
  733. // 提前 5 分钟过期
  734. accessTokenExpireAt = now + (data.expires_in - 300) * 1000;
  735. console.log('[WeChat JSAPI] access_token 刷新成功, 过期时间:', new Date(accessTokenExpireAt).toISOString());
  736. return cachedAccessToken;
  737. } catch (e) {
  738. console.error('[WeChat JSAPI] 获取 access_token 异常:', e);
  739. return null;
  740. }
  741. }
  742. // 获取微信 JS-SDK ticket(带缓存)
  743. export async function getWechatJsapiTicket(): Promise<string | null> {
  744. const now = Date.now();
  745. if (cachedJsapiTicket && now < jsapiTicketExpireAt) {
  746. return cachedJsapiTicket;
  747. }
  748. const accessToken = await getWechatAccessToken();
  749. if (!accessToken) return null;
  750. try {
  751. const data = await httpsGet(
  752. `https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${accessToken}&type=jsapi`
  753. );
  754. if (data.errcode !== 0) {
  755. console.error('[WeChat JSAPI] 获取 jsapi_ticket 失败:', data);
  756. return null;
  757. }
  758. cachedJsapiTicket = data.ticket;
  759. // 提前 5 分钟过期
  760. jsapiTicketExpireAt = now + (data.expires_in - 300) * 1000;
  761. console.log('[WeChat JSAPI] jsapi_ticket 刷新成功');
  762. return cachedJsapiTicket;
  763. } catch (e) {
  764. console.error('[WeChat JSAPI] 获取 jsapi_ticket 异常:', e);
  765. return null;
  766. }
  767. }
  768. // 生成微信 JS-SDK 签名配置
  769. export async function getWechatJsapiConfig(url: string): Promise<{
  770. appId: string;
  771. timestamp: string;
  772. nonceStr: string;
  773. signature: string;
  774. } | null> {
  775. const ticket = await getWechatJsapiTicket();
  776. if (!ticket) return null;
  777. const nonceStr = crypto.randomBytes(16).toString('hex');
  778. const timestamp = Math.floor(Date.now() / 1000).toString();
  779. const signStr = `jsapi_ticket=${ticket}&noncestr=${nonceStr}&timestamp=${timestamp}&url=${url}`;
  780. const signature = crypto.createHash('sha1').update(signStr).digest('hex');
  781. return {
  782. appId: process.env.WECHAT_APP_ID || '',
  783. timestamp,
  784. nonceStr,
  785. signature,
  786. };
  787. }
  788. // 通过 OAuth code 获取用户 openid
  789. export async function getWechatOpenid(code: string): Promise<string | null> {
  790. const appId = process.env.WECHAT_APP_ID;
  791. const appSecret = process.env.WECHAT_APP_SECRET;
  792. if (!appId || !appSecret) {
  793. console.warn('[WeChat OAuth] 未配置 WECHAT_APP_ID 或 WECHAT_APP_SECRET');
  794. return null;
  795. }
  796. try {
  797. const data = await httpsGet(
  798. `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appId}&secret=${appSecret}&code=${code}&grant_type=authorization_code`
  799. );
  800. if (data.errcode || !data.openid) {
  801. console.error('[WeChat OAuth] 获取 openid 失败:', data);
  802. return null;
  803. }
  804. console.log('[WeChat OAuth] 获取 openid 成功');
  805. return data.openid;
  806. } catch (e) {
  807. console.error('[WeChat OAuth] 获取 openid 异常:', e);
  808. return null;
  809. }
  810. }
  811. // 生成微信 JSAPI 支付(公众号内支付)
  812. // 注意:SDK的transactions_jsapi已自动处理签名,result.data直接可用于wx.chooseWXPay
  813. export async function generateWechatJsapiPayment(
  814. orderNo: string,
  815. amount: number,
  816. subject: string,
  817. openid: string
  818. ): Promise<{
  819. appId: string;
  820. timeStamp: string;
  821. nonceStr: string;
  822. package: string;
  823. signType: string;
  824. paySign: string;
  825. } | null> {
  826. const wechat = getWechatPay();
  827. if (!wechat) {
  828. console.warn('[WeChat JSAPI] 未配置微信支付');
  829. return null;
  830. }
  831. try {
  832. console.log('[WeChat JSAPI] 开始创建 JSAPI 支付订单:', { orderNo, amount, subject, openid });
  833. // SDK 的 transactions_jsapi 会自动:
  834. // 1. 添加 appid 和 mchid
  835. // 2. 调用 /v3/pay/transactions/jsapi
  836. // 3. 对 prepay_id 进行 RSA 签名
  837. // 4. 返回可直接传给 wx.chooseWXPay 的参数
  838. const result: any = await wechat.transactions_jsapi({
  839. description: subject,
  840. out_trade_no: orderNo,
  841. amount: {
  842. total: Math.round(amount * 100), // 转换为分
  843. currency: 'CNY',
  844. },
  845. payer: {
  846. openid,
  847. },
  848. notify_url: process.env.WECHAT_NOTIFY_URL || `${process.env.BASE_URL || 'https://your-domain.com'}/api/payment/wechat/notify`,
  849. });
  850. console.log('[WeChat JSAPI] 支付接口返回 status:', result.status);
  851. // 检查返回状态
  852. if (result.status !== 200 && result.status !== '200') {
  853. const errorMsg = JSON.stringify(result);
  854. console.error('[WeChat JSAPI] 支付失败:', errorMsg);
  855. throw new Error(`微信支付接口返回错误: ${errorMsg}`);
  856. }
  857. // result.data 已经包含完整的 wx.chooseWXPay 参数:
  858. // { appId, timeStamp, nonceStr, package, signType, paySign }
  859. if (!result.data || !result.data.package) {
  860. console.error('[WeChat JSAPI] 返回数据异常:', JSON.stringify(result));
  861. throw new Error('微信支付返回数据异常,未获取到支付参数');
  862. }
  863. console.log('[WeChat JSAPI] 调起支付参数生成成功');
  864. return result.data;
  865. } catch (error: any) {
  866. const detail = error.message || JSON.stringify(error);
  867. console.error('[WeChat JSAPI] JSAPI支付创建失败:', detail);
  868. throw new Error(`微信支付暂不可用(${detail}),请稍后重试或使用其他支付方式`);
  869. }
  870. }
  871. // 初始化 SDK
  872. initPaymentSdks();