【课程板块表分析】
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">筛 选:</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>
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 }}人在学 课程总时长:{{ 22 course_info.sections 23 }}课时/{{ course_info.pub_sections }}小时 难度:{{ 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>
【视频托管】
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>
[搜索功能接口]
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 在代码中,使用【支付宝公钥】和刚刚生成的【私钥】,放到代码中
第二步下载安装
pip install python-alipay-sdk
第三步:
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-----
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 }}人在学 课程总时长:{{ 22 course_info.sections 23 }}课时/{{ course_info.pub_sections }}小时 难度:{{ 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>
支付成功前端回调
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')