☰
SpringBoot+Android大学生勤工助学管理系统开发实战
2026/9/26 13:29:59 网站建设 项目流程

1. 这个系统到底要解决什么问题

先说结论:大学生勤工助学管理系统,本质上就是个多端协同的业务系统。学生要找岗位、要打卡工时、要看工资;老师要发岗位、要审批申请、要核对结算;管理员要管用户、管数据、管整个流程。三个角色在一个系统里协作,牵扯到信息发布、流程审批、数据统计、权限隔离——这些东西如果靠纸质表格或微信群,基本就是灾难。

我当时接到这个项目的时候,第一反应是:这不就是一个“校园版BOSS直聘+考勤打卡+工资结算”的结合体吗?想清楚这一点,后面的技术选型和功能设计就顺了。

整个项目的核心需求拆出来就三条:

  • 信息流的匹配:岗位发布→学生浏览→投递申请→老师审核,这是主干流程
  • 工时的闭环:上岗签到→工时登记→考勤确认→工资核算,这是数据底座
  • 权限的分层:学生、教师/用工单位、系统管理员三类角色,各自只能看到和自己相关的数据

适合谁参考?两条线:一是要做毕业设计的学生,这个题目的完整度和技术栈覆盖面刚好卡在“能体现工作量、又不至于失控”的范围;二是学校里真的要做勤工助学管理信息化的老师或技术团队,本文的架构和部分设计可以直接抄作业。

而我自己在做这个项目时最深刻的体会是:勤工助学系统的难点不在技术,而在“流程状态”的建模。申请、审批、上岗、签到、结算,每个节点都有状态流转,状态之间还有边界情况——比如学生申请了岗位但没去报到,老师发布了岗位但一直没人申请,这些都要在系统里兜住。状态建模做得清楚,代码写起来就是水到渠成;状态建模模糊,后续每个功能都会返工。

2. 技术选型:为什么是SpringBoot+Android原生

2.1 后端选型的核心考量

后端选择SpringBoot,这个不用多说,Java生态里做管理类系统最成熟、资料最多的框架,没有之一。但要注意的是版本选择——我见过太多人一上来就拉最新的SpringBoot 3.x,结果各种兼容问题。

SpringBoot 3.x要求JDK 17+,如果你的部署环境是JDK 8,就直接劝退。我当时用的是SpringBoot 2.7.x + JDK 8 + MyBatis-Plus 3.5.x,这个组合经过了大量生产验证,网上搜问题一搜一个准,踩坑成本极低。

有同学问为什么不直接用SpringCloud那一套?答案很简单:勤工助学系统属于典型的单体应用,用户量级撑死几千人同时在线,分布式架构带来的复杂度远超收益。单体应用配合合理的模块划分,开发效率最高,部署也最简单。

ORM选型我直接用了MyBatis-Plus。为什么不用JPA?说实话JPA的自动建表和对象关系映射在简单场景下确实省事,但勤工助学系统里有大量SQL涉及多表关联、分组聚合统计——比如“按月统计各岗位工时汇总”“按学院统计学生参与人数”,这种报表类查询用MP的条件构造器加自定义SQL,写起来比JPA的Specification和QueryDSL顺手多了。

2.2 Android端为什么选择原生开发

客户端这块,标题里直接写了Android,那就要考虑是原生还是跨平台。我当时纠结过UniApp,因为一套代码能同时出安卓和iOS,想省事。但最后还是选了Android原生,原因有三:

第一,这系统涉及大量原生交互:拍照上传证明材料、扫码签到、定位打卡、文件下载(导入导出Excel),这些功能在UniApp里要么需要写原生插件,要么性能有损耗,绕了一圈还是绕回原生。

第二,音视频、摄像头、定位、文件读写这些底层能力的调试,Android Studio原生环境下最直观,有问题直接看日志就能定位。

第三,这个项目是教学和毕设场景,原生Android更能体现你的技术深度——Activity/Fragment生命周期、Adapter的复用机制、权限动态申请、网络框架封装,这些在面试时都是加分项。

Android端技术栈:Java + Retrofit2 + OkHttp3 + Gson + RxJava(后来换成协程的同学可以平替)+ Glide + LitePal或Room。不用太花哨,够用就行。

