课程接口的编写
""" Django settings for LuffyBoy project. Generated by 'django-admin startproject' using Django 1.11.11. For more information on this file, see https://docs.djangoproject.com/en/1.11/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.11/ref/settings/ """ import os # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '#v&kmj7$b3veb*36@ha^=#!f&psro#tw_3nt03vg+9+x$oqa&r' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'course.apps.CourseConfig', "rest_framework", 'login', 'pay' ] CORS_ALLOW_CREDENTIALS = True CORS_ORIGIN_ALLOW_ALL = True MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'utils.middlewares.MyCors', ] ROOT_URLCONF = 'LuffyBoy.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates')] , 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'LuffyBoy.wsgi.application' # Database # https://docs.djangoproject.com/en/1.11/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), } } # Password validation # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/1.11/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_L10N = True USE_TZ = False # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.11/howto/static-files/ STATIC_URL = '/static/' # media配置 MEDIA_URL = "media/" MEDIA_ROOT = os.path.join(BASE_DIR, "media")
from django.urls import path, re_path from .views import CourseCategoryView, CourseView,CourseDetailView urlpatterns = [ re_path(r'category$', CourseCategoryView.as_view()), re_path(r'^$', CourseView.as_view()), re_path(r'detail/(?P<id>d+)$',CourseDetailView.as_view()), ]
from django.contrib import admin from . import models # Register your models here. for table in models.__all__: admin.site.register(getattr(models, table))
from rest_framework import serializers from . import models class CategorySerializer(serializers.ModelSerializer): class Meta: model = models.Category fields = "__all__" class CourseSerializer(serializers.ModelSerializer): # 重写choise字段 level = serializers.CharField(source='get_level_display') price = serializers.SerializerMethodField() # 课程图片 course_img = serializers.SerializerMethodField() def get_price(self, obj): price_policy_obj = obj.price_policy.all().order_by('price').first() return price_policy_obj.price def get_course_img(self, obj): return 'http://127.0.0.1:8001/media/' + str(obj.course_img) class Meta: model = models.Course fields = ['id', 'title', 'course_img', 'brief', 'level', "study_num", "is_free", 'price'] class CourseDetailSerializer(serializers.ModelSerializer): recommend_courses = serializers.SerializerMethodField() teachers = serializers.SerializerMethodField() # 课程大纲 outline = serializers.SerializerMethodField() # 价格策略 price_policy = serializers.SerializerMethodField() # 标题 title = serializers.SerializerMethodField() # 难度 level = serializers.SerializerMethodField() # 学习人数 study_num = serializers.SerializerMethodField() def get_recommend_courses(self, obj): # 多对多 return [{'id': item.id, 'title': item.title} for item in obj.recommend_courses.all()] def get_teachers(self, obj): # 多对多 return [{'id': item.id, 'name': item.name, 'brief': item.brief} for item in obj.teachers.all()] # 课程大纲 def get_outline(self, obj): # course_outline 反向查询字段,一对多 return [{'id': item.id, 'title': item.title, 'content': item.content} for item in obj.course_outline.all().order_by('order')] # 价格策略 def get_price_policy(self, obj): # Content_type # 跨表到Course表 return [{'id': item.id, 'price': item.price, 'valid_period': item.get_valid_period_display()} for item in obj.course.price_policy.all()] def get_title(self, obj): return obj.course.title # 难度 def get_level(self, obj): return obj.course.get_level_display() # 学习人数 def get_study_num(self, obj): return obj.course.study_num class Meta: model = models.CourseDetail exclude = ['course']
from django.shortcuts import render from rest_framework.views import APIView from . import models from . import serializers from rest_framework.response import Response import json # Create your views here. class CourseCategoryView(APIView): def get(self, request): # 从数据库中拿出所有的分类 queryset = models.Category.objects.all() # 序列化所有的分类 ser_obj = serializers.CategorySerializer(queryset, many=True) # 返回序列化好的数据 return Response(ser_obj.data) class CourseView(APIView): def get(self, request, ): # 判断category_id category_id = request.query_params.get('category_id', 0) category_id = int(category_id) if category_id == 0: queryset = models.Course.objects.all().order_by('order') # print(queryset) else: # 获取响应的分类数据 queryset = models.Course.objects.filter(category_id=category_id).order_by('order') print(queryset) # 序列化数据 ser_obj = serializers.CourseSerializer(queryset, many=True) # 返回序列化好的数据 print(type(ser_obj.data)) return Response(ser_obj.data) class CourseDetailView(APIView): def get(self, request, id): # 获取这个课程id找到课程详情对象 queryset = models.CourseDetail.objects.filter(course_id=id).first() # 序列化这个课程详情对象 ser_obj = serializers.CourseDetailSerializer(queryset) # 返回序列化数据 return Response(ser_obj.data)
from django.db import models from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey from django.contrib.contenttypes.models import ContentType # Create your models here. __all__ = ["Category", "Course", "CourseDetail", "Teacher", "DegreeCourse", "CourseChapter", "CourseSection", "PricePolicy", "OftenAskedQuestion", "Comment", "Account", "CourseOutline", 'CouponRecord', 'Coupon'] class Category(models.Model): """课程分类表""" title = models.CharField(max_length=32, unique=True, verbose_name="课程的分类") def __str__(self): return self.title class Meta: verbose_name = "01-课程分类表" db_table = verbose_name verbose_name_plural = verbose_name class Course(models.Model): """课程表""" title = models.CharField(max_length=128, unique=True, verbose_name="课程的名称") course_img = models.ImageField(upload_to="course/%Y-%m", verbose_name='课程的图片') # media/course/2018-11/xxx.png category = models.ForeignKey(to="Category", verbose_name="课程的分类", on_delete=models.CASCADE) COURSE_TYPE_CHOICES = ((0, "付费"), (1, "vip专享"), (2, "学位课程")) course_type = models.SmallIntegerField(choices=COURSE_TYPE_CHOICES) degree_course = models.ForeignKey(to="DegreeCourse", blank=True, null=True, help_text="如果是学位课程,必须关联学位表", on_delete=models.CASCADE) # course_type degree_course_id # 0 null # 1 null # 2 2 brief = models.CharField(verbose_name="课程简介", max_length=1024) level_choices = ((0, '初级'), (1, '中级'), (2, '高级')) level = models.SmallIntegerField(choices=level_choices, default=1) status_choices = ((0, '上线'), (1, '下线'), (2, '预上线')) status = models.SmallIntegerField(choices=status_choices, default=0) pub_date = models.DateField(verbose_name="发布日期", blank=True, null=True) order = models.IntegerField(verbose_name="课程顺序", help_text="从上一个课程数字往后排, 建议中间空几个数字") study_num = models.IntegerField(verbose_name="学习人数", help_text="只要有人买课程,订单表加入数据的同时给这个字段+1") is_free = models.BooleanField(default=False) # order_details = GenericRelation("OrderDetail", related_query_name="course") # coupon = GenericRelation("Coupon") # 只用于反向查询不生成字段 price_policy = GenericRelation("PricePolicy") often_ask_questions = GenericRelation("OftenAskedQuestion") course_comments = GenericRelation("Comment") def save(self, *args, **kwargs): if self.course_type == 2: if not self.degree_course: raise ValueError("学位课必须关联学位课程表") super(Course, self).save(*args, **kwargs) def __str__(self): return self.title class Meta: verbose_name = "02-课程表" db_table = verbose_name verbose_name_plural = verbose_name class CourseDetail(models.Model): """课程详细表""" course = models.OneToOneField(to="Course", on_delete=models.CASCADE) hours = models.IntegerField(verbose_name="课时") course_slogan = models.CharField(max_length=125, blank=True, null=True, verbose_name="课程口号") video_brief_link = models.CharField(max_length=255, blank=True, null=True) summary = models.TextField(max_length=2048, verbose_name="课程概述") why_study = models.TextField(verbose_name="为什么学习这门课程") service = models.TextField(verbose_name="你将获得哪些服务") what_to_study_brief = models.TextField(verbose_name="我将学到哪些内容") career_improvement = models.TextField(verbose_name="此项目如何有助于我的职业生涯") prerequisite = models.TextField(verbose_name="课程先修要求", max_length=1024) recommend_courses = models.ManyToManyField("Course", related_name="recommend_by", blank=True) # 推荐课程 teachers = models.ManyToManyField("Teacher", verbose_name="课程讲师") def __str__(self): return self.course.title class Meta: verbose_name = "03-课程详细表" db_table = verbose_name verbose_name_plural = verbose_name class Teacher(models.Model): """讲师表""" name = models.CharField(max_length=32, verbose_name="讲师名字") brief = models.TextField(max_length=1024, verbose_name="讲师介绍") def __str__(self): return self.name class Meta: verbose_name = "04-教师表" db_table = verbose_name verbose_name_plural = verbose_name class DegreeCourse(models.Model): """ 字段大体跟课程表相同,哪些不同根据业务逻辑去区分 """ title = models.CharField(max_length=32, verbose_name="学位课程名字") def __str__(self): return self.title class Meta: verbose_name = "05-学位课程表" db_table = verbose_name verbose_name_plural = verbose_name class CourseChapter(models.Model): """课程章节表""" course = models.ForeignKey(to="Course", related_name="course_chapters", on_delete=models.CASCADE) # 排序用的 chapter = models.SmallIntegerField(default=1, verbose_name="第几章") title = models.CharField(max_length=32, verbose_name="课程章节名称") def __str__(self): return self.title class Meta: verbose_name = "06-课程章节表" db_table = verbose_name verbose_name_plural = verbose_name unique_together = ("course", "chapter") class CourseSection(models.Model): """课时表""" chapter = models.ForeignKey(to="CourseChapter", related_name="course_sections", on_delete=models.CASCADE) title = models.CharField(max_length=32, verbose_name="课时") section_order = models.SmallIntegerField(verbose_name="课时排序", help_text="建议每个课时之间空1至2个值,以备后续插入课时") section_type_choices = ((0, '文档'), (1, '练习'), (2, '视频')) free_trail = models.BooleanField("是否可试看", default=False) section_type = models.SmallIntegerField(default=2, choices=section_type_choices) section_link = models.CharField(max_length=255, blank=True, null=True, help_text="若是video,填vid,若是文档,填link") def __str__(self): return "%s-%s" % (self.chapter, self.title) class Meta: verbose_name = "07-课程课时表" db_table = verbose_name verbose_name_plural = verbose_name unique_together = ('chapter', 'section_link') class PricePolicy(models.Model): """价格策略表""" content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) # 关联course or degree_course object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') valid_period_choices = ((1, '1天'), (3, '3天'), (7, '1周'), (14, '2周'), (30, '1个月'), (60, '2个月'), (90, '3个月'), (120, '4个月'), (180, '6个月'), (210, '12个月'), (540, '18个月'), (720, '24个月') ) valid_period = models.SmallIntegerField(choices=valid_period_choices) price = models.FloatField() def __str__(self): return "%s(%s)%s" % (self.content_object, self.get_valid_period_display(), self.price) class Meta: verbose_name = "08-价格策略表" db_table = verbose_name verbose_name_plural = verbose_name unique_together = ("content_type", 'object_id', "valid_period") class OftenAskedQuestion(models.Model): """常见问题""" content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) # 关联course or degree_course object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') question = models.CharField(max_length=255) answer = models.TextField(max_length=1024) def __str__(self): return "%s-%s" % (self.content_object, self.question) class Meta: verbose_name = "09-常见问题表" db_table = verbose_name verbose_name_plural = verbose_name unique_together = ('content_type', 'object_id', 'question') class Comment(models.Model): """通用的评论表""" # 定位表 content_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=models.CASCADE) # 定位对象的id object_id = models.PositiveIntegerField(blank=True, null=True) # 定位对象 content_object = GenericForeignKey('content_type', 'object_id') content = models.TextField(max_length=1024, verbose_name="评论内容") account = models.ForeignKey("Account", verbose_name="会员名", on_delete=models.CASCADE) date = models.DateTimeField(auto_now_add=True) def __str__(self): return self.content class Meta: verbose_name = "10-评价表" db_table = verbose_name verbose_name_plural = verbose_name class Account(models.Model): username = models.CharField(max_length=32, verbose_name="用户姓名") pwd = models.CharField(max_length=128, verbose_name='密码') token = models.UUIDField(null=True, blank=True) create_token_time = models.DateTimeField(auto_now=True) beli = models.IntegerField(default=1000) def __str__(self): return self.username class Meta: verbose_name = "11-用户表" db_table = verbose_name verbose_name_plural = verbose_name class CourseOutline(models.Model): """课程大纲""" course_detail = models.ForeignKey(to="CourseDetail", related_name="course_outline", on_delete=models.CASCADE) title = models.CharField(max_length=128) order = models.PositiveSmallIntegerField(default=1) # 前端显示顺序 content = models.TextField("内容", max_length=2048) def __str__(self): return "%s" % self.title class Meta: verbose_name = "12-课程大纲表" db_table = verbose_name verbose_name_plural = verbose_name unique_together = ('course_detail', 'title') #######################################优惠券表 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='券类型') # 立减券只需要填等值货币 # 满减券需要填等值货币,最低消费,如果不绑定课程,相当于通用优惠卷,满足金额即减 # 折扣券只需填折扣百分比 money_equivalent_value = models.IntegerField(verbose_name='等值货币', blank=True, null=True) off_percent = models.PositiveSmallIntegerField("折扣百分比", help_text="只针对折扣券,例7.9折,写79", blank=True, null=True) minimun_consume = models.PositiveIntegerField('最低消费', default=0, help_text='仅在满减券时填写此字段') content_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=models.CASCADE) object_id = models.PositiveIntegerField('绑定课程', blank=True, null=True, help_text='可以把优惠券跟课程绑定') content_object = GenericForeignKey('content_type', 'object_id') quantity = models.PositiveIntegerField('数量(张)', default=1) open_data = models.DateField('优惠券领取的开始时间') close_data = models.DateField('优惠券领取结束时间') vaild_begin_data = models.DateTimeField(verbose_name='有效期开始时间', blank=True, null=True) vaild_end_data = models.DateTimeField(verbose_name='有效期结束时间', blank=True, null=True) coupon_valid_days = models.PositiveIntegerField(verbose_name='优惠劵的有效期(天)', blank=True, null=True, help_text='自券被领时开始算起') data = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = "13-优惠券生成规则" db_table = verbose_name verbose_name_plural = "13-优惠券生成规则" def __str__(self): return "%s(%s)" % (self.get_coupon_type_display(), self.name) class CouponRecord(models.Model): """优惠券发放、消费纪录""" coupon = models.ForeignKey("Coupon", on_delete=models.CASCADE) user = models.ForeignKey("Account", verbose_name="拥有者", on_delete=models.CASCADE) status_choices = ((0, '未使用'), (1, '已使用'), (2, '已过期')) status = models.SmallIntegerField(choices=status_choices, default=0) get_time = models.DateTimeField(verbose_name="领取时间", help_text="用户领取时间", null=True, blank=True) used_time = models.DateTimeField(blank=True, null=True, verbose_name="使用时间") class Meta: verbose_name = "14-优惠券发放、消费纪录" db_table = verbose_name verbose_name_plural = verbose_name def __str__(self): return '%s-%s-%s' % (self.user, self.coupon, self.get_status_display()) ###################################### 订单表 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="支付第3方订单号", null=True, blank=True) order_number = models.CharField(max_length=128, verbose_name="订单号", unique=True) # 考虑到订单合并支付的问题 user = models.ForeignKey("Account", on_delete=models.CASCADE) 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 = "15-订单表" db_table = verbose_name verbose_name_plural = verbose_name def __str__(self): return "%s" % self.order_number class OrderDetail(models.Model): """订单详情""" order = models.ForeignKey("Order", on_delete=models.CASCADE) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) # 可关联普通课程或学位 object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') original_price = models.FloatField("课程原价") price = models.FloatField("折后价格") content = models.CharField(max_length=255, blank=True, null=True) # valid_period_display = models.CharField("有效期显示", max_length=32) # 在订单页显示 valid_period = models.PositiveIntegerField("有效期(days)") # 课程有效期 memo = models.CharField(max_length=255, blank=True, null=True) def __str__(self): return "%s - %s - %s" % (self.order, self.content_type, self.price) class Meta: verbose_name = "16-订单详细" db_table = verbose_name verbose_name_plural = verbose_name unique_together = ("order", 'content_type', 'object_id')
登录,注册接口的编写
from django.urls import path, include from .views import RegisterView, LoginView urlpatterns = [ path('register/', RegisterView.as_view()), path('login/', LoginView.as_view()), ]
from rest_framework import serializers from course.models import Account import hashlib # 后端加密 class AccountSerializer(serializers.ModelSerializer): class Meta: model = Account fields = ["username", "pwd"] def create(self, validated_data): username = validated_data["username"] pwd = validated_data["pwd"] hash_pwd = hashlib.md5(pwd.encode()).hexdigest() user_obj = Account.objects.create(username=username, pwd=hash_pwd) return user_obj
from django.shortcuts import render from rest_framework.views import APIView from rest_framework.response import Response from .serializers import AccountSerializers from utils.base_response import BaseResponse from course.models import Account import uuid import hashlib class RegisterView(APIView): def post(self, request): # 获取用户名和密码 # 拿序列化器做验证 ser_obj = AccountSerializers(data=request.data) if ser_obj.is_valid(): ser_obj.save() return Response('注册成功') return Response(ser_obj.errors) class LoginView(APIView): def post(self, request): ret = BaseResponse() # 获取用户名和密码 username = request.data.get('username', '') if not username: ret.code = 1010 ret.error = '用户名不能为空' pwd = request.data.get('pwd', '') if pwd: pwd = hashlib.md5(pwd.encode()).hexdigest() if not pwd: ret.code = 1011 ret.error = '密码不能为空' try: # 判断是否有这个对象 user_obj = Account.objects.filter(username=username, pwd=pwd).first() if not user_obj: ret.code = 1012 ret.error = '用户名或密码错误' # 有这个对象,生成token user_obj.token = uuid.uuid4() user_obj.save() ret.data = '登录成功' except Exception as e: ret.code = 1013 ret.error = '登录失败' return Response(ret.dict)
认证
from rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed from course.models import Account import datetime from django.utils.timezone import now class MyAuth(BaseAuthentication): def authenticate(self, request): # 过滤前端传来的复杂请求 if request.method == 'OPTIONS': return None # 拿到前端带过来的token # print(request.META) token = request.META.get('HTTP_AUTHENTICATE', "") # 判断是否有这个token if not token: raise AuthenticationFailed({"code": 1020, "error": "没有携带token"}) user_obj = Account.objects.filter(token=token).first() if not user_obj: raise AuthenticationFailed({"code": 1021, "error": "token不合法"}) # 判断token是否过期 old_time = user_obj.create_token_time # print(old_time) # print(type(old_time)) now_time = now() # print(now) # print(type(now)) # print((now_time-old_time).days) if (now_time - old_time).days > 7: raise AuthenticationFailed({"code": 1022, "error": "token过期请重新登录"}) return (user_obj, token)
自定义状态码
class BaseResponse(object): def __init__(self): self.code = 1000 self.error = '' self.data = '' @property def dict(self): return self.__dict__
自定义中间件
from django.utils.deprecation import MiddlewareMixin class MyCors(MiddlewareMixin): def process_response(self, request, response): response["Access-Control-Allow-Origin"] = "*" if request.method == "OPTIONS": # 复杂请求会先发预检 response["Access-Control-Allow-Headers"] = "Content-Type,AUTHENTICATE" # AUTHENTICATE 把token加入进来防止前端报跨域 response["Access-Control-Allow-Methods"] = "PUT,PATCH,DELETE" return response # 中间件添加相应头
自定义异常
class CommonException(Exception): def __init__(self, msg, code): self.msg = msg self.code = code
redis
import redis # redis的连接池 pool = redis.ConnectionPool(host='127.0.0.1', port=6379, decode_responses=True) # decode_responses=True 不用转为bytes conn = redis.Redis(connection_pool=pool)
购物车,结算,支付接口的编写
from django.urls import path, include from .views import ShoppingCarView,AccountView,PaymenyView urlpatterns = [ path('shopping_car', ShoppingCarView.as_view()), path('account', AccountView.as_view()), path('payment', PaymenyView.as_view()) ]
from rest_framework.views import APIView from rest_framework.response import Response from utils.authentication import MyAuth from utils.radis_pool import pool import redis from utils.base_response import BaseResponse from course import models import json from utils.exceptions import CommonException import datetime from django.utils.timezone import now from utils.pay import AliPay from django.core.exceptions import ObjectDoesNotExist SHOPPING_CAR_KEY = 'shopping_car_%s_%s' REDIS_CONN = redis.Redis(connection_pool=pool) ACCOUNT_KEY = 'account_%s_%s' class ShoppingCarView(APIView): """ shopping_car_%s_%s: { id: 1, title: CMDB, course_img: xxxx, price_policy_dict: { 1: {有效期1个月, 99} }, default_price_policy_id :3 } """ authentication_classes = [MyAuth, ] def post(self, request): res = BaseResponse() try: # 1 获取前端传过来的course_id 以及price_policy_id user_id course_id = request.data.get("course_id", "") price_policy_id = request.data.get("price_policy_id", "") user_id = request.user.id # 2 验证数据的合法性 # 2.1 验证course_id是否合法 course_obj = models.Course.objects.filter(id=course_id).first() if not course_obj: res.code = 1031 res.error = "课程不存在" return Response(res.dict) # 2.2 验证价格策略是否合法 # 该课程的所有价格策略对象 price_policy_queryset = course_obj.price_policy.all() # 循环获得每个价格策略的详细信息 price_policy_dict = {} for price_policy_obj in price_policy_queryset: price_policy_dict[price_policy_obj.id] = { "valid_period_text": price_policy_obj.get_valid_period_display(), "price": price_policy_obj.price } # 判断价格策略是否在价格策略的字典里 if price_policy_id not in price_policy_dict: res.code = 1032 res.error = "价格策略不存在" return Response(res.dict) # 3 构建我们想要的数据结构 course_info = { "id": course_id, "title": course_obj.title, "course_img": course_obj.course_img, "price_policy_dict": json.dumps(price_policy_dict, ensure_ascii=False), "default_policy_id": price_policy_id } # 4 写入redis # 4.1 先拼接购物车的key shopping_car_key = SHOPPING_CAR_KEY % (user_id, course_id) # 4.2 写入redis REDIS_CONN.hmset(shopping_car_key, course_info) res.data = "加入购物车成功" except Exception as e: res.code = 1030 res.error = "加入购物车失败" return Response(res.dict) def get(self, request): res = BaseResponse() try: # 1 取到user_id user_id = request.user.id # 2 拼接购物车的key,redis的key支持模糊匹配 shopping_car_key = SHOPPING_CAR_KEY % (user_id, "*") # shopping_car_1_* # shopping_car_1_asdgnlaksdj # 3 去redis读取该用户的所有加入购物车的课程 # 3.1 先去模糊匹配出所有符合要求的key all_keys = REDIS_CONN.scan_iter(shopping_car_key) print(all_keys) # 3.2 循环所有的keys 得到每个可以 shopping_car_list = [] for key in all_keys: course_info = REDIS_CONN.hgetall(key) course_info["price_policy_dict"] = json.loads(course_info["price_policy_dict"]) shopping_car_list.append(course_info) res.data = shopping_car_list except Exception as e: res.code = 1033 res.error = "获取购物车失败" return Response(res.dict) def put(self, request): res = BaseResponse() try: course_id = request.data.get("course_id", "") price_policy_id = request.data.get("price_policy_id", "") user_id = request.user.id shopping_car_key = SHOPPING_CAR_KEY % (user_id, course_id) if not REDIS_CONN.exists(shopping_car_key): res.code = 1035 res.error = '课程不不存在' return Response(res.dict) course_info = REDIS_CONN.hgetall(shopping_car_key) price_policy_dict = json.loads(course_info['price_policy_dict']) if str(price_policy_id) not in price_policy_dict: res.code = 1036 res.error = '价格策略不合法' return Response(res.dict) course_info['default_price_policy_id'] = price_policy_id REDIS_CONN.hmset(shopping_car_key, course_info) res.data = '更新成功' return Response(res.dict) except Exception as e: res.code = 1034 res.error = '更新价格策略失败' return Response(res.dict) def delete(self, request): res = BaseResponse() try: course_id = request.data.get('course_id', '') user_id = request.user.id shopping_car_key = SHOPPING_CAR_KEY % (user_id, course_id) if not REDIS_CONN.exists(shopping_car_key): res.code = 1039 res.error = '删除的课程不存在' return Response(res.dict) REDIS_CONN.delete(shopping_car_key) res.data = '删除成功' except Exception as e: res.code = 1037 res.error = '删除失败' return Response(res.dict) class AccountView(APIView): ''' 结算接口 shopping_car_ 1_ 1: { id: 1, title: CMDB, course_img: xxxxx, price_policy_dict: { 1: {有效期1个月, 99} }, default_price_policy_id: 3 } account_%s_%s:{ "course_info":{ id: 1, title: CMDB, course_img: xxxxx, price_policy_dict: { 1: {有效期1个月, 99} }, default_price_policy_id: 3 }, "coupons":{ # 课程优惠卷 1:{}, 3:{}, } } global_coupon_1:{} # 通用优惠卷 ''' authentication_classes = [MyAuth, ] def post(self, request, *args, **kwargs): res = BaseResponse() # 1. 获取数据 user = request.user course_id_list = request.data.get('course_id_list') try: # 2 创建数据结构 # 清空操作 # 找到所有以account_userid_*,全部清空 del_list = REDIS_CONN.keys(ACCOUNT_KEY % (user.pk, '*')) REDIS_CONN.delete(*del_list) # 删除需要加*, for course_id in course_id_list: shopping_car_key = SHOPPING_CAR_KEY % (user.pk, course_id) account_key = ACCOUNT_KEY % (user.pk, course_id) account_dict = {} # 判断课程是否存在购物车中 if not REDIS_CONN.exists(shopping_car_key): raise CommonException('课程不存在', 1040) # 将课程信息加入到每一个课程结算字典中 course_info = REDIS_CONN.hgetall(shopping_car_key) account_dict['course_info'] = course_info # 将课程优惠卷加入结算的每一个字典里 account_dict['course_coupons'] = self.get_coupon_dict(request, course_id) # print(account_dict) # 存储结算信息 REDIS_CONN.set(account_key, json.dumps(account_dict)) # 使用redis字符串方法 # 获取通用优惠卷,不传课程id即可,存入redis中 REDIS_CONN.set('global_coupon_%s' % (user.pk), json.dumps(self.get_coupon_dict(request))) res.data = '结算成功' except CommonException as e: res.code = e.code res.error = e.msg except Exception as e: res.code = 600 res.error = str(e) return Response(res.dict) def get_coupon_dict(self, request, course_id=None): now = datetime.datetime.utcnow() # 当前时间转为同一时区 # 将课程优惠卷加入结算的每一个字典里 coupon_record_list = models.CouponRecord.objects.filter( user=request.user, status=0, coupon__vaild_begin_data__lte=now, coupon__vaild_end_data__gt=now, coupon__content_type_id=9, # 9对应content_type表绑定课程表的id,如果优惠卷绑定不是课程表,那就会查不到 coupon__object_id=course_id, ) print('coupon_record_list', coupon_record_list) coupon_dict = {} for coupon_record in coupon_record_list: coupon_dict[coupon_record.pk] = { 'name': coupon_record.coupon.name, 'coupon_type': coupon_record.coupon.get_coupon_type_display(), 'money_equivalent_value': coupon_record.coupon.money_equivalent_value, 'off_percent': coupon_record.coupon.off_percent, 'minimun_consume': coupon_record.coupon.minimun_consume, 'vaild_begin_data': coupon_record.coupon.vaild_begin_data.strftime('%Y-%m-%d'), # 这里需要转一下,不然json无法序列化这个格式 'vaild_end_data': coupon_record.coupon.vaild_end_data.strftime('%Y-%m-%d'), } return coupon_dict def get(self, request, *args, **kwargs): pass # 支付接口 class PaymenyView(APIView): authentication_classes = [MyAuth] def cal_coupun_price(self, price, coupon_record): coupon_type = coupon_record.coupon.coupon_type money_equivalent_value = coupon_record.coupon.money_equivalent_value off_percent = coupon_record.coupon.off_percent minimun_consume = coupon_record.coupon.minimun_consume rebate_price = 0 if coupon_type == 0: # 立减券 rebate_price = price - money_equivalent_value if rebate_price < 0: rebate_price = 0 elif coupon_type == 1: # 满减券 if price < minimun_consume: # 不满足最低消费 raise CommonException("不满足最低消费", 1060) rebate_price = price - money_equivalent_value else: # 折扣 rebate_price = price * off_percent / 100 return rebate_price def post(self, request): ''' { "courses_info":[ { course_id:1, price_policy_id:1, coupon_record_id:2 }, { course_id:2, price_policy_id:5, coupon_record_id:3 } ] global_coupon_id:1, beli:1000, "pay_money":268, } :param request: :return: ''' # 1 获取数据 user = request.user courses_info = request.data.get("courses_info") global_coupon_id = request.data.get("global_coupon_id") beli = request.data.get("beli") pay_money = request.data.get("pay_money") now = datetime.datetime.utcnow() response = BaseResponse() # 2 循环课程信息 try: course_price_list = [] for course_info in courses_info: course_id = course_info.get("course_id") price_policy_id = course_info.get("price_policy_id") coupon_record_id = course_info.get("coupon_record_id") # 3 校验数据 # 3.1判断课程是否存在 course_obj = models.Course.objects.get(pk=course_id) # 3.2 价格策略是否合法 if price_policy_id not in [obj.pk for obj in course_obj.price_policy.all()]: raise CommonException("价格策略不存在", 1051) # 3.3 课程优惠券是否合法 couponrecord = models.CouponRecord.objects.filter( pk=coupon_record_id, user=request.user, status=0, coupon__vaild_begin_data__lte=now, coupon__vaild_end_data__gt=now, coupon__content_type_id=9, coupon__object_id=course_id ).first() if not couponrecord: raise CommonException("课程优惠券有问题", 1052) # 3.4 计算课程优惠券惠后价格 course_price = models.PricePolicy.objects.get(pk=price_policy_id).price coupon_price = self.cal_coupun_price(course_price, couponrecord) course_price_list.append(coupon_price) # 4 通用优惠券处理 # 4.1 通用优惠券是否能合法 global_couponrecord = models.CouponRecord.objects.filter( pk=global_coupon_id, user=request.user, status=0, coupon__vaild_begin_data__lte=now, coupon__vaild_end_data__gt=now, coupon__content_type_id=9, coupon__object_id=None ).first() if not global_couponrecord: raise CommonException("通用优惠券有问题", 1053) # 4.2 计算通用优惠券惠后价格 global_coupon_price = self.cal_coupun_price(sum(course_price_list), global_couponrecord) # 5 处理贝里 # 5.1 校验贝里是否充足 if beli > request.user.beli: raise CommonException("贝里数不够", 1054) # 5.2 计算贝里后的价格 final_price = global_coupon_price - beli / 10 if final_price < 0: final_price = 0 # 返还贝里数 print("final", final_price) # 计算优惠后的价格 # 6 比较参数pay_money与实际支付价格是否一致 if final_price != pay_money: raise CommonException("实际支付价格与参数价格并不一致", 1055) # 7 生成订单 # Order记录 # OrderDetail # OrderDetail # OrderDetail # 8 构建支付宝的二维码页面的url import time alipay = self.get_alipay() # 生成支付的url query_params = alipay.direct_pay( subject="Django课程", # 商品简单描述 out_trade_no="x2" + str(time.time()), # 商户订单号 total_amount=pay_money, # 交易金额(单位: 元 保留俩位小数) ) pay_url = "https://openapi.alipaydev.com/gateway.do?{}".format(query_params) response.data = "订单创建成功!" response.url = pay_url except ObjectDoesNotExist as e: response.code = 1050 response.error = "课程不存在" except CommonException as e: response.code = e.code response.error = e.msg # except Exception as e: # response.code = 500 # response.error = str(e) return Response(response.dict) def get_alipay(self): # 沙箱环境地址:https://openhome.alipay.com/platform/appDaily.htm?tab=info app_id = "2016091100486897" # POST请求,用于最后的检测 notify_url = "http://47.94.172.250:8804/page2/" # notify_url = "http://www.wupeiqi.com:8804/page2/" # GET请求,用于页面的跳转展示 return_url = "http://47.94.172.250:8804/page2/" # return_url = "http://www.wupeiqi.com:8804/page2/" merchant_private_key_path = "utils/keys/app_private_2048.txt" alipay_public_key_path = "utils/keys/alipay_public_2048.txt" alipay = AliPay( appid=app_id, app_notify_url=notify_url, return_url=return_url, app_private_key_path=merchant_private_key_path, alipay_public_key_path=alipay_public_key_path, # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥 debug=True, # 默认False, ) return alipay
支付宝接口
from datetime import datetime from Crypto.PublicKey import RSA from Crypto.Signature import PKCS1_v1_5 from Crypto.Hash import SHA256 from urllib.parse import quote_plus from urllib.parse import urlparse, parse_qs from base64 import decodebytes, encodebytes import json class AliPay(object): """ 支付宝支付接口(PC端支付接口) """ def __init__(self, appid, app_notify_url, app_private_key_path, alipay_public_key_path, return_url, debug=False): self.appid = appid self.app_notify_url = app_notify_url self.app_private_key_path = app_private_key_path self.app_private_key = None self.return_url = return_url with open(self.app_private_key_path) as fp: self.app_private_key = RSA.importKey(fp.read()) self.alipay_public_key_path = alipay_public_key_path with open(self.alipay_public_key_path) as fp: self.alipay_public_key = RSA.importKey(fp.read()) if debug is True: self.__gateway = "https://openapi.alipaydev.com/gateway.do" else: self.__gateway = "https://openapi.alipay.com/gateway.do" def direct_pay(self, subject, out_trade_no, total_amount, return_url=None, **kwargs): biz_content = { "subject": subject, "out_trade_no": out_trade_no, "total_amount": total_amount, "product_code": "FAST_INSTANT_TRADE_PAY", # "qr_pay_mode":4 } biz_content.update(kwargs) data = self.build_body("alipay.trade.page.pay", biz_content, self.return_url) return self.sign_data(data) def build_body(self, method, biz_content, return_url=None): data = { "app_id": self.appid, "method": method, "charset": "utf-8", "sign_type": "RSA2", "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "version": "1.0", "biz_content": biz_content } if return_url is not None: data["notify_url"] = self.app_notify_url data["return_url"] = self.return_url return data def sign_data(self, data): data.pop("sign", None) # 排序后的字符串 unsigned_items = self.ordered_data(data) unsigned_string = "&".join("{0}={1}".format(k, v) for k, v in unsigned_items) sign = self.sign(unsigned_string.encode("utf-8")) # ordered_items = self.ordered_data(data) quoted_string = "&".join("{0}={1}".format(k, quote_plus(v)) for k, v in unsigned_items) # 获得最终的订单信息字符串 signed_string = quoted_string + "&sign=" + quote_plus(sign) return signed_string def ordered_data(self, data): complex_keys = [] for key, value in data.items(): if isinstance(value, dict): complex_keys.append(key) # 将字典类型的数据dump出来 for key in complex_keys: data[key] = json.dumps(data[key], separators=(',', ':')) return sorted([(k, v) for k, v in data.items()]) def sign(self, unsigned_string): # 开始计算签名 key = self.app_private_key signer = PKCS1_v1_5.new(key) signature = signer.sign(SHA256.new(unsigned_string)) # base64 编码,转换为unicode表示并移除回车 sign = encodebytes(signature).decode("utf8").replace(" ", "") return sign def _verify(self, raw_content, signature): # 开始计算签名 key = self.alipay_public_key signer = PKCS1_v1_5.new(key) digest = SHA256.new() digest.update(raw_content.encode("utf8")) if signer.verify(digest, decodebytes(signature.encode("utf8"))): return True return False def verify(self, data, signature): if "sign_type" in data: sign_type = data.pop("sign_type") # 排序后的字符串 unsigned_items = self.ordered_data(data) message = "&".join(u"{}={}".format(k, v) for k, v in unsigned_items) return self._verify(message, signature)
Django的MEDIA配置
# settings.py STATIC_URL = '/static/' # Media配置 MEDIA_URL = "media/" MEDIA_ROOT = os.path.join(BASE_DIR, "media")
# urls.py from django.conf.urls import url, include from django.contrib import admin from django.views.static import serve from new_luffy import settings urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^api/course/', include("course.urls")), # media路径配置 url(r'media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}) ]
这样我们上传的图片~数据库存的是路径地址~~我们前端向后端的media路径发送请求~~
拿到我们想要的图片,视频等资源~~