《寿险核保模块详细技术文档》,专为 Java 开发人员深度设计,涵盖核保的业务定义、核心流程、系统架构、关键规则、合规要求,并附带完整可运行的 Java 代码示例,每行代码均配有中文注释说明,帮助你快速掌握核保模块的开发与维护要点。
📄 寿险核保模块深度技术文档
—— 业务定义、流程、规则引擎与 Java 实现详解
适用对象:Java 开发工程师、系统架构师、风控开发人员
版本:v2.1(基于寿险 2025 年核保系统架构)
更新时间:2025年4月
一、什么是核保?它的核心作用是什么?
✅ 核保(Underwriting)定义
核保是保险公司根据投保人提交的投保信息(如健康状况、职业、年龄、财务状况等),通过规则判断与风险评估,决定是否承保、以何种条件承保(如标准体、加费、延期、拒保)的核心风控流程。
🎯 核保的核心作用(四大价值)
| 作用 | 说明 |
|---|---|
| 风险控制 | 防止高风险客户逆向选择(如带病投保),避免理赔率失控 |
| 定价公平 | 对不同风险等级客户差异化定价(加费、除外责任),确保产品盈利 |
| 合规保障 | 满足银保监会“偿二代”对风险资本要求,避免监管处罚 |
| 客户体验 | 自动核保秒级响应,人工核保透明可追溯,提升信任感 |
💡 一句话总结:
“核保是保险公司的‘防火墙’——没有它,保费收得越多,赔得越惨。”
二、核保的业务流程全景图
graph TD
A[客户提交投保申请] --> B[自动核保引擎]
B --> C{是否需要人工介入?}
C -- 是 --> D[转人工核保工单]
C -- 否 --> E[生成核保结论]
D --> F[核保员审核资料]
F --> E
E --> G[输出核保结果]
G --> H[结果类型:标准体 / 加费 / 延期 / 拒保]
H --> I[同步至保单服务生成保单]
H --> J[若拒保,发送拒绝通知]
🔍 核保四类结论详解(业务规则)
| 结论类型 | 含义 | 示例场景 | Java 枚举值 |
|---|---|---|---|
STANDARD | 标准体 | 年轻健康、无病史、职业非高危 | STANDARD |
ADDITIONAL_PREMIUM | 加费 | 有高血压、BMI超标、高风险职业 | ADDITIONAL_PREMIUM |
DEFERRED | 延期 | 近期做过手术、待体检报告 | DEFERRED |
REJECTED | 拒保 | 患有癌症、严重遗传病、投保欺诈 | REJECTED |
三、核保的核心业务规则(规则引擎核心)
核保依赖数百条业务规则,这些规则由精算、风控、合规团队制定,开发人员需将其代码化、可配置化、可测试化。
📜 典型核保规则清单(示例)
| 规则编号 | 规则描述 | 触发条件 | 结论 |
|---|---|---|---|
| U001 | 职业风险 | 职业代码 ∈ {1001, 1002}(高空作业、矿工) | ADDITIONAL_PREMIUM |
| U002 | BMI超标 | BMI ≥ 30 且 年龄 ≥ 40 | ADDITIONAL_PREMIUM |
| U003 | 既往病史 | 健康告知中勾选“糖尿病” | REJECTED(1型) / ADDITIONAL_PREMIUM(2型) |
| U004 | 投保年龄 | 年龄 > 70 | REJECTED |
| U005 | 重复投保 | 同一被保人3个月内投保≥3次 | DEFERRED(防欺诈) |
| U006 | 体检异常 | 体检报告中“肝功能异常” | DEFERRED(待复检) |
| U007 | 无健康告知 | 未填写健康问卷 | REJECTED(合规风险) |
⚠️ 注意:规则可能随监管政策动态调整,必须支持热加载,禁止硬编码!
四、系统架构设计(Java 微服务视角)
✅ 核保服务核心组件
| 组件 | 技术选型 | 说明 |
|---|---|---|
| 核心服务 | Spring Boot + Spring Web | 提供 /underwrite REST 接口 |
| 规则引擎 | Drools 7.70+ | 支持 .drl 规则文件热加载 |
| 数据访问 | MyBatis + Druid | 连接 Oracle 核保历史表 |
| 外部调用 | Feign + Hystrix | 调用体检、医保系统(熔断降级) |
| 缓存 | Redis | 缓存客户历史核保记录(减少重复查询) |
| 日志 | Logback + ELK | 全链路 TraceID 追踪 |
| 配置中心 | Nacos | 动态加载规则开关、阈值参数 |
| 监控 | Prometheus + SkyWalking | 监控核保成功率、响应时延 |
五、核心 Java 代码实现(含超详细中文注释)
✅ 项目结构建议:
src/main/java/com/pingan/life/underwriting/
├── config/ # 配置类
│ ├── DroolsConfig.java
│ └── UnderwritingProperties.java
├── model/ # 实体类
│ ├── Application.java
│ ├── UnderwritingResult.java
│ └── HealthDeclaration.java
├── rule/ # Drools 规则文件
│ └── underwriting-rules.drl
├── service/ # 核心服务
│ ├── UnderwritingService.java
│ └── ExternalDataClient.java
├── repository/ # 数据访问
│ └── UnderwritingHistoryRepository.java
└── controller/ # API入口
└── UnderwritingController.java
📁 1. Application.java —— 投保申请实体(核心输入)
package com.pingan.life.underwriting.model;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
/**
* 投保申请实体(核保输入核心对象)
* 由前端投保页面提交,包含所有用于核保判断的字段
*
* 注意:所有字段必须经过前端校验 + 后端二次校验
*
* @author 平安寿险核保开发组
* @since 2025-04
*/
@Data
public class Application {
// 唯一申请编号,用于追踪
private String applicationNo;
// 投保人身份证号(用于关联客户画像)
private String applicantIdCard;
// 被保险人身份证号(核保核心对象)
private String insuredIdCard;
// 被保险人姓名
private String insuredName;
// 被保险人年龄(计算规则:当前年 - 出生年)
private Integer insuredAge;
// 被保险人性别
private String gender; // "M" 男, "F" 女
// 身高(厘米)
private Integer heightCm;
// 体重(公斤)
private Integer weightKg;
// BMI = 体重(kg) / (身高(m))²
// 核保规则会基于此值判断肥胖风险
public BigDecimal getBmi() {
if (heightCm == null || heightCm <= 0 || weightKg == null) {
return BigDecimal.ZERO;
}
double heightM = heightCm / 100.0;
return BigDecimal.valueOf(weightKg / (heightM * heightM)).setScale(2, BigDecimal.ROUND_HALF_UP);
}
// 职业代码(参考平安职业分类表,如 1001=矿工,1002=消防员)
// 来源:投保表单选择
private String occupationCode;
// 健康告知:JSON 格式,存储客户勾选的健康问题
// 示例:{"diabetes": true, "hypertension": false, "cancer": false, "surgery": "2024-01-15"}
private HealthDeclaration healthDeclaration;
// 投保金额(保额),用于判断是否触发“高保额复核”规则
private BigDecimal sumAssured;
// 投保渠道(用于反欺诈分析)
private String channel; // "AGENT", "APP", "BANK", "TELEMARKETING"
// 投保时间(用于检测重复投保)
private LocalDate applicationDate;
// 历史投保次数(从缓存/数据库读取)
private Integer previousApplicationsInLast90Days;
// 体检报告ID(若有,用于调用外部系统)
private String medicalReportId;
// 扩展字段:用于未来扩展
private String extensionJson;
}
📁 2. HealthDeclaration.java —— 健康告知数据模型
package com.pingan.life.underwriting.model;
import lombok.Data;
/**
* 健康告知信息模型
* 客户在投保时勾选的健康问题,是核保最关键的输入数据
* 使用 JSON 存储,便于灵活扩展
*
* 注意:所有健康问题均为“是否曾患/曾接受治疗”
* 不是“当前是否患病”——避免误导
*
* @author 平安寿险核保开发组
*/
@Data
public class HealthDeclaration {
// 是否患有糖尿病(1型或2型)【核心规则U003】
private Boolean diabetes;
// 是否患有高血压(收缩压≥140或舒张压≥90)【核心规则U003】
private Boolean hypertension;
// 是否曾患癌症、肿瘤、白血病等恶性疾病【核心规则U003】
private Boolean cancer;
// 是否曾接受过重大手术(如心脏搭桥、器官移植)【核心规则U006】
private Boolean surgery;
// 手术时间(格式:yyyy-MM-dd),若为空表示未手术
private String surgeryDate;
// 是否有肝功能异常、乙肝、丙肝等【核心规则U006】
private Boolean liverDisease;
// 是否有精神疾病史(抑郁症、精神分裂等)
private Boolean mentalDisorder;
// 是否有家族遗传病史(如遗传性心脏病)
private Boolean hereditaryDisease;
// 是否吸烟(日均≥1支)
private Boolean smoker;
// 是否饮酒(每周≥3次)
private Boolean drinker;
// 是否有其他疾病(客户填写)
private String otherDiseases;
// 是否完整填写健康告知?若为null或空,触发拒保规则U007
public boolean isCompleted() {
// 至少填写了1项健康问题才算完整
return this.diabetes != null ||
this.hypertension != null ||
this.cancer != null ||
this.surgery != null ||
this.liverDisease != null ||
this.mentalDisorder != null ||
this.hereditaryDisease != null;
}
}
📁 3. UnderwritingResult.java —— 核保结论输出模型
package com.pingan.life.underwriting.model;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 核保结论输出模型
* 核保引擎的最终输出,将被保单服务消费
*
* 所有字段均为业务语义,非数据库字段
*
* @author 平安寿险核保开发组
*/
@Data
public class UnderwritingResult {
// 关联的投保申请编号
private String applicationNo;
// 核保最终结论(枚举)
private UnderwritingStatus status;
// 核保结论描述(用于客户通知)
private String message;
// 若为加费,此处记录加费比例(如 0.2 表示加20%)
private BigDecimal additionalPremiumRate;
// 若为延期,记录建议复检日期
private String deferredDate;
// 若为拒保,记录拒保原因代码(用于内部分析)
private String rejectionCode;
// 核保引擎执行耗时(毫秒),用于性能监控
private Long executionTimeMs;
// 核保执行时间
private LocalDateTime executedAt;
// 触发的规则ID列表(用于审计和规则优化)
private List<String> triggeredRules;
// 是否需要人工审核(true时会生成工单)
private Boolean requireManualReview;
// 核保员ID(人工审核时填写)
private String underwriterId;
// 扩展字段
private String extensionJson;
/**
* 核保状态枚举(对应业务四类结论)
*/
public enum UnderwritingStatus {
STANDARD, // 标准体:直接承保
ADDITIONAL_PREMIUM, // 加费:需多交保费
DEFERRED, // 延期:需等待体检/观察
REJECTED // 拒保:不予承保
}
}
📁 4. UnderwritingRules.drl —— Drools 规则文件(核心!)
路径:
src/main/resources/rules/underwriting-rules.drl
package com.pingan.life.underwriting.rule
import com.pingan.life.underwriting.model.Application;
import com.pingan.life.underwriting.model.HealthDeclaration;
import com.pingan.life.underwriting.model.UnderwritingResult;
// =========================================================
// 🚨 规则引擎说明:
// 1. 所有规则使用 "when-then" 结构
// 2. 每条规则必须有唯一 ID,便于追踪
// 3. 使用 salience 控制优先级(数字越大越先执行)
// 4. 使用 retract() 移除事实,避免冲突
// 5. 所有规则必须可测试、可热加载
// =========================================================
// ==================== 规则 U001:高风险职业 ====================
rule "U001: High-Risk Occupation - 1001/1002"
salience 100 // 高优先级,先于年龄、BMI判断
when
$app : Application( occupationCode in ("1001", "1002") ) // 矿工、消防员
then
// 设置加费结论
UnderwritingResult result = new UnderwritingResult();
result.setApplicationNo($app.getApplicationNo());
result.setStatus(UnderwritingResult.UnderwritingStatus.ADDITIONAL_PREMIUM);
result.setMessage("因职业为高风险类别(" + $app.getOccupationCode() + "),需加收20%保费");
result.setAdditionalPremiumRate(BigDecimal.valueOf(0.2));
result.getTriggeredRules().add("U001");
result.setExecutionTimeMs(System.currentTimeMillis() - $app.getCreateTime().getTime()); // 假设应用有创建时间
insert(result); // 插入结果到规则引擎工作内存
// 注意:不立即结束,允许其他规则继续评估(如同时存在BMI超标)
end
// ==================== 规则 U002:BMI超标 ====================
rule "U002: BMI Over 30 for Age >= 40"
salience 90
when
$app : Application(
// BMI ≥ 30 且年龄 ≥ 40
$bmi : BigDecimal(this >= 30) from this.bmi,
$age : Integer(this >= 40) from this.insuredAge
)
then
UnderwritingResult result = new UnderwritingResult();
result.setApplicationNo($app.getApplicationNo());
result.setStatus(UnderwritingResult.UnderwritingStatus.ADDITIONAL_PREMIUM);
result.setMessage("因BMI指数为" + $bmi + "且年龄≥40岁,需加收15%保费");
result.setAdditionalPremiumRate(BigDecimal.valueOf(0.15));
result.getTriggeredRules().add("U002");
insert(result);
end
// ==================== 规则 U003:糖尿病/癌症 ====================
rule "U003: Diabetes or Cancer - Reject or Add Premium"
salience 80
when
$app : Application(
$health : HealthDeclaration(
cancer == true // 癌症直接拒保
)
)
then
UnderwritingResult result = new UnderwritingResult();
result.setApplicationNo($app.getApplicationNo());
result.setStatus(UnderwritingResult.UnderwritingStatus.REJECTED);
result.setMessage("被保险人曾患癌症,依据监管规定予以拒保");
result.setRejectionCode("CANCER_HISTORY");
result.getTriggeredRules().add("U003-CANCER");
insert(result);
// 拒保后不再执行其他规则(可选:使用 halt() 终止引擎)
// halt(); // 可选,但不建议,保留多规则评估能力
end
rule "U003: Diabetes Type 2 - Add Premium"
salience 79 // 比癌症规则低,仅当癌症未触发时才执行
when
$app : Application(
$health : HealthDeclaration(
diabetes == true,
cancer == false // 明确排除癌症,避免冲突
)
)
then
UnderwritingResult result = new UnderwritingResult();
result.setApplicationNo($app.getApplicationNo());
result.setStatus(UnderwritingResult.UnderwritingStatus.ADDITIONAL_PREMIUM);
result.setMessage("被保险人患有2型糖尿病,需加收25%保费");
result.setAdditionalPremiumRate(BigDecimal.valueOf(0.25));
result.getTriggeredRules().add("U003-DIABETES");
insert(result);
end
// ==================== 规则 U004:年龄超限 ====================
rule "U004: Age > 70 - Reject"
salience 70
when
$app : Application( insuredAge > 70 )
then
UnderwritingResult result = new UnderwritingResult();
result.setApplicationNo($app.getApplicationNo());
result.setStatus(UnderwritingResult.UnderwritingStatus.REJECTED);
result.setMessage("被保险人年龄超过70岁,超出本产品承保年龄范围");
result.setRejectionCode("AGE_LIMIT_EXCEEDED");
result.getTriggeredRules().add("U004");
insert(result);
end
// ==================== 规则 U005:重复投保 ====================
rule "U005: Multiple Applications in 90 Days - Deferred"
salience 60
when
$app : Application( previousApplicationsInLast90Days >= 3 )
then
UnderwritingResult result = new UnderwritingResult();
result.setApplicationNo($app.getApplicationNo());
result.setStatus(UnderwritingResult.UnderwritingStatus.DEFERRED);
result.setMessage("近90天内累计投保≥3次,存在欺诈嫌疑,需延期审核");
result.setDeferredDate("2025-04-20"); // 建议3天后复审
result.setRequireManualReview(true); // 强制转人工
result.getTriggeredRules().add("U005");
insert(result);
end
// ==================== 规则 U006:体检异常 ====================
rule "U006: Liver Disease or Surgery - Deferred"
salience 50
when
$app : Application(
$health : HealthDeclaration(
liverDisease == true || (surgery == true && surgeryDate != null && surgeryDate.compareTo("2024-01-01") > 0)
)
)
then
UnderwritingResult result = new UnderwritingResult();
result.setApplicationNo($app.getApplicationNo());
result.setStatus(UnderwritingResult.UnderwritingStatus.DEFERRED);
result.setMessage("体检显示肝功能异常或近半年内有手术史,需等待复检报告");
result.setDeferredDate("2025-05-15");
result.setRequireManualReview(true);
result.getTriggeredRules().add("U006");
insert(result);
end
// ==================== 规则 U007:未填写健康告知 ====================
rule "U007: Health Declaration Not Completed - Reject"
salience 40
when
$app : Application(
$health : HealthDeclaration(
// 未填写任何健康问题
this.diabetes == null &&
this.hypertension == null &&
this.cancer == null &&
this.surgery == null &&
this.liverDisease == null &&
this.mentalDisorder == null &&
this.hereditaryDisease == null
)
)
then
UnderwritingResult result = new UnderwritingResult();
result.setApplicationNo($app.getApplicationNo());
result.setStatus(UnderwritingResult.UnderwritingStatus.REJECTED);
result.setMessage("未填写健康告知信息,违反监管合规要求,予以拒保");
result.setRejectionCode("HEALTH_DECLARATION_MISSING");
result.getTriggeredRules().add("U007");
insert(result);
end
// ==================== 默认规则:标准体 ====================
// 所有规则未触发时,默认为标准体
rule "U999: Default - Standard"
salience -100 // 最低优先级,最后执行
when
not(UnderwritingResult()) // 没有任何规则产生结果
then
UnderwritingResult result = new UnderwritingResult();
result.setApplicationNo("UNKNOWN"); // 仅用于测试,实际由服务传入
result.setStatus(UnderwritingResult.UnderwritingStatus.STANDARD);
result.setMessage("符合承保标准,无需加费或延期");
result.getTriggeredRules().add("U999");
insert(result);
end
✅ 规则文件部署说明:
- 该文件放在
src/main/resources/rules/下- 使用 Drools KieScanner 实现热加载(无需重启服务)
- 生产环境通过 Nacos 推送
.drl文件到应用,自动重新加载
📁 5. DroolsConfig.java —— Drools 规则引擎配置(核心!)
package com.pingan.life.underwriting.config;
import org.kie.api.KieServices;
import org.kie.api.builder.KieBuilder;
import org.kie.api.builder.KieFileSystem;
import org.kie.api.builder.KieRepository;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
/**
* Drools 规则引擎配置类
* 负责加载所有 .drl 规则文件,构建 KieContainer
* 支持热部署:当 rules/ 目录下文件变更时,自动重载规则
*
* @author 平安寿险核保开发组
* @since 2025-04
*/
@Configuration
public class DroolsConfig {
// 从配置中心读取规则路径(支持多环境)
@Value("${drools.rule.path:rules/underwriting-rules.drl}")
private String rulePath;
// 规则文件路径列表(可扩展)
private static final List<String> RULE_FILES = Arrays.asList(
"rules/underwriting-rules.drl",
"rules/fraud-detection.drl" // 可扩展反欺诈规则
);
// 获取KieServices实例(Drools核心工厂)
private final KieServices kieServices = KieServices.Factory.get();
/**
* 构建 KieContainer(规则容器)
* 该 Bean 将被 UnderwritingService 注入使用
*
* 注意:生产环境建议使用 KieScanner 实现热更新
*/
@Bean
public KieContainer kieContainer() throws IOException {
KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
// 加载所有规则文件
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources("classpath*:" + rulePath);
for (Resource resource : resources) {
if (resource.exists()) {
// 将规则文件内容写入 KieFileSystem
kieFileSystem.write(
kieServices.resources()
.newInputStreamResource(resource.getInputStream())
.setTargetPath(resource.getFilename())
);
System.out.println("[Drools] 加载规则文件: " + resource.getFilename());
}
}
// 构建 KieBuilder,生成 KieModule
KieBuilder kieBuilder = kieServices.newKieBuilder(kieFileSystem);
kieBuilder.buildAll(); // 编译所有规则
// 如果有编译错误,抛出异常
if (kieBuilder.getResults().hasMessages(org.kie.api.builder.Message.Level.ERROR)) {
throw new IllegalStateException("Drools 规则编译失败: " + kieBuilder.getResults().toString());
}
// 返回可执行的 KieContainer
return kieServices.newKieContainer(kieBuilder.getKieModule().getReleaseId());
}
/**
* 提供 KieSession 工厂方法,供服务调用
* 每次核保请求都应创建新的 KieSession(线程安全)
*
* @return 新的 KieSession 实例
*/
@Bean
public KieSession kieSession(KieContainer kieContainer) {
return kieContainer.newKieSession(); // 每次调用创建新会话,避免状态污染
}
}
📁 6. UnderwritingService.java —— 核心业务服务(完整实现)
package com.pingan.life.underwriting.service;
import com.pingan.life.underwriting.model.*;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* 核保核心业务服务
* 负责接收投保申请,调用 Drools 规则引擎,输出核保结论
*
* 本服务是核保模块的“大脑”,所有规则执行入口
*
* @author 平安寿险核保开发组
* @since 2025-04
*/
@Service
public class UnderwritingService {
private static final Logger logger = LoggerFactory.getLogger(UnderwritingService.class);
@Autowired
private KieContainer kieContainer; // 从配置类注入
@Autowired
private ExternalDataClient externalDataClient; // 外部系统调用(体检、医保)
/**
* 核心核保方法:接收投保申请,返回核保结论
*
* 步骤:
* 1. 补充外部数据(如体检报告)
* 2. 创建 KieSession
* 3. 插入投保申请对象
* 4. 执行规则
* 5. 查询所有结论
* 6. 合并结果(避免多结论冲突)
* 7. 记录审计日志
* 8. 返回最终结论
*
* @param application 投保申请对象
* @return 核保结果
*/
public UnderwritingResult executeUnderwriting(Application application) {
long startTime = System.currentTimeMillis();
// 1. 补充外部数据(如:调用平安好医生获取体检报告)
if (application.getMedicalReportId() != null) {
HealthDeclaration health = externalDataClient.fetchHealthDataFromMedicalSystem(application.getMedicalReportId());
if (health != null) {
application.setHealthDeclaration(health);
}
}
// 2. 创建新的 KieSession(每次请求独立会话,线程安全)
KieSession kieSession = kieContainer.newKieSession();
try {
// 3. 插入投保申请对象(作为规则引擎的事实)
kieSession.insert(application);
// 4. 执行所有规则
int firedRules = kieSession.fireAllRules();
// 5. 查询所有生成的核保结果
List<UnderwritingResult> results = new ArrayList<>();
kieSession.getObjects().stream()
.filter(obj -> obj instanceof UnderwritingResult)
.map(obj -> (UnderwritingResult) obj)
.forEach(results::add);
// 6. 合并规则结果(优先级策略:拒保 > 延期 > 加费 > 标准体)
UnderwritingResult finalResult = mergeResults(results);
// 7. 设置元数据
if (finalResult != null) {
finalResult.setExecutionTimeMs(System.currentTimeMillis() - startTime);
finalResult.setExecutedAt(LocalDateTime.now());
finalResult.setApplicationNo(application.getApplicationNo());
// 记录核保日志(用于审计)
logger.info("[核保完成] 申请号: {}, 结论: {}, 触发规则数: {}, 耗时: {}ms",
finalResult.getApplicationNo(),
finalResult.getStatus(),
finalResult.getTriggeredRules().size(),
finalResult.getExecutionTimeMs()
);
} else {
// 未触发任何规则(理论上不会发生,因有U999兜底)
finalResult = new UnderwritingResult();
finalResult.setApplicationNo(application.getApplicationNo());
finalResult.setStatus(UnderwritingResult.UnderwritingStatus.STANDARD);
finalResult.setMessage("系统未识别到风险,自动通过");
finalResult.setTriggeredRules(List.of("U999"));
finalResult.setExecutionTimeMs(System.currentTimeMillis() - startTime);
finalResult.setExecutedAt(LocalDateTime.now());
}
// 8. 返回最终结论
return finalResult;
} catch (Exception e) {
logger.error("[核保异常] 申请号: {}, 错误信息: {}", application.getApplicationNo(), e.getMessage(), e);
// 异常时返回“延期”以保守处理
UnderwritingResult errorResult = new UnderwritingResult();
errorResult.setApplicationNo(application.getApplicationNo());
errorResult.setStatus(UnderwritingResult.UnderwritingStatus.DEFERRED);
errorResult.setMessage("系统核保服务异常,请转人工审核");
errorResult.setRequireManualReview(true);
errorResult.setExecutionTimeMs(System.currentTimeMillis() - startTime);
return errorResult;
} finally {
// 9. 释放 KieSession(重要!避免内存泄漏)
kieSession.dispose();
}
}
/**
* 合并多个核保结论(优先级策略)
* 优先级:REJECTED > DEFERRED > ADDITIONAL_PREMIUM > STANDARD
*
* 例如:若同时触发 U001(加费)和 U003(拒保),应选择拒保
*
* @param results 所有规则产生的结果
* @return 最终核保结论
*/
private UnderwritingResult mergeResults(List<UnderwritingResult> results) {
if (results.isEmpty()) {
return null;
}
// 按优先级排序:拒保 > 延期 > 加费 > 标准体
return results.stream()
.max((r1, r2) -> {
int priority1 = getPriority(r1.getStatus());
int priority2 = getPriority(r2.getStatus());
return Integer.compare(priority2, priority1); // 降序排列
})
.orElse(null);
}
/**
* 核保结论优先级映射(数字越大优先级越高)
*
* @param status 核保状态
* @return 优先级数值
*/
private int getPriority(UnderwritingResult.UnderwritingStatus status) {
switch (status) {
case REJECTED: return 4;
case DEFERRED: return 3;
case ADDITIONAL_PREMIUM: return 2;
case STANDARD: return 1;
default: return 0;
}
}
}
📁 7. UnderwritingController.java —— REST API 入口
package com.pingan.life.underwriting.controller;
import com.pingan.life.underwriting.model.Application;
import com.pingan.life.underwriting.model.UnderwritingResult;
import com.pingan.life.underwriting.service.UnderwritingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
/**
* 核保服务 REST API 控制器
* 提供 /underwrite 接口供投保服务调用
*
* 使用 Spring Validation 校验请求参数
*
* @author 平安寿险核保开发组
* @since 2025-04
*/
@RestController
@RequestMapping("/api/v1/underwriting")
public class UnderwritingController {
@Autowired
private UnderwritingService underwritingService;
/**
* 核保核心接口
*
* 请求示例:
* POST /api/v1/underwriting
* Content-Type: application/json
*
* {
* "applicationNo": "APP202504050001",
* "insuredIdCard": "110101198501011234",
* "insuredAge": 38,
* "heightCm": 175,
* "weightKg": 85,
* "occupationCode": "1001",
* "healthDeclaration": {
* "diabetes": false,
* "hypertension": true,
* "cancer": false,
* "surgery": null
* },
* "sumAssured": 500000,
* "channel": "APP"
* }
*
* 响应示例:
* {
* "applicationNo": "APP202504050001",
* "status": "ADDITIONAL_PREMIUM",
* "message": "因高血压,需加收15%保费",
* "additionalPremiumRate": 0.15,
* "triggeredRules": ["U002", "U003"]
* }
*
* @param application 投保申请
* @return 核保结果
*/
@PostMapping
public ResponseEntity<UnderwritingResult> underwrite(@Valid @RequestBody Application application) {
UnderwritingResult result = underwritingService.executeUnderwriting(application);
return ResponseEntity.ok(result);
}
}
六、测试用例示例(JUnit5)
package com.pingan.life.underwriting.service;
import com.pingan.life.underwriting.model.Application;
import com.pingan.life.underwriting.model.HealthDeclaration;
import com.pingan.life.underwriting.model.UnderwritingResult;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class UnderwritingServiceTest {
@Autowired
private UnderwritingService underwritingService;
@Test
void testHighRiskOccupation() {
Application app = new Application();
app.setApplicationNo("TEST-001");
app.setOccupationCode("1001"); // 矿工
app.setInsuredAge(35);
app.setHealthDeclaration(new HealthDeclaration());
UnderwritingResult result = underwritingService.executeUnderwriting(app);
assertNotNull(result);
assertEquals(UnderwritingResult.UnderwritingStatus.ADDITIONAL_PREMIUM, result.getStatus());
assertTrue(result.getTriggeredRules().contains("U001"));
assertEquals(BigDecimal.valueOf(0.2), result.getAdditionalPremiumRate());
}
@Test
void testCancerHistoryReject() {
Application app = new Application();
app.setApplicationNo("TEST-002");
app.setInsuredAge(45);
HealthDeclaration health = new HealthDeclaration();
health.setCancer(true); // 癌症史
app.setHealthDeclaration(health);
UnderwritingResult result = underwritingService.executeUnderwriting(app);
assertNotNull(result);
assertEquals(UnderwritingResult.UnderwritingStatus.REJECTED, result.getStatus());
assertTrue(result.getTriggeredRules().contains("U003-CANCER"));
}
@Test
void testNoHealthDeclarationReject() {
Application app = new Application();
app.setApplicationNo("TEST-003");
app.setHealthDeclaration(new HealthDeclaration()); // 所有字段为null
UnderwritingResult result = underwritingService.executeUnderwriting(app);
assertNotNull(result);
assertEquals(UnderwritingResult.UnderwritingStatus.REJECTED, result.getStatus());
assertTrue(result.getTriggeredRules().contains("U007"));
}
}
七、生产部署与运维建议
| 项目 | 建议 |
|---|---|
| 规则热加载 | 使用 KieScanner + Nacos 配置中心,实现 .drl 文件变更自动重载 |
| 性能监控 | 监控 fireAllRules() 耗时,超 500ms 报警 |
| 规则版本管理 | 每次发布规则使用 Git 提交,Tag 版本号(如 v1.2.3) |
| 灰度发布 | 先对 5% 流量启用新规则,验证后再全量 |
| 审计日志 | 所有核保结论写入 underwriting_audit_log 表,保留10年 |
| 回滚机制 | 支持一键回滚到上一版本规则文件 |
八、总结:核保模块开发核心要点
| 维度 | 要点 |
|---|---|
| 业务理解 | 核保不是“自动通过”,是“风险定价” |
| 规则设计 | 每条规则必须可解释、可测试、可追溯 |
| 代码实现 | 使用 Drools + KieSession 每次独立,避免状态污染 |
| 异常处理 | 核保失败必须返回“延期”,宁可保守,不可误承 |
| 合规底线 | 未填写健康告知 → 必须拒保(银保监会红线) |
| 性能优化 | 缓存客户历史核保记录(Redis),减少重复查询 |
✅ 黄金法则:
“核保系统的使命,不是让最多人买保险,而是让最该买的人买得起、买得安全。”
📎 附录:Drools 规则调试技巧
| 场景 | 方法 |
|---|---|
| 查看触发了哪些规则 | 在 then 中加 System.out.println("触发规则: " + drools.getRule().getName()); |
| 单步调试 | 使用 IntelliJ IDEA + Drools 插件,支持 .drl 断点调试 |
| 规则冲突排查 | 使用 kieSession.getAgenda().getAgendaGroup("agenda-name").getActivations() 查看激活列表 |
| 日志输出 | 在 application.yml 中设置 logging.level.org.drools=DEBUG |
✅ 结语:你写的每一条规则,都在守护一个家庭的未来
核保模块是寿险系统中最“沉默”但最关键的模块。
你写的 if (cancer == true),可能决定一个人能否获得保障;
你写的 additionalPremiumRate = 0.2,可能影响一个家庭的月供能力。
技术是冰冷的,但保险是温暖的。
273

被折叠的 条评论
为什么被折叠?



