yeg
2026-08-29 76f9639e3c0e14cc6a4e05d3a3212799ea1c48fb
实现 Ticket05 共享字段与电池组持久化

增加字段类型约束、模板值校验、引用冲突保护及 Flyway 迁移。完善电池组状态与删除接口,前端字段改由后端事实恢复,并补充自动化验证。
1个文件已添加
8个文件已修改
133 ■■■■■ 已修改文件
ruoyi-admin/src/main/resources/db/migration/V12__custom_field_type_constraints.sql 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/com/ruoyi/config/FlywayMigrationContractTest.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TBatteryGroupFieldServiceImplTest.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TBatteryGroupServiceImplTest.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TBatteryGroupFieldMapper.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TBatteryGroupFieldServiceImpl.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TBatteryGroupServiceImpl.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/TBatteryGroupFieldMapper.xml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/TBatteryGroupMapper.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/db/migration/V12__custom_field_type_constraints.sql
New file
@@ -0,0 +1,3 @@
ALTER TABLE t_battery_group_field
    ADD CONSTRAINT chk_battery_group_field_type
        CHECK (field_type IN ('text', 'number', 'time', 'remark'));
ruoyi-admin/src/test/java/com/ruoyi/config/FlywayMigrationContractTest.java
@@ -94,6 +94,11 @@
        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)
ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TBatteryGroupFieldServiceImplTest.java
@@ -187,6 +187,19 @@
        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();
ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TBatteryGroupServiceImplTest.java
@@ -71,7 +71,8 @@
        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);
@@ -88,7 +89,34 @@
        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));
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TBatteryGroupFieldMapper.java
@@ -31,4 +31,7 @@
    /** 逻辑删除字段。 */
    int deleteBatteryGroupFieldById(@Param("fieldId") Long fieldId, @Param("updateBy") String updateBy);
    /** 查询字段是否已被模板或电池组使用。 */
    int countFieldReferences(Long fieldId);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TBatteryGroupFieldServiceImpl.java
@@ -113,6 +113,10 @@
        for (Long fieldId : fieldIds)
        {
            selectBatteryGroupFieldById(fieldId);
            if (batteryGroupFieldMapper.countFieldReferences(fieldId) > 0)
            {
                throw new ServiceException("字段已被模板或电池组引用,不能删除");
            }
        }
        int rows = 0;
        for (Long fieldId : fieldIds)
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TBatteryGroupServiceImpl.java
@@ -1,7 +1,13 @@
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;
@@ -207,6 +213,7 @@
            throw new ServiceException("模板不存在");
        }
        List<Long> fieldIds = new ArrayList<>();
        Map<Long, String> fieldTypes = new HashMap<>();
        if (group.getTemplateFieldRelations() != null)
        {
            for (TBatteryGroupRelationField relation : group.getTemplateFieldRelations())
@@ -234,9 +241,22 @@
        {
            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());
            }
        }
    }
@@ -279,6 +299,41 @@
        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)
@@ -287,4 +342,10 @@
            throw new ServiceException("测试中的电池组不能禁用");
        }
    }
    private String trimToNull(String value)
    {
        String trimmed = StringUtils.trim(value);
        return StringUtils.isEmpty(trimmed) ? null : trimmed;
    }
}
ruoyi-system/src/main/resources/mapper/system/TBatteryGroupFieldMapper.xml
@@ -95,4 +95,12 @@
        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>
ruoyi-system/src/main/resources/mapper/system/TBatteryGroupMapper.xml
@@ -55,7 +55,7 @@
    <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>