简介:这是一套基于Django开发的线上课程推荐数据分析系统,面向高校计算机专业学生及Web开发初学者,适用于毕业设计、课程设计与工程实训场景。系统聚焦慕课平台课程数据的可视化分析与属性管理,融合BI式图表展示(如课程类型分布、推荐效果趋势等),帮助学习者掌握Django全栈开发、数据建模与前端可视化集成能力。压缩包共252个文件,含26个核心Python源码、44个JS交互脚本、21个CSS样式文件、75个GIF动效资源及1个SQL数据库初始化文件,另有HTML页面、字体资源与文档素材,整体8.83MB,结构清晰、开箱即用。已有84人学习下载,提供完整可运行源码、配套SQL建表语句与说明文档(LW),涵盖前后端协同逻辑、Bootstrap+Layui双框架整合实践及Chartist图表库应用范例,是理解教育数据分析系统架构的优质入门项目。
1. 这不是又一个“课程列表页”:Django驱动的慕课推荐分析系统,把线上教育数据真正跑通成BI看板
你见过多少个“中国大学慕课”相关的毕业设计?十有八九是爬虫+前端展示+简单统计——点开就三张饼图、五个柱状图,后台连用户行为日志都没存。但这个5p014系统不一样:它用 Django 原生 ORM 构建了完整的课程-用户-推荐-反馈四维关系模型,SQL 文件里包含course_click_log、recommend_session、user_profile_v2三张核心表,字段命名直接对标真实教育平台埋点规范(比如click_duration_sec、rec_source_type ENUM('collab','content_based','popularity'))。它不只展示“哪些课热门”,而是能回答:“协同过滤推荐在理工科用户中点击率比热度推荐高12.7%,但完课率低8.3%——为什么?”首页那张主题图不是装饰,而是 Chartist.js 渲染的实时推荐转化漏斗图,数据源直连 Django 的RecommendAnalyticsView。适合正在做课程设计、毕设选题卡在“没业务深度”的同学,也适合想练手真实教育场景下 Django + SQL + 可视化闭环的同学——它不是玩具,是能跑通从用户点击、算法打分、AB测试分组到归因分析的最小可行分析系统。
2. Django后端架构解析:为什么用原生ORM而非Pandas直连数据库做分析?
2.1 选型逻辑:教育类分析系统对事务性与可维护性的硬性要求
很多初学者一上来就想用 Pandas 读取 CSV 或 SQLite 做分析,但在慕课场景下这会迅速失效。真实线上课程平台每天产生数万条点击日志,且需支持“用户修改标签偏好→实时影响推荐结果→回溯该用户历史推荐链路”的强一致性操作。本项目采用 Django ORM 而非 raw SQL 或 Pandas,核心原因有三点:第一,recommend_session表与user_profile_v2存在外键级联更新(如用户取消关注某学科标签,需同步清空其所有未过期的基于内容的推荐缓存);第二,Django Admin 后台需直接管理course_attribute(课程属性表),该表含difficulty_level TINYINT CHECK (difficulty_level BETWEEN 1 AND 5)等带约束的字段,Pandas 无法校验;第三,所有分析接口(如/api/v1/analytics/click_rate_by_category/)必须通过 Django 中间件统一鉴权与请求限流,这是 Web 框架层能力,非数据分析库能覆盖。项目 SQL 文件中CREATE TABLE recommend_session (...) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;明确指定 InnoDB 引擎,正是为支撑这些事务需求。
2.2 核心模型设计:四张表如何构成推荐分析的数据骨架
项目数据库共 12 张表,但真正驱动分析逻辑的是以下四张主表。它们不是孤立存在,而是通过外键形成闭环:
| 表名 | 关键字段 | 作用说明 | Django Model 关联方式 |
|---|---|---|---|
course | id,title,category_id,difficulty_level,avg_rating | 课程元数据主表,category_id外键指向course_category | class Course(models.Model): ... |
user_profile_v2 | user_id,major,grade_year,preferred_categories JSON | 用户画像增强版,preferred_categories存储["AI","DataScience"]类数组 | class UserProfileV2(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) |
recommend_session | session_id,user_id,course_id,rec_algorithm,is_clicked,click_time | 单次推荐事件原子记录,is_clicked为布尔值用于计算 CTR | class RecommendSession(models.Model): user = models.ForeignKey(UserProfileV2, on_delete=models.CASCADE) |
course_click_log | log_id,user_id,course_id,duration_sec,exit_point | 用户观看行为明细,exit_point记录跳出章节(如 "Ch3_Section2") | class CourseClickLog(models.Model): course = models.ForeignKey(Course, on_delete=models.CASCADE) |
提示:
preferred_categories字段使用 JSON 类型而非新建关联表,是为支持用户动态增删兴趣标签(如新增“量子计算”),避免频繁 ALTER TABLE。Django 3.1+ 原生支持JSONField,无需第三方包。
2.3 分析视图实现:如何用 Django ORM 写出等效于 BI 工具的聚合查询
以首页漏斗图中的“推荐→点击→完课”三阶段转化率为例,后端views.py中的RecommendFunnelView并未用pd.read_sql(),而是通过链式 QuerySet 实现:
# analytics/views.py from django.db.models import Count, Sum, Avg, Q, F from django.http import JsonResponse from .models import RecommendSession, CourseClickLog def get_recommend_funnel_data(): # 阶段1:总推荐数(按算法分组) total_recommends = ( RecommendSession.objects .values('rec_algorithm') .annotate(total_count=Count('id')) ) # 阶段2:点击数(需关联 click_log 判断是否有效点击) clicked_recommends = ( RecommendSession.objects .filter(is_clicked=True) .values('rec_algorithm') .annotate(clicked_count=Count('id')) ) # 阶段3:完课数(定义:观看时长 ≥ 课程平均时长 × 0.8) avg_duration_per_course = CourseClickLog.objects.values('course_id').annotate( avg_duration=Avg('duration_sec') ).values_list('course_id', 'avg_duration') # 构建字典加速查询 course_avg_map = {cid: avg for cid, avg in avg_duration_per_course} completed_recommends = [] for rec in RecommendSession.objects.filter(is_clicked=True): if rec.course_id in course_avg_map: expected_min = course_avg_map[rec.course_id] * 0.8 actual_log = CourseClickLog.objects.filter( user_id=rec.user_id, course_id=rec.course_id ).order_by('-click_time').first() if actual_log and actual_log.duration_sec >= expected_min: completed_recommends.append(rec.rec_algorithm) # 汇总为字典格式供前端渲染 funnel_data = {} for algo in ['collab', 'content_based', 'popularity']: total = next((item['total_count'] for item in total_recommends if item['rec_algorithm'] == algo), 0) clicked = next((item['clicked_count'] for item in clicked_recommends if item['rec_algorithm'] == algo), 0) completed = completed_recommends.count(algo) funnel_data[algo] = { 'recommend': total, 'click': clicked, 'complete': completed, 'ctr': round(clicked / total * 100, 2) if total else 0, 'completion_rate': round(completed / clicked * 100, 2) if clicked else 0 } return funnel_data这段代码的关键在于:它没有把全量数据拉到 Python 内存再计算,而是用values().annotate()下推聚合到数据库层处理total_recommends和clicked_recommends;对“完课”这种需跨表关联且含业务规则(0.8倍阈值)的逻辑,则用course_avg_map缓存预计算结果,避免 N+1 查询。参数说明:rec_algorithm字段值限定为三个枚举,确保前端图表分类不混乱;completion_rate分母用clicked而非recommend,符合漏斗分析标准口径。
3. 前端可视化实现:Layui + Chartist.js 如何承载教育数据的多维表达
3.1 技术栈组合的深层考量:为什么不用 ECharts 或 AntV?
项目引入的chartist.min.js和chartist.min.css并非随意选择。对比主流可视化库:ECharts 功能强大但体积超 500KB,而本系统部署目标是校园内网低带宽环境(实测部分高校实验室出口带宽仅 20Mbps);AntV 生态复杂,需额外配置 G6、F2 等子库。Chartist 优势在于:压缩后仅 86KB,支持 SVG 渲染(适配 Retina 屏且可无损缩放),且 API 极简——new Chartist.Line('.ct-chart', data, options)一行即可初始化。更重要的是,它与 Layui 的 CSS 体系天然兼容:Layui 的.layui-card圆角阴影与 Chartist 的.ct-chart容器无缝嵌套,无需重写样式。项目static/js/charts/course_category_analysis.js中,所有图表均采用响应式配置:
// static/js/charts/course_category_analysis.js const categoryData = { labels: ['人工智能', '数据科学', '操作系统', '计算机网络', '软件工程'], series: [ [42, 58, 39, 65, 48], // 推荐数 [28, 41, 26, 49, 33], // 点击数 [19, 27, 18, 32, 22] // 完课数 ] }; const categoryOptions = { fullWidth: true, chartPadding: { right: 40 }, axisX: { labelInterpolationFnc: function(value) { return value.length > 4 ? value.substring(0, 4) + '...' : value; } }, plugins: [ Chartist.plugins.ctAxisTitle({ axisX: { axisTitle: '课程类别', axisClass: 'ct-axis-title', offset: { x: 0, y: 30 } } }) ] }; // 初始化双Y轴图表(左侧推荐/点击,右侧完课) new Chartist.Bar('.ct-category-bar', categoryData, categoryOptions);参数说明:fullWidth: true使图表占满容器宽度,适配 Layui 的栅格系统;labelInterpolationFnc解决中文标签过长导致重叠问题;ctAxisTitle插件为 X 轴添加标题,避免手动插入 DOM 元素破坏 Layui 卡片结构。
3.2 Layui 组件与 Django 模板的深度集成策略
项目未使用 Vue/React,而是用 Layui 的layui.use(['table', 'form', 'laydate'])加载模块,原因在于:Django 模板引擎(Django Template Language)与 Layui 的># admin_views.py def course_attribute_list(request): attributes = CourseAttribute.objects.select_related('course').values( 'id', 'course__title', 'difficulty_level', 'is_certified', 'update_time' ) return JsonResponse({'code': 0, 'msg': '', 'count': attributes.count(), 'data': list(attributes)})
前端模板中,Layui Table 直接绑定:
<!-- templates/admin/course_attribute_list.html --> <table id="attributeTable" lay-filter="attributeFilter"></table> <script> layui.use(['table'], function(){ var table = layui.table; table.render({ elem: '#attributeTable', url: '{% url "admin:course_attribute_list" %}', // Django URL 反向解析 page: true, cols: [[ {field:'id', title:'ID', width:80}, {field:'course__title', title:'课程名称', width:200}, {field:'difficulty_level', title:'难度等级', width:120, templet: '#levelTpl'}, {field:'is_certified', title:'认证课程', width:120, templet: '#certTpl'}, {field:'update_time', title:'更新时间', width:180} ]] }); }); </script>关键点在于:url使用{% url "admin:course_attribute_list" %}而非硬编码/api/...,确保 Django 的 URL 命名空间机制生效;templet: '#levelTpl'引用页面内<script type="text/html" id="levelTpl">模板,实现难度等级数字到星级图标(★☆☆☆☆)的转换,完全复用 Layui 图标体系。
3.3 Bootstrap 与 Layui 的样式冲突规避方案
项目同时引入bootstrap.min.css和layui.css,必然存在 CSS 优先级冲突(如.btn类定义)。解决方案是:Bootstrap 仅用于基础栅格与表单组件,Layui 覆盖所有交互组件。具体实施如下:
- 在
base.html中,Bootstrap CSS 放置在<head>最前,Layui CSS 紧随其后; - 所有按钮、弹窗、表格、分页均使用 Layui 组件(
<button class="layui-btn">),禁用 Bootstrap 按钮类; - Bootstrap 的
container和row/col-*仍用于页面布局,因其.container的 max-width 与.row的负 margin 设计更灵活; - 自定义覆盖样式写在
style.css中,且强制使用!important修正冲突(如input.form-control { border-radius: 2px !important; })。
注意:
animate.css仅用于首页主题图入场动画(class="animated fadeInUp"),不与其他组件混用,避免性能损耗。项目实测在 Chrome 115 下,首页首屏渲染时间控制在 1.2s 内(含 3 个 Chartist 图表初始化)。
4. 数据分析实战:从SQL文件还原真实慕课平台的埋点逻辑与归因路径
4.1 SQL文件中的隐藏线索:一张表揭示教育平台的核心指标体系
项目附带的init_db.sql不是简单建表语句,而是教育数据产品的指标设计蓝图。重点解析course_click_log表结构:
CREATE TABLE `course_click_log` ( `log_id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `course_id` int(11) NOT NULL, `chapter_id` varchar(50) DEFAULT NULL, `section_id` varchar(50) DEFAULT NULL, `duration_sec` int(11) NOT NULL DEFAULT '0', `exit_point` varchar(100) DEFAULT NULL, `device_type` enum('PC','Mobile','Tablet') NOT NULL DEFAULT 'PC', `referrer` varchar(255) DEFAULT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`log_id`), KEY `idx_user_course_time` (`user_id`,`course_id`,`created_at`), KEY `idx_chapter_section` (`chapter_id`,`section_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;字段深意:
chapter_id与section_id组合构成视频章节定位,支持“用户在操作系统课程第3章第2节跳出”这类精准归因;device_type枚举值明确区分终端,为后续分析“移动端用户完课率比PC低22%”提供依据;referrer字段存储来源(如https://www.icourse163.org/course/XXX),可追踪外部渠道推荐效果;- 复合索引
idx_user_course_time是查询某用户某课程全部观看记录的性能保障,避免全表扫描。
4.2 归因分析案例:如何用Django ORM复现“协同过滤推荐提升新用户留存”的结论
假设毕业论文需验证“协同过滤算法对注册30天内新用户的7日留存率影响”。SQL 文件中user_profile_v2表含register_time DATETIME字段,recommend_session表含session_start_time DATETIME。Django 中实现归因查询:
# analytics/attributions.py from django.db.models import Count, Case, When, IntegerField, Avg from django.utils import timezone from datetime import timedelta from .models import UserProfileV2, RecommendSession, CourseClickLog def calculate_cf_retention_impact(): # 筛选注册30天内的新用户 thirty_days_ago = timezone.now() - timedelta(days=30) new_users = UserProfileV2.objects.filter( register_time__gte=thirty_days_ago ).values_list('user_id', flat=True) # 获取这些用户中使用协同过滤的会话 cf_sessions = RecommendSession.objects.filter( user_id__in=new_users, rec_algorithm='collab', session_start_time__gte=thirty_days_ago ).select_related('user') # 计算7日留存:注册后第7天仍有点击行为 retention_results = [] for session in cf_sessions: # 查找该用户在 session_start_time 后7天内是否有任何点击 has_click_in_7days = CourseClickLog.objects.filter( user_id=session.user_id, created_at__range=( session.session_start_time, session.session_start_time + timedelta(days=7) ) ).exists() retention_results.append(has_click_in_7days) cf_retention_rate = sum(retention_results) / len(retention_results) if retention_results else 0 # 对照组:同批新用户中使用热度推荐的留存率 pop_sessions = RecommendSession.objects.filter( user_id__in=new_users, rec_algorithm='popularity', session_start_time__gte=thirty_days_ago ) pop_retention_results = [] for session in pop_sessions: has_click_in_7days = CourseClickLog.objects.filter( user_id=session.user_id, created_at__range=( session.session_start_time, session.session_start_time + timedelta(days=7) ) ).exists() pop_retention_results.append(has_click_in_7days) pop_retention_rate = sum(pop_retention_results) / len(pop_retention_results) if pop_retention_results else 0 return { 'cf_retention_rate': round(cf_retention_rate * 100, 2), 'pop_retention_rate': round(pop_retention_rate * 100, 2), 'lift': round((cf_retention_rate - pop_retention_rate) * 100, 2) } # 调用示例 result = calculate_cf_retention_impact() print(f"协同过滤组7日留存率: {result['cf_retention_rate']}%") print(f"热度推荐组7日留存率: {result['pop_retention_rate']}%") print(f"提升幅度: {result['lift']}%")此代码严格遵循教育数据分析规范:以“推荐发生时刻”为起点(非注册时刻),计算后续7天行为;对照组与实验组用户池完全一致(new_users),排除用户分层偏差;exists()方法替代count()>0,提升查询效率。
5. 部署与调试技巧:宝塔面板下Django项目的Nginx反向代理避坑指南
5.1 宝塔部署关键配置:静态资源分离与Gunicorn进程管理
项目在宝塔面板(v8.0+)部署时,必须将 Django 静态文件与用户上传文件物理分离。settings.py中配置:
# settings.py import os from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent.parent # 注意:项目解压后是5p014/目录,故上溯三级 STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'collected_static') # 宝塔中需将此目录映射为网站静态路径 MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # 用户上传的课程封面等文件存放处宝塔操作步骤:
- 在网站设置 → 网站目录中,根目录设为
5p014/(即 manage.py 所在目录); - 在网站设置 → 静态文件中,添加静态路径:URI
/static/→ 实际路径/www/wwwroot/your-site.com/5p014/collected_static/; - 在网站设置 → 静态文件中,添加媒体路径:URI
/media/→ 实际路径/www/wwwroot/your-site.com/5p014/media/; - 在宝塔终端中执行:
cd /www/wwwroot/your-site.com/5p014 && python3 manage.py collectstatic --noinput,生成collected_static目录。
提示:若跳过第2步直接让 Nginx 代理
/static/到STATIC_ROOT,会导致 Django Debug 模式下静态文件 404(因 DEBUG=False 时 Django 不再自动提供静态文件)。
5.2 Gunicorn 启动脚本与内存泄漏防护
项目未使用 uWSGI,而是 Gunicorn(gunicorn.conf.py已内置)。关键参数配置:
# gunicorn.conf.py import multiprocessing bind = "127.0.0.1:8000" bind_address = "127.0.0.1:8000" workers = multiprocessing.cpu_count() * 2 + 1 worker_class = "sync" worker_connections = 1000 timeout = 30 keepalive = 2 max_requests = 1000 max_requests_jitter = 100 preload = True daemon = True pidfile = "/www/wwwroot/your-site.com/5p014/gunicorn.pid" accesslog = "/www/wwwroot/your-site.com/5p014/logs/access.log" errorlog = "/www/wwwroot/your-site.com/5p014/logs/error.log" loglevel = "info" capture_output = True enable_stdio_inheritance = True参数说明:max_requests=1000是防止内存泄漏的核心——每个 worker 处理 1000 个请求后自动重启;preload=True确保所有 worker 共享同一份 Django 应用实例,避免重复加载模型;loglevel="info"开启详细日志,便于排查recommend_session表写入失败问题(常见于 MySQL 连接超时)。
5.3 常见报错定位:当首页主题图不显示时的三层排查法
若部署后首页大图(<div class="hero-banner">)空白,按顺序检查:
- 前端层:浏览器开发者工具 Network 标签页,查看
hero-banner.jpg是否返回 404。若是,检查宝塔静态路径是否正确映射到5p014/static/images/; - Django 层:查看
error.log中是否有TemplateDoesNotExist: base.html错误。若有,确认TEMPLATES配置中DIRS包含os.path.join(BASE_DIR, 'templates'); - 数据库层:运行
python3 manage.py dbshell,执行SELECT COUNT(*) FROM course WHERE is_active=1;。若返回 0,说明init_db.sql未成功导入,需在宝塔数据库管理中手动执行。
最终验证命令:curl -I http://localhost:8000/static/css/bootstrap.min.css应返回HTTP/1.1 200 OK,证明静态服务已通。
本文还有配套的精品资源,点击获取