实现 Ticket05 共享字段与电池组持久化
增加字段类型约束、模板值校验、引用冲突保护及 Flyway 迁移。完善电池组状态与删除接口,前端字段改由后端事实恢复,并补充自动化验证。
| New file |
| | |
| | | ALTER TABLE t_battery_group_field |
| | | ADD CONSTRAINT chk_battery_group_field_type |
| | | CHECK (field_type IN ('text', 'number', 'time', 'remark')); |
| | |
| | | assertTrue(v11.contains("ALTER TABLE t_battery_group_relation_field")); |
| | | assertTrue(v11.contains("field_value VARCHAR(500)")); |
| | | assertFalse(v11.matches("(?s).*\\bINSERT\\s+INTO\\b.*")); |
| | | |
| | | String v12 = resource("db/migration/V12__custom_field_type_constraints.sql"); |
| | | assertTrue(v12.contains("chk_battery_group_field_type")); |
| | | assertTrue(v12.contains("'text', 'number', 'time', 'remark'")); |
| | | assertFalse(v12.matches("(?s).*\\bINSERT\\s+INTO\\b.*")); |
| | | } |
| | | |
| | | private static String sha256(String path) |
| | |
| | | verify(batteryGroupFieldMapper).deleteBatteryGroupFieldById(2L, "admin"); |
| | | } |
| | | |
| | | @Test |
| | | void deleteByIdsShouldRejectReferencedField() |
| | | { |
| | | when(batteryGroupFieldMapper.selectBatteryGroupFieldById(1L)).thenReturn(field); |
| | | when(batteryGroupFieldMapper.countFieldReferences(1L)).thenReturn(1); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | | () -> batteryGroupFieldService.deleteBatteryGroupFieldByIds(new Long[] { 1L }, "admin")); |
| | | |
| | | assertEquals("字段已被模板或电池组引用,不能删除", exception.getMessage()); |
| | | verify(batteryGroupFieldMapper, never()).deleteBatteryGroupFieldById(anyLong(), any(String.class)); |
| | | } |
| | | |
| | | private TBatteryGroupField statusCommand(String status) |
| | | { |
| | | TBatteryGroupField command = new TBatteryGroupField(); |
| | |
| | | group.setTemplateId(5L); |
| | | TBatteryGroupRelationField relation = new TBatteryGroupRelationField(); |
| | | relation.setBatteryGroupFieldId(7L); |
| | | relation.setFieldValue("72Ah"); |
| | | relation.setFieldValue("72"); |
| | | relation.setFieldType("number"); |
| | | group.setTemplateFieldRelations(Collections.singletonList(relation)); |
| | | |
| | | when(batteryGroupMapper.selectBatteryGroupById(12L)).thenReturn(current); |
| | |
| | | verify(batteryGroupMapper).updateBatteryGroup(group); |
| | | ArgumentCaptor<List<TBatteryGroupRelationField>> captor = ArgumentCaptor.forClass(List.class); |
| | | verify(batteryGroupMapper).batchInsertRelationFields(captor.capture()); |
| | | assertEquals("72Ah", captor.getValue().get(0).getFieldValue()); |
| | | assertEquals("72", captor.getValue().get(0).getFieldValue()); |
| | | assertEquals("1", group.getStatus()); |
| | | } |
| | | |
| | | @Test |
| | | void insertShouldRejectInvalidTypedFieldValue() |
| | | { |
| | | TBatteryGroup group = new TBatteryGroup(); |
| | | group.setGroupName("样品组"); |
| | | group.setModel("M-01"); |
| | | group.setBatteryNos(Collections.singletonList("BAT-01")); |
| | | group.setTemplateId(5L); |
| | | TBatteryGroupRelationField relation = new TBatteryGroupRelationField(); |
| | | relation.setBatteryGroupFieldId(7L); |
| | | relation.setFieldValue("72Ah"); |
| | | group.setTemplateFieldRelations(Collections.singletonList(relation)); |
| | | TBatteryGroupRelationField validField = new TBatteryGroupRelationField(); |
| | | validField.setBatteryGroupFieldId(7L); |
| | | validField.setFieldType("number"); |
| | | when(batteryGroupMapper.selectBatteryGroupByName("样品组")).thenReturn(null); |
| | | when(batteryGroupMapper.selectBatteryByNo("BAT-01", null)).thenReturn(null); |
| | | when(batteryGroupMapper.selectTemplate(5L)).thenReturn(new TBatteryGroup()); |
| | | when(batteryGroupMapper.selectTemplateFieldIds(5L)).thenReturn(Collections.singletonList(7L)); |
| | | when(batteryGroupMapper.selectValidTemplateFields(5L, Collections.singletonList(7L))) |
| | | .thenReturn(Collections.singletonList(validField)); |
| | | |
| | | org.junit.jupiter.api.Assertions.assertThrows(com.ruoyi.common.exception.ServiceException.class, |
| | | () -> batteryGroupService.insertBatteryGroup(group)); |
| | | verify(batteryGroupMapper, never()).insertBatteryGroup(any(TBatteryGroup.class)); |
| | | } |
| | | } |
| | |
| | | |
| | | /** 逻辑删除字段。 */ |
| | | int deleteBatteryGroupFieldById(@Param("fieldId") Long fieldId, @Param("updateBy") String updateBy); |
| | | |
| | | /** 查询字段是否已被模板或电池组使用。 */ |
| | | int countFieldReferences(Long fieldId); |
| | | } |
| | |
| | | for (Long fieldId : fieldIds) |
| | | { |
| | | selectBatteryGroupFieldById(fieldId); |
| | | if (batteryGroupFieldMapper.countFieldReferences(fieldId) > 0) |
| | | { |
| | | throw new ServiceException("字段已被模板或电池组引用,不能删除"); |
| | | } |
| | | } |
| | | int rows = 0; |
| | | for (Long fieldId : fieldIds) |
| | |
| | | package com.ruoyi.system.service.impl; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.time.format.DateTimeParseException; |
| | | import java.util.Date; |
| | | import java.util.HashMap; |
| | | import java.util.HashSet; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | |
| | | throw new ServiceException("模板不存在"); |
| | | } |
| | | List<Long> fieldIds = new ArrayList<>(); |
| | | Map<Long, String> fieldTypes = new HashMap<>(); |
| | | if (group.getTemplateFieldRelations() != null) |
| | | { |
| | | for (TBatteryGroupRelationField relation : group.getTemplateFieldRelations()) |
| | |
| | | { |
| | | throw new ServiceException("电池组字段必须与模板字段一致"); |
| | | } |
| | | if (batteryGroupMapper.selectValidTemplateFields(group.getTemplateId(), fieldIds).size() != fieldIds.size()) |
| | | List<TBatteryGroupRelationField> validFields = batteryGroupMapper.selectValidTemplateFields(group.getTemplateId(), fieldIds); |
| | | if (validFields == null || validFields.size() != fieldIds.size()) |
| | | { |
| | | throw new ServiceException("电池组字段不属于指定模板"); |
| | | } |
| | | for (TBatteryGroupRelationField field : validFields) |
| | | { |
| | | fieldTypes.put(field.getBatteryGroupFieldId(), field.getFieldType()); |
| | | } |
| | | if (group.getTemplateFieldRelations() != null) |
| | | { |
| | | for (TBatteryGroupRelationField relation : group.getTemplateFieldRelations()) |
| | | { |
| | | relation.setFieldValue(trimToNull(relation.getFieldValue())); |
| | | validateFieldValue(fieldTypes.get(relation.getBatteryGroupFieldId()), relation.getFieldValue()); |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | batteryGroupMapper.batchInsertRelationFields(relations); |
| | | } |
| | | |
| | | /** 按字段定义校验业务值,空值表示该字段暂未填写。 */ |
| | | private void validateFieldValue(String fieldType, String value) |
| | | { |
| | | if (value == null || fieldType == null) return; |
| | | try |
| | | { |
| | | if ("number".equals(fieldType)) |
| | | { |
| | | new BigDecimal(value); |
| | | } |
| | | else if ("time".equals(fieldType)) |
| | | { |
| | | try |
| | | { |
| | | LocalDate.parse(value); |
| | | } |
| | | catch (DateTimeParseException exception) |
| | | { |
| | | try |
| | | { |
| | | LocalDateTime.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); |
| | | } |
| | | catch (DateTimeParseException nested) |
| | | { |
| | | LocalDateTime.parse(value); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | catch (NumberFormatException | DateTimeParseException exception) |
| | | { |
| | | throw new ServiceException("模板字段值格式不正确"); |
| | | } |
| | | } |
| | | |
| | | private void validateStatusChange(TBatteryGroup group, String currentStatus) |
| | | { |
| | | if ("1".equals(group.getStatus()) && !"1".equals(currentStatus) |
| | |
| | | throw new ServiceException("测试中的电池组不能禁用"); |
| | | } |
| | | } |
| | | |
| | | private String trimToNull(String value) |
| | | { |
| | | String trimmed = StringUtils.trim(value); |
| | | return StringUtils.isEmpty(trimmed) ? null : trimmed; |
| | | } |
| | | } |
| | |
| | | where field_id = #{fieldId} |
| | | and del_flag = 0 |
| | | </update> |
| | | |
| | | <select id="countFieldReferences" resultType="int"> |
| | | select |
| | | (select count(1) from t_battery_group_template_field |
| | | where field_id = #{fieldId} and del_flag = 0) |
| | | + (select count(1) from t_battery_group_relation_field |
| | | where battery_group_field_id = #{fieldId} and del_flag = '0') |
| | | </select> |
| | | </mapper> |
| | |
| | | <select id="selectTemplate" resultType="TBatteryGroup">select template_id, template_name from t_battery_group_template where template_id = #{templateId}</select> |
| | | <select id="selectTemplateFieldIds" resultType="Long">select field_id from t_battery_group_template_field where template_id = #{templateId} and del_flag = '0' order by sort_order, relation_id</select> |
| | | <select id="selectValidTemplateFields" resultMap="TBatteryGroupRelationFieldResult">select r.relation_id, r.template_id, r.field_id as battery_group_field_id, f.field_name, f.field_type, '0' as del_flag from t_battery_group_template_field r inner join t_battery_group_field f on f.field_id = r.field_id and f.del_flag = '0' where r.template_id = #{templateId} and r.del_flag = '0' and r.field_id in <foreach collection="fieldIds" item="fieldId" open="(" separator="," close=")">#{fieldId}</foreach></select> |
| | | <select id="selectBatteryByNo" resultMap="TBatteryGroupBatteryResult">select relation_id, group_id, battery_no, sort_order, del_flag from t_battery_group_battery where battery_no = #{batteryNo} and del_flag = '0' limit 1</select> |
| | | <select id="selectBatteryByNo" resultMap="TBatteryGroupBatteryResult">select relation_id, group_id, battery_no, sort_order, del_flag from t_battery_group_battery where battery_no = #{batteryNo} and del_flag = '0' <if test="groupId != null">and group_id != #{groupId}</if> limit 1</select> |
| | | <insert id="insertBatteryGroup" parameterType="TBatteryGroup" useGeneratedKeys="true" keyProperty="groupId">insert into t_battery_group (group_name, model, purpose, z, h, f, a, b, status, create_by, create_time, remark, del_flag) values (#{groupName}, #{model}, #{purpose}, #{z}, #{h}, #{f}, #{a}, #{b}, #{status}, #{createBy}, sysdate(), #{remark}, '0')</insert> |
| | | <update id="updateBatteryGroup" parameterType="TBatteryGroup">update t_battery_group set group_name=#{groupName}, model=#{model}, purpose=#{purpose}, z=#{z}, h=#{h}, f=#{f}, a=#{a}, b=#{b}, status=#{status}, update_by=#{updateBy}, update_time=sysdate(), remark=#{remark} where group_id=#{groupId} and del_flag='0'</update> |
| | | <update id="updateBatteryGroupStatus" parameterType="TBatteryGroup">update t_battery_group set status=#{status}, update_by=#{updateBy}, update_time=sysdate() where group_id=#{groupId} and del_flag='0'</update> |