11 小时以前 9bad721754fe8bbe2e5f459d0706e0fefac569f3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
package cn.iocoder.yudao.module.mes.service.qc.indicator;
 
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.controller.admin.qc.indicator.vo.MesQcIndicatorSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicator.MesQcIndicatorDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.indicator.MesQcIndicatorMapper;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcIndicatorItemTypeEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcResultValueTypeEnum;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import org.springframework.context.annotation.Import;
 
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
import static org.junit.jupiter.api.Assertions.*;
 
/**
 * {@link MesQcIndicatorServiceImpl} 的单元测试
 * <p>
 * 覆盖分组层级:层级校验、排序号补齐、结果值字段清空、级联删除。
 *
 * @author 超级管理员
 */
@Import(MesQcIndicatorServiceImpl.class)
public class MesQcIndicatorServiceImplTest extends BaseDbUnitTest {
 
    /**
     * 检测项类型(字典 mes_indicator_type 的「性能」,现网分组行用的就是这个值)
     */
    private static final Integer TYPE_PERFORMANCE = 4;
 
    @Resource
    private MesQcIndicatorServiceImpl indicatorService;
 
    @Resource
    private MesQcIndicatorMapper indicatorMapper;
 
    // ==================== 创建:分组项 ====================
 
    @Test
    public void testCreateGroup_clearsResultFields() {
        // 准备参数:分组项即使带了 FILE 类型与空的结果值属性,也不应被「结果值属性不能为空」拦住
        MesQcIndicatorSaveReqVO reqVO = buildReqVO("T01-G", "水分", MesQcIndicatorItemTypeEnum.GROUP.getType(),
                null, MesQcResultValueTypeEnum.FILE.getType());
        reqVO.setFormulaText("w = (m1 - m0) / m × 100%");
 
        // 调用
        Long id = indicatorService.createIndicator(reqVO);
 
        // 断言:分组项落库时结果值字段被清空,父级归 0,排序号自动补 10(首个顶级)
        MesQcIndicatorDO dbIndicator = indicatorMapper.selectById(id);
        assertNotNull(dbIndicator);
        assertEquals(MesQcIndicatorItemTypeEnum.GROUP.getType(), dbIndicator.getItemType());
        assertEquals(MesQcIndicatorDO.PARENT_ID_ROOT, dbIndicator.getParentId());
        assertNull(dbIndicator.getResultType());
        assertNull(dbIndicator.getResultSpecification());
        assertEquals("w = (m1 - m0) / m × 100%", dbIndicator.getFormulaText());
        assertEquals(10, dbIndicator.getSortOrder());
    }
 
    @Test
    public void testCreateGroup_cannotNest() {
        // mock 数据:一个已存在的分组
        MesQcIndicatorDO parent = insertIndicator("T02-G1", "水分", MesQcIndicatorItemTypeEnum.GROUP.getType(),
                MesQcIndicatorDO.PARENT_ID_ROOT, null, 10);
        // 准备参数:分组项挂到另一个分组下
        MesQcIndicatorSaveReqVO reqVO = buildReqVO("T02-G2", "灰分", MesQcIndicatorItemTypeEnum.GROUP.getType(),
                parent.getId(), null);
 
        // 调用,并断言异常
        assertMessageException(() -> indicatorService.createIndicator(reqVO),
                QC_INDICATOR_GROUP_CANNOT_NEST, "灰分", "必须作为顶级指标", "水分");
 
        // 断言:未入库
        assertEquals(1, indicatorMapper.selectCount());
    }
 
    // ==================== 创建:录入项 ====================
 
    @Test
    public void testCreateEntry_defaultSortOrder() {
        // mock 数据:一个顶级分组,排序号 30
        MesQcIndicatorDO group = insertIndicator("T03-G", "水分", MesQcIndicatorItemTypeEnum.GROUP.getType(),
                MesQcIndicatorDO.PARENT_ID_ROOT, null, 30);
 
        // 调用:组内第一条子项,排序号留空
        Long firstId = indicatorService.createIndicator(buildReqVO("T03-E1", "游离水",
                MesQcIndicatorItemTypeEnum.ENTRY.getType(), group.getId(), MesQcResultValueTypeEnum.FLOAT.getType()));
        // 调用:组内第二条子项,排序号留空
        Long secondId = indicatorService.createIndicator(buildReqVO("T03-E2", "结合水",
                MesQcIndicatorItemTypeEnum.ENTRY.getType(), group.getId(), MesQcResultValueTypeEnum.FLOAT.getType()));
 
        // 断言:子项步长为 1,依次排到同级末尾
        assertEquals(1, indicatorMapper.selectById(firstId).getSortOrder());
        assertEquals(2, indicatorMapper.selectById(secondId).getSortOrder());
        assertEquals(group.getId(), indicatorMapper.selectById(firstId).getParentId());
    }
 
