案例 18:供应链金融供应商准入评级
本案例对应 USE-CASES 场景 18「供应链金融供应商准入评级」,属于供应链金融领域,面向核心企业上游供应商的保理业务准入,自动完成供应商评级、准入结论、保理额度核定、担保要求与结算账期建议。
在核心企业主导的供应链金融业务中,上游供应商申请保理授信需要提交一整套档案:工商注册年限、注册资本、资质等级(S/A/B/C/NONE)、近 12 个月与核心企业的交易额、准时交付率、来料/交付质量合格率、合同履约率、合作月数、财务健康状况(HEALTHY/STABLE/WEAK/RISKY),以及失信被执行、近 2 年重大合同违约两个负面标记。核心企业与保理银行需要在合作洽谈或额度续作时快速给出统一口径的准入结论。
决策先由 Service 执行两条一票否决:供应商为失信被执行人的,禁止准入并终止合作洽谈;近 2 年发生重大合同违约(断供/质量事故)的,暂停准入 24 个月。通过否决项后,规则脚本进行五维评分:企业资质(0-20,资质等级为轴、注册资本微调)、履约记录(0-30,准时交付率、质量合格率、合同履约率三项比率各占 10 分)、合作深度(0-15,合作月数 8 分 + 年度交易额 7 分)、财务状况(0-20)、注册年限(0-15),合计满分 100。
总分映射 A/B/C/D 四级:≥85 为 A 级战略供应商、≥75 为 B 级合格、≥60 为 C 级限制类、其余 D 级淘汰。A/B 级直接 APPROVE,C 级转为 CONDITIONAL 附条件准入——须缴纳 10% 履约保证金或引入第三方担保、保理按单笔应收账款逐笔审核、设 6 个月观察期;D 级拒绝。保理额度按年交易额乘以等级比例(A 20%、B 12%、C 5%)测算,并经 C 级 200 万元、单户 1000 万元两道封顶;账期按等级建议 90/60/30 天。财务偏弱(WEAK/RISKY)时附加回款进监管专户封闭运行条款,合作不足 6 个月的新供应商首笔降额 50% 试单。
- 评级口径分散在采购、质量、金融多个部门:交付率分档、资质等级对应分值、保理比例与账期散落在供应商管理制度和各系统代码里,跨部门口径难以统一。
- 评级与额度政策随核心企业经营策略调整:保理比例 20%/12%/5%、1000 万单户上限、C 级 200 万限制线会随风控偏好变化,硬编码每次调整都要发版。
- 履约三维度需要独立调参:准时交付率、质量合格率、合同履约率各 10 分,质量管理部希望单独调严质量口径,耦合在一个总分公式里无法实现。
- 附条件准入条款多且组合复杂:保证金、逐笔审核、观察期、监管专户、新供应商试单等条件按评级、财务、合作月数分别触发,靠人工勾选容易漏项。
- 失信与重大违约必须硬拦截且可审计:即使其他维度满分,失信供应商也不得进入评分流程,评审记录需要明确显示否决依据。
- 核心企业信创与老旧 ERP 环境难引入重型规则平台:供应链系统多与存量 ERP、SRM 深度集成并运行在 JDK 8 上,需要无独立中间件、可嵌入式的规则方案。
本案例沿用六件套骨架:入参模型 + 决策结果模型 + 规则接口 + XML 规则脚本 + Service 编排 + 6 组真实业务形态测试。
| 层次 | 职责 | 对应文件 |
|---|---|---|
| 领域模型 | 供应商档案要素、准入与保理授信决策结果(评级/额度/条件/评分明细) | SupplierProfile.java、SupplierAdmissionDecision.java |
| 规则集与 XML | 五维评分口径、评级阈值、保理比例与账期映射 | SupplierAdmissionRules.java、supplier-admission.xml |
| 编排服务 | 失信/重大违约否决、评级到结论映射、额度双封顶、附加条件组装 | SupplierAdmissionService.java |
| 测试 | 6 组供应商形态(战略/合格/限制/失信/重大违约/新设) | SupplierAdmissionServiceTest.java |
规则方法拆分
Section titled “规则方法拆分”| 规则方法 | 口径 | 分值/输出 |
|---|---|---|
scoreQualification |
资质等级 S/A/B/C 为轴,注册资本 2000 万/500 万/100 万分档微调,无等级 0 分 | 0-20 |
scoreFulfillment |
准时交付率、质量合格率、合同履约率各按 ≥98/95/90/85% 分档,每项 10 分 | 0-30 |
scoreCooperation |
合作月数(≥36/18/6/3 月)0-8 分 + 年交易额(5000 万/2000 万/500 万)0-7 分 | 0-15 |
scoreFinance |
财务状况 HEALTHY 20、STABLE 15、WEAK 8、其余 2 | 0-20 |
scoreRegisteredYears |
注册年限 ≥8/5/3/1 年得 15/12/8/4 分,否则 0 | 0-15 |
grade |
总分 ≥85 A、≥75 B、≥60 C、否则 D | A/B/C/D |
factoringRatio |
评级对应保理额度占年交易额比例:A 0.20、B 0.12、C 0.05、D 0 | 比例 |
settlementDays |
评级对应建议账期:A 90 天、B 60 天、C 30 天、D 0 天 | 天数 |
代码实现(完整可运行)
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=SupplierAdmissionServiceTest 即可运行全部 6 个用例。
SupplierProfile.java
Section titled “SupplierProfile.java”package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;
/** * 核心企业上游供应商档案(供应链金融保理准入要素) */@Data@NoArgsConstructor@AllArgsConstructorpublic class SupplierProfile {
/** * 供应商编号 */ private String supplierId;
/** * 供应商名称 */ private String supplierName;
/** * 工商注册年限 */ private int registeredYears;
/** * 注册资本(万元) */ private int registeredCapitalWan;
/** * 资质等级:S / A / B / C / NONE(未取得体系认证) */ private String qualificationLevel;
/** * 近 12 个月与核心企业交易额(元) */ private double annualOrderAmount;
/** * 准时交付率(百分比,可含一位小数,如 97.5) */ private double onTimeDeliveryRate;
/** * 来料/交付质量合格率(百分比) */ private double qualityPassRate;
/** * 合同履约率(百分比) */ private double contractFulfillRate;
/** * 与核心企业合作月数 */ private int cooperationMonths;
/** * 财务健康状况:HEALTHY / STABLE / WEAK / RISKY */ private String financeStatus;
/** * 是否失信被执行人 */ private boolean dishonest;
/** * 近 2 年是否发生重大合同违约 */ private boolean majorBreach;}SupplierAdmissionDecision.java
Section titled “SupplierAdmissionDecision.java”package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;import java.util.List;
/** * 供应商准入与保理授信决策结果 */@Datapublic class SupplierAdmissionDecision {
/** * 供应商编号 */ private String supplierId;
/** * 准入结论:APPROVE-准入 / CONDITIONAL-附条件准入 / REJECT-淘汰 */ private String decision;
/** * 供应商评级:A-战略 / B-合格 / C-限制 / D-淘汰 */ private String grade;
/** * 综合评分(0-100) */ private int totalScore;
/** * 核定保理额度(元) */ private double factoringLimit;
/** * 是否要求追加担保/保证金 */ private boolean requireGuarantee;
/** * 建议结算账期(天) */ private int settlementDays;
/** * 评分明细 */ private List<String> scoreDetails = new ArrayList<String>();
/** * 拒绝原因 */ private List<String> rejectReasons = new ArrayList<String>();
/** * 风险提示与附加条件 */ private List<String> conditions = new ArrayList<String>();
public void addScoreDetail(String detail) { this.scoreDetails.add(detail); }
public void addRejectReason(String reason) { this.rejectReasons.add(reason); }
public void addCondition(String condition) { this.conditions.add(condition); }}SupplierAdmissionRules.java
Section titled “SupplierAdmissionRules.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/** * 场景 18(生产级样板):供应链金融供应商准入规则集 * <p> * 五维评分(企业资质 / 履约记录 / 合作深度 / 财务状况 / 注册年限), * 再映射评级、保理额度比例、担保要求与账期;失信与重大违约由 Service 一票否决。 * 规则脚本见 {@code src/test/resources/demo/supplier-admission.xml}。 */public interface SupplierAdmissionRules {
/** * 企业资质评分(0-20):资质等级 + 注册资本 */ int scoreQualification(@Param("qualificationLevel") String qualificationLevel, @Param("registeredCapitalWan") int registeredCapitalWan);
/** * 履约记录评分(0-30):准时交付率、质量合格率、合同履约率各 10 分 */ int scoreFulfillment(@Param("onTimeDeliveryRate") double onTimeDeliveryRate, @Param("qualityPassRate") double qualityPassRate, @Param("contractFulfillRate") double contractFulfillRate);
/** * 合作深度评分(0-15):合作月数 + 年度交易额 */ int scoreCooperation(@Param("cooperationMonths") int cooperationMonths, @Param("annualOrderAmount") double annualOrderAmount);
/** * 财务状况评分(0-20) */ int scoreFinance(@Param("financeStatus") String financeStatus);
/** * 注册年限评分(0-15) */ int scoreRegisteredYears(@Param("registeredYears") int registeredYears);
/** * 供应商评级:A / B / C / D */ String grade(@Param("totalScore") int totalScore);
/** * 保理额度占年交易额比例 */ double factoringRatio(@Param("grade") String grade);
/** * 建议结算账期(天) */ int settlementDays(@Param("grade") String grade);}supplier-admission.xml
Section titled “supplier-admission.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.SupplierAdmissionRules">
<!-- 企业资质评分:0-20 --> <java name="scoreQualification" returnClass="int"> <![CDATA[ int def scoreQualification(str:qualificationLevel, int:registeredCapitalWan) { if (qualificationLevel == "S") { if (registeredCapitalWan >= 2000) { return 20; } else if (registeredCapitalWan >= 500) { return 18; } else { return 16; } } else if (qualificationLevel == "A") { if (registeredCapitalWan >= 2000) { return 16; } else if (registeredCapitalWan >= 500) { return 14; } else { return 12; } } else if (qualificationLevel == "B") { if (registeredCapitalWan >= 500) { return 10; } else if (registeredCapitalWan >= 100) { return 8; } else { return 6; } } else if (qualificationLevel == "C") { return 4; } else { return 0; } } ]]> </java>
<!-- 履约记录评分:0-30,三项比率各 10 分 --> <java name="scoreFulfillment" returnClass="int"> <![CDATA[ int def scoreFulfillment(double:onTimeDeliveryRate, double:qualityPassRate, double:contractFulfillRate) { int total = 0; if (onTimeDeliveryRate >= 98.0) { total = total + 10; } else if (onTimeDeliveryRate >= 95.0) { total = total + 8; } else if (onTimeDeliveryRate >= 90.0) { total = total + 5; } else if (onTimeDeliveryRate >= 85.0) { total = total + 3; } else { total = total + 1; } if (qualityPassRate >= 98.0) { total = total + 10; } else if (qualityPassRate >= 95.0) { total = total + 8; } else if (qualityPassRate >= 90.0) { total = total + 5; } else if (qualityPassRate >= 85.0) { total = total + 3; } else { total = total + 1; } if (contractFulfillRate >= 98.0) { total = total + 10; } else if (contractFulfillRate >= 95.0) { total = total + 8; } else if (contractFulfillRate >= 90.0) { total = total + 5; } else if (contractFulfillRate >= 85.0) { total = total + 3; } else { total = total + 1; } return total; } ]]> </java>
<!-- 合作深度评分:0-15 --> <java name="scoreCooperation" returnClass="int"> <![CDATA[ int def scoreCooperation(int:cooperationMonths, double:annualOrderAmount) { int monthsScore = 0; if (cooperationMonths >= 36) { monthsScore = 8; } else if (cooperationMonths >= 18) { monthsScore = 6; } else if (cooperationMonths >= 6) { monthsScore = 4; } else if (cooperationMonths >= 3) { monthsScore = 2; } int orderScore = 0; if (annualOrderAmount >= 50000000.0) { orderScore = 7; } else if (annualOrderAmount >= 20000000.0) { orderScore = 5; } else if (annualOrderAmount >= 5000000.0) { orderScore = 3; } else { orderScore = 1; } return monthsScore + orderScore; } ]]> </java>
<!-- 财务状况评分:0-20 --> <java name="scoreFinance" returnClass="int"> <![CDATA[ int def scoreFinance(str:financeStatus) { if (financeStatus == "HEALTHY") { return 20; } else if (financeStatus == "STABLE") { return 15; } else if (financeStatus == "WEAK") { return 8; } else { return 2; } } ]]> </java>
<!-- 注册年限评分:0-15 --> <java name="scoreRegisteredYears" returnClass="int"> <![CDATA[ int def scoreRegisteredYears(int:registeredYears) { if (registeredYears >= 8) { return 15; } else if (registeredYears >= 5) { return 12; } else if (registeredYears >= 3) { return 8; } else if (registeredYears >= 1) { return 4; } else { return 0; } } ]]> </java>
<!-- 评级映射 --> <java name="grade" returnClass="java.lang.String"> <![CDATA[ str def grade(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="factoringRatio" returnClass="double"> <![CDATA[ double def factoringRatio(str:grade) { if (grade == "A") { return 0.20; } else if (grade == "B") { return 0.12; } else if (grade == "C") { return 0.05; } else { return 0.0; } } ]]> </java>
<!-- 建议账期 --> <java name="settlementDays" returnClass="int"> <![CDATA[ int def settlementDays(str:grade) { if (grade == "A") { return 90; } else if (grade == "B") { return 60; } else if (grade == "C") { return 30; } else { return 0; } } ]]> </java>
</javas>SupplierAdmissionService.java
Section titled “SupplierAdmissionService.java”package com.github.paohaijiao.demo.service;
import com.github.paohaijiao.demo.SupplierAdmissionRules;import com.github.paohaijiao.demo.model.SupplierAdmissionDecision;import com.github.paohaijiao.demo.model.SupplierProfile;
import java.math.BigDecimal;import java.math.RoundingMode;
/** * 场景 18:供应链金融供应商准入服务 * <p> * 规则侧(XML):五维评分口径、评级阈值、保理比例与账期。 * Java 侧:失信/重大违约一票否决、评级到准入结论映射、保理额度封顶与附加条件组装。 */public class SupplierAdmissionService {
/** * 单户保理额度绝对上限(元):1000 万 */ private static final double FACTORING_CEILING = 10000000.0;
/** * C 级(限制类)保理额度上限(元):200 万 */ private static final double GRADE_C_CEILING = 2000000.0;
private final SupplierAdmissionRules rules;
public SupplierAdmissionService(SupplierAdmissionRules rules) { this.rules = rules; }
public SupplierAdmissionDecision evaluate(SupplierProfile supplier) { SupplierAdmissionDecision decision = new SupplierAdmissionDecision(); decision.setSupplierId(supplier.getSupplierId());
if (supplier.isDishonest()) { reject(decision, "供应商为失信被执行人,禁止准入并终止合作洽谈"); return decision; } if (supplier.isMajorBreach()) { reject(decision, "近 2 年发生重大合同违约(断供/质量事故),暂停准入 24 个月"); return decision; }
int qualificationScore = rules.scoreQualification( supplier.getQualificationLevel(), supplier.getRegisteredCapitalWan()); int fulfillmentScore = rules.scoreFulfillment( supplier.getOnTimeDeliveryRate(), supplier.getQualityPassRate(), supplier.getContractFulfillRate()); int cooperationScore = rules.scoreCooperation( supplier.getCooperationMonths(), supplier.getAnnualOrderAmount()); int financeScore = rules.scoreFinance(supplier.getFinanceStatus()); int yearsScore = rules.scoreRegisteredYears(supplier.getRegisteredYears()); int totalScore = qualificationScore + fulfillmentScore + cooperationScore + financeScore + yearsScore; if (totalScore > 100) { totalScore = 100; }
decision.addScoreDetail("企业资质评分: " + qualificationScore + " / 20"); decision.addScoreDetail("履约记录评分: " + fulfillmentScore + " / 30"); decision.addScoreDetail("合作深度评分: " + cooperationScore + " / 15"); decision.addScoreDetail("财务状况评分: " + financeScore + " / 20"); decision.addScoreDetail("注册年限评分: " + yearsScore + " / 15"); decision.addScoreDetail("汇总评分: " + totalScore); decision.setTotalScore(totalScore);
String grade = rules.grade(totalScore); decision.setGrade(grade); decision.setSettlementDays(rules.settlementDays(grade));
if ("A".equals(grade) || "B".equals(grade)) { decision.setDecision("APPROVE"); } else if ("C".equals(grade)) { decision.setDecision("CONDITIONAL"); } else { decision.setDecision("REJECT"); decision.addRejectReason("综合评分 " + totalScore + " 低于准入线 60,评为 D 级供应商"); return decision; }
double ratio = rules.factoringRatio(grade); double limit = supplier.getAnnualOrderAmount() * ratio; if ("C".equals(grade) && limit > GRADE_C_CEILING) { limit = GRADE_C_CEILING; } if (limit > FACTORING_CEILING) { limit = FACTORING_CEILING; } decision.setFactoringLimit(round(limit));
if ("C".equals(grade)) { decision.setRequireGuarantee(true); decision.addCondition("附条件准入:需缴纳 10% 履约保证金或引入第三方担保"); decision.addCondition("保理业务按单笔应收账款逐笔审核,不授循环额度"); decision.addCondition("观察期 6 个月,履约指标连续达标后可申请升为 B 级"); } if ("WEAK".equals(supplier.getFinanceStatus()) || "RISKY".equals(supplier.getFinanceStatus())) { decision.addCondition("财务状况偏弱,保理回款须进入核心企业监管专户封闭运行"); } if (supplier.getCooperationMonths() < 6) { decision.addCondition("与核心企业合作不足 6 个月,首笔业务降额 50% 试单"); } return decision; }
private void reject(SupplierAdmissionDecision decision, String reason) { decision.setDecision("REJECT"); decision.setGrade("D"); decision.setFactoringLimit(0.0); decision.addRejectReason(reason); }
private double round(double value) { return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_UP).doubleValue(); }}SupplierAdmissionServiceTest.java
Section titled “SupplierAdmissionServiceTest.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.demo.model.SupplierAdmissionDecision;import com.github.paohaijiao.demo.model.SupplierProfile;import com.github.paohaijiao.demo.service.SupplierAdmissionService;import com.github.paohaijiao.xml.JQuickJava;import org.junit.Assert;import org.junit.Before;import org.junit.Test;
/** * 场景 18 生产级样板:供应链金融上游供应商准入评级 * <p> * 覆盖 6 组供应商形态:S 级战略(额度触顶 1000 万)、A/B 级合格、 * C 级附条件(担保+监管专户)、失信、重大违约、资质不全新供应商。 */public class SupplierAdmissionServiceTest {
private SupplierAdmissionService service;
@Before public void setUp() { SupplierAdmissionRules rules = JQuickJava.create() .createApi(SupplierAdmissionRules.class, "demo/supplier-admission.xml"); this.service = new SupplierAdmissionService(rules); }
/** * S 级战略供应商:100 分 A 级,理论保理 1600 万触单户上限核 1000 万、账期 90 天 */ @Test public void shouldAdmitStrategicSupplierWithCeiling() { SupplierProfile supplier = new SupplierProfile( "SUP001", "中盛新材料股份有限公司", 8, 3000, "S", 80000000.0, 99.0, 99.2, 99.5, 60, "HEALTHY", false, false);
SupplierAdmissionDecision decision = service.evaluate(supplier);
Assert.assertEquals(100, decision.getTotalScore()); Assert.assertEquals("A", decision.getGrade()); Assert.assertEquals("APPROVE", decision.getDecision()); Assert.assertEquals(10000000.0, decision.getFactoringLimit(), 0.001); Assert.assertFalse(decision.isRequireGuarantee()); Assert.assertEquals(90, decision.getSettlementDays()); Assert.assertTrue(decision.getScoreDetails().contains("履约记录评分: 30 / 30")); }
/** * 合格供应商:78 分 B 级准入,保理 360 万、账期 60 天 */ @Test public void shouldAdmitQualifiedSupplier() { SupplierProfile supplier = new SupplierProfile( "SUP002", "广联五金制品有限公司", 5, 800, "A", 30000000.0, 97.0, 98.0, 96.0, 24, "STABLE", false, false);
SupplierAdmissionDecision decision = service.evaluate(supplier);
Assert.assertEquals(78, decision.getTotalScore()); Assert.assertEquals("B", decision.getGrade()); Assert.assertEquals("APPROVE", decision.getDecision()); Assert.assertEquals(3600000.0, decision.getFactoringLimit(), 0.001); Assert.assertEquals(60, decision.getSettlementDays()); Assert.assertTrue(decision.getConditions().isEmpty()); }
/** * 限制类供应商:62 分 C 级附条件准入,需保证金、监管专户、账期缩至 30 天 */ @Test public void shouldAdmitConditionalSupplierWithGuarantee() { SupplierProfile supplier = new SupplierProfile( "SUP003", "鑫泰包装材料厂", 5, 800, "B", 20000000.0, 96.0, 96.0, 94.0, 24, "WEAK", false, false);
SupplierAdmissionDecision decision = service.evaluate(supplier);
Assert.assertEquals(62, decision.getTotalScore()); Assert.assertEquals("C", decision.getGrade()); Assert.assertEquals("CONDITIONAL", decision.getDecision()); Assert.assertEquals(1000000.0, decision.getFactoringLimit(), 0.001); Assert.assertTrue(decision.isRequireGuarantee()); Assert.assertEquals(30, decision.getSettlementDays()); Assert.assertTrue(decision.getConditions().contains( "附条件准入:需缴纳 10% 履约保证金或引入第三方担保")); Assert.assertTrue(decision.getConditions().contains( "财务状况偏弱,保理回款须进入核心企业监管专户封闭运行")); }
/** * 失信被执行人:一票否决 */ @Test public void shouldRejectDishonestSupplier() { SupplierProfile supplier = new SupplierProfile( "SUP004", "失信供应商", 10, 5000, "A", 60000000.0, 98.0, 98.0, 98.0, 48, "HEALTHY", true, false);
SupplierAdmissionDecision decision = service.evaluate(supplier);
Assert.assertEquals("REJECT", decision.getDecision()); Assert.assertEquals("D", decision.getGrade()); Assert.assertEquals(0.0, decision.getFactoringLimit(), 0.001); Assert.assertTrue(decision.getRejectReasons().contains( "供应商为失信被执行人,禁止准入并终止合作洽谈")); }
/** * 近 2 年重大违约:暂停准入 24 个月 */ @Test public void shouldRejectMajorBreachSupplier() { SupplierProfile supplier = new SupplierProfile( "SUP005", "曾断供供应商", 7, 600, "B", 15000000.0, 92.0, 90.0, 88.0, 30, "STABLE", false, true);
SupplierAdmissionDecision decision = service.evaluate(supplier);
Assert.assertEquals("REJECT", decision.getDecision()); Assert.assertTrue(decision.getRejectReasons().contains( "近 2 年发生重大合同违约(断供/质量事故),暂停准入 24 个月")); }
/** * 资质不全的新设供应商:20 分 D 级淘汰 */ @Test public void shouldRejectUnqualifiedNewSupplier() { SupplierProfile supplier = new SupplierProfile( "SUP006", "某个体户加工厂", 1, 50, "NONE", 2000000.0, 88.0, 86.0, 85.0, 4, "RISKY", false, false);
SupplierAdmissionDecision decision = service.evaluate(supplier);
Assert.assertEquals(18, decision.getTotalScore()); Assert.assertEquals("D", decision.getGrade()); Assert.assertEquals("REJECT", decision.getDecision()); Assert.assertTrue(decision.getRejectReasons().contains("综合评分 18 低于准入线 60,评为 D 级供应商")); Assert.assertEquals(0, decision.getSettlementDays()); }}在工程根目录执行:
mvn test -Dtest=SupplierAdmissionServiceTest测试覆盖形态
Section titled “测试覆盖形态”| 测试方法 | 构造的业务形态 | 关键断言/期望决策 |
|---|---|---|
shouldAdmitStrategicSupplierWithCeiling |
SUP001:注册 8 年、资本 3000 万、S 级资质、年交易 8000 万、三项比率约 99%、合作 60 月、HEALTHY | 100 分 A 级 APPROVE;理论 1600 万触单户上限核定 1000 万;不要求担保、账期 90 天;履约评分 30 / 30 |
shouldAdmitQualifiedSupplier |
SUP002:5 年、800 万、A 级资质、年交易 3000 万、合作 24 月、STABLE | 78 分 B 级 APPROVE;保理 3000 万 × 12% = 360 万;账期 60 天;附加条件为空 |
shouldAdmitConditionalSupplierWithGuarantee |
SUP003:B 级资质、年交易 2000 万、三项比率 94-96%、WEAK | 62 分 C 级 CONDITIONAL;保理 2000 万 × 5% = 100 万(未触 C 级 200 万线);要求担保、账期 30 天;条件含 10% 保证金与监管专户封闭运行 |
shouldRejectDishonestSupplier |
各项指标优良但 dishonest=true |
REJECT、评级 D、保理额度 0,原因含「供应商为失信被执行人,禁止准入并终止合作洽谈」 |
shouldRejectMajorBreachSupplier |
majorBreach=true 的曾断供供应商 |
REJECT,原因含「暂停准入 24 个月」 |
shouldRejectUnqualifiedNewSupplier |
注册 1 年、资本 50 万、资质 NONE、合作 4 月、RISKY 的个体户加工厂 | 18 分 D 级 REJECT;原因含「综合评分 18 低于准入线 60,评为 D 级供应商」;建议账期 0 天 |
- 评级与额度政策全部外置 XML:资质/履约/合作/财务/年限五档分值、85/75/60 评级线、保理比例、账期天数都在 XML 中,核心企业调整供应商政策不必改动 ERP/SRM 主程序。
- 履约三维度一方法内独立计子分、一方法一口径可审计:准时交付率、质量合格率、合同履约率各占 10 分、各自分档累加,质量部门可单独调档;评分明细逐项输出。
- Service 双一票否决前置 + D 级短路:失信、重大违约在评分前直接拒绝;D 级评级立即返回不测算额度;汇总分 clamp 到 100。
- 保理额度双封顶:先按评级比例测算理论额度,C 级压到 200 万以内,再统一受单户 1000 万绝对上限约束,分层风控清晰。
- 附条件准入自动组装:C 级的保证金/逐笔审核/观察期、WEAK/RISKY 财务的监管专户、合作不足 6 个月的试单降额条款按触发条件自动加入条件清单,杜绝人工漏项。
@Param多参按名绑定 + 代理复用 + ASM 调用链:8 个规则方法(含三比率、双轴合作深度方法)共用一个动态代理;Java 8 无重型依赖,可嵌入存量供应链系统与信创环境。