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')
views.py
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())

]
urls.py

支付宝配置详情

使用的技术:
1.DRF
2.事务,乐观锁
3.F查询, 聚合查询
4.支付宝
5.celery 监测订单在规定时间内是否支付,若没支付,数据回滚,标记为''订单
(celery这里没使用,详情看celcey笔记)

 

 

 

 

 

 

posted @ 2019-11-21 20:09  waller  阅读(387)  评论(0)    收藏  举报