2.3 数据库设计:先想清楚表结构再写代码

勤工助学系统的表结构,核心就这八张:

  • t_user:用户表,角色字段区分学生/教师/管理员
  • t_position:岗位表,关联用工单位(教师)、岗位类型、招聘人数、薪资标准
  • t_application:申请表,记录学生投递状态(待审核/通过/拒绝)
  • t_work_log:工时表,记录学生每次打卡的起止时间和时长
  • t_salary:结算表,按月汇总每个学生的工时和工资
  • t_leave:请假表,学生请假信息
  • t_notice:公告表,系统通知和岗位公告
  • t_file:附件表,保存学生上传的证明材料

其中最关键的是t_work_log的设计。我见过不少系统把工时表做得特别简单,就记一个日期和一个时长,结果到月底对账的时候根本算不清——学生哪天该来没来?迟到早退怎么扣?加班时长怎么算?这些在表结构里必须有字段承接。我的设计里有work_date、start_time、end_time、actual_hours、status(正常/迟到/缺勤),再加一个audit_status(待确认/已确认),由用工老师在月末统一确认考勤,确认后工时进入工资结算流程。

岗位表也一样,要留字段记录上下限:min_hours和max_hours,表示这个岗位每周要求的最低和最高工时。申请通过之后,学生的工时记录必须落在这个范围内,超出的部分不结算或者按加班价结算——这个规则在业务层判断。

数据库设计这事,一定要在写代码之前想清楚。你后端Controller写得再好,表结构有问题也是白搭。我当时就是先画ER图、再敲定字段、最后才开工写代码的,后面开发过程中几乎没有因为改表导致的大重构。

3. 后端SpringBoot核心功能实现

3.1 登录认证与权限控制的落地

勤工助学系统有三个角色,权限模型不复杂,但一定要做干净。我用的是JWT + 拦截器的方式,没有引入Spring Security那套重家伙——说实话,三个角色的简单权限控制,引入Security反而让学习成本变高,配置一堆东西就为了几个接口的放行和拦截,划不来。

JWT流程是这样的:

  • 用户登录成功后,后端生成Token,里面塞入userId和role两个关键信息
  • 客户端每次请求在Header里带上Authorization: Bearer <token>
  • 后端拦截器解析Token,把userId和role塞进ThreadLocal,供Controller和Service层使用

拦截器里做三件事:放行登录接口和静态资源路径,校验Token合法性,校验接口角色权限。我用的是自定义注解@RequireRole,标注在Controller方法上,拦截器里读取注解并和Token里的角色比对,不匹配直接返回403。

注意:JWT的过期时间不要设太长。我一开始设了7天,后来发现有学生手机丢了,Token被别人拿去乱用,出了点小麻烦。后面改成2小时过期、客户端维护一个refreshToken做无感刷新的方案,才算踏实。

密码存储用BCrypt加密,不要用MD5——MD5彩虹表破解成本太低,这在生产环境上是硬伤。

// 登录认证核心代码(简化版) public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } // 放行登录接口 if (request.getRequestURI().contains("/api/auth/")) { return true; } String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { response.setStatus(401); return false; } Claims claims = JwtUtil.parseToken(token.replace("Bearer ", "")); // 校验角色权限 HandlerMethod handlerMethod = (HandlerMethod) handler; RequireRole requireRole = handlerMethod.getMethodAnnotation(RequireRole.class); if (requireRole != null && !requireRole.value().equals(claims.get("role"))) { response.setStatus(403); return false; } UserContext.set(claims); return true; } }

3.2 岗位发布、申请审批的状态机设计

岗位的整个生命周期,状态流转是核心。我定义了六个状态:

  • 草稿(0):老师创建但未发布
  • 招聘中(1):已发布,学生可见可申请
  • 已满员(2):招聘人数够了,停止接收申请
  • 进行中(3):学生已经上岗,正在执行
  • 已结束(4):岗位到期或完成
  • 已取消(5):异常情况终止

状态流转不是乱来的,必须遵守规则。比如“招聘中”才能接收申请,“已满员”不能转成“进行中”除非有人退出。我把这些规则封装在PositionService里,用状态机模式管理,避免在每个业务方法里写if-else判断导致逻辑分散。

