@alipay/alipay-aipay 1.7.0 → 1.7.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/README.md +30 -0
- package/dist/commands/install-current.js +15 -7
- package/dist/commands/install.js +20 -16
- package/dist/commands/uninstall.js +8 -3
- package/dist/commands/update.js +34 -16
- package/dist/index.js +10 -4
- package/dist/utils/channels.js +67 -0
- package/dist/utils/config.js +15 -11
- package/dist/utils/file.js +23 -10
- package/dist/utils/npm.js +19 -33
- package/package.json +8 -3
- package/skill/SKILL.md +2 -0
- package/skill/references/normal/json/release-channel.json +1 -0
- package/skill/references/normal/scripts/self_update.mjs +62 -17
- package/skill/references/normal/self-update.md +3 -1
- package/skill-pre/SKILL.md +65 -0
- package/skill-pre/VERSION +1 -0
- package/skill-pre/references/integration/flow.md +602 -0
- package/skill-pre/references/integration/modules/alipay-sdk-reminder.md +281 -0
- package/skill-pre/references/integration/modules/checklist.md +154 -0
- package/skill-pre/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +199 -0
- package/skill-pre/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/345/205/263/351/227/255/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +68 -0
- package/skill-pre/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +83 -0
- package/skill-pre/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +80 -0
- package/skill-pre/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +73 -0
- package/skill-pre/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 +62 -0
- package/skill-pre/references/integration/modules/code-examples/csharp/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +132 -0
- package/skill-pre/references/integration/modules/code-examples/csharp/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.md +640 -0
- package/skill-pre/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +184 -0
- package/skill-pre/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/345/205/263/351/227/255/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +72 -0
- package/skill-pre/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +96 -0
- package/skill-pre/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +81 -0
- package/skill-pre/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +77 -0
- package/skill-pre/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 +69 -0
- package/skill-pre/references/integration/modules/code-examples/java/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +138 -0
- package/skill-pre/references/integration/modules/code-examples/java/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemoController.md +623 -0
- package/skill-pre/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +135 -0
- package/skill-pre/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/345/205/263/351/227/255/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +51 -0
- package/skill-pre/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +63 -0
- package/skill-pre/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +59 -0
- package/skill-pre/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +54 -0
- package/skill-pre/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 +84 -0
- package/skill-pre/references/integration/modules/code-examples/nodejs/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +71 -0
- package/skill-pre/references/integration/modules/code-examples/nodejs/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.md +498 -0
- package/skill-pre/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +157 -0
- package/skill-pre/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/345/205/263/351/227/255/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +59 -0
- package/skill-pre/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +73 -0
- package/skill-pre/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +69 -0
- package/skill-pre/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +65 -0
- package/skill-pre/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 +59 -0
- package/skill-pre/references/integration/modules/code-examples/php/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +110 -0
- package/skill-pre/references/integration/modules/code-examples/php/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.md +806 -0
- package/skill-pre/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +128 -0
- package/skill-pre/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/345/205/263/351/227/255/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +57 -0
- package/skill-pre/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +70 -0
- package/skill-pre/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +65 -0
- package/skill-pre/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +62 -0
- package/skill-pre/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 +51 -0
- package/skill-pre/references/integration/modules/code-examples/python/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +47 -0
- package/skill-pre/references/integration/modules/code-examples/python/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.md +541 -0
- package/skill-pre/references/integration/modules/interface-contract/aipay-interface-contract.md +52 -0
- package/skill-pre/references/integration/modules/interface-contract/apppay-interface-contract.md +45 -0
- package/skill-pre/references/integration/modules/interface-contract/common-trade-contract.md +42 -0
- package/skill-pre/references/integration/modules/interface-contract/notify-url-contract.md +68 -0
- package/skill-pre/references/integration/modules/interface-contract/webpay-interface-contract.md +44 -0
- package/skill-pre/references/integration/modules/interface-guide.md +61 -0
- package/skill-pre/references/integration/modules/product-decision.md +73 -0
- package/skill-pre/references/integration/modules/sandbox/a2m-sandbox-test.md +235 -0
- package/skill-pre/references/integration/modules/sandbox/alipay-sandbox-tool.md +142 -0
- package/skill-pre/references/integration/modules/sandbox/sandbox-setup-guide.md +239 -0
- package/skill-pre/references/integration/modules/scripts/local_402_sandbox_pay.py +900 -0
- package/skill-pre/references/integration/modules/scripts/sandbox_config.sh +347 -0
- package/skill-pre/references/integration/modules/troubleshooting-flow.md +28 -0
- package/skill-pre/references/normal/alipay-cli-env.md +169 -0
- package/skill-pre/references/normal/conversation-telemetry.md +101 -0
- package/skill-pre/references/normal/full-process-routing.md +92 -0
- package/skill-pre/references/normal/integration-worker.md +90 -0
- package/skill-pre/references/normal/json/customer-messages.json +1 -0
- package/skill-pre/references/normal/json/managed-jq-assets.json +1 -0
- package/skill-pre/references/normal/json/policy-rules.json +1 -0
- package/skill-pre/references/normal/json/release-channel.json +1 -0
- package/skill-pre/references/normal/rejection-guide.md +41 -0
- package/skill-pre/references/normal/scripts/agent_platform.mjs +43 -0
- package/skill-pre/references/normal/scripts/alipay_cli_env.mjs +34 -0
- package/skill-pre/references/normal/scripts/alipay_cli_env.sh +55 -0
- package/skill-pre/references/normal/scripts/alipay_cli_refresh.mjs +129 -0
- package/skill-pre/references/normal/scripts/auth_runtime_runner.mjs +60 -0
- package/skill-pre/references/normal/scripts/checklist_sections.mjs +43 -0
- package/skill-pre/references/normal/scripts/common.sh +365 -0
- package/skill-pre/references/normal/scripts/conversation_telemetry.mjs +105 -0
- package/skill-pre/references/normal/scripts/detect_dev_tool.sh +79 -0
- package/skill-pre/references/normal/scripts/flow_loader.mjs +237 -0
- package/skill-pre/references/normal/scripts/integration_context_runner.mjs +151 -0
- package/skill-pre/references/normal/scripts/integration_message_runner.mjs +1456 -0
- package/skill-pre/references/normal/scripts/interaction_reply.mjs +140 -0
- package/skill-pre/references/normal/scripts/managed_jq.mjs +215 -0
- package/skill-pre/references/normal/scripts/onboarding_discovery_runner.mjs +387 -0
- package/skill-pre/references/normal/scripts/onboarding_message_runner.mjs +1425 -0
- package/skill-pre/references/normal/scripts/onboarding_recovery_runner.mjs +100 -0
- package/skill-pre/references/normal/scripts/open_official_url.sh +205 -0
- package/skill-pre/references/normal/scripts/platform_compat.mjs +42 -0
- package/skill-pre/references/normal/scripts/preview_runtime.mjs +151 -0
- package/skill-pre/references/normal/scripts/project_route_inspector.sh +132 -0
- package/skill-pre/references/normal/scripts/python_script_runner.mjs +58 -0
- package/skill-pre/references/normal/scripts/render_customer_message.mjs +1792 -0
- package/skill-pre/references/normal/scripts/runtime.mjs +271 -0
- package/skill-pre/references/normal/scripts/self_update.mjs +376 -0
- package/skill-pre/references/normal/scripts/telemetry.mjs +2102 -0
- package/skill-pre/references/normal/self-update.md +7 -0
- package/skill-pre/references/normal/telemetry.md +70 -0
- package/skill-pre/references/onboarding/assets/keygen.html +927 -0
- package/skill-pre/references/onboarding/flow.md +607 -0
- package/skill-pre/references/onboarding/modules/app-release.md +90 -0
- package/skill-pre/references/onboarding/modules/authorization.md +43 -0
- package/skill-pre/references/onboarding/modules/error-handling.md +62 -0
- package/skill-pre/references/onboarding/modules/product-sign.md +49 -0
- package/skill-pre/references/onboarding/modules/scripts/app.sh +1485 -0
- package/skill-pre/references/onboarding/modules/scripts/ar_sign_apply.sh +331 -0
- package/skill-pre/references/onboarding/modules/scripts/auth.sh +1040 -0
- package/skill-pre/references/onboarding/modules/scripts/download_key_tool.sh +4 -0
- package/skill-pre/references/onboarding/modules/scripts/error_handler.sh +841 -0
- package/skill-pre/references/onboarding/modules/scripts/mcc.sh +1085 -0
- package/skill-pre/references/onboarding/modules/scripts/network_retry.sh +135 -0
- package/skill-pre/references/onboarding/modules/scripts/query_sign_status.sh +105 -0
- package/skill-pre/references/onboarding/modules/scripts/service.sh +528 -0
- package/skill-pre/references/onboarding/modules/scripts/sign_status_parser.sh +58 -0
- package/skill-pre/references/onboarding/modules/scripts/upload_screenshots.sh +208 -0
- package/skill-pre/references/onboarding/modules/scripts/upload_special_license.sh +195 -0
- package/skill-pre/references/onboarding/modules/service-registration.md +40 -0
- package/skill-pre/references/runtime-entry.md +99 -0
- package/dist/commands/install-current.d.ts +0 -4
- package/dist/commands/install-current.d.ts.map +0 -1
- package/dist/commands/install-current.js.map +0 -1
- package/dist/commands/install.d.ts +0 -10
- package/dist/commands/install.d.ts.map +0 -1
- package/dist/commands/install.js.map +0 -1
- package/dist/commands/prepare-tools.d.ts +0 -5
- package/dist/commands/prepare-tools.d.ts.map +0 -1
- package/dist/commands/prepare-tools.js.map +0 -1
- package/dist/commands/uninstall.d.ts +0 -10
- package/dist/commands/uninstall.d.ts.map +0 -1
- package/dist/commands/uninstall.js.map +0 -1
- package/dist/commands/update.d.ts +0 -10
- package/dist/commands/update.d.ts.map +0 -1
- package/dist/commands/update.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/types.d.ts +0 -42
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/utils/alipay-cli.d.ts +0 -20
- package/dist/utils/alipay-cli.d.ts.map +0 -1
- package/dist/utils/alipay-cli.js.map +0 -1
- package/dist/utils/config.d.ts +0 -52
- package/dist/utils/config.d.ts.map +0 -1
- package/dist/utils/config.js.map +0 -1
- package/dist/utils/file.d.ts +0 -77
- package/dist/utils/file.d.ts.map +0 -1
- package/dist/utils/file.js.map +0 -1
- package/dist/utils/jq.d.ts +0 -17
- package/dist/utils/jq.d.ts.map +0 -1
- package/dist/utils/jq.js.map +0 -1
- package/dist/utils/npm.d.ts +0 -17
- package/dist/utils/npm.d.ts.map +0 -1
- package/dist/utils/npm.js.map +0 -1
- package/dist/utils/shell.d.ts +0 -24
- package/dist/utils/shell.d.ts.map +0 -1
- package/dist/utils/shell.js.map +0 -1
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
# AI 按量付费完整接入示例 - Node.js
|
|
2
|
+
|
|
3
|
+
以下代码展示严格验付和幂等履约的完整控制流。生成项目代码时,请按当前项目结构拆分并绑定真实持久化实现。
|
|
4
|
+
|
|
5
|
+
```javascript
|
|
6
|
+
/**
|
|
7
|
+
* A2M 智能收产品接入示例 - Node.js 版本
|
|
8
|
+
*
|
|
9
|
+
* 本示例展示 A2M 核心协议、严格验付和幂等履约流程。
|
|
10
|
+
* 必须通过 configureOrderRepository 绑定项目真实持久化实现;
|
|
11
|
+
* 端口未绑定时明确失败,不使用内存订单、固定资源或验付字段缺失兜底。
|
|
12
|
+
*
|
|
13
|
+
* 本文件演示 A2M 核心协议调用流程:
|
|
14
|
+
* 1. 返回 402 Payment-Needed Header
|
|
15
|
+
* 2. 验证 Payment-Proof 支付凭证
|
|
16
|
+
* 3. 发送履约回执确认
|
|
17
|
+
* 4. 返回资源内容
|
|
18
|
+
*
|
|
19
|
+
* 依赖安装:
|
|
20
|
+
* npm install express alipay-sdk
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const express = require('express');
|
|
24
|
+
const { AlipaySdk } = require('alipay-sdk');
|
|
25
|
+
const crypto = require('crypto');
|
|
26
|
+
|
|
27
|
+
const app = express();
|
|
28
|
+
const PORT = 3000;
|
|
29
|
+
|
|
30
|
+
// ==================== 配置信息(实际使用时请从受保护的配置读取)====================
|
|
31
|
+
const CONFIG = {
|
|
32
|
+
// 支付宝配置
|
|
33
|
+
alipay: {
|
|
34
|
+
appId: '<APP_ID>', // 运行时映射沙箱 appId
|
|
35
|
+
privateKey: '<APP_PRIVATE_KEY>', // 运行时映射 appPrivatePkcsKey(PKCS#1)
|
|
36
|
+
alipayPublicKey: '<ALIPAY_PUBLIC_KEY>', // 运行时映射 alipayPublicKey
|
|
37
|
+
// 默认用于本 Skill 的快速沙箱联调;生产部署时显式设置 ALIPAY_GATEWAY=https://openapi.alipay.com/gateway.do
|
|
38
|
+
gateway: process.env.ALIPAY_GATEWAY || 'https://openapi-sandbox.dl.alipaydev.com/gateway.do',
|
|
39
|
+
sellerId: '<SELLER_ID_2088>', // 商户 ID(2088 格式)
|
|
40
|
+
serviceId: 'api_mock_service_id', // 仅用于沙箱联调;上线前替换为服务市场真实 serviceId
|
|
41
|
+
merchantPrivateKey: '<APP_PRIVATE_KEY>' // 与 privateKey 使用同一运行时配置,用于商家签名
|
|
42
|
+
},
|
|
43
|
+
// 资源服务配置
|
|
44
|
+
resource: {
|
|
45
|
+
path: '/demo/a2m/resource',
|
|
46
|
+
goodsName: 'AI 生成内容服务'
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
let orderRepository = null;
|
|
51
|
+
|
|
52
|
+
// 真实实现必须以数据库事务和唯一约束实现以下端口;未绑定时明确失败。
|
|
53
|
+
function configureOrderRepository(repository) {
|
|
54
|
+
const requiredMethods = ['createPending', 'findByOutTradeNo', 'prepareFulfillment', 'markFulfilled'];
|
|
55
|
+
if (!repository || requiredMethods.some((name) => typeof repository[name] !== 'function')) {
|
|
56
|
+
throw new Error(`orderRepository 必须实现 ${requiredMethods.join(', ')}`);
|
|
57
|
+
}
|
|
58
|
+
orderRepository = repository;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function requireOrderRepository() {
|
|
62
|
+
if (!orderRepository) {
|
|
63
|
+
throw new Error('尚未绑定项目真实 orderRepository,禁止回退到内存订单');
|
|
64
|
+
}
|
|
65
|
+
return orderRepository;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const alipaySdk = new AlipaySdk({
|
|
69
|
+
appId: CONFIG.alipay.appId,
|
|
70
|
+
privateKey: CONFIG.alipay.privateKey,
|
|
71
|
+
alipayPublicKey: CONFIG.alipay.alipayPublicKey,
|
|
72
|
+
gateway: CONFIG.alipay.gateway,
|
|
73
|
+
timeout: 30000,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ==================== 工具方法 ====================
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 格式化支付宝时间戳:yyyy-MM-dd HH:mm:ss
|
|
80
|
+
*/
|
|
81
|
+
function formatAlipayTimestamp(date = new Date()) {
|
|
82
|
+
const pad = (n) => n.toString().padStart(2, '0');
|
|
83
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 生成商家签名(seller_signature)
|
|
88
|
+
* @param {Object} params - 待签名参数
|
|
89
|
+
* @param {String} privateKey - 商户私钥
|
|
90
|
+
* @returns {String} Base64 编码的签名
|
|
91
|
+
*/
|
|
92
|
+
function generateSellerSignature(params, privateKey) {
|
|
93
|
+
// 1. 按 key 字典序排序
|
|
94
|
+
const keys = Object.keys(params).sort();
|
|
95
|
+
|
|
96
|
+
// 2. 拼接签名内容
|
|
97
|
+
const signContent = keys
|
|
98
|
+
.filter(key => params[key] !== null && params[key] !== '')
|
|
99
|
+
.map((key, index) => {
|
|
100
|
+
const value = params[key];
|
|
101
|
+
return `${key}=${value}`;
|
|
102
|
+
})
|
|
103
|
+
.join('&');
|
|
104
|
+
|
|
105
|
+
// 3. 将裸 PKCS#1 Base64 临时解析为 DER 密钥对象,不修改原始配置。
|
|
106
|
+
const sellerPrivateKey = crypto.createPrivateKey({
|
|
107
|
+
key: Buffer.from(privateKey, 'base64'),
|
|
108
|
+
format: 'der',
|
|
109
|
+
type: 'pkcs1'
|
|
110
|
+
});
|
|
111
|
+
const sign = crypto
|
|
112
|
+
.createSign('RSA-SHA256')
|
|
113
|
+
.update(signContent, 'utf8')
|
|
114
|
+
.sign(sellerPrivateKey, 'base64');
|
|
115
|
+
|
|
116
|
+
return sign;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 格式化日期为 ISO 8601 带时区偏移量格式(如 2026-05-15T12:08:36+08:00)
|
|
121
|
+
* @param {Date} date - 日期对象
|
|
122
|
+
* @returns {String} ISO 8601 带时区偏移量字符串
|
|
123
|
+
*/
|
|
124
|
+
function formatISO8601WithTimezone(date) {
|
|
125
|
+
const pad = (n) => n.toString().padStart(2, '0');
|
|
126
|
+
const year = date.getFullYear();
|
|
127
|
+
const month = pad(date.getMonth() + 1);
|
|
128
|
+
const day = pad(date.getDate());
|
|
129
|
+
const hours = pad(date.getHours());
|
|
130
|
+
const minutes = pad(date.getMinutes());
|
|
131
|
+
const seconds = pad(date.getSeconds());
|
|
132
|
+
const offset = -date.getTimezoneOffset();
|
|
133
|
+
const offsetHours = pad(Math.floor(Math.abs(offset) / 60));
|
|
134
|
+
const offsetMinutes = pad(Math.abs(offset) % 60);
|
|
135
|
+
const offsetSign = offset >= 0 ? '+' : '-';
|
|
136
|
+
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${offsetSign}${offsetHours}:${offsetMinutes}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Base64URL 编码
|
|
141
|
+
*/
|
|
142
|
+
function base64UrlEncode(str) {
|
|
143
|
+
return Buffer.from(str, 'utf8')
|
|
144
|
+
.toString('base64')
|
|
145
|
+
.replace(/\+/g, '-')
|
|
146
|
+
.replace(/\//g, '_')
|
|
147
|
+
.replace(/=/g, '');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Base64URL 解码
|
|
152
|
+
*/
|
|
153
|
+
function base64UrlDecode(str) {
|
|
154
|
+
// 补充 padding
|
|
155
|
+
let padding = '=';
|
|
156
|
+
while (str.length % 4) {
|
|
157
|
+
str += padding;
|
|
158
|
+
}
|
|
159
|
+
str = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
160
|
+
return Buffer.from(str, 'base64').toString('utf8');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function normalizeAmount(value) {
|
|
164
|
+
const match = /^(\d+)(?:\.(\d{1,2}))?$/.exec(String(value ?? '').trim());
|
|
165
|
+
if (!match) return null;
|
|
166
|
+
return `${BigInt(match[1]).toString()}.${(match[2] || '').padEnd(2, '0')}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function amountsEqual(left, right) {
|
|
170
|
+
const leftAmount = normalizeAmount(left);
|
|
171
|
+
const rightAmount = normalizeAmount(right);
|
|
172
|
+
return leftAmount !== null && rightAmount !== null && leftAmount === rightAmount;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isExactSandboxMode() {
|
|
176
|
+
return CONFIG.alipay.gateway === 'https://openapi-sandbox.dl.alipaydev.com/gateway.do'
|
|
177
|
+
&& CONFIG.alipay.serviceId === 'api_mock_service_id';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 智能收产品统一接口
|
|
182
|
+
*
|
|
183
|
+
* 核心协议流程演示:
|
|
184
|
+
* 1. 不带 Payment-Proof Header:返回 HTTP 402 + Payment-Needed Header
|
|
185
|
+
* 2. 带 Payment-Proof Header:验证支付 → 自动履约 → 返回资源
|
|
186
|
+
*
|
|
187
|
+
* @route GET /demo/a2m/resource
|
|
188
|
+
* @param {String} Payment-Proof - 支付凭证(从 Header 获取,可选)
|
|
189
|
+
* @returns {Object} 未支付时返回 402,已支付时返回资源内容
|
|
190
|
+
*/
|
|
191
|
+
app.get(CONFIG.resource.path, async (req, res) => {
|
|
192
|
+
const paymentProof = req.headers['payment-proof'];
|
|
193
|
+
|
|
194
|
+
if (!paymentProof || paymentProof.trim() === '') {
|
|
195
|
+
return createPaymentRequiredResponse(req, res);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return verifyPaymentAndDeliverResource(req, res, paymentProof);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 创建 402 支付请求响应
|
|
203
|
+
*/
|
|
204
|
+
async function createPaymentRequiredResponse(req, res) {
|
|
205
|
+
try {
|
|
206
|
+
const outTradeNo = `ORDER_${Date.now()}_${crypto.randomUUID().replace(/-/g, '').slice(0, 12)}`;
|
|
207
|
+
const amount = '0.01'; // 单位:元
|
|
208
|
+
const currency = 'CNY';
|
|
209
|
+
const resourceId = CONFIG.resource.path;
|
|
210
|
+
const goodsName = CONFIG.resource.goodsName;
|
|
211
|
+
|
|
212
|
+
const payBefore = new Date(Date.now() + 30 * 60 * 1000);
|
|
213
|
+
const payBeforeStr = formatISO8601WithTimezone(payBefore);
|
|
214
|
+
|
|
215
|
+
const sellerSignature = generateSellerSignature({
|
|
216
|
+
amount,
|
|
217
|
+
currency,
|
|
218
|
+
goods_name: goodsName,
|
|
219
|
+
out_trade_no: outTradeNo,
|
|
220
|
+
pay_before: payBeforeStr,
|
|
221
|
+
resource_id: resourceId,
|
|
222
|
+
seller_id: CONFIG.alipay.sellerId,
|
|
223
|
+
service_id: CONFIG.alipay.serviceId
|
|
224
|
+
}, CONFIG.alipay.merchantPrivateKey);
|
|
225
|
+
|
|
226
|
+
const order = {
|
|
227
|
+
outTradeNo,
|
|
228
|
+
amount: normalizeAmount(amount),
|
|
229
|
+
currency,
|
|
230
|
+
resourceId,
|
|
231
|
+
goodsName,
|
|
232
|
+
payBefore: payBeforeStr,
|
|
233
|
+
orderStatus: 'PENDING_PAYMENT',
|
|
234
|
+
fulfillStatus: 'UNFULFILLED'
|
|
235
|
+
};
|
|
236
|
+
await requireOrderRepository().createPending(order);
|
|
237
|
+
|
|
238
|
+
const paymentNeeded = {
|
|
239
|
+
protocol: {
|
|
240
|
+
out_trade_no: outTradeNo,
|
|
241
|
+
amount,
|
|
242
|
+
currency,
|
|
243
|
+
resource_id: resourceId,
|
|
244
|
+
pay_before: payBeforeStr,
|
|
245
|
+
seller_signature: sellerSignature,
|
|
246
|
+
seller_sign_type: 'RSA2',
|
|
247
|
+
seller_unique_id: CONFIG.alipay.sellerId
|
|
248
|
+
},
|
|
249
|
+
method: {
|
|
250
|
+
seller_name: '测试商户',
|
|
251
|
+
seller_id: CONFIG.alipay.sellerId,
|
|
252
|
+
seller_app_id: CONFIG.alipay.appId,
|
|
253
|
+
goods_name: goodsName,
|
|
254
|
+
seller_unique_id_key: 'seller_id',
|
|
255
|
+
service_id: CONFIG.alipay.serviceId
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const paymentNeededEncoded = base64UrlEncode(JSON.stringify(paymentNeeded));
|
|
260
|
+
|
|
261
|
+
res.set('Payment-Needed', paymentNeededEncoded);
|
|
262
|
+
res.status(402).json({
|
|
263
|
+
code: 'Payment-Needed',
|
|
264
|
+
message: '需要支付',
|
|
265
|
+
out_trade_no: outTradeNo,
|
|
266
|
+
amount,
|
|
267
|
+
currency,
|
|
268
|
+
goods_name: goodsName
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
console.log(`创建支付订单成功:outTradeNo=${outTradeNo}, amount=${amount}`);
|
|
272
|
+
|
|
273
|
+
} catch (error) {
|
|
274
|
+
console.error('创建订单失败:', error.message);
|
|
275
|
+
res.status(500).json({
|
|
276
|
+
code: 'CREATE_ORDER_ERROR',
|
|
277
|
+
message: '创建订单失败:' + error.message
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* 验证支付凭证并交付资源
|
|
284
|
+
*/
|
|
285
|
+
async function verifyPaymentAndDeliverResource(req, res, paymentProof) {
|
|
286
|
+
try {
|
|
287
|
+
let paymentProofValue, tradeNo, clientSession;
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const decodedProof = base64UrlDecode(paymentProof);
|
|
291
|
+
const proofJson = JSON.parse(decodedProof);
|
|
292
|
+
|
|
293
|
+
if (proofJson.protocol) {
|
|
294
|
+
paymentProofValue = proofJson.protocol.payment_proof;
|
|
295
|
+
tradeNo = proofJson.protocol.trade_no;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (proofJson.method) {
|
|
299
|
+
clientSession = proofJson.method.client_session;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (!paymentProofValue || paymentProofValue.trim() === '') {
|
|
303
|
+
return createPaymentRequiredResponse(req, res);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!tradeNo || tradeNo.trim() === '') {
|
|
307
|
+
return createPaymentRequiredResponse(req, res);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
} catch (error) {
|
|
311
|
+
console.error('Payment-Proof 解析失败:', error.message);
|
|
312
|
+
return createPaymentRequiredResponse(req, res);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const verifyBizContent = {
|
|
316
|
+
payment_proof: paymentProofValue,
|
|
317
|
+
trade_no: tradeNo
|
|
318
|
+
};
|
|
319
|
+
if (clientSession) {
|
|
320
|
+
verifyBizContent.client_session = clientSession;
|
|
321
|
+
}
|
|
322
|
+
const verifyResponse = await alipaySdk.exec('alipay.aipay.agent.payment.verify', {
|
|
323
|
+
bizContent: verifyBizContent
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// SDK 可能返回嵌套响应,也可能直接返回业务字段。
|
|
327
|
+
const nestedResponse = verifyResponse.alipay_aipay_agent_payment_verify_response;
|
|
328
|
+
const responseData = nestedResponse || verifyResponse;
|
|
329
|
+
|
|
330
|
+
if (responseData.code !== '10000' && responseData.code !== 10000) {
|
|
331
|
+
const errorCode = responseData.sub_code || responseData.errorCode;
|
|
332
|
+
const errorMessage = responseData.sub_msg || responseData.msg || '支付凭证验证失败';
|
|
333
|
+
console.error('支付凭证验证失败:', errorMessage);
|
|
334
|
+
return createPaymentRequiredResponse(req, res);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 同时兼容 SDK 的 snake_case 与 camelCase 业务字段。
|
|
338
|
+
const returnedTradeNo = responseData.trade_no || responseData.tradeNo || '';
|
|
339
|
+
const verifyOutTradeNo = responseData.out_trade_no || responseData.outTradeNo || '';
|
|
340
|
+
const returnedAmount = responseData.amount;
|
|
341
|
+
const returnedResourceId = responseData.resource_id || responseData.resourceId || '';
|
|
342
|
+
const active = responseData.active;
|
|
343
|
+
|
|
344
|
+
const repository = requireOrderRepository();
|
|
345
|
+
const order = verifyOutTradeNo ? await repository.findByOutTradeNo(verifyOutTradeNo) : null;
|
|
346
|
+
const sandboxMode = isExactSandboxMode();
|
|
347
|
+
const verifyTradeNo = returnedTradeNo || (sandboxMode ? tradeNo : '');
|
|
348
|
+
const verifyAmount = returnedAmount || (sandboxMode && order ? order.amount : '');
|
|
349
|
+
const resourceIdVerified = returnedResourceId || (sandboxMode && order ? order.resourceId : '');
|
|
350
|
+
|
|
351
|
+
if (process.env.A2M_DEBUG === 'true') {
|
|
352
|
+
console.log('验付响应字段:', {
|
|
353
|
+
code: responseData.code,
|
|
354
|
+
tradeNo: verifyTradeNo,
|
|
355
|
+
outTradeNo: verifyOutTradeNo,
|
|
356
|
+
resourceId: resourceIdVerified,
|
|
357
|
+
active
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
console.log(`支付凭证验证成功:tradeNo=${verifyTradeNo}, outTradeNo=${verifyOutTradeNo}`);
|
|
362
|
+
|
|
363
|
+
if (active !== true || !verifyTradeNo || verifyTradeNo !== tradeNo || !verifyOutTradeNo || !resourceIdVerified) {
|
|
364
|
+
console.error(`支付凭证无效或已过期:outTradeNo=${verifyOutTradeNo}`);
|
|
365
|
+
return createPaymentRequiredResponse(req, res);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const amountMatches = order && amountsEqual(order.amount, verifyAmount);
|
|
369
|
+
const resourceMatches = order && order.resourceId === resourceIdVerified && resourceIdVerified === CONFIG.resource.path;
|
|
370
|
+
const fulfillmentInProgress = order && ['PENDING_CONFIRM', 'FULFILLED'].includes(order.fulfillStatus);
|
|
371
|
+
const orderUsable = order
|
|
372
|
+
&& order.currency === 'CNY'
|
|
373
|
+
&& ['PENDING_PAYMENT', 'PAID'].includes(order.orderStatus)
|
|
374
|
+
&& ['UNFULFILLED', 'PENDING_CONFIRM', 'FULFILLED'].includes(order.fulfillStatus)
|
|
375
|
+
&& (fulfillmentInProgress || Date.parse(order.payBefore) > Date.now());
|
|
376
|
+
if (!amountMatches || !resourceMatches || !orderUsable) {
|
|
377
|
+
return createPaymentRequiredResponse(req, res);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const fulfillment = await repository.prepareFulfillment({
|
|
381
|
+
outTradeNo: verifyOutTradeNo,
|
|
382
|
+
tradeNo: verifyTradeNo,
|
|
383
|
+
expectedAmount: normalizeAmount(verifyAmount),
|
|
384
|
+
expectedResourceId: resourceIdVerified,
|
|
385
|
+
createResource: () => generateServiceResource(resourceIdVerified)
|
|
386
|
+
});
|
|
387
|
+
if (!fulfillment || !['PENDING_CONFIRM', 'FULFILLED'].includes(fulfillment.state) || !fulfillment.serviceResult) {
|
|
388
|
+
throw new Error('orderRepository.prepareFulfillment 未返回已持久化的履约结果');
|
|
389
|
+
}
|
|
390
|
+
const serviceResult = fulfillment.serviceResult;
|
|
391
|
+
|
|
392
|
+
if (fulfillment.state === 'FULFILLED') {
|
|
393
|
+
return sendSuccessfulResource(res, verifyTradeNo, verifyOutTradeNo, resourceIdVerified, serviceResult, true);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const fulfillmentTradeNo = verifyTradeNo;
|
|
397
|
+
console.log(`资源已生成,准备发送履约确认:outTradeNo=${verifyOutTradeNo}, tradeNo=${fulfillmentTradeNo}`);
|
|
398
|
+
|
|
399
|
+
const fulfillmentConfirmed = await sendFulfillmentConfirm(fulfillmentTradeNo);
|
|
400
|
+
if (!fulfillmentConfirmed) {
|
|
401
|
+
return res.status(502).json({
|
|
402
|
+
code: 'FULFILLMENT_CONFIRM_FAILED',
|
|
403
|
+
message: '资源已生成但履约确认失败,请稍后使用同一 Payment-Proof 重试'
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
await repository.markFulfilled(verifyOutTradeNo, fulfillmentTradeNo);
|
|
408
|
+
|
|
409
|
+
console.log(`履约确认成功:outTradeNo=${verifyOutTradeNo}, tradeNo=${fulfillmentTradeNo}`);
|
|
410
|
+
|
|
411
|
+
return sendSuccessfulResource(res, fulfillmentTradeNo, verifyOutTradeNo, resourceIdVerified, serviceResult, false);
|
|
412
|
+
|
|
413
|
+
} catch (error) {
|
|
414
|
+
console.error('支付凭证验证异常:', error.message);
|
|
415
|
+
res.status(500).json({
|
|
416
|
+
code: 'VERIFY_FAILED',
|
|
417
|
+
message: '支付凭证验证失败:' + error.message
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function sendSuccessfulResource(res, tradeNo, outTradeNo, resourceId, serviceResult, alreadyFulfilled) {
|
|
423
|
+
res.set('Payment-Validation', base64UrlEncode(JSON.stringify({
|
|
424
|
+
trade_no: tradeNo,
|
|
425
|
+
out_trade_no: outTradeNo,
|
|
426
|
+
validated: true,
|
|
427
|
+
resource_id: resourceId
|
|
428
|
+
})));
|
|
429
|
+
return res.json({
|
|
430
|
+
resource_id: resourceId,
|
|
431
|
+
content: serviceResult,
|
|
432
|
+
trade_no: tradeNo,
|
|
433
|
+
out_trade_no: outTradeNo,
|
|
434
|
+
already_fulfilled: alreadyFulfilled,
|
|
435
|
+
fulfillment_confirmed: true
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* 生成服务资源内容
|
|
441
|
+
*/
|
|
442
|
+
function generateServiceResource(resourceId) {
|
|
443
|
+
return JSON.stringify({
|
|
444
|
+
status: 'success',
|
|
445
|
+
service_type: 'AI_CONTENT_GENERATION',
|
|
446
|
+
resource_id: resourceId,
|
|
447
|
+
content: '这是 AI 生成的内容示例,可根据实际业务替换为任意数字服务内容',
|
|
448
|
+
generated_at: formatISO8601WithTimezone(new Date())
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* 发送履约确认
|
|
454
|
+
*/
|
|
455
|
+
async function sendFulfillmentConfirm(tradeNo) {
|
|
456
|
+
if (!tradeNo) {
|
|
457
|
+
console.error('履约确认失败:tradeNo 为空');
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
console.log(`开始发送履约确认:tradeNo=${tradeNo}`);
|
|
463
|
+
|
|
464
|
+
const response = await alipaySdk.exec('alipay.aipay.agent.fulfillment.confirm', {
|
|
465
|
+
bizContent: {
|
|
466
|
+
trade_no: tradeNo
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// 注意:SDK 返回的响应可能是扁平结构,也可能嵌套在响应键下,需兼容两种情况
|
|
471
|
+
const responseData = response.alipay_aipay_agent_fulfillment_confirm_response || response;
|
|
472
|
+
if (responseData.code === '10000' || responseData.code === 10000) {
|
|
473
|
+
console.log(`履约确认成功:tradeNo=${tradeNo}`);
|
|
474
|
+
return true;
|
|
475
|
+
} else {
|
|
476
|
+
const errorCode = responseData.sub_code || responseData.errorCode;
|
|
477
|
+
const errorMessage = responseData.sub_msg || responseData.msg || '履约确认失败';
|
|
478
|
+
console.error(`履约确认失败:tradeNo=${tradeNo}, errorCode=${errorCode}, errorMsg=${errorMessage}`);
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
} catch (error) {
|
|
483
|
+
console.error(`履约确认异常:tradeNo=${tradeNo}, error=${error.message}`);
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (require.main === module) {
|
|
489
|
+
app.listen(PORT, () => {
|
|
490
|
+
console.log(`A2M 按量付费服务已启动:http://localhost:${PORT}${CONFIG.resource.path}`);
|
|
491
|
+
console.log('测试步骤:');
|
|
492
|
+
console.log('1. 无 Payment-Proof Header: curl http://localhost:3000/demo/a2m/resource');
|
|
493
|
+
console.log('2. 有 Payment-Proof Header: curl -H "Payment-Proof: <value>" http://localhost:3000/demo/a2m/resource');
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
module.exports = { app, configureOrderRepository };
|
|
498
|
+
```
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# 异步通知处理 - PHP 示例
|
|
2
|
+
|
|
3
|
+
适用产品:AI 网页应用收款、AI 移动应用收款。AI 按量付费不使用异步通知。
|
|
4
|
+
|
|
5
|
+
```php
|
|
6
|
+
<?php
|
|
7
|
+
|
|
8
|
+
// 支付宝通知是 POST 表单,不是 JSON。
|
|
9
|
+
// $deps 接入项目真实 SDK 验签适配器、支付配置、订单仓储、通知事件仓储和脱敏日志。
|
|
10
|
+
function handleAlipayNotify(array $params, array $deps): void
|
|
11
|
+
{
|
|
12
|
+
try {
|
|
13
|
+
$config = $deps['paymentConfigRepository']->findByAppId($params['app_id'] ?? '');
|
|
14
|
+
if ($config === null) {
|
|
15
|
+
fail($deps, 'alipay notify app config not found', $params);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!verifyNotifyWithAlipaySdk($params, $deps['alipayNotifyVerifier'], $config)) {
|
|
19
|
+
fail($deps, 'alipay notify signature check failed', $params);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
$order = $deps['orderRepository']->findByOutTradeNo($params['out_trade_no'] ?? '');
|
|
23
|
+
if ($order === null || !businessFieldsMatch($order, $params, $config)) {
|
|
24
|
+
fail($deps, 'alipay notify business check failed', $params);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
$inserted = $deps['notifyEventRepository']->insertOnce([
|
|
28
|
+
'notify_id' => $params['notify_id'] ?? '',
|
|
29
|
+
'trade_no' => $params['trade_no'] ?? '',
|
|
30
|
+
'out_trade_no' => $params['out_trade_no'] ?? '',
|
|
31
|
+
'trade_status' => $params['trade_status'] ?? '',
|
|
32
|
+
'raw_params' => $params,
|
|
33
|
+
]);
|
|
34
|
+
if (!$inserted) {
|
|
35
|
+
respondPlainText('success');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (isPaidTradeNotification($params)) {
|
|
39
|
+
$deps['orderRepository']->markPaidIdempotently($order['out_trade_no'], $params['trade_no'] ?? '', $params);
|
|
40
|
+
} else {
|
|
41
|
+
$deps['orderRepository']->recordNonPaidTradeEvent($order['out_trade_no'], $params['trade_status'] ?? '', $params);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
respondPlainText('success');
|
|
45
|
+
} catch (Throwable $e) {
|
|
46
|
+
safeLogNotifyError($deps, 'alipay notify unexpected exception', $params);
|
|
47
|
+
respondPlainText('fail');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function businessFieldsMatch(array $order, array $params, array $config): bool
|
|
52
|
+
{
|
|
53
|
+
return ($params['app_id'] ?? '') === ($config['app_id'] ?? '')
|
|
54
|
+
&& ($params['out_trade_no'] ?? '') === $order['out_trade_no']
|
|
55
|
+
&& amountEquals($order['total_amount'], $params['total_amount'] ?? null)
|
|
56
|
+
&& expectedSellerMatches($config, $params['seller_id'] ?? null, $params['seller_email'] ?? null);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isPaidTradeNotification(array $params): bool
|
|
60
|
+
{
|
|
61
|
+
$paidStatus = ($params['trade_status'] ?? '') === 'TRADE_SUCCESS'
|
|
62
|
+
|| ($params['trade_status'] ?? '') === 'TRADE_FINISHED';
|
|
63
|
+
return $paidStatus
|
|
64
|
+
&& empty($params['out_biz_no'])
|
|
65
|
+
&& empty($params['gmt_refund'])
|
|
66
|
+
&& empty($params['refund_fee']);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function amountEquals($expected, $actual): bool
|
|
70
|
+
{
|
|
71
|
+
if ($actual === null) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
$expectedAmount = normalizeAmountText($expected);
|
|
75
|
+
$actualAmount = normalizeAmountText($actual);
|
|
76
|
+
return $expectedAmount !== null && $expectedAmount === $actualAmount;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeAmountText($value): ?string
|
|
80
|
+
{
|
|
81
|
+
$text = trim((string) $value);
|
|
82
|
+
if (!preg_match('/^(\d+)(?:\.(\d{1,2}))?$/', $text, $matches)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
$yuan = ltrim($matches[1], '0');
|
|
86
|
+
return ($yuan === '' ? '0' : $yuan) . '.' . str_pad($matches[2] ?? '', 2, '0');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function expectedSellerMatches(array $config, ?string $sellerId, ?string $sellerEmail): bool
|
|
90
|
+
{
|
|
91
|
+
$expectedSellerId = firstNonBlank($config['seller_id'] ?? null, null);
|
|
92
|
+
$expectedSellerEmail = firstNonBlank($config['seller_email'] ?? null, null);
|
|
93
|
+
return ($expectedSellerId !== null && $sellerId === $expectedSellerId)
|
|
94
|
+
|| ($expectedSellerEmail !== null && $sellerEmail === $expectedSellerEmail);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function firstNonBlank(?string $primary, ?string $fallback): ?string
|
|
98
|
+
{
|
|
99
|
+
if ($primary !== null && trim($primary) !== '') {
|
|
100
|
+
return $primary;
|
|
101
|
+
}
|
|
102
|
+
if ($fallback !== null && trim($fallback) !== '') {
|
|
103
|
+
return $fallback;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function verifyNotifyWithAlipaySdk(array $params, object $verifier, array $config): bool
|
|
109
|
+
{
|
|
110
|
+
// 使用当前项目已安装的支付宝 SDK 验签能力校验 $params['sign']。
|
|
111
|
+
// $verifier->verify($params, $config) 必须封装目标项目当前 SDK 实际通知验签 API;落地前先查已安装包文档或类型定义,不能手写临时 RSA 验签。
|
|
112
|
+
return $verifier->verify($params, $config);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function fail(array $deps, string $message, array $params): void
|
|
116
|
+
{
|
|
117
|
+
safeLogNotifyError($deps, $message, $params);
|
|
118
|
+
respondPlainText('fail');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function respondPlainText(string $body): void
|
|
122
|
+
{
|
|
123
|
+
header('Content-Type: text/plain; charset=UTF-8');
|
|
124
|
+
echo $body;
|
|
125
|
+
exit;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function safeLogNotifyError(array $deps, string $message, array $params): void
|
|
129
|
+
{
|
|
130
|
+
try {
|
|
131
|
+
if (isset($deps['logger'])) {
|
|
132
|
+
$deps['logger']->warning($message, sanitizeNotifyParams($params));
|
|
133
|
+
}
|
|
134
|
+
} catch (Throwable $e) {
|
|
135
|
+
// 日志失败不能影响支付宝收到纯文本 fail。
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function sanitizeNotifyParams(array $params): array
|
|
140
|
+
{
|
|
141
|
+
unset($params['sign']);
|
|
142
|
+
return $params;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 在项目路由入口中由 DI 容器提供 $deps 后调用:
|
|
146
|
+
// handleAlipayNotify($_POST, $deps);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
要点:
|
|
150
|
+
|
|
151
|
+
- 支付宝通知是 POST 表单,验签前必须保留完整 `$_POST` 参数。
|
|
152
|
+
- 验签必须调用当前项目支付宝 SDK 的实际验签 API,验签前不得丢字段或改值。
|
|
153
|
+
- `app_id`、`seller_id` / `seller_email` 从运行时支付配置校验,不从订单对象默认取值。
|
|
154
|
+
- 金额校验使用字符串或定点数规范化,不要用浮点数直接比较,也不要默认依赖未安装的 PHP 扩展。
|
|
155
|
+
- 只有付款成功状态且不是退款、关单、分账等事件时,才认定付款成功。
|
|
156
|
+
- 内存订单或内存幂等只能用于非生产 demo,生产必须使用持久化订单表和通知事件表。
|
|
157
|
+
- 处理成功后返回纯文本 `success`;异常、验签失败或业务校验失败返回 `fail`。
|