做毕设选了交通违章管理系统这个题目的同学,十有八九是被"Web 版车辆违章查询处理系统"这个场景吸引来的。我自己当年就是从这套系统入手的 SpringBoot,从数据库设计到前端页面再到最后的部署上线,全程踩了无数坑,熬夜修过 Bug,也推倒重来过两版。这篇博客不灌水,把整套系统的设计思路、核心实现、排错经验完整拉出来说清楚,正在做毕业设计的同学可以直接照着搭,想在 SpringBoot 项目里练手的小白也能找到自己想要的干货。
SpringBoot 加持下的交通违章管理平台,核心其实就是三件事:违章数据的管理、违章处理流程的闭环、以及不同角色的权限划分。听起来像政府项目,但落到毕设层面,它本质上是一套标准的 Java Web 增删改查增强版系统,难点不在于某个技术的深度,而在于业务关系的设计,以及对权限、统计报表、日志记录这类非核心功能的取舍。
这套系统适合两类人:一类是正在做计算机毕业设计、选题方向是 Java Web 系统开发的同学,另一类是刚学完 SSM 基础、想用 SpringBoot+MyBatis-Plus 做完整项目练手的初级开发者。因为它的模块边界清晰、业务逻辑有代表性,麻雀虽小五脏俱全,RESTful 接口、ORM 映射、权限拦截、数据统计全都覆盖得到,做完这一套,你对整个 Java Web 开发链路的理解会系统很多。
1. 项目整体设计与技术选型思路
1.1 先把业务边界画清楚,别一上来就写代码
很多同学拿到课题第一反应是建工程、写实体类,这是最大的坑。交通违章管理系统这个词看起来很具体,但你仔细想一想,里面包含了好几套完全不同的业务场景:违章数据的录入和查询是一套逻辑,违章处理(确认、处罚、缴款、销案)是一套流程,车辆和驾驶员的档案管理是另一套数据,再加上后台的统计分析和系统管理,每一块都能拆出不少东西。
我先给当年自己的设计做了一个减法,把系统边界定为三条线:管理端负责车辆信息、驾驶员信息、违章记录的维护;处理端负责对违章记录做审核、处罚登记、处理状态变更;查询端面向普通用户,提供根据车牌号或驾驶证号的违章查询和详情查看。系统管理作为辅助模块,负责用户账号、角色权限和基础数据字典的维护。
这个边界的价值在于,它天然形成了三种角色:管理员、处理员、普通用户,角色的权限差异直接决定了接口设计和菜单显示的逻辑。如果你不做角色拆分,把所有功能堆在一个页面上,答辩的时候老师第一个问题就是"你的系统如何保证不同身份的人只能操作自己能操作的数据",到那个时候再改权限模型,成本就大了。
1.2 为什么 SpringBoot 是毕设选题的默认答案
技术选型上,SpringBoot 几乎是这类毕设的标准答案,原因很实在。
第一,它把 Spring 生态的配置成本压到了极低。以前用 SSM 做项目,光 applicationContext.xml 就要写一大堆 Bean 定义,SpringBoot 用自动配置全部接管了,我可以把精力集中在业务代码上,而不是反复调整 XML 配置。对我来说,这节省了至少两天的时间。
第二,内置 Tomcat,打包即部署。我做完之后直接mvn package打成 jar 包,放到服务器上java -jar就启动了,不用单独装 Tomcat 配置端口、发布 war 包,这个体验对没有服务器运维经验的学生党非常友好。
第三,社区生态成熟,遇到问题搜得到答案。SpringBoot 相关的博客、示例代码、踩坑记录全网到处都是,毕设阶段你大概率会遇到各种连环报错,如果一个技术搜不到解决方案,那才是真的灾难。
结合同学们的实际情况,我建议技术栈固定为:SpringBoot 2.x + MyBatis-Plus + MySQL 8.x + Thymeleaf(或 Vue,后面我会细说前端选型)。值得一提的是 MyBatis-Plus,它让我省去了绝大部分手写 SQL 的活,单表 CRUD 直接继承 BaseMapper 就能用,这让代码量肉眼可见地降了下来,非常适合时间紧任务重的毕设节奏。
1.3 功能模块怎么拆才合理
模块划分是项目结构的骨架,我最终选的是按业务域分包,而不是按技术层分包。按技术层分包是那种 controller/service/mapper 一刀切三层目录的做法,乍一看规整,但业务一多就会乱。我用的方式是每个功能域一个完整的分层包:
modules/user:用户管理、登录认证、角色权限modules/vehicle:车辆信息管理modules/driver:驾驶员信息管理modules/violation:违章记录、违章处理流程modules/system:数据字典、系统配置modules/statistics:统计报表
这样写的最大好处是,每个模块你都可以看作一个独立的小系统来开发,业务内聚性很强。我做违章模块的时候只需要在 violation 包内部操作,完全不需要去别的地方找代码,调试时定位问题也顺手得多。后面如果你想要加功能,比如增加一个"事故记录管理",直接新建一个 accident 包,不动其他模块的代码。
2. 数据库设计与实体建模
2.1 六张核心表搞定全部业务
数据库设计是整个系统里我会放最多心思的地方,因为表结构一旦定下来,后面改起来代价极高。我最早一版设计用了九张表,后来发现有几张表其实可以合并,自己做了一次重构,最终稳定在下面这六张核心表上:
| 表名 | 用途 | 关键字段 | 说明 |
|---|---|---|---|
sys_user | 系统用户 | user_id, username, password, role_type, status | 三种角色用 role_type 区分 |
vehicle_info | 车辆档案 | vehicle_id, plate_number, owner_name, owner_id_card, vehicle_type, register_date | 车牌号是业务主键,做唯一索引 |
driver_info | 驾驶员档案 | driver_id, name, license_number, license_type, phone, address | 驾驶证号唯一 |
violation_record | 违章记录 | record_id, vehicle_id, driver_id, violation_type, violation_time, location, penalty_points, fine_amount, status | 核心表,外键关联车辆和驾驶员 |
violation_process | 违章处理记录 | process_id, record_id, handler_id, process_time, process_type, result, remark | 处理和违章记录是一对多关系 |
sys_dict | 数据字典 | dict_id, dict_type, dict_code, dict_value, sort_order | 存违章类型、车辆类型等枚举 |
这个设计的核心思路是:把车辆的静态信息和违章的动态行为拆开,开车的人(驾驶员)和管理对象(车辆)也分开。违章记录里同时存车辆 ID 和驾驶员 ID,这样查一辆车的历史违章,直接按车辆 ID 过滤就行;查一个驾驶员的扣分情况,按驾驶员 ID 分组求和就好,查询路径非常清晰。
业务主键这块我有话要说。车牌号和驾驶证号在现实业务里都是业务主键,但在系统里我仍然建议用自增 ID 做物理主键,车牌号只加唯一索引。原因很现实,车牌号可能因为车辆过户等原因变更,一旦用业务字段做物理主键,改数据的时候会牵扯所有外键关联,简直生不如死。自增 ID 是个无意义的纯技术标识,永远不需要改,这才是主键该有的样子。
2.2 实体类设计:从建表 SQL 到 Java 类的转换
表结构确定后,Java 实体类的设计要跟表字段一一对应,这里 MyBatis-Plus 的约定优于配置帮了大忙。我只需要保证驼峰命名和数据库下划线命名的自动映射开好,就完全不用手写 ResultMap。
以违章记录表为例,实体类的核心写法大概是这样的:
@Data @TableName("violation_record") public class ViolationRecord { @TableId(type = IdType.AUTO) private Long recordId; private Long vehicleId; private Long driverId; private String violationType; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime violationTime; private String location; private Integer penaltyPoints; private BigDecimal fineAmount; private Integer status; @TableField(exist = false) private String plateNumber; @TableField(exist = false) private String driverName; }这里有两个细节容易被忽略,我特意标注出来。
@TableField(exist = false)这个注解太重要了。上面代码里的plateNumber和driverName不是数据库表里的字段,而是我为了前端展示方便,做多表联查时手动填充的"虚拟字段"。如果不加这个注解,MyBatis-Plus 在做插入和更新的时候会默认把这两个字段拼进 SQL,直接报 "Unknown column" 错误。第一次遇到这个报错的人,大概率会去改表结构加字段,其实完全没必要,一个注解就能解决。
另一个细节是 LocalDateTime 的时间格式化。数据库存的是 datetime 类型,Java 这边用 LocalDateTime 接收,如果不对 JSON 序列化做处理,前端拿到的是一长串数字时间戳。加上@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")后,接口返回值会自动变成人类可读的时间字符串。这个我在第一个版本里没加,结果前端页面时间显示全乱了,后来才发现是这个注解的问题。
2.3 时间字段与状态字段设计的四个关键细节
建表时的字段类型选择,看起来是小事,实际影响很大。这里分享我总结出来的四个经验,都是改过线上数据才记住的教训。
第一,时间字段一律用 datetime 类型,不要用 timestamp。timestamp 的范围到 2038 年就结束了(2038 年问题),而且它依赖数据库服务器的时区设置,一旦服务器时区配置不对,你存进去的时间跟实际时间差八个小时。datetime 不受时区影响,你用 MySQL 连接串里的 serverTimezone 参数来控制,反而更可控。
第二,凡是涉及金额的字段,一律用 Decimal,不要用 float 或者 double。违章罚款金额虽然都是整数,但如果以后扩展出滞纳金、折扣这类逻辑,浮点类型会出现精度丢失问题,0.1+0.2 不等于 0.3 这种事情在财务数据里是绝对不能出现的。Decimal(10, 2) 足够覆盖绝大部分业务金额场景。
第三,最重要的表都要带 status 字段,而且定义一个全局统一的状态含义。比如违章记录表里,我用 0 表示待处理、1 表示已处理、2 表示已撤销。这个状态字段是把系统的查询和统计串起来的关键,后面做处理流程和首页统计都要靠它。
第四,每个表都要有create_time和update_time两个审计字段。MyBatis-Plus 有自动填充的机制,实现MetaObjectHandler接口就能自动在插入和更新时填充这两个字段,不用手动 set。花十分钟配置一次,后面排查数据问题的时间能省好几个小时。
3. 核心业务功能实现
3.1 违章记录录入与多条件组合查询
违章记录录入是整个系统使用频率最高的功能之一。处理员在录入界面选择车辆、选择驾驶员、选择违章类型、填写地点、选择时间,系统自动带出该类型对应的罚款金额和扣分,确认后保存到数据库。
这里有个很实用的实现技巧:违章类型不要硬编码在前端下拉框里,而是从 sys_dict 数据字典表读取。我在系统管理里维护了一张字典表,存违章类型的代码(比如 "speeding" 对应超速)和值(对应 200 元和 3 分),前端加载页面时调用字典接口动态渲染下拉选项。这样做的好处是,以后违章类型要调整,只需要在系统管理界面改字典数据,代码一行都不用动。
违章查询功能我用的是 MyBatis-Plus 的 LambdaQueryWrapper 做动态条件拼装,这是它最让我省心的地方:
public Page<ViolationRecordVO> queryViolationPage(ViolationQueryDTO dto) { LambdaQueryWrapper<ViolationRecord> wrapper = Wrappers.lambdaQuery(); wrapper.eq(StringUtils.hasText(dto.getPlateNumber()), ViolationRecord::getVehicleId, vehicleIdCache.get(dto.getPlateNumber())); wrapper.eq(dto.getStatus() != null, ViolationRecord::getStatus, dto.getStatus()); wrapper.ge(dto.getStartTime() != null, ViolationRecord::getViolationTime, dto.getStartTime()); wrapper.le(dto.getEndTime() != null, ViolationRecord::getViolationTime, dto.getEndTime()); wrapper.orderByDesc(ViolationRecord::getViolationTime); return violationMapper.selectPage(new Page<>(dto.getPageNum(), dto.getPageSize()), wrapper); }注意我用的全部是 Lambda 表达式引用实体类的 getter 方法,而不是字符串字段名。这么做的好处是,如果实体类字段重命名,编译期就能发现错误,不会等到运行时报 SQL 异常。
车牌号转 vehicleId 这块,我用了一个缓存集合来避免每次查询都去查一次车辆表。这个优化其实很小,但在数据量大、用户反复查询同一个车牌的场景下,响应速度提升是能感知到的。当然这只是毕设级别的优化,真实生产系统会用 Redis 缓存,但毕设做这种程度已经能展现思考深度了。
3.2 违章处理流程的状态机设计
违章处理是这套系统的灵魂功能,也是答辩时最能体现技术含量的一部分。我建议把它设计成一个简单的状态机:违章记录在录入时是待处理状态,处理员确认违章事实无误后,进入已处理状态并生成处理记录;如果录入时信息有误或者重复录入,可以作废,进入已撤销状态。
实现状态机时,我在 Service 层定义了清晰的状态流转方法,而不是直接在 Controller 里改状态字段:
@Service public class ViolationProcessService { @Transactional(rollbackFor = Exception.class) public void processViolation(ProcessRequestDTO dto) { ViolationRecord record = violationMapper.selectById(dto.getRecordId()); if (record == null || record.getStatus() != 0) { throw new BusinessException("该违章记录不存在或已被处理"); } // 更新违章记录状态 record.setStatus(1); violationMapper.updateById(record); // 插入处理记录 ViolationProcess process = new ViolationProcess(); process.setRecordId(record.getRecordId()); process.setHandlerId(CurrentUserHolder.getUserId()); process.setProcessType(dto.getProcessType()); process.setResult(dto.getResult()); process.setRemark(dto.getRemark()); processMapper.insert(process); } }这里有两个关键点,一个是事务,一个是逻辑校验。
@Transactional(rollbackFor = Exception.class)必须加上,因为更新违章状态和插入处理记录是两个独立的数据库操作,任何一个失败都不能留下半完成的脏数据。RollbackFor 默认只回滚 RuntimeException,rollbackFor = Exception.class 是为了把受检异常也纳入回滚范围,这是很多教程里没讲的细节。
逻辑校验也很重要。用户传过来的 recordId 如果对应的记录不存在,或者状态已经是已处理,那就是非法请求,必须抛异常拒绝掉。没做这层校验的第一个版本里,我遇到过前端连续点击两次提交按钮,导致同一条违章记录被处理了两遍的问题。那时候我才意识到,除了后端校验,前端按钮提交后还要置为 disabled 状态,双重保险才靠得住。
处理流程里的扣分和罚款逻辑,建议在 service 层一并处理。处理员确认处罚后,系统自动把 penaltyPoints 和 fineAmount 从违章记录表读取出来,写入处理结果并形成汇总信息展示给用户。扣分累计超过 12 分的提示逻辑也可以做,这会成为答辩时一个很好的加分亮点。
3.3 车辆与驾驶员信息的联动管理
车辆和驾驶员的管理模块,技术上最核心的就是"联动"两个字。录入违章记录时要选择车辆,选择了车辆之后,系统要能自动带出这辆车所属的驾驶员信息。这个联动用 mybatis-plus 的条件查询很好实现。
我的做法是,车辆表和驾驶员表之间做一个映射字段 driver_id 放在车辆表上,一辆车查询时连带把车主信息查出来:
public VehicleVO getVehicleDetail(Long vehicleId) { VehicleInfo vehicle = vehicleMapper.selectById(vehicleId); DriverInfo driver = driverMapper.selectById(vehicle.getDriverId()); VehicleVO vo = new VehicleVO(); BeanUtils.copyProperties(vehicle, vo); vo.setDriverName(driver.getName()); vo.setDriverPhone(driver.getPhone()); return vo; }这属于典型的单表查询加代码组装,比写复杂的多表 join 更好维护。我用这种方式做联查的经验是,在数据量不大、查询逻辑不复杂的毕设项目里,避免写长 SQL 是更明智的选择。因为一旦 SQL 写复杂了,MyBatis 的 XML 映射里各种占位符拼接就会成为新的 bug 源。
车辆和驾驶员的新增、修改、删除界面都有表单校验逻辑。车牌号要校验格式(省份简称+字母+数字的组合)、驾驶证号要校验位数、手机号要校验位数,这些校验在前端做一次,后端再校验一次。前端校验是为了用户体验,后端校验是为了数据安全,缺一不可。我说的"安全"不是指网络安全攻防,而是指这条数据进入数据库之前必须保证有效性,否则脏数据会污染所有关联的业务统计。
删除功能这块我还要提醒一句:车辆和驾驶员有关联的违章记录时,不能直接物理删除。我的做法是在删除前检查违反记录是否有关联数据,有关联就给出提示"该车辆存在违章记录,不能删除",你可以选择做逻辑删除(用一个 deleted 标志位顶替物理删除),这是生产系统里最常见的做法,答辩老师对这个设计通常是很认可的。
3.4 统计报表的实现思路
统计报表是交通违章管理系统里最能体现项目完成度的模块,我建议无论如何都要做。答辩时老师看到一堆表格页面和一堆图表页面,对项目的好感度会完全不一样。
我的统计模块主要在管理端,包括以下几个维度:按月份统计违章数量趋势、按违章类型统计占比、按区域统计违章分布、按驾驶员统计扣分排行。用 SpringBoot 做聚合查询的思路其实很朴素,在 Service 层写一个统计方法,使用 MyBatis-Plus 提供的selectMaps返回 map 列表:
public List<Map<String, Object>> countByMonth() { return violationMapper.selectMaps( new QueryWrapper<ViolationRecord>() .select("DATE_FORMAT(violation_time, '%Y-%m') as month", "COUNT(*) as total") .groupBy("month") .orderByAsc("month") ); }前端拿到这个 list 之后,用 ECharts 的折线图直接渲染出来就行。这块我的经验是,后端只负责给出结构化的统计结果,所有的图表样式、颜色、动画都属于前端的事,后端不要做任何跟展示相关的处理,职责边界要清晰。
这里我要特别提一下selectMaps的这个用法。它的返回结果是List<Map<String, Object>>,因为 select 子句里的字段都不是实体类的属性,MyBatis-Plus 会把它包装成 map。注意这里的 map 的 key 是 SQL 里起的别名(month、total),对外接口的字段命名讲究的是稳定,所以我在 Controller 层把统计结果的 key 重命名成了count对应的英文,这样前端不管怎么改,只要接口字段约定不变就行。
4. 前端页面与 API 接口设计
4.1 Thymeleaf 还是 Vue:前端选型实录
前端方案是毕设里一个比较纠结的问题。如果你没有学过前端框架,我建议直接选 Thymeleaf 模板引擎,它跟 SpringBoot 天然集成,后端 Controller 返回视图名,模板里通过th:each循环渲染 list,前端代码量小很多,学习成本低,一个人也能完得成。
如果你学过 Vue 或者有一定前端基础,选 Vue 拆前后端项目会让系统架构更现代,答辩的时候也更好讲。但代价是你需要处理跨域问题,要把前端的 8080 端口和后端的 8081 端口协调好,还要做接口联调,工作量至少多出百分之三十。
我最终选的是 Thymeleaf + 原生 JavaScript + Bootstrap 的组合。选这个方案的原因很朴素:我不需要额外安装 Node 环境、不用配置代理转发,整个 SpringBoot 项目打包成一个可运行的 jar 就够了。我的心态是"毕设的核心是把业务做完整,前端够用就行",实际用下来 Bootstrap 的栅格布局和表格样式让页面不至于太丑。
不管是哪种方案,有一个原则都要守住:页面上所有的数据展示和操作结果,都必须依赖后端接口返回的 JSON 数据,不能通过直接拼接 SQL 的方式从页面上取数。页面只是"视图","模型"必须由后端统一提供。
4.2 接口路径与返回格式的统一约定
接口设计直接影响前后端联调的效率。我最早做的时候接口路径很随意,一会儿/getList,一会儿/queryList,维护起来非常混乱。后来我给自己定了一套 RESTful 风格的规范:
| 功能 | 接口路径 | 请求方式 | 说明 |
|---|---|---|---|
| 违章分页查询 | /api/violations | GET | 参数 pageNum, pageSize, 各种筛选条件 |
| 违章详情 | /api/violations/{id} | GET | 路径参数传违章记录 ID |
| 新增违章记录 | /api/violations | POST | 请求体传 JSON 数据 |
| 处理违章 | /api/violations/{id}/process | POST | 违章处理动作单独一个接口 |
| 删除违章记录 | /api/violations/{id} | DELETE | 逻辑删除 |
返回格式上,我统一使用一个 Result 类包装:
public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> ok(T data) { ... } public static <T> Result<T> error(String message) { ... } }code 为 200 表示成功,其他为失败,配合 message 给前端展示错误提示。这套约定让前端的错误处理变得很简单,只需要判断 code 是不是 200 就行,不需要为每个接口单独写 try-catch。建议在做所有接口之前先把 Result 类写好,后面每个 Controller 方法都直接复用。
4.3 前后端联调最容易踩的三个坑
第一个坑是日期格式不一致。后端默认返回的 LocalDateTime 格式如果你没做处理,前端拿到的是时间戳或者带 T 的 ISO 格式字符串,跟页面上显示的年月日不是一回事。这个在前面实体类设计时已经说过了,加@JsonFormat就能解决,但要注意时区。
第二个坑是分页参数对不上。MyBatis-Plus 的 Page 对象在 JSON 序列化时,分页信息字段名是records、total、current、size,前端如果要展示"共 xx 条记录",得从 total 字段拿。有的同学一开始写的分页接口返回的是整个 Page 对象,前端不熟悉 MP 的字段命名,直接找不到总数在哪儿,白白来回传了好几趟。
第三个坑是 POST 请求的 content-type。如果前端用原生 ajax POST 提交 form 格式的数据,而后端 Controller 是用@RequestBody接收 JSON 对象,那就会报 415 Unsupported Media Type。我的经验是,毕设项目里统一用 JSON 字符串交互,前端提交前用JSON.stringify(data),请求头设Content-Type: application/json,后端用@RequestBody接收,这套组合最省心。
5. 部署上线与问题排查实录
5.1 本地跑通的最小环境清单
我整理一份能跑起整套系统的环境清单,做毕设的同学装好这个就能开始开发了:
| 软件 | 版本建议 | 用途 |
|---|---|---|
| JDK | 1.8 或 11 | Java 运行环境 |
| Maven | 3.6+ | 依赖管理和打包 |
| MySQL | 8.0 | 数据库 |
| IDEA | 2022 及以上 | 开发工具 |
| Navicat 或 DBeaver | 均可 | 数据库可视化工具 |
导入项目的步骤我这里不展开了,就说一个关键配置:application.yml 里的数据库连接串必须带时区参数。我推荐serverTimezone=Asia/Shanghai,如果不带这个参数,MySQL 8.x 会报 "The server time zone value" 的异常,这个错误几乎每个用 MySQL 8 的 SpringBoot 项目都会遇到。
项目跑起来之后先做一次完整的连通性测试:进管理员界面新建一个用户,再录一条车辆信息、一条驾驶员信息、一条违章记录,走一遍违章处理流程,最后看一眼统计图表是否正常。这五个动作覆盖了系统所有核心表,任何一个有问题都能及时暴露出,然后集中排查修复。
5.2 我遇到过的五个典型问题
这里把我踩过的坑整理成一张速查表,每一列都是从实际操作中记录的解决方案:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 启动时报端口被占用 | 上一次启动没有正常关闭 | 找到占用 8080 端口的进程并 kill 掉 |
| 数据库连接失败 | 密码错误或 MySQL 服务未启动 | 检查 application.yml 中的用户名密码配置 |
| 表单提交报 400 错误 | JSON 参数类型不匹配 | 检查前端传参的字段名和后端实体类是否一致 |
| 列表页分页不生效 | 没有配置 MyBatis-Plus 分页插件 | 新建一个 MybatisPlusConfig 配置类,添加分页拦截器 |
| 时间显示差 8 小时 | MySQL 时区配置问题 | 连接串加 serverTimezone=Asia/Shanghai,前端展示时统一用后端格式化后的字符串 |
分页不生效那个问题,我要着重说一下。很多人继承 BaseMapper 后发现selectPage方法返回的记录数就是全部数据,没有真正的分页效果,原因就是漏了 MybatisPlusConfig 里的分页插件配置。SpirngBoot 3.x 和 2.x 的配置写法有差异,你搜到的教程要确认跟你项目版本一致,不然照抄了还是报错。
5.3 让系统更稳的几个实用细节
系统能跑通只是第一步,稳定性和安全性是确保答辩顺利的关键。
第一个细节是密码加密。用户表的密码不能明文存储,SpringBoot 里最简单的方案是使用 BCryptPasswordEncoder 加密后入库,登录时再匹配。这个类是 Spring Security 的一部分,但单独引一个 spring-security-crypto 包就能用,不需要引入整套 Security。
第二个细节是登录拦截。没有登录的人不能直接访问管理端接口,做法是写一个 HandlerInterceptor 拦截所有 /api/** 请求,检查 session 里有没有用户信息,没有就返回 401 错误码。
第三个细节是全局异常处理。在 Controller 层加一个@RestControllerAdvice,统一捕获业务异常和系统异常,把异常信息包装成 Result 格式返回给前端,不要让堆栈信息直接暴露在前端页面。这个对你的代码可维护性和答辩观感都是加分项。
第二个细节和第三个细节是配套的,不写拦截器你的接口就是裸奔的,不写全局异常处理你的报错页面可能就是一堆英文堆栈,这两件事花半天时间做完,整个系统给人的感觉完全不一样。
这套系统做完之后,我对 SpringBoot 的整个开发链路有了完整的认知。从前端的页面渲染逻辑,到后端的接口处理和事务控制,再到数据库的表结构设计,每一个环节出了问题,你都能在这个系统里找到对应的排查路径。
最后再分享一个小经验,也是我踩过几次坑之后学到的:状态字段的取值含义一定要写注释,宁可注释写在实体类的每个字段上,也不要让后人(包括一个月后的自己)去猜 0 和 1 到底代表什么状态。我做第一版的时候没写注释,中间隔了两周回去改需求,自己都看不懂那个 status 字段什么意思了,只好去翻数据库用例数据才猜出来。
具体到交通违章管理系统,你还可以在基础功能上继续扩展,比如增加短信通知或邮件通知的违章提醒、增加微信小程序查询端、把统计报表升级成 ECharts 大屏展示,这些都是答辩时很有冲击力的扩展点。先把这套核心系统做扎实,后续的每一步扩展都能让你的项目上一个台阶。