简介这是一套面向本科毕业设计的小程序上课考勤系统完整源代码基于Spring Boot与微信小程序开发适合Java学习者、毕设学生作为项目参考。系统实现了后台管理、小程序端GPS签到、定位打卡、迟到统计等核心考勤功能设计获得优秀毕业设计兼具实用性与教学价值。压缩包共包含488个文件约4.3MB涵盖Java后端代码、小程序前端js/wxml/wxss页面、SQL初始化脚本、Dockerfile、批处理启动脚本及docx操作文档文件分类清晰便于按模块阅读和部署。系统文档详细说明运行环境配置与数据库导入方式可帮助读者快速跑通项目并理解考勤业务逻辑。目前已有944人学习下载适合需要完成类似课设或快速上手Spring Boot与小程序的开发者学习借鉴。1. 一个 Java 毕设考勤系统真正难的不是写代码每年毕业季都会看到一批「学生上课考勤系统」的毕设选题Java 后端、微信小程序前端、考勤打卡三个词凑在一起看起来是标准的课程设计模板。但真正动手跑过一遍的人都知道这个题目最坑的地方不在 CRUD而在「考勤」这两个字本身时间怎么对齐、位置怎么判定、重复打卡怎么防、老师那边怎么看到可信的统计结果。我见过太多人把系统写完了演示的时候学生改个手机时间就签到了或者人没到教室却定位成功答辩现场直接翻车。这篇笔记我会按自己带项目的习惯把从表设计到接口实现、小程序端联调、再到打包交付的完整路径写清楚照着做能少踩一半坑新手跟得动熟手也能看到参数和边界。2. 先把架子搭对Spring Boot 小程序 MySQL 的四层结构2.1 为什么选 Spring Boot 原生小程序而不是 uni-app常见做法是后端用 Spring Boot 2.x MyBatis-Plus MySQL前端用微信原生小程序不引入 uni-app。原因很实际毕设答辩时老师问的最多的就是「每个文件在干什么」原生小程序的pages/index/index.js、index.wxml、index.wxss一一对应讲起来不绕。uni-app 虽然能一套代码跑多端但启动阶段多一层vue编译链路小程序基础库版本稍有差异就出幺蛾子不值得为了省那点事增加调试成本。前后端分离的结构也方便单独演示后端接口用 Postman 就能测小程序端独立开发。整条链路是「小程序 - 后端 Controller - Service - Mapper - MySQL」四层结构清晰答辩时画一张架构图就能讲五分钟。2.2 数据库四张核心表的设计考勤系统的核心不在用户表而在「考勤任务」和「考勤记录」两张表的关联关系。我一般会设计四张表用户表、课程表、考勤任务表、考勤记录表。课程表和用户表之间通过teacher_id关联学生选课关系用选课表单独维护避免在用户表里堆字段。先看建表脚本这是整个项目的地基-- 用户表学生和教师共用用 role 区分 CREATE TABLE t_user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 学号/工号, password varchar(255) DEFAULT NULL COMMENT 密码MD5 存储, name varchar(50) NOT NULL COMMENT 姓名, openid varchar(64) DEFAULT NULL COMMENT 微信 openid小程序登录用, role tinyint NOT NULL DEFAULT 2 COMMENT 1教师 2学生, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username), KEY idx_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 课程表 CREATE TABLE t_course ( id bigint NOT NULL AUTO_INCREMENT, course_name varchar(100) NOT NULL, teacher_id bigint NOT NULL COMMENT 教师用户 id, classroom varchar(100) DEFAULT NULL COMMENT 上课地点用于定位辅助, latitude decimal(10,6) DEFAULT NULL COMMENT 教室纬度, longitude decimal(10,6) DEFAULT NULL COMMENT 教室经度, start_time time DEFAULT NULL COMMENT 上课时间, end_time time DEFAULT NULL COMMENT 下课时间, PRIMARY KEY (id), KEY idx_teacher_id (teacher_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程表;这里有两个细节值得说。第一openid字段必须加索引小程序每次登录都靠它反查用户没有索引数据量大了以后查询会明显变慢。第二课程表里直接存教室的经纬度这是后面做定位考勤的判断基准不要在打卡接口里写死坐标不然换个教室你的判断逻辑就废了。考勤任务表和记录表是核心单独拆出来写-- 考勤任务表老师发起一次考勤就生成一条 CREATE TABLE t_attendance ( id bigint NOT NULL AUTO_INCREMENT, course_id bigint NOT NULL COMMENT 所属课程, teacher_id bigint NOT NULL COMMENT 发起人, start_time datetime NOT NULL COMMENT 考勤开始时间, end_time datetime NOT NULL COMMENT 考勤截止时间, location_range int DEFAULT 200 COMMENT 允许的定位误差范围单位米, status tinyint DEFAULT 1 COMMENT 1进行中 2已结束, PRIMARY KEY (id), KEY idx_course_id (course_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考勤任务表; -- 考勤记录表每个学生提交一次打卡对应一条 CREATE TABLE t_attendance_record ( id bigint NOT NULL AUTO_INCREMENT, attendance_id bigint NOT NULL COMMENT 考勤任务 id, student_id bigint NOT NULL COMMENT 学生用户 id, checkin_time datetime NOT NULL COMMENT 打卡时间服务器时间, latitude decimal(10,6) DEFAULT NULL COMMENT 打卡时的纬度, longitude decimal(10,6) DEFAULT NULL COMMENT 打卡时的经度, distance decimal(10,2) DEFAULT NULL COMMENT 与教室距离单位米, status tinyint DEFAULT 1 COMMENT 1正常 2迟到 3缺勤, PRIMARY KEY (id), UNIQUE KEY uk_attendance_student (attendance_id, student_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考勤记录表;t_attendance_record表最关键的约束是uk_attendance_student这个联合唯一索引它保证一个学生在同一次考勤任务里只能有一条记录这是防重复打卡的地基。就算前端重复提交、后端并发处理数据库这一层也能兜住。很多毕设在这个表上不建唯一索引结果就是学生多点几次按钮统计报表里一个人的出勤次数变成了好几条。2.3 项目目录结构与环境变量准备后端项目结构我习惯按 controller、service、mapper、entity、common 分包考勤相关的接口单独放一个AttendanceController不要和用户接口混在一起。Java 环境上直接用 JDK 1.8 配好环境变量Spring Boot 2.7.x 足够稳定。application.yml里有几个参数是必须提前定好的server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/attendance_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted # 自定义参数token 过期时间单位分钟 jwt: expire-minutes: 720这里说一下logic-delete-field这个配置MyBatis-Plus 的逻辑删除在毕设里很实用删除课程、删除用户这些操作不会真的把记录从表里抹掉而是打一个删除标记。答辩时老师问「数据删错了怎么办」你直接说是逻辑删除这就是一个可讲的亮点。serverTimezoneAsia/Shanghai这个参数很多人忽略MySQL 8.x 不指定时区经常会报错或者时间差八个小时属于事前加一行、事后省一天的配置。3. 后端三个核心接口登录换 token、打卡判距离、统计出报表3.1 登录鉴权wx.login 换 code后端再用 code 换 openid微信小程序登录的流程是固定的小程序端调wx.login拿到一个临时code传给后端后端拿这个code去微信接口换openid再用openid查用户表查到就发一个 JWT token 给前端。这里不要自己去记 session小程序端每个请求都带 token后端用一个拦截器统一校验Stateless 的方式在答辩时也好解释。我一般用 hutool 的JWTUtil来生成和解析 token省去手写 JJWT 的一大段配置。核心代码这样写Service public class LoginService { Autowired private UserMapper userMapper; private static final String SECRET attendance-demo-secret; /** * 用 wx.login 的 code 换 openid再查用户并签发 token */ public MapString, Object login(String code) { // 1. 调微信接口code 换 openid String url https://api.weixin.qq.com/sns/jscode2session ?appidYOUR_APPID secretYOUR_SECRET js_code code grant_typeauthorization_code; String result HttpUtil.get(url); JSONObject json JSONUtil.parseObj(result); String openid json.getStr(openid); if (StrUtil.isBlank(openid)) { throw new RuntimeException(微信登录失败 result); } // 2. 查用户表找不到就跳转绑定页 User user userMapper.selectOne( new LambdaQueryWrapperUser().eq(User::getOpenid, openid)); if (user null) { throw new RuntimeException(该微信号未绑定账号请先到 PC 端绑定); } // 3. 生成 JWT过期时间从配置读取 String token JWTUtil.createToken( Map.of(userId, user.getId(), role, user.getRole()), SECRET.getBytes()); MapString, Object resultMap new HashMap(); resultMap.put(token, token); resultMap.put(userId, user.getId()); resultMap.put(role, user.getRole()); resultMap.put(name, user.getName()); return resultMap; } }这段代码里有三个值得展开的点。第一jscode2session的返回结果里包含openid和session_keysession_key用不到就不要存库你只要openid。第二Map.of是 Java 9 的语法如果你用的是 JDK 1.8要改成HashMap手动 put不然编译不过这是新手最容易卡住的一分钟。第三token 里只放userId和role就够了别把姓名、手机号全塞进去token 越短网络传输的损耗越小。后续每个接口在拦截器里解析 token拿到userId再查数据库这就是无状态登录的基本链路。3.2 发布考勤与提交打卡经纬度距离判断的完整逻辑老师发起一次考勤后端只做一件事往t_attendance表插一条记录start_time取当前时间end_time取当前时间加 10 分钟作为考勤窗口。学生提交打卡后端要同时做三件事判断考勤任务是否在有效时间内、判断打卡位置和教室的距离是否在允许范围内、写入考勤记录。距离判断用 Haversine 公式算球面距离这是最常考的算法点代码也不复杂public class DistanceUtil { private static final double EARTH_RADIUS 6371000.0; // 地球半径单位米 /** * 计算两个经纬度点的球面距离 */ public static double distance(double lat1, double lon1, double lat2, double lon2) { double radLat1 Math.toRadians(lat1); double radLat2 Math.toRadians(lat2); double deltaLat Math.toRadians(lat2 - lat1); double deltaLon Math.toRadians(lon2 - lon1); double a Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) Math.cos(radLat1) * Math.cos(radLat2) * Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2); double c 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return EARTH_RADIUS * c; } }这个公式在很多定位场景里都能用本质是把经纬度换算成弧度再用余弦定理求球面距离。EARTH_RADIUS取 6371000 米而不是 6371 千米是为了让返回结果直接是米。误差在几十米级别对教室考勤来说完全够用。如果你想偷懒用Math.hypot按平面直角坐标算在学校这种小范围场景下误差不大但答辩时被问「你这个距离准不准」就不好解释了。打卡接口的 Controller 层这样写PostMapping(/student/checkin) public Result checkin(RequestBody CheckinRequest req) { // 1. 找考勤任务判断是否在有效窗口内 Attendance attendance attendanceService.getById(req.getAttendanceId()); Date now new Date(); if (now.before(attendance.getStartTime()) || now.after(attendance.getEndTime())) { return Result.fail(不在考勤时间段内); } if (attendance.getStatus() ! 1) { return Result.fail(考勤已结束); } // 2. 算距离超过范围直接拒绝 Course course courseService.getById(attendance.getCourseId()); double distance DistanceUtil.distance( req.getLatitude(), req.getLongitude(), course.getLatitude().doubleValue(), course.getLongitude().doubleValue()); if (distance attendance.getLocationRange()) { return Result.fail(打卡位置距离教室 (int) distance 米超出允许范围); } // 3. 插入记录数据库唯一索引兜底 AttendanceRecord record new AttendanceRecord(); record.setAttendanceId(req.getAttendanceId()); record.setStudentId(req.getStudentId()); record.setCheckinTime(now); record.setLatitude(req.getLatitude()); record.setLongitude(req.getLongitude()); record.setDistance(distance); record.setStatus(now.before(attendance.getStartTime()) ? 1 : 2); // 早到算正常迟到算迟到 attendanceRecordService.save(record); return Result.ok(打卡成功); }这里要注意的是服务器时间判断new Date()用的是后端服务器的时间绝不能用小程序传上来的时间。理由很简单手机时间用户可以随便改但后端服务器时间改不了这也是整个考勤系统防作弊的第一道防线。locationRange如果没传默认 200 米这个值在校园场景需要现场调——教学楼密集的话 200 米可能覆盖隔壁栋楼一般我建议先去教室实测一次再定。3.3 统计报表按课程聚合出出勤率统计模块是老师端最看重的功能也是答辩加分项。核心需求就一句话某门课某次考勤应到多少人、实到多少人、迟到多少人、缺勤多少人。实现上用一条 SQL 关联两张表就能完成Mapper public interface AttendanceRecordMapper extends BaseMapperAttendanceRecord { /** * 统计某次考勤的汇总数据 */ Select(SELECT COUNT(*) AS total, SUM(CASE WHEN status 1 THEN 1 ELSE 0 END) AS normal, SUM(CASE WHEN status 2 THEN 1 ELSE 0 END) AS late, COUNT(*) - SUM(CASE WHEN status IN (1,2) THEN 1 ELSE 0 END) AS absent FROM t_attendance_record WHERE attendance_id #{attendanceId}) MapString, Object summaryByAttendance(Param(attendanceId) Long attendanceId); }COUNT(*)是应到人数因为每次考勤所有学生都会生成一条记录缺勤的人记录状态是 3但记录本身存在。所以缺勤人数不是查出来的是用总人数减去正常和迟到的人数算出来的。这个逻辑在答辩时一定要讲清楚很多同学做统计报表的时候只统计了打卡成功的人缺勤的人根本没进表那报表里的应到人数就是错的。更完整一点的做法是把学生选课表也关联进来先查出这门课所有选课学生再 LEFT JOIN 考勤记录这样没提交打卡的学生也会出现在结果里这是另一种实现思路数据上更严谨。4. 小程序端闭环从 wx.login 到定位打卡的完整链路4.1 小程序端目录结构与 request 封装小程序端我习惯用微信原生开发目录结构这样组织pages/login/放登录页pages/index/放学生首页和打卡按钮pages/teacher/放老师端考勤管理utils/放 request 封装和工具函数。小程序每个页面是四件套js、json、wxml、wxss页面的生命周期写在 js 里结构不用额外引入。请求封装是整个小程序端的命脉统一在这里处理 token 注入和 401 跳转// utils/request.js const BASE_URL http://localhost:8080; function request(path, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method || GET, data: data || {}, header: { Content-Type: application/json, token: wx.getStorageSync(token) || }, success(res) { if (res.statusCode 401) { // token 失效清理缓存并跳登录页 wx.removeStorageSync(token); wx.removeStorageSync(userId); wx.navigateTo({ url: /pages/login/login }); reject(new Error(登录已过期)); return; } if (res.data.code ! 200) { wx.showToast({ title: res.data.msg, icon: none }); reject(new Error(res.data.msg)); return; } resolve(res.data.data); }, fail(err) { wx.showToast({ title: 网络请求失败, icon: none }); reject(err); } }); }); } module.exports { request, BASE_URL };封装的核心就是把重复的事做掉每个请求自动带头 token接口返回code ! 200时统一弹 toast401时统一踢回登录页。如果不做这个封装每个页面都要写一遍wx.request的完整回调代码量翻倍而且容易漏掉 token 的处理。BASE_URL这里写的localhost是开发者工具里的跑法后面真机调试要换成电脑的局域网 IP这个坑后面避坑章节详细说。4.2 学生打卡页getLocation 获取位置并提交学生端打卡页面的核心逻辑就两步调用wx.getLocation拿经纬度然后调后端的/student/checkin接口。这里有个容易被忽略的前提wx.getLocation需要在小程序后台申请权限而且在app.json里要声明permission字段不声明的话接口直接报错。打卡页的 js 逻辑这样写// pages/checkin/checkin.js const { request } require(../../utils/request); Page({ data: { attendanceId: null, courseName: , latitude: null, longitude: null, loading: false }, onLoad(options) { this.setData({ attendanceId: options.attendanceId }); }, // 获取当前位置并打卡 handleCheckin() { if (this.data.loading) return; this.setData({ loading: true }); wx.getLocation({ type: gcj02, success: (res) { const { latitude, longitude } res; this.setData({ latitude, longitude }); request(/student/checkin, POST, { attendanceId: this.data.attendanceId, latitude: latitude, longitude: longitude }).then(() { wx.showToast({ title: 打卡成功, icon: success }); }).catch(() { // 具体的错误信息已经在 request 里 toast 过了 }).finally(() { this.setData({ loading: false }); }); }, fail: (err) { this.setData({ loading: false }); wx.showToast({ title: 定位失败请检查手机定位权限, icon: none }); } }); } })type: gcj02这个参数必须写它返回的是国测局坐标和腾讯地图、微信生态的坐标系一致。如果你后端用的坐标是 GPS 的 WGS84 原始坐标两者之间会有几十到几百米的偏移考勤距离判断直接从「允许 200 米」变成「永远不在范围内」。这是一个很典型的坐标系不一致翻车现场最简单的处理方式就是前端传gcj02后端基准教室坐标也用gcj02全链路统一不要在中间做转换。4.3 教师端发布考勤一个按钮搞定任务创建教师端页面相对简单核心功能是选择课程、发起考勤、查看记录。发起考勤的按钮事件里调用后端的/teacher/attendance/create接口传courseId即可。一个值得做的增强功能是调用wx.scanCode扫描课程二维码扫码后自动填充课程信息这个功能在答辩演示时效果很好代码量也不大// pages/teacher/create.js scanAndCreate() { wx.scanCode({ onlyFromCamera: true, success: (res) { const courseId res.result; // 二维码内容就是课程 id this.createAttendance(courseId); } }); }, createAttendance(courseId) { request(/teacher/attendance/create, POST, { courseId }) .then((data) { wx.showToast({ title: 考勤已发起, icon: success }); }); }教师端发起考勤后学生端怎么知道有新的考勤任务两种常见方案。第一种是最简单的学生进入首页时拉取「进行中的考勤列表」适合毕设第二种是接入小程序的订阅消息老师发起考勤后给学生推送一条通知效果更好但需要申请消息模板还要处理用户授权复杂度上升一个档次。我一般建议毕设里做第一种答辩时把第二种作为「后续优化方向」一句话带过既能体现思考深度又不增加开发量。小程序的wx.scanCode在真机上体验很顺但在开发者工具里经常扫码失败属于正常现象演示前要提前用真机测试一遍。5. 考勤系统避坑手册时间作弊、模拟定位与授权消失考勤系统的坑集中在「可信度」三个字上这里把我自己踩过的、以及学生常遇到的五类问题整理出来每条按「现象 - 原因 - 解决」展开。5.1 现象人没到教室考勤却通过了这是考勤系统里最要命的问题。现象是学生人在宿舍用开发者工具或真机模拟了教室的定位后端没有拦下来。原因有两层。第一层是前端定位本身就能伪造wx.getLocation返回的数据在开发者工具里可以手动设置真机上也可以用第三方工具模拟第二层是后端如果不校验「坐标是否真的在教室附近」只接受前端传上来的值那整个定位考勤就是形同虚设。解决思路是后端必须做距离校验这也是前面DistanceUtil存在的意义。更进一步的加固是多点采样让学生在前端连续获取三次定位后端判断三次坐标是否在合理范围内且相互之间距离不过大如果三次坐标完全相同或者跳动异常直接判定为疑似作弊。这个方案在答辩时讲出来老师会觉得你考虑问题很全面。代价是接口调用次数翻三倍对毕设项目来说无压力。5.2 现象学生改了手机时间签到记录全乱了现象是学生在考勤截止时间之后提交打卡但在前端把手机时间改成截止时间之前后端记录的却是「正常打卡」。原因是前端代码里如果用new Date()取的是手机本地时间这个时间完全可控甚至某些安卓机型在时间被修改后网络请求里的某个环节会把本地时间带进请求体。解决方法是后端所有时间判断统一用服务器时间前端传上来的时间字段一律忽略只作为展示用。具体到代码层面就是打卡记录里的checkin_time永远在后端new Date()生成不在前端设置。另外一个细节数据库连接串里要配好serverTimezoneAsia/Shanghai不然 MySQL 默认按服务器时区解析时间可能产生八小时偏差你以为没事实际上记录的时间全部错位。5.3 现象token 过期后接口全部 401页面白屏现象是学生用着用着接口突然全部返回 401页面没有任何提示看起来像程序卡死了。原因是 JWT token 有过期时间过期后后端拦截器直接拒绝请求但前端没有处理 401 的公共逻辑也没有让用户重新登录的机制。解决方法是统一在utils/request.js里处理 401清理本地缓存的 token 和用户信息跳转登录页。我在 4.1 节封装的代码里已经处理了。这里要注意的细节是wx.navigateTo在页面栈超过十层时会失效登录页跳转建议用wx.reLaunch它会清空整个页面栈避免用户从登录页返回时又回到打卡页。token 过期时间也不要设得太长我一般设 720 分钟也就是 12 小时够一天使用又不会让 token 在长时间内一直有效。5.4 现象开发者工具请求正常真机上 request 全部失败现象是电脑上用微信开发者工具调试一切正常数据都能加载但是拿手机一打开所有请求全部失败。原因是开发者工具默认开启了「不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书」这个选项你在开发者工具里请求http://localhost:8080没问题真机上微信客户端强制要求域名必须是 HTTPS 且在小程序后台配置过合法域名localhost在真机上是手机自己不是你的电脑。解决方法是分两步开发阶段让电脑和手机连同一个 WiFiBASE_URL改成电脑的局域网 IP比如http://192.168.1.100:8080同时在开发者工具里勾选「不校验合法域名」如果要真正在真机体验只能把后端部署到有公网 IP 的服务器上并配 HTTPS。如果你没有服务器本地电脑配合内网穿透或者把项目部署到云服务器是最常见的做法云平台的免费额度通常够毕设演示用。这个问题是我见过最多人卡住的地方不是代码错是环境没对上。5.5 现象学生连续点击打卡按钮生成了多条记录现象是学生在打卡页面快速连点按钮Network 里能看到同一个打卡接口被调了多次数据库里出现了同一考勤任务下的多条记录。原因是点击事件没有加防抖loading状态在第一次请求回来之前就已经被快速点击绕过了如果加了loading判断但前端的setData是异步的也存在短暂窗口期。解决方法是三层防护。第一层是前端按钮加loading标志点击后立即置灰第二层是t_attendance_record表的联合唯一索引uk_attendance_student在 2.2 节建表时已经加上了即使并发请求到达数据库也会拒绝第二条插入第三层是在 Service 层用synchronized或者分布式锁项目不大就用数据库唯一索引就够了。后端在插入时捕获DuplicateKeyException返回友好提示「你已经打过卡了」不要直接抛 500 让学生看到一堆英文堆栈。这三层里数据库唯一索引是兜底前两层是体验优化少了任何一个都可能在极端场景下翻车。6. 交作业前的最后一轮验证用例、源码打包与答辩表达项目写完到交源码之间我习惯先过一遍验证用例不要直接打包。这里列一个最小验证清单照着跑一遍基本不会在演示时出丑编号验证场景操作步骤预期结果1首次登录微信开发者工具中调用wx.login后端用 code 换 openid未绑定用户提示去绑定已绑定用户返回有效 token2正常打卡教师端发起考勤学生端在教室范围内点击打卡返回「打卡成功」考勤记录状态为正常3超范围打卡将开发者工具中的定位改为校门口返回「超出允许范围」并显示距离值4重复打卡打卡成功后再次点击按钮返回「你已经打过卡」数据库仍只有一条记录5token 过期修改jwt.expire-minutes为 1等两分钟后请求任意接口小程序自动跳转登录页验证用例通过之后再处理源码包。打包时注意三件事第一application.yml里的数据库密码不要用真实密码改成123456之类并写进 README第二建表 SQL 脚本单独放一个db/init.sql文件名字起清楚一点很多同学把 SQL 丢散在代码注释里老师找不到就印象分大减第三README 里写清楚「从零跑通」的完整步骤包括环境版本JDK 1.8、MySQL 8.0、微信开发者工具稳定版、BASE_URL怎么改、真机调试注意事项。代码的完整性和可复现性比代码本身的复杂度更重要这是毕设源码评分的潜规则。最后说答辩表达。我通常会让学生准备两个问题的答案第一「JWT 和传统 Session 有什么区别」——不要背八股文用自己的话讲Session 是服务器记一份用户数据JWT 是服务器发一个签过名的凭证每次请求带着服务器验证签名就认账不占服务器内存第二「定位考勤怎么防作弊」——把距离校验、服务器时间、联合唯一索引、多次采样这四点讲一遍每个点对应一个表或一段代码老师立刻能看出这项目不是你网上找的。如果老师追问「数据量大怎么办」你就说考勤记录表按attendance_id建索引单表数据超过百万级可以按月分表——这属于加分的延伸答案答不上来也没关系前面几点扎实就够了。我自己带毕设的习惯是交付前一定亲自用真机把整条链路走两遍一遍在开发者工具里模拟一遍用手机开热点跑真机。定位考勤这类系统玄学问题特别多模拟器正常、真机偏移几十米是常有的事提前把location_range调好比你多写一个功能有用得多。希望这篇笔记能帮你少踩几个坑把精力花在真正能加分的地方。本文还有配套的精品资源点击获取
