ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Spring Boot公考学习平台:从源码拆解到调试跑通全记录

Spring Boot公考学习平台:从源码拆解到调试跑通全记录 基于Spring Boot的公考知识学习平台从源码拆解到调试跑通的完整实操记录接手这个项目的时候第一感受是公考学习平台这个题目在毕业设计里确实够典型——既有用户端、管理端的清晰业务边界又能把登录鉴权、题库管理、刷题判分、数据统计这些Spring Boot全栈链路该有的知识点全部串起来。但真正动手把源码跑起来、把每一行核心代码读懂、再讲明白才意识到这里面的坑远比想象中多。这篇博客就围绕这套源码文档调试讲解的完整交付方案把整个项目从功能设计、技术选型到启动调试、答辩准备的过程全部梳理一遍适合正在做Java课程设计、毕业设计或者想通过一个真实项目吃透Spring Boot开发流程的同学参考。先说一下我拿到这套源码之后做的事不急着看代码先把目录结构、数据库脚本、配置文件这三样东西摸清楚。很多时候项目跑不起来不是因为代码逻辑有问题而是配置文件里的环境信息和你本机不一致或者数据库初始化脚本没有执行完毕。下面我会按照从整体到细节的顺序把这个项目拆开揉碎来讲。1. 公考学习平台的项目定位与功能全景1.1 这类平台解决的是什么问题公考备考场景里刷题错题记录知识点速记模拟练习是最高频的需求。市面上的商业平台要么收费、要么功能冗余而课程设计与毕业设计阶段需要的是一个业务边界清晰、技术栈完整、能够在有限时间内讲明白的示例项目。这个平台的核心定位就是模拟公考学习场景的在线刷题系统。考生在平台上注册登录后可以按照行政职业能力测验行测和申论等不同考试类型浏览题库、进行分科目练习、查看答题解析、整理错题本管理员则负责维护题目信息、管理用户、发布学习资讯。整个业务闭环落在用户—题目—答题记录—错题本这条主链路上前后端通过RESTful接口交互数据存储在MySQL中缓存和登录态管理用Redis处理。1.2 适合谁来学习和复现建议以下三类人群重点关注这套源码和本文的梳理即将做Java课程设计或毕业设计的学生这个项目的功能体量、技术栈组合Spring Boot MyBatis-Plus MySQL Redis Vue与学校要求的有完整前后端交互、有数据库设计、有角色权限的评审标准完全对齐。刚学完Spring Boot基础但缺乏项目经验的开发者可以通过读源码理解一个真实项目的分层结构——Controller、Service、Mapper、entity、config各层如何协作而不是停留在写一个HelloWorld接口的阶段。需要快速做技术预研的测试或前端同学项目文档完整、接口路径规范拿来当一套可运行的接口服务非常方便。提示复现这个项目之前最好先具备Java基础语法、Maven基本命令、MySQL建库导表操作的能力。对前端Vue没有深入要求但至少要会执行npm install和npm run serve。1.3 项目目录结构与源码阅读顺序拿到源码压缩包后建议按下面这个顺序去读而不是从头到尾翻文件sql/目录下的数据库初始化脚本先建库建表搞清表关系。application.yml或application.properties了解端口、数据库、Redis等核心配置。pom.xml看清依赖版本尤其是Spring Boot和MyBatis-Plus的版本兼容性。entity层数据实体对应数据库表结构。mapper层数据访问接口配合XML或注解SQL。service层 controller层核心业务逻辑和接口对外暴露。config/包拦截器、跨域配置、MyBatis-Plus分页插件等。这样走一遍整个项目的调用链会很清楚地呈现在脑子里前端请求到达Controller → Controller调用Service处理业务 → Service调用Mapper操作数据库 → 结果逐层返回。2. 核心业务模块的设计思路与表结构拆解2.1 用户端模块注册登录与个人信息用户模块是几乎所有系统中都会出现的地基模块但地基恰恰决定了整个系统的骨架质量。这个项目采用用户名或手机号密码的注册登录模式。密码在数据库里不是明文存储而是通过MD5加密后落库更严谨的方案是加盐处理比如BCrypt这里用MD5是为了便于课程设计的答辩讲解。登录成功之后后端生成一个token返回给前端前端每次请求在请求头里携带这个token后端通过拦截器校验登录状态。表结构上用户表sys_user一般包含字段id、username、password、nickname、avatar、role角色用于区分管理员和普通用户、create_time、update_time。这里我特别想强调一个细节很多同学在课程设计里会把登录逻辑做成查询到了用户就放行完全没有考虑用户被禁用、角色不同权限不同等场景。这套源码里其实是有这部分处理的——用户表里有一个status字段0表示正常1表示锁定登录校验时会判断这个状态。这个小设计在答辩时很加分因为它体现了设计的完整性。2.2 题库模块题目分类与题目管理题库是整个平台的内容核心一切业务都围绕题目展开。题目表question包含id、type_id题型分类ID、subject_id科目ID、title题干、options选项通常用JSON或特定分隔符存储、answer正确答案、analysis解析、difficulty难度系数、create_time。这里有一个关键设计点题目的选项如何存储。常见做法有两种一是单独建一个选项表通过question_id关联二是直接在当前表的options字段里存JSON字符串。这个项目采用的是第二种原因很实际——考试类题目的选项数量是离散的行测的单选题固定四个选项但有些多选或判断题型字段结构不同用JSON存储灵活性更高读取时用Fastjson或Jackson解析即可。对于课程设计来说这种方案实现简单也不影响演示效果。题目按照考试科目细分行政职业能力测验下设常识判断、言语理解与表达、数量关系、判断推理、资料分析等分类申论则是一个独立科目。这种分类方式直接对应到subject表和category表用户刷题时可以按科目进入逐层筛选。2.3 刷题与答题记录学习行为的完整闭环用户选择科目进入刷题页面后系统一次性返回一组题目例如10道用户逐题作答并提交。提交后后端把用户ID 题目ID 用户答案 是否正确写入答题记录表answer_record如果答错则自动写入错题本表wrong_question。答题记录表是这套系统里数据量增长最快的表也是面试和答辩时最容易被人追问你的系统如何应对数据膨胀的地方。这个项目里给了一个很好的基础答案记录表通过user_id建立索引分页查询错题和统计正确率时走索引避免全表扫描。如果你的项目时间充裕还可以再往深讲一步——比如定期归档历史年份的数据或者引入分库分表但作为课程设计把索引讲清楚已经非常够用了。2.4 后台管理模块内容运营与用户管控管理端和前台的模块划分非常清晰管理员登录后进入独立的页面链路可以管理用户查看列表、禁用/启用、管理题目新增、编辑、删除、按科目筛选、管理分类增删改查、发布公告资讯。权限控制的实现思路也值得展开说拦截器判断请求路径是否以/admin开头然后校验当前登录用户的role字段是否为管理员。这种实现方式虽然不算特别精细但对于没有引入Spring Security的课程设计项目来说已经足够清晰、也足够应付演示——因为前端已经把管理入口隐藏掉了后端再拦一道双保险。3. 关键技术点拆解配置、分页、登录校验与接口规范3.1 配置文件里的几个关键项项目的application.yml是启动的第一道关。下面是配置核心要点路径和密码在实际交付包中会以文档形式给出server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/gongkao_platform?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 database: 0 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto这里有几个非常容易踩坑的点serverTimezoneAsia/Shanghai必须有。MySQL 8.x 的驱动对时区要求严格不设置的话会直接报连接超时或The server time zone value错误。map-underscore-to-camel-case设为 true可以让数据库里的create_time自动映射到实体类的createTime少写很多TableField注解。MyBatis-Plus 的log-impl建议开发阶段保留它会在控制台打印所有执行的SQL调试时非常有用。3.2 数据库初始化与测试数据导入sx_gongkao.sql这个脚本文件里包含建库建表语句和初始数据。执行时需要注意顺序先建库再建表最后导入数据。很多同学用 Navicat 直接运行脚本后看到报错仔细看都是因为当前连接的库不对或者脚本语句里USE指令没被正确执行。初始化数据里包含了一个管理员账号比如admin/admin123和几个普通测试账号、一套完整的行测模拟题。这些数据是为了保证项目启动后不需要额外造数据登录进去就能看到效果。提醒如果自己另建了数据库名字记得同步修改application.yml里的url否则启动时报错Unknown database的概率是100%。3.3 MyBatis-Plus分页插件的标准配置与使用项目里凡是列表接口都使用了MyBatis-Plus的分页功能。分页插件不是依赖配置就能自动生效的必须手动添加一个配置类MybatisPlusConfigConfiguration MapperScan(com.gongkao.mapper) public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }有了这个配置Service层就可以直接用Page对象做分页查询了public PageQuestion getQuestionPage(int current, int size, Long subjectId) { PageQuestion page new Page(current, size); LambdaQueryWrapperQuestion wrapper new LambdaQueryWrapper(); if (subjectId ! null) { wrapper.eq(Question::getSubjectId, subjectId); } wrapper.orderByDesc(Question::getCreateTime); return questionMapper.selectPage(page, wrapper); }分页插件在答辩中是一个高频考点你需要准备这些问题分页的底层原理是什么MyBatis-Plus 会在执行前自动把 SQL 拼接上LIMIT语句、current从1开始还是从0开始从1开始、总数是自动查询还是手动查询插件自动执行 COUNT——把这些都弄明白这一块就稳了。3.4 登录状态管理拦截器与ThreadLocal这个项目没有引入Spring Security或Shiro而是用最直观的方式实现登录校验一个自定义拦截器 ThreadLocal存储用户信息。拦截器注册配置Configuration public class WebConfig implements WebMvcConfigurer { Autowired private LoginInterceptor loginInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/**) .excludePathPatterns(/api/user/login, /api/user/register, /error); } }拦截器核心逻辑Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(token); String tokenKey RedisUtil.TOKEN_PREFIX token; if (!StringUtils.hasText(token) || !RedisUtil.hasKey(tokenKey)) { response.setStatus(401); response.getWriter().write(未登录或登录已过期); return false; } Long userId Long.valueOf(RedisUtil.get(tokenKey).toString()); UserContext.setUserId(userId); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.remove(); } }UserContext本质上就是一个ThreadLocal的包装类public class UserContext { private static final ThreadLocalLong USER_HOLDER new ThreadLocal(); public static void setUserId(Long userId) { USER_HOLDER.set(userId); } public static Long getUserId() { return USER_HOLDER.get(); } public static void remove() { USER_HOLDER.remove(); } }这里把用户ID放到ThreadLocal里后续Service层和Mapper层不需要每次把userId作为参数层层传递直接UserContext.getUserId()即可。但要注意必须在请求结束后调用 remove() 清除否则线程池复用线程时可能拿到上一个请求的用户ID。项目里已经在afterCompletion做了清理这点在答辩时可以着重讲说明你考虑到了线程安全问题。3.5 统一返回结果与全局异常处理学习这个项目时值得重点看下它的统一响应体设计。项目里所有Controller接口都不直接返回实体对象而是返回一个ResultTpublic class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }对应的Controller写法就很统一PostMapping(/submit) public Result? submitAnswer(RequestBody SubmitDTO dto) { answerService.submit(dto); return Result.success(null); }同时项目里还有一个RestControllerAdvice全局异常处理器把业务异常、参数校验异常统一捕获转换成JSON返回给前端。这个设计的意义在于后端不会因为某个未捕获异常直接返回一大段堆栈给前端接口永远保持结构一致的响应格式。课程设计里加这个整体代码档次会明显不一样。4. 从零到跑通启动部署全流程与踩坑记录4.1 环境准备清单动手复现前先确保本机满足以下条件软件版本建议用途JDK1.8 或 11运行后端Maven3.6 以上依赖管理与打包MySQL5.7 或 8.0主数据库Redis5.x 以上缓存与登录token存储Node.js可选14以上运行前端如果源码含Vue前端IDEIDEA 2020以上开发调试这里要重点提醒版本兼容问题如果JDK版本过高比如17而Maven依赖里用的是Spring Boot 2.x部分版本会导致编译失败或运行时反射异常。已知比较稳妥的组合是 JDK 8 Spring Boot 2.7.x MyBatis-Plus 3.5.x或者 JDK 11 Spring Boot 2.7.x。如果你本地只有JDK 17建议去pom.xml里把Spring Boot版本升级到3.x但随之而来的问题是部分依赖的javax包名需要改为jakarta改动量不小。所以我的建议很直接再装一个JDK 8切换环境变量一劳永逸。4.2 启动后端服务的完整流程第一步把源码解压后用IDEA打开等待Maven自动下载依赖。如果IDEA没有自动识别为Maven项目右键pom.xml→Add as Maven Project。第二步启动Redis。Windows环境可以下载一个Windows版Redis运行redis-server.exeMac环境用brew services start redis。如果本机没装Redis也可以把application.yml里的Redis相关配置临时注释掉但登录功能就没法用了因为token是存在Redis里的。第三步在IDEA中编辑application.yml把数据源和Redis的地址账号密码改成自己本机的配置。第四步在MySQL中执行数据库脚本mysql -u root -p sx_gongkao.sql注意执行前确认脚本里是否包含建库语句如果没有需要先手动创建数据库再导入。第五步找到主启动类类名类似GongkaoApplication右键Run。看到类似下面的日志输出就说明启动成功了Started GongkaoApplication in 5.32 seconds Tomcat started on port(s): 8080 (http) with context path 4.3 前端项目的启动与联调如果源码包中包含Vue前端目录启动方式如下cd frontend npm install npm run serve前端默认端口是8081项目里配置了vue.config.js的反向代理module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } };这里解读一下前端请求/api/user/login时通过代理转发到后端的http://localhost:8080/api/user/login从而绕过了跨域限制。这也是前后端分离项目中最常见的联调手段。注意如果后端也开启了CORS跨域配置而前端又在用代理转发可能出现重复的跨域头导致的请求失败。解决办法是把后端的CrossOrigin注解删掉或者设origin *但前端不走代理二选一即可。4.4 首次启动失败的高频错误对照表这是一个排错速查表覆盖了我在实际调试中遇到的高频问题报错现象根本原因解决方式Cannot load driver class: com.mysql.cj.jdbc.Driverpom中缺少MySQL驱动依赖在pom.xml中加入mysql-connector-javaAccess denied for user rootlocalhost数据库密码错误检查application.yml中的密码与本机MySQL密码Unknown database gongkao_platform尚未执行建库脚本执行CREATE DATABASE gongkao_platform DEFAULT CHARSET utf8mb4;后再导入表Redis connection refusedRedis没有启动启动Redis服务或临时注释掉redis相关配置Port 8080 was already in use本地8080端口被占用netstat -ano找到占用PID后结束进程或修改server.portNo qualifying bean of type UserMapperMapper接口没有被扫描到确认启动类上有MapperScan注解Invalid bound statementMapper XML文件没有编译到classpath检查application.yml中的mapper-locations配置和XML路径这一步花的时间最久但做完一次之后整个Spring Boot项目的启动流程就通了以后再跑任何项目都会有手感。5. 调试阶段的高频故障与完整排查链路这一章是文章的核心重量级部分。学习这个项目的关键不是跑通之后就开始背代码而是把调试过程中遇到的问题当成最好的学习材料。这里我挑两个最有代表性的疑难故障把排查链路完整写出来。5.1 案例一接口返回500控制台报Error attempting to get column create_time from result set现象前端调用题目列表接口时返回HTTP 500后端控制台抛出异常SQL能正常打印出来但映射到实体类时失败。完整排查链路第一步看异常堆栈。堆栈里出现org.apache.ibatis.exceptions.PersistenceException和Error attempting to get column create_time from result set。第二步定位MySQL语句SELECT id,title,options,answer,analysis,create_time FROM question LIMIT 0,10这条SQL能正常执行说明问题出在结果集转换为Java对象这一步。第三步检查实体类字段类型。发现createTime对应的Java类型是LocalDateTime。这一步是核心问题当数据库字段是datetime类型而Java实体类的类型是LocalDateTime并且驱动版本是MySQL Connector/J 5.x时根本无法正确转换会直接报错。换成MySQL 8.x的驱动com.mysql.cj.jdbc.Driver后问题消失。第四步验证修复。修改后重启项目再次调用接口返回正常。这个案例的通用价值在于数据库字段与Java类型映射不匹配是MyBatis项目里最高频的错误类型之一。排查时一定要先确认SQL单独执行OK、类型对应没问题、驱动版本没问题这三件事。5.2 案例二注册接口返回200但数据库没有数据现象前端调用注册接口后提示操作成功但打开数据库表发现刚才注册的用户根本不存在。完整排查链路第一步检查Controller层。确认PostMapping(/register)被正确调用入参对象UserDTO能够正常接收前端的JSON数据。第二步检查Service层。发现Service里有这样一段代码public Result? register(UserDTO dto) { User user new User(); BeanUtils.copyProperties(dto, user); user.setPassword(MD5Util.md5(dto.getPassword())); user.setRole(user); user.setStatus(0); userMapper.insert(user); return Result.success(null); }这段逻辑看起来是正确的。但为什么数据库没数据第三步单步调试在userMapper.insert(user)这行打上断点执行后发现user对象里createTime为null。虽然MySQL的create_time字段设置了DEFAULT CURRENT_TIMESTAMP但是MyBatis-Plus在执行insert时默认会拼接所有非null字段的插入语句。如果实体类的createTime为null这个字段根本不会出现在INSERT语句里MySQL默认值自然也不会生效。第四步修复方案有两种一是在实体类createTime字段上加TableField(fill FieldFill.INSERT)并配合MetaObjectHandler自动填充二是手写INSERT INTO user (..., create_time) VALUES (..., NOW())。这个项目选择了第一种方案添加了一个MyMetaObjectHandler配置类Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }至此再次注册问题解决。这个案例的排查心得是遇到接口成功但数据库没数据的问题不要第一时间怀疑SQL写错先断点观察实体对象的字段值再结合MyBatis-Plus生成的SQL来反推问题。很多时候你预期MySQL的默认值自动兜底但框架的插入策略并不这么认为。5.3 调试工具使用技巧IDEA断点与表达式求值最后补充几个调试技巧对读源码和排查问题都能明显提效条件断点刷题接口里循环遍历题目列表时如果只想在questionId 38时停下来不需要每一条记录都打断。右键断点Condition里面写questionId 38即可。Evaluate Expression程序停在断点处后使用Alt F8调出表达式求值窗口可以手动执行new LambdaQueryWrapper()这样的代码查看在特定上下文环境下的运行结果不用写任何日志。Drop Frame如果方法逻辑走到了错误的返回值那条路径但又不想重新启动整个项目IDEA调试面板里的Drop Frame可以让你回退到当前方法的调用处重新执行在排查Service层参数被错误修改时特别有用。更改方法行高亮IDEA中按Ctrl Shift F7可以对某个方法的高亮调用点进行快速导航梳理调用链时很好用。6. 答辩与面试中如何把一个课程设计讲成亮点项目很多人觉得课程设计项目太小、太简单面试时不敢拿出手。其实面试官考察的重点往往不是项目规模而是你对项目里每个技术细节的理解深度。同一个题目有人讲出来是我写了一个CRUD有人讲出来是我理解了一个系统的数据流差距就在这里。下面整理最高频的几个追问和参考回答思路。6.1 为什么选择Spring Boot而不是SSM或者Spring Cloud这题考察的是技术选型能力。参考话术Spring Boot在SSMSpring Spring MVC MyBatis基础上做了大量自动化配置让开发者可以更专注于业务逻辑适合中小型项目和快速迭代。本项目的用户量级在几百到几千人用Spring Cloud微服务架构反而会增加运维复杂度和分布式事务成本。所以选Spring Boot是对当前项目规模最合适的方案它保留了Spring生态的核心能力同时又减少了冗余配置。6.2 你的系统的登录状态是怎么控制的不用Spring Security的原因参考话术系统采用拦截器 Redis Token的方式实现登录态控制没有引入Spring Security是因为当前项目的权限需求只有用户和管理员两种角色用框架反而增加理解成本。Redis中存储Token可以主动控制过期时间用户退出或管理员封禁后可以立刻让Token失效这是Session方案做不到的。如果后续权限需求复杂化比如按角色细分操作权限再引入Spring Security或Sa-Token扩展也是顺理成章的。6.3 如果错题本表数据量过大怎么优化这题考察数据库优化意识。参考话术首先是索引优化——对user_id和question_id建联合索引让按用户查询错题列表走索引其次是分页优化——用LIMIT分页在数据量大时会有深翻页问题可以改成基于游标的分页方式每次传入上一页的最后一条ID再次是数据生命周期管理——定期把半年以上的错题记录归档到历史表业务表保持轻盈。对于一个课程设计项目来说把前两点讲清楚已经能证明你的思考深度了。6.4 项目中最难解决的一个问题是什么这道题的黄金回答结构是遇到什么问题 → 怎么排查的 → 最终怎么解决 → 你学到了什么。不要对面试官复述我会做注册登录、增删改查你应该挑一个真实发生过的问题比如上面写的MyBatis-Plus insert时create_time为NULL导致默认值失效把这个问题的排查过程完整讲出来。这类回答的含金量远高于背十个八股文答案。7. MyBatis-Plus 3.x在项目里的高级用法补充前面已经提到分页插件的基础用法但项目中还有几个非常实用、但课程设计里不太常见的高级用法能极大减少代码量也值得在答辩中展示。7.1 LambdaQueryWrapper 的条件构造器在题目的条件筛选接口里如果不用MyBatis-Plus你得手动拼接SQL或写一堆XML里的if标签。用LambdaQueryWrapper后代码变成public ListQuestion searchQuestions(String keyword, Long subjectId, Integer difficulty) { LambdaQueryWrapperQuestion wrapper new LambdaQueryWrapper(); // 关键词模糊查询且null时不生效 wrapper.like(StringUtils.hasText(keyword), Question::getTitle, keyword); // 科目精确匹配 wrapper.eq(subjectId ! null, Question::getSubjectId, subjectId); // 难度精确匹配 wrapper.eq(difficulty ! null, Question::getDifficulty, difficulty); // 按时间倒序 wrapper.orderByDesc(Question::getCreateTime); return questionMapper.selectList(wrapper); }每个条件的第一个参数是是否启用该条件这样前端传keywordnull或difficultynull时不会产生无意义的SQL条件。这种方式在接口设计里字段特别多的筛选场景下非常高效。7.2 逻辑删除字段的配置用户管理模块里普通用户的前端不提供物理删除用户的入口管理员在后台删除用户后数据不应该直接从库中消失毕竟用户还有答题记录关联。项目里通过在用户表加入deleted字段并在实体类中加注解TableLogic private Integer deleted;配置后MyBatis-Plus的deleteById会自动变成UPDATE user SET deleted 1 WHERE id ?而查询时自动加上AND deleted 0。这个设计可以解释为软删除既保留了业务逻辑上的删除操作又能保留数据的完整性和可追溯性。7.3 自动填充字段配合乐观锁除了createTime/updateTime自动填充外项目里还在错题表上使用了Version乐观锁字段来解决并发场景下的重复提交问题。具体来说用户在刷题页面快速勾选答案后多次点击提交如果不加控制同一道题可能会被插入多条答题记录。项目里的方案是提交前先根据userId questionId查一次如果已有记录则跳过插入。但并发请求下会有两次查询都发现没有记录然后都执行插入的间隙——这时候引入数据库唯一索引是最可靠的兜底而乐观锁在更新错题重做次数时能有效防止覆盖更新。课程设计里关于并发这块只要能说清唯一索引兜底 乐观锁兜底两种方案的优缺点已经超出大多数人的认知范围了。我个人觉得对于这个项目用唯一索引解决重复插入是最直接有效的——在answer_record表上建(user_id, question_id)唯一索引插入时 catch 到DuplicateKeyException就直接返回请勿重复作答简单有效。8. 项目讲解文档的整理思路这套源码交付的文档不是写给人凑数的而是真正要帮你把项目讲清楚。我拿到文档后的建议是不要一字不改地背而是把文档转译成自己的话结合你实际调试时遇到的报错变成我的经历。我自己整理答辩稿时习惯用三段式项目概述一句话说清楚系统是给谁用的、解决什么问题面向公考备考人群的在线刷题学习平台提供分类题库、在线答题、错题记录、后台题目管理等功能。核心技术与架构技术栈列表 系统架构图前端Vue通过Nginx/Node代理请求后端Spring Boot应用Spring Boot通过MyBatis-Plus操作MySQL通过Redis管理登录态 模块划分图。个人工作亮点挑3个最拿得出手的技术细节不必多但要讲透每个细节按背景—方案—效果来组织。举一个文档里实际有的亮点描述业务背景用户在刷题时每次提交答案都要实时判断正确性并记录结果。方案前端每答完一题调用后端/api/answer/submit接口后端根据题目ID查出正确答案与用户提交答案比对写入answer_record表同时判断是否同步写入wrong_question错题本。整个过程使用Transactional保证答题记录和错题本写入的一致性避免出现只写了记录、没写错题本的情况。效果保证了用户学习行为数据的一致性也方便后续的数据统计与分析。这种写法比实现了刷题功能有说服力得多。9. 项目扩展思路从课程设计到能写进简历的作品最后聊点实际的。如果你的目标是让这个项目从完成课程任务升级为简历上的亮点项目我会建议在现有架构上做以下扩展这些扩展都不需要推翻重建属于在这个骨架上加肉的操作扩展一加入学习统计模块。基于answer_record表增加一个UserStatisticsService按天聚合用户的答题量、正确率、做题科目分布。前端用ECharts画折线图展示近七天的刷题趋势和各科目正确率雷达图。这一块业务逻辑不复杂但视觉效果好、数据分析感强非常吸引眼球。扩展二知识点标签体系。在题目表上增加knowledge_point字段如数量关系-工程问题用户答错时除了进错题本还可以给对应知识点打上薄弱标签。之后可以做一个弱点专区集中推荐薄弱知识点的题目。这一扩展涉及简单的推荐逻辑能体现你对业务场景的理解深度。扩展三无缝对接更多第三方能力。比如引入对象存储服务用来上传用户头像、接入短信服务做验证码登录等。这些扩展的实际意义是让项目看起来真实可用而不是只存在于本机的演示品。我在实际打磨这个项目的过程中最深的一个体会是调试一个Spring Boot项目收获最大的时刻不是看到Started那一刻而是在控制台看到第一行红色报错、然后顺着堆栈一步步挖到最终根因的那个过程。很多同学遇到报错第一反应是截图发给别人但如果你自己动手搜、动手断点、动手改同样的错误下次再出现时你就是那个能给别人讲清楚的人。这也是我为什么在这篇文章里花了很大篇幅去写故障排查链路——项目代码只是起点你通过这个项目获得的排查能力和对框架底层机制的理解才是课程设计能带给你的最值钱的东西。如果你后面对这个项目还有具体的问题——比如某个接口看不懂、某个功能想改造成别的业务场景可以在评论区把场景描述清楚我尽量按需求→数据库设计→接口设计→代码实现的链路帮你拆一遍。
返回列表