@alipay/alipay-aipay 0.1.1-beta.1
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 +1 -0
- package/bin/alipay-aipay.js +6 -0
- package/dist/commands/install.d.ts +10 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +92 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/uninstall.d.ts +10 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +82 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/commands/update.d.ts +10 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +125 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/alipay-cli.d.ts +15 -0
- package/dist/utils/alipay-cli.d.ts.map +1 -0
- package/dist/utils/alipay-cli.js +228 -0
- package/dist/utils/alipay-cli.js.map +1 -0
- package/dist/utils/config.d.ts +48 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +207 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/file.d.ts +55 -0
- package/dist/utils/file.d.ts.map +1 -0
- package/dist/utils/file.js +154 -0
- package/dist/utils/file.js.map +1 -0
- package/dist/utils/gpg.d.ts +14 -0
- package/dist/utils/gpg.d.ts.map +1 -0
- package/dist/utils/gpg.js +247 -0
- package/dist/utils/gpg.js.map +1 -0
- package/dist/utils/jq.d.ts +14 -0
- package/dist/utils/jq.d.ts.map +1 -0
- package/dist/utils/jq.js +221 -0
- package/dist/utils/jq.js.map +1 -0
- package/dist/utils/npm.d.ts +17 -0
- package/dist/utils/npm.d.ts.map +1 -0
- package/dist/utils/npm.js +42 -0
- package/dist/utils/npm.js.map +1 -0
- package/dist/utils/shell.d.ts +11 -0
- package/dist/utils/shell.d.ts.map +1 -0
- package/dist/utils/shell.js +54 -0
- package/dist/utils/shell.js.map +1 -0
- package/package.json +53 -0
- package/skill/SKILL.md +118 -0
- package/skill/references/integration/flow.md +487 -0
- package/skill/references/integration/modules/alipay-sdk-reminder.md +256 -0
- package/skill/references/integration/modules/checklist.md +122 -0
- package/skill/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/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 +77 -0
- package/skill/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 +74 -0
- package/skill/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/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 +58 -0
- package/skill/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 +128 -0
- package/skill/references/integration/modules/code-examples/csharp/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.cs +500 -0
- package/skill/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/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 +80 -0
- package/skill/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 +78 -0
- package/skill/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/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 +66 -0
- package/skill/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 +135 -0
- package/skill/references/integration/modules/code-examples/java/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemoController.java +610 -0
- package/skill/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/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 +57 -0
- package/skill/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 +55 -0
- package/skill/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/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 +77 -0
- package/skill/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 +65 -0
- package/skill/references/integration/modules/code-examples/nodejs/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.js +461 -0
- package/skill/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/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 +67 -0
- package/skill/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 +66 -0
- package/skill/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/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 +56 -0
- package/skill/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 +107 -0
- package/skill/references/integration/modules/code-examples/php/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.php +444 -0
- package/skill/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/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 +65 -0
- package/skill/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 +63 -0
- package/skill/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/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 +48 -0
- package/skill/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 +43 -0
- package/skill/references/integration/modules/code-examples/python/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.py +465 -0
- package/skill/references/integration/modules/interface-guide.md +90 -0
- package/skill/references/integration/modules/product-decision.md +90 -0
- package/skill/references/integration/modules/sandbox/a2m-sandbox-test.md +270 -0
- package/skill/references/integration/modules/sandbox/alipay-sandbox-tool.md +133 -0
- package/skill/references/integration/modules/sandbox/sandbox-setup-guide.md +218 -0
- package/skill/references/integration/modules/scripts/local_402_sandbox_pay.py +659 -0
- package/skill/references/normal/alipay-cli-env.md +211 -0
- package/skill/references/normal/rejection-guide.md +37 -0
- package/skill/references/normal/scripts/common.sh +32 -0
- package/skill/references/normal/scripts/detect_dev_tool.sh +46 -0
- package/skill/references/onboarding/flow.md +571 -0
- package/skill/references/onboarding/modules/app-release.md +411 -0
- package/skill/references/onboarding/modules/authorization.md +269 -0
- package/skill/references/onboarding/modules/error-handling.md +247 -0
- package/skill/references/onboarding/modules/mcc-reference.md +209 -0
- package/skill/references/onboarding/modules/product-sign.md +304 -0
- package/skill/references/onboarding/modules/scripts/app.sh +705 -0
- package/skill/references/onboarding/modules/scripts/ar_sign_apply.sh +248 -0
- package/skill/references/onboarding/modules/scripts/auth.sh +505 -0
- package/skill/references/onboarding/modules/scripts/error_handler.sh +312 -0
- package/skill/references/onboarding/modules/scripts/query_sign_status.sh +146 -0
- package/skill/references/onboarding/modules/scripts/service.sh +267 -0
- package/skill/references/onboarding/modules/scripts/upload_screenshots.sh +152 -0
- package/skill/references/onboarding/modules/service-registration.md +294 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2015-2026 Ant Group
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
package com.alipay.aipayweb.demo;
|
|
17
|
+
|
|
18
|
+
import com.alibaba.fastjson.JSONObject;
|
|
19
|
+
import com.alipay.api.AlipayApiException;
|
|
20
|
+
import com.alipay.api.AlipayClient;
|
|
21
|
+
import com.alipay.api.AlipayConfig;
|
|
22
|
+
import com.alipay.api.DefaultAlipayClient;
|
|
23
|
+
import com.alipay.api.domain.AlipayAipayAgentFulfillmentConfirmModel;
|
|
24
|
+
import com.alipay.api.domain.AlipayAipayAgentPaymentVerifyModel;
|
|
25
|
+
import com.alipay.api.internal.util.AlipaySignature;
|
|
26
|
+
import com.alipay.api.request.AlipayAipayAgentFulfillmentConfirmRequest;
|
|
27
|
+
import com.alipay.api.request.AlipayAipayAgentPaymentVerifyRequest;
|
|
28
|
+
import com.alipay.api.response.AlipayAipayAgentFulfillmentConfirmResponse;
|
|
29
|
+
import com.alipay.api.response.AlipayAipayAgentPaymentVerifyResponse;
|
|
30
|
+
import org.springframework.http.HttpHeaders;
|
|
31
|
+
import org.springframework.http.HttpStatus;
|
|
32
|
+
import org.springframework.http.ResponseEntity;
|
|
33
|
+
import org.springframework.web.bind.annotation.GetMapping;
|
|
34
|
+
import org.springframework.web.bind.annotation.RequestHeader;
|
|
35
|
+
import org.springframework.web.bind.annotation.RequestMapping;
|
|
36
|
+
import org.springframework.web.bind.annotation.RestController;
|
|
37
|
+
|
|
38
|
+
import java.nio.charset.StandardCharsets;
|
|
39
|
+
import java.time.ZoneId;
|
|
40
|
+
import java.time.ZonedDateTime;
|
|
41
|
+
import java.time.format.DateTimeFormatter;
|
|
42
|
+
import java.util.*;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A2M智能收产品接入示例 Controller
|
|
46
|
+
*
|
|
47
|
+
* 本Controller为开发者示例代码,演示完整的智能收产品接入流程:
|
|
48
|
+
* 1. 返回 402 Payment-Needed Header
|
|
49
|
+
* 2. 验证 Payment-Proof 支付凭证
|
|
50
|
+
* 3. 发送履约回执确认
|
|
51
|
+
* 4. 返回资源内容
|
|
52
|
+
*
|
|
53
|
+
* GET /demo/a2m/resource
|
|
54
|
+
* ├─ 场景1: 无 Payment-Proof Header
|
|
55
|
+
* │ └─ 返回 402 + Payment-Needed Header
|
|
56
|
+
* │
|
|
57
|
+
* └─ 场景2: 有 Payment-Proof Header
|
|
58
|
+
* ├─ 调用支付宝API验证凭证
|
|
59
|
+
* ├─ 从响应获取: tradeNo, outTradeNo, resourceId, active
|
|
60
|
+
* ├─ 校验 active=true (凭证有效性)
|
|
61
|
+
* ├─ 资源防串校验
|
|
62
|
+
* ├─ 履约防重放校验
|
|
63
|
+
* ├─ 发送履约确认
|
|
64
|
+
* ├─ 返回资源内容
|
|
65
|
+
*
|
|
66
|
+
* 注意:本文档为示例代码,仅用于演示接入流程,开发者需根据实际业务场景进行调整。
|
|
67
|
+
*
|
|
68
|
+
* 按量付费生产实现提醒:
|
|
69
|
+
* - 返回 Payment-Needed 前必须把 outTradeNo、resourceId、amount、订单状态和过期时间持久化到商户数据库。
|
|
70
|
+
* - 本地内存订单在服务重启后会丢失,旧付款链接对应的 Payment-Proof 将无法再匹配本地订单。
|
|
71
|
+
* - 沙箱验付响应中的 outTradeNo/resourceId 可能为空字符串,联调排查时先判空;生产环境不得因字段为空而跳过核心校验。
|
|
72
|
+
* - 履约结果应先保存为可重试状态,fulfillment.confirm 成功后再标记订单 FULFILLED 并返回成功交付。
|
|
73
|
+
*
|
|
74
|
+
*/
|
|
75
|
+
@RestController
|
|
76
|
+
@RequestMapping("/demo/a2m")
|
|
77
|
+
public class A2MPaymentDemoController {
|
|
78
|
+
|
|
79
|
+
// ==================== 配置信息(实际使用时请从配置中心读取)====================
|
|
80
|
+
// 以下配置仅为示例,实际开发时请替换为真实配置或从配置中心读取
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 支付宝SDK客户端
|
|
84
|
+
*/
|
|
85
|
+
private final AlipayClient alipayClient;
|
|
86
|
+
|
|
87
|
+
public A2MPaymentDemoController() {
|
|
88
|
+
// 使用AlipayConfig初始化支付宝客户端(推荐方式)
|
|
89
|
+
try {
|
|
90
|
+
this.alipayClient = new DefaultAlipayClient(getAlipayConfig());
|
|
91
|
+
} catch (AlipayApiException e) {
|
|
92
|
+
throw new RuntimeException("初始化支付宝客户端失败", e);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 获取支付宝配置
|
|
98
|
+
*
|
|
99
|
+
* @return AlipayConfig
|
|
100
|
+
*/
|
|
101
|
+
private static AlipayConfig getAlipayConfig() {
|
|
102
|
+
AlipayConfig alipayConfig = new AlipayConfig();
|
|
103
|
+
alipayConfig.setServerUrl("https://openapi.alipay.com/gateway.do");
|
|
104
|
+
alipayConfig.setAppId("<APP_ID>");
|
|
105
|
+
alipayConfig.setPrivateKey("<APP_PRIVATE_KEY>"); // 请填写您的应用私钥
|
|
106
|
+
alipayConfig.setFormat("json");
|
|
107
|
+
alipayConfig.setAlipayPublicKey("<ALIPAY_PUBLIC_KEY>"); // 请填写您的支付宝公钥
|
|
108
|
+
alipayConfig.setCharset("UTF-8");
|
|
109
|
+
alipayConfig.setSignType("RSA2");
|
|
110
|
+
return alipayConfig;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ==================== 智能收产品接入示例接口 ====================
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 智能收产品统一接口
|
|
117
|
+
*
|
|
118
|
+
* 完整流程演示:
|
|
119
|
+
* 1. 不带 Payment-Proof Header:返回 HTTP 402 + Payment-Needed Header
|
|
120
|
+
* 2. 带 Payment-Proof Header:验证支付 → 自动履约 → 返回资源
|
|
121
|
+
*
|
|
122
|
+
* @param paymentProof 支付凭证(从Header获取,可选)
|
|
123
|
+
* @return 未支付时返回402,已支付时返回资源内容
|
|
124
|
+
*/
|
|
125
|
+
@GetMapping("/resource")
|
|
126
|
+
public ResponseEntity<?> getResource(
|
|
127
|
+
@RequestHeader(value = "Payment-Proof", required = false) String paymentProof) {
|
|
128
|
+
|
|
129
|
+
// 场景1:用户未支付,返回402 + Payment-Needed Header
|
|
130
|
+
if (paymentProof == null || paymentProof.trim().isEmpty()) {
|
|
131
|
+
return createPaymentRequiredResponse();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 场景2:用户已支付,验证Payment-Proof并返回资源
|
|
135
|
+
return verifyPaymentAndDeliverResource(paymentProof);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 创建402支付请求响应
|
|
140
|
+
*
|
|
141
|
+
* @return 402响应 + Payment-Needed Header
|
|
142
|
+
*/
|
|
143
|
+
private ResponseEntity<?> createPaymentRequiredResponse() {
|
|
144
|
+
try {
|
|
145
|
+
// 1. 构造订单信息
|
|
146
|
+
String outTradeNo = "ORDER_" + System.currentTimeMillis(); //用户可自定义外部订单号的生成逻辑
|
|
147
|
+
String amount = "0.01"; // 单位:元
|
|
148
|
+
String currency = "CNY"; //固定用CNY
|
|
149
|
+
String resourceId = "/demo/a2m/resource"; //用户可自定义资源id的生成逻辑,用于资源防串
|
|
150
|
+
String goodsName = "AI 生成内容服务"; //用户可自行定义商品名称
|
|
151
|
+
|
|
152
|
+
// 2. 计算支付截止时间(30分钟后),用户可自行设置
|
|
153
|
+
ZonedDateTime payBefore = ZonedDateTime.now(ZoneId.of("Asia/Shanghai")).plusMinutes(30);
|
|
154
|
+
String payBeforeStr = payBefore.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
|
|
155
|
+
|
|
156
|
+
// 示例逻辑:
|
|
157
|
+
// 必须在返回 402 前落库。否则用户付款后携带 Payment-Proof 回来时,
|
|
158
|
+
// 商户服务无法把支付宝验付结果映射回自己的本地订单,会出现 ORDER_NOT_FOUND。
|
|
159
|
+
// OrderEntity order = new OrderEntity();
|
|
160
|
+
// order.setOutTradeNo(outTradeNo);
|
|
161
|
+
// order.setAmount(new BigDecimal(amount)));
|
|
162
|
+
// order.setCurrency(currency);
|
|
163
|
+
// order.setResourceId(resourceId);
|
|
164
|
+
// order.setGoodsName(goodsName);
|
|
165
|
+
// order.setPayBefore(payBeforeStr);
|
|
166
|
+
// order.setOrderStatus("INIT");
|
|
167
|
+
// order.setFulfillStatus("UNFULFILLED");
|
|
168
|
+
// order.setCreatedAt(LocalDateTime.now());
|
|
169
|
+
// order.setUpdatedAt(LocalDateTime.now());
|
|
170
|
+
// order.setClientIp(getClientIp()); // 从request获取客户端IP
|
|
171
|
+
// orderRepository.save(order);
|
|
172
|
+
|
|
173
|
+
// 4. 生成商家签名(需要使用商户私钥)
|
|
174
|
+
// 注意:实际使用时请从配置中读取商户ID和服务ID
|
|
175
|
+
String sellerId = "<SELLER_ID_2088>"; // 商户ID(2088格式)
|
|
176
|
+
String serviceId = "<SERVICE_ID>"; // 商户服务ID
|
|
177
|
+
|
|
178
|
+
Map<String, String> signParams = new HashMap<>();
|
|
179
|
+
signParams.put("amount", amount);
|
|
180
|
+
signParams.put("currency", currency);
|
|
181
|
+
signParams.put("goods_name", goodsName);
|
|
182
|
+
signParams.put("out_trade_no", outTradeNo);
|
|
183
|
+
signParams.put("pay_before", payBeforeStr);
|
|
184
|
+
signParams.put("resource_id", resourceId);
|
|
185
|
+
signParams.put("seller_id", sellerId);
|
|
186
|
+
signParams.put("service_id", serviceId);
|
|
187
|
+
|
|
188
|
+
// 注意:实际使用时需要从配置中读取商户私钥进行签名
|
|
189
|
+
String privateKey = "<APP_PRIVATE_KEY>"; // 请填写您的应用私钥
|
|
190
|
+
String sellerSignature = generateSellerSignature(signParams, privateKey);
|
|
191
|
+
|
|
192
|
+
// 5. 构造 Payment-Needed Header 内容(分层结构)
|
|
193
|
+
JSONObject paymentNeeded = new JSONObject();
|
|
194
|
+
|
|
195
|
+
// protocol 层
|
|
196
|
+
JSONObject protocol = new JSONObject();
|
|
197
|
+
protocol.put("out_trade_no", outTradeNo);
|
|
198
|
+
protocol.put("amount", amount);
|
|
199
|
+
protocol.put("currency", currency);
|
|
200
|
+
protocol.put("resource_id", resourceId);
|
|
201
|
+
protocol.put("pay_before", payBeforeStr);
|
|
202
|
+
protocol.put("seller_signature", sellerSignature);
|
|
203
|
+
protocol.put("seller_sign_type", "RSA2");
|
|
204
|
+
protocol.put("seller_unique_id", sellerId);
|
|
205
|
+
|
|
206
|
+
// method 层
|
|
207
|
+
JSONObject method = new JSONObject();
|
|
208
|
+
method.put("seller_name", "测试商户");
|
|
209
|
+
method.put("seller_id", sellerId);
|
|
210
|
+
method.put("seller_app_id", "<APP_ID>"); // 请填写您的 AppId
|
|
211
|
+
method.put("goods_name", goodsName);
|
|
212
|
+
method.put("seller_unique_id_key", "seller_id"); // 固定用卖家 id
|
|
213
|
+
method.put("service_id", serviceId);
|
|
214
|
+
|
|
215
|
+
paymentNeeded.put("protocol", protocol);
|
|
216
|
+
paymentNeeded.put("method", method);
|
|
217
|
+
// 6. Base64URL编码
|
|
218
|
+
String paymentNeededEncoded = Base64.getUrlEncoder()
|
|
219
|
+
.withoutPadding()
|
|
220
|
+
.encodeToString(paymentNeeded.toJSONString().getBytes(StandardCharsets.UTF_8));
|
|
221
|
+
|
|
222
|
+
// 7. 构造402响应
|
|
223
|
+
HttpHeaders headers = new HttpHeaders();
|
|
224
|
+
headers.set("Payment-Needed", paymentNeededEncoded);
|
|
225
|
+
|
|
226
|
+
JSONObject responseBody = new JSONObject();
|
|
227
|
+
responseBody.put("code", "Payment-Needed");
|
|
228
|
+
responseBody.put("message", "需要支付");
|
|
229
|
+
responseBody.put("out_trade_no", outTradeNo);
|
|
230
|
+
responseBody.put("amount", amount);
|
|
231
|
+
responseBody.put("currency", currency);
|
|
232
|
+
responseBody.put("goods_name", goodsName);
|
|
233
|
+
|
|
234
|
+
// 记录日志
|
|
235
|
+
System.out.println("创建支付订单成功: outTradeNo=" + outTradeNo + ", amount=" + amount);
|
|
236
|
+
|
|
237
|
+
return ResponseEntity.status(HttpStatus.PAYMENT_REQUIRED)
|
|
238
|
+
.headers(headers)
|
|
239
|
+
.body(responseBody);
|
|
240
|
+
|
|
241
|
+
} catch (AlipayApiException e) {
|
|
242
|
+
System.err.println("签名失败: " + e.getMessage());
|
|
243
|
+
JSONObject errorResponse = new JSONObject();
|
|
244
|
+
errorResponse.put("code", "SIGN_ERROR");
|
|
245
|
+
errorResponse.put("message", "签名失败: " + e.getMessage());
|
|
246
|
+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
|
|
247
|
+
} catch (Exception e) {
|
|
248
|
+
System.err.println("创建订单失败: " + e.getMessage());
|
|
249
|
+
JSONObject errorResponse = new JSONObject();
|
|
250
|
+
errorResponse.put("code", "CREATE_ORDER_ERROR");
|
|
251
|
+
errorResponse.put("message", "创建订单失败: " + e.getMessage());
|
|
252
|
+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 验证支付凭证并交付资源
|
|
258
|
+
*
|
|
259
|
+
* @param paymentProof 支付凭证
|
|
260
|
+
* @return 资源内容或错误信息
|
|
261
|
+
*/
|
|
262
|
+
private ResponseEntity<?> verifyPaymentAndDeliverResource(String paymentProof) {
|
|
263
|
+
try {
|
|
264
|
+
// 1. 从 Payment-Proof 中解析订单信息
|
|
265
|
+
// Payment-Proof 是 Base64URL 编码的 JSON,需要先解码
|
|
266
|
+
// Payment-Proof 结构:{"protocol":{"payment_proof":"xxx","trade_no":"xxx"},"method":{"client_session":"xxx"}}
|
|
267
|
+
String paymentProofValue = null;
|
|
268
|
+
String tradeNo = null;
|
|
269
|
+
String clientSession = null; // 新增:买家客户端会话标识
|
|
270
|
+
try {
|
|
271
|
+
String decodedProof = new String(Base64.getUrlDecoder().decode(paymentProof), StandardCharsets.UTF_8);
|
|
272
|
+
JSONObject proofJson = JSONObject.parseObject(decodedProof);
|
|
273
|
+
|
|
274
|
+
// 从 protocol 层获取 payment_proof 和 trade_no
|
|
275
|
+
JSONObject protocol = proofJson.getJSONObject("protocol");
|
|
276
|
+
if (protocol != null) {
|
|
277
|
+
paymentProofValue = protocol.getString("payment_proof");
|
|
278
|
+
tradeNo = protocol.getString("trade_no");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 从 method 层获取 client_session(本次新增)
|
|
282
|
+
JSONObject method = proofJson.getJSONObject("method");
|
|
283
|
+
if (method != null) {
|
|
284
|
+
clientSession = method.getString("client_session");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 校验必要字段
|
|
288
|
+
if (paymentProofValue == null || paymentProofValue.trim().isEmpty()) {
|
|
289
|
+
System.err.println("Payment-Proof 中缺少 payment_proof 字段");
|
|
290
|
+
JSONObject errorResponse = new JSONObject();
|
|
291
|
+
errorResponse.put("code", "INVALID_PAYMENT_PROOF_FORMAT");
|
|
292
|
+
errorResponse.put("message", "Payment-Proof 格式错误:缺少 payment_proof");
|
|
293
|
+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (tradeNo == null || tradeNo.trim().isEmpty()) {
|
|
297
|
+
System.err.println("Payment-Proof 中缺少 trade_no 字段");
|
|
298
|
+
JSONObject errorResponse = new JSONObject();
|
|
299
|
+
errorResponse.put("code", "INVALID_PAYMENT_PROOF_FORMAT");
|
|
300
|
+
errorResponse.put("message", "Payment-Proof 格式错误:缺少 trade_no");
|
|
301
|
+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
|
|
302
|
+
}
|
|
303
|
+
} catch (Exception e) {
|
|
304
|
+
System.err.println("Payment-Proof 解析失败:" + e.getMessage());
|
|
305
|
+
JSONObject errorResponse = new JSONObject();
|
|
306
|
+
errorResponse.put("code", "INVALID_PAYMENT_PROOF_FORMAT");
|
|
307
|
+
errorResponse.put("message", "Payment-Proof 格式错误:" + e.getMessage());
|
|
308
|
+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 2. 调用支付宝 API 验证支付凭证
|
|
312
|
+
// 参考:alipay.aipay.agent.payment.verify(A2A 商户支付凭证验证接口)
|
|
313
|
+
AlipayAipayAgentPaymentVerifyRequest verifyRequest = new AlipayAipayAgentPaymentVerifyRequest();
|
|
314
|
+
AlipayAipayAgentPaymentVerifyModel model = new AlipayAipayAgentPaymentVerifyModel();
|
|
315
|
+
|
|
316
|
+
// 设置支付凭证和支付宝订单号(均来自用户的 Payment-Proof)
|
|
317
|
+
model.setPaymentProof(paymentProofValue);
|
|
318
|
+
model.setTradeNo(tradeNo);
|
|
319
|
+
model.setClientSession(clientSession);
|
|
320
|
+
|
|
321
|
+
verifyRequest.setBizModel(model);
|
|
322
|
+
|
|
323
|
+
AlipayAipayAgentPaymentVerifyResponse verifyResponse = alipayClient.execute(verifyRequest);
|
|
324
|
+
|
|
325
|
+
// 3. 验证失败,返回错误
|
|
326
|
+
if (!verifyResponse.isSuccess()) {
|
|
327
|
+
System.err.println("支付凭证验证失败: " + verifyResponse.getSubMsg());
|
|
328
|
+
JSONObject errorResponse = new JSONObject();
|
|
329
|
+
errorResponse.put("code", verifyResponse.getSubCode());
|
|
330
|
+
errorResponse.put("message", verifyResponse.getSubMsg());
|
|
331
|
+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 4. 验证成功,获取订单信息
|
|
335
|
+
// 注意:验证响应中的订单信息可能与凭证中的信息一致,但仍需从响应中获取以确保安全性
|
|
336
|
+
String verifyTradeNo = verifyResponse.getTradeNo(); // 支付宝订单号
|
|
337
|
+
String verifyOutTradeNo = verifyResponse.getOutTradeNo(); // 商户订单号
|
|
338
|
+
String resourceId = verifyResponse.getResourceId(); // 资源ID
|
|
339
|
+
Boolean active = verifyResponse.getActive(); // 凭证有效标识
|
|
340
|
+
|
|
341
|
+
System.out.println("支付凭证验证成功: tradeNo=" + verifyTradeNo + ", outTradeNo=" + verifyOutTradeNo);
|
|
342
|
+
|
|
343
|
+
// 5. 校验凭证有效性(active=true表示凭证有效)
|
|
344
|
+
if (active == null || !active) {
|
|
345
|
+
System.err.println("支付凭证无效或已过期: outTradeNo=" + verifyOutTradeNo);
|
|
346
|
+
JSONObject errorResponse = new JSONObject();
|
|
347
|
+
errorResponse.put("code", "INVALID_PAYMENT_PROOF");
|
|
348
|
+
errorResponse.put("message", "支付凭证无效或已过期");
|
|
349
|
+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// 6. 查询订单是否存在(以数据库为准)
|
|
353
|
+
// 注意:alipay.aipay.agent.payment.verify 成功只代表支付宝凭证有效,
|
|
354
|
+
// 仍然需要映射回商户本地订单后才能交付资源。
|
|
355
|
+
// 沙箱环境下 verifyResponse.getOutTradeNo() 可能为空字符串,联调时使用前先判空;
|
|
356
|
+
// 生产环境应把关键字段为空作为异常处理,不应跳过本地订单校验。
|
|
357
|
+
// 如果本地调试使用内存仓储,服务重启会丢失订单;此时旧付款链接不能继续验证。
|
|
358
|
+
// 示例逻辑:
|
|
359
|
+
// if (!hasText(verifyOutTradeNo)) {
|
|
360
|
+
// JSONObject errorResponse = new JSONObject();
|
|
361
|
+
// errorResponse.put("code", "MISSING_OUT_TRADE_NO");
|
|
362
|
+
// errorResponse.put("message", "验付响应缺少商户订单号");
|
|
363
|
+
// return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
|
|
364
|
+
// }
|
|
365
|
+
// OrderEntity order = hasText(verifyOutTradeNo)
|
|
366
|
+
// ? orderRepository.findByOutTradeNo(verifyOutTradeNo).orElse(null)
|
|
367
|
+
// : null;
|
|
368
|
+
// if (order == null) {
|
|
369
|
+
// System.err.println("订单不存在: outTradeNo=" + verifyOutTradeNo);
|
|
370
|
+
// JSONObject errorResponse = new JSONObject();
|
|
371
|
+
// errorResponse.put("code", "ORDER_NOT_FOUND");
|
|
372
|
+
// errorResponse.put("message", "订单不存在");
|
|
373
|
+
// return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
|
|
374
|
+
// }
|
|
375
|
+
|
|
376
|
+
// 7. 资源防串校验
|
|
377
|
+
// 商户需要验证resourceId是否与商户系统中该outTradeNo对应的资源一致
|
|
378
|
+
// 防止恶意用户通过篡改resourceId获取其他资源
|
|
379
|
+
// 沙箱字段为空时不要把空字符串当作有效资源字段参与强校验;
|
|
380
|
+
// 生产环境应把关键资源字段缺失作为异常处理。
|
|
381
|
+
// 示例逻辑:
|
|
382
|
+
// if (!hasText(resourceId)) {
|
|
383
|
+
// JSONObject errorResponse = new JSONObject();
|
|
384
|
+
// errorResponse.put("code", "MISSING_RESOURCE_ID");
|
|
385
|
+
// errorResponse.put("message", "验付响应缺少资源标识");
|
|
386
|
+
// return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
|
|
387
|
+
// }
|
|
388
|
+
// if (!resourceId.equals(order.getResourceId())) {
|
|
389
|
+
// System.err.println("资源ID不匹配,可能存在资源串改风险: orderResourceId=" + order.getResourceId() +
|
|
390
|
+
// ", requestResourceId=" + resourceId);
|
|
391
|
+
// JSONObject errorResponse = new JSONObject();
|
|
392
|
+
// errorResponse.put("code", "RESOURCE_ID_MISMATCH");
|
|
393
|
+
// errorResponse.put("message", "资源ID不匹配,可能存在资源串改风险");
|
|
394
|
+
// return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errorResponse);
|
|
395
|
+
// }
|
|
396
|
+
|
|
397
|
+
// 8. 履约防重放校验(数据库幂等控制)
|
|
398
|
+
// 商户需要根据outTradeNo查询自己的业务订单,检查是否已经履约
|
|
399
|
+
// 防止重复履约和重复交付资源
|
|
400
|
+
// 示例逻辑:
|
|
401
|
+
// if ("FULFILLED".equals(order.getFulfillStatus())) {
|
|
402
|
+
// System.out.println("订单已履约,返回历史履约结果: outTradeNo=" + verifyOutTradeNo);
|
|
403
|
+
//
|
|
404
|
+
// // 查询历史履约记录
|
|
405
|
+
// FulfillmentRecordEntity record = fulfillmentRecordRepository
|
|
406
|
+
// .findTopByOrderNoOrderByCreatedAtDesc(verifyOutTradeNo).orElse(null);
|
|
407
|
+
//
|
|
408
|
+
// JSONObject response = new JSONObject();
|
|
409
|
+
// response.put("code", "ALREADY_FULFILLED");
|
|
410
|
+
// response.put("message", "订单已履约,不重复提供");
|
|
411
|
+
// response.put("already_fulfilled", true);
|
|
412
|
+
// if (record != null) {
|
|
413
|
+
// response.put("resource_content", record.getServiceResult());
|
|
414
|
+
// response.put("fulfilled_at", record.getCreatedAt().toString());
|
|
415
|
+
// }
|
|
416
|
+
// return ResponseEntity.ok(response);
|
|
417
|
+
// }
|
|
418
|
+
|
|
419
|
+
// 9. 更新订单状态为已支付
|
|
420
|
+
// 示例逻辑:
|
|
421
|
+
// order.setOrderStatus("PAID");
|
|
422
|
+
// order.setTradeNo(verifyTradeNo); // 保存支付宝订单号
|
|
423
|
+
// order.setPaidAt(LocalDateTime.now());
|
|
424
|
+
// order.setUpdatedAt(LocalDateTime.now());
|
|
425
|
+
// orderRepository.save(order);
|
|
426
|
+
|
|
427
|
+
// 10. 执行业务逻辑,生成资源内容
|
|
428
|
+
String serviceResult = generateServiceResource(resourceId);
|
|
429
|
+
|
|
430
|
+
// 11. 履约记录落库(用于审计/售后/对账)
|
|
431
|
+
// 示例逻辑:
|
|
432
|
+
// FulfillmentRecordEntity record = new FulfillmentRecordEntity();
|
|
433
|
+
// record.setOutTradeNo(verifyOutTradeNo);
|
|
434
|
+
// record.setTradeNo(verifyTradeNo);
|
|
435
|
+
// record.setServiceResult(serviceResult);
|
|
436
|
+
// record.setServiceType("AI_CONTENT_GENERATION");
|
|
437
|
+
// record.setCreatedAt(LocalDateTime.now());
|
|
438
|
+
// fulfillmentRecordRepository.save(record);
|
|
439
|
+
|
|
440
|
+
// 12. 保存待确认履约状态
|
|
441
|
+
// 实际生产中建议先保存 serviceResult 和 PENDING_CONFIRM 状态。
|
|
442
|
+
// fulfillment.confirm 成功后再标记 FULFILLED;如果确认失败,
|
|
443
|
+
// 可允许同一笔 Payment-Proof 重试确认,避免误返回成功。
|
|
444
|
+
// 示例逻辑:
|
|
445
|
+
// order.setFulfillStatus("PENDING_CONFIRM");
|
|
446
|
+
// order.setServiceResult(serviceResult);
|
|
447
|
+
// order.setUpdatedAt(LocalDateTime.now());
|
|
448
|
+
// orderRepository.save(order);
|
|
449
|
+
// 注意:如果生成资源会产生扣减额度、调用第三方、写业务数据等副作用,
|
|
450
|
+
// 必须把 serviceResult 和幂等键落库后再允许重试,否则重试可能重复生成或重复扣减。
|
|
451
|
+
|
|
452
|
+
System.out.println("资源已生成,准备发送履约确认: outTradeNo=" + verifyOutTradeNo + ", tradeNo=" + verifyTradeNo);
|
|
453
|
+
|
|
454
|
+
// 13. 发送履约确认到支付宝
|
|
455
|
+
boolean fulfillmentConfirmed = sendFulfillmentConfirm(verifyTradeNo);
|
|
456
|
+
if (!fulfillmentConfirmed) {
|
|
457
|
+
JSONObject errorResponse = new JSONObject();
|
|
458
|
+
errorResponse.put("code", "FULFILLMENT_CONFIRM_FAILED");
|
|
459
|
+
errorResponse.put("message", "资源已生成但履约确认失败,请稍后使用同一 Payment-Proof 重试");
|
|
460
|
+
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(errorResponse);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// 13.1 履约确认成功后,再标记订单已履约
|
|
464
|
+
// 示例逻辑:
|
|
465
|
+
// order.setFulfillStatus("FULFILLED");
|
|
466
|
+
// order.setFulfilledAt(LocalDateTime.now());
|
|
467
|
+
// order.setUpdatedAt(LocalDateTime.now());
|
|
468
|
+
// orderRepository.save(order);
|
|
469
|
+
|
|
470
|
+
System.out.println("履约确认成功: outTradeNo=" + verifyOutTradeNo + ", tradeNo=" + verifyTradeNo);
|
|
471
|
+
|
|
472
|
+
// payload 只包含最核心的验证信息
|
|
473
|
+
JSONObject payload = new JSONObject();
|
|
474
|
+
payload.put("trade_no", verifyTradeNo); // 支付宝订单号
|
|
475
|
+
payload.put("out_trade_no", verifyOutTradeNo); // 商户订单号
|
|
476
|
+
payload.put("validated", true); // 验证通过标识
|
|
477
|
+
payload.put("resource_id", resourceId); // 资源ID(校验用)
|
|
478
|
+
|
|
479
|
+
// Base64URL编码
|
|
480
|
+
String paymentValidationEncoded = Base64.getUrlEncoder()
|
|
481
|
+
.withoutPadding()
|
|
482
|
+
.encodeToString(payload.toJSONString().getBytes(StandardCharsets.UTF_8));
|
|
483
|
+
|
|
484
|
+
// 14. 返回资源内容
|
|
485
|
+
JSONObject resourceContent = new JSONObject();
|
|
486
|
+
resourceContent.put("resource_id", resourceId);
|
|
487
|
+
resourceContent.put("content", serviceResult);
|
|
488
|
+
resourceContent.put("trade_no", verifyTradeNo);
|
|
489
|
+
resourceContent.put("out_trade_no", verifyOutTradeNo);
|
|
490
|
+
resourceContent.put("already_fulfilled", false);
|
|
491
|
+
resourceContent.put("fulfillment_confirmed", true);
|
|
492
|
+
|
|
493
|
+
// 设置 Payment-Validation Header
|
|
494
|
+
HttpHeaders headers = new HttpHeaders();
|
|
495
|
+
headers.set("Payment-Validation", paymentValidationEncoded);
|
|
496
|
+
|
|
497
|
+
return ResponseEntity.ok()
|
|
498
|
+
.headers(headers)
|
|
499
|
+
.body(resourceContent);
|
|
500
|
+
|
|
501
|
+
} catch (AlipayApiException e) {
|
|
502
|
+
System.err.println("支付凭证验证异常: " + e.getErrMsg());
|
|
503
|
+
JSONObject errorResponse = new JSONObject();
|
|
504
|
+
errorResponse.put("code", "VERIFY_FAILED");
|
|
505
|
+
errorResponse.put("message", "支付凭证验证失败: " + e.getErrMsg());
|
|
506
|
+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
|
|
507
|
+
} catch (Exception e) {
|
|
508
|
+
System.err.println("履约处理异常: " + e.getMessage());
|
|
509
|
+
JSONObject errorResponse = new JSONObject();
|
|
510
|
+
errorResponse.put("code", "FULFILLMENT_ERROR");
|
|
511
|
+
errorResponse.put("message", "履约处理失败: " + e.getMessage());
|
|
512
|
+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* 生成服务资源内容
|
|
518
|
+
*
|
|
519
|
+
* 这里演示生成AI内容。实际项目中可替换为任意数字服务内容,例如:
|
|
520
|
+
* - 报告摘要
|
|
521
|
+
* - 数据下载链接
|
|
522
|
+
* - AI 生成结果
|
|
523
|
+
* - 第三方接口调用额度发放结果
|
|
524
|
+
*
|
|
525
|
+
* @param resourceId 资源ID
|
|
526
|
+
* @return 服务资源内容
|
|
527
|
+
*/
|
|
528
|
+
private String generateServiceResource(String resourceId) {
|
|
529
|
+
// 示例:生成AI内容
|
|
530
|
+
JSONObject result = new JSONObject();
|
|
531
|
+
result.put("status", "success");
|
|
532
|
+
result.put("service_type", "AI_CONTENT_GENERATION");
|
|
533
|
+
result.put("resource_id", resourceId);
|
|
534
|
+
result.put("content", "这是AI生成的内容示例,可根据实际业务替换为任意数字服务内容");
|
|
535
|
+
result.put("generated_at", ZonedDateTime.now(ZoneId.of("Asia/Shanghai")).toString());
|
|
536
|
+
return result.toJSONString();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ==================== 3. 发送履约确认 ====================
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* 发送履约确认
|
|
543
|
+
*
|
|
544
|
+
* 商家向用户交付资源后,调用此接口向支付宝发送履约回执
|
|
545
|
+
*
|
|
546
|
+
* @param tradeNo 支付宝订单号
|
|
547
|
+
* @return 履约确认是否成功
|
|
548
|
+
*/
|
|
549
|
+
private boolean sendFulfillmentConfirm(String tradeNo) {
|
|
550
|
+
try {
|
|
551
|
+
System.out.println("开始发送履约确认: tradeNo=" + tradeNo);
|
|
552
|
+
|
|
553
|
+
AlipayAipayAgentFulfillmentConfirmRequest request = new AlipayAipayAgentFulfillmentConfirmRequest();
|
|
554
|
+
AlipayAipayAgentFulfillmentConfirmModel model = new AlipayAipayAgentFulfillmentConfirmModel();
|
|
555
|
+
|
|
556
|
+
// 设置交易号
|
|
557
|
+
model.setTradeNo(tradeNo);
|
|
558
|
+
|
|
559
|
+
request.setBizModel(model);
|
|
560
|
+
|
|
561
|
+
AlipayAipayAgentFulfillmentConfirmResponse response = alipayClient.execute(request);
|
|
562
|
+
|
|
563
|
+
if (response.isSuccess()) {
|
|
564
|
+
System.out.println("履约确认成功: tradeNo=" + tradeNo);
|
|
565
|
+
return true;
|
|
566
|
+
} else {
|
|
567
|
+
System.err.println("履约确认失败: tradeNo=" + tradeNo + ", errorCode=" + response.getSubCode() + ", errorMsg=" + response.getSubMsg());
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
} catch (AlipayApiException e) {
|
|
572
|
+
System.err.println("履约确认异常: tradeNo=" + tradeNo + ", error=" + e.getErrMsg());
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ==================== 工具方法 ====================
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* 生成商家签名(seller_signature)
|
|
581
|
+
*
|
|
582
|
+
* 参考文档:第5章节"私钥加签"
|
|
583
|
+
*
|
|
584
|
+
* @param params 待签名参数
|
|
585
|
+
* @param privateKey 商户私钥
|
|
586
|
+
* @return Base64编码的签名
|
|
587
|
+
* @throws AlipayApiException 签名异常
|
|
588
|
+
*/
|
|
589
|
+
private String generateSellerSignature(Map<String, String> params, String privateKey) throws AlipayApiException {
|
|
590
|
+
// 1. 按key字典序排序
|
|
591
|
+
List<String> keys = new ArrayList<>(params.keySet());
|
|
592
|
+
Collections.sort(keys);
|
|
593
|
+
|
|
594
|
+
// 2. 拼接签名内容
|
|
595
|
+
StringBuilder signContent = new StringBuilder();
|
|
596
|
+
for (int i = 0; i < keys.size(); i++) {
|
|
597
|
+
String key = keys.get(i);
|
|
598
|
+
String value = params.get(key);
|
|
599
|
+
if (value != null && !value.trim().isEmpty()) {
|
|
600
|
+
signContent.append(key).append("=").append(value);
|
|
601
|
+
if (i < keys.size() - 1) {
|
|
602
|
+
signContent.append("&");
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// 3. RSA2签名
|
|
608
|
+
return AlipaySignature.rsaSign(signContent.toString(), privateKey, "UTF-8", "RSA2");
|
|
609
|
+
}
|
|
610
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# 统一收单交易关闭接口 - Node.js 示例
|
|
2
|
+
|
|
3
|
+
## 请求示例
|
|
4
|
+
|
|
5
|
+
```javascript
|
|
6
|
+
const { AlipaySdk } = require("alipay-sdk");
|
|
7
|
+
|
|
8
|
+
const alipaySdk = new AlipaySdk({
|
|
9
|
+
appId: "<-- 请填写您的AppId,例如:20190xxxxxx -->",
|
|
10
|
+
privateKey: "<-- 请填写您的应用私钥,例如:MIIE...(脱敏示例) -->", // Node.js 使用 PKCS#1 格式私钥
|
|
11
|
+
alipayPublicKey: "<-- 请填写您的支付宝公钥,例如:MIIB...(脱敏示例) -->",
|
|
12
|
+
gateway: "https://openapi.alipay.com/gateway.do",
|
|
13
|
+
signType: "RSA2",
|
|
14
|
+
// 第三方代调用模式下请设置 appAuthToken
|
|
15
|
+
// appAuthToken: "<-- 请填写应用授权令牌 -->",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// 交易关闭用于关闭未支付的订单
|
|
19
|
+
// 注意:交易关闭与交易撤销是不同接口 —— 关闭用于未支付订单,撤销用于已支付订单(仅限支付当天)
|
|
20
|
+
const result = await alipaySdk.exec("alipay.trade.close", {
|
|
21
|
+
bizContent: {
|
|
22
|
+
out_trade_no: "MERCHANT_ORDER_NO_PLACEHOLDER", // 商户订单号,与 trade_no 二选一
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (result.code === "10000") {
|
|
27
|
+
console.log("调用成功");
|
|
28
|
+
} else {
|
|
29
|
+
console.log("调用失败");
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 响应示例
|
|
34
|
+
### 正常示例
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"code": "10000",
|
|
38
|
+
"msg": "Success",
|
|
39
|
+
"trade_no": "ALIPAY_TRADE_NO_PLACEHOLDER",
|
|
40
|
+
"out_trade_no": "MERCHANT_ORDER_NO_PLACEHOLDER"
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
### 异常示例
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"code": "20000",
|
|
47
|
+
"msg": "Service Currently Unavailable",
|
|
48
|
+
"sub_code": "isp.unknow-error",
|
|
49
|
+
"sub_msg": "系统繁忙"
|
|
50
|
+
}
|
|
51
|
+
```
|