@vobs/payment 0.3.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +182 -0
  3. package/dist/alipay/client.cjs +112 -0
  4. package/dist/alipay/client.cjs.map +1 -0
  5. package/dist/alipay/client.d.cts +36 -0
  6. package/dist/alipay/client.d.ts +36 -0
  7. package/dist/alipay/client.js +110 -0
  8. package/dist/alipay/client.js.map +1 -0
  9. package/dist/alipay/config.cjs +67 -0
  10. package/dist/alipay/config.cjs.map +1 -0
  11. package/dist/alipay/config.d.cts +30 -0
  12. package/dist/alipay/config.d.ts +30 -0
  13. package/dist/alipay/config.js +65 -0
  14. package/dist/alipay/config.js.map +1 -0
  15. package/dist/alipay/index.cjs +592 -0
  16. package/dist/alipay/index.cjs.map +1 -0
  17. package/dist/alipay/index.d.cts +12 -0
  18. package/dist/alipay/index.d.ts +12 -0
  19. package/dist/alipay/index.js +580 -0
  20. package/dist/alipay/index.js.map +1 -0
  21. package/dist/alipay/notify.cjs +70 -0
  22. package/dist/alipay/notify.cjs.map +1 -0
  23. package/dist/alipay/notify.d.cts +52 -0
  24. package/dist/alipay/notify.d.ts +52 -0
  25. package/dist/alipay/notify.js +68 -0
  26. package/dist/alipay/notify.js.map +1 -0
  27. package/dist/alipay/payment.cjs +49 -0
  28. package/dist/alipay/payment.cjs.map +1 -0
  29. package/dist/alipay/payment.d.cts +24 -0
  30. package/dist/alipay/payment.d.ts +24 -0
  31. package/dist/alipay/payment.js +47 -0
  32. package/dist/alipay/payment.js.map +1 -0
  33. package/dist/alipay/plugin.cjs +300 -0
  34. package/dist/alipay/plugin.cjs.map +1 -0
  35. package/dist/alipay/plugin.d.cts +43 -0
  36. package/dist/alipay/plugin.d.ts +43 -0
  37. package/dist/alipay/plugin.js +296 -0
  38. package/dist/alipay/plugin.js.map +1 -0
  39. package/dist/alipay/query.cjs +80 -0
  40. package/dist/alipay/query.cjs.map +1 -0
  41. package/dist/alipay/query.d.cts +26 -0
  42. package/dist/alipay/query.d.ts +26 -0
  43. package/dist/alipay/query.js +78 -0
  44. package/dist/alipay/query.js.map +1 -0
  45. package/dist/alipay/refund.cjs +104 -0
  46. package/dist/alipay/refund.cjs.map +1 -0
  47. package/dist/alipay/refund.d.cts +32 -0
  48. package/dist/alipay/refund.d.ts +32 -0
  49. package/dist/alipay/refund.js +102 -0
  50. package/dist/alipay/refund.js.map +1 -0
  51. package/dist/alipay/response.cjs +38 -0
  52. package/dist/alipay/response.cjs.map +1 -0
  53. package/dist/alipay/response.d.cts +19 -0
  54. package/dist/alipay/response.d.ts +19 -0
  55. package/dist/alipay/response.js +35 -0
  56. package/dist/alipay/response.js.map +1 -0
  57. package/dist/alipay/types.cjs +4 -0
  58. package/dist/alipay/types.cjs.map +1 -0
  59. package/dist/alipay/types.d.cts +242 -0
  60. package/dist/alipay/types.d.ts +242 -0
  61. package/dist/alipay/types.js +3 -0
  62. package/dist/alipay/types.js.map +1 -0
  63. package/dist/alipay/wap.cjs +50 -0
  64. package/dist/alipay/wap.cjs.map +1 -0
  65. package/dist/alipay/wap.d.cts +24 -0
  66. package/dist/alipay/wap.d.ts +24 -0
  67. package/dist/alipay/wap.js +48 -0
  68. package/dist/alipay/wap.js.map +1 -0
  69. package/dist/index-B1aUR7Vt.d.ts +49 -0
  70. package/dist/index-B8pzZxGq.d.cts +44 -0
  71. package/dist/index-BWnIrwRi.d.ts +44 -0
  72. package/dist/index-Dyrkl86r.d.cts +49 -0
  73. package/dist/index.cjs +1071 -0
  74. package/dist/index.cjs.map +1 -0
  75. package/dist/index.d.cts +23 -0
  76. package/dist/index.d.ts +23 -37
  77. package/dist/index.js +1052 -37
  78. package/dist/index.js.map +1 -0
  79. package/dist/wechat/client.cjs +119 -0
  80. package/dist/wechat/client.cjs.map +1 -0
  81. package/dist/wechat/client.d.cts +39 -0
  82. package/dist/wechat/client.d.ts +39 -0
  83. package/dist/wechat/client.js +117 -0
  84. package/dist/wechat/client.js.map +1 -0
  85. package/dist/wechat/config.cjs +76 -0
  86. package/dist/wechat/config.cjs.map +1 -0
  87. package/dist/wechat/config.d.cts +29 -0
  88. package/dist/wechat/config.d.ts +29 -0
  89. package/dist/wechat/config.js +74 -0
  90. package/dist/wechat/config.js.map +1 -0
  91. package/dist/wechat/index.cjs +612 -0
  92. package/dist/wechat/index.cjs.map +1 -0
  93. package/dist/wechat/index.d.cts +10 -0
  94. package/dist/wechat/index.d.ts +10 -0
  95. package/dist/wechat/index.js +602 -0
  96. package/dist/wechat/index.js.map +1 -0
  97. package/dist/wechat/notify.cjs +73 -0
  98. package/dist/wechat/notify.cjs.map +1 -0
  99. package/dist/wechat/notify.d.cts +53 -0
  100. package/dist/wechat/notify.d.ts +53 -0
  101. package/dist/wechat/notify.js +71 -0
  102. package/dist/wechat/notify.js.map +1 -0
  103. package/dist/wechat/payment.cjs +186 -0
  104. package/dist/wechat/payment.cjs.map +1 -0
  105. package/dist/wechat/payment.d.cts +50 -0
  106. package/dist/wechat/payment.d.ts +50 -0
  107. package/dist/wechat/payment.js +184 -0
  108. package/dist/wechat/payment.js.map +1 -0
  109. package/dist/wechat/plugin.cjs +305 -0
  110. package/dist/wechat/plugin.cjs.map +1 -0
  111. package/dist/wechat/plugin.d.cts +40 -0
  112. package/dist/wechat/plugin.d.ts +40 -0
  113. package/dist/wechat/plugin.js +301 -0
  114. package/dist/wechat/plugin.js.map +1 -0
  115. package/dist/wechat/refund.cjs +112 -0
  116. package/dist/wechat/refund.cjs.map +1 -0
  117. package/dist/wechat/refund.d.cts +26 -0
  118. package/dist/wechat/refund.d.ts +26 -0
  119. package/dist/wechat/refund.js +110 -0
  120. package/dist/wechat/refund.js.map +1 -0
  121. package/dist/wechat/response.cjs +50 -0
  122. package/dist/wechat/response.cjs.map +1 -0
  123. package/dist/wechat/response.d.cts +20 -0
  124. package/dist/wechat/response.d.ts +20 -0
  125. package/dist/wechat/response.js +46 -0
  126. package/dist/wechat/response.js.map +1 -0
  127. package/dist/wechat/types.cjs +4 -0
  128. package/dist/wechat/types.cjs.map +1 -0
  129. package/dist/wechat/types.d.cts +200 -0
  130. package/dist/wechat/types.d.ts +200 -0
  131. package/dist/wechat/types.js +3 -0
  132. package/dist/wechat/types.js.map +1 -0
  133. package/package.json +37 -28
  134. package/src/alipay/alipay.test.ts +231 -0
  135. package/src/alipay/client.ts +67 -0
  136. package/src/alipay/config.ts +72 -0
  137. package/src/alipay/index.ts +32 -0
  138. package/src/alipay/notify.ts +92 -0
  139. package/src/alipay/payment.ts +60 -0
  140. package/src/alipay/plugin.ts +64 -0
  141. package/src/alipay/query.ts +52 -0
  142. package/src/alipay/refund.ts +85 -0
  143. package/src/alipay/response.ts +40 -0
  144. package/src/alipay/types.ts +271 -0
  145. package/src/alipay/wap.ts +55 -0
  146. package/src/index.ts +78 -0
  147. package/src/wechat/client.ts +68 -0
  148. package/src/wechat/config.ts +81 -0
  149. package/src/wechat/index.ts +30 -0
  150. package/src/wechat/notify.ts +93 -0
  151. package/src/wechat/payment.ts +177 -0
  152. package/src/wechat/plugin.ts +59 -0
  153. package/src/wechat/refund.ts +80 -0
  154. package/src/wechat/response.ts +54 -0
  155. package/src/wechat/types.ts +223 -0
  156. package/src/wechat/wechat.test.ts +393 -0
  157. package/dist/alipay-official.d.ts +0 -34
  158. package/dist/alipay-official.js +0 -206
  159. package/dist/handlers.d.ts +0 -45
  160. package/dist/handlers.js +0 -105
  161. package/dist/jeepay.d.ts +0 -39
  162. package/dist/jeepay.js +0 -206
  163. package/dist/lakala.d.ts +0 -41
  164. package/dist/lakala.js +0 -243
  165. package/dist/manager.d.ts +0 -67
  166. package/dist/manager.js +0 -538
  167. package/dist/mock-driver.d.ts +0 -22
  168. package/dist/mock-driver.js +0 -59
  169. package/dist/state-machine.d.ts +0 -41
  170. package/dist/state-machine.js +0 -60
  171. package/dist/store.d.ts +0 -33
  172. package/dist/store.js +0 -76
  173. package/dist/types.d.ts +0 -168
  174. package/dist/types.js +0 -11
  175. package/dist/verify.d.ts +0 -34
  176. package/dist/verify.js +0 -55
  177. package/dist/wechat-official.d.ts +0 -40
  178. package/dist/wechat-official.js +0 -208
