Django 如何将购物车总金额传递给 Payment gateway
结论
document.getElementById('{{ cart.get_total_price }}') 的用法不对。getElementById() 接收的是 HTML 元素的 id,而 {{ cart.get_total_price }} 渲染后只是金额,例如 49.90,不是元素 ID。
可以先把购物车总额放入模板上下文,再通过隐藏字段或 Django 的 json_script 安全地传给 JavaScript。前端金额只能用于显示,提交给支付网关的金额必须由服务器重新计算。否则,用户可以修改 JavaScript 或表单中的金额。
推荐实现
1. 在视图中传递金额
不要用 replace('.', '') 处理金额。这样会把 12.50 变成 1250,但支付接口需要的是 12.50 还是 1250,要看网关要求使用主货币单位还是最小货币单位。
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
@login_required
def CartView(request):
cart = Cart(request)
total = cart.get_total_price()
return render(
request,
"payment/payment_form.html",
{
"cart": cart,
"total": format(total, "f"),
},
)
这里将 Decimal 金额格式化成普通的十进制字符串,例如 "12.50"。
2. 使用 json_script 输出金额
Django 提供的 json_script 可以安全地把数据传给 JavaScript:
<button id="submit" type="button">
Pay ${{ total }}
</button>
{{ total|json_script:"cart-total" }}
模板渲染后会生成一个包含金额的 JSON 数据节点。
3. 在 JavaScript 中读取金额
document.addEventListener("DOMContentLoaded", () => {
document.getElementById("submit").addEventListener("click", (event) => {
event.preventDefault();
const PBFKey = "xxxxxxxxxxxxxxxxxxxxxxxxxx";
const txRef = String(
Math.floor(Math.random() * 1000000000) + 1
);
const email = document.getElementById("email").value;
const fullname = document.getElementById("fullName").value;
const address1 = document.getElementById("custAdd").value;
const address2 = document.getElementById("custAdd2").value;
const country = document.getElementById("country").value;
const state = document.getElementById("state").value;
const postcode = document.getElementById("postCode").value;
const amountText = JSON.parse(
document.getElementById("cart-total").textContent
);
const amount = Number(amountText);
if (!Number.isFinite(amount) || amount <= 0) {
console.error("Invalid payment amount:", amountText);
return;
}
getpaidSetup({
PBFPubKey: PBFKey,
customer_email: email,
amount: amount,
currency: "USD",
txref: txRef,
onclose: function () {},
callback: function (response) {
const flwRef = response.tx.flwRef;
console.log(
"This is the response returned after a charge",
response
);
if (
response.tx.chargeResponse === "00" ||
response.tx.chargeResponse === "0"
) {
// 将交易引用发送到服务器,由服务器向支付网关验证交易
} else {
// 跳转到支付失败页面
}
},
});
});
});
这段代码还修正了原代码中的变量覆盖问题:
var address1 = document.getElementById("postCode").value;
这行代码会覆盖前面保存的 address1,邮编应保存在单独的 postcode 变量中。
使用隐藏字段的简单写法
如果暂时不使用 json_script,也可以添加一个隐藏字段:
<input
type="hidden"
id="payment-amount"
value="{{ total }}"
>
然后读取字段中的值:
const amount = Number(
document.getElementById("payment-amount").value
);
不要把金额当作元素 ID:
// 错误
document.getElementById("{{ cart.get_total_price }}").value;
是否需要乘以 100
在没有确认支付网关要求的金额格式前,不要直接这样写:
amount: amount * 100
如果接口接收主货币单位,12.50 美元应提交为:
amount: 12.50
只有接口明确要求最小货币单位时,才提交 1250。此时最好在服务端使用 Decimal 转换,避免 JavaScript 浮点数产生误差:
from decimal import Decimal, ROUND_HALF_UP
total = cart.get_total_price()
amount_in_cents = int(
(total * Decimal("100")).quantize(
Decimal("1"),
rounding=ROUND_HALF_UP,
)
)
最终采用哪种格式,应以当前支付网关接口的参数说明为准。
必须注意支付安全
模板中的金额、隐藏字段和 JavaScript 变量都可以被用户修改。安全的支付流程应当是:
- 浏览器向 Django 提交结算请求。
- Django 根据当前购物车中的商品、数量和价格重新计算总额。
- Django 在服务端生成唯一交易编号并创建订单。
- 服务端将可信金额提交给支付网关,或生成支付初始化参数。
- 收到支付回调后,服务端使用网关提供的交易引用再次查询并验证。
- 确认交易状态、币种、金额和订单编号全部匹配后,才能把订单标记为已支付。
前端回调中的:
response.tx.chargeResponse
只能控制界面跳转,不能单独作为支付成功的最终依据。支付金额也不能以浏览器提交的值为准。
备注:内容仅供参考。
