尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Django教育推荐系统:ORM驱动的BI分析闭环实践

Django教育推荐系统:ORM驱动的BI分析闭环实践 简介这是一套基于Django开发的线上课程推荐数据分析系统面向高校计算机专业学生及Web开发初学者适用于毕业设计、课程设计与工程实训场景。系统聚焦慕课平台课程数据的可视化分析与属性管理融合BI式图表展示如课程类型分布、推荐效果趋势等帮助学习者掌握Django全栈开发、数据建模与前端可视化集成能力。压缩包共252个文件含26个核心Python源码、44个JS交互脚本、21个CSS样式文件、75个GIF动效资源及1个SQL数据库初始化文件另有HTML页面、字体资源与文档素材整体8.83MB结构清晰、开箱即用。已有84人学习下载提供完整可运行源码、配套SQL建表语句与说明文档LW涵盖前后端协同逻辑、BootstrapLayui双框架整合实践及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 (...) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;明确指定 InnoDB 引擎正是为支撑这些事务需求。2.2 核心模型设计四张表如何构成推荐分析的数据骨架项目数据库共 12 张表但真正驱动分析逻辑的是以下四张主表。它们不是孤立存在而是通过外键形成闭环表名关键字段作用说明Django Model 关联方式courseid,title,category_id,difficulty_level,avg_rating课程元数据主表category_id外键指向course_categoryclass Course(models.Model): ...user_profile_v2user_id,major,grade_year,preferred_categories JSON用户画像增强版preferred_categories存储[AI,DataScience]类数组class UserProfileV2(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE)recommend_sessionsession_id,user_id,course_id,rec_algorithm,is_clicked,click_time单次推荐事件原子记录is_clicked为布尔值用于计算 CTRclass RecommendSession(models.Model): user models.ForeignKey(UserProfileV2, on_deletemodels.CASCADE)course_click_loglog_id,user_id,course_id,duration_sec,exit_point用户观看行为明细exit_point记录跳出章节如 Ch3_Section2class CourseClickLog(models.Model): course models.ForeignKey(Course, on_deletemodels.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_countCount(id)) ) # 阶段2点击数需关联 click_log 判断是否有效点击 clicked_recommends ( RecommendSession.objects .filter(is_clickedTrue) .values(rec_algorithm) .annotate(clicked_countCount(id)) ) # 阶段3完课数定义观看时长 ≥ 课程平均时长 × 0.8 avg_duration_per_course CourseClickLog.objects.values(course_id).annotate( avg_durationAvg(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_clickedTrue): if rec.course_id in course_avg_map: expected_min course_avg_map[rec.course_id] * 0.8 actual_log CourseClickLog.objects.filter( user_idrec.user_id, course_idrec.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缓存预计算结果避免 N1 查询。参数说明rec_algorithm字段值限定为三个枚举确保前端图表分类不混乱completion_rate分母用clicked而非recommend符合漏斗分析标准口径。3. 前端可视化实现Layui Chartist.js 如何承载教育数据的多维表达3.1 技术栈组合的深层考量为什么不用 ECharts 或 AntV项目引入的chartist.min.js和chartist.min.css并非随意选择。对比主流可视化库ECharts 功能强大但体积超 500KB而本系统部署目标是校园内网低带宽环境实测部分高校实验室出口带宽仅 20MbpsAntV 生态复杂需额外配置 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 idattributeTable lay-filterattributeFilter/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 typetext/html idlevelTpl模板实现难度等级数字到星级图标★☆☆☆☆的转换完全复用 Layui 图标体系。3.3 Bootstrap 与 Layui 的样式冲突规避方案项目同时引入bootstrap.min.css和layui.css必然存在 CSS 优先级冲突如.btn类定义。解决方案是Bootstrap 仅用于基础栅格与表单组件Layui 覆盖所有交互组件。具体实施如下在base.html中Bootstrap CSS 放置在head最前Layui CSS 紧随其后所有按钮、弹窗、表格、分页均使用 Layui 组件button classlayui-btn禁用 Bootstrap 按钮类Bootstrap 的container和row/col-*仍用于页面布局因其.container的 max-width 与.row的负 margin 设计更灵活自定义覆盖样式写在style.css中且强制使用!important修正冲突如input.form-control { border-radius: 2px !important; }。注意animate.css仅用于首页主题图入场动画classanimated 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段深意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(days30) new_users UserProfileV2.objects.filter( register_time__gtethirty_days_ago ).values_list(user_id, flatTrue) # 获取这些用户中使用协同过滤的会话 cf_sessions RecommendSession.objects.filter( user_id__innew_users, rec_algorithmcollab, session_start_time__gtethirty_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_idsession.user_id, created_at__range( session.session_start_time, session.session_start_time timedelta(days7) ) ).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__innew_users, rec_algorithmpopularity, session_start_time__gtethirty_days_ago ) pop_retention_results [] for session in pop_sessions: has_click_in_7days CourseClickLog.objects.filter( user_idsession.user_id, created_at__range( session.session_start_time, session.session_start_time timedelta(days7) ) ).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因 DEBUGFalse 时 Django 不再自动提供静态文件。5.2 Gunicorn 启动脚本与内存泄漏防护项目未使用 uWSGI而是 Gunicorngunicorn.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_requests1000是防止内存泄漏的核心——每个 worker 处理 1000 个请求后自动重启preloadTrue确保所有 worker 共享同一份 Django 应用实例避免重复加载模型loglevelinfo开启详细日志便于排查recommend_session表写入失败问题常见于 MySQL 连接超时。5.3 常见报错定位当首页主题图不显示时的三层排查法若部署后首页大图div classhero-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_active1;。若返回 0说明init_db.sql未成功导入需在宝塔数据库管理中手动执行。最终验证命令curl -I http://localhost:8000/static/css/bootstrap.min.css应返回HTTP/1.1 200 OK证明静态服务已通。本文还有配套的精品资源点击获取
返回列表