跳转到内容

案例 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

规则接口共 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

将以下六个文件按源码原包名放入 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 即可运行。

package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 产线批次质检记录(抽检指标与实测数据快照)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public 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;
}
package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 批次质检决策结果
*/
@Data
public 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);
}
}
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);
}
<?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>
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)流程");
}
}
}
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;
}
}

在工程根目录执行:

Terminal window
mvn test -Dtest=QualityInspectionServiceTest
测试方法 构造的业务形态 关键断言/期望决策
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 与信创产线系统,无需独立规则服务器。