# 牦牛存栏数据填报 — 技术方案 > 依据:同目录 `牦牛存栏数据填报功能需求.md`;模板 `E:\luhao\工作交接\巴青项目\农牧数据\农业农村和科技水利局-业务办\畜群结构\2026年.xlsx`(第一个 Sheet)。 --- ## 1. 技术架构 | 项 | 说明 | | --- | --- | | **后端** | RuoYi **v3.9.2**(springboot2):JDK 8、Spring Boot 2.x、Spring MVC、MyBatis、Druid | | **数据库** | MySQL **5.7.39**,InnoDB,`utf8mb4` | | **前端** | 若依 Vue2 + Element UI | | **Excel** | Apache POI 或项目已有 EasyExcel(与若依导入规范一致) | | **组织** | `sys_dept` 乡镇字典;`SysDeptMapper` 查询二级节点 | **分层**:`BizYakHerdInventoryController` → `IBizYakHerdInventoryService` → `BizYakHerdInventoryMapper` → `domain`;Excel 由 `YakHerdInventoryExcelParser`(导入解析)、`YakHerdInventoryExcelExporter`(按年导出)承担。 **代码位置**:`baqing-admin` → `com.ruoyi.web.modules.industryservice`(与产业数据模型同包)。 **模板文件**:`baqing-admin/src/main/resources/templates/yak_herd_inventory_import_template.xlsx`(导入、导出、下载模板共用)。 --- ## 2. 数据库设计 ### 2.1 表 `biz_yak_herd_inventory`(牦牛存栏数据填报) | 字段 | 类型 | 非空 | 说明 | | --- | --- | --- | --- | | `id` | `bigint(20)` | Y | 主键 | | `stat_year` | `int(11)` | Y | 所属年份(自然年) | | `town_dept_id` | `bigint(20)` | Y | 所属乡镇,`sys_dept.dept_id` | | `town_name` | `varchar(64)` | Y | 乡镇名称冗余,便于列表展示 | | `village_dept_id` | `bigint(20)` | N | **预留**所属村居,`sys_dept.dept_id`(乡镇下三级节点) | | `village_name` | `varchar(128)` | N | **预留**所属村居名称冗余 | | `village_count` | `int(11)` | N | 村居数 | | `herdsman_household_count` | `int(11)` | N | 牧户数 | | `yak_total` | `int(11)` | N | 牦牛存栏数 | | `bull_total` | `int(11)` | N | 公牛存栏数 | | `cow_total` | `int(11)` | N | 母牛存栏数 | | `breeding_bull_count` | `int(11)` | N | 种公牛存栏数 | | `bull_castrated_count` | `int(11)` | N | 公牛/阉畜存栏数 | | `cow_under_1_count` | `int(11)` | N | 1 岁以下母牛存栏数 | | `cow_1_3_count` | `int(11)` | N | 1~3 岁母牛存栏数 | | `cow_3_12_count` | `int(11)` | N | 3~12 岁母牛存栏数 | | `cow_over_12_count` | `int(11)` | N | 12 岁以上母牛存栏数 | | `create_by` / `create_time` | — | — | 审计 | | `update_by` / `update_time` | — | — | 审计 | | `remark` | `varchar(500)` | N | 备注 | **说明**:本表**不使用逻辑删除**,无 `del_flag` 字段;删除操作为物理删除。 **索引与约束** - `UNIQUE uk_year_town (stat_year, town_dept_id)` — 年份+乡镇唯一,支撑导入 upsert。 - `KEY idx_stat_year (stat_year)` - `KEY idx_town_dept_id (town_dept_id)` **DDL 文件**:`sql/biz_yak_herd_inventory.sql` ### 2.2 乡镇字典查询(`sys_dept`) ```sql SELECT dept_id, dept_name, order_num FROM sys_dept WHERE parent_id = 100 AND order_num > 12 AND del_flag = '0' AND status = '0' ORDER BY order_num; ``` 前端乡镇下拉、导入匹配均调用封装方法 `TownDeptSupport.listReportTowns()`(名称与根 `parent_id` 可配置化 `ruoyi.town-dept-parent-id=100`、`ruoyi.town-dept-min-order-num=12`)。 ### 2.3 可选年份 ```java // 当前年起向前 5 年,共 6 个 int current = Year.now().getValue(); for (int y = current; y >= current - 5; y--) { ... } ``` --- ## 3. 接口设计 **Base Path**:`/dataModel/yakHerdInventory` **权限前缀**:`dataModel:yakHerdInventory` | # | 说明 | Method | URI | 权限 | | --- | --- | --- | --- | --- | | 3.1 | 分页列表 | GET | `/list` | `list` | | 3.2 | 详情 | GET | `/{id}` | `query` | | 3.3 | 新增 | POST | `/` | `add` | | 3.4 | 修改 | PUT | `/` | `edit` | | 3.5 | 删除 | DELETE | `/{ids}` | `remove` | | 3.6 | 乡镇下拉 | GET | `/townOptions` | `list` | | 3.7 | 年份下拉 | GET | `/yearOptions` | `list` | | 3.8 | 导入 | POST | `/importData` | `import` | | 3.9 | 下载导入模板 | GET | `/importTemplate` | `import` | | 3.10 | 按年份导出 | GET | `/export` | `export` | #### 3.1.1 列表 Query | 参数 | 类型 | 说明 | | --- | --- | --- | | `pageNum` / `pageSize` | int | 默认 1 / 20 | | `statYear` | int | 所属年份,精确 | | `townDeptId` | long | 所属乡镇 dept_id,精确 | **列表 VO 字段**(列标题与需求 **§5.1** 一致): | 列标题 | 字段 | | --- | --- | | 所属年份 | `statYear` | | 乡镇 | `townName` | | 村居数 | `villageCount` | | 牧户数 | `herdsmanHouseholdCount` | | 存栏数 | `yakTotal` | | 公牛存栏数 | `bullTotal` | | 母牛存栏数 | `cowTotal` | #### 3.2.1 详情 返回 `BizYakHerdInventory` **全字段**(驼峰),含预留村居、分龄母牛、种公牛、阉畜、备注及 `createBy` / `createTime` / `updateBy` / `updateTime` 等。 #### 3.3.1 新增/修改 Body | 字段 | 必填 | 校验 | | --- | --- | --- | | `statYear` | Y | 在 `yearOptions` 范围内 | | `townDeptId` | Y | 须在乡镇字典内 | | `villageDeptId` | N | **预留**;若填写须为有效 `sys_dept.dept_id`,且 `parent_id = townDeptId`(可选校验) | | `villageName` | N | **预留**村居名称 | | 各 `*Count` | N | `>= 0` 整数 | | `id` | 修改必填 | — | **唯一性**:`countByYearAndTownExcludeId(statYear, townDeptId, excludeId) > 0` 则拒绝(**手工新增/修改**);**导入**走 upsert,不受此限。 #### 3.8 导入 **请求**:`multipart/form-data` | 参数 | 必填 | 说明 | | --- | --- | --- | | `file` | Y | `.xlsx`(如 `牦牛存栏数据.xlsx`) | | `statYear` | Y | 填报年份(所属年份) | **Upsert 规则**(`BizYakHerdInventoryServiceImpl.upsertImportRow`): | 步骤 | 说明 | | --- | --- | | 1 | 按 `statYear` + `townDeptId` 查询 `selectByYearAndTown` | | 2 | 无记录 → `insert`;`insertCount++` | | 3 | 有记录 → `updateBizYakHerdInventory` 全字段覆盖;`updateCount++` | 同一文件对**同一 `statYear`** 多次导入:各乡镇行均为更新,不产生重复有效记录。 **响应 `data`** | 字段 | 说明 | | --- | --- | | `insertCount` | 新增条数 | | `updateCount` | 更新条数 | | `failCount` | 失败条数 | | `failMessages` | 如 `第8行[杂色镇]: 乡镇未识别` | | `warnMessages` | 分龄加总与母牛合计不一致等警告(可选) | #### 3.8.1 Excel 第一个 Sheet 解析约定(`2026年.xlsx`) | 项 | 约定 | | --- | --- | | Sheet | `workbook.getSheetAt(0)` | | 标题行 | 前若干行为「巴青县…畜群结构…统计表」及填表说明,**自动扫描**首列含「序号」且相邻列含「乡镇」的行作为表头行 `headerRow` | | 数据行 | `headerRow + 1` 起至「乡镇」列为空 | | 列映射(默认,实施对照模板微调) | | | 列号(0-based) | Excel 列 | 实体字段 | | --- | --- | --- | | 1 | B | 乡镇(按 `dept_name` 解析 `townDeptId`) | | 2 | C | `villageCount` | | 3 | D | `herdsmanHouseholdCount` | | 4 | E | `yakTotal`(存栏数) | | 5 | F | 与 E 同值(牦牛公母分类合计) | | 6 | G | `bullTotal`(公牛(头)) | | 7 | H | `cowTotal`(牦牛公母分类 — 母牛(头)) | | 8 | I | 与 G 同值(公牛区块合计) | | 9 | J | `breedingBullCount` | | 10 | K | `bullCastratedCount` | | 11 | L | `cowTotal` | | 12 | M | `cowUnder1Count` | | 13 | N | `cow13Count` | | 14 | O | `cow312Count` | | 15 | P | `cowOver12Count` | | 16 | Q | `remark` | > 数据区起始行:`headerRow + 3`(表头行由「序号」「乡镇」自动识别)。模板存在合并单元格,列号以代码常量为准。 **导入事务**:逐乡镇独立处理,单乡镇失败不影响其余。 #### 3.10 按年份导出 **请求**:`GET /export?statYear=2026` | 参数 | 必填 | 说明 | | --- | --- | --- | | `statYear` | Y | 导出年份,须在 `yearOptions` 范围内 | **响应**:`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` 文件流;文件名 `{statYear}年牦牛存栏数据.xlsx`。 **实现**:`BizYakHerdInventoryServiceImpl.exportByYear` → `YakHerdInventoryExcelExporter.export`: 1. 加载 classpath 模板 `YakHerdInventoryExcelExporter.TEMPLATE_CLASSPATH` 2. 替换表头中的四位年份为 `statYear` 3. 查询 `stat_year = statYear` 的全部记录,按 `town_dept_id` 建 Map 4. 遍历 `TownDeptSupport.listReportTowns()`,逐乡镇写行;无数据则仅写乡镇名 5. 末行写「合计」,对已填报乡镇数值列求和 **与 3.9 区别**:`/importTemplate` 返回空白模板;`/export` 返回填充后的全县报表。 --- ## 4. 前端设计(要点) | 项 | 说明 | | --- | --- | | **路径** | `ruoyi-ui/src/views/dataModel/yakHerdInventory/index.vue` | | **API** | `ruoyi-ui/src/api/dataModel/yakHerdInventory.js` | | **列表** | 年份、乡镇筛选;列:所属年份、乡镇、村居数、牧户数、存栏数、公牛存栏数、母牛存栏数 | | **详情** | 抽屉/对话框只读展示全部字段 | | **表单** | 抽屉或对话框;年份/乡镇下拉;数字 `el-input-number` `:min="0"` `:precision="0"` | | **导入** | 先选「填报年份」→ 上传 → 展示导入结果摘要 | | **模板下载** | `GET /importTemplate`;空白模板 | | **按年份导出** | 列表先选「所属年份」→ `GET /export`;`download('dataModel/yakHerdInventory/export', { statYear })` | --- ## 5. 菜单与权限(示例) | 类型 | 名称 | 权限标识 | | --- | --- | --- | | 菜单 | 牦牛存栏数据填报 | `dataModel:yakHerdInventory:list` | | 按钮 | 查询 | `dataModel:yakHerdInventory:query` | | 按钮 | 新增 | `dataModel:yakHerdInventory:add` | | 按钮 | 修改 | `dataModel:yakHerdInventory:edit` | | 按钮 | 删除 | `dataModel:yakHerdInventory:remove` | | 按钮 | 导入 | `dataModel:yakHerdInventory:import` | | 按钮 | 导出 | `dataModel:yakHerdInventory:export` | 挂载目录:**产业数据模型及服务**。菜单 SQL 见 `sql/biz_sys_menu_yak_herd_inventory.sql`。 --- ## 6. 与养殖产业数据看板的关系 「养殖产业数据」看板(`doc/.../养殖产业数据`)当前以生物资产台账汇总为主;本表为**乡镇级手工/导入汇总**的权威填报源。后续若看板需展示「乡镇填报存栏」,可增加只读聚合接口读取 `biz_yak_herd_inventory` 指定 `stat_year` 全县加总,**本期不实现**。 --- ## 7. 交付清单 - [x] `sql/biz_yak_herd_inventory.sql` - [x] `BizYakHerdInventory` Domain / Mapper / XML / Service / Controller - [x] `YakHerdInventoryExcelParser` + `YakHerdInventoryExcelExporter` - [x] `TownDeptSupport` 乡镇字典 - [x] 前端列表/表单/导入/导出 - [x] 单元测试 + 导入/导出集成测试(`yak_herd_inventory_2026.xlsx`) --- ## 8. 修订记录 | 版本 | 说明 | | --- | --- | | 1.0 | 初稿:表结构、CRUD、导入、sys_dept 乡镇、年份前推 5 年 | | 1.1 | 增加预留字段 `village_dept_id`(非必填) | | 1.2 | 实现按年份导出全县乡镇 Excel;列映射与代码对齐;交付清单勾选 | | 1.3 | 导入 upsert:`selectByYearAndTown` + `updateBizYakHerdInventory`;支持重复导入 | | 1.4 | 取消逻辑删除:无 `del_flag`;`deleteBizYakHerdInventoryByIds` 物理删除;`uk_year_town` 唯一约束 |