订单系统
Step 1:创建 orders 应用
在你项目根目录执行:
cd meiduo_mall/apps
python ../manage.py startapp orders
Step 2:注册 App
meiduo_mall/settings.py
INSTALLED_APPS = [
...
'apps.orders',
]
Step 3:models.py(订单模型)
apps/orders/models.py
from django.db import models
from apps.goods.models import SKU
from apps.users.models import User, Address
from utils.models import BaseModel
class OrderInfo(BaseModel):
"""订单信息"""
PAY_METHODS_ENUM = {
"CASH": 1,
"ALIPAY": 2,
}
PAY_METHOD_CHOICES = (
(1, "货到付款"),
(2, "支付宝"),
)
ORDER_STATUS_ENUM = {
"UNPAID": 1,
"UNSEND": 2,
"UNRECEIVED": 3,
"UNCOMMENT": 4,
"FINISHED": 5,
"CANCELED": 6,
}
ORDER_STATUS_CHOICES = (
(1, "待支付"),
(2, "待发货"),
(3, "待收货"),
(4, "待评价"),
(5, "已完成"),
(6, "已取消"),
)
order_id = models.CharField(max_length=64, primary_key=True, verbose_name="订单号")
user = models.ForeignKey(User, on_delete=models.PROTECT, verbose_name="下单用户")
address = models.ForeignKey(Address, on_delete=models.PROTECT, verbose_name="收货地址")
total_count = models.IntegerField(default=0, verbose_name="商品总数")
total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="商品总金额")
freight = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="运费")
pay_method = models.SmallIntegerField(choices=PAY_METHOD_CHOICES, default=1, verbose_name="支付方式")
status = models.SmallIntegerField(choices=ORDER_STATUS_CHOICES, default=1, verbose_name="订单状态")
class Meta:
db_table = "tb_order_info"
verbose_name = "订单基本信息"
verbose_name_plural = verbose_name
def __str__(self):
return self.order_id
class OrderGoods(BaseModel):
"""订单商品"""
SCORE_CHOICES = (
(0, '0分'),
(1, '20分'),
(2, '40分'),
(3, '60分'),
(4, '80分'),
(5, '100分'),
)
order = models.ForeignKey(
OrderInfo,
related_name='skus',
on_delete=models.CASCADE,
verbose_name="订单"
)
sku = models.ForeignKey(SKU, on_delete=models.PROTECT, verbose_name="订单商品")
count = models.IntegerField(default=1, verbose_name="数量")
price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="单价")
comment = models.TextField(default="", verbose_name="评价信息")
score = models.SmallIntegerField(choices=SCORE_CHOICES, default=5, verbose_name='满意度评分')
is_anonymous = models.BooleanField(default=False, verbose_name='是否匿名评价')
is_commented = models.BooleanField(default=False, verbose_name='是否评价了')
class Meta:
db_table = "tb_order_goods"
verbose_name = "订单商品"
verbose_name_plural = verbose_name
def __str__(self):
return self.sku.name
Step 4:apps.py
apps/orders/apps.py
from django.apps import AppConfig
class OrdersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.orders'
verbose_name = '订单'
Step 5:urls.py
apps/orders/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('settlement/', views.PlaceOrderView.as_view(), name='place_order'),
path('commit/', views.OrderView.as_view(), name='order_commit'),
path('success/', views.OrderSuccessView.as_view(), name='order_success'),
]
并在主路由 meiduo_mall/urls.py 中 include:
from django.urls import path, include
urlpatterns = [
...
path('orders/', include(('apps.orders.urls', 'orders'), namespace='orders')),
]
Step 6:views.py(核心)
apps/orders/views.py
import json
from decimal import Decimal
from django import http
from django.shortcuts import render
from django.views import View
from django.contrib.auth.mixins import LoginRequiredMixin
from django_redis import get_redis_connection
from django.db import transaction
from django.utils import timezone
from apps.goods.models import SKU
from apps.users.models import Address
from apps.orders.models import OrderInfo, OrderGoods
from utils.response_code import RETCODE
class PlaceOrderView(LoginRequiredMixin, View):
"""
确认订单页
GET /orders/settlement/
"""
def get(self, request):
user = request.user
# 1. 收货地址
addresses = Address.objects.filter(user=user, is_deleted=False)
# 2. Redis 取选中商品
redis_conn = get_redis_connection('carts')
cart_key = f"carts_{user.id}"
selected_key = f"selected_{user.id}"
id_count = redis_conn.hgetall(cart_key)
selected_ids = redis_conn.smembers(selected_key)
selected_carts = {}
for sku_id in selected_ids:
sku_id = int(sku_id)
selected_carts[sku_id] = int(id_count.get(str(sku_id).encode(), 0))
# 3. 查 SKU
skus = SKU.objects.filter(id__in=selected_carts.keys())
total_count = 0
total_amount = Decimal('0.00')
freight = Decimal('10.00')
for sku in skus:
sku.count = selected_carts.get(sku.id, 0)
sku.amount = sku.count * sku.price
total_count += sku.count
total_amount += sku.amount
context = {
"addresses": addresses,
"skus": skus,
"total_count": total_count,
"total_amount": total_amount,
"freight": freight,
"payment_amount": total_amount + freight,
}
return render(request, "place_order.html", context)
class OrderView(LoginRequiredMixin, View):
"""
提交订单
POST /orders/commit/
"""
def post(self, request):
user = request.user
try:
data = json.loads(request.body.decode())
except Exception:
return http.JsonResponse({"code": RETCODE.PARAMERR, "errmsg": "参数格式错误"})
address_id = data.get("address_id")
pay_method = data.get("pay_method")
if not all([address_id, pay_method]):
return http.JsonResponse({"code": RETCODE.PARAMERR, "errmsg": "参数不全"})
# 1. 地址校验
try:
address = Address.objects.get(pk=address_id, user=user, is_deleted=False)
except Address.DoesNotExist:
return http.JsonResponse({"code": RETCODE.NODATAERR, "errmsg": "地址不存在"})
if pay_method not in (
OrderInfo.PAY_METHODS_ENUM["CASH"],
OrderInfo.PAY_METHODS_ENUM["ALIPAY"],
):
return http.JsonResponse({"code": RETCODE.PARAMERR, "errmsg": "支付方式错误"})
# 2. 生成订单号
order_id = timezone.localtime().strftime("%Y%m%d%H%M%S") + f"{user.id:09d}"
total_count = 0
total_amount = Decimal("0.00")
freight = Decimal("10.00")
if pay_method == OrderInfo.PAY_METHODS_ENUM["CASH"]:
status = OrderInfo.ORDER_STATUS_ENUM["UNSEND"]
else:
status = OrderInfo.ORDER_STATUS_ENUM["UNPAID"]
redis_conn = get_redis_connection("carts")
cart_key = f"carts_{user.id}"
selected_key = f"selected_{user.id}"
id_count = redis_conn.hgetall(cart_key)
selected_ids = redis_conn.smembers(selected_key)
selected_carts = {
int(sku_id): int(id_count.get(str(sku_id).encode(), 0))
for sku_id in selected_ids
}
with transaction.atomic():
save_id = transaction.savepoint()
try:
order = OrderInfo.objects.create(
order_id=order_id,
user=user,
address=address,
total_count=0,
total_amount=Decimal("0.00"),
freight=freight,
pay_method=pay_method,
status=status,
)
for sku_id, count in selected_carts.items():
sku = SKU.objects.select_for_update().get(id=sku_id)
if sku.stock < count:
transaction.savepoint_rollback(save_id)
return http.JsonResponse({"code": RETCODE.STOCKERR, "errmsg": "库存不足"})
old_stock = sku.stock
new_stock = old_stock - count
new_sales = sku.sales + count
result = SKU.objects.filter(
id=sku.id,
stock=old_stock
).update(stock=new_stock, sales=new_sales)
if result == 0:
transaction.savepoint_rollback(save_id)
return http.JsonResponse({"code": RETCODE.STOCKERR, "errmsg": "下单失败"})
OrderGoods.objects.create(
order=order,
sku=sku,
count=count,
price=sku.price,
)
total_count += count
total_amount += count * sku.price
order.total_count = total_count
order.total_amount = total_amount
order.save()
# 清理 Redis 选中商品
pl = redis_conn.pipeline()
for sku_id in selected_carts.keys():
pl.hdel(cart_key, str(sku_id))
pl.srem(selected_key, str(sku_id))
pl.execute()
transaction.savepoint_commit(save_id)
except Exception as e:
transaction.savepoint_rollback(save_id)
return http.JsonResponse({"code": RETCODE.SERVERERR, "errmsg": "下单异常"})
return http.JsonResponse({
"code": RETCODE.OK,
"errmsg": "ok",
"order_id": order_id,
"payment_amount": str(total_amount + freight),
"pay_method": pay_method,
})
class OrderSuccessView(LoginRequiredMixin, View):
"""
订单成功页
GET /orders/success/
"""
def get(self, request):
order_id = request.GET.get("order_id")
payment_amount = request.GET.get("payment_amount")
pay_method = request.GET.get("pay_method")
context = {
"order_id": order_id,
"payment_amount": payment_amount,
"pay_method": pay_method,
}
return render(request, "order_success.html", context)
Step 7:前端对接(place_order.js)
static/js/place_order.js
var vm = new Vue({
el: '#app',
// 修改Vue变量的读取语法,避免和django模板语法冲突
delimiters: ['[[', ']]'],
data: {
host: host,
order_submitting: false, // 正在提交订单标志
pay_method: 2, // 支付方式,默认支付宝支付
nowsite: '', // 默认地址
payment_amount: '',
},
mounted(){
// 初始化
this.payment_amount = payment_amount;
// 绑定默认地址
this.nowsite = default_address_id;
},
methods: {
// 提交订单
on_order_submit(){
if (!this.nowsite) {
alert('请补充收货地址');
return;
}
if (!this.pay_method) {
alert('请选择付款方式');
return;
}
if (this.order_submitting === false){
this.order_submitting = true;
var url = this.host + '/orders/commit/';
axios.post(url, {
address_id: this.nowsite,
pay_method: this.pay_method
}, {
headers:{
'X-CSRFToken': getCookie('csrftoken')
},
responseType: 'json'
})
.then(response => {
this.order_submitting = false;
if (response.data.code === '0') {
location.href = '/orders/success/?order_id='
+ response.data.order_id
+ '&payment_amount=' + response.data.payment_amount
+ '&pay_method=' + response.data.pay_method;
} else if (response.data.code === '4101') {
location.href = '/login/?next=/orders/settlement/';
} else {
alert(response.data.errmsg);
}
})
.catch(error => {
this.order_submitting = false;
console.log(error.response);
alert('提交订单失败,请稍后重试');
})
}
}
}
});
templates/place_order.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>美多商城-订单确认</title>
<link rel="stylesheet" type="text/css" href="/static/css/reset.css">
<link rel="stylesheet" type="text/css" href="/static/css/main.css">
<script type="text/javascript" src="/static/js/host.js"></script>
<script type="text/javascript" src="/static/js/vue-2.5.16.js"></script>
<script type="text/javascript" src="/static/js/axios-0.18.0.min.js"></script>
</head>
<body>
<div id="app" v-cloak>
<div class="header_con">
<div class="header">
<div class="welcome fl">欢迎来到美多商城!</div>
<div class="fr">
<div class="login_info fl">
欢迎您:<em>{{ request.user.username }}</em>
</div>
<div class="user_link fl">
<span>|</span>
<a href="/users/center/">用户中心</a>
<span>|</span>
<a href="/carts/">我的购物车</a>
<span>|</span>
<a href="/users/center/orders/">我的订单</a>
</div>
</div>
</div>
</div>
<div class="search_bar clearfix">
<a href="/" class="logo fl"><img src="/static/images/logo.png"></a>
<div class="search_wrap fl">
<form method="get" action="/search/" class="search_con">
<input type="text" class="input_text fl" name="q" placeholder="搜索商品">
<input type="submit" class="input_btn fr" value="搜索">
</form>
</div>
</div>
<h3 class="common_title">确认收货地址</h3>
<div class="common_list_con clearfix" id="get_site">
<dl>
<dt>寄送到:</dt>
{% for address in addresses %}
<dd>
<input type="radio"
name="address_id"
value="{{ address.id }}"
{% if address.id == request.user.default_address_id %}checked{% endif %}
v-model="nowsite">
{{ address.province.name }} {{ address.city.name }} {{ address.district.name }}
{{ address.place }} ({{ address.receiver }} 收) {{ address.mobile|slice:":3" }}****{{ address.mobile|slice:"7:" }}
</dd>
{% empty %}
<dd>暂无收货地址,请先添加地址</dd>
{% endfor %}
</dl>
<a href="/users/addresses/" class="edit_site">编辑收货地址</a>
</div>
<h3 class="common_title">支付方式</h3>
<div class="common_list_con clearfix">
<div class="pay_style_con clearfix">
<input type="radio" name="pay_method" value="1" v-model="pay_method">
<label class="cash">货到付款</label>
<input type="radio" name="pay_method" value="2" v-model="pay_method">
<label class="zhifubao"></label>
</div>
</div>
<h3 class="common_title">商品列表</h3>
<div class="common_list_con clearfix">
<ul class="goods_list_th clearfix">
<li class="col01">商品名称</li>
<li class="col02">商品单位</li>
<li class="col03">商品价格</li>
<li class="col04">数量</li>
<li class="col05">小计</li>
</ul>
{% for sku in skus %}
<ul class="goods_list_td clearfix">
<li class="col01">{{ forloop.counter }}</li>
<li class="col02">
<img src="{{ sku.default_image.url }}" alt="{{ sku.name }}">
</li>
<li class="col03">{{ sku.name }}</li>
<li class="col04">台</li>
<li class="col05">{{ sku.price }}元</li>
<li class="col06">{{ sku.count }}</li>
<li class="col07">{{ sku.amount }}元</li>
</ul>
{% endfor %}
</div>
<h3 class="common_title">总金额结算</h3>
<div class="common_list_con clearfix">
<div class="settle_con">
<div class="total_goods_count">
共<em>{{ total_count }}</em>件商品,总金额<b>{{ total_amount }}元</b>
</div>
<div class="transit">运费:<b>{{ freight }}元</b></div>
<div class="total_pay">实付款:<b>{{ payment_amount }}元</b></div>
</div>
</div>
<div class="order_submit clearfix">
<a href="javascript:;" id="order_btn" @click="on_order_submit">提交订单</a>
</div>
<div class="footer">
<div class="foot_link">
<a href="#">关于我们</a>
<span>|</span>
<a href="#">联系我们</a>
<span>|</span>
<a href="#">招聘人才</a>
<span>|</span>
<a href="#">友情链接</a>
</div>
<p>CopyRight © 2016 北京美多商业股份有限公司 All Rights Reserved</p>
<p>电话:010-****888 京ICP备*******8号</p>
</div>
</div>
<script>
var payment_amount = "{{ payment_amount }}";
var default_address_id = "{{ request.user.default_address_id|default:'' }}";
</script>
<script type="text/javascript" src="/static/js/host.js"></script>
<script type="text/javascript" src="/static/js/common.js"></script>
<script type="text/javascript" src="/static/js/place_order.js"></script>
</body>
</html>
最后完善下之前 cart 模块,点击“去结算”跳转到结算页面
templates/cart.html
<li class="col04">
<a href="javascript:;">去结算</a>
</li>
修改为:
<li class="col04">
<a href="{{ url('orders:place_order') }}">去结算</a>
</li>
测试
我们要验证 4 件事:
- Redis 勾选商品 → 页面能显示商品
- 页面金额计算正确
- 点击【提交订单】→
/orders/commit/正常返回 - Redis 被清空 + 订单落库成功
打开商品详情页
http://127.0.0.1:8000/detail/3/

