案例 24:行业许可证年审合规校验
本案例对应 USE-CASES 场景 24「行业许可证年审合规校验(诉求 23)」,属于政务监管领域,以食品经营许可证年审为样板,面向市场监管部门对持证经营主体的年度合规审查,自动完成八个维度正向计分、通过/附条件/不予通过分档,并对证照失效与重大违法违规执行红线驳回。
食品经营主体每年须报送许可证年审材料,申报要素覆盖经营资质与日常合规两类:许可证是否在有效期内、营业执照是否有效、健康证过期人数与应持健康证总人数、食品安全培训覆盖率、是否按规定配备食品安全员、日常监督检查一般不符合项数、上年度不符合项是否完成整改、年内有效消费投诉件数、环境卫生量化等级(A/B/C)、是否投保食品安全责任险、年度重大违法违规次数。
年审采用八个维度正向计分、满分 100:健康证管理 20 分(零过期满分,过期比例低于 10% 得 12 分、低于 30% 得 6 分)、食安培训 15 分(覆盖率 100%/80%/60% 对应 15/12/8,其余 2 分)、食安员配备 10 分、检查缺陷项 15 分(0/≤2/≤5 项对应 15/10/5)、问题整改 10 分、投诉处置 10 分(0/≤3/≤10 件对应 10/6/2)、环境卫生 10 分(A/B/C 对应 10/6/0)、食安责任险 10 分(未投保仍得 5 分基础分)。
审查结论分四档:总分 ≥85 的 PASS 通过,≥70 的 CONDITIONAL 附条件通过,其余 FAIL 不予通过;此外单设 REJECT 红线驳回档,三条红线不进入计分直接驳回:许可证超过有效期、营业执照失效或被吊销、年度内存在重大食品安全违法违规记录。附条件与不予通过的主体还会收到逐项不合规告知,包括健康证过期调离、培训补训补考、配备食安员、缺陷项整改、整改闭环、投诉处置、C 级停业整顿、建议投保责任险等。
决策结果携带合规总分、九个维度的得分明细(含各维度满分分母与合规总分行)以及不合规项与整改要求清单,直接生成年审结论书与限期整改通知;PASS 主体不再罗列不合规项。健康证应持总人数为 0(台账缺失)时,Java 侧直接判 0 分并在不合规项中要求限期补建台账,不调用比率规则,从源头规避除零问题。
- 评分口径随年审细则逐年调参:各维度档位分值、85/70 分档线、培训覆盖率与投诉件数门槛每年都可能修订,硬编码进监管系统后调整成本高。
- 比率维度用整数运算易出错:健康证过期比例 10%/30% 的判断若写成整数除法会直接截断为 0,导致大面积过期仍拿满分。
- 红线必须先于评分驳回:证照失效、重大违规的主体不能因为其他维度满分而通过年审,红线短路顺序是合规底线。
- 除零与台账缺失场景要兜底:应持健康证总人数为 0 时不能直接算比率,须单独识别为「未建立台账」并给 0 分。
- 整改意见必须逐项对应失分项:附条件与不予通过门店需要拿到具体的人员调离、补训、整改、投保等告知,笼统结论无法支撑基层执法。
- 基层监管环境信创与存量 JDK 8 约束:年审系统多为存量政务应用,不能为评分能力引入重型独立平台。
本案例沿用六件套骨架:入参模型 + 决策结果模型 + 规则接口 + XML 规则脚本 + Service 编排 + 6 组真实业务形态测试。
| 层次 | 职责 | 对应文件 |
|---|---|---|
| 领域模型 | 许可证年审申报要素、年审决策结果(结论/总分/得分明细/不合规项) | LicenseReviewApplication.java、LicenseReviewDecision.java |
| 规则集与 XML | 八维计分口径、健康证比率分档、85/70 结论线 | LicenseAnnualReviewRules.java、license-annual-review.xml |
| 编排服务 | 三条红线前置驳回、除零保护、八维汇总、不合规项逐项告知 | LicenseAnnualReviewService.java |
| 测试 | 6 组主体形态(满分通过/轻微附条件/严重不予通过/重大违规/证照失效/85 分临界) | LicenseAnnualReviewServiceTest.java |
规则方法拆分
Section titled “规则方法拆分”| 规则方法 | 口径 | 分值/输出 |
|---|---|---|
scoreHealth |
零过期 20;过期比例低于 10%(expired×10 小于 total)12;低于 30%(expired×10 小于 total×3)6;否则 0 | 0-20 |
scoreTraining |
覆盖率 ≥100% 得 15、≥80% 得 12、≥60% 得 8,否则 2 | 2-15 |
scoreOfficer |
已配备食品安全员 10,否则 0 | 0/10 |
scoreDefects |
缺陷 0 项 15、≤2 项 10、≤5 项 5,否则 0 | 0-15 |
scoreRectification |
上年度不符合项已完成整改 10,否则 0 | 0/10 |
scoreComplaints |
投诉 0 件 10、≤3 件 6、≤10 件 2,否则 0 | 0-10 |
scoreEnvironment |
环境量化等级 A 10、B 6、其余(含 C)0 | 0-10 |
scoreInsurance |
已投保食安责任险 10,未投保保留 5 分基础分 | 5/10 |
reviewLevel |
总分 ≥85 PASS、≥70 CONDITIONAL、否则 FAIL | 三种结论 |
代码实现(完整可运行)
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=LicenseAnnualReviewServiceTest 即可运行全部 6 个用例。
LicenseReviewApplication.java
Section titled “LicenseReviewApplication.java”package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;
/** * 食品经营许可证年审申报要素 */@Data@NoArgsConstructor@AllArgsConstructorpublic class LicenseReviewApplication {
/** * 许可证编号 */ private String licenseId;
/** * 经营主体名称 */ private String enterpriseName;
/** * 许可证是否在有效期内 */ private boolean validWithinPeriod;
/** * 营业执照是否有效 */ private boolean businessLicenseValid;
/** * 健康证过期人数 */ private int healthCertificateExpired;
/** * 应持健康证总人数 */ private int healthCertificateTotal;
/** * 食品安全培训覆盖率(整数百分比) */ private int trainingRate;
/** * 是否按规定配备食品安全员 */ private boolean foodSafetyOfficer;
/** * 日常监督检查一般不符合项数 */ private int defectCount;
/** * 上年度不符合项是否完成整改 */ private boolean rectificationCompleted;
/** * 年内有效消费投诉件数 */ private int complaintCount;
/** * 环境卫生量化等级:A / B / C */ private String environmentalRating;
/** * 是否投保食品安全责任险 */ private boolean liabilityInsurance;
/** * 年度重大违法违规次数 */ private int majorViolationCount;}LicenseReviewDecision.java
Section titled “LicenseReviewDecision.java”package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;import java.util.List;
/** * 许可证年审合规决策结果 */@Datapublic class LicenseReviewDecision {
/** * 许可证编号 */ private String licenseId;
/** * 年审结论:PASS-通过 / CONDITIONAL-附条件通过 / FAIL-不予通过 / REJECT-红线驳回 */ private String result;
/** * 合规评分(0-100) */ private int totalScore;
/** * 各维度得分明细 */ private List<String> scoreDetails = new ArrayList<String>();
/** * 不合规项与整改要求 */ private List<String> nonConformities = new ArrayList<String>();
public void addScoreDetail(String detail) { this.scoreDetails.add(detail); }
public void addNonConformity(String nonConformity) { this.nonConformities.add(nonConformity); }}LicenseAnnualReviewRules.java
Section titled “LicenseAnnualReviewRules.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/** * 场景 24:行业许可证(食品经营)年审合规评分规则集 * <p> * 八个维度正向计分,满分 100:健康证 20 / 培训 15 / 食安员 10 / 缺陷项 15 * / 整改 10 / 投诉 10 / 环境等级 10 / 责任险 10;85 通过、70 附条件。 * 规则脚本见 {@code src/test/resources/demo/license-annual-review.xml}。 */public interface LicenseAnnualReviewRules {
/** 健康证合规得分:零过期满分,过期比例 10%/30% 两段,交叉相乘避免整数除法 */ int scoreHealth(@Param("expired") int expired, @Param("total") int total);
/** 食安培训覆盖率得分:100% 满分,80%/60% 两段 */ int scoreTraining(@Param("rate") int rate);
/** 是否配备食品安全员 */ int scoreOfficer(@Param("hasOfficer") boolean hasOfficer);
/** 日常检查缺陷项得分:0 项满分,2 项/5 项两段 */ int scoreDefects(@Param("defectCount") int defectCount);
/** 上年度问题整改完成情况 */ int scoreRectification(@Param("completed") boolean completed);
/** 有效投诉得分:0 件满分,3 件/10 件两段 */ int scoreComplaints(@Param("complaintCount") int complaintCount);
/** 环境卫生量化等级得分 */ int scoreEnvironment(@Param("rating") String rating);
/** 食品安全责任险投保情况(未投保仍得基础分 5) */ int scoreInsurance(@Param("hasInsurance") boolean hasInsurance);
/** 年审结论:85 分通过,70 分附条件,其余不予通过 */ String reviewLevel(@Param("totalScore") int totalScore);}license-annual-review.xml
Section titled “license-annual-review.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.LicenseAnnualReviewRules">
<!-- 健康证:expired/total 比率用交叉相乘(expired*10 对 total、total*3) --> <java name="scoreHealth" returnClass="int"> <![CDATA[ int def scoreHealth(int:expired, int:total) { if (expired == 0) { return 20; } else if (expired * 10 < total) { return 12; } else if (expired * 10 < total * 3) { return 6; } else { return 0; } } ]]> </java>
<!-- 培训覆盖率 --> <java name="scoreTraining" returnClass="int"> <![CDATA[ int def scoreTraining(int:rate) { if (rate >= 100) { return 15; } else if (rate >= 80) { return 12; } else if (rate >= 60) { return 8; } else { return 2; } } ]]> </java>
<!-- 食品安全员配备 --> <java name="scoreOfficer" returnClass="int"> <![CDATA[ int def scoreOfficer(boolean:hasOfficer) { if (hasOfficer) { return 10; } else { return 0; } } ]]> </java>
<!-- 日常检查缺陷项 --> <java name="scoreDefects" returnClass="int"> <![CDATA[ int def scoreDefects(int:defectCount) { if (defectCount == 0) { return 15; } else if (defectCount <= 2) { return 10; } else if (defectCount <= 5) { return 5; } else { return 0; } } ]]> </java>
<!-- 上年度整改 --> <java name="scoreRectification" returnClass="int"> <![CDATA[ int def scoreRectification(boolean:completed) { if (completed) { return 10; } else { return 0; } } ]]> </java>
<!-- 有效投诉 --> <java name="scoreComplaints" returnClass="int"> <![CDATA[ int def scoreComplaints(int:complaintCount) { if (complaintCount == 0) { return 10; } else if (complaintCount <= 3) { return 6; } else if (complaintCount <= 10) { return 2; } else { return 0; } } ]]> </java>
<!-- 环境卫生量化等级 --> <java name="scoreEnvironment" returnClass="int"> <![CDATA[ int def scoreEnvironment(str:rating) { if (rating == "A") { return 10; } else if (rating == "B") { return 6; } else { return 0; } } ]]> </java>
<!-- 食安责任险 --> <java name="scoreInsurance" returnClass="int"> <![CDATA[ int def scoreInsurance(boolean:hasInsurance) { if (hasInsurance) { return 10; } else { return 5; } } ]]> </java>
<!-- 年审结论分档 --> <java name="reviewLevel" returnClass="java.lang.String"> <![CDATA[ str def reviewLevel(int:totalScore) { if (totalScore >= 85) { return "PASS"; } else if (totalScore >= 70) { return "CONDITIONAL"; } else { return "FAIL"; } } ]]> </java>
</javas>LicenseAnnualReviewService.java
Section titled “LicenseAnnualReviewService.java”package com.github.paohaijiao.demo.service;
import com.github.paohaijiao.demo.LicenseAnnualReviewRules;import com.github.paohaijiao.demo.model.LicenseReviewApplication;import com.github.paohaijiao.demo.model.LicenseReviewDecision;
/** * 场景 24:行业许可证年审合规校验服务 * <p> * 职责划分: * <ul> * <li><b>Java 侧</b>:三条红线前置驳回、八维得分汇总、不合规项逐项告知</li> * <li><b>规则侧</b>:八维计分口径与通过/附条件/不通过阈值,外置在 XML 中</li> * </ul> */public class LicenseAnnualReviewService {
private final LicenseAnnualReviewRules rules;
public LicenseAnnualReviewService(LicenseAnnualReviewRules rules) { this.rules = rules; }
public LicenseReviewDecision review(LicenseReviewApplication app) { LicenseReviewDecision decision = new LicenseReviewDecision(); decision.setLicenseId(app.getLicenseId());
if (!app.isValidWithinPeriod()) { decision.setResult("REJECT"); decision.addNonConformity("许可证已超过有效期,须重新申领后再经营"); return decision; } if (!app.isBusinessLicenseValid()) { decision.setResult("REJECT"); decision.addNonConformity("营业执照失效或被吊销,不予年审"); return decision; } if (app.getMajorViolationCount() >= 1) { decision.setResult("REJECT"); decision.addNonConformity("年度内存在 " + app.getMajorViolationCount() + " 次重大食品安全违法违规记录,不予年审"); return decision; }
int health = app.getHealthCertificateTotal() <= 0 ? 0 : rules.scoreHealth(app.getHealthCertificateExpired(), app.getHealthCertificateTotal()); int training = rules.scoreTraining(app.getTrainingRate()); int officer = rules.scoreOfficer(app.isFoodSafetyOfficer()); int defects = rules.scoreDefects(app.getDefectCount()); int rectification = rules.scoreRectification(app.isRectificationCompleted()); int complaints = rules.scoreComplaints(app.getComplaintCount()); int environment = rules.scoreEnvironment(app.getEnvironmentalRating()); int insurance = rules.scoreInsurance(app.isLiabilityInsurance()); int total = health + training + officer + defects + rectification + complaints + environment + insurance;
decision.addScoreDetail("健康证管理: " + health + " / 20"); decision.addScoreDetail("食安培训: " + training + " / 15"); decision.addScoreDetail("食安员配备: " + officer + " / 10"); decision.addScoreDetail("检查缺陷项: " + defects + " / 15"); decision.addScoreDetail("问题整改: " + rectification + " / 10"); decision.addScoreDetail("投诉处置: " + complaints + " / 10"); decision.addScoreDetail("环境卫生: " + environment + " / 10"); decision.addScoreDetail("食安责任险: " + insurance + " / 10"); decision.addScoreDetail("合规总分: " + total); decision.setTotalScore(total); decision.setResult(rules.reviewLevel(total));
if (!"PASS".equals(decision.getResult())) { buildNonConformities(app, decision); } return decision; }
private void buildNonConformities(LicenseReviewApplication app, LicenseReviewDecision decision) { if (app.getHealthCertificateTotal() <= 0) { decision.addNonConformity("未建立从业人员健康证管理台账,须限期补建"); } else if (app.getHealthCertificateExpired() > 0) { decision.addNonConformity(app.getHealthCertificateExpired() + " 名从业人员健康证已过期,须立即调离接触直接入口食品岗位"); } if (app.getTrainingRate() < 80) { decision.addNonConformity("食品安全培训覆盖率 " + app.getTrainingRate() + "% 低于 80%,须补训补考"); } if (!app.isFoodSafetyOfficer()) { decision.addNonConformity("未按规定配备食品安全总监或食品安全员"); } if (app.getDefectCount() > 0) { decision.addNonConformity("日常监督检查发现 " + app.getDefectCount() + " 项一般不符合项,须逐项整改"); } if (!app.isRectificationCompleted()) { decision.addNonConformity("上年度不符合项未完成整改闭环,须提交整改报告"); } if (app.getComplaintCount() > 0) { decision.addNonConformity("年内有效消费投诉 " + app.getComplaintCount() + " 件,须优化处置时效"); } if ("C".equals(app.getEnvironmentalRating())) { decision.addNonConformity("环境卫生量化等级为 C 级,须停业整顿后复查"); } if (!app.isLiabilityInsurance()) { decision.addNonConformity("建议投保食品安全责任保险,完善风险分担机制"); } }}LicenseAnnualReviewServiceTest.java
Section titled “LicenseAnnualReviewServiceTest.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.demo.model.LicenseReviewApplication;import com.github.paohaijiao.demo.model.LicenseReviewDecision;import com.github.paohaijiao.demo.service.LicenseAnnualReviewService;import com.github.paohaijiao.xml.JQuickJava;import org.junit.Assert;import org.junit.Before;import org.junit.Test;
/** * 场景 24:食品经营许可证年审合规校验 * <p> * 覆盖 6 组主体形态:标杆门店满分通过、轻微问题附条件、问题门店不予通过、 * 重大违规红线、证照失效红线、85 分临界通过。 */public class LicenseAnnualReviewServiceTest {
private LicenseAnnualReviewService service;
@Before public void setUp() { LicenseAnnualReviewRules rules = JQuickJava.create() .createApi(LicenseAnnualReviewRules.class, "demo/license-annual-review.xml"); this.service = new LicenseAnnualReviewService(rules); }
/** * 各项全合规标杆门店 → 100 分通过 */ @Test public void shouldPassModelStoreWithFullScore() { LicenseReviewApplication app = baseApplication("LIC2026001"); app.setHealthCertificateExpired(0); app.setHealthCertificateTotal(10); app.setTrainingRate(100); app.setFoodSafetyOfficer(true); app.setDefectCount(0); app.setRectificationCompleted(true); app.setComplaintCount(0); app.setEnvironmentalRating("A"); app.setLiabilityInsurance(true);
LicenseReviewDecision decision = service.review(app);
Assert.assertEquals(100, decision.getTotalScore()); Assert.assertEquals("PASS", decision.getResult()); Assert.assertEquals(0, decision.getNonConformities().size()); Assert.assertTrue(decision.getScoreDetails().contains("健康证管理: 20 / 20")); }
/** * 个别健康证过期、2 项缺陷、2 件投诉、B 级环境、未投保 → 71 分附条件通过 */ @Test public void shouldPassConditionallyWithMinorIssues() { LicenseReviewApplication app = baseApplication("LIC2026002"); app.setHealthCertificateExpired(1); app.setHealthCertificateTotal(20); app.setTrainingRate(85); app.setFoodSafetyOfficer(true); app.setDefectCount(2); app.setRectificationCompleted(true); app.setComplaintCount(2); app.setEnvironmentalRating("B"); app.setLiabilityInsurance(false);
LicenseReviewDecision decision = service.review(app);
Assert.assertEquals(71, decision.getTotalScore()); Assert.assertEquals("CONDITIONAL", decision.getResult()); Assert.assertTrue(decision.getScoreDetails().contains("健康证管理: 12 / 20")); Assert.assertTrue(decision.getNonConformities().contains( "日常监督检查发现 2 项一般不符合项,须逐项整改")); Assert.assertTrue(decision.getNonConformities().contains( "建议投保食品安全责任保险,完善风险分担机制")); }
/** * 健康证大面积过期、无食安员、缺陷多、未整改、投诉多、C 级环境 → 7 分不予通过 */ @Test public void shouldFailStoreWithSeriousNonConformities() { LicenseReviewApplication app = baseApplication("LIC2026003"); app.setHealthCertificateExpired(8); app.setHealthCertificateTotal(20); app.setTrainingRate(50); app.setFoodSafetyOfficer(false); app.setDefectCount(7); app.setRectificationCompleted(false); app.setComplaintCount(12); app.setEnvironmentalRating("C"); app.setLiabilityInsurance(false);
LicenseReviewDecision decision = service.review(app);
Assert.assertEquals(7, decision.getTotalScore()); Assert.assertEquals("FAIL", decision.getResult()); Assert.assertTrue(decision.getNonConformities().contains( "环境卫生量化等级为 C 级,须停业整顿后复查")); Assert.assertTrue(decision.getNonConformities().contains( "未按规定配备食品安全总监或食品安全员")); Assert.assertTrue(decision.getScoreDetails().contains("食安培训: 2 / 15")); }
/** * 其余全合规但年度内发生重大违规 → 红线直接驳回 */ @Test public void shouldRejectForMajorViolation() { LicenseReviewApplication app = baseApplication("LIC2026004"); app.setHealthCertificateExpired(0); app.setHealthCertificateTotal(8); app.setTrainingRate(100); app.setFoodSafetyOfficer(true); app.setDefectCount(0); app.setRectificationCompleted(true); app.setComplaintCount(0); app.setEnvironmentalRating("A"); app.setLiabilityInsurance(true); app.setMajorViolationCount(1);
LicenseReviewDecision decision = service.review(app);
Assert.assertEquals("REJECT", decision.getResult()); Assert.assertTrue(decision.getNonConformities().contains( "年度内存在 1 次重大食品安全违法违规记录,不予年审")); }
/** * 许可证过期、营业执照失效分别驳回 */ @Test public void shouldRejectForInvalidLicenseAndBusinessLicense() { LicenseReviewApplication expired = baseApplication("LIC2026005"); expired.setValidWithinPeriod(false); LicenseReviewDecision d1 = service.review(expired); Assert.assertEquals("REJECT", d1.getResult()); Assert.assertTrue(d1.getNonConformities().contains("许可证已超过有效期,须重新申领后再经营"));
LicenseReviewApplication revoked = baseApplication("LIC2026006"); revoked.setBusinessLicenseValid(false); LicenseReviewDecision d2 = service.review(revoked); Assert.assertEquals("REJECT", d2.getResult()); Assert.assertTrue(d2.getNonConformities().contains("营业执照失效或被吊销,不予年审")); }
/** * 恰好 85 分通过线:投诉 5 件、B 级环境但其他项满分 → PASS,不再列不合规项 */ @Test public void shouldPassAtExactScoreThreshold() { LicenseReviewApplication app = baseApplication("LIC2026007"); app.setHealthCertificateExpired(0); app.setHealthCertificateTotal(12); app.setTrainingRate(80); app.setFoodSafetyOfficer(true); app.setDefectCount(0); app.setRectificationCompleted(true); app.setComplaintCount(5); app.setEnvironmentalRating("B"); app.setLiabilityInsurance(true);
LicenseReviewDecision decision = service.review(app);
Assert.assertEquals(85, decision.getTotalScore()); Assert.assertEquals("PASS", decision.getResult()); Assert.assertEquals(0, decision.getNonConformities().size()); Assert.assertTrue(decision.getScoreDetails().contains("投诉处置: 2 / 10")); }
private LicenseReviewApplication baseApplication(String licenseId) { LicenseReviewApplication app = new LicenseReviewApplication(); app.setLicenseId(licenseId); app.setEnterpriseName("测试餐饮单位"); app.setValidWithinPeriod(true); app.setBusinessLicenseValid(true); app.setHealthCertificateExpired(0); app.setHealthCertificateTotal(10); app.setTrainingRate(100); app.setFoodSafetyOfficer(true); app.setDefectCount(0); app.setRectificationCompleted(true); app.setComplaintCount(0); app.setEnvironmentalRating("A"); app.setLiabilityInsurance(true); app.setMajorViolationCount(0); return app; }}在工程根目录执行:
mvn test -Dtest=LicenseAnnualReviewServiceTest测试覆盖形态
Section titled “测试覆盖形态”| 测试方法 | 构造的业务形态 | 关键断言/期望决策 |
|---|---|---|
shouldPassModelStoreWithFullScore |
零健康证过期、培训 100%、有食安员、零缺陷、已整改、零投诉、A 级、已投保 | 100 分 PASS;nonConformities 0 条;明细含「健康证管理: 20 / 20」 |
shouldPassConditionallyWithMinorIssues |
20 人中 1 人健康证过期、培训 85%、缺陷 2 项、投诉 2 件、B 级、未投保 | 71 分 CONDITIONAL;明细含「健康证管理: 12 / 20」;不合规项含 2 项缺陷整改与建议投保责任险 |
shouldFailStoreWithSeriousNonConformities |
20 人中 8 人过期、培训 50%、无食安员、缺陷 7 项、未整改、投诉 12 件、C 级、未投保 | 7 分 FAIL;含「C 级,须停业整顿后复查」「未按规定配备食品安全总监或食品安全员」;明细含「食安培训: 2 / 15」 |
shouldRejectForMajorViolation |
其余全合规但 majorViolationCount=1 | REJECT(红线先于计分);含「年度内存在 1 次重大食品安全违法违规记录,不予年审」 |
shouldRejectForInvalidLicenseAndBusinessLicense |
两个子场景:许可证超期;营业执照失效 | 均 REJECT;分别命中「须重新申领后再经营」「营业执照失效或被吊销,不予年审」 |
shouldPassAtExactScoreThreshold |
培训 80%、投诉 5 件(得 2 分)、B 级(6 分),其余满分 | 恰好 85 分 PASS(压线通过);nonConformities 0 条;明细含「投诉处置: 2 / 10」 |
- 评分口径全量外置 XML:八个维度的分档分值、10%/30% 健康证比率线、85/70 结论线都在脚本中,年审细则年度调参只需修改 XML 重建代理,监管主系统免发版。
- 交叉相乘规避整数除法陷阱:脚本为整数运算且不自动提升浮点,
scoreHealth以expired * 10 < total、expired * 10 < total * 3表达 10%/30% 比率,避免expired / total截断为 0 的误判。 - 三条红线 Java 侧前置短路:许可证超期、营业执照失效、重大违规在八维计分之前依次 REJECT 返回,红线主体不可能凭其他维度高分通过年审。
- 除零保护与台账缺失合并处理:
healthCertificateTotal <= 0时 Java 侧直接给 0 分、不调用比率规则,同时在不合规项中要求补建健康证台账,杜绝脚本侧除零异常。 - 正向计分 + 失分项逐项告知:Service 汇总九行得分明细(含合规总分),仅对非 PASS 主体按原始申报值逐项生成整改告知,PASS 不罗列问题,结论书与整改通知一次生成。
@Param按名绑定 + ASM 动态代理 + Java 8 无重型依赖:9 个规则方法共用一个代理,可嵌入存量年审系统与信创配套 JDK,评分过程可解释、可复核。