简介:基于SpringBoot+Vue开发的学生考勤管理系统完整毕业设计项目,面向计算机专业正在准备毕设的学生及需要项目实战经验的Java学习者,同样适用于课程设计、期末大作业等场景。系统采用B/S架构,以Java为核心技术、MySQL为后台数据库,围绕管理员、教师、学生三类角色构建了学生管理、教师管理、班级信息、课程信息、签到信息、考勤信息、请假信息及考勤统计等完整功能模块,业务完整性与可扩展性兼备。
压缩包共包含428个文件,压缩后体积约69.47MB,涵盖java后端源码、vue前端页面、svg图标、sql数据库脚本、docx开发说明文档、答辩PPT、演示视频及代码注释等,目录结构清晰便于按需检索。所有功能模块均经过严格调试,可直接运行并作为毕业设计提交。目前已有677人学习下载,适合需要快速落地考勤管理系统,或希望参考SpringBoot与Vue全栈项目做二次开发的读者。
1. 学生考勤管理系统:手工点名太累,不如把它做成一个项目
一个班四五十人,老师喊一圈名字要两三分钟,课代表课后还要对着记录表手工统计迟到、缺勤、请假,月底汇总时经常要熬夜对账。学生考勤管理系统要解决的就是这件事:学生通过手机或电脑打卡,系统根据课程时间自动判定状态,老师随时能看到出勤率报表,从“人工点名、月底对账”变成“自动记录、实时统计”。这个项目用 SpringBoot 做后端接口、Vue 做前端页面,配合 MySQL 数据库,是一套典型的前后端分离全栈项目,交付形式是源码加数据库脚本加说明文档。适合毕业设计选题、课程设计练手,也适合刚入门全栈开发的人拿来当第一个完整项目——它的业务规则清楚,表结构不复杂,但又足够撑起一个完整的开发流程。
2. 技术选型与项目结构:SpringBoot 管接口、Vue 管页面,各司其职
2.1 为什么选 SpringBoot + Vue 而不是别的组合
考勤管理系统本质是一个 CRUD 业务系统:录入学生、排课、打卡、查统计,核心操作就是增删改查加一些业务规则判断。对这种系统,SpringBoot 几乎是当前最省事的后端选择。它内置了 Tomcat,一个mvn spring-boot:run就能起服务;MyBatis 或 MyBatis-Plus 把数据库操作简化到接口声明;Spring Security 或 JWT 拦截器用来做登录鉴权,整套生态非常成熟。同样是做这个题目,用 SSM 要自己配一堆 XML,用 PHP 写起来快但后续维护和答辩展示时架构感弱,用 Python Django 也不是不行,但对于国内大多数课程设计和毕业设计而言,SpringBoot 是主流,参考资料最多,出问题最容易搜到答案。
前端选 Vue,核心原因是它适合中小型管理系统的开发节奏。基于 SpringBoot Vue 的项目现在已经成了毕业设计和中小型管理系统的主流标配。Vue 的响应式数据处理让考勤记录的实时刷新变得很简单,组件化开发把登录页、打卡页、报表页拆开,每个人负责一块也不容易打架。相比 React 需要自己抉择状态管理方案,Vue 的上手路径更平滑,而且 Element UI 或 Element Plus 这类组件库几乎把表单、表格、弹出框都封装好了,做管理后台效率很高。
还有一点容易被忽视:前后端分离带来的部署灵活性。后端是纯接口服务,前端打包成静态文件用 Nginx 托管,学生端和老师端可以共用一套接口,以后要加一个移动端小程序,后端接口基本不用动。虽然不是每个考勤系统都需要这种扩展性,但技术选型时留出这条路,后期改造成本会低很多。
2.2 拿到源码包后先看这三块目录
一个标准的基于 SpringBoot 加 Vue 的考勤管理系统源码包,通常不会把前后端混在一个工程里,而是分成几块独立目录。拿到手先别急着跑,把结构看清楚再动手。常见的目录划分是这样:
student-attendance/ ├── backend/ # SpringBoot 后端工程(Maven 结构) │ ├── src/main/java │ ├── src/main/resources │ └── pom.xml ├── frontend/ # Vue 前端工程 │ ├── src/ │ ├── package.json │ └── vite.config.js 或 vue.config.js ├── sql/ # 数据库初始化脚本 │ └── attendance.sql └── doc/ # 设计文档、答辩 PPT、使用说明后端src/main/java下面一般按controller / service / mapper / entity / config分包,对应接口层、业务层、持久层、实体类和配置类。前端src下常见的是views(页面)、router(路由)、api(请求封装)、store(状态管理)。sql目录里的脚本决定了你能不能跑起来,先用它建库建表,再启动后端,再启动前端,这个顺序一般不会错。
建议你拿到源码后第一件事不是看代码,而是先把 SQL 脚本导入数据库,确保库表都在,再去启动后端。很多人一上来就npm install然后报错,折腾半天发现数据库根本没初始化,后端一启动就报表不存在的错误,白浪费一下午。
2.3 环境版本搭配:照着配不会翻车
版本问题是这类项目第一个坑。我见过太多人卡在环境上,这里给出一套稳妥的组合,照着配基本不会出问题。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 8 或 11 | SpringBoot 2.7.x 用这两个版本最稳 |
| SpringBoot | 2.7.x | 不要一上来就用 3.x,javax 和 jakarta 命名空间不兼容 |
| MySQL | 8.0 | 5.7 也能跑,但 8.0 更常见 |
| Node.js | 16.x 或 18 LTS | 太新的 20+ 有时装老依赖会出兼容问题 |
| Vue | 2.6 + Element UI 或 Vue 3 + Element Plus | 取决于源码用的哪个,别混 |
| Maven | 3.6+ | 后端依赖管理 |
这里重点提醒 SpringBoot 版本。很多最新教程已经用 SpringBoot 3.x,但源码包的 pom.xml 如果基于 2.7 写的,直接升到 3.x 会报javax.servlet找不到之类的错误,因为 SpringBoot 3 把javax换成了jakarta。拿到源码先看pom.xml里的<parent>版本号,不要自作主张升级。前端也一样,Vue 2 的项目用 Element UI,Vue 3 的项目用 Element Plus,两者 API 不通用,混着用页面直接白屏。
数据库方面,MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver,连接 URL 必须带时区参数,否则启动报错。这个细节后面避坑章节会展开。
3. 数据库设计:六张表把考勤状态机立住
3.1 核心表结构与字段释义
考勤系统的业务可以拆成四个实体:学生、教师、课程、考勤记录,再加上请假申请,一共六张表足够覆盖整个业务流程。很多源码包的表设计大同小异,核心就是这几张。表结构设计的好坏直接影响后面接口的复杂度,设计得好,统计接口只需要一条 SQL;设计得差,业务层要写一堆循环去补数据。
学生表是基础数据,字段一般包括学号、姓名、班级、手机号。注意学号要设唯一索引,因为打卡和统计都需要按学号定位学生。教师表简单一点,姓名、工号、账号密码。课程表是考勤的判定依据,里面必须有上课时间、下课时间、上课地点(经纬度或教室编号),这三个字段是做自动判定和位置校验的基础。
考勤记录表是整个系统的核心,建议这样设计:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint 主键自增 | 记录主键 |
| student_id | bigint | 学生 ID,关联学生表 |
| course_id | bigint | 课程 ID,关联课程表 |
| attendance_date | date | 上课日期,精确到天 |
| check_in_time | datetime | 打卡时间,用于判定迟到早退 |
| status | tinyint | 出勤状态:1 正常,2 迟到,3 早退,4 缺勤,5 请假 |
| location | varchar | 打卡地点,经纬度字符串,可选字段 |
| remark | varchar | 备注,比如请假原因 |
这张表上要建联合唯一索引(student_id, course_id, attendance_date),这是防止同一个人同一天同一门课重复打卡的关键。很多初学的人建表时忽略这一步,后面接口层写得再复杂,也挡不住数据库层面的重复数据。
请假申请表字段包括学生 ID、课程 ID、请假日期、请假原因、审核状态。审核通过后,数据要么把考勤记录的状态改成请假,要么在统计时单独排除,两种方案都行,但要在系统里保持一致。
3.2 出勤状态的判定规则与流转逻辑
手工考勤时代,老师对“迟到”的判断是看学生进教室的时间点;系统做自动判定就必须把规则固化成可计算的逻辑。常见做法是:以上课时间为基准,提前 10 分钟内打卡算正常,上课后 15 分钟内打卡算迟到,超过 15 分钟算缺勤。早退的判定逻辑相对复杂,因为学生离开教室的时间系统很难自动捕获,除非做了签退功能,否则早退状态一般靠老师手动修改。
状态流转的核心代码逻辑是这样的:
// 判定出勤状态的核心方法 // 以服务器时间为准,上课时间从课程表读取 public Integer judgeStatus(AttendanceRecord record, Course course) { LocalTime now = LocalTime.now(); // 服务器当前时间 LocalTime start = course.getStartTime(); // 上课时间 LocalTime end = course.getEndTime(); // 下课时间 if (Duration.between(now, start).toMinutes() <= 15) { return 2; // 上课后15分钟内打卡,迟到 } if (now.isBefore(start.plusMinutes(10))) { return 1; // 课前10分钟内打卡,正常 } return 4; // 超过15分钟,缺勤 }这里有个容易踩的逻辑坑:判断顺序很重要。必须先把“迟到”的区间判断完,再判断“正常”,最后兜底算缺勤。如果把正常判断写在前面,上课后 5 分钟打卡也会被算成正常,因为now.isBefore(start)对上课后 5 分钟依然成立,得把条件写成“上课前 10 分钟到上课时间”这个区间内才算正常。
请假的状态流转稍微复杂一点。学生在请假表提交申请,教师审核通过后,系统要把对应课程对应日期的考勤记录状态更新为请假。这个操作放在审核接口里做,不要等统计时再判断,否则统计逻辑里会混入“请假申请已经通过但考勤记录还是空”的脏数据。
3.3 初始化数据的写法与常见错误
SQL 脚本的前半部分是建表语句,后半部分是初始化数据。初始化数据至少要包含一个管理员账号和几个测试学生账号,否则系统启动后连登录都进不去。密码字段千万不要明文存储,要用 BCrypt 加密。SpringBoot 的spring-security-crypto依赖里自带 BCrypt 工具,初始化 SQL 里可以先用预生成的密文插入,或者干脆在启动类里写一个CommandLineRunner去做初始账号的创建。
初始化数据最常见的错误是外键依赖顺序。学生表、课程表之间的关联属于基础数据,考勤记录表引用了学生和课程,所以插入顺序必须是:先学生,再课程,最后考勤记录。很多 SQL 脚本一执行就报foreign key constraint fails,就是建表时把外键约束提前加了,而插入数据时引用表还是空的。解决方法是建表时先不加外键约束,等数据初始化完成后再用ALTER TABLE ADD CONSTRAINT补上,或者干脆不在数据库层加外键,外键关系全在业务代码里维护。
另一个常见错误是字符集没指定,导致中文全部变成乱码。建库脚本要显式声明:
CREATE DATABASE attendance DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;MySQL 的 utf8 是假的 utf8,最多存 3 个字节,遇到表情符号就报错。utf8mb4 才是真正的完整 UTF-8,考勤记录里万一有学生名字生僻字多,用 utf8mb4 最保险。我一般习惯把character_set_server默认值也设成 utf8mb4,宁可多写一行配置也不去赌默认值。
4. 后端核心实现:登录、打卡、统计三个接口就够了
4.1 JWT 登录接口与全局拦截器
考勤系统不需要像电商平台那样复杂的权限体系,角色无非两种:学生和教师(管理员可以归到教师角色里)。JWT 是这个场景最合适的方案——无状态、不存 Session、前端拿到 token 存 localStorage,请求时放进 Header 就行。
先看 JWT 工具类的核心代码:
// JWT 生成与解析工具类 public class JwtUtil { // 密钥要足够长,生产环境从配置读取,不要硬编码 private static final String SECRET = "your-256-bit-secret-key-change-me"; public static String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("role", role) .setExpiration(new Date(System.currentTimeMillis() + 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET) .parseClaimsJws(token).getBody(); } }登录接口本身没什么玄学,就是把前端传来的账号密码拿到数据库比对。密码用 BCrypt 的matches方法验证,比对成功返回 token 和用户基本信息。token 的有效期建议设 24 小时,考勤系统一天最多登录几次,没必要设太长,失效后重新登录的成本也低。
全局拦截器是鉴权的关键一环。在 SpringBoot 里实现HandlerInterceptor,在preHandle方法里取 Header 的 token,解析失败就返回 401:
// 拦截器:除了登录接口,其他接口都要带 token @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { response.setStatus(401); return false; } try { Claims claims = JwtUtil.parseToken(token.substring(7)); request.setAttribute("userId", claims.getSubject()); return true; } catch (Exception e) { response.setStatus(401); return false; } }注册拦截器时注意放行路径:
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns("/api/**") .excludePathPatterns("/api/login", "/api/register"); }这里的坑是路径匹配规则。/**是匹配多级路径,/*只匹配一级,写错了要么拦截不住接口,要么把登录接口也拦了。我一般习惯接口统一用/api/前缀,让拦截器只管这个前缀,前端静态资源完全不受影响。
4.2 打卡接口:服务器时间、位置范围、防重复三件事
打卡是整个系统里业务规则最密集的接口。前端提交的请求长这样:POST /api/attendance/check-in,参数是studentId、courseId、longitude、latitude。注意参数里故意没有时间——这是故意的,因为前端设备时间不可信,改个系统时间就能伪造打卡,必须以后端服务器时间为准。
打卡接口的核心逻辑分三步:
// 打卡接口核心逻辑 public Result checkIn(CheckInRequest request) { // 第一步:用服务器时间 LocalDateTime now = LocalDateTime.now(); // 第二步:查课程,取上课时间和地点 Course course = courseMapper.selectById(request.getCourseId()); if (course == null) { return Result.error("课程不存在"); } // 第三步:位置校验(可选) double distance = calculateDistance( request.getLatitude(), request.getLongitude(), course.getLatitude(), course.getLongitude()); if (distance > 500) { return Result.error("不在课程地点范围内"); } // 判定状态并写入记录 AttendanceRecord record = new AttendanceRecord(); record.setStudentId(request.getStudentId()); record.setCourseId(request.getCourseId()); record.setAttendanceDate(now.toLocalDate()); record.setCheckInTime(now); record.setStatus(judgeStatus(now, course)); try { attendanceMapper.insert(record); } catch (DuplicateKeyException e) { return Result.error("今天已经打过卡了"); } return Result.success("打卡成功"); }位置校验用经纬度距离计算的常见做法是Haversine公式,把球面距离近似成平面距离。对于教室这种小范围场景,国测局坐标系和 WGS84 的偏差影响不大,直接用前端navigator.geolocation拿到的经纬度和课程表里预存的教室经纬度做距离判断就行,允许范围一般设 200 到 500 米,太严了学生在走廊打卡会被误判。
防重复打卡是这里最容易漏的点。就算数据库有唯一索引,业务层也得先查一次记录是否存在,因为DuplicateKeyException的异常信息不够友好,直接暴露给用户显示“Duplicate entry”会很奇怪。先查再插的方式会多一次查询,但换来的是明确的业务提示“今天已经打过卡了”,用户体验好很多。
4.3 统计接口:SQL 聚合与返回结构
统计接口是老师端最关心的功能。出勤率的计算方式要事先约定清楚:出勤率 =(正常人数 + 迟到人数)/ 应到人数,缺勤和请假都不算在分母里,还是请假不算出勤但也不算缺勤?这里推荐的口径是:请假不计入应到人数,因为请假是经过审批的合理未到,把请假算进缺勤会打击学生提交请假申请的积极性。
按月统计出勤率的 SQL 写法:
-- 按月份统计每个班级的出勤率 SELECT DATE_FORMAT(a.attendance_date, '%Y-%m') AS month, COUNT(DISTINCT a.student_id) AS total_students, ROUND( SUM(CASE WHEN a.status IN (1, 2) THEN 1 ELSE 0 END) / COUNT(*), 2 ) AS attendance_rate FROM attendance_record a JOIN student s ON a.student_id = s.id WHERE s.class_id = #{classId} GROUP BY DATE_FORMAT(a.attendance_date, '%Y-%m') ORDER BY month;这段 SQL 里有几个容易被忽略的点。COUNT(DISTINCT student_id)是为了防止同一学生多条记录把总数撑大,虽然唯一索引已经挡了重复打卡,但统计时做一层保护不亏。出勤率算出来是小数,用ROUND保留两位在表格里展示更清爽。GROUP BY 的字段和 SELECT 里的聚合字段必须一致,MySQL 在ONLY_FULL_GROUP_BY模式下写法不严谨会直接报错。
前端拿到的统计接口返回结构建议设计成两层:上层是按时间分组的总览,下层是明细列表。总览给 ECharts 画折线图用,明细给表格展示用。不要把明细数据塞到图表接口里,前端处理起来会多一堆无意义的过滤逻辑。
接口返回统一封装成Result对象,包含code、message、data三个字段。这个习惯一定要养成,前后端联调时少了这个封装会非常痛苦——前端每个请求都要手动判断返回状态,出错时连错误信息都拿不到。
5. 做考勤系统最容易翻车的 5 个坑:现象、原因、解决
5.1 前后端时间对不上,出勤结果全错
现象:学生早上 8 点 02 分上课,8 点 05 分打卡,系统判定为正常,但课程表里写的上课时间是 8 点整。
原因:前端打卡时把new Date()的时间直接传给了后端,而后端系统时间比真实时间慢了 3 分钟,或者前端设备时间和服务器时间不一致。考勤判定依赖的是打卡时间,时间基准错了,整个判定全错。
解决:打卡接口不接收前端传的时间参数,只用后端服务器的LocalDateTime.now()。如果担心服务器时间不准,可以在部署时同步 NTP 时间服务。前端页面可以显示一个“当前时间”供学生参考,但接口层面信后端时间。这是考勤系统设计的底线原则,不要在前端和后端之间做时间校准的复杂逻辑,直接后端一言堂最简单。
5.2 跨域请求被浏览器拦截
现象:前端跑在localhost:5173(Vite 默认端口),后端跑在localhost:8080,前端请求后端接口,浏览器控制台报Access-Control-Allow-Origin错误。
原因:前后端分离后端口不同,浏览器同源策略默认阻止跨端口请求。开发环境最常见,生产环境如果前端和后端分别用不同域名,也会遇到。
解决:开发环境用 Vite 代理,在vite.config.js里配置:
// vite.config.js 开发环境代理配置 server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }后端也要配置 CORS,二者缺一不可。开发期内用后端全放行的简单方式就够了,生产环境再用 Nginx 做同域反代,把/api转发到后端端口,这样浏览器看起来是同源请求,跨域问题从根上消失。
5.3 Jackson 序列化 LocalDateTime 抛异常
现象:后端接口返回的数据里带LocalDateTime字段,前端拿到的值是一串数字(时间戳),或者后端直接报InvalidDefinitionException: Java 8 date/time type not supported。
原因:SpringBoot 默认的 Jackson 不认 Java 8 的时间类型,需要额外注册JavaTimeModule。这个问题在 SpringBoot 2.0 之后有所缓解,但很多源码包从老版本升级上来时就漏了这个配置。
解决:在application.yml里加一行配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai再在pom.xml里确认有jackson-datatype-jsr310依赖,SpringBoot 的 web 起步依赖里默认带上,但如果是精简过的工程就要手动补。前端拿到的LocalDateTime字段会按yyyy-MM-dd HH:mm:ss格式返回,展示时不用再做格式化处理。
5.4 MySQL 8 连接报 Public Key Retrieval 错误
现象:后端启动时控制台报Public Key Retrieval is not allowed for user,或者连接超时。
原因:MySQL 8 默认的caching_sha2_password认证方式要求客户端在首次连接时获取公钥,而 JDBC 驱动默认禁止这个行为。同时,MySQL 8 的连接 URL 必须带时区参数,否则驱动会报The server time zone value错误。
解决:修改application.yml里的数据源 URL:
spring: datasource: url: jdbc:mysql://localhost:3306/attendance?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=utf8mb4 username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver三个关键参数:serverTimezone必须填,否则时区报错;allowPublicKeyRetrieval=true专门解决公钥获取问题;useSSL=false是本地开发省事,生产环境如果要求安全连接再改回 true。characterEncoding=utf8mb4防止中文乱码。这些参数组合是最常见的一套配置,直接抄过去不会有坑。
5.5 前端重复提交打卡请求
现象:学生手速快,对着打卡按钮连点,接口被并发请求打了两三次,数据库里出现同一个人同一天同一门课的多条考勤记录。
原因:前端虽然做了按钮disabled状态,但异步请求发出后按钮的状态重置时机不对,或者学生直接绕过了前端用手工发 HTTP 请求。数据库层面唯一索引没建或者建了但索引字段不全,挡不住重复数据。
解决:三层防线缺一不可。第一层前端在请求发出后立即把按钮置灰,收到响应后再恢复;第二层数据库建联合唯一索引(student_id, course_id, attendance_date);第三层后端接口在业务代码里先查记录是否存在。三层都做,才能既挡住普通用户的手滑,也挡住测试工具发的并发请求。数据库唯一索引是最后一道防线,也是最可靠的一道,只要这条索引在,重复数据就进不到表里。
6. 部署验证与进阶技巧:让系统真正跑起来
6.1 部署的最小路径
源码包到手后,最快跑通的路径是这样:先把sql目录下的脚本导入 MySQL,建好库和表;然后启动后端,mvn spring-boot:run或者打成 jar 包运行;最后启动前端,npm install后npm run dev,浏览器打开本地地址。整个过程里最容易出问题的是npm install慢或者报错,常见原因是 Node 版本和依赖不兼容。前端装依赖时如果看到node-sass报错,直接把它换成sass,新版 Node 已经不支持node-sass了。
生产环境部署时,后端打成 jar 包用nohup java -jar放后台跑,前端npm run build产出dist目录,交给 Nginx 托管,再把/api路径反向代理到后端端口。这样前端请求看起来是同源的,跨域问题不存在,也方便以后上 HTTPS。
6.2 验证清单:怎么确认系统可用
部署完一定要按业务路径走一遍,而不是只看看页面能不能打开。推荐按这个顺序验证:管理员登录系统,确认能进后台;创建一门课程并设置上课时间、教室位置;导入几个测试学生账号;用学生账号打卡,确认状态判定正确——在上课前 10 分钟内打是正常,上课后 15 分钟内打是迟到;到统计页面确认出勤率数字和手算结果一致;测试重复打卡,确认被拦截;最后测请假流程,学生提交申请,老师审核,状态变成请假。
这套流程走完,系统才算真正可用。很多人部署完只看了登录页就开始写报告,结果答辩时演示打卡功能才发现状态判定逻辑有 bug,那时候改代码就来不及了。
6.3 一个进阶技巧:用 Excel 批量导入学生数据
考勤系统的学生数据动辄几百条,一条条手动录入不现实,这是源码包里经常会缺的功能,加上它会让整个系统完整度提升一个档次。用 EasyExcel 或者 Apache POI 做一个导入接口,前端用<input type="file">选 Excel,后端读取后校验学号是否重复,批量插入数据库。
代码逻辑不复杂:解析每一行,校验学号格式、姓名非空、班级存在,通过校验的插入,不通过的收集错误信息返回给前端。关键是批量插入要用 MyBatis 的insertBatch或者手动拼 SQL 一次插入多条,循环单条插入几百条数据会很慢,数据库连接也容易被拖垮。这个功能做完,你会发现“系统导入学生数据只要点一下上传”这个点,在验收和答辩时比任何炫酷页面都更能打动老师。
做这个系统最深的体会是:考勤系统技术难度不大,难的是把业务规则想清楚——迟到怎么判定、请假怎么流转、统计口径怎么统一。把这些边界条件定义好,写代码只是一天的事;边界条件没想清楚,改来改去才是真正耗时间的地方。希望这些经验能帮你少走点弯路,项目跑通了之后,你自然会明白哪些地方值得再加一层设计。
本文还有配套的精品资源,点击获取