@alipay/alipay-aipay 1.2.2 → 1.5.0
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 +5 -0
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +13 -12
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/uninstall.js +17 -9
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +5 -5
- package/dist/commands/update.js.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/utils/config.d.ts +5 -3
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +34 -12
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/file.d.ts +8 -0
- package/dist/utils/file.d.ts.map +1 -1
- package/dist/utils/file.js +161 -9
- package/dist/utils/file.js.map +1 -1
- package/dist/utils/jq.d.ts.map +1 -1
- package/dist/utils/jq.js +8 -0
- package/dist/utils/jq.js.map +1 -1
- package/package.json +9 -5
- package/skill/SKILL.md +15 -169
- package/skill/VERSION +1 -1
- package/skill/references/integration/flow.md +388 -291
- package/skill/references/integration/modules/alipay-sdk-reminder.md +5 -3
- package/skill/references/integration/modules/checklist.md +54 -32
- package/skill/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/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 +8 -4
- 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 +6 -2
- package/skill/references/integration/modules/code-examples/csharp/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.cs +3 -1
- package/skill/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/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 +4 -1
- 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 +5 -2
- package/skill/references/integration/modules/code-examples/java/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemoController.java +3 -1
- package/skill/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/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 +16 -10
- 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 +10 -4
- package/skill/references/integration/modules/code-examples/nodejs/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.js +2 -1
- package/skill/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/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 +4 -1
- 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 +5 -2
- package/skill/references/integration/modules/code-examples/php/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.php +2 -1
- package/skill/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/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 +4 -1
- 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 +5 -1
- package/skill/references/integration/modules/code-examples/python/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.py +4 -2
- package/skill/references/integration/modules/interface-contract/aipay-interface-contract.md +52 -0
- package/skill/references/integration/modules/interface-contract/apppay-interface-contract.md +45 -0
- package/skill/references/integration/modules/interface-contract/common-trade-contract.md +42 -0
- package/skill/references/integration/modules/interface-contract/notify-url-contract.md +68 -0
- package/skill/references/integration/modules/interface-contract/webpay-interface-contract.md +44 -0
- package/skill/references/integration/modules/interface-guide.md +38 -71
- package/skill/references/integration/modules/product-decision.md +18 -35
- package/skill/references/integration/modules/sandbox/a2m-sandbox-test.md +62 -94
- package/skill/references/integration/modules/sandbox/alipay-sandbox-tool.md +43 -40
- package/skill/references/integration/modules/sandbox/sandbox-setup-guide.md +93 -114
- package/skill/references/integration/modules/scripts/local_402_sandbox_pay.py +358 -148
- package/skill/references/integration/modules/scripts/sandbox_config.sh +299 -0
- package/skill/references/integration/modules/troubleshooting-flow.md +28 -0
- package/skill/references/normal/alipay-cli-env.md +15 -5
- package/skill/references/normal/full-process-routing.md +82 -0
- package/skill/references/normal/json/customer-messages.json +942 -0
- package/skill/references/normal/json/policy-rules.json +179 -0
- package/skill/references/normal/rejection-guide.md +12 -7
- package/skill/references/normal/scripts/alipay_cli_env.mjs +33 -0
- package/skill/references/normal/scripts/alipay_cli_env.sh +55 -0
- package/skill/references/normal/scripts/common.sh +151 -0
- package/skill/references/normal/scripts/detect_dev_tool.sh +5 -1
- package/skill/references/normal/scripts/flow_loader.mjs +241 -0
- package/skill/references/normal/scripts/integration_context_runner.mjs +166 -0
- package/skill/references/normal/scripts/integration_message_runner.mjs +1023 -0
- package/skill/references/normal/scripts/interaction_reply.mjs +133 -0
- package/skill/references/normal/scripts/onboarding_discovery_runner.mjs +224 -0
- package/skill/references/normal/scripts/onboarding_message_runner.mjs +451 -0
- package/skill/references/normal/scripts/onboarding_recovery_runner.mjs +96 -0
- package/skill/references/normal/scripts/open_official_url.sh +82 -0
- package/skill/references/normal/scripts/platform_compat.mjs +39 -0
- package/skill/references/normal/scripts/project_route_inspector.sh +132 -0
- package/skill/references/normal/scripts/python_script_runner.mjs +56 -0
- package/skill/references/normal/scripts/render_customer_message.mjs +1288 -0
- package/skill/references/normal/scripts/self_update.mjs +160 -0
- package/skill/references/normal/scripts/telemetry.mjs +1327 -0
- package/skill/references/normal/self-update.md +18 -51
- package/skill/references/normal/telemetry.md +26 -0
- package/skill/references/onboarding/flow.md +421 -309
- package/skill/references/onboarding/modules/app-release.md +44 -438
- package/skill/references/onboarding/modules/authorization.md +25 -281
- package/skill/references/onboarding/modules/error-handling.md +31 -237
- package/skill/references/onboarding/modules/product-sign.md +25 -294
- package/skill/references/onboarding/modules/scripts/app.sh +445 -169
- package/skill/references/onboarding/modules/scripts/ar_sign_apply.sh +47 -28
- package/skill/references/onboarding/modules/scripts/auth.sh +484 -167
- package/skill/references/onboarding/modules/scripts/download_key_tool.sh +157 -0
- package/skill/references/onboarding/modules/scripts/error_handler.sh +101 -130
- package/skill/references/onboarding/modules/scripts/network_retry.sh +96 -0
- package/skill/references/onboarding/modules/scripts/query_sign_status.sh +20 -71
- package/skill/references/onboarding/modules/scripts/service.sh +154 -89
- package/skill/references/onboarding/modules/scripts/sign_status_parser.sh +58 -0
- package/skill/references/onboarding/modules/scripts/upload_screenshots.sh +34 -20
- package/skill/references/onboarding/modules/service-registration.md +20 -317
- package/skill/references/runtime-entry.md +63 -0
|
@@ -46,6 +46,7 @@ SDK 的引入方式取决于对应语言的包管理方式和实际导出方式
|
|
|
46
46
|
**注意:**
|
|
47
47
|
- package.json 的 `exports` 字段是入口路径配置,不是导出方式
|
|
48
48
|
- 包的导出方式可能随版本更新而改变,务必验证
|
|
49
|
+
- `code-examples/nodejs/` 中的 `require` / `import` 行只用于展示 SDK 调用位置,不作为当前项目可复制依据;生成或修改真实 Node.js 项目前,必须以目标项目实际安装的 `alipay-sdk` 包和项目模块制式为准重新确定导入方式
|
|
49
50
|
|
|
50
51
|
---
|
|
51
52
|
|
|
@@ -61,7 +62,7 @@ SDK 的引入方式取决于对应语言的包管理方式和实际导出方式
|
|
|
61
62
|
| Node.js | `alipaySdk.pageExec('alipay.trade.page.pay', 'POST', {...})` | `alipaySdk.exec('alipay.trade.query', {...})` | 同上 |
|
|
62
63
|
| Python | `alipay_client.page_execute(request)` | `alipay_client.execute(request)` | 同上 |
|
|
63
64
|
|
|
64
|
-
**此外,APP支付(`alipay.trade.app.pay`)属于客户端 SDK 调用类 API,需使用 `sdkExecute`/`sdkExec` 方法:**
|
|
65
|
+
**此外,APP 支付(`alipay.trade.app.pay`)属于客户端 SDK 调用类 API,需使用 `sdkExecute`/`sdkExec` 方法:**
|
|
65
66
|
|
|
66
67
|
| 语言 | 客户端 SDK 方法 |
|
|
67
68
|
|------|---------------|
|
|
@@ -204,6 +205,7 @@ if (data.paymentHtml) {
|
|
|
204
205
|
|
|
205
206
|
5. **检查网关地址**
|
|
206
207
|
- 沙箱环境必须使用沙箱网关 `https://openapi-sandbox.dl.alipaydev.com/gateway.do`,生产环境使用 `https://openapi.alipay.com/gateway.do`。
|
|
208
|
+
- 五语言 A2M 示例默认读取 `ALIPAY_GATEWAY`,未设置时使用沙箱网关;只有生产部署才把该环境变量显式设置为生产网关。禁止把生产网关硬编码成沙箱联调默认值。
|
|
207
209
|
|
|
208
210
|
> **核心教训**:验签失败 ≠ 私钥问题。应先通过日志/输出验证实际请求行为(参数是否完整传入),而非基于技术传言做假设。
|
|
209
211
|
|
|
@@ -219,9 +221,9 @@ if (data.paymentHtml) {
|
|
|
219
221
|
4. 检查 SDK 实际传入的 `return_url` 与已验证地址完全一致,防止域名、端口、协议或路径在配置映射中被替换。
|
|
220
222
|
5. 回跳处理必须先验证支付宝同步参数签名,或使用服务端保存且与当前用户会话绑定的订单标识定位待查订单;不得信任可任意修改的订单号。
|
|
221
223
|
6. 同步回跳参数不是支付成功的可信依据。页面应先展示“正在确认支付结果”,再通过服务端交易查询或已验签的异步通知状态展示最终结果。
|
|
222
|
-
7. 当前 Agent 有浏览器或 UI
|
|
224
|
+
7. 当前 Agent 有浏览器或 UI 验证能力时,必须实际打开回跳页,确认非空白、无可见报错且关键状态内容已渲染。当前无该能力时,不追加用户确认,将回跳页 UI 证据标记为人工待验证;不得宣称页面渲染已验证或网站支付代码开发完成。
|
|
223
225
|
|
|
224
|
-
回跳地址无法访问、路由不存在、页面无法渲染,或 SDK
|
|
226
|
+
回跳地址无法访问、路由不存在、页面无法渲染,或 SDK 实际值与已验证地址不一致时,必须先修正,不得宣称网站支付代码开发完成。
|
|
225
227
|
|
|
226
228
|
---
|
|
227
229
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 支付宝支付产品代码开发校验清单
|
|
2
2
|
|
|
3
|
-
本文档整理了支付宝支付集成过程中的通用校验项,仅包含必须校验和高危项目。代码实现后、集成收口前必须逐项核对;逐项核对不等于为每项安装工具或启动运行环境,自动执行边界以 `../flow.md` 步骤
|
|
3
|
+
本文档整理了支付宝支付集成过程中的通用校验项,仅包含必须校验和高危项目。代码实现后、集成收口前必须逐项核对;逐项核对不等于为每项安装工具或启动运行环境,自动执行边界以 `../flow.md` 步骤 8 为准,无法自动取得证据时标记为“人工待验证”。默认按第八节摘要模板输出,用户要求明细时再展开全部逐项结果。校验结果供参考,开发者务必按照支付宝最新开放平台文档进行检查。
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
<!-- ALIPAY_AIPAY_CHECKLIST_PRODUCTS:aipay,webpay,apppay -->
|
|
7
8
|
## 一、密钥与安全校验
|
|
8
9
|
|
|
9
10
|
| 校验项 | 校验要求 | 说明 |
|
|
@@ -11,11 +12,13 @@
|
|
|
11
12
|
| 私钥存储位置 | 私钥不得保存在客户端代码中 | 构造交易数据并签名必须在商家服务端完成,私钥绝对不能保存在商家APP客户端中,也不能从服务端下发。商户需妥善保存私钥,不建议在代码或配置中明文写入私钥,应当通过加密配置文件或密钥管理平台进行管理 |
|
|
12
13
|
| 私钥日志安全 | 私钥不得出现在日志中 | 私钥的保密等级比源码高,出现在日志中将增加私钥泄露的风险 |
|
|
13
14
|
| 私钥仓库安全 | 私钥不得上传公共仓库 | 私钥泄露将导致应用和支付宝交互的安全性完全丧失 |
|
|
14
|
-
| 沙箱配置后置校验 | 沙箱 appId
|
|
15
|
+
| 沙箱配置后置校验 | 沙箱 appId、应用私钥、支付宝公钥接入项目后必须完成配置准确性校验 | `sandboxConfigState=READY` 时,Unix/macOS/Linux 必须确认商家服务端加载器直接读取已验证的 `.alipay-sandbox.json`,从同一 `appIds[0]` 选择当前语言字段且不在源码、普通日志或额外 `.env` 留下密钥副本;Windows 必须确认项目实际读取值属于本次手工申领和密钥配置的同一沙箱应用。待配置时本项直接不通过,代码只能保留会在配置缺失时明确失败的加载边界,禁止占位符、示例值或其他来源密钥 |
|
|
15
16
|
| 用户项目文件保护 | 既有项目结构、文件和无关支付能力均保留;新项目只创建在尚不存在或为空的目录 | 检查本轮变更不存在未经独立二次确认的既有文件删除、既有文件整体覆盖,或与已确认目标产品无关的修改;新建文件不属于既有文件整体覆盖。发现违规立即判定不通过并停止后续流程 |
|
|
17
|
+
| 沙箱配置防提交 | Unix/macOS/Linux 快速沙箱:配置未被 Git 跟踪,项目根目录 `.gitignore` 包含精确规则 `/.alipay-sandbox.json`,配置权限为 `0600`;Windows 手工沙箱:实际敏感配置未被 Git 跟踪并具有对应忽略保护,Windows 文件访问控制能核验时仅当前用户可访问,不能核验时标记人工待验证 | 禁止私钥、沙箱账号或密码随项目进入 Git 暂存区或公共仓库;缺少适用保护时判定不通过 |
|
|
16
18
|
|
|
17
19
|
---
|
|
18
20
|
|
|
21
|
+
<!-- ALIPAY_AIPAY_CHECKLIST_PRODUCTS:webpay,apppay -->
|
|
19
22
|
## 二、异步通知校验
|
|
20
23
|
|
|
21
24
|
先区分两种验收层级,再判断是否通过:
|
|
@@ -34,36 +37,39 @@
|
|
|
34
37
|
| 重定向 | notify_url 不能重定向 | 重定向会导致支付宝收不到 success |
|
|
35
38
|
| 外网可访问 | 真实生产上线时,notify_url 必须外网可访问;本地生产参数验收模式下标记为人工待验证 | 需确保支付宝服务器可访问;本地 `localhost`、局域网地址或 TLS 未就绪域名不阻塞本地正式验收 |
|
|
36
39
|
|
|
40
|
+
<!-- ALIPAY_AIPAY_CHECKLIST_PRODUCTS:aipay,webpay,apppay -->
|
|
37
41
|
## 三、适用接口覆盖校验
|
|
38
42
|
|
|
39
43
|
| 校验项 | 校验要求 | 说明 |
|
|
40
44
|
|----------|----------|----------|
|
|
41
|
-
|
|
|
42
|
-
| 按量付费 | 已实现 402 协议、`alipay.aipay.agent.payment.verify` 和 `alipay.aipay.agent.fulfillment.confirm` |
|
|
45
|
+
| AI 网页应用收款/AI 移动应用收款 | 已实现产品下单、交易查询、退款、退款查询、关闭交易和异步通知处理代码 | 以产品和接口文档为准;缺少本地示例不得作为省略理由。公网通知联调是否完成单独在上一节判断 |
|
|
46
|
+
| AI 按量付费 | 已实现 402 协议、`alipay.aipay.agent.payment.verify` 和 `alipay.aipay.agent.fulfillment.confirm` | 不使用 AI 网页应用收款/AI 移动应用收款的通用收单接口 |
|
|
43
47
|
| 限定范围 | 用户明确排除的适用接口已列为待办 | 存在未实现的适用接口时不得判定完整集成 |
|
|
44
48
|
|
|
45
49
|
---
|
|
46
50
|
|
|
51
|
+
<!-- ALIPAY_AIPAY_CHECKLIST_PRODUCTS:webpay,apppay -->
|
|
47
52
|
## 四、支付结果处理校验
|
|
48
53
|
|
|
49
54
|
| 校验项 | 校验要求 | 说明 |
|
|
50
55
|
|----------|----------|----------|
|
|
51
56
|
| 前台同步结果防范 | 前台同步跳转结果仅作通知,**不能作为支付成功的依据** | 必须以异步通知或查询接口结果为准 |
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
-
|
|
|
57
|
+
| AI 网页应用收款回跳默认行为 | 用户未明确关闭同步回跳时,已配置项目实际 `return_url` | 地址不得包含示例域名、占位符、猜测端口或未实现路由 |
|
|
58
|
+
| AI 网页应用收款回跳页可用性 | 启动服务后 GET 访问精确 `return_url`,并在 Agent 具备浏览器/UI 能力时验证页面正常渲染;当前环境无法取得 UI 证据时标记人工待验证 | 不得出现 404、500、空白页、可见报错、认证循环或重定向循环;单页应用直接刷新路由也必须可用 |
|
|
59
|
+
| AI 网页应用收款回跳状态展示 | 先验证同步参数签名,或使用与当前用户会话绑定的服务端订单标识;再通过服务端查询或已验签通知状态展示最终支付结果 | 同步回跳参数不得直接改写订单或作为支付成功依据,不得使用可任意修改的订单号访问其他订单 |
|
|
60
|
+
| AI 网页应用收款关闭同步回跳 | 仅当用户已明确表示不使用同步回跳时适用;SDK 请求未传 `return_url` | 用户未明确关闭时,缺少 `return_url` 必须修复为默认同步回跳分支;关闭分支必须保留异步通知、交易查询和商户订单查询页,且沙箱提示不得声称付款后会自动返回商户网站 |
|
|
56
61
|
| 支付成功判断 | 仅 `trade_status` 为 `TRADE_SUCCESS` 或 `TRADE_FINISHED` 时才认定付款成功 | 其他状态不算支付成功 |
|
|
57
62
|
| 主动查询兜底 | 主动查询订单状态必须作为支付结果确认的兜底逻辑 | 无论异步通知是否到达,都应具备主动查询订单状态的能力,防止因网络抖动、通知延迟或丢失导致的状态不一致 |
|
|
58
63
|
| 未知异常处理 | 网络超时或未知异常时,必须调用查询接口确认 | 不能简单推断为成功或失败 |
|
|
59
64
|
| 商家资损防范 | 不依赖前台同步结果,以异步通知或查询为准 | 防止用户未付款但商家判定成功 |
|
|
60
65
|
| 用户资损防范 | 未确认支付结果前不要求用户再次付款 | 防止用户已付款但商家判定失败导致重复扣款 |
|
|
61
66
|
|
|
62
|
-
> 按量付费不使用前台同步跳转结果和异步通知作为支付成功依据,需按下一节专项校验。
|
|
67
|
+
> AI 按量付费不使用前台同步跳转结果和异步通知作为支付成功依据,需按下一节专项校验。
|
|
63
68
|
|
|
64
69
|
---
|
|
65
70
|
|
|
66
|
-
|
|
71
|
+
<!-- ALIPAY_AIPAY_CHECKLIST_PRODUCTS:aipay -->
|
|
72
|
+
## 五、AI 按量付费专项校验
|
|
67
73
|
|
|
68
74
|
| 校验项 | 校验要求 | 说明 |
|
|
69
75
|
|----------|----------|----------|
|
|
@@ -74,12 +80,15 @@
|
|
|
74
80
|
| 金额一致性 | 必须校验本地订单金额与 `Payment-Needed` 账单金额和服务定价一致;若验付接口返回金额字段,再校验实际支付金额 | 防止金额篡改或错误定价 |
|
|
75
81
|
| 幂等履约 | 同一订单重复携带 `Payment-Proof` 时不得重复发放资源 | 应返回历史履约结果或可重试确认结果 |
|
|
76
82
|
| 履约确认 | 资源生成后必须调用 `alipay.aipay.agent.fulfillment.confirm` | 确认成功后再标记订单 `FULFILLED` 并返回成功交付 |
|
|
83
|
+
| 自动联调成功证据 | 必须同时具备 HTTP 200、非空 `resource_id` / `resourceId`、非空 `content`、无明确业务失败,以及当前五语言示例有效的 `Payment-Validation` | 204、空体、空对象、`VERIFY_FAILED`、履约失败或异常响应头均不得判为通过 |
|
|
84
|
+
| 敏感过程产物 | 单次联调的临时目录/文件权限不得宽于 `0700/0600`,命令结束时立即清理 | 禁止打印 Payment-Proof 或 Payment-Validation 原始值;失败后重新执行完整联调 |
|
|
77
85
|
| 沙箱 serviceId | 沙箱联调的 `serviceId` 固定为 `api_mock_service_id` | 不向用户索要正式 `serviceId`,不使用其他占位符 |
|
|
78
86
|
| 沙箱空字段边界 | 沙箱联调中字段为空可用于排查定位,生产环境关键字段为空必须按异常处理 | 不得把沙箱容错逻辑直接迁移成生产跳过校验 |
|
|
79
87
|
| 生产实现完整性 | 订单持久化、本地订单匹配、金额一致性、资源防串、幂等履约、待确认状态和确认失败重试均已实际实现 | 任一关键控制仍为 TODO、注释、内存演示或伪代码时只能判定沙箱演示可测试,不得判定生产就绪 |
|
|
80
88
|
|
|
81
89
|
---
|
|
82
90
|
|
|
91
|
+
<!-- ALIPAY_AIPAY_CHECKLIST_PRODUCTS:webpay,apppay -->
|
|
83
92
|
## 六、退款校验
|
|
84
93
|
|
|
85
94
|
| 校验项 | 校验要求 | 说明 |
|
|
@@ -90,16 +99,17 @@
|
|
|
90
99
|
|
|
91
100
|
---
|
|
92
101
|
|
|
102
|
+
<!-- ALIPAY_AIPAY_CHECKLIST_PRODUCTS:aipay,webpay,apppay -->
|
|
93
103
|
## 七、上线前校验
|
|
94
104
|
|
|
95
105
|
| 校验项 | 校验要求 | 说明 |
|
|
96
106
|
|----------|----------|----------|
|
|
97
|
-
| 网关地址切换 |
|
|
107
|
+
| 网关地址切换 | 五语言 A2M 示例默认沙箱网关为 `https://openapi-sandbox.dl.alipaydev.com/gateway.do`;上线时显式切换为 `https://openapi.alipay.com/gateway.do` | 禁止把生产网关复制进沙箱运行配置 |
|
|
98
108
|
| APPID 切换 | 使用生产环境的 APPID | 与沙箱 APPID 不同 |
|
|
99
109
|
| 密钥切换 | 使用生产环境的应用私钥和支付宝公钥 | 与沙箱密钥不同 |
|
|
100
110
|
| 生产密钥对应关系 | `appId`、应用公钥、应用私钥属于同一套生产应用密钥 | 不得混用沙箱应用、其他生产应用或另一套重新生成的密钥 |
|
|
101
111
|
| 生产私钥格式 | 应用私钥格式与项目语言 SDK 匹配 | Java 使用 PKCS#8;非 Java 使用 PKCS#1。格式不匹配时使用支付宝开放平台密钥工具转换 |
|
|
102
|
-
| 按量付费 serviceId 替换 | 生产配置中不得保留 `api_mock_service_id` | 上线前替换为服务市场实际返回的真实 `serviceId` |
|
|
112
|
+
| AI 按量付费 serviceId 替换 | 生产配置中不得保留 `api_mock_service_id` | 上线前替换为服务市场实际返回的真实 `serviceId` |
|
|
103
113
|
| 证书切换 | 公钥证书模式需切换为生产环境证书 | 应用公钥证书、支付宝公钥证书、支付宝根证书 |
|
|
104
114
|
| 产品开通状态 | 确认产品已在生产环境开通 | 沙箱无需开通,生产环境需要申请开通 |
|
|
105
115
|
| 私钥安全 | 私钥不在代码、日志、公共仓库中明文存储 | 使用安全的方式管理私钥 |
|
|
@@ -108,7 +118,8 @@
|
|
|
108
118
|
|
|
109
119
|
---
|
|
110
120
|
|
|
111
|
-
|
|
121
|
+
<!-- ALIPAY_AIPAY_CHECKLIST_PRODUCTS:aipay,webpay,apppay -->
|
|
122
|
+
## 八、代码开发结果输出
|
|
112
123
|
|
|
113
124
|
> 内部逐项判断使用以下状态:
|
|
114
125
|
> - ✅ 通过:已实现且符合规范
|
|
@@ -117,35 +128,46 @@
|
|
|
117
128
|
> - ➖ 不适用:当前集成场景不涉及此项
|
|
118
129
|
> - 🟡 人工待验证:当前环境无法自动取得充分证据
|
|
119
130
|
|
|
120
|
-
|
|
131
|
+
先按当前实际证据完成内部逐项判断。本轮范围内仍可直接修复代码或配置时,先修复并重检,不渲染中间结果;只有已通过、只剩人工待验证,或本轮无法继续修复时,才使用 `../../normal/json/customer-messages.json` 的 `integration.checklist.result` 按产品选择 `AIPAY`、`WEBPAY` 或 `APPPAY`,并传入最终一轮实际整体结论、不通过/部分通过项和人工待验证项。renderer 会按结果自动选择精简的通过或未完成模板。必须从本文档所在目录执行:
|
|
121
132
|
|
|
122
|
-
|
|
133
|
+
Unix/macOS/Linux 的 `sandboxConfigState` 必须复用 integration flow 的固定脚本终态。Windows 必须复用步骤 1/3 记录的 `READY|VERIFY_PENDING`。待配置时不在本步骤重跑创建或复核,不读取当前配置,把配置后置校验、摘要和适用沙箱测试列入未完成;若代码实现与其他安全检查均通过,整体结果为“部分通过”,否则按其他缺口判为“未通过”。
|
|
123
134
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
-
|
|
135
|
+
```bash
|
|
136
|
+
printf '%s' "$CHECKLIST_RESULT_JSON" \
|
|
137
|
+
| node ../../normal/scripts/integration_message_runner.mjs checklist-result --variant <AIPAY|WEBPAY|APPPAY> --project-path "$PROJECT_PATH" --scope-receipt "$CHECKLIST_SCOPE_RECEIPT"
|
|
138
|
+
```
|
|
127
139
|
|
|
128
|
-
|
|
140
|
+
上方 stdin 命令保持用于 Unix/macOS/Linux。Windows 不使用管道或续行符,按当前产品执行对应 PowerShell 单行命令:
|
|
129
141
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
142
|
+
```powershell
|
|
143
|
+
node ../../normal/scripts/integration_message_runner.mjs checklist-result --variant AIPAY --project-path "$PROJECT_PATH" --scope-receipt "$CHECKLIST_SCOPE_RECEIPT" --overall-result "$OVERALL_RESULT" --failed-items "$FAILED_ITEMS" --manual-items "$MANUAL_ITEMS" --sandbox-result "$SANDBOX_RESULT" --sandbox-config-state "$SANDBOX_CONFIG_STATE" --blocking-defect-state "$BLOCKING_DEFECT_STATE" --execution-mode "$EXECUTION_MODE" --next-flow-reminder "$NEXT_FLOW_REMINDER"
|
|
144
|
+
node ../../normal/scripts/integration_message_runner.mjs checklist-result --variant WEBPAY --project-path "$PROJECT_PATH" --scope-receipt "$CHECKLIST_SCOPE_RECEIPT" --overall-result "$OVERALL_RESULT" --failed-items "$FAILED_ITEMS" --manual-items "$MANUAL_ITEMS" --webpay-experience-result "$WEBPAY_EXPERIENCE_RESULT" --sandbox-config-state "$SANDBOX_CONFIG_STATE" --blocking-defect-state "$BLOCKING_DEFECT_STATE" --execution-mode "$EXECUTION_MODE" --next-flow-reminder "$NEXT_FLOW_REMINDER"
|
|
145
|
+
node ../../normal/scripts/integration_message_runner.mjs checklist-result --variant APPPAY --project-path "$PROJECT_PATH" --scope-receipt "$CHECKLIST_SCOPE_RECEIPT" --overall-result "$OVERALL_RESULT" --failed-items "$FAILED_ITEMS" --manual-items "$MANUAL_ITEMS" --sandbox-config-state "$SANDBOX_CONFIG_STATE" --blocking-defect-state "$BLOCKING_DEFECT_STATE" --execution-mode "$EXECUTION_MODE" --next-flow-reminder "$NEXT_FLOW_REMINDER"
|
|
146
|
+
```
|
|
133
147
|
|
|
134
|
-
|
|
148
|
+
只有 `AIPAY` 传入独立 `sandboxResult`;`WEBPAY` 必须按步骤 6 的实际证据传入消息目录登记的 `webpayExperienceResult` 枚举。禁止跨产品传入其他产品结论。这些字段只用于校验代码开发总结果与实际沙箱事实一致,不在最终精简消息中重复展示。内部必须保留所有适用项的检查结论和依据;用户要求完整明细时,再从实际证据输出逐项表格,不维护第二份固定模板。
|
|
135
149
|
|
|
136
|
-
|
|
150
|
+
三个产品的内部结论校验差异如下,具体对客文本只在消息目录维护:
|
|
137
151
|
|
|
138
|
-
|
|
152
|
+
| 产品 | checklist 内部校验事实 | 实际待办来源 |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| `AIPAY` | 代码开发通过必须绑定本轮 AI 按量付费沙箱服务端联调通过 | checklist 失败/部分通过项、人工待验证项和联调恢复动作 |
|
|
155
|
+
| `WEBPAY` | 未提供浏览器付款入口或完整指引时不得标记代码开发通过 | checklist 失败/部分通过项、人工待验证项和待用户体验事项 |
|
|
156
|
+
| `APPPAY` | 当前没有沙箱付款验证结论,不据此生成通过或失败事实 | checklist 失败/部分通过项和人工待验证项 |
|
|
139
157
|
|
|
140
|
-
|
|
141
|
-
|------|------|------|------|
|
|
142
|
-
| 仅列出不通过、部分通过和人工待验证项 | | | |
|
|
158
|
+
`APPPAY` 变体在 `customer-messages.json` 保留不渲染的维护 TODO。后续确定 AI 移动应用收款沙箱测试动作时,只修改该变体仍不够,必须同步 integration flow、本文适用校验项和自动化测试;在此之前 Agent 禁止临场补充沙箱付款步骤。
|
|
143
159
|
|
|
144
|
-
|
|
160
|
+
代码开发校验结论必须与沙箱测试结论分开:沙箱配置字段完整、配置后置校验通过、AI 网页应用收款付款指引已提供或 AI 移动应用收款不适用步骤 6,都不得表述为“沙箱测试通过”或“沙箱环境测试通过”。只有 AI 按量付费步骤 6 已自动完成 402 Payment-Needed、沙箱收银、Payment-Proof 重试和资源交付全链路并取得成功证据时,才可在本摘要中复用“AI 按量付费沙箱测试通过”的结论。
|
|
161
|
+
|
|
162
|
+
涉及 AI 网页应用收款或 AI 移动应用收款时,校验结论还必须显式区分:
|
|
163
|
+
- “本地正式验收完成 / 本地生产参数验收完成,公网异步通知待补齐”
|
|
164
|
+
- “真实生产上线条件已满足”
|
|
165
|
+
|
|
166
|
+
未完成公网 HTTPS `notify_url` 联调时,禁止把第一种表述写成第二种。
|
|
145
167
|
|
|
146
|
-
|
|
168
|
+
`failedItems` 与 `manualItems` 必须在各自受控单行值中包含对应的确定下一步;没有对应项目时明确传入“无”。`overallResult=通过` 时两者必须都是“无”、`sandboxConfigState=READY` 且 `blockingDefectState=NONE`;“部分通过”必须至少存在一类缺口;“未通过”必须列出未通过项。`blockingDefectState` 只表示除沙箱配置及其直接依赖动作之外,是否存在代码、安全或必要依据缺口;当前环境无法自动取得 UI、浏览器或公网联调证据但代码与安全依据已完成时,只登记 `manualItems`,不把该人工待验证项自动计为 `PRESENT`。待配置时恢复口令由 renderer 固定生成。`full_process` 在待配置且无其他阻塞缺口,或 `READY` 且仅剩非阻塞人工待验证项时,输出“部分通过”并继续产品开通;其他未通过组合保持停留提醒。
|
|
147
169
|
|
|
148
|
-
|
|
170
|
+
同一次代码开发只渲染一次最终结果;renderer stdout 是默认收口的唯一对客正文,禁止在前后追加通过摘要、风险说明、产品开通提醒或第二份待办。
|
|
149
171
|
|
|
150
172
|
---
|
|
151
173
|
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# 异步通知处理 - C# 示例
|
|
2
|
+
|
|
3
|
+
适用产品:AI 网页应用收款、AI 移动应用收款。AI 按量付费不使用异步通知。
|
|
4
|
+
|
|
5
|
+
```csharp
|
|
6
|
+
using System;
|
|
7
|
+
using System.Collections.Generic;
|
|
8
|
+
using System.Globalization;
|
|
9
|
+
using System.Linq;
|
|
10
|
+
using System.Text.RegularExpressions;
|
|
11
|
+
using System.Threading.Tasks;
|
|
12
|
+
using Microsoft.AspNetCore.Mvc;
|
|
13
|
+
|
|
14
|
+
[ApiController]
|
|
15
|
+
public class AlipayNotifyController : ControllerBase
|
|
16
|
+
{
|
|
17
|
+
private readonly IAlipayNotifyVerifier _alipayNotifyVerifier;
|
|
18
|
+
private readonly IPaymentConfigRepository _paymentConfigRepository;
|
|
19
|
+
private readonly IOrderRepository _orderRepository;
|
|
20
|
+
private readonly INotifyEventRepository _notifyEventRepository;
|
|
21
|
+
private readonly INotifyLogger _notifyLogger;
|
|
22
|
+
|
|
23
|
+
public AlipayNotifyController(
|
|
24
|
+
IAlipayNotifyVerifier alipayNotifyVerifier,
|
|
25
|
+
IPaymentConfigRepository paymentConfigRepository,
|
|
26
|
+
IOrderRepository orderRepository,
|
|
27
|
+
INotifyEventRepository notifyEventRepository,
|
|
28
|
+
INotifyLogger notifyLogger)
|
|
29
|
+
{
|
|
30
|
+
_alipayNotifyVerifier = alipayNotifyVerifier;
|
|
31
|
+
_paymentConfigRepository = paymentConfigRepository;
|
|
32
|
+
_orderRepository = orderRepository;
|
|
33
|
+
_notifyEventRepository = notifyEventRepository;
|
|
34
|
+
_notifyLogger = notifyLogger;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
[HttpPost("/alipay/notify")]
|
|
38
|
+
public async Task<IActionResult> Notify()
|
|
39
|
+
{
|
|
40
|
+
var paramsMap = new Dictionary<string, string>();
|
|
41
|
+
try
|
|
42
|
+
{
|
|
43
|
+
paramsMap = Request.Form.ToDictionary(kv => kv.Key, kv => kv.Value.ToString());
|
|
44
|
+
var config = await _paymentConfigRepository.FindByAppId(paramsMap.GetValueOrDefault("app_id"));
|
|
45
|
+
if (config == null)
|
|
46
|
+
{
|
|
47
|
+
return Fail("alipay notify app config not found", paramsMap);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!VerifyNotifyWithAlipaySdk(paramsMap, config))
|
|
51
|
+
{
|
|
52
|
+
return Fail("alipay notify signature check failed", paramsMap);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
var order = await _orderRepository.FindByOutTradeNo(paramsMap.GetValueOrDefault("out_trade_no"));
|
|
56
|
+
if (order == null || !BusinessFieldsMatch(order, paramsMap, config))
|
|
57
|
+
{
|
|
58
|
+
return Fail("alipay notify business check failed", paramsMap);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
var inserted = await _notifyEventRepository.InsertOnce(
|
|
62
|
+
paramsMap.GetValueOrDefault("notify_id"),
|
|
63
|
+
paramsMap.GetValueOrDefault("trade_no"),
|
|
64
|
+
paramsMap.GetValueOrDefault("out_trade_no"),
|
|
65
|
+
paramsMap.GetValueOrDefault("trade_status"),
|
|
66
|
+
paramsMap);
|
|
67
|
+
if (!inserted)
|
|
68
|
+
{
|
|
69
|
+
return Content("success", "text/plain");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (IsPaidTradeNotification(paramsMap))
|
|
73
|
+
{
|
|
74
|
+
await _orderRepository.MarkPaidIdempotently(order.OutTradeNo, paramsMap.GetValueOrDefault("trade_no"), paramsMap);
|
|
75
|
+
}
|
|
76
|
+
else
|
|
77
|
+
{
|
|
78
|
+
await _orderRepository.RecordNonPaidTradeEvent(order.OutTradeNo, paramsMap.GetValueOrDefault("trade_status"), paramsMap);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return Content("success", "text/plain");
|
|
82
|
+
}
|
|
83
|
+
catch (Exception)
|
|
84
|
+
{
|
|
85
|
+
SafeLogNotifyError("alipay notify unexpected exception", paramsMap);
|
|
86
|
+
return Content("fail", "text/plain");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private bool BusinessFieldsMatch(MerchantOrder order, Dictionary<string, string> paramsMap, AlipayNotifyConfig config)
|
|
91
|
+
{
|
|
92
|
+
return paramsMap.GetValueOrDefault("app_id") == config.AppId
|
|
93
|
+
&& paramsMap.GetValueOrDefault("out_trade_no") == order.OutTradeNo
|
|
94
|
+
&& AmountEquals(order.TotalAmount, paramsMap.GetValueOrDefault("total_amount"))
|
|
95
|
+
&& ExpectedSellerMatches(config, paramsMap.GetValueOrDefault("seller_id"), paramsMap.GetValueOrDefault("seller_email"));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private static bool AmountEquals(decimal expected, string? actual)
|
|
99
|
+
{
|
|
100
|
+
var expectedAmount = NormalizeAmountText(expected);
|
|
101
|
+
var actualAmount = NormalizeAmountText(actual);
|
|
102
|
+
return expectedAmount != null && expectedAmount == actualAmount;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private static string? NormalizeAmountText(decimal value)
|
|
106
|
+
{
|
|
107
|
+
if (decimal.Round(value, 2) != value) return null;
|
|
108
|
+
return value.ToString("0.00", CultureInfo.InvariantCulture);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private static string? NormalizeAmountText(string? value)
|
|
112
|
+
{
|
|
113
|
+
if (string.IsNullOrWhiteSpace(value)) return null;
|
|
114
|
+
var match = Regex.Match(value.Trim(), @"^(\d+)(?:\.(\d{1,2}))?$");
|
|
115
|
+
if (!match.Success) return null;
|
|
116
|
+
var yuan = match.Groups[1].Value.TrimStart('0');
|
|
117
|
+
if (yuan.Length == 0) yuan = "0";
|
|
118
|
+
return $"{yuan}.{match.Groups[2].Value.PadRight(2, '0')}";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private static bool IsPaidTradeNotification(Dictionary<string, string> paramsMap)
|
|
122
|
+
{
|
|
123
|
+
var paidStatus = paramsMap.GetValueOrDefault("trade_status") == "TRADE_SUCCESS"
|
|
124
|
+
|| paramsMap.GetValueOrDefault("trade_status") == "TRADE_FINISHED";
|
|
125
|
+
return paidStatus
|
|
126
|
+
&& string.IsNullOrEmpty(paramsMap.GetValueOrDefault("out_biz_no"))
|
|
127
|
+
&& string.IsNullOrEmpty(paramsMap.GetValueOrDefault("gmt_refund"))
|
|
128
|
+
&& string.IsNullOrEmpty(paramsMap.GetValueOrDefault("refund_fee"));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private static bool ExpectedSellerMatches(AlipayNotifyConfig config, string? sellerId, string? sellerEmail)
|
|
132
|
+
{
|
|
133
|
+
var expectedSellerId = config.SellerId;
|
|
134
|
+
var expectedSellerEmail = config.SellerEmail;
|
|
135
|
+
return (!string.IsNullOrEmpty(expectedSellerId) && sellerId == expectedSellerId)
|
|
136
|
+
|| (!string.IsNullOrEmpty(expectedSellerEmail) && sellerEmail == expectedSellerEmail);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private bool VerifyNotifyWithAlipaySdk(Dictionary<string, string> paramsMap, AlipayNotifyConfig config)
|
|
140
|
+
{
|
|
141
|
+
// 使用当前项目已安装的支付宝 SDK 验签能力校验 paramsMap["sign"]。
|
|
142
|
+
// _alipayNotifyVerifier.Verify(paramsMap, config) 必须封装目标项目当前 SDK 实际通知验签 API;落地前先查已安装包文档或类型定义,不能手写临时 RSA 验签。
|
|
143
|
+
return _alipayNotifyVerifier.Verify(paramsMap, config);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private IActionResult Fail(string message, Dictionary<string, string> paramsMap)
|
|
147
|
+
{
|
|
148
|
+
SafeLogNotifyError(message, paramsMap);
|
|
149
|
+
return Content("fail", "text/plain");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private void SafeLogNotifyError(string message, Dictionary<string, string> paramsMap)
|
|
153
|
+
{
|
|
154
|
+
try
|
|
155
|
+
{
|
|
156
|
+
_notifyLogger.Warn(message, SanitizeNotifyParams(paramsMap));
|
|
157
|
+
}
|
|
158
|
+
catch (Exception)
|
|
159
|
+
{
|
|
160
|
+
// 日志失败不能影响支付宝收到纯文本 fail。
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private static Dictionary<string, string> SanitizeNotifyParams(Dictionary<string, string> paramsMap)
|
|
165
|
+
{
|
|
166
|
+
var safeParams = new Dictionary<string, string>(paramsMap);
|
|
167
|
+
safeParams.Remove("sign");
|
|
168
|
+
return safeParams;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 以下接口对接商家项目的真实 SDK 验签、支付配置、订单仓储、通知事件表和脱敏日志。
|
|
173
|
+
// 生产环境必须使用持久化订单表和通知事件表;内存 Dictionary 只能用于非生产 demo。
|
|
174
|
+
public interface IAlipayNotifyVerifier { bool Verify(Dictionary<string, string> paramsMap, AlipayNotifyConfig config); }
|
|
175
|
+
public interface IPaymentConfigRepository { Task<AlipayNotifyConfig?> FindByAppId(string? appId); }
|
|
176
|
+
public interface IOrderRepository
|
|
177
|
+
{
|
|
178
|
+
Task<MerchantOrder?> FindByOutTradeNo(string? outTradeNo);
|
|
179
|
+
Task MarkPaidIdempotently(string outTradeNo, string? tradeNo, Dictionary<string, string> paramsMap);
|
|
180
|
+
Task RecordNonPaidTradeEvent(string outTradeNo, string? tradeStatus, Dictionary<string, string> paramsMap);
|
|
181
|
+
}
|
|
182
|
+
public interface INotifyEventRepository
|
|
183
|
+
{
|
|
184
|
+
Task<bool> InsertOnce(string? notifyId, string? tradeNo, string? outTradeNo, string? tradeStatus, Dictionary<string, string> paramsMap);
|
|
185
|
+
}
|
|
186
|
+
public interface INotifyLogger { void Warn(string message, Dictionary<string, string> paramsMap); }
|
|
187
|
+
public record AlipayNotifyConfig(string AppId, string AlipayPublicKey, string? SellerId, string? SellerEmail);
|
|
188
|
+
public record MerchantOrder(string OutTradeNo, decimal TotalAmount);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
要点:
|
|
192
|
+
|
|
193
|
+
- 支付宝通知是 POST 表单,验签前必须保留完整表单参数。
|
|
194
|
+
- 验签必须调用当前项目支付宝 SDK 的实际验签 API,验签前不得丢字段或改值。
|
|
195
|
+
- `app_id`、`seller_id` / `seller_email` 从运行时支付配置校验,不从订单对象默认取值。
|
|
196
|
+
- 金额校验使用 `decimal` 或项目定点数工具,不要用浮点数直接比较。
|
|
197
|
+
- 只有付款成功状态且不是退款、关单、分账等事件时,才认定付款成功。
|
|
198
|
+
- 内存订单或内存幂等只能用于非生产 demo,生产必须使用持久化订单表和通知事件表。
|
|
199
|
+
- 处理成功后返回纯文本 `success`;异常、验签失败或业务校验失败返回 `fail`。
|
|
@@ -12,7 +12,11 @@ using Aop.Api.Domain;
|
|
|
12
12
|
IAopClient alipayClient = new DefaultAopClient(GetAlipayConfig());
|
|
13
13
|
|
|
14
14
|
var request = new AlipayTradePagePayRequest();
|
|
15
|
-
|
|
15
|
+
var notifyUrl = Environment.GetEnvironmentVariable("ALIPAY_NOTIFY_URL");
|
|
16
|
+
if (!string.IsNullOrWhiteSpace(notifyUrl))
|
|
17
|
+
{
|
|
18
|
+
request.SetNotifyUrl(notifyUrl.Trim()); // 真实生产或公网联调时传入;本地没有公网 HTTPS notify_url 时不要传 null、空字符串或占位 URL
|
|
19
|
+
}
|
|
16
20
|
request.SetReturnUrl("https://your-domain.com/return"); // 默认传入并替换为已验证结果页;仅用户明确关闭同步回跳时省略
|
|
17
21
|
|
|
18
22
|
var model = new AlipayTradePagePayModel
|
|
@@ -24,11 +28,11 @@ var model = new AlipayTradePagePayModel
|
|
|
24
28
|
};
|
|
25
29
|
request.SetBizModel(model);
|
|
26
30
|
|
|
27
|
-
// ⚠️ 网站支付必须使用
|
|
31
|
+
// ⚠️ 网站支付必须使用 PageExecute(),不能使用 Execute()
|
|
28
32
|
// ⚠️ 第二个参数传 null(accessToken),第三个参数 "POST" 表示生成 POST 表单
|
|
29
|
-
AlipayTradePagePayResponse response = alipayClient.
|
|
33
|
+
AlipayTradePagePayResponse response = alipayClient.PageExecute(request, null, "POST");
|
|
30
34
|
string formHtml = response.Body;
|
|
31
|
-
//
|
|
35
|
+
// PageExecute 返回的是 HTML 表单字符串,前端需要渲染并自动提交
|
|
32
36
|
|
|
33
37
|
private static AlipayConfig GetAlipayConfig()
|
|
34
38
|
{
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# APP支付接口 - C# 示例
|
|
1
|
+
# APP 支付接口 - C# 示例
|
|
2
2
|
|
|
3
3
|
## 请求示例
|
|
4
4
|
|
|
@@ -20,6 +20,11 @@ namespace SdkDemoTest
|
|
|
20
20
|
IAopClient alipayClient = new DefaultAopClient(GetAlipayConfig());
|
|
21
21
|
// 构造请求参数以调用接口
|
|
22
22
|
AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
|
|
23
|
+
var notifyUrl = Environment.GetEnvironmentVariable("ALIPAY_NOTIFY_URL");
|
|
24
|
+
if (!string.IsNullOrWhiteSpace(notifyUrl))
|
|
25
|
+
{
|
|
26
|
+
request.SetNotifyUrl(notifyUrl.Trim()); // 真实生产或公网联调时传入;本地没有公网 HTTPS notify_url 时不要传 null、空字符串或占位 URL
|
|
27
|
+
}
|
|
23
28
|
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
|
|
24
29
|
|
|
25
30
|
// 设置商户订单号
|
|
@@ -125,4 +130,3 @@ namespace SdkDemoTest
|
|
|
125
130
|
```
|
|
126
131
|
app_id=20190xxxxxx&biz_content=%7B%22time_expire%22%3A%222016-12-31+10%3A05%3A00%22%2C%22extend_params%22%3A%22%22%2C%22query_options%22%3A%22%5B%5C%22hyb_amount%5C%22%2C%5C%22enterprise_pay_info%5C%22%5D%22%2C%22subject%22%3A%22%E5%A4%A7%E4%B9%90%E9%80%8F%22%2C%22product_code%22%3A%22QUICK_MSECURITY_PAY%22%2C%22body%22%3A%22Iphone6+16G%22%2C%22passback_params%22%3A%22merchantBizType%253d3C%2526merchantBizNo%253dMERCHANT_BIZ_NO_PLACEHOLDER%22%2C%22specified_channel%22%3A%22pcredit%22%2C%22goods_detail%22%3A%22%22%2C%22merchant_order_no%22%3A%22MERCHANT_ORDER_NO_PLACEHOLDER%22%2C%22enable_pay_channels%22%3A%22pcredit%2CmoneyFund%2CdebitCardExpress%22%2C%22out_trade_no%22%3A%22MERCHANT_ORDER_NO_PLACEHOLDER%22%2C%22ext_user_info%22%3A%22%22%2C%22total_amount%22%3A%229.00%22%2C%22timeout_express%22%3A%2290m%22%2C%22disable_pay_channels%22%3A%22pcredit%2CmoneyFund%2CdebitCardExpress%22%2C%22agreement_sign_params%22%3A%22%22%7D&charset=UTF-8&format=json&method=alipay.trade.app.pay&sign=SIGN_PLACEHOLDER&sign_type=RSA2×tamp=2014-07-24+03%3A07%3A50&version=1.0
|
|
127
132
|
```
|
|
128
|
-
|
|
@@ -40,7 +40,9 @@ namespace A2MPaymentDemo
|
|
|
40
40
|
// ==================== 配置信息(实际使用时请从配置中心读取)====================
|
|
41
41
|
private static readonly AlipayConfig AlipayConfig = new AlipayConfig
|
|
42
42
|
{
|
|
43
|
-
|
|
43
|
+
// 默认用于本 Skill 的快速沙箱联调;生产部署时设置 ALIPAY_GATEWAY=https://openapi.alipay.com/gateway.do
|
|
44
|
+
ServerUrl = Environment.GetEnvironmentVariable("ALIPAY_GATEWAY")
|
|
45
|
+
?? "https://openapi-sandbox.dl.alipaydev.com/gateway.do",
|
|
44
46
|
AppId = "<APP_ID>",
|
|
45
47
|
PrivateKey = "<APP_PRIVATE_KEY>", // 请填写您的应用私钥(PKCS#1 格式)
|
|
46
48
|
AlipayPublicKey = "<ALIPAY_PUBLIC_KEY>", // 请填写您的支付宝公钥
|