目录
Flask 前后端分离架构实现支付宝电脑网站支付功能
概述
在当今的电子商务和在线服务领域,集成安全可靠的支付功能是至关重要的。支付宝作为中国领先的第三方支付平台,提供了丰富的API接口供开发者使用。本文将详细讲解如何基于Flask框架,采用前后端分离的架构,实现支付宝电脑网站支付功能的完整集成。我们将从原理分析、环境搭建、密钥配置,到前端页面构建、后端接口实现,最后进行联调测试,提供一个完整的、可操作的解决方案。本文假设您已具备基本的Python和Web开发知识。
1. 支付宝支付原理与流程
1.1 核心概念理解
在开始编码之前,必须理解支付宝交互中的几个核心概念:
- 应用ID (
app_id
):在支付宝开放平台创建应用后获得,是应用的唯一标识。 - 商户订单号 (
out_trade_no
):由商户网站自行生成的唯一订单号,用于标识一次交易请求。 - 加签与验签:为了保证交易请求在传输过程中不被篡改,商户需要使用应用私钥对请求参数进行签名(加签),支付宝服务器使用支付宝公钥来验证这个签名是否有效(验签)。同理,支付宝发送的异步通知也会包含签名,商户需要用支付宝公钥进行验证。推荐使用 RSA2 算法。
- 异步通知 (
notify_url
):支付成功后,支付宝服务器会主动向商户配置的后端接口(一个URL)发送POST请求,通知支付结果。这是确定交易最终状态最可靠的方式,所有核心业务逻辑(如更新订单状态、发放商品)都应在此处完成。 - 同步通知 (
return_url
):支付完成后,支付宝会引导用户的浏览器跳转回商户网站的一个页面。注意:这个跳转可能因用户关闭页面等原因而不发生,因此只能用于结果展示,不能作为支付成功的依据。
1.2 支付交互流程
整个支付的交互过程涉及用户、商户前端、商户后端和支付宝服务器四方,其序列图如下:
2. 环境准备与配置
2.1 项目结构与依赖
创建项目文件夹,结构如下:
flask_alipay_project/
├── app.py # Flask应用主入口
├── alipay_utils.py # 支付宝SDK封装工具类
├── requirements.txt # 项目依赖
├── keys/ # 存放密钥的目录(务必加入.gitignore)
│ ├── app_private_key.pem
│ └── alipay_public_key.pem
└── templates/ # Jinja2模板目录
├── index.html # 下单页面
├── payment.html # 支付跳转页
├── success.html # 支付成功展示页
└── failed.html # 支付失败展示页
安装必要的Python库:
# requirements.txt
Flask==2.3.3
python-alipay-sdk==3.3.0
执行:
pip install -r requirements.txt
2.2 密钥生成与配置
生成商户密钥对:
# 生成PKCS8格式的私钥(2048位) openssl genrsa -out app_private_key.pem 2048 # 从私钥生成公钥 openssl rsa -in app_private_key.pem -pubout -out app_public_key.pem
支付宝平台配置:
- 登录支付宝开放平台。
- 创建应用(例如“电脑网站支付”应用),获取
APPID
。 - 在“接口加签方式”中,设置“公钥”。将刚生成的
app_public_key.pem
文件内容(去除-----BEGIN PUBLIC KEY-----
和-----END PUBLIC KEY-----
,合并为一行)粘贴进去。 - 保存后,平台会生成一个“支付宝公钥”,请将其下载保存为
alipay_public_key.pem
。
重要安全提示:keys/
文件夹必须添加到.gitignore
中,严禁将私钥提交到代码仓库。
3. 后端实现 (Flask API)
3.1 封装支付宝SDK工具类
首先创建一个工具类,统一初始化AliPay
对象。
# alipay_utils.py
from alipay import AliPay
from alipay.utils import AliPayConfig
import os
def get_alipay_client():
"""
创建并返回一个配置好的AliPay实例。
用于生产环境,如需沙箱环境,将debug=True并更换网关。
Returns:
AliPay: 初始化好的AliPay客户端对象。
"""
# 应用ID
app_id = "202100xxxxxxxxxx" # 替换为你的APPID
# 获取当前文件所在目录的绝对路径,并构建密钥文件路径
current_dir = os.path.dirname(os.path.abspath(__file__))
key_dir = os.path.join(current_dir, 'keys')
# 读取应用私钥
app_private_key_path = os.path.join(key_dir, 'app_private_key.pem')
with open(app_private_key_path, 'r', encoding='utf-8') as f:
app_private_key_string = f.read()
# 读取支付宝公钥
alipay_public_key_path = os.path.join(key_dir, 'alipay_public_key.pem')
with open(alipay_public_key_path, 'r', encoding='utf-8') as f:
alipay_public_key_string = f.read()
# 创建并返回AliPay实例
alipay_client = AliPay(
appid=app_id,
app_private_key_string=app_private_key_string,
alipay_public_key_string=alipay_public_key_string,
sign_type="RSA2", # 推荐使用RSA2
debug=False, # 此处为False,指向生产环境。沙箱环境请改为True
verbose=False, # 是否输出调试信息
config=AliPayConfig(
timeout=15, # 请求超时时间(秒)
ciphers=None # 可设置SSL加密套件
)
)
return alipay_client
3.2 核心路由实现
接下来在app.py
中实现主要的业务路由。
# app.py
from flask import Flask, request, jsonify, render_template, redirect
from alipay_utils import get_alipay_client
import time
app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False # 确保中文正常显示
# 模拟的订单存储,实际项目中应使用数据库
# 格式: { out_trade_no: { 'subject': ..., 'amount': ..., 'status': ... } }
orders = {}
@app.route('/')
def index():
"""首页,展示下单页面"""
return render_template('index.html')
@app.route('/api/create_payment', methods=['POST'])
def create_payment():
"""
创建支付订单接口 (API)
接收前端传来的订单信息,调用支付宝接口生成支付URL。
Expected JSON: {"subject": "商品标题", "total_amount": "0.01"}
"""
try:
# 1. 获取前端传入的参数
data = request.get_json()
if not data:
return jsonify({'code': 400, 'msg': '无效的请求数据'}), 400
subject = data.get('subject')
total_amount = str(data.get('total_amount')) # 确保是字符串
if not subject or not total_amount:
return jsonify({'code': 400, 'msg': '参数subject和total_amount不能为空'}), 400
# 2. 生成商户订单号(必须唯一)
out_trade_no = "T" + str(int(time.time() * 1000))
# 3. 初始化支付宝客户端
alipay_client = get_alipay_client()
# 4. 调用SDK,生成支付请求参数
order_string = alipay_client.api_alipay_trade_page_pay(
out_trade_no=out_trade_no, # 商户订单号
total_amount=total_amount, # 订单金额(单位:元,字符串)
subject=subject, # 订单标题
return_url=request.host_url + "payment/return", # 同步通知URL
notify_url=request.host_url + "payment/notify", # 异步通知URL(需公网能访问)
# 更多可选参数,如商品详情`body`、超时时间`time_expire`等
# product_code="FAST_INSTANT_TRADE_PAY" # 销售产品码,电脑网站支付固定值
)
# 5. 拼接支付页面URL
# 生产环境网关
gateway = "https://openapi.alipay.com/gateway.do?"
# 如果是沙箱环境,使用以下网关,并确保get_alipay_client中debug=True
# gateway = "https://openapi.alipaydev.com/gateway.do?"
pay_url = gateway + order_string
# 6. (模拟)保存订单信息
orders[out_trade_no] = {
'subject': subject,
'total_amount': total_amount,
'status': '待支付' # 初始状态
}
# 7. 返回支付URL和订单号给前端
return jsonify({
'code': 200,
'msg': 'success',
'data': {
'pay_url': pay_url,
'out_trade_no': out_trade_no
}
})
except Exception as e:
app.logger.error(f"创建支付订单时发生错误: {e}")
return jsonify({'code': 500, 'msg': '服务器内部错误'}), 500
@app.route('/payment/notify', methods=['POST'])
def payment_notify():
"""
支付宝异步通知接口 (API)
支付宝服务器会在用户支付成功后主动POST消息到此接口。
这是处理业务逻辑(如更新订单状态)的核心。
"""
# 1. 获取POST参数并转换为字典
data = request.form.to_dict()
app.logger.info(f"接收到支付宝异步通知: {data}")
# 2. 获取签名并从参数字典中移除(sign和sign_type不参与签名验证)
signature = data.pop('sign', None)
sign_type = data.pop('sign_type', None) # 通常是RSA2
# 3. 初始化支付宝客户端并验证签名
alipay_client = get_alipay_client()
success = alipay_client.verify(data, signature)
if success:
# 4. 签名验证通过
trade_status = data.get('trade_status')
out_trade_no = data.get('out_trade_no')
total_amount = data.get('total_amount')
trade_no = data.get('trade_no') # 支付宝交易号
app.logger.info(f"签名验证成功。订单: {out_trade_no}, 状态: {trade_status}")
if trade_status in ('TRADE_SUCCESS', 'TRADE_FINISHED'):
# 5. 支付成功,处理业务逻辑
# TODO: 这里应访问数据库,更新订单状态为“已支付”
# 重要:需要检查该订单是否已经处理过(幂等性),以及金额是否匹配
if out_trade_no in orders:
orders[out_trade_no]['status'] = '已支付'
orders[out_trade_no]['alipay_trade_no'] = trade_no
app.logger.info(f"订单 {out_trade_no} 状态更新为已支付。")
# 6. 处理成功后,必须返回 'success'(不带引号)字符串
# 否则支付宝会认为通知失败,在一定策略下重复发送通知
return 'success'
else:
# 其他状态,如 TRADE_CLOSED(交易关闭)
app.logger.warning(f"订单 {out_trade_no} 支付未成功,状态为: {trade_status}")
# 仍然返回'success',告知支付宝已收到通知,但业务上不更新订单为成功
return 'success'
else:
# 7. 签名验证失败,记录日志并返回'failure'
app.logger.error("支付宝异步通知签名验证失败!潜在的安全风险!")
return 'failure'
@app.route('/payment/return', methods=['GET'])
def payment_return():
"""
支付同步跳转返回页面
用户支付完成后,支付宝会引导用户跳转回此页面。
注意:此页面不可靠,仅用于展示结果,不能作为支付成功的依据。
"""
# 1. 获取URL参数
data = request.args.to_dict()
app.logger.info(f"接收到支付宝同步返回: {data}")
# 2. 获取并移除签名
signature = data.pop('sign', None)
sign_type = data.pop('sign_type', None)
# 3. 验证签名
alipay_client = get_alipay_client()
success = alipay_client.verify(data, signature)
if success:
out_trade_no = data.get('out_trade_no')
# 4. 验证通过,通常渲染一个“支付成功”的页面,并提示用户“等待服务器确认”
# 为了更好的用户体验,可以在这里主动查询一次订单状态(见下方/query_order接口)
return render_template('success.html',
out_trade_no=out_trade_no,
data=data)
else:
# 5. 验证失败
return render_template('failed.html', message="返回参数验证失败")
@app.route('/api/query_order', methods=['GET'])
def query_order():
"""
查询订单状态接口 (API)
供前端在同步返回页面或用户主动查询时调用,以确认订单最终状态。
"""
out_trade_no = request.args.get('out_trade_no')
if not out_trade_no:
return jsonify({'code': 400, 'msg': '缺少参数out_trade_no'}), 400
# 1. 先检查本地订单状态(模拟数据库查询)
local_order = orders.get(out_trade_no, {})
local_status = local_order.get('status', '订单不存在')
# 2. 如果本地状态已是“已支付”,则直接返回
if local_status == '已支付':
return jsonify({
'code': 200,
'msg': 'success',
'data': {
'out_trade_no': out_trade_no,
'status': 'paid', # 统一状态标识
'msg': '支付成功',
'source': 'local_db'
}
})
# 3. 如果本地未支付,则主动向支付宝查询订单状态
try:
alipay_client = get_alipay_client()
result = alipay_client.api_alipay_trade_query(out_trade_no=out_trade_no)
if result.get('code') == '10000': # 接口调用成功
trade_status = result.get('trade_status')
total_amount = result.get('total_amount')
if trade_status in ('TRADE_SUCCESS', 'TRADE_FINISHED'):
# 查询结果显示支付成功,更新本地订单状态
# TODO: 更新数据库
if out_trade_no in orders:
orders[out_trade_no]['status'] = '已支付'
orders[out_trade_no]['alipay_trade_no'] = result.get('trade_no')
return jsonify({
'code': 200,
'msg': 'success',
'data': {
'out_trade_no': out_trade_no,
'status': 'paid',
'msg': '支付成功',
'source': 'alipay_query'
}
})
elif trade_status == 'WAIT_BUYER_PAY':
return jsonify({
'code': 200,
'msg': 'success',
'data': {
'out_trade_no': out_trade_no,
'status': 'waiting',
'msg': '等待用户付款',
'source': 'alipay_query'
}
})
else:
# 其他状态,如TRADE_CLOSED
return jsonify({
'code': 200,
'msg': 'success',
'data': {
'out_trade_no': out_trade_no,
'status': 'failed',
'msg': f'交易未成功: {trade_status}',
'source': 'alipay_query'
}
})
else:
# 支付宝查询接口返回错误
sub_code = result.get('sub_code')
sub_msg = result.get('sub_msg')
return jsonify({
'code': 500,
'msg': f'支付宝查询失败: {sub_code} - {sub_msg}'
}), 500
except Exception as e:
app.logger.error(f"查询订单 {out_trade_no} 时发生异常: {e}")
return jsonify({'code': 500, 'msg': '查询订单状态失败'}), 500
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)
4. 前端实现 (Jinja2模板与JavaScript)
4.1 下单页面 (index.html)
<!DOCTYPE html>
<!-- templates/index.html -->
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flask支付宝支付演示</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; }
input { width: 100%; padding: 8px; box-sizing: border-box; }
button { background-color: #1677FF; color: white; padding: 10px 15px; border: none; cursor: pointer; }
#result { margin-top: 20px; padding: 10px; border: 1px solid #ddd; }
</style>
</head>
<body>
<h1>生成支付订单</h1>
<form id="paymentForm">
<div class="form-group">
<label for="subject">商品名称 (subject):</label>
<input type="text" id="subject" name="subject" value="测试商品" required>
</div>
<div class="form-group">
<label for="total_amount">支付金额 (元) (total_amount):</label>
<input type="number" id="total_amount" name="total_amount" value="0.01" min="0.01" step="0.01" required>
</div>
<button type="submit">生成支付链接</button>
</form>
<div id="result" style="display: none;">
<p>订单号: <span id="outTradeNo"></span></p>
<p>支付URL已生成,点击按钮跳转到支付宝完成支付。</p>
<!-- 方式一:直接显示链接 -->
<!-- <p><a id="payLink" href="#" target="_blank">点击去支付</a></p> -->
<!-- 方式二:自动提交表单(更常用) -->
<form id="alipayForm" action="" method="GET" style="display: none;">
<!-- 参数已包含在URL中,无需额外字段 -->
</form>
<button onclick="document.getElementById('alipayForm').submit();">跳转到支付宝支付</button>
</div>
<script>
document.getElementById('paymentForm').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = {
subject: document.getElementById('subject').value,
total_amount: document.getElementById('total_amount').value
};
try {
const response = await fetch('/api/create_payment', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
const result = await response.json();
if (result.code === 200) {
const data = result.data;
document.getElementById('outTradeNo').textContent = data.out_trade_no;
// 设置表单的Action为支付URL
document.getElementById('alipayForm').action = data.pay_url;
document.getElementById('result').style.display = 'block';
// 如果想自动跳转,可以取消下一行的注释
// document.getElementById('alipayForm').submit();
} else {
alert('生成支付链接失败: ' + result.msg);
}
} catch (error) {
console.error('Error:', error);
alert('请求失败,请检查网络或控制台。');
}
});
</script>
</body>
</html>
4.2 支付成功展示页 (success.html)
<!DOCTYPE html>
<!-- templates/success.html -->
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>支付成功 - 等待确认</title>
<script src="//cdn.jsdelivr.net/npm/sweetalert2@11"></script>
</head>
<body>
<h1>支付完成!</h1>
<p>订单号: {{ out_trade_no }}</p>
<p>正在向服务器确认支付结果,请稍候...</p>
<p>如果页面长时间无响应,<a href="/">点此返回首页</a>。</p>
<script>
// 页面加载后,主动查询订单状态
const outTradeNo = "{{ out_trade_no }}";
function queryOrderStatus() {
fetch(`/api/query_order?out_trade_no=${outTradeNo}`)
.then(response => response.json())
.then(result => {
if (result.code === 200) {
const data = result.data;
if (data.status === 'paid') {
Swal.fire({
icon: 'success',
title: '支付成功!',
text: `订单 ${outTradeNo} 已支付成功。`,
confirmButtonText: '好的'
}).then(() => {
// 可跳转到订单详情页等
window.location.href = "/";
});
} else if (data.status === 'waiting') {
Swal.fire({
icon: 'info',
title: '等待付款',
text: '您尚未完成支付,请在支付宝App内完成付款。',
confirmButtonText: '知道了'
});
} else {
Swal.fire({
icon: 'error',
title: '支付未成功',
text: `订单状态: ${data.msg}`,
confirmButtonText: '知道了'
});
}
} else {
Swal.fire({
icon: 'error',
title: '查询失败',
text: result.msg,
confirmButtonText: '知道了'
});
}
})
.catch(error => {
console.error('查询错误:', error);
Swal.fire({
icon: 'error',
title: '网络错误',
text: '查询订单状态时发生网络错误,请稍后刷新页面重试。',
confirmButtonText: '知道了'
});
});
}
// 页面加载后延迟一秒查询,给后端处理异步通知留点时间
setTimeout(queryOrderStatus, 1000);
</script>
</body>
</html>
4.3 支付失败页 (failed.html)
<!DOCTYPE html>
<!-- templates/failed.html -->
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>支付失败或异常</title>
</head>
<body>
<h1>支付过程发生异常</h1>
<p>原因: {{ message }}</p>
<p>请返回<a href="/">首页</a>重新尝试,或联系客服。</p>
</body>
</html>
4.4 支付跳转页 (payment.html - 可选)
如果不想在前端直接提交表单,可以创建一个中间页来处理跳转。
<!DOCTYPE html>
<!-- templates/payment.html -->
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>正在跳转到支付宝...</title>
</head>
<body>
<h1>正在跳转到支付宝支付页面</h1>
<p>如果跳转失败,<a id="payLink" href="#">请点击这里</a>。</p>
<script>
// 从URL中获取支付URL
const urlParams = new URLSearchParams(window.location.search);
const payUrl = urlParams.get('pay_url');
if (payUrl) {
document.getElementById('payLink').href = payUrl;
// 自动跳转
window.location.href = payUrl;
} else {
document.body.innerHTML = '<h1>错误:缺少支付参数</h1>';
}
</script>
</body>
</html>
5. 测试与部署
5.1 本地测试(沙箱环境)
- 修改配置:在
alipay_utils.py
中,将debug=True
,并使用沙箱环境的APPID
和对应的密钥。 - 启动应用:运行
python app.py
。 - 访问:打开
http://127.0.0.1:5000
。 - 支付测试:使用支付宝提供的沙箱钱包App(需单独下载)扫描二维码进行支付测试。使用沙箱买家账号付款。
5.2 生产环境部署
- 配置:确保
debug=False
,并使用生产环境的APPID
和密钥。 - HTTPS:生产环境的
notify_url
和return_url
必须支持HTTPS。可以使用Nginx反向代理Flask应用并配置SSL证书。 - WSGI服务器:不要使用Flask自带的开发服务器。使用Gunicorn或uWSGI等生产级WSGI服务器。
pip install gunicorn gunicorn -w 4 -b 0.0.0.0:5000 app:app
- 网络:确保您的服务器IP地址没有被防火墙屏蔽,支付宝服务器能够访问到您的
/payment/notify
接口。
6. 常见问题排查 (FAQ)
Q1: 签名错误 (sign check failed: check Sign and Data Fail
)?
A1: 这是最常见的问题。请按以下步骤排查:
- 密钥匹配:确认开放平台设置的是你的公钥,代码里读取的是你的私钥和支付宝给的公钥。
- 密钥格式:确保密钥文件是正确的PEM格式,没有多余空格或换行。
python-alipay-sdk
需要PKCS8格式的私钥。 - 参数编码:确保所有字符串参数为UTF-8编码。
Q2: 收不到异步通知 (notify_url
)?
A2:
- 公网可达:确保你的
notify_url
是公网HTTPS地址,并能被支付宝服务器访问。 - 及时响应:你的接口必须在处理完成后返回纯文本的
success
(不能有多余字符或JSON),否则支付宝会认为通知失败并重试。 - 日志排查:仔细查看Flask应用的日志,确认是否有POST请求到来。
Q3: 如何处理重复的异步通知?
A3: 在处理通知的业务逻辑中,必须实现幂等性。即在更新订单状态前,先检查数据库中该订单是否已经是“已支付”状态,如果是,则直接返回success
,不再执行后续更新和发货逻辑。
Q4: 沙箱测试一切正常,上线生产后失败?
A4:
- 检查
debug
标志是否已设为False
。 - 检查生产环境的密钥和APPID是否正确配置。
- 确保生产环境的域名已在支付宝开放平台的应用设置中配置好。
结论
本文详细介绍了如何使用Flask框架前后端分离地实现支付宝电脑网站支付功能。核心要点包括:
- 理解流程:深刻理解同步通知和异步通知的区别与用途。
- 安全第一:妥善保管私钥,严格进行签名验证。
- 依赖SDK:使用
python-alipay-sdk
极大简化了签名和API调用的复杂度。 - 幂等性:异步通知处理必须具备幂等性,以防止重复业务操作。
- 主动查询:在同步返回页面,结合主动查询接口来向用户展示最终状态,提供更好的用户体验。
此方案提供了一个健壮的生产环境支付集成基础,您可以根据实际业务需求,将其与用户系统、订单数据库、日志监控等模块进行对接。希望本指南能帮助您顺利完成支付功能的开发。