    @Test
    public void testCreateIndicator_defaultSortOrder_topLevelBlockStep() {
        // mock 数据:顶级分组排序号 30
        insertIndicator("T04-G", "水分", MesQcIndicatorItemTypeEnum.GROUP.getType(),
                MesQcIndicatorDO.PARENT_ID_ROOT, null, 30);
 
        // 调用:顶级新指标排序号留空,顶级步长为 10
        Long id = indicatorService.createIndicator(buildReqVO("T04-E", "外观",
                MesQcIndicatorItemTypeEnum.ENTRY.getType(), null, MesQcResultValueTypeEnum.TEXT.getType()));
 
        // 断言
        MesQcIndicatorDO dbIndicator = indicatorMapper.selectById(id);
        assertEquals(MesQcIndicatorDO.PARENT_ID_ROOT, dbIndicator.getParentId());
        assertEquals(40, dbIndicator.getSortOrder());
    }
 
    @Test
    public void testCreateEntry_parentNotGroup() {
        // mock 数据:一个录入项被拿来当父级
        MesQcIndicatorDO entry = insertIndicator("T05-E1", "外观", MesQcIndicatorItemTypeEnum.ENTRY.getType(),
                MesQcIndicatorDO.PARENT_ID_ROOT, MesQcResultValueTypeEnum.TEXT.getType(), 10);
        // 准备参数
        MesQcIndicatorSaveReqVO reqVO = buildReqVO("T05-E2", "色泽", MesQcIndicatorItemTypeEnum.ENTRY.getType(),
                entry.getId(), MesQcResultValueTypeEnum.TEXT.getType());
 
        // 调用,并断言异常
        assertMessageException(() -> indicatorService.createIndicator(reqVO),
                QC_INDICATOR_PARENT_NOT_GROUP, "外观", "不是分组项", "录入项");
    }
 
    @Test
    public void testCreateEntry_parentNotExists() {
        // 准备参数:父级编号不存在
        Long notExistsParentId = randomLongId();
        MesQcIndicatorSaveReqVO reqVO = buildReqVO("T06-E", "色泽", MesQcIndicatorItemTypeEnum.ENTRY.getType(),
                notExistsParentId, MesQcResultValueTypeEnum.TEXT.getType());
 
        // 调用,并断言异常
        assertMessageException(() -> indicatorService.createIndicator(reqVO),
                QC_INDICATOR_PARENT_NOT_EXISTS, "所属分组不存在", String.valueOf(notExistsParentId));
    }
 
    @Test
    public void testCreateEntry_resultTypeRequired() {
        // 准备参数:录入项没有结果值类型
        MesQcIndicatorSaveReqVO reqVO = buildReqVO("T07-E", "色泽", MesQcIndicatorItemTypeEnum.ENTRY.getType(),
                null, null);
 
        // 调用,并断言异常
        assertServiceException(() -> indicatorService.createIndicator(reqVO), QC_INDICATOR_RESULT_TYPE_REQUIRED);
 
        // 断言:未入库
        assertEquals(0, indicatorMapper.selectCount());
    }
 
    @Test
    public void testCreateEntry_fileResultSpecificationRequired() {
        // 准备参数:录入项为 FILE 类型但没填结果值属性
        MesQcIndicatorSaveReqVO reqVO = buildReqVO("T08-E", "外观照片", MesQcIndicatorItemTypeEnum.ENTRY.getType(),
                null, MesQcResultValueTypeEnum.FILE.getType());
 
        // 调用,并断言异常
        assertServiceException(() -> indicatorService.createIndicator(reqVO), QC_INDICATOR_RESULT_SPECIFICATION_REQUIRED);
    }
 
    // ==================== 修改 ====================
 
