跳转到内容

案例 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
规则方法 口径 分值/输出
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 不授价 年利率

将下列文件按源码包结构放入 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 个用例。

package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 企业经营贷申请件(对公进件要素)
* <p>
* 字段取自企业授信调查表与税务/银行数据授权回传,仅保留额度测算与评级所需要素。
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public 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;
}
package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 企业经营贷授信决策结果
*/
@Data
public 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);
}
}
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);
}
<?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>
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();
}
}
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("企业属于授信受限行业,不予准入"));
}
}

在工程根目录执行:

Terminal window
mvn test -Dtest=CorporateLoanServiceTest
测试方法 构造的业务形态 关键断言/期望决策
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 环境。