@alipay/alipay-aipay 1.2.2 → 1.4.9
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 +6 -5
- package/skill/SKILL.md +15 -169
- package/skill/VERSION +1 -1
- package/skill/references/integration/flow.md +321 -292
- package/skill/references/integration/modules/alipay-sdk-reminder.md +5 -3
- package/skill/references/integration/modules/checklist.md +41 -27
- 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 +53 -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 +37 -72
- package/skill/references/integration/modules/product-decision.md +10 -27
- 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 +357 -147
- 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 +80 -0
- package/skill/references/normal/json/customer-messages.json +937 -0
- package/skill/references/normal/json/policy-rules.json +179 -0
- package/skill/references/normal/rejection-guide.md +11 -6
- package/skill/references/normal/scripts/alipay_cli_env.mjs +29 -0
- package/skill/references/normal/scripts/alipay_cli_env.sh +21 -0
- package/skill/references/normal/scripts/common.sh +87 -0
- package/skill/references/normal/scripts/detect_dev_tool.sh +5 -1
- package/skill/references/normal/scripts/integration_message_runner.mjs +801 -0
- package/skill/references/normal/scripts/onboarding_message_runner.mjs +436 -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 +1263 -0
- package/skill/references/normal/scripts/self_update.mjs +160 -0
- package/skill/references/normal/scripts/telemetry.mjs +1278 -0
- package/skill/references/normal/self-update.md +18 -51
- package/skill/references/onboarding/flow.md +381 -283
- package/skill/references/onboarding/modules/app-release.md +132 -179
- package/skill/references/onboarding/modules/authorization.md +53 -64
- package/skill/references/onboarding/modules/error-handling.md +31 -34
- package/skill/references/onboarding/modules/product-sign.md +62 -66
- package/skill/references/onboarding/modules/scripts/app.sh +397 -158
- package/skill/references/onboarding/modules/scripts/ar_sign_apply.sh +38 -22
- package/skill/references/onboarding/modules/scripts/auth.sh +479 -167
- package/skill/references/onboarding/modules/scripts/download_key_tool.sh +157 -0
- package/skill/references/onboarding/modules/scripts/error_handler.sh +100 -130
- package/skill/references/onboarding/modules/scripts/network_retry.sh +96 -0
- package/skill/references/onboarding/modules/scripts/query_sign_status.sh +17 -70
- package/skill/references/onboarding/modules/scripts/service.sh +153 -89
- package/skill/references/onboarding/modules/scripts/sign_status_parser.sh +58 -0
- package/skill/references/onboarding/modules/scripts/upload_screenshots.sh +16 -14
- package/skill/references/onboarding/modules/service-registration.md +67 -72
- package/skill/references/runtime-entry.md +62 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# APP支付接口 - Java 示例
|
|
1
|
+
# APP 支付接口 - Java 示例
|
|
2
2
|
|
|
3
3
|
## 请求示例
|
|
4
4
|
|
|
@@ -29,6 +29,10 @@ public class AlipayTradeAppPay {
|
|
|
29
29
|
|
|
30
30
|
// 构造请求参数以调用接口
|
|
31
31
|
AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
|
|
32
|
+
String notifyUrl = System.getenv("ALIPAY_NOTIFY_URL");
|
|
33
|
+
if (notifyUrl != null && !notifyUrl.isBlank()) {
|
|
34
|
+
request.setNotifyUrl(notifyUrl.trim()); // 真实生产或公网联调时传入;本地没有公网 HTTPS notifyUrl 时不要调用 setNotifyUrl(null/空字符串/占位 URL)
|
|
35
|
+
}
|
|
32
36
|
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
|
|
33
37
|
|
|
34
38
|
// 设置商户订单号
|
|
@@ -132,4 +136,3 @@ public class AlipayTradeAppPay {
|
|
|
132
136
|
```
|
|
133
137
|
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
|
|
134
138
|
```
|
|
135
|
-
|
|
@@ -107,7 +107,9 @@ public class A2MPaymentDemoController {
|
|
|
107
107
|
*/
|
|
108
108
|
private static AlipayConfig getAlipayConfig() {
|
|
109
109
|
AlipayConfig alipayConfig = new AlipayConfig();
|
|
110
|
-
|
|
110
|
+
// 默认用于本 Skill 的快速沙箱联调;生产部署时显式设置 ALIPAY_GATEWAY=https://openapi.alipay.com/gateway.do
|
|
111
|
+
alipayConfig.setServerUrl(System.getenv().getOrDefault(
|
|
112
|
+
"ALIPAY_GATEWAY", "https://openapi-sandbox.dl.alipaydev.com/gateway.do"));
|
|
111
113
|
alipayConfig.setAppId("<APP_ID>");
|
|
112
114
|
alipayConfig.setPrivateKey("<APP_PRIVATE_KEY>"); // 请填写您的应用私钥
|
|
113
115
|
alipayConfig.setFormat("json");
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# 异步通知处理 - Node.js 示例
|
|
2
|
+
|
|
3
|
+
适用产品:网站支付、APP 支付。按量付费不使用异步通知。
|
|
4
|
+
|
|
5
|
+
```javascript
|
|
6
|
+
const express = require("express");
|
|
7
|
+
|
|
8
|
+
// 支付宝异步通知是 application/x-www-form-urlencoded 表单,不能用 JSON parser 代替。
|
|
9
|
+
// createAlipayNotifyRouter 依赖项目里的真实 SDK 工厂、支付配置、订单仓储和通知事件仓储。
|
|
10
|
+
// 生产环境必须用持久化订单表和通知事件表做幂等;内存 Map 只能用于非生产 demo。
|
|
11
|
+
function createAlipayNotifyRouter(deps) {
|
|
12
|
+
const router = express.Router();
|
|
13
|
+
router.use(express.urlencoded({ extended: false }));
|
|
14
|
+
|
|
15
|
+
router.post("/alipay/notify", async (req, res) => {
|
|
16
|
+
const params = { ...req.body };
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const config = await deps.paymentConfigRepository.findByAppId(params.app_id);
|
|
20
|
+
if (!config) {
|
|
21
|
+
return fail(res, deps, "alipay notify app config not found", params);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const alipaySdk = deps.alipaySdkFactory.create(config);
|
|
25
|
+
if (!(await verifyNotifyWithAlipaySdk(params, alipaySdk))) {
|
|
26
|
+
return fail(res, deps, "alipay notify signature check failed", params);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const order = await deps.orderRepository.findByOutTradeNo(params.out_trade_no);
|
|
30
|
+
if (!order || !businessFieldsMatch(order, params, config)) {
|
|
31
|
+
return fail(res, deps, "alipay notify business check failed", params);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const inserted = await deps.notifyEventRepository.insertOnce({
|
|
35
|
+
notifyId: params.notify_id,
|
|
36
|
+
tradeNo: params.trade_no,
|
|
37
|
+
outTradeNo: params.out_trade_no,
|
|
38
|
+
tradeStatus: params.trade_status,
|
|
39
|
+
rawParams: params,
|
|
40
|
+
});
|
|
41
|
+
if (!inserted) {
|
|
42
|
+
return res.type("text/plain").send("success");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (isPaidTradeNotification(params)) {
|
|
46
|
+
await deps.orderRepository.markPaidIdempotently(order.outTradeNo, params.trade_no, params);
|
|
47
|
+
} else {
|
|
48
|
+
await deps.orderRepository.recordNonPaidTradeEvent(order.outTradeNo, params.trade_status, params);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return res.type("text/plain").send("success");
|
|
52
|
+
} catch (error) {
|
|
53
|
+
safeLogNotifyError(deps, "alipay notify unexpected exception", params);
|
|
54
|
+
return res.type("text/plain").send("fail");
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return router;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function verifyNotifyWithAlipaySdk(params, alipaySdk) {
|
|
62
|
+
// 落地前先查看目标项目 node_modules/alipay-sdk 的 package.json 和 .d.ts。
|
|
63
|
+
// 如果当前版本类型定义确认存在 checkNotifySignV2(params),按当前 SDK 实际 API 调用。
|
|
64
|
+
if (typeof alipaySdk.checkNotifySignV2 !== "function") {
|
|
65
|
+
throw new Error("current alipay-sdk does not expose checkNotifySignV2; confirm SDK types before coding");
|
|
66
|
+
}
|
|
67
|
+
return await Promise.resolve(alipaySdk.checkNotifySignV2(params));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function businessFieldsMatch(order, params, config) {
|
|
71
|
+
return params.app_id === config.appId
|
|
72
|
+
&& params.out_trade_no === order.outTradeNo
|
|
73
|
+
&& amountEquals(order.totalAmount, params.total_amount)
|
|
74
|
+
&& expectedSellerMatches(config, params.seller_id, params.seller_email);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isPaidTradeNotification(params) {
|
|
78
|
+
const paidStatus = params.trade_status === "TRADE_SUCCESS" || params.trade_status === "TRADE_FINISHED";
|
|
79
|
+
return paidStatus && !params.out_biz_no && !params.gmt_refund && !params.refund_fee;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function amountEquals(expected, actual) {
|
|
83
|
+
const expectedAmount = normalizeAmountText(expected);
|
|
84
|
+
const actualAmount = normalizeAmountText(actual);
|
|
85
|
+
return expectedAmount !== null && expectedAmount === actualAmount;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 返回“元”为单位、保留两位小数的金额文本,例如 88.00;不要用浮点数直接比较金额。
|
|
89
|
+
function normalizeAmountText(value) {
|
|
90
|
+
if (value == null) return null;
|
|
91
|
+
const text = String(value).trim();
|
|
92
|
+
const match = text.match(/^(\d+)(?:\.(\d{1,2}))?$/);
|
|
93
|
+
if (!match) return null;
|
|
94
|
+
return `${BigInt(match[1]).toString()}.${(match[2] || "").padEnd(2, "0")}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function expectedSellerMatches(config, sellerId, sellerEmail) {
|
|
98
|
+
const expectedSellerId = config.sellerId;
|
|
99
|
+
const expectedSellerEmail = config.sellerEmail;
|
|
100
|
+
return Boolean(
|
|
101
|
+
(expectedSellerId && sellerId === expectedSellerId)
|
|
102
|
+
|| (expectedSellerEmail && sellerEmail === expectedSellerEmail)
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function fail(res, deps, message, params) {
|
|
107
|
+
safeLogNotifyError(deps, message, params);
|
|
108
|
+
return res.type("text/plain").send("fail");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function safeLogNotifyError(deps, message, params) {
|
|
112
|
+
try {
|
|
113
|
+
deps.logger.warn(message, sanitizeNotifyParams(params));
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// 日志失败不能影响支付宝收到纯文本 fail。
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function sanitizeNotifyParams(params) {
|
|
120
|
+
const { sign, ...safeParams } = params;
|
|
121
|
+
return safeParams;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { createAlipayNotifyRouter };
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
要点:
|
|
128
|
+
|
|
129
|
+
- `notify_url` 路由必须使用表单解析中间件,不要用 JSON parser 读取支付宝通知。
|
|
130
|
+
- 验签必须调用当前项目支付宝 SDK 的实际验签 API;Node.js 落地前先查目标项目 `alipay-sdk` 类型定义确认 `checkNotifySignV2(params)` 是否存在。
|
|
131
|
+
- `app_id`、`seller_id` / `seller_email` 从运行时支付配置校验,不从订单对象默认取值。
|
|
132
|
+
- 金额校验使用“元”为单位的两位小数字符串或项目定点数工具,不要用浮点数直接比较。
|
|
133
|
+
- 只有付款成功状态且不是退款、关单、分账等事件时,才认定付款成功。
|
|
134
|
+
- 内存订单或内存幂等只能用于非生产 demo,生产必须使用持久化订单表和通知事件表。
|
|
135
|
+
- 处理成功后返回纯文本 `success`;异常、验签失败或业务校验失败返回 `fail`。
|
|
@@ -18,20 +18,26 @@ const alipaySdk = new AlipaySdk({
|
|
|
18
18
|
// ⚠️ 网站支付必须使用 pageExec() 方法,不能使用 exec()
|
|
19
19
|
// ⚠️ pageExec() 第二个参数必须传 'POST'
|
|
20
20
|
// ⚠️ notifyUrl 在真实生产或公网联调时传入;本地生产参数验收模式可临时不传,但仍需实现通知处理代码
|
|
21
|
+
// ⚠️ 本地没有公网 HTTPS notifyUrl 时,直接省略 notifyUrl,不要向 SDK 传 undefined、空字符串或占位 URL
|
|
21
22
|
// ⚠️ returnUrl 默认传入,仅在用户明确关闭同步回跳时省略
|
|
23
|
+
const notifyUrl = (process.env.ALIPAY_NOTIFY_URL || "").trim();
|
|
24
|
+
const requestOptions = {
|
|
25
|
+
returnUrl: "https://your-domain.com/return", // 默认传入并替换为已验证结果页;仅用户明确关闭同步回跳时省略
|
|
26
|
+
bizContent: {
|
|
27
|
+
out_trade_no: "MERCHANT_ORDER_NO_PLACEHOLDER", // 商户订单号,必填
|
|
28
|
+
total_amount: "88.88", // 订单总金额,必填,单位为元,精确到小数点后两位
|
|
29
|
+
subject: "Iphone6 16G", // 订单标题,必填
|
|
30
|
+
product_code: "FAST_INSTANT_TRADE_PAY", // 网站支付固定值,必填
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
if (notifyUrl) {
|
|
34
|
+
requestOptions.notifyUrl = notifyUrl;
|
|
35
|
+
}
|
|
36
|
+
|
|
22
37
|
const result = alipaySdk.pageExec(
|
|
23
38
|
"alipay.trade.page.pay",
|
|
24
39
|
"POST",
|
|
25
|
-
|
|
26
|
-
notifyUrl: "https://your-domain.com/notify", // 真实生产或公网联调时传入;本地生产参数验收模式可临时不传
|
|
27
|
-
returnUrl: "https://your-domain.com/return", // 默认传入并替换为已验证结果页;仅用户明确关闭同步回跳时省略
|
|
28
|
-
bizContent: {
|
|
29
|
-
out_trade_no: "MERCHANT_ORDER_NO_PLACEHOLDER", // 商户订单号,必填
|
|
30
|
-
total_amount: "88.88", // 订单总金额,必填,单位为元,精确到小数点后两位
|
|
31
|
-
subject: "Iphone6 16G", // 订单标题,必填
|
|
32
|
-
product_code: "FAST_INSTANT_TRADE_PAY", // 网站支付固定值,必填
|
|
33
|
-
},
|
|
34
|
-
}
|
|
40
|
+
requestOptions
|
|
35
41
|
);
|
|
36
42
|
|
|
37
43
|
// pageExec 返回的是 HTML 表单字符串,前端需要渲染并自动提交
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# APP支付接口 - Node.js 示例
|
|
1
|
+
# APP 支付接口 - Node.js 示例
|
|
2
2
|
|
|
3
3
|
## 请求示例
|
|
4
4
|
|
|
@@ -12,7 +12,10 @@ const alipaySdk = new AlipaySdk({
|
|
|
12
12
|
gateway: "https://openapi.alipay.com/gateway.do",
|
|
13
13
|
appAuthToken: "<-- 请填写应用授权令牌 -->",
|
|
14
14
|
});
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
// 真实生产或公网联调时传入 notifyUrl;本地没有公网 HTTPS notifyUrl 时直接省略,不要向 SDK 传 undefined、空字符串或占位 URL。
|
|
17
|
+
const notifyUrl = (process.env.ALIPAY_NOTIFY_URL || "").trim();
|
|
18
|
+
const requestOptions = {
|
|
16
19
|
bizContent: {
|
|
17
20
|
out_trade_no: "MERCHANT_ORDER_NO_PLACEHOLDER",
|
|
18
21
|
total_amount: "9.00",
|
|
@@ -54,7 +57,11 @@ const result = await alipaySdk.sdkExec("alipay.trade.app.pay", {
|
|
|
54
57
|
},
|
|
55
58
|
query_options: ["hyb_amount", "enterprise_pay_info"],
|
|
56
59
|
},
|
|
57
|
-
}
|
|
60
|
+
};
|
|
61
|
+
if (notifyUrl) {
|
|
62
|
+
requestOptions.notifyUrl = notifyUrl;
|
|
63
|
+
}
|
|
64
|
+
const result = await alipaySdk.sdkExec("alipay.trade.app.pay", requestOptions);
|
|
58
65
|
```
|
|
59
66
|
|
|
60
67
|
## 响应示例
|
|
@@ -62,4 +69,3 @@ const result = await alipaySdk.sdkExec("alipay.trade.app.pay", {
|
|
|
62
69
|
```
|
|
63
70
|
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
|
|
64
71
|
```
|
|
65
|
-
|
|
@@ -30,7 +30,8 @@ const CONFIG = {
|
|
|
30
30
|
appId: '<APP_ID>', // 运行时映射沙箱 appId
|
|
31
31
|
privateKey: '<APP_PRIVATE_KEY>', // 运行时映射 appPrivatePkcsKey(PKCS#1)
|
|
32
32
|
alipayPublicKey: '<ALIPAY_PUBLIC_KEY>', // 运行时映射 alipayPublicKey
|
|
33
|
-
|
|
33
|
+
// 默认用于本 Skill 的快速沙箱联调;生产部署时显式设置 ALIPAY_GATEWAY=https://openapi.alipay.com/gateway.do
|
|
34
|
+
gateway: process.env.ALIPAY_GATEWAY || 'https://openapi-sandbox.dl.alipaydev.com/gateway.do',
|
|
34
35
|
sellerId: '<SELLER_ID_2088>', // 商户 ID(2088 格式)
|
|
35
36
|
serviceId: 'api_mock_service_id', // 仅用于沙箱联调;上线前替换为服务市场真实 serviceId
|
|
36
37
|
merchantPrivateKey: '<APP_PRIVATE_KEY>' // 与 privateKey 使用同一运行时配置,用于商家签名
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# 异步通知处理 - PHP 示例
|
|
2
|
+
|
|
3
|
+
适用产品:网站支付、APP 支付。按量付费不使用异步通知。
|
|
4
|
+
|
|
5
|
+
```php
|
|
6
|
+
<?php
|
|
7
|
+
|
|
8
|
+
// 支付宝通知是 POST 表单,不是 JSON。
|
|
9
|
+
// $deps 接入项目真实 SDK 验签适配器、支付配置、订单仓储、通知事件仓储和脱敏日志。
|
|
10
|
+
function handleAlipayNotify(array $params, array $deps): void
|
|
11
|
+
{
|
|
12
|
+
try {
|
|
13
|
+
$config = $deps['paymentConfigRepository']->findByAppId($params['app_id'] ?? '');
|
|
14
|
+
if ($config === null) {
|
|
15
|
+
fail($deps, 'alipay notify app config not found', $params);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!verifyNotifyWithAlipaySdk($params, $deps['alipayNotifyVerifier'], $config)) {
|
|
19
|
+
fail($deps, 'alipay notify signature check failed', $params);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
$order = $deps['orderRepository']->findByOutTradeNo($params['out_trade_no'] ?? '');
|
|
23
|
+
if ($order === null || !businessFieldsMatch($order, $params, $config)) {
|
|
24
|
+
fail($deps, 'alipay notify business check failed', $params);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
$inserted = $deps['notifyEventRepository']->insertOnce([
|
|
28
|
+
'notify_id' => $params['notify_id'] ?? '',
|
|
29
|
+
'trade_no' => $params['trade_no'] ?? '',
|
|
30
|
+
'out_trade_no' => $params['out_trade_no'] ?? '',
|
|
31
|
+
'trade_status' => $params['trade_status'] ?? '',
|
|
32
|
+
'raw_params' => $params,
|
|
33
|
+
]);
|
|
34
|
+
if (!$inserted) {
|
|
35
|
+
respondPlainText('success');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (isPaidTradeNotification($params)) {
|
|
39
|
+
$deps['orderRepository']->markPaidIdempotently($order['out_trade_no'], $params['trade_no'] ?? '', $params);
|
|
40
|
+
} else {
|
|
41
|
+
$deps['orderRepository']->recordNonPaidTradeEvent($order['out_trade_no'], $params['trade_status'] ?? '', $params);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
respondPlainText('success');
|
|
45
|
+
} catch (Throwable $e) {
|
|
46
|
+
safeLogNotifyError($deps, 'alipay notify unexpected exception', $params);
|
|
47
|
+
respondPlainText('fail');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function businessFieldsMatch(array $order, array $params, array $config): bool
|
|
52
|
+
{
|
|
53
|
+
return ($params['app_id'] ?? '') === ($config['app_id'] ?? '')
|
|
54
|
+
&& ($params['out_trade_no'] ?? '') === $order['out_trade_no']
|
|
55
|
+
&& amountEquals($order['total_amount'], $params['total_amount'] ?? null)
|
|
56
|
+
&& expectedSellerMatches($config, $params['seller_id'] ?? null, $params['seller_email'] ?? null);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isPaidTradeNotification(array $params): bool
|
|
60
|
+
{
|
|
61
|
+
$paidStatus = ($params['trade_status'] ?? '') === 'TRADE_SUCCESS'
|
|
62
|
+
|| ($params['trade_status'] ?? '') === 'TRADE_FINISHED';
|
|
63
|
+
return $paidStatus
|
|
64
|
+
&& empty($params['out_biz_no'])
|
|
65
|
+
&& empty($params['gmt_refund'])
|
|
66
|
+
&& empty($params['refund_fee']);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function amountEquals($expected, $actual): bool
|
|
70
|
+
{
|
|
71
|
+
if ($actual === null) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
$expectedAmount = normalizeAmountText($expected);
|
|
75
|
+
$actualAmount = normalizeAmountText($actual);
|
|
76
|
+
return $expectedAmount !== null && $expectedAmount === $actualAmount;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeAmountText($value): ?string
|
|
80
|
+
{
|
|
81
|
+
$text = trim((string) $value);
|
|
82
|
+
if (!preg_match('/^(\d+)(?:\.(\d{1,2}))?$/', $text, $matches)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
$yuan = ltrim($matches[1], '0');
|
|
86
|
+
return ($yuan === '' ? '0' : $yuan) . '.' . str_pad($matches[2] ?? '', 2, '0');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function expectedSellerMatches(array $config, ?string $sellerId, ?string $sellerEmail): bool
|
|
90
|
+
{
|
|
91
|
+
$expectedSellerId = firstNonBlank($config['seller_id'] ?? null, null);
|
|
92
|
+
$expectedSellerEmail = firstNonBlank($config['seller_email'] ?? null, null);
|
|
93
|
+
return ($expectedSellerId !== null && $sellerId === $expectedSellerId)
|
|
94
|
+
|| ($expectedSellerEmail !== null && $sellerEmail === $expectedSellerEmail);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function firstNonBlank(?string $primary, ?string $fallback): ?string
|
|
98
|
+
{
|
|
99
|
+
if ($primary !== null && trim($primary) !== '') {
|
|
100
|
+
return $primary;
|
|
101
|
+
}
|
|
102
|
+
if ($fallback !== null && trim($fallback) !== '') {
|
|
103
|
+
return $fallback;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function verifyNotifyWithAlipaySdk(array $params, object $verifier, array $config): bool
|
|
109
|
+
{
|
|
110
|
+
// 使用当前项目已安装的支付宝 SDK 验签能力校验 $params['sign']。
|
|
111
|
+
// $verifier->verify($params, $config) 必须封装目标项目当前 SDK 实际通知验签 API;落地前先查已安装包文档或类型定义,不能手写临时 RSA 验签。
|
|
112
|
+
return $verifier->verify($params, $config);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function fail(array $deps, string $message, array $params): void
|
|
116
|
+
{
|
|
117
|
+
safeLogNotifyError($deps, $message, $params);
|
|
118
|
+
respondPlainText('fail');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function respondPlainText(string $body): void
|
|
122
|
+
{
|
|
123
|
+
header('Content-Type: text/plain; charset=UTF-8');
|
|
124
|
+
echo $body;
|
|
125
|
+
exit;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function safeLogNotifyError(array $deps, string $message, array $params): void
|
|
129
|
+
{
|
|
130
|
+
try {
|
|
131
|
+
if (isset($deps['logger'])) {
|
|
132
|
+
$deps['logger']->warning($message, sanitizeNotifyParams($params));
|
|
133
|
+
}
|
|
134
|
+
} catch (Throwable $e) {
|
|
135
|
+
// 日志失败不能影响支付宝收到纯文本 fail。
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function sanitizeNotifyParams(array $params): array
|
|
140
|
+
{
|
|
141
|
+
unset($params['sign']);
|
|
142
|
+
return $params;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 在项目路由入口中由 DI 容器提供 $deps 后调用:
|
|
146
|
+
// handleAlipayNotify($_POST, $deps);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
要点:
|
|
150
|
+
|
|
151
|
+
- 支付宝通知是 POST 表单,验签前必须保留完整 `$_POST` 参数。
|
|
152
|
+
- 验签必须调用当前项目支付宝 SDK 的实际验签 API,验签前不得丢字段或改值。
|
|
153
|
+
- `app_id`、`seller_id` / `seller_email` 从运行时支付配置校验,不从订单对象默认取值。
|
|
154
|
+
- 金额校验使用字符串或定点数规范化,不要用浮点数直接比较,也不要默认依赖未安装的 PHP 扩展。
|
|
155
|
+
- 只有付款成功状态且不是退款、关单、分账等事件时,才认定付款成功。
|
|
156
|
+
- 内存订单或内存幂等只能用于非生产 demo,生产必须使用持久化订单表和通知事件表。
|
|
157
|
+
- 处理成功后返回纯文本 `success`;异常、验签失败或业务校验失败返回 `fail`。
|
|
@@ -20,7 +20,10 @@ $aop->signType = 'RSA2';
|
|
|
20
20
|
|
|
21
21
|
// 构造请求参数
|
|
22
22
|
$request = new AlipayTradePagePayRequest();
|
|
23
|
-
$
|
|
23
|
+
$notifyUrl = trim(getenv('ALIPAY_NOTIFY_URL') ?: '');
|
|
24
|
+
if ($notifyUrl !== '') {
|
|
25
|
+
$request->setNotifyUrl($notifyUrl); // 真实生产或公网联调时传入;本地没有公网 HTTPS notify_url 时不要传 null、空字符串或占位 URL
|
|
26
|
+
}
|
|
24
27
|
$request->setReturnUrl('https://your-domain.com/return'); // 默认传入并替换为已验证结果页;仅用户明确关闭同步回跳时省略
|
|
25
28
|
|
|
26
29
|
$bizContent = [
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# APP支付接口 - PHP 示例
|
|
1
|
+
# APP 支付接口 - PHP 示例
|
|
2
2
|
|
|
3
3
|
## 请求示例
|
|
4
4
|
|
|
@@ -14,6 +14,10 @@ require_once '../aop/request/AlipayTradeAppPayRequest.php';
|
|
|
14
14
|
$alipayClient = new AopClient(getAlipayConfig());
|
|
15
15
|
// 构造请求参数以调用接口
|
|
16
16
|
$request = new AlipayTradeAppPayRequest();
|
|
17
|
+
$notifyUrl = trim(getenv('ALIPAY_NOTIFY_URL') ?: '');
|
|
18
|
+
if ($notifyUrl !== '') {
|
|
19
|
+
$request->setNotifyUrl($notifyUrl); // 真实生产或公网联调时传入;本地没有公网 HTTPS notify_url 时不要传 null、空字符串或占位 URL
|
|
20
|
+
}
|
|
17
21
|
$model = array();
|
|
18
22
|
|
|
19
23
|
// 设置商户订单号
|
|
@@ -104,4 +108,3 @@ function getAlipayConfig()
|
|
|
104
108
|
```
|
|
105
109
|
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
|
|
106
110
|
```
|
|
107
|
-
|
|
@@ -22,7 +22,8 @@ define('ALIPAY_CONFIG', [
|
|
|
22
22
|
'appId' => '<APP_ID>',
|
|
23
23
|
'privateKey' => '<APP_PRIVATE_KEY>', // 请填写您的应用私钥(PKCS#1 格式)
|
|
24
24
|
'alipayPublicKey' => '<ALIPAY_PUBLIC_KEY>', // 请填写您的支付宝公钥
|
|
25
|
-
|
|
25
|
+
// 默认用于本 Skill 的快速沙箱联调;生产部署时设置 ALIPAY_GATEWAY=https://openapi.alipay.com/gateway.do
|
|
26
|
+
'gateway' => getenv('ALIPAY_GATEWAY') ?: 'https://openapi-sandbox.dl.alipaydev.com/gateway.do',
|
|
26
27
|
'sellerId' => '<SELLER_ID_2088>', // 商户 ID(2088 格式)
|
|
27
28
|
'serviceId' => 'api_mock_service_id', // 仅用于沙箱联调;上线前替换为服务市场真实 serviceId
|
|
28
29
|
'merchantPrivateKey' => '<APP_PRIVATE_KEY>' // 请填写您的应用私钥(用于商家签名)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# 异步通知处理 - Python 示例
|
|
2
|
+
|
|
3
|
+
适用产品:网站支付、APP 支付。按量付费不使用异步通知。
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
import re
|
|
7
|
+
from flask import Blueprint, Response, request
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_alipay_notify_blueprint(deps):
|
|
11
|
+
"""deps 接入项目真实 SDK 验签适配器、支付配置、订单仓储、通知事件仓储和脱敏日志。"""
|
|
12
|
+
blueprint = Blueprint("alipay_notify", __name__)
|
|
13
|
+
|
|
14
|
+
@blueprint.post("/alipay/notify")
|
|
15
|
+
def alipay_notify():
|
|
16
|
+
params = {}
|
|
17
|
+
try:
|
|
18
|
+
params = request.form.to_dict(flat=True)
|
|
19
|
+
config = deps["payment_config_repository"].find_by_app_id(params.get("app_id"))
|
|
20
|
+
if config is None:
|
|
21
|
+
return fail(deps, "alipay notify app config not found", params)
|
|
22
|
+
|
|
23
|
+
if not verify_notify_with_alipay_sdk(params, deps["alipay_notify_verifier"], config):
|
|
24
|
+
return fail(deps, "alipay notify signature check failed", params)
|
|
25
|
+
|
|
26
|
+
order = deps["order_repository"].find_by_out_trade_no(params.get("out_trade_no"))
|
|
27
|
+
if order is None or not business_fields_match(order, params, config):
|
|
28
|
+
return fail(deps, "alipay notify business check failed", params)
|
|
29
|
+
|
|
30
|
+
inserted = deps["notify_event_repository"].insert_once(
|
|
31
|
+
notify_id=params.get("notify_id"),
|
|
32
|
+
trade_no=params.get("trade_no"),
|
|
33
|
+
out_trade_no=params.get("out_trade_no"),
|
|
34
|
+
trade_status=params.get("trade_status"),
|
|
35
|
+
raw_params=params,
|
|
36
|
+
)
|
|
37
|
+
if not inserted:
|
|
38
|
+
return Response("success", mimetype="text/plain")
|
|
39
|
+
|
|
40
|
+
if is_paid_trade_notification(params):
|
|
41
|
+
deps["order_repository"].mark_paid_idempotently(order["out_trade_no"], params.get("trade_no"), params)
|
|
42
|
+
else:
|
|
43
|
+
deps["order_repository"].record_non_paid_trade_event(order["out_trade_no"], params.get("trade_status"), params)
|
|
44
|
+
|
|
45
|
+
return Response("success", mimetype="text/plain")
|
|
46
|
+
except Exception:
|
|
47
|
+
safe_log_notify_error(deps, "alipay notify unexpected exception", params)
|
|
48
|
+
return Response("fail", mimetype="text/plain")
|
|
49
|
+
|
|
50
|
+
return blueprint
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def business_fields_match(order, params, config):
|
|
54
|
+
return (
|
|
55
|
+
params.get("app_id") == config["app_id"]
|
|
56
|
+
and params.get("out_trade_no") == order["out_trade_no"]
|
|
57
|
+
and amount_equals(order["total_amount"], params.get("total_amount"))
|
|
58
|
+
and expected_seller_matches(config, params.get("seller_id"), params.get("seller_email"))
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def is_paid_trade_notification(params):
|
|
63
|
+
paid_status = params.get("trade_status") in ("TRADE_SUCCESS", "TRADE_FINISHED")
|
|
64
|
+
return paid_status and not params.get("out_biz_no") and not params.get("gmt_refund") and not params.get("refund_fee")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def amount_equals(expected, actual):
|
|
68
|
+
expected_amount = normalize_amount_text(expected)
|
|
69
|
+
actual_amount = normalize_amount_text(actual)
|
|
70
|
+
return expected_amount is not None and expected_amount == actual_amount
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def normalize_amount_text(value):
|
|
74
|
+
if value is None:
|
|
75
|
+
return None
|
|
76
|
+
match = re.fullmatch(r"(\d+)(?:\.(\d{1,2}))?", str(value).strip())
|
|
77
|
+
if not match:
|
|
78
|
+
return None
|
|
79
|
+
return f"{int(match.group(1))}.{(match.group(2) or '').ljust(2, '0')}"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def expected_seller_matches(config, seller_id, seller_email):
|
|
83
|
+
expected_seller_id = config.get("seller_id")
|
|
84
|
+
expected_seller_email = config.get("seller_email")
|
|
85
|
+
return (
|
|
86
|
+
bool(expected_seller_id and seller_id == expected_seller_id)
|
|
87
|
+
or bool(expected_seller_email and seller_email == expected_seller_email)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def verify_notify_with_alipay_sdk(params, verifier, config):
|
|
92
|
+
"""
|
|
93
|
+
使用当前项目已安装的支付宝 SDK 验签能力校验 params["sign"]。
|
|
94
|
+
verifier.verify(params, config) 必须封装目标项目当前 SDK 实际通知验签 API;落地前先查已安装包文档或类型定义,不能手写临时 RSA 验签。
|
|
95
|
+
"""
|
|
96
|
+
return verifier.verify(params, config)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def fail(deps, message, params):
|
|
100
|
+
safe_log_notify_error(deps, message, params)
|
|
101
|
+
return Response("fail", mimetype="text/plain")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def safe_log_notify_error(deps, message, params):
|
|
105
|
+
try:
|
|
106
|
+
logger = deps.get("logger")
|
|
107
|
+
if logger is not None:
|
|
108
|
+
logger.warning(message, extra={"alipay_notify": sanitize_notify_params(params)})
|
|
109
|
+
except Exception:
|
|
110
|
+
# 日志失败不能影响支付宝收到纯文本 fail。
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def sanitize_notify_params(params):
|
|
115
|
+
safe_params = dict(params)
|
|
116
|
+
safe_params.pop("sign", None)
|
|
117
|
+
return safe_params
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
要点:
|
|
121
|
+
|
|
122
|
+
- 支付宝通知是 POST 表单,不能用 JSON body 解析。
|
|
123
|
+
- 验签必须调用当前项目支付宝 SDK 的实际验签 API,验签前不得丢字段或改值。
|
|
124
|
+
- `app_id`、`seller_id` / `seller_email` 从运行时支付配置校验,不从订单对象默认取值。
|
|
125
|
+
- 金额校验使用字符串规范化、`Decimal` 或项目定点数工具,不要用浮点数直接比较。
|
|
126
|
+
- 只有付款成功状态且不是退款、关单、分账等事件时,才认定付款成功。
|
|
127
|
+
- 内存订单或内存幂等只能用于非生产 demo,生产必须使用持久化订单表和通知事件表。
|
|
128
|
+
- 处理成功后返回纯文本 `success`;异常、验签失败或业务校验失败返回 `fail`。
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
## 请求示例
|
|
4
4
|
|
|
5
5
|
```python
|
|
6
|
+
import os
|
|
6
7
|
from alipay.aop.api.AlipayClientConfig import AlipayClientConfig
|
|
7
8
|
from alipay.aop.api.DefaultAlipayClient import DefaultAlipayClient
|
|
8
9
|
from alipay.aop.api.request.AlipayTradePagePayRequest import AlipayTradePagePayRequest
|
|
@@ -20,7 +21,9 @@ config.sign_type = "RSA2"
|
|
|
20
21
|
client = DefaultAlipayClient(alipay_client_config=config)
|
|
21
22
|
|
|
22
23
|
request = AlipayTradePagePayRequest()
|
|
23
|
-
|
|
24
|
+
notify_url = os.getenv("ALIPAY_NOTIFY_URL", "").strip()
|
|
25
|
+
if notify_url:
|
|
26
|
+
request.notify_url = notify_url # 真实生产或公网联调时传入;本地没有公网 HTTPS notify_url 时不要传 None、空字符串或占位 URL
|
|
24
27
|
request.return_url = "https://your-domain.com/return" # 默认传入并替换为已验证结果页;仅用户明确关闭同步回跳时省略
|
|
25
28
|
|
|
26
29
|
model = AlipayTradePagePayModel()
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
# APP支付接口 - Python 示例
|
|
1
|
+
# APP 支付接口 - Python 示例
|
|
2
2
|
|
|
3
3
|
## 请求示例
|
|
4
4
|
|
|
5
5
|
```python
|
|
6
6
|
import json
|
|
7
|
+
import os
|
|
7
8
|
from alipay.aop.api.AlipayClientConfig import AlipayClientConfig
|
|
8
9
|
from alipay.aop.api.DefaultAlipayClient import DefaultAlipayClient
|
|
9
10
|
from alipay.aop.api.request.AlipayTradeAppPayRequest import AlipayTradeAppPayRequest
|
|
@@ -21,6 +22,9 @@ client = DefaultAlipayClient(alipay_client_config=config)
|
|
|
21
22
|
|
|
22
23
|
# 构造请求参数以调用接口
|
|
23
24
|
request = AlipayTradeAppPayRequest()
|
|
25
|
+
notify_url = os.getenv("ALIPAY_NOTIFY_URL", "").strip()
|
|
26
|
+
if notify_url:
|
|
27
|
+
request.notify_url = notify_url # 真实生产或公网联调时传入;本地没有公网 HTTPS notify_url 时不要传 None、空字符串或占位 URL
|
|
24
28
|
model = AlipayTradeAppPayModel()
|
|
25
29
|
|
|
26
30
|
model.out_trade_no = "MERCHANT_ORDER_NO_PLACEHOLDER"
|
|
@@ -21,9 +21,10 @@ pip install flask alipay-sdk-python pycryptodome
|
|
|
21
21
|
不能直接使用 dict 赋值给 biz_model,因为 SDK 会调用 to_alipay_dict() 方法。
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
+
import base64
|
|
24
25
|
import json
|
|
26
|
+
import os
|
|
25
27
|
import time
|
|
26
|
-
import base64
|
|
27
28
|
from datetime import datetime, timedelta, timezone
|
|
28
29
|
from urllib.parse import quote, unquote
|
|
29
30
|
from Crypto.Signature import pkcs1_15
|
|
@@ -44,7 +45,8 @@ ALIPAY_CONFIG = {
|
|
|
44
45
|
'appId': '<APP_ID>',
|
|
45
46
|
'privateKey': '<APP_PRIVATE_KEY>', # 请填写您的应用私钥(PKCS#1 格式)
|
|
46
47
|
'alipayPublicKey': '<ALIPAY_PUBLIC_KEY>', # 请填写您的支付宝公钥
|
|
47
|
-
|
|
48
|
+
# 默认用于本 Skill 的快速沙箱联调;生产部署时显式设置为 https://openapi.alipay.com/gateway.do
|
|
49
|
+
'gateway': os.environ.get('ALIPAY_GATEWAY', 'https://openapi-sandbox.dl.alipaydev.com/gateway.do'),
|
|
48
50
|
'sellerId': '<SELLER_ID_2088>', # 商户 ID(2088 格式)
|
|
49
51
|
'serviceId': 'api_mock_service_id', # 仅用于沙箱联调;上线前替换为服务市场真实 serviceId
|
|
50
52
|
'merchantPrivateKey': '<APP_PRIVATE_KEY>' # 请填写您的应用私钥(用于商家签名)
|