Mishop - 购物模块
逻辑
-
立即购买
场景: 用户点击立即购买 -> 跳转到支付页面 ->支付
-
加入购物车再购买
场景: 用户点击加入购物车 -> 点击查看购物车 -> 点击立即支付 -> 支付
前端记录用户加入购物车的产品,当用户查看购物车时,将已经插入到购物车的商品展示出来,当用户点击立即支付,前端将商品信息发送到后端
1.前端将商品ID,数量,用户token发送到后端 2.后端在功能视图类里先校验用户是否登录 3.生成唯一的订单编号,在购物车表中创建商品信息,这些商品用同一个订单编号 4.在总订单表中创建同一订单编号的总订单信息,并标记未支付 4.将该订单编号的商品们从购物车表中查出返回到前端的支付页面 5.用户支付(支付宝) 6.支付成功,后端判断支付宝返回的
数据文档
-
前端
前端传输商品信息数据格式: goods_dic = { goods_id : goods_num, ... }
-
后端
...
代码
-
表设计
from django.db import models from user.models import User # 导入用户表 from goods.models import Goods # 导入商品表
总订单 class Order(models.Model): order_id = models.CharField(max_length=50, unique=True, primary_key=True, verbose_name='订单编号') is_pay_choices = ((0, '未支付'),(1, '已支付'),(2, '已取消'),(3, '超时取消'),) is_pay = models.CharField(choices=is_pay_choices, default=0, max_length=64 , verbose_name='订单状态') pay_choices = ((1, '支付宝'),(2, '微信支付')) pay_type = models.CharField(choices=pay_choices, default=1, max_length=64, verbose_name='支付方式') ship_choices = ((0, '未发货'), (1, '已发货')) ship = models.CharField(choices=ship_choices, default=0, max_length=64, verbose_name='是否发货') user = models.ForeignKey(User, db_constraint=False, on_delete=models.CASCADE) goods_nums = models.IntegerField(default=0, verbose_name='商品总数量') price_nums = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name='订单金额') consignee_name = models.CharField(max_length=200, default=0, verbose_name='收货人') consignee_phone = models.CharField(max_length=200, default=0, verbose_name='收货人电话') consignee_pro = models.CharField(max_length=200, default=0, verbose_name='收货人省') consignee_city = models.CharField(max_length=200, default=0, verbose_name='收货人市') consignee_addr = models.CharField(max_length=200, default=0, verbose_name='收货人地址') creat_time = models.DateTimeField(auto_now_add=True) update_time = models.DateTimeField(auto_now=True) class Meta: db_table = "mishop_order" verbose_name = "总订单表" verbose_name_plural = "总订单表" def __str__(self): return self.user_id 购物车订单 class OrderCar(models.Model): order = models.ForeignKey(Order, to_field='order_id', db_constraint=False, on_delete=models.CASCADE) # 订单编号外键字段 goods = models.ForeignKey(Goods, db_constraint=False,on_delete=models.DO_NOTHING) # 商品关联外键字段 user = models.ForeignKey(User, db_constraint=False,on_delete=models.DO_NOTHING) # 用户外键字段 goods_name = models.CharField(max_length=200, verbose_name='商品名') goods_price = models.DecimalField(max_digits=10, decimal_places=2,default=0, verbose_name='商品单价') goods_num = models.IntegerField(default=0, verbose_name='商品数量') goods_price_num = models.DecimalField(max_digits=10, decimal_places=2,default=0, verbose_name='单品总价') # is_order = models.CharField(max_length=64, default=0, verbose_name='是否创总单') creat_time = models.DateTimeField(auto_now_add=True) update_time = models.DateTimeField(auto_now=True) # 自定义序列化字段 # 订单总金额 def price_nums(self): return self.order.price_nums # 订单商品总数量 def goods_nums(self): return self.order.goods_nums # 地址信息页要自定义字段,返回到前端 class Meta: db_table = "mishop_ordercar" verbose_name = "购物车表" verbose_name_plural = "购物车表"
-
后端实现