学生申请岗位的状态,则是四个:

  • 待审核(0)
  • 已通过(1)
  • 已拒绝(2)
  • 已撤回(3)

这里有一个容易被忽略的业务点:学生申请通过后,如果最终没有上岗(比如没去报到),后台要有个“失效”动作把申请记录标记掉,否则列表里会残留一条“已通过但并未上岗”的脏数据,影响统计报表。

审批流程我用了异步消息通知——学生提交申请、老师审核通过,这两个节点都触发站内消息通知。最开始用的是同步推送,结果高峰期老师批量审核几十个申请的时候,接口响应明显变慢。后来换成了SpringBoot自带的@Async异步处理,体验大幅改善。

3.3 工时打卡与工资结算的算法设计

工时打卡是学生端用得最多的功能。学生到达用工地点后,点击“签到”按钮,后端记录当前时间和定位信息(可选)。离开时点击“签退”,后端根据起止时间计算出工时。

这里有个细节:计算工时要考虑最小时间单位。比如某个岗位规定“不足30分钟不计入工时”,那你在计算的时候要做溢出处理。我的实现是:

public double calculateHours(Date startTime, Date endTime) { long diffMillis = endTime.getTime() - startTime.getTime(); double hours = diffMillis / 3600000.0; // 不足30分钟按30分钟计 BigDecimal bd = new BigDecimal(hours); return bd.setScale(1, RoundingMode.FLOOR).doubleValue(); }

工资结算按自然月进行。每月1号凌晨,定时任务跑批:

  • 查询上个月所有已确认(考勤通过)的工时记录
  • 按学生ID分组,汇总总工时
  • 乘以岗位单价,得到应发工资
  • 生成结算单,状态为“待发放”
  • 管理员确认后,状态变为“已发放”

这里要特别注意:结算逻辑务必做成“可重复执行”的幂等操作。因为定时任务可能重跑,如果每次跑都新增结算单,数据就翻倍了。我的方案是:在t_salary表设置唯一索引(student_id + month),插入时用ON DUPLICATE KEY UPDATE,重跑任务只会更新数据,不会新增记录。

3.4 SpringBoot全局过滤器处理XSS攻击

这个问题我在开发过程中碰到过——学生在“意见反馈”功能里输入了一段带<script>标签的内容,结果页面直接弹窗了。这就是典型的XSS攻击。

勤工助学系统里有大量文本输入场景:个人简介、岗位描述、申请理由、公告内容。如果不做XSS防护,恶意脚本一旦入库,所有看到这个内容的用户都会中招——尤其是管理员端浏览器打开公告页面的时候,一弹一个准。

我采用的方案是全局过滤器,在请求进入Controller之前统一处理:

public class XssFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; // 包装请求,对Parameter、Header、Body中的内容进行转义 XssWrapper xssWrapper = new XssWrapper(httpRequest); chain.doFilter(xssWrapper, response); } }

核心思想:继承HttpServletRequestWrapper,重写getParameter、getHeader和getInputStream方法,将<script>、javascript:等内容进行HTML转义。同时要注意白名单配置——富文本编辑器允许的标签(如<b>、<i>、<a>)不要一刀切转义,否则老师发布岗位描述的时候加粗效果全没了。

这里有一个“坑”要强调:XSS防护只管入站数据,如果数据库里已经有了脏数据,过滤器是救不回来的。上线前务必写一个脚本把库里的<script等危险字符串清洗一遍,或者通过管理端增加一个“数据清洗”按钮,一次性处理存量数据。

3.5 MyBatis-Plus分页与多表关联查询

列表页是管理系统的门面——岗位列表、学生列表、结算列表、申请列表,清一色的分页查询。这里一定要用MyBatis-Plus的分页插件,不要自己在SQL里写LIMIT+COUNT两条语句来回传参,那是上古时期的玩法了。

分页插件的配置很简单:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

然后Service里直接:

Page<PositionVo> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Position> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Position::getStatus, 1).orderByDesc(Position::getCreateTime); IPage<PositionVo> result = positionMapper.selectPositionPage(page, wrapper);

