ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于SpringBoot+MyBatis+MySQL的保险合同管理系统实战解析

基于SpringBoot+MyBatis+MySQL的保险合同管理系统实战解析 简介这套基于SpringBoot、MyBatis、Maven和MySQL的保险合同管理系统可用作一份完整的Java毕业设计项目面向计算机专业毕业生或需要全栈案例的开发者能有效解决选题无方向、源码不完整、配套文档缺失等问题。资源包为ZIP格式共727个文件、约15.66MB内含项目源码、SQL脚本、任务书和毕业论文具体包含93个Java后端类、46个Vue组件、156个JS脚本、162个SVG图标、49个CSS样式并附带启动运行脚本目录结构清晰便于分模块深入学习。系统功能覆盖用户登录注册、客户管理、合同管理、合同文件下载、保险合同统计报表饼图、公告留言、轮播图管理等整体功能丰富、界面简洁前端结合Vue与Element UI后端基于Spring Boot搭建在IDEA或Eclipse中配置MySQL即可直接运行非常适合毕业设计阶段学习。目前已有65人学习下载适合希望快速搭建可运行项目、掌握Spring Boot与Vue整合流程的读者借助源码、SQL脚本及论文文档可有效缩短开发周期并为答辩做好充足准备。1. 一个保险合同管理系统到底要管什么如果你在保险公司做过内部系统多半见过这样的场景业务员拿着一叠投保单找到开发说“帮忙改一下保单生效日期”再过两天理赔岗又要拉一份合同清单。保险合同管理系统的本质不是给 PDF 合同做个网盘而是把“投保、核保、承保、续期、理赔”这几个环节变成可查询、可计算、可追溯的数据库状态。用 SpringBootMyBatisMavenMySQL 来做这套后端既能在一周内搭出可运行的骨架也方便后续接报表和对账。这个组合对毕业设计和中小型企业内部工具都很合适因为你不需要引入太重的中台依赖管理交给 MavenSQL 交给 MyBatis事务交给 Spring。2. SpringBootMyBatisMavenMySQL 搭建保险合同管理系统的骨架2.1 为什么选这个组合而不是 JPA 或 MyBatis-Plus合同管理天生适合“半自动 SQL”。一个保单的查询往往带很多条件投保人姓名、证件号、险种编码、生效日期区间、状态。如果用 JPA 拼命写 Specification后期动态 SQL 会变得很难维护。MyBatis 允许你在 XML 里写where和if动态拼条件SQL 看着直观性能也好控制。SpringBoot 解决的是“装配”问题。它用起步依赖把内嵌 Tomcat、DataSource、自动配置全部塞进来你只需要在application.yml里写数据库连接参数。MyBatis 官方也提供了mybatis-spring-boot-starter不用再像 SSM 时代那样手工注册 SqlSessionFactory。MySQL 是这套组合里唯一要你单独安装的组件表结构设计得好不好直接决定整个项目能跑多久。有人问为什么不用 MyBatis-Plus。不是不能用而是如果这是一个教学型项目手写 Mapper 和 XML 能让你看清 SQL 执行过程一旦用 LambdaQueryWrapper反而掩盖了 MyBatis 的核心机制。实际工作中 MyBatis-Plus 很常见但先掌握原生 MyBatis后面切到 Plus 只需要半天。2.2 用 Maven 管理依赖关键坐标与版本陷阱Maven 在这里负责三件事依赖版本统一、构建产物、打包部署。保险合同管理系统需要的依赖不算多核心坐标如下表所示依赖坐标作用说明org.springframework.boot:spring-boot-starter-webWeb 容器与接口内置 Tomcat提供RestControllerorg.mybatis.spring.boot:mybatis-spring-boot-starterSpringBoot 与 MyBatis 集成自动配置 SqlSessionFactorycom.mysql:mysql-connector-jMySQL 驱动用 8.x 版本时驱动类为com.mysql.cj.jdbc.Driverorg.projectlombok:lombok减少实体类 getter/setter编译期生成代码org.springframework.boot:spring-boot-starter-test测试含 JUnit 5、MockMvc版本上要注意spring-boot-starter-parent不要盲目追高。很多人遇到“springboot版本太高”导致的报错多半是mybatis-spring-boot-starter还没适配最新的 SpringBoot 3.x。如果你看到Property sqlSessionFactory or sqlSessionTemplate are required先查依赖版本匹配。常见做法是用 SpringBoot 2.7.x 搭配 MyBatis Starter 2.3.xJDK 8 环境下非常稳定。Maven 仓库默认从中央仓库拉包国内网络环境可以配置阿里云镜像这个属于构建加速和运行无关。2.3 从 pom.xml 到启动类的完整骨架先看最小可用骨架。按惯例我会把项目拆成contract-admin和contract-core两个模块核心模块放实体、Mapper、ServiceWeb 模块放 Controller。但如果只是一个毕业设计规模单模块依然够用结构更清晰。单模块的骨架是这样的parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies启动类只有一个注解要加SpringBootApplication MapperScan(com.example.insurance.mapper) public class InsuranceApplication { public static void main(String[] args) { SpringApplication.run(InsuranceApplication.class, args); } }MapperScan可以省略但省略后每个 Mapper 接口都要标Mapper一旦 Mapper 多了代码很啰嗦。启动类所在包必须放在 Controller、Service、Mapper 的上级否则 SpringBoot 的组件扫描扫不到子包。你看到的许多“明明加了注解但 Spring 注入失败”的报错九成是包路径放错了。3. 保险合同内容建模MySQL 表结构与状态字段设计3.1 保单主表、投保人表、险种表怎么设计“合同”在数据库里不是一张表能装下的。一个完整的保险合同管理系统至少要有四类表合同主表insurance_contract、投保人表policy_holder、被保人表insured、险种表insurance_product。如果还有理赔流程再拆claim_record表。主表存合同编号、状态、保费、起止日期人员表存个人信息险种表存费率。这样设计是为了避免合同表里塞太多文本字段后期想按险种汇总保费时只靠合同表会写出很差的 SQL。一张能用的合同主表结构可以参考下面这个简表字段类型说明contract_idbigint主键自增contract_novarchar(32)合同编号唯一索引product_idbigint险种外键policy_holder_idbigint投保人外键insured_idbigint被保人外键statustinyint0草稿 1待核保 2有效 3终止premiumdecimal(18,2)保费金额用 decimalstart_datedate生效日end_datedate终止日金额字段千万不要用 float 或 double保险业务对精度要求很高这是面试里常考的点。合同编号建议不要用自增 ID因为对外展示时容易被遍历我会用yyyyMMdd 6位随机数生成并在数据库建唯一索引兜底。3.2 用 SQL 脚本初始化数据库和测试数据我习惯把 SQL 脚本放在项目sql/目录下不放进 main 资源目录。因为初始化脚本只在第一次建库时执行配置进 SpringBoot 的schema.sql反而不方便修改。一个带测试数据的脚本大致长这样CREATE DATABASE IF NOT EXISTS insurance DEFAULT CHARACTER SET utf8mb4; USE insurance; CREATE TABLE insurance_product ( product_id BIGINT PRIMARY KEY AUTO_INCREMENT, product_code VARCHAR(16) NOT NULL UNIQUE, product_name VARCHAR(64) NOT NULL, rate DECIMAL(6,4) NOT NULL COMMENT 基准费率 ) ENGINEInnoDB; CREATE TABLE insurance_contract ( contract_id BIGINT PRIMARY KEY AUTO_INCREMENT, contract_no VARCHAR(32) NOT NULL UNIQUE, product_id BIGINT NOT NULL, policy_holder_id BIGINT NOT NULL, insured_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 0, premium DECIMAL(18,2) NOT NULL, start_date DATE, end_date DATE, INDEX idx_status (status), CONSTRAINT fk_product FOREIGN KEY (product_id) REFERENCES insurance_product(product_id) ) ENGINEInnoDB;;utf8mb4比utf8多支持表情符号和更多字符MySQL 8 默认就是utf8mb4但建库时显式声明一次能避免老环境配置不同。外键在小项目里可以加但线上高并发保险系统一般会去掉外键用代码保证一致性。这个脚本适合开发环境生产环境初始化还要考虑权限账号。3.3 用状态字段管理合同流转合同状态是多变的最怕业务上出现“重复生效”和“状态乱跳”。一个实用的方案是给状态流转建立规则并且让 SQL 来兜底。比如核保通过后只允许待核保(1)变成有效(2)不允许从终止(3)直接变到有效(2)。在数据库层面可以用UPDATE ... WHERE status ?做乐观锁UPDATE insurance_contract SET status 2, start_date ?, end_date ? WHERE contract_id ? AND status 1;如果影响行数为 0说明当前状态不是待核保程序抛异常不允许继续执行。这种方式的优势是并发下不会发生两个线程同时把合同改成生效。很多项目管理做到后面状态字段会变成一个state_machine表来维护允许的流转边但对这个规模系统来说在代码里用枚举判断就够了。注意的是MySQL 的DECIMAL字段读出来是BigDecimal前端序列化时不要直接转 double否则金额可能丢精度。4. 在 SpringBoot 中用 MyBatis 实现保险合同的核心增删改查4.1 配置数据源与 MyBatis 日志SpringBoot 工程创建之后先把application.yml配好。我一般配置三块数据源、MyBatis、日志。下面是一个可复制的配置spring: datasource: url: jdbc:mysql://localhost:3306/insurance?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl logging: level: com.example.insurance.mapper: debugURL 里的serverTimezone必须设置否则 MySQL 8 的驱动会报时区错误。driver-class-name用com.mysql.cj.jdbc.Driver老的com.mysql.jdbc.Driver在新版驱动里已经被移除了。map-underscore-to-camel-case让你可以把contract_no自动映射到contractNo少写很多 resultMap。log-impl配StdOutImpl会在控制台把每条 SQL 打出来开发阶段调试特别好用这个就是很多人找的“mybatis配置打印”技巧。4.2 投保单创建和变更 Mapper 实现Mapper 接口的方法名和 XML 里的 id 要一致。先写一个ContractMapper接口public interface ContractMapper { int insertContract(Contract contract); Contract selectByContractNo(String contractNo); ListContract selectContractsByCondition(Param(productId) Long productId, Param(status) Integer status); }对应的 XML 放在resources/mapper/ContractMapper.xmlinsert idinsertContract parameterTypecom.example.insurance.entity.Contract useGeneratedKeystrue keyPropertycontractId INSERT INTO insurance_contract ( contract_no, product_id, policy_holder_id, insured_id, status, premium, start_date, end_date ) VALUES ( #{contractNo}, #{productId}, #{policyHolderId}, #{insuredId}, #{status}, #{premium}, #{startDate}, #{endDate} ) /insert select idselectContractsByCondition resultTypecom.example.insurance.entity.Contract SELECT * FROM insurance_contract where if testproductId ! null AND product_id #{productId} /if if teststatus ! null AND status #{status} /if /where ORDER BY contract_id DESC /select这里有几个要点#{}会被解析成 JDBC 的占位符?能防止 SQL 注入如果写成${}就是字符串拼接千万不要用在用户输入字段上。useGeneratedKeys是拿到数据库自增主键的关键否则 insert 之后contract对象的contractId还是 null后面做关联流程会踩坑。在selectContractsByCondition里where会自动去掉第一个多余的AND这是 MyBatis 动态 SQL 最常见的用法比11拼接要干净得多。若要实现分页可以手写LIMIT #{offset}, #{pageSize}但更推荐引入PageHelper。它通过拦截器在 SQL 后面自动拼LIMIT使用前要在依赖里加pagehelper-spring-boot-starter。实际开发中分页是刚需可以用它不过要搞懂它执行原理避免统计查询时出现奇怪的count。4.3 核保通过后的事务控制合同从“待核保”到“有效”这一步往往伴随多个写操作更新合同状态、生成一份批改记录、通知下游系统。任何一个失败都不能留下半成品所以要加Transactional。示例代码如下Service public class ContractService { private final ContractMapper contractMapper; public ContractService(ContractMapper contractMapper) { this.contractMapper contractMapper; } Transactional(rollbackFor Exception.class) public void approveContract(String contractNo, Date startDate, Date endDate) { Contract contract contractMapper.selectByContractNo(contractNo); if (contract null || contract.getStatus() ! 1) { throw new BusinessException(合同不存在或状态不允许核保); } int updated contractMapper.updateStatusDirect( contractNo, 2, startDate, endDate, 1 ); if (updated ! 1) { throw new BusinessException(核保冲突请刷新后重试); } contractMapper.insertContractChangeLog(contractNo, 核保通过); } }updateStatusDirect对应前面那条带status 1条件的 SQL。事务生效的前提是方法必须被 Spring 代理调用Transactional放在 Controller 里也是可以的但更合理的粒度是 Service 方法。同一个类里内部调用this.approveContract()会让注解失效这是常见的“事务不生效”原因因为没有走代理对象。rollbackFor Exception.class值得单独说。Spring 默认只对运行时异常回滚如果业务代码抛的是受检异常比如IOException事务反而不会回滚。保险系统里有很多外部接口调用一定要显式指定rollbackFor或者把受检异常包成运行时异常。另外selectByContractNo和updateStatusDirect之间的并发靠数据库条件更新来保证这是乐观锁的简化版。5. 保险合同管理系统的验证、打包与常见坑5.1 用测试类和 curl 验证接口接口写好后不要直接启动 SpringBoot 项目用浏览器点。我一般先写一个SpringBootTest集成测试把 Mapper 和 Service 都拉起来跑一遍SpringBootTest class ContractMapperTest { Autowired private ContractMapper contractMapper; Test void testInsertAndQuery() { Contract contract new Contract(); contract.setContractNo(20260601A000001); contract.setProductId(1L); contract.setPolicyHolderId(1L); contract.setInsuredId(1L); contract.setStatus(0); contract.setPremium(new BigDecimal(1280.00)); contractMapper.insertContract(contract); assertNotNull(contract.getContractId()); Contract call contractMapper.selectByContractNo(20260601A000001); assertEquals(0, call.getStatus()); } }测试通过后再启动应用用curl验证 HTTP 接口是否真的通curl -X POST http://localhost:8080/contract \ -H Content-Type: application/json \ -d {contractNo:20260601A000002,productId:1,policyHolderId:2,insuredId:2,premium:99.00}注意 POST 参数里的premium是数字后端收到后如果直接强转Double会产生99.00变成99.0的显示问题。Java 端应该用BigDecimal接收Jackson 在处理数字时也要注意反序列化配置。5.2 Maven 打包和 MySQL 适配的坑打包命令没什么特别的mvn clean package -DskipTests生成target目录下的 jar 后用java -jar target/insurance-0.0.1-SNAPSHOT.jar启动。这一步常见的坑有三个第一SpringBoot 多模块项目打出的 jar 不是可执行 jar需要在主模块的 pom.xml 里配置spring-boot-maven-plugin的repackage执行目标第二MySQL 连接时Public Key Retrieval is not allowed报错需要在 URL 后面加allowPublicKeyRetrievaltrue第三打包时测试类因为访问数据库失败导致构建失败可以用-DskipTests跳过测试编译。如果用的是 MySQL 5.7驱动版本不能太高mysql-connector-j8.x 对旧库兼容但 URL 里的字符集参数要写characterEncodingutf8。如果本机并没有安装 MySQL跑项目会报Access denied for user rootlocalhost和代码无关先去确认mysql安装配置教程里的账号密码、端口、权限三条是否都对得上。5.3 防止 MyBatis 缓存和 N1 查询拖垮列表页MyBatis 的一级缓存默认开启作用范围是同一个 SqlSession。在 Spring 中每次数据库操作都会重新获取 SqlSession所以一级缓存通常感受不到问题反而要小心二级缓存不明确开启的状态。如果你在 XML 里加了cache/同一 Mapper 的查询结果会共享合同数据一旦被更新别的事务可能读到旧值。我的建议是合同管理系统里不要开 MyBatis 二级缓存需要缓存就用 Redis放在 Service 层。另一个高频问题是 N1。查询合同列表时如果先查 10 条合同再循环查每一条的险种名称就会产生 11 条 SQL。更简单的做法是直接写连表查询再封装成视图对象select idselectContractVOList resultTypecom.example.insurance.vo.ContractVO SELECT c.contract_no, p.product_name, c.status, c.premium FROM insurance_contract c LEFT JOIN insurance_product p ON c.product_id p.product_id WHERE c.status #{status} /select如果列表数据量很大分页查询一定要在 SQL 层面做不要先查全表再在内存中截取。MySQL 的LIMIT 100000, 20往后翻页越翻越慢可以用“延迟关联”优化先只查主键再回表查详情例如WHERE contract_id 上一页最大ID LIMIT 20。这类细节对保险这种历史数据增长很快的业务来说比任何框架技巧都有用。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进