首页 / 资讯中心 / 文章详情

Spring Boot+Vue+Android三端联动:在线学习作业提交平台开发实战

Spring Boot+Vue+Android三端联动:在线学习作业提交平台开发实战 ★ FEATURED ARTICLE
直接说结论这个标题一眼看过去就知道是那种典型的教育信息化毕业设计或者课程项目但我更愿意把它当成一个三端联动的企业级开发练手项目来看待。Spring Boot作后端、Vue 3作Web管理端、Android(Android Studio)作学生端再加一个在线学习作业提交的核心业务这个组合覆盖了目前中小型业务系统最常用、也最值得掌握的整套技术链路。这篇文章我会按照实际操作顺序把系统设计、数据库建模、后端接口、管理端页面、安卓端开发一个个拆开讲重点放在为什么要这么设计和实操中到底会遇到哪些坑上保证你跟着思路走完就能有自己的完整落地版本。1. 项目整体思路与技术选型1.1 这个项目到底解决什么问题在线学习作业提交平台先别急着把它想复杂。拆开来看无非是三个核心痛点第一老师布置了作业纸质版收起来麻烦、批改后反馈慢第二学生的作业成果文档、图片、代码压缩包没有统一的提交入口微信群里乱传容易丢第三学习资料和课件散落在各个地方学生课后找资源很费劲。这个平台要做的就是把课程内容展示—作业发布—作业提交—批改反馈这条链路全部线上化。所以这个系统的主角其实是两类人学生和管理端角色比如老师或助教。学生通过安卓端完成登录后能看到自己选修的课程、下载课件资源、查看待做作业、在线提交作业附件管理端则通过浏览器Vue 3页面完成课程管理、作业发布、查看学生提交列表、下载作业文件。当然再往下细化还可以加公告、成绩统计、消息提醒这些模块但主线永远是作业这个核心对象。1.2 为什么选择三端分离结构有人会问既然有Web端了为什么还要单独做一个安卓App直接用浏览器不好吗这个问题在技术选型时确实值得认真思考。实际原因往往是使用场景驱动的学生每天拿着手机很多作业是需要拍照上传的比如手写演算过程、纸质实验报告比起在浏览器里打开网址、调用相机上传原生App在文件选择、相机调用、通知提醒上的体验要稳定得多。而管理端停留在浏览器是因为课程管理和作业批改需要在电脑上操作看文件、录入评分、批量导出都更方便。从技术上来说这种后端API Web管理端 移动端的三端模式也正是目前工业界最标准的分工方式。后端只负责提供JSON接口不关心谁来调用Web端和安卓端各自独立开发、独立部署。这样一来做安卓端的同学不需要懂后端细节做后端的同学也不用操心界面样式只要能保证接口契约统一三条开发线就可以并行推进。实际做项目时这种拆分方式对排期和协作都友好得多。1.3 技术栈选型的理由与取舍技术框架选型最重要的是挑一批自己熟练、生态成熟、遇到问题搜得到答案的组合而不是追求新。Spring Boot当前Java后端开发的事实标准。内置Tomcat自动配置大大减少了XML配置的工作量加上Spring家族强大的整合能力Spring Security、Spring Data JPA/MyBatis做教育类业务系统效率很高。版本我建议直接用Spring Boot 2.7.x或3.x注意3.x要求JDK 17如果本机是JDK 8就老老实实用2.7.x避免环境折腾。MyBatis-Plus持久层我用得最多的是MyBatis-Plus而非原生MyBatis或JPA。理由是它的通用Mapper和条件构造器能省下大量单表CRUD代码但又不像JPA那样封装过度复杂SQL依然可以手写。对作业提交这类业务来说多表关联查询不算特别复杂MyBatis-Plus足够覆盖了。Vue 3 Element PlusWeb管理端选Vue 3的理由很直接——Composition API对逻辑复用友好配合Element Plus组件库能快速搭出后台管理界面。Vite作为构建工具比Webpack快非常多HMR体验极其舒适。如果之前只会Vue 2学Vue 3其实没什么心智负担核心差异主要是setup语法和响应式API的写法。Android原生Kotlin/Java Android Studio既然标题里写了基于Android Studio那端上基本就走原生开发。语言方面我比较推荐Kotlin现代Android开发已经全面转向Kotlin协程处理异步比Java的回调舒服很多。如果对Kotlin不熟用Java写也可以思路完全一致谈不上哪个绝对好。MySQL Redis主数据库用MySQL存储课程、作业、用户、提交记录等结构化数据。Redis不是必须的但如果要做登录会话集中管理、热点课程访问量统计加一个Redis会更顺手。这里提醒一句Redis只是增强方案初学者不做完全OK别让架构复杂度拖垮开发进度。技术选型有一条朴素的原则你的时间比炫技重要。这个项目里所有技术都是被大规模验证过的做出来的东西既能在答辩/展示时有说服力又能在日常工作里真正复用这是选这套组合最大的底气。2. 系统架构与核心模块设计2.1 整体架构的分层思路在真正动手写代码之前先把架构图在脑子里画清楚。整个系统可以拆成四层展示层包含Vue 3管理端和Android客户端负责页面渲染和用户交互向服务端发起HTTP请求。接口层Controller层接收前端请求、做参数校验、调用业务服务、统一包装返回结果常用的统一返回类是ResultT包含code、message、data三个字段。业务逻辑层Service层核心业务都在这一层比如课程查询、作业创建、作业提交记录的插入与状态流转。数据访问层DAO/Mapper层通过MyBatis-Plus操作MySQL表。模块划分上后端代码按业务包来组织。我习惯用controller、service、mapper、entity、dto、config、common这几个顶层包每个包内再按业务模块user、course、homework、submission细分。这样包的层次清晰后续即使要加考勤、成绩统计之类的模块也不会把代码搅成一锅粥。2.2 数据库表设计围绕作业展开数据库设计是整个系统里最影响后续开发效率的一环。我建议在设计阶段投入足够时间因为表结构一旦定了后面改起来牵扯接口、页面、App联动代价很大。核心表我通常设计六张用户表t_userid、username、passwordBCrypt加密后存储、real_name、role取值STUDENT或TEACHER也可以叫ADMIN、avatar、phone、create_time。角色字段极其重要几乎每个业务接口都要判断当前登录用户是学生还是管理端所以这张表就是权限的根。课程表t_courseid、course_name、course_code、teacher_id关联用户表、description、cover_url、create_time。课程和学生之间是多对多关系需要一张中间表。选课表t_student_courseid、student_id、course_id、select_time。一张典型的关联表用来支撑我的课程列表查询。作业表t_homeworkid、course_id、title、content、deadline、attachment_url老师发布的作业说明文件、create_time。作业表是核心业务表字段设计时要注意deadline既能用来在安卓端展示剩余时间这种倒计时效果又要后端正则校验过期后不能提交。作业提交表t_submissionid、homework_id、student_id、submit_content文字备注、attachment_url学生提交的文件路径、submit_time、status取值已提交、已批改、已打回、score、teacher_comment。这张表解决的是谁在什么时候交了什么东西的问题也是整个平台的数据中枢。公告表t_noticeid、title、content、create_time、publisher_id。相对独立的模块有精力就做没精力可先砍掉。设计表的时候有几个容易被忽视的细节。第一个是时间字段统一用DATETIME而不是TIMESTAMP避免2038年问题和时区转换的麻烦第二个是金额/分数字段分数建议用DECIMAL(5,1)虽然作业分基本是整数但保留一位小数兼容平时分第三个是大字段作业描述content用TEXT类型不要硬塞进VARCHAR(255)第四个是逻辑删除给每张表加一个deleted字段0或1配合MyBatis-Plus的TableLogic实现逻辑删除避免物理删数据导致历史关联记录悬空。另外索引设计同样值得提前规划。t_submission表上需要建(homework_id, student_id)的联合唯一索引保证一个学生对同一份作业只能保留一条提交记录t_homework表在course_id上建普通索引查询某个课程的作业列表时才能快。索引这东西表数据量几百条时看不出区别等积累几千提交记录之后差别就非常明显了。3. 后端Spring Boot核心实现3.1 工程初始化与基础配置创建Spring Boot工程用IDEA的Spring Initializr最方便勾选Web、MyBatis-Plus如果是旧版Initializr没有MyBatis-Plus就手动加依赖、MySQL Driver、Lombok、Validation这几个依赖。非要我总结一个最小依赖清单大概是spring-boot-starter-web提供Web服务和内置Tomcatmybatis-plus-boot-starter持久层增强mysql-connector-java数据库驱动lombok省略getter/setter等样板代码jjwt或java-jwt生成和解析JWT Tokenspring-boot-starter-validation参数校验注解支持commons-fileupload或直接用Spring Boot内置的上传机制处理文件上传application.yml的配置要注意几个地方。数据源配置里serverTimezone设置成Asia/Shanghai否则日期时间会差8小时MyBatis-Plus配置里打开map-underscore-to-camel-case下划线转驼峰和log-impl开发期打印SQL上线前关掉servlet.multipart.max-file-size和max-request-size作业文件经常有几十兆的压缩包默认1MB肯定不够建议都设成50MB或100MB。有一个我在项目里必做的小配置日期格式化。在application.yml里配好Jackson的日期格式否则前端拿到的时间戳是一串秒数前端解析起来非常痛苦。spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT83.2 用户认证设计与JWT拦截器在线学习平台最常见的认证方案就是JWTJSON Web Token无状态、不需要服务端存Session天然适合前后端分离和移动端。流程是用户登录成功后后端根据用户信息一般是id和角色签发一个带有过期时间的Token返回给客户端客户端后续请求都在请求头Authorization: Bearer token中带上它后端通过拦截器解析Token、校验合法性再把用户信息放入上下文。我一般会写一个JwtInterceptor实现HandlerInterceptor在preHandle里取token并解析解析失败直接返回401状态码解析成功就把用户id和角色写入ThreadLocal或者用HttpServletRequest的attribute方便后续业务接口获取当前操作人。Token里放角色信息有个很好用的设计在Controller层通过自定义注解比如RequireRole(TEACHER)拦截器来实现权限控制。这样学生调用管理端接口时会直接被拦截不用在每个方法里手写if判断。JWT本身是个无状态协议所以有一个难以回避的痛点用户改了密码或者被封号后已经签发的Token还是有效的直到过期。解决办法有几种比如引入Redis存储Token黑名单或者把Token的有效期设短一些比如2小时。我个人偏好后端把用户状态字段status的校验做在拦截器里同时配合短Token refresh token机制既保证安全性又不让用户频繁重新登录。3.3 作业发布与提交接口核心业务逻辑后端最重要的四个接口大概是这几个创建作业管理端接收课程id、标题、内容、截止时间、附件文件等参数写入t_homework表同时把附件文件保存到服务器磁盘。PostMapping(/teacher/homework) public Result? createHomework(Valid RequestBody HomeworkDTO dto) { // 逻辑校验当前角色是老师 - 检查课程归属 - 保存附件 - 插入作业记录 return Result.success(); }这里我要重点说一个容易踩的坑不要直接用MultipartFile和JSON字段混在一个RequestBody里。文件上传要用multipart/form-data格式而JSON用application/json格式两者天然不兼容。常见的解法有两种一是把所有业务字段用RequestParam接收附件用MultipartFile接收缺点是参数多了很乱二是先把文件单独上传到一个通用上传接口拿到文件URL再用JSON提交业务数据时带上这个URL。我个人更推荐第二种文件上传接口独立出来不仅作业附件可以用课程封面图、学生头像也都能复用一下子解决所有文件上传需求。学生提交作业接收作业id、内容、附件文件先校验作业是否存在且未过截止时间再校验该学生是否选了这个课然后进行插入或更新因为一个学生同一份作业只能有一条提交记录第二次提交应该是更新。查看我的作业列表学生端输入当前用户id联表查询该学生所选课程的作业列表并且给每个作业标注状态未提交、已提交、已批改、已打回、提交时间、得分。这个接口通常配合截止时间在安卓端展示倒计时信息。批改作业管理端老师查看某份作业的提交详情录入分数和评语更新t_submission表的状态字段。业务层设计有个值得养成的习惯事务注解别乱用但该用一定要用。比如创建作业时如果保存附件记录成功了但插入作业记录失败了两边就产生了脏数据。解决办法是在Service方法上标注Transactional遇到任何异常自动回滚。3.4 文件存储与访问策略文件存储是作业提交平台绕不开的环节。我的做法是把上传目录配置在application.yml里向本地路径比如D:/upload或Linux服务器上的/var/upload保存文件文件名用UUID 原文件后缀重新生成避免中文文件名和重名问题。数据库里存的是相对路径或URL路径客户端通过一个/file/{path}的静态资源接口访问文件配合Spring Boot的ResourceHandler映射到本地磁盘目录。有一个安全性问题在很多项目里都被忽略作业附件不应该允许访问者随便下载。我通常会做一个专用的文件下载接口在接口内先校验当前登录用户是否有权限访问该文件是作业所属课程的学生或是老师/管理员校验通过后用InputStreamResource把文件流式写回。如果图省事直接走静态资源映射那别人只要拿到文件URL就能绕过登录下载这对作业系统来说是明显的隐私漏洞。大文件传输的进度问题在安卓端/Web端也很常见。前端的Axios和OkHttp都提供上传进度回调能把百分比展示到UI上。后端需要注意超过50MB的MultipartFile默认会被Tomcat拒绝吗不会Tomcat默认的单文件上限其实是1MB但我们已经在application.yml里把max-file-size调大了所以这一步不会出问题。至于大文件秒传、分片上传这些高端方案在这个项目里属于过度设计有精力可以做没精力完全跳过。4. Web管理端Vue 3实现要点4.1 工程初始化与项目结构Vue 3管理端用Vite构建一条命令npm create vitelatest admin-web -- --template vue就能建好基础工程。目录结构我习惯这样组织src/ api/ # 接口请求封装按模块拆分 assets/ # 静态资源 components/ # 公共组件文件上传组件、分页组件等 layout/ # 后台布局侧边栏、头部 router/ # 路由配置 store/ # Pinia状态管理用户信息、Token views/ # 页面登录、课程管理、作业管理、提交详情等 utils/ # axios实例、工具函数管理端依赖的第三方库没有太多Element Plus做UI组件库Pinia做状态管理Vue Router做路由Axios做HTTP请求。构建工具的体验真的比旧版Webpack时代舒服太多了代码改了页面秒刷新调试效率提升非常明显。4.2 登录态管理与权限路由管理端登录流程看着简单但细节很多。用户输入账号密码后调用登录接口拿到Token然后立刻调一次获取当前用户信息接口把用户角色、姓名存进Pinia。路由守卫根据用户的角色动态生成可访问路由管理员能看到所有菜单普通老师只能看到授课相关页面。实现方式可以是router.beforeEach里判断to.meta.requiresAuth和store.token是否存在不存在就重定向到/login。Axios封装上务必要写好请求拦截器和响应拦截器。请求拦截器负责从Pinia中取Token挂到请求头响应拦截器统一处理错误状态码——遇到401说明Token失效了这时应该清除本地用户状态并跳转登录页而不是让用户收到一个莫名其妙的报错。还有一个小细节Token要同时放到Pinia和localStorage。Pinia用于运行时读取localStorage用于页面刷新后恢复登录态。两者缺一不可只放Pinia刷新就丢只放localStorage每次启动要读一次也麻烦。4.3 作业管理页面的开发作业管理页面是整个Web端最核心的页面功能区可以拆成两个Tab作业列表、提交记录。作业列表页展示字段课程名称、作业标题、截止时间、发布状态未开始/进行中/已截止、附件下载按钮以及新增作业按钮。新增作业用Dialog弹出表单表单包含课程选择器下拉框数据来自课程接口、富文本输入框作业内容、时间选择器截止时间和上传组件作业附件。这里用Element Plus的el-upload组件配自定义http-request而不是默认的action上传这样才能方便地附加token请求头。提交记录页输入条件作业id或其他查询条件后表格展示所有学生对该作业的提交列表每条记录有提交时间、附件下载/在线预览按钮、未批改/已批改状态、得分与评语。点击批改按钮打开抽屉详情老师录入分数和评语后保存。开发这个页面时我最想提醒的一个坑是表格数据的分页参数命名要前后端对齐。后端如果用的是MyBatis-Plus的Page前端传参数一般是current和size但如果你自己拼SQL可能设计成pageNum和pageSize。这种不一致问题在前后端联调时反复出现建议一开始就固定参数名。另外el-table的el-pagination配置里current-change和size-change两个事件都要绑定否则切换每页条数后页码不会重置翻到第5页再切每页条数会变成空列表。4.4 文件上传与在线预览细节文件这块知识点密集值得单独拉出来说。Web管理端的上传部分el-upload可以设置accept限制上传类型比如作业附件允许.doc,.docx,.pdf,.zip,.rar,.png,.jpg。但注意accept只是前端选择文件时的过滤器恶意用户可以绕过它后端必须做二次校验。最稳妥的做法是校验文件扩展名和后端保存时的白名单一致判断不合规的直接拒绝。文件校验还涉及一个MIME类型问题。有些浏览器上传文件时给的MIME类型并不可靠特别是在Windows下所以后端最好用扩展名判断而前端不要用file.type做太严格判断。在线预览对PDF比较好办直接新窗口打开文件URL浏览器自带PDF预览能力。图片同理。Word和Excel预览是难题最简单的方案是让用户先下载再查看也可以引入docx-preview或转换服务但那些要么复杂要么贵课程项目阶段完全不必纠结。5. 安卓端实现要点5.1 工程初始化与网络层封装用Android Studio创建项目语言选Kotlin最低SDK版本建议24Android 7.0或者26目标SDK选当前主流版本。工程目录按功能分包data网络和存储、ui各页面Activity/Fragment和Adapter、utils等。Android端最基础的工作是网络层封装非常推荐Retrofit OkHttp Gson组合。在build.gradle里引入依赖后创建一个ApiService接口用注解声明接口interface ApiService { POST(login) suspend fun login(Body request: LoginRequest): BaseResponseLoginResponse Multipart POST(student/homework/submit) suspend fun submitHomework( Part(homeworkId) homeworkId: RequestBody, Part(content) content: RequestBody, Part file: MultipartBody.Part ): BaseResponseUnit }用协程的suspend函数配合Retrofit写接口请求像写普通函数一样简单不用再手动处理回调线程切换。OkHttp的Interceptor统一在请求头加入Token每个接口都自动携带登录凭证。5.2 登录注册与Token持久化登录页面是学生端的第一道门。界面很简单账号输入框、密码输入框、登录按钮底部一个注册账号入口。注册页面比登录多一个确认密码和昵称字段。注册成功后自动跳转登录页用户重新输一遍账号密码这步体验虽然不够丝滑但胜在实现简单可靠。登录成功后的Token保存我用的是SharedPreferences或可以换Jetpack DataStore效果类似。要点是Token不仅要拿来请求接口还要拿它去拉取用户信息。Utils层写一个UserManager单例负责Token的读取/写入/清除App启动时检查UserManager中是否有有效Token决定是跳首页还是登录页。加上Kotlin协程这个检查可以放到SplashActivity里闪屏页停留一秒后自动选择跳转。5.3 作业列表与提交功能开发作业列表页用RecyclerView展示卡片列表。每张卡片显示课程名称、作业标题、截止日期和状态标签。这个页面的数据接口是根据当前学生id查询作业列表后端返回每条作业附带的提交状态未提交/已提交/已批改。列表页到提交页的跳转是典型的携带参数场景点击卡片通过Intent把homeworkId、title、deadline传给HomeworkDetailActivity。详情页上半部分展示作业内容、截止时间倒计时可以用CountDownTimer实现下半部分是一个Multipart表单文字说明输入框、选择附件按钮、提交按钮。附件选择通过系统文件选择器val intent Intent(Intent.ACTION_GET_CONTENT).apply { type */* addCategory(Intent.CATEGORY_OPENABLE) }拿到Uri后要先用ContentResolver查询真实的文件路径或直接读取输入流然后再构造MultipartBody.Part上传。很多新手在这个环节踩坑直接用Uri构造文件传给后端后文件名和后缀全都乱了。正确的做法是先读文件名、文件大小、MIME类型再创建临时文件或直接用字节数组构造RequestBody。5.4 文件下载与打开作业详情页通常也需要支持下载作业附件或者学生要查看自己已提交的附件。OkHttp做下载很简单把response body写入到应用专属目录getExternalFilesDir()或getCacheDir()然后通过系统FileProvider分享/打开文件。这里一定要重点提醒从Android 7.0开始应用不能直接用file://Uri打开文件必须通过FileProvider.getUriForFile()生成一个content://Uri同时给Intent加上FLAG_GRANT_READ_URI_PERMISSION权限。很多同学在附件下载后点击打不开的问题上浪费很久八成就是没做这一步。还有一点是存储权限的动态申请。如果选择把文件下载到公共下载目录Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS)Android 10及以后需要处理分区存储Scoped Storage权限而如果存入应用专属目录完全不需要任何存储权限推荐直接走后一种少操很多心。6. 常见问题与排查技巧实录6.1 跨域问题的完整解法前后端分离项目中跨域几乎是必遇问题。浏览器里Vue页面跑在http://localhost:5173后端接口在http://localhost:8080端口不同就构成跨域。后端最简单的解法是配置一个CorsConfig类或直接用CrossOrigin注解。但我更推荐用Spring Boot的全局配置类这样不用每个Controller都加注解Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }注意如果设置了allowCredentials(true)前端Axios就必须同时设置withCredentials:true否则预检请求会挂掉。还有一个常见误区是只在Spring Security里配了一下CORS但顺序不对导致预检请求OPTIONS被Security过滤器拦截返回到前端就是401这坑我也踩过。处理方式是在Spring Security的配置链里放行OPTIONS请求。6.2 Android访问本机后端地址的问题模拟器或者真机调试时最经典的一个问题是安卓端访问不到后端的接口。这里有个关键知识点Android模拟器里的localhost指代的是模拟器自己而不是你的电脑。如果你在模拟器里请求http://localhost:8080大概率连接拒绝。正确写法是模拟器Android Studio自带AVD用http://10.0.2.2:8080访问电脑本机服务真机调试用电脑的局域网IP比如http://192.168.1.5:8080另外从Android 9开始应用默认禁止明文HTTP流量。开发期最简单的处理办法是在AndroidManifest.xml的application节点设置android:usesCleartextTraffictrue或者在network_security_config.xml中针对开发环境配置白名单。这个配置不写你在模拟器上请求HTTP接口会直接报CLEARTEXT communication not permitted很多人一看到这个错误就懵了就是网络安全策略在拦截。6.3 文件上传超时与大小限制我遇到过几次上传大作业压缩包时请求卡死几十秒后失败的情况排查下来有三层原因一是后端上传限制。max-file-size和max-request-size没调大或者正好卡在临界值请求直接被拒绝。解决办法是统一设成100MB并且异常处理时要能捕获到MaxUploadSizeExceededException返回一个通俗的提示文件过大请压缩后重试。二是Web端Axios超时时间太短。默认timeout如果是5秒传个30MB文件必然超时。设置成timeout: 60000或者不设超时配合进度条让用户知道上传还在进行。三是OkHttp的上传超时设置。Retrofit和OkHttp的超时时间要分别设置connectTimeout连接超时设短10秒足够readTimeout和writeTimeout设长60秒以上。写超时容易被忽略但下载大文件时如果读超时太短进度稍微卡一下就把连接断掉了。6.4 Token过期与并发刷新策略Token过期是最容易让用户体验崩掉的环节。设想这个场景学生正在写作业详情页写完内容点提交后端返回401表单数据还没传上去页面倒是跳回登录页了刚刚写的东西全没了砸屏幕的心都有。我处理这个问题的方案分为两层。第一层是乐观策略登录时给Token设置一个较长的有效期比如24小时到期时间在Android端存下来本地判断剩余时间不足2小时就主动跳登录页让用户在提交前先登录好避免操作中途失效。第二层是刷新机制后端额外提供一个refreshToken接口用旧Token换新TokenAndroid端在收到401响应时先尝试刷新再重放一次请求。这两层配合实际使用中能把操作到一半被踢下线的概率降到很低。6.5 中文乱码与时间格式不一致中文乱码问题在开发时配置好了其实不会再出现。后端务必设置响应编码为UTF-8Spring Boot默认就是UTF-8数据库连接串上加characterEncodingutf8MySQL表的字符集用utf8mb4。如果这三处不统一就会出现中文存入数据库后变成问号或乱码的奇怪现象尤其是作业内容里夹杂表情符号时utf8mb4是必须的。时间格式不一致的问题主要集中在Jackson自动序列化上后端返回的LocalDateTime默认是一串数组或ISO字符串前端不统一解析会很痛苦。我在3.1节已经提过在application.yml里配spring.jackson.date-format此外还有一个更可靠的办法在实体字段上用JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)注解固定格式双重保障。关于前端时间处理再补一句安卓端拿到的是yyyy-MM-dd HH:mm:ss字符串如果要计算剩余时间要先把字符串解析成LocalDateTime或Date再和当前时间做差。这里又经常遇到一个坑后端返回的是服务器时间字符串比如2025-06-30 23:59:59而学生手机系统时间不是北京时间或者不准导致倒计时显示不正确。最稳的办法是登录接口在返回Token的同时返回服务器当前时间戳所有倒计时都基于服务器时间 差值计算。6.6 数据库死锁与并发提交问题并发量不大时这个问题几乎不会暴露但一旦多个学生同时提交或者一个学生快速重复提交就有可能出现问题。具体场景学生A在手机点了一次提交网络卡顿又点了第二次两个请求几乎同时打到后端。如果没有做幂等处理t_submission表里就可能出现同一个人对同一作业的两条提交记录或者出现更新丢失。解决思路是三层保底表设计上已经加了(homework_id, student_id)的唯一索引所以第二条重复插入会直接爆主键冲突异常后端捕获到后返回请勿重复提交业务代码上采用先查后写的逻辑查到存在记录就执行更新而不是插入更进一步可以用乐观锁字段version列或MySQL的INSERT ... ON DUPLICATE KEY UPDATE实现原子性的插入或更新。对课程项目来说前两层已经足够稳定第三层可以当加分项提出来在文档中展示你思考过并发安全这个问题。7. 项目后续扩展与复盘建议这个系统做主流程做完之后我还建议大家从完成视角切换到产品视角去看一遍想想还有哪些低成本高价值的功能可以补。比如消息通知模块学生提交作业后给老师推一条通知老师批改后给学生推一条通知用WebSocket或者简单的站内信都能实现。这个功能对实际使用的粘性提升非常大因为作业场景最核心的时间点是提交—批改这个闭环两端都要随时知道状态变化。再比如统计模块老师端增加一个小仪表盘展示某门课程应交人数、实交人数、未交学生名单、平均分走势后端写几个带GROUP BY的统计SQL就能搞定前端用ECharts画柱状图或折线图。这个功能在答辩或项目展示时极其抢眼因为评委一眼就能看出你做了数据可视化和复杂查询性价比特别高。代码结构上再做一次Review把Controller里的业务逻辑尽量下沉到Service层Controller只负责参数解析和结果返回把DTO、VO和Entity彻底分离不要用一个类打天下——虽然Entity直接返回给前端也能跑但后续加字段、加接口时会不断膨胀很快变得不可维护。接口的异常处理统一改用RestControllerAdvice做全局异常处理器这样任何Service里抛出业务异常都能转成统一格式的Result返回前端不用每个接口单独判断错误结构。我自己做完这类项目最深的体会是技术栈只是工具真正考验人的是把业务链路梳理清楚的能力。作业提交平台看起来是个很常见的业务但里面每一环都有值得深入思考的细节——文件怎么安全地存过期判断放在哪一层学生重复提交怎么防谁能看附件、谁能批改把这些细节逐个想明白、落地验证写出来的系统自然就比能跑高一个档次。如果你正准备做类似的三端系统建议先别急着写代码花一个下午把表和接口定义清楚后面的开发会让你少走非常多弯路。最后分享一个小技巧整个项目完成后把自己当用户从安卓端注册一个新账号走一遍选课、查看作业、提交文件、查看批改的全流程同时打开管理端走一遍创建课程、发布作业、下载提交、打分评语的全流程。真实走完这两条线你会发现不少代码没问题但体验很奇怪的地方——比如空状态提示不友好、附件名字显示不全、倒计时为负数时的文案。把这些问题逐一修掉这个项目就不只是一份能交差的作业而是真正能让人愿意用起来的工具了。
阅读完成 · 觉得有帮助?
咨询建站