@vobs/payment 0.3.0 → 1.2.1

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 +40 -27
  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,93 @@
1
+ import { Aes, Formatter, Rsa } from 'wechatpay-axios-plugin'
2
+ import type { WechatClient } from './client'
3
+ import type {
4
+ WechatNotifyHeaders,
5
+ WechatNotifyRefund,
6
+ WechatNotifyResource,
7
+ WechatNotifyTransaction
8
+ } from './types'
9
+
10
+ /**
11
+ * 微信支付异步通知处理(APIv3)
12
+ *
13
+ * 完整流程:
14
+ * 1. verify() — 用 certs(按 Wechatpay-Serial 查找平台证书/微信支付公钥)验证报文签名
15
+ * 2. decrypt() — 用 apiv3Key 做 AES-256-GCM 解密 resource 得到业务数据
16
+ * 3. validate() — 比对商户侧期望值(订单号 / 金额),防错配
17
+ * 4. 处理业务后用 successResponse()/failResponse() 应答微信
18
+ */
19
+ export function createNotifyHandler(client: WechatClient) {
20
+ return {
21
+ /**
22
+ * 验签异步通知
23
+ * @param headers 微信支付回调的 Wechatpay-* 系列 HTTP 头
24
+ * @param body 原始请求体字符串(必须是未解析的原文,不可先 JSON.parse 再序列化)
25
+ * @returns 验签是否通过
26
+ */
27
+ verify(headers: WechatNotifyHeaders, body: string): boolean {
28
+ const publicKey = client.config.options.certs[headers.serial]
29
+ if (!publicKey) {
30
+ return false
31
+ }
32
+ try {
33
+ const message = Formatter.response(headers.timestamp, headers.nonce, body)
34
+ return Rsa.verify(message, headers.signature, publicKey)
35
+ } catch {
36
+ return false
37
+ }
38
+ },
39
+
40
+ /**
41
+ * 解密通知 resource 字段(AES-256-GCM,apiv3Key)
42
+ * @param resource 回调 JSON 中的 resource 字段
43
+ */
44
+ decrypt<T extends WechatNotifyTransaction | WechatNotifyRefund = WechatNotifyTransaction>(
45
+ resource: WechatNotifyResource
46
+ ): T {
47
+ const plaintext = Aes.AesGcm.decrypt(
48
+ resource.ciphertext,
49
+ client.config.apiv3Key,
50
+ resource.nonce,
51
+ resource.associatedData ?? ''
52
+ )
53
+ return JSON.parse(plaintext) as T
54
+ },
55
+
56
+ /**
57
+ * 校验解密后的通知与商户侧订单是否一致。
58
+ * 验签只保证通知来自微信支付,不保证与你的订单一致。
59
+ * @param payload 解密后的通知数据
60
+ * @param expected 商户侧期望值:outTradeNo 订单号、total 订单金额(分)
61
+ */
62
+ validate(
63
+ payload: WechatNotifyTransaction | WechatNotifyRefund,
64
+ expected?: { outTradeNo?: string; total?: number }
65
+ ): { valid: boolean; errors: string[] } {
66
+ const errors: string[] = []
67
+
68
+ if (payload.mchid !== client.config.mchid) {
69
+ errors.push(`mchid 不匹配: 期望 ${client.config.mchid},收到 ${payload.mchid}`)
70
+ }
71
+
72
+ if (expected?.outTradeNo && payload.outTradeNo !== expected.outTradeNo) {
73
+ errors.push(`out_trade_no 不匹配: 期望 ${expected.outTradeNo},收到 ${payload.outTradeNo}`)
74
+ }
75
+
76
+ if (expected?.total !== undefined && payload.amount?.total !== expected.total) {
77
+ errors.push(`amount.total 不匹配: 期望 ${expected.total},收到 ${payload.amount?.total}`)
78
+ }
79
+
80
+ return { valid: errors.length === 0, errors }
81
+ },
82
+
83
+ /** 通知处理成功应答(HTTP 200/201,body 恰为 {"code":"SUCCESS"}) */
84
+ successResponse(): { code: 'SUCCESS'; message?: undefined } {
85
+ return { code: 'SUCCESS' }
86
+ },
87
+
88
+ /** 通知处理失败应答(微信会按衰减频率重发通知) */
89
+ failResponse(message?: string): { code: 'FAIL'; message: string } {
90
+ return { code: 'FAIL', message: message ?? '处理失败' }
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,177 @@
1
+ import { Formatter, Rsa } from 'wechatpay-axios-plugin'
2
+ import type { WechatClient } from './client'
3
+ import { unwrapV3 } from './response'
4
+ import type {
5
+ WechatAppPayResult,
6
+ WechatH5PayParams,
7
+ WechatH5PayResult,
8
+ WechatJsapiPayResult,
9
+ WechatNativePayResult,
10
+ WechatPayParams,
11
+ WechatQueryParams,
12
+ WechatQueryResult,
13
+ WechatTradeState
14
+ } from './types'
15
+
16
+ /**
17
+ * 微信支付下单(APIv3)
18
+ *
19
+ * 覆盖四种收款场景:
20
+ * - jsapiPay:公众号网页 / 小程序(wx.requestPayment)
21
+ * - appPay:APP 拉起微信支付
22
+ * - nativePay:PC 扫码支付(返回二维码链接)
23
+ * - h5Pay:微信外浏览器 H5 支付(返回收银台跳转链接)
24
+ *
25
+ * ⚠️ 支付结果以异步通知或 query() 为准。
26
+ */
27
+ export function createPayment(client: WechatClient) {
28
+ return {
29
+ /**
30
+ * 公众号 / 小程序支付下单(POST /v3/pay/transactions/jsapi)
31
+ * @param params 下单参数,openid 必填
32
+ * @returns 调起 wx.requestPayment 所需的签名参数
33
+ */
34
+ async jsapiPay(params: WechatPayParams & { openid: string }): Promise<WechatJsapiPayResult> {
35
+ const { data } = await postTransaction(client, 'jsapi', params)
36
+ return buildInvokeParams(client, data.prepay_id ?? '')
37
+ },
38
+
39
+ /**
40
+ * APP 支付下单(POST /v3/pay/transactions/app)
41
+ * @param params 下单参数
42
+ * @returns 传给客户端 SDK 调起微信支付的签名参数
43
+ */
44
+ async appPay(params: WechatPayParams): Promise<WechatAppPayResult> {
45
+ const { data } = await postTransaction(client, 'app', params)
46
+ return buildInvokeParams(client, data.prepay_id ?? '')
47
+ },
48
+
49
+ /**
50
+ * Native 扫码支付下单(POST /v3/pay/transactions/native)
51
+ * @returns 二维码链接(前端渲染二维码,用户扫码支付)
52
+ */
53
+ async nativePay(params: WechatPayParams): Promise<WechatNativePayResult> {
54
+ const { data } = await postTransaction(client, 'native', params)
55
+ if (!data.code_url) {
56
+ throw new Error('WechatClient: 下单响应缺少 code_url')
57
+ }
58
+ return { codeUrl: data.code_url }
59
+ },
60
+
61
+ /**
62
+ * H5 支付下单(POST /v3/pay/transactions/h5)
63
+ * @param params 下单参数,payerClientIp 必填
64
+ * @returns 收银台跳转链接(302 重定向即可拉起微信支付)
65
+ */
66
+ async h5Pay(params: WechatH5PayParams): Promise<WechatH5PayResult> {
67
+ const body = buildTransactionBody(client, params)
68
+ body.scene_info = {
69
+ payer_client_ip: params.payerClientIp,
70
+ ...(params.sceneInfo?.deviceType ? { device_type: params.sceneInfo.deviceType } : {})
71
+ }
72
+ const { data } = await unwrapV3(client.sdk.v3.pay.transactions.h5.post(body))
73
+ return { h5Url: data.h5_url }
74
+ },
75
+
76
+ /**
77
+ * 交易查询(GET /v3/pay/transactions/out-trade-no/{no} 或 .../id/{id})
78
+ * @throws {WechatApiError} 订单不存在等失败(RESOURCE_NOT_EXISTS)
79
+ */
80
+ async query(params: WechatQueryParams): Promise<WechatQueryResult> {
81
+ const mchid = client.config.mchid
82
+ const response = params.transactionId
83
+ ? await unwrapV3(
84
+ client.sdk.client.request(
85
+ `/v3/pay/transactions/id/${encodeURIComponent(params.transactionId)}`,
86
+ 'GET',
87
+ undefined,
88
+ { params: { mchid } }
89
+ )
90
+ )
91
+ : await unwrapV3(
92
+ client.sdk.client.request(
93
+ `/v3/pay/transactions/out-trade-no/${encodeURIComponent(params.outTradeNo ?? '')}`,
94
+ 'GET',
95
+ undefined,
96
+ { params: { mchid } }
97
+ )
98
+ )
99
+ return mapTransaction((response as { data: Record<string, any> }).data)
100
+ }
101
+ }
102
+ }
103
+
104
+ type TransactionChain = 'jsapi' | 'app' | 'native'
105
+
106
+ async function postTransaction(
107
+ client: WechatClient,
108
+ chain: TransactionChain,
109
+ params: WechatPayParams
110
+ ): Promise<{ data: { prepay_id?: string; code_url?: string } }> {
111
+ const body = buildTransactionBody(client, params)
112
+ return unwrapV3(client.sdk.v3.pay.transactions[chain].post(body))
113
+ }
114
+
115
+ /** 构造下单请求体(camelCase -> snake_case,注入 appid/mchid/notify_url) */
116
+ function buildTransactionBody(
117
+ client: WechatClient,
118
+ params: WechatPayParams
119
+ ): Record<string, any> {
120
+ const body: Record<string, any> = {
121
+ appid: client.config.appid,
122
+ mchid: client.config.mchid,
123
+ description: params.description,
124
+ out_trade_no: params.outTradeNo,
125
+ time_expire: params.timeExpire,
126
+ attach: params.attach,
127
+ goods_tag: params.goodsTag,
128
+ notify_url: client.config.notifyUrl,
129
+ amount: {
130
+ total: params.amount.total,
131
+ currency: params.amount.currency ?? 'CNY'
132
+ }
133
+ }
134
+ if (params.openid) {
135
+ body.payer = { openid: params.openid }
136
+ }
137
+ // 移除 undefined 字段
138
+ for (const key of Object.keys(body)) {
139
+ if (body[key] === undefined) delete body[key]
140
+ }
141
+ return body
142
+ }
143
+
144
+ /**
145
+ * 生成拉起支付的签名参数。
146
+ * 签名串:appid\ntimeStamp\nnonceStr\nprepay_id=xxx\n(SHA256withRSA,商户 API 私钥)
147
+ */
148
+ function buildInvokeParams(client: WechatClient, prepayId: string): WechatJsapiPayResult {
149
+ if (!prepayId) {
150
+ throw new Error('WechatClient: 下单响应缺少 prepay_id')
151
+ }
152
+ const appId = client.config.appid
153
+ const timeStamp = `${Formatter.timestamp()}`
154
+ const nonceStr = Formatter.nonce(32)
155
+ const pkg = `prepay_id=${prepayId}`
156
+ const message = `${appId}\n${timeStamp}\n${nonceStr}\n${pkg}\n`
157
+ const paySign = Rsa.sign(message, client.config.options.privateKey)
158
+ return { appId, timeStamp, nonceStr, package: pkg, signType: 'RSA', paySign }
159
+ }
160
+
161
+ /** snake_case 响应映射 */
162
+ function mapTransaction(data: Record<string, any>): WechatQueryResult {
163
+ return {
164
+ appid: data.appid,
165
+ mchid: data.mchid,
166
+ outTradeNo: data.out_trade_no,
167
+ transactionId: data.transaction_id,
168
+ tradeType: data.trade_type,
169
+ tradeState: data.trade_state as WechatTradeState,
170
+ tradeStateDesc: data.trade_state_desc,
171
+ bankType: data.bank_type,
172
+ attach: data.attach,
173
+ successTime: data.success_time,
174
+ total: data.amount?.total,
175
+ payerTotal: data.amount?.payer_total
176
+ }
177
+ }
@@ -0,0 +1,59 @@
1
+ import { createInjectionKey, inject, type VobsContext, type VobsPlugin } from '@vobs/vobs'
2
+ import { WechatClient } from './client'
3
+ import type { WechatClientOptions } from './types'
4
+
5
+ export const WECHAT_KEY = createInjectionKey<WechatClient>('vobs.payment.wechat')
6
+
7
+ export interface WechatPluginOptions {
8
+ /** 微信支付客户端配置(未传入 client 时必填) */
9
+ config?: WechatClientOptions
10
+ /** 可选的已有客户端实例(指定后忽略 config) */
11
+ client?: WechatClient
12
+ }
13
+
14
+ /** 从 vobs 插件上下文获取微信支付客户端 */
15
+ export function useWechat(): WechatClient {
16
+ const client = inject(WECHAT_KEY)
17
+ if (!client) {
18
+ throw new Error('Vobs Payment: 找不到 WechatClient,请安装 wechatPlugin')
19
+ }
20
+ return client
21
+ }
22
+
23
+ /**
24
+ * 微信支付插件
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * createVobs({
29
+ * plugins: [
30
+ * wechatPlugin({
31
+ * config: {
32
+ * mchid: '1900000000',
33
+ * serial: '...',
34
+ * privateKey: '...',
35
+ * certs: { 'PUB_KEY_ID_xxx': '...' },
36
+ * appid: 'wx1234567890',
37
+ * apiv3Key: '...'
38
+ * }
39
+ * })
40
+ * ]
41
+ * })
42
+ * ```
43
+ */
44
+ export function wechatPlugin(options: WechatPluginOptions): VobsPlugin {
45
+ return {
46
+ name: '@vobs/payment/wechat',
47
+ version: '0.1.0',
48
+ install(context: VobsContext) {
49
+ if (!options.client && !options.config) {
50
+ throw new Error('Vobs Payment: wechatPlugin 需要提供 config 或 client 之一')
51
+ }
52
+ // 仅销毁插件自建的客户端;外部传入的实例由其所有者负责生命周期。
53
+ const ownClient = !options.client
54
+ const client = options.client ?? new WechatClient(options.config!)
55
+ context.provide(WECHAT_KEY, client)
56
+ return ownClient ? () => client.dispose() : undefined
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,80 @@
1
+ import type { WechatClient } from './client'
2
+ import { unwrapV3 } from './response'
3
+ import type {
4
+ WechatRefundParams,
5
+ WechatRefundQueryParams,
6
+ WechatRefundResult,
7
+ WechatRefundStatus
8
+ } from './types'
9
+
10
+ /**
11
+ * 微信退款(APIv3)
12
+ *
13
+ * 覆盖申请退款与退款状态查询。
14
+ * ⚠️ 退款结果以异步通知(refund.irqevent)或 queryRefund 为准。
15
+ */
16
+ export function createRefund(client: WechatClient) {
17
+ return {
18
+ /**
19
+ * 申请退款(POST /v3/refund/domestic/refunds)
20
+ * @param params 退款参数(outTradeNo 与 transactionId 二选一)
21
+ * @throws {WechatApiError} 参数错误、余额不足、订单不可退等失败
22
+ */
23
+ async refund(params: WechatRefundParams): Promise<WechatRefundResult> {
24
+ const body: Record<string, any> = {
25
+ out_trade_no: params.outTradeNo,
26
+ transaction_id: params.transactionId,
27
+ out_refund_no: params.outRefundNo,
28
+ reason: params.reason,
29
+ notify_url: params.notifyUrl ?? client.config.notifyUrl,
30
+ funds_account: params.fundsAccount,
31
+ amount: {
32
+ refund: params.amount.refund,
33
+ total: params.amount.total,
34
+ currency: params.amount.currency ?? 'CNY'
35
+ }
36
+ }
37
+ // 移除 undefined 字段
38
+ for (const key of Object.keys(body)) {
39
+ if (body[key] === undefined) delete body[key]
40
+ }
41
+ for (const key of Object.keys(body.amount)) {
42
+ if ((body.amount as any)[key] === undefined) delete (body.amount as any)[key]
43
+ }
44
+
45
+ const { data } = await unwrapV3(client.sdk.v3.refund.domestic.refunds.post(body))
46
+ return mapRefund(data)
47
+ },
48
+
49
+ /**
50
+ * 查询退款(GET /v3/refund/domestic/refunds/{out_refund_no | refund_id})
51
+ * @throws {WechatApiError} 退款单不存在等失败
52
+ */
53
+ async queryRefund(params: WechatRefundQueryParams): Promise<WechatRefundResult> {
54
+ const id = params.refundId ?? params.outRefundNo
55
+ if (!id) {
56
+ throw new Error('WechatClient: queryRefund 需要 outRefundNo 或 refundId')
57
+ }
58
+ const { data } = await unwrapV3(
59
+ client.sdk.client.request(`/v3/refund/domestic/refunds/${encodeURIComponent(id)}`, 'GET')
60
+ )
61
+ return mapRefund(data)
62
+ }
63
+ }
64
+ }
65
+
66
+ /** snake_case 响应映射 */
67
+ function mapRefund(data: Record<string, any>): WechatRefundResult {
68
+ return {
69
+ refundId: data.refund_id,
70
+ outRefundNo: data.out_refund_no,
71
+ outTradeNo: data.out_trade_no,
72
+ transactionId: data.transaction_id,
73
+ status: data.status as WechatRefundStatus,
74
+ refundAmount: data.amount?.refund,
75
+ total: data.amount?.total,
76
+ userReceivedAmount: data.amount?.user_received_amount,
77
+ successTime: data.success_time,
78
+ reason: data.reason
79
+ }
80
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * 微信支付 APIv3 错误归一化。
3
+ *
4
+ * v3 接口没有"业务体 code"约定:HTTP 2xx 即成功,失败以
5
+ * 非 2xx 状态码 + JSON body {code, message} 表达(axios 会抛错)。
6
+ * 本模块把 axios 错误统一转换为 WechatApiError。
7
+ */
8
+ export class WechatApiError extends Error {
9
+ /** HTTP 状态码(网络层错误时为 undefined) */
10
+ readonly status?: number
11
+ /** 错误码,如 PARAM_ERROR / RESOURCE_NOT_EXISTS / NETWORK_ERROR */
12
+ readonly code: string
13
+
14
+ constructor(message: string, status: number | undefined, code: string) {
15
+ super(message)
16
+ this.name = 'WechatApiError'
17
+ this.status = status
18
+ this.code = code
19
+ }
20
+ }
21
+
22
+ /** 将任意请求异常归一化为 WechatApiError */
23
+ export function toWechatApiError(error: unknown): WechatApiError {
24
+ if (error instanceof WechatApiError) return error
25
+ const axiosError = error as {
26
+ response?: { status?: number; data?: { code?: string; message?: string } }
27
+ code?: string
28
+ message?: string
29
+ }
30
+ const status = axiosError?.response?.status
31
+ const bizCode = axiosError?.response?.data?.code
32
+ const bizMessage = axiosError?.response?.data?.message
33
+ if (status !== undefined) {
34
+ return new WechatApiError(
35
+ `微信支付接口调用失败: ${bizMessage ?? axiosError?.message ?? '未知错误'} (${status}${bizCode ? `, ${bizCode}` : ''})`,
36
+ status,
37
+ bizCode ?? String(status)
38
+ )
39
+ }
40
+ return new WechatApiError(
41
+ `微信支付请求失败: ${axiosError?.message ?? '未知错误'}${axiosError?.code ? ` (${axiosError.code})` : ''}`,
42
+ undefined,
43
+ axiosError?.code ?? 'NETWORK_ERROR'
44
+ )
45
+ }
46
+
47
+ /** 包装请求 promise:失败时抛出 WechatApiError */
48
+ export async function unwrapV3<T>(promise: PromiseLike<T>): Promise<T> {
49
+ try {
50
+ return await promise
51
+ } catch (error) {
52
+ throw toWechatApiError(error)
53
+ }
54
+ }
@@ -0,0 +1,223 @@
1
+ /** 微信支付 APIv3 交易状态 */
2
+ export type WechatTradeState =
3
+ | 'SUCCESS'
4
+ | 'REFUND'
5
+ | 'NOTPAY'
6
+ | 'CLOSED'
7
+ | 'REVOKED'
8
+ | 'USERPAYING'
9
+ | 'PAYERROR'
10
+
11
+ /** 微信支付 APIv3 退款状态 */
12
+ export type WechatRefundStatus = 'SUCCESS' | 'CLOSED' | 'PROCESSING' | 'ABNORMAL'
13
+
14
+ export interface WechatPayParams {
15
+ /** 商户订单号(6-32 位,商户侧唯一) */
16
+ outTradeNo: string
17
+ /** 商品描述 */
18
+ description: string
19
+ /** 订单金额(单位:分) */
20
+ amount: WechatOrderAmount
21
+ /** 支付者 openid(jsapi / 小程序支付必填) */
22
+ openid?: string
23
+ /** 附加数据,回调时原样返回 */
24
+ attach?: string
25
+ /** 优惠标记 */
26
+ goodsTag?: string
27
+ /** 订单过期时间(RFC 3339,如 2018-06-08T10:34:56+08:00) */
28
+ timeExpire?: string
29
+ }
30
+
31
+ /** H5 支付参数(额外要求支付者 IP) */
32
+ export interface WechatH5PayParams extends WechatPayParams {
33
+ /** 用户客户端 IP */
34
+ payerClientIp: string
35
+ /** 场景信息 */
36
+ sceneInfo?: {
37
+ payerClientIp?: string
38
+ deviceType?: string
39
+ }
40
+ }
41
+
42
+ /** JSAPI / 小程序调起支付所需参数(wx.requestPayment / WeixinJSBridge) */
43
+ export interface WechatJsapiPayResult {
44
+ appId: string
45
+ timeStamp: string
46
+ nonceStr: string
47
+ package: string
48
+ signType: 'RSA'
49
+ paySign: string
50
+ }
51
+
52
+ /** APP 调起支付所需参数(与 JSAPI 同构) */
53
+ export interface WechatAppPayResult extends WechatJsapiPayResult {}
54
+
55
+ /** Native 下单结果:二维码链接 */
56
+ export interface WechatNativePayResult {
57
+ codeUrl: string
58
+ }
59
+
60
+ /** H5 下单结果:收银台跳转链接 */
61
+ export interface WechatH5PayResult {
62
+ h5Url: string
63
+ }
64
+
65
+ /** 交易查询参数(outTradeNo 与 transactionId 二选一) */
66
+ export interface WechatQueryParams {
67
+ outTradeNo?: string
68
+ transactionId?: string
69
+ }
70
+
71
+ /** 交易查询结果(snake_case 已映射为 camelCase) */
72
+ export interface WechatQueryResult {
73
+ appid: string
74
+ mchid: string
75
+ outTradeNo: string
76
+ transactionId: string
77
+ tradeType?: string
78
+ tradeState: WechatTradeState
79
+ tradeStateDesc?: string
80
+ bankType?: string
81
+ attach?: string
82
+ successTime?: string
83
+ /** 订单总金额(分) */
84
+ total: number
85
+ /** 用户实付金额(分) */
86
+ payerTotal?: number
87
+ }
88
+
89
+ /** 退款金额 */
90
+ export interface WechatRefundParams {
91
+ outTradeNo?: string
92
+ transactionId?: string
93
+ /** 商户退款单号(商户侧唯一) */
94
+ outRefundNo: string
95
+ /** 退款金额(单位:分) */
96
+ amount: WechatRefundAmount
97
+ /** 退款原因 */
98
+ reason?: string
99
+ /** 退款结果回调地址 */
100
+ notifyUrl?: string
101
+ /** 退款资金来源:AVAILABLE(可用余额)| UNSETTLED(待结算资金),默认 AVAILABLE */
102
+ fundsAccount?: 'AVAILABLE' | 'UNSETTLED'
103
+ }
104
+
105
+ /** 退款申请结果 */
106
+ export interface WechatRefundResult {
107
+ refundId: string
108
+ outRefundNo: string
109
+ outTradeNo: string
110
+ transactionId: string
111
+ status: WechatRefundStatus
112
+ /** 退款金额(分) */
113
+ refundAmount: number
114
+ /** 订单总金额(分) */
115
+ total: number
116
+ /** 退款入账账户金额(分) */
117
+ userReceivedAmount?: number
118
+ successTime?: string
119
+ reason?: string
120
+ }
121
+
122
+ /** 退款查询参数(outRefundNo 与 refundId 二选一) */
123
+ export interface WechatRefundQueryParams {
124
+ outRefundNo?: string
125
+ refundId?: string
126
+ }
127
+
128
+ /** 异步通知报文 resource 字段(AES-256-GCM 加密) */
129
+ export interface WechatNotifyResource {
130
+ originalType?: string
131
+ algorithm?: string
132
+ ciphertext: string
133
+ associatedData?: string
134
+ nonce: string
135
+ }
136
+
137
+ /** 异步通知验签所需的 HTTP 头 */
138
+ export interface WechatNotifyHeaders {
139
+ /** Wechatpay-Timestamp */
140
+ timestamp: string
141
+ /** Wechatpay-Nonce */
142
+ nonce: string
143
+ /** Wechatpay-Serial(平台证书序列号 / 微信支付公钥 ID) */
144
+ serial: string
145
+ /** Wechatpay-Signature */
146
+ signature: string
147
+ }
148
+
149
+ /** 解密后的支付成功/状态变更通知 */
150
+ export interface WechatNotifyTransaction {
151
+ mchid: string
152
+ appid: string
153
+ outTradeNo: string
154
+ transactionId: string
155
+ tradeType?: string
156
+ tradeState: WechatTradeState
157
+ tradeStateDesc?: string
158
+ bankType?: string
159
+ attach?: string
160
+ successTime?: string
161
+ amount: {
162
+ total: number
163
+ payerTotal?: number
164
+ currency?: string
165
+ payerCurrency?: string
166
+ }
167
+ }
168
+
169
+ /** 解密后的退款结果通知 */
170
+ export interface WechatNotifyRefund {
171
+ mchid: string
172
+ outTradeNo: string
173
+ transactionId: string
174
+ outRefundNo: string
175
+ refundId: string
176
+ refundStatus: WechatRefundStatus
177
+ successTime?: string
178
+ amount: {
179
+ total: number
180
+ refund: number
181
+ payerTotal?: number
182
+ userReceivedAmount?: number
183
+ currency?: string
184
+ }
185
+ userAccount?: string
186
+ }
187
+
188
+ /** 微信支付客户端配置 */
189
+ export interface WechatClientOptions {
190
+ /** 商户号 */
191
+ mchid: string
192
+ /** 商户 API 证书序列号 */
193
+ serial: string
194
+ /** 商户 API 私钥(PEM 字符串) */
195
+ privateKey: string
196
+ /** 平台证书 / 微信支付公钥,{序列号(或公钥ID): PEM} 映射;通知验签按 Wechatpay-Serial 查找 */
197
+ certs: Record<string, string>
198
+ /** 应用 ID(公众号 / 小程序 / APP),用于下单与调起签名 */
199
+ appid: string
200
+ /** APIv3 密钥(32 字节,回调解密用) */
201
+ apiv3Key: string
202
+ /** 异步通知地址(下单时可被 params.notifyUrl 覆盖) */
203
+ notifyUrl?: string
204
+ /** 请求超时(毫秒) */
205
+ timeout?: number
206
+ }
207
+
208
+ /** 订单金额(单位:分) */
209
+ export interface WechatOrderAmount {
210
+ /** 订单总金额(分) */
211
+ total: number
212
+ /** 币种,默认 CNY */
213
+ currency?: string
214
+ }
215
+
216
+ /** 退款金额(单位:分) */
217
+ export interface WechatRefundAmount {
218
+ /** 退款金额(分) */
219
+ refund: number
220
+ /** 原订单金额(分),分账等场景必填 */
221
+ total?: number
222
+ currency?: string
223
+ }