打开购物车页,勾选几个商品,点击“去结算”
http://127.0.0.1:8000/carts/

再访问确认页:
http://127.0.0.1:8000/orders/settlement/

你应该看到什么:
- 商品列表里出现 SKU
- 数量正确
- 小计 = 单价 × 数量
- 总金额正确
- 实付款 = 总金额 + 运费
校验 Redis 数据
Django 的购物车 Redis 在 3 号库:

carts_30中:(hash 结构)
SKU 1 -> 数量 4
SKU 5 -> 数量 1
SKU 7 -> 数量 1
但只有 selected_30 里的 SKU 才会进入订单:
selected_30(set 结构)
selected = {1, 7}
支付系统
现在订单提交接口已经 OK
- 从 Redis 取
carts_{uid}+selected_{uid} - 创建
OrderInfo+OrderGoods - 扣库存 / 增销量(同时用了
select_for_update()+ “stock=old_stock” 乐观锁判断,有点重复,不影响跑) - 最后清 Redis 里选中的 SKU(hdel + srem)
- 返回
order_id + payment_amount + pay_method
安装支付宝 SDK
适用于版本:Django 5 + Python 3.11
pip install python-alipay-sdk
新增:支付信息表 Payment(保存支付宝流水)
修改 apps/orders/models.py
保留原来的 OrderInfo / OrderGoods,并新增 Payment
from django.db import models
from apps.goods.models import SKU
from apps.users.models import User, Address
from utils.models import BaseModel
class OrderInfo(BaseModel):
"""订单信息"""
PAY_METHODS_ENUM = {
"CASH": 1,
"ALIPAY": 2,
}
PAY_METHOD_CHOICES = (
(1, "货到付款"),
(2, "支付宝"),
)
ORDER_STATUS_ENUM = {
"UNPAID": 1,
"UNSEND": 2,
"UNRECEIVED": 3,
"UNCOMMENT": 4,
"FINISHED": 5,
"CANCELED": 6,
}
ORDER_STATUS_CHOICES = (
(1, "待支付"),
(2, "待发货"),
(3, "待收货"),
(4, "待评价"),
(5, "已完成"),
(6, "已取消"),
)
order_id = models.CharField(max_length=64, primary_key=True, verbose_name="订单号")
user = models.ForeignKey(User, on_delete=models.PROTECT, verbose_name="下单用户")
address = models.ForeignKey(Address, on_delete=models.PROTECT, verbose_name="收货地址")
total_count = models.IntegerField(default=0, verbose_name="商品总数")
total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="商品总金额")
freight = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="运费")
pay_method = models.SmallIntegerField(choices=PAY_METHOD_CHOICES, default=1, verbose_name="支付方式")
status = models.SmallIntegerField(choices=ORDER_STATUS_CHOICES, default=1, verbose_name="订单状态")
class Meta:
db_table = "tb_order_info"
verbose_name = "订单基本信息"
verbose_name_plural = verbose_name
def __str__(self):
return self.order_id
class OrderGoods(BaseModel):
"""订单商品"""
SCORE_CHOICES = (
(0, '0分'),
(1, '20分'),
(2, '40分'),
(3, '60分'),
(4, '80分'),
(5, '100分'),
)
order = models.ForeignKey(
OrderInfo,
related_name='skus',
on_delete=models.CASCADE,
verbose_name="订单"
)
sku = models.ForeignKey(SKU, on_delete=models.PROTECT, verbose_name="订单商品")
count = models.IntegerField(default=1, verbose_name="数量")
price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="单价")
comment = models.TextField(default="", verbose_name="评价信息")
score = models.SmallIntegerField(choices=SCORE_CHOICES, default=5, verbose_name='满意度评分')
is_anonymous = models.BooleanField(default=False, verbose_name='是否匿名评价')
is_commented = models.BooleanField(default=False, verbose_name='是否评价了')
class Meta:
db_table = "tb_order_goods"
verbose_name = "订单商品"
verbose_name_plural = verbose_name
def __str__(self):
return self.sku.name
class Payment(BaseModel):
"""
支付信息表:用于记录支付宝流水号 trade_no 等
一个订单对应一条支付记录(通常)
"""
order = models.OneToOneField(
OrderInfo,
on_delete=models.CASCADE,
related_name="payment",
verbose_name="订单"
)
trade_id = models.CharField(max_length=128, unique=True, verbose_name="支付宝交易号")
total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="支付金额")
class Meta:
db_table = "tb_payment"
verbose_name = "支付信息"
verbose_name_plural = verbose_name
def __str__(self):
return f"{self.order.order_id} - {self.trade_id}"
迁移
python manage.py makemigrations
python manage.py migrate
新增:支付宝配置
一、获取 ALIPAY_APP_ID:
我们使用沙箱模式进行测试:
https://open.alipay.com/develop/sandbox/app

