商城——结算中心模块
一、结算中心表结构
编写 LuffyCity/shopping/models.py 文件,设计结算中心表结构。
1、优惠券
该类定义的是优惠券生成规则。总共设计有三种优惠券类型:通用券、满减券、折扣券。
from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey from Course.models import Account # Create your models here. __all__ = ["Coupon", "CouponRecord", "Order", "OrderDetail", "TransactionRecord"] class Coupon(models.Model): """优惠券生成规则""" name = models.CharField(max_length=64, verbose_name="活动名称") brief = models.TextField(blank=True, null=True, verbose_name="优惠券介绍") # 优惠券类型 coupon_type_choices = ((0, '通用券'),(1, '满减劵'),(2, '折扣券')) coupon_type = models.SmallIntegerField(choices=coupon_type_choices, default=0, verbose_name="券类型") # null=True:数据库创建时该字段可不填,用NULL填充 # blank=True:创建数据库记录时该字段可传空白 money_equivalent_value = models.IntegerField(verbose_name="等值货币", null=True, blank=True, default=0) off_percent = models.PositiveSmallIntegerField("折扣百分比", help_text="只针对折扣券,例7.9折,写79", null=True, blank=True, default=100) minimum_consume = models.PositiveIntegerField("最低消费", help_text="仅在满减券时填写此字段", null=True, blank=True, default=0) content_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=None) object_id = models.PositiveIntegerField("绑定课程", blank=True, null=True, help_text="可以把优惠券跟课程绑定") # 不绑定代表全局优惠券 content_object = GenericForeignKey("content_type", "object_id") open_date = models.DateField("优惠券领取开始时间") close_date = models.DateField("优惠券领取结束时间") valid_begin_date = models.DateField(verbose_name="有效期开始时间", blank=True, null=True) valid_end_date = models.DateField(verbose_name="有效结束时间", blank=True, null=True) coupon_valid_days = models.PositiveIntegerField(verbose_name="优惠券有效期(天)", blank=True, null=True, help_text="自券被领时开始算起") date = models.DateTimeField(auto_now_add=True) class Meta: verbose_name_plural = "13. 优惠券生成规则记录" db_table = verbose_name_plural verbose_name = verbose_name_plural def __str__(self): return "%s(%s)" % (self.get_coupon_type_display(), self.name) def save(self, *args, **kwargs): # save前做如下判断 # 如果没有优惠券有效期,也没有优惠券起止时间 if not self.coupon_valid_days or (self.valid_begin_date and self.valid_end_date): # 如果有优惠券起止时间 if self.valid_begin_date and self.valid_end_date: # 如果结束时间早于开始时间,抛出异常 if self.valid_end_date <= self.valid_begin_date: raise ValueError("valid_end_date 有效期结束日期必须晚于 valid_begin_date ") # 如果优惠券有效期为0,抛出异常 if self.coupon_valid_days == 0: raise ValueError("coupon_valid_days 有效期不能为0") # 如果优惠券结束时间早于优惠券开始时间,抛出异常 if self.close_date < self.open_date: raise ValueError("close_date 优惠券领取结束时间必须晚于 open_date优惠券领取开始时间") super(Coupon, self).save(*args, **kwargs)
2、优惠券记录
一个优惠券往往对应多个优惠券记录。
class CouponRecord(models.Model): """优惠券发放、消费记录""" coupon = models.ForeignKey("Coupon", on_delete=None) number = models.CharField(max_length=64, unique=True, verbose_name="用户优惠券记录的流水号") account = models.ForeignKey(to=Account, verbose_name="拥有者", on_delete=None) status_choices = ((0, '未使用'), (1, '已使用'), (2, '已过期')) status = models.SmallIntegerField(choices=status_choices, default=0) get_time = models.DateTimeField(verbose_name="领取时间", help_text="用户领取时间") used_time = models.DateTimeField(verbose_name="使用时间", blank=True, null=True) # 一个订单可以有多张优惠券,因此是一对多关系 order = models.ForeignKey("Order", verbose_name="关联订单", blank=True, null=True, on_delete=None) class Meta: verbose_name_plural = "14. 用户优惠券领取使用记录表" db_table = verbose_name_plural verbose_name = verbose_name_plural def __str__(self): return '%s-%s-%s' % (self.account, self.number, self.status)
3、订单
只要创建订单,订单即存在,与是否完成支付无关。
class Order(models.Model): """订单""" payment_type_choices = ( (0, '微信'), (1, '支付宝'), (2, '优惠码'), (3, '贝里') ) payment_type = models.SmallIntegerField(choices=payment_type_choices) payment_number = models.CharField(max_length=128, verbose_name="支付第三方订单号", null=True, blank=True) # 考虑到订单合并支付的问题 order_number = models.CharField(max_length=128, verbose_name="订单号", unique=True) account = models.ForeignKey(to=Account, on_delete=None) actual_amount = models.FloatField(verbose_name="实付金额") # 注意只要创建了订单,订单即存在,与完成付款无关 status_choices = ( (0, '交易成功'), (1, '待支付'), (2, '退费申请中'), (3, '已退费'), (4, '主动取消'), (5, '超时取消') ) status = models.SmallIntegerField(choices=status_choices, verbose_name="状态") date = models.DateTimeField(auto_now_add=True, verbose_name="订单生成时间") pay_time = models.DateTimeField(blank=True, null=True, verbose_name="付款时间") cancel_time = models.DateTimeField(blank=True, null=True, verbose_name="订单取消时间") class Meta: verbose_name_plural = "15. 订单表" db_table = verbose_name_plural verbose_name = verbose_name_plural def __str__(self): return "%s" % self.order_number
4、订单详情
一个订单可能包含购买的多个商品。需要通过订单详情来展示订单中包含的所有商品。
class OrderDetail(models.Model): """订单详情""" order = models.ForeignKey("Order", on_delete=None) # 关联订单,一个订单可能有多个订单详情 content_type = models.ForeignKey(ContentType, on_delete=None) # 关联普通课程或学位 object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') original_price = models.FloatField("课程原价") price = models.FloatField("折后价格") valid_period_display = models.CharField("有效期显示", max_length=32) # 订单页显示 valid_period = models.PositiveIntegerField("有效期(days)") # 课程有效期 memo = models.CharField(verbose_name="备忘录", max_length=255, blank=True, null=True) class Meta: verbose_name_plural = "16. 订单详细" db_table = verbose_name_plural verbose_name = verbose_name_plural def __str__(self): return "%s - %s -%s" % (self.order, self.content_type, self.price)
5、贝里(积分)交易记录
class TransactionRecord(models.Model): """贝里交易记录""" account = models.ForeignKey(to=Account, on_delete=None) amount = models.IntegerField("金额") balance = models.IntegerField("账户余额") # 交易类型 transaction_type_choices = ( (0, '收入'), (1, '支出'), (2, '退款'), (3, '提现') ) transaction_type = models.SmallIntegerField(choices=transaction_type_choices) transaction_number = models.CharField(verbose_name="流水号", unique=True, max_length=128) date = models.DateTimeField(auto_now_add=True) # 创建交易记录时间 memo = models.CharField(verbose_name="备忘录", max_length=128, blank=True, null=True) class Meta: verbose_name_plural = "17. 贝里交易记录" db_table = verbose_name_plural verbose_name = verbose_name_plural def __str__(self): return "%s" % self.transaction_number
二、加入结算中心接口
1、添加结算路由
在 LuffyCity/shopping/urls.py 添加结算中心路由:
from django.urls import path from .views import ShoppingCarView from .settlement_view import SettlementView urlpatterns = [ path('shopping_car', ShoppingCarView.as_view()), # 购物车 path('settlement', SettlementView.as_view()) # 结算中心 ]
由于view.py已经写了购物车视图,这里添加settlement_view.py作为结算中心视图。
2、redis数据结构
post提交购物车的商品,生成订单,前端传来的数据:couse_list。
设计写入reids的数据如下所示:
redis = { settlement_userid_courseid: { id, 课程id, title, course_img, valid_period_display(有效期), price, course_coupon_dict: { coupon_id: {优惠券信息}, coupon_id2: {优惠券信息}, coupon_id3: {优惠券信息}, } # 默认不给选优惠券,这个字段只有更新的时候添加 default_coupon_id:1 } global_coupon_userid: { coupon_id: {优惠券信息} coupon_id2: {优惠券信息}, coupon_id3: {优惠券信息}, # 这个字段只有更新的时候才添加 default_global_coupon_id: 1 } }
3、添加结算中心接口视图
在结算中心视图settlement_view.py中,添加结算视图类:SettlementView,编写post方法以添加结算中心:
import redis import json from django.utils.timezone import now from rest_framework.views import APIView from rest_framework.response import Response from utils.base_response import BaseResponse from utils.redis_pool import POOL # 连接池 from utils.my_auth import LoginAuth # 登录认证 from .views import SHOPPINGCAR_KEY from .models import CouponRecord CONN = redis.Redis(connection_pool=POOL) SETTLEMENT_KEY = "SETTLEMENT_%s_%s" GLOBAL_COUPON_KEY = "GLOBAL_COUPON_%s" class SettlementView(APIView): authentication_classes = [LoginAuth,] def post(self, request): res = BaseResponse() # 1.获取前端的数据以及user_id course_list = request.data.get("course_list", "") user_id = request.user.pk # 2.校验数据的合法性 for course_id in course_list: # 2.1 判断course_id 是否在购物车中 shopping_car_key = SHOPPINGCAR_KEY % (user_id, course_id) if not CONN.exists(shopping_car_key): res.code = 1050 res.error = "课程ID不合法" return Response(res.dict) # 3.构建数据结构 # 3.1 获取用户所有合法优惠券 user_all_coupons = CouponRecord.objects.filter( account_id = user_id, status = 0, coupon__valid_begin_date__lte = now(), # 开始时间小于现在时间 coupon__valid_end_date__gte = now(), # 结束时间大于现在时间 ).all() # 拿到所有对象 # 3.2 构建两个优惠券的dict course_coupon_dict = {} global_coupon_dict = {} for coupon_record in user_all_coupons: coupon = coupon_record.coupon if coupon.objects_id == course_id: # 说明是这个课程的所有优惠券 course_coupon_dict[coupon.id] = { "id": coupon.id, "name": coupon.name, "coupon_type": coupon.get_coupon_type_display(), # 拿到中文类型 "object_id": coupon.object_id, "money_equivalent_value": coupon.money_equivalent_value, # 等值货币 "off_percent": coupon.off_percent, "minimum_consume": coupon.minimum_consume } elif coupon.object_id == "": # 为空说明是全局优惠券 global_coupon_dict[coupon.id] = { "id": coupon.id, "name": coupon.name, "coupon_type": coupon.get_coupon_type_display(), # 拿到中文类型 "money_equivalent_value": coupon.money_equivalent_value, # 等值货币 "off_percent": coupon.off_percent, "minimum_consume": coupon.minimum_consume } # 3.3 构建将写入redis的数据结构 course_info = CONN.hgetall(shopping_car_key) price_policy_dict = json.loads(course_info["price_policy_dict"]) default_policy_id = course_info["default_price_policy_id"] # 默认价格策略 valid_period = price_policy_dict[default_policy_id]["valid_period_display"] # 有效期 price = price_policy_dict[default_policy_id]["price"] # 价格 settlement_info = { "id": course_id, "title": course_info["title"], "course_img": course_info["course_img"], "valid_period": valid_period, "price": price, "course_coupon_dict": json.dumps(course_coupon_dict, ensure_ascii=False) } # 4.写入redis settlement_key = SETTLEMENT_KEY % (user_id, course_id) global_coupon_key = GLOBAL_COUPON_KEY % user_id CONN.hmset(settlement_key, settlement_info) if global_coupon_dict: CONN.hmset(global_coupon_key, global_coupon_dict) # 5.删除购物车中的数据 CONN.delete(shopping_car_key) res.data = "加入结算中心成功" return Response(res.dict)
4、添加结算测试
(1)加入购物车
首先登录获取token:
两次添加购物车:{"course_id":1, "price_policy_id": 2}、{"course_id":2, "price_policy_id": 3}
查看当前用户所有购物车信息:
(2)加入结算中心
提交结算信息:{"course_list": [1, 2]}
如上所示,添加结算中心成功,此时再查看购物车,可以发现购物车清空:
三、查看结算中心接口
1、查看结算中心接口视图
import redis import json from django.utils.timezone import now from rest_framework.views import APIView from rest_framework.response import Response from utils.base_response import BaseResponse from utils.redis_pool import POOL # 连接池 from utils.my_auth import LoginAuth # 登录认证 from .views import SHOPPINGCAR_KEY from .models import CouponRecord CONN = redis.Redis(connection_pool=POOL) SETTLEMENT_KEY = "SETTLEMENT_%s_%s" GLOBAL_COUPON_KEY = "GLOBAL_COUPON_%s" class SettlementView(APIView): authentication_classes = [LoginAuth,] def post(self, request):... def get(self, request): res = BaseResponse() # 1.获取user_id user_id = request.user.pk # 2.拼接所有key settlement_key = SETTLEMENT_KEY % (user_id, "*") global_coupon_key = GLOBAL_COUPON_KEY % user_id all_keys = CONN.scan_iter(settlement_key) # 增量式迭代获取,redis里匹配的的name # 3.去redis取数据 ret = [] for key in all_keys: ret.append(CONN.hgetall(key)) # 取key对应所有数据 global_coupon_info = CONN.hgetall(global_coupon_key) res.data = { "settlement_info": ret, "global_coupon_dict": global_coupon_info } return Response(res.dict)
2、查看结算中心测试
用户登录后,使用获取的token来查看当前用户结算中心信息:
四、更新结算中心接口
1、更新结算中心接口视图
class SettlementView(APIView): authentication_classes = [LoginAuth,] def post(self, request)... def get(self, request):... def put(self, request): # 更新课程优惠券会传递course_id、course_coupon_id、global_coupon_id res = BaseResponse() # 获取前端传递数据 # 1.获取前端传递过来的数据 course_id = request.data.get("course_id", "") course_coupon_id = request.data.get("course_coupon_id", "") global_coupon_id = request.data.get("global_coupon_id", "") user_id = request.user.pk # 2.校验数据合法性 key = SETTLEMENT_KEY % (user_id, course_id) # 2.1 校验course_id是否合法 if course_id: if not CONN.exists(key): # 不存在这个key res.code = 1060 res.error = "课程id不合法" return Response(res.data) # 2.2 校验course_coupon_id 是否合法 if course_coupon_id: course_coupon_dict = json.loads(CONN.hget(key, "course_coupon_dict")) if str(course_coupon_id) not in course_coupon_dict: res.code = 1061 res.error = "课程优惠券id不合法" return Response(res.dict) # 2.3 校验global_coupon_id 是否合法 if global_coupon_id: global_coupon_key = GLOBAL_COUPON_KEY % user_id if not CONN.exists(global_coupon_key): res.code = 1062 res.error = "全局优惠券id不合法" return Response(res.dict) CONN.hset(global_coupon_key, "default_global_coupon_id", global_coupon_id) # 3.修改redis中数据 CONN.hset(key, "default_coupon_id", course_coupon_id) res.data = "更新成功" return Response(res.dict)
2、更新结算中心接口测试
使用POSTMAN提交PUT请求:{"course_id":1, "course_coupon_id": 2}