简介:一套面向保险业务场景的Java企业级项目源码,适合Java开发者、后端工程师及求职者学习理赔业务系统的完整实现。项目覆盖客户提交理赔申请、业务处理、审核到赔付的主要流程,代码中应用了Spring/Spring Boot、MyBatis、MVC架构、RESTful API及Spring Security等主流技术,并涉及用户、保单、理赔申请与状态等表结构设计。压缩包共1729个文件,以js、html、css、png/gif等前端资源为主,同时包含java、class、xml、sql等后端与配置文件,附带jar、war及readme说明,整体约75.19MB,目录结构清晰便于查阅。目前已有158人学习下载。通过研读源码,开发者可以理解企业级理赔系统的分层设计、权限安全、异常处理与日志记录方式,并积累将Spring技术栈落地到实际业务场景的实战经验。
1. Java保险理赔系统:一套能直接跑起来的完整业务闭环
这套Java保险理赔系统源码,我拆完第一反应是:它没有把理赔做成一堆散装的CRUD,而是把报案、资料上传、核赔计算、财务支付拼成了一条完整闭环。适合两类人:一是中小保险公司、保险经纪公司的内部系统想快速落地,二是做外包的同学接保险类项目时拿来做底子改造。纯后端源码,数据库脚本、接口文档都在包里,跑起来就能用。对新手来说,它最大的价值是能让你看清楚一个真实业务系统里,状态是怎么流转的、金额是怎么算的、权限是怎么控的——这些在教程项目里基本学不到。下面按我拆包时的顺序,从架构、表设计、核心代码到部署和改扩展讲透。
2. 系统架构与技术选型:单体应用也把模块边界划清楚了
2.1 技术栈清单与选型原因
先看整体选型,源码里依赖管理用的是Maven,打包后是一个标准的Spring Boot可执行Jar。技术栈不花哨,但每一项都踩在真实业务系统的需求点上。
| 技术组件 | 在项目里的用途 | 为什么这么选 |
|---|---|---|
| JDK 1.8 + Spring Boot 2.x | 应用基础框架 | 国内保险行业存量系统大多是 JDK 8,二次开发时不会遇到环境墙 |
| MyBatis-Plus | 数据访问层 | 单表 CRUD 不用写 SQL,复杂统计走自定义 XML,效率和可控性平衡 |
| MySQL 5.7+ | 业务数据存储 | 理赔系统事务多,MySQL 的 InnoDB 行锁和事务隔离够用 |
| Redis | 幂等控制、热点数据缓存、登录凭证 | 报案和支付环节必须防重,Redis 的 setIfAbsent 是最简单的解法 |
| JWT + Spring Security | 登录鉴权与接口权限控制 | 前后端分离时 JWT 无状态,多个管理端共享同一套登录态 |
| MinIO(可选) | 理赔资料图片、PDF 存储 | 本地磁盘目录也可以,MinIO 模式留给部署方自己切换 |
我拆包时特意确认了一点:这套系统不是微服务,就是一个单体应用,但包结构按业务模块切得很干净。对保险理赔这种强事务、强流程的系统来说,单体起步反而更稳。省掉了服务间调用、分布式事务这些复杂度,把精力留给核赔规则和状态机,这笔账划算。
2.2 模块划分与调用链路
源码的顶层包结构大概是这样的,按业务域而不是按技术层划分:
com.example.claim ├── controller // 接口层,只做参数接收和结果封装 ├── service // 业务层,核心逻辑都在这层 │ ├── claim // 报案、受理、结案 │ ├── settlement // 赔款计算 │ ├── material // 资料上传与审核 │ └── policy // 保单查询与校验 ├── mapper // MyBatis-Plus 数据访问 ├── entity // 数据库实体 ├── common // 统一返回体、异常、工具类 └── config // 安全配置、Redis 配置、文件存储配置controller 层很薄,controller → service → mapper 的调用是单向的,service 之间允许互相调用。比如报案服务里会调用保单服务去校验保单有效性,核赔服务会调用理赔服务去读取历史案件。这样设计的好处是:业务闭环在一个 Service 里能看完整,而不是散落在多个微服务里来回跳。
一次理赔请求从进入到结案的链路是这样的:报案接口接收报案信息 → 校验保单号有效性和报案时限 → 生成理赔案件并置为待审核状态 → 业务员上传资料 → 系统自动做资料完整性校验 → 核赔员初审、复审 → 赔款计算器按险种规则算出金额 → 财务确认打款 → 案件状态置为已结案。链路里每一步都有状态记录,任何一个环节驳回,案件都会回到对应状态而不是直接断掉。
3. 数据库设计与核心表结构:保单、报案单、赔款怎么串起来
3.1 六张核心表的字段设计
打开数据库脚本,核心表一共六张:保单表、理赔案件表、理赔资料表、审核日志表、赔款结算表、系统用户表。字段设计不追求大而全,但每个表都围绕状态流转和金额安全做了必要约束。
理赔案件表是核心,字段设计如下:
CREATE TABLE `claim_case` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `case_no` varchar(32) NOT NULL COMMENT '案件编号,唯一索引', `policy_no` varchar(32) NOT NULL COMMENT '保单号', `insured_name` varchar(50) DEFAULT NULL COMMENT '被保险人姓名', `accident_time` datetime DEFAULT NULL COMMENT '出险时间', `report_time` datetime NOT NULL COMMENT '报案时间', `case_status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1待审核 2审核中 3已通过 4已驳回 5待补材料 6已结案', `apply_amount` decimal(12,2) DEFAULT NULL COMMENT '用户申请金额', `settle_amount` decimal(12,2) DEFAULT NULL COMMENT '最终赔付金额', `create_by` varchar(32) DEFAULT NULL COMMENT '报案人ID', `create_time` datetime NOT NULL, `update_time` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_case_no` (`case_no`), KEY `idx_policy_no` (`policy_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='理赔案件表';这里有个容易忽略的细节:case_no建了唯一索引。案件编号不是自增 ID,而是由业务规则生成,格式类似CL + 年月日 + 四位随机码。所有接口对外暴露的编号都是这个case_no,内部关联才用自增 ID。为什么要这样?因为自增 ID 会暴露业务量,而且批量导入数据时容易撞号,业务编号唯一索引能从源头防重。
金额字段全部用decimal(12,2),这一点是保险系统的红线。理赔金额涉及真金白银,任何用double存金额的做法在核赔计算时都可能出现精度偏差,后面第四章写赔款计算器时会看到具体处理。
3.2 案件状态机与非法流转校验
case_status字段是整个系统的灵魂,六种状态之间的流转有严格约束。源码里没有把这套约束散落在各个 Service 里,而是抽成了一个状态机校验工具:
public class CaseStatusMachine { // 状态流转表:key 是当前状态,value 是允许流转到的状态集合 private static final Map<Integer, Set<Integer>> TRANSITIONS = new HashMap<>(); static { TRANSITIONS.put(1, new HashSet<>(Arrays.asList(2, 4))); // 待审核 -> 审核中/驳回 TRANSITIONS.put(2, new HashSet<>(Arrays.asList(3, 4))); // 审核中 -> 通过/驳回 TRANSITIONS.put(4, new HashSet<>(Arrays.asList(1, 5))); // 驳回 -> 重新待审核/待补材料 TRANSITIONS.put(5, new HashSet<>(Arrays.asList(2))); // 待补材料 -> 重新审核 TRANSITIONS.put(3, new HashSet<>(Arrays.asList(6))); // 通过 -> 已结案 } public static void validate(int currentStatus, int targetStatus) { Set<Integer> allowed = TRANSITIONS.get(currentStatus); if (allowed == null || !allowed.contains(targetStatus)) { throw new BizException("非法的案件状态流转:" + currentStatus + " -> " + targetStatus); } } }注意这个状态机的几个关键点:已结案状态没有出口,案件一旦结案就不能再改任何信息,只能走线下人工处理。这对应了保险业务里的“结案归档”要求,防止资金风险。待补材料状态可以直接回到审核中,说明补材料后不需要重新走一遍初审,简化了流程。所有 Service 在更新case_status之前必须先调用validate做校验,这样即使未来新增了接口入口,状态流转也不会被绕过。
3.3 审核日志表:可追溯是合规底线
保险理赔系统除了功能要做出来,还要能回答一个问题:这个案子是谁在什么时候改的、为什么改。审核日志表就是为追溯而设计的:
CREATE TABLE `claim_audit_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `case_id` bigint(20) NOT NULL COMMENT '案件ID', `audit_user_id` bigint(20) NOT NULL COMMENT '审核人ID', `action` varchar(20) NOT NULL COMMENT '动作:SUBMIT/AUDIT/RETURN/PAY', `from_status` tinyint(4) DEFAULT NULL COMMENT '变更前状态', `to_status` tinyint(4) DEFAULT NULL COMMENT '变更后状态', `remark` varchar(500) DEFAULT NULL COMMENT '审核意见', `create_time` datetime NOT NULL, PRIMARY KEY (`id`), KEY `idx_case_id` (`case_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审核日志表';每次状态变更,Service 层都会插入一条日志,这个动作和业务更新放在同一个事务里。日志里记录了 from_status 和 to_status,即使后续要还原某个案件当时的审核现场,也能从日志里拼出完整链路。这块很容易被当成辅助功能忽略,但在真实给保险公司做系统时,审计日志是验收的硬指标。
4. 核心模块拆解:一次理赔请求从进入到结案的完整链路
4.1 报案接口:参数校验与幂等控制
报案是整个流程的入口,也是并发压力最大的一环。用户可能会因为网络超时重复点击报案按钮,如果后端不做幂等控制,同一个出险事件会被创建出多笔理赔案件。源码里报案接口的处理方式非常典型:
@RestController @RequestMapping("/api/claim") public class ClaimController { @Autowired private ClaimService claimService; @PostMapping("/report") public R<Long> report(@RequestBody @Validated ClaimReportRequest req) { // 参数校验由 @Validated 完成,业务层只处理核心逻辑 return R.ok(claimService.createCase(req)); } }Service 层的关键在幂等控制:
@Override @Transactional(rollbackFor = Exception.class) public Long createCase(ClaimReportRequest req) { // 1. 以报案号+保单号为维度生成幂等key,防止重复报案 String idempotentKey = "claim:report:" + req.getPolicyNo() + ":" + req.getReportNo(); Boolean first = redisTemplate.opsForValue() .setIfAbsent(idempotentKey, "1", 5, TimeUnit.MINUTES); if (first == null || !first) { throw new BizException("该报案号已提交,请勿重复操作"); } // 2. 校验保单是否存在且在保障期内 PolicyInfo policy = policyService.getValidPolicy(req.getPolicyNo()); if (policy == null) { throw new BizException("保单不存在或已失效"); } // 3. 创建案件,初始状态为待审核 ClaimCase claimCase = new ClaimCase(); claimCase.setCaseNo(generateCaseNo()); claimCase.setPolicyNo(req.getPolicyNo()); claimCase.setInsuredName(req.getInsuredName()); claimCase.setAccidentTime(req.getAccidentTime()); claimCase.setApplyAmount(req.getApplyAmount()); claimCase.setCaseStatus(1); claimCase.setReportTime(LocalDateTime.now()); claimCaseMapper.insert(claimCase); // 4. 记录操作日志 auditLogService.log(claimCase.getId(), null, 1, "SUBMIT", "用户提交报案"); return claimCase.getId(); }参数说明里要重点看两个点:第一,setIfAbsent加过期时间这一步是幂等的核心,key 存在时返回 false 表示重复请求,直接抛异常。过期时间设置 5 分钟,覆盖了用户从提交到看到结果的最长等待时间,同时避免 key 永久残留占用 Redis 内存。第二,@Transactional保证案件插入和日志记录要么都成功要么都回滚。这里有个血泪经验:幂等 key 的写入不能放在被事务管理的代码里,因为如果事务回滚了,key 却已经写入,用户重新提交会被误判为重复。常见做法是让 redis 操作在事务方法内先执行,事务回滚后再删除 key,或者干脆把幂等校验放在事务外层。
4.2 赔款计算器:策略模式按险种算钱
理赔系统里最容易被业务方反复改的就是赔款计算规则。今天这个险种要加免赔额,明天那个险种要调整报销比例,如果计算逻辑全部堆在一个 Service 里,改一次就要动一大片。这套源码用策略模式处理了这个问题:
public interface SettlementCalculator { // 判断当前计算器是否支持该险种 boolean support(String productCode); // 计算赔付金额,返回最终赔款数 BigDecimal calc(ClaimCase claimCase, PolicyInfo policy); }医疗险的计算器实现:
@Component public class MedicalSettlementCalculator implements SettlementCalculator { @Override public boolean support(String productCode) { return "MEDICAL".equals(productCode); } @Override public BigDecimal calc(ClaimCase claimCase, PolicyInfo policy) { // 申请金额 BigDecimal applyAmount = claimCase.getApplyAmount(); // 免赔额,比如 200 元以下不赔 BigDecimal deductible = policy.getDeductible(); // 报销比例,比如 80% BigDecimal rate = policy.getReimbursementRate(); // 计算公式:(申请金额 - 免赔额) * 报销比例,保留两位小数 BigDecimal result = applyAmount.subtract(deductible) .multiply(rate) .setScale(2, RoundingMode.HALF_UP); // 赔付金额不能超过保单保额上限 if (result.compareTo(policy.getCoverageLimit()) > 0) { result = policy.getCoverageLimit(); } return result; } }工厂类负责把计算器分发到对应险种:
@Component public class SettlementCalculatorFactory { @Autowired private List<SettlementCalculator> calculators; public SettlementCalculator getCalculator(String productCode) { return calculators.stream() .filter(c -> c.support(productCode)) .findFirst() .orElseThrow(() -> new BizException("未找到该险种的赔款计算规则")); } }calc方法里有一个保险业务里特别容易踩坑的细节:赔付金额不能超过保额上限。如果没有这层校验,遇到医疗费用特别高的案件,计算器可能算出比保单保额还高的赔付额。compareTo方法用于金额比较,在 Java 里 BigDecimal 不能用>直接比较,这也是新手经常翻车的地方。新增一个险种时,只需要写一个新的实现类并注册成 Spring Bean,工厂类自动就能发现它,不需要改动原有的任何分支判断。
4.3 资料上传与完整性校验:文件和案件绑定
理赔资料模块处理的是图片和 PDF,源码里文件存储的默认实现是本地磁盘目录,配置文件把上传路径指向一个可配置的目录。文件上传后生成一条资料记录,和案件 ID 关联。资料校验这里有一个容易被忽略的小设计:报案时只上传了清单,真正的资料文件可以分多次补传。
@Override public void uploadMaterial(Long caseId, MultipartFile file, Integer materialType) { // 校验案件是否存在且未结案 ClaimCase claimCase = claimCaseMapper.selectById(caseId); if (claimCase == null || claimCase.getCaseStatus() == 6) { throw new BizException("案件不存在或已结案"); } // 校验文件类型,只允许图片和PDF String suffix = StringUtils.substringAfterLast(file.getOriginalFilename(), "."); if (!Arrays.asList("jpg", "jpeg", "png", "pdf").contains(suffix.toLowerCase())) { throw new BizException("仅支持 jpg/png/pdf 格式的资料"); } // 保存文件到本地目录,文件名用 UUID 防止重名覆盖 String fileName = UUID.randomUUID().toString() + "." + suffix; File dest = new File(uploadPath + File.separator + fileName); file.transferTo(dest); // 插入资料记录 ClaimMaterial material = new ClaimMaterial(); material.setCaseId(caseId); material.setMaterialType(materialType); material.setFilePath(uploadPath + "/" + fileName); material.setUploadUserId(SecurityUtils.getCurrentUserId()); material.setCreateTime(LocalDateTime.now()); claimMaterialMapper.insert(material); }文件名用 UUID 重新生成,这个习惯很重要。如果直接用用户上传的原始文件名,可能遇到重名覆盖、文件名包含特殊字符导致路径异常等问题。资料类型materialType是业务方自定义的枚举:1 身份证、2 病历、3 发票、4 其他。审核环节会按这个类型检查是否齐全,比如医疗险案件必须同时有病历和发票才能进入核赔初审,这个检查逻辑在validateMaterialComplete方法里,不满足就直接返回待补材料,不会让人工审核员反复看缺材料。
5. 部署与避坑记录:从零把项目跑起来的过程与排查要点
5.1 环境准备与配置参数说明
要跑起这套源码,本地环境需要准备 JDK 1.8、Maven 3.6+、MySQL 5.7+、Redis 5.0+。拿到源码包先不要急着启动,按顺序做三件事:建库导入 SQL、改配置、确认 Redis 已启动。
数据库配置在application.yml里:
spring: datasource: url: jdbc:mysql://localhost:3306/claim_system ?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 password: database: 0 servlet: multipart: max-file-size: 10MB max-request-size: 50MB mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImplurl 里的serverTimezone=Asia/Shanghai几乎是必须的,MySQL 8.x 的驱动版本对时区很敏感,不加这一项启动时会直接报连接超时或时区错误。max-file-size限制单文件 10MB,max-request-size限制一次请求总大小 50MB,这两个值会被 Spring 的 Multipart 解析器强制执行,如果理赔资料里包含大体积的 PDF 病历,建议按实际需要调大。
5.2 启动步骤与接口验证
配置改完后,直接启动:
# 打包(跳过测试,本地跑不需要跑单测) mvn clean package -DskipTests # 启动 java -jar target/claim-system-1.0.0.jar启动成功后控制台会出现 Spring Boot 的启动日志和端口号,默认端口配置在application.yml的server.port,源码里默认是 8080。验证系统是否正常,不要先去点前端页面,先用两个接口做冒烟测试:登录接口获取 token,然后带 token 调报案接口。登录接口路径是/api/auth/login,POST 一个用户名和密码,返回结构里data.token就是后续接口要带的凭证。报案成功后数据库claim_case表里会多一条状态为 1 的记录,看到这条记录说明整条链路已经通透了。
如果启动时报端口占用,在启动命令里换掉端口:
java -jar target/claim-system-1.0.0.jar --server.port=8081注意这种方式只是临时覆盖端口,不会改配置文件里的默认值,适合本地调试多个实例时用。如果是部署到服务器,建议直接改application-prod.yml里的端口,不要用命令行参数,避免运维同事在启动脚本里找不到默认值。
5.3 高频踩坑记录:现象、原因、解决
我在拆包验证过程中遇到了几个比较典型的坑,写下来给要跑这套源码的同学当参考。
第一个坑:前端传的时间字段一直解析失败,接口直接报 400。
现象是报案接口接收accidentTime字段,前端传的是2025-06-01 14:30:00这种字符串,后端返回 “JSON parse error”。原因是实体里用的是LocalDateTime,Jackson 默认不支持这种格式字符串,需要显式声明格式。解决方法是给字段加上注解:
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime accidentTime;排查思路:先看请求日志里报错的具体字段,不要一上来就猜是数据库问题,JSON 解析错误只会在接口层出现。我一般会先用 Postman 直接传一次纯字符串和一次时间戳,对比两次的错误信息,基本就能定位是格式问题还是类型不匹配。
第二个坑:赔款金额算出来多了几分钱。
现象是同样的申请金额和报销比例,系统算出来的结果和业务人员用计算器按出来的不一致。原因是代码里有人在计算时用了double,浮点数乘法在二进制里无法精确表示,导致结果出现微小偏差。解决方法是全部金额计算改用BigDecimal,而且一定要在multiply之后调用setScale(2, RoundingMode.HALF_UP),否则金额位数可能超长。保险项目里金额一致的校验是最容易被验收方揪住的问题,出现金额对不上时优先排查代码里是否有double参与金额计算,这属于源头问题,比临时改数据有用得多。
第三个坑:重启系统后登录凭证全部失效,每个接口都报未授权。
现象是系统第一次启动后登录正常,重启过一次再登录就报 token 无效。原因是源码里的 JWT 密钥写死在了配置文件中,而且没有配置持久化,每次重启生成的新密钥签名了新的 token。解决方法是确认application.yml里的jwt.secret是否设置了固定值,如果为空则每次启动随机生成。实际项目中这个密钥应该放到环境变量或配置中心里,本地调试时固定下来至少能少踩一次坑。
第四个坑:上传图片提示成功但前端图片裂了。
现象是上传接口返回成功,但浏览器上图片不显示,打开图片 URL 显示 404。原因是源码默认的本地存储路径写的是相对路径,文件实际保存到了启动目录下,而前端的请求 URL 映射不到这个目录。解决方法是把上传路径改成绝对路径,同时给项目加一个静态资源映射,把/upload/**映射到磁盘的实际目录。这类问题在 Windows 上和 Linux 上表现还不一样,Windows 下相对路径拼出来的是反斜杠,Linux 下是正斜杠,排查时要先 console 打印一遍最终的文件路径,避免被路径分隔符干扰。
第五个坑:数据库连接一多就开始卡,偶尔报连接池超时。
现象是系统跑一阵子后接口响应变慢,报connection is not available。原因可能是默认的 HikariCP 连接池参数太小,业务高峰期不够用。解决的常见做法是把配置调整为:
spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000maximum-pool-size控制最大连接数,不是越大越好,MySQL 默认最大连接数是 151,应用连接池配置过大会反过来把数据库拖垮。20 是本地开发和中小业务量的合理取值,线上要根据压测结果来调。
6. 二次开发与改造思路:给系统加一款意外险产品的完整路径
拿到这套源码做二次开发,最常见的需求是扩展新的保险产品。以新增一款“意外险”为例,说清楚要动哪几个地方,这套系统的扩展性在哪个位置发力。
第一步在保单表或者险种参数表里新增险种定义,意外险的产品编码定为ACCIDENT,配置免赔额、保额上限、赔付比例等字段。第二步写一个新的赔款计算器:
@Component public class AccidentSettlementCalculator implements SettlementCalculator { @Override public boolean support(String productCode) { return "ACCIDENT".equals(productCode); } @Override public BigDecimal calc(ClaimCase claimCase, PolicyInfo policy) { // 意外险按伤残等级比例赔付,实际项目中伤残等级在审核阶段已确认 BigDecimal rate = new BigDecimal("0.3"); // 假设伤残等级对应30%赔付比例 return policy.getCoverageLimit() .multiply(rate) .setScale(2, RoundingMode.HALF_UP); } }第三步需要修改的地方是产品代码的校验逻辑,如果前端下拉框或者管理端配置里没有“意外险”这个选项,业务员在录单时根本选不到这个险种。把这几个点改完,重新打包启动,一个新产品就能走完整条理赔链路。不需要改报案接口、不需要改状态机、不需要动审核逻辑,这就是策略模式 + 状态机拆分带来的红利。
我拆这套源码的时候最大的感受是:一个系统的扩展性不是靠后期重构改出来的,而是在写状态机、写计算器工厂的时候就已经定好了边界。从那以后我自己写代码也有一个习惯——凡是变化频繁的规则点,先问一句“未来这里会不会新增”,然后强制自己走一遍“先定接口、再写实现、最后接工厂”的流程,希望能帮到你。
本文还有配套的精品资源,点击获取