From 76f9639e3c0e14cc6a4e05d3a3212799ea1c48fb Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期六, 29 八月 2026 09:31:40 +0800
Subject: [PATCH] 实现 Ticket05 共享字段与电池组持久化

---
 ruoyi-admin/src/test/java/com/ruoyi/config/FlywayMigrationContractTest.java                    |    5 +
 ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TBatteryGroupFieldServiceImplTest.java |   13 ++++
 ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TBatteryGroupServiceImplTest.java      |   32 ++++++++++
 ruoyi-admin/src/main/resources/db/migration/V12__custom_field_type_constraints.sql             |    3 +
 ruoyi-system/src/main/resources/mapper/system/TBatteryGroupMapper.xml                          |    2 
 ruoyi-system/src/main/resources/mapper/system/TBatteryGroupFieldMapper.xml                     |    8 ++
 ruoyi-system/src/main/java/com/ruoyi/system/mapper/TBatteryGroupFieldMapper.java               |    3 +
 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TBatteryGroupFieldServiceImpl.java    |    4 +
 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TBatteryGroupServiceImpl.java         |   63 ++++++++++++++++++++
 9 files changed, 129 insertions(+), 4 deletions(-)

diff --git a/ruoyi-admin/src/main/resources/db/migration/V12__custom_field_type_constraints.sql b/ruoyi-admin/src/main/resources/db/migration/V12__custom_field_type_constraints.sql
new file mode 100644
index 0000000..128e68e
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/db/migration/V12__custom_field_type_constraints.sql
@@ -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'));
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/config/FlywayMigrationContractTest.java b/ruoyi-admin/src/test/java/com/ruoyi/config/FlywayMigrationContractTest.java
index 0bab1a8..c7d2b28 100644
--- a/ruoyi-admin/src/test/java/com/ruoyi/config/FlywayMigrationContractTest.java
+++ b/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)
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TBatteryGroupFieldServiceImplTest.java b/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TBatteryGroupFieldServiceImplTest.java
index 58e5394..167d687 100644
--- a/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TBatteryGroupFieldServiceImplTest.java
+++ b/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();
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TBatteryGroupServiceImplTest.java b/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TBatteryGroupServiceImplTest.java
index 88a70bc..d47148e 100644
--- a/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TBatteryGroupServiceImplTest.java
+++ b/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));
+    }
 }
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TBatteryGroupFieldMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TBatteryGroupFieldMapper.java
index b04dadd..5ef1fec 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TBatteryGroupFieldMapper.java
+++ b/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);
 }
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TBatteryGroupFieldServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TBatteryGroupFieldServiceImpl.java
index c88e51a..6783d95 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TBatteryGroupFieldServiceImpl.java
+++ b/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)
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TBatteryGroupServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TBatteryGroupServiceImpl.java
index 87023d2..1b62df7 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TBatteryGroupServiceImpl.java
+++ b/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;
+    }
 }
diff --git a/ruoyi-system/src/main/resources/mapper/system/TBatteryGroupFieldMapper.xml b/ruoyi-system/src/main/resources/mapper/system/TBatteryGroupFieldMapper.xml
index b7e600c..ee534ef 100644
--- a/ruoyi-system/src/main/resources/mapper/system/TBatteryGroupFieldMapper.xml
+++ b/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>
diff --git a/ruoyi-system/src/main/resources/mapper/system/TBatteryGroupMapper.xml b/ruoyi-system/src/main/resources/mapper/system/TBatteryGroupMapper.xml
index 38ce8c6..aa4aaad 100644
--- a/ruoyi-system/src/main/resources/mapper/system/TBatteryGroupMapper.xml
+++ b/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>

--
Gitblit v1.7.1