package cn.iocoder.yudao.module.qcreport.service.instance; import cn.iocoder.yudao.module.mes.api.qc.dto.MesQcReportItemRespDTO; import cn.iocoder.yudao.module.mes.api.qc.dto.MesQcReportRespDTO; import cn.iocoder.yudao.module.qcreport.engine.context.InspectionItem; import cn.iocoder.yudao.module.qcreport.engine.context.ReportContext; import cn.iocoder.yudao.module.qcreport.engine.context.ReportFields; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; import java.util.Locale; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; /** * 质检单 → 报告上下文的字段搬运。 *
* 这里守的是「哪一列搬到哪儿」:搬错了不会有异常,只会让报告上印着别人的数据,
* 而这类错误在端到端联调里很难一眼看出来。所以每条搬运动作都在这里钉死。
*/
class MesQcReportContextMapperTest {
@Test
@DisplayName("单头字段各归各位")
void mapsHeaderFields() {
MesQcReportRespDTO data = baseOrder()
.setQcCode("IQC_20260917001")
.setQcName("精萘来料检验-0917")
.setQcTypeName("来料检验")
.setCheckResult(2)
.setCheckResultText("特采")
.setItemCode("M-001")
.setItemName("精萘")
.setItemSpecification("工业级 99%")
.setVendorBatch("VB-2026-0917")
.setBatchCode("")
.setVendorName("南通化工")
.setInspectorName("张三")
.setInspectDate(LocalDateTime.of(2026, 9, 17, 14, 30, 5))
.setItems(List.of(item(1, "S1", "0.09")));
ReportFields report = MesQcReportContextMapper.map(data, "QR20260919-0001").context().getReport();
assertEquals("QR20260919-0001", report.getReportNo());
assertEquals("精萘来料检验-0917", report.getReportName());
assertEquals("M-001", report.getProductCode());
assertEquals("精萘", report.getProductName());
assertEquals("工业级 99%", report.getSpec());
// 来料检验只有供应商批号,批次号为空,报告头「批次」要落到供应商批号上
assertEquals("VB-2026-0917", report.getBatchNo());
assertEquals("来料检验", report.getInspectType());
assertEquals("张三", report.getInspector());
assertEquals("2026-09-17 14:30:05", report.getInspectDate());
assertEquals("南通化工", report.getSupplierName());
// 人工拍的判定走 qcResult,不是引擎的 result
assertEquals("2", report.getQcResult());
assertEquals("特采", report.getQcResultText());
}
@Test
@DisplayName("引擎负责的字段映射器一律不碰")
void leavesEngineFieldsUntouched() {
MesQcReportRespDTO data = baseOrder().setItems(List.of(item(1, "S1", "0.09")));
ReportFields report = MesQcReportContextMapper.map(data, "QR1").context().getReport();
// 判定结果、结论、合格率、计数都该由 ReportEvaluator 算完写回;
// 映射器若在这里填了值,等于让「没验过」的报告显示合格
assertEquals("", report.getResult());
assertEquals("", report.getResultText());
assertEquals("", report.getConclusion());
assertEquals("", report.getPassRate());
assertEquals(0, report.getTotal());
assertEquals(0, report.getPassCount());
assertEquals(0, report.getFailCount());
}
@Test
@DisplayName("检验项字段映射,含上下限与单位")
void mapsItemFields() {
MesQcReportRespDTO data = baseOrder().setItems(List.of(
item(1, "S1", "0.09")
.setIndicatorCode("IND-30")
.setIndicatorName("正丁醇含量")
.setStandardValue("≤0.10")
.setUnit("%")
.setUpperLimit(new BigDecimal("0.10"))
.setLowerLimit(new BigDecimal("0.05"))
.setRemark("复测")));
InspectionItem item = onlyItem(MesQcReportContextMapper.map(data, "QR1"));
assertEquals(1, item.getIndex());
assertEquals("IND-30", item.getItemCode());
assertEquals("正丁醇含量", item.getItemName());
assertEquals("≤0.10", item.getStandardValue());
assertEquals("0.09", item.getActualValue());
assertEquals("%", item.getUnit());
assertEquals(0.10, item.getUpperLimit());
assertEquals(0.05, item.getLowerLimit());
assertEquals("复测", item.getRemark());
}
@Test
@DisplayName("检测方法逐行搬进上下文,与检测要求各占一处")
void mapsCheckMethod() {
MesQcReportRespDTO data = baseOrder().setItems(List.of(
item(1, "S1", "0.09")
.setIndicatorName("水分")
.setCheckMethod("GB 5009.3-2016")
.setRequirement("w = (m1 - m0) / m × 100%")));
InspectionItem item = onlyItem(MesQcReportContextMapper.map(data, "QR1"));
// 模板里写 {{item.checkMethod}} 的「检测方法」列能不能取到值,全看这一处有没有搬;
// 漏搬不会报错,只会让那一整列印成空白并在问题清单里挂一条「绑定取不到值」
assertEquals("GB 5009.3-2016", item.getCheckMethod());
assertEquals("w = (m1 - m0) / m × 100%", item.getRequirement());
}
@Test
@DisplayName("检测方法为空得空串,不会印成 null")
void blankCheckMethodIsEmptyString() {
MesQcReportRespDTO data = baseOrder().setItems(List.of(item(1, "S1", "0.09")));
assertEquals("", onlyItem(MesQcReportContextMapper.map(data, "QR1")).getCheckMethod());
}
@Test
@DisplayName("没有规格上下限时保持 null,不能变成 0")
void keepsMissingLimitsNull() {
MesQcReportRespDTO data = baseOrder().setItems(List.of(item(1, "S1", "澄清透明液体")));
InspectionItem item = onlyItem(MesQcReportContextMapper.map(data, "QR1"));
// 变成 0 的话,实测值 0.09 会被判成「超上限不合格」——凭空造出一个不合格
assertNull(item.getUpperLimit());
assertNull(item.getLowerLimit());
}
@Test
@DisplayName("单样品:样品号进报告头,不必每行重复")
void singleSampleGoesToHeaderOnly() {
MesQcReportRespDTO data = baseOrder().setItems(List.of(
item(1, "S1", "0.09"),
item(2, "S1", "0.10")));
MesQcReportContextMapper.Mapped mapped = MesQcReportContextMapper.map(data, "QR1");
assertEquals("S1", mapped.context().getReport().getSampleNo());
assertEquals("", mapped.context().getInspectionItems().get(0).getRemark());
assertTrue(mapped.warnings().isEmpty(), "单样品不该产生样品取舍提示:" + mapped.warnings());
}
@Test
@DisplayName("多样品:样品号并列进报告头,同时写进每行备注,一行都不丢")
void multipleSamplesFoldIntoRemark() {
MesQcReportRespDTO data = baseOrder().setItems(List.of(
item(1, "S1", "0.09"),
item(2, "S2", "0.12").setRemark("复测")));
MesQcReportContextMapper.Mapped mapped = MesQcReportContextMapper.map(data, "QR1");
assertEquals("S1、S2", mapped.context().getReport().getSampleNo());
List