
最近把一套Java方向的典型毕业设计项目从零搭了一遍基于SpringBootAndroid的大学生勤工助学管理系统业务定位在企业招聘。这个课题网上很多人做过但大多数模板要么只有学生端要么把企业招聘简化成了公告列表。真正像样的版本应该把“学生找岗位、企业发岗位、管理员管岗位、考勤结算一条龙”完整打通。这篇文章我会把整个项目的设计思路、数据库结构、后端接口实现、Android端联调细节以及整理源码、文档、运行视频、讲解视频时踩过的坑一次性讲清楚。适合正在选毕业设计题目、拿到类似课题不知道怎么动手或者想对已有模板项目做二次开发的同学。1. 项目整体设计与技术栈选型1.1 勤工助学场景里有哪些角色、哪些事、哪些状态拿到这个题目先别急着写代码我习惯先把业务拆开。大学生勤工助学管理系统里至少有三类使用者学生、企业用户、系统管理员。在很多高校场景里企业用户也可以是校内部门、食堂档口、图书馆助理团队但本质上它们都有一个共同诉求发布兼职/实习性质的岗位找学生来干活并完成从报名到结算的闭环。三者的核心功能可以通过一张表看清角色核心功能学生浏览岗位、投递简历、查看报名状态、签到打卡、查看工时与工资企业/校内部门发布岗位、上下架岗位、审核学生报名、确认考勤、发起工资结算管理员用户管理、岗位审核、数据统计、日志查看、系统基础配置这里有个比较容易忽略的点岗位状态和报名状态不是孤立的而是串在一条业务线上。企业发布的岗位要先经过管理员审核审核通过后学生才能看到学生报名后状态流转为“待审核”企业审核“通过”后进入上岗阶段上岗阶段要有考勤记录最终根据工时生成结算单。如果不把这条线理清楚代码写到后面会非常乱经常出现“岗位已经下架了还能报名”“学生还没通过审核就能签到”这类逻辑漏洞。所以在后端设计时我刻意给每个核心环节加了状态机校验。岗位有审核中、已上架、已下架、已拒绝四种状态报名有五到六种状态考勤和结算也各自有状态。这样虽然开发量稍微增加了一点但演示时逻辑非常清晰答辩时也更容易讲出设计感。1.2 为什么是SpringBoot加Android原生而不是Web加小程序技术选型是这个题目最关键的决定。我见过很多同学一上来就想做小程序或者纯Web不是不行但结合这套题目的考察点和演示效果SpringBootAndroid原生是更合理的组合。SpringBoot在后端生态里属于“默认选项”。它内置Tomcat、自动配置、生态成熟配合MyBatis操作MySQL写起来比SSH那套老框架快得多而且部署简单一个java -jar就能跑。对毕业设计来说SpringBoot本身就是一个加分项因为很多面试官会默认你接触过Spring家族项目里能讲出IOC、AOP、自动配置、拦截器等概念比单纯用Servlet/JSP有话题性。Android原生端则能让项目以独立App的形态出现。学生扫码或安装APK后就能浏览岗位、报名、签到这种“移动端体验”比一个管理系统挂在浏览器里更有冲击力。同时App端可以展示FileProvider文件上传、权限申请、Retrofit网络请求、RecyclerView列表优化等移动端核心知识点这些内容放在简历和答辩里都很能打。小程序当然也是个选项但它更适合快速上线、强调轻量体验的场景。本地开发需要注册小程序账号、配置AppID还要面对审核和调试环境限制对个人开发者来说反而容易卡进度。而Android原生开发只要装好Android Studio就能跑模拟器、真机都可以调试自由度更高。如果是给企业做一个长期使用的招聘工具App能覆盖的交互方式和离线能力也比小程序更完整。1.3 技术栈版本选择怎么避开“SpringBoot版本太高”的坑版本问题看着不起眼实际上能让人浪费一整天。现在网上教程满天飞不少还是基于SpringBoot 2.x写的如果你直接新建一个Spring Boot 3.x项目会发现一堆旧代码用不了尤其是javax.servlet.*变成了jakarta.servlet.*MyBatis的starter也要换成新版本很多底层配置默认值还变了。对毕业设计来说完全没必要给自己上难度。我推荐一套比较稳的版本组合组件推荐版本备注JDK1.8或118最稳妥很多老教程都基于8SpringBoot2.7.182.x系列最后一个版本生态成熟MyBatis3.5.x mybatis-spring-boot-starter 2.3.x与SpringBoot 2.x配套MySQL5.7或8.0两者都行注意驱动版本JWT库jjwt 0.9.1简单易用解析token方便AndroidminSdk 21targetSdk 31左右兼容性较好不踩分区存储的过度细节Retrofit/OkHttp2.9.0 / 4.11.0网络层稳定组合Glide4.12.0图片加载如果搜索相关技术文章常会看到“SpringBoot版本太高”的讨论。我的建议很直接不要为了追新用SpringBoot 3.x除非你已经确信自己能把jakarta包的差异、新版本MyBatis配置和Spring Security 6的变化都搞定。2.7版本在功能上完全够用遇到问题搜答案也容易因为绝大多数教程都是针对2.x的。2. 核心业务模块与数据库设计2.1 角色权限模型用一张表还是三张表很多模板项目把用户表和角色设计得非常简单一张user表加一个role字段搞定。这种设计在演示时可以跑通但我更推荐带角色表的方案哪怕只是“用户表角色表用户角色关系表”这种轻量RBAC。听起来复杂其实就多建两张表。用户表只存账号密码和基本信息角色表存管理员、企业、学生三类角色用户角色关系表做关联。好处很明显将来某个用户想同时扮演企业和管理员角色不需要改表结构插一条关系记录就行权限判断也可以做成基于角色的注解而不是到处写if (role 1)。权限判断在后端落地有几种方式。一种是在拦截器里解析token后把当前用户ID和角色塞到请求上下文中Controller方法里手动判断另一种是自定义RequireRole(ENTERPRISE)注解配合AOP统一拦截。第二种看起来更高大上代码也更干净但实现起来要处理注解切面和异常回滚。我的建议是如果项目整体不复杂手动判断就够了关键是判断逻辑要集中最好封装一个UserContext工具类避免每个Controller里重复写从token取用户信息的逻辑。2.2 核心表结构设计与字段说明勤工助学系统核心表大概六张用户表、角色表、岗位表、报名表、考勤表、工资结算表。在这里给出几张关键表的简化SQL计划换成正式项目时可以直接参考。用户表CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, real_name varchar(50) DEFAULT NULL, student_no varchar(20) DEFAULT NULL COMMENT 学号学生角色使用, phone varchar(20) DEFAULT NULL, avatar varchar(255) DEFAULT NULL COMMENT 头像相对路径, status int(11) NOT NULL DEFAULT 1 COMMENT 1正常 0禁用, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;岗位表CREATE TABLE job ( id int(11) NOT NULL AUTO_INCREMENT, enterprise_id int(11) NOT NULL COMMENT 发布企业用户ID, title varchar(100) NOT NULL, category varchar(50) DEFAULT NULL COMMENT 岗位分类助教、行政助理、图书管理员等, description text, salary_type int(11) NOT NULL COMMENT 1按小时 2按月 3按次, salary_value decimal(10,2) NOT NULL, total_quota int(11) NOT NULL COMMENT 招聘总人数, work_time varchar(255) DEFAULT NULL COMMENT 工作时间描述, address varchar(255) DEFAULT NULL, status int(11) NOT NULL DEFAULT 0 COMMENT 0待审核 1上架 2下架 3拒绝, create_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_enterprise (enterprise_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;报名表CREATE TABLE application ( id int(11) NOT NULL AUTO_INCREMENT, job_id int(11) NOT NULL, student_id int(11) NOT NULL, resume_path varchar(255) DEFAULT NULL COMMENT 简历附件相对路径, status int(11) NOT NULL DEFAULT 0 COMMENT 0待审核 1通过 2拒绝 3取消 4完成, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_job_student (job_id, student_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;考勤表和结算表大同小异考勤表记录每次签到的开始时间、结束时间、工时结算表记录某个结算周期内某学生的总工时、总金额和发放状态。这里我强调三个设计细节第一密码字段必须存加密后的值。用BCrypt而不是MD5因为MD5加盐也挡不住彩虹表BCrypt这种自带随机的算法是行业惯例。第二金额字段用decimal(10,2)绝对不能用float或double否则工资计算会出现0.01元级别的误差。第三所有状态字段都用int并在代码里定义一个常量类统一管理。状态值散落在代码里是最难维护的写法之一今天写个1明天写个1三个月后谁都不知道这个1是上架还是通过。2.3 业务状态流转设计从岗位发布到工资结算状态流转是这套项目里最值得花时间设计的模块。我见过不少代码写了五个表状态字段也建了但没有任何业务规则前端传什么状态就更新成什么状态最后的后果就是脏数据一大堆。我在这套项目里规定了下面的流转链路环节参与角色前置状态目标状态发布岗位企业无待审核审核岗位管理员待审核上架/拒绝学生报名学生岗位上架待审核审核报名企业待审核通过/拒绝签到上岗学生/企业报名通过考勤记录生成生成结算企业考勤存在待发放确认发放管理员/企业待发放已发放每个状态跳转都在Service层做一个前置校验。比如更新报名状态的SQL我会写成update application set status #{newStatus} where id #{id} and status #{oldStatus}影响行数为0就说明当前状态和预期不符直接抛出业务异常。这种做法叫乐观锁思想在并发场景下也能防止两个人同时操作导致状态错乱。其实状态机不是只在大厂高并发项目里才有价值像这种勤工助学系统状态字段就是业务本身的骨骼。答辩时老师问“你怎么保证业务流程正确性”你把这条链路讲清楚再配合数据库唯一约束和更新时前置状态校验一次就能说明白。3. 后端SpringBoot实现细节与踩坑3.1 工程分层与统一返回结构后端项目我采用的是非常经典的分层结构com.example.scholarship ├── controller // 接口层 ├── service // 业务层接口impl ├── mapper // MyBatis持久层 ├── entity // 数据库实体 ├── dto // 请求/响应对象 ├── common // Result、异常、工具类 └── config // WebMvc、拦截器、跨域配置很多同学在分层上容易犯的毛病是Controller里直接写SQL或者业务逻辑Service层形同虚设。其实分层最大的价值不是代码多规范而是可读性和可维护性。Controller只负责参数接收和返回包装Service负责业务逻辑和事务Mapper负责数据访问这样出问题时查日志非常快。统一返回结构我也强烈建议做一个ResultT包装类public class ResultT { private Integer code; private String msg; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.code 200; r.msg success; r.data data; return r; } public static T ResultT error(String msg) { ResultT r new Result(); r.code 500; r.msg msg; return r; } }同时配合一个全局异常处理器RestControllerAdvice捕获BusinessException、参数校验异常、未知异常统一转换成语义友好的Result返回。这样Android端解析JSON特别省事只看一个code字段就能判断请求是否成功不用去解析一堆奇奇怪怪的Spring默认错误页。3.2 JWT登录鉴权与拦截器实现登录鉴权我选择JWT方式流程是用户提交账号密码后端验证通过后生成一个包含用户ID和角色的token前端保存这个token在后续请求的Header里带上Authorization: Bearer xxx后端拦截器解析token并放行。JWT工具类大致长这样public class JwtUtil { private static final String SECRET your-secret-key; private static final long EXPIRE 7 * 24 * 60 * 60 * 1000L; public static String createToken(Integer userId, String role) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }拦截器里要做的事情不复杂但有两个细节必须注意。第一token解析失败要返回业务状态码401不能直接抛异常让全局处理返回500。第二要放行登录、注册、静态资源、文件访问这些路径否则用户还没登录就被拦截界面永远进不去。配置拦截器时我习惯把放行路径写在一个构造函数里类似excludePathPatterns(/api/auth/**, /upload/**, /static/**, /error)。曾经有段时间我为了省事只放行了/api/auth/login结果Image请求也被拦截测试时页面图片全挂排查了好久才发现是拦截器路径问题。这个坑建议新手提前避开。3.3 文件上传与简历附件处理文件上传是这套系统里比较有代表性的功能学生上传简历企业上传岗位图片头像上传也属于同一类。后端接口我用一个通用的上传接口处理PostMapping(/api/file/upload) public ResultString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { throw new BusinessException(文件不能为空); } // 原始文件名可能包含中文和特殊字符不能直接用 String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.) 1).toLowerCase(); // 扩展名白名单 ListString allowed Arrays.asList(jpg, jpeg, png, pdf, doc, docx); if (!allowed.contains(ext)) { throw new BusinessException(不支持的文件类型); } String fileName UUID.randomUUID().toString().replace(-, ) . ext; // 保存到本地 upload 目录 file.transferTo(new File(UPLOAD_DIR File.separator fileName)); return Result.ok(/upload/ fileName); }这里要特别说明一下为什么不用原始文件名。一方面是安全问题用户可能上传../../xxx.jsp这类路径穿越文件另一方面是中文文件名在不同操作系统和Tomcat版本下的编码处理不一致容易变成乱码。统一用UUID重命名既干净又安全。访问上传文件还需要在WebMvcConfig里做静态资源映射Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadDir /); }如果不做这步上传成功后返回的URL在浏览器里会404Android端图片也加载不出来。很多联调超时的项目问题就出在这里。3.4 Maven构建与SpringBoot项目打包运行用Maven构建SpringBoot项目时有一个关键点pom.xml里要配好spring-boot-maven-plugin。这是SpringBoot能打成可执行jar包的原因它会执行repackage操作把普通jar重新组织成内含Tomcat和所有依赖的fat jar。build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build如果少这个插件你用mvn clean package打出来的jar执行java -jar时会直接报“没有主清单属性”。反过来讲如果你只是普通Web项目根本不需要这个插件。这个坑在“maven项目构建方法springboot”这类搜索词下特别常见本质就是插件没配对。开发阶段我建议在application.yml里多配置几个环境spring: profiles: active: dev然后写application-dev.yml连本地MySQLapplication-prod.yml连线上数据库或可交付的演示数据库。切换环境就改一行避免每次换数据库都要改一堆连接参数。4. Android端开发与联调细节4.1 网络层封装Retrofit加OkHttp和统一Token注入Android端网络层我用Retrofit2 OkHttp Gson这是主流组合。核心封装思路是写一个ApiClient把OkHttpClient、Retrofit实例、接口Service都集中管理。OkHttpClient okHttpClient new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .addInterceptor(new Interceptor() { Override public Response intercept(Chain chain) throws IOException { Request original chain.request(); String token TokenManager.getToken(); Request request; if (token ! null !token.isEmpty()) { request original.newBuilder() .addHeader(Authorization, Bearer token) .build(); } else { request original; } return chain.proceed(request); } }) .build(); Retrofit retrofit new Retrofit.Builder() .baseUrl(BASE_URL) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) .build();在拦截器里统一注入token接口定义处就不需要每个方法都加Header参数。这样做还有一个好处如果某个请求返回401可以在拦截器里统一判断并踢回登录页业务代码里不用到处处理登录过期。接口定义就变成声明式的非常清爽public interface ApiService { POST(api/auth/login) CallResultLoginResponse login(Body LoginRequest request); GET(api/job/list) CallResultListJob getJobList(Query(page) int page); Multipart POST(api/file/upload) CallResultString uploadFile(Part MultipartBody.Part file); }如果你用的是Kotlin协程可以把Call换成suspend但我在这个项目里用了Java和Call回调因为毕业设计里Java代码可读性更强面试讲线程切换也更好理解。4.2 从登录到岗位列表的页面流程App端页面我按角色拆成了两个基本框架学生端是底部三个Tab分别是岗位大厅、我的报名、个人中心企业端是岗位管理、报名审核、个人中心。登录页放在进入之前统一处理账号密码关联了角色后进入主界面时根据角色展示不同Tab。岗位大厅是学生端最核心的页面。用RecyclerView展示岗位卡片卡片上显示岗位名称、薪资待遇、工作地点、状态标签。配合SwipeRefreshLayout做下拉刷新网络加载过程中用一个全局的进度状态而不是阻塞弹窗。Android的进度条这里有两种选择列表首次加载时在布局中间显示一个圆形进度条刷新时用SwipeRefreshLayout自带的loading动画。我不建议用AlertDialog把整个页面挡住体验很差又容易让用户误以为App卡死。点击岗位进入详情页展示完整介绍底部是“立即报名”按钮。这里有一个必须处理的细节如果当前用户已经报名过该岗位按钮应该置灰并显示“已报名”同时后端也要防止重复提交。前端置灰只是体验优化真正的防重复逻辑靠后端的唯一索引uk_job_student保证。关于页面架构很多教程喜欢上Fragment嵌套但初学者很容易在Fragment生命周期上踩坑。我建议学生端的页面直接用多个Activity页面加底部导航或者用一个Fragment容器管理三个页签。别为了追求架构先进而引入太多概念先把页面流程跑通后面再谈优化。4.3 FileProvider与简历上传实现Android端上传简历时让我掉过几次头发的点就是文件Uri处理。当用户从文件管理器选择一个PDF简历返回给App的是一个content://格式的Uri。如果你老实地直接调uri.getPath()去拿文件路径在某些机型上会拿到一串奇怪的路径根本定位不到文件。正确做法是通过ContentResolver打开输入流读取文件内容。private InputStream getInputStreamFromUri(Uri uri) { return getContentResolver().openInputStream(uri); }然后用Retrofit构造Multipart上传RequestBody requestFile RequestBody.create(MediaType.parse(application/pdf), fileBytes); MultipartBody.Part body MultipartBody.Part.createFormData(file, fileName, requestFile);注意createFormData的第一个参数必须和后端RequestParam(file)的参数名一致。我调试时曾遇到后端一直报“Required request part file is not present”查了很久才发现Android端把参数名写成了multipartFile两边不一致请求体里自然没有后端的file。同时Android 7.0以上如果想自己通过FileProvider分享App内文件需要在Manifest注册provider android:nameandroidx.core.content.FileProvider android:authorities${applicationId}.fileprovider android:exportedfalse android:grantUriPermissionstrue meta-data android:nameandroid.support.FILE_PROVIDER_PATHS android:resourcexml/file_paths / /providerfile_paths.xml里配置好需要暴露的目录。这部分知识在搜索热词里能频繁看到类似问题核心思路就是一个content://不是文件路径要通过流式读取拿到真实内容。4.4 真机调试、局域网访问与HTTP明文限制Android端联调最基础也最容易出问题的是后端地址配置。我总结下来三种情况模拟器访问本机后端用http://10.0.2.2:8080/真机访问电脑后端用http://电脑局域网IP:8080/真机访问远端服务器用服务器公网IP或域名如果是真机调试手机和电脑必须连同一个局域网。但要注意部分校园网和企业WiFi开启了AP隔离手机根本访问不到电脑的IP这时候要么用手机热点把电脑也连进来要么用USB网络共享。我实际调试中遇到过好多次后端接口在电脑浏览器上访问完全正常手机却是长时间转圈最后发现都是WiFi隔离导致的。还有一个Android高版本的大坑Android 9及以上默认禁止明文HTTP流量直接访问http://地址会报CLEARTEXT communication not permitted。解决方案有两个简单粗暴的是一个manifest开关application android:usesCleartextTraffictrue ... 严谨一点的做法是配置networkSecurityConfig只放行开发环境IP或域名。对毕设项目来说用usesCleartextTraffictrue问题不大但如果要做发布级应用最好用网络安全配置文件。我在项目里给BASE_URL单独做了常量类切模拟器、真机、服务器就改一行演示前全局搜一遍防止把本机调试地址留在包里。4.5 图片加载与列表性能头像和岗位图片用Glide加载界面上只用一两行代码Glide.with(context) .load(imageUrl) .placeholder(R.drawable.default_avatar) .error(R.drawable.default_avatar) .circleCrop() .into(ivAvatar);这里最需要注意的是图片URL的拼接。后端返回给Android的字段可能是相对路径/upload/avatar.jpg需要在展示之前拼上BASE_URL得到完整地址。我见过很多项目这个拼接逻辑散落在各处适配器甚至工具类里有些接口返回的是完整URL有些返回的是相对路径导致页面里有的图片能显示有的显示默认图。统一做法是写一个UrlUtil.getFullImageUrl(String relativePath)所有字段在进入Ui之前都经过它处理绝对不要在Adapter里到处拼接。另外列表页别用大尺寸原图Glide的.override(200, 200)缩一下尺寸同时使用RecyclerView.setHasFixedSize(true)列表滑动会流畅非常多。5. 联调阶段的高频问题与排查经验5.1 联调高频问题速查表前后端联调是项目里花时间最多的环节。下面这些坑几乎每个用这套技术栈的人都会遇到整理成一个速查表问题现象可能原因解决办法登录后调接口仍返回401请求头没带token或token已过期检查拦截器是否成功注入header检查TokenManager读取时机Android请求报Failed to connect后端地址错误、防火墙拦截、WiFi隔离先用浏览器访问接口确认后端通再用curl或ping排查网络连通数据库中文变为问号连接串未指定编码或表不是utf8mb4jdbc url增加characterEncodingutf8表和库统一为utf8mb4上传文件后无法访问图片没配置静态资源映射WebMvcConfig添加/upload/**映射到本地目录日期格式变成一串数字Jackson序列化格式问题实体字段加JsonFormat(patternyyyy-MM-dd HH:mm:ss, timezoneGMT8)真机请求超时但模拟器正常手机与电脑不在同一网络或AP隔离使用同一WiFi或热点关闭电脑防火墙或有线网共享页面图片全部加载失败URL拼接问题或缺INTERNET权限统一UrlUtil拼接完整地址Manifest加网络权限更新报名状态返回失败前置状态校验不通过SQL影响行数为0检查当前状态在Service层打印入参和旧状态5.2 一次让人印象深刻的报名状态不更新故障我在联调时碰到过一个特别典型的场景学生端App报名成功了后端数据库里也看到了新记录但企业端那边页面一直显示“待审核”的列表是空的退出重进后却又能看到。我当时第一反应是接口写错了查了半天日志发现后端接口返回的数据明明是对的问题出在Android端列表页没有主动刷新。原因很简单企业端进入“报名审核”页面后只在onCreate里加载一次数据从其他页面切回来时没有重新请求。后来我在页面的onResume里加了自动刷新逻辑问题就消失了。这个经历给了我一个教训联调问题不一定是后端问题也可能是前端页面的生命周期处理不当。另一个印象很深的bug是状态更新影响行数为0。学生取消报名时前端发了请求后端却一直返回“操作失败”。排查后发现我的取消逻辑在SQL里指定了where status 0但那条记录已经被企业审核通过状态早变成1了。学生会取消一个已经被通过的岗位吗按理说业务上是不允许的所以后端主动拦截是正确的。但拦截得不够友好前端应该收到明确的“该岗位已通过审核无法取消”提示。后来我在Service层增加了状态判断分支给不同情况返回不同错误信息比一个笼统的“操作失败”好很多。这类问题在网上经常被问本质都是状态机逻辑没有前置判断完全。如果你在写update语句时带上旧状态作为条件并且检查影响行数就能把很多隐藏bug变成可控提示。5.3 容易被忽视的开发细节最后说几个不太容易注意但对项目质量影响很大的细节。密码加密方面一定用BCrypt而不是MD5登录时用BCryptPasswordEncoder.matches(rawPassword, encodedPassword)校验。明文密码存在数据库里在答辩时被老师看到会非常尴尬。金额计算统一用BigDecimal。工资按小时算的时候学生干了2.5小时时薪18.5元double计算出来的结果可能带一堆小数尾巴虽然多数时候看起来没问题但积累多了会出现分账对不上的情况。列表分页一定要做。哪怕岗位现在就几十条也得用PageHelper或者手动limit。不然答辩时老师说“如果有一万条岗位数据你这个列表会不会卡死”你没办法只能心虚。分页实现不复杂后端返回一个带total和list的分页对象Android端上滑加载下一页。接口幂等和并发安全值得稍微提一下。报名接口最关键的是“同一学生同一岗位只能报一次”这个约束必须在数据库层用唯一索引保证而不是靠前端按钮置灰。只有数据库唯一约束才能防并发重复插入否则快速点击两次报名按钮就可能生成两条记录。6. 源码、文档与讲解视频的整理思路6.1 源码目录与Git管理规范项目交付时源码质量直接影响使用者能不能顺利跑起来。我的建议目录是这样scholarship-server/ SpringBoot后端项目 scholarship-app/ Android客户端项目 sql/ 数据库初始化脚本带示例数据 docs/ 项目设计文档、答辩PPT、运行说明.gitignore一定要配好。后端忽略target文件夹Android忽略build、.gradle、.idea、*.iml和本地local.properties。这些目录如果提交上去压缩包体积会特别大而且不同电脑的本地配置直接冲突。去掉之后项目干干净净对方拉下来重新构建也没问题。README文件我建议写清楚四件事项目简介、环境要求、数据库导入步骤、默认账号。尤其是默认账号很多项目分享出去后运行者连登录入口都进不去因为没有给账号密码。我在这个项目里默认放了三类账号管理员、企业、学生各一个并在README里注明。delivery时不要把upload的本地文件也打进去那属于运行时数据不属于源码。数据库脚本单独放里面同时包含建表语句和示例数据这样新环境的搭建成本最低。6.2 项目文档怎么写才经得起答辩毕设文档和项目代码一样重要因为它直接决定答辩老师第一印象。标准章节结构可以这样安排选题背景与意义关键技术介绍SpringBoot、MyBatis、Android、JWT需求分析角色、用例图、业务流程系统设计总体架构、功能模块、数据库设计系统实现核心代码、界面截图、关键逻辑说明测试功能测试用例、结果分析总结与展望写文档最容易犯的毛病是贴大段代码凑字数。老师更想看到“为什么这样设计”。比如角色权限为什么用状态字段、状态机为什么要防跳转、工时如何计算、工资如何校验。将业务逻辑讲清楚配合流程图和表结构说明远比堆代码有说服力。文档里的截图要注意清晰度Android端页面最好用模拟器或者真机截图不要用电脑屏幕拍照。6.3 运行视频和讲解视频的录制要点运行视频的核心是演示完整业务闭环。我录制时按这个流程走管理员登录并审核岗位企业登录发布新岗位学生登录浏览岗位并投递简历企业审核报名学生签到打卡企业发起结算最终学生端看到工资到账。整个过程一条线走下来观看者很快就能理解系统价值。录制前先把数据库重置到初始状态把示例账号、示例岗位、示例报名数据都准备好。录的时候每个页面切换稍微停两秒别让鼠标飞点观看者根本跟不上。视频分辨率至少1080p屏幕缩放比例调大一点。讲解视频不要逐行念代码。我最推荐的模式是先讲项目整体架构图再挑三个核心技术点展开比如JWT拦截器、文件上传、Android网络封装。每个技术点用“为什么用、怎么实现、踩过什么坑”三段式来讲。这样既有深度又不至于变成代码阅读课。录制工具用常见的录屏软件就可以重点是声音清晰、节奏稳定不用追特效。6.4 演示数据准备演示数据直接决定演示效果。一套好的初始数据要覆盖各种状态。账号方面我给你一个常用组合账号密码角色adminadmin123管理员enterprise1123456企业student1123456学生岗位数据至少准备六到八条覆盖按时计费、按月计费、按次计费三种薪资类型。学生报名数据要尽量覆盖“待审核、已通过、已拒绝、已完成”四种状态只放一种状态会让演示过程显得单薄。考勤记录最好覆盖最近两周这样结算工资时有据可查。演示最忌讳的事是临时造数据造到一半发现关联记录不完整。我一般会在录视频前把整个操作流程先走两遍然后把数据库恢复到初始脚本状态再正式录制。数据脚本里放什么内容、哪些岗位对应哪些报名记录都可以在文档附录里列出来观看者拿到后能完整复现。这套项目整体过下来花时间最多的其实不是接口本身而是状态流转、文件上传、Token鉴权这些联调细节。我自己做完后的最大体会是像勤工助学这类系统业务并不复杂但正是因为它牵扯到学生、企业、管理员三套角色又有报名、考勤、结算一系列状态才能把SpringBoot和Android两端需要掌握的知识点串起来。如果你准备拿这个课题去答辩我建议不要只停留在把代码跑通的层面而是把业务线完整演示一遍再准备两三个场景问自己重复报名怎么办、工资结算怎么防错、文件上传怎么防不安全这些问题基本都能问到点上。最后分享一个实用小技巧开发时把Android端的BaseURL单独放到一个常量类里切换模拟器和真机只需要改一行真正演示之前再全局搜一遍确保没有把本机调试地址留在发布包里。这些小细节往往才是让一个毕业设计从“能跑”升级到“经得起问”的关键。