Case 16: Corporate business loan limit
This case corresponds to USE-CASES scenario 16, “Intelligent calculation of corporate business loan credit limits,” in the financial risk control domain. It targets SME business-loan applications in a bank’s corporate line of business, automatically producing an integrated calculation of credit rating, approval decision, approved limit, and execution interest rate.
Industry scenario
Section titled “Industry scenario”Application data for a corporate business loan comes from the enterprise credit investigation form plus authorized feeds from tax and banking systems: years in continuous operation, revenue and taxes actually paid over the last 12 months, valid corporate account flow, debt-to-asset ratio, tax credit level (A/B/M/C/D), number of loan overdue events in the last 2 years, dishonesty/litigation flags, industry category, and requested amount. The bank needs to deliver an explainable, auditable credit decision immediately after the account manager submits the application, rather than waiting through a lengthy manual review.
The decision is made in two stages. The first stage is a hard admission gate placed in front of the Service: restricted industries (RESTRICTED, such as quasi-finance and real estate), dishonest judgment debtor / major litigation records, debt-to-asset ratio above 85%, tax credit level D, or missing revenue/requested-amount data — any hit rejects the application outright without entering scoring. The second stage is the six-dimension scoring performed by the rule script: years in operation (0-15), revenue scale (0-20), tax credit (0-20), flow verification (0-15), debt-to-asset ratio (0-15), and credit history (0-15), totaling 100 points. The total score then maps to an A/B/C/D credit rating and an APPROVE / MANUAL_REVIEW / REJECT approval decision.
For non-rejected applications, the system also outputs a complete risk-pricing and limit plan: the tax-amount credit multiplier is determined by tax credit level and total score (up to 9x for level A); the tax paid multiplied by the multiplier gives the theoretical limit, which is then capped in turn by 30% of revenue, a hard 20-million-yuan per-customer ceiling, and the requested amount, producing the approved limit and recording which cap actually took effect. The execution interest rate floats with the credit rating (A 3.45%, B 3.85%, C 4.35%). The final decision object CorporateCreditDecision also carries the scoring breakdown, rejection reasons, and risk warnings (such as flow below revenue, overdue records, and the 60-74 manual-review interval notice), directly supporting approval audit trails and post-loan monitoring.
Business pain points
Section titled “Business pain points”- Six-dimension rules scattered in hard-coded logic: thresholds for operating-year brackets, cross brackets of tax level and tax amount, and flow/revenue matching ratios are hard-wired into the
if-elseof various approval systems, with many branches and inconsistent criteria across branches. - Limit policy changes require a full release: tax multipliers, the revenue-ratio cap, the 20-million per-customer ceiling, and risk-based rates change frequently with monetary policy and internal credit policy; hard coding means every adjustment goes through a complete release cycle.
- Multi-dimensional scoring is hard to tune and trace independently: the weights and brackets of all six dimensions are coupled in one large method, so the flow-verification criteria cannot be adjusted in isolation, and it is hard to explain the origin of every point to the credit committee afterwards.
- Hard vetoes mixed with scoring logic: if one-vote vetoes such as restricted industry, dishonesty/litigation, and over-leverage are buried after scoring, a “high-scoring enterprise still gets vetoed” situation may occur, or rejected applications waste a full scoring pass — auditors cannot tell which came first.
- Multiple caps lack an explainable record: after the theoretical limit is reduced by the revenue ratio, the absolute ceiling, or the requested amount, the approval form does not show which constraint actually took effect, so account managers cannot explain the reduction to the enterprise.
- Legacy credit systems still run on JDK 8, and domestic-platform environments cannot adopt heavy rule platforms: traditional rule engines require standalone middleware and database tables, with high runtime-upgrade and operations costs, making them hard to embed into existing credit systems.
Solution architecture
Section titled “Solution architecture”This case follows the six-piece skeleton of the JQuick-Java production-grade template: input model + decision result model + rule interface + XML rule script + Service orchestration + 6 test cases covering real business profiles.
| Layer | Responsibility | Files |
|---|---|---|
| Domain model | Corporate application fields, credit decision result (score breakdown / rejection reasons / risk warnings / effective cap) | CorporateLoanApplication.java, CorporateCreditDecision.java |
| Rule set & XML | Six-dimension scoring criteria, rating and approval thresholds, tax multipliers, risk pricing — all externalized | CorporateLoanRules.java, corporate-credit.xml |
| Orchestration service | Hard vetoes up front, score aggregation clamped to 0-100, triple caps on the limit, decision assembly | CorporateLoanService.java |
| Tests | 6 enterprise application profiles (top-tier tech / stable manufacturing / borderline review / dishonest debtor / over-leveraged / restricted industry) | CorporateLoanServiceTest.java |
Rule method breakdown
Section titled “Rule method breakdown”| Rule method | Criteria | Score/output |
|---|---|---|
scoreOperatingYears |
Years in continuous operation: >=10 years 15, >=5 years 12, >=3 years 8, >=1 year 4, otherwise 0 | 0-15 |
scoreRevenue |
Annual revenue brackets: 100M / 50M / 20M / 5M yuan | 0-20 |
scoreTaxCredit |
Axis on tax credit level (A/B/M/C), fine-tuned by tax paid at 1M / 300K yuan | 0-20 |
scoreBankFlow |
Match between valid flow and revenue via cross multiplication; non-positive revenue scores 0 directly | 0-15 |
scoreDebtRatio |
Debt-to-asset ratio: ≤40% 15, ≤55% 12, ≤70% 8, ≤80% 4, otherwise 0 | 0-15 |
scoreCreditRecord |
Dishonesty/litigation record scores 0 directly; 0 overdues 15, ≤2 11, ≤4 6, otherwise 0 | 0-15 |
creditRating |
Total score >=85 A, >=75 B, >=60 C, otherwise D | A/B/C/D |
approveDecision |
Total score >=75 APPROVE, >=60 MANUAL_REVIEW, otherwise REJECT | Approval decision |
taxMultiplier |
Tax-amount credit multiplier from tax level x score bracket (up to 9.0 for level A, 0 outside C/M) | Multiplier |
interestRate |
Execution annual rate by rating: A 3.45%, B 3.85%, C 4.35%, no pricing for D | Annual rate |
Code implementation (complete & runnable)
Section titled “Code implementation (complete & runnable)”Place the following files into the test directory of a JQuick-Java project following the source package structure: the two models go to src/test/java/com/github/paohaijiao/demo/model/, the rule interface and test class go to src/test/java/com/github/paohaijiao/demo/, the Service goes to src/test/java/com/github/paohaijiao/demo/service/, and the XML script goes to src/test/resources/demo/. Keep the package names exactly as in the source (com.github.paohaijiao.demo / .model / .service). Then run mvn test -Dtest=CorporateLoanServiceTest to execute all 6 cases.
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("企业属于授信受限行业,不予准入")); }}Run & verified results
Section titled “Run & verified results”Run from the project root:
mvn test -Dtest=CorporateLoanServiceTestTest coverage
Section titled “Test coverage”| Test method | Constructed business profile | Key assertions / expected decision |
|---|---|---|
shouldApproveTopTechEnterpriseWithRevenueCap |
Top-tier tech enterprise: 9 years, tax level A, 3M yuan annual tax, 100M flow, zero overdues, requesting 25M | 90 points, level A, APPROVE; theoretical limit 27M, compressed by the 30% revenue ratio (24M) and then by the per-customer ceiling to 20M, cappedBy=CEILING, rate 0.0345, includes the reduction warning |
shouldApproveStableManufacturer |
Stable manufacturer: 6 years, tax level B, 55% debt ratio, 3.5M tax, 30M revenue | 78 points, level B, APPROVE; theoretical 15.75M, approved 9M under the 30% revenue ratio, cappedBy=REVENUE, rate 0.0385 |
shouldRouteBorderlineEnterpriseToManualReview |
Borderline trader: 5 years, 1 overdue, 58% debt ratio, flow below revenue | 64 points, level C, MANUAL_REVIEW; approved 875K (350K x 2.5), cappedBy=TAX, rate 0.0435; includes the 60-74 review-interval warning, flow score 8 / 15 |
shouldRejectDishonestEnterprise |
Otherwise strong profile but hasJudicialRecord=true |
REJECT, total score 0, approved limit 0, rejection reason is the one-vote veto for dishonest judgment debtor |
shouldRejectOverLeveragedEnterprise |
Trading enterprise with 88% debt ratio | REJECT, reason contains “debt ratio 88% exceeds the hard limit of 85%, insufficient solvency” |
shouldRejectRestrictedIndustry |
12 years, tax level A, 120M revenue, but industry is RESTRICTED | REJECT, reason contains “enterprise belongs to a credit-restricted industry, admission denied” |
Technical highlights
Section titled “Technical highlights”- All thresholds externalized to XML with hot-updatable policy: the six-dimension brackets, A/B/C/D rating lines, 75/60 approval lines, tax multipliers, and risk rates all live in
corporate-credit.xml; credit-policy changes only require editing the XML and rebuilding the proxy, with no recompilation of the core credit system. - One method per criterion; multi-dimensional scoring independently tunable and auditable: 10 rule methods each own a single concern — adjusting only the flow-verification ratio does not affect the years score; the Service writes every dimension’s score into the score breakdown so the approval form can be traced item by item.
- Service-side one-vote veto up front + score clamping + reject short-circuit: restricted industry, dishonesty/litigation, debt above 85%, tax level D, and missing data are intercepted before scoring; the aggregated score is clamped to 0-100; REJECT cases return immediately without computing the multiplier and rate.
- Explainable triple caps on the limit: the theoretical limit is reduced in turn by the tax multiplier, the 30% revenue ratio, the 20M absolute ceiling, and the requested amount;
cappedByrecords the constraint that actually took effect, and a risk warning with both theoretical and approved values is generated. - Cross multiplication avoids division-precision pitfalls: the flow-verification script expresses flow/revenue ratios via
bankFlowAnnual * 10 >= annualRevenue * 12,* 10 >= * 8, and similar cross products, avoiding the trap where integer division is not promoted to floating point in the engine. - Monetary text avoids scientific notation: the
moneymethod usesBigDecimal.setScale(2).toPlainString()so 27000000.0 is never concatenated as2.7E7into approval comments. @Paramname binding + proxy reuse + ASM call chain: interface parameters are injected into script variables by name, and a single Rules proxy is reused across 10 methods in the Service; starting from Java 8 with no heavy dependencies, it embeds directly into JDK 8 legacy credit systems and domestic-platform JDK environments.