# 牦牛出栏数据填报 — 技术方案 > 依据:同目录 `牦牛出栏数据填报功能需求.md`;模板 `E:\luhao\工作交接\巴青项目\农牧数据\农业农村和科技水利局-业务办\牲畜数据.xlsx` 第 **6** 个 Sheet(2026 年度出栏及自食统计表)。 --- ## 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(与「牦牛存栏数据填报」一致) | | **组织** | `sys_dept` 乡镇字典;复用 `TownDeptSupport` | **分层**:`BizYakOutboundReportController` → `IBizYakOutboundReportService` → `BizYakOutboundReportMapper` → `domain`;Excel 由 `YakOutboundReportExcelParser`(导入解析)、`YakOutboundReportExcelExporter`(按年导出)承担。 **代码位置**:`baqing-admin` → `com.ruoyi.web.modules.industryservice`(与产业数据模型同包)。 **模板文件**:`baqing-admin/src/main/resources/templates/yak_outbound_report_import_template.xlsx`(导入、导出、下载模板共用)。 **参考实现**:`BizYakHerdInventory*`、`YakHerdInventoryExcelParser`、`TownDeptSupport`(存栏填报模块,模式一致)。 --- ## 2. 数据库设计 ### 2.1 表 `biz_yak_outbound_report`(牦牛出栏数据填报) | 字段 | 类型 | 非空 | 说明 | | --- | --- | --- | --- | | `id` | `bigint(20)` | Y | 主键 | | `stat_year` | `int(11)` | Y | 所属年份(自然年) | | `town_dept_id` | `bigint(20)` | Y | 所属乡镇,`sys_dept.dept_id` | | `town_name` | `varchar(64)` | Y | 乡镇名称冗余 | | `farmer_household_count` | `int(11)` | N | 农牧户户数 | | `farmer_population_count` | `int(11)` | N | 农牧户人数 | | `yak_outbound_count` | `int(11)` | N | 牦牛出栏数(头) | | `cattle_outbound_count` | `int(11)` | N | 黄牛/奶牛/犏牛出栏数(头) | | `sheep_outbound_count` | `int(11)` | N | 绵羊出栏数(只) | | `goat_outbound_count` | `int(11)` | N | 山羊出栏数(只) | | `horse_mule_outbound_count` | `int(11)` | N | 马骡出栏数(匹) | | `donkey_outbound_count` | `int(11)` | N | 驴出栏数(头) | | `self_consumption_cattle_count` | `int(11)` | N | 自食牛头数(头) | | `self_consumption_sheep_count` | `int(11)` | N | 自食绵羊只数(只) | | `self_consumption_goat_count` | `int(11)` | N | 自食山羊只数(只) | | `create_by` / `create_time` | — | — | 审计 | | `update_by` / `update_time` | — | — | 审计 | | `remark` | `varchar(500)` | N | 备注 | **说明**:本表**不使用逻辑删除**,无 `del_flag`;删除为物理 `DELETE`。 **索引与约束** - `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_outbound_report.sql` ### 2.2 乡镇字典(复用) ```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()`、`requireReportTown(deptId)`。 **乡镇别名(导入)**:模板「扎色镇」→ `sys_dept`「杂色镇」,在 `YakOutboundReportExcelParser.resolveTownDeptId` 或 `TownDeptSupport` 扩展 `normalizeTownName`。 ### 2.3 可选年份 与存栏模块一致:`YakOutboundReportYearSupport` 或复用 `YakHerdInventoryYearSupport` 逻辑,返回当前年起向前 5 年共 6 个年份。 --- ## 3. 接口设计 **Base Path**:`/dataModel/yakOutboundReport` **权限前缀**:`dataModel:yakOutboundReport` | # | 说明 | 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** | 列标题 | 字段 | | --- | --- | | 所属年份 | `statYear` | | 所属乡镇 | `townName` | | 农牧户户数 | `farmerHouseholdCount` | | 农牧户人数 | `farmerPopulationCount` | | 牦牛出栏数 | `yakOutboundCount` | | 自食情况(牛) | `selfConsumptionCattleCount` | #### 3.2.1 详情 返回需求 **§5.5** 指定字段(驼峰): `statYear`、`townName`、`farmerHouseholdCount`、`farmerPopulationCount`、`yakOutboundCount`、`selfConsumptionCattleCount` 可选附带 `id`、`townDeptId` 供前端跳转修改。 #### 3.3.1 新增/修改 Body | 字段 | 必填 | 校验 | | --- | --- | --- | | `statYear` | Y | 在 `yearOptions` 范围内 | | `townDeptId` | Y | 须在乡镇字典内 | | `farmerHouseholdCount` 等数量字段 | N | `>= 0` 整数 | | `id` | 修改必填 | — | **唯一性(手工新增)**:`countByYearAndTownExcludeId > 0` 则拒绝。**导入**走 upsert。 #### 3.5.1 删除 `DELETE FROM biz_yak_outbound_report WHERE id IN (...)`,物理删除。 #### 3.8 导入 **请求**:`multipart/form-data` | 参数 | 必填 | 说明 | | --- | --- | --- | | `file` | Y | `.xlsx` | | `statYear` | Y | 填报年份 | **Upsert**(`BizYakOutboundReportServiceImpl.upsertImportRow`): 1. `selectByYearAndTown(statYear, townDeptId)` 2. 无记录 → `insert`;有记录 → `update` 全字段覆盖 **响应 `data`**:`insertCount`、`updateCount`、`failCount`、`failMessages`、`warnMessages`(自食大于出栏等) #### 3.8.1 Excel 第 6 Sheet 解析约定 | 项 | 约定 | | --- | --- | | Sheet | `workbook.getSheetAt(5)`(第 6 个 Sheet,0-based 索引 5) | | 表头识别 | 扫描含「乡镇」且同行含「户数」「牦牛」的行;数据自 `headerRow + 1` 起 | | 终止行 | 乡镇为空,或乡镇名为「合计」 | | 列映射(0-based) | | | 列号 | 实体字段 | | --- | --- | | 1 | 乡镇 → `townDeptId` / `townName` | | 2 | `farmerHouseholdCount` | | 3 | `farmerPopulationCount` | | 4 | `yakOutboundCount` | | 5 | `cattleOutboundCount` | | 6 | `sheepOutboundCount` | | 7 | `goatOutboundCount` | | 8 | `horseMuleOutboundCount` | | 9 | `donkeyOutboundCount` | | 10 | **跳过**(出栏小计) | | 11 | `selfConsumptionCattleCount` | | 12 | `selfConsumptionSheepCount` | | 13 | `selfConsumptionGoatCount` | | 14 | **跳过**(自食小计) | | 15 | `remark` | > 模板存在合并单元格,部分行 POI 读列可能错位;解析器宜按表头行固定列号读取,并对空单元格按 0 或 null 处理(与存栏导入一致)。 **导入事务**:逐乡镇独立处理,单乡镇失败不影响其余。 #### 3.10 按年份导出 **请求**:`GET /export?statYear=2026` | 参数 | 必填 | 说明 | | --- | --- | --- | | `statYear` | Y | 导出年份,须在 `yearOptions` 范围内 | **响应**:`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` 文件流;文件名 `{statYear}年牦牛出栏数据.xlsx`。 **实现**:`BizYakOutboundReportServiceImpl.exportByYear` → `YakOutboundReportExcelExporter.export`: 1. 加载 classpath 模板 `YakOutboundReportExcelExporter.TEMPLATE_CLASSPATH` 2. 解析出栏 Sheet(`YakOutboundReportExcelParser.resolveSheet`) 3. 替换表头中的四位年份为 `statYear`(含标题行与「年牲畜出栏数」表头区块;年份与「年」之间允许空格) 4. 将 Sheet 名称更新为 `{年份后两位}年出栏`(如 2024 → `24年出栏`) 5. 查询 `stat_year = statYear` 的全部记录,按 `town_dept_id` 建 Map 6. 遍历 `TownDeptSupport.listReportTowns()`,逐乡镇写行;无数据则仅写乡镇名 7. 末行写「合计」,对已填报乡镇数值列求和;K/O 列写入出栏/自食小计 **与 3.9 区别**:`/importTemplate` 返回空白模板;`/export` 返回填充后的全县报表。 --- ## 4. 前端设计(要点) | 项 | 说明 | | --- | --- | | **路径** | `ruoyi-ui/src/views/dataModel/yakOutboundReport/index.vue` | | **API** | `ruoyi-ui/src/api/dataModel/yakOutboundReport.js` | | **列表** | 年份、乡镇筛选;列:所属年份、所属乡镇、农牧户户数、农牧户人数、牦牛出栏数、自食情况(牛) | | **详情** | 对话框只读,展示 **§5.5** 六字段 | | **表单** | 新增/修改展示全部数量字段;年份/乡镇下拉;`el-input-number` `:min="0"` `:precision="0"` | | **导入** | 先选「填报年份」→ 上传 → 展示导入结果摘要 | | **模板下载** | `GET /importTemplate`;空白模板 | | **按年份导出** | 列表先选「所属年份」→ `GET /export`;`download('dataModel/yakOutboundReport/export', { statYear })` | --- ## 5. 菜单与权限(示例) | 类型 | 名称 | 权限标识 | | --- | --- | --- | | 菜单 | 牦牛出栏数据填报 | `dataModel:yakOutboundReport:list` | | 按钮 | 查询 | `dataModel:yakOutboundReport:query` | | 按钮 | 新增 | `dataModel:yakOutboundReport:add` | | 按钮 | 修改 | `dataModel:yakOutboundReport:edit` | | 按钮 | 删除 | `dataModel:yakOutboundReport:remove` | | 按钮 | 导入 | `dataModel:yakOutboundReport:import` | | 按钮 | 导出 | `dataModel:yakOutboundReport:export` | 挂载目录:**产业数据模型及服务**。菜单 SQL:`sql/biz_sys_menu_yak_outbound_report.sql`(实施阶段)。 --- ## 6. 与存栏填报模块的关系 | 模块 | 表 | 模板 Sheet | 业务含义 | | --- | --- | --- | --- | | 牦牛存栏数据填报 | `biz_yak_herd_inventory` | `牲畜数据.xlsx` 第 1 Sheet(畜群结构) | 年末存栏结构 | | 牦牛出栏数据填报 | `biz_yak_outbound_report` | `牲畜数据.xlsx` 第 6 Sheet | 年度出栏及自食 | 两模块共用 `TownDeptSupport`、年份范围规则、导入 upsert 模式;**本期不实现**跨表校验(如出栏数不大于存栏数)。 --- ## 7. 交付清单 - [x] `sql/biz_yak_outbound_report.sql` - [x] `BizYakOutboundReport` Domain / Mapper / XML / Service / Controller - [x] `YakOutboundReportExcelParser` + `YakOutboundReportExcelExporter` + 导入模板 xlsx(第 6 Sheet) - [x] 复用 `TownDeptSupport`、年份 Support - [ ] 前端列表/表单/导入/导出 - [x] 单元测试 + 接口测试 - [ ] `sql/biz_sys_menu_yak_outbound_report.sql` --- ## 8. 修订记录 | 版本 | 说明 | | --- | --- | | 1.0 | 初稿:表结构、CRUD、第 6 Sheet 导入、物理删除、乡镇字典 | | 1.1 | 实现按年份导出全县乡镇 Excel;`YakOutboundReportExcelExporter` |