【课程板块表分析】

1 课程分类表:一个课程分类下,有多门课程  

  -id
  -分类名
2 课程表
  -课程有多种类型---》多个表,还是一个表--》
  -不同课程字段不一样--》所以不建议放到一个表中---》
    -免费
    -实战
    -轻课
    -不同课程,使用不同表来存
  -开发时候,不同人,开发不同模块,不会冲突
  -实战课板块:实战课表---》其它表,其它板块写

3 老师表:跟课程一对多

4 章节:章节和课程一对多

5 课时表:课时跟章节一对多

===========================================================================

【创建表】

  1 from django.db import models
  2 from utils.common_model import BaseModel
  3 
  4 
  5 # Create your models here.
  6 
  7 # 课程分类表,实战课表,老师表,章节表,课时表
  8 
  9 class CourseCategory(BaseModel):
 10     """课程分类表"""
 11     name = models.CharField(max_length=64, unique=True, verbose_name="分类名称")
 12 
 13     class Meta:
 14         db_table = "luffy_course_category"
 15         verbose_name = "分类"
 16         verbose_name_plural = verbose_name
 17 
 18     def __str__(self):
 19         return "%s" % self.name
 20 
 21 
 22 class Course(BaseModel):
 23     """课程表"""
 24     course_type = (
 25         (0, '付费'),
 26         (1, 'VIP专享'),
 27     )
 28     level_choices = (
 29         (0, '初级'),
 30         (1, '中级'),
 31         (2, '高级'),
 32     )
 33     status_choices = (
 34         (0, '上线'),
 35         (1, '下线'),
 36         (2, '预上线'),
 37     )
 38     name = models.CharField(max_length=128, verbose_name="课程名称")
 39     course_img = models.ImageField(upload_to="courses", max_length=255, verbose_name="封面图片", blank=True, null=True)
 40     course_type = models.SmallIntegerField(choices=course_type, default=0, verbose_name="付费类型")
 41     brief = models.TextField(max_length=2048, verbose_name="详情介绍", null=True, blank=True)
 42     level = models.SmallIntegerField(choices=level_choices, default=0, verbose_name="难度等级")
 43     pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True)
 44     period = models.IntegerField(verbose_name="建议学习周期(day)", default=7)
 45     attachment_path = models.FileField(upload_to="attachment", max_length=128, verbose_name="课件路径", blank=True,
 46                                        null=True)
 47     status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="课程状态")
 48     students = models.IntegerField(verbose_name="学习人数", default=0)
 49     sections = models.IntegerField(verbose_name="总课时数量", default=0)
 50     pub_sections = models.IntegerField(verbose_name="课时更新数量", default=0)
 51     price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价", default=0)
 52 
 53     # on_delete 可以选择字段
 54     # db_constraint
 55     teacher = models.ForeignKey("Teacher", on_delete=models.DO_NOTHING, null=True, blank=True, verbose_name="授课老师")
 56     course_category = models.ForeignKey("CourseCategory", on_delete=models.SET_NULL, db_constraint=False, null=True,
 57                                         blank=True, verbose_name="课程分类")
 58 
 59     class Meta:
 60         db_table = "luffy_course"
 61         verbose_name = "课程"
 62         verbose_name_plural = "课程"
 63 
 64     def __str__(self):
 65         return "%s" % self.name
 66 
 67 
 68 class Teacher(BaseModel):
 69     """老师表"""
 70     role_choices = (
 71         (0, '讲师'),
 72         (1, '导师'),
 73         (2, '班主任'),
 74     )
 75     name = models.CharField(max_length=32, verbose_name="导师名")
 76     role = models.SmallIntegerField(choices=role_choices, default=0, verbose_name="导师身份")
 77     title = models.CharField(max_length=64, verbose_name="职位、职称")
 78     signature = models.CharField(max_length=255, verbose_name="导师签名", help_text="导师签名", blank=True, null=True)
 79     image = models.ImageField(upload_to="teacher", null=True, verbose_name="导师封面")
 80     brief = models.TextField(max_length=1024, verbose_name="导师描述")
 81 
 82     class Meta:
 83         db_table = "luffy_teacher"
 84         verbose_name = "导师"
 85         verbose_name_plural = verbose_name
 86 
 87     def __str__(self):
 88         return "%s" % self.name
 89 
 90 
 91 class CourseChapter(BaseModel):
 92     """章节表"""
 93     # related_name
 94     course = models.ForeignKey("Course", related_name='coursechapters', on_delete=models.CASCADE,
 95                                verbose_name="课程名称")
 96     chapter = models.SmallIntegerField(verbose_name="第几章", default=1)
 97     name = models.CharField(max_length=128, verbose_name="章节标题")
 98     summary = models.TextField(verbose_name="章节介绍", blank=True, null=True)
 99     pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True)
100 
101     class Meta:
102         db_table = "luffy_course_chapter"
103         verbose_name = "章节"
104         verbose_name_plural = verbose_name
105 
106     def __str__(self):
107         return "%s:(第%s章)%s" % (self.course, self.chapter, self.name)
108 
109 
110 class CourseSection(BaseModel):
111     """课时表"""
112     section_type_choices = (
113         (0, '文档'),
114         (1, '练习'),
115         (2, '视频')
116     )
117     chapter = models.ForeignKey("CourseChapter", related_name='coursesections', on_delete=models.CASCADE,
118                                 verbose_name="课程章节")
119     name = models.CharField(max_length=128, verbose_name="课时标题")
120     orders = models.PositiveSmallIntegerField(verbose_name="课时排序")
121     section_type = models.SmallIntegerField(default=2, choices=section_type_choices, verbose_name="课时种类")
122     section_link = models.CharField(max_length=255, blank=True, null=True, verbose_name="课时链接",
123                                     help_text="若是video,填vid,若是文档,填link")
124     duration = models.CharField(verbose_name="视频时长", blank=True, null=True, max_length=32)  # 仅在前端展示使用
125     pub_date = models.DateTimeField(verbose_name="发布时间", auto_now_add=True)
126     free_trail = models.BooleanField(verbose_name="是否可试看", default=False)
127 
128     class Meta:
129         db_table = "luffy_course_section"
130         verbose_name = "课时"
131         verbose_name_plural = verbose_name
132 
133     def __str__(self):
134         return "%s-%s" % (self.chapter, self.name)
135 
136 --------------------------------------------------------------
137 admin中注册表
138 from django.contrib import admin
139 from .models import *
140 
141 # Register your models here.
142 
143 admin.site.register(CourseCategory)
144 admin.site.register(Course)
145 admin.site.register(Teacher)
146 admin.site.register(CourseChapter)
147 admin.site.register(CourseSection)
148 
149 --------------------------------------------------------------
150 迁移表
151     python manage.py makemigrations
152     python manage.py migrate
创建课程表模型

【ForeignKey中参数】

1 to:跟哪个表管理,需要配合to_field,如果不写,会关联主键
 2 to_field=None
---------------------------

3 on_delete:当这条记录删除时--》外键
    -CASCADE:级联删除:用户和用户详情,课程和章节,章节和课时
    -SET_NULL:关联的删除,这个字段设为空,但是需要配合:null=True
    -SET_DEFAULT:关联的删除,这个字段设为默认值,但是需要配合:default=xx
    -SET(函数内存地址):关联的删除,会触发这个函数执行
    
---------------------
# orm查询,正向和反向
    -基于对象的跨表查询
        -book.publish --> 正向
        -publish.book_set.all()-->反向
        
    -基于双下划线的跨表查询
        -book__publish_name-->正向
        -publish__book_name-->反向
    -正向按字段
    -反向:按表名小写(是否带set取决于是否是多),基于双下划线的都是表名小写
4 related_name=None:基于对象跨表查,反向查询的名字 (原来:按表名小写-是否带set取决于是否是多),现在按这个字段
    -原来:course.coursechapter_set.all()
    -现在course.coursechapters.all()
5 related_query_name=None 基于下划线跨表查,反向查询的名字,现在按这个字段
    publish__指定的字段_name



6 db_constraint=False  不建立强外键关系,默认是True
    -强外键--》er图上有条线--》关联操作时,会有限制,会有约束
        -会消耗性能
    -实际工作中,不建立强外键,但是有外键关系--》er图上没有有条线--》orm关联操作一样用
        -以后存数据,删除数据,就不会检索关联表,性能高
        -可能会录入 脏数据 :程序层面控制

【课程相关数据录入】

INSERT INTO luffy_teacher(id, orders, is_show, is_delete, created_time, updated_time, name, role, title, signature, image, brief) VALUES (1, 1, 1, 0, '2022-07-14 13:44:19.661327', '2022-07-14 13:46:54.246271', 'Alex', 1, '老男孩Python教学总监', '金角大王', 'teacher/alex_icon.png', '老男孩教育CTO & CO-FOUNDER 国内知名PYTHON语言推广者 51CTO学院2016\2017年度最受学员喜爱10大讲师之一 多款开源软件作者 曾任职公安部、飞信、中金公司、NOKIA中国研究院、华尔街英语、ADVENT、汽车之家等公司');


INSERT INTO luffy_teacher(id, orders, is_show, is_delete, created_time, updated_time, name, role, title, signature, image, brief) VALUES (2, 2, 1, 0, '2022-07-14 13:45:25.092902', '2022-07-14 13:45:25.092936', 'Mjj', 0, '前美团前端项目组架构师', NULL, 'teacher/mjj_icon.png', '是马JJ老师, 一个集美貌与才华于一身的男人,搞过几年IOS,又转了前端开发几年,曾就职于美团网任高级前端开发,后来因为不同意王兴(美团老板)的战略布局而出家做老师去了,有丰富的教学经验,开起车来也毫不含糊。一直专注在前端的前沿技术领域。同时,爱好抽烟、喝酒、烫头(锡纸烫)。 我的最爱是前端,因为前端妹子多。');

INSERT INTO luffy_teacher(id, orders, is_show, is_delete, created_time, updated_time, name, role, title, signature, image, brief) VALUES (3, 3, 1, 0, '2022-07-14 13:46:21.997846', '2022-07-14 13:46:21.997880', 'Lyy', 0, '老男孩Linux学科带头人', NULL, 'teacher/lyy_icon.png', 'Linux运维技术专家,老男孩Linux金牌讲师,讲课风趣幽默、深入浅出、声音洪亮到爆炸');


-- 分类表
INSERT INTO luffy_course_category(id, orders, is_show, is_delete, created_time, updated_time, name) VALUES (1, 1, 1, 0, '2022-07-14 13:40:58.690413', '2022-07-14 13:40:58.690477', 'Python');

INSERT INTO luffy_course_category(id, orders, is_show, is_delete, created_time, updated_time, name) VALUES (2, 2, 1, 0, '2022-07-14 13:41:08.249735', '2022-07-14 13:41:08.249817', 'Linux');


-- 课程表
INSERT INTO luffy_course(id, orders, is_show, is_delete, created_time, updated_time, name, course_img, course_type, brief, level, pub_date, period, attachment_path, status, students, sections, pub_sections, price, course_category_id, teacher_id) VALUES (1, 1, 1, 0, '2022-07-14 13:54:33.095201', '2022-07-14 13:54:33.095238', 'Python开发21天入门', 'courses/alex_python.png', 0, 'Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土', 0, '2022-07-14', 21, '', 0, 231, 120, 120, 0.00, 1, 1);

INSERT INTO luffy_course(id, orders, is_show, is_delete, created_time, updated_time, name, course_img, course_type, brief, level, pub_date, period, attachment_path, status, students, sections, pub_sections, price, course_category_id, teacher_id) VALUES (2, 2, 1, 0, '2022-07-14 13:56:05.051103', '2022-07-14 13:56:05.051142', 'Python项目实战', 'courses/mjj_python.png', 0, '', 1, '2022-07-14', 30, '', 0, 340, 120, 120, 99.00, 1, 2);

INSERT INTO luffy_course(id, orders, is_show, is_delete, created_time, updated_time, name, course_img, course_type, brief, level, pub_date, period, attachment_path, status, students, sections, pub_sections, price, course_category_id, teacher_id) VALUES (3, 3, 1, 0, '2022-07-14 13:57:21.190053', '2022-07-14 13:57:21.190095', 'Linux系统基础5周入门精讲', 'courses/lyy_linux.png', 0, '', 0, '2022-07-14', 25, '', 0, 219, 100, 100, 39.00, 2, 3);


-- 章节表
INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (1, 1, 1, 0, '2022-07-14 13:58:34.867005', '2022-07-14 14:00:58.276541', 1, '计算机原理', '', '2022-07-14', 1);

INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (2, 2, 1, 0, '2022-07-14 13:58:48.051543', '2022-07-14 14:01:22.024206', 2, '环境搭建', '', '2022-07-14', 1);

INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (3, 3, 1, 0, '2022-07-14 13:59:09.878183', '2022-07-14 14:01:40.048608', 1, '项目创建', '', '2022-07-14', 2);

INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (4, 4, 1, 0, '2022-07-14 13:59:37.448626', '2022-07-14 14:01:58.709652', 1, 'Linux环境创建', '', '2022-07-14', 3);


-- 课时表
INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (1, 1, 0, '2022-07-14 14:02:33.779098', '2022-07-14 14:02:33.779135', '计算机原理上', 1, 2, NULL, NULL, '2022-07-14 14:02:33.779193', 1, 1);

INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (2, 1, 0, '2022-07-14 14:02:56.657134', '2022-07-14 14:02:56.657173', '计算机原理下', 2, 2, NULL, NULL, '2022-07-14 14:02:56.657227', 1, 1);

INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (3, 1, 0, '2022-07-14 14:03:20.493324', '2022-07-14 14:03:52.329394', '环境搭建上', 1, 2, NULL, NULL, '2022-07-14 14:03:20.493420', 0, 2);

INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (4, 1, 0, '2022-07-14 14:03:36.472742', '2022-07-14 14:03:36.472779', '环境搭建下', 2, 2, NULL, NULL, '2022-07-14 14:03:36.472831', 0, 2);

INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (5, 1, 0, '2022-07-14 14:04:19.338153', '2022-07-14 14:04:19.338192', 'web项目的创建', 1, 2, NULL, NULL, '2022-07-14 14:04:19.338252', 1, 3);

INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (6, 1, 0, '2022-07-14 14:04:52.895855', '2022-07-14 14:04:52.895890', 'Linux的环境搭建', 1, 2, NULL, NULL, '2022-07-14 14:04:52.895942', 1, 4);

-- 课程
INSERT INTO `luffy_course` VALUES (4, '2022-04-28 12:06:36.564933', '2022-04-28 12:36:04.812789', 0, 1, 4, 'DRF从入门到放弃', 'courses/drf.png', 0, 'drf很牛逼', 4, '2022-04-28', 7, '', 0, 399, 0, 0, 77.00, 1, 1);
INSERT INTO `luffy_course` VALUES (5, '2022-04-28 12:35:44.319734', '2022-04-28 12:35:44.319757', 0, 1, 5, 'Go语言从入门到入坑', 'courses/msbd.png', 0, 'Go语言从入门到入坑Go语言从入门到入坑Go语言从入门到入坑Go语言从入门到入坑', 0, '2022-04-28', 20, '', 0, 30, 200, 100, 66.00, 3, 1);
INSERT INTO `luffy_course` VALUES (6, '2022-04-28 12:39:55.562716', '2022-04-28 12:39:55.562741', 0, 1, 6, 'Go语言微服务', 'courses/celery.png', 0, 'Go语言微服务Go语言微服务Go语言微服务Go语言微服务', 4, '2022-04-28', 7, '', 0, 122, 0, 0, 299.00, 3, 2);



-- 分类
INSERT INTO `luffy_course_category` VALUES (3, '2022-04-28 12:07:33.314057', '2022-04-28 12:07:33.314088', 0, 1, 3, 'Go语言');


