django获取支付宝支付qrcode并使用canvas绘制
支付老李
结论
支付宝接口返回的 qr_code 通常是用于生成二维码的字符串,不是可以直接交给 Canvas 绘制的图片数据。
一般按下面的流程处理:
- Django 后端调用
alipay.trade.precreate创建支付宝预下单。 - 从支付宝响应中读取
qr_code。 - 后端以 JSON 格式向前端返回
qr_code、商户订单号等必要信息。 - 前端通过二维码库对
qr_code编码,并绘制到<canvas>。 - 支付结果以支付宝异步通知为准,不能只根据前端页面判断。
Canvas 不会自动把字符串转换成二维码,需要调用 QRCode.toCanvas() 等二维码编码方法。
Django 后端返回二维码内容
支付宝当面付预下单成功后,响应通常包含类似内容:
{
"code": "10000",
"msg": "Success",
"out_trade_no": "ORDER_202609090001",
"qr_code": "https://qr.alipay.com/xxxxxxxx"
}
前端生成二维码时,使用的原始内容就是其中的 qr_code。
下面假设项目中已经封装了支付宝客户端,并且 precreate() 返回解析后的字典。各个 Python SDK 的调用方式可能不同,具体写法应以所用 SDK 的文档为准。
# views.py
import json
import uuid
from decimal import Decimal, InvalidOperation
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from .alipay_service import alipay_client
@require_POST
def create_alipay_order(request):
try:
body = json.loads(request.body or "{}")
amount = Decimal(str(body.get("amount", "")))
except (json.JSONDecodeError, InvalidOperation):
return JsonResponse(
{"success": False, "message": "请求参数格式错误"},
status=400,
)
if amount <= 0:
return JsonResponse(
{"success": False, "message": "支付金额必须大于 0"},
status=400,
)
# 实际项目中,金额应从数据库中的商品或订单信息计算,
# 不应直接信任前端传入的 amount。
out_trade_no = f"ORDER_{uuid.uuid4().hex}"
try:
result = alipay_client.precreate(
out_trade_no=out_trade_no,
total_amount=f"{amount:.2f}",
subject="订单支付",
)
except Exception:
# 这里应记录完整异常,但不要把密钥、签名或支付宝原始敏感信息返回前端。
return JsonResponse(
{"success": False, "message": "创建支付订单失败"},
status=502,
)
if result.get("code") != "10000" or not result.get("qr_code"):
return JsonResponse(
{
"success": False,
"message": result.get("sub_msg") or result.get("msg") or "支付宝未返回二维码",
},
status=502,
)
return JsonResponse({
"success": True,
"out_trade_no": out_trade_no,
"qr_code": result["qr_code"],
})
支付宝调用的封装逻辑大致如下:
# alipay_service.py
class AlipayService:
def precreate(self, out_trade_no, total_amount, subject):
biz_content = {
"out_trade_no": out_trade_no,
"total_amount": total_amount,
"subject": subject,
}
# 此处调用实际 SDK 的 alipay.trade.precreate 方法。
# 返回值应先完成支付宝签名验证,再解析为字典。
return call_alipay_precreate_api(biz_content)
alipay_client = AlipayService()
应用私钥、支付宝公钥配置和签名参数生成逻辑都必须放在服务端,不能暴露给浏览器。
前端使用 Canvas 绘制二维码
前端可以使用支持 Canvas 输出的二维码库,例如 qrcode。下面的代码假设页面已经正确加载该库,并且它的全局对象名为 QRCode。
<canvas id="alipay-qrcode" width="240" height="240"></canvas>
<p id="payment-message"></p>
async function createPayment() {
const message = document.getElementById("payment-message");
const canvas = document.getElementById("alipay-qrcode");
message.textContent = "正在创建支付订单……";
try {
const response = await fetch("/api/alipay/precreate/", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": getCookie("csrftoken")
},
body: JSON.stringify({
amount: "99.00"
})
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.message || "创建支付订单失败");
}
await QRCode.toCanvas(canvas, data.qr_code, {
width: 240,
margin: 2,
errorCorrectionLevel: "M",
color: {
dark: "#000000",
light: "#ffffff"
}
});
message.textContent = `订单号:${data.out_trade_no},请使用支付宝扫码付款`;
} catch (error) {
const context = canvas.getContext("2d");
context.clearRect(0, 0, canvas.width, canvas.height);
message.textContent = error.message;
}
}
function getCookie(name) {
const cookies = document.cookie ? document.cookie.split(";") : [];
for (const item of cookies) {
const cookie = item.trim();
if (cookie.startsWith(`${name}=`)) {
return decodeURIComponent(cookie.slice(name.length + 1));
}
}
return "";
}
createPayment();
负责二维码编码的是这一行:
QRCode.toCanvas(canvas, data.qr_code);
下面的写法则不能生成二维码:
const context = canvas.getContext("2d");
context.fillText(data.qr_code, 0, 20);
fillText() 只会把字符串绘制成普通文字,不会将它转换成二维码的黑白矩阵。
如果后端返回的是二维码图片
如果后端已经生成 PNG 格式的二维码,并返回 Base64 图片,前端可以把它当作普通图片绘制到 Canvas:
function drawBase64Image(canvas, base64Data) {
const context = canvas.getContext("2d");
const image = new Image();
image.onload = function () {
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(image, 0, 0, canvas.width, canvas.height);
};
image.onerror = function () {
console.error("二维码图片加载失败");
};
image.src = base64Data;
}
后端可以返回以下格式:
{
"success": true,
"qrcode_image": "data:image/png;base64,iVBORw0KGgoAAA..."
}
不过,支付宝已经返回 qr_code 字符串时,通常不必再由后端生成图片。直接把字符串交给前端生成二维码,传输的数据更少,处理也更简单。
支付状态不能由 Canvas 判断
二维码成功显示,只能说明预下单已经完成,不能证明用户已经付款。
完整的支付流程如下:
浏览器请求创建订单
↓
Django 创建本地订单
↓
Django 调用 alipay.trade.precreate
↓
浏览器绘制 qr_code
↓
用户扫码并付款
↓
支付宝异步通知 Django
↓
Django 验证签名、金额、商户号和订单号
↓
更新本地订单状态
↓
前端查询本地订单状态并显示支付结果
前端可以定时查询 Django 提供的订单状态接口:
function watchPaymentStatus(outTradeNo) {
const timer = setInterval(async () => {
try {
const response = await fetch(
`/api/orders/${encodeURIComponent(outTradeNo)}/status/`
);
const data = await response.json();
if (data.status === "paid") {
clearInterval(timer);
document.getElementById("payment-message").textContent = "支付成功";
}
if (data.status === "closed") {
clearInterval(timer);
document.getElementById("payment-message").textContent = "订单已关闭";
}
} catch (error) {
console.error("查询支付状态失败", error);
}
}, 2000);
}
该接口应读取本地数据库中的订单状态。这个状态可以由通过验签的支付宝异步通知更新,也可以由后端主动调用支付宝订单查询接口确认。
注意事项
- 支付金额应由后端根据商品、订单和优惠规则计算,不能信任前端提交的金额。
out_trade_no必须唯一,并保存到本地订单表。- 调用支付宝接口后,需要检查
code、sub_code、msg和sub_msg,不能只看 HTTP 状态码。 - 支付宝响应和异步通知都需要验证签名。
- 处理异步通知时,还要核对订单号、金额、应用身份和收款方信息,不能只验签而不检查业务数据。
- 支付宝可能重复发送异步通知,因此订单更新必须支持幂等处理。
- 二维码需要设置合理的过期时间。订单关闭或超时后,应停止展示二维码。
- 前端不能接收或保存私钥、应用密钥等服务端配置。
- 二维码周围需要保留空白边距。缩放模糊、遮挡或过于复杂的背景都可能影响扫码。
- 正式环境应使用 HTTPS,日志中不要记录完整签名、密钥或敏感支付参数。
备注:内容仅供参考。