二、生成「你自己的 RSA 私钥 + 公钥」
打开终端,执行
cd ~/Desktop
openssl genrsa -out app_private_key.pem 2048
从私钥生成公钥
openssl rsa -in app_private_key.pem -pubout -out app_public_key.pem
生成公钥文件:
app_public_key.pem
三、「公钥」粘到支付宝沙箱后台

这里需要注意:不要粘PEM 全格式(不能带 BEGIN/END 行),然后会生成一个支付宝公钥要用到

你在 meiduo_mall/settings.py里加上:
# =========================
# 支付宝配置(开发 / 生产通用)
# =========================
# 应用 APPID(沙箱 / 正式不同,但你现在用的是沙箱)
ALIPAY_APP_ID = "902100015965XXXX"
# =========================
# 私钥 / 公钥:优先用 pem 文件(生产推荐)
# =========================
# 应用私钥(你方)
# 生产环境:放 keys/app_private_key.pem,然后打开这行
# ALIPAY_APP_PRIVATE_KEY_PATH = BASE_DIR / "keys/app_private_key.pem"
# 支付宝公钥(支付宝方)
# 生产环境:放 keys/alipay_public_key.pem,然后打开这行
# ALIPAY_ALIPAY_PUBLIC_KEY_PATH = BASE_DIR / "keys/alipay_public_key.pem"
# =========================
# fallback:开发期直接写字符串(你现在这套)
# =========================
# 注意:这是【支付宝公钥】(不是你自己的)
ALIPAY_ALIPAY_PUBLIC_KEY = """
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy9cXXXXX...
-----END PUBLIC KEY-----
"""
# 注意:这是【应用私钥】(你自己的)
ALIPAY_APP_PRIVATE_KEY = """
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIXXXXX...
-----END PRIVATE KEY-----
"""
# =========================
# 回调地址
# =========================
# 同步回跳(用户浏览器跳回)
ALIPAY_RETURN_URL = "http://127.0.0.1:8000/orders/alipay/return/"
# 异步通知(支付宝服务器回调)
# 本地 127.0.0.1 支付宝服务器访问不到
# 生产必须改成公网 https 地址
ALIPAY_NOTIFY_URL = "http://127.0.0.1:8000/orders/alipay/notify/"
# =========================
# 网关 + debug 开关
# =========================
# 沙箱网关
ALIPAY_GATEWAY = "https://openapi-sandbox.dl.alipaydev.com/gateway.do"
# 正式网关(上线时切换)
# ALIPAY_GATEWAY = "https://openapi.alipay.com/gateway.do"
# 明确 debug 标志(别再靠字符串猜了)
ALIPAY_DEBUG = True # 沙箱 True,正式 False
新增:支付宝工具封装
新建文件:apps/orders/alipay.py
from __future__ import annotations
from pathlib import Path
from django.conf import settings
from alipay import AliPay
def _read_key_from_path(path_value) -> str | None:
if not path_value:
return None
p = Path(path_value)
if not p.exists():
print("❌ KEY FILE NOT FOUND:", p)
return None
content = p.read_text(encoding="utf-8").strip()
print("✅ READ KEY FILE:", p, "LEN =", len(content))
return content
def get_alipay_client() -> AliPay:
print("🔥 ENTER get_alipay_client()")
# 1) 私钥
app_private_key = _read_key_from_path(
getattr(settings, "ALIPAY_APP_PRIVATE_KEY_PATH", None)
)
if not app_private_key:
app_private_key = getattr(settings, "ALIPAY_APP_PRIVATE_KEY", "").strip()
print("🔥 APP PRIVATE KEY LEN:", len(app_private_key))
if not app_private_key:
raise RuntimeError("Missing Alipay app private key")
# 2) 支付宝公钥
alipay_public_key = _read_key_from_path(
getattr(settings, "ALIPAY_ALIPAY_PUBLIC_KEY_PATH", None)
)
if not alipay_public_key:
alipay_public_key = getattr(settings, "ALIPAY_ALIPAY_PUBLIC_KEY", "").strip()
print("🔥 ALIPAY PUBLIC KEY LEN:", len(alipay_public_key))
if not alipay_public_key:
raise RuntimeError("Missing Alipay public key")
# 3) debug
debug = getattr(settings, "ALIPAY_DEBUG", None)
if debug is None:
gateway = getattr(settings, "ALIPAY_GATEWAY", "")
debug = "sandbox" in gateway
print("🔥 ALIPAY DEBUG =", debug)
print("🔥 ALIPAY APPID =", settings.ALIPAY_APP_ID)
print("🔥 ALIPAY GATEWAY =", settings.ALIPAY_GATEWAY)
# 4) 创建 client
client = AliPay(
appid=settings.ALIPAY_APP_ID,
app_notify_url=settings.ALIPAY_NOTIFY_URL,
app_private_key_string=app_private_key,
alipay_public_key_string=alipay_public_key,
sign_type="RSA2",
debug=bool(debug),
)
print("🔥 AliPay CLIENT CREATED OK")
return client
新增:支付接口 3 个 URL
apps/orders/urls.py
我们加 3 个:
GET /orders/payment/<order_id>/→ 返回支付 URLGET /orders/alipay/return/→ 同步回跳(展示支付结果)POST /orders/alipay/notify/→ 异步回调(更新订单状态、保存流水)
from django.urls import path
from . import views
app_name = "orders"
urlpatterns = [
# =========================
# 下单流程
# =========================
# 确认订单页
# GET /orders/settlement/
path("settlement/", views.PlaceOrderView.as_view(), name="place_order"),
# 提交订单
# POST /orders/commit/
path("commit/", views.OrderView.as_view(), name="order_commit"),
# 订单成功页
# GET /orders/success/?order_id=xxx&pay_method=xxx
path("success/", views.OrderSuccessView.as_view(), name="order_success"),
# =========================
# 支付相关
# =========================
# 生成支付宝支付 URL
# GET /orders/payment/<order_id>/
path("payment/<str:order_id>/", views.PaymentURLView.as_view(), name="payment"),
# 同步回调(用户浏览器跳回)
# GET /orders/alipay/return/?out_trade_no=xxx&trade_no=xxx&sign=xxx
path("alipay/return/", views.AlipayReturnView.as_view(), name="alipay_return"),
# 异步通知(支付宝服务器回调)
# POST /orders/alipay/notify/
path("alipay/notify/", views.AlipayNotifyView.as_view(), name="alipay_notify"),
]
加入支付 View 和回调 View
改:apps/orders/views.py
import json
from decimal import Decimal
from django import http
from django.shortcuts import render
from django.views import View
from django.contrib.auth.mixins import LoginRequiredMixin
from django_redis import get_redis_connection
from django.db import transaction
from django.utils import timezone
from django.conf import settings
from django.http import JsonResponse
from apps.goods.models import SKU
from apps.users.models import Address
from apps.orders.models import OrderInfo, OrderGoods, Payment
from apps.orders.alipay import get_alipay_client
from utils.response_code import RETCODE
print("🔥 LOADED orders.views.py FROM:", __file__)
# =========================
# ✅ 小工具:从 Redis 读出选中的商品 {sku_id(int): count(int)}
# =========================
def _get_selected_carts(redis_conn, user_id: int) -> dict[int, int]:
cart_key = f"carts_{user_id}"
selected_key = f"selected_{user_id}"
id_count = redis_conn.hgetall(cart_key) # {b'3': b'1', ...}
selected_ids = redis_conn.smembers(selected_key) # {b'3', b'5', ...}
selected_carts: dict[int, int] = {}
for sku_id_b in selected_ids:
# sku_id_b 是 bytes: b'3'
try:
sku_id = int(sku_id_b)
except Exception:
continue
# ✅ 关键:hash 的 key 也是 bytes,所以必须用 bytes 去 get
count_b = id_count.get(sku_id_b)
if not count_b:
continue
try:
count = int(count_b)
except Exception:
continue
if count > 0:
selected_carts[sku_id] = count
return selected_carts
class PlaceOrderView(LoginRequiredMixin, View):
"""
确认订单页
GET /orders/settlement/
"""
def get(self, request):
user = request.user
# 1) 收货地址
addresses = Address.objects.filter(user=user, is_deleted=False)
# 2) 取 Redis 选中商品
redis_conn = get_redis_connection("carts")
selected_carts = _get_selected_carts(redis_conn, user.id)
# 没有勾选商品也允许进结算页(前端可以提示)
skus = SKU.objects.filter(id__in=selected_carts.keys())
total_count = 0
total_amount = Decimal("0.00")
freight = Decimal("10.00")
for sku in skus:
sku.count = selected_carts.get(sku.id, 0)
sku.amount = sku.count * sku.price
total_count += sku.count
total_amount += sku.amount
context = {
"addresses": addresses,
"skus": skus,
"total_count": total_count,
"total_amount": total_amount,
"freight": freight,
"payment_amount": total_amount + freight,
}
return render(request, "place_order.html", context)
class OrderView(LoginRequiredMixin, View):
"""
提交订单
POST /orders/commit/
"""
def post(self, request):
user = request.user
try:
data = json.loads(request.body.decode())
except Exception:
return http.JsonResponse({"code": RETCODE.PARAMERR, "errmsg": "参数格式错误"})
address_id = data.get("address_id")
pay_method = data.get("pay_method")
if not all([address_id, pay_method]):
return http.JsonResponse({"code": RETCODE.PARAMERR, "errmsg": "参数不全"})
# 1) 地址校验
try:
address = Address.objects.get(pk=address_id, user=user, is_deleted=False)
except Address.DoesNotExist:
return http.JsonResponse({"code": RETCODE.NODATAERR, "errmsg": "地址不存在"})
if pay_method not in (
OrderInfo.PAY_METHODS_ENUM["CASH"],
OrderInfo.PAY_METHODS_ENUM["ALIPAY"],
):
return http.JsonResponse({"code": RETCODE.PARAMERR, "errmsg": "支付方式错误"})
# 2) 生成订单号
order_id = timezone.localtime().strftime("%Y%m%d%H%M%S") + f"{user.id:09d}"
total_count = 0
total_amount = Decimal("0.00")
freight = Decimal("10.00")
if pay_method == OrderInfo.PAY_METHODS_ENUM["CASH"]:
status = OrderInfo.ORDER_STATUS_ENUM["UNSEND"]
else:
status = OrderInfo.ORDER_STATUS_ENUM["UNPAID"]
redis_conn = get_redis_connection("carts")
selected_carts = _get_selected_carts(redis_conn, user.id)
# 必须有选中商品
if not selected_carts:
return http.JsonResponse({"code": RETCODE.PARAMERR, "errmsg": "没有勾选商品,无法下单"})
cart_key = f"carts_{user.id}"
selected_key = f"selected_{user.id}"
with transaction.atomic():
save_id = transaction.savepoint()
try:
# 3) 创建订单主表
order = OrderInfo.objects.create(
order_id=order_id,
user=user,
address=address,
total_count=0,
total_amount=Decimal("0.00"),
freight=freight,
pay_method=pay_method,
status=status,
)
# 4) 遍历选中商品
for sku_id, count in selected_carts.items():
sku = SKU.objects.select_for_update().get(id=sku_id)
if sku.stock < count:
transaction.savepoint_rollback(save_id)
return http.JsonResponse({"code": RETCODE.STOCKERR, "errmsg": "库存不足"})
old_stock = sku.stock
new_stock = old_stock - count
new_sales = sku.sales + count
result = SKU.objects.filter(id=sku_id, stock=old_stock).update(
stock=new_stock,
sales=new_sales,
)
if result == 0:
transaction.savepoint_rollback(save_id)
return http.JsonResponse({"code": RETCODE.STOCKERR, "errmsg": "下单失败,请重试"})
OrderGoods.objects.create(
order=order,
sku=sku,
count=count,
price=sku.price,
)
total_count += count
total_amount += sku.price * count
# 兜底:金额必须 > 0
if total_amount <= 0:
transaction.savepoint_rollback(save_id)
return http.JsonResponse({"code": RETCODE.PARAMERR, "errmsg": "订单金额异常"})
# 5) 回写订单金额(⚠️ 不要写 pay_amount)
order.total_count = total_count
order.total_amount = total_amount
order.freight = freight
order.save(update_fields=[
"total_count",
"total_amount",
"freight",
"update_time",
])
# 6) 清理 Redis
pl = redis_conn.pipeline()
for sku_id in selected_carts.keys():
pl.hdel(cart_key, str(sku_id))
pl.srem(selected_key, str(sku_id))
pl.srem(selected_key, str(sku_id).encode())
pl.execute()
transaction.savepoint_commit(save_id)
except SKU.DoesNotExist:
transaction.savepoint_rollback(save_id)
return http.JsonResponse({"code": RETCODE.NODATAERR, "errmsg": "商品不存在"})
except Exception as e:
print("🔥 ORDER ERROR:", e)
transaction.savepoint_rollback(save_id)
return http.JsonResponse({"code": RETCODE.DBERR, "errmsg": "下单异常"})
return http.JsonResponse({
"code": RETCODE.OK,
"errmsg": "ok",
"order_id": order_id,
"payment_amount": str(total_amount + freight),
"pay_method": pay_method,
})
class OrderSuccessView(LoginRequiredMixin, View):
"""
订单成功页
GET /orders/success/
"""
def get(self, request):
order_id = request.GET.get("order_id")
pay_method = request.GET.get("pay_method")
if not order_id:
return http.HttpResponseBadRequest("缺少订单号")
try:
order = OrderInfo.objects.get(order_id=order_id, user=request.user)
except OrderInfo.DoesNotExist:
return http.HttpResponseBadRequest("订单不存在")
# ✅ 以数据库为准
payment_amount = str(order.total_amount + order.freight)
context = {
"order_id": order_id,
"payment_amount": payment_amount,
"pay_method": pay_method,
}
return render(request, "order_success.html", context)
class PaymentURLView(LoginRequiredMixin, View):
def get(self, request, order_id):
print("🔥 ENTER PaymentURLView", order_id)
user = request.user
try:
order = OrderInfo.objects.get(
order_id=order_id,
user=user,
pay_method=OrderInfo.PAY_METHODS_ENUM["ALIPAY"],
status=OrderInfo.ORDER_STATUS_ENUM["UNPAID"],
)
except OrderInfo.DoesNotExist:
return http.JsonResponse({"code": RETCODE.NODATAERR, "errmsg": "订单不存在或不可支付"})
try:
alipay = get_alipay_client()
print("🔥 GOT alipay client")
order_string = alipay.api_alipay_trade_page_pay(
out_trade_no=order.order_id,
total_amount=str(order.total_amount + order.freight),
subject=f"美多商城订单{order.order_id}",
return_url=settings.ALIPAY_RETURN_URL,
notify_url=settings.ALIPAY_NOTIFY_URL,
)
print("🔥 ORDER STRING =", order_string[:120])
pay_url = f"{settings.ALIPAY_GATEWAY}?{order_string}"
return http.JsonResponse({"code": RETCODE.OK, "errmsg": "ok", "pay_url": pay_url})
except Exception as e:
print("🔥 PAYMENT ERROR:", type(e), e)
import traceback
traceback.print_exc()
return http.JsonResponse({"code": RETCODE.DBERR, "errmsg": "生成支付链接失败"})
class PaymentView(LoginRequiredMixin, View):
"""
发起支付宝支付(你前端如果用的是 /orders/payment/<order_id>/ 也可以走这个)
GET /orders/payment/<order_id>/
"""
def get(self, request, order_id):
user = request.user
order = OrderInfo.objects.filter(
order_id=order_id,
user=user,
status=OrderInfo.ORDER_STATUS_ENUM["UNPAID"],
).first()
if not order:
return JsonResponse({"code": RETCODE.NODATAERR, "errmsg": "订单不存在或状态错误"})
alipay = get_alipay_client()
subject = f"美多商城订单 {order.order_id}"
alipay_url = alipay.api_alipay_trade_page_pay(
out_trade_no=order.order_id,
total_amount=str(order.total_amount + order.freight),
subject=subject,
return_url=settings.ALIPAY_RETURN_URL,
notify_url=settings.ALIPAY_NOTIFY_URL,
)
pay_url = settings.ALIPAY_GATEWAY + "?" + alipay_url
return JsonResponse({
"code": RETCODE.OK,
"errmsg": "ok",
"alipay_url": pay_url,
})
class AlipayReturnView(LoginRequiredMixin, View):
"""
同步回调:用户支付后浏览器跳回(不可信,只做展示)
GET /orders/alipay/return/?out_trade_no=xxx&trade_no=xxx&sign=xxx...
"""
def get(self, request):
data = request.GET.dict()
sign = data.pop("sign", None)
alipay = get_alipay_client()
success = alipay.verify(data, sign)
if success:
return render(request, "pay_success.html", {"data": data})
return render(request, "pay_fail.html", {"data": data})
class AlipayNotifyView(View):
"""
异步通知:支付宝服务器回调(可信,需要验签)
POST /orders/alipay/notify/
"""
def post(self, request):
data = request.POST.dict()
sign = data.pop("sign", None)
alipay = get_alipay_client()
success = alipay.verify(data, sign)
if not success:
return http.HttpResponse("fail")
out_trade_no = data.get("out_trade_no")
trade_no = data.get("trade_no")
trade_status = data.get("trade_status")
total_amount = data.get("total_amount")
if trade_status not in ("TRADE_SUCCESS", "TRADE_FINISHED"):
return http.HttpResponse("success")
try:
order = OrderInfo.objects.get(order_id=out_trade_no)
except OrderInfo.DoesNotExist:
return http.HttpResponse("fail")
Payment.objects.update_or_create(
order=order,
defaults={
"trade_id": trade_no,
"total_amount": Decimal(total_amount),
}
)
if order.status == OrderInfo.ORDER_STATUS_ENUM["UNPAID"]:
order.status = OrderInfo.ORDER_STATUS_ENUM["UNSEND"]
order.save(update_fields=["status", "update_time"])
return http.HttpResponse("success")
前端代码
目标:
- 用后端传来的
order_id / payment_amount / pay_method
动态展示订单信息 - 如果是 支付宝 (pay_method == 2)
→ 点击「确认支付」时:- 请求:
GET /orders/payment/<order_id>/ - 拿到
pay_url location.href = pay_url
- 请求:
- 如果是 货到付款 (pay_method == 1)
→ 直接提示成功,不跳支付宝
直接替换:templates/order_success.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>美多商城-订单提交成功</title>
<link rel="stylesheet" type="text/css" href="{{ static('css/reset.css') }}">
<link rel="stylesheet" type="text/css" href="{{ static('css/main.css') }}">
<script type="text/javascript" src="{{ static('js/host.js') }}"></script>
<script type="text/javascript" src="{{ static('js/vue-2.5.16.js') }}"></script>
<script type="text/javascript" src="{{ static('js/axios-0.18.0.min.js') }}"></script>
</head>
<body>
<div id="app" v-cloak>
<div class="header_con">
<div class="header">
<div class="welcome fl">欢迎来到美多商城!</div>
<div class="fr">
<div class="login_info fl">
欢迎您:<em>[[ username ]]</em>
</div>
<div class="user_link fl">
<span>|</span>
<a href="{{ url('users:center') }}">用户中心</a>
<span>|</span>
<a href="{{ url('carts:carts') }}">我的购物车</a>
<span>|</span>
<a href="/users/center/orders/">我的订单</a>
</div>
</div>
</div>
</div>
<div class="search_bar clearfix">
<a href="{{ url('contents:index') }}" class="logo fl">
<img src="{{ static('images/logo.png') }}">
</a>
<div class="search_wrap fl">
<form method="get" action="/search/" class="search_con">
<input type="text" class="input_text fl" name="q" placeholder="搜索商品">
<input type="submit" class="input_btn fr" value="搜索">
</form>
</div>
</div>
<div class="common_list_con clearfix">
<div class="order_success">
<p>
<b>
订单提交成功,订单总价
<em>¥[[ payment_amount ]]</em>
</b>
</p>
<p>
您的订单已成功生成,订单号:
<span>[[ order_id ]]</span>
</p>
<p>
<a href="/users/center/orders/">
您可以在【用户中心】->【我的订单】查看该订单
</a>
</p>
</div>
</div>
<div class="order_submit clearfix">
<!-- 支付宝 -->
<a v-if="pay_method == 2"
href="javascript:;"
@click="on_pay">
确认支付
</a>
<!-- 货到付款 -->
<a v-else
href="/users/center/orders/">
确认完成
</a>
</div>
<div class="footer">
<div class="foot_link">
<a href="#">关于我们</a>
<span>|</span>
<a href="#">联系我们</a>
<span>|</span>
<a href="#">招聘人才</a>
<span>|</span>
<a href="#">友情链接</a>
</div>
<p>CopyRight © 2016 北京美多商业股份有限公司 All Rights Reserved</p>
<p>电话:010-****888 京ICP备*******8号</p>
</div>
</div>
<script>
var order_id = "{{ order_id }}";
var payment_amount = "{{ payment_amount }}";
var pay_method = "{{ pay_method }}";
</script>
<script type="text/javascript" src="{{ static('js/common.js') }}"></script>
<script type="text/javascript" src="{{ static('js/order_success.js') }}"></script>
</body>
</html>
添加:static/js/order_success.js
var vm = new Vue({
el: '#app',
// 修改 Vue 变量的读取语法,避免和 django 模板语法冲突
delimiters: ['[[', ']]'],
data: {
host: host,
order_id: '',
payment_amount: '',
pay_method: 1,
paying: false
},
mounted() {
// 这些变量由 order_success.html 里的 <script> 注入
this.order_id = order_id;
this.payment_amount = payment_amount;
this.pay_method = parseInt(pay_method);
},
methods: {
// =========================
// 发起支付
// =========================
on_pay() {
if (this.paying) {
return;
}
this.paying = true;
var url = this.host + '/orders/payment/' + this.order_id + '/';
axios.get(url, {
responseType: 'json'
})
.then(response => {
this.paying = false;
if (response.data.code === '0') {
// ✅ 跳转到支付宝
location.href = response.data.pay_url;
} else if (response.data.code === '4101') {
// 未登录,带回当前订单
location.href = '/login/?next=/orders/success/?order_id=' + this.order_id;
} else {
console.log(response.data);
alert(response.data.errmsg);
}
})
.catch(error => {
this.paying = false;
console.log(error.response);
alert('获取支付链接失败,请稍后重试');
});
}
}
});
static/js/place_order.js
var vm = new Vue({
el: '#app',
// 修改Vue变量的读取语法,避免和django模板语法冲突
delimiters: ['[[', ']]'],
data: {
host: host,
order_submitting: false, // 正在提交订单标志
pay_method: 2, // 支付方式,默认支付宝支付
nowsite: '', // 默认地址
payment_amount: '',
},
mounted(){
// 初始化
this.payment_amount = payment_amount;
// 绑定默认地址
this.nowsite = default_address_id;
},
methods: {
// 提交订单
on_order_submit(){
if (!this.nowsite) {
alert('请补充收货地址');
return;
}
if (!this.pay_method) {
alert('请选择付款方式');
return;
}
if (this.order_submitting === false){
this.order_submitting = true;
var url = this.host + '/orders/commit/';
axios.post(url, {
address_id: this.nowsite,
pay_method: this.pay_method
}, {
headers:{
'X-CSRFToken': getCookie('csrftoken')
},
responseType: 'json'
})
.then(response => {
this.order_submitting = false;
if (response.data.code === '0') {
location.href = '/orders/success/?order_id='
+ response.data.order_id
+ '&payment_amount=' + response.data.payment_amount
+ '&pay_method=' + response.data.pay_method;
} else if (response.data.code === '4101') {
location.href = '/login/?next=/orders/settlement/';
} else {
alert(response.data.errmsg);
}
})
.catch(error => {
this.order_submitting = false;
console.log(error.response);
alert('提交订单失败,请稍后重试');
})
}
}
}
});
templates/order_success.html:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>美多商城-订单提交成功</title>
<link rel="stylesheet" type="text/css" href="{{ static('css/reset.css') }}">
<link rel="stylesheet" type="text/css" href="{{ static('css/main.css') }}">
<script type="text/javascript" src="{{ static('js/host.js') }}"></script>
<script type="text/javascript" src="{{ static('js/vue-2.5.16.js') }}"></script>
<script type="text/javascript" src="{{ static('js/axios-0.18.0.min.js') }}"></script>
</head>
<body>
<div id="app" v-cloak>
<div class="header_con">
<div class="header">
<div class="welcome fl">欢迎来到美多商城!</div>
<div class="fr">
<div class="login_info fl">
欢迎您:<em>[[ username ]]</em>
</div>
<div class="user_link fl">
<span>|</span>
<a href="{{ url('users:center') }}">用户中心</a>
<span>|</span>
<a href="{{ url('carts:carts') }}">我的购物车</a>
<span>|</span>
<a href="/users/center/orders/">我的订单</a>
</div>
</div>
</div>
</div>
<div class="search_bar clearfix">
<a href="{{ url('contents:index') }}" class="logo fl">
<img src="{{ static('images/logo.png') }}">
</a>
<div class="search_wrap fl">
<form method="get" action="/search/" class="search_con">
<input type="text" class="input_text fl" name="q" placeholder="搜索商品">
<input type="submit" class="input_btn fr" value="搜索">
</form>
</div>
</div>
<div class="common_list_con clearfix">
<div class="order_success">
<p>
<b>
订单提交成功,订单总价
<em>¥[[ payment_amount ]]</em>
</b>
</p>
<p>
您的订单已成功生成,订单号:
<span>[[ order_id ]]</span>
</p>
<p>
<a href="/users/center/orders/">
您可以在【用户中心】->【我的订单】查看该订单
</a>
</p>
</div>
</div>
<div class="order_submit clearfix">
<!-- 支付宝 -->
<a v-if="pay_method == 2"
href="javascript:;"
@click="on_pay">
确认支付
</a>
<!-- 货到付款 -->
<a v-else
href="/users/center/orders/">
确认完成
</a>
</div>
<div class="footer">
<div class="foot_link">
<a href="#">关于我们</a>
<span>|</span>
<a href="#">联系我们</a>
<span>|</span>
<a href="#">招聘人才</a>
<span>|</span>
<a href="#">友情链接</a>
</div>
<p>CopyRight © 2016 北京美多商业股份有限公司 All Rights Reserved</p>
<p>电话:010-****888 京ICP备*******8号</p>
</div>
</div>
<script>
var order_id = "{{ order_id }}";
var payment_amount = "{{ payment_amount }}";
var pay_method = "{{ pay_method }}";
</script>
<script type="text/javascript" src="{{ static('js/common.js') }}"></script>
<script type="text/javascript" src="{{ static('js/order_success.js') }}"></script>
</body>
</html>
templates/place_order.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>美多商城-订单确认</title>
<link rel="stylesheet" type="text/css" href="/static/css/reset.css">
<link rel="stylesheet" type="text/css" href="/static/css/main.css">
<script type="text/javascript" src="/static/js/host.js"></script>
<script type="text/javascript" src="/static/js/vue-2.5.16.js"></script>
<script type="text/javascript" src="/static/js/axios-0.18.0.min.js"></script>
</head>
<body>
<div id="app" v-cloak>
<div class="header_con">
<div class="header">
<div class="welcome fl">欢迎来到美多商城!</div>
<div class="fr">
<div class="login_info fl">
欢迎您:<em>{{ request.user.username }}</em>
</div>
<div class="user_link fl">
<span>|</span>
<a href="/users/center/">用户中心</a>
<span>|</span>
<a href="/carts/">我的购物车</a>
<span>|</span>
<a href="/users/center/orders/">我的订单</a>
</div>
</div>
</div>
</div>
<div class="search_bar clearfix">
<a href="/" class="logo fl"><img src="/static/images/logo.png"></a>
<div class="search_wrap fl">
<form method="get" action="/search/" class="search_con">
<input type="text" class="input_text fl" name="q" placeholder="搜索商品">
<input type="submit" class="input_btn fr" value="搜索">
</form>
</div>
</div>
<h3 class="common_title">确认收货地址</h3>
<div class="common_list_con clearfix" id="get_site">
<dl>
<dt>寄送到:</dt>
{% for address in addresses %}
<dd>
<input type="radio"
name="address_id"
value="{{ address.id }}"
{% if address.id == request.user.default_address_id %}checked{% endif %}
v-model="nowsite">
{{ address.province.name }} {{ address.city.name }} {{ address.district.name }}
{{ address.place }} ({{ address.receiver }} 收)
{{ address.mobile[:3] }}****{{ address.mobile[7:] }}
</dd>
{% else %}
<dd>暂无收货地址,请先添加地址</dd>
{% endfor %}
</dl>
<a href="/users/addresses/" class="edit_site">编辑收货地址</a>
</div>
<h3 class="common_title">支付方式</h3>
<div class="common_list_con clearfix">
<div class="pay_style_con clearfix">
<input type="radio" name="pay_method" value="1" v-model="pay_method">
<label class="cash">货到付款</label>
<input type="radio" name="pay_method" value="2" v-model="pay_method">
<label class="zhifubao"></label>
</div>
</div>
<h3 class="common_title">商品列表</h3>
<div class="common_list_con clearfix">
<ul class="goods_list_th clearfix">
<li class="col01">商品名称</li>
<li class="col02">商品单位</li>
<li class="col03">商品价格</li>
<li class="col04">数量</li>
<li class="col05">小计</li>
</ul>
{% for sku in skus %}
<ul class="goods_list_td clearfix">
<li class="col01">{{ loop.index }}</li>
<li class="col02">
<img src="{{ sku.default_image.url if sku.default_image else '' }}" alt="{{ sku.name }}">
</li>
<li class="col03">{{ sku.name }}</li>
<li class="col04">台</li>
<li class="col05">{{ sku.price }}元</li>
<li class="col06">{{ sku.count }}</li>
<li class="col07">{{ sku.amount }}元</li>
</ul>
{% endfor %}
</div>
<h3 class="common_title">总金额结算</h3>
<div class="common_list_con clearfix">
<div class="settle_con">
<div class="total_goods_count">
共<em>{{ total_count }}</em>件商品,总金额<b>{{ total_amount }}元</b>
</div>
<div class="transit">运费:<b>{{ freight }}元</b></div>
<div class="total_pay">实付款:<b>{{ payment_amount }}元</b></div>
</div>
</div>
<div class="order_submit clearfix">
<a href="javascript:;" id="order_btn" @click="on_order_submit">提交订单</a>
</div>
<div class="footer">
<div class="foot_link">
<a href="#">关于我们</a>
<span>|</span>
<a href="#">联系我们</a>
<span>|</span>
<a href="#">招聘人才</a>
<span>|</span>
<a href="#">友情链接</a>
</div>
<p>CopyRight © 2016 北京美多商业股份有限公司 All Rights Reserved</p>
<p>电话:010-****888 京ICP备*******8号</p>
</div>
</div>
<script>
var payment_amount = "{{ payment_amount }}";
var default_address_id = "{{ request.user.default_address_id or '' }}";
</script>
<script type="text/javascript" src="/static/js/host.js"></script>
<script type="text/javascript" src="/static/js/common.js"></script>
<script type="text/javascript" src="/static/js/place_order.js"></script>
</body>
</html>
templates/cart.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>美多商城-购物车</title>
<link rel="stylesheet" type="text/css" href="{{ static('css/reset.css') }}">
<link rel="stylesheet" type="text/css" href="{{ static('css/main.css') }}">
<script type="text/javascript" src="{{ static('js/host.js') }}"></script>
<script type="text/javascript" src="{{ static('js/vue-2.5.16.js') }}"></script>
<script type="text/javascript" src="{{ static('js/axios-0.18.0.min.js') }}"></script>
</head>
<body>
<div id="app" v-cloak>
<!-- 顶部 -->
<div class="header_con">
<div class="header">
<div class="welcome fl">欢迎来到美多商城!</div>
<div class="fr">
<div class="login_btn fl" v-if="username">
欢迎您:<em>[[ username ]]</em>
<span>|</span>
<a href="{{ url('users:logout') }}">退出</a>
</div>
<div class="login_btn fl" v-else>
<a href="{{ url('users:login') }}">登录</a>
<span>|</span>
<a href="{{ url('users:register') }}">注册</a>
</div>
<div class="user_link fl">
<span>|</span>
<a href="{{ url('users:center') }}">用户中心</a>
<span>|</span>
<a href="{{ url('carts:carts') }}">我的购物车</a>
</div>
</div>
</div>
</div>
<!-- 搜索 -->
<div class="search_bar clearfix">
<a href="{{ url('contents:index') }}" class="logo fl">
<img src="{{ static('images/logo.png') }}">
</a>
<div class="search_wrap fl">
<form method="get" action="/search/" class="search_con">
<input type="text" class="input_text fl" name="q" placeholder="搜索商品">
<input type="submit" class="input_btn fr" value="搜索">
</form>
</div>
</div>
<!-- 总数量 -->
<div class="total_count">全部商品<em>[[ total_count ]]</em>件</div>
<!-- 表头 -->
<ul class="cart_list_th clearfix">
<li class="col01">选择</li>
<li class="col02">商品图片</li>
<li class="col03">商品名称</li>
<li class="col04">商品单位</li>
<li class="col05">商品价格</li>
<li class="col06">数量</li>
<li class="col07">小计</li>
<li class="col08">操作</li>
</ul>
<!-- 商品列表 -->
<ul class="cart_list_td clearfix" v-for="(cart,index) in carts" :key="cart.id">
<li class="col01">
<input type="checkbox" v-model="cart.selected" @change="update_selected(index)">
</li>
<li class="col02">
<img :src="cart.default_image_url">
</li>
<li class="col03">[[ cart.name ]]</li>
<!-- ✅ 新增:单位列(你没有单位字段就写固定值) -->
<li class="col04">部</li>
<li class="col05">[[ cart.price ]] 元</li>
<li class="col06">
<div class="num_add">
<a @click="on_minus(index)" class="minus fl">-</a>
<input v-model="cart.count" @blur="on_input(index)" type="text" class="num_show fl">
<a @click="on_add(index)" class="add fl">+</a>
</div>
</li>
<li class="col07">[[ cart.amount ]] 元</li>
<li class="col08">
<a @click="on_delete(index)">删除</a>
</li>
</ul>
<!-- 结算 -->
<ul class="settlements">
<li class="col01">
<input type="checkbox" v-model="selected_all" @change="on_selected_all">
</li>
<li class="col02">全选</li>
<li class="col03">
合计:¥ <em>[[ total_selected_amount ]]</em><br>
共 <b>[[ total_selected_count ]]</b> 件商品
</li>
<li class="col04">
<a href="javascript:;" @click="on_settlement">去结算</a>
</li>
</ul>
</div>
<script>
var cart_skus = {{ cart_skus|safe }};
</script>
<script type="text/javascript" src="{{ static('js/common.js') }}"></script>
<script type="text/javascript" src="{{ static('js/cart.js') }}"></script>
</body>
</html>
static/js/cart.js
var vm = new Vue({
el: '#app',
// 修改 Vue 变量读取语法,避免和 Django 模板冲突
delimiters: ['[[', ']]'],
data: {
host,
carts: [],
total_count: 0,
total_selected_count: 0,
total_selected_amount: 0,
carts_tmp: [],
username: '',
},
computed: {
// 是否全选
selected_all() {
var selected = true;
for (var i = 0; i < this.carts.length; i++) {
if (this.carts[i].selected === false) {
selected = false;
break;
}
}
return selected;
},
},
mounted() {
// 初始化购物车数据
this.render_carts();
// 计算总数量
this.compute_total_count();
// 计算选中商品数量和金额
this.compute_total_selected_amount_count();
// 获取用户名
this.username = getCookie('username');
},
methods: {
// 初始化购物车数据
render_carts() {
// 深拷贝,避免引用问题
this.carts = JSON.parse(JSON.stringify(cart_skus));
// 兼容后端返回 bool / 字符串
for (var i = 0; i < this.carts.length; i++) {
if (typeof this.carts[i].selected === 'string') {
this.carts[i].selected = this.carts[i].selected === 'True';
} else {
this.carts[i].selected = Boolean(this.carts[i].selected);
}
}
// 保存一份初始快照(用于失败回滚)
this.carts_tmp = JSON.parse(JSON.stringify(this.carts));
},
// 计算商品总数量(不论是否选中)
compute_total_count() {
var total_count = 0;
for (var i = 0; i < this.carts.length; i++) {
total_count += parseInt(this.carts[i].count);
}
this.total_count = total_count;
},
// 计算选中商品的数量和总金额
compute_total_selected_amount_count() {
var amount = 0;
var total_count = 0;
for (var i = 0; i < this.carts.length; i++) {
if (this.carts[i].selected) {
amount += parseFloat(this.carts[i].price) * parseInt(this.carts[i].count);
total_count += parseInt(this.carts[i].count);
}
}
this.total_selected_amount = amount.toFixed(2);
this.total_selected_count = total_count;
},
// 减少数量
on_minus(index) {
if (this.carts[index].count > 1) {
this.update_count(index, this.carts[index].count - 1);
}
},
// 增加数量
on_add(index) {
var count = this.carts[index].count + 1;
if (count > 5) {
count = 5;
alert('超过商品数量上限');
}
this.update_count(index, count);
},
// 输入数量
on_input(index) {
var count = parseInt(this.carts[index].count);
if (isNaN(count) || count <= 0) {
count = 1;
} else if (count > 5) {
count = 5;
alert('超过商品数量上限');
}
this.update_count(index, count);
},
// =========================
// 更新购物车数量(只改数量)
// =========================
update_count(index, count) {
var url = this.host + '/carts/';
axios.put(url, {
sku_id: this.carts[index].id,
count: count
}, {
headers: {
'X-CSRFToken': getCookie('csrftoken')
},
withCredentials: true,
responseType: 'json'
}).then(response => {
if (response.data.code === '0') {
// 用后端返回的数据替换当前项
Vue.set(this.carts, index, response.data.cart_sku);
// 重新计算
this.compute_total_selected_amount_count();
this.compute_total_count();
// 更新快照(深拷贝)
this.carts_tmp = JSON.parse(JSON.stringify(this.carts));
} else {
alert(response.data.errmsg);
this.carts[index] = JSON.parse(JSON.stringify(this.carts_tmp[index]));
}
}).catch(() => {
// 回滚
this.carts[index] = JSON.parse(JSON.stringify(this.carts_tmp[index]));
});
},
// =========================
// 更新选中状态(只改 selected)
// =========================
update_selected(index) {
var url = this.host + '/carts/selection/';
axios.put(url, {
sku_id: this.carts[index].id,
selected: this.carts[index].selected
}, {
headers: {
'X-CSRFToken': getCookie('csrftoken')
},
withCredentials: true,
responseType: 'json'
}).then(response => {
if (response.data.code === '0') {
this.carts[index].selected = response.data.cart_sku.selected;
this.compute_total_selected_amount_count();
this.compute_total_count();
} else {
alert(response.data.errmsg);
}
});
},
// 删除商品
on_delete(index) {
var url = this.host + '/carts/';
axios.delete(url, {
data: { sku_id: this.carts[index].id },
headers: {
'X-CSRFToken': getCookie('csrftoken')
},
withCredentials: true,
responseType: 'json'
}).then(response => {
if (response.data.code === '0') {
this.carts.splice(index, 1);
this.compute_total_selected_amount_count();
this.compute_total_count();
} else {
alert(response.data.errmsg);
}
});
},
// =========================
// 全选 / 取消全选
// =========================
on_selected_all() {
var selected = !this.selected_all;
axios.put(this.host + '/carts/selection/', {
selected: selected
}, {
headers: {
'X-CSRFToken': getCookie('csrftoken')
},
withCredentials: true,
responseType: 'json'
}).then(response => {
if (response.data.code === '0') {
for (var i = 0; i < this.carts.length; i++) {
this.carts[i].selected = selected;
}
this.compute_total_selected_amount_count();
this.compute_total_count();
} else {
alert(response.data.errmsg);
}
});
},
// =========================
// 去结算
// =========================
on_settlement() {
if (this.total_selected_count === 0) {
alert('请先勾选要结算的商品');
return;
}
// 🧠 兜底同步所有 selected 状态到后端
var requests = [];
for (var i = 0; i < this.carts.length; i++) {
requests.push(
axios.put(this.host + '/carts/selection/', {
sku_id: this.carts[i].id,
selected: this.carts[i].selected
}, {
headers: {
'X-CSRFToken': getCookie('csrftoken')
},
withCredentials: true,
responseType: 'json'
})
);
}
Promise.all(requests).then(() => {
// 所有选中状态都已同步到 Redis
location.href = '/orders/settlement/';
}).catch(() => {
alert('同步购物车状态失败,请重试');
});
}
}
});
测试
登录用户后:
http://127.0.0.1:8000/carts/

提交订单



发布者:LJH,转发请注明出处:https://www.ljh.cool/44784.html