-- 章节
INSERT INTO `luffy_course_chapter` VALUES (5, '2022-04-28 12:08:36.679922', '2022-04-28 12:08:36.680014', 0, 1, 2, 2, 'Linux5周第二章', 'Linux5周第二章Linux5周第二章Linux5周第二章Linux5周第二章Linux5周第二章', '2022-04-28', 3);
INSERT INTO `luffy_course_chapter` VALUES (6, '2022-04-28 12:09:19.324504', '2022-04-28 12:09:19.324533', 0, 1, 2, 2, 'py实战项目第二章', 'py实战项目第二章py实战项目第二章py实战项目第二章py实战项目第二章', '2022-04-28', 2);
INSERT INTO `luffy_course_chapter` VALUES (7, '2022-04-28 12:09:32.532905', '2022-04-29 10:11:57.546455', 0, 1, 3, 3, 'py实战项目第三章', 'py实战项目第三章py实战项目第三章py实战项目第三章', '2022-04-28', 2);
INSERT INTO `luffy_course_chapter` VALUES (8, '2022-04-28 12:09:55.496622', '2022-04-28 12:09:55.496686', 0, 1, 1, 1, 'drf入门1', 'drf入门1drf入门1drf入门1', '2022-04-28', 4);
INSERT INTO `luffy_course_chapter` VALUES (9, '2022-04-28 12:10:08.490618', '2022-04-28 12:10:08.490642', 0, 1, 2, 2, 'drf入门2', 'drf入门drf入门1drf入门1drf入门1drf入门1', '2022-04-28', 4);
INSERT INTO `luffy_course_chapter` VALUES (10, '2022-04-28 12:10:22.088684', '2022-04-28 12:10:22.088710', 0, 1, 3, 3, 'drf入门3', 'drf入门1drf入门1drf入门1drf入门1drf入门1drf入门1', '2022-04-28', 4);
INSERT INTO `luffy_course_chapter` VALUES (11, '2022-04-28 12:10:33.564141', '2022-04-28 12:10:33.564177', 0, 1, 4, 4, 'drf入门4', 'drf入门1drf入门1drf入门1drf入门1', '2022-04-28', 4);
INSERT INTO `luffy_course_chapter` VALUES (12, '2022-04-28 12:10:43.242918', '2022-04-28 12:10:43.242947', 0, 1, 5, 5, 'drf入门5', 'drf入门1drf入门1drf入门1drf入门1', '2022-04-28', 4);
INSERT INTO `luffy_course_chapter` VALUES (13, '2022-04-28 12:36:58.508995', '2022-04-28 12:36:58.509020', 0, 1, 1, 1, 'go第一章', 'go第一章', '2022-04-28', 5);
INSERT INTO `luffy_course_chapter` VALUES (14, '2022-04-28 12:37:08.588265', '2022-04-28 12:37:08.588287', 0, 1, 2, 2, 'go第二章', 'go第一章go第一章go第一章', '2022-04-28', 5);
INSERT INTO `luffy_course_chapter` VALUES (15, '2022-04-28 12:37:19.219405', '2022-04-28 12:37:19.219426', 0, 1, 3, 3, 'go第三章', 'go第一章go第一章go第一章', '2022-04-28', 5);
INSERT INTO `luffy_course_chapter` VALUES (16, '2022-04-28 12:40:11.445750', '2022-04-28 12:40:11.445774', 0, 1, 1, 1, '微服务第一章', '微服务第一章', '2022-04-28', 6);
INSERT INTO `luffy_course_chapter` VALUES (17, '2022-04-28 12:40:22.811647', '2022-04-28 12:40:22.811670', 0, 1, 2, 2, '微服务第二章', '微服务第二章微服务第二章微服务第二章', '2022-04-28', 6);

-- 课时
INSERT INTO `luffy_course_section` VALUES (7, '2022-04-28 12:12:01.304920', '2022-04-28 12:12:01.304994', 0, 1, '文件操作', 2, 2, NULL, NULL, '2022-04-28 12:12:01.305074', 0, 5);
INSERT INTO `luffy_course_section` VALUES (8, '2022-04-28 12:12:11.287759', '2022-04-28 12:12:11.287884', 0, 1, '软件操作', 2, 2, NULL, NULL, '2022-04-28 12:12:11.288079', 0, 5);
INSERT INTO `luffy_course_section` VALUES (9, '2022-04-28 12:12:26.326077', '2022-04-28 12:12:26.326112', 0, 1, '请求响应', 1, 2, NULL, NULL, '2022-04-28 12:12:26.326174', 0, 8);
INSERT INTO `luffy_course_section` VALUES (10, '2022-04-28 12:12:36.364356', '2022-04-28 12:12:36.364391', 0, 1, '序列化类', 2, 2, NULL, NULL, '2022-04-28 12:12:36.364446', 0, 8);
INSERT INTO `luffy_course_section` VALUES (11, '2022-04-28 12:12:48.306119', '2022-04-28 12:12:48.306187', 0, 1, '三大认证', 1, 2, NULL, NULL, '2022-04-28 12:12:48.306396', 0, 9);
INSERT INTO `luffy_course_section` VALUES (12, '2022-04-28 12:13:06.882558', '2022-04-28 12:13:06.882620', 0, 1, '认证', 2, 2, NULL, NULL, '2022-04-28 12:13:06.882826', 0, 9);
INSERT INTO `luffy_course_section` VALUES (13, '2022-04-28 12:13:15.799043', '2022-04-28 12:13:15.799084', 0, 1, 'jwt认证', 1, 2, NULL, NULL, '2022-04-28 12:13:15.799146', 0, 10);
INSERT INTO `luffy_course_section` VALUES (14, '2022-04-28 12:13:27.852981', '2022-04-28 12:13:27.853011', 0, 1, 'jwt认证2', 3, 2, NULL, NULL, '2022-04-28 12:13:27.853066', 0, 10);
INSERT INTO `luffy_course_section` VALUES (15, '2022-04-28 12:13:37.292779', '2022-04-28 12:13:37.292806', 0, 1, '后台管理', 1, 2, NULL, NULL, '2022-04-28 12:13:37.292855', 0, 11);
INSERT INTO `luffy_course_section` VALUES (16, '2022-04-28 12:13:51.194585', '2022-04-28 12:13:51.194612', 0, 1, '后台管理2', 2, 2, NULL, NULL, '2022-04-28 12:13:51.194660', 0, 11);
INSERT INTO `luffy_course_section` VALUES (17, '2022-04-28 12:14:05.334836', '2022-04-28 12:14:05.334902', 0, 1, 'rbac1', 1, 2, NULL, NULL, '2022-04-28 12:14:05.335053', 0, 12);
INSERT INTO `luffy_course_section` VALUES (18, '2022-04-28 12:14:14.039605', '2022-04-28 12:14:14.039770', 0, 1, 'rbac2', 2, 2, NULL, NULL, '2022-04-28 12:14:14.039895', 0, 12);
INSERT INTO `luffy_course_section` VALUES (19, '2022-04-28 12:37:34.682049', '2022-04-28 12:37:34.682072', 0, 1, '环境搭建', 1, 2, NULL, NULL, '2022-04-28 12:37:34.682116', 0, 13);
INSERT INTO `luffy_course_section` VALUES (20, '2022-04-28 12:37:46.317414', '2022-04-28 12:37:46.317440', 0, 1, '第一个helloworld', 2, 2, NULL, NULL, '2022-04-28 12:37:46.317483', 0, 13);
INSERT INTO `luffy_course_section` VALUES (21, '2022-04-28 12:37:54.200236', '2022-04-28 12:37:54.200257', 0, 1, '变量定义', 1, 2, NULL, NULL, '2022-04-28 12:37:54.200297', 0, 14);
INSERT INTO `luffy_course_section` VALUES (22, '2022-04-28 12:38:03.465663', '2022-04-28 12:38:03.465686', 0, 1, '常量', 2, 2, NULL, NULL, '2022-04-28 12:38:03.465731', 0, 14);
INSERT INTO `luffy_course_section` VALUES (23, '2022-04-28 12:38:13.144613', '2022-04-28 12:38:13.144636', 0, 1, 'go结构体', 1, 2, NULL, NULL, '2022-04-28 12:38:13.144679', 0, 15);
INSERT INTO `luffy_course_section` VALUES (24, '2022-04-28 12:38:26.312273', '2022-04-28 12:38:26.312306', 0, 1, 'go接口', 2, 2, NULL, NULL, '2022-04-28 12:38:26.312380', 0, 15);
INSERT INTO `luffy_course_section` VALUES (25, '2022-04-28 12:40:36.531566', '2022-04-29 10:12:42.497098', 0, 1, '微服务第一章第一课时', 1, 2, NULL, NULL, '2022-04-28 12:40:36.531625', 1, 16);
INSERT INTO `luffy_course_section` VALUES (26, '2022-04-28 12:40:45.120568', '2022-04-28 12:41:14.341536', 0, 1, '微服务第一章第二课时', 2, 2, NULL, NULL, '2022-04-28 12:40:45.120627', 0, 16);
INSERT INTO `luffy_course_section` VALUES (27, '2022-04-28 12:40:57.477026', '2022-04-28 12:40:57.477048', 0, 1, '微服务第二章第一课时', 1, 2, NULL, NULL, '2022-04-28 12:40:57.477088', 0, 17);
INSERT INTO `luffy_course_section` VALUES (28, '2022-04-28 12:41:04.673613', '2022-04-28 12:41:04.673634', 0, 1, '微服务第二章第二课时', 2, 2, NULL, NULL, '2022-04-28 12:41:04.673673', 0, 17);
数据录入

【课程分类接口】

  视图类

 1 from django.shortcuts import render
 2 from rest_framework.viewsets import GenericViewSet
 3 
 4 from utils.common_mixin import APIListModelMixin
 5 from .models import CourseCategory
 6 from .serializer import CourseCategorySerializer
 7 
 8 
 9 # Create your views here.
10 # 课程分类接口,因为是自动生成的路由,要继承APIListModelMixin
11 class CourseCategoryView(GenericViewSet, APIListModelMixin):
12     queryset = CourseCategory.objects.all().filter(is_delete=False, is_show=True).order_by('-orders')
13     serializer_class = CourseCategorySerializer

序列化类

1 from .models import CourseCategory
2 from rest_framework import serializers
3 
4 
5 class CourseCategorySerializer(serializers.ModelSerializer):
6     class Meta:
7         model = CourseCategory
8         fields = ['id', 'name']

路由

 1 from django.urls import path
 2 from .views import CourseCategoryView
 3 from rest_framework.routers import SimpleRouter
 4 
 5 router = SimpleRouter()
 6 router.register('category', CourseCategoryView, 'category')
 7 
 8 urlpatterns = [
 9 ]
10 urlpatterns += router.urls

 

【课程列表接口】

1 查询所有课程,带过滤,带分页,关联表数据也要返回
  -返回课时:如果总课时数,大于4,就返回4条,如果小于4,有多少返回多少

视图类

1 class CourseView(GenericViewSet,APIListModelMixin):
2     queryset = Course.objects.all().filter(is_delete=False, is_show=True).order_by('-orders')
3     serializer_class = CourseSerializer

序列化类

 1 class TeacherSerializer(serializers.ModelSerializer):
 2     class Meta:
 3         model = Teacher
 4         fields = [
 5             'name',
 6             'role_name',  # 重写
 7             'title',
 8             'signature',
 9             'image',
10             'brief'
11         ]
12 
13 
14 # 课程序列化类
15 class CourseSerializer(serializers.ModelSerializer):
16     teacher = TeacherSerializer()  # 子序列化
17 
18     class Meta:
19         model = Course
20         fields = [
21             'id',
22             'name',
23             'course_img',
24             'price',
25             'students',
26             'pub_sections',  # 发布多少课时
27             'sections',  # 列表页面不显示,详情接口会显示
28             'period',  # 建议学习周期
29             'brief',  # 列表页面不显示,详情接口会显示
30             'attachment_path',  # 文档地址
31             # choice字段,定制返回格式--》表模型中写
32             'course_type_name',
33             'level_name',
34             'status_name',
35             # 关联表
36             'teacher',  # teacher 所有数据---》子序列化
37             'section_list'  # 返回课时:如果总课时数,大于4,就返回4条,如果小于4,有多少返回多少--表模型
38         ]

表模型(有改动)

 1 # 课程表
 2 
 3 @property
 4     def course_type_name(self):
 5         return self.get_course_type_display()
 6 
 7     @property
 8     def level_name(self):
 9         return self.get_level_display()
10 
11     @property
12     def status_name(self):
13         return self.get_status_display()
14 
15     def section_list(self):
16         # 如果总课时数,大于4,就返回4条,如果小于4,有多少返回多少
17         l = []
18         # 先循环章节[课程拿到所有章节:反向],
19         for course_chapter in self.coursechapters.all():
20             # 再循环课时【从章节拿到课时:反】
21             for course_section in course_chapter.coursesections.all():
22                 l.append({
23                     'name': course_section.name,
24                     'section_link': course_section.section_link,
25                     'duration': course_section.duration,
26                     'free_trail': course_section.free_trail,
27                 })
28                 if len(l) == 4:
29                     return l
30         return l
31 
32 
33 ====================================
34 老师表
35     
36     def role_name(self):
37         return self.get_role_display()

路由
router.register('actual', CourseView, 'actual')

 

 。

【分页和过滤】

分页类

pip install django_filter,注册

1 from rest_framework.pagination import PageNumberPagination
2 
3 
4 class CommonPageNumberPagination(PageNumberPagination):
5     page_size = 2
6     page_query_param = 'page'
7     page_size_query_param = 'size'
8     max_page_size = 5

视图类

class CourseView(GenericViewSet, APIListModelMixin):
queryset = Course.objects.all().filter(is_delete=False, is_show=True).order_by('orders')
serializer_class = CourseSerializer
pagination_class = PageNumberPagination # 分页
# 过滤:按课程分类id号过滤
# 排序
filter_backends = [OrderingFilter, DjangoFilterBackend]
ordering_fields = ['id', 'students', 'price', ]
filterset_fields = ['course_category'] # 按分类过滤

minxin进一步封装

class APIListModelMixin(ListModelMixin):
    def list(self, request, *args, **kwargs):
        res = super().list(request, *args, **kwargs)
        # 分页封装
        if self.paginator:
            return APIResponse(
                count=res.data.get('count'),
                next=res.data.get('next'),
                previous=res.data.get('previous'),
                results=res.data.get('results'),
            )

        return APIResponse(results=res.data)

【课程详情接口】

 

 

# 1 写课程详情接口,只需要再视图类上配置 APIRetrieveModelMixin类即可
class CourseView(GenericViewSet, APIListModelMixin,APIRetrieveModelMixin)

# 2 但是存在问题是:课程详情页面的原型图上有些数据,没有返回的
  -1 方案一: 再写一个序列化类,通过重写get_serializer_class,控制不同请求,使用不同序列化类
  -在序列化类中使用两层子序列化

  -2 方案二:再写个接口--》查询所有章节接口+按课程过滤

 1 方案一
 2 
 3 视图类
 4 class CourseView(GenericViewSet, APIListModelMixin, APIRetrieveModelMixin):
 5     def get_serializer_class(self):
 6         if self.action == 'retrieve':
 7             return CourseDetailSerializer
 8         else:
 9             return CourseSerializer
10 
11 序列化类
12 # 课程详情序列化类
13 class CourseSectionSerializer(serializers.ModelSerializer):
14     class Meta:
15         model = CourseSection
16         fields = [
17             'id',
18             'name',
19             'orders',
20             'section_link',
21             'duration',
22             'free_trail'
23         ]
24 
25 
26 class CourseChapterSerializer(serializers.ModelSerializer):
27     coursesections = CourseSectionSerializer(many=True)  # 子序列化,多条,要写many=True
28 
29     class Meta:
30         model = CourseChapter
31         fields = [
32             'id',
33             'name',
34             'coursesections',
35         ]
36 
37 
38 class CourseDetailSerializer(serializers.ModelSerializer):
39     teacher = TeacherSerializer()  # 子序列化
40     coursechapters = CourseChapterSerializer(many=True)  # 子序列化,多条,要写many=True
41 
42     class Meta:
43         model = Course
44         fields = [
45             'id',
46             'name',
47             'course_img',
48             'price',  #
49             'students',
50             'pub_sections',  # 发布多少课时
51             'sections',
52 
53             # 列表页面不显示,详情接口会显示
54             'period',  # 建议学习周期
55             'brief',
56             'attachment_path',  # 文档地址
57 
58             # choice字段,定制返回格式--》表模型中写
59             'course_type_name',
60             'level_name',
61             'status_name',
62             # 关联表
63             'teacher',  # teacher 所有数据---》子序列化
64             'coursechapters',  # 所有章节
65         ]
66 
67 
68 =====================================
69 方案二:
70 # 查询所有章节接口,带按课程过滤
71 class CourseChapterView(GenericViewSet, APIListModelMixin):
72     queryset = CourseChapter.objects.all().filter(is_delete=False, is_show=True)
73     serializer_class = CourseChapterSerializer
74     filter_backends = [DjangoFilterBackend]
75     filterset_fields = ['course']  # 按课程过滤

 。