    @Test
    public void testUpdateEntry_parentSelf() {
        // mock 数据:一个录入项
        MesQcIndicatorDO entry = insertIndicator("T09-E", "外观", MesQcIndicatorItemTypeEnum.ENTRY.getType(),
                MesQcIndicatorDO.PARENT_ID_ROOT, MesQcResultValueTypeEnum.TEXT.getType(), 10);
        // 准备参数:把自己当父级
        MesQcIndicatorSaveReqVO reqVO = buildReqVO("T09-E", "外观", MesQcIndicatorItemTypeEnum.ENTRY.getType(),
                entry.getId(), MesQcResultValueTypeEnum.TEXT.getType());
        reqVO.setId(entry.getId());
 
        // 调用,并断言异常
        assertServiceException(() -> indicatorService.updateIndicator(reqVO), QC_INDICATOR_PARENT_SELF);
    }
 
    @Test
    public void testUpdateIndicator_toGroup_clearsResultFields() {
        // mock 数据:一个 FILE 类型的录入项
        MesQcIndicatorDO entry = insertIndicator("T10-E", "酸含量", MesQcIndicatorItemTypeEnum.ENTRY.getType(),
                MesQcIndicatorDO.PARENT_ID_ROOT, MesQcResultValueTypeEnum.FILE.getType(), 10);
        entry.setResultSpecification("IMG");
        indicatorMapper.updateById(entry);
        // 准备参数:把它改成分组项
        MesQcIndicatorSaveReqVO reqVO = buildReqVO("T10-E", "酸含量", MesQcIndicatorItemTypeEnum.GROUP.getType(),
                null, MesQcResultValueTypeEnum.FILE.getType());
        reqVO.setId(entry.getId());
        reqVO.setFormulaText("c = V × N / m");
 
        // 调用
        indicatorService.updateIndicator(reqVO);
 
        // 断言:条目类型已改,结果值字段被清空,公式写入
        MesQcIndicatorDO dbIndicator = indicatorMapper.selectById(entry.getId());
        assertEquals(MesQcIndicatorItemTypeEnum.GROUP.getType(), dbIndicator.getItemType());
        assertNull(dbIndicator.getResultType());
        assertNull(dbIndicator.getResultSpecification());
        assertEquals("c = V × N / m", dbIndicator.getFormulaText());
    }
 
    @Test
    public void testUpdateIndicator_hasChildrenCannotChangeToEntry() {
        // mock 数据:一个带子项的分组
        MesQcIndicatorDO group = insertIndicator("T11-G", "水分", MesQcIndicatorItemTypeEnum.GROUP.getType(),
                MesQcIndicatorDO.PARENT_ID_ROOT, null, 10);
        insertIndicator("T11-E1", "游离水", MesQcIndicatorItemTypeEnum.ENTRY.getType(), group.getId(),
                MesQcResultValueTypeEnum.FLOAT.getType(), 1);
        // 准备参数:把分组改成录入项
        MesQcIndicatorSaveReqVO reqVO = buildReqVO("T11-G", "水分", MesQcIndicatorItemTypeEnum.ENTRY.getType(),
                null, MesQcResultValueTypeEnum.FLOAT.getType());
        reqVO.setId(group.getId());
 
        // 调用,并断言异常
        assertMessageException(() -> indicatorService.updateIndicator(reqVO),
                QC_INDICATOR_HAS_CHILDREN, "不能再把它的条目类型改为录入项", "水分");
 
        // 断言:条目类型未被改动
        assertEquals(MesQcIndicatorItemTypeEnum.GROUP.getType(), indicatorMapper.selectById(group.getId()).getItemType());
    }
 
    @Test
    public void testUpdateIndicator_hasChildrenCannotReparent() {
        // mock 数据:一个带子项的条目(历史脏数据:录入项也可能被挂过子项,报告侧只认 parent_id)
        MesQcIndicatorDO entry = insertIndicator("T12-E", "杂质", MesQcIndicatorItemTypeEnum.ENTRY.getType(),
                MesQcIndicatorDO.PARENT_ID_ROOT, MesQcResultValueTypeEnum.FLOAT.getType(), 10);
        insertIndicator("T12-E1", "不溶物", MesQcIndicatorItemTypeEnum.ENTRY.getType(), entry.getId(),
                MesQcResultValueTypeEnum.FLOAT.getType(), 1);
        MesQcIndicatorDO group = insertIndicator("T12-G", "水分", MesQcIndicatorItemTypeEnum.GROUP.getType(),
                MesQcIndicatorDO.PARENT_ID_ROOT, null, 20);
        // 准备参数:把带子项的指标挂到分组下
        MesQcIndicatorSaveReqVO reqVO = buildReqVO("T12-E", "杂质", MesQcIndicatorItemTypeEnum.ENTRY.getType(),
                group.getId(), MesQcResultValueTypeEnum.FLOAT.getType());
        reqVO.setId(entry.getId());
 
        // 调用,并断言异常
        assertMessageException(() -> indicatorService.updateIndicator(reqVO),
                QC_INDICATOR_HAS_CHILDREN, "不能再挂到其它分组下", "杂质");
 
        // 断言:父级未被改动
        assertEquals(MesQcIndicatorDO.PARENT_ID_ROOT, indicatorMapper.selectById(entry.getId()).getParentId());
    }
 
