案例 17:保单核保风险判定
本案例对应 USE-CASES 场景 17「保单核保风险判定」,属于保险行业,面向寿险公司人身险(寿险、重疾险、医疗险、意外险)投保单的自动核保,对投保人健康风险打分并输出承保结论、保费调整、保额核定与除外责任清单。
人身险投保要素取自投保书、健康告知问卷与职业分类表:年龄、1-6 类职业类别(数字越大风险越高,6 类为高危拒保职业)、投保险种、投保保额与标准保费、BMI、是否吸烟、慢性病史(无/高血压/糖尿病/恶性肿瘤史)以及健康告知是否如实填写。核保系统需要在投保单录入后即时给出标准体、加费、除外、延期或拒保的结论,支撑线上自动核保与人工核保员复核。
决策采用硬门槛前置两段式。Service 先做四条硬否决:健康告知未如实填写(依据保险法第十六条解除承保并拒赔)、恶性肿瘤既往史投保重疾/医疗/寿险、年龄超过 65 周岁自动核保上限、职业类别达到 6 类高危职业;命中任一即 DECLINE 且不进入评分。通过硬门槛后,规则脚本完成五维健康评分:年龄(0-25,越年轻越高)、职业类别(0-25)、BMI(0-20)、吸烟(0-10)、慢性病史(0-20),合计满分 100。
核保结论由脚本 underwritingResult 统一映射:≥85 优选体 PREFERRED、≥70 标准体 STANDARD、≥55 加费承保 LOADED、其余延期观察 POSTPONE;特定慢病与险种的匹配优先于评分——糖尿病投保医疗险或重疾险、高血压投保医疗险,无论分数高低均判为 EXCLUSION 除外责任承保。对加费件统一加费 25%,优选体享 10% 保费折扣;保额还要经过「职业类别 × 年龄」双轴的免体检/自动核保上限(最高 200 万、最低 30 万)校验,超限时降额承保并在核保说明中注明;除外件会输出糖尿病、高血压相关的具体除外责任条款清单。
- 医学核保口径分散在核保手册与代码中:年龄分档、BMI 区间、职业类别表、加费 25%、优选体折扣等阈值散落各处,医务管理部调整口径后研发排期长。
- 慢病与险种交叉规则复杂:糖尿病在医疗险/重疾险下要责任除外,高血压只对医疗险除外,这类「病种 × 险种」矩阵与评分结论存在覆盖关系,硬编码极易写错优先级。
- 硬性拒保与健康评分混杂:未如实告知、恶性肿瘤史、超龄、高危职业属于法理或渠道层面的硬拒保,若混在评分流程后,可能出现打出高分又被退回的尴尬链路,且不合监管留痕要求。
- 保额上限是职业与年龄的双轴矩阵:不同职业类别、年龄段对应 200/100/60/30 万不同上限,超额时要降额承保并给出解释,人工核保容易核错额度。
- 决策结果要同时支撑保费、除外清单与核保说明:加费金额、折扣金额、最终保费、除外责任条款、延期/加费提示需要一体化组装,多系统拼装容易口径不一致。
- 核心寿险系统老旧、信创环境难上重型规则平台:存量保单系统多运行在 JDK 8 上,独立规则中间件改造代价大,需要可嵌入的轻量规则能力。
本案例沿用六件套骨架:入参模型 + 决策结果模型 + 规则接口 + XML 规则脚本 + Service 编排 + 6 组真实业务形态测试。
| 层次 | 职责 | 对应文件 |
|---|---|---|
| 领域模型 | 投保单核保要素、核保决策结果(结论/降额/保费/除外清单/评分明细) | UnderwritingApplication.java、UnderwritingDecision.java |
| 规则集与 XML | 五维健康评分、结论分档、加费与折扣比例、职业年龄保额上限 | InsuranceUnderwritingRules.java、insurance-underwriting.xml |
| 编排服务 | 四条硬拒保前置、评分汇总、降额承保、保费调整与除外责任组装 | UnderwritingService.java |
| 测试 | 6 组投保形态(优选/标准/加费/除外降额/未告知/肿瘤史) | UnderwritingServiceTest.java |
规则方法拆分
Section titled “规则方法拆分”| 规则方法 | 口径 | 分值/输出 |
|---|---|---|
scoreAge |
年龄分档:≤30 岁 25、≤40 岁 22、≤50 岁 17、≤60 岁 10、否则 4 | 0-25 |
scoreOccupation |
职业 1-5 类依次 25/22/17/11/5 分(6 类由 Service 硬拒保) | 0-25 |
scoreBmi |
BMI 18.5-24.0 得 20、24-28 得 14、28-32 得 7、其余 3 | 0-20 |
scoreSmoker |
不吸烟 10、吸烟 4 | 0-10 |
scoreChronic |
无慢病 20、高血压 10、糖尿病 6、其他 0 | 0-20 |
underwritingResult |
分数 ≥85/70/55 映射 PREFERRED/STANDARD/LOADED,否则 POSTPONE;糖尿病+医疗/重疾、高血压+医疗覆盖为 EXCLUSION | 六种核保结论 |
extraPremiumRate |
仅 LOADED 加费件返回 25%,其余 0 | 0/25(%) |
premiumDiscountRate |
仅 PREFERRED 优选体返回 10% 折扣,其余 0 | 0/10(%) |
maxSumInsuredWan |
职业类别 × 年龄双轴:1-2 类且 ≤40 岁 200 万;≤3 类且 ≤50 岁 100 万;≤4 类 60 万;否则 30 万 | 万元保额上限 |
代码实现(完整可运行)
Section titled “代码实现(完整可运行)”将下列文件按源码包结构放入 JQuick-Java 工程的测试目录:两个模型放入 src/test/java/com/github/paohaijiao/demo/model/,规则接口与测试类放入 src/test/java/com/github/paohaijiao/demo/,Service 放入 src/test/java/com/github/paohaijiao/demo/service/,XML 脚本放入 src/test/resources/demo/,包名保持源码原样(com.github.paohaijiao.demo / .model / .service)。放入后执行 mvn test -Dtest=UnderwritingServiceTest 即可运行全部 6 个用例。
UnderwritingApplication.java
Section titled “UnderwritingApplication.java”package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;
/** * 人身险投保单核保要素 * <p> * 字段取自投保书、健康告知问卷与职业分类表,用于核保员风险评估与自动核保决策。 */@Data@NoArgsConstructor@AllArgsConstructorpublic class UnderwritingApplication {
/** * 投保单号 */ private String policyNo;
/** * 投保人/被保人姓名 */ private String applicantName;
/** * 年龄 */ private int age;
/** * 职业类别(1-6 类,数字越大风险越高,6 类为高危拒保职业) */ private int occupationClass;
/** * 投保险种:LIFE-寿险 / CRITICAL_ILLNESS-重疾险 / MEDICAL-医疗险 / ACCIDENT-意外险 */ private String productType;
/** * 投保保额(元) */ private double sumInsured;
/** * 标准保费(元) */ private double premiumAmount;
/** * 身体质量指数 BMI */ private double bmi;
/** * 是否吸烟 */ private boolean smoker;
/** * 慢性病史:NONE-无 / HYPERTENSION-高血压 / DIABETES-糖尿病 / CANCER_HISTORY-恶性肿瘤史 */ private String chronicDisease;
/** * 健康告知是否如实填写(false 视为未如实告知,直接拒保) */ private boolean healthDeclarationPass;}UnderwritingDecision.java
Section titled “UnderwritingDecision.java”package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;import java.util.List;
/** * 核保决策结果 */@Datapublic class UnderwritingDecision {
/** * 投保单号 */ private String policyNo;
/** * 核保结论: * PREFERRED-优选体 / STANDARD-标准体 / LOADED-加费承保 / * EXCLUSION-除外责任承保 / POSTPONE-延期观察 / DECLINE-拒保 */ private String result;
/** * 健康风险评分(0-100,分数越高越健康) */ private int totalScore;
/** * 核定承保保额(元),超职业/年龄上限时自动降额 */ private double approvedSumInsured;
/** * 是否发生降额承保 */ private boolean sumInsuredReduced;
/** * 加费比例(百分比整数,如 25 表示加费 25%) */ private int extraPremiumRate;
/** * 加费金额(元) */ private double extraPremium;
/** * 优选体保费折扣比例(百分比整数) */ private int premiumDiscountRate;
/** * 最终保费(元) */ private double finalPremium;
/** * 除外责任清单 */ private List<String> exclusionItems = new ArrayList<String>();
/** * 拒保原因 */ private List<String> declineReasons = new ArrayList<String>();
/** * 核保说明与提示 */ private List<String> notes = new ArrayList<String>();
/** * 评分明细 */ private List<String> scoreDetails = new ArrayList<String>();
public void addExclusionItem(String item) { this.exclusionItems.add(item); }
public void addDeclineReason(String reason) { this.declineReasons.add(reason); }
public void addNote(String note) { this.notes.add(note); }
public void addScoreDetail(String detail) { this.scoreDetails.add(detail); }}InsuranceUnderwritingRules.java
Section titled “InsuranceUnderwritingRules.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/** * 场景 17(生产级样板):人身险核保规则集 * <p> * 五维健康评分(年龄 / 职业 / BMI / 吸烟 / 慢病),再映射核保结论、加费比例、 * 保额上限与优选体折扣;拒保硬否决(未如实告知、恶性肿瘤史、超龄、高危职业)由 Service 前置。 * 规则脚本见 {@code src/test/resources/demo/insurance-underwriting.xml}。 */public interface InsuranceUnderwritingRules {
/** * 年龄风险评分(0-25,越年轻越高) */ int scoreAge(@Param("age") int age);
/** * 职业类别评分(0-25) */ int scoreOccupation(@Param("occupationClass") int occupationClass);
/** * BMI 评分(0-20) */ int scoreBmi(@Param("bmi") double bmi);
/** * 吸烟评分(0-10) */ int scoreSmoker(@Param("smoker") boolean smoker);
/** * 慢性病史评分(0-20) */ int scoreChronic(@Param("chronicDisease") String chronicDisease);
/** * 核保结论:慢病与险种匹配优先于评分(糖尿病/高血压 → 除外责任) */ String underwritingResult(@Param("totalScore") int totalScore, @Param("chronicDisease") String chronicDisease, @Param("productType") String productType);
/** * 加费比例(%):仅加费承保件有效 */ int extraPremiumRate(@Param("result") String result);
/** * 优选体保费折扣比例(%) */ int premiumDiscountRate(@Param("result") String result);
/** * 职业与年龄对应的免体检/自动核保保额上限(万元) */ int maxSumInsuredWan(@Param("occupationClass") int occupationClass, @Param("age") int age);}insurance-underwriting.xml
Section titled “insurance-underwriting.xml”<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE javas PUBLIC "-//PAOHAIJIAO//DTD API JAVA 1.0//EN" "classpath:paohaijiao/dtd/Jquick-java.dtd"><javas namespace="com.github.paohaijiao.demo.InsuranceUnderwritingRules">
<!-- 年龄评分:0-25 --> <java name="scoreAge" returnClass="int"> <![CDATA[ int def scoreAge(int:age) { if (age <= 30) { return 25; } else if (age <= 40) { return 22; } else if (age <= 50) { return 17; } else if (age <= 60) { return 10; } else { return 4; } } ]]> </java>
<!-- 职业类别评分:0-25 --> <java name="scoreOccupation" returnClass="int"> <![CDATA[ int def scoreOccupation(int:occupationClass) { if (occupationClass == 1) { return 25; } else if (occupationClass == 2) { return 22; } else if (occupationClass == 3) { return 17; } else if (occupationClass == 4) { return 11; } else { return 5; } } ]]> </java>
<!-- BMI 评分:0-20 --> <java name="scoreBmi" returnClass="int"> <![CDATA[ int def scoreBmi(double:bmi) { if (bmi >= 18.5 && bmi <= 24.0) { return 20; } else if (bmi > 24.0 && bmi <= 28.0) { return 14; } else if (bmi > 28.0 && bmi <= 32.0) { return 7; } else { return 3; } } ]]> </java>
<!-- 吸烟评分:0-10 --> <java name="scoreSmoker" returnClass="int"> <![CDATA[ int def scoreSmoker(boolean:smoker) { if (smoker) { return 4; } else { return 10; } } ]]> </java>
<!-- 慢性病史评分:0-20 --> <java name="scoreChronic" returnClass="int"> <![CDATA[ int def scoreChronic(str:chronicDisease) { if (chronicDisease == "NONE") { return 20; } else if (chronicDisease == "HYPERTENSION") { return 10; } else if (chronicDisease == "DIABETES") { return 6; } else { return 0; } } ]]> </java>
<!-- 核保结论:特定慢病+险种先判除外责任,其余按评分分档 --> <java name="underwritingResult" returnClass="java.lang.String"> <![CDATA[ str def underwritingResult(int:totalScore, str:chronicDisease, str:productType) { str result = "POSTPONE"; if (totalScore >= 55) { result = "LOADED"; } if (totalScore >= 70) { result = "STANDARD"; } if (totalScore >= 85) { result = "PREFERRED"; } if (chronicDisease == "DIABETES") { if (productType == "MEDICAL") { result = "EXCLUSION"; } if (productType == "CRITICAL_ILLNESS") { result = "EXCLUSION"; } } if (chronicDisease == "HYPERTENSION") { if (productType == "MEDICAL") { result = "EXCLUSION"; } } return result; } ]]> </java>
<!-- 加费比例:加费件统一加费 25% --> <java name="extraPremiumRate" returnClass="int"> <![CDATA[ int def extraPremiumRate(str:result) { if (result == "LOADED") { return 25; } else { return 0; } } ]]> </java>
<!-- 优选体折扣:10% --> <java name="premiumDiscountRate" returnClass="int"> <![CDATA[ int def premiumDiscountRate(str:result) { if (result == "PREFERRED") { return 10; } else { return 0; } } ]]> </java>
<!-- 自动核保保额上限(万元):职业类别与年龄双轴 --> <java name="maxSumInsuredWan" returnClass="int"> <![CDATA[ int def maxSumInsuredWan(int:occupationClass, int:age) { if (occupationClass <= 2 && age <= 40) { return 200; } else if (occupationClass <= 3 && age <= 50) { return 100; } else if (occupationClass <= 4) { return 60; } else { return 30; } } ]]> </java>
</javas>UnderwritingService.java
Section titled “UnderwritingService.java”package com.github.paohaijiao.demo.service;
import com.github.paohaijiao.demo.InsuranceUnderwritingRules;import com.github.paohaijiao.demo.model.UnderwritingApplication;import com.github.paohaijiao.demo.model.UnderwritingDecision;
import java.math.BigDecimal;import java.math.RoundingMode;
/** * 场景 17:保单核保服务 * <p> * 规则侧(XML):五维健康评分、核保结论分档、加费/折扣比例、职业年龄保额上限。 * Java 侧:未如实告知/恶性肿瘤史/超龄/高危职业硬拒保、降额承保、除外责任与最终保费组装。 */public class UnderwritingService {
/** * 自动核保年龄上限 */ private static final int MAX_AGE = 65;
/** * 高危职业类别 */ private static final int HIGH_RISK_OCCUPATION = 6;
private final InsuranceUnderwritingRules rules;
public UnderwritingService(InsuranceUnderwritingRules rules) { this.rules = rules; }
public UnderwritingDecision underwrite(UnderwritingApplication app) { UnderwritingDecision decision = new UnderwritingDecision(); decision.setPolicyNo(app.getPolicyNo());
if (!passHardRules(app, decision)) { return decision; }
int ageScore = rules.scoreAge(app.getAge()); int occupationScore = rules.scoreOccupation(app.getOccupationClass()); int bmiScore = rules.scoreBmi(app.getBmi()); int smokerScore = rules.scoreSmoker(app.isSmoker()); int chronicScore = rules.scoreChronic(app.getChronicDisease()); int totalScore = ageScore + occupationScore + bmiScore + smokerScore + chronicScore;
decision.addScoreDetail("年龄风险评分: " + ageScore + " / 25"); decision.addScoreDetail("职业类别评分: " + occupationScore + " / 25"); decision.addScoreDetail("BMI 评分: " + bmiScore + " / 20"); decision.addScoreDetail("吸烟评分: " + smokerScore + " / 10"); decision.addScoreDetail("慢性病史评分: " + chronicScore + " / 20"); decision.addScoreDetail("汇总评分: " + totalScore); decision.setTotalScore(totalScore);
String result = rules.underwritingResult(totalScore, app.getChronicDisease(), app.getProductType()); decision.setResult(result);
applySumInsuredCap(app, decision); applyPremium(app, decision, result);
if ("EXCLUSION".equals(result)) { buildExclusions(app, decision); } else if ("POSTPONE".equals(result)) { decision.addNote("健康评分低于 55,延期 6-12 个月观察并要求提供体检/复查报告后再申请"); } else if ("LOADED".equals(result)) { decision.addNote("健康风险高于标准体,按加费 " + decision.getExtraPremiumRate() + "% 承保"); } return decision; }
/** * 硬否决规则:未如实告知、恶性肿瘤史投保重疾/医疗、超龄、高危职业 */ private boolean passHardRules(UnderwritingApplication app, UnderwritingDecision decision) { if (!app.isHealthDeclarationPass()) { decline(decision, "健康告知未如实填写,依据保险法第十六条解除承保并拒赔"); return false; } if ("CANCER_HISTORY".equals(app.getChronicDisease()) && ("CRITICAL_ILLNESS".equals(app.getProductType()) || "MEDICAL".equals(app.getProductType()) || "LIFE".equals(app.getProductType()))) { decline(decision, "存在恶性肿瘤既往史," + productName(app.getProductType()) + "不予承保"); return false; } if (app.getAge() > MAX_AGE) { decline(decision, "年龄 " + app.getAge() + " 周岁超过自动核保上限 " + MAX_AGE + " 周岁"); return false; } if (app.getOccupationClass() >= HIGH_RISK_OCCUPATION) { decline(decision, "职业类别为第 " + app.getOccupationClass() + " 类高危职业,普通险种不予承保,建议转投专属高危职业意外险"); return false; } return true; }
private void decline(UnderwritingDecision decision, String reason) { decision.setResult("DECLINE"); decision.addDeclineReason(reason); }
/** * 保额超过职业/年龄上限时降额承保 */ private void applySumInsuredCap(UnderwritingApplication app, UnderwritingDecision decision) { int maxWan = rules.maxSumInsuredWan(app.getOccupationClass(), app.getAge()); double maxAmount = maxWan * 10000.0; if (app.getSumInsured() > maxAmount) { decision.setApprovedSumInsured(round(maxAmount)); decision.setSumInsuredReduced(true); decision.addNote("投保保额 " + round(app.getSumInsured()) + " 元超出第 " + app.getOccupationClass() + " 类职业/" + app.getAge() + " 周岁自动核保上限 " + maxWan + " 万元,降额至 " + maxWan + " 万元承保"); } else { decision.setApprovedSumInsured(round(app.getSumInsured())); } }
/** * 保费调整:加费件上浮、优选体打折、其余按标准保费 */ private void applyPremium(UnderwritingApplication app, UnderwritingDecision decision, String result) { int extraRate = rules.extraPremiumRate(result); int discountRate = rules.premiumDiscountRate(result); decision.setExtraPremiumRate(extraRate); decision.setPremiumDiscountRate(discountRate);
double extra = app.getPremiumAmount() * extraRate / 100.0; double discount = app.getPremiumAmount() * discountRate / 100.0; decision.setExtraPremium(round(extra)); double finalPremium = app.getPremiumAmount() + extra - discount; decision.setFinalPremium(round(finalPremium));
if (discountRate > 0) { decision.addNote("优选体承保,标准保费享 " + discountRate + "% 折扣,省 " + round(discount) + " 元"); } }
private void buildExclusions(UnderwritingApplication app, UnderwritingDecision decision) { if ("DIABETES".equals(app.getChronicDisease())) { decision.addExclusionItem("糖尿病及其并发症(含血糖异常相关治疗)"); decision.addExclusionItem("糖尿病继发心脑血管、眼底、肾脏病变"); } if ("HYPERTENSION".equals(app.getChronicDisease())) { decision.addExclusionItem("高血压及其并发症(含心脑血管、肾脏疾病)"); } decision.addNote("已就既往症做责任除外,其余保险责任正常承保,保费不加费"); }
private String productName(String productType) { if ("LIFE".equals(productType)) { return "寿险"; } if ("CRITICAL_ILLNESS".equals(productType)) { return "重疾险"; } if ("MEDICAL".equals(productType)) { return "医疗险"; } return "意外险"; }
private double round(double value) { return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_UP).doubleValue(); }}UnderwritingServiceTest.java
Section titled “UnderwritingServiceTest.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.demo.model.UnderwritingApplication;import com.github.paohaijiao.demo.model.UnderwritingDecision;import com.github.paohaijiao.demo.service.UnderwritingService;import com.github.paohaijiao.xml.JQuickJava;import org.junit.Assert;import org.junit.Before;import org.junit.Test;
/** * 场景 17 生产级样板:人身险保单核保风险判定 * <p> * 覆盖 6 组投保形态:优选体折扣、标准体、吸烟慢病加费、糖尿病责任除外(含降额)、 * 未如实告知拒保、恶性肿瘤史拒保。 */public class UnderwritingServiceTest {
private UnderwritingService service;
@Before public void setUp() { InsuranceUnderwritingRules rules = JQuickJava.create() .createApi(InsuranceUnderwritingRules.class, "demo/insurance-underwriting.xml"); this.service = new UnderwritingService(rules); }
/** * 优选体:28 岁 1 类职业不吸烟无慢病,100 分,寿险保费 9 折 */ @Test public void shouldIssuePreferredWithDiscount() { UnderwritingApplication app = new UnderwritingApplication( "UW20260001", "林悦", 28, 1, "LIFE", 1000000.0, 5000.0, 22.0, false, "NONE", true);
UnderwritingDecision decision = service.underwrite(app);
Assert.assertEquals(100, decision.getTotalScore()); Assert.assertEquals("PREFERRED", decision.getResult()); Assert.assertEquals(1000000.0, decision.getApprovedSumInsured(), 0.001); Assert.assertFalse(decision.isSumInsuredReduced()); Assert.assertEquals(10, decision.getPremiumDiscountRate()); Assert.assertEquals(4500.0, decision.getFinalPremium(), 0.001); Assert.assertTrue(decision.getNotes().contains("优选体承保,标准保费享 10% 折扣,省 500.0 元")); }
/** * 标准体:42 岁 3 类职业 BMI 偏高,78 分,重疾险标准保费承保 */ @Test public void shouldIssueStandardWithoutAdjustment() { UnderwritingApplication app = new UnderwritingApplication( "UW20260002", "赵磊", 42, 3, "CRITICAL_ILLNESS", 500000.0, 8000.0, 26.0, false, "NONE", true);
UnderwritingDecision decision = service.underwrite(app);
Assert.assertEquals(78, decision.getTotalScore()); Assert.assertEquals("STANDARD", decision.getResult()); Assert.assertEquals(0, decision.getExtraPremiumRate()); Assert.assertEquals(8000.0, decision.getFinalPremium(), 0.001); Assert.assertTrue(decision.getExclusionItems().isEmpty()); }
/** * 加费承保:48 岁吸烟高血压投保人买寿险,62 分加费 25% */ @Test public void shouldIssueLoadedWithExtraPremium() { UnderwritingApplication app = new UnderwritingApplication( "UW20260003", "孙建国", 48, 3, "LIFE", 800000.0, 6000.0, 27.0, true, "HYPERTENSION", true);
UnderwritingDecision decision = service.underwrite(app);
Assert.assertEquals(62, decision.getTotalScore()); Assert.assertEquals("LOADED", decision.getResult()); Assert.assertEquals(25, decision.getExtraPremiumRate()); Assert.assertEquals(1500.0, decision.getExtraPremium(), 0.001); Assert.assertEquals(7500.0, decision.getFinalPremium(), 0.001); Assert.assertTrue(decision.getNotes().contains("健康风险高于标准体,按加费 25% 承保")); }
/** * 除外责任 + 降额:糖尿病投保医疗险,300 万保额降至 200 万,糖尿病相关责任除外 */ @Test public void shouldIssueWithExclusionAndReducedSumInsured() { UnderwritingApplication app = new UnderwritingApplication( "UW20260004", "周敏", 38, 2, "MEDICAL", 3000000.0, 4000.0, 25.0, false, "DIABETES", true);
UnderwritingDecision decision = service.underwrite(app);
Assert.assertEquals(74, decision.getTotalScore()); Assert.assertEquals("EXCLUSION", decision.getResult()); Assert.assertTrue(decision.isSumInsuredReduced()); Assert.assertEquals(2000000.0, decision.getApprovedSumInsured(), 0.001); Assert.assertEquals(0, decision.getExtraPremiumRate()); Assert.assertEquals(4000.0, decision.getFinalPremium(), 0.001); Assert.assertTrue(decision.getExclusionItems().contains("糖尿病及其并发症(含血糖异常相关治疗)")); Assert.assertTrue(decision.getNotes().contains( "投保保额 3000000.0 元超出第 2 类职业/38 周岁自动核保上限 200 万元,降额至 200 万元承保")); }
/** * 未如实健康告知:直接拒保,不进入评分 */ @Test public void shouldDeclineFalseHealthDeclaration() { UnderwritingApplication app = new UnderwritingApplication( "UW20260005", "吴海", 35, 2, "CRITICAL_ILLNESS", 800000.0, 7000.0, 23.0, false, "DIABETES", false);
UnderwritingDecision decision = service.underwrite(app);
Assert.assertEquals("DECLINE", decision.getResult()); Assert.assertEquals(0, decision.getTotalScore()); Assert.assertTrue(decision.getDeclineReasons().contains( "健康告知未如实填写,依据保险法第十六条解除承保并拒赔")); }
/** * 恶性肿瘤既往史投保重疾险:直接拒保 */ @Test public void shouldDeclineCancerHistoryForCriticalIllness() { UnderwritingApplication app = new UnderwritingApplication( "UW20260006", "郑芳", 45, 2, "CRITICAL_ILLNESS", 600000.0, 9000.0, 24.5, false, "CANCER_HISTORY", true);
UnderwritingDecision decision = service.underwrite(app);
Assert.assertEquals("DECLINE", decision.getResult()); Assert.assertTrue(decision.getDeclineReasons().contains( "存在恶性肿瘤既往史,重疾险不予承保")); Assert.assertEquals(0.0, decision.getFinalPremium(), 0.001); }}在工程根目录执行:
mvn test -Dtest=UnderwritingServiceTest测试覆盖形态
Section titled “测试覆盖形态”| 测试方法 | 构造的业务形态 | 关键断言/期望决策 |
|---|---|---|
shouldIssuePreferredWithDiscount |
28 岁、1 类职业、BMI 22、不吸烟、无慢病,投保 100 万寿险、标准保费 5000 元 | 100 分 PREFERRED;保额不降额;折扣率 10%,最终保费 4500 元,含「省 500.0 元」说明 |
shouldIssueStandardWithoutAdjustment |
42 岁、3 类职业、BMI 26、重疾险 50 万、标准保费 8000 元 | 78 分 STANDARD;加费率 0,最终保费 8000 元,除外责任清单为空 |
shouldIssueLoadedWithExtraPremium |
48 岁、3 类职业、吸烟、高血压,投保寿险 80 万、标准保费 6000 元 | 62 分 LOADED;加费率 25%、加费 1500 元、最终保费 7500 元,含加费承保说明 |
shouldIssueWithExclusionAndReducedSumInsured |
38 岁、2 类职业、糖尿病,投保医疗险 300 万、标准保费 4000 元 | 74 分但结论 EXCLUSION;保额由 300 万降至 200 万(职业/年龄上限);不加费、终费 4000 元;除外清单含「糖尿病及其并发症(含血糖异常相关治疗)」 |
shouldDeclineFalseHealthDeclaration |
糖尿病投保人但 healthDeclarationPass=false |
DECLINE、总分 0,原因含「依据保险法第十六条解除承保并拒赔」 |
shouldDeclineCancerHistoryForCriticalIllness |
45 岁、恶性肿瘤既往史投保重疾险 | DECLINE,原因含「存在恶性肿瘤既往史,重疾险不予承保」,最终保费 0 |
- 核保口径全量外置 XML:五维健康分档、85/70/55 结论线、25% 加费、10% 折扣、职业年龄保额矩阵全部在 XML 中,医务/精算调整阈值无需改动核心保单系统,重建代理即生效。
- 慢病 × 险种矩阵在脚本中显式覆盖评分结论:
underwritingResult先按分数给结论,再用糖尿病+医疗/重疾、高血压+医疗的正向判断覆盖为 EXCLUSION,优先级一目了然,可独立审计。 - Service 侧四条硬拒保前置:未如实告知、恶性肿瘤史、超 65 岁、6 类高危职业在评分前短路返回 DECLINE,法理类拒保不参与健康打分,链路清晰留痕完整。
- 自动降额承保可解释:
maxSumInsuredWan以职业类别与年龄双轴给出免体检上限,超限时 Service 自动降额、置降额标记并生成含原保额、上限、核定保额的核保说明。 - 保费一体化组装:加费件
标准保费 × 25%上浮、优选体-10%折扣在同一方法内计算最终保费,除外件不加费,规则比例外置、金额计算留 Java 侧。 - 脚本不支持
!取反,布尔条件一律正向书写:如吸烟评分if (smoker) return 4 else return 10,规避取反语法限制。 @Param按名绑定 + ASM 代理调用链:9 个规则方法共用一个动态代理,接口即契约;Java 8 无重型依赖,可嵌入 JDK 8 寿险核心系统与信创环境。