【课程列表前端】

 

  1 <template>
  2   <div class="course">
  3     <Header></Header>
  4     <div class="main">
  5 <!--      teacher字段改过,有问题来找这个?-->
  6       <!-- 筛选条件 -->
  7       <div class="condition">
  8         <ul class="cate-list">
  9           <li class="title">课程分类:</li>
 10           <li :class="filter.course_category==0?'this':''" @click="filter.course_category=0">全部</li>
 11           <li :class="filter.course_category==category.id?'this':''" v-for="category in category_list"
 12               @click="filter.course_category=category.id" :key="category.name">{{ category.name }}
 13           </li>
 14         </ul>
 15 
 16         <div class="ordering">
 17           <ul>
 18             <li class="title">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;选:</li>
 19             <li class="default" :class="(filter.ordering=='id' || filter.ordering=='-id')?'this':''"
 20                 @click="filter.ordering='-id'">默认
 21             </li>
 22             <li class="hot" :class="(filter.ordering=='students' || filter.ordering=='-students')?'this':''"
 23                 @click="filter.ordering=(filter.ordering=='-students'?'students':'-students')">人气
 24             </li>
 25             <li class="price"
 26                 :class="filter.ordering=='price'?'price_up this':(filter.ordering=='-price'?'price_down this':'')"
 27                 @click="filter.ordering=(filter.ordering=='-price'?'price':'-price')">价格
 28             </li>
 29           </ul>
 30           <p class="condition-result">共{{ course_total }}个课程</p>
 31         </div>
 32 
 33       </div>
 34       <!-- 课程列表 -->
 35       <div class="course-list">
 36         <div class="course-item" v-for="course in course_list" :key="course.name">
 37           <div class="course-image">
 38             <img :src="course.course_img" alt="">
 39           </div>
 40           <div class="course-info">
 41             <h3>
 42               <router-link :to="'/actual/detail/'+course.id">{{ course.name }}</router-link>
 43               <span><img src="@/assets/img/avatar1.svg" alt="">{{ course.students }}人已加入学习</span></h3>
 44             <p class="teacher-info">
 45               {{ course.teacher.name }} {{ course.teacher.title }} {{ course.teacher.signature }}
 46               <span
 47                   v-if="course.sections>course.pub_sections">共{{ course.sections }}课时/已更新{{ course.pub_sections }}课时</span>
 48               <span v-else>共{{ course.sections }}课时/更新完成</span>
 49             </p>
 50             <ul class="section-list">
 51               <li v-for="(section, key) in course.section_list" :key="section.name"><span
 52                   class="section-title">0{{ key + 1 }}  |  {{ section.name }}</span>
 53                 <span class="free" v-if="section.free_trail">免费</span></li>
 54             </ul>
 55             <div class="pay-box">
 56               <div v-if="course.discount_type">
 57                 <span class="discount-type">{{ course.discount_type }}</span>
 58                 <span class="discount-price">¥{{ course.real_price }}元</span>
 59                 <span class="original-price">原价:{{ course.price }}元</span>
 60               </div>
 61               <span v-else class="discount-price">¥{{ course.price }}元</span>
 62               <span class="buy-now">立即购买</span>
 63             </div>
 64           </div>
 65         </div>
 66       </div>
 67       <div class="course_pagination block">
 68         <el-pagination
 69             @size-change="handleSizeChange"
 70             @current-change="handleCurrentChange"
 71             :current-page.sync="filter.page"
 72             :page-sizes="[2, 3, 5, 10]"
 73             :page-size="filter.page_size"
 74             layout="sizes, prev, pager, next"
 75             :total="course_total">
 76         </el-pagination>
 77       </div>
 78     </div>
 79     <Footer></Footer>
 80   </div>
 81 </template>
 82 
 83 <script>
 84 import Header from "@/components/Header"
 85 import Footer from "@/components/Footer"
 86 import api from '../assets/js/settings'
 87 
 88 export default {
 89   name: "Course",
 90   data() {
 91     return {
 92       category_list: [], // 课程分类列表
 93       course_list: [],   // 课程列表
 94       course_total: 0,   // 当前课程的总数量
 95       filter: {
 96         course_category: 0, // 当前用户选择的课程分类,刚进入页面默认为全部,值为0
 97         ordering: "-id",    // 数据的排序方式,默认值是-id,表示对于id进行降序排列
 98         page_size: 2,       // 单页数据量
 99         page: 1,
100       }
101     }
102   },
103   created() {
104     this.get_category();
105     this.get_course();
106   },
107   components: {
108     Header,
109     Footer,
110   },
111   watch: {
112     "filter.course_category": function () {
113       this.filter.page = 1;
114       this.get_course();
115     },
116     "filter.ordering": function () {
117       this.get_course();
118     },
119     "filter.page_size": function () {
120       this.get_course();
121     },
122     "filter.page": function () {
123       this.get_course();
124     }
125   },
126   methods: {
127 
128     handleSizeChange(val) {
129       // 每页数据量发生变化时执行的方法
130       this.filter.page = 1;
131       this.filter.page_size = val;
132     },
133     handleCurrentChange(val) {
134       // 页码发生变化时执行的方法
135       this.filter.page = val;
136     },
137     get_category() {
138       // 获取课程分类信息
139       this.$axios.get(api.course_category).then(response => {
140         this.category_list = response.data.results;
141       }).catch(() => {
142         this.$message({
143           message: "获取课程分类信息有误,请联系客服工作人员",
144         })
145       })
146     },
147     get_course() {
148       // 排序
149       let filters = {
150         ordering: this.filter.ordering, // 排序
151       };
152       // 判决是否进行分类课程的展示
153       if (this.filter.course_category > 0) {
154         filters.course_category = this.filter.course_category;
155       }
156 
157       // 设置单页数据量
158       if (this.filter.page_size > 0) {
159         filters.page_size = this.filter.page_size;
160       } else {
161         filters.page_size = 5;
162       }
163 
164       // 设置当前页码
165       if (this.filter.page > 1) {
166         filters.page = this.filter.page;
167       } else {
168         filters.page = 1;
169       }
170 
171 
172       // 获取课程列表信息
173       this.$axios.get(api.actual, {
174         params: filters
175       }).then(response => {
176         this.course_list = response.data.results;
177         this.course_total = response.data.count;
178       }).catch(() => {
179         this.$message({
180           message: "获取课程信息有误,请联系客服工作人员"
181         })
182       })
183     }
184   }
185 }
186 </script>
187 
188 <style scoped>
189 .course {
190   background: #f6f6f6;
191 }
192 
193 .course .main {
194   width: 1100px;
195   margin: 35px auto 0;
196 }
197 
198 .course .condition {
199   margin-bottom: 35px;
200   padding: 25px 30px 25px 20px;
201   background: #fff;
202   border-radius: 4px;
203   box-shadow: 0 2px 4px 0 #f0f0f0;
204 }
205 
206 .course .cate-list {
207   border-bottom: 1px solid #333;
208   border-bottom-color: rgba(51, 51, 51, .05);
209   padding-bottom: 18px;
210   margin-bottom: 17px;
211 }
212 
213 .course .cate-list::after {
214   content: "";
215   display: block;
216   clear: both;
217 }
218 
219 .course .cate-list li {
220   float: left;
221   font-size: 16px;
222   padding: 6px 15px;
223   line-height: 16px;
224   margin-left: 14px;
225   position: relative;
226   transition: all .3s ease;
227   cursor: pointer;
228   color: #4a4a4a;
229   border: 1px solid transparent; /* transparent 透明 */
230 }
231 
232 .course .cate-list .title {
233   color: #888;
234   margin-left: 0;
235   letter-spacing: .36px;
236   padding: 0;
237   line-height: 28px;
238 }
239 
240 .course .cate-list .this {
241   color: #ffc210;
242   border: 1px solid #ffc210 !important;
243   border-radius: 30px;
244 }
245 
246 .course .ordering::after {
247   content: "";
248   display: block;
249   clear: both;
250 }
251 
252 .course .ordering ul {
253   float: left;
254 }
255 
256 .course .ordering ul::after {
257   content: "";
258   display: block;
259   clear: both;
260 }
261 
262 .course .ordering .condition-result {
263   float: right;
264   font-size: 14px;
265   color: #9b9b9b;
266   line-height: 28px;
267 }
268 
269 .course .ordering ul li {
270   float: left;
271   padding: 6px 15px;
272   line-height: 16px;
273   margin-left: 14px;
274   position: relative;
275   transition: all .3s ease;
276   cursor: pointer;
277   color: #4a4a4a;
278 }
279 
280 .course .ordering .title {
281   font-size: 16px;
282   color: #888;
283   letter-spacing: .36px;
284   margin-left: 0;
285   padding: 0;
286   line-height: 28px;
287 }
288 
289 .course .ordering .this {
290   color: #ffc210;
291 }
292 
293 .course .ordering .price {
294   position: relative;
295 }
296 
297 .course .ordering .price::before,
298 .course .ordering .price::after {
299   cursor: pointer;
300   content: "";
301   display: block;
302   width: 0px;
303   height: 0px;
304   border: 5px solid transparent;
305   position: absolute;
306   right: 0;
307 }
308 
309 .course .ordering .price::before {
310   border-bottom: 5px solid #aaa;
311   margin-bottom: 2px;
312   top: 2px;
313 }
314 
315 .course .ordering .price::after {
316   border-top: 5px solid #aaa;
317   bottom: 2px;
318 }
319 
320 .course .ordering .price_up::before {
321   border-bottom-color: #ffc210;
322 }
323 
324 .course .ordering .price_down::after {
325   border-top-color: #ffc210;
326 }
327 
328 .course .course-item:hover {
329   box-shadow: 4px 6px 16px rgba(0, 0, 0, .5);
330 }
331 
332 .course .course-item {
333   width: 1100px;
334   background: #fff;
335   padding: 20px 30px 20px 20px;
336   margin-bottom: 35px;
337   border-radius: 2px;
338   cursor: pointer;
339   box-shadow: 2px 3px 16px rgba(0, 0, 0, .1);
340   /* css3.0 过渡动画 hover 事件操作 */
341   transition: all .2s ease;
342 }
343 
344 .course .course-item::after {
345   content: "";
346   display: block;
347   clear: both;
348 }
349 
350 /* 顶级元素 父级元素  当前元素{} */
351 .course .course-item .course-image {
352   float: left;
353   width: 423px;
354   height: 210px;
355   margin-right: 30px;
356 }
357 
358 .course .course-item .course-image img {
359   max-width: 100%;
360   max-height: 210px;
361 }
362 
363 .course .course-item .course-info {
364   float: left;
365   width: 596px;
366 }
367 
368 .course-item .course-info h3 a {
369   font-size: 26px;
370   color: #333;
371   font-weight: normal;
372   margin-bottom: 8px;
373 }
374 
375 .course-item .course-info h3 span {
376   font-size: 14px;
377   color: #9b9b9b;
378   float: right;
379   margin-top: 14px;
380 }
381 
382 .course-item .course-info h3 span img {
383   width: 11px;
384   height: auto;
385   margin-right: 7px;
386 }
387 
388 .course-item .course-info .teather-info {
389   font-size: 14px;
390   color: #9b9b9b;
391   margin-bottom: 14px;
392   padding-bottom: 14px;
393   border-bottom: 1px solid #333;
394   border-bottom-color: rgba(51, 51, 51, .05);
395 }
396 
397 .course-item .course-info .teather-info span {
398   float: right;
399 }
400 
401 .course-item .section-list::after {
402   content: "";
403   display: block;
404   clear: both;
405 }
406 
407 .course-item .section-list li {
408   float: left;
409   width: 44%;
410   font-size: 14px;
411   color: #666;
412   padding-left: 22px;
413   /* background: url("路径") 是否平铺 x轴位置 y轴位置 */
414   background: url("/src/assets/img/play-icon-gray.svg") no-repeat left 4px;
415   margin-bottom: 15px;
416 }
417 
418 .course-item .section-list li .section-title {
419   /* 以下3句,文本内容过多,会自动隐藏,并显示省略符号 */
420   text-overflow: ellipsis;
421   overflow: hidden;
422   white-space: nowrap;
423   display: inline-block;
424   max-width: 200px;
425 }
426 
427 .course-item .section-list li:hover {
428   background-image: url("/src/assets/img/play-icon-yellow.svg");
429   color: #ffc210;
430 }
431 
432 .course-item .section-list li .free {
433   width: 34px;
434   height: 20px;
435   color: #fd7b4d;
436   vertical-align: super;
437   margin-left: 10px;
438   border: 1px solid #fd7b4d;
439   border-radius: 2px;
440   text-align: center;
441   font-size: 13px;
442   white-space: nowrap;
443 }
444 
445 .course-item .section-list li:hover .free {
446   color: #ffc210;
447   border-color: #ffc210;
448 }
449 
450 .course-item {
451   position: relative;
452 }
453 
454 .course-item .pay-box {
455   position: absolute;
456   bottom: 20px;
457   width: 600px;
458 }
459 
460 .course-item .pay-box::after {
461   content: "";
462   display: block;
463   clear: both;
464 }
465 
466 .course-item .pay-box .discount-type {
467   padding: 6px 10px;
468   font-size: 16px;
469   color: #fff;
470   text-align: center;
471   margin-right: 8px;
472   background: #fa6240;
473   border: 1px solid #fa6240;
474   border-radius: 10px 0 10px 0;
475   float: left;
476 }
477 
478 .course-item .pay-box .discount-price {
479   font-size: 24px;
480   color: #fa6240;
481   float: left;
482 }
483 
484 .course-item .pay-box .original-price {
485   text-decoration: line-through;
486   font-size: 14px;
487   color: #9b9b9b;
488   margin-left: 10px;
489   float: left;
490   margin-top: 10px;
491 }
492 
493 .course-item .pay-box .buy-now {
494   width: 120px;
495   height: 38px;
496   background: transparent;
497   color: #fa6240;
498   font-size: 16px;
499   border: 1px solid #fd7b4d;
500   border-radius: 3px;
501   transition: all .2s ease-in-out;
502   float: right;
503   text-align: center;
504   line-height: 38px;
505   position: absolute;
506   right: 0;
507   bottom: 5px;
508 }
509 
510 .course-item .pay-box .buy-now:hover {
511   color: #fff;
512   background: #ffc210;
513   border: 1px solid #ffc210;
514 }
515 
516 .course .course_pagination {
517   margin-bottom: 60px;
518   text-align: center;
519 }
520 </style>
ActualCourseView
 
  1 <template>
  2   <div class="detail">
  3     <Header/>
  4     <div class="main">
  5       <div class="course-info">
  6         <div class="wrap-left">
  7           <vue-core-video-player :src="mp4_url"
  8                                  :muted="true"
  9                                  :autoplay="false"
 10                                  title="致命诱惑"
 11                                  preload="none"
 12                                  :loop="true"
 13                                  controls="auto"
 14                                  cover='http://127.0.0.1:8000/media/icon/default.png'
 15                                  @play="playFunc"
 16                                  @pause="pauseFunc"></vue-core-video-player>
 17         </div>
 18         <div class="wrap-right">
 19           <h3 class="course-name">{{ course_info.name }}</h3>
 20           <p class="data">
 21             {{ course_info.students }}人在学&nbsp;&nbsp;&nbsp;&nbsp;课程总时长:{{
 22               course_info.sections
 23             }}课时/{{ course_info.pub_sections }}小时&nbsp;&nbsp;&nbsp;&nbsp;难度:{{ course_info.level_name }}</p>
 24           <div class="sale-time">
 25             <p class="sale-type">价格 <span class="original_price">¥{{ course_info.price }}</span></p>
 26             <p class="expire"></p>
 27           </div>
 28           <div class="buy">
 29             <div class="buy-btn">
 30               <button class="buy-now">立即购买</button>
 31               <button class="free">免费试学</button>
 32             </div>
 33             <!--<div class="add-cart" @click="add_cart(course_info.id)">-->
 34             <!--<img src="@/assets/img/cart-yellow.svg" alt="">加入购物车-->
 35             <!--</div>-->
 36           </div>
 37         </div>
 38       </div>
 39       <div class="course-tab">
 40         <ul class="tab-list">
 41           <li :class="tabIndex==1?'active':''" @click="tabIndex=1">详情介绍</li>
 42           <li :class="tabIndex==2?'active':''" @click="tabIndex=2">课程章节 <span :class="tabIndex!=2?'free':''">(试学)</span>
 43           </li>
 44           <li :class="tabIndex==3?'active':''" @click="tabIndex=3">用户评论</li>
 45           <li :class="tabIndex==4?'active':''" @click="tabIndex=4">常见问题</li>
 46         </ul>
 47       </div>
 48       <div class="course-content">
 49         <div class="course-tab-list">
 50           <div class="tab-item" v-if="tabIndex==1">
 51             <div class="course-brief" v-html="course_info.brief"></div>
 52           </div>
 53           <div class="tab-item" v-if="tabIndex==2">
 54             <div class="tab-item-title">
 55               <p class="chapter">课程章节</p>
 56               <p class="chapter-length">共{{ course_chapters.length }}章 {{ course_info.sections }}个课时</p>
 57             </div>
 58             <div class="chapter-item" v-for="chapter in course_chapters" :key="chapter.name">
 59               <p class="chapter-title"><img src="@/assets/img/enum.svg"
 60                                             alt="">第{{ chapter.chapter }}章·{{ chapter.name }}
 61               </p>
 62               <ul class="section-list">
 63                 <li class="section-item" v-for="section in chapter.coursesections" :key="section.name">
 64                   <p class="name"><span class="index">{{ chapter.chapter }}-{{ section.orders }}</span>
 65                     {{ section.name }}<span class="free" v-if="section.free_trail">免费</span></p>
 66                   <p class="time">{{ section.duration }} <img src="@/assets/img/chapter-player.svg"></p>
 67                   <button class="try" v-if="section.free_trail">立即试学</button>
 68                   <button class="try" v-else>立即购买</button>
 69                 </li>
 70               </ul>
 71             </div>
 72           </div>
 73           <div class="tab-item" v-if="tabIndex==3">
 74             用户评论
 75           </div>
 76           <div class="tab-item" v-if="tabIndex==4">
 77             常见问题
 78           </div>
 79         </div>
 80         <div class="course-side">
 81           <div class="teacher-info">
 82             <h4 class="side-title"><span>授课老师</span></h4>
 83             <div class="teacher-content">
 84               <div class="cont1">
 85                 <img :src="course_info.teacher.image">
 86                 <div class="name">
 87                   <p class="teacher-name">{{ course_info.teacher.name }}
 88                     {{ course_info.teacher.title }}</p>
 89                   <p class="teacher-title">{{ course_info.teacher.signature }}</p>
 90                 </div>
 91               </div>
 92               <p class="narrative">{{ course_info.teacher.brief }}</p>
 93             </div>
 94           </div>
 95         </div>
 96       </div>
 97     </div>
 98     <Footer/>
 99   </div>
