案例 12:产线质检分级与处置
本案例对应 JQuick-Java 仓库 USE-CASES.md 中的场景 12,面向制造 MES 领域的离散制造批次质检。系统对每批抽检记录按致命/严重/轻微缺陷数扣分,叠加外观、关键尺寸公差、设备状态加扣项,结合以千分比整数运算得出的不良率,判定接收(ACCEPT)、返工(REWORK)、报废(SCRAP)、挂起(HOLD)四级结论,并进一步决策处置去向、批次是否放行以及是否触发 CAPA 纠正预防措施,全部判定口径由 JQuick-Java 的 XML 规则代理驱动。
离散制造车间按批次组织生产与抽检。一批精密轴套下线后,质检工位录入批次号、产品信息、抽检数量,以及抽检中发现的致命缺陷(安全/功能丧失)、严重缺陷、轻微缺陷件数,同时记录关键尺寸的实测值、标称值与允许偏差、0-100 分的外观评分、生产设备状态(NORMAL / WARNING)以及是否首件检验。
系统先计算两个基础指标:不良率按「不良数 ×1000 / 抽检数」做整数运算,输出千分比,避免浮点除法的精度问题;关键尺寸则对标称值做双向公差判定,超出标称值 ±公差即为不合格。质量分采用扣分制,从 100 分起扣:致命缺陷每件 -40、严重 -15、轻微 -5(脚本内下限 0),外观低于 95/85 分别再扣 10/20,尺寸超差扣 30,设备告警扣 5,Service 汇总后再 clamp 到 0-100 区间。
等级判定遵循「致命缺陷一票报废」原则:存在致命缺陷或不良率 ≥200‰ 直接 SCRAP;质量分低于 40 或不良率 ≥100‰ 判 REWORK;低于 60 分或不良率 ≥30‰ 判 HOLD;其余 ACCEPT。四级结论分别映射处置去向:报废区(SCRAP_BIN)、返工线(REWORK_LINE)、QA 挂起(QA_HOLD)、下一工序(NEXT_PROCESS)。
此外还要输出两个并行决策:出现致命缺陷、严重缺陷 ≥3 件或设备告警时触发 CAPA 纠正预防流程;只有 ACCEPT 且关键尺寸合格的批次才允许放行。决策中的 notes 逐条记录合格流转、致命缺陷隔离、尺寸超差转返工、挂起加严复检、首件确认、设备点检与 CAPA 提示,构成完整的质检判定说明。
- 缺陷扣分、加扣项、等级阈值散落硬编码:致命 -40 / 严重 -15 / 轻微 -5、200‰/100‰/30‰ 不良率红线、40/60 分数界限分散在 MES 代码中,换产线或换物料时调整困难。
- 质量标准随客户/物料变化需频繁发版:不同产品族的公差、外观分档、扣分值可能不同,传统写法每调整一次都要修改 Java 并重新部署 MES 系统。
- 扣分制容易出现负分或超 100 分:多重扣项叠加后原始分可能为负(如大批量报废用例原始分 -55),不在 Java 侧 clamp 就会产出非法质量分。
- 不良率浮点除法存在精度隐患:用
double计算比例再比较阈值,可能因舍入误差在 30‰、100‰ 等边界出现误判,产线对临界批次的错判代价很高。 - 等级、处置、CAPA、放行是四个耦合决策:致命缺陷必须无视分数直接报废且不放行,尺寸超差必须转返工线,这些先后与联动关系写乱会导致问题批次流入下一工序。
- 老旧产线系统多为 JDK 8、信创环境难引入重型平台:车间服务器资源有限,无法承载独立规则平台或数据库型中间件,需要可嵌入式的轻量规则方案,且判定结果要能审计追溯质量责任。
本案例采用「入参模型 + 决策结果模型 + 规则接口 + XML 规则脚本 + Service 编排 + JUnit 测试」六件套结构,公差、扣分口径、等级阈值与处置映射全部外置在 XML 中,Service 负责不良率/扣分汇总、分数 clamp 与说明装配:
| 层次 | 职责 | 对应文件 |
|---|---|---|
| 领域模型 | 批次抽检记录、质检决策(分数/不良率/等级/处置/CAPA/放行/说明) | InspectionBatch.java、InspectionDecision.java |
| 规则集与 XML | 不良率/公差/扣分 + 等级/处置/CAPA/放行 | QualityInspectionRules.java、quality-inspection.xml |
| 编排服务 | 不良率与扣分汇总、0-100 clamp、等级装配、判定说明组装 | QualityInspectionService.java |
| 测试 | 6 组批次形态(放行、报废、返工、挂起、CAPA、高不良率) | QualityInspectionServiceTest.java |
规则方法拆分
Section titled “规则方法拆分”规则接口共 10 个方法,指标计算、扣分、等级映射、并联决策各自独立:
| 规则方法 | 口径 | 分值/输出 |
|---|---|---|
defectRatePermil |
抽检数 ≤0 返回 0;否则不良数 ×1000 / 抽检数,整数运算输出千分比 | 千分比整数 |
dimensionPass |
实测值 > 标称值+公差 或 < 标称值-公差 返回 false,否则 true |
boolean |
scoreDefects |
100 - 致命×40 - 严重×15 - 轻微×5,低于 0 取 0 | 0 ~ 100 |
scoreAppearance |
外观 ≥95 不扣;≥85 扣 10;否则扣 20 | 0 / -10 / -20 |
scoreDimension |
关键尺寸超差扣 30,合格不扣 | 0 / -30 |
scoreEquipment |
设备状态 WARNING 扣 5,NORMAL 不扣 | 0 / -5 |
qualityLevel |
致命缺陷 ≥1 或不良率 ≥200‰ 报废;<40 分 或 ≥100‰ 返工;<60 分 或 ≥30‰ 挂起;否则接收 |
SCRAP/REWORK/HOLD/ACCEPT |
disposition |
SCRAP→SCRAP_BIN;REWORK→REWORK_LINE;HOLD→QA_HOLD;ACCEPT→NEXT_PROCESS | 处置去向字符串 |
needCapa |
致命缺陷 ≥1、严重缺陷 ≥3 或设备告警任一命中返回 true | boolean |
releaseAllowed |
等级为 ACCEPT 且关键尺寸合格才返回 true | boolean |
代码实现(完整可运行)
Section titled “代码实现(完整可运行)”将以下六个文件按源码原包名放入 JQuick-Java 的测试工程:两个模型放在 com.github.paohaijiao.demo.model,规则接口与测试类放在 com.github.paohaijiao.demo,编排服务放在 com.github.paohaijiao.demo.service,XML 脚本放入 src/test/resources/demo/。保持目录与包名不变后,在工程根目录执行 mvn test -Dtest=QualityInspectionServiceTest 即可运行。
InspectionBatch.java
Section titled “InspectionBatch.java”package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;
/** * 产线批次质检记录(抽检指标与实测数据快照) */@Data@NoArgsConstructor@AllArgsConstructorpublic class InspectionBatch {
/** * 批次号 */ private String batchNo;
/** * 产品编码 */ private String productCode;
/** * 产品名称 */ private String productName;
/** * 车间 */ private String workshop;
/** * 抽检数量 */ private int sampleSize;
/** * 致命缺陷数(安全/功能丧失) */ private int criticalDefects;
/** * 严重缺陷数 */ private int majorDefects;
/** * 轻微缺陷数 */ private int minorDefects;
/** * 关键尺寸实测值 */ private double measuredSize;
/** * 关键尺寸标称值 */ private double targetSize;
/** * 允许偏差(绝对值) */ private double tolerance;
/** * 外观评分(0-100) */ private int surfaceScore;
/** * 设备状态:NORMAL / WARNING */ private String equipmentStatus;
/** * 是否首件检验 */ private boolean firstArticle;}InspectionDecision.java
Section titled “InspectionDecision.java”package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;import java.util.List;
/** * 批次质检决策结果 */@Datapublic class InspectionDecision {
/** * 批次号 */ private String batchNo;
/** * 综合质量分(0-100,扣分制) */ private int qualityScore;
/** * 不良率(千分比) */ private int defectRatePermil;
/** * 关键尺寸是否合格 */ private boolean dimensionPass;
/** * 质量等级:ACCEPT-接收 / REWORK-返工 / SCRAP-报废 / HOLD-挂起待检 */ private String qualityLevel;
/** * 处置去向:NEXT_PROCESS / REWORK_LINE / SCRAP_BIN / QA_HOLD */ private String disposition;
/** * 是否启动纠正预防措施(CAPA) */ private boolean needCapa;
/** * 是否允许批次放行 */ private boolean releaseAllowed;
/** * 判定说明 */ private List<String> notes = new ArrayList<String>();
public void addNote(String note) { this.notes.add(note); }}QualityInspectionRules.java
Section titled “QualityInspectionRules.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/** * 场景 12:产线质检多指标判定规则集 * <p> * 规则按「缺陷扣分 + 加扣项修正 → 等级判定 → 处置与 CAPA」拆分: * <ul> * <li>扣分类:致命/严重/轻微缺陷、外观、关键尺寸、设备状态</li> * <li>决策类:质量等级、处置去向、纠正措施、放行许可</li> * </ul> * 规则脚本见 {@code src/test/resources/demo/quality-inspection.xml}。 */public interface QualityInspectionRules {
int defectRatePermil(@Param("totalDefects") int totalDefects, @Param("sampleSize") int sampleSize);
boolean dimensionPass(@Param("measuredSize") double measuredSize, @Param("targetSize") double targetSize, @Param("tolerance") double tolerance);
int scoreDefects(@Param("criticalDefects") int criticalDefects, @Param("majorDefects") int majorDefects, @Param("minorDefects") int minorDefects);
int scoreAppearance(@Param("surfaceScore") int surfaceScore);
int scoreDimension(@Param("dimensionPass") boolean dimensionPass);
int scoreEquipment(@Param("equipmentStatus") String equipmentStatus);
String qualityLevel(@Param("qualityScore") int qualityScore, @Param("criticalDefects") int criticalDefects, @Param("defectRatePermil") int defectRatePermil);
String disposition(@Param("qualityLevel") String qualityLevel);
boolean needCapa(@Param("criticalDefects") int criticalDefects, @Param("majorDefects") int majorDefects, @Param("equipmentWarning") boolean equipmentWarning);
boolean releaseAllowed(@Param("qualityLevel") String qualityLevel, @Param("dimensionPass") boolean dimensionPass);}quality-inspection.xml
Section titled “quality-inspection.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.QualityInspectionRules">
<!-- 不良率(千分比,整数运算避免精度问题) --> <java name="defectRatePermil" returnClass="int"> <![CDATA[ int def defectRatePermil(int:totalDefects, int:sampleSize) { if (sampleSize <= 0) { return 0; } return totalDefects * 1000 / sampleSize; } ]]> </java>
<!-- 关键尺寸双向公差判定 --> <java name="dimensionPass" returnClass="boolean"> <![CDATA[ boolean def dimensionPass(double:measuredSize, double:targetSize, double:tolerance) { if (measuredSize > targetSize + tolerance) { return false; } else if (measuredSize < targetSize - tolerance) { return false; } else { return true; } } ]]> </java>
<!-- 缺陷扣分:致命 40 / 严重 15 / 轻微 5,下限 0 --> <java name="scoreDefects" returnClass="int"> <![CDATA[ int def scoreDefects(int:criticalDefects, int:majorDefects, int:minorDefects) { int score = 100 - criticalDefects * 40 - majorDefects * 15 - minorDefects * 5; if (score < 0) { score = 0; } return score; } ]]> </java>
<!-- 外观加扣项 --> <java name="scoreAppearance" returnClass="int"> <![CDATA[ int def scoreAppearance(int:surfaceScore) { if (surfaceScore >= 95) { return 0; } else if (surfaceScore >= 85) { return -10; } else { return -20; } } ]]> </java>
<!-- 尺寸加扣项 --> <java name="scoreDimension" returnClass="int"> <![CDATA[ int def scoreDimension(boolean:dimensionPass) { if (dimensionPass) { return 0; } else { return -30; } } ]]> </java>
<!-- 设备状态加扣项 --> <java name="scoreEquipment" returnClass="int"> <![CDATA[ int def scoreEquipment(str:equipmentStatus) { if (equipmentStatus == "WARNING") { return -5; } else { return 0; } } ]]> </java>
<!-- 质量等级:致命缺陷或高不良率直接报废,其次返工、挂起、接收 --> <java name="qualityLevel" returnClass="java.lang.String"> <![CDATA[ str def qualityLevel(int:qualityScore, int:criticalDefects, int:defectRatePermil) { if (criticalDefects >= 1 || defectRatePermil >= 200) { return "SCRAP"; } else if (qualityScore < 40 || defectRatePermil >= 100) { return "REWORK"; } else if (qualityScore < 60 || defectRatePermil >= 30) { return "HOLD"; } else { return "ACCEPT"; } } ]]> </java>
<!-- 处置去向 --> <java name="disposition" returnClass="java.lang.String"> <![CDATA[ str def disposition(str:qualityLevel) { if (qualityLevel == "SCRAP") { return "SCRAP_BIN"; } else if (qualityLevel == "REWORK") { return "REWORK_LINE"; } else if (qualityLevel == "HOLD") { return "QA_HOLD"; } else { return "NEXT_PROCESS"; } } ]]> </java>
<!-- 纠正预防措施:致命缺陷、严重缺陷成片、设备告警时触发 --> <java name="needCapa" returnClass="boolean"> <![CDATA[ boolean def needCapa(int:criticalDefects, int:majorDefects, boolean:equipmentWarning) { if (criticalDefects >= 1 || majorDefects >= 3 || equipmentWarning) { return true; } else { return false; } } ]]> </java>
<!-- 放行许可:必须接收且关键尺寸合格 --> <java name="releaseAllowed" returnClass="boolean"> <![CDATA[ boolean def releaseAllowed(str:qualityLevel, boolean:dimensionPass) { if (qualityLevel == "ACCEPT" && dimensionPass) { return true; } else { return false; } } ]]> </java>
</javas>QualityInspectionService.java
Section titled “QualityInspectionService.java”package com.github.paohaijiao.demo.service;
import com.github.paohaijiao.demo.QualityInspectionRules;import com.github.paohaijiao.demo.model.InspectionBatch;import com.github.paohaijiao.demo.model.InspectionDecision;
/** * 场景 12:产线批次质检分级服务 * <p> * 职责划分: * <ul> * <li><b>Java 侧</b>:不良率与扣分汇总、等级装配、处置说明与 CAPA 提示组装</li> * <li><b>规则侧</b>:公差判定、缺陷扣分口径、等级阈值与报废/返工界限,全部外置在 XML 中</li> * </ul> */public class QualityInspectionService {
private static final int SCORE_FLOOR = 0; private static final int SCORE_CEIL = 100;
private final QualityInspectionRules rules;
public QualityInspectionService(QualityInspectionRules rules) { this.rules = rules; }
/** * 执行批次质检判定 * * @param batch 批次抽检记录 * @return 质检决策 */ public InspectionDecision inspect(InspectionBatch batch) { InspectionDecision decision = new InspectionDecision(); decision.setBatchNo(batch.getBatchNo());
int totalDefects = batch.getCriticalDefects() + batch.getMajorDefects() + batch.getMinorDefects(); int ratePermil = rules.defectRatePermil(totalDefects, batch.getSampleSize()); boolean dimensionPass = rules.dimensionPass(batch.getMeasuredSize(), batch.getTargetSize(), batch.getTolerance());
int rawScore = rules.scoreDefects(batch.getCriticalDefects(), batch.getMajorDefects(), batch.getMinorDefects()) + rules.scoreAppearance(batch.getSurfaceScore()) + rules.scoreDimension(dimensionPass) + rules.scoreEquipment(batch.getEquipmentStatus()); int qualityScore = clamp(rawScore);
String level = rules.qualityLevel(qualityScore, batch.getCriticalDefects(), ratePermil); boolean equipmentWarning = "WARNING".equals(batch.getEquipmentStatus());
decision.setQualityScore(qualityScore); decision.setDefectRatePermil(ratePermil); decision.setDimensionPass(dimensionPass); decision.setQualityLevel(level); decision.setDisposition(rules.disposition(level)); decision.setNeedCapa(rules.needCapa(batch.getCriticalDefects(), batch.getMajorDefects(), equipmentWarning)); decision.setReleaseAllowed(rules.releaseAllowed(level, dimensionPass));
buildNotes(batch, decision, totalDefects); return decision; }
private int clamp(int score) { if (score < SCORE_FLOOR) { return SCORE_FLOOR; } if (score > SCORE_CEIL) { return SCORE_CEIL; } return score; }
private void buildNotes(InspectionBatch batch, InspectionDecision decision, int totalDefects) { if ("ACCEPT".equals(decision.getQualityLevel())) { decision.addNote("抽检 " + batch.getSampleSize() + " 件、不良 " + totalDefects + " 件,批次合格,流转下一工序"); } if (batch.getCriticalDefects() >= 1) { decision.addNote("发现致命缺陷 " + batch.getCriticalDefects() + " 件,整批判废并隔离"); } if (!decision.isDimensionPass()) { decision.addNote("关键尺寸实测 " + batch.getMeasuredSize() + " 超出公差 " + batch.getTolerance() + ",转返工线"); } if ("HOLD".equals(decision.getQualityLevel())) { decision.addNote("不良率或质量分处于临界区间,挂起等待加严复检"); } if (batch.isFirstArticle()) { decision.addNote("首件检验,需质量工程师确认后再批量投产"); } if ("WARNING".equals(batch.getEquipmentStatus())) { decision.addNote("生产设备处于告警状态,建议同步安排点检"); } if (decision.isNeedCapa()) { decision.addNote("已触发纠正预防措施(CAPA)流程"); } }}QualityInspectionServiceTest.java
Section titled “QualityInspectionServiceTest.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.demo.model.InspectionBatch;import com.github.paohaijiao.demo.model.InspectionDecision;import com.github.paohaijiao.demo.service.QualityInspectionService;import com.github.paohaijiao.xml.JQuickJava;import org.junit.Assert;import org.junit.Before;import org.junit.Test;
/** * 场景 12 生产级样板:产线质检多指标判定 * <p> * 覆盖 6 组批次形态:合格放行、致命缺陷报废、尺寸超差返工、 * 不良率临界挂起、设备告警成片缺陷、大批量报废。 */public class QualityInspectionServiceTest {
private QualityInspectionService service;
@Before public void setUp() { QualityInspectionRules rules = JQuickJava.create() .createApi(QualityInspectionRules.class, "demo/quality-inspection.xml"); this.service = new QualityInspectionService(rules); }
/** * 优质批次:200 抽 1 轻微不良、尺寸合格、外观 97 → 95 分接收放行 */ @Test public void shouldReleaseConformingBatch() { InspectionBatch batch = batch("BAT-1201", 200, 0, 0, 1, 10.01, 10.0, 0.05, 97, "NORMAL", false);
InspectionDecision decision = service.inspect(batch);
Assert.assertEquals(95, decision.getQualityScore()); Assert.assertEquals(5, decision.getDefectRatePermil()); Assert.assertTrue(decision.isDimensionPass()); Assert.assertEquals("ACCEPT", decision.getQualityLevel()); Assert.assertEquals("NEXT_PROCESS", decision.getDisposition()); Assert.assertTrue(decision.isReleaseAllowed()); Assert.assertFalse(decision.isNeedCapa()); Assert.assertTrue(decision.getNotes().contains("抽检 200 件、不良 1 件,批次合格,流转下一工序")); }
/** * 致命缺陷:1 件致命缺陷 → 不看分数直接整批报废 */ @Test public void shouldScrapBatchWithCriticalDefect() { InspectionBatch batch = batch("BAT-1202", 100, 1, 0, 2, 10.02, 10.0, 0.05, 80, "NORMAL", false);
InspectionDecision decision = service.inspect(batch);
Assert.assertEquals(30, decision.getQualityScore()); Assert.assertEquals("SCRAP", decision.getQualityLevel()); Assert.assertEquals("SCRAP_BIN", decision.getDisposition()); Assert.assertFalse(decision.isReleaseAllowed()); Assert.assertTrue(decision.isNeedCapa()); Assert.assertTrue(decision.getNotes().contains("发现致命缺陷 1 件,整批判废并隔离")); }
/** * 尺寸超差:2 严重 + 2 轻微、关键尺寸超差 → 20 分返工 */ @Test public void shouldReworkDimensionOutOfToleranceBatch() { InspectionBatch batch = batch("BAT-1203", 100, 0, 2, 2, 10.12, 10.0, 0.05, 90, "NORMAL", false);
InspectionDecision decision = service.inspect(batch);
Assert.assertEquals(20, decision.getQualityScore()); Assert.assertEquals(40, decision.getDefectRatePermil()); Assert.assertFalse(decision.isDimensionPass()); Assert.assertEquals("REWORK", decision.getQualityLevel()); Assert.assertEquals("REWORK_LINE", decision.getDisposition()); Assert.assertFalse(decision.isReleaseAllowed()); Assert.assertFalse(decision.isNeedCapa()); Assert.assertTrue(decision.getNotes().contains("关键尺寸实测 10.12 超出公差 0.05,转返工线")); }
/** * 不良率临界:4 件轻微不良、千分之 40 → 挂起待加严复检 */ @Test public void shouldHoldBorderlineDefectRateBatch() { InspectionBatch batch = batch("BAT-1204", 100, 0, 0, 4, 10.00, 10.0, 0.05, 97, "NORMAL", false);
InspectionDecision decision = service.inspect(batch);
Assert.assertEquals(80, decision.getQualityScore()); Assert.assertEquals(40, decision.getDefectRatePermil()); Assert.assertEquals("HOLD", decision.getQualityLevel()); Assert.assertEquals("QA_HOLD", decision.getDisposition()); Assert.assertFalse(decision.isReleaseAllowed()); Assert.assertFalse(decision.isNeedCapa()); Assert.assertTrue(decision.getNotes().contains("不良率或质量分处于临界区间,挂起等待加严复检")); }
/** * 设备告警 + 3 件严重缺陷 → 40 分挂起,触发 CAPA */ @Test public void shouldTriggerCapaForEquipmentWarningAndMajorDefects() { InspectionBatch batch = batch("BAT-1205", 200, 0, 3, 0, 10.01, 10.0, 0.05, 90, "WARNING", true);
InspectionDecision decision = service.inspect(batch);
Assert.assertEquals(40, decision.getQualityScore()); Assert.assertEquals(15, decision.getDefectRatePermil()); Assert.assertEquals("HOLD", decision.getQualityLevel()); Assert.assertTrue(decision.isNeedCapa()); Assert.assertTrue(decision.getNotes().contains("生产设备处于告警状态,建议同步安排点检")); Assert.assertTrue(decision.getNotes().contains("首件检验,需质量工程师确认后再批量投产")); Assert.assertTrue(decision.getNotes().contains("已触发纠正预防措施(CAPA)流程")); }
/** * 大批量报废:50 抽 16 件不良含 2 件致命、不良率千分之 320 */ @Test public void shouldScrapHighDefectRateBatch() { InspectionBatch batch = batch("BAT-1206", 50, 2, 4, 10, 10.20, 10.0, 0.05, 70, "WARNING", false);
InspectionDecision decision = service.inspect(batch);
Assert.assertEquals(0, decision.getQualityScore()); Assert.assertEquals(320, decision.getDefectRatePermil()); Assert.assertEquals("SCRAP", decision.getQualityLevel()); Assert.assertEquals("SCRAP_BIN", decision.getDisposition()); Assert.assertFalse(decision.isReleaseAllowed()); Assert.assertTrue(decision.isNeedCapa()); }
private InspectionBatch batch(String batchNo, int sampleSize, int critical, int major, int minor, double measured, double target, double tolerance, int surfaceScore, String equipmentStatus, boolean firstArticle) { InspectionBatch b = new InspectionBatch(); b.setBatchNo(batchNo); b.setProductCode("P-001"); b.setProductName("精密轴套"); b.setWorkshop("一号车间"); b.setSampleSize(sampleSize); b.setCriticalDefects(critical); b.setMajorDefects(major); b.setMinorDefects(minor); b.setMeasuredSize(measured); b.setTargetSize(target); b.setTolerance(tolerance); b.setSurfaceScore(surfaceScore); b.setEquipmentStatus(equipmentStatus); b.setFirstArticle(firstArticle); return b; }}在工程根目录执行:
mvn test -Dtest=QualityInspectionServiceTest测试覆盖形态
Section titled “测试覆盖形态”| 测试方法 | 构造的业务形态 | 关键断言/期望决策 |
|---|---|---|
shouldReleaseConformingBatch |
200 抽 1 件轻微不良、尺寸合格、外观 97、设备正常(BAT-1201) | 95 分、不良率 5‰、ACCEPT、NEXT_PROCESS、放行 true、不触发 CAPA |
shouldScrapBatchWithCriticalDefect |
100 抽含 1 件致命 + 2 件轻微、外观 80(BAT-1202) | 30 分仍 SCRAP、SCRAP_BIN、不放行、CAPA true、含致命缺陷隔离说明 |
shouldReworkDimensionOutOfToleranceBatch |
2 严重 + 2 轻微、实测 10.12 超公差 0.05(BAT-1203) | 20 分、不良率 40‰、尺寸不合格、REWORK、REWORK_LINE、不放行 |
shouldHoldBorderlineDefectRateBatch |
100 抽 4 件轻微、尺寸合格、外观 97(BAT-1204) | 80 分但不良率 40‰ → HOLD、QA_HOLD、不放行、含加严复检说明 |
shouldTriggerCapaForEquipmentWarningAndMajorDefects |
200 抽 3 严重、设备 WARNING、首件检验(BAT-1205) | 40 分、15‰、HOLD、CAPA true,含设备点检/首件确认/CAPA 三条说明 |
shouldScrapHighDefectRateBatch |
50 抽 16 件不良(2 致命 + 4 严重 + 10 轻微)、尺寸超差(BAT-1206) | 原始负分 clamp 到 0、不良率 320‰、SCRAP、SCRAP_BIN、CAPA true |
优质批次(200 抽 1 轻微不良、尺寸合格、外观 97):
质量分: 95 不良率: 5‰ 等级: ACCEPT 处置: NEXT_PROCESS 放行: true致命缺陷批次(1 件致命缺陷,不看分数):
质量分: 30 等级: SCRAP 处置: SCRAP_BIN CAPA: true说明: 发现致命缺陷 1 件,整批判废并隔离- 不良率交叉相乘走整数运算:
defectRatePermil用totalDefects * 1000 / sampleSize计算千分比,全程整数,既避免浮点除法的舍入误差,又把比较阈值统一成整数,30‰/100‰/200‰ 边界判定稳定可复现。 - 扣分制双层兜底:
scoreDefects脚本内部对缺陷扣分做 0 下限保护,Service 再把四类扣分汇总结果 clamp 到 0-100,大批量报废用例原始分 -55 最终落为 0,杜绝非法分数进入等级判定。 - 致命缺陷一票报废优先级硬编码在规则顺序中:
qualityLevel第一个分支就是criticalDefects >= 1 || defectRatePermil >= 200,30 分的致命缺陷批次与 0 分批次同样 SCRAP,不依赖分数偶然结果。 - 布尔判定全部正向 return,不使用取反:
dimensionPass对「超出上公差」「超出下公差」分别return false、合格才return true,与脚本不支持!取反的特性契合,逻辑直观且可逐分支审计。 - 并联决策各自独立、Service 统一装配:等级、处置、CAPA、放行是四个独立规则方法,
needCapa可因严重缺陷 ≥3 或设备告警独立于等级触发,releaseAllowed同时约束等级与尺寸,联动关系清晰可测。 - 阈值外置 XML + Java 8 轻量嵌入:扣分值、公差判定、不良率红线全部在
quality-inspection.xml中,配合 ASM 调用链与 JDK 8 兼容性,可直接嵌入车间老旧 MES 与信创产线系统,无需独立规则服务器。