@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,623 @@
|
|
|
1
|
+
# AI 按量付费完整接入示例 - Java
|
|
2
|
+
|
|
3
|
+
以下代码展示严格验付和幂等履约的完整控制流。生成项目代码时,请按当前项目结构拆分并绑定真实持久化实现。
|
|
4
|
+
|
|
5
|
+
```java
|
|
6
|
+
/*
|
|
7
|
+
* Copyright (C) 2015-2026 Ant Group
|
|
8
|
+
*
|
|
9
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
10
|
+
* you may not use this file except in compliance with the License.
|
|
11
|
+
* You may obtain a copy of the License at
|
|
12
|
+
*
|
|
13
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
14
|
+
*
|
|
15
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
16
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
17
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
18
|
+
* See the License for the specific language governing permissions and
|
|
19
|
+
* limitations under the License.
|
|
20
|
+
*/
|
|
21
|
+
/*
|
|
22
|
+
* 本示例展示 A2M 核心协议、严格验付和幂等履约流程。
|
|
23
|
+
* 必须通过 OrderRepository 绑定项目真实持久化实现;端口未绑定时明确失败,
|
|
24
|
+
* 不使用内存订单、固定资源或验付字段缺失兜底。
|
|
25
|
+
*/
|
|
26
|
+
package com.alipay.aipayweb.demo;
|
|
27
|
+
|
|
28
|
+
import com.alibaba.fastjson.JSONObject;
|
|
29
|
+
import com.alipay.api.AlipayApiException;
|
|
30
|
+
import com.alipay.api.AlipayClient;
|
|
31
|
+
import com.alipay.api.AlipayConfig;
|
|
32
|
+
import com.alipay.api.DefaultAlipayClient;
|
|
33
|
+
import com.alipay.api.domain.AlipayAipayAgentFulfillmentConfirmModel;
|
|
34
|
+
import com.alipay.api.internal.util.AlipaySignature;
|
|
35
|
+
import com.alipay.api.request.AlipayAipayAgentFulfillmentConfirmRequest;
|
|
36
|
+
import com.alipay.api.request.AlipayAipayAgentPaymentVerifyRequest;
|
|
37
|
+
import com.alipay.api.response.AlipayAipayAgentFulfillmentConfirmResponse;
|
|
38
|
+
import com.alipay.api.response.AlipayAipayAgentPaymentVerifyResponse;
|
|
39
|
+
import org.springframework.http.HttpHeaders;
|
|
40
|
+
import org.springframework.http.HttpStatus;
|
|
41
|
+
import org.springframework.http.ResponseEntity;
|
|
42
|
+
import org.springframework.web.bind.annotation.GetMapping;
|
|
43
|
+
import org.springframework.web.bind.annotation.RequestHeader;
|
|
44
|
+
import org.springframework.web.bind.annotation.RequestMapping;
|
|
45
|
+
import org.springframework.web.bind.annotation.RestController;
|
|
46
|
+
|
|
47
|
+
import java.nio.charset.StandardCharsets;
|
|
48
|
+
import java.math.BigDecimal;
|
|
49
|
+
import java.time.ZoneId;
|
|
50
|
+
import java.time.ZonedDateTime;
|
|
51
|
+
import java.time.format.DateTimeFormatter;
|
|
52
|
+
import java.util.*;
|
|
53
|
+
import java.util.function.Supplier;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A2M智能收产品接入示例 Controller
|
|
57
|
+
*
|
|
58
|
+
* 本Controller为开发者示例代码,演示 A2M 核心协议调用流程:
|
|
59
|
+
* 1. 返回 402 Payment-Needed Header
|
|
60
|
+
* 2. 验证 Payment-Proof 支付凭证
|
|
61
|
+
* 3. 发送履约回执确认
|
|
62
|
+
* 4. 返回资源内容
|
|
63
|
+
*
|
|
64
|
+
* GET /demo/a2m/resource
|
|
65
|
+
* ├─ 场景1: 无 Payment-Proof Header
|
|
66
|
+
* │ └─ 返回 402 + Payment-Needed Header
|
|
67
|
+
* │
|
|
68
|
+
* └─ 场景2: 有 Payment-Proof Header
|
|
69
|
+
* ├─ 调用支付宝API验证凭证
|
|
70
|
+
* ├─ 从响应获取: tradeNo, outTradeNo, resourceId, active
|
|
71
|
+
* ├─ 校验 active=true (凭证有效性)
|
|
72
|
+
* ├─ 资源防串校验
|
|
73
|
+
* ├─ 履约防重放校验
|
|
74
|
+
* ├─ 发送履约确认
|
|
75
|
+
* ├─ 返回资源内容
|
|
76
|
+
*
|
|
77
|
+
* OrderRepository 的项目实现必须依赖数据库事务和唯一约束,禁止替换为进程内 Map。
|
|
78
|
+
*/
|
|
79
|
+
@RestController
|
|
80
|
+
@RequestMapping("/demo/a2m")
|
|
81
|
+
public class A2MPaymentDemoController {
|
|
82
|
+
|
|
83
|
+
// ==================== 配置信息(实际使用时请从配置中心读取)====================
|
|
84
|
+
// 以下配置仅为示例,实际开发时请替换为真实配置或从配置中心读取
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 支付宝SDK客户端
|
|
88
|
+
*/
|
|
89
|
+
private final AlipayClient alipayClient;
|
|
90
|
+
private static final String RESOURCE_PATH = "/demo/a2m/resource";
|
|
91
|
+
private static final String SANDBOX_GATEWAY = "https://openapi-sandbox.dl.alipaydev.com/gateway.do";
|
|
92
|
+
private static final String GATEWAY = System.getenv().getOrDefault("ALIPAY_GATEWAY", SANDBOX_GATEWAY);
|
|
93
|
+
private static final String SERVICE_ID = System.getenv().getOrDefault("ALIPAY_SERVICE_ID", "api_mock_service_id");
|
|
94
|
+
private final OrderRepository orderRepository;
|
|
95
|
+
|
|
96
|
+
public A2MPaymentDemoController(OrderRepository orderRepository) {
|
|
97
|
+
this.orderRepository = Objects.requireNonNull(orderRepository, "必须绑定项目真实 OrderRepository");
|
|
98
|
+
// 使用AlipayConfig初始化支付宝客户端(推荐方式)
|
|
99
|
+
try {
|
|
100
|
+
this.alipayClient = new DefaultAlipayClient(getAlipayConfig());
|
|
101
|
+
} catch (AlipayApiException e) {
|
|
102
|
+
throw new RuntimeException("初始化支付宝客户端失败", e);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 获取支付宝配置
|
|
108
|
+
*
|
|
109
|
+
* @return AlipayConfig
|
|
110
|
+
*/
|
|
111
|
+
private static AlipayConfig getAlipayConfig() {
|
|
112
|
+
AlipayConfig alipayConfig = new AlipayConfig();
|
|
113
|
+
// 默认用于本 Skill 的快速沙箱联调;生产部署时显式设置 ALIPAY_GATEWAY=https://openapi.alipay.com/gateway.do
|
|
114
|
+
alipayConfig.setServerUrl(GATEWAY);
|
|
115
|
+
alipayConfig.setAppId("<APP_ID>");
|
|
116
|
+
alipayConfig.setPrivateKey("<APP_PRIVATE_KEY>"); // 请填写您的应用私钥
|
|
117
|
+
alipayConfig.setFormat("json");
|
|
118
|
+
alipayConfig.setAlipayPublicKey("<ALIPAY_PUBLIC_KEY>"); // 请填写您的支付宝公钥
|
|
119
|
+
alipayConfig.setCharset("UTF-8");
|
|
120
|
+
alipayConfig.setSignType("RSA2");
|
|
121
|
+
return alipayConfig;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 项目持久化端口。prepareFulfillment 必须在同一事务内校验订单并且只生成一次资源,
|
|
126
|
+
* 返回已持久化的 PENDING_CONFIRM 或 FULFILLED 结果。
|
|
127
|
+
* OrderSnapshot 将支付状态映射为 PENDING_PAYMENT/PAID,
|
|
128
|
+
* 将履约状态映射为 UNFULFILLED/PENDING_CONFIRM/FULFILLED。
|
|
129
|
+
*/
|
|
130
|
+
public interface OrderRepository {
|
|
131
|
+
void createPending(OrderSnapshot order);
|
|
132
|
+
OrderSnapshot findByOutTradeNo(String outTradeNo);
|
|
133
|
+
FulfillmentPreparation prepareFulfillment(FulfillmentRequest request);
|
|
134
|
+
void markFulfilled(String outTradeNo, String tradeNo);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
public static final class OrderSnapshot {
|
|
138
|
+
public final String outTradeNo;
|
|
139
|
+
public final String amount;
|
|
140
|
+
public final String currency;
|
|
141
|
+
public final String resourceId;
|
|
142
|
+
public final String goodsName;
|
|
143
|
+
public final String payBefore;
|
|
144
|
+
public final String orderStatus;
|
|
145
|
+
public final String fulfillStatus;
|
|
146
|
+
|
|
147
|
+
public OrderSnapshot(String outTradeNo, String amount, String currency, String resourceId,
|
|
148
|
+
String goodsName, String payBefore, String orderStatus, String fulfillStatus) {
|
|
149
|
+
this.outTradeNo = outTradeNo;
|
|
150
|
+
this.amount = amount;
|
|
151
|
+
this.currency = currency;
|
|
152
|
+
this.resourceId = resourceId;
|
|
153
|
+
this.goodsName = goodsName;
|
|
154
|
+
this.payBefore = payBefore;
|
|
155
|
+
this.orderStatus = orderStatus;
|
|
156
|
+
this.fulfillStatus = fulfillStatus;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
public static final class FulfillmentRequest {
|
|
161
|
+
public final String outTradeNo;
|
|
162
|
+
public final String tradeNo;
|
|
163
|
+
public final String expectedAmount;
|
|
164
|
+
public final String expectedResourceId;
|
|
165
|
+
public final Supplier<String> createResource;
|
|
166
|
+
|
|
167
|
+
public FulfillmentRequest(String outTradeNo, String tradeNo, String expectedAmount,
|
|
168
|
+
String expectedResourceId, Supplier<String> createResource) {
|
|
169
|
+
this.outTradeNo = outTradeNo;
|
|
170
|
+
this.tradeNo = tradeNo;
|
|
171
|
+
this.expectedAmount = expectedAmount;
|
|
172
|
+
this.expectedResourceId = expectedResourceId;
|
|
173
|
+
this.createResource = createResource;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public static final class FulfillmentPreparation {
|
|
178
|
+
public final String state;
|
|
179
|
+
public final String serviceResult;
|
|
180
|
+
|
|
181
|
+
public FulfillmentPreparation(String state, String serviceResult) {
|
|
182
|
+
this.state = state;
|
|
183
|
+
this.serviceResult = serviceResult;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ==================== 智能收产品接入示例接口 ====================
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 智能收产品统一接口
|
|
191
|
+
*
|
|
192
|
+
* 核心协议流程演示:
|
|
193
|
+
* 1. 不带 Payment-Proof Header:返回 HTTP 402 + Payment-Needed Header
|
|
194
|
+
* 2. 带 Payment-Proof Header:验证支付 → 自动履约 → 返回资源
|
|
195
|
+
*
|
|
196
|
+
* @param paymentProof 支付凭证(从Header获取,可选)
|
|
197
|
+
* @return 未支付时返回402,已支付时返回资源内容
|
|
198
|
+
*/
|
|
199
|
+
@GetMapping("/resource")
|
|
200
|
+
public ResponseEntity<?> getResource(
|
|
201
|
+
@RequestHeader(value = "Payment-Proof", required = false) String paymentProof) {
|
|
202
|
+
|
|
203
|
+
// 场景1:用户未支付,返回402 + Payment-Needed Header
|
|
204
|
+
if (paymentProof == null || paymentProof.trim().isEmpty()) {
|
|
205
|
+
return createPaymentRequiredResponse();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 场景2:用户已支付,验证Payment-Proof并返回资源
|
|
209
|
+
return verifyPaymentAndDeliverResource(paymentProof);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 创建402支付请求响应
|
|
214
|
+
*
|
|
215
|
+
* @return 402响应 + Payment-Needed Header
|
|
216
|
+
*/
|
|
217
|
+
private ResponseEntity<?> createPaymentRequiredResponse() {
|
|
218
|
+
try {
|
|
219
|
+
// 1. 构造订单信息
|
|
220
|
+
String outTradeNo = "ORDER_" + System.currentTimeMillis() + "_"
|
|
221
|
+
+ UUID.randomUUID().toString().replace("-", "").substring(0, 12);
|
|
222
|
+
String amount = "0.01"; // 单位:元
|
|
223
|
+
String currency = "CNY"; //固定用CNY
|
|
224
|
+
String resourceId = RESOURCE_PATH; //用户可自定义资源id的生成逻辑,用于资源防串
|
|
225
|
+
String goodsName = "AI 生成内容服务"; //用户可自行定义商品名称
|
|
226
|
+
|
|
227
|
+
// 2. 计算支付截止时间(30分钟后),用户可自行设置
|
|
228
|
+
ZonedDateTime payBefore = ZonedDateTime.now(ZoneId.of("Asia/Shanghai")).plusMinutes(30);
|
|
229
|
+
String payBeforeStr = payBefore.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
|
|
230
|
+
|
|
231
|
+
// 3. 生成商家签名(需要使用商户私钥)
|
|
232
|
+
// 注意:实际使用时请从配置中读取商户ID和服务ID
|
|
233
|
+
String sellerId = "<SELLER_ID_2088>"; // 商户ID(2088格式)
|
|
234
|
+
String serviceId = SERVICE_ID;
|
|
235
|
+
|
|
236
|
+
Map<String, String> signParams = new HashMap<>();
|
|
237
|
+
signParams.put("amount", amount);
|
|
238
|
+
signParams.put("currency", currency);
|
|
239
|
+
signParams.put("goods_name", goodsName);
|
|
240
|
+
signParams.put("out_trade_no", outTradeNo);
|
|
241
|
+
signParams.put("pay_before", payBeforeStr);
|
|
242
|
+
signParams.put("resource_id", resourceId);
|
|
243
|
+
signParams.put("seller_id", sellerId);
|
|
244
|
+
signParams.put("service_id", serviceId);
|
|
245
|
+
|
|
246
|
+
// 注意:实际使用时需要从配置中读取商户私钥进行签名
|
|
247
|
+
String privateKey = "<APP_PRIVATE_KEY>"; // 请填写您的应用私钥
|
|
248
|
+
String sellerSignature = generateSellerSignature(signParams, privateKey);
|
|
249
|
+
|
|
250
|
+
// 4. 必须在返回 402 前持久化;项目实现负责事务与 outTradeNo 唯一约束。
|
|
251
|
+
orderRepository.createPending(new OrderSnapshot(
|
|
252
|
+
outTradeNo, normalizeAmount(amount), currency, resourceId, goodsName, payBeforeStr,
|
|
253
|
+
"PENDING_PAYMENT", "UNFULFILLED"));
|
|
254
|
+
|
|
255
|
+
// 5. 持久化成功后构造 Payment-Needed Header 内容(分层结构)
|
|
256
|
+
JSONObject paymentNeeded = new JSONObject();
|
|
257
|
+
|
|
258
|
+
// protocol 层
|
|
259
|
+
JSONObject protocol = new JSONObject();
|
|
260
|
+
protocol.put("out_trade_no", outTradeNo);
|
|
261
|
+
protocol.put("amount", amount);
|
|
262
|
+
protocol.put("currency", currency);
|
|
263
|
+
protocol.put("resource_id", resourceId);
|
|
264
|
+
protocol.put("pay_before", payBeforeStr);
|
|
265
|
+
protocol.put("seller_signature", sellerSignature);
|
|
266
|
+
protocol.put("seller_sign_type", "RSA2");
|
|
267
|
+
protocol.put("seller_unique_id", sellerId);
|
|
268
|
+
|
|
269
|
+
// method 层
|
|
270
|
+
JSONObject method = new JSONObject();
|
|
271
|
+
method.put("seller_name", "测试商户");
|
|
272
|
+
method.put("seller_id", sellerId);
|
|
273
|
+
method.put("seller_app_id", "<APP_ID>"); // 请填写您的 AppId
|
|
274
|
+
method.put("goods_name", goodsName);
|
|
275
|
+
method.put("seller_unique_id_key", "seller_id"); // 固定用卖家 id
|
|
276
|
+
method.put("service_id", serviceId);
|
|
277
|
+
|
|
278
|
+
paymentNeeded.put("protocol", protocol);
|
|
279
|
+
paymentNeeded.put("method", method);
|
|
280
|
+
// 6. Base64URL编码
|
|
281
|
+
String paymentNeededEncoded = Base64.getUrlEncoder()
|
|
282
|
+
.withoutPadding()
|
|
283
|
+
.encodeToString(paymentNeeded.toJSONString().getBytes(StandardCharsets.UTF_8));
|
|
284
|
+
|
|
285
|
+
// 7. 构造402响应
|
|
286
|
+
HttpHeaders headers = new HttpHeaders();
|
|
287
|
+
headers.set("Payment-Needed", paymentNeededEncoded);
|
|
288
|
+
|
|
289
|
+
JSONObject responseBody = new JSONObject();
|
|
290
|
+
responseBody.put("code", "Payment-Needed");
|
|
291
|
+
responseBody.put("message", "需要支付");
|
|
292
|
+
responseBody.put("out_trade_no", outTradeNo);
|
|
293
|
+
responseBody.put("amount", amount);
|
|
294
|
+
responseBody.put("currency", currency);
|
|
295
|
+
responseBody.put("goods_name", goodsName);
|
|
296
|
+
|
|
297
|
+
// 记录日志
|
|
298
|
+
System.out.println("创建支付订单成功: outTradeNo=" + outTradeNo + ", amount=" + amount);
|
|
299
|
+
|
|
300
|
+
return ResponseEntity.status(HttpStatus.PAYMENT_REQUIRED)
|
|
301
|
+
.headers(headers)
|
|
302
|
+
.body(responseBody);
|
|
303
|
+
|
|
304
|
+
} catch (AlipayApiException e) {
|
|
305
|
+
System.err.println("签名失败: " + e.getMessage());
|
|
306
|
+
JSONObject errorResponse = new JSONObject();
|
|
307
|
+
errorResponse.put("code", "SIGN_ERROR");
|
|
308
|
+
errorResponse.put("message", "签名失败: " + e.getMessage());
|
|
309
|
+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
|
|
310
|
+
} catch (Exception e) {
|
|
311
|
+
System.err.println("创建订单失败: " + e.getMessage());
|
|
312
|
+
JSONObject errorResponse = new JSONObject();
|
|
313
|
+
errorResponse.put("code", "CREATE_ORDER_ERROR");
|
|
314
|
+
errorResponse.put("message", "创建订单失败: " + e.getMessage());
|
|
315
|
+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* 验证支付凭证并交付资源
|
|
321
|
+
*
|
|
322
|
+
* @param paymentProof 支付凭证
|
|
323
|
+
* @return 资源内容或错误信息
|
|
324
|
+
*/
|
|
325
|
+
private ResponseEntity<?> verifyPaymentAndDeliverResource(String paymentProof) {
|
|
326
|
+
try {
|
|
327
|
+
// 1. Payment-Proof 是 Base64URL 编码的 JSON。
|
|
328
|
+
String paymentProofValue = null;
|
|
329
|
+
String tradeNo = null;
|
|
330
|
+
String clientSession = null;
|
|
331
|
+
try {
|
|
332
|
+
String decodedProof = new String(Base64.getUrlDecoder().decode(paymentProof), StandardCharsets.UTF_8);
|
|
333
|
+
JSONObject proofJson = JSONObject.parseObject(decodedProof);
|
|
334
|
+
JSONObject protocol = proofJson.getJSONObject("protocol");
|
|
335
|
+
if (protocol != null) {
|
|
336
|
+
paymentProofValue = protocol.getString("payment_proof");
|
|
337
|
+
tradeNo = protocol.getString("trade_no");
|
|
338
|
+
}
|
|
339
|
+
JSONObject method = proofJson.getJSONObject("method");
|
|
340
|
+
if (method != null) {
|
|
341
|
+
clientSession = method.getString("client_session");
|
|
342
|
+
}
|
|
343
|
+
if (!hasText(paymentProofValue) || !hasText(tradeNo)) {
|
|
344
|
+
return createPaymentRequiredResponse();
|
|
345
|
+
}
|
|
346
|
+
} catch (Exception e) {
|
|
347
|
+
System.err.println("Payment-Proof 解析失败:" + e.getMessage());
|
|
348
|
+
return createPaymentRequiredResponse();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 2. 调用支付宝 API 验证支付凭证
|
|
352
|
+
AlipayAipayAgentPaymentVerifyRequest verifyRequest = new AlipayAipayAgentPaymentVerifyRequest();
|
|
353
|
+
// 当前已验证 Java SDK 的 Model 未暴露可选 client_session setter,
|
|
354
|
+
// 使用请求自身的结构化 bizContent 能力,避免调用不存在的方法。
|
|
355
|
+
JSONObject verifyBizContent = new JSONObject();
|
|
356
|
+
verifyBizContent.put("payment_proof", paymentProofValue);
|
|
357
|
+
verifyBizContent.put("trade_no", tradeNo);
|
|
358
|
+
if (hasText(clientSession)) {
|
|
359
|
+
verifyBizContent.put("client_session", clientSession);
|
|
360
|
+
}
|
|
361
|
+
verifyRequest.setBizContent(verifyBizContent.toJSONString());
|
|
362
|
+
AlipayAipayAgentPaymentVerifyResponse verifyResponse = alipayClient.execute(verifyRequest);
|
|
363
|
+
|
|
364
|
+
if (!verifyResponse.isSuccess()) {
|
|
365
|
+
System.err.println("支付凭证验证失败: " + verifyResponse.getSubMsg());
|
|
366
|
+
return createPaymentRequiredResponse();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
String returnedTradeNo = emptyIfNull(verifyResponse.getTradeNo());
|
|
370
|
+
String verifyOutTradeNo = emptyIfNull(verifyResponse.getOutTradeNo());
|
|
371
|
+
String returnedAmount = emptyIfNull(verifyResponse.getAmount());
|
|
372
|
+
String returnedResourceId = emptyIfNull(verifyResponse.getResourceId());
|
|
373
|
+
Boolean active = verifyResponse.getActive();
|
|
374
|
+
OrderSnapshot order = hasText(verifyOutTradeNo)
|
|
375
|
+
? orderRepository.findByOutTradeNo(verifyOutTradeNo)
|
|
376
|
+
: null;
|
|
377
|
+
boolean sandboxMode = isExactSandboxMode();
|
|
378
|
+
String verifyTradeNo = hasText(returnedTradeNo) ? returnedTradeNo : (sandboxMode ? tradeNo : "");
|
|
379
|
+
String verifyAmount = hasText(returnedAmount) ? returnedAmount : (sandboxMode && order != null ? order.amount : "");
|
|
380
|
+
String resourceIdVerified = hasText(returnedResourceId)
|
|
381
|
+
? returnedResourceId
|
|
382
|
+
: (sandboxMode && order != null ? order.resourceId : "");
|
|
383
|
+
|
|
384
|
+
if (!Boolean.TRUE.equals(active)
|
|
385
|
+
|| !hasText(verifyTradeNo)
|
|
386
|
+
|| !verifyTradeNo.equals(tradeNo)
|
|
387
|
+
|| !hasText(verifyOutTradeNo)
|
|
388
|
+
|| !hasText(resourceIdVerified)) {
|
|
389
|
+
return createPaymentRequiredResponse();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
boolean amountMatches = order != null && amountsEqual(order.amount, verifyAmount);
|
|
393
|
+
boolean resourceMatches = order != null
|
|
394
|
+
&& RESOURCE_PATH.equals(order.resourceId)
|
|
395
|
+
&& order.resourceId.equals(resourceIdVerified);
|
|
396
|
+
boolean fulfillmentInProgress = order != null
|
|
397
|
+
&& Arrays.asList("PENDING_CONFIRM", "FULFILLED").contains(order.fulfillStatus);
|
|
398
|
+
boolean orderUsable = order != null
|
|
399
|
+
&& "CNY".equals(order.currency)
|
|
400
|
+
&& Arrays.asList("PENDING_PAYMENT", "PAID").contains(order.orderStatus)
|
|
401
|
+
&& Arrays.asList("UNFULFILLED", "PENDING_CONFIRM", "FULFILLED").contains(order.fulfillStatus)
|
|
402
|
+
&& (fulfillmentInProgress || isFuture(order.payBefore));
|
|
403
|
+
if (!amountMatches || !resourceMatches || !orderUsable) {
|
|
404
|
+
return createPaymentRequiredResponse();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
FulfillmentPreparation fulfillment = orderRepository.prepareFulfillment(new FulfillmentRequest(
|
|
408
|
+
verifyOutTradeNo,
|
|
409
|
+
verifyTradeNo,
|
|
410
|
+
normalizeAmount(verifyAmount),
|
|
411
|
+
resourceIdVerified,
|
|
412
|
+
() -> generateServiceResource(resourceIdVerified)));
|
|
413
|
+
if (fulfillment == null
|
|
414
|
+
|| !("PENDING_CONFIRM".equals(fulfillment.state) || "FULFILLED".equals(fulfillment.state))
|
|
415
|
+
|| !hasText(fulfillment.serviceResult)) {
|
|
416
|
+
throw new IllegalStateException("OrderRepository.prepareFulfillment 未返回已持久化的履约结果");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if ("FULFILLED".equals(fulfillment.state)) {
|
|
420
|
+
return sendSuccessfulResource(
|
|
421
|
+
verifyTradeNo, verifyOutTradeNo, resourceIdVerified, fulfillment.serviceResult, true);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
System.out.println("资源已生成,准备发送履约确认: outTradeNo=" + verifyOutTradeNo
|
|
425
|
+
+ ", tradeNo=" + verifyTradeNo);
|
|
426
|
+
boolean fulfillmentConfirmed = sendFulfillmentConfirm(verifyTradeNo);
|
|
427
|
+
if (!fulfillmentConfirmed) {
|
|
428
|
+
JSONObject errorResponse = new JSONObject();
|
|
429
|
+
errorResponse.put("code", "FULFILLMENT_CONFIRM_FAILED");
|
|
430
|
+
errorResponse.put("message", "资源已生成但履约确认失败,请稍后使用同一 Payment-Proof 重试");
|
|
431
|
+
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(errorResponse);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
orderRepository.markFulfilled(verifyOutTradeNo, verifyTradeNo);
|
|
435
|
+
return sendSuccessfulResource(
|
|
436
|
+
verifyTradeNo, verifyOutTradeNo, resourceIdVerified, fulfillment.serviceResult, false);
|
|
437
|
+
} catch (AlipayApiException e) {
|
|
438
|
+
System.err.println("支付凭证验证异常: " + e.getErrMsg());
|
|
439
|
+
JSONObject errorResponse = new JSONObject();
|
|
440
|
+
errorResponse.put("code", "VERIFY_FAILED");
|
|
441
|
+
errorResponse.put("message", "支付凭证验证失败: " + e.getErrMsg());
|
|
442
|
+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
|
|
443
|
+
} catch (Exception e) {
|
|
444
|
+
System.err.println("履约处理异常: " + e.getMessage());
|
|
445
|
+
JSONObject errorResponse = new JSONObject();
|
|
446
|
+
errorResponse.put("code", "FULFILLMENT_ERROR");
|
|
447
|
+
errorResponse.put("message", "履约处理失败: " + e.getMessage());
|
|
448
|
+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private ResponseEntity<?> sendSuccessfulResource(String tradeNo, String outTradeNo,
|
|
453
|
+
String resourceId, String serviceResult,
|
|
454
|
+
boolean alreadyFulfilled) {
|
|
455
|
+
JSONObject payload = new JSONObject();
|
|
456
|
+
payload.put("trade_no", tradeNo);
|
|
457
|
+
payload.put("out_trade_no", outTradeNo);
|
|
458
|
+
payload.put("validated", true);
|
|
459
|
+
payload.put("resource_id", resourceId);
|
|
460
|
+
String paymentValidationEncoded = Base64.getUrlEncoder()
|
|
461
|
+
.withoutPadding()
|
|
462
|
+
.encodeToString(payload.toJSONString().getBytes(StandardCharsets.UTF_8));
|
|
463
|
+
|
|
464
|
+
JSONObject resourceContent = new JSONObject();
|
|
465
|
+
resourceContent.put("resource_id", resourceId);
|
|
466
|
+
resourceContent.put("content", serviceResult);
|
|
467
|
+
resourceContent.put("trade_no", tradeNo);
|
|
468
|
+
resourceContent.put("out_trade_no", outTradeNo);
|
|
469
|
+
resourceContent.put("already_fulfilled", alreadyFulfilled);
|
|
470
|
+
resourceContent.put("fulfillment_confirmed", true);
|
|
471
|
+
|
|
472
|
+
HttpHeaders headers = new HttpHeaders();
|
|
473
|
+
headers.set("Payment-Validation", paymentValidationEncoded);
|
|
474
|
+
return ResponseEntity.ok().headers(headers).body(resourceContent);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* 生成服务资源内容
|
|
479
|
+
*
|
|
480
|
+
* 这里演示生成AI内容。实际项目中可替换为任意数字服务内容,例如:
|
|
481
|
+
* - 报告摘要
|
|
482
|
+
* - 数据下载链接
|
|
483
|
+
* - AI 生成结果
|
|
484
|
+
* - 第三方接口调用额度发放结果
|
|
485
|
+
*
|
|
486
|
+
* @param resourceId 资源ID
|
|
487
|
+
* @return 服务资源内容
|
|
488
|
+
*/
|
|
489
|
+
private String generateServiceResource(String resourceId) {
|
|
490
|
+
// 示例:生成AI内容
|
|
491
|
+
JSONObject result = new JSONObject();
|
|
492
|
+
result.put("status", "success");
|
|
493
|
+
result.put("service_type", "AI_CONTENT_GENERATION");
|
|
494
|
+
result.put("resource_id", resourceId);
|
|
495
|
+
result.put("content", "这是AI生成的内容示例,可根据实际业务替换为任意数字服务内容");
|
|
496
|
+
result.put("generated_at", ZonedDateTime.now(ZoneId.of("Asia/Shanghai")).toString());
|
|
497
|
+
return result.toJSONString();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ==================== 3. 发送履约确认 ====================
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* 发送履约确认
|
|
504
|
+
*
|
|
505
|
+
* 商家向用户交付资源后,调用此接口向支付宝发送履约回执
|
|
506
|
+
*
|
|
507
|
+
* @param tradeNo 支付宝订单号
|
|
508
|
+
* @return 履约确认是否成功
|
|
509
|
+
*/
|
|
510
|
+
private boolean sendFulfillmentConfirm(String tradeNo) {
|
|
511
|
+
if (tradeNo == null || tradeNo.trim().isEmpty()) {
|
|
512
|
+
System.err.println("履约确认失败: tradeNo 为空");
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
try {
|
|
517
|
+
System.out.println("开始发送履约确认: tradeNo=" + tradeNo);
|
|
518
|
+
|
|
519
|
+
AlipayAipayAgentFulfillmentConfirmRequest request = new AlipayAipayAgentFulfillmentConfirmRequest();
|
|
520
|
+
AlipayAipayAgentFulfillmentConfirmModel model = new AlipayAipayAgentFulfillmentConfirmModel();
|
|
521
|
+
|
|
522
|
+
// 设置交易号
|
|
523
|
+
model.setTradeNo(tradeNo);
|
|
524
|
+
|
|
525
|
+
request.setBizModel(model);
|
|
526
|
+
|
|
527
|
+
AlipayAipayAgentFulfillmentConfirmResponse response = alipayClient.execute(request);
|
|
528
|
+
|
|
529
|
+
if (response.isSuccess()) {
|
|
530
|
+
System.out.println("履约确认成功: tradeNo=" + tradeNo);
|
|
531
|
+
return true;
|
|
532
|
+
} else {
|
|
533
|
+
System.err.println("履约确认失败: tradeNo=" + tradeNo + ", errorCode=" + response.getSubCode() + ", errorMsg=" + response.getSubMsg());
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
} catch (AlipayApiException e) {
|
|
538
|
+
System.err.println("履约确认异常: tradeNo=" + tradeNo + ", error=" + e.getErrMsg());
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ==================== 工具方法 ====================
|
|
544
|
+
|
|
545
|
+
private static boolean hasText(String value) {
|
|
546
|
+
return value != null && !value.trim().isEmpty();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private static String emptyIfNull(String value) {
|
|
550
|
+
return value == null ? "" : value.trim();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private static String normalizeAmount(String value) {
|
|
554
|
+
BigDecimal amount = parseAmount(value);
|
|
555
|
+
if (amount == null) {
|
|
556
|
+
throw new IllegalArgumentException("金额格式非法");
|
|
557
|
+
}
|
|
558
|
+
return amount.setScale(2).toPlainString();
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private static boolean amountsEqual(String left, String right) {
|
|
562
|
+
BigDecimal leftAmount = parseAmount(left);
|
|
563
|
+
BigDecimal rightAmount = parseAmount(right);
|
|
564
|
+
return leftAmount != null && rightAmount != null && leftAmount.compareTo(rightAmount) == 0;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private static BigDecimal parseAmount(String value) {
|
|
568
|
+
if (!hasText(value) || !value.trim().matches("\\d+(?:\\.\\d{1,2})?")) {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
BigDecimal amount = new BigDecimal(value.trim());
|
|
573
|
+
return amount.signum() >= 0 ? amount : null;
|
|
574
|
+
} catch (NumberFormatException e) {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private static boolean isFuture(String value) {
|
|
580
|
+
try {
|
|
581
|
+
return hasText(value) && ZonedDateTime.parse(value).isAfter(ZonedDateTime.now());
|
|
582
|
+
} catch (Exception e) {
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
private static boolean isExactSandboxMode() {
|
|
588
|
+
return SANDBOX_GATEWAY.equals(GATEWAY) && "api_mock_service_id".equals(SERVICE_ID);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* 生成商家签名(seller_signature)
|
|
593
|
+
*
|
|
594
|
+
* 参考文档:第5章节"私钥加签"
|
|
595
|
+
*
|
|
596
|
+
* @param params 待签名参数
|
|
597
|
+
* @param privateKey 商户私钥
|
|
598
|
+
* @return Base64编码的签名
|
|
599
|
+
* @throws AlipayApiException 签名异常
|
|
600
|
+
*/
|
|
601
|
+
private String generateSellerSignature(Map<String, String> params, String privateKey) throws AlipayApiException {
|
|
602
|
+
// 1. 按key字典序排序
|
|
603
|
+
List<String> keys = new ArrayList<>(params.keySet());
|
|
604
|
+
Collections.sort(keys);
|
|
605
|
+
|
|
606
|
+
// 2. 拼接签名内容
|
|
607
|
+
StringBuilder signContent = new StringBuilder();
|
|
608
|
+
for (int i = 0; i < keys.size(); i++) {
|
|
609
|
+
String key = keys.get(i);
|
|
610
|
+
String value = params.get(key);
|
|
611
|
+
if (value != null && !value.trim().isEmpty()) {
|
|
612
|
+
signContent.append(key).append("=").append(value);
|
|
613
|
+
if (i < keys.size() - 1) {
|
|
614
|
+
signContent.append("&");
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// 3. RSA2签名
|
|
620
|
+
return AlipaySignature.rsaSign(signContent.toString(), privateKey, "UTF-8", "RSA2");
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
```
|