100 </template>
101 
102 <script>
103 import Header from "@/components/Header"
104 import Footer from "@/components/Footer"
105 import api from '../assets/js/settings'
106 export default {
107   name: "Detail",
108   data() {
109     return {
110       tabIndex: 2,   // 当前选项卡显示的下标
111       course_id: 0, // 当前课程信息的ID
112       course_info: {
113         teacher: {},
114       }, // 课程信息
115       course_chapters: [], // 课程的章节课时列表
116       // mp4_url: [
117       //   {
118       //     src: 'https://video.pearvideo.com/mp4/short/20240516/cont-1794244-71106834-hd.mp4',
119       //     resolution: 360,
120       //   },
121       //   {
122       //     src: 'https://video.pearvideo.com/mp4/short/20240517/cont-1794270-71106842-hd.mp4',
123       //     resolution: 720,
124       //   },
125       //   {
126       //     src: 'https://video.pearvideo.com/mp4/short/20240125/cont-1791495-16017396-hd.mp4',
127       //     resolution: '4k',
128       //
129       //   }],
130       mp4_url: 'http://sdtv9qtii.hd-bkt.clouddn.com/7%20%E5%AE%89%E8%A3%85git%E5%92%8C%E5%85%B6%E4%BB%96%E4%BE%9D%E8%B5%96.mp4'
131 
132     }
133   },
134   created() {
135     this.get_course_id();
136     this.get_course_data();
137     this.get_chapter();
138   },
139   methods: {
140     playFunc() {
141       // 当视频播放时,执行的方法
142       console.log('视频开始播放')
143     },
144     pauseFunc() {
145       // 当视频暂停播放时,执行的方法
146       console.log('视频暂停,可以打开广告了')
147     },
148     get_course_id() {
149       // 获取地址栏上面的课程ID
150       this.course_id = this.$route.params.pk
151       if (this.course_id < 1) {
152         let _this = this;
153         _this.$alert("对不起,当前视频不存在!", "警告", {
154           callback() {
155             _this.$router.go(-1);
156           }
157         });
158       }
159     },
160     get_course_data() {
161       // ajax请求课程信息
162       this.$axios.get(`${api.actual}${this.course_id}/`).then(response => {
163         this.course_info = response.data.result
164       }).catch(() => {
165         this.$message({
166           message: "对不起,访问页面出错!请联系客服工作人员!"
167         });
168       })
169     },
170 
171     get_chapter() {
172       // 获取当前课程对应的章节课时信息
173       this.$axios.get(api.chapter, {
174         params: {
175           "course": this.course_id,
176         }
177       }).then(response => {
178         this.course_chapters = response.data.results;
179       }).catch(error => {
180         this.$message({
181           message: "对不起,访问页面出错!请联系客服工作人员!"
182         });
183       })
184     },
185   },
186   components: {
187     Header,
188     Footer,
189   }
190 }
191 </script>
192 
193 <style scoped>
194 .main {
195   background: #fff;
196   padding-top: 30px;
197 }
198 
199 .course-info {
200   width: 1200px;
201   margin: 0 auto;
202   overflow: hidden;
203 }
204 
205 .wrap-left {
206   float: left;
207   width: 690px;
208   height: 388px;
209   background-color: #000;
210 }
211 
212 .wrap-right {
213   float: left;
214   position: relative;
215   height: 388px;
216 }
217 
218 .course-name {
219   font-size: 20px;
220   color: #333;
221   padding: 10px 23px;
222   letter-spacing: .45px;
223 }
224 
225 .data {
226   padding-left: 23px;
227   padding-right: 23px;
228   padding-bottom: 16px;
229   font-size: 14px;
230   color: #9b9b9b;
231 }
232 
233 .sale-time {
234   width: 464px;
235   background: #fa6240;
236   font-size: 14px;
237   color: #4a4a4a;
238   padding: 10px 23px;
239   overflow: hidden;
240 }
241 
242 .sale-type {
243   font-size: 16px;
244   color: #fff;
245   letter-spacing: .36px;
246   float: left;
247 }
248 
249 .sale-time .expire {
250   font-size: 14px;
251   color: #fff;
252   float: right;
253 }
254 
255 .sale-time .expire .second {
256   width: 24px;
257   display: inline-block;
258   background: #fafafa;
259   color: #5e5e5e;
260   padding: 6px 0;
261   text-align: center;
262 }
263 
264 .course-price {
265   background: #fff;
266   font-size: 14px;
267   color: #4a4a4a;
268   padding: 5px 23px;
269 }
270 
271 .discount {
272   font-size: 26px;
273   color: #fa6240;
274   margin-left: 10px;
275   display: inline-block;
276   margin-bottom: -5px;
277 }
278 
279 .original {
280   font-size: 14px;
281   color: #9b9b9b;
282   margin-left: 10px;
283   text-decoration: line-through;
284 }
285 
286 .buy {
287   width: 464px;
288   padding: 0px 23px;
289   position: absolute;
290   left: 0;
291   bottom: 20px;
292   overflow: hidden;
293 }
294 
295 .buy .buy-btn {
296   float: left;
297 }
298 
299 .buy .buy-now {
300   width: 125px;
301   height: 40px;
302   border: 0;
303   background: #ffc210;
304   border-radius: 4px;
305   color: #fff;
306   cursor: pointer;
307   margin-right: 15px;
308   outline: none;
309 }
310 
311 .buy .free {
312   width: 125px;
313   height: 40px;
314   border-radius: 4px;
315   cursor: pointer;
316   margin-right: 15px;
317   background: #fff;
318   color: #ffc210;
319   border: 1px solid #ffc210;
320 }
321 
322 .add-cart {
323   float: right;
324   font-size: 14px;
325   color: #ffc210;
326   text-align: center;
327   cursor: pointer;
328   margin-top: 10px;
329 }
330 
331 .add-cart img {
332   width: 20px;
333   height: 18px;
334   margin-right: 7px;
335   vertical-align: middle;
336 }
337 
338 .course-tab {
339   width: 100%;
340   background: #fff;
341   margin-bottom: 30px;
342   box-shadow: 0 2px 4px 0 #f0f0f0;
343 
344 }
345 
346 .course-tab .tab-list {
347   width: 1200px;
348   margin: auto;
349   color: #4a4a4a;
350   overflow: hidden;
351 }
352 
353 .tab-list li {
354   float: left;
355   margin-right: 15px;
356   padding: 26px 20px 16px;
357   font-size: 17px;
358   cursor: pointer;
359 }
360 
361 .tab-list .active {
362   color: #ffc210;
363   border-bottom: 2px solid #ffc210;
364 }
365 
366 .tab-list .free {
367   color: #fb7c55;
368 }
369 
370 .course-content {
371   width: 1200px;
372   margin: 0 auto;
373   background: #FAFAFA;
374   overflow: hidden;
375   padding-bottom: 40px;
376 }
377 
378 .course-tab-list {
379   width: 880px;
380   height: auto;
381   padding: 20px;
382   background: #fff;
383   float: left;
384   box-sizing: border-box;
385   overflow: hidden;
386   position: relative;
387   box-shadow: 0 2px 4px 0 #f0f0f0;
388 }
389 
390 .tab-item {
391   width: 880px;
392   background: #fff;
393   padding-bottom: 20px;
394   box-shadow: 0 2px 4px 0 #f0f0f0;
395 }
396 
397 .tab-item-title {
398   justify-content: space-between;
399   padding: 25px 20px 11px;
400   border-radius: 4px;
401   margin-bottom: 20px;
402   border-bottom: 1px solid #333;
403   border-bottom-color: rgba(51, 51, 51, .05);
404   overflow: hidden;
405 }
406 
407 .chapter {
408   font-size: 17px;
409   color: #4a4a4a;
410   float: left;
411 }
412 
413 .chapter-length {
414   float: right;
415   font-size: 14px;
416   color: #9b9b9b;
417   letter-spacing: .19px;
418 }
419 
420 .chapter-title {
421   font-size: 16px;
422   color: #4a4a4a;
423   letter-spacing: .26px;
424   padding: 12px;
425   background: #eee;
426   border-radius: 2px;
427   display: -ms-flexbox;
428   display: flex;
429   -ms-flex-align: center;
430   align-items: center;
431 }
432 
433 .chapter-title img {
434   width: 18px;
435   height: 18px;
436   margin-right: 7px;
437   vertical-align: middle;
438 }
439 
440 .section-list {
441   padding: 0 20px;
442 }
443 
444 .section-list .section-item {
445   padding: 15px 20px 15px 36px;
446   cursor: pointer;
447   justify-content: space-between;
448   position: relative;
449   overflow: hidden;
450 }
451 
452 .section-item .name {
453   font-size: 14px;
454   color: #666;
455   float: left;
456 }
457 
458 .section-item .index {
459   margin-right: 5px;
460 }
461 
462 .section-item .free {
463   font-size: 12px;
464   color: #fff;
465   letter-spacing: .19px;
466   background: #ffc210;
467   border-radius: 100px;
468   padding: 1px 9px;
469   margin-left: 10px;
470 }
471 
472 .section-item .time {
473   font-size: 14px;
474   color: #666;
475   letter-spacing: .23px;
476   opacity: 1;
477   transition: all .15s ease-in-out;
478   float: right;
479 }
480 
481 .section-item .time img {
482   width: 18px;
483   height: 18px;
484   margin-left: 15px;
485   vertical-align: text-bottom;
486 }
487 
488 .section-item .try {
489   width: 86px;
490   height: 28px;
491   background: #ffc210;
492   border-radius: 4px;
493   font-size: 14px;
494   color: #fff;
495   position: absolute;
496   right: 20px;
497   top: 10px;
498   opacity: 0;
499   transition: all .2s ease-in-out;
500   cursor: pointer;
501   outline: none;
502   border: none;
503 }
504 
505 .section-item:hover {
506   background: #fcf7ef;
507   box-shadow: 0 0 0 0 #f3f3f3;
508 }
509 
510 .section-item:hover .name {
511   color: #333;
512 }
513 
514 .section-item:hover .try {
515   opacity: 1;
516 }
517 
518 .course-side {
519   width: 300px;
520   height: auto;
521   margin-left: 20px;
522   float: right;
523 }
524 
525 .teacher-info {
526   background: #fff;
527   margin-bottom: 20px;
528   box-shadow: 0 2px 4px 0 #f0f0f0;
529 }
530 
531 .side-title {
532   font-weight: normal;
533   font-size: 17px;
534   color: #4a4a4a;
535   padding: 18px 14px;
536   border-bottom: 1px solid #333;
537   border-bottom-color: rgba(51, 51, 51, .05);
538 }
539 
540 .side-title span {
541   display: inline-block;
542   border-left: 2px solid #ffc210;
543   padding-left: 12px;
544 }
545 
546 .teacher-content {
547   padding: 30px 20px;
548   box-sizing: border-box;
549 }
550 
551 .teacher-content .cont1 {
552   margin-bottom: 12px;
553   overflow: hidden;
554 }
555 
556 .teacher-content .cont1 img {
557   width: 54px;
558   height: 54px;
559   margin-right: 12px;
560   float: left;
561 }
562 
563 .teacher-content .cont1 .name {
564   float: right;
565 }
566 
567 .teacher-content .cont1 .teacher-name {
568   width: 188px;
569   font-size: 16px;
570   color: #4a4a4a;
571   padding-bottom: 4px;
572 }
573 
574 .teacher-content .cont1 .teacher-title {
575   width: 188px;
576   font-size: 13px;
577   color: #9b9b9b;
578   white-space: nowrap;
579 }
580 
581 .teacher-content .narrative {
582   font-size: 14px;
583   color: #666;
584   line-height: 24px;
585 }
586 </style>
ActualCourseDetail.vue

 

【视频托管】

 1 # 1  视频托管
 2     -1 放在项目media中(图片还行,视频不好)
 3     -2 第三方视频托管
 4         -保利威视频
 5         -文件托管
 6             -七牛云托管
 7             -阿里oss
 8             -腾讯云:文件存储
 9     - 3 自己搭建文件服务器
10         -ceph
11         -fastdfs
12         -minIo
13             
14 # 2 七牛云视频托管
15     -1注册账号
16     -2 创建空间,会自动绑定一个测试域名
17     -3 手动,代码上传视频
18         -地址
19     -4 通过地址访问视频
20     
21 # 3 代码上传
22     -pip install qiniu
23 
24 # 4代码
25 from qiniu import Auth, put_file, etag
26 import qiniu.config
27 
28 # 需要填写你的 Access Key 和 Secret Key
29 access_key = 'Mv6JYHQnmDw_w6v72sKg85PbIik_dzwrEshq6CnT'
30 secret_key = 'BkkknlGf7XmRWkfL2aBKT9goQQJNDHMuyBb9S9i1'
31 # 构建鉴权对象
32 q = Auth(access_key, secret_key)
33 # 要上传的空间
34 bucket_name = 'luffy-test1'
35 # 上传后保存的文件名
36 key = '总结.mp4'
37 # 生成上传 Token,可以指定过期时间等
38 token = q.upload_token(bucket_name, key, 3600)
39 # 要上传文件的本地路径
40 localfile = './总结.mp4'
41 ret, info = put_file(token, key, localfile, version='v2')
42 print(info)
43 assert ret['key'] == key
44 assert ret['hash'] == etag(localfile)

 

 

 

使用代码上传到minio

 1 #pip install minio
 2 
 3 from minio import Minio
 4 
 5 # 使用endpoint、access key和secret key来初始化minioClient对象。
 6 minioClient = Minio('192.168.1.252:9000',
 7                     access_key='B1SKQVR6PRS1DT0NCSYM',
 8                     secret_key='Nqk3O0lHsbrv58OtyiMoCI41ZnTmSCMhsZZ2hptS',
 9                     secure=False)
10 # 调用make_bucket来创建一个存储桶。
11 # minioClient.make_bucket("maylogs", location="us-east-1")
12 # test01 为桶名字
13 res = minioClient.fput_object('lqz-test', 'lqz.jpg', './lqz.jpg')
14 print(res.object_name)
15 print('http://192.168.1.252:9090/lqz-test/lqz.jpg')
16 print('文件地址为【文件在浏览器打开会直接下载,放到index.html 中使用img引入查看】:\n', 'http://192.168.1.252:9000/test01/' + res.object_name)

 

使用代码上传到fastdfs

 1 # pip3 install py3Fdfs
 2 from fdfs_client.client import get_tracker_conf, Fdfs_client
 3 
 4 tracker_conf = get_tracker_conf('./client.conf')
 5 client = Fdfs_client(tracker_conf)
 6 
 7 #文件上传
 8 # result = client.upload_by_filename('./lqz.jpg')
 9 # print(result)
