Spring Boot汽车保险管理系统,这个选题在计算机毕业设计里算是出现频率相当高的一个。一方面是因为车险业务本身流程清晰、表结构好设计,另一方面是它覆盖了用户管理、车辆信息、保单录入、保费计算、理赔登记、统计报表这些典型业务场景,用来展示Spring Boot + MyBatis Plus + Vue这套主流技术栈再合适不过。
我做完这套系统大概用了三周时间,每天下班后写两三个小时。整体功能涵盖了客户管理、车辆档案、险种配置、保单管理、续保提醒、理赔登记和简单统计,前后端分离,接口文档都有。今天把完整的实现思路、数据库设计、核心代码和踩过的坑都整理出来,代码量不小,建议收藏后慢慢看。
1. 系统整体设计与技术选型
1.1 为什么选Spring Boot + MyBatis Plus + Vue
先说结论:这套技术栈放在当前找工作和毕业答辩场景下,性价比最高。
Spring Boot是目前Java后端开发的事实标准,面试必问,用它做毕设不会被质疑技术老旧。MyBatis Plus比原生MyBatis多了内置CRUD方法、分页插件、代码生成器,写业务代码的速度能快不少,对没多少项目经验的同学来说特别友好。前端选Vue 2 + Element UI,最直接的考虑是中文文档全、组件丰富,表格表单需求几乎不用自己造轮子,网上现成模板也多,三到五天就能把管理端页面搭完。
我见过不少人纠结是不是要用微服务,或者非要上Spring Cloud Alibaba。我的建议很直接:不要。单机单体就够了,微服务涉及的服务注册、配置中心、分布式事务,每一块都会把你拖进深坑。毕设的核心是讲清楚一个完整业务闭环,不是炫技。
1.2 系统功能模块划分
汽车保险管理系统说白了就是给保险公司业务员用的一个内部管理后台,核心是围绕“人-车-单-赔”这条业务线展开的。
我最终落地的模块划分如下:
- 登录与权限:基于JWT的认证授权,按角色区分管理员和业务员
- 客户管理:个人客户和企业客户的基本信息维护
- 车辆管理:车辆档案登记,包括车牌号、品牌型号、车架号、发动机号、注册日期、车辆价值等
- 险种管理:维护交强险、车损险、第三者责任险、车上人员责任险等险种及费率参数
- 保单管理:保单录入、审核、批改、退保、续保操作,支持PDF保单导出
- 理赔管理:报案登记、查勘信息录入、理赔审核、结案归档,关联原保单
- 统计报表:按月度展示保费收入、理赔支出、保单数量趋势
- 续保提醒:通过定时任务扫描即将到期的保单,自动生成待办提醒
模块数量控制在八到十个之间,答辩的时候每个功能都能说上话,不会出现某个模块做得很浅被老师追问后答不上来的尴尬。
1.3 技术栈版本选择与项目结构
版本选择上,我踩过一次坑,这里先给大家避雷:Spring Boot别用太高的版本。我用的是2.7.18,这是2.x系列的最终版本,稳定性和教程资料都最全。3.x系列虽然也已经很成熟,但很多老教程和老依赖还没有适配,遇到问题搜解决办法的时间成本会翻倍。
| 技术组件 | 版本选择 | 说明 |
|---|---|---|
| JDK | 1.8 | 企业存量项目最多的版本,兼容性最好 |
| Spring Boot | 2.7.18 | 2.x最终版本,资料全、稳定 |
| MyBatis Plus | 3.5.3 | 内置分页、代码生成、乐观锁插件 |
| MySQL | 8.0 | 5.7也能跑,但8.0更好用 |
| Redis | 可选 | 用户token可放Redis,简单项目也能不用 |
| Vue | 2.7 | Element UI生态最完善 |
| JWT | jjwt 0.11.5 | 用于无状态登录认证 |
| Hutool | 5.8.x | 日期、Excel等工具库,省代码 |
| Apache POI | 4.1.2 | 导出Excel,做统计报表用 |
后端项目结构采用标准的分层架构,按照包名就能看出代码职责:
com.insurance ├── controller # 接口层,接收参数、返回结果 ├── service # 业务逻辑层,核心业务处理 ├── mapper # 数据访问层,MyBatis Plus的Mapper接口 ├── entity # 数据库实体类 ├── dto # 前端传输对象,避免直接暴露实体 ├── vo # 视图对象,用于接口返回数据组装 ├── config # 配置类,如JWT拦截器、跨域配置 ├── common # 公共类,统一返回结果、异常处理 └── utils # 工具类2. 数据库设计与核心表结构
2.1 从业务角度设计数据表
数据库设计是整套系统的地基。我画E-R图的时候没有一上来就对着原型画表,而是先理清了业务关系:一个客户可以有多辆车,一辆车在一个时间段内对应一份保单,一份保单会关联多个险种,一份保单可能发生多次理赔。理清这个关系后,表结构自然就出来了。
核心数据表一共八张:
- sys_user:系统用户表,存放管理员和业务员账号
- customer:客户表,区分个人客户和企业客户
- vehicle_info:车辆信息表,通过customer_id关联客户
- insurance_type:险种定义表,存放险种名称、基础费率
- insurance_order:保单主表,记录保单号、被保车辆、起止日期、总保费、状态
- insurance_order_item:保单明细表,记录每份保单下买了哪些险种、各自的保费
- claim_record:理赔记录表,记录每个保单下的报案和理赔信息
- sys_log:操作日志表,记录关键操作,答辩时有用
2.2 保单主表设计要点
保单主表是整个系统的核心表,字段设计直接影响后面业务逻辑写的顺不顺畅。我最终的表结构大致如下(已简化):
CREATE TABLE `insurance_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '保单号', `customer_id` bigint(20) NOT NULL COMMENT '客户ID', `vehicle_id` bigint(20) NOT NULL COMMENT '车辆ID', `insurance_start_date` date NOT NULL COMMENT '保险生效日期', `insurance_end_date` date NOT NULL COMMENT '保险到期日期', `total_premium` decimal(10,2) NOT NULL COMMENT '总保费', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态 0待支付 1已生效 2已到期 3已退保', `operator_id` bigint(20) DEFAULT NULL COMMENT '操作员ID', `remark` varchar(500) DEFAULT NULL COMMENT '备注', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_customer_id` (`customer_id`), KEY `idx_vehicle_id` (`vehicle_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='保单主表';有两个设计细节想重点说一下。
第一个是金额字段必须用decimal(10,2),绝对不能用float或double。凡是涉及钱的小数运算,浮点数都会产生精度问题,这是财务系统的铁律。
第二个是状态字段用tinyint存数字状态码,而不是直接存字符串。用数字的好处是后端代码里可以定义常量或枚举来统一管理,前端再根据数字映射成对应的中文标签,数据层面也更省空间。状态流转通过后端Service统一控制,不会出现乱改状态的情况。
2.3 客户表设计:区分个人与企业
客户表我用了一个字段type来区分个人客户和企业客户。个人客户填身份证号、手机号、性别,企业客户填统一社会信用代码、法定代表人、注册地址。这里需要注意字段冗余的问题:两种客户的信息差异不大,没必要拆成两张表,用一张表加type字段是最简单的方案。
字段示例:
CREATE TABLE `customer` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `customer_no` varchar(32) NOT NULL COMMENT '客户编号', `customer_name` varchar(100) NOT NULL COMMENT '姓名或企业名称', `type` tinyint(4) NOT NULL COMMENT '客户类型 1个人 2企业', `id_card` varchar(18) DEFAULT NULL COMMENT '身份证号/信用代码', `phone` varchar(20) DEFAULT NULL COMMENT '联系电话', `address` varchar(200) DEFAULT NULL COMMENT '联系地址', `contact_person` varchar(50) DEFAULT NULL COMMENT '企业联系人姓名', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_customer_no` (`customer_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户表';3. 核心功能实现与代码要点
3.1 登录认证与权限控制
登录这块采用JWT方案。用户输入用户名密码后,后端校验通过,生成一个有效期为两小时的token返回给前端。前端把token存在localStorage,每次请求在请求头加Authorization字段,后端通过拦截器统一校验。
代码实现不复杂,核心就三步。
第一步,生成token:
public String generateToken(Integer userId, String username) { return Jwts.builder() .setSubject(username) .claim("userId", userId) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 2 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }第二步,写拦截器校验token:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains("/login")) { return true; } String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { response.setStatus(401); return false; } try { Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody(); request.setAttribute("userId", claims.get("userId")); return true; } catch (Exception e) { response.setStatus(401); return false; } }第三步,把拦截器注册到Spring MVC中,并配置放行路径。
这一步有个容易忽略的坑:跨域配置和拦截器注册的顺序会导致OPTIONS预检请求被拦截。我调试的时候发现前端请求一直报401,查了很久才发现是拦截器没有放行OPTIONS请求,加上之后就正常了。解决办法是拦截器里加上一句:如果是OPTIONS请求,直接返回true。
3.2 保费计算模块:从简单公式开始
保费计算是保险系统里最有技术含量的业务逻辑之一。真实保险公司的费率模型非常复杂,受到地区、车型、NCD系数、渠道折扣等多个因素影响,但作为毕业设计,不需要做到那么细,重点是体现计算逻辑的完整性和可扩展性。
我采用的方案是:基础保费乘以费率调整系数。
具体的计算规则:
- 交强险:家庭自用6座以下基础保费950元,连续未出险可下浮
- 车损险:车辆价值的1.5%左右
- 第三者责任险:保额50万对应保费约1200元,保额100万对应约1600元
- 车上人员责任险:每座保费约100元
每一种险种在insurance_type表中配置基础保费或费率参数,前端选择了险种和保额后,后端根据规则计算出各项保费,再汇总出总保费。
保费计算的Service代码示意:
public BigDecimal calculatePremium(CalculatePremiumDTO dto) { BigDecimal total = BigDecimal.ZERO; VehicleInfo vehicle = vehicleMapper.selectById(dto.getVehicleId()); // 车辆价值 BigDecimal vehicleValue = vehicle.getVehiclePrice(); for (InsuranceItemDTO item : dto.getItems()) { InsuranceType type = insuranceTypeMapper.selectById(item.getTypeId()); BigDecimal premium; switch (type.getCalcType()) { case "FIXED": // 固定金额 premium = type.getBasePremium(); break; case "RATE": // 按车辆价值比例 premium = vehicleValue.multiply(type.getRate()).setScale(2, RoundingMode.HALF_UP); break; case "BY_AMOUNT": // 按保额档位 premium = getPremiumByCoverage(type.getId(), item.getCoverage()); break; default: premium = BigDecimal.ZERO; } total = total.add(premium); } return total; }这里特别强调一下,所有金额计算必须使用BigDecimal,并且注意精度设置。计算过程中先用BigDecimal完成运算,最后统一调用setScale(2, RoundingMode.HALF_UP)保留两位小数。这个细节不仅在保费计算模块要用,统计报表的汇总金额也要用。
3.3 保单生成与PDF导出
保单号是这个系统里比较有辨识度的功能。我生成保单号的规则是:年份+三位业务员编号+五位流水号,比如2024 + 001 + 00001。用Redis自增或者数据库表的自增列生成流水号都可以,考虑到项目为了简单没有强制依赖Redis,我直接在数据库维护了一张流水号表,通过乐观锁更新保证并发下不重复。
PDF导出是答辩时很加分的功能点,因为能让老师直观看到系统输出真实业务单据。我用的是Apache POI,生成一个简化版的保单PDF文件,包含保单号、被保险人、车辆信息、保险期间、险种清单和总保费。如果在答辩现场打开这个PDF,效果比干巴巴的页面截图好很多。
3.4 理赔审核流程的状态机设计
理赔模块是整个系统里逻辑最复杂的部分,核心是状态流转。
我把理赔状态定义为五个:已报案、查勘中、审核中、已结案、已驳回。每一次状态变更都要求满足前置条件。比如只有审核中状态才能被驳回,已结案的理赔单不允许再被修改。
实现思路很直接,在Service里定义状态流转的方法,方法内先判断当前状态和目标状态是否允许流转,不允许就直接抛出业务异常:
public void auditClaim(Long claimId, boolean pass, String auditOpinion) { ClaimRecord claim = claimMapper.selectById(claimId); if (claim == null) { throw new BusinessException("理赔记录不存在"); } if (!ClaimStatusEnum.AUDITING.getValue().equals(claim.getStatus())) { throw new BusinessException("当前状态不允许审核操作"); } if (pass) { claim.setStatus(ClaimStatusEnum.FINISHED.getValue()); } else { claim.setStatus(ClaimStatusEnum.REJECTED.getValue()); } claim.setAuditOpinion(auditOpinion); claimMapper.updateById(claim); }这套逻辑虽然代码不复杂,但它体现的是一个合格程序员应有的状态机思维。我后期看自己代码的时候发现,如果没有在Service层严格控制状态流转,直接让前端传状态字段来更新,系统很容易产生脏数据。这也是答辩时老师可能会问到的点,提前想明白为什么这么设计。
3.5 到期续保提醒:定时任务实现
续保提醒功能用Spring Boot自带的@Scheduled注解就能实现。系统每天凌晨一点执行一次扫描,查询保险到期日期在30天内且状态仍为生效中的保单记录,为每条记录生成一条待办提醒,记录在remind表中。
定时任务代码简单清晰:
@Component public class InsuranceExpireJob { @Autowired private InsuranceOrderService orderService; @Scheduled(cron = "0 0 1 * * ?") public void checkExpiringOrders() { LocalDate today = LocalDate.now(); LocalDate expireDay = today.plusDays(30); List<InsuranceOrder> orders = orderService.list(new QueryWrapper<InsuranceOrder>() .eq("status", 1) .le("insurance_end_date", expireDay) .ge("insurance_end_date", today)); // 生成提醒记录 } }这里要注意一个使用@Scheduled的细节:默认情况下Spring Boot是单线程执行定时任务的,如果系统里同时挂了多个定时任务,它们会排队执行,一个任务阻塞会导致其他任务延迟。我在实际项目里就用到了两个定时任务,一个处理续保,一个做每日数据统计,所以我在配置类里把调度器改成线程池方式执行,避免相互影响。
4. 完整部署流程与源码使用
4.1 环境准备
先把运行环境准备好。JDK建议用8或11,数据库用MySQL 8.0,前端依赖Node环境。我用的是Maven进行依赖管理,IDEA开发,这些都是常规配置,不做赘述。
需要注意一个常见的兼容性问题:如果你的MySQL版本是8.0以上,JDBC驱动要选择com.mysql.cj.jdbc.Driver,如果用的仍然是com.mysql.jdbc.Driver,连接数据库的时候会直接报错。
4.2 导入项目与数据库初始化
源码里附带了完整的SQL初始化脚本,包含建表语句和测试数据。拿到项目后,第一步是在MySQL中执行sql/init.sql脚本完成建库建表。
然后修改application.yml配置中的数据库连接信息:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/insurance_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password server: port: 8080 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这里有个特别容易踩的坑:MySQL的serverTimezone必须设置为Asia/Shanghai,不设置的话,在插入日期类型数据时会发现数据时间比真实时间少了8个小时,这是因为数据库时区默认为UTC导致的。
4.3 前后端配置与联调
前端项目是用Vue CLI创建的,启动方式很常规:
npm install npm run serve前端开发服务器运行在8081端口,和后端8080端口不一样,所以必须配置跨域。我在后端写了一个CorsConfig配置类,允许8081端口访问,同时把JWT拦截器的配置路径也检查了一遍。
前端通过axios统一管理请求,在request.js里做了请求拦截器,每次请求自动从localStorage取token并放到请求头。登录失效时统一跳转到登录页。这块代码虽然简单,但能保证整个系统的请求规范和统一性。
4.4 打包部署演示
答辩前最好把系统打包成可执行jar,演示时直接一条命令启动,会显得很专业。执行以下命令完成打包:
mvn clean package -DskipTests java -jar target/insurance-system.jar前端打包同样简单,生成dist目录后,可以放在Nginx下作为静态资源部署。不过在答辩演示场景下,保持前后端分开运行反而更方便调试,所以是否部署到Nginx完全看个人时间和需求,不用强求。
5. 常见问题与排查技巧实录
5.1 数据库连接和时区问题
我开发过程中遇到的第一个比较诡异的问题是,插入数据后查看数据库,发现日期字段总是比实际时间早8个小时。我一开始以为是Java代码里时间序列化的问题,排查了很长时间,最后才发现是JDBC连接串里没有设置serverTimezone=Asia/Shanghai所致。这个问题看起来不大,但是不解决的话,所有的保单起止日期都会错一天,对保险系统来说是不可接受的数据错误。
5.2 金额计算精度问题
第二次踩坑是在写统计报表模块时,用Double类型累加保费,结果在计算年度合计时发现最后两位小数偶尔会出现莫名其妙的值。比如134.56加245.44,理论上应该等于380.00,实际却可能出现380.00000000000006之类的诡异结果。查了一圈,根源就是浮点数精度问题。从那之后,所有涉及金额的代码我全部换成了BigDecimal。
经验总结:写涉及金额或费率的代码,第一条原则就是禁用double和float,统一使用BigDecimal,并且在初始化时用字符串构造方法new BigDecimal("134.56"),而不是new BigDecimal(134.56)。
5.3 状态并发更新问题
第三个比较典型的问题是,在进行理赔审核时,如果两个管理员同时打开同一个理赔单,分别进行了不同操作,后提交的那个人会覆盖先提交的那个人的审核结果。这是典型的并发覆盖问题。
最简单的解决方案是给关键表加一个version字段,更新时在SQL语句中带上version条件,MyBatis Plus提供了现成的乐观锁插件,配置好后,在并发更新的场景下,后提交的请求会因为version不匹配而更新失败,从而保护数据不被覆盖。
5.4 前端跨域和拦截器冲突
还有一个前端联调阶段比较头疼的问题,就是前端调用后端接口时,第一次请求总是报401,刷新页面后又正常了。我反复排查后发现,这是因为前端请求会先发一个OPTIONS预检请求,而这个请求没有携带Authorization头,直接撞上后端JWT拦截器被拦截了。解决方法是拦截器里放行OPTIONS请求,或者在Spring CorsFilter中处理预检请求。这个问题如果你的项目也做了前后端分离,大概率会遇到。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 启动报数据库连接失败 | JDBC驱动类名错误或MySQL版本不匹配 | 检查驱动类名和pom中mysql版本 |
| 日期字段少8小时 | JDBC连接串未设置时区 | URL增加serverTimezone=Asia/Shanghai |
| 前端请求跨域报错 | 后端未配置CORS或配置冲突 | 编写CorsConfig并检查拦截器是否拦截预检 |
| 金额计算出莫名小数 | 使用了double/float类型 | 全部替换为BigDecimal并统一精度 |
| 登录后接口返回401 | token过期或拦截器未放行相关路径 | 检查token有效期及拦截器放行配置 |
| 前端页面样式错乱 | Element UI按需引入遗漏组件 | 改为完整引入或检查babel-plugin-component配置 |
| 打包jar包体积过大 | 依赖较多且未配置排除 | 使用spring-boot-maven-plugin打可执行胖包 |
| 数据库中文乱码 | 连接串未指定characterEncoding | URL添加characterEncoding=utf8 |
最后想说的
做完这个汽车保险管理系统,我最大的感受是:毕设项目最重要的不是功能堆得有多全,而是每一条业务线都是闭环的。客户进来能建档,车辆能关联,保单能从录入走到审核再到到期续保,理赔能从报案走到结案,这条链路只要走顺畅了,答辩时把你做的业务讲清楚,老师就没什么可挑剔的。
源码里我放了完整的数据库脚本、后端工程、前端页面和部署说明,拿到之后跟着文档跑起来并不是难事,但如果时间充裕,我更建议自己动手敲一遍核心代码。保费计算、状态流转、定时提醒这几个点只要亲手写过一次,后面面试聊项目时你会有底气得多。
最后再分享一个在做这个项目过程中对我帮助很大的习惯:代码里遇到拿不准的设计,先画一张简单的草图把流程理清楚,再动手写。画草图的时候就能发现很多逻辑漏洞,这笔时间花得非常值。