    // ==================== 删除 ====================
 
    @Test
    public void testDeleteGroup_cascadeChildren() {
        // mock 数据:一个分组 + 两个子项 + 一个不相关的顶级指标
        MesQcIndicatorDO group = insertIndicator("T13-G", "水分", MesQcIndicatorItemTypeEnum.GROUP.getType(),
                MesQcIndicatorDO.PARENT_ID_ROOT, null, 10);
        Long child1Id = insertIndicator("T13-E1", "游离水", MesQcIndicatorItemTypeEnum.ENTRY.getType(),
                group.getId(), MesQcResultValueTypeEnum.FLOAT.getType(), 1).getId();
        Long child2Id = insertIndicator("T13-E2", "结合水", MesQcIndicatorItemTypeEnum.ENTRY.getType(),
                group.getId(), MesQcResultValueTypeEnum.FLOAT.getType(), 2).getId();
        Long otherId = insertIndicator("T13-E3", "外观", MesQcIndicatorItemTypeEnum.ENTRY.getType(),
                MesQcIndicatorDO.PARENT_ID_ROOT, MesQcResultValueTypeEnum.TEXT.getType(), 20).getId();
 
        // 调用
        indicatorService.deleteIndicator(group.getId());
 
        // 断言:分组与其子项一并删除,不相关的指标保留
        assertNull(indicatorMapper.selectById(group.getId()));
        assertNull(indicatorMapper.selectById(child1Id));
        assertNull(indicatorMapper.selectById(child2Id));
        assertNotNull(indicatorMapper.selectById(otherId));
    }
 
    @Test
    public void testDeleteIndicator_notExists() {
        // 调用,并断言异常
        assertServiceException(() -> indicatorService.deleteIndicator(randomLongId()), QC_INDICATOR_NOT_EXISTS);
    }
 
    // ==================== 辅助方法 ====================
 
    private MesQcIndicatorSaveReqVO buildReqVO(String code, String name, Integer itemType, Long parentId,
                                               Integer resultType) {
        MesQcIndicatorSaveReqVO reqVO = new MesQcIndicatorSaveReqVO();
        reqVO.setCode(code);
        reqVO.setName(name);
        reqVO.setType(TYPE_PERFORMANCE);
        reqVO.setItemType(itemType);
        reqVO.setParentId(parentId);
        reqVO.setResultType(resultType);
        return reqVO;
    }
 
    private MesQcIndicatorDO insertIndicator(String code, String name, Integer itemType, Long parentId,
                                             Integer resultType, Integer sortOrder) {
        MesQcIndicatorDO indicator = new MesQcIndicatorDO();
        indicator.setCode(code);
        indicator.setName(name);
        indicator.setType(TYPE_PERFORMANCE);
        indicator.setItemType(itemType);
        indicator.setParentId(parentId);
        indicator.setResultType(resultType);
        indicator.setSortOrder(sortOrder);
        indicatorMapper.insert(indicator);
        return indicator;
    }
 
    /**
     * 断言抛出的业务异常错误码正确,且提示文案包含给定的关键片段。
     * <p>
     * 层级校验的文案带分组名 / 父指标名 / 子项条数,无法用
     * {@link cn.iocoder.yudao.framework.test.core.util.AssertUtils#assertServiceException} 逐字比对。
     */
    private void assertMessageException(Executable executable, ErrorCode errorCode, String... messageFragments) {
        ServiceException exception = assertThrows(ServiceException.class, executable);
        assertEquals(errorCode.getCode(), exception.getCode(), "错误码不匹配");
        for (String fragment : messageFragments) {
            assertTrue(exception.getMessage().contains(fragment),
                    String.format("错误提示[%s]未包含[%s]", exception.getMessage(), fragment));
        }
    }
 
}