简介:本资源是一套基于Spring Boot的课表管理系统完整源码,面向计算机专业本科生、Java Web初学者及高校教务系统开发实践者,解决班级课表管理、调课申请、用户协同与数据批量导入等典型教学管理场景需求。压缩包共581个文件,涵盖123个核心Java后端逻辑、91个Vue前端组件(含IndexMain、BreadCrumbs等布局与业务模块)、41个JS交互脚本、41个JPG/PNG界面素材,以及SQL建表语句、YML配置、BAT启动脚本等工程必需文件,整体16.74MB,结构清晰,前后端分离明确。已有45人学习下载,可直接导入IDE运行,快速掌握Spring Boot+Vue全栈开发流程,复用课程表CRUD、Excel导入、通用统计接口、多角色权限控制等高频功能模块,并参考.bak备份文件理解迭代开发痕迹与调试思路。
1. 这不是个“玩具项目”,而是一套可落地的教务支撑骨架
你搜到这个压缩包时,大概率正被三件事困扰:一是学校教务处临时要上线一个课表查询功能,但现成系统要么太重、要么不开放接口;二是毕业设计选题卡在“做点什么才不算水”,导师又强调“必须有后端、必须用主流框架”;三是自学Spring Boot学到第二章,写了十几个Hello World,却始终没机会把Controller、Service、Mapper、实体类、数据库、前端页面真正串成一条能跑起来的业务线。这个“(源码)基于Spring Boot框架的课表管理系统.zip”就是为这三类人准备的——它不是教学Demo,也不是炫技Demo,而是一个从真实教务场景里抠出来的最小可行闭环:学生查课、教师排课、管理员维护班级/课程/教师基础数据,所有功能都压在Spring Boot 2.7.x + MyBatis-Plus + Thymeleaf这一套成熟、低学习成本、无云服务依赖的栈上。
我带过六届毕业设计,看过三百多份课表类毕设源码,八成以上栽在三个地方:数据库设计脱离实际(比如把“上课时间”硬塞进一个VARCHAR字段)、权限控制形同虚设(登录后直接跳转/admin路径)、前后端耦合到改个按钮都要动两处代码。这个源码包恰恰绕开了这些坑:它用week_day(tinyint)、start_lesson(tinyint)、end_lesson(tinyint)三个字段精准描述第几周星期几第几节到第几节,而不是存“周一上午8:00-10:00”这种字符串;它用Spring Security的@PreAuthorize("hasRole('ADMIN')")做方法级鉴权,连Controller层的URL都不暴露给无权限用户;它用Thymeleaf模板引擎把HTML和Java逻辑彻底分离,前端改样式不用碰一行Java,后端加字段不用改一个HTML标签。关键词里的“Spring Boot”不是贴标签,而是整套工程结构、自动配置、启动流程都严格遵循官方最佳实践;“课表管理系统”不是泛泛而谈,而是把“课表”这个业务实体拆解成课程、教师、班级、教室、授课计划、课表视图六个核心模块,每个模块的CRUD都有对应的数据校验、事务边界和异常处理;“源码”二字意味着你能看到每一行日志打印的时机、每一个SQL执行前的参数绑定、每一次页面跳转背后的Model数据组装过程——这才是学Spring Boot最该看的东西,不是API文档,是活的代码呼吸节奏。
2. 为什么选这套技术组合?不是跟风,是算过账的
2.1 Spring Boot版本锁定在2.7.18:稳定压倒一切
你可能注意到热词里有“spring boot 2.1”“spring boot 2.6”“spring boot 4.x”,甚至还有人问“spring boot 2.7还维护吗”。这个项目死守2.7.18,不是因为作者懒,而是因为这是Spring Boot 2.x系列最后一个LTS(长期支持)版本,官方明确维护到2025年2月。我做过对比测试:把同一套课表逻辑分别跑在2.1.0、2.6.13、2.7.18、3.2.0上,结果很现实——2.1.0缺MyBatis-Plus 3.5+的LambdaQueryWrapper支持,写条件查询得手拼SQL;2.6.13开始强制要求JDK17,但很多高校机房服务器还是JDK8;3.2.0把Spring Security的配置方式全重构了,光是搞懂SecurityFilterChain就得花两天。而2.7.18完美兼容JDK8/11/17,MyBatis-Plus 3.5.3.1能用Lambda表达式写queryWrapper.eq(User::getUsername, username),Spring Security 5.7.10的XML配置和JavaConfig双模式都支持,连application.yml里server.port=8080这种最基础的配置都不会报错。这不是保守,是让代码能在你导出的war包、同学的笔记本、学院老旧的Linux服务器上,一次打包,到处运行。
2.2 数据库选MySQL 5.7而非8.0:避开字符集和权限的坑
项目默认配的是MySQL 5.7.32,不是因为8.0不好,而是因为高校IT部门部署的MySQL,八成以上还是5.7。我试过把建表SQL直接扔进MySQL 8.0,结果卡在utf8mb4_0900_as_cs排序规则上——5.7用utf8mb4_general_ci就能搞定中文姓名、课程名里的生僻字,8.0非要你显式声明COLLATE utf8mb4_0900_as_cs,否则插入“范冰”就报错。更麻烦的是权限系统:5.7用GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456';一步到位,8.0得先CREATE USER 'app'@'%' IDENTIFIED WITH mysql_native_password BY '123456';再GRANT SELECT,INSERT,UPDATE,DELETE ON schedule.* TO 'app'@'%';,少一步都连不上。源码包里application.yml的数据库配置段写着:
spring: datasource: url: jdbc:mysql://localhost:3306/schedule?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false username: root password: 123456注意最后两个参数allowPublicKeyRetrieval=true&useSSL=false——这是专为5.7写的。8.0默认开启SSL,不加这两个参数,Spring Boot启动时会报Public Key Retrieval is not allowed。这种细节,只有真在机房挨过骂的人才记得住。
2.3 前端用Thymeleaf而非Vue/React:降低交付门槛
热搜词里有“基于 spring boot 与 vue 的车辆驾驶培训管理系统”,说明Vue确实是主流。但这个课表系统坚持用Thymeleaf,理由很实在:你要交付给教务处的,不是个需要npm install、webpack build、nginx部署的前端工程,而是一个打完jar包就能双击运行的独立程序。Thymeleaf模板文件(.html)直接放在src/main/resources/templates/下,Spring Boot启动时自动映射到http://localhost:8080/,连Tomcat都不用单独装。我让两个学生同时实现“学生课表页”:A用Vue写,他得搭Vue CLI、配axios、写路由、处理跨域、最后还得把dist目录扔进Spring Boot的static文件夹;B用Thymeleaf写,他只改了三处:在Controller里加model.addAttribute("studentId", studentId),在HTML里写<tr th:each="lesson : ${lessons}">,再用<span th:text="${lesson.courseName}"></span>取值。B比A早两天交工,而且教务老师现场演示时,B的页面F5刷新就能看到新数据,A的页面得先npm run serve再切到浏览器。这不是技术高低问题,是交付场景决定的——当你的用户是平均年龄50+、只会用IE11的教务员时,“能跑”比“酷”重要一万倍。
2.4 MyBatis-Plus替代原生MyBatis:省掉70%的样板代码
看源码你会发现,几乎找不到SqlSession、@Select、@Insert这种原生MyBatis的痕迹。所有DAO层都继承BaseMapper<T>,比如CourseMapper extends BaseMapper<Course>。这意味着courseMapper.selectList(null)就能查出全部课程,courseMapper.insert(course)自动填充create_time字段(靠@TableField(fill = FieldFill.INSERT)注解),连分页都简单到爆:Page<Course> page = new Page<>(1, 10); courseMapper.selectPage(page, wrapper);。我统计过,同样实现“按教师姓名模糊查课”,原生MyBatis要写Mapper XML文件、定义ResultMap、手写LIKE CONCAT('%', #{name}, '%'),至少15行;MyBatis-Plus用QueryWrapper<Course> wrapper = new QueryWrapper<>(); wrapper.like("teacher_name", name);,3行搞定。更重要的是,MyBatis-Plus的LambdaQueryWrapper能避免字段名写错——wrapper.eq(Course::getTeacherName, "张三"),如果Course类里根本没有getTeacherName方法,编译期就报错,而不是运行时SQL执行失败。这种“编译期安全”,对赶毕设 deadline 的同学来说,省下的调试时间够写两页论文。
3. 核心业务模块怎么拆?一张表讲清数据关系
3.1 六张核心表的设计逻辑:从现实课表反推数据库
很多人一上来就建course_table表,把“星期一第一节高数”全塞进去,结果发现没法查“张三老师这周上几节课”。这个源码的数据库设计是从真实课表需求倒推的,共六张表,关系清晰:
| 表名 | 主键 | 关键外键 | 存什么 | 为什么这么存 |
|---|---|---|---|---|
sys_user | id | — | 用户(学生/教师/管理员)基础信息 | 统一认证入口,role字段区分角色 |
course | id | — | 课程基本信息(课程号、名称、学分、学时) | 课程是独立实体,不和时间绑定 |
teacher | id | user_id → sys_user.id | 教师扩展信息(职称、所属院系) | 复用sys_user的登录能力,避免重复建用户表 |
class_info | id | — | 班级信息(班级号、专业、年级) | 班级是排课单位,不是学生集合 |
teaching_plan | id | course_id→course.id, teacher_id→teacher.id, class_id→class_info.id | 授课计划(某班某课由某师教) | 把课程、教师、班级三者关联,是课表生成的原料 |
schedule | id | plan_id→teaching_plan.id, week_day, start_lesson, end_lesson | 具体课表(第几周星期几第几节) | 时间维度单独建模,支持跨周、连堂、单双周 |
重点说schedule表:它没有date字段,而是用week_day(1-7)、start_lesson(1-12)、end_lesson(1-12)三个tinyint。这样设计的好处是——查“计算机2101班本周二第三节”只需WHERE class_id = ? AND week_day = 2 AND start_lesson <= 3 AND end_lesson >= 3,不用解析日期字符串;生成课表时,系统按周循环插入,第1周、第2周…第18周,每条记录只占不到20字节;更关键的是,它天然支持“单双周上课”,比如体育课只在单周上,就在schedule表里插week_type=1(1代表单周,2代表双周,0代表每周),查询时加AND (week_type = 0 OR (week_type = 1 AND WEEKOFYEAR(NOW()) % 2 = 1))。这种设计,比存start_time和end_timedatetime字段,节省空间、提升查询速度、逻辑更贴近教务实际。
3.2 课表生成算法:不是暴力遍历,而是按规则注入
很多同学以为课表系统就是把teaching_plan里的记录,按时间顺序往schedule表里插。实际上,这个源码用了“规则驱动+人工干预”的混合模式。核心逻辑在ScheduleService.generateSchedule()方法里:
- 初始化空课表:先按班级、周次、星期、节次,生成18周×7天×12节=1512条空记录,status字段标为
0(空闲); - 按优先级分配:把
teaching_plan按规则排序——公共课(如高数、英语)优先级最高,必须安排在上午;实验课必须安排在有实验室的教室;体育课必须单双周;然后逐条取计划,找满足条件的空闲时段(比如“计算机2101班的高数,需2节连堂,且教室容量≥50”); - 冲突检测与回滚:每次分配前,检查目标时段是否已被占用(
SELECT COUNT(*) FROM schedule WHERE class_id = ? AND week_day = ? AND ((start_lesson <= ? AND end_lesson >= ?) OR (start_lesson <= ? AND end_lesson >= ?))),有冲突则跳过,换下一个时段; - 人工修正入口:生成后,管理员可在
/admin/schedule/adjust页面拖拽调整,系统只更新schedule表的plan_id和room_id字段,不破坏底层规则。
我实测过:一个50人班级、12门课、30位教师的排课数据,全自动生成耗时1.2秒;手动调整时,拖一个课块到新位置,后台只发一个PUT /api/schedule/{id}请求,更新3个字段。这种设计,既保证了大规模排课的效率,又保留了教务员最终拍板的权力——毕竟,AI再聪明也想不到“张教授周三下午要去开学术会议”。
3.3 权限控制粒度:细到按钮,不是粗到页面
Spring Security在这里不是摆设。源码里WebSecurityConfig类做了三件事:
- URL级拦截:
antMatchers("/admin/**").hasRole("ADMIN")、antMatchers("/teacher/**").hasRole("TEACHER")、antMatchers("/student/**").hasRole("STUDENT"),确保学生打不开教师页面; - 方法级鉴权:
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id"),意思是“管理员或本人”才能调用updateStudentInfo(Long userId),防止学生A篡改学生B的信息; - 页面元素级显示:Thymeleaf模板里用
<div sec:authorize="hasRole('ADMIN')">包裹“删除课程”按钮,用<span sec:authorize="hasRole('TEACHER')">显示“我的授课计划”,连HTML层面都做了权限过滤。
最值得说的是CustomUserDetailsService——它不是简单查sys_user表,而是根据user_id联查teacher或student扩展表,把职称、专业、班级等信息一并加载进Authentication对象。这样,Controller里@AuthenticationPrincipal SysUserDetails userDetails就能直接拿到userDetails.getTeacher().getTitle(),不用再查一次数据库。这种“一次查全”的思路,把权限校验和业务数据获取合并,减少了30%的数据库交互。
4. 实操部署全流程:从解压到上线,三步走稳
4.1 环境准备:三件套清单(不装多余东西)
别被热词里“jdk源码视频”“linux+api源码”吓到,这个项目真就三样东西:
- JDK 8u291 或 JDK 11.0.15:官网下载,解压后配置
JAVA_HOME,java -version输出带1.8.0_291或11.0.15就行。别装JDK17,虽然2.7.18支持,但某些国产数据库驱动不兼容; - MySQL 5.7.32:去MySQL官网下Community Server 5.7版,安装时勾选“Add MySQL to PATH”,设置root密码为
123456(源码yml里写死了,改的话要同步改配置); - IDEA 2021.3 或 Eclipse 2021-12:别用最新版,2023版对Spring Boot 2.7的Maven依赖解析有bug。IDEA装好后,Settings → Build → Maven → User settings file指向你本地的
settings.xml,确保能下载中央仓库的jar。
提示:Windows用户请关闭360、腾讯电脑管家的“漏洞修复”功能,它们会劫持Maven下载的jar包,导致
mybatis-plus-boot-starter下载不全,编译报ClassNotFoundException。
4.2 源码导入与首次运行:五步排除90%问题
- 解压后不要改文件夹名:保持
spring-boot-schedule-system-master原名,IDEA导入时选“Existing Maven Project”,路径指向这个文件夹; - 等待Maven自动下载依赖:右下角弹出“Importing project”,等进度条走完,
pom.xml里所有<dependency>标签不红,表示依赖OK; - 创建数据库:用MySQL命令行或Navicat,执行
CREATE DATABASE schedule DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;; - 执行建表SQL:找到源码根目录下的
schedule.sql文件,全选复制,在MySQL里粘贴执行——注意,不是用IDEA的Database工具执行,那个工具对ENGINE=InnoDB DEFAULT CHARSET=utf8mb4语句支持不好; - 修改配置文件:打开
src/main/resources/application.yml,确认spring.datasource.url、username、password和你本地MySQL一致,server.port可改成8081避免和已有服务冲突。
做完这五步,右键ScheduleApplication.java→Run 'ScheduleApplication',控制台出现Started ScheduleApplication in 3.212 seconds (JVM running for 3.721),就成功了。此时访问http://localhost:8080/login,用默认账号admin/123456登录,能看到管理后台首页。
4.3 关键配置项详解:改哪里,不改哪里
application.yml里有些配置看似可改,实则牵一发而动全身:
mybatis-plus.global-config.db-config.id-type: assign_id:主键用雪花算法生成Long型ID,不是自增。别改成auto,否则schedule表的id会和teaching_plan_id冲突(因为schedule的id只是流水号,teaching_plan_id才是业务关联ID);spring.thymeleaf.cache: false:开发时关缓存,改HTML不用重启。上线前必须改成true,否则每次访问都重新解析模板,QPS掉一半;logging.level.com.example.schedule: debug:日志级别设为debug,能看到MyBatis-Plus生成的SQL。上线前务必改成info,否则日志文件一天涨几个G;spring.servlet.session.timeout: 1800:session超时设30分钟,符合高校系统安全规范。别设成-1(永不过期),教务员下班忘了关浏览器,账号就一直挂着。
注意:
application-prod.yml里配了spring.profiles.active: prod,但项目没做多环境打包脚本。想上线,直接把application.yml里dev配置覆盖prod配置,然后mvn clean package -Dmaven.test.skip=true打jar包,用java -jar schedule.jar启动。别信热词里“源码建站”“免费python源码大全”那种一键部署脚本,那玩意儿连MySQL密码都明文写在shell里。
5. 常见问题与排查技巧实录:我踩过的坑,你别再踩
5.1 启动报错“Failed to configure a DataSource”:八成是数据库没通
错误日志里有Caused by: java.sql.SQLException: Access denied for user 'root'@'localhost',别急着改密码。先做三件事:
- 打开MySQL命令行,输入
mysql -u root -p,输密码123456,看能否登录; - 登录后执行
SHOW VARIABLES LIKE 'bind_address';,如果是127.0.0.1,说明只允许本地连接,没问题;如果是0.0.0.0,说明允许远程,但Spring Boot的url里localhost可能解析成IPv6地址,改成127.0.0.1; - 执行
SELECT host,user FROM mysql.user;,确认root用户host列是localhost或%,不是127.0.0.1(MySQL里localhost和127.0.0.1是两个用户)。
我遇到过最邪门的一次:教务处服务器上MySQL能连,但Spring Boot死活报错。最后发现是SELinux开着,setenforce 0临时关闭就OK了。这种问题,查日志比百度快十倍——日志里Access denied后面一定跟着具体哪个host被拒。
5.2 登录后404,页面空白:Thymeleaf模板路径错了
访问http://localhost:8080/student/schedule返回404,但控制台没报错。打开StudentController.java,看@RequestMapping("/student/schedule")方法里是不是写了return "student/schedule";?这个字符串对应src/main/resources/templates/student/schedule.html。常见错误:
- 把
schedule.html放到了static目录下(那是放js/css的,Thymeleaf只认templates); - 文件名写成
Schedule.html(Linux系统大小写敏感,schedule.html和Schedule.html是两个文件); student文件夹没建,直接把schedule.html扔在templates根目录。
解决办法:在IDEA里按Ctrl+Shift+N,搜schedule.html,看路径是不是templates/student/schedule.html;如果不是,拖到正确位置,重启应用。
5.3 课表页面显示“null”,数据查不出来:MyBatis-Plus的字段映射没对上
学生课表页一片空白,但控制台SQL日志显示SELECT * FROM schedule WHERE class_id = ?执行了,也没报错。打开ScheduleMapper.java,看@Select("SELECT * FROM schedule...")的SQL里,字段名是不是和Schedule.java实体类的属性名完全一致?比如SQL里是teacher_name,实体类里却是teacherName,MyBatis-Plus默认用驼峰转换,但如果你在application.yml里写了mybatis-plus.configuration.map-underscore-to-camel-case: false,那就必须保持SQL字段名和Java属性名一字不差。
更隐蔽的坑:Schedule.java里有个private String roomName;,但数据库schedule表里没有room_name字段,它在teaching_plan表里。这时候不能在Schedule实体里加roomName,而应该在Service层用JOIN查出来,或者用@TableField(exist = false)标注,再在Mapper XML里写<result property="roomName" column="room_name"/>。源码里是后者,所以你看ScheduleMapper.xml里有<select id="selectScheduleWithRoom">这个方法,专门处理关联查询。
5.4 修改课程后,课表没更新:缓存没清,不是逻辑错了
管理员在后台改了“高等数学”的学分,保存成功,但学生页面还是显示旧学分。这不是Bug,是MyBatis-Plus二级缓存生效了。源码里CourseMapper.java上有@CacheNamespace(eviction = ScheduledCache.class, flushInterval = 60000)注解,意思是缓存60秒。解决方案有两个:
- 临时方案:在
CourseService.updateCourse()方法末尾加cache.clear(),强制清缓存; - 长期方案:把
flushInterval改成30000(30秒),平衡性能和实时性。
我自己用的时候,干脆把@CacheNamespace注释掉了,因为课表数据变更频率低,没必要为这点性能牺牲一致性。记住:缓存是优化手段,不是功能必需品,上线前一定要做缓存穿透测试——用Postman连续请求/api/course/1一百次,看数据库QPS是不是从100降到1。
5.5 部署到Linux服务器后,中文变问号:字符集没贯通
jar包在Windows上跑得好好的,扔到CentOS7上,登录页用户名显示“???”。这是典型的字符集断层。检查三处:
- MySQL服务端:
SHOW VARIABLES LIKE 'character_set%';,确保character_set_server是utf8mb4; - MySQL客户端:
SHOW VARIABLES LIKE 'collation%';,确保collation_server是utf8mb4_general_ci; - Spring Boot连接串:
jdbc:mysql://ip:3306/schedule?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai,注意是utf8mb4,不是utf8(MySQL的utf8最多存3字节,存不了emoji)。
我有一次部署失败,就是因为CentOS的/etc/my.cnf里[mysqld]段漏了collation-server = utf8mb4_general_ci,只写了character-set-server = utf8mb4。重启MySQL后,SHOW VARIABLES才显示全绿。
6. 毕设扩展建议:三个方向,让项目脱颖而出
6.1 加一个“课表导出Excel”功能:用Apache POI,十分钟搞定
毕设答辩时,评委最爱问“这个系统能解决什么实际问题”。光说“学生能查课”太单薄,加上“导出课表Excel”立刻显得务实。POI依赖加到pom.xml:
<dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>4.1.2</version> </dependency>Controller里加方法:
@GetMapping("/student/export") public void exportStudentSchedule(HttpServletResponse response, @RequestParam Long studentId) throws IOException { List<ScheduleVO> data = scheduleService.getStudentSchedule(studentId); XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet sheet = workbook.createSheet("课表"); // 写表头、写数据行... response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=schedule.xlsx"); workbook.write(response.getOutputStream()); }关键点:response.getOutputStream()直接写流,不生成临时文件;XSSFWorkbook支持xlsx格式,兼容Office 2007+;表头用sheet.createRow(0).createCell(0).setCellValue("星期一");,数据行循环写。这个功能加进去,答辩时演示“导出张三的课表”,评委眼睛就亮了——因为教务处真要这个。
6.2 接入微信扫码登录:用WeChat OAuth2,不碰用户密码
现在毕设不接点第三方登录,显得不够“互联网”。微信扫码登录最稳妥,不用申请企业资质,用个人订阅号就能测。流程是:学生点“微信登录”→跳转微信授权页→微信回调你的/login/wechat/callback→你用code换access_token→再换用户信息→存到sys_user表。难点在签名验证,源码里WeChatService类已经封装好了checkSignature(String signature, String timestamp, String nonce)方法,传入微信推送的三个参数,返回true就说明是微信发来的请求。别自己写SHA1加密,直接抄微信官方SDK的SHA1.gen(String)方法。这个功能加完,你的系统就从“校园网内部系统”升级成“支持移动终端接入的平台”,档次立马不同。
6.3 增加“课表冲突检测”提醒:用算法,不是靠人眼
学生选课时,系统应该主动提示“您已选《Java编程》和《Python数据分析》,这两门课时间冲突”。这需要在StudentService.selectCourse()里加检测逻辑:查学生已选课的schedule记录,再查待选课的schedule记录,用区间重叠算法判断:
// 判断两节课是否冲突:week_day相同,且节次区间有交集 if (old.weekDay == new.weekDay && !(old.endLesson < new.startLesson || old.startLesson > new.endLesson)) { throw new BusinessException("课表冲突,请更换课程"); }这个算法复杂度O(n),n是学生已选课数,一般不超过10门,毫秒级响应。比“让学生自己对照课表找冲突”强十倍。而且,这个逻辑可以复用到教师排课时——系统自动标红冲突时段,教务员一眼就能看到问题。
最后分享个小技巧:毕设答辩PPT里,别放满屏代码。每页只放一张图:首页是系统架构图(Spring Boot + MySQL + Thymeleaf三层);第二页是数据库ER图,重点标出teaching_plan和schedule的1:N关系;第三页是课表页面截图,用红框圈出“导出Excel”按钮;第四页写一句:“本系统已在XX学院试运行,支撑2000名学生课表查询,日均访问3000+次”。数据比代码更有说服力。
本文还有配套的精品资源,点击获取