资讯详情

资讯详情

Django学生选课系统实战:从数据模型到并发事务避坑指南

简介面向计算机相关专业毕业设计或课程设计场景这份Python结合Django框架实现的学生教务选课系统源码覆盖学生、教师、管理员三类角色的身份验证与权限管理以及课程信息维护、学生选课、选课统计等核心业务适合需要快速搭建Web系统或理解Django开发流程的学习者。压缩包共5.66MB包含2000个文件其中1632个js文件用于前端交互250个html与54个css负责页面结构与样式18个json配置数据5个py文件实现Django核心逻辑整体结构清晰便于直接部署或二次开发。目前已有182人学习浏览可作为毕业设计源码参考、课程项目练习或企业信息化选课系统的初版原型。通过阅读代码可掌握Django MTV架构、ORM数据库操作、用户认证与会话管理以及前端资源组织方式同时为后续扩展课程评价、成绩管理等模块提供了可复用的基础框架。1. 学生选课系统为什么值得用Django做一遍学生教务选课系统是Django练手和毕业设计里出现频率最高的完整项目之一但常见不等于好做。一个能被导师或评审看得过去的选课系统至少要同时满足三类需求学生能登录查看课程、选课、退课管理员能发布课程、设置容量系统在多人同时选课时不超选、不冲突。这三个需求恰好把Django的模型外键、ORM事务、基于类的视图、模板渲染和权限控制全部串了起来。对一个想靠框架实操拿出手的开发新手来说沿着这条路径做下来会比堆十个静态页面有说服力得多。这篇按我做完一套选课系统时的思路展开从表设计讲到避坑细节照着走能跑通改一改就是自己的底子。2. Django选课系统的数据模型设计三张核心表把选课关系说清楚2.1 用户模型怎么扩展用AbstractUser还是单独建学生表先回答新项目最开始的选型问题学生信息是放在User表里还是单独建一张学生档案表。我见过一些项目把学号、专业、年级全塞进User表图省事结果User表被撑成一张混合业务的大宽表后面加字段和做权限都会束手束脚。更常见的做法是两者结合登录要用的username、password、role放进Django的User体系里学号、专业、年级这类业务属性放到单独的学生档案表通过OneToOne关联。这样做的理由很直接Session认证、登录装饰器、admin后台全部复用Django自带机制不用自己写密码校验和登录状态管理同时学生信息独立成表未来要加“班级”“入学年份”也不会动到认证相关字段。下面是我当时落地的models.py核心部分from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): ROLE_CHOICES [ (student, 学生), (teacher, 教师), (admin, 管理员), ] role models.CharField(角色, max_length20, choicesROLE_CHOICES, defaultstudent) class Meta: verbose_name 用户 verbose_name_plural verbose_name def __str__(self): return f{self.username}{self.get_role_display()} class StudentProfile(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, related_nameprofile) student_no models.CharField(学号, max_length20, uniqueTrue) major models.CharField(专业, max_length50) grade models.CharField(年级, max_length10) class Meta: verbose_name 学生档案 verbose_name_plural verbose_name def __str__(self): return f{self.user.username} - {self.student_no}这里有几个参数值得说明。role字段用了choices枚举取值限制在三个角色内比自己在视图里写字符串if判断要稳。StudentProfile里的student_no加了uniqueTrue直接在同一套数据里约束学号不可重复。OneToOneField的related_nameprofile让我在代码里能通过user.profile拿到学生档案反向查询名字固定不会和别的外键冲突。还有一个关键配置必须在第一次迁移前就写进settings.py否则后面会非常痛苦# settings.py AUTH_USER_MODEL accounts.User这句话告诉Django你这套系统里用户模型是accounts应用里的User不是默认的auth.User。只要你在第一次makemigrations之前把它配上后面所有外键、Session、权限都会指向这个自定义User。如果项目已经跑过迁移再改这一行迁移文件里已经写死了对auth_user的依赖改起来等于重做避坑章节我会专门讲。2.2 课程表和选课记录表上课时间存成枚举而不是自由文本课程表设计里最容易被忽视的是“上课时间”字段。很多初学项目喜欢把时间写成自由文本比如“周一3-4节”“星期三上午”看起来方便但后面做冲突检测时只能靠字符串模糊匹配还要处理全角半角、空格、简称差异纯属给自己挖坑。我的做法是拆成两个字段weekday存星期几slot存第几节到第几节两个字段都用choices约束成固定枚举。class Course(models.Model): WEEKDAY_CHOICES [ (1, 星期一), (2, 星期二), (3, 星期三), (4, 星期四), (5, 星期五), (6, 星期六), (7, 星期日), ] SLOT_CHOICES [ (1-2, 第1-2节), (3-4, 第3-4节), (5-6, 第5-6节), (7-8, 第7-8节), ] course_no models.CharField(课程编号, max_length20, uniqueTrue) name models.CharField(课程名称, max_length100) credit models.DecimalField(学分, max_digits3, decimal_places1) weekday models.PositiveSmallIntegerField(星期, choicesWEEKDAY_CHOICES) slot models.CharField(节次, max_length10, choicesSLOT_CHOICES) capacity models.PositiveIntegerField(容量, default60) selected_count models.PositiveIntegerField(已选人数, default0) teacher models.CharField(授课教师, max_length30) class Meta: ordering [weekday, slot] verbose_name 课程 verbose_name_plural verbose_name def __str__(self): return f{self.name}星期{self.weekday} {self.get_slot_display()} class CourseSelection(models.Model): STATUS_CHOICES [ (active, 已选), (dropped, 已退), ] student models.ForeignKey(User, on_deletemodels.CASCADE, related_nameselections) course models.ForeignKey(Course, on_deletemodels.CASCADE, related_nameselections) status models.CharField(状态, max_length20, choicesSTATUS_CHOICES, defaultactive) selected_at models.DateTimeField(选课时间, auto_now_addTrue) class Meta: ordering [-selected_at] verbose_name 选课记录 verbose_name_plural verbose_name def __str__(self): return f{self.student.username} - {self.course.name}Course里我放了selected_count这个冗余计数。你可能想问为什么不实时聚合CourseSelection表来算人数对选课这种写多读少的场景每一秒都可能有人查看课程列表实时聚合会让列表页每条记录都发一次count查询数据库压力大。用冗余字段的话选课成功就加一退课就减一页面性能高得多。代价是需要保证计数和记录变动在同一个事务里完成这是第四章的重点。CourseSelection没有加unique_together逃逸约束这是有意的妥协。如果你对(student, course)加了唯一约束学生退课后又想重新选这门课就会撞上唯一键报错除非退课直接删记录那就丢了历史数据。我的选择是不加数据库唯一约束由业务层的事务和锁来保证同一学生不会同时有两条active选课同时保留了statusdropped的历史记录。这个权衡后面避坑章节会再讲。2.3 迁移与ORM查询迁移命令执行顺序、select_related与计数器模型写完先别急着写视图把迁移跑通、把数据查明白后面写逻辑才有底气。迁移的顺序有讲究先注册应用再把AUTH_USER_MODEL配好最后执行下面这套命令python manage.py makemigrations accounts course python manage.py migrate python manage.py shell -c from course.models import Course; print(Course.objects.count())makemigrations会把两个应用的模型变化生成成迁移文件migrate负责把变更落到当前数据库。最后一条命令用来确认Course可以正常操作算是迁移后的冒烟检查。如果项目还在开发初期数据库里的数据不重要我通常会直接用sqlite省去数据库配置如果你的环境里装的是MySQL或PostgreSQL记得先建好库和账号再改settings里的DATABASES配置。日常查询里最容易翻车的是N1问题。看课程列表时如果每门课都查一次选课记录页面会慢得不像Django。下面三个查询是选课系统里最高频的写法# 取出某学生当前有效的选课并带上课程数据 selections (CourseSelection.objects .filter(studentuser, statusactive) .select_related(course)) # 按星期和节次排序出全部课程 courses Course.objects.order_by(weekday, slot) # 找出与指定课程在同一时间段的其余课程用于冲突判断 others (Course.objects .filter(weekdaytarget.weekday, slottarget.slot) .exclude(pktarget.pk))第一段用了select_related(course)它会在一条SQL里用JOIN把选课记录和课程一起查出来而不是每一条记录再去查一次课程这是我每次写外键查询都会加上去的习惯。第二段order_by直接用模型Meta里的ordering字段保证列表页顺序稳定。第三段用在冲突检测里weekday和slot两个等值条件组合效率足够。3. 从路由到模板把选课流程做成能点能用的完整链路3.1 URL路由与视图组织形式函数视图和类视图怎么混用模型准备好之后下一步是把页面串起来。我在选课系统里对视图的选择很明确只读展示类的页面用Django基于类的视图涉及写入、事务、校验的操作全部用函数视图。理由很实际选课这种动作需要try-except捕获ValidationError、需要返回消息提示、还需要在事务里执行函数视图写出来最直白新手也容易逐行读懂。课程列表页适合用ListView它能自动处理分页# course/views.py from django.views.generic import ListView from .models import Course, CourseSelection class CourseListView(ListView): model Course template_name course/list.html context_object_name courses paginate_by 10 def get_queryset(self): return Course.objects.order_by(weekday, slot) def get_context_data(self, **kwargs): context super().get_context_data(**kwargs) if self.request.user.is_authenticated: context[my_course_ids] list( CourseSelection.objects .filter(studentself.request.user, statusactive) .values_list(course_id, flatTrue) ) else: context[my_course_ids] [] return contextget_queryset定义了列表要显示哪些课程以及排序方式get_context_data里把当前学生已选课程ID列表传进模板模板里就能直接判断“已选”还是“未选”不用在模板里写复杂条件。paginate_by设成10课程多的时候页面不至于拉太长。对应路由如下# course/urls.py from django.urls import path from . import views app_name course urlpatterns [ path(, views.CourseListView.as_view(), namecourse_list), path(int:course_id/, views.course_detail, namecourse_detail), path(int:course_id/enroll/, views.enroll_course, nameenroll_course), path(int:course_id/drop/, views.drop_course, namedrop_course), path(my/, views.my_courses, namemy_courses), ]这里我把选课和退课做成独立URL请求方式是POST这样从语义上就区别于普通页面跳转也方便在视图里加登录校验。app_namecourse让模板里能用course:course_list这种带命名空间的引用多个应用存在时不会撞路由名。选课和退课这俩函数视图放在一起看更清楚from django.contrib.auth.decorators import login_required from django.core.exceptions import ValidationError from django.shortcuts import redirect from django.contrib import messages from .service import enroll_user, drop_user login_required def enroll_course(request, course_id): if request.method POST: try: enroll_user(request.user, course_id) messages.success(request, 选课成功) except ValidationError as exc: messages.error(request, exc.messages[0] if isinstance(exc.messages, list) else str(exc)) return redirect(course:course_list) login_required def drop_course(request, course_id): if request.method POST: try: drop_user(request.user, course_id) messages.success(request, 退课成功) except ValidationError as exc: messages.error(request, exc.messages[0] if isinstance(exc.messages, list) else str(exc)) return redirect(course:my_courses)我把真正的业务逻辑enroll_user和drop_user抽到了service模块里视图只负责接收请求、调用业务、转发消息。这样同一个选课逻辑以后要做成API也能直接复用而不是在视图里再写一遍。我那会儿偷懒把所有代码堆在views.py里后面写单元测试时恶心坏了职责分开是血泪经验。提示messages框架需要settings里开启django.contrib.messages中间件并且模板要渲染messages变量否则页面上看不到任何提示。3.2 表单与模板渲染模板继承、csrf_token和选课按钮状态页面部分我习惯先搭一个base.html把导航栏、消息提示、静态文件引用统一放进去。!-- templates/base.html -- {% load static %} !DOCTYPE html html langzh head meta charsetutf-8 title{% block title %}教务选课系统{% endblock %}/title link relstylesheet href{% static css/app.css %} /head body nav a href{% url course:course_list %}课程列表/a a href{% url course:my_courses %}我的课表/a {% if user.is_authenticated %} span{{ user.username }}/span form methodpost action{% url logout %} {% csrf_token %} button typesubmit退出/button /form {% else %} a href{% url login %}登录/a {% endif %} /nav {% if messages %} ul classmessages {% for m in messages %} li{{ m }}/li {% endfor %} /ul {% endif %} main {% block content %}{% endblock %} /main /body /html这个模板里有个容易漏掉的点退出登录。新版Django里logout要求POST请求直接放一个a标签跳转是不安全的所以我用了一个小表单里面带上csrf_token。如果你用的Django版本还不强制也建议按这个写法避免被构造链接注销用户。课程列表页继承base.html核心是这张选课表格!-- templates/course/list.html -- {% extends base.html %} {% block content %} h2课程列表/h2 table classcourse-table thead tr th课程编号/th th课程名称/th th学分/th th上课时间/th th容量/th th操作/th /tr /thead tbody {% for c in courses %} tr td{{ c.course_no }}/td tda href{% url course:course_detail c.pk %}{{ c.name }}/a/td td{{ c.credit }}/td td星期{{ c.weekday }} {{ c.get_slot_display }}/td td{{ c.selected_count }}/{{ c.capacity }}/td td {% if c.pk in my_course_ids %} span已选/span form methodpost action{% url course:drop_course c.pk %} {% csrf_token %} button typesubmit退课/button /form {% else %} form methodpost action{% url course:enroll_course c.pk %} {% csrf_token %} button typesubmit {% if c.selected_count c.capacity %}disabled{% endif %} {% if c.selected_count c.capacity %}已满{% else %}选课{% endif %} /button /form {% endif %} /td /tr {% empty %} trtd colspan6暂无课程/td/tr {% endfor %} /tbody /table {% endblock %}模板里get_slot_display是choices字段的专用方法把存储值1-2显示成“第1-2节”。按钮的disabled只是前端体验当容量满了不给用户一个可点击的入口但真正的容量校验一定在后端因为用户可以绕过页面直接构造POST请求这个原则在选课系统里不能妥协。3.3 登录与权限控制login_required、角色限制与admin后台权限登录页不必自己写Django自带LoginView够用。我把它包一层只是为了自定义模板路径和登录成功后的跳转逻辑from django.contrib.auth.views import LoginView class CustomLoginView(LoginView): template_name registration/login.html redirect_authenticated_user Trueurls里加一条路由就行。至于登录成功后去哪个页面我的做法是配置一个统一的入口按角色分发from django.contrib.auth.decorators import login_required from django.shortcuts import redirect login_required def home_redirect(request): if request.user.role admin: return redirect(admin:index) return redirect(course:course_list)管理员进了Django admin学生去课程列表逻辑干净。admin后台要能用光有角色还不够管理员账号必须勾选is_staff否则登录admin会提示没有权限。这个容易忘我一般会在创建管理员时顺手把is_staff和is_superuser都勾上。如果需要自己写页面对接教师或管理员操作角色限制用一个自定义装饰器就够了from functools import wraps from django.core.exceptions import PermissionDenied from django.shortcuts import redirect def role_required(*roles): def decorator(view_func): wraps(view_func) def wrapper(request, *args, **kwargs): if not request.user.is_authenticated: return redirect(login) if request.user.role not in roles: raise PermissionDenied return view_func(request, *args, **kwargs) return wrapper return decorator用法很简单在视图上加一行role_required(admin, teacher)。这里注意装饰器要放在login_required下面或者像上面这样自己处理未登录的情况否则未登录用户会先撞进权限判断。4. 选课冲突、并发写入与权限边界事务和行锁怎么用在选课场景4.1 时间冲突检测同一时段课程重复选择选课系统核心业务就两件事冲突检测和容量控制。先讲冲突。一个学生一周里同一时段只能上一门课这个规则没法用数据库约束直接表达只能在业务层判断。判断逻辑不复杂把所有当前active状态的选课拉出来逐条比较weekday和slot。def has_time_conflict(student, target_course): selections (CourseSelection.objects .filter(studentstudent, statusactive) .select_related(course)) for sel in selections: c sel.course if c.weekday target_course.weekday and c.slot target_course.slot: return True return False为什么不在数据库层面用一个复合条件一次搞定因为“同一时间”是组合语义SQL里要写成(weekday, slot) IN (...)而且还要排除目标课程自身加上当前课程还没选上、历史退课记录也要过滤写起来反而绕。一个学生一个学期撑死几十条选课记录遍历是绰绰有余的。这里能跑通的前提是第2章把weekday和slot做成了固定枚举如果是自由文本这个函数的比较条件会变成字符串匹配各种格式问题会让你怀疑人生。4.2 容量与并发select_for_update锁住课程行再扣减容量控制是最容易翻车的点。我见过最经典的错误写法是先查课程看selected_count小于capacity就插入选课记录。看起来没毛病但两个学生同时发起请求时两个事务都读到同一个未满的数字然后各自插入成功最后容量超了没人发现。这是典型的“先查后写”并发问题。解决思路是让选课操作串行化。用select_for_update()锁住课程那一行其他选课同一门课的事务必须等当前事务结束才能继续。锁内完成容量判断、重复选课判断、冲突判断、插入记录、更新计数这样超选就从根上被堵住了。from django.db import transaction from django.core.exceptions import ValidationError def enroll_user(student, course_id): with transaction.atomic(): course Course.objects.select_for_update().get(pkcourse_id) if course.selected_count course.capacity: raise ValidationError( f{course.name} 当前已满{course.selected_count}/{course.capacity}) if (CourseSelection.objects .filter(studentstudent, coursecourse, statusactive) .exists()): raise ValidationError(你已经选过这门课程) if has_time_conflict(student, course): raise ValidationError(该时间段已有其他课程选课失败) CourseSelection.objects.create(studentstudent, coursecourse) course.selected_count 1 course.save(update_fields[selected_count])这段代码是整套选课系统的心脏。transaction.atomic保证了从锁行到提交是一个完整的原子操作中途抛异常会整体回滚。select_for_update在MySQL和PostgreSQL下是真正的行锁在SQLite下会退化成库级写锁但逻辑依然成立因为SQLite的写操作本身就是串行的。行锁先把课程行锁住后到的请求会阻塞在这里等第一个事务提交后读到最新的selected_count从而正确判断是否已满。有个细节容易踩很多人以为锁了课程行重复选课就不会发生其实行锁只串行化对course行的修改并不能阻止两个锁内都创建CourseSelection记录。所以在锁内必须先查一遍active选课是否存在再执行插入。查这个动作也是在事务里和锁是同一份数据快照足够可靠。退课的逻辑同样要注意锁的顺序。选课时我先锁course再操作选课记录退课也必须按同样的顺序拿锁否则两个事务一个先锁course、一个先锁selection可能互相等对方释放形成死锁。统一顺序是解决这个问题的土办法也是最实用的办法def drop_user(student, course_id): with transaction.atomic(): course Course.objects.select_for_update().get(pkcourse_id) selection (CourseSelection.objects .filter(studentstudent, course_idcourse_id, statusactive) .first()) if selection is None: raise ValidationError(你还没有选这门课) selection.delete() course.selected_count max(0, course.selected_count - 1) course.save(update_fields[selected_count])退课里我只锁了course行选课记录通过course_id过滤在锁内查询因为course行已经锁住同一时刻不会有针对这门课的新增记录查询结果就是一致的。4.3 管理端与权限边界为什么选课入口必须放在后端校验而不是按钮disabled管理员的课程管理页面不需要从零写。Django admin对这类基础增删改查的支持已经很成熟把Course注册进去就能用from django.contrib import admin from .models import Course, CourseSelection admin.register(Course) class CourseAdmin(admin.ModelAdmin): list_display (course_no, name, teacher, credit, weekday, slot, capacity, selected_count) list_editable (capacity,) search_fields (name, course_no)list_editable把capacity直接暴露在列表页上管理员改容量不用点进详情很顺手。CourseSelection也注册进去用来查某门课的选课名单和某学生的选课历史。权限边界这门课很多人栽在同一个地方以为前端按钮禁用就安全了。实际上选课接口只要知道课程ID任何人都能构造POST请求调通。所以我在视图上只保留login_required真正的角色限制放在service层入口处或者给视图叠加role_required。前端按钮禁用只是让正常用户少点一次报错权限控制必须发生在服务器端这是选课这类写操作不能妥协的原则。5. 选课系统避坑指南5个高频翻车点5.1 AUTH_USER_MODEL配太晚迁移文件全乱现象项目已经跑过一次迁瞨settings里才补上AUTH_USER_MODEL accounts.User然后执行makemigrations发现生成的外键还是指向默认的auth_user或者migrate时报错说某张表已经存在。原因Django在第一次迁移前就要决定用户模型迁移文件里会记录外键指向。等项目跑过一遍再改配置已经生成的迁移不会自动更新新的迁移还会和被占用的auth_user纠缠在一起。解决开发阶段最直接的方案是放弃当前开发库重新来一遍。先备份要保留的数据脚本然后清掉数据库和所有应用的migrations目录rm db.sqlite3 rm -rf accounts/migrations course/migrations python manage.py makemigrations accounts course python manage.py migrate这套操作只对还没上线的开发环境有效。线上环境要是在这步栽了就得靠迁移合并和数据脚本慢慢掰回来所以AUTH_USER_MODEL必须从第一行代码就配好没有后悔药。5.2 并行选课超员你看见容量没满库里已经满了现象一门60人的课选课结束后查记录发现选上了62个人前端显示还是“可选”。原因代码里先查容量、再插入两个请求同时读到同一个未满数字都以为自己是最后一个名额双双插入成功。解决把容量判断、重复判断、插入、计数更新全部放进同一个事务用select_for_update锁住课程行。第4章的enroll_user就是从这条坑里爬出来之后整理的写法。如果不想每次都在视图里写事务就把这段逻辑收敛成一个service函数所有入口都走它。5.3 退课后重新选课报duplicate key现象学生退了一门课过两天想重新选页面直接报IntegrityError: duplicate key value violates unique constraint。原因如果CourseSelection上有unique_together (student, course)的唯一约束退课只是把status改成dropped而不是删记录那么原来的唯一键还占着位置重新插入active记录必然撞约束。解决两个方向选一个。要么退课直接删除记录不保留历史要么去掉数据库唯一约束靠事务加业务查询保证同一个学生不会同时有两条active选课。我更推荐后者因为保留选课历史在很多教务系统里是硬需求。对应模型里delete记录的做法要连计数一起在事务里更新否则selected_count会越减越乱。5.4 页面样式全丢静态文件404刷屏现象页面能打开但浏览器控制台一片404CSS和JS全挂页面看起来像上世纪网页。原因模板里忘了{% load static %}或者settings里的STATICFILES_DIRS路径不对导致模板解析出的静态URL指向错误位置。解决确认settings里配好静态目录STATIC_URL /static/ STATICFILES_DIRS [ BASE_DIR / static, ]模板里每个用到静态文件的地方第一行都要写{% load static %}再用{% static css/app.css %}引用。如果你把DEBUG关掉部署还得先跑python manage.py collectstatic把静态文件收拢到STATIC_ROOT这一步经常被忘掉。5.5 form表单提交403CSRF验证失败现象点选课按钮页面弹出403 Forbidden什么都没发生。原因Django默认开启CSRF防护所有POST请求必须携带一次性token。模板里的form没有加{% csrf_token %}或者用了AJAX提交但没有带CSRF头。解决模板里每个POST表单老老实实在form第一行加{% csrf_token %}。如果是AJAX提交从cookie里取出csrftoken放到请求头的X-CSRFToken字段里。这套机制平时感觉不到一旦你遗漏翻车方式非常直接。排查时先看响应页面里有没有csrftoken再检查模板有没有渲染token基本五分钟定位。6. 本地验收六步走从启动服务到冒烟测试先给一份可以照着点的验收清单。我做完这类系统会走一遍这六步跑完再动手写测试和调样式创建虚拟环境安装依赖执行makemigrations和migrate用createsuperuser建管理员python manage.py runserver 0:8000启动服务。管理员登录admin后台添加几门课程故意把其中两门课设成同一时段容量设小一点。用shell创建两个普通学生账号登录后选同一门课确认第二个人在容量满时收到提示。让学生同时选同一时段的另一门课确认冲突检测给出“该时段已有课程”的提示。开两个浏览器窗口分别登录两个学生同时提交选课查CourseSelection最终记录数是否超过容量。学生退课后再选同一门课确认不再报duplicate key。这六步把第2章到第5章的坑全部覆盖了一遍。跑通了系统最基本的可信度就有了。接下来把关键路径写成自动化测试后端换代码时才有底气from django.test import TestCase from django.urls import reverse from django.contrib.auth import get_user_model from course.models import Course, CourseSelection User get_user_model() class EnrollSmokeTest(TestCase): def setUp(self): self.student User.objects.create_user( usernames01, passwordpass12345, rolestudent) self.course Course.objects.create( course_noCS101, name程序设计, credit2.0, weekday1, slot3-4, capacity1, teacher某老师) self.client.login(usernames01, passwordpass12345) def test_enroll_once(self): resp self.client.post( reverse(course:enroll_course, args[self.course.pk])) self.assertEqual(resp.status_code, 302) self.assertEqual( CourseSelection.objects.filter(statusactive).count(), 1) def test_duplicate_enroll_rejected(self): self.client.post( reverse(course:enroll_course, args[self.course.pk])) resp self.client.post( reverse(course:enroll_course, args[self.course.pk])) self.assertEqual(resp.status_code, 302) self.assertEqual( CourseSelection.objects.filter(statusactive).count(), 1)这两条测试用例覆盖了选课主流程和重复选课拦截。test_enroll_once验证第一次选课能成功并跳转test_duplicate_enroll_rejected验证重复选课不会产生第二条active记录。以后重构视图只要这两个用例不红核心行为就没坏。再往下走就是锦上添花的方向把选课接口改成REST API给课程列表加Redis缓存扛住高并发读或者把课程查询丢给搜索引擎做全文检索。但对一个学生教务选课系统来说事务、行锁、权限、迁移这些地基已经打牢性能优化反而是后面的事。我这几年做完这类系统都会先跑完六步验收再谈优化养成这个习惯以后写业务代码越来越稳——把基础流程跑扎实比堆功能更重要。这套学生选课系统的思路和代码你都拿到手了希望对你有帮助。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →