@alipay/alipay-aipay 1.1.0 → 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.
Files changed (41) hide show
  1. package/dist/commands/install.js +1 -1
  2. package/dist/commands/install.js.map +1 -1
  3. package/dist/commands/update.js +1 -1
  4. package/dist/commands/update.js.map +1 -1
  5. package/dist/utils/alipay-cli.js +4 -4
  6. package/dist/utils/alipay-cli.js.map +1 -1
  7. package/package.json +3 -4
  8. package/skill/SKILL.md +75 -12
  9. package/skill/VERSION +1 -1
  10. package/skill/references/integration/flow.md +150 -108
  11. package/skill/references/integration/modules/alipay-sdk-reminder.md +29 -13
  12. package/skill/references/integration/modules/checklist.md +49 -19
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. package/skill/references/integration/modules/interface-guide.md +7 -3
  24. package/skill/references/integration/modules/sandbox/a2m-sandbox-test.md +76 -54
  25. package/skill/references/integration/modules/sandbox/alipay-sandbox-tool.md +3 -3
  26. package/skill/references/integration/modules/sandbox/sandbox-setup-guide.md +80 -24
  27. package/skill/references/integration/modules/scripts/local_402_sandbox_pay.py +35 -4
  28. package/skill/references/normal/alipay-cli-env.md +2 -2
  29. package/skill/references/normal/self-update.md +2 -2
  30. package/skill/references/onboarding/flow.md +193 -297
  31. package/skill/references/onboarding/modules/app-release.md +91 -35
  32. package/skill/references/onboarding/modules/authorization.md +19 -13
  33. package/skill/references/onboarding/modules/error-handling.md +19 -14
  34. package/skill/references/onboarding/modules/product-sign.md +22 -21
  35. package/skill/references/onboarding/modules/scripts/app.sh +139 -40
  36. package/skill/references/onboarding/modules/scripts/auth.sh +168 -99
  37. package/skill/references/onboarding/modules/scripts/error_handler.sh +148 -18
  38. package/skill/references/onboarding/modules/scripts/query_sign_status.sh +17 -11
  39. package/skill/references/onboarding/modules/scripts/service.sh +208 -37
  40. package/skill/references/onboarding/modules/scripts/upload_screenshots.sh +1 -1
  41. 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 crypto-js
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>', // 请填写您的应用私钥(PKCS#1 格式)
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: '<SERVICE_ID>', // 商户服务 ID
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. RSA2 签名
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(privateKey, 'base64');
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
- // 注意:SDK 返回的响应可能是扁平结构(code/trade_no 在顶层),
287
- // 也可能嵌套在 alipay_aipay_agent_payment_verify_response 键下,需兼容两种情况
288
- const responseData = verifyResponse.alipay_aipay_agent_payment_verify_response || verifyResponse;
289
- if (responseData.code !== '10000') {
290
- console.error('支付凭证验证失败:', responseData.sub_msg);
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: responseData.sub_code,
293
- message: responseData.sub_msg
306
+ code: errorCode,
307
+ message: errorMessage
294
308
  });
295
309
  }
296
310
 
297
311
  // 4. 验证成功,获取订单信息
298
- const {
299
- trade_no: verifyTradeNo,
300
- out_trade_no: verifyOutTradeNo,
301
- resource_id: resourceId,
302
- active
303
- } = responseData;
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 (resourceId !== order.resourceId) {
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(resourceId);
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 fulfillmentConfirmed = await sendFulfillmentConfirm(verifyTradeNo);
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=${verifyTradeNo}`);
401
+ console.log(`履约确认成功:outTradeNo=${verifyOutTradeNo}, tradeNo=${fulfillmentTradeNo}`);
377
402
 
378
403
  // 13. 构造 Payment-Validation Header
379
404
  const paymentValidation = {
380
- trade_no: verifyTradeNo,
405
+ trade_no: fulfillmentTradeNo,
381
406
  out_trade_no: verifyOutTradeNo,
382
407
  validated: true,
383
- resource_id: resourceId
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: resourceId,
416
+ resource_id: resourceIdVerified,
392
417
  content: serviceResult,
393
- trade_no: verifyTradeNo,
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
- console.error(`履约确认失败:tradeNo=${tradeNo}, errorCode=${responseData.sub_code}, errorMsg=${responseData.sub_msg}`);
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 智能收服务已启动:http://localhost:${PORT}${CONFIG.resource.path}`);
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' => '<SERVICE_ID>', // 商户服务 ID
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. RSA2 签名
77
- $privateKeyResource = openssl_pkey_get_private($privateKey);
78
- openssl_sign($signString, $signature, $privateKeyResource, OPENSSL_ALGO_SHA256);
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 ?? null;
283
- $verifyOutTradeNo = $verifyResponse->out_trade_no ?? null;
284
- $resourceId = $verifyResponse->resource_id ?? null;
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($resourceId);
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
- error_log("资源已生成,准备发送履约确认:outTradeNo={$verifyOutTradeNo}, tradeNo={$verifyTradeNo}");
356
+ $fulfillmentTradeNo = $verifyTradeNo ?: $tradeNo;
357
+ error_log("资源已生成,准备发送履约确认:outTradeNo={$verifyOutTradeNo}, tradeNo={$fulfillmentTradeNo}");
342
358
 
343
359
  // 12. 发送履约确认到支付宝,确认成功后才返回成功交付
344
- if (!sendFulfillmentConfirm($verifyTradeNo)) {
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={$verifyTradeNo}");
373
+ error_log("履约确认成功:outTradeNo={$verifyOutTradeNo}, tradeNo={$fulfillmentTradeNo}");
358
374
 
359
375
  // 13. 构造 Payment-Validation Header
360
376
  $paymentValidation = [
361
- 'trade_no' => $verifyTradeNo,
377
+ 'trade_no' => $fulfillmentTradeNo,
362
378
  'out_trade_no' => $verifyOutTradeNo,
363
379
  'validated' => true,
364
- 'resource_id' => $resourceId
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' => $resourceId,
387
+ 'resource_id' => $resourceIdVerified,
372
388
  'content' => $serviceResult,
373
- 'trade_no' => $verifyTradeNo,
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': '<SERVICE_ID>', # 商户服务 ID
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. RSA2 签名
100
- key = RSA.import_key(private_key)
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(resource_id)
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
- print(f"资源已生成,准备发送履约确认:outTradeNo={verify_out_trade_no}, tradeNo={verify_trade_no}")
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(verify_trade_no):
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={verify_trade_no}")
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': verify_trade_no,
393
+ 'trade_no': fulfillment_trade_no,
385
394
  'out_trade_no': verify_out_trade_no,
386
395
  'validated': True,
387
- 'resource_id': 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': resource_id,
403
+ 'resource_id': resource_id_verified,
395
404
  'content': service_result,
396
- 'trade_no': verify_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 智能收服务已启动:http://localhost:5000{RESOURCE_CONFIG['path']}")
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
- - 示例中的 `sellerId`、`serviceId`、`appId`、`alipayPublicKey` 只能作为占位说明。正式集成时必须替换为签约入驻产物:`sellerId` 来自商户 PID/2088,`serviceId` 来自服务市场注册或复用结果,`appId/alipayPublicKey` 来自应用发布或复用结果。
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`、验签、幂等和补偿查询。