Browse Source

对接牦牛资产档案

wwh 1 month ago
parent
commit
0e2cdebf7b

+ 0 - 12
baqing-admin/src/main/java/com/ruoyi/web/modules/industryservice/domain/BizYakHerdInventory.java

@@ -48,8 +48,6 @@ public class BizYakHerdInventory extends BaseEntity
48 48
 
49 49
     private Integer cowOver12Count;
50 50
 
51
-    private String delFlag;
52
-
53 51
     public Long getId()
54 52
     {
55 53
         return id;
@@ -219,14 +217,4 @@ public class BizYakHerdInventory extends BaseEntity
219 217
     {
220 218
         this.cowOver12Count = cowOver12Count;
221 219
     }
222
-
223
-    public String getDelFlag()
224
-    {
225
-        return delFlag;
226
-    }
227
-
228
-    public void setDelFlag(String delFlag)
229
-    {
230
-        this.delFlag = delFlag;
231
-    }
232 220
 }

+ 3 - 3
baqing-admin/src/main/java/com/ruoyi/web/modules/industryservice/mapper/BizYakHerdInventoryMapper.java

@@ -11,10 +11,10 @@ public interface BizYakHerdInventoryMapper
11 11
 {
12 12
     BizYakHerdInventory selectBizYakHerdInventoryById(Long id);
13 13
 
14
-    BizYakHerdInventory selectActiveByYearAndTown(@Param("statYear") Integer statYear,
14
+    BizYakHerdInventory selectByYearAndTown(@Param("statYear") Integer statYear,
15 15
             @Param("townDeptId") Long townDeptId);
16 16
 
17
-    int countActiveByYearAndTownExcludeId(@Param("statYear") Integer statYear,
17
+    int countByYearAndTownExcludeId(@Param("statYear") Integer statYear,
18 18
             @Param("townDeptId") Long townDeptId, @Param("excludeId") Long excludeId);
19 19
 
20 20
     List<BizYakHerdInventory> selectBizYakHerdInventoryList(BizYakHerdInventory query);
@@ -23,5 +23,5 @@ public interface BizYakHerdInventoryMapper
23 23
 
24 24
     int updateBizYakHerdInventory(BizYakHerdInventory row);
25 25
 
26
-    int logicDeleteBizYakHerdInventoryByIds(@Param("ids") Long[] ids, @Param("updateBy") String updateBy);
26
+    int deleteBizYakHerdInventoryByIds(@Param("ids") Long[] ids);
27 27
 }

+ 11 - 10
baqing-admin/src/main/java/com/ruoyi/web/modules/industryservice/service/impl/BizYakHerdInventoryServiceImpl.java

@@ -17,7 +17,6 @@ import com.ruoyi.web.modules.industryservice.support.TownDeptSupport;
17 17
 import com.ruoyi.web.modules.industryservice.support.YakHerdInventoryExcelExporter;
18 18
 import com.ruoyi.web.modules.industryservice.support.YakHerdInventoryExcelParser;
19 19
 import com.ruoyi.web.modules.industryservice.support.YakHerdInventoryExcelParser.ParsedRow;
20
-import com.ruoyi.web.modules.industryservice.support.YakHerdInventoryRules;
21 20
 import com.ruoyi.web.modules.industryservice.support.YakHerdInventoryValidation;
22 21
 import com.ruoyi.web.modules.industryservice.support.YakHerdInventoryYearSupport;
23 22
 
@@ -55,7 +54,6 @@ public class BizYakHerdInventoryServiceImpl implements IBizYakHerdInventoryServi
55 54
         YakHerdInventoryValidation.validateRowForSave(row, townDeptSupport);
56 55
         fillTownName(row);
57 56
         assertUniqueYearTown(row.getStatYear(), row.getTownDeptId(), null);
58
-        row.setDelFlag(YakHerdInventoryRules.DEL_FLAG_NORMAL);
59 57
         return bizYakHerdInventoryMapper.insertBizYakHerdInventory(row);
60 58
     }
61 59
 
@@ -68,10 +66,9 @@ public class BizYakHerdInventoryServiceImpl implements IBizYakHerdInventoryServi
68 66
             throw new ServiceException("主键不能为空");
69 67
         }
70 68
         YakHerdInventoryValidation.validateRowForSave(row, townDeptSupport);
71
-        requireActiveRow(row.getId());
69
+        requireExistingRow(row.getId());
72 70
         fillTownName(row);
73 71
         assertUniqueYearTown(row.getStatYear(), row.getTownDeptId(), row.getId());
74
-        row.setDelFlag(YakHerdInventoryRules.DEL_FLAG_NORMAL);
75 72
         return bizYakHerdInventoryMapper.updateBizYakHerdInventory(row);