10 # {'Group name': b'group1', 'Remote file_id': b'group1/M00/00/00/rBMGZWCeGhqAR_vRAAIAABZebgw.sqlite', 'Status': 'Upload successed.', 'Local file name': './db.sqlite3', 'Uploaded size': '128.00KB', 'Storage IP': b'101.133.225.166'}
11 # 访问地址即可下载:http://192.168.1.252:8888/group1/M00/00/00/CgAAzmSihyKAUybqAAH8LXKkrrY060.jpg
12 
13 
14 #文件下载
15 # result = client.download_to_file('./xx.jpg', b'group1/M00/00/00/CgAAzmSihyKAUybqAAH8LXKkrrY060.jpg')
16 # print(result)
17 
18 
19 # #文件删除
20 result = client.delete_file(b'group1/M00/00/00/CgAAzmSihyKAUybqAAH8LXKkrrY060.jpg')
21 print(result)
22 # ('Delete file successed.', b'group1/M00/00/00/rBMGZWCeGhqAR_vRAAIAABZebgw.sqlite', b'192.168.1.252')
23 
24 # #列出所有的group信息
25 # result = client.list_all_groups()
26 # print(result)

[搜索功能前端]

 1 Heade组件
 2 
 3 ##### html####
 4 <form class="search">
 5 <div class="tips" v-if="is_search_tip">
 6 <span @click="search_action('Python')">Python</span>
 7 <span @click="search_action('Linux')">Linux</span>
 8 </div>
 9 <input type="text" :placeholder="search_placeholder" @focus="on_search" @blur="off_search" v-model="search_word">
10 <el-button icon="el-icon-search" @click="search_action(search_word)"></el-button>
11 </form>
12 
13 ============================
14 #### js  data:      
15 is_search_tip: true,
16 search_placeholder: '',
17 search_word: ''
18 ###js :methods 19 search_action(search_word) { 20 if (!search_word) { 21 this.$message('请输入要搜索的内容'); 22 return 23 } 24 if (search_word !== this.$route.query.word) { 25 this.$router.push(`/course/search?word=${search_word}`); 26 } 27 this.search_word = ''; 28 }, 29 on_search() { 30 this.search_placeholder = '请输入想搜索的课程'; 31 this.is_search_tip = false; 32 }, 33 off_search() { 34 this.search_placeholder = ''; 35 this.is_search_tip = true; 36 }, 37 ===================================== 38 39 # css 40 41 .search { 42 float: right; 43 position: relative; 44 margin-top: 22px; 45 margin-right: 10px; 46 } 47 48 .search input, .search button { 49 border: none; 50 outline: none; 51 background-color: white; 52 } 53 54 .search input { 55 border-bottom: 1px solid #eeeeee; 56 } 57 58 .search input:focus { 59 border-bottom-color: orange; 60 } 61 62 .search input:focus + button { 63 color: orange; 64 } 65 66 .search .tips { 67 position: absolute; 68 bottom: 3px; 69 left: 0; 70 } 71 72 .search .tips span { 73 border-radius: 11px; 74 background-color: #eee; 75 line-height: 22px; 76 display: inline-block; 77 padding: 0 7px; 78 margin-right: 3px; 79 cursor: pointer; 80 color: #aaa; 81 font-size: 14px; 82 83 } 84 85 .search .tips span:hover { 86 color: orange; 87 }

 

【搜索结果页面】

  1 <template>
  2   <div class="search-course course">
  3     <Header/>
  4 
  5     <!-- 课程列表 -->
  6     <div class="main">
  7       <div v-if="course_list.length > 0" class="course-list">
  8         <div class="course-item" v-for="course in course_list" :key="course.name">
  9           <div class="course-image">
 10             <img :src="course.course_img" alt="">
 11           </div>
 12           <div class="course-info">
 13             <h3>
 14               <router-link :to="'/free/detail/'+course.id">{{ course.name }}</router-link>
 15               <span><img src="@/assets/img/avatar1.svg" alt="">{{ course.students }}人已加入学习</span></h3>
 16             <p class="teather-info">
 17               {{ course.teacher.name }} {{ course.teacher.title }} {{ course.teacher.signature }}
 18               <span
 19                   v-if="course.sections>course.pub_sections">共{{ course.sections }}课时/已更新{{ course.pub_sections }}课时</span>
 20               <span v-else>共{{ course.sections }}课时/更新完成</span>
 21             </p>
 22             <ul class="section-list">
 23               <li v-for="(section, key) in course.section_list" :key="section.name"><span
 24                   class="section-title">0{{ key + 1 }}  |  {{ section.name }}</span>
 25                 <span class="free" v-if="section.free_trail">免费</span></li>
 26             </ul>
 27             <div class="pay-box">
 28               <div v-if="course.discount_type">
 29                 <span class="discount-type">{{ course.discount_type }}</span>
 30                 <span class="discount-price">¥{{ course.real_price }}元</span>
 31                 <span class="original-price">原价:{{ course.price }}元</span>
 32               </div>
 33               <span v-else class="discount-price">¥{{ course.price }}元</span>
 34               <span class="buy-now">立即购买</span>
 35             </div>
 36           </div>
 37         </div>
 38       </div>
 39       <div v-else style="text-align: center; line-height: 60px">
 40         没有搜索结果
 41       </div>
 42       <div class="course_pagination block">
 43         <el-pagination
 44             @size-change="handleSizeChange"
 45             @current-change="handleCurrentChange"
 46             :current-page.sync="filter.page"
 47             :page-sizes="[2, 3, 5, 10]"
 48             :page-size="filter.page_size"
 49             layout="sizes, prev, pager, next"
 50             :total="course_total">
 51         </el-pagination>
 52       </div>
 53     </div>
 54   </div>
 55 </template>
 56 
 57 <script>
 58 import Header from '../components/Header'
 59 import api from '../assets/js/settings'
 60 
 61 export default {
 62   name: "SearchCourse",
 63   components: {
 64     Header,
 65   },
 66   data() {
 67     return {
 68       course_list: [],
 69       course_total: 0,
 70       filter: {
 71         page_size: 10,
 72         page: 1,
 73         search: '',
 74       }
 75     }
 76   },
 77   created() {
 78     this.get_course()
 79   },
 80   watch: {
 81     '$route.query'() {
 82       this.get_course()
 83     }
 84   },
 85   methods: {
 86     handleSizeChange(val) {
 87       // 每页数据量发生变化时执行的方法
 88       this.filter.page = 1;
 89       this.filter.page_size = val;
 90     },
 91     handleCurrentChange(val) {
 92       // 页码发生变化时执行的方法
 93       this.filter.page = val;
 94     },
 95     get_course() {
 96       // 获取搜索的关键字
 97       this.filter.search = this.$route.query.word
 98 
 99       // 获取课程列表信息
100       this.$axios.get(api.search, {
101         params: this.filter
102       }).then(response => {
103         // 如果后台不分页,数据在response.data中;如果后台分页,数据在response.data.results中
104         this.course_list = response.data.results;
105         this.course_total = response.data.count;
106       }).catch(() => {
107         this.$message({
108           message: "获取课程信息有误,请联系客服工作人员"
109         })
110       })
111     }
112   }
113 }
114 </script>
115 
116 <style scoped>
117 .course {
118   background: #f6f6f6;
119 }
120 
121 .course .main {
122   width: 1100px;
123   margin: 35px auto 0;
124 }
125 
126 .course .condition {
127   margin-bottom: 35px;
128   padding: 25px 30px 25px 20px;
129   background: #fff;
130   border-radius: 4px;
131   box-shadow: 0 2px 4px 0 #f0f0f0;
132 }
133 
134 .course .cate-list {
135   border-bottom: 1px solid #333;
136   border-bottom-color: rgba(51, 51, 51, .05);
137   padding-bottom: 18px;
138   margin-bottom: 17px;
139 }
140 
141 .course .cate-list::after {
142   content: "";
143   display: block;
144   clear: both;
145 }
146 
147 .course .cate-list li {
148   float: left;
149   font-size: 16px;
150   padding: 6px 15px;
151   line-height: 16px;
152   margin-left: 14px;
153   position: relative;
154   transition: all .3s ease;
155   cursor: pointer;
156   color: #4a4a4a;
157   border: 1px solid transparent; /* transparent 透明 */
158 }
159 
160 .course .cate-list .title {
161   color: #888;
162   margin-left: 0;
163   letter-spacing: .36px;
164   padding: 0;
165   line-height: 28px;
166 }
167 
168 .course .cate-list .this {
169   color: #ffc210;
170   border: 1px solid #ffc210 !important;
171   border-radius: 30px;
172 }
173 
174 .course .ordering::after {
175   content: "";
176   display: block;
177   clear: both;
178 }
179 
180 .course .ordering ul {
181   float: left;
182 }
183 
184 .course .ordering ul::after {
185   content: "";
186   display: block;
187   clear: both;
188 }
189 
190 .course .ordering .condition-result {
191   float: right;
192   font-size: 14px;
193   color: #9b9b9b;
194   line-height: 28px;
195 }
196 
197 .course .ordering ul li {
198   float: left;
199   padding: 6px 15px;
200   line-height: 16px;
201   margin-left: 14px;
202   position: relative;
203   transition: all .3s ease;
204   cursor: pointer;
205   color: #4a4a4a;
206 }
207 
208 .course .ordering .title {
209   font-size: 16px;
210   color: #888;
211   letter-spacing: .36px;
212   margin-left: 0;
213   padding: 0;
214   line-height: 28px;
215 }
216 
217 .course .ordering .this {
218   color: #ffc210;
219 }
220 
221 .course .ordering .price {
222   position: relative;
223 }
224 
225 .course .ordering .price::before,
226 .course .ordering .price::after {
227   cursor: pointer;
228   content: "";
229   display: block;
230   width: 0px;
231   height: 0px;
232   border: 5px solid transparent;
233   position: absolute;
234   right: 0;
235 }
236 
237 .course .ordering .price::before {
238   border-bottom: 5px solid #aaa;
239   margin-bottom: 2px;
240   top: 2px;
241 }
242 
243 .course .ordering .price::after {
244   border-top: 5px solid #aaa;
245   bottom: 2px;
246 }
247 
248 .course .ordering .price_up::before {
249   border-bottom-color: #ffc210;
250 }
251 
252 .course .ordering .price_down::after {
253   border-top-color: #ffc210;
254 }
255 
256 .course .course-item:hover {
257   box-shadow: 4px 6px 16px rgba(0, 0, 0, .5);
258 }
259 
260 .course .course-item {
261   width: 1100px;
262   background: #fff;
263   padding: 20px 30px 20px 20px;
264   margin-bottom: 35px;
265   border-radius: 2px;
266   cursor: pointer;
267   box-shadow: 2px 3px 16px rgba(0, 0, 0, .1);
268   /* css3.0 过渡动画 hover 事件操作 */
269   transition: all .2s ease;
270 }
271 
272 .course .course-item::after {
273   content: "";
274   display: block;
275   clear: both;
276 }
277 
278 /* 顶级元素 父级元素  当前元素{} */
279 .course .course-item .course-image {
280   float: left;
281   width: 423px;
282   height: 210px;
283   margin-right: 30px;
284 }
285 
286 .course .course-item .course-image img {
287   max-width: 100%;
288   max-height: 210px;
289 }
290 
291 .course .course-item .course-info {
292   float: left;
293   width: 596px;
294 }
295 
296 .course-item .course-info h3 a {
297   font-size: 26px;
298   color: #333;
299   font-weight: normal;
300   margin-bottom: 8px;
301 }
302 
303 .course-item .course-info h3 span {
304   font-size: 14px;
305   color: #9b9b9b;
306   float: right;
307   margin-top: 14px;
308 }
309 
310 .course-item .course-info h3 span img {
311   width: 11px;
312   height: auto;
313   margin-right: 7px;
314 }
315 
316 .course-item .course-info .teather-info {
317   font-size: 14px;
318   color: #9b9b9b;
319   margin-bottom: 14px;
320   padding-bottom: 14px;
321   border-bottom: 1px solid #333;
322   border-bottom-color: rgba(51, 51, 51, .05);
323 }
324 
325 .course-item .course-info .teather-info span {
326   float: right;
327 }
328 
329 .course-item .section-list::after {
330   content: "";
331   display: block;
332   clear: both;
333 }
334 
335 .course-item .section-list li {
336   float: left;
337   width: 44%;
338   font-size: 14px;
339   color: #666;
340   padding-left: 22px;
341   /* background: url("路径") 是否平铺 x轴位置 y轴位置 */
342   background: url("/src/assets/img/play-icon-gray.svg") no-repeat left 4px;
343   margin-bottom: 15px;
344 }
345 
346 .course-item .section-list li .section-title {
347   /* 以下3句,文本内容过多,会自动隐藏,并显示省略符号 */
348   text-overflow: ellipsis;
349   overflow: hidden;
350   white-space: nowrap;
351   display: inline-block;
352   max-width: 200px;
353 }
354 
355 .course-item .section-list li:hover {
356   background-image: url("/src/assets/img/play-icon-yellow.svg");
357   color: #ffc210;
358 }
359 
360 .course-item .section-list li .free {
361   width: 34px;
362   height: 20px;
363   color: #fd7b4d;
364   vertical-align: super;
365   margin-left: 10px;
366   border: 1px solid #fd7b4d;
367   border-radius: 2px;
368   text-align: center;
369   font-size: 13px;
370   white-space: nowrap;
371 }
372 
373 .course-item .section-list li:hover .free {
374   color: #ffc210;
375   border-color: #ffc210;
376 }
377 
378 .course-item {
379   position: relative;
380 }
381 
382 .course-item .pay-box {
383   position: absolute;
384   bottom: 20px;
385   width: 600px;
386 }
387 
388 .course-item .pay-box::after {
389   content: "";
390   display: block;
391   clear: both;
392 }
393 
394 .course-item .pay-box .discount-type {
395   padding: 6px 10px;
396   font-size: 16px;
397   color: #fff;
398   text-align: center;
399   margin-right: 8px;
400   background: #fa6240;
401   border: 1px solid #fa6240;
402   border-radius: 10px 0 10px 0;
403   float: left;
404 }
405 
406 .course-item .pay-box .discount-price {
407   font-size: 24px;
408   color: #fa6240;
409   float: left;
410 }
411 
412 .course-item .pay-box .original-price {
413   text-decoration: line-through;
414   font-size: 14px;
415   color: #9b9b9b;
416   margin-left: 10px;
417   float: left;
418   margin-top: 10px;
419 }
420 
421 .course-item .pay-box .buy-now {
422   width: 120px;
423   height: 38px;
424   background: transparent;
425   color: #fa6240;
426   font-size: 16px;
427   border: 1px solid #fd7b4d;
428   border-radius: 3px;
429   transition: all .2s ease-in-out;
430   float: right;
431   text-align: center;
432   line-height: 38px;
433   position: absolute;
434   right: 0;
435   bottom: 5px;
436 }
437 
438 .course-item .pay-box .buy-now:hover {
439   color: #fff;
440   background: #ffc210;
441   border: 1px solid #ffc210;
442 }
443 
444 .course .course_pagination {
445   margin-bottom: 60px;
446   text-align: center;
447 }
448 </style>
SearchCourse

[搜索功能接口]

1 from rest_framework.filters import SearchFilter
2 
3 
4 class CourseSearchView(GenericViewSet, APIListModelMixin):
5     queryset = Course.objects.all().filter(is_delete=False, is_show=True).order_by('orders')
6     serializer_class = CourseSerializer
7     pagination_class = PageNumberPagination  # 分页
8     filter_backends = [SearchFilter]
9     search_fields = ['name']

 

 

[支付宝支付介绍]

 1 # 1 支付
 2     -1 支付宝支付
 3     -2 微信支付
 4     -3 银联支付
 5     -4 自己支付:支付牌照
 6     
 7     
 8 # 2 支付宝支付
 9     -商户号:别人把钱付款--》付到商户里面
