@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.
- package/LICENSE +1 -1
- package/README.md +182 -0
- package/dist/alipay/client.cjs +112 -0
- package/dist/alipay/client.cjs.map +1 -0
- package/dist/alipay/client.d.cts +36 -0
- package/dist/alipay/client.d.ts +36 -0
- package/dist/alipay/client.js +110 -0
- package/dist/alipay/client.js.map +1 -0
- package/dist/alipay/config.cjs +67 -0
- package/dist/alipay/config.cjs.map +1 -0
- package/dist/alipay/config.d.cts +30 -0
- package/dist/alipay/config.d.ts +30 -0
- package/dist/alipay/config.js +65 -0
- package/dist/alipay/config.js.map +1 -0
- package/dist/alipay/index.cjs +592 -0
- package/dist/alipay/index.cjs.map +1 -0
- package/dist/alipay/index.d.cts +12 -0
- package/dist/alipay/index.d.ts +12 -0
- package/dist/alipay/index.js +580 -0
- package/dist/alipay/index.js.map +1 -0
- package/dist/alipay/notify.cjs +70 -0
- package/dist/alipay/notify.cjs.map +1 -0
- package/dist/alipay/notify.d.cts +52 -0
- package/dist/alipay/notify.d.ts +52 -0
- package/dist/alipay/notify.js +68 -0
- package/dist/alipay/notify.js.map +1 -0
- package/dist/alipay/payment.cjs +49 -0
- package/dist/alipay/payment.cjs.map +1 -0
- package/dist/alipay/payment.d.cts +24 -0
- package/dist/alipay/payment.d.ts +24 -0
- package/dist/alipay/payment.js +47 -0
- package/dist/alipay/payment.js.map +1 -0
- package/dist/alipay/plugin.cjs +300 -0
- package/dist/alipay/plugin.cjs.map +1 -0
- package/dist/alipay/plugin.d.cts +43 -0
- package/dist/alipay/plugin.d.ts +43 -0
- package/dist/alipay/plugin.js +296 -0
- package/dist/alipay/plugin.js.map +1 -0
- package/dist/alipay/query.cjs +80 -0
- package/dist/alipay/query.cjs.map +1 -0
- package/dist/alipay/query.d.cts +26 -0
- package/dist/alipay/query.d.ts +26 -0
- package/dist/alipay/query.js +78 -0
- package/dist/alipay/query.js.map +1 -0
- package/dist/alipay/refund.cjs +104 -0
- package/dist/alipay/refund.cjs.map +1 -0
- package/dist/alipay/refund.d.cts +32 -0
- package/dist/alipay/refund.d.ts +32 -0
- package/dist/alipay/refund.js +102 -0
- package/dist/alipay/refund.js.map +1 -0
- package/dist/alipay/response.cjs +38 -0
- package/dist/alipay/response.cjs.map +1 -0
- package/dist/alipay/response.d.cts +19 -0
- package/dist/alipay/response.d.ts +19 -0
- package/dist/alipay/response.js +35 -0
- package/dist/alipay/response.js.map +1 -0
- package/dist/alipay/types.cjs +4 -0
- package/dist/alipay/types.cjs.map +1 -0
- package/dist/alipay/types.d.cts +242 -0
- package/dist/alipay/types.d.ts +242 -0
- package/dist/alipay/types.js +3 -0
- package/dist/alipay/types.js.map +1 -0
- package/dist/alipay/wap.cjs +50 -0
- package/dist/alipay/wap.cjs.map +1 -0
- package/dist/alipay/wap.d.cts +24 -0
- package/dist/alipay/wap.d.ts +24 -0
- package/dist/alipay/wap.js +48 -0
- package/dist/alipay/wap.js.map +1 -0
- package/dist/index-B1aUR7Vt.d.ts +49 -0
- package/dist/index-B8pzZxGq.d.cts +44 -0
- package/dist/index-BWnIrwRi.d.ts +44 -0
- package/dist/index-Dyrkl86r.d.cts +49 -0
- package/dist/index.cjs +1071 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -37
- package/dist/index.js +1052 -37
- package/dist/index.js.map +1 -0
- package/dist/wechat/client.cjs +119 -0
- package/dist/wechat/client.cjs.map +1 -0
- package/dist/wechat/client.d.cts +39 -0
- package/dist/wechat/client.d.ts +39 -0
- package/dist/wechat/client.js +117 -0
- package/dist/wechat/client.js.map +1 -0
- package/dist/wechat/config.cjs +76 -0
- package/dist/wechat/config.cjs.map +1 -0
- package/dist/wechat/config.d.cts +29 -0
- package/dist/wechat/config.d.ts +29 -0
- package/dist/wechat/config.js +74 -0
- package/dist/wechat/config.js.map +1 -0
- package/dist/wechat/index.cjs +612 -0
- package/dist/wechat/index.cjs.map +1 -0
- package/dist/wechat/index.d.cts +10 -0
- package/dist/wechat/index.d.ts +10 -0
- package/dist/wechat/index.js +602 -0
- package/dist/wechat/index.js.map +1 -0
- package/dist/wechat/notify.cjs +73 -0
- package/dist/wechat/notify.cjs.map +1 -0
- package/dist/wechat/notify.d.cts +53 -0
- package/dist/wechat/notify.d.ts +53 -0
- package/dist/wechat/notify.js +71 -0
- package/dist/wechat/notify.js.map +1 -0
- package/dist/wechat/payment.cjs +186 -0
- package/dist/wechat/payment.cjs.map +1 -0
- package/dist/wechat/payment.d.cts +50 -0
- package/dist/wechat/payment.d.ts +50 -0
- package/dist/wechat/payment.js +184 -0
- package/dist/wechat/payment.js.map +1 -0
- package/dist/wechat/plugin.cjs +305 -0
- package/dist/wechat/plugin.cjs.map +1 -0
- package/dist/wechat/plugin.d.cts +40 -0
- package/dist/wechat/plugin.d.ts +40 -0
- package/dist/wechat/plugin.js +301 -0
- package/dist/wechat/plugin.js.map +1 -0
- package/dist/wechat/refund.cjs +112 -0
- package/dist/wechat/refund.cjs.map +1 -0
- package/dist/wechat/refund.d.cts +26 -0
- package/dist/wechat/refund.d.ts +26 -0
- package/dist/wechat/refund.js +110 -0
- package/dist/wechat/refund.js.map +1 -0
- package/dist/wechat/response.cjs +50 -0
- package/dist/wechat/response.cjs.map +1 -0
- package/dist/wechat/response.d.cts +20 -0
- package/dist/wechat/response.d.ts +20 -0
- package/dist/wechat/response.js +46 -0
- package/dist/wechat/response.js.map +1 -0
- package/dist/wechat/types.cjs +4 -0
- package/dist/wechat/types.cjs.map +1 -0
- package/dist/wechat/types.d.cts +200 -0
- package/dist/wechat/types.d.ts +200 -0
- package/dist/wechat/types.js +3 -0
- package/dist/wechat/types.js.map +1 -0
- package/package.json +37 -28
- package/src/alipay/alipay.test.ts +231 -0
- package/src/alipay/client.ts +67 -0
- package/src/alipay/config.ts +72 -0
- package/src/alipay/index.ts +32 -0
- package/src/alipay/notify.ts +92 -0
- package/src/alipay/payment.ts +60 -0
- package/src/alipay/plugin.ts +64 -0
- package/src/alipay/query.ts +52 -0
- package/src/alipay/refund.ts +85 -0
- package/src/alipay/response.ts +40 -0
- package/src/alipay/types.ts +271 -0
- package/src/alipay/wap.ts +55 -0
- package/src/index.ts +78 -0
- package/src/wechat/client.ts +68 -0
- package/src/wechat/config.ts +81 -0
- package/src/wechat/index.ts +30 -0
- package/src/wechat/notify.ts +93 -0
- package/src/wechat/payment.ts +177 -0
- package/src/wechat/plugin.ts +59 -0
- package/src/wechat/refund.ts +80 -0
- package/src/wechat/response.ts +54 -0
- package/src/wechat/types.ts +223 -0
- package/src/wechat/wechat.test.ts +393 -0
- package/dist/alipay-official.d.ts +0 -34
- package/dist/alipay-official.js +0 -206
- package/dist/handlers.d.ts +0 -45
- package/dist/handlers.js +0 -105
- package/dist/jeepay.d.ts +0 -39
- package/dist/jeepay.js +0 -206
- package/dist/lakala.d.ts +0 -41
- package/dist/lakala.js +0 -243
- package/dist/manager.d.ts +0 -67
- package/dist/manager.js +0 -538
- package/dist/mock-driver.d.ts +0 -22
- package/dist/mock-driver.js +0 -59
- package/dist/state-machine.d.ts +0 -41
- package/dist/state-machine.js +0 -60
- package/dist/store.d.ts +0 -33
- package/dist/store.js +0 -76
- package/dist/types.d.ts +0 -168
- package/dist/types.js +0 -11
- package/dist/verify.d.ts +0 -34
- package/dist/verify.js +0 -55
- package/dist/wechat-official.d.ts +0 -40
- 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
|
+
}
|