@alipay/alipay-aipay 1.0.2 → 1.2.2
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/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +71 -22
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/prepare-tools.d.ts.map +1 -1
- package/dist/commands/prepare-tools.js +16 -4
- package/dist/commands/prepare-tools.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +9 -9
- package/dist/commands/update.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/alipay-cli.d.ts +6 -0
- package/dist/utils/alipay-cli.d.ts.map +1 -1
- package/dist/utils/alipay-cli.js +38 -22
- package/dist/utils/alipay-cli.js.map +1 -1
- package/dist/utils/file.d.ts +9 -2
- package/dist/utils/file.d.ts.map +1 -1
- package/dist/utils/file.js +33 -3
- package/dist/utils/file.js.map +1 -1
- package/dist/utils/gpg.d.ts.map +1 -1
- package/dist/utils/gpg.js +97 -53
- package/dist/utils/gpg.js.map +1 -1
- package/dist/utils/jq.d.ts.map +1 -1
- package/dist/utils/jq.js +6 -9
- package/dist/utils/jq.js.map +1 -1
- package/dist/utils/npm.d.ts.map +1 -1
- package/dist/utils/npm.js +2 -6
- package/dist/utils/npm.js.map +1 -1
- package/dist/utils/shell.d.ts +15 -2
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +216 -12
- package/dist/utils/shell.js.map +1 -1
- package/package.json +3 -4
- package/skill/SKILL.md +77 -11
- package/skill/VERSION +1 -0
- package/skill/references/integration/flow.md +154 -136
- package/skill/references/integration/modules/alipay-sdk-reminder.md +29 -13
- package/skill/references/integration/modules/checklist.md +49 -19
- package/skill/references/integration/modules/code-examples/csharp/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +3 -3
- package/skill/references/integration/modules/code-examples/csharp/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.cs +27 -13
- package/skill/references/integration/modules/code-examples/java/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +3 -3
- package/skill/references/integration/modules/code-examples/java/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemoController.java +31 -12
- package/skill/references/integration/modules/code-examples/nodejs/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +5 -4
- package/skill/references/integration/modules/code-examples/nodejs/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.js +69 -37
- package/skill/references/integration/modules/code-examples/php/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +3 -3
- package/skill/references/integration/modules/code-examples/php/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.php +38 -17
- package/skill/references/integration/modules/code-examples/python/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +3 -3
- package/skill/references/integration/modules/code-examples/python/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.py +31 -18
- package/skill/references/integration/modules/interface-guide.md +7 -3
- package/skill/references/integration/modules/sandbox/a2m-sandbox-test.md +77 -48
- package/skill/references/integration/modules/sandbox/alipay-sandbox-tool.md +8 -5
- package/skill/references/integration/modules/sandbox/sandbox-setup-guide.md +157 -117
- package/skill/references/integration/modules/scripts/local_402_sandbox_pay.py +35 -4
- package/skill/references/normal/alipay-cli-env.md +13 -11
- package/skill/references/normal/self-update.md +59 -0
- package/skill/references/onboarding/flow.md +202 -282
- package/skill/references/onboarding/modules/app-release.md +91 -35
- package/skill/references/onboarding/modules/authorization.md +49 -22
- package/skill/references/onboarding/modules/error-handling.md +19 -14
- package/skill/references/onboarding/modules/product-sign.md +23 -20
- package/skill/references/onboarding/modules/scripts/app.sh +139 -40
- package/skill/references/onboarding/modules/scripts/auth.sh +183 -101
- package/skill/references/onboarding/modules/scripts/error_handler.sh +148 -18
- package/skill/references/onboarding/modules/scripts/query_sign_status.sh +17 -11
- package/skill/references/onboarding/modules/scripts/service.sh +208 -37
- package/skill/references/onboarding/modules/scripts/upload_screenshots.sh +1 -1
- package/skill/references/onboarding/modules/service-registration.md +72 -29
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A2M 智能收产品接入示例 - Node.js 版本
|
|
3
|
+
*
|
|
4
|
+
* 本示例展示 A2M 核心协议与 SDK 调用流程。
|
|
5
|
+
* 订单持久化、本地订单匹配、金额一致性、资源防串、幂等履约和失败重试
|
|
6
|
+
* 需要结合商户实际数据库与订单模型实现。在上述控制完成实现并通过 checklist 前,
|
|
7
|
+
* 禁止将本文件原样用于生产或判定为生产就绪。
|
|
3
8
|
*
|
|
4
|
-
*
|
|
9
|
+
* 本文件演示 A2M 核心协议调用流程:
|
|
5
10
|
* 1. 返回 402 Payment-Needed Header
|
|
6
11
|
* 2. 验证 Payment-Proof 支付凭证
|
|
7
12
|
* 3. 发送履约回执确认
|
|
8
13
|
* 4. 返回资源内容
|
|
9
14
|
*
|
|
10
15
|
* 依赖安装:
|
|
11
|
-
* npm install express alipay-sdk
|
|
16
|
+
* npm install express alipay-sdk
|
|
12
17
|
*/
|
|
13
18
|
|
|
14
19
|
const express = require('express');
|
|
@@ -18,17 +23,17 @@ const crypto = require('crypto');
|
|
|
18
23
|
const app = express();
|
|
19
24
|
const PORT = 3000;
|
|
20
25
|
|
|
21
|
-
// ====================
|
|
26
|
+
// ==================== 配置信息(实际使用时请从受保护的配置读取)====================
|
|
22
27
|
const CONFIG = {
|
|
23
28
|
// 支付宝配置
|
|
24
29
|
alipay: {
|
|
25
|
-
appId: '<APP_ID>',
|
|
26
|
-
privateKey: '<APP_PRIVATE_KEY>', //
|
|
27
|
-
alipayPublicKey: '<ALIPAY_PUBLIC_KEY>', //
|
|
30
|
+
appId: '<APP_ID>', // 运行时映射沙箱 appId
|
|
31
|
+
privateKey: '<APP_PRIVATE_KEY>', // 运行时映射 appPrivatePkcsKey(PKCS#1)
|
|
32
|
+
alipayPublicKey: '<ALIPAY_PUBLIC_KEY>', // 运行时映射 alipayPublicKey
|
|
28
33
|
gateway: 'https://openapi.alipay.com/gateway.do',
|
|
29
34
|
sellerId: '<SELLER_ID_2088>', // 商户 ID(2088 格式)
|
|
30
|
-
serviceId: '
|
|
31
|
-
merchantPrivateKey: '<APP_PRIVATE_KEY>' //
|
|
35
|
+
serviceId: 'api_mock_service_id', // 仅用于沙箱联调;上线前替换为服务市场真实 serviceId
|
|
36
|
+
merchantPrivateKey: '<APP_PRIVATE_KEY>' // 与 privateKey 使用同一运行时配置,用于商家签名
|
|
32
37
|
},
|
|
33
38
|
// 资源服务配置
|
|
34
39
|
resource: {
|
|
@@ -43,6 +48,7 @@ const alipaySdk = new AlipaySdk({
|
|
|
43
48
|
privateKey: CONFIG.alipay.privateKey,
|
|
44
49
|
alipayPublicKey: CONFIG.alipay.alipayPublicKey,
|
|
45
50
|
gateway: CONFIG.alipay.gateway,
|
|
51
|
+
timeout: 30000,
|
|
46
52
|
});
|
|
47
53
|
|
|
48
54
|
// ==================== 工具方法 ====================
|
|
@@ -74,11 +80,16 @@ function generateSellerSignature(params, privateKey) {
|
|
|
74
80
|
})
|
|
75
81
|
.join('&');
|
|
76
82
|
|
|
77
|
-
// 3.
|
|
83
|
+
// 3. 将裸 PKCS#1 Base64 临时解析为 DER 密钥对象,不修改原始配置。
|
|
84
|
+
const sellerPrivateKey = crypto.createPrivateKey({
|
|
85
|
+
key: Buffer.from(privateKey, 'base64'),
|
|
86
|
+
format: 'der',
|
|
87
|
+
type: 'pkcs1'
|
|
88
|
+
});
|
|
78
89
|
const sign = crypto
|
|
79
90
|
.createSign('RSA-SHA256')
|
|
80
91
|
.update(signContent, 'utf8')
|
|
81
|
-
.sign(
|
|
92
|
+
.sign(sellerPrivateKey, 'base64');
|
|
82
93
|
|
|
83
94
|
return sign;
|
|
84
95
|
}
|
|
@@ -132,7 +143,7 @@ function base64UrlDecode(str) {
|
|
|
132
143
|
/**
|
|
133
144
|
* 智能收产品统一接口
|
|
134
145
|
*
|
|
135
|
-
*
|
|
146
|
+
* 核心协议流程演示:
|
|
136
147
|
* 1. 不带 Payment-Proof Header:返回 HTTP 402 + Payment-Needed Header
|
|
137
148
|
* 2. 带 Payment-Proof Header:验证支付 → 自动履约 → 返回资源
|
|
138
149
|
*
|
|
@@ -283,24 +294,37 @@ async function verifyPaymentAndDeliverResource(req, res, paymentProof) {
|
|
|
283
294
|
});
|
|
284
295
|
|
|
285
296
|
// 3. 验证失败,返回错误
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
const responseData =
|
|
289
|
-
|
|
290
|
-
|
|
297
|
+
// SDK 可能返回嵌套响应,也可能直接返回业务字段。
|
|
298
|
+
const nestedResponse = verifyResponse.alipay_aipay_agent_payment_verify_response;
|
|
299
|
+
const responseData = nestedResponse || verifyResponse;
|
|
300
|
+
|
|
301
|
+
if (responseData.code !== '10000' && responseData.code !== 10000) {
|
|
302
|
+
const errorCode = responseData.sub_code || responseData.errorCode;
|
|
303
|
+
const errorMessage = responseData.sub_msg || responseData.msg || '支付凭证验证失败';
|
|
304
|
+
console.error('支付凭证验证失败:', errorMessage);
|
|
291
305
|
return res.status(400).json({
|
|
292
|
-
code:
|
|
293
|
-
message:
|
|
306
|
+
code: errorCode,
|
|
307
|
+
message: errorMessage
|
|
294
308
|
});
|
|
295
309
|
}
|
|
296
310
|
|
|
297
311
|
// 4. 验证成功,获取订单信息
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
312
|
+
// 同时兼容 SDK 的 snake_case 与 camelCase 业务字段。
|
|
313
|
+
const verifyTradeNo = responseData.trade_no || responseData.tradeNo || '';
|
|
314
|
+
const verifyOutTradeNo = responseData.out_trade_no || responseData.outTradeNo || '';
|
|
315
|
+
// 当前 demo 在沙箱字段为空时回退到固定资源;生产实现必须从本地订单读取并校验资源 ID。
|
|
316
|
+
const resourceIdVerified = responseData.resource_id || responseData.resourceId || CONFIG.resource.path;
|
|
317
|
+
const active = responseData.active;
|
|
318
|
+
|
|
319
|
+
if (process.env.A2M_DEBUG === 'true') {
|
|
320
|
+
console.log('验付响应字段:', {
|
|
321
|
+
code: responseData.code,
|
|
322
|
+
tradeNo: verifyTradeNo,
|
|
323
|
+
outTradeNo: verifyOutTradeNo,
|
|
324
|
+
resourceId: resourceIdVerified,
|
|
325
|
+
active
|
|
326
|
+
});
|
|
327
|
+
}
|
|
304
328
|
|
|
305
329
|
console.log(`支付凭证验证成功:tradeNo=${verifyTradeNo}, outTradeNo=${verifyOutTradeNo}`);
|
|
306
330
|
|
|
@@ -323,7 +347,7 @@ async function verifyPaymentAndDeliverResource(req, res, paymentProof) {
|
|
|
323
347
|
// }
|
|
324
348
|
|
|
325
349
|
// 7. 【TODO】资源防串校验
|
|
326
|
-
// if (
|
|
350
|
+
// if (resourceIdVerified !== order.resourceId) {
|
|
327
351
|
// return res.status(403).json({
|
|
328
352
|
// code: 'RESOURCE_ID_MISMATCH',
|
|
329
353
|
// message: '资源 ID 不匹配,可能存在资源串改风险'
|
|
@@ -340,7 +364,7 @@ async function verifyPaymentAndDeliverResource(req, res, paymentProof) {
|
|
|
340
364
|
// }
|
|
341
365
|
|
|
342
366
|
// 9. 执行业务逻辑,生成资源内容
|
|
343
|
-
const serviceResult = generateServiceResource(
|
|
367
|
+
const serviceResult = generateServiceResource(resourceIdVerified);
|
|
344
368
|
|
|
345
369
|
// 10. 【TODO】履约记录落库(用于审计/售后/对账)
|
|
346
370
|
// await fulfillmentRecordRepository.save({ ... });
|
|
@@ -356,10 +380,11 @@ async function verifyPaymentAndDeliverResource(req, res, paymentProof) {
|
|
|
356
380
|
// serviceResult
|
|
357
381
|
// });
|
|
358
382
|
|
|
359
|
-
console.log(`资源已生成,准备发送履约确认:outTradeNo=${verifyOutTradeNo}, tradeNo=${verifyTradeNo}`);
|
|
360
|
-
|
|
361
383
|
// 12. 发送履约确认到支付宝,确认成功后才返回成功交付
|
|
362
|
-
const
|
|
384
|
+
const fulfillmentTradeNo = verifyTradeNo || tradeNo;
|
|
385
|
+
console.log(`资源已生成,准备发送履约确认:outTradeNo=${verifyOutTradeNo}, tradeNo=${fulfillmentTradeNo}`);
|
|
386
|
+
|
|
387
|
+
const fulfillmentConfirmed = await sendFulfillmentConfirm(fulfillmentTradeNo);
|
|
363
388
|
if (!fulfillmentConfirmed) {
|
|
364
389
|
return res.status(502).json({
|
|
365
390
|
code: 'FULFILLMENT_CONFIRM_FAILED',
|
|
@@ -373,14 +398,14 @@ async function verifyPaymentAndDeliverResource(req, res, paymentProof) {
|
|
|
373
398
|
// fulfilledAt: new Date()
|
|
374
399
|
// });
|
|
375
400
|
|
|
376
|
-
console.log(`履约确认成功:outTradeNo=${verifyOutTradeNo}, tradeNo=${
|
|
401
|
+
console.log(`履约确认成功:outTradeNo=${verifyOutTradeNo}, tradeNo=${fulfillmentTradeNo}`);
|
|
377
402
|
|
|
378
403
|
// 13. 构造 Payment-Validation Header
|
|
379
404
|
const paymentValidation = {
|
|
380
|
-
trade_no:
|
|
405
|
+
trade_no: fulfillmentTradeNo,
|
|
381
406
|
out_trade_no: verifyOutTradeNo,
|
|
382
407
|
validated: true,
|
|
383
|
-
resource_id:
|
|
408
|
+
resource_id: resourceIdVerified
|
|
384
409
|
};
|
|
385
410
|
|
|
386
411
|
const paymentValidationEncoded = base64UrlEncode(JSON.stringify(paymentValidation));
|
|
@@ -388,9 +413,9 @@ async function verifyPaymentAndDeliverResource(req, res, paymentProof) {
|
|
|
388
413
|
|
|
389
414
|
// 14. 返回资源内容
|
|
390
415
|
res.json({
|
|
391
|
-
resource_id:
|
|
416
|
+
resource_id: resourceIdVerified,
|
|
392
417
|
content: serviceResult,
|
|
393
|
-
trade_no:
|
|
418
|
+
trade_no: fulfillmentTradeNo,
|
|
394
419
|
out_trade_no: verifyOutTradeNo,
|
|
395
420
|
already_fulfilled: false,
|
|
396
421
|
fulfillment_confirmed: true
|
|
@@ -422,6 +447,11 @@ function generateServiceResource(resourceId) {
|
|
|
422
447
|
* 发送履约确认
|
|
423
448
|
*/
|
|
424
449
|
async function sendFulfillmentConfirm(tradeNo) {
|
|
450
|
+
if (!tradeNo) {
|
|
451
|
+
console.error('履约确认失败:tradeNo 为空');
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
|
|
425
455
|
try {
|
|
426
456
|
console.log(`开始发送履约确认:tradeNo=${tradeNo}`);
|
|
427
457
|
|
|
@@ -433,11 +463,13 @@ async function sendFulfillmentConfirm(tradeNo) {
|
|
|
433
463
|
|
|
434
464
|
// 注意:SDK 返回的响应可能是扁平结构,也可能嵌套在响应键下,需兼容两种情况
|
|
435
465
|
const responseData = response.alipay_aipay_agent_fulfillment_confirm_response || response;
|
|
436
|
-
if (responseData.code === '10000') {
|
|
466
|
+
if (responseData.code === '10000' || responseData.code === 10000) {
|
|
437
467
|
console.log(`履约确认成功:tradeNo=${tradeNo}`);
|
|
438
468
|
return true;
|
|
439
469
|
} else {
|
|
440
|
-
|
|
470
|
+
const errorCode = responseData.sub_code || responseData.errorCode;
|
|
471
|
+
const errorMessage = responseData.sub_msg || responseData.msg || '履约确认失败';
|
|
472
|
+
console.error(`履约确认失败:tradeNo=${tradeNo}, errorCode=${errorCode}, errorMsg=${errorMessage}`);
|
|
441
473
|
return false;
|
|
442
474
|
}
|
|
443
475
|
|
|
@@ -451,7 +483,7 @@ async function sendFulfillmentConfirm(tradeNo) {
|
|
|
451
483
|
|
|
452
484
|
if (require.main === module) {
|
|
453
485
|
app.listen(PORT, () => {
|
|
454
|
-
console.log(`A2M
|
|
486
|
+
console.log(`A2M 按量付费服务已启动:http://localhost:${PORT}${CONFIG.resource.path}`);
|
|
455
487
|
console.log('测试步骤:');
|
|
456
488
|
console.log('1. 无 Payment-Proof Header: curl http://localhost:3000/demo/a2m/resource');
|
|
457
489
|
console.log('2. 有 Payment-Proof Header: curl -H "Payment-Proof: <value>" http://localhost:3000/demo/a2m/resource');
|
|
@@ -20,8 +20,8 @@ $aop->signType = 'RSA2';
|
|
|
20
20
|
|
|
21
21
|
// 构造请求参数
|
|
22
22
|
$request = new AlipayTradePagePayRequest();
|
|
23
|
-
$request->setNotifyUrl('https://your-domain.com/notify'); //
|
|
24
|
-
$request->setReturnUrl('https://your-domain.com/return'); //
|
|
23
|
+
$request->setNotifyUrl('https://your-domain.com/notify'); // 真实生产或公网联调时传入;本地生产参数验收模式可临时不传,但仍需实现通知处理代码
|
|
24
|
+
$request->setReturnUrl('https://your-domain.com/return'); // 默认传入并替换为已验证结果页;仅用户明确关闭同步回跳时省略
|
|
25
25
|
|
|
26
26
|
$bizContent = [
|
|
27
27
|
'out_trade_no' => date('YmdHis') . mt_rand(1000, 9999), // 商户订单号
|
|
@@ -53,4 +53,4 @@ echo $response;
|
|
|
53
53
|
<input type="submit" value="立即支付" style="display:none" >
|
|
54
54
|
</form>
|
|
55
55
|
<script>document.forms[0].submit();</script>
|
|
56
|
-
```
|
|
56
|
+
```
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
<?php
|
|
2
2
|
/**
|
|
3
3
|
* A2M 智能收产品接入示例 - PHP 版本
|
|
4
|
+
*
|
|
5
|
+
* 本示例展示 A2M 核心协议与 SDK 调用流程。
|
|
6
|
+
* 订单持久化、本地订单匹配、金额一致性、资源防串、幂等履约和失败重试
|
|
7
|
+
* 需要结合商户实际数据库与订单模型实现。在上述控制完成实现并通过 checklist 前,
|
|
8
|
+
* 禁止将本文件原样用于生产或判定为生产就绪。
|
|
4
9
|
*
|
|
5
|
-
*
|
|
10
|
+
* 本文件演示 A2M 核心协议调用流程:
|
|
6
11
|
* 1. 返回 402 Payment-Needed Header
|
|
7
12
|
* 2. 验证 Payment-Proof 支付凭证
|
|
8
13
|
* 3. 发送履约回执确认
|
|
@@ -19,7 +24,7 @@ define('ALIPAY_CONFIG', [
|
|
|
19
24
|
'alipayPublicKey' => '<ALIPAY_PUBLIC_KEY>', // 请填写您的支付宝公钥
|
|
20
25
|
'gateway' => 'https://openapi.alipay.com/gateway.do',
|
|
21
26
|
'sellerId' => '<SELLER_ID_2088>', // 商户 ID(2088 格式)
|
|
22
|
-
'serviceId' => '
|
|
27
|
+
'serviceId' => 'api_mock_service_id', // 仅用于沙箱联调;上线前替换为服务市场真实 serviceId
|
|
23
28
|
'merchantPrivateKey' => '<APP_PRIVATE_KEY>' // 请填写您的应用私钥(用于商家签名)
|
|
24
29
|
]);
|
|
25
30
|
|
|
@@ -73,9 +78,17 @@ function generateSellerSignature($params, $privateKey) {
|
|
|
73
78
|
}
|
|
74
79
|
$signString = implode('&', $signContent);
|
|
75
80
|
|
|
76
|
-
// 3.
|
|
77
|
-
$
|
|
78
|
-
|
|
81
|
+
// 3. OpenSSL 需要 PEM;仅为本次签名临时包装裸 PKCS#1 Base64,不修改原始配置。
|
|
82
|
+
$privateKeyPem = "-----BEGIN RSA PRIVATE KEY-----\n"
|
|
83
|
+
. chunk_split($privateKey, 64, "\n")
|
|
84
|
+
. "-----END RSA PRIVATE KEY-----";
|
|
85
|
+
$privateKeyResource = openssl_pkey_get_private($privateKeyPem);
|
|
86
|
+
if ($privateKeyResource === false) {
|
|
87
|
+
throw new RuntimeException('无法解析 PKCS#1 应用私钥');
|
|
88
|
+
}
|
|
89
|
+
if (!openssl_sign($signString, $signature, $privateKeyResource, OPENSSL_ALGO_SHA256)) {
|
|
90
|
+
throw new RuntimeException('seller_signature 生成失败');
|
|
91
|
+
}
|
|
79
92
|
|
|
80
93
|
return base64_encode($signature);
|
|
81
94
|
}
|
|
@@ -118,7 +131,7 @@ function jsonResponse($data, $statusCode = 200, $headers = []) {
|
|
|
118
131
|
/**
|
|
119
132
|
* 智能收产品统一接口
|
|
120
133
|
*
|
|
121
|
-
*
|
|
134
|
+
* 核心协议流程演示:
|
|
122
135
|
* 1. 不带 Payment-Proof Header:返回 HTTP 402 + Payment-Needed Header
|
|
123
136
|
* 2. 带 Payment-Proof Header:验证支付 → 自动履约 → 返回资源
|
|
124
137
|
*/
|
|
@@ -279,9 +292,11 @@ function verifyPaymentAndDeliverResource($paymentProof) {
|
|
|
279
292
|
}
|
|
280
293
|
|
|
281
294
|
// 4. 验证成功,获取订单信息
|
|
282
|
-
$verifyTradeNo = $verifyResponse->trade_no ??
|
|
283
|
-
$verifyOutTradeNo = $verifyResponse->out_trade_no ??
|
|
284
|
-
$resourceId = $verifyResponse->resource_id ??
|
|
295
|
+
$verifyTradeNo = $verifyResponse->trade_no ?? $verifyResponse->tradeNo ?? '';
|
|
296
|
+
$verifyOutTradeNo = $verifyResponse->out_trade_no ?? $verifyResponse->outTradeNo ?? '';
|
|
297
|
+
$resourceId = $verifyResponse->resource_id ?? $verifyResponse->resourceId ?? '';
|
|
298
|
+
// 当前 demo 在沙箱字段为空时回退到固定资源;生产实现必须从本地订单读取并校验资源 ID。
|
|
299
|
+
$resourceIdVerified = !empty($resourceId) ? $resourceId : RESOURCE_CONFIG['path'];
|
|
285
300
|
$active = $verifyResponse->active ?? null;
|
|
286
301
|
|
|
287
302
|
error_log("支付凭证验证成功:tradeNo={$verifyTradeNo}, outTradeNo={$verifyOutTradeNo}");
|
|
@@ -322,7 +337,7 @@ function verifyPaymentAndDeliverResource($paymentProof) {
|
|
|
322
337
|
// }
|
|
323
338
|
|
|
324
339
|
// 9. 执行业务逻辑,生成资源内容
|
|
325
|
-
$serviceResult = generateServiceResource($
|
|
340
|
+
$serviceResult = generateServiceResource($resourceIdVerified);
|
|
326
341
|
|
|
327
342
|
// 10. 【TODO】履约记录落库(用于审计/售后/对账)
|
|
328
343
|
// $fulfillmentRecordRepository->save([...]);
|
|
@@ -338,10 +353,11 @@ function verifyPaymentAndDeliverResource($paymentProof) {
|
|
|
338
353
|
// 'serviceResult' => $serviceResult
|
|
339
354
|
// ]);
|
|
340
355
|
|
|
341
|
-
|
|
356
|
+
$fulfillmentTradeNo = $verifyTradeNo ?: $tradeNo;
|
|
357
|
+
error_log("资源已生成,准备发送履约确认:outTradeNo={$verifyOutTradeNo}, tradeNo={$fulfillmentTradeNo}");
|
|
342
358
|
|
|
343
359
|
// 12. 发送履约确认到支付宝,确认成功后才返回成功交付
|
|
344
|
-
if (!sendFulfillmentConfirm($
|
|
360
|
+
if (!sendFulfillmentConfirm($fulfillmentTradeNo)) {
|
|
345
361
|
jsonResponse([
|
|
346
362
|
'code' => 'FULFILLMENT_CONFIRM_FAILED',
|
|
347
363
|
'message' => '资源已生成但履约确认失败,请稍后使用同一 Payment-Proof 重试'
|
|
@@ -354,23 +370,23 @@ function verifyPaymentAndDeliverResource($paymentProof) {
|
|
|
354
370
|
// 'fulfilledAt' => date('c')
|
|
355
371
|
// ]);
|
|
356
372
|
|
|
357
|
-
error_log("履约确认成功:outTradeNo={$verifyOutTradeNo}, tradeNo={$
|
|
373
|
+
error_log("履约确认成功:outTradeNo={$verifyOutTradeNo}, tradeNo={$fulfillmentTradeNo}");
|
|
358
374
|
|
|
359
375
|
// 13. 构造 Payment-Validation Header
|
|
360
376
|
$paymentValidation = [
|
|
361
|
-
'trade_no' => $
|
|
377
|
+
'trade_no' => $fulfillmentTradeNo,
|
|
362
378
|
'out_trade_no' => $verifyOutTradeNo,
|
|
363
379
|
'validated' => true,
|
|
364
|
-
'resource_id' => $
|
|
380
|
+
'resource_id' => $resourceIdVerified
|
|
365
381
|
];
|
|
366
382
|
|
|
367
383
|
$paymentValidationEncoded = base64UrlEncode(json_encode($paymentValidation, JSON_UNESCAPED_UNICODE));
|
|
368
384
|
|
|
369
385
|
// 14. 返回资源内容
|
|
370
386
|
jsonResponse([
|
|
371
|
-
'resource_id' => $
|
|
387
|
+
'resource_id' => $resourceIdVerified,
|
|
372
388
|
'content' => $serviceResult,
|
|
373
|
-
'trade_no' => $
|
|
389
|
+
'trade_no' => $fulfillmentTradeNo,
|
|
374
390
|
'out_trade_no' => $verifyOutTradeNo,
|
|
375
391
|
'already_fulfilled' => false,
|
|
376
392
|
'fulfillment_confirmed' => true
|
|
@@ -402,6 +418,11 @@ function generateServiceResource($resourceId) {
|
|
|
402
418
|
* 发送履约确认
|
|
403
419
|
*/
|
|
404
420
|
function sendFulfillmentConfirm($tradeNo) {
|
|
421
|
+
if (empty($tradeNo)) {
|
|
422
|
+
error_log('履约确认失败:tradeNo 为空');
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
|
|
405
426
|
try {
|
|
406
427
|
error_log("开始发送履约确认:tradeNo={$tradeNo}");
|
|
407
428
|
|
|
@@ -20,8 +20,8 @@ config.sign_type = "RSA2"
|
|
|
20
20
|
client = DefaultAlipayClient(alipay_client_config=config)
|
|
21
21
|
|
|
22
22
|
request = AlipayTradePagePayRequest()
|
|
23
|
-
request.notify_url = "https://your-domain.com/notify" #
|
|
24
|
-
request.return_url = "https://your-domain.com/return" #
|
|
23
|
+
request.notify_url = "https://your-domain.com/notify" # 真实生产或公网联调时传入;本地生产参数验收模式可临时不传,但仍需实现通知处理代码
|
|
24
|
+
request.return_url = "https://your-domain.com/return" # 默认传入并替换为已验证结果页;仅用户明确关闭同步回跳时省略
|
|
25
25
|
|
|
26
26
|
model = AlipayTradePagePayModel()
|
|
27
27
|
model.out_trade_no = "MERCHANT_ORDER_NO_PLACEHOLDER" # 商户订单号,必填
|
|
@@ -45,4 +45,4 @@ print(form_html)
|
|
|
45
45
|
<input type="submit" value="立即支付" style="display:none" >
|
|
46
46
|
</form>
|
|
47
47
|
<script>document.forms[0].submit();</script>
|
|
48
|
-
```
|
|
48
|
+
```
|
|
@@ -3,14 +3,19 @@
|
|
|
3
3
|
"""
|
|
4
4
|
A2M 智能收产品接入示例 - Python 版本
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
本示例展示 A2M 核心协议与 SDK 调用流程。
|
|
7
|
+
订单持久化、本地订单匹配、金额一致性、资源防串、幂等履约和失败重试
|
|
8
|
+
需要结合商户实际数据库与订单模型实现。在上述控制完成实现并通过 checklist 前,
|
|
9
|
+
禁止将本文件原样用于生产或判定为生产就绪。
|
|
10
|
+
|
|
11
|
+
本文件演示 A2M 核心协议调用流程:
|
|
7
12
|
1. 返回 402 Payment-Needed Header
|
|
8
13
|
2. 验证 Payment-Proof 支付凭证
|
|
9
14
|
3. 发送履约回执确认
|
|
10
15
|
4. 返回资源内容
|
|
11
16
|
|
|
12
17
|
依赖安装:
|
|
13
|
-
pip install flask alipay-sdk-python
|
|
18
|
+
pip install flask alipay-sdk-python pycryptodome
|
|
14
19
|
|
|
15
20
|
注意:Python SDK 3.7+ 版本必须使用 Model 类(如 AlipayAipayAgentPaymentVerifyModel),
|
|
16
21
|
不能直接使用 dict 赋值给 biz_model,因为 SDK 会调用 to_alipay_dict() 方法。
|
|
@@ -41,7 +46,7 @@ ALIPAY_CONFIG = {
|
|
|
41
46
|
'alipayPublicKey': '<ALIPAY_PUBLIC_KEY>', # 请填写您的支付宝公钥
|
|
42
47
|
'gateway': 'https://openapi.alipay.com/gateway.do',
|
|
43
48
|
'sellerId': '<SELLER_ID_2088>', # 商户 ID(2088 格式)
|
|
44
|
-
'serviceId': '
|
|
49
|
+
'serviceId': 'api_mock_service_id', # 仅用于沙箱联调;上线前替换为服务市场真实 serviceId
|
|
45
50
|
'merchantPrivateKey': '<APP_PRIVATE_KEY>' # 请填写您的应用私钥(用于商家签名)
|
|
46
51
|
}
|
|
47
52
|
|
|
@@ -96,8 +101,9 @@ def generate_seller_signature(params, private_key):
|
|
|
96
101
|
|
|
97
102
|
sign_string = '&'.join(sign_content)
|
|
98
103
|
|
|
99
|
-
# 3.
|
|
100
|
-
|
|
104
|
+
# 3. 将裸 PKCS#1 Base64 临时解码为 DER 密钥对象,不修改原始配置。
|
|
105
|
+
key_der = base64.b64decode(private_key, validate=True)
|
|
106
|
+
key = RSA.import_key(key_der)
|
|
101
107
|
h = SHA256.new(sign_string.encode('utf-8'))
|
|
102
108
|
signature = pkcs1_15.new(key).sign(h)
|
|
103
109
|
|
|
@@ -143,7 +149,7 @@ def handle_resource():
|
|
|
143
149
|
"""
|
|
144
150
|
智能收产品统一接口
|
|
145
151
|
|
|
146
|
-
|
|
152
|
+
核心协议流程演示:
|
|
147
153
|
1. 不带 Payment-Proof Header:返回 HTTP 402 + Payment-Needed Header
|
|
148
154
|
2. 带 Payment-Proof Header:验证支付 → 自动履约 → 返回资源
|
|
149
155
|
"""
|
|
@@ -306,9 +312,11 @@ def verify_payment_and_deliver_resource(payment_proof):
|
|
|
306
312
|
}, status_code=400)
|
|
307
313
|
|
|
308
314
|
# 4. 验证成功,获取订单信息
|
|
309
|
-
verify_trade_no = response_data.get('trade_no')
|
|
310
|
-
verify_out_trade_no = response_data.get('out_trade_no')
|
|
311
|
-
resource_id = response_data.get('resource_id')
|
|
315
|
+
verify_trade_no = response_data.get('trade_no') or response_data.get('tradeNo') or ''
|
|
316
|
+
verify_out_trade_no = response_data.get('out_trade_no') or response_data.get('outTradeNo') or ''
|
|
317
|
+
resource_id = response_data.get('resource_id') or response_data.get('resourceId') or ''
|
|
318
|
+
# 当前 demo 在沙箱字段为空时回退到固定资源;生产实现必须从本地订单读取并校验资源 ID。
|
|
319
|
+
resource_id_verified = resource_id or RESOURCE_CONFIG['path']
|
|
312
320
|
active = response_data.get('active')
|
|
313
321
|
|
|
314
322
|
print(f"支付凭证验证成功:tradeNo={verify_trade_no}, outTradeNo={verify_out_trade_no}")
|
|
@@ -345,7 +353,7 @@ def verify_payment_and_deliver_resource(payment_proof):
|
|
|
345
353
|
# }, status_code=200)
|
|
346
354
|
|
|
347
355
|
# 9. 生成资源内容
|
|
348
|
-
service_result = generate_service_resource(
|
|
356
|
+
service_result = generate_service_resource(resource_id_verified)
|
|
349
357
|
|
|
350
358
|
# 10. 【TODO】履约记录落库
|
|
351
359
|
# fulfillment_record_repository.save({...})
|
|
@@ -361,11 +369,12 @@ def verify_payment_and_deliver_resource(payment_proof):
|
|
|
361
369
|
# 'service_result': service_result
|
|
362
370
|
# })
|
|
363
371
|
|
|
364
|
-
|
|
372
|
+
fulfillment_trade_no = verify_trade_no or trade_no
|
|
373
|
+
print(f"资源已生成,准备发送履约确认:outTradeNo={verify_out_trade_no}, tradeNo={fulfillment_trade_no}")
|
|
365
374
|
|
|
366
375
|
# 12. 发送履约确认到支付宝,确认成功后才返回成功交付
|
|
367
376
|
# 注意:同样需要使用 Model 类
|
|
368
|
-
if not send_fulfillment_confirm(
|
|
377
|
+
if not send_fulfillment_confirm(fulfillment_trade_no):
|
|
369
378
|
return json_response({
|
|
370
379
|
'code': 'FULFILLMENT_CONFIRM_FAILED',
|
|
371
380
|
'message': '资源已生成但履约确认失败,请稍后使用同一 Payment-Proof 重试'
|
|
@@ -377,23 +386,23 @@ def verify_payment_and_deliver_resource(payment_proof):
|
|
|
377
386
|
# 'fulfilled_at': datetime.now(timezone.utc).astimezone().isoformat()
|
|
378
387
|
# })
|
|
379
388
|
|
|
380
|
-
print(f"履约确认成功:outTradeNo={verify_out_trade_no}, tradeNo={
|
|
389
|
+
print(f"履约确认成功:outTradeNo={verify_out_trade_no}, tradeNo={fulfillment_trade_no}")
|
|
381
390
|
|
|
382
391
|
# 13. 构造 Payment-Validation Header
|
|
383
392
|
payment_validation = {
|
|
384
|
-
'trade_no':
|
|
393
|
+
'trade_no': fulfillment_trade_no,
|
|
385
394
|
'out_trade_no': verify_out_trade_no,
|
|
386
395
|
'validated': True,
|
|
387
|
-
'resource_id':
|
|
396
|
+
'resource_id': resource_id_verified
|
|
388
397
|
}
|
|
389
398
|
|
|
390
399
|
payment_validation_encoded = base64url_encode(json.dumps(payment_validation, ensure_ascii=False))
|
|
391
400
|
|
|
392
401
|
# 14. 返回资源内容
|
|
393
402
|
return json_response({
|
|
394
|
-
'resource_id':
|
|
403
|
+
'resource_id': resource_id_verified,
|
|
395
404
|
'content': service_result,
|
|
396
|
-
'trade_no':
|
|
405
|
+
'trade_no': fulfillment_trade_no,
|
|
397
406
|
'out_trade_no': verify_out_trade_no,
|
|
398
407
|
'already_fulfilled': False,
|
|
399
408
|
'fulfillment_confirmed': True
|
|
@@ -420,6 +429,10 @@ def generate_service_resource(resource_id):
|
|
|
420
429
|
|
|
421
430
|
def send_fulfillment_confirm(trade_no):
|
|
422
431
|
"""发送履约确认"""
|
|
432
|
+
if not trade_no:
|
|
433
|
+
print("履约确认失败:tradeNo 为空")
|
|
434
|
+
return False
|
|
435
|
+
|
|
423
436
|
try:
|
|
424
437
|
print(f"开始发送履约确认:tradeNo={trade_no}")
|
|
425
438
|
|
|
@@ -457,7 +470,7 @@ def send_fulfillment_confirm(trade_no):
|
|
|
457
470
|
# ==================== 启动服务 ====================
|
|
458
471
|
|
|
459
472
|
if __name__ == '__main__':
|
|
460
|
-
print(f"A2M
|
|
473
|
+
print(f"A2M 按量付费服务已启动:http://localhost:5000{RESOURCE_CONFIG['path']}")
|
|
461
474
|
print('测试步骤:')
|
|
462
475
|
print(f'1. 无 Payment-Proof Header: curl http://localhost:5000{RESOURCE_CONFIG["path"]}')
|
|
463
476
|
print(f'2. 有 Payment-Proof Header: curl -H "Payment-Proof: <value>" http://localhost:5000{RESOURCE_CONFIG["path"]}')
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
| --- | --- | --- | --- | --- | --- | --- |
|
|
67
67
|
| 统一收单下单并支付页面接口 | [文档](https://ideservice.alipay.com/cms/site/0iztfv) | [示例](./code-examples/java/2-网站支付/统一收单下单并支付页面接口代码示例.md) | [示例](./code-examples/python/2-网站支付/统一收单下单并支付页面接口代码示例.md) | [示例](./code-examples/nodejs/2-网站支付/统一收单下单并支付页面接口代码示例.md) | [示例](./code-examples/php/2-网站支付/统一收单下单并支付页面接口代码示例.md) | [示例](./code-examples/csharp/2-网站支付/统一收单下单并支付页面接口代码示例.md) |
|
|
68
68
|
|
|
69
|
-
> **说明**:网站支付使用统一收单下单并支付页面接口 `alipay.trade.page.pay
|
|
69
|
+
> **说明**:网站支付使用统一收单下单并支付页面接口 `alipay.trade.page.pay`,适用于电脑网页与手机网页场景。上表仅是接口调用示例,实际集成不得止于设置 `return_url`;用户未明确关闭同步回跳时,必须继续按 `alipay-sdk-reminder.md` 的“网站支付同步回跳”实现并验证结果页。本地未上线场景的异步通知口径以 `../flow.md` 为准。
|
|
70
70
|
|
|
71
71
|
### APP支付 API
|
|
72
72
|
|
|
@@ -81,10 +81,14 @@
|
|
|
81
81
|
### 按量付费
|
|
82
82
|
- 使用 `4-按量付费/` 目录下的代码示例
|
|
83
83
|
- 按量付费**不需要**参考通用接口
|
|
84
|
-
-
|
|
85
|
-
-
|
|
84
|
+
- 五语言 A2M 示例用于说明 402、Payment-Needed、Payment-Proof、验付和履约确认的核心协议与 SDK 调用流程;关键生产控制仍可能是 TODO、注释占位、内存模拟或伪代码,因此禁止原样作为生产实现或据此宣称集成完成。
|
|
85
|
+
- 沙箱联调时 `serviceId` 固定使用 `api_mock_service_id`,该值是本 Skill 定义的沙箱开发值,不得改为其他占位符,也不得为继续沙箱测试向用户索要正式 `serviceId`。正式上线前必须替换为服务市场注册或复用结果中的真实 `serviceId`。
|
|
86
|
+
- 示例中的 `sellerId`、`appId`、`alipayPublicKey` 只能作为占位说明。正式集成时必须替换为签约入驻产物:`sellerId` 来自商户 PID/2088,`appId/alipayPublicKey` 来自应用发布或复用结果。
|
|
87
|
+
- 生成按量付费代码时必须补齐订单持久化、本地订单匹配、金额一致性、资源防串、幂等履约和履约确认失败可重试逻辑;不要把示例中的内存/占位实现当作生产实现。
|
|
86
88
|
|
|
87
89
|
### 网站支付 / APP支付
|
|
88
90
|
- 必须同时参考:
|
|
89
91
|
1. 对应产品的专用接口(如 `2-网站支付/` 或 `3-APP支付/`)
|
|
90
92
|
2. 通用接口 `1-通用接口/`(交易查询、退款、退款查询、关闭交易等)
|
|
93
|
+
- 完整集成必须覆盖产品下单、交易查询、退款、退款查询、关闭交易和异步通知处理代码。本地示例缺失时仍须依据上述已核验的官方文档实现,不得因缺少示例而省略适用接口。
|
|
94
|
+
- 若项目尚未上线,可先按 `../flow.md` 的“本地生产参数验收模式”完成本地结果确认链路验证;正式上线前仍必须补齐公网 HTTPS `notify_url`、验签、幂等和补偿查询。
|