payment.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. /**
  2. * 支付工具函数
  3. * 封装微信支付相关逻辑
  4. */
  5. import Taro from '@tarojs/taro'
  6. /**
  7. * 微信支付参数类型
  8. */
  9. export interface WechatPaymentParams {
  10. timeStamp: string
  11. nonceStr: string
  12. package: string
  13. signType: string
  14. paySign: string
  15. }
  16. /**
  17. * 支付结果类型
  18. */
  19. export interface PaymentResult {
  20. success: boolean
  21. type?: 'success' | 'cancel' | 'fail' | 'error'
  22. message?: string
  23. result?: any
  24. }
  25. /**
  26. * 调用微信支付
  27. * @param paymentData 支付参数
  28. * @returns 支付结果
  29. */
  30. export const requestWechatPayment = async (paymentData: WechatPaymentParams): Promise<PaymentResult> => {
  31. try {
  32. const result = await Taro.requestPayment({
  33. timeStamp: paymentData.timeStamp,
  34. nonceStr: paymentData.nonceStr,
  35. package: paymentData.package,
  36. signType: paymentData.signType as any, // Taro类型定义问题,使用any绕过
  37. paySign: paymentData.paySign
  38. })
  39. return {
  40. success: true,
  41. type: 'success',
  42. result
  43. }
  44. } catch (error: any) {
  45. console.error('微信支付调用失败:', error)
  46. // 根据错误码处理不同场景
  47. if (error.errMsg?.includes('cancel')) {
  48. return {
  49. success: false,
  50. type: 'cancel',
  51. message: '用户取消支付'
  52. }
  53. } else if (error.errMsg?.includes('fail')) {
  54. return {
  55. success: false,
  56. type: 'fail',
  57. message: '支付失败'
  58. }
  59. } else {
  60. return {
  61. success: false,
  62. type: 'error',
  63. message: error.errMsg || '支付异常'
  64. }
  65. }
  66. }
  67. }
  68. /**
  69. * 验证支付参数
  70. * @param paymentData 支付参数
  71. * @returns 验证结果
  72. */
  73. export const validatePaymentParams = (paymentData: WechatPaymentParams): { valid: boolean; errors: string[] } => {
  74. const errors: string[] = []
  75. if (!paymentData.timeStamp) {
  76. errors.push('时间戳不能为空')
  77. }
  78. if (!paymentData.nonceStr) {
  79. errors.push('随机字符串不能为空')
  80. }
  81. if (!paymentData.package) {
  82. errors.push('预支付ID不能为空')
  83. }
  84. if (!paymentData.signType) {
  85. errors.push('签名类型不能为空')
  86. }
  87. if (!paymentData.paySign) {
  88. errors.push('签名不能为空')
  89. }
  90. return {
  91. valid: errors.length === 0,
  92. errors
  93. }
  94. }
  95. /**
  96. * 处理支付金额
  97. * @param amount 金额(元)
  98. * @returns 金额(分)
  99. */
  100. export const formatPaymentAmount = (amount: number): number => {
  101. // 微信支付金额单位为分,需要乘以100
  102. return Math.round(amount * 100)
  103. }
  104. /**
  105. * 检查支付环境
  106. * @returns 是否支持微信支付
  107. */
  108. export const checkPaymentEnvironment = async (): Promise<boolean> => {
  109. try {
  110. // 检查是否在微信小程序环境中
  111. if (typeof Taro.requestPayment === 'undefined') {
  112. console.error('当前环境不支持微信支付')
  113. return false
  114. }
  115. // 可以添加更多环境检查逻辑
  116. return true
  117. } catch (error) {
  118. console.error('检查支付环境失败:', error)
  119. return false
  120. }
  121. }
  122. /**
  123. * 支付安全验证
  124. * @param orderId 订单ID
  125. * @param amount 支付金额
  126. * @param paymentParams 支付参数
  127. * @returns 验证结果
  128. */
  129. export const validatePaymentSecurity = (
  130. orderId: number,
  131. amount: number,
  132. paymentParams: WechatPaymentParams
  133. ): { valid: boolean; reason?: string } => {
  134. // 验证订单ID
  135. if (!orderId || orderId <= 0) {
  136. return { valid: false, reason: '订单ID无效' }
  137. }
  138. // 验证金额
  139. if (!amount || amount <= 0) {
  140. return { valid: false, reason: '支付金额无效' }
  141. }
  142. // 验证支付参数
  143. const paramValidation = validatePaymentParams(paymentParams)
  144. if (!paramValidation.valid) {
  145. return {
  146. valid: false,
  147. reason: `支付参数错误: ${paramValidation.errors.join(', ')}`
  148. }
  149. }
  150. // 时间戳验证(防止重放攻击)
  151. const timestamp = parseInt(paymentParams.timeStamp)
  152. const currentTime = Math.floor(Date.now() / 1000)
  153. const timeDiff = Math.abs(currentTime - timestamp)
  154. // 时间戳应该在5分钟内有效
  155. if (timeDiff > 300) {
  156. return { valid: false, reason: '支付参数已过期,请重新发起支付' }
  157. }
  158. // 随机字符串长度验证
  159. if (paymentParams.nonceStr.length < 16 || paymentParams.nonceStr.length > 32) {
  160. return { valid: false, reason: '随机字符串长度无效' }
  161. }
  162. // 签名类型验证
  163. if (paymentParams.signType !== 'RSA' && paymentParams.signType !== 'HMAC-SHA256') {
  164. return { valid: false, reason: '签名类型不支持' }
  165. }
  166. // 预支付ID格式验证
  167. if (!paymentParams.package.startsWith('prepay_id=')) {
  168. return { valid: false, reason: '预支付ID格式错误' }
  169. }
  170. // 签名长度验证
  171. if (paymentParams.paySign.length < 32) {
  172. return { valid: false, reason: '签名长度过短' }
  173. }
  174. return { valid: true }
  175. }
  176. /**
  177. * 生成支付参数哈希(用于防篡改验证)
  178. * @param paymentParams 支付参数
  179. * @returns 参数哈希
  180. */
  181. export const generatePaymentParamsHash = (paymentParams: WechatPaymentParams): string => {
  182. const paramsString = [
  183. paymentParams.timeStamp,
  184. paymentParams.nonceStr,
  185. paymentParams.package,
  186. paymentParams.signType
  187. ].join('&')
  188. // 在实际项目中,这里应该使用更安全的哈希算法
  189. // 这里使用简单的哈希作为示例
  190. let hash = 0
  191. for (let i = 0; i < paramsString.length; i++) {
  192. const char = paramsString.charCodeAt(i)
  193. hash = ((hash << 5) - hash) + char
  194. hash = hash & hash // 转换为32位整数
  195. }
  196. return Math.abs(hash).toString(16)
  197. }
  198. /**
  199. * 验证支付参数完整性
  200. * @param originalParams 原始支付参数
  201. * @param receivedParams 接收到的支付参数
  202. * @returns 验证结果
  203. */
  204. export const verifyPaymentParamsIntegrity = (
  205. originalParams: WechatPaymentParams,
  206. receivedParams: WechatPaymentParams
  207. ): { valid: boolean; reason?: string } => {
  208. const originalHash = generatePaymentParamsHash(originalParams)
  209. const receivedHash = generatePaymentParamsHash(receivedParams)
  210. if (originalHash !== receivedHash) {
  211. return { valid: false, reason: '支付参数被篡改' }
  212. }
  213. return { valid: true }
  214. }
  215. /**
  216. * 支付金额一致性验证
  217. * @param expectedAmount 预期金额(元)
  218. * @param paymentAmount 支付金额(分)
  219. * @returns 验证结果
  220. */
  221. export const validateAmountConsistency = (
  222. expectedAmount: number,
  223. paymentAmount: number
  224. ): { valid: boolean; reason?: string } => {
  225. const expectedInFen = Math.round(expectedAmount * 100)
  226. if (expectedInFen !== paymentAmount) {
  227. return {
  228. valid: false,
  229. reason: `金额不一致: 预期 ${expectedInFen} 分,实际 ${paymentAmount} 分`
  230. }
  231. }
  232. return { valid: true }
  233. }
  234. /**
  235. * 支付频率限制检查
  236. */
  237. export class PaymentRateLimiter {
  238. private static instance: PaymentRateLimiter
  239. private attempts: Map<number, number[]> = new Map()
  240. private readonly MAX_ATTEMPTS = 5
  241. private readonly TIME_WINDOW = 60000 // 1分钟
  242. private constructor() {}
  243. static getInstance(): PaymentRateLimiter {
  244. if (!PaymentRateLimiter.instance) {
  245. PaymentRateLimiter.instance = new PaymentRateLimiter()
  246. }
  247. return PaymentRateLimiter.instance
  248. }
  249. /**
  250. * 检查是否超过支付频率限制
  251. */
  252. isRateLimited(orderId: number): { limited: boolean; remainingTime?: number } {
  253. const now = Date.now()
  254. const attempts = this.attempts.get(orderId) || []
  255. // 清理过期的尝试记录
  256. const recentAttempts = attempts.filter(time => now - time < this.TIME_WINDOW)
  257. this.attempts.set(orderId, recentAttempts)
  258. if (recentAttempts.length >= this.MAX_ATTEMPTS) {
  259. const oldestAttempt = Math.min(...recentAttempts)
  260. const remainingTime = this.TIME_WINDOW - (now - oldestAttempt)
  261. return { limited: true, remainingTime }
  262. }
  263. return { limited: false }
  264. }
  265. /**
  266. * 记录支付尝试
  267. */
  268. recordAttempt(orderId: number): void {
  269. const attempts = this.attempts.get(orderId) || []
  270. attempts.push(Date.now())
  271. this.attempts.set(orderId, attempts)
  272. }
  273. /**
  274. * 清除支付尝试记录
  275. */
  276. clearAttempts(orderId: number): void {
  277. this.attempts.delete(orderId)
  278. }
  279. }
  280. /**
  281. * 支付重试逻辑
  282. * @param paymentFn 支付函数
  283. * @param maxRetries 最大重试次数
  284. * @param delay 重试延迟(毫秒)
  285. * @returns 支付结果
  286. */
  287. export const retryPayment = async (
  288. paymentFn: () => Promise<PaymentResult>,
  289. maxRetries: number = 3,
  290. delay: number = 1000
  291. ): Promise<PaymentResult> => {
  292. let lastError: any = null
  293. for (let attempt = 1; attempt <= maxRetries; attempt++) {
  294. try {
  295. const result = await paymentFn()
  296. if (result.success) {
  297. return result
  298. }
  299. // 如果是用户取消,不重试
  300. if (result.type === 'cancel') {
  301. return result
  302. }
  303. lastError = result
  304. if (attempt < maxRetries) {
  305. console.log(`支付失败,第${attempt}次重试...`)
  306. await new Promise(resolve => setTimeout(resolve, delay))
  307. }
  308. } catch (error) {
  309. lastError = error
  310. if (attempt < maxRetries) {
  311. console.log(`支付异常,第${attempt}次重试...`)
  312. await new Promise(resolve => setTimeout(resolve, delay))
  313. }
  314. }
  315. }
  316. return {
  317. success: false,
  318. type: 'error',
  319. message: `支付失败,已重试${maxRetries}次: ${lastError?.message || '未知错误'}`
  320. }
  321. }
  322. /**
  323. * 支付状态枚举
  324. */
  325. export enum PaymentStatus {
  326. PENDING = '待支付',
  327. PROCESSING = '支付中',
  328. SUCCESS = '支付成功',
  329. FAILED = '支付失败',
  330. REFUNDED = '已退款',
  331. CLOSED = '已关闭'
  332. }
  333. /**
  334. * 支付状态管理类
  335. */
  336. export class PaymentStateManager {
  337. private static instance: PaymentStateManager
  338. private state: Map<number, PaymentStatus> = new Map()
  339. private constructor() {}
  340. static getInstance(): PaymentStateManager {
  341. if (!PaymentStateManager.instance) {
  342. PaymentStateManager.instance = new PaymentStateManager()
  343. }
  344. return PaymentStateManager.instance
  345. }
  346. /**
  347. * 设置支付状态
  348. */
  349. setPaymentState(orderId: number, status: PaymentStatus): void {
  350. this.state.set(orderId, status)
  351. console.log(`订单 ${orderId} 支付状态更新为: ${status}`)
  352. }
  353. /**
  354. * 获取支付状态
  355. */
  356. getPaymentState(orderId: number): PaymentStatus | undefined {
  357. return this.state.get(orderId)
  358. }
  359. /**
  360. * 检查是否重复支付
  361. */
  362. isDuplicatePayment(orderId: number): boolean {
  363. const currentStatus = this.getPaymentState(orderId)
  364. return currentStatus === PaymentStatus.PROCESSING || currentStatus === PaymentStatus.SUCCESS
  365. }
  366. /**
  367. * 清除支付状态
  368. */
  369. clearPaymentState(orderId: number): void {
  370. this.state.delete(orderId)
  371. }
  372. /**
  373. * 获取所有支付状态
  374. */
  375. getAllPaymentStates(): Map<number, PaymentStatus> {
  376. return new Map(this.state)
  377. }
  378. }
  379. /**
  380. * 支付超时处理
  381. * @param orderId 订单ID
  382. * @param timeout 超时时间(毫秒)
  383. * @returns 超时Promise
  384. */
  385. export const createPaymentTimeout = (timeout: number = 30000): Promise<PaymentResult> => {
  386. return new Promise((resolve) => {
  387. setTimeout(() => {
  388. resolve({
  389. success: false,
  390. type: 'error',
  391. message: '支付超时,请检查网络或重试'
  392. })
  393. }, timeout)
  394. })
  395. }
  396. /**
  397. * 支付状态同步
  398. * @param orderId 订单ID
  399. * @param expectedStatus 期望状态
  400. * @param maxAttempts 最大尝试次数
  401. * @param interval 检查间隔(毫秒)
  402. * @returns 同步结果
  403. */
  404. export const syncPaymentStatus = async (
  405. orderId: number,
  406. expectedStatus: PaymentStatus,
  407. maxAttempts: number = 10,
  408. interval: number = 2000
  409. ): Promise<{ synced: boolean; currentStatus?: PaymentStatus }> => {
  410. for (let attempt = 1; attempt <= maxAttempts; attempt++) {
  411. try {
  412. // 这里可以调用后端API查询实际支付状态
  413. // 暂时使用状态管理器模拟
  414. const stateManager = PaymentStateManager.getInstance()
  415. const currentStatus = stateManager.getPaymentState(orderId)
  416. if (currentStatus === expectedStatus) {
  417. return { synced: true, currentStatus }
  418. }
  419. console.log(`支付状态同步中... 第${attempt}次检查,当前状态: ${currentStatus}`)
  420. if (attempt < maxAttempts) {
  421. await new Promise(resolve => setTimeout(resolve, interval))
  422. }
  423. } catch (error) {
  424. console.error(`支付状态同步失败,第${attempt}次尝试:`, error)
  425. }
  426. }
  427. return { synced: false }
  428. }