10     -商户再提现
11     ----需要营业执照----没有可以使用沙箱环境测试----
12     
13     
14     -扫码登录:沙箱环境--》不需要申请条件--》可以测试
15         -https://open.alipay.com/develop/manage 
16         
17     -网站支付:https://opendocs.alipay.com/open/270/105899
18     
19     -手机网站支付:可以掉起支付宝app
20         -咱们不会:输入账号密码支付
21         -https://opendocs.alipay.com/open/270/105898?pathHash=b3b2b667
22     
23     -网站支付:
24         -跳转到支付宝支付页面
25             -手机扫码付款
26             -在网页上输入支付宝账号密码付款
27             
28             
29 # 3 申请支付宝商户号,限制条件
30     #申请条件
31      支持的账号类型:支付宝企业账号、支付宝个人账号。
32     # 签约申请提交材料要求如下:
33     • 提供网站地址,网站能正常访问且页面显示完整,网站需要明确经营内容且有完整的商品信息。
34     • 网站必须通过 ICP 备案,且备案主体需与支付宝账号主体一致。若网站备案主体与当前账号主体不同时需上传授权函。
35     • 如以个人账号申请,需提供营业执照,且支付宝账号名称需与营业执照主体一致。
36 
37 注意:需按照要求提交材料,若部分材料不合格,收款额度将受到限制(单笔收款 ≤ 2000 元,单日收款 ≤ 20000 元)。若签约时未能提供相关材料(如营业执照),请在合约生效后的 30 天内补全,否则会影响正常收款
38 
39 
40 # 4 沙箱环境--》测试环境
41     -https://open.alipay.com/develop/sandbox/app
42     -商户号:
43         myrqvt2236@sandbox.com
44         111111
45     -买家号:
46         nmkrnw5996@sandbox.com
47         111111
48     -安卓沙箱app--》跟支付宝一样
49     
50     
51 # 5 web端,集成,支付流程
52     -1 前端:购买按钮
53         -2 点击支付按钮,触发后端下单接口:生成支付链接,生成订单[订单表生成一条记录]
54         -3 用户扫码付款[登陆后输入密码付款]
55         -4 支付宝收到付款成功---》get回调--》回调到前端--》前端支付成功页面
56         -5 支付宝收到付款成功---》post回调--》回调后端---》修改订单状态--》已支付状态    

 

 使用流程

 

 

 

[快速体验]

 

# 1 API 接口和sdk
    -早期没有python的sdk---》只能使用api接口--》第三方基于api接口封装了非官方sdk
        -https://github.com/fzlee/alipay
        -pip install python-alipay-sdk 
    -后期有了官方sdk:
        -https://opendocs.alipay.com/common/02np8q?pathHash=7847ca4f
        
        
# 2 支付宝支付通信,验证签名,都是使用非对称加密--》支付宝提供的软件
    -软件:https://opendocs.alipay.com/common/02kipk?pathHash=0d20b438
    -公钥
    -私钥
    
# 3 在支付宝沙箱环境中[正式环境]
    -把刚刚生成的公钥填入
    -会生成一个支付宝公钥--》把这个东西复制出来
    
# 4 在代码中,使用【支付宝公钥】和刚刚生成的【私钥】,放到代码中

第三方sdk测试

 第二步下载安装

  pip install python-alipay-sdk 

第三步:

 

 

 

 

 

 

 

 

 

 

go_pay.py


from alipay import AliPay
from alipay.utils import AliPayConfig

# 支付宝网页下载的证书不能直接被使用,需要加上头尾
# 你可以在此处找到例子: tests/certs/ali/ali_private_key.pem
app_private_key_string = open("./private_key.pem").read()
alipay_public_key_string = open("./al_public_key.pem").read()

alipay = AliPay(
appid="9021000137628387", # 商户申请好久有了
app_notify_url=None, # 默认回调 url
app_private_key_string=app_private_key_string,
# 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
alipay_public_key_string=alipay_public_key_string,
sign_type="RSA2", # RSA 或者 RSA2
debug=False, # 默认 False
verbose=False, # 输出调试数据
config=AliPayConfig(timeout=15) # 可选,请求超时时间
)

# 老版本
order_string = alipay.api_alipay_trade_page_pay(
out_trade_no="10001010",
total_amount=99,
subject='Go语言入门',
return_url="https://example.com",
notify_url="https://example.com/notify" # 可选,不填则使用默认 notify url
)
print(f'支付地址:https://openapi-sandbox.dl.alipaydev.com/gateway.do?{order_string}')

# 新版本--暂时没通
# res=alipay.client_api(
# "alipay.trade.page.pay",
# biz_content={
# "out_trade_no": "100212",
# "total_amount": 8889,
# "subject": "性感内衣"
# },
# return_url="https://example.com", # this is optional
# notify_url='https://example.com'
# )

private_key.pem

-----BEGIN RSA PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCuK0anMHHafiIMMJlp4s3+DFO3zn2YYacL60g+xDUzHi3o/dbc2fcc3ynay6oTwNqOZlVvaVVXlO3chK3LQsjccv0JnNF4bbCtO5Bfoh14/l+LASRZsuwdscXH2t/Iy99q9N1fddD+bzAldsT0aoRxJXauU/LHVRoZGbjGmc5qNjZk/JRasuU+0edmHgU3Dk0yWVgfL13fLhITT7u7c2S+88QJXkpxAX0Grj6E/AHNCx89EajoXNJvtDTeIFBy/k5mgnZKO712Y5fkSAjeckQu82lcHIxcVQZK20139+mGngCKhxW7xNg9Ei0JuY79/cuncwb0iI3/gUg4UDz8PCutAgMBAAECggEAPEdqCo8vuGkTK5jeX9FJbfWiO+lRep3BjNR/iompx+lYBl1kMceWSP3LpJf8Yx3KBqLQSuDv0oIO2NVquQCCOBnsDZmivcVo8mu9Cfw3qxqOwrNAe3L7WUOfqg3MidhVmNTqkRFbpdOhnDXA8L6029wVeTxAuFBx2eIXG1U8JXHOFDG4R1TvaZ8a4BmRttq7xSMRfCoHW8w3W4dj6wxPgNYQ4pMGATcjttYoyJLQ8kqNWu+107XpjPXnPxh0Tj/lWP0boQNynZGQA1RdSJLa2oWzyhmrq3wspYtkWIbY2mpEwrWn1Hs7/D+oWOhpi3oEEfup/Gkx7J9iRIT4wM9VQQKBgQD1EXNDbwhHbHc3Tumi/oSndskYLpI0h5fM8Y9bL/tF8KdrazsvZJWD+QhnASnj6QaGh5ba0r1SxoGTJXL+HDcg0XvJNI5nrlJAlF1VDRSAQ+duNoZGa8+KPZ3kWZeWWMYf8o7vkZ5zYiqj8x4UvjFLP8jws+c4hTx4kcequBZOxQKBgQC18DHuSZQxcU6mnnTxx7aD8UFOGhA4pghokwOg0+F2TuyBJst+UlzLGFOFxRs2pAd9ZsF47FcMPEoVK7wdGp5qc5FbgS4SqR5B7O4aoIoVHsFUJ30+DGxhx6to8GG1eFgOUL82xtywHoO2DBoyciQ5qUl5mGFtEYP10o8xhVw3yQKBgFiW/U7UNW70U8hHm4fTcArFkv5N34ZjuclZTVRObQwkabEYK2X/e1kgzhvGPOlplHVPUIY8BjqFDdQuhno7ouYXNCNQ/2WVi6BSwcZ8GvwfD+s0hKeyU20KxygEBxtPbhegbHFH97qIMxffS/F9q34jSbVRo3U4HNsM/9vD/jRtAoGARdYBFHphy+i2S/ae5P/H6zV738LZXyU8LQfhaKZr8MGyvpBpo/9xZvPbe4mBPKQRy+zZbtUpRKUPurii6HkDPdFGhl07liYcWyna65Wb6yd3BhpyVJLoN1AxV4KNvt6GzlfoTdpfc6sC26ohlCOEDC4S6f23x+nlQUKlfFi4YSECgYAd2j6YTmjKuMzsEZIUDAYy2Wu6nhh4YcfJIAd5CJX6FS73hoWHpbp9Wcy51Zanuxji2nQIA+u/0KP4EiHtVFKyqs9LPecdsSSLMKmWhGAlOav6QLqtz2fp1XtqB+Qx3KDe//ZEMgB05TDYFVNGBdT/88319bHEPSZZ4uUkFASD1A==
-----END RSA PRIVATE KEY-----

al_public_key.pem

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjujzCsieaipgziK/eMBoOTKkmvMNVvLiwvArpsSy3uxMHMJV/7P1IDO0rnvugWobigrHASkUdHIgguF4vUIiijHjkBY2qBBFp4VK+kZOch8kVJPF97YCLb7lx1PsS5KqGkISwfzIqMAckWUoEPpF8DNpKrTc2kyGOrpIqNMIuI+m57OWhCTh/Nom3y5YAzEnTSk+ZOXgEiQqjDcbrXIDsuUx9sNRD3q4AOY0oNN1m0/12X95ouENOmuGHiyw/8HTVdLE9/yKz4BDGnzc/bdC35N5sCSIdD8+iRXP1SCdoWdK8+aQe8KViAUahImTKUedSzs9Nl44rzIP5nEW72u8GQIDAQAB
-----END PUBLIC KEY-----

 

官方sdk测试(不行)

from alipay.aop.api.AlipayClientConfig import AlipayClientConfig
from alipay.aop.api.DefaultAlipayClient import DefaultAlipayClient
from alipay.aop.api.domain.AlipayTradePagePayModel import AlipayTradePagePayModel
from alipay.aop.api.request.AlipayTradePagePayRequest import AlipayTradePagePayRequest
if __name__ == '__main__':
    """
    设置配置,包括支付宝网关地址、app_id、应用私钥、支付宝公钥等,其他配置值可以查看AlipayClientConfig的定义。
    """
    alipay_client_config = AlipayClientConfig()
    # alipay_client_config.server_url = 'https://openapi.alipay.com/gateway.do'  # 真实环境
    alipay_client_config.server_url = 'https://openapi-sandbox.dl.alipaydev.com/gateway.do'  # 沙箱环境
    alipay_client_config.app_id = '9021000129694319'
    # 应用私钥
    alipay_client_config.app_private_key = 'MIIEowIBAAKCAQEA2+5a8/zdCXHnzE3T0DrWgalKE2CLhRX0+HVhVxC7+OL1Aoxag5evU6CQW0F/aixnu6bBowy+tKOggqLuRzJsZhitCpt1PyJAsQO10STIa7GAhfmZMW+SEkmLvo5n/DIQmHmjcEjQj+dS1QQH/qnIsquT7D3BPxJanTUQurIiiL/TlxV5ur4qvuYu/1Pg4uixhi/UJa2sunwQK9u1+D8fYRf4zuYEltSA3ZfgFDFOyZK/mkVdEWL9snZqJRYVQIC6xPX3qeVX7Rx0768B5HzTlzT1zp9j5tMktxjRHJX98/lYluE6vy9loBYa6VF6UOVAvRLenQKyswSiUIU+WqyQ1wIDAQABAoIBAQDBwkqrLhlmWs3GtsJnb47QCN9UFviUNXXu9yrc08dnTDxjFFgiGx7B5HGJlDi2x4xUTGPITFAvQQEGVpjqbMgHYrIA6FjxWDH6QbSLH4bbKjR61B1c6licd+L/7OI707e/PVr6b4wfW8MkHDsW52oDzxmxRe7crETcv4WPlaTLJ+JPRILVP3rWcG/DZJTe+HwHHEwWZmBKrSRUqy+PF9CCQ3FYxRIKYcg7JxnA/JlGPjnzQMckrBfDoNi2XsK4w23ioh6n6mWKQ86ENDyUQMRoB32zcSOGj2If5uVYf6ydMNTNJeGT6CUn2o4jkTdKtljXWKSnDm8zRQDqif6TsSLxAoGBAPyOO5E9dmXtPBD9YyjUauEzLNG5B87iibOn2jLfQ7PuenZgPXXG2L+q6+iAUxJzQKFUkbvQXVwGHk2/pVCeogoZdCOUQebuSSuqSkBH+UGP0WiHcOFeoJckmT74ijZ1ESWVHek8foIBab2+8gSWNBMyWaTsmYI02zq8VF9r3IiTAoGBAN7uN9DpUD70Ev5yydVLkxwC6vI85ML/G9b6iHDvd3HAlG5GcRraSdThRQ3YCDkLJufGJ7bnAEYHARdaohgNZfRLWK1HkUYk3tQThMPYBxDXbJtfm7p2Kl+/WKm4+TtrKUdnDhZPod8bL2awVth4hfoKO9GzF80R8vBPo9W6yZUtAoGAA5CKXLFuY1/m0iKRbLkazRTo1Aj1iEEASo3a8Y7fKMH77oHLPEdTNdlWvRBam88OoXhNGkaFms/nS5eh4LJsfRIA5qOoDndchwY/SAr8BKXgAcavnC62u4tjslTVtpEObeZd5rXY30Lf2DLCvbfVAlRamY5RWFogogKYekROd4sCgYAFkmKmwA4XZLZM0cWlpRvqKVCB+W+mSAYEG4Lpf7K2jx+mmfAdwbLytSaqr+mUs2inhlZbxe5F0cr/MG64ty0DLBbtTcqdvDItjsdUtcOHcjrurzcPNADfH8MxisP/7i+77yF1AUyEbQOER4gEJQ8ELtlL5nQD1h0CUJtBrkd3iQKBgHab6GJDk5JGapKSwC32FP+LNDsm++0RhZGYlGzlsa8JnkOYuMoY4Y7D9IfJA1xbfxNECDFAg/FdyOQ+vzESx1i39HYZ6WXa0wmy2kCVugGVM/8Wdp3gjbZ378iMl3GQ+rDerkmyYpzm9t1L0/8GRrhiS+OkfMBhs7gcZbL74BsZ'
    # 阿里公钥
    alipay_client_config.alipay_public_key = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhddgdkn8X2t+3gnPA9dkqUNe1+SIcPQ8Mmb/8Ynac0In/s9BC5rBNrtkKEtvejQN+jmOJh9x03yHGwObYYUckwAXEIKw9LhXQWu3LdTbczl3UWgH9IW3BsZYvOY9xJWZJN1cq3MskFbqoIgB+lDiq86JRYS/QpyvCk7t5ZtY58w5A/iSTOGAqINzIW9BZBmQM8euJd26u5JNEMuotXHWlJzPeERNnxzJRUi8MpltDXfSzlxmATI/Aw2u1HGY91OIv1h7A46lURaCdyc57aj7ot+rLFymTMvKyYhyyfA2FjyyXlwZQowBFE7rDBIe+uLttgBu6O+1sUl2kRw9IeOk0QIDAQAB'

    """
    得到客户端对象。
    注意,一个alipay_client_config对象对应一个DefaultAlipayClient,定义DefaultAlipayClient对象后,alipay_client_config不得修改,如果想使用不同的配置,请定义不同的DefaultAlipayClient。
    logger参数用于打印日志,不传则不打印,建议传递。
    """
    client = DefaultAlipayClient(alipay_client_config=alipay_client_config)
    """
    页面接口示例:alipay.trade.page.pay
    """
    # 对照接口文档,构造请求对象
    model = AlipayTradePagePayModel()
    model.out_trade_no = "000010004"
    model.total_amount = 999
    model.subject = "重启娃娃-保密发货"
    model.body = "重启娃娃"
    model.product_code = "FAST_INSTANT_TRADE_PAY"

    request = AlipayTradePagePayRequest(biz_model=model)
    # 两个回调地址:get回调  post 回调
    request.return_url='http://www.baidu.com' # get回调
    request.notify_url='http://www.baidu.com/post' # post 回调  我们看不到
    # 得到构造的请求,如果http_method是GET,则是一个带完成请求参数的url,如果http_method是POST,则是一段HTML表单片段
    response = client.page_execute(request, http_method="GET")

    print("alipay.trade.page.pay response:" + response)

 

支付宝二次封装

libs
├── al_pay # aliapy二次封装包
│ ├── __init__.py # 包文件
│ ├── pem # 公钥私钥文件夹
│ │ ├── alipay_public_key.pem # 支付宝公钥文件
│ │ ├── app_private_key.pem # 应用私钥文件
│ ├── pay.py # 支付文件
└── └── settings.py # 应用配置

 

 1 __init__.py
 2 
 3 from .pay import alipay
 4 from .settings import GATEWAY
 5 
 6 ============================
 7 settings.py
 8 
 9 import os