from rest_framework.views import APIView from utils.response import APIResponse from rest_framework_jwt.authentication import JSONWebTokenAuthentication from rest_framework.permissions import IsAuthenticated from goods.models import Goods, Stock from .models import OrderCar, Order import time ,random from django.db import transaction from django.db.models import F from .serializers import OrderModelSerializer from django.db.models import Sum # 生成订单编号 def get_order_id(): st="012345679qwertyui" order_id=str(time.strftime("%Y%m%d%h%M%S"))+"".join(random.sample(st,5)) return order_id # 下单接口 class PlaceOrderAPIView(APIView): # 前端将购物车内的商品们的信息打包成一个字典: goods_dict = { goods_id : goods_num , ...} @transaction.atomic def post(self, request, *args, **kwargs): user_id = request.user.pk goods_dict = request.data.get('goods_dict') if not goods_dict: return APIResponse(1, '数据为空') goods_id_list = goods_dict.keys() # 拿到商品ID的元组 goods_obj_list = Goods.objects.filter(pk__in=goods_id_list) # 生成总订单表的订单编号 order_id_num = get_order_id() # 定义一个准备保存到购物车表的数据字典 ordercar_dict = {} ordercar_dict['user'] = user_id # 用户ID ordercar_dict['order'] = order_id_num # 订单编号 for goods_obj in goods_obj_list: ordercar_dict['goods_id'] = goods_obj.pk # 商品ID ordercar_dict['goods_name'] = goods_obj.name # 商品名 ordercar_dict['goods_price'] = goods_obj.price # 商品单价 ordercar_dict['goods_num'] = goods_dict.get('goods_id') # 商品购买数量 ordercar_dict['goods_price_num'] = goods_obj.price * goods_dict.get('goods_id') # 单品总价 # 开启事务 sid = transaction.savepoint() for i in range(3): # 判断库存数量是否大于购买数量,若大于,库存数量=原库存-购买量,开启乐观锁,改变库存数量,小于则回滚数据 old_scort_num = goods_obj.stock.quantity # 当前库存量 new_goods_num = old_scort_num - goods_dict.get('goods_id') if new_goods_num < 0: # 回滚 transaction.savepoint_rollback(sid) return APIResponse(1, '库存数量不够') # 利用乐观锁,来检测成功对库存进行了新修改 res = Stock.objects.filter(stock_id=goods_obj.stock.stock_id, quantity=old_scort_num).update( quantity=new_goods_num) if not res: if i == 2: transaction.savepoint_rollback(sid) return APIResponse(1, '订单创建失败') else:continue # 修改商品表中被购买商品的销量 利用 F 查询,在原销量基础上增加 goods_obj.update(buy_count=F('buy_count') + goods_dict.get('goods_id')) # 创建购物车表记录 OrderCar.objects.create(**ordercar_dict) # 查询该下单用户的默认收货地址 addr_obj_list = request.user.addr_set.all() addr_obj = '' for obj in addr_obj_list: if obj.is_default == True: # 找到默认地址 addr_obj = obj break else: continue phone = addr_obj.phone province = addr_obj.province city = addr_obj.city addr_details = addr_obj.addr_details name = addr_obj.name # 利用聚合查询,查出此次同一订单编号下的商品总数量和总价格 goods_obj_car = OrderCar.objects.filter(order=order_id_num).all() nums = goods_obj_car.aggregate(num=Sum('goods_num')) prices = goods_obj_car.aggregate(price=Sum('goods_price')) # 创建总订单 order_obj = Order.objects.create( order_id=order_id_num, # 订单编号 user=user_id, # 用户ID goods_nums=nums['num'], # 商品总数量 price_nums=prices['price'], # 订单总金额 consignee_name=name, # 收货人 consignee_phone=phone, # 收货人电话 consignee_pro=province, # 省 consignee_city=city, # 市 consignee_addr=addr_details, # 地址详情 ) # 将此次购物车表中的商品们序列化返回到前端,这些单品有同一个订单号 order_obj_data = OrderModelSerializer(instance=goods_obj_car, many=True).data print(order_obj_data) return APIResponse( 0, 'OK', results=order_obj_data, ) # 支付接口 class PayAPIView(APIView): authentication_classes = [JSONWebTokenAuthentication] permission_classes = [IsAuthenticated] def post(self, request, *args, **kwargs): # 获取订单编号, 总价, 支付方式 request_data = request.data order_id = request_data.get('order_id') # 订单编号 price_nums = request_data.get('price_nums') # 订单总价 pay_type = request_data.get('pay_type') # 支付方式 if not (order_id and price_nums and pay_type): return APIResponse(2, '数据有误') # 查询该订单号的总订单记录,修改支付方式 # 这里是支付宝方式支付,正常是需要通过反射的方式,映射不同的支付接口 Order.objects.filter(order_id = order_id).update(pay_type=pay_type) # 生成支付链接,并返回 order_string = alipay.api_alipay_trade_page_pay( out_trade_no=order_id, # 订单号 total_amount=price_nums, # 订单总价 subject=order_id, # 订单标题(这里用的订单编号) return_url=settings.RETURN_URL, notify_url=settings.NOTIFY_URL ) order_url = pay_url + order_string return APIResponse(order_url=order_url) # 支付成功的回调不需要登录认证 - 支付宝回调不会携带jwt-token,但是支付回调参数需要自己做校验 class SuccessAPIView(APIView): # 同步回调 def patch(self, request, *args, **kwargs): # request.query_params是QueryDict类型,不能调用pop方法 request_data = request.query_params.dict() signature = request_data.pop("sign") success = alipay.verify(request_data, signature) if success: # 校验通过 print("通过") # 一般不在该处修改订单状态 return APIResponse() return APIResponse(1, '校验失败') # 支付宝异步回调 def post(self, request, *args, **kwargs): # 默认是QueryDict类型,不能使用pop方法 request_data = request.data.dict() # 必须将 sign、sign_type(内部有安全处理) 从数据中取出,拿sign与剩下的数据进行校验 sign = request_data.pop('sign') result = alipay.verify(request_data, sign) # 异步回调:修改订单状态 if result and request_data["trade_status"] in ("TRADE_SUCCESS", "TRADE_FINISHED"): out_trade_no = request_data.get('out_trade_no') logger.critical('%s支付成功' % out_trade_no) try: order = Order.objects.get(order_id=out_trade_no) if order.is_pay != 1: # 是否是未支付 order.is_pay = 1 # 改为已支付 order.save() return Response('success') # 必须返回success字符串,8次异步回调机制 except: pass return Response('failed')

from django.urls import path from . import views urlpatterns = [ # 下单接口 path('placeorder/', views.PlaceOrderAPIView.as_view()), # 支付接口 - 订单信息换支付链接 path('pay/', views.PayAPIView.as_view()), # 支付成功结果 - 修改订单状态 path('success/', views.SuccessAPIView.as_view()) ]
使用的技术: 1.DRF 2.事务,乐观锁 3.F查询, 聚合查询 4.支付宝 5.celery 监测订单在规定时间内是否支付,若没支付,数据回滚,标记为'死'订单 (celery这里没使用,详情看celcey笔记)