案例 16:企业经营贷授信额度智能测算
本案例对应 USE-CASES 场景 16「企业经营贷授信额度智能测算」,属于金融风控领域,面向银行对公条线的中小微企业经营贷进件,自动完成信用评级、审批结论、核定额度与执行利率的一体化测算。
企业经营贷的进件要素来自企业授信调查表以及税务、银行数据授权回传:持续经营年限、近 12 个月营业收入与实缴税额、对公账户有效流水、资产负债率、纳税信用等级(A/B/M/C/D)、近 2 年贷款逾期次数、失信涉诉标记、行业类别与申请额度。银行需要在客户经理录入进件后,立刻给出可解释、可审计的授信结论,而不是等待漫长的人工评审。
决策分两段完成。第一段是 Service 前置的硬性准入:受限行业(RESTRICTED,如类金融、房地产)、失信被执行/重大涉诉、资产负债率超过 85%、纳税信用 D 级、营收或申请额数据缺失,任一命中即直接拒绝,不进入评分。第二段是规则脚本承担的六维评分:持续经营年限(0-15)、营收规模(0-20)、纳税信用(0-20)、流水核验(0-15)、资产负债率(0-15)、征信记录(0-15),合计满分 100,再由总分映射 A/B/C/D 信用等级与 APPROVE / MANUAL_REVIEW / REJECT 审批结论。
对非拒绝件,系统还要输出完整的风险定价与额度方案:按纳税信用等级与总分确定纳税额授信倍数(A 级最高 9 倍),实缴税额乘以倍数得到理论额度,再依次经受营收占比 30%、单户 2000 万元绝对上限、不超申请额三重封顶,得到核定额度并记录实际生效的封顶项;执行利率随信用等级浮动(A 3.45%、B 3.85%、C 4.35%)。最终决策物 CorporateCreditDecision 同时携带评分明细、拒绝原因与风险提示(如流水低于营收、逾期记录、60-74 分复核区间提示),直接支撑审批留痕与贷后监测。
- 六维口径散落硬编码:经营年限分档、纳税等级与税额交叉分档、流水/营收匹配比例等阈值写死在各个审批系统的
if-else中,分支数量多且不同分行口径不一致。 - 额度政策调整需重新发版:纳税倍数、营收占比上限、单户 2000 万天花板、风险利率随货币政策和行内授信政策频繁变动,硬编码意味着每次调整都要走完整发版流程。
- 多维评分难以独立调参与回溯:六个维度权重与分档耦合在一个大方法里,无法单独调整流水核验口径,事后也难以向审委会解释每一分的来源。
- 硬性否决与评分逻辑混杂:受限行业、失信涉诉、超负债等一票否决若埋在评分之后,可能出现“高分企业仍被否决”或否决件白白跑完评分链路的问题,审计难以说清先后。
- 多重封顶缺乏可解释记录:理论额度被营收占比、绝对上限、申请额压减后,审批单上看不到究竟是哪一项约束生效,客户经理无法向企业解释核减原因。
- 老旧信贷系统仍在 JDK 8、信创环境难引入重型规则平台:传统规则引擎需要独立中间件与数据库表,升级运行时和运维成本高,难以嵌入存量信贷系统。
本案例沿用 JQuick-Java 生产级样板的六件套骨架:入参模型 + 决策结果模型 + 规则接口 + XML 规则脚本 + Service 编排 + 6 组真实业务形态测试。
| 层次 | 职责 | 对应文件 |
|---|---|---|
| 领域模型 | 对公进件要素、授信决策结果(评分明细/拒绝原因/风险提示/封顶项) | CorporateLoanApplication.java、CorporateCreditDecision.java |
| 规则集与 XML | 六维评分口径、等级与审批阈值、纳税倍数、风险定价,全部外置 | CorporateLoanRules.java、corporate-credit.xml |
| 编排服务 | 硬否决前置、评分汇总 clamp 0-100、额度三重封顶、决策组装 | CorporateLoanService.java |
| 测试 | 6 组企业进件形态(优质科技/稳健制造/临界复核/失信/超负债/受限行业) | CorporateLoanServiceTest.java |
规则方法拆分
Section titled “规则方法拆分”| 规则方法 | 口径 | 分值/输出 |
|---|---|---|
scoreOperatingYears |
持续经营年限分档:≥10 年 15、≥5 年 12、≥3 年 8、≥1 年 4、否则 0 | 0-15 |
scoreRevenue |
年营收规模分档:1 亿/5000 万/2000 万/500 万元四档 | 0-20 |
scoreTaxCredit |
纳税信用等级(A/B/M/C)为轴,实缴税额 100 万/30 万分档微调 | 0-20 |
scoreBankFlow |
有效流水与营收匹配度,交叉相乘比较,营收非正直接 0 分 | 0-15 |
scoreDebtRatio |
资产负债率分档:≤40% 15、≤55% 12、≤70% 8、≤80% 4、否则 0 | 0-15 |
scoreCreditRecord |
有失信/涉诉记录直接 0 分;逾期 0 次 15、≤2 次 11、≤4 次 6、否则 0 | 0-15 |
creditRating |
总分 ≥85 A、≥75 B、≥60 C、否则 D | A/B/C/D |
approveDecision |
总分 ≥75 APPROVE、≥60 MANUAL_REVIEW、否则 REJECT | 审批结论 |
taxMultiplier |
纳税信用等级 × 总分档确定纳税额授信倍数(A 级最高 9.0,C/M 之外为 0) | 倍数 |
interestRate |
信用等级对应执行年利率:A 3.45%、B 3.85%、C 4.35%、D 不授价 | 年利率 |
代码实现(完整可运行)
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=CorporateLoanServiceTest 即可运行全部 6 个用例。
CorporateLoanApplication.java
Section titled “CorporateLoanApplication.java”package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;
/** * 企业经营贷申请件(对公进件要素) * <p> * 字段取自企业授信调查表与税务/银行数据授权回传,仅保留额度测算与评级所需要素。 */@Data@NoArgsConstructor@AllArgsConstructorpublic class CorporateLoanApplication {
/** * 申请编号 */ private String applicationId;
/** * 企业名称 */ private String enterpriseName;
/** * 持续经营年限 */ private int operatingYears;
/** * 近 12 个月营业收入(元) */ private double annualRevenue;
/** * 近 12 个月实缴纳税总额(元) */ private double annualTaxAmount;
/** * 近 12 个月对公账户有效流水(元) */ private double bankFlowAnnual;
/** * 资产负债率(百分比整数,如 55 表示 55%) */ private int debtRatio;
/** * 纳税信用等级:A / B / M / C / D */ private String taxCreditLevel;
/** * 近 2 年对公贷款逾期次数 */ private int overdueCount;
/** * 是否存在失信被执行 / 重大涉诉记录 */ private boolean hasJudicialRecord;
/** * 行业类别:MANUFACTURING-制造 / TECH-科技 / TRADE-商贸 / RESTRICTED-受限类 */ private String industryCategory;
/** * 申请授信额度(元) */ private double applyAmount;}CorporateCreditDecision.java
Section titled “CorporateCreditDecision.java”package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;import java.util.List;
/** * 企业经营贷授信决策结果 */@Datapublic class CorporateCreditDecision {
/** * 申请编号 */ private String applicationId;
/** * 审批结论:APPROVE-批准 / MANUAL_REVIEW-人工复核 / REJECT-拒绝 */ private String decision;
/** * 综合评分(0-100) */ private int totalScore;
/** * 企业信用等级:A / B / C / D */ private String creditRating;
/** * 核定授信额度(元) */ private double approvedLimit;
/** * 授信额度测算理论值(元,封顶前) */ private double theoreticalLimit;
/** * 执行年利率 */ private double interestRate;
/** * 额度触发的封顶项:TAX-纳税倍数 / REVENUE-营收占比 / CEILING-绝对上限 / APPLY-申请额 */ private String cappedBy;
/** * 评分明细 */ private List<String> scoreDetails = new ArrayList<String>();
/** * 拒绝原因 */ private List<String> rejectReasons = new ArrayList<String>();
/** * 风险提示 */ private List<String> riskWarnings = new ArrayList<String>();
public void addScoreDetail(String detail) { this.scoreDetails.add(detail); }
public void addRejectReason(String reason) { this.rejectReasons.add(reason); }
public void addRiskWarning(String warning) { this.riskWarnings.add(warning); }}CorporateLoanRules.java
Section titled “CorporateLoanRules.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/** * 场景 16(生产级样板):企业经营贷授信测算规则集 * <p> * 六维评分(经营年限 / 营收规模 / 纳税信用 / 流水核验 / 负债率 / 征信记录), * 再由总分映射信用等级、审批结论、纳税倍数与利率;行业准入与失信否决由 Service 前置。 * 规则脚本见 {@code src/test/resources/demo/corporate-credit.xml}。 */public interface CorporateLoanRules {
/** * 持续经营年限评分(0-15) */ int scoreOperatingYears(@Param("operatingYears") int operatingYears);
/** * 营收规模评分(0-20) */ int scoreRevenue(@Param("annualRevenue") double annualRevenue);
/** * 纳税信用评分(0-20):纳税信用等级 + 实缴税额 */ int scoreTaxCredit(@Param("taxCreditLevel") String taxCreditLevel, @Param("annualTaxAmount") double annualTaxAmount);
/** * 流水核验评分(0-15):有效流水与营收的匹配度,交叉相乘规避除法精度问题 */ int scoreBankFlow(@Param("bankFlowAnnual") double bankFlowAnnual, @Param("annualRevenue") double annualRevenue);
/** * 资产负债率评分(0-15) */ int scoreDebtRatio(@Param("debtRatio") int debtRatio);
/** * 征信记录评分(0-15):逾期次数 + 失信记录 */ int scoreCreditRecord(@Param("overdueCount") int overdueCount, @Param("hasJudicialRecord") boolean hasJudicialRecord);
/** * 信用等级:A / B / C / D */ String creditRating(@Param("totalScore") int totalScore);
/** * 审批结论:APPROVE / MANUAL_REVIEW / REJECT */ String approveDecision(@Param("totalScore") int totalScore);
/** * 纳税额授信倍数:随信用等级与综合分浮动 */ double taxMultiplier(@Param("taxCreditLevel") String taxCreditLevel, @Param("totalScore") int totalScore);
/** * 执行年利率:等级越低利率越高 */ double interestRate(@Param("creditRating") String creditRating);}corporate-credit.xml
Section titled “corporate-credit.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.CorporateLoanRules">
<!-- 持续经营年限评分:0-15 --> <java name="scoreOperatingYears" returnClass="int"> <![CDATA[ int def scoreOperatingYears(int:operatingYears) { if (operatingYears >= 10) { return 15; } else if (operatingYears >= 5) { return 12; } else if (operatingYears >= 3) { return 8; } else if (operatingYears >= 1) { return 4; } else { return 0; } } ]]> </java>
<!-- 营收规模评分:0-20(单位:元) --> <java name="scoreRevenue" returnClass="int"> <![CDATA[ int def scoreRevenue(double:annualRevenue) { if (annualRevenue >= 100000000.0) { return 20; } else if (annualRevenue >= 50000000.0) { return 16; } else if (annualRevenue >= 20000000.0) { return 12; } else if (annualRevenue >= 5000000.0) { return 7; } else { return 3; } } ]]> </java>
<!-- 纳税信用评分:0-20,等级为轴、税额微调 --> <java name="scoreTaxCredit" returnClass="int"> <![CDATA[ int def scoreTaxCredit(str:taxCreditLevel, double:annualTaxAmount) { if (taxCreditLevel == "A") { if (annualTaxAmount >= 1000000.0) { return 20; } else if (annualTaxAmount >= 300000.0) { return 17; } else { return 14; } } else if (taxCreditLevel == "B") { if (annualTaxAmount >= 1000000.0) { return 15; } else if (annualTaxAmount >= 300000.0) { return 13; } else { return 10; } } else if (taxCreditLevel == "M") { if (annualTaxAmount >= 300000.0) { return 10; } else if (annualTaxAmount >= 100000.0) { return 8; } else { return 6; } } else if (taxCreditLevel == "C") { return 4; } else { return 0; } } ]]> </java>
<!-- 流水核验评分:0-15,流水/营收交叉相乘比较 --> <java name="scoreBankFlow" returnClass="int"> <![CDATA[ int def scoreBankFlow(double:bankFlowAnnual, double:annualRevenue) { if (annualRevenue <= 0.0) { return 0; } if (bankFlowAnnual * 10 >= annualRevenue * 12) { return 15; } else if (bankFlowAnnual >= annualRevenue) { return 12; } else if (bankFlowAnnual * 10 >= annualRevenue * 8) { return 8; } else if (bankFlowAnnual * 10 >= annualRevenue * 5) { return 4; } else { return 0; } } ]]> </java>
<!-- 资产负债率评分:0-15 --> <java name="scoreDebtRatio" returnClass="int"> <![CDATA[ int def scoreDebtRatio(int:debtRatio) { if (debtRatio <= 40) { return 15; } else if (debtRatio <= 55) { return 12; } else if (debtRatio <= 70) { return 8; } else if (debtRatio <= 80) { return 4; } else { return 0; } } ]]> </java>
<!-- 征信记录评分:0-15 --> <java name="scoreCreditRecord" returnClass="int"> <![CDATA[ int def scoreCreditRecord(int:overdueCount, boolean:hasJudicialRecord) { if (hasJudicialRecord) { return 0; } if (overdueCount == 0) { return 15; } else if (overdueCount <= 2) { return 11; } else if (overdueCount <= 4) { return 6; } else { return 0; } } ]]> </java>
<!-- 信用等级映射 --> <java name="creditRating" returnClass="java.lang.String"> <![CDATA[ str def creditRating(int:totalScore) { if (totalScore >= 85) { return "A"; } else if (totalScore >= 75) { return "B"; } else if (totalScore >= 60) { return "C"; } else { return "D"; } } ]]> </java>
<!-- 审批结论映射 --> <java name="approveDecision" returnClass="java.lang.String"> <![CDATA[ str def approveDecision(int:totalScore) { if (totalScore >= 75) { return "APPROVE"; } else if (totalScore >= 60) { return "MANUAL_REVIEW"; } else { return "REJECT"; } } ]]> </java>
<!-- 纳税额授信倍数 --> <java name="taxMultiplier" returnClass="double"> <![CDATA[ double def taxMultiplier(str:taxCreditLevel, int:totalScore) { if (taxCreditLevel == "A") { if (totalScore >= 85) { return 9.0; } else if (totalScore >= 75) { return 7.0; } else { return 4.0; } } else if (taxCreditLevel == "B") { if (totalScore >= 85) { return 6.0; } else if (totalScore >= 75) { return 4.5; } else { return 2.5; } } else if (taxCreditLevel == "M") { if (totalScore >= 75) { return 3.0; } else if (totalScore >= 60) { return 2.0; } else { return 1.0; } } else { return 0.0; } } ]]> </java>
<!-- 执行年利率 --> <java name="interestRate" returnClass="double"> <![CDATA[ double def interestRate(str:creditRating) { if (creditRating == "A") { return 0.0345; } else if (creditRating == "B") { return 0.0385; } else if (creditRating == "C") { return 0.0435; } else { return 0.0; } } ]]> </java>
</javas>CorporateLoanService.java
Section titled “CorporateLoanService.java”package com.github.paohaijiao.demo.service;
import com.github.paohaijiao.demo.CorporateLoanRules;import com.github.paohaijiao.demo.model.CorporateCreditDecision;import com.github.paohaijiao.demo.model.CorporateLoanApplication;
import java.math.BigDecimal;import java.math.RoundingMode;
/** * 场景 16:企业经营贷授信测算服务 * <p> * 规则侧(XML):六维评分口径、等级与审批阈值、纳税倍数、风险定价。 * Java 侧:行业/失信/负债硬否决、评分汇总、额度三重封顶(纳税倍数 → 营收占比 30% → * 绝对上限 2000 万 → 不超申请额)、决策组装。 */public class CorporateLoanService {
/** * 单户授信绝对上限(元):2000 万 */ private static final double LIMIT_CEILING = 20000000.0;
/** * 授信额度占年营收比例上限:30% */ private static final double REVENUE_RATIO_CAP = 0.30;
/** * 资产负债率硬性上限(%) */ private static final int DEBT_RATIO_HARD_LIMIT = 85;
private final CorporateLoanRules rules;
public CorporateLoanService(CorporateLoanRules rules) { this.rules = rules; }
public CorporateCreditDecision evaluate(CorporateLoanApplication app) { CorporateCreditDecision decision = new CorporateCreditDecision(); decision.setApplicationId(app.getApplicationId());
if (!passPreCheck(app, decision)) { return decision; }
int yearsScore = rules.scoreOperatingYears(app.getOperatingYears()); int revenueScore = rules.scoreRevenue(app.getAnnualRevenue()); int taxScore = rules.scoreTaxCredit(app.getTaxCreditLevel(), app.getAnnualTaxAmount()); int flowScore = rules.scoreBankFlow(app.getBankFlowAnnual(), app.getAnnualRevenue()); int debtScore = rules.scoreDebtRatio(app.getDebtRatio()); int creditScore = rules.scoreCreditRecord(app.getOverdueCount(), app.isHasJudicialRecord());
int totalScore = yearsScore + revenueScore + taxScore + flowScore + debtScore + creditScore; if (totalScore < 0) { totalScore = 0; } else if (totalScore > 100) { totalScore = 100; }
decision.addScoreDetail("持续经营年限评分: " + yearsScore + " / 15"); decision.addScoreDetail("营收规模评分: " + revenueScore + " / 20"); decision.addScoreDetail("纳税信用评分: " + taxScore + " / 20"); decision.addScoreDetail("流水核验评分: " + flowScore + " / 15"); decision.addScoreDetail("资产负债率评分: " + debtScore + " / 15"); decision.addScoreDetail("征信记录评分: " + creditScore + " / 15"); decision.addScoreDetail("汇总评分: " + totalScore);
String rating = rules.creditRating(totalScore); String result = rules.approveDecision(totalScore);
decision.setTotalScore(totalScore); decision.setCreditRating(rating); decision.setDecision(result);
if ("REJECT".equals(result)) { buildRejectReasons(app, decision, totalScore); return decision; }
decision.setInterestRate(rules.interestRate(rating)); double multiplier = rules.taxMultiplier(app.getTaxCreditLevel(), totalScore); double theoretical = app.getAnnualTaxAmount() * multiplier; decision.setTheoreticalLimit(round(theoretical)); decision.setApprovedLimit(applyLimitCaps(app, theoretical, decision, multiplier)); buildWarnings(app, decision); return decision; }
/** * 前置硬规则:受限行业、失信被执行、超负债、基础数据缺失直接拒绝 */ private boolean passPreCheck(CorporateLoanApplication app, CorporateCreditDecision decision) { if ("RESTRICTED".equals(app.getIndustryCategory())) { reject(decision, "企业属于授信受限行业,不予准入"); return false; } if (app.isHasJudicialRecord()) { reject(decision, "企业存在失信被执行/重大涉诉记录,触发一票否决"); return false; } if (app.getDebtRatio() > DEBT_RATIO_HARD_LIMIT) { reject(decision, "资产负债率 " + app.getDebtRatio() + "% 超过硬性上限 " + DEBT_RATIO_HARD_LIMIT + "%,偿债能力不足"); return false; } if (app.getAnnualRevenue() <= 0.0 || app.getApplyAmount() <= 0.0) { reject(decision, "营收或申请额度数据缺失,无法完成授信测算"); return false; } if ("D".equals(app.getTaxCreditLevel())) { reject(decision, "纳税信用等级为 D,属于重点监控企业,不予准入"); return false; } return true; }
private void reject(CorporateCreditDecision decision, String reason) { decision.setDecision("REJECT"); decision.setCreditRating("D"); decision.setTotalScore(0); decision.addRejectReason(reason); }
/** * 额度三重封顶,记录实际起作用的封顶项 */ private double applyLimitCaps(CorporateLoanApplication app, double theoretical, CorporateCreditDecision decision, double multiplier) { double limit = theoretical; decision.setCappedBy("TAX");
double revenueCap = app.getAnnualRevenue() * REVENUE_RATIO_CAP; if (revenueCap < limit) { limit = revenueCap; decision.setCappedBy("REVENUE"); } if (LIMIT_CEILING < limit) { limit = LIMIT_CEILING; decision.setCappedBy("CEILING"); } if (app.getApplyAmount() < limit) { limit = app.getApplyAmount(); decision.setCappedBy("APPLY"); } if (!"TAX".equals(decision.getCappedBy()) && theoretical > limit) { decision.addRiskWarning("理论授信额度 " + money(theoretical) + " 元(纳税额 × " + multiplier + ")受" + capName(decision.getCappedBy()) + "约束,核定额度 " + money(limit) + " 元"); } return round(limit); }
private String capName(String code) { if ("REVENUE".equals(code)) { return "营收占比上限"; } if ("CEILING".equals(code)) { return "单户 2000 万绝对上限"; } if ("APPLY".equals(code)) { return "申请额"; } return "纳税倍数"; }
private void buildRejectReasons(CorporateLoanApplication app, CorporateCreditDecision decision, int totalScore) { if (app.getOperatingYears() < 2) { decision.addRejectReason("持续经营年限不足 2 年,经营稳定性无法评估"); } if (app.getDebtRatio() > 70) { decision.addRejectReason("资产负债率 " + app.getDebtRatio() + "% 偏高"); } if (app.getOverdueCount() > 2) { decision.addRejectReason("近 2 年逾期 " + app.getOverdueCount() + " 次,信用记录较差"); } if (decision.getRejectReasons().isEmpty()) { decision.addRejectReason("综合评分 " + totalScore + " 低于自动审批线 60"); } }
private void buildWarnings(CorporateLoanApplication app, CorporateCreditDecision decision) { if (app.getDebtRatio() > 70) { decision.addRiskWarning("资产负债率 " + app.getDebtRatio() + "%,高于 70% 关注线"); } if (app.getBankFlowAnnual() < app.getAnnualRevenue()) { decision.addRiskWarning("对公流水低于营业收入,需核实收入真实性与回款周期"); } if (app.getOverdueCount() > 0) { decision.addRiskWarning("近 2 年存在 " + app.getOverdueCount() + " 次逾期记录,贷后需重点监测"); } if ("MANUAL_REVIEW".equals(decision.getDecision())) { decision.addRiskWarning("评分处于复核区间(60-74),需补充财报与实地尽调"); } }
private double round(double value) { return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_UP).doubleValue(); }
/** 金额文本:固定两位小数且禁用科学计数法(27000000.0 直接拼接会变成 2.7E7) */ private String money(double value) { return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_UP).toPlainString(); }}CorporateLoanServiceTest.java
Section titled “CorporateLoanServiceTest.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.demo.model.CorporateCreditDecision;import com.github.paohaijiao.demo.model.CorporateLoanApplication;import com.github.paohaijiao.demo.service.CorporateLoanService;import com.github.paohaijiao.xml.JQuickJava;import org.junit.Assert;import org.junit.Before;import org.junit.Test;
/** * 场景 16 生产级样板:企业经营贷授信额度智能测算 * <p> * 覆盖 6 组企业形态:优质科技(纳税倍数+营收封顶)、稳健制造(B 级批准)、 * 临界复核(C 级人工)、失信被执行、超高负债、受限行业。 */public class CorporateLoanServiceTest {
private CorporateLoanService service;
@Before public void setUp() { CorporateLoanRules rules = JQuickJava.create() .createApi(CorporateLoanRules.class, "demo/corporate-credit.xml"); this.service = new CorporateLoanService(rules); }
/** * 优质科技企业:90 分 A 级,理论 2700 万先受营收占比压到 2400 万、再触单户 2000 万上限 */ @Test public void shouldApproveTopTechEnterpriseWithRevenueCap() { CorporateLoanApplication app = new CorporateLoanApplication(); app.setApplicationId("CL20260001"); app.setEnterpriseName("华芯半导体科技有限公司"); app.setOperatingYears(9); app.setAnnualRevenue(80000000.0); app.setAnnualTaxAmount(3000000.0); app.setBankFlowAnnual(100000000.0); app.setDebtRatio(45); app.setTaxCreditLevel("A"); app.setOverdueCount(0); app.setHasJudicialRecord(false); app.setIndustryCategory("TECH"); app.setApplyAmount(25000000.0);
CorporateCreditDecision decision = service.evaluate(app);
Assert.assertEquals(90, decision.getTotalScore()); Assert.assertEquals("A", decision.getCreditRating()); Assert.assertEquals("APPROVE", decision.getDecision()); Assert.assertEquals(27000000.0, decision.getTheoreticalLimit(), 0.001); Assert.assertEquals(20000000.0, decision.getApprovedLimit(), 0.001); Assert.assertEquals("CEILING", decision.getCappedBy()); Assert.assertEquals(0.0345, decision.getInterestRate(), 0.00001); Assert.assertTrue(decision.getRiskWarnings().contains( "理论授信额度 27000000.00 元(纳税额 × 9.0)受单户 2000 万绝对上限约束,核定额度 20000000.00 元")); }
/** * 稳健制造企业:78 分 B 级,纳税 350 万 × 4.5 倍理论 1575 万,按营收 30% 核 900 万 */ @Test public void shouldApproveStableManufacturer() { CorporateLoanApplication app = new CorporateLoanApplication(); app.setApplicationId("CL20260002"); app.setEnterpriseName("恒峰精密制造有限公司"); app.setOperatingYears(6); app.setAnnualRevenue(30000000.0); app.setAnnualTaxAmount(3500000.0); app.setBankFlowAnnual(35000000.0); app.setDebtRatio(55); app.setTaxCreditLevel("B"); app.setOverdueCount(0); app.setHasJudicialRecord(false); app.setIndustryCategory("MANUFACTURING"); app.setApplyAmount(10000000.0);
CorporateCreditDecision decision = service.evaluate(app);
Assert.assertEquals(78, decision.getTotalScore()); Assert.assertEquals("B", decision.getCreditRating()); Assert.assertEquals("APPROVE", decision.getDecision()); Assert.assertEquals(15750000.0, decision.getTheoreticalLimit(), 0.001); Assert.assertEquals(9000000.0, decision.getApprovedLimit(), 0.001); Assert.assertEquals("REVENUE", decision.getCappedBy()); Assert.assertEquals(0.0385, decision.getInterestRate(), 0.00001); }
/** * 临界商贸企业:64 分 C 级转人工复核,额度按纳税 2.5 倍核 87.5 万 */ @Test public void shouldRouteBorderlineEnterpriseToManualReview() { CorporateLoanApplication app = new CorporateLoanApplication(); app.setApplicationId("CL20260003"); app.setEnterpriseName("瑞达商贸有限公司"); app.setOperatingYears(5); app.setAnnualRevenue(21000000.0); app.setAnnualTaxAmount(350000.0); app.setBankFlowAnnual(18000000.0); app.setDebtRatio(58); app.setTaxCreditLevel("B"); app.setOverdueCount(1); app.setHasJudicialRecord(false); app.setIndustryCategory("TRADE"); app.setApplyAmount(1000000.0);
CorporateCreditDecision decision = service.evaluate(app);
Assert.assertEquals(64, decision.getTotalScore()); Assert.assertEquals("C", decision.getCreditRating()); Assert.assertEquals("MANUAL_REVIEW", decision.getDecision()); Assert.assertEquals(875000.0, decision.getApprovedLimit(), 0.001); Assert.assertEquals("TAX", decision.getCappedBy()); Assert.assertEquals(0.0435, decision.getInterestRate(), 0.00001); Assert.assertTrue(decision.getRiskWarnings().contains( "评分处于复核区间(60-74),需补充财报与实地尽调")); Assert.assertTrue(decision.getScoreDetails().contains("流水核验评分: 8 / 15")); }
/** * 失信被执行:一票否决,不进入评分 */ @Test public void shouldRejectDishonestEnterprise() { CorporateLoanApplication app = new CorporateLoanApplication(); app.setApplicationId("CL20260004"); app.setEnterpriseName("失信黑名单企业"); app.setOperatingYears(10); app.setAnnualRevenue(50000000.0); app.setAnnualTaxAmount(2000000.0); app.setBankFlowAnnual(60000000.0); app.setDebtRatio(50); app.setTaxCreditLevel("B"); app.setOverdueCount(0); app.setHasJudicialRecord(true); app.setIndustryCategory("TRADE"); app.setApplyAmount(5000000.0);
CorporateCreditDecision decision = service.evaluate(app);
Assert.assertEquals("REJECT", decision.getDecision()); Assert.assertEquals(0, decision.getTotalScore()); Assert.assertEquals(0.0, decision.getApprovedLimit(), 0.001); Assert.assertTrue(decision.getRejectReasons().contains( "企业存在失信被执行/重大涉诉记录,触发一票否决")); }
/** * 资产负债率 88%:超过 85% 硬性上限直接拒绝 */ @Test public void shouldRejectOverLeveragedEnterprise() { CorporateLoanApplication app = new CorporateLoanApplication(); app.setApplicationId("CL20260005"); app.setEnterpriseName("高负债贸易企业"); app.setOperatingYears(7); app.setAnnualRevenue(40000000.0); app.setAnnualTaxAmount(800000.0); app.setBankFlowAnnual(42000000.0); app.setDebtRatio(88); app.setTaxCreditLevel("B"); app.setOverdueCount(1); app.setHasJudicialRecord(false); app.setIndustryCategory("TRADE"); app.setApplyAmount(3000000.0);
CorporateCreditDecision decision = service.evaluate(app);
Assert.assertEquals("REJECT", decision.getDecision()); Assert.assertTrue(decision.getRejectReasons().contains( "资产负债率 88% 超过硬性上限 85%,偿债能力不足")); }
/** * 受限行业(类金融/房地产):行业准入直接拒绝 */ @Test public void shouldRejectRestrictedIndustry() { CorporateLoanApplication app = new CorporateLoanApplication(); app.setApplicationId("CL20260006"); app.setEnterpriseName("某受限行业公司"); app.setOperatingYears(12); app.setAnnualRevenue(120000000.0); app.setAnnualTaxAmount(5000000.0); app.setBankFlowAnnual(150000000.0); app.setDebtRatio(40); app.setTaxCreditLevel("A"); app.setOverdueCount(0); app.setHasJudicialRecord(false); app.setIndustryCategory("RESTRICTED"); app.setApplyAmount(20000000.0);
CorporateCreditDecision decision = service.evaluate(app);
Assert.assertEquals("REJECT", decision.getDecision()); Assert.assertTrue(decision.getRejectReasons().contains("企业属于授信受限行业,不予准入")); }}在工程根目录执行:
mvn test -Dtest=CorporateLoanServiceTest测试覆盖形态
Section titled “测试覆盖形态”| 测试方法 | 构造的业务形态 | 关键断言/期望决策 |
|---|---|---|
shouldApproveTopTechEnterpriseWithRevenueCap |
优质科技企业:9 年、A 级纳税、年纳税 300 万、流水 1 亿、零逾期、申请 2500 万 | 90 分 A 级 APPROVE;理论额度 2700 万,经营收 30%(2400 万)后触单户上限,核定 2000 万,cappedBy=CEILING,利率 0.0345,含核减提示 |
shouldApproveStableManufacturer |
稳健制造:6 年、B 级纳税、负债 55%、纳税 350 万、营收 3000 万 | 78 分 B 级 APPROVE;理论 1575 万,按营收 30% 核定 900 万,cappedBy=REVENUE,利率 0.0385 |
shouldRouteBorderlineEnterpriseToManualReview |
临界商贸:5 年、1 次逾期、负债 58%、流水低于营收 | 64 分 C 级 MANUAL_REVIEW;核定 87.5 万(35 万 × 2.5),cappedBy=TAX,利率 0.0435;含 60-74 分复核提示,流水评分 8 / 15 |
shouldRejectDishonestEnterprise |
其他条件优良但 hasJudicialRecord=true |
REJECT、总分 0、核定额度 0,拒绝原因为失信被执行一票否决 |
shouldRejectOverLeveragedEnterprise |
资产负债率 88% 的贸易企业 | REJECT,原因含「资产负债率 88% 超过硬性上限 85%,偿债能力不足」 |
shouldRejectRestrictedIndustry |
12 年、A 级纳税、营收 1.2 亿但行业为 RESTRICTED | REJECT,原因含「企业属于授信受限行业,不予准入」 |
- 阈值全量外置 XML,政策调整热更新:六维分档线、A/B/C/D 等级线、75/60 审批线、纳税倍数、风险利率全部在
corporate-credit.xml中,授信政策调整只需改 XML 并重建代理,无需重新编译信贷主系统。 - 一方法一口径,多维评分独立调参与审计:10 个规则方法各管一件事,例如只调流水核验比例不会影响年限评分;Service 把每一维得分写入评分明细,审批单可逐项回溯。
- Service 侧一票否决前置 + 评分 clamp + 拒绝短路:受限行业、失信涉诉、超 85% 负债、纳税 D 级、数据缺失在评分前拦截;汇总分 clamp 到 0-100;REJECT 件直接返回,不计算倍数与利率。
- 额度三重封顶可解释:理论额度依次经纳税倍数、营收占比 30%、2000 万绝对上限、申请额压减,
cappedBy记录实际生效的约束项,并生成带理论值与核定值的风险提示。 - 交叉相乘规避除法精度问题:流水核验脚本用
bankFlowAnnual * 10 >= annualRevenue * 12、* 10 >= * 8等交叉相乘表达流水/营收比率,规避引擎中整数除法不提升为浮点的陷阱。 - 金额文本避免科学计数法:
money方法用BigDecimal.setScale(2).toPlainString(),避免 27000000.0 拼接成2.7E7进审批意见。 @Param按名绑定 + 代理复用 + ASM 调用链:接口参数与脚本变量按名称注入,一个 Rules 代理在 Service 中复用 10 个方法;Java 8 起步、无重型依赖,可直接嵌入 JDK 8 老旧信贷系统与信创配套 JDK 环境。