10 
11 # 替换应用私钥   支付宝公钥  和 应用ID即可
12 
13 # 应用私钥
14 APP_PRIVATE_KEY_STRING = open(
15     os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pem', 'app_private_key.pem')).read()
16 # 支付宝公钥
17 ALIPAY_PUBLIC_KEY_STRING = open(
18     os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pem', 'alipay_public_key.pem')).read()
19 # 应用ID
20 APP_ID = '9021000137628387'
21 # 加密方式
22 SIGN = 'RSA2'
23 # 是否是支付宝测试环境(沙箱环境),如果采用真是支付宝环境,配置False
24 DEBUG = True
25 # 支付网关
26 GATEWAY = 'https://openapi-sandbox.dl.alipaydev.com/gateway.do' if DEBUG else 'https://openapi.alipay.com/gateway.do'
27 
28 =================================
29 
30 pay.py
31 
32 from alipay import AliPay
33 from alipay.utils import AliPayConfig
34 from . import settings
35 
36 alipay = AliPay(
37     appid=settings.APP_ID,
38     app_notify_url=None,  # 默认回调 url
39     app_private_key_string=settings.APP_PRIVATE_KEY_STRING,
40     # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
41     alipay_public_key_string=settings.ALIPAY_PUBLIC_KEY_STRING,
42     sign_type=settings.SIGN,  # RSA 或者 RSA2
43     debug=settings.DEBUG,  # 默认 False
44     verbose=False,  # 输出调试数据
45     config=AliPayConfig(timeout=15)  # 可选,请求超时时间
46 )

支付相关表

# 1 创建一个新的app,order
# 2 表模型有
# 用户在前端点击立即购买---》触发我们后端下单接口---》下单接口返回给前端支付地址---》前端跳转到支付链接---》用户去付款
# 表分析
-1 订单表
-2 订单详情表 :一个订单有多个订单详情

 1 表模型
 2 
 3 from django.db import models
 4 
 5 from user.models import User
 6 from course.models import Course
 7 
 8 
 9 # 订单表
10 
11 
12 class Order(models.Model):
13     status_choices = (
14         (0, '未支付'),
15         (1, '已支付'),
16         (2, '已取消'),
17         (3, '超时取消'),
18     )
19     pay_choices = (
20         (1, '支付宝'),
21         (2, '微信支付'),
22     )
23     subject = models.CharField(max_length=150, verbose_name="订单标题")
24     total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="订单总价", default=0)
25     # 咱们生成的---全局唯一
26     out_trade_no = models.CharField(max_length=64, verbose_name="订单号", unique=True)
27     # 支付宝付款后会返回这个号---》支付宝流水号
28     trade_no = models.CharField(max_length=64, null=True, verbose_name="流水号")
29     order_status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="订单状态")
30     pay_type = models.SmallIntegerField(choices=pay_choices, default=1, verbose_name="支付方式")
31     # 支付宝会返回支付时间
32     pay_time = models.DateTimeField(null=True, verbose_name="支付时间")
33 
34     user = models.ForeignKey(User, related_name='order_user', on_delete=models.DO_NOTHING, db_constraint=False,
35                              verbose_name="下单用户")
36     # 下单时间
37     created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
38 
39     class Meta:
40         db_table = "luffy_order"
41         verbose_name = "订单记录"
42         verbose_name_plural = "订单记录"
43 
44     def __str__(self):
45         return "%s - ¥%s" % (self.subject, self.total_amount)
46 
47 
48 # 订单详情表
49 class OrderDetail(models.Model):
50     # 跟订单一对多,关联字段写在多的一方
51     order = models.ForeignKey(Order, related_name='order_courses', on_delete=models.CASCADE, db_constraint=False,
52                               verbose_name="订单")
53     # 课程和订单详情,一对多,一个课程,可以对应多个订单详情
54     course = models.ForeignKey(Course, related_name='course_orders', on_delete=models.CASCADE, db_constraint=False,
55                                verbose_name="课程")
56     price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价")
57     real_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程实价")
58 
59     class Meta:
60         db_table = "luffy_order_detail"
61         verbose_name = "订单详情"
62         verbose_name_plural = "订单详情"
63 
64     def __str__(self):
65         try:
66             return "%s的订单:%s" % (self.course.name, self.order.out_trade_no)
67         except:
68             return super().__str__()

下单接口(登陆后才能使用)

# 1 前端携带数据什么格式?不需要携带用户id,携带token--》request.user 就是当前登陆用户
post请求--》{'courses':[1,],'total_amount':0.1,'subject':课程名,'pay_type':1}

# 2 后端接口

视图类

 1 from django.shortcuts import render
 2 from rest_framework.permissions import IsAuthenticated
 3 from rest_framework.viewsets import GenericViewSet
 4 from rest_framework_simplejwt.authentication import JWTAuthentication
 5 
 6 from order.serializer import OrderPaySerializer
 7 from utils.common_response import APIResponse
 8 
 9 
10 # Create your views here.
11 class OrderPayView(GenericViewSet):
12     # 必须登录才能访问:认证类  权限类
13     authentication_classes = [JWTAuthentication]
14     permission_classes = [IsAuthenticated]
15     # 校验--》生成支付链接--》生成订单--》序列化类的validate中
16     serializer_class = OrderPaySerializer
17 
18     def create(self, request, *args, **kwargs):
19         serializer = self.get_serializer(data=request.data, context={'request': request})
20         serializer.is_valid(raise_exception=True)
21         serializer.save()  # 下单-》生成订单,--》重写create方法
22         pay_url = serializer.context.get('pay_url')
23         return APIResponse(pay_url=pay_url)

序列化类

 1 from rest_framework import serializers
 2 from .models import Order, OrderDetail
 3 from course.models import Course
 4 from rest_framework.exceptions import APIException
 5 import uuid
 6 from django.conf import settings
 7 from libs.al_pay import alipay, GATEWAY
 8 
 9 
10 # 1 校验--》{'courses':[1,],'total_amount':0.1,'subject':课程名,'pay_type':1}
11 # 2 反序列化的保存
12 class OrderPaySerializer(serializers.ModelSerializer):
13     # courses 重写
14     # courses 本来是 [1,4,5]--->会去Course.objects.all() 数据集中映射---》变成 --》[course1对象,course4对象,course5对象,]
15     courses = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), many=True)
16 
17     # courses=serializers.ListField()
18 
19     class Meta:
20         model = Order
21         fields = [
22             'courses',  # 不是表中字段,需要重写
23             'total_amount',
24             'subject',
25             'pay_type'
26         ]
27 
28     def _check_total_amount(self, attrs):
29         # 1 取出用户传的 courses---》课程 对象 列表
30         courses = attrs.get('courses')
31         # 2 取出总价格
32         real_total_amount = 0
33         total_amount = attrs.get('total_amount')
34         # 3 通过  课程对象 列表 获取价格--》累加到一起,跟传入的 总价格比较,如果一致,就什么都不做,如果不一致,抛异常
35         for course in courses:
36             real_total_amount += course.price
37         if real_total_amount != total_amount:
38             raise APIException('价格不合法')
39         return total_amount
40 
41     def _get_out_trade_no(self):
42         # 使用uuid生成--》后期会有别的生成id的方案:1 效率高 2 不重复 3 单调递增趋势 4 在分布式节点中不会重复
43         out_trade_no = str(uuid.uuid4()).replace('-', '')
44         return out_trade_no
45 
46     def _get_user(self):
47         return self.context.get('request').user
48 
49     def _get_pay_url(self, out_trade_no, total_amount, subject):
50         order_string = alipay.api_alipay_trade_page_pay(
51             out_trade_no=out_trade_no,
52             total_amount=float(total_amount),  # 只有生成支付宝链接时,不能用Decimal
53             subject=subject,
54             return_url=settings.RETURN_URL,  # get 回调 --》前端
55             notify_url=settings.NOTIFY_URL,  # post 回调--》后端
56         )
57         pay_url = GATEWAY + '?' + order_string
58         # 将支付链接存入,传递给views
59         self.context['pay_url'] = pay_url
60 
61     def _before_create(self, attrs, user, out_trade_no):
62         # attrs ={'courses':[对象,],'total_amount':0.1,'subject':课程名,'pay_type':1}
63         attrs['user'] = user
64         attrs['out_trade_no'] = out_trade_no
65 
66     def validate(self, attrs):
67         # 1 校验数据是否正确[订单总价校验]--》total_amount 和 courses 比较价格是否正确
68         total_amount = self._check_total_amount(attrs)
69         # 2 生成订单号--》唯一的--》
70         out_trade_no = self._get_out_trade_no()
71         # 3 获取支付人 --》当前登录用户
72         user = self._get_user()
73         # 4 获取支付链接--》
74         self._get_pay_url(out_trade_no, total_amount, attrs.get('subject'))
75         # 5 入库(两个表)的信息准备
76         self._before_create(attrs, user, out_trade_no)
77         return attrs
78 
79     def create(self, validated_data):
80         # validated_data = {'courses': [对象, ], 'total_amount': 0.1, 'subject': 课程名, 'pay_type': 1,user:对象,out_trade_no:3333}
81         # 存两个表
82         courses = validated_data.pop('courses')
83         order = Order.objects.create(**validated_data)
84         for course in courses:
85             OrderDetail.objects.create(order=order, course=course, price=course.price, real_price=course.price)
86 
87         return order

配置文件

# 回调配置文件
# 前台基于URL
LUFFY_URL = 'http://127.0.0.1:8080/'
# 后台异步回调接口
NOTIFY_URL = BACKEND_URL + "api/v1/order/success/"
# 前台同步回调接口
RETURN_URL = LUFFY_URL + "/pay/success"

 

 

前端支付

  1 <template>
  2   <div class="detail">
  3     <Header/>
  4     <div class="main">
  5       <div class="course-info">
  6         <div class="wrap-left">
  7           <vue-core-video-player :src="mp4_url"
  8                                  :muted="true"
  9                                  :autoplay="false"
 10                                  title="致命诱惑"
 11                                  preload="none"
 12                                  :loop="true"
 13                                  controls="auto"
 14                                  cover='http://127.0.0.1:8000/media/icon/default.png'
 15                                  @play="playFunc"
 16                                  @pause="pauseFunc"></vue-core-video-player>
 17         </div>
 18         <div class="wrap-right">
 19           <h3 class="course-name">{{ course_info.name }}</h3>
 20           <p class="data">
 21             {{ course_info.students }}人在学&nbsp;&nbsp;&nbsp;&nbsp;课程总时长:{{
 22               course_info.sections
 23             }}课时/{{ course_info.pub_sections }}小时&nbsp;&nbsp;&nbsp;&nbsp;难度:{{ course_info.level_name }}</p>
 24           <div class="sale-time">
 25             <p class="sale-type">价格 <span class="original_price">¥{{ course_info.price }}</span></p>
 26             <p class="expire"></p>
 27           </div>
 28           <div class="buy">
 29             <div class="buy-btn">
 30               <button class="buy-now" @click="handleGOPay">立即购买</button>
 31               <button class="free">免费试学</button>
 32             </div>
 33             <!--<div class="add-cart" @click="add_cart(course_info.id)">-->
 34             <!--<img src="@/assets/img/cart-yellow.svg" alt="">加入购物车-->
 35             <!--</div>-->
 36           </div>
 37         </div>
 38       </div>
 39       <div class="course-tab">
 40         <ul class="tab-list">
 41           <li :class="tabIndex==1?'active':''" @click="tabIndex=1">详情介绍</li>
 42           <li :class="tabIndex==2?'active':''" @click="tabIndex=2">课程章节 <span
 43               :class="tabIndex!=2?'free':''">(试学)</span>
 44           </li>
 45           <li :class="tabIndex==3?'active':''" @click="tabIndex=3">用户评论</li>
 46           <li :class="tabIndex==4?'active':''" @click="tabIndex=4">常见问题</li>
 47         </ul>
 48       </div>
 49       <div class="course-content">
 50         <div class="course-tab-list">
 51           <div class="tab-item" v-if="tabIndex==1">
 52             <div class="course-brief" v-html="course_info.brief"></div>
 53           </div>
 54           <div class="tab-item" v-if="tabIndex==2">
 55             <div class="tab-item-title">
 56               <p class="chapter">课程章节</p>
 57               <p class="chapter-length">共{{ course_chapters.length }}章 {{ course_info.sections }}个课时</p>
 58             </div>
 59             <div class="chapter-item" v-for="chapter in course_chapters" :key="chapter.name">
 60               <p class="chapter-title"><img src="@/assets/img/enum.svg"
 61                                             alt="">第{{ chapter.chapter }}章·{{ chapter.name }}
 62               </p>
 63               <ul class="section-list">
 64                 <li class="section-item" v-for="section in chapter.coursesections" :key="section.name">
 65                   <p class="name"><span class="index">{{ chapter.chapter }}-{{ section.orders }}</span>
 66                     {{ section.name }}<span class="free" v-if="section.free_trail">免费</span></p>
 67                   <p class="time">{{ section.duration }} <img src="@/assets/img/chapter-player.svg"></p>
 68                   <button class="try" v-if="section.free_trail">立即试学</button>
 69                   <button class="try" v-else>立即购买</button>
 70                 </li>
 71               </ul>
 72             </div>
 73           </div>
 74           <div class="tab-item" v-if="tabIndex==3">
 75             用户评论
 76           </div>
 77           <div class="tab-item" v-if="tabIndex==4">
 78             常见问题
 79           </div>
 80         </div>
 81         <div class="course-side">
 82           <div class="teacher-info">
 83             <h4 class="side-title"><span>授课老师</span></h4>
 84             <div class="teacher-content">
 85               <div class="cont1">
 86                 <img :src="course_info.teacher.image">
 87                 <div class="name">
 88                   <p class="teacher-name">{{ course_info.teacher.name }}
 89                     {{ course_info.teacher.title }}</p>
 90                   <p class="teacher-title">{{ course_info.teacher.signature }}</p>
 91                 </div>
 92               </div>
 93               <p class="narrative">{{ course_info.teacher.brief }}</p>
 94             </div>
 95           </div>
 96         </div>
 97       </div>
 98     </div>
 99     <Footer/>
