# 牦牛出栏数据填报 — 技术方案 > 依据:同目录 `牦牛出栏数据填报功能需求.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*`、`YakHerdInventoryMonthSupport`、`TownDeptSupport`(存栏填报模块,年月模式一致)。 --- ## 2. 数据库设计 ### 2.1 表 `biz_yak_outbound_report`(牦牛出栏数据填报) | 字段 | 类型 | 非空 | 说明 | | --- | --- | --- | --- | | `id` | `bigint(20)` | Y | 主键 | | `stat_year` | `int(11)` | Y | 所属年份(自然年) | | `stat_month` | `int(11)` | Y | 所属月份(1~12) | | `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_month_town (stat_year, stat_month, town_dept_id)` — 年月+乡镇唯一,支撑导入 upsert。 - `KEY idx_stat_year_month (stat_year, stat_month)` - `KEY idx_town_dept_id (town_dept_id)` **DDL 文件**:`sql/biz_yak_outbound_report.sql`(新库,已含 `stat_month`)。 **已有库升级**(v1.1 按年表 `uk_year_town`):增加 `stat_month` 列(历史行默认 **12**),调整唯一键为 `uk_year_month_town`(实施阶段提供迁移 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 可选年份 与存栏模块一致:复用 `YakHerdInventoryYearSupport.resolveYearOptions()`,返回当前年起向前 5 年共 6 个年份。 ### 2.4 可选月份 复用 `YakHerdInventoryMonthSupport.resolveMonthOptions()` 返回 **1~12**;`validateStatPeriod(year, month)` 校验年份在 **§2.3** 范围内、月份合法且**非未来月**。出栏校验类 `YakOutboundReportValidation` 亦调用该 Support。 --- ## 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 | 月份下拉 | GET | `/monthOptions` | `list` | | 3.9 | 导入 | POST | `/importData` | `import` | | 3.10 | 下载导入模板 | GET | `/importTemplate` | `import` | | 3.11 | 按年月导出 | GET | `/export` | `export` | #### 3.1.1 列表 Query | 参数 | 类型 | 说明 | | --- | --- | --- | | `pageNum` / `pageSize` | int | 默认 1 / 20 | | `statYear` | int | 所属年份,精确 | | `statMonth` | int | 所属月份(1~12),精确 | | `townDeptId` | long | 所属乡镇 dept_id,精确 | **列表 VO** | 列标题 | 字段 | | --- | --- | | 所属年份 | `statYear` | | 所属月份 | `statMonth` | | 所属乡镇 | `townName` | | 农牧户户数 | `farmerHouseholdCount` | | 农牧户人数 | `farmerPopulationCount` | | 牦牛出栏数 | `yakOutboundCount` | | 自食情况(牛) | `selfConsumptionCattleCount` | #### 3.2.1 详情 返回需求 **§5.5** 指定字段(驼峰): `statYear`、`statMonth`、`townName`、`farmerHouseholdCount`、`farmerPopulationCount`、`yakOutboundCount`、`selfConsumptionCattleCount` 可选附带 `id`、`townDeptId` 供前端跳转修改。 #### 3.3.1 新增/修改 Body | 字段 | 必填 | 校验 | | --- | --- | --- | | `statYear` | Y | 在 `yearOptions` 范围内 | | `statMonth` | Y | 1~12;非未来月 | | `townDeptId` | Y | 须在乡镇字典内 | | `farmerHouseholdCount` 等数量字段 | N | `>= 0` 整数 | | `id` | 修改必填 | — | **唯一性(手工新增)**:`countByYearMonthAndTownExcludeId > 0` 则拒绝,提示「该年该月该乡镇已填报」。**导入**走 upsert。 #### 3.5.1 删除 `DELETE FROM biz_yak_outbound_report WHERE id IN (...)`,物理删除。 #### 3.9 导入 **请求**:`multipart/form-data` | 参数 | 必填 | 说明 | | --- | --- | --- | | `file` | Y | `.xlsx` | | `statYear` | Y | 填报年份 | | `statMonth` | Y | 填报月份(1~12) | **Upsert**(`BizYakOutboundReportServiceImpl.upsertImportRow`): 1. `selectByYearMonthAndTown(statYear, statMonth, townDeptId)` 2. 无记录 → `insert`;有记录 → `update` 全字段覆盖 同一文件对**同一填报年月**多次导入:各乡镇行均为更新,不产生重复有效记录。 **响应 `data`**:`insertCount`、`updateCount`、`failCount`、`failMessages`、`warnMessages`(自食大于出栏等) #### 3.9.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.11 按年月导出 **请求**:`GET /export?statYear=2026&statMonth=7` | 参数 | 必填 | 说明 | | --- | --- | --- | | `statYear` | Y | 导出年份,须在 `yearOptions` 范围内 | | `statMonth` | Y | 导出月份(1~12);须通过 `validateStatPeriod` | **响应**:`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` 文件流;文件名 `{statYear}年{statMonth}月牦牛出栏数据.xlsx`。 **实现**:`BizYakOutboundReportServiceImpl.exportByYearMonth` → `YakOutboundReportExcelExporter.export`: 1. 加载 classpath 模板 `YakOutboundReportExcelExporter.TEMPLATE_CLASSPATH` 2. 解析出栏 Sheet(`YakOutboundReportExcelParser.resolveSheet`) 3. 替换表头中的四位年份为 `statYear`(含标题行与「年牲畜出栏数」表头区块;年份与「年」之间允许空格) 4. 将 Sheet 名称更新为 `{年份后两位}年出栏`(如 2024 → `24年出栏`) 5. 查询 `stat_year = statYear AND stat_month = statMonth` 的全部记录,按 `town_dept_id` 建 Map 6. 遍历 `TownDeptSupport.listReportTowns()`,逐乡镇写行;无数据则仅写乡镇名 7. 末行写「合计」,对已填报乡镇数值列求和;K/O 列写入出栏/自食小计 **与 3.10 区别**:`/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?statYear=&statMonth=` | --- ## 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`、年份范围规则、`YakHerdInventoryMonthSupport` 年月校验、导入 upsert 模式;**本期不实现**跨表校验(如出栏数不大于存栏数)。 ### 6.1 与大屏 / 养殖看板(暂不改) 产业总览中「牦牛出栏数」「农牧户户数」等字段当前仍通过 `HomeScreenStatsMapper.sumOutboundReport*ByStatYear` **仅按 `stat_year` 汇总**,不按月份过滤。同一年存在多个月份填报时可能重复累加;**后续单独设计看板口径**,本模块按年月维护明细。 --- ## 7. 交付清单 - [x] `sql/biz_yak_outbound_report.sql`(含 `stat_month`) - [x] `BizYakOutboundReport` Domain / Mapper / XML / Service / Controller - [x] `YakOutboundReportExcelParser` + `YakOutboundReportExcelExporter` + 导入模板 xlsx(第 6 Sheet) - [x] 复用 `TownDeptSupport`、`YakHerdInventoryYearSupport`、`YakHerdInventoryMonthSupport` - [ ] 前端列表/表单/导入/导出(年月 UI,对齐存栏模块) - [x] 单元测试 + 接口测试 - [ ] `sql/biz_sys_menu_yak_outbound_report.sql` --- ## 8. 修订记录 | 版本 | 说明 | | --- | --- | | 1.0 | 初稿:表结构、CRUD、第 6 Sheet 导入、物理删除、乡镇字典 | | 1.1 | 实现按年份导出全县乡镇 Excel;`YakOutboundReportExcelExporter` | | 1.2 | 按年月填报:`stat_month`;`uk_year_month_town`;`/monthOptions`;导入/导出/CRUD 支持月份;复用 `YakHerdInventoryMonthSupport` |