76 73
     }
77 74
 
@@ -95,7 +92,7 @@ public class BizYakHerdInventoryServiceImpl implements IBizYakHerdInventoryServi
95 92
         {
96 93
             return 0;
97 94
         }
98
-        return bizYakHerdInventoryMapper.logicDeleteBizYakHerdInventoryByIds(cleaned.toArray(new Long[0]), updateBy);
95
+        return bizYakHerdInventoryMapper.deleteBizYakHerdInventoryByIds(cleaned.toArray(new Long[0]));
99 96
     }
100 97
 
101 98
     @Override
@@ -190,14 +187,18 @@ public class BizYakHerdInventoryServiceImpl implements IBizYakHerdInventoryServi
190 187
                 YakHerdInventoryExcelExporter.toTownMap(list));
191 188
     }
192 189
 
190
+    /**
191
+     * 导入 upsert:唯一键为「所属年份 + 所属乡镇(town_dept_id)」;已存在则全量覆盖更新,否则新增。
192
+     *
193
+     * @return true 更新,false 新增
194
+     */
193 195
     private boolean upsertImportRow(BizYakHerdInventory row, String username)
194 196
     {
195
-        BizYakHerdInventory existing = bizYakHerdInventoryMapper.selectActiveByYearAndTown(row.getStatYear(),
197
+        BizYakHerdInventory existing = bizYakHerdInventoryMapper.selectByYearAndTown(row.getStatYear(),
196 198
                 row.getTownDeptId());
197 199
         if (existing == null)
198 200
         {
199 201
             row.setCreateBy(username);
200
-            row.setDelFlag(YakHerdInventoryRules.DEL_FLAG_NORMAL);
201 202
             bizYakHerdInventoryMapper.insertBizYakHerdInventory(row);
202 203
             return false;
203 204
         }
@@ -218,18 +219,18 @@ public class BizYakHerdInventoryServiceImpl implements IBizYakHerdInventoryServi
218 219
 
219 220
     private void assertUniqueYearTown(Integer statYear, Long townDeptId, Long excludeId)
220 221
     {
221
-        if (bizYakHerdInventoryMapper.countActiveByYearAndTownExcludeId(statYear, townDeptId, excludeId) > 0)
222
+        if (bizYakHerdInventoryMapper.countByYearAndTownExcludeId(statYear, townDeptId, excludeId) > 0)
222 223
         {
223 224
             throw new ServiceException("该年份该乡镇已填报");
224 225
         }
225 226
     }
226 227
 
227
-    private BizYakHerdInventory requireActiveRow(Long id)
228
+    private BizYakHerdInventory requireExistingRow(Long id)
228 229
     {
229 230
         BizYakHerdInventory row = bizYakHerdInventoryMapper.selectBizYakHerdInventoryById(id);
230 231
         if (row == null)
231 232
         {
232
-            throw new ServiceException("记录不存在或已删除");
233
+            throw new ServiceException("记录不存在");
233 234
         }
234 235
         return row;
235 236
     }

+ 0 - 4
baqing-admin/src/main/java/com/ruoyi/web/modules/industryservice/support/YakHerdInventoryRules.java

@@ -5,10 +5,6 @@ package com.ruoyi.web.modules.industryservice.support;
5 5
  */
6 6
 public final class YakHerdInventoryRules
7 7
 {
8
-    public static final String DEL_FLAG_NORMAL = "0";
9
-
10
-    public static final String DEL_FLAG_DELETED = "2";
11
-
12 8
     public static final long TOWN_PARENT_ID = 100L;
13 9
 
14 10
     public static final int TOWN_MIN_ORDER_NUM = 12;

+ 10 - 15
baqing-admin/src/main/resources/mapper/industryservice/BizYakHerdInventoryMapper.xml

@@ -20,7 +20,6 @@
20 20
         <result property="cow13Count"             column="cow_1_3_count"/>
21 21
         <result property="cow312Count"            column="cow_3_12_count"/>
22 22
         <result property="cowOver12Count"         column="cow_over_12_count"/>
23
-        <result property="delFlag"                column="del_flag"/>
24 23
         <result property="createBy"               column="create_by"/>
25 24
         <result property="createTime"             column="create_time"/>
26 25
         <result property="updateBy"               column="update_by"/>
@@ -35,29 +34,27 @@
35 34
                h.yak_total, h.bull_total, h.cow_total,
36 35
                h.breeding_bull_count, h.bull_castrated_count,
37 36
                h.cow_under_1_count, h.cow_1_3_count, h.cow_3_12_count, h.cow_over_12_count,
38
-               h.del_flag, h.create_by, h.create_time, h.update_by, h.update_time, h.remark
37
+               h.create_by, h.create_time, h.update_by, h.update_time, h.remark
39 38
         from biz_yak_herd_inventory h
40 39
     </sql>
41 40
 
42 41
     <select id="selectBizYakHerdInventoryById" parameterType="long" resultMap="BizYakHerdInventoryResult">
43 42
         <include refid="selectVo"/>
44
-        where h.id = #{id} and h.del_flag = '0'
43
+        where h.id = #{id}
45 44
     </select>
46 45
 
47
-    <select id="selectActiveByYearAndTown" resultMap="BizYakHerdInventoryResult">
46
+    <select id="selectByYearAndTown" resultMap="BizYakHerdInventoryResult">
48 47
         <include refid="selectVo"/>
49 48
         where h.stat_year = #{statYear}
50 49
           and h.town_dept_id = #{townDeptId}
51
-          and h.del_flag = '0'
52 50
         limit 1
53 51
     </select>
54 52
 
55
-    <select id="countActiveByYearAndTownExcludeId" resultType="int">
53
+    <select id="countByYearAndTownExcludeId" resultType="int">
56 54
         select count(1)
57 55
         from biz_yak_herd_inventory
58 56
         where stat_year = #{statYear}
59 57
           and town_dept_id = #{townDeptId}
60
-          and del_flag = '0'
61 58
         <if test="excludeId != null">
62 59
             and id != #{excludeId}
63 60
         </if>
@@ -68,7 +65,6 @@
68 65
         <include refid="selectVo"/>
69 66
         left join sys_dept d on h.town_dept_id = d.dept_id
70 67
         <where>
71
-            h.del_flag = '0'
72 68
             <if test="statYear != null">
73 69
                 and h.stat_year = #{statYear}
74 70
             </if>
@@ -88,7 +84,7 @@
88 84
             yak_total, bull_total, cow_total,
89 85
             breeding_bull_count, bull_castrated_count,
90 86
             cow_under_1_count, cow_1_3_count, cow_3_12_count, cow_over_12_count,
91
-            del_flag, create_by, create_time, remark
87
+            create_by, create_time, remark
92 88
         ) values (
93 89
             #{statYear}, #{townDeptId}, #{townName},
94 90
             #{villageDeptId}, #{villageName},
@@ -96,7 +92,7 @@
96 92
             #{yakTotal}, #{bullTotal}, #{cowTotal},
97 93
             #{breedingBullCount}, #{bullCastratedCount},
98 94
             #{cowUnder1Count}, #{cow13Count}, #{cow312Count}, #{cowOver12Count},
99
-            #{delFlag}, #{createBy}, sysdate(), #{remark}
95
+            #{createBy}, sysdate(), #{remark}
100 96
         )
101 97
     </insert>
102 98
 
@@ -121,14 +117,13 @@
121 117
             update_by = #{updateBy},
122 118
             update_time = sysdate(),
123 119
             remark = #{remark}
124
-        where id = #{id} and del_flag = '0'
120
+        where id = #{id}
125 121
     </update>
126 122
 
127
-    <update id="logicDeleteBizYakHerdInventoryByIds">
128
-        update biz_yak_herd_inventory set del_flag = '2', update_by = #{updateBy}, update_time = sysdate()
129
-        where del_flag = '0' and id in
123
+    <delete id="deleteBizYakHerdInventoryByIds">
124
+        delete from biz_yak_herd_inventory where id in
130 125
         <foreach collection="ids" item="id" open="(" separator="," close=")">
131 126
             #{id}
132 127
         </foreach>
133
-    </update>
128
+    </delete>
134 129
 </mapper>

+ 6 - 8
baqing-admin/src/test/java/com/ruoyi/web/modules/industryservice/service/impl/BizYakHerdInventoryServiceImplTest.java

@@ -3,7 +3,6 @@ package com.ruoyi.web.modules.industryservice.service.impl;
3 3
 import static org.junit.jupiter.api.Assertions.assertEquals;
4 4
 import static org.junit.jupiter.api.Assertions.assertThrows;
5 5
 import static org.mockito.ArgumentMatchers.any;
6
-import static org.mockito.ArgumentMatchers.eq;
7 6
 import static org.mockito.Mockito.never;
8 7
 import static org.mockito.Mockito.verify;
9 8
 import static org.mockito.Mockito.when;
@@ -20,7 +19,6 @@ import com.ruoyi.common.exception.ServiceException;
20 19
 import com.ruoyi.web.modules.industryservice.domain.BizYakHerdInventory;
21 20
 import com.ruoyi.web.modules.industryservice.mapper.BizYakHerdInventoryMapper;
22 21
 import com.ruoyi.web.modules.industryservice.support.TownDeptSupport;
23
-import com.ruoyi.web.modules.industryservice.support.YakHerdInventoryRules;
24 22
 import com.ruoyi.web.modules.industryservice.support.YakHerdInventoryYearSupport;
25 23
 
26 24
 @ExtendWith(MockitoExtension.class)
@@ -61,7 +59,7 @@ class BizYakHerdInventoryServiceImplTest
61 59
     void ut005_duplicateYearTown()
62 60
     {
63 61
         mockTown();
64
-        when(mapper.countActiveByYearAndTownExcludeId(2026, 101L, null)).thenReturn(1);
62
+        when(mapper.countByYearAndTownExcludeId(2026, 101L, null)).thenReturn(1);
65 63
         assertThrows(ServiceException.class, () -> service.insertBizYakHerdInventory(sampleRow()));
66 64
         verify(mapper, never()).insertBizYakHerdInventory(any());
67 65
     }
@@ -71,7 +69,7 @@ class BizYakHerdInventoryServiceImplTest
71 69
     void ut006_insertSuccess()
72 70
     {
73 71
         mockTown();
74
-        when(mapper.countActiveByYearAndTownExcludeId(2026, 101L, null)).thenReturn(0);
72
+        when(mapper.countByYearAndTownExcludeId(2026, 101L, null)).thenReturn(0);
75 73
         when(mapper.insertBizYakHerdInventory(any())).thenReturn(1);
76 74
         assertEquals(1, service.insertBizYakHerdInventory(sampleRow()));
77 75
         verify(mapper).insertBizYakHerdInventory(any());
@@ -88,11 +86,12 @@ class BizYakHerdInventoryServiceImplTest
88 86
     }
89 87
 
90 88
     @Test
91
-    @DisplayName("ZCZX-MYCLTB-UT-014 逻辑删除")
89
+    @DisplayName("ZCZX-MYCLTB-UT-014 物理删除")
92 90
     void ut014_delete()
93 91
     {
94
-        when(mapper.logicDeleteBizYakHerdInventoryByIds(any(), eq("tester"))).thenReturn(1);
92
+        when(mapper.deleteBizYakHerdInventoryByIds(any())).thenReturn(1);
95 93
         assertEquals(1, service.deleteBizYakHerdInventoryByIds(new Long[] { 1L }, "tester"));
94
+        verify(mapper).deleteBizYakHerdInventoryByIds(any());
96 95
     }
97 96
 
98 97
     @Test
@@ -100,7 +99,7 @@ class BizYakHerdInventoryServiceImplTest
100 99
     void ut016_villageDeptOptional()
101 100
     {
102 101
         mockTown();
103
-        when(mapper.countActiveByYearAndTownExcludeId(2026, 101L, null)).thenReturn(0);
102
+        when(mapper.countByYearAndTownExcludeId(2026, 101L, null)).thenReturn(0);
104 103
         when(mapper.insertBizYakHerdInventory(any())).thenReturn(1);
105 104
         BizYakHerdInventory row = sampleRow();
106 105
         row.setVillageDeptId(null);
@@ -133,7 +132,6 @@ class BizYakHerdInventoryServiceImplTest
133 132
         row.setTownDeptId(101L);
134 133
         row.setTownName("巴青乡");
135 134
         row.setYakTotal(100);
136
-        row.setDelFlag(YakHerdInventoryRules.DEL_FLAG_NORMAL);
137 135
         return row;
138 136
     }
139 137
 }

+ 10 - 7
doc/产业数据模型及服务/牦牛存栏数据填报/牦牛存栏数据填报功能需求.md

@@ -19,7 +19,7 @@
19 19
 
20 20
 - 一条记录表示:**某一自然年、某一乡镇**的牦牛存栏汇总数据(**不按村居拆行**)。  
21 21
 - **所属村居**(`village_name`、`village_dept_id`)为预留字段,**非必填**,本期可空;后续若按村居下钻再扩展。  
22
-- 同一「所属年份 + 所属乡镇」在系统中**仅允许一条有效记录**(逻辑删除后可重新填报)。
22
+- 同一「所属年份 + 所属乡镇」在系统中**仅允许一条记录**(物理删除后可重新填报)。
23 23
 
24 24
 ### 2.2 所属年份
25 25
 
@@ -80,7 +80,7 @@
80 80
 | 分页列表 | 按所属年份、所属乡镇筛选;见 **§5.1** |
81 81
 | 新增 | 手工填报一条乡镇年度数据;见 **§5.2** |
82 82
 | 修改 | 编辑已有记录;见 **§5.3** |
83
-| 删除 | 逻辑删除;见 **§5.4** |
83
+| 删除 | 物理删除;见 **§5.4** |
84 84
 | 详情 | 查看全部字段;见 **§5.5** |
85 85
 | Excel 导入 | 按模板批量导入**指定填报年份**下所有乡镇数据;见 **§5.6** |
86 86
 | Excel 导出 | 按**指定所属年份**导出巴青县**全部乡镇**存栏数据;见 **§5.7** |
@@ -128,7 +128,6 @@ flowchart TD
128 128
 **列表规则**
129 129
 
130 130
 - 默认每页 **20** 条;排序:**所属年份倒序**,同年内按乡镇 `order_num` 升序(或乡镇名称拼音)。  
131
-- 仅展示 `del_flag = '0'` 的有效记录。  
132 131
 - 支持**重置**筛选。
133 132
 
134 133
 **列表展示列**
@@ -172,9 +171,9 @@ flowchart TD
172 171
 
173 172
 | 项 | 要求 |
174 173
 | --- | --- |
175
-| 方式 | 逻辑删除(`del_flag = '2'`) |
174
+| 方式 | **物理删除**(`DELETE FROM biz_yak_herd_inventory`) |
176 175
 | 确认 | 删除前二次确认 |
177
-| 效果 | 列表不再展示;同年同乡镇可重新新增或导入 |
176
+| 效果 | 记录从库中移除;同年同乡镇可重新新增或导入 |
178 177
 
179 178
 ---
180 179
 
@@ -196,8 +195,10 @@ flowchart TD
196 195
 | 填报年份 | 导入前用户**必须选择**「填报年份」(范围 **§2.2**);文件中**不写年份**时以所选年份为准 |
197 196
 | 解析范围 | 从数据区首行(乡镇列有值)起,至乡镇为空的行结束;**跳过**表头、标题、填表单位等说明行 |
198 197
 | 乡镇匹配 | Excel「乡镇」列文本与 `sys_dept.dept_name` **精确匹配**(trim 后);仅接受 **§2.3** 范围内部门 |
199
-| 合并策略 | 同年同乡镇已存在 → **更新**;不存在 → **新增** |
200
-| 结果反馈 | 返回成功/失败条数及失败明细(行号、乡镇、原因) |
198
+| 唯一键 | **所属年份(填报年份)+ 所属乡镇(`town_dept_id`)**;同一键在库中仅保留一条有效业务数据 |
199
+| 合并策略 | 按「年份 + 乡镇」**upsert**:该键**已存在** → **全量覆盖更新**;**不存在** → **新增** |
200
+| 重复导入 | **允许**对同一 `牦牛存栏数据.xlsx` 多次导入;第二次及以后 `updateCount` 增加、`insertCount` 为 0(乡镇均有数据时) |
201
+| 结果反馈 | 返回 `insertCount`、`updateCount`、失败条数及失败明细(行号、乡镇、原因) |
201 202
 | 文件限制 | `.xlsx`;大小上限与项目统一上传限制一致 |
202 203
 | 列映射 | 与模板列顺序一致,见技术方案 **§3.8** |
203 204
 
@@ -299,3 +300,5 @@ flowchart TD
299 300
 | 1.0 | 初稿 |
300 301
 | 1.1 | 增加 `village_dept_id` 预留字段;列表列「乡镇」「存栏数」 |
301 302
 | 1.2 | 增加 **§5.7 按年份导出** 全县乡镇 Excel;权限 `export` |
303
+| 1.3 | 明确导入 upsert:唯一键「年份+乡镇」;重复导入更新 |
304
+| 1.4 | 取消逻辑删除:表无 `del_flag`,删除为物理删除;唯一约束 `uk_year_town` |

+ 20 - 8
doc/产业数据模型及服务/牦牛存栏数据填报/牦牛存栏数据填报技术方案.md

@@ -45,17 +45,17 @@
45 45
 | `cow_1_3_count` | `int(11)` | N | 1~3 岁母牛存栏数 |
46 46
 | `cow_3_12_count` | `int(11)` | N | 3~12 岁母牛存栏数 |
47 47
 | `cow_over_12_count` | `int(11)` | N | 12 岁以上母牛存栏数 |
48
-| `del_flag` | `char(1)` | Y | `0` 正常 `2` 删除 |
49 48
 | `create_by` / `create_time` | — | — | 审计 |
50 49
 | `update_by` / `update_time` | — | — | 审计 |
51 50
 | `remark` | `varchar(500)` | N | 备注 |
52 51
 
52
+**说明**:本表**不使用逻辑删除**,无 `del_flag` 字段;删除操作为物理删除。
53
+
53 54
 **索引与约束**
54 55
 
55
-- 应用层校验:同年同乡镇仅一条 `del_flag='0'` 有效记录(`countActiveByYearAndTownExcludeId`)
56
+- `UNIQUE uk_year_town (stat_year, town_dept_id)` — 年份+乡镇唯一,支撑导入 upsert
56 57
 - `KEY idx_stat_year (stat_year)`  
57
-- `KEY idx_town_dept_id (town_dept_id)`  
58
-- `KEY idx_del_flag (del_flag)`
58
+- `KEY idx_town_dept_id (town_dept_id)`
59 59
 
60 60
 **DDL 文件**:`sql/biz_yak_herd_inventory.sql`
61 61
 
@@ -137,7 +137,7 @@ for (int y = current; y >= current - 5; y--) { ... }
137 137
 | 各 `*Count` | N | `>= 0` 整数 |
138 138
 | `id` | 修改必填 | — |
139 139
 
140
-**唯一性**:`existsActive(statYear, townDeptId, excludeId)` 为 true 则拒绝
140
+**唯一性**:`countByYearAndTownExcludeId(statYear, townDeptId, excludeId) > 0` 则拒绝(**手工新增/修改**);**导入**走 upsert,不受此限
141 141
 
142 142
 #### 3.8 导入
143 143
 
@@ -145,8 +145,18 @@ for (int y = current; y >= current - 5; y--) { ... }
145 145
 
146 146
 | 参数 | 必填 | 说明 |
147 147
 | --- | --- | --- |
148
-| `file` | Y | `.xlsx` |
149
-| `statYear` | Y | 填报年份 |
148
+| `file` | Y | `.xlsx`(如 `牦牛存栏数据.xlsx`) |
149
+| `statYear` | Y | 填报年份(所属年份) |
150
+
151
+**Upsert 规则**(`BizYakHerdInventoryServiceImpl.upsertImportRow`):
152
+
153
+| 步骤 | 说明 |
154
+| --- | --- |
155
+| 1 | 按 `statYear` + `townDeptId` 查询 `selectByYearAndTown` |
156
+| 2 | 无记录 → `insert`;`insertCount++` |
157
+| 3 | 有记录 → `updateBizYakHerdInventory` 全字段覆盖;`updateCount++` |
158
+
159
+同一文件对**同一 `statYear`** 多次导入:各乡镇行均为更新,不产生重复有效记录。
150 160
 
151 161
 **响应 `data`**
152 162
 
@@ -202,7 +212,7 @@ for (int y = current; y >= current - 5; y--) { ... }
202 212
 
203 213
 1. 加载 classpath 模板 `YakHerdInventoryExcelExporter.TEMPLATE_CLASSPATH`  
204 214
 2. 替换表头中的四位年份为 `statYear`  
205
-3. 查询 `stat_year = statYear` 且 `del_flag='0'` 的全部记录,按 `town_dept_id` 建 Map  
215
+3. 查询 `stat_year = statYear` 的全部记录,按 `town_dept_id` 建 Map  
206 216
 4. 遍历 `TownDeptSupport.listReportTowns()`,逐乡镇写行;无数据则仅写乡镇名  
207 217
 5. 末行写「合计」,对已填报乡镇数值列求和  
208 218
 
@@ -265,3 +275,5 @@ for (int y = current; y >= current - 5; y--) { ... }
265 275
 | 1.0 | 初稿:表结构、CRUD、导入、sys_dept 乡镇、年份前推 5 年 |
266 276
 | 1.1 | 增加预留字段 `village_dept_id`(非必填) |
267 277
 | 1.2 | 实现按年份导出全县乡镇 Excel;列映射与代码对齐;交付清单勾选 |
278
+| 1.3 | 导入 upsert:`selectByYearAndTown` + `updateBizYakHerdInventory`;支持重复导入 |
279
+| 1.4 | 取消逻辑删除:无 `del_flag`;`deleteBizYakHerdInventoryByIds` 物理删除;`uk_year_town` 唯一约束 |

+ 8 - 6
doc/产业数据模型及服务/牦牛存栏数据填报/牦牛存栏数据填报测试用例.md

@@ -18,15 +18,15 @@
18 18
 | ZCZX-MYCLTB-UT-003 | 校验 | 必填年份乡镇 | 单元测试 | JUnit5 | §5.2 | 无 | `validate(row)` 缺 `statYear` | 抛 `ServiceException` |
19 19
 | ZCZX-MYCLTB-UT-004 | 校验 | 负数拒绝 | 单元测试 | JUnit5 | §2.5 | 无 | `yakTotal=-1` | 校验失败 |
20 20
 | ZCZX-MYCLTB-UT-005 | 唯一性 | 同年同乡镇 | 单元测试 | JUnit5+Mockito | §5.2 | 已存在 2026+巴青乡 | `insert` 同键 | 失败「已填报」 |
21
-| ZCZX-MYCLTB-UT-006 | 唯一性 | 删除后可新增 | 单元测试 | JUnit5+Mockito | §5.4 | 同年同乡镇 `del_flag=2` | `insert` | 成功 |
21
+| ZCZX-MYCLTB-UT-006 | 新增 | 新增成功 | 单元测试 | JUnit5+Mockito | §5.2 | 无同年同乡镇记录 | `insert` | 成功 |
22 22
 | ZCZX-MYCLTB-UT-007 | 导入解析 | 识别表头行 | 单元测试 | JUnit5 | §3.8.1 | `2026年.xlsx` | `detectHeaderRow(sheet)` | 定位含「乡镇」表头 |
23 23
 | ZCZX-MYCLTB-UT-008 | 导入解析 | 乡镇名匹配 | 单元测试 | JUnit5 | §5.6 | 行「杂色镇」 | `resolveTownDeptId` | 返回对应 `dept_id` |
24 24
 | ZCZX-MYCLTB-UT-009 | 导入解析 | 未知乡镇 | 单元测试 | JUnit5 | §5.6 | 行「不存在乡」 | 解析 | 记入 `failMessages` |
25 25
 | ZCZX-MYCLTB-UT-010 | 导入解析 | 空行终止 | 单元测试 | JUnit5 | §5.6 | 数据后空行 | `parseRows` | 仅 10 乡镇行 |
26 26
 | ZCZX-MYCLTB-UT-011 | 导入 | upsert 新增 | 单元测试 | JUnit5+Mockito | §5.6 | DB 无 2025+本塔乡 | `importYear(2025,file)` | `insertCount>=1` |
27
-| ZCZX-MYCLTB-UT-012 | 导入 | upsert 更新 | 单元测试 | JUnit5+Mockito | §5.6 | 已有 2026+杂色镇 | 改 `yakTotal` 再导入 | `updateCount>=1`;值更新 |
28
-| ZCZX-MYCLTB-UT-013 | 警告 | 母牛分龄不等 | 单元测试 | JUnit5 | §2.5 | 分龄和≠母牛合计 | 保存/导入 | 成功;`warnMessages` 非空(若启用) |
29
-| ZCZX-MYCLTB-UT-014 | Mapper | 列表排序 | 单元测试 | JUnit5 | §5.1 | 多年多条 | 查 XML | `stat_year DESC`;乡镇 `order_num` |
27
+| ZCZX-MYCLTB-UT-012 | 导入 | upsert 更新 | 单元测试 | JUnit5+Mockito | §5.6 | 已有 2026+各乡镇 | 同一 xlsx 再导入 | `updateCount=10`;`insertCount=0` |
28
+| ZCZX-MYCLTB-UT-013 | 修改 | 记录不存在 | 单元测试 | JUnit5+Mockito | §5.3 | id 不存在 | `update` | 抛 `ServiceException` |
29
+| ZCZX-MYCLTB-UT-014 | 删除 | 物理删除 | 单元测试 | JUnit5+Mockito | §5.4 | 有效 id | `deleteBizYakHerdInventoryByIds` | 调用 Mapper 物理删除 |
30 30
 | ZCZX-MYCLTB-UT-015 | 数据权限 | 乡镇账号过滤 | 单元测试 | JUnit5+Mockito | §6 | 用户 dept=杂色镇 | `selectList` | 仅本乡镇 |
31 31
 | ZCZX-MYCLTB-UT-016 | 预留字段 | village_dept_id 可空 | 单元测试 | JUnit5 | §2.4 | 无 | `insert` 不传 `villageDeptId` | 成功 |
32 32
 | ZCZX-MYCLTB-UT-017 | 预留字段 | village_dept_id 父子校验 | 单元测试 | JUnit5+Mockito | §2.4 | 乡镇 A、村居 B 非子节点 | `insert` 带错误 `villageDeptId` | 失败(若启用校验)或成功仅存 id |
@@ -49,7 +49,7 @@
49 49
 | ZCZX-MYCLTB-API-008 | 新增 | 缺乡镇 | 接口测试 | Postman | §5.2 | 已登录 | 缺 `townDeptId` | 失败 |
50 50
 | ZCZX-MYCLTB-API-009 | 新增 | 重复同年乡镇 | 接口测试 | Postman | §5.2 | 已有记录 | 再 `POST` 同键 | 失败 |
51 51
 | ZCZX-MYCLTB-API-010 | 修改 | 更新指标 | 接口测试 | Postman | §5.3 | 有效 id | `PUT` 改 `yakTotal` | 成功;详情为新值 |
52
-| ZCZX-MYCLTB-API-011 | 删除 | 逻辑删除 | 接口测试 | Postman | §5.4 | 有效 id | `DELETE /{id}` | 成功;列表不可见 |
52
+| ZCZX-MYCLTB-API-011 | 删除 | 物理删除 | 接口测试 | Postman | §5.4 | 有效 id | `DELETE /{id}` | 成功;记录从库中移除 |
53 53
 | ZCZX-MYCLTB-API-012 | 下拉 | 年份选项 | 接口测试 | Postman | §2.2 | 已登录 | `GET /yearOptions` | 6 个年份 |
54 54
 | ZCZX-MYCLTB-API-013 | 下拉 | 乡镇选项 | 接口测试 | Postman | §2.3 | 已登录 | `GET /townOptions` | 10 个乡镇;`order_num>12` |
55 55
 | ZCZX-MYCLTB-API-014 | 导入 | 全量乡镇 | 接口测试 | Postman | §5.6 | 空库 2026 | `POST /importData`+`2026年.xlsx`+`statYear=2026` | `insertCount=10`;`failCount=0` |
@@ -99,7 +99,7 @@
99 99
 | §2.2 年份 | 向前 5 年 | UT-001;API-012;UI-005 |
100 100
 | §2.3 乡镇 | sys_dept 二级 order_num>12 | UT-002;API-013;UI-006 |
101 101
 | §5.1 列表 | 7 数据列+操作;详情全字段 | API-001~004、005;UI-001~003、009 |
102
-| §5.2~5.4 CRUD | 必填、唯一、删除 | UT-003~006;API-006~011 |
102
+| §5.2~5.4 CRUD | 必填、唯一、删除 | UT-003~006、013~014;API-006~011 |
103 103
 | §5.5 详情 | 全字段 | API-005;UI-009 |
104 104
 | §5.6 导入 | 2026年.xlsx | UT-007~012;API-014~017;UI-010~011 |
105 105
 | §5.7 导出 | 按年全县乡镇 | UT-018~019;API-021~025;UI-014~015 |
@@ -114,3 +114,5 @@
114 114
 | 1.0 | 初版:单元 15、接口 20、UI 13;编号前缀 **ZCZX-MYCLTB** |
115 115
 | 1.1 | 补充 `village_dept_id` 预留字段用例 UT-016~017 |
116 116
 | 1.2 | 补充按年份导出用例 UT-018~019、API-021~025、UI-014~016 |
117
+| 1.3 | 补充重复导入 upsert 用例 UT-011~012 |
118
+| 1.4 | 取消逻辑删除:UT-014 改为物理删除;移除 UT-020 |

+ 3 - 3
sql/biz_yak_herd_inventory.sql

@@ -1,4 +1,4 @@
1
+-- 牦牛存栏数据填报(物理删除,无 del_flag)
1 2
 CREATE TABLE IF NOT EXISTS `biz_yak_herd_inventory` (
2 3
   `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
3 4
   `stat_year` int(11) NOT NULL COMMENT '所属年份',
@@ -17,14 +17,13 @@ CREATE TABLE IF NOT EXISTS `biz_yak_herd_inventory` (
17 17
   `cow_1_3_count` int(11) DEFAULT NULL COMMENT '1-3岁母牛存栏数',
18 18
   `cow_3_12_count` int(11) DEFAULT NULL COMMENT '3-12岁母牛存栏数',
19 19
   `cow_over_12_count` int(11) DEFAULT NULL COMMENT '12岁以上母牛存栏数',
20
-  `del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '0存在 2删除',
21 20
   `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
22 21
   `create_time` datetime DEFAULT NULL COMMENT '创建时间',
23 22
   `update_by` varchar(64) DEFAULT '' COMMENT '更新者',
24 23
   `update_time` datetime DEFAULT NULL COMMENT '更新时间',
25 24
   `remark` varchar(500) DEFAULT NULL COMMENT '备注',
26 25
   PRIMARY KEY (`id`),
26
+  UNIQUE KEY `uk_year_town` (`stat_year`, `town_dept_id`),
27 27
   KEY `idx_stat_year` (`stat_year`),
28
-  KEY `idx_town_dept_id` (`town_dept_id`),
29
-  KEY `idx_del_flag` (`del_flag`)
28
+  KEY `idx_town_dept_id` (`town_dept_id`)
30 29
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='牦牛存栏数据填报';