100   </div>
101 </template>
102 
103 <script>
104 import Header from "@/components/Header"
105 import Footer from "@/components/Footer"
106 import api from '../assets/js/settings'
107 
108 export default {
109   name: "Detail",
110   data() {
111     return {
112       tabIndex: 2,   // 当前选项卡显示的下标
113       course_id: 0, // 当前课程信息的ID
114       course_info: {
115         teacher: {},
116       }, // 课程信息
117       course_chapters: [], // 课程的章节课时列表
118       // mp4_url: [
119       //   {
120       //     src: 'https://video.pearvideo.com/mp4/short/20240516/cont-1794244-71106834-hd.mp4',
121       //     resolution: 360,
122       //   },
123       //   {
124       //     src: 'https://video.pearvideo.com/mp4/short/20240517/cont-1794270-71106842-hd.mp4',
125       //     resolution: 720,
126       //   },
127       //   {
128       //     src: 'https://video.pearvideo.com/mp4/short/20240125/cont-1791495-16017396-hd.mp4',
129       //     resolution: '4k',
130       //
131       //   }],
132       mp4_url: 'https://video.pearvideo.com/mp4/short/20240125/cont-1791495-16017396-hd.mp4'
133 
134     }
135   },
136   created() {
137     this.get_course_id();
138     this.get_course_data();
139     this.get_chapter();
140   },
141   methods: {
142     handleGOPay() {
143       let token = this.$cookies.get('token')
144       if (token) {
145         this.$axios.post(api.pay, {
146           courses: [this.course_id,],
147           total_amount: this.course_info.price,
148           subject: this.course_info.name,
149           pay_type: 1,
150         }, {
151           headers: {
152             'Authorization': 'Bearer ' + token
153           }
154         }).then(response => {
155           if (response.data.code == '100') {
156             let pay_url = response.data.pay_url;
157             //在浏览器中打开当前地址
158             open(pay_url, '_self')
159           }
160         }).catch(() => {
161           this.$message({
162             message: "对不起,访问页面出错!请联系客服工作人员!"
163           });
164         })
165       } else {
166         this.$message({
167           type: "error",
168           message: "请先登录!"
169         });
170 
171       }
172     },
173     playFunc() {
174       // 当视频播放时,执行的方法
175       console.log('视频开始播放')
176     },
177     pauseFunc() {
178       // 当视频暂停播放时,执行的方法
179       console.log('视频暂停,可以打开广告了')
180     },
181     get_course_id() {
182       // 获取地址栏上面的课程ID
183       this.course_id = this.$route.params.pk
184       if (this.course_id < 1) {
185         let _this = this;
186         _this.$alert("对不起,当前视频不存在!", "警告", {
187           callback() {
188             _this.$router.go(-1);
189           }
190         });
191       }
192     },
193     get_course_data() {
194       // ajax请求课程信息
195       this.$axios.get(`${api.actual}${this.course_id}/`).then(response => {
196         this.course_info = response.data.result
197       }).catch(() => {
198         this.$message({
199           message: "对不起,访问页面出错!请联系客服工作人员!"
200         });
201       })
202     },
203 
204     get_chapter() {
205       // 获取当前课程对应的章节课时信息
206       this.$axios.get(api.chapter, {
207         params: {
208           "course": this.course_id,
209         }
210       }).then(response => {
211         this.course_chapters = response.data.results;
212       }).catch(error => {
213         this.$message({
214           message: "对不起,访问页面出错!请联系客服工作人员!"
215         });
216       })
217     },
218   },
219   components: {
220     Header,
221     Footer,
222   }
223 }
224 </script>
225 
226 <style scoped>
227 .main {
228   background: #fff;
229   padding-top: 30px;
230 }
231 
232 .course-info {
233   width: 1200px;
234   margin: 0 auto;
235   overflow: hidden;
236 }
237 
238 .wrap-left {
239   float: left;
240   width: 690px;
241   height: 388px;
242   background-color: #000;
243 }
244 
245 .wrap-right {
246   float: left;
247   position: relative;
248   height: 388px;
249 }
250 
251 .course-name {
252   font-size: 20px;
253   color: #333;
254   padding: 10px 23px;
255   letter-spacing: .45px;
256 }
257 
258 .data {
259   padding-left: 23px;
260   padding-right: 23px;
261   padding-bottom: 16px;
262   font-size: 14px;
263   color: #9b9b9b;
264 }
265 
266 .sale-time {
267   width: 464px;
268   background: #fa6240;
269   font-size: 14px;
270   color: #4a4a4a;
271   padding: 10px 23px;
272   overflow: hidden;
273 }
274 
275 .sale-type {
276   font-size: 16px;
277   color: #fff;
278   letter-spacing: .36px;
279   float: left;
280 }
281 
282 .sale-time .expire {
283   font-size: 14px;
284   color: #fff;
285   float: right;
286 }
287 
288 .sale-time .expire .second {
289   width: 24px;
290   display: inline-block;
291   background: #fafafa;
292   color: #5e5e5e;
293   padding: 6px 0;
294   text-align: center;
295 }
296 
297 .course-price {
298   background: #fff;
299   font-size: 14px;
300   color: #4a4a4a;
301   padding: 5px 23px;
302 }
303 
304 .discount {
305   font-size: 26px;
306   color: #fa6240;
307   margin-left: 10px;
308   display: inline-block;
309   margin-bottom: -5px;
310 }
311 
312 .original {
313   font-size: 14px;
314   color: #9b9b9b;
315   margin-left: 10px;
316   text-decoration: line-through;
317 }
318 
319 .buy {
320   width: 464px;
321   padding: 0px 23px;
322   position: absolute;
323   left: 0;
324   bottom: 20px;
325   overflow: hidden;
326 }
327 
328 .buy .buy-btn {
329   float: left;
330 }
331 
332 .buy .buy-now {
333   width: 125px;
334   height: 40px;
335   border: 0;
336   background: #ffc210;
337   border-radius: 4px;
338   color: #fff;
339   cursor: pointer;
340   margin-right: 15px;
341   outline: none;
342 }
343 
344 .buy .free {
345   width: 125px;
346   height: 40px;
347   border-radius: 4px;
348   cursor: pointer;
349   margin-right: 15px;
350   background: #fff;
351   color: #ffc210;
352   border: 1px solid #ffc210;
353 }
354 
355 .add-cart {
356   float: right;
357   font-size: 14px;
358   color: #ffc210;
359   text-align: center;
360   cursor: pointer;
361   margin-top: 10px;
362 }
363 
364 .add-cart img {
365   width: 20px;
366   height: 18px;
367   margin-right: 7px;
368   vertical-align: middle;
369 }
370 
371 .course-tab {
372   width: 100%;
373   background: #fff;
374   margin-bottom: 30px;
375   box-shadow: 0 2px 4px 0 #f0f0f0;
376 
377 }
378 
379 .course-tab .tab-list {
380   width: 1200px;
381   margin: auto;
382   color: #4a4a4a;
383   overflow: hidden;
384 }
385 
386 .tab-list li {
387   float: left;
388   margin-right: 15px;
389   padding: 26px 20px 16px;
390   font-size: 17px;
391   cursor: pointer;
392 }
393 
394 .tab-list .active {
395   color: #ffc210;
396   border-bottom: 2px solid #ffc210;
397 }
398 
399 .tab-list .free {
400   color: #fb7c55;
401 }
402 
403 .course-content {
404   width: 1200px;
405   margin: 0 auto;
406   background: #FAFAFA;
407   overflow: hidden;
408   padding-bottom: 40px;
409 }
410 
411 .course-tab-list {
412   width: 880px;
413   height: auto;
414   padding: 20px;
415   background: #fff;
416   float: left;
417   box-sizing: border-box;
418   overflow: hidden;
419   position: relative;
420   box-shadow: 0 2px 4px 0 #f0f0f0;
421 }
422 
423 .tab-item {
424   width: 880px;
425   background: #fff;
426   padding-bottom: 20px;
427   box-shadow: 0 2px 4px 0 #f0f0f0;
428 }
429 
430 .tab-item-title {
431   justify-content: space-between;
432   padding: 25px 20px 11px;
433   border-radius: 4px;
434   margin-bottom: 20px;
435   border-bottom: 1px solid #333;
436   border-bottom-color: rgba(51, 51, 51, .05);
437   overflow: hidden;
438 }
439 
440 .chapter {
441   font-size: 17px;
442   color: #4a4a4a;
443   float: left;
444 }
445 
446 .chapter-length {
447   float: right;
448   font-size: 14px;
449   color: #9b9b9b;
450   letter-spacing: .19px;
451 }
452 
453 .chapter-title {
454   font-size: 16px;
455   color: #4a4a4a;
456   letter-spacing: .26px;
457   padding: 12px;
458   background: #eee;
459   border-radius: 2px;
460   display: -ms-flexbox;
461   display: flex;
462   -ms-flex-align: center;
463   align-items: center;
464 }
465 
466 .chapter-title img {
467   width: 18px;
468   height: 18px;
469   margin-right: 7px;
470   vertical-align: middle;
471 }
472 
473 .section-list {
474   padding: 0 20px;
475 }
476 
477 .section-list .section-item {
478   padding: 15px 20px 15px 36px;
479   cursor: pointer;
480   justify-content: space-between;
481   position: relative;
482   overflow: hidden;
483 }
484 
485 .section-item .name {
486   font-size: 14px;
487   color: #666;
488   float: left;
489 }
490 
491 .section-item .index {
492   margin-right: 5px;
493 }
494 
495 .section-item .free {
496   font-size: 12px;
497   color: #fff;
498   letter-spacing: .19px;
499   background: #ffc210;
500   border-radius: 100px;
501   padding: 1px 9px;
502   margin-left: 10px;
503 }
504 
505 .section-item .time {
506   font-size: 14px;
507   color: #666;
508   letter-spacing: .23px;
509   opacity: 1;
510   transition: all .15s ease-in-out;
511   float: right;
512 }
513 
514 .section-item .time img {
515   width: 18px;
516   height: 18px;
517   margin-left: 15px;
518   vertical-align: text-bottom;
519 }
520 
521 .section-item .try {
522   width: 86px;
523   height: 28px;
524   background: #ffc210;
525   border-radius: 4px;
526   font-size: 14px;
527   color: #fff;
528   position: absolute;
529   right: 20px;
530   top: 10px;
531   opacity: 0;
532   transition: all .2s ease-in-out;
533   cursor: pointer;
534   outline: none;
535   border: none;
536 }
537 
538 .section-item:hover {
539   background: #fcf7ef;
540   box-shadow: 0 0 0 0 #f3f3f3;
541 }
542 
543 .section-item:hover .name {
544   color: #333;
545 }
546 
547 .section-item:hover .try {
548   opacity: 1;
549 }
550 
551 .course-side {
552   width: 300px;
553   height: auto;
554   margin-left: 20px;
555   float: right;
556 }
557 
558 .teacher-info {
559   background: #fff;
560   margin-bottom: 20px;
561   box-shadow: 0 2px 4px 0 #f0f0f0;
562 }
563 
564 .side-title {
565   font-weight: normal;
566   font-size: 17px;
567   color: #4a4a4a;
568   padding: 18px 14px;
569   border-bottom: 1px solid #333;
570   border-bottom-color: rgba(51, 51, 51, .05);
571 }
572 
573 .side-title span {
574   display: inline-block;
575   border-left: 2px solid #ffc210;
576   padding-left: 12px;
577 }
578 
579 .teacher-content {
580   padding: 30px 20px;
581   box-sizing: border-box;
582 }
583 
584 .teacher-content .cont1 {
585   margin-bottom: 12px;
586   overflow: hidden;
587 }
588 
589 .teacher-content .cont1 img {
590   width: 54px;
591   height: 54px;
592   margin-right: 12px;
593   float: left;
594 }
595 
596 .teacher-content .cont1 .name {
597   float: right;
598 }
599 
600 .teacher-content .cont1 .teacher-name {
601   width: 188px;
602   font-size: 16px;
603   color: #4a4a4a;
604   padding-bottom: 4px;
605 }
606 
607 .teacher-content .cont1 .teacher-title {
608   width: 188px;
609   font-size: 13px;
610   color: #9b9b9b;
611   white-space: nowrap;
612 }
613 
614 .teacher-content .narrative {
615   font-size: 14px;
616   color: #666;
617   line-height: 24px;
618 }
619 </style>
ActualCourseDetail

 

 

支付成功前端回调

  1 <template>
  2   <div class="pay-success">
  3     <!--如果是单独的页面,就没必要展示导航栏(带有登录的用户)-->
  4     <Header/>
  5     <div class="main">
  6       <div class="title">
  7         <div class="success-tips">
  8           <p class="tips">您已成功购买 1 门课程!</p>
  9         </div>
 10       </div>
 11       <div class="order-info">
 12         <p class="info"><b>订单号:</b><span>{{ result.out_trade_no }}</span></p>
 13         <p class="info"><b>交易号:</b><span>{{ result.trade_no }}</span></p>
 14         <p class="info"><b>付款时间:</b><span><span>{{ result.timestamp }}</span></span></p>
 15       </div>
 16       <div class="study">
 17         <span>立即学习</span>
 18       </div>
 19     </div>
 20   </div>
 21 </template>
 22 
 23 <script>
 24 import Header from "@/components/Header"
 25 import api from '../assets/js/settings'
 26 export default {
 27   name: "Success",
 28   data() {
 29     return {
 30       result: {},
 31     };
 32   },
 33   created() {
 34     // url后拼接的参数:?及后面的所有参数 => ?a=1&b=2
 35     console.log(location.search);
 36 
 37     // 解析支付宝回调的url参数
 38     let params = location.search.substring(1);  // 去除? => a=1&b=2
 39 
 40     let items = params.length ? params.split('&') : [];  // ['a=1', 'b=2']
 41     //逐个将每一项添加到args对象中
 42     for (let i = 0; i < items.length; i++) {  // 第一次循环a=1,第二次b=2
 43       let k_v = items[i].split('=');  // ['a', '1']
 44       //解码操作,因为查询字符串经过编码的
 45       if (k_v.length >= 2) {
 46         // url编码反解
 47         let k = decodeURIComponent(k_v[0]);
 48         this.result[k] = decodeURIComponent(k_v[1]);
 49         // 没有url编码反解
 50         // this.result[k_v[0]] = k_v[1];
 51       }
 52 
 53     }
 54     // 解析后的结果
 55     console.log(this.result);
 56 
 57 
 58     // 把地址栏上面的支付结果,再get请求转发给后端
 59     this.$axios({
 60       url: api.success + location.search,
 61       method: 'get',
 62     }).then(response => {
 63       // console.log(response.data);
 64         this.$message({
 65           type:"success",
 66           message:response.data.msg
 67         })
 68     }).catch(() => {
 69       console.log('支付结果同步失败');
 70     })
 71   },
 72   components: {
 73     Header,
 74   }
 75 }
 76 </script>
 77 
 78 <style scoped>
 79 .main {
 80   padding: 60px 0;
 81   margin: 0 auto;
 82   width: 1200px;
 83   background: #fff;
 84 }
 85 
 86 .main .title {
 87   display: flex;
 88   -ms-flex-align: center;
 89   align-items: center;
 90   padding: 25px 40px;
 91   border-bottom: 1px solid #f2f2f2;
 92 }
 93 
 94 .main .title .success-tips {
 95   box-sizing: border-box;
 96 }
 97 
 98 .title img {
 99   vertical-align: middle;
100   width: 60px;
101   height: 60px;
102   margin-right: 40px;
103 }
104 
105 .title .success-tips {
106   box-sizing: border-box;
107 }
108 
109 .title .tips {
110   font-size: 26px;
111   color: #000;
112 }
113 
114 
115 .info span {
116   color: #ec6730;
117 }
118 
119 .order-info {
120   padding: 25px 48px;
121   padding-bottom: 15px;
122   border-bottom: 1px solid #f2f2f2;
123 }
124 
125 .order-info p {
126   display: -ms-flexbox;
127   display: flex;
128   margin-bottom: 10px;
129   font-size: 16px;
130 }
131 
132 .order-info p b {
133   font-weight: 400;
134   color: #9d9d9d;
135   white-space: nowrap;
136 }
137 
138 .study {
139   padding: 25px 40px;
140 }
141 
142 .study span {
143   display: block;
144   width: 140px;
145   height: 42px;
146   text-align: center;
147   line-height: 42px;
148   cursor: pointer;
149   background: #ffc210;
150   border-radius: 6px;
151   font-size: 16px;
152   color: #fff;
153 }
154 </style>

 后端回调支付接口

 1 # 支付成功回调接口
 2 class OrderSuccessView(APIView):
 3     def get(self, request, *args, **kwargs):
 4         # 1.取出订单号
 5         out_trade_no = request.query_params.get('out_trade_no')
 6         # 2.去数据库中查询
 7         order = Order.objects.filter(out_trade_no=out_trade_no, order_status=1).first()  
8
      if order: # 如果有值,说明支付宝的post回调成功,修改了订单状态
9
        return APIResponse(code=200, msg='支付成功,请去学习吧')
10       else:
11         return APIResponse(code=101, msg='暂未收到你的付款,请稍后再试')
12
13
14 ------------------------------
15
路由
16
# 127.0.0.1:8000/api/v1/order/success/--->get

17 path('success/', OrderSuccessView.as_view()),

 

----------------------------

给支付宝回调用(我们没办法实现)

支付宝回调数据格式

#  回调数据格式
data = {
     "subject": "测试订单",
     "gmt_payment": "2016-11-16 11:42:19",
     "charset": "utf-8",
     "seller_id": "xxxx",
     "trade_status": "TRADE_SUCCESS",
     "buyer_id": "xxxx",
     "auth_app_id": "xxxx",
     "buyer_pay_amount": "0.01",
     "version": "1.0",
     "gmt_create": "2016-11-16 11:42:18",
     "trade_no": "xxxx",
     "fund_bill_list": "[{\"amount\":\"0.01\",\"fundChannel\":\"ALIPAYACCOUNT\"}]",
     "app_id": "xxxx",
     "notify_time": "2016-11-16 11:42:19",
     "point_amount": "0.00",
     "total_amount": "0.01",
     "notify_type": "trade_status_sync",
     "out_trade_no": "订单号", # 咱们的uuid
     "buyer_logon_id": "xxxx",
     "notify_id": "xxxx",
     "seller_email": "xxxx",
     "receipt_amount": "0.01",
     "invoice_amount": "0.01",
     "sign": "签名" # 验证签名
}

内网穿透

  https://zhuanlan.zhihu.com/p/370483324详细操作步骤,请移步

    # post 给支付宝回调用--修改订单状态--->
    # 支付宝异步回调--》如果不返回正常的响应,会多次回调
    # 这个接口需要加认证类吗?--》支付宝用,没有token的,所以一定不能加认证类
    # 支付宝,在公网---》目前咱们测试--》永远回调不进来--》写完没法测试
    # 内网穿透:花生壳,第三方内外穿透软件,原来免费,后来收费

    def post(self, request, *args, **kwargs):
        try:
            # json编码 -->是字典
            # urlencoded--》querydict---》dict()----》纯字典
            # 支付宝回调编码是:urlencoded
            result_data = request.data.dict()  # 把request.data  ---> 转成字典格式
            out_trade_no = result_data.get('out_trade_no')
            trade_no = result_data.get('trade_no')
            signature = result_data.pop('sign')
            pay_time = result_data.pop('notify_time', '2024-06-01')

            result = alipay.verify(result_data, signature)
            if result and result_data["trade_status"] in ("TRADE_SUCCESS", "TRADE_FINISHED"):
                # 完成订单修改:订单状态、流水号、支付时间
                Order.objects.filter(out_trade_no=out_trade_no).update(order_status=1, trade_no=trade_no,
                                                                       pay_time=pay_time)
                # 完成日志记录
                logger.warning('%s订单支付成功' % out_trade_no)
                return Response('success')
            else:
                logger.error('%s订单支付失败' % out_trade_no)
        except:
            pass
        return Response('failed')

 

posted on 2024-05-20 22:25  认真的六六  阅读(2)  评论(0编辑  收藏  举报