@@ -0,0 +1,231 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { AlipayConfig } from './config'
3
+ import { AlipayApiError, unwrapResponse } from './response'
4
+ import { createNotifyHandler } from './notify'
5
+ import { createQuery } from './query'
6
+ import { createRefund } from './refund'
7
+ import { createWapPay } from './wap'
8
+ import { alipayPlugin, ALIPAY_KEY } from './plugin'
9
+ import type { AlipayClient } from './client'
10
+ import type { AlipayClientOptions } from './types'
11
+ import type { VobsContext } from '@vobs/vobs'
12
+
13
+ function makeClient(sdkOverrides: Partial<AlipayClient['sdk']> = {}): AlipayClient {
14
+ const sdk = {
15
+ exec: vi.fn(),
16
+ checkNotifySign: vi.fn(),
17
+ pageExecute: vi.fn(),
18
+ ...sdkOverrides
19
+ }
20
+ const config = new AlipayConfig({
21
+ appId: '2021000000000000',
22
+ privateKey: 'test-private-key',
23
+ alipayPublicKey: 'test-public-key'
24
+ })
25
+ return { config, sdk } as unknown as AlipayClient
26
+ }
27
+
28
+ const baseNotify = {
29
+ appId: '2021000000000000',
30
+ outTradeNo: 'ORDER-1',
31
+ totalAmount: '299.00',
32
+ tradeStatus: 'TRADE_SUCCESS'
33
+ }
34
+
35
+ describe('alipay response unwrapping', () => {
36
+ it('unwrapResponse 业务失败时抛出 AlipayApiError 并携带 sub_code', () => {
37
+ try {
38
+ unwrapResponse({
39
+ alipay_trade_query_response: {
40
+ code: '40004',
41
+ msg: 'Business Failed',
42
+ sub_code: 'ACQ.TRADE_NOT_EXIST',
43
+ sub_msg: '交易不存在'
44
+ }
45
+ }, 'alipay.trade.query')
46
+ expect.unreachable()
47
+ } catch (error) {
48
+ expect(error).toBeInstanceOf(AlipayApiError)
49
+ const apiError = error as AlipayApiError
50
+ expect(apiError.code).toBe('40004')
51
+ expect(apiError.subCode).toBe('ACQ.TRADE_NOT_EXIST')
52
+ expect(apiError.message).toContain('ACQ.TRADE_NOT_EXIST')
53
+ }
54
+ })
55
+
56
+ it('unwrapResponse 成功响应返回业务体', () => {
57
+ const body = { code: '10000', msg: 'Success', trade_no: 'T-1' }
58
+ expect(unwrapResponse({ alipay_trade_query_response: body }, 'alipay.trade.query')).toBe(body)
59
+ })
60
+ })
61
+
62
+ describe('alipay query', () => {
63
+ it('query 成功时映射 snake_case 响应字段', async () => {
64
+ const client = makeClient()
65
+ ;(client.sdk.exec as ReturnType<typeof vi.fn>).mockResolvedValue({
66
+ alipay_trade_query_response: {
67
+ code: '10000',
68
+ msg: 'Success',
69
+ trade_no: 'T-1',
70
+ out_trade_no: 'ORDER-1',
71
+ buyer_logon_id: '138****8000',
72
+ trade_status: 'TRADE_SUCCESS',
73
+ total_amount: '299.00',
74
+ receipt_amount: '299.00',
75
+ buyer_pay_amount: '289.00'
76
+ }
77
+ })
78
+
79
+ const result = await createQuery(client).query({ outTradeNo: 'ORDER-1' })
80
+ expect(result.tradeStatus).toBe('TRADE_SUCCESS')
81
+ expect(result.totalAmount).toBe('299.00')
82
+ expect(result.buyerPayAmount).toBe('289.00')
83
+ expect(result.sendPayDate).toBe('')
84
+ expect(result.gmtClose).toBeNull()
85
+ })
86
+
87
+ it('query 业务失败时抛出 AlipayApiError,不返回伪成功结果', async () => {
88
+ const client = makeClient()
89
+ ;(client.sdk.exec as ReturnType<typeof vi.fn>).mockResolvedValue({
90
+ alipay_trade_query_response: {
91
+ code: '40004',
92
+ sub_code: 'ACQ.TRADE_NOT_EXIST',
93
+ sub_msg: '交易不存在'
94
+ }
95
+ })
96
+
97
+ await expect(createQuery(client).query({ outTradeNo: 'ORDER-404' }))
98
+ .rejects.toThrow('ACQ.TRADE_NOT_EXIST')
99
+ })
100
+ })
101
+
102
+ describe('alipay refund', () => {
103
+ it('refund 成功时映射 refund_fee 并提供默认值', async () => {
104
+ const client = makeClient()
105
+ ;(client.sdk.exec as ReturnType<typeof vi.fn>).mockResolvedValue({
106
+ alipay_trade_refund_response: {
107
+ code: '10000',
108
+ trade_no: 'T-1',
109
+ out_trade_no: 'ORDER-1',
110
+ refund_fee: '10.00',
111
+ fund_change: 'Y'
112
+ }
113
+ })
114
+
115
+ const result = await createRefund(client).refund({ outTradeNo: 'ORDER-1', refundAmount: '10.00' })
116
+ expect(result.refundAmount).toBe('10.00')
117
+ expect(result.fundChange).toBe('Y')
118
+ expect(result.gmtRefundPay).toBe('')
119
+ expect(result.refundDetailItemList).toBeNull()
120
+ })
121
+
122
+ it('close 业务失败时抛出 AlipayApiError', async () => {
123
+ const client = makeClient()
124
+ ;(client.sdk.exec as ReturnType<typeof vi.fn>).mockResolvedValue({
125
+ alipay_trade_close_response: {
126
+ code: '40004',
127
+ sub_code: 'ACQ.TRADE_STATUS_ERROR',
128
+ sub_msg: '交易状态不合法'
129
+ }
130
+ })
131
+
132
+ await expect(createRefund(client).close({ outTradeNo: 'ORDER-1' }))
133
+ .rejects.toBeInstanceOf(AlipayApiError)
134
+ })
135
+ })
136
+
137
+ describe('alipay notify validation', () => {
138
+ it('app_id 与配置一致且无期望值时通过', () => {
139
+ const handler = createNotifyHandler(makeClient())
140
+ expect(handler.validate(baseNotify as never).valid).toBe(true)
141
+ })
142
+
143
+ it('app_id 不匹配时报告错误', () => {
144
+ const handler = createNotifyHandler(makeClient())
145
+ const result = handler.validate({ ...baseNotify, appId: 'other-app' } as never)
146
+ expect(result.valid).toBe(false)
147
+ expect(result.errors[0]).toContain('app_id')
148
+ })
149
+
150
+ it('订单号与金额期望值不一致时报告错误', () => {
151
+ const handler = createNotifyHandler(makeClient())
152
+ const result = handler.validate(baseNotify as never, { outTradeNo: 'ORDER-2', totalAmount: '300.00' })
153
+ expect(result.valid).toBe(false)
154
+ expect(result.errors.some(error => error.includes('out_trade_no'))).toBe(true)
155
+ expect(result.errors.some(error => error.includes('total_amount'))).toBe(true)
156
+ })
157
+
158
+ it('金额按数值比较,"299.0" 与 "299.00" 视为一致', () => {
159
+ const handler = createNotifyHandler(makeClient())
160
+ const result = handler.validate({ ...baseNotify, totalAmount: '299.0' } as never, { totalAmount: '299.00' })
161
+ expect(result.valid).toBe(true)
162
+ })
163
+
164
+ it('非终态 trade_status 报告错误', () => {
165
+ const handler = createNotifyHandler(makeClient())
166
+ const result = handler.validate({ ...baseNotify, tradeStatus: 'WAIT_BUYER_PAY' } as never)
167
+ expect(result.valid).toBe(false)
168
+ expect(result.errors[0]).toContain('trade_status')
169
+ })
170
+
171
+ it('verify 透传 raw 参数,SDK 异常时返回 false', () => {
172
+ const client = makeClient()
173
+ ;(client.sdk.checkNotifySign as ReturnType<typeof vi.fn>).mockImplementation((_params: unknown, raw?: boolean) => {
174
+ expect(raw).toBe(true)
175
+ return true
176
+ })
177
+ const handler = createNotifyHandler(client)
178
+ expect(handler.verify({ a: '1' }, true)).toBe(true)
179
+
180
+ const broken = makeClient()
181
+ ;(broken.sdk.checkNotifySign as ReturnType<typeof vi.fn>).mockImplementation(() => {
182
+ throw new Error('bad key')
183
+ })
184
+ expect(createNotifyHandler(broken).verify({ a: '1' })).toBe(false)
185
+ })
186
+ })
187
+
188
+ describe('alipay plugin lifecycle', () => {
189
+ it('安装自建客户端并在卸载时销毁', () => {
190
+ const provide = vi.fn()
191
+ const plugin = alipayPlugin({
192
+ config: { appId: '2021000000000000', privateKey: 'k', alipayPublicKey: 'p' } as AlipayClientOptions
193
+ })
194
+ const cleanup = plugin.install?.({ provide } as unknown as VobsContext)
195
+ const client = provide.mock.calls[0]![1] as AlipayClient
196
+ expect(provide).toHaveBeenCalledWith(ALIPAY_KEY, client)
197
+ expect(typeof cleanup).toBe('function')
198
+ cleanup!()
199
+ expect(() => client.sdk).toThrow('客户端已销毁')
200
+ })
201
+
202
+ it('外部传入的客户端在卸载时不被销毁', () => {
203
+ const client = makeClient()
204
+ const provide = vi.fn()
205
+ const plugin = alipayPlugin({ client })
206
+ const cleanup = plugin.install?.({ provide } as unknown as VobsContext)
207
+ expect(provide).toHaveBeenCalledWith(ALIPAY_KEY, client)
208
+ expect(cleanup).toBeUndefined()
209
+ expect(() => client.sdk).not.toThrow()
210
+ })
211
+ })
212
+
213
+ describe('alipay wap pay', () => {
214
+ it('wap.pay 使用 QUICK_WAP_WAY 产品码并透传 quit_url', async () => {
215
+ const client = makeClient()
216
+ ;(client.sdk.pageExecute as ReturnType<typeof vi.fn>).mockReturnValue('<form name="pay">wap</form>')
217
+
218
+ const result = await createWapPay(client).pay({
219
+ outTradeNo: 'ORDER-9',
220
+ totalAmount: '1.00',
221
+ subject: 'H5 会员',
222
+ quitUrl: 'https://merchant.example.com/quit'
223
+ })
224
+
225
+ expect(result.formHtml).toBe('<form name="pay">wap</form>')
226
+ const [apiName, , payload] = (client.sdk.pageExecute as ReturnType<typeof vi.fn>).mock.calls[0] as unknown as [string, string, Record<string, any>]
227
+ expect(apiName).toBe('alipay.trade.wap.pay')
228
+ expect(payload.bizContent.productCode).toBe('QUICK_WAP_WAY')
229
+ expect(payload.bizContent.quitUrl).toBe('https://merchant.example.com/quit')
230
+ })
231
+ })
@@ -0,0 +1,67 @@
1
+ import { AlipaySdk } from 'alipay-sdk'
2
+ import { AlipayConfig } from './config'
3
+ import type { AlipayClientOptions, AlipayKeyType } from './types'
4
+
5
+ /**
6
+ * 支付宝客户端封装
7
+ *
8
+ * 封装 alipay-sdk 的 AlipaySdk 实例,提供统一的生命周期管理。
9
+ * 可独立使用,也可通过 alipayPlugin 注册到 vobs 框架。
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const client = new AlipayClient({
14
+ * appId: '202100...',
15
+ * privateKey: '...',
16
+ * alipayPublicKey: '...',
17
+ * gateway: 'https://openapi-sandbox.dl.alipaydev.com/gateway.do'
18
+ * })
19
+ * ```
20
+ */
21
+ export class AlipayClient {
22
+ readonly config: AlipayConfig
23
+ private _sdk: AlipaySdk
24
+ private _disposed = false
25
+
26
+ constructor(options: AlipayClientOptions) {
27
+ this.config = new AlipayConfig(options)
28
+ this._sdk = this.createSdk(options)
29
+ }
30
+
31
+ /** 获取底层 AlipaySdk 实例 */
32
+ get sdk(): AlipaySdk {
33
+ this.ensureActive()
34
+ return this._sdk
35
+ }
36
+
37
+ /** 更新配置并重建 SDK 实例 */
38
+ updateConfig(patch: Partial<AlipayClientOptions>): void {
39
+ this.ensureActive()
40
+ this.config.update(patch)
41
+ this._sdk = this.createSdk(this.config.options as AlipayClientOptions)
42
+ }
43
+
44
+ /** 销毁客户端 */
45
+ dispose(): void {
46
+ this._disposed = true
47
+ ;(this._sdk as any) = null
48
+ }
49
+
50
+ private createSdk(options: AlipayClientOptions): AlipaySdk {
51
+ return new AlipaySdk({
52
+ appId: options.appId,
53
+ privateKey: options.privateKey,
54
+ alipayPublicKey: options.alipayPublicKey,
55
+ signType: options.signType ?? 'RSA2',
56
+ gateway: options.gateway,
57
+ timeout: options.timeout ?? 5000,
58
+ keyType: (options.keyType ?? 'PKCS1') as AlipayKeyType
59
+ })
60
+ }
61
+
62
+ private ensureActive(): void {
63
+ if (this._disposed) {
64
+ throw new Error('AlipayClient: 客户端已销毁')
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,72 @@
1
+ import type { AlipayClientOptions } from './types'
2
+
3
+ /**
4
+ * 支付宝配置管理
5
+ * 遵循 vobs 生命周期管理:可创建、可销毁、可替换
6
+ */
7
+ export class AlipayConfig {
8
+ private _options: AlipayClientOptions
9
+
10
+ constructor(options: AlipayClientOptions) {
11
+ this.validate(options)
12
+ this._options = { ...options }
13
+ }
14
+
15
+ /** 获取完整配置 */
16
+ get options(): Readonly<AlipayClientOptions> {
17
+ return this._options
18
+ }
19
+
20
+ /** 应用 ID */
21
+ get appId(): string {
22
+ return this._options.appId
23
+ }
24
+
25
+ /** 网关地址 */
26
+ get gateway(): string {
27
+ return this._options.gateway ?? 'https://openapi.alipay.com/gateway.do'
28
+ }
29
+
30
+ /** 异步通知地址 */
31
+ get notifyUrl(): string | undefined {
32
+ return this._options.notifyUrl
33
+ }
34
+
35
+ /** 同步跳转地址 */
36
+ get returnUrl(): string | undefined {
37
+ return this._options.returnUrl
38
+ }
39
+
40
+ /** 更新配置(部分更新) */
41
+ update(patch: Partial<AlipayClientOptions>): void {
42
+ const merged = { ...this._options, ...patch }
43
+ this.validate(merged)
44
+ this._options = merged
45
+ }
46
+
47
+ /** 替换配置 */
48
+ replace(options: AlipayClientOptions): void {
49
+ this.validate(options)
50
+ this._options = { ...options }
51
+ }
52
+
53
+ /** 创建沙箱配置的快捷方法 */
54
+ static forSandbox(options: Omit<AlipayClientOptions, 'gateway'>): AlipayConfig {
55
+ return new AlipayConfig({
56
+ ...options,
57
+ gateway: 'https://openapi-sandbox.dl.alipaydev.com/gateway.do'
58
+ })
59
+ }
60
+
61
+ private validate(options: AlipayClientOptions): void {
62
+ if (!options.appId) {
63
+ throw new Error('AlipayConfig: appId 不能为空')
64
+ }
65
+ if (!options.privateKey) {
66
+ throw new Error('AlipayConfig: privateKey 不能为空')
67
+ }
68
+ if (!options.alipayPublicKey) {
69
+ throw new Error('AlipayConfig: alipayPublicKey 不能为空')
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,32 @@
1
+ export { AlipayClient } from './client'
2
+ export { AlipayConfig } from './config'
3
+ export { AlipayApiError } from './response'
4
+ export { createPagePay } from './payment'
5
+ export { createWapPay } from './wap'
6
+ export { createQuery } from './query'
7
+ export { createRefund } from './refund'
8
+ export { createNotifyHandler } from './notify'
9
+ export { alipayPlugin, useAlipay, ALIPAY_KEY } from './plugin'
10
+ export type { AlipayPluginOptions } from './plugin'
11
+ export type {
12
+ AlipayClientOptions,
13
+ AlipayKeyType,
14
+ AlipaySdkConfig,
15
+ AlipaySdkSignType,
16
+ AlipayPagePayConfig,
17
+ AlipayPagePayParams,
18
+ AlipayPagePayResult,
19
+ AlipayWapPayConfig,
20
+ AlipayWapPayParams,
21
+ AlipayWapPayResult,
22
+ AlipayQueryParams,
23
+ AlipayQueryResult,
24
+ AlipayRefundParams,
25
+ AlipayRefundResult,
26
+ AlipayRefundQueryParams,
27
+ AlipayRefundQueryResult,
28
+ AlipayCloseParams,
29
+ AlipayCloseResult,
30
+ AlipayNotifyParams,
31
+ TradeStatus
32
+ } from './types'
@@ -0,0 +1,92 @@
1
+ import type { AlipayClient } from './client'
2
+ import type { AlipayNotifyParams, TradeStatus } from './types'
3
+
4
+ /**
5
+ * 异步通知处理
6
+ *
7
+ * 支付宝在交易状态变更时,通过 POST 请求 notify_url 发送异步通知。
8
+ * 处理要点:
9
+ * 1. 收到通知后必须先验签
10
+ * 2. 验签后校验关键业务字段(app_id、out_trade_no、total_amount)
11
+ * 3. 处理成功后返回字符串 "success"(仅此 7 个字符)
12
+ * 4. 需做幂等处理,过滤重复通知
13
+ */
14
+ export function createNotifyHandler(client: AlipayClient) {
15
+ return {
16
+ /**
17
+ * 验签异步通知
18
+ * @param params 异步通知参数(支付宝 POST 过来的原始参数)
19
+ * @param raw 使用原始报文验签(推荐:v4.15+ 支持,规避 encodeURIComponent 差异导致的验签失败)
20
+ * @returns 验签是否通过
21
+ */
22
+ verify(params: Record<string, any>, raw = false): boolean {
23
+ try {
24
+ return client.sdk.checkNotifySign(params, raw)
25
+ } catch {
26
+ return false
27
+ }
28
+ },
29
+
30
+ /**
31
+ * 校验异步通知的业务字段
32
+ *
33
+ * 验签只保证通知确实来自支付宝,不保证通知与你系统中的订单一致;
34
+ * 因此除了 app_id,还应传入商户侧的期望值(outTradeNo / totalAmount)进行比对。
35
+ * @param params 异步通知参数
36
+ * @param expected 商户侧期望值:outTradeNo 订单号、totalAmount 订单金额(单位元)
37
+ */
38
+ validate(
39
+ params: AlipayNotifyParams,
40
+ expected?: { outTradeNo?: string; totalAmount?: string }
41
+ ): NotifyValidationResult {
42
+ const errors: string[] = []
43
+
44
+ // 校验 app_id
45
+ if (params.appId !== client.config.appId) {
46
+ errors.push(`app_id 不匹配: 期望 ${client.config.appId},收到 ${params.appId}`)
47
+ }
48
+
49
+ // 校验 out_trade_no 与商户订单一致
50
+ if (expected?.outTradeNo && params.outTradeNo !== expected.outTradeNo) {
51
+ errors.push(`out_trade_no 不匹配: 期望 ${expected.outTradeNo},收到 ${params.outTradeNo}`)
52
+ }
53
+
54
+ // 校验 total_amount 与订单金额一致(金额为两位小数的服务端字符串,按数值比较)
55
+ if (expected?.totalAmount && Number(params.totalAmount) !== Number(expected.totalAmount)) {
56
+ errors.push(`total_amount 不匹配: 期望 ${expected.totalAmount},收到 ${params.totalAmount}`)
57
+ }
58
+
59
+ // 校验 trade_status
60
+ const validStatuses: TradeStatus[] = ['TRADE_SUCCESS', 'TRADE_FINISHED']
61
+ if (!validStatuses.includes(params.tradeStatus as TradeStatus)) {
62
+ errors.push(`trade_status 非终态: ${params.tradeStatus}`)
63
+ }
64
+
65
+ return {
66
+ valid: errors.length === 0,
67
+ errors
68
+ }
69
+ },
70
+
71
+ /**
72
+ * 生成支付宝异步通知的成功响应
73
+ * 处理完成后必须返回字符串 "success"
74
+ */
75
+ successResponse(): string {
76
+ return 'success'
77
+ },
78
+
79
+ /**
80
+ * 生成支付宝异步通知的失败响应
81
+ * 返回空字符串或非 success 内容,支付宝会重试通知
82
+ */
83
+ failResponse(): string {
84
+ return 'fail'
85
+ }
86
+ }
87
+ }
88
+
89
+ export interface NotifyValidationResult {
90
+ valid: boolean
91
+ errors: string[]
92
+ }
@@ -0,0 +1,60 @@
1
+ import type { AlipayClient } from './client'
2
+ import type {
3
+ AlipayPagePayConfig,
4
+ AlipayPagePayParams,
5
+ AlipayPagePayResult
6
+ } from './types'
7
+
8
+ /**
9
+ * 电脑网站支付 (alipay.trade.page.pay)
10
+ *
11
+ * 生成支付宝支付表单 HTML,前端渲染后自动提交表单跳转至支付宝收银台。
12
+ *
13
+ * ⚠️ 必须使用 pageExecute() 方法,禁止使用 exec()。
14
+ * ⚠️ 返回值为 HTML 表单字符串,不是 URL,前端必须渲染 form 并自动提交。
15
+ * ⚠️ 支付结果以异步通知或查询接口为准,不可依赖同步跳转。
16
+ */
17
+ export function createPagePay(client: AlipayClient) {
18
+ return {
19
+ /**
20
+ * 发起电脑网站支付
21
+ * @param config 支付配置
22
+ * @returns 支付表单 HTML
23
+ */
24
+ async pay(config: AlipayPagePayConfig): Promise<AlipayPagePayResult> {
25
+ const { outTradeNo, totalAmount, subject, body, passbackParams } = config
26
+
27
+ const bizParams: AlipayPagePayParams = {
28
+ outTradeNo,
29
+ totalAmount,
30
+ subject,
31
+ productCode: 'FAST_INSTANT_TRADE_PAY',
32
+ body,
33
+ passbackParams,
34
+ returnUrl: client.config.returnUrl,
35
+ notifyUrl: client.config.notifyUrl
36
+ }
37
+
38
+ // 使用 pageExecute() 生成 POST 表单 HTML
39
+ // 注意:必须使用 pageExecute,不能使用 exec()
40
+ const formHtml = client.sdk.pageExecute(
41
+ 'alipay.trade.page.pay',
42
+ 'POST',
43
+ { bizContent: sanitizeParams(bizParams) }
44
+ )
45
+
46
+ return { formHtml }
47
+ }
48
+ }
49
+ }
50
+
51
+ /** 移除 undefined 字段 */
52
+ function sanitizeParams<T extends Record<string, any>>(params: T): Record<string, any> {
53
+ const result: Record<string, any> = {}
54
+ for (const [key, value] of Object.entries(params)) {
55
+ if (value !== undefined && value !== null) {
56
+ result[key] = value
57
+ }
58
+ }
59
+ return result
60
+ }
@@ -0,0 +1,64 @@
1
+ import { createInjectionKey, inject, type VobsPlugin, type VobsContext } from '@vobs/vobs'
2
+ import { AlipayClient } from './client'
3
+ import type { AlipayClientOptions } from './types'
4
+
5
+ export const ALIPAY_KEY = createInjectionKey<AlipayClient>('vobs.payment.alipay')
6
+
7
+ export interface AlipayPluginOptions {
8
+ /** 支付宝客户端配置(未传入 client 时必填) */
9
+ config?: AlipayClientOptions
10
+ /** 可选的已有客户端实例(指定后忽略 config) */
11
+ client?: AlipayClient
12
+ }
13
+
14
+ /**
15
+ * 支付宝支付插件
16
+ *
17
+ * 将 AlipayClient 注入到 vobs 框架中,可通过 useAlipay() 获取。
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * createVobs({
22
+ * render: App,
23
+ * plugins: [
24
+ * alipayPlugin({
25
+ * config: {
26
+ * appId: '202100...',
27
+ * privateKey: '...',
28
+ * alipayPublicKey: '...',
29
+ * }
30
+ * })
31
+ * ]
32
+ * })
33
+ * ```
34
+ */
35
+ export function alipayPlugin(options: AlipayPluginOptions): VobsPlugin {
36
+ return {
37
+ name: '@vobs/payment/alipay',
38
+ version: '0.1.0',
39
+ install(context: VobsContext) {
40
+ if (!options.client && !options.config) {
41
+ throw new Error('Vobs Payment: alipayPlugin 需要提供 config 或 client 之一')
42
+ }
43
+ // 仅销毁插件自建的客户端;外部传入的实例由其所有者负责生命周期。
44
+ const ownClient = !options.client
45
+ const client = options.client ?? new AlipayClient(options.config!)
46
+ context.provide(ALIPAY_KEY, client)
47
+ return ownClient ? () => client.dispose() : undefined
48
+ }
49
+ }
50
+ }
51
+
52
+ /**
53
+ * 获取支付宝客户端实例
54
+ * 必须在已安装 alipayPlugin 的组件或上下文中调用
55
+ */
56
+ export function useAlipay(): AlipayClient {
57
+ const client = inject(ALIPAY_KEY)
58
+ if (!client) {
59
+ throw new Error(
60
+ 'Vobs Payment: 找不到 AlipayClient,请安装 alipayPlugin'
61
+ )
62
+ }
63
+ return client
64
+ }
@@ -0,0 +1,52 @@
1
+ import type { AlipayClient } from './client'
2
+ import { unwrapResponse } from './response'
3
+ import type { AlipayQueryParams, AlipayQueryResult, TradeStatus } from './types'
4
+
5
+ /**
6
+ * 交易查询 (alipay.trade.query)
7
+ *
8
+ * 查询支付宝交易状态,用于确认支付结果。
9
+ * 当异步通知未收到时,应主动调用此接口兜底。
10
+ */
11
+ export function createQuery(client: AlipayClient) {
12
+ return {
13
+ /**
14
+ * 查询交易状态
15
+ * @param params 查询参数(outTradeNo 和 tradeNo 至少传一个)
16
+ * @throws {AlipayApiError} 业务失败(如 ACQ.TRADE_NOT_EXIST)时抛出
17
+ */
18
+ async query(params: AlipayQueryParams): Promise<AlipayQueryResult> {
19
+ const alipayResponse = unwrapResponse(
20
+ await client.sdk.exec('alipay.trade.query', {
21
+ bizContent: params
22
+ }),
23
+ 'alipay.trade.query'
24
+ )
25
+
26
+ return {
27
+ tradeNo: alipayResponse.trade_no,
28
+ outTradeNo: alipayResponse.out_trade_no,
29
+ buyerLogonId: alipayResponse.buyer_logon_id,
30
+ tradeStatus: alipayResponse.trade_status as TradeStatus,
31
+ totalAmount: alipayResponse.total_amount,
32
+ receiptAmount: alipayResponse.receipt_amount ?? '0.00',
33
+ buyerPayAmount: alipayResponse.buyer_pay_amount ?? '0.00',
34
+ sendPayDate: alipayResponse.send_pay_date ?? '',
35
+ gmtPayment: alipayResponse.gmt_payment ?? null,
36
+ gmtClose: alipayResponse.gmt_close ?? null,
37
+ refundAmount: alipayResponse.refund_amount ?? '0.00',
38
+ subject: alipayResponse.subject ?? '',
39
+ passbackParams: alipayResponse.passback_params ?? null,
40
+ fundChannel: alipayResponse.fund_channel ?? null
41
+ }
42
+ },
43
+
44
+ /**
45
+ * 判断交易是否支付成功
46
+ * 只有 TRADE_SUCCESS 或 TRADE_FINISHED 才表示支付成功
47
+ */
48
+ isSuccessful(status: TradeStatus): boolean {
49
+ return status === 'TRADE_SUCCESS' || status === 'TRADE_FINISHED'
50
+ }
51
+ }
52
+ }