特别提醒:如果分页对象里需要关联其他表的数据(比如岗位列表要显示用工单位名称),建议在Mapper里写自定义SQL,用LEFT JOIN一次性查出来,然后用@Select注解或XML映射到VO对象。不要用循环查询——那样数据量一上来就爆发N+1问题,接口直接卡死。

4. Android客户端的核心实现与踩坑记录

4.1 项目环境准备与基本结构

Android端我用Android Studio开发,Gradle版本选了个兼容性良好的组合:Android Studio 4.2+(新版下载装好就行,中文设置在两年前的版本里集成在设置里,现在新用户一般装默认英文,关系不大)+ Gradle 6.7.1 + 编译SDK 30 + 最低支持SDK 21(Android 5.0)。这个组合覆盖了绝大多数在校学生的手机,不用考虑新版本适配(Android 16适配那是后话,等系统真正普及再说——但目标SDK已经要求31+,上架应用商店时记得把targetSdkVersion调高)。

项目分包结构:

  • activity:启动页、登录页、主界面、岗位列表、岗位详情、个人中心
  • adapter:各类RecyclerView的Adapter(注意使用ListAdapter或DiffUtil优化刷新效率)
  • api:Retrofit请求接口定义、统一响应体、拦截器
  • model:实体类和数据Bean
  • utils:工具类(日期、加密、存储)
  • view:自定义View(Banner轮播、进度条等)

4.2 Retrofit封装与统一请求处理

Android端网络请求,我直接选了Retrofit2 + OkHttp。这套组合在Java开发Android的圈子里属于标配,资料多、坑少、扩展性强。

关键的封装逻辑是统一处理三类情况:

  • 网络异常(超时、断网):Toast提示“网络连接失败”,不崩溃
  • 业务异常(业务码非200):弹出后端返回的错误消息
  • Token失效(HTTP 401):拦截器里清除本地Token,跳转登录页
public class TokenInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request original = chain.request(); String token = SPUtils.getToken(); Request request = original.newBuilder() .header("Authorization", "Bearer " + token) .build(); Response response = chain.proceed(request); if (response.code() == 401) { // 清除Token 重新登录 SPUtils.clear(); } return response; } }

BaseUrl的切换也要注意。开发时用局域网地址(比如http://192.168.1.100:8080),测试时用模拟器地址(10.0.2.2),上线时用域名。我建议把BaseUrl写在一个Constants类里,并且在BuildConfig里区分debug和release——这样切换环境只需要改Gradle配置,不用动代码。

关于定位打卡的权限动态申请,Android 6.0以上都需要运行时权限。勤工助学系统用到的主要权限有:定位权限(签到打卡)、相机权限(拍证明材料)、存储权限(下载附件)、通知权限(推送公告)。建议针对每个权限做理由说明的弹窗,不要一上来直接申请一大坨——有学生拒绝后整个功能用不了,排查了大半天。

4.3 下拉刷新、加载更多与进度条

岗位列表页是学生使用频率最高的页面。这里有两个体验细节非常重要:

一个是下拉刷新。用SwipeRefreshLayout包住RecyclerView,刷新的时候重新拉取第一页数据。要注意的是:下拉刷新的网络请求和滚动加载更多的请求不能冲突——我的做法是设置一个isLoading布尔值,请求没完成前禁止再次触发刷新或加载。

另一个是“加载更多”的滑动分页。滚动到底部时自动加载下一页,同时显示一个“正在加载更多”的底部View。这里有个细节:当加载完成后数据为空时,要显示“没有更多了”而不是继续无限请求。我的处理方式是:当返回的list.size() < pageSize时,判定为没有更多数据,把底部的状态切换到“没有更多了”。

类似这种列表页面的加载状态,Android上有StatefulLayout开源库可以直接用——加载中、空数据、错误、正常内容四种状态。手动写虽然也就几十行,但状态切换极容易漏——边界情况一多,最容易出Bug。用现成的库不丢人。

4.4 协调布局+Banner:首页聚合展示的实现细节

首页不建议做成一堆文字列表硬堆。我的方案是CoordinatorLayout + AppBarLayout + 顶部Banner轮播图,配合TabLayout切换多个Tab页(最新岗位、热门企业、公告),整体观感清爽,信息层次也清晰。

Banner轮播图这里有两个容易踩的坑:

第一,图片加载用Glide,不要在RecyclerView的Adapter里直接用Glide.with(context).load(url).into(holder.imageView)完事——要处理图片加载失败时的占位图和圆角裁剪。占位图很重要,否则弱网环境下图片加载慢,用户盯着空白卡片,体验很差。

第二,轮播图自动轮播的定时器用Handler.postDelayed实现时,一定记得在onPause生命周期中移除回调,否则页面不可见时还在切图,不仅浪费资源,还有可能引发内存泄漏。

4.5 客户端缓存与离线数据保留

勤工助学系统的学生用户,经常在宿舍、教室、食堂不同WiFi环境下切换,网络并不是时刻稳定。我的方案是给岗位列表和公告列表做本地缓存:

  • 用SQLite(Room)存最近一次拉取的数据,页面加载时先读缓存展示,再发网络请求更新
  • 用SharedPreferences存用户的登录状态和基本信息
  • 用File存储下载的附件(Excel工资表、岗位说明书等)

这个设计在弱网环境下效果显著,实测下来页面打开速度从“转圈3秒”变成“先展示再刷新”,体感流畅很多。

4.6 拍照、选择图片与文件Uri适配

学生申请岗位时往往需要提交证明材料——比如家庭经济困难证明的拍照件。Android端需要调用相机或相册,这里在Android 7.0+必须用FileProvider,否则拍完照直接闪退。

FileProvider配置示例(在AndroidManifest里注册provider,在xml里配置paths):

<provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider>

注意authorities的值要写${applicationId}.fileprovider,不同渠道包会自动替换成对应的包名。有同学在这里踩过坑:包名改了没有同步改provider配置,导致相册选图后闪退。

另外,从相册选择图片时,不同机型返回的Uri格式不一样——有的返回content://media/...,有的返回content://com.tencent.wework.fileprovider/...。写兼容代码时要统一走ContentResolver读取流,不要直接拼文件路径,否则老版本的Android会直接崩。

4.7 Android项目移植与自定义混淆配置

Android项目在不同机器上协作开发的时候,最怕Gradle版本不一致导致的构建问题。搜索“移植android studio项目”时常见的问题场景是:从GitLab拉下来代码,sync的时候Gradle版本对不上,或者SDK版本不对。

我的建议是:项目里的gradle-wrapper.properties指定Gradle版本(distributionUrl),build.gradle里指定SDK编译版本,这两处确认清楚,基本不会有大问题。

关于混淆——如果要发布Release包,ProGuard或R8的混淆规则要配置好。有两个关键点:

第一,混淆字典设置不了时,检查build.gradle里是否配置了proguardFiles和optimizationPasses。如果自定义-obfuscationdictionary失效,多半是因为你的minifyEnabled没有开——这就是搜索“android自定义混淆字典无效”这个问题的标准答案。

第二,要保留一些类和成员的映射关系——凡是Retrofit的接口、数据Bean(模型类)、使用了Gson的字段,都要加@Keep注解或配置keep规则,否则Release包运行时反射拿不到字段,直接抛异常。

写完代码千万别忘了跑一遍Release包的回归测试——我第一版混淆规则漏了模型类的keep,发布后学生端登录就闪退,排查了一天才发现是Gson反序列化所有字段变成了null。

5. 部署上线与项目管理实战

5.1 数据库脚本与初始化数据处理

上线前有一件事特别容易漏:初始化数据准备不充分。勤工助学系统至少需要以下初始数据:

  • 管理员账号(默认1个)
  • 学院列表(用于学生注册时选择)
  • 岗位类型字典(家教类、行政助理类、实验室助理类、图书管理员类等)
  • 薪资标准字典(按小时计价或按月计价)

这些数据的插入脚本,建议写在schema.sql的后面一段,用INSERT INTO语句。不要跟我一样,系统快上线了才想起来没有学院基础数据,注册页面下拉框空空如也。

5.2 SpringBoot的Docker化部署

后端服务我最后用Docker部署在了学校的服务器上。写一个Dockerfile:

FROM openjdk:8-jre-alpine WORKDIR /app COPY target/hardwork-system.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar", "--spring.profiles.active=prod"]

这里有一个我踩过的大坑:SpringBoot的配置文件(application.yml)不要打进镜像里,用-v挂载外部配置文件。这样后续调整数据库连接、调整端口,不需要重新构建镜像,直接改宿主机的配置文件再重启容器就行。

Docker启动命令:

docker run -d --name hardwork-server \ -p 8080:8080 \ -v /opt/hardwork/config/application.yml:/app/application.yml \ -v /opt/hardwork/logs:/app/logs \ --restart=always \ hardwork-system:1.0.0

这样部署的优势是:回滚快(旧版本镜像还在),资源隔离好,迁移服务器只要把镜像导过去就行。

5.3 Android APK的持续构建与签名

Android客户端发布时,直接导出Release APK,签名用的是JKS文件。签名这事要特别注意:项目里要有一个keystore.properties文件保存签名信息,并且这个文件必须从版本库里排除(加进.gitignore)。我见过有同学把签名文件和密码直接推到GitHub公共仓库,等于把自家大门的钥匙挂在了大街上。

输出路径建议直接命名成hardwork-system-v1.0.0-release.apk,并在文件名里带版本号。打出来的包,交给学院信息中心的老师安装测试之前,先在模拟器上跑一遍最小回归:登录→刷列表→看详情→提交申请→退出登录,确保主流程没有明显问题。

5.4 日志、监控与后期运维

上线不是终点,运维才是重头戏。这里分享一下我的实操配置:

日志框架用Logback,输出到文件,按天滚动。关键日志要分三个文件:

  • error.log:只有ERROR级别,报警用
  • biz.log:业务操作日志(谁在什么时候申请了什么岗位)
  • access.log:接口访问日志(哪个IP在什么时间调用了哪些接口)

监控方面,SpringBoot接入Spring Boot Admin,能够直观看到内存、线程、HTTP接口的调用情况。小型项目不需要搞什么链路追踪和Prometheus那套,扛不住也不需要。

学生用户反馈的问题里,最常见的是“App更新了,但手机没收到新版本提示”。这里建议做一个简单的版本更新机制:后端维护一个版本号接口,客户端启动时拉取对比,不一致时弹窗提示去下载新APK。有了这个,再也不用靠辅导员在群里发安装包链接了。

6. 常见问题排查与操作细节记录

6.1 后端高频问题速查

问题现象可能原因解决方案
前端请求一直转圈,日志无输出端口未开放/防火墙拦截检查服务器防火墙和安全组规则,放行8080端口
接口报错“Bad SQL Grammar”SQL语法错误或字段名不匹配打开MyBatis-Plus的SQL日志输出,检查生成的SQL语句
数据库连接超时MySQL连接被空闲回收配置HikariCP的connection-timeout和max-lifetime参数
定时任务不执行未开启@EnableScheduling检查启动类是否添加了@EnableScheduling注解
中文乱码数据库连接URL缺少字符集参数JDBC URL加上characterEncoding=utf8
跨域请求被拦截CORS配置缺失全局配置CorsFilter,允许指定来源

这里重点说说“接口请求404但Controller存在”这个诡异问题。有一次我后端接口明明写在Controller里,但Android端一直请求404,排查了两个小时发现是路径映射问题——Controller类上的@RequestMapping写的是/api/admin,方法上的@PostMapping写的是/createPosition,但前端Retrofit里拼接的URL多了一个斜杠,变成了/api/admin//createPosition,SpringBoot把双斜杠路径认为是不存在的资源,直接404。

解决方案:统一在Retrofit的BaseUrl末尾不加斜杠,接口注解里路径开头也不加斜杠,两边保持一致,并且用工具类做路径拼接检查。

6.2 Android端高频问题速查

问题现象可能原因解决方案
Android Studio构建卡在Gradle syncGradle版本不匹配/网络问题检查gradle-wrapper.properties,换成稳定版本;配置阿里云镜像仓库
相机拍照后闪退没有配置FileProviderAndroidManifest里注册FileProvider,按上面示例配置xml路径
请求一直报401Token过期/Token未携带检查拦截器是否在请求头中加了Authorization字段
图片加载不出来后台返回的图片地址是局域网IP,外网不可达部署时把图片域名配置成正式域名,客户端BaseUrl统一切换
点击“加载更多”没反应pageNum和pageSize参数传递有误检查请求参数是否每页都传了正确的页码,返回数据是否为空列表时需要终止加载

6.3 打Release包后一些“反直觉”的坑

Release包和Debug包行为不一样,这是Android开发的经典现象。具体到本项目:

第一个坑:网络请求在Release包下全部失败。原因是Android 9.0(API 28)开始默认禁止明文HTTP流量。Debug包还能访问,Release包直接拦截。解决方案:在AndroidManifest.xml里声明usesCleartextTraffic="true",或者在网络安全配置里单独允许某个域名使用明文HTTP。我当时就是因为这个,发布出去的包在真机上一整个模块都打不开,后来查了半天的logcat才定位到是网络安全策略的问题。

第二个坑:混淆把数据类字段名改了。Gson的反序列化靠反射读取字段名,如果你的数据Bean没有配置@SerializedName注解,混淆之后字段名全变了,JSON解析全部失败。此事无解,只能靠KEEP规则解决——配置文件里把model包下的所有类keep住。

第三个坑:日志系统级别在Release包默认是INFO以上,你Debug时输出的日志在Release包里全看不见。遇到线上问题排查时,要么把日志级别调到DEBUG(不推荐,性能损耗),要么靠Upload后的崩溃日志定位Crash位置。

6.4 涉及移动端文件访问的“恐怖”问题

搜索热词里那个content://com.tencent.wework.fileprovider/external_path/android/data/com...的报错,大家如果正在做“用App读取下载目录或者分享文件”的功能,大概率会遇到。

这个问题本质上是Android 11(API 30)的包可见性和数据目录访问限制导致的。当你在App里使用系统的文件选择器选择文件时,有些第三方的文件选择器会向你返回一个content://格式的Uri,但这个Uri的来源是其他应用(比如企业微信、QQ)的内容提供者。你拿着这个Uri去访问文件时,如果没有对应的授权或你的App没有权限访问该ContentProvider,就会直接报SecurityException。

解决思路很简单:不要自己去解析Uri获取文件路径,而是统一用ContentResolver.openInputStream(uri)读取流。同时,把清单文件里queries标签加上对常见文件选择器的声明,或者直接申请系统全部文件的访问权限(MANAGE_EXTERNAL_STORAGE,但是这个权限审核很严格,慎用)。

7. 写在最后的个人经验总结

这个项目从头到尾做下来,我最大的收获是:技术选型永远不要赶时髦,能解决问题、能稳定运行、能维护得了的技术栈,才是好技术栈。SpringBoot 2.7 + Android原生Java + MySQL 8.0,这套组合放到今天依然非常扎实。

另外分享几个实际运行中摸索出来的细节经验,供你们参考。

第一,勤工助学系统的工资结算模块,一定要做成“可分账期、可追溯、可重跑”的设计。学校财务老师会反复核对数据,如果结算数据不能重新生成,后续调整岗位单价或者补录工时时会很被动。

第二,Android端的全局异常处理要做好。网络请求的回调里,只要是失败状态——业务码非200、HTTP错误、解析异常——都要有兜底逻辑,否则用户看到的不是“数据加载失败,请下拉重试”,而是“App已停止运行”,这对口碑的伤害是致命的。

第三,代码写好之后,一定要抽时间把项目的架构图和数据库ER图整理出来。这不仅是毕业答辩的必备材料,也是后续你自己接手维护的最大捷径。我后来看自己写过的代码,经常需要先看图,再进代码,不然真的会迷路。

最后,关于勤工助学系统,后续如果想继续扩展,可以考虑的方向有:微信公众号对接通知、钉钉工作通知、数据大屏实时展示统计结果、移动端人脸识别打卡,以及Excel工资表自动导出。骨架搭好了,扩展只是时间和精力的问题。

以上是我的全部实操记录,希望对正在做这个项目或者类似管理系统的人有帮助。如果你们在过程中遇到代码层面说不清楚的问题,欢迎在评论区聊。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询