# 牦牛存栏数据填报 — 技术方案

> 依据：同目录 `牦牛存栏数据填报功能需求.md`；模板 `baqing-admin/src/main/resources/templates/yak_herd_inventory_import_template.xlsx`。

---

## 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 + `@riophae/vue-treeselect` |
| **Excel** | Apache POI |
| **组织** | `sys_dept` 乡镇/村字典；`TownDeptSupport` |

**分层**：`BizYakHerdInventoryController` → `IBizYakHerdInventoryService` → `BizYakHerdInventoryMapper` → `domain`；Excel 由 `YakHerdInventoryExcelParser`（导入）、`YakHerdInventoryExcelExporter`（导出）承担；校验与派生计算由 `YakHerdInventoryValidation`。

**代码位置**：`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 | 所属年份 |
| `stat_month` | `int(11)` | Y | 所属月份（1～12） |
| `town_dept_id` | `bigint(20)` | Y | 所属乡镇，`sys_dept.dept_id`（由村反推） |
| `town_name` | `varchar(64)` | Y | 乡镇名称冗余 |
| `village_dept_id` | `bigint(20)` | Y | 所属村，`sys_dept.dept_id` |
| `village_name` | `varchar(128)` | Y | 村名称冗余 |
| `village_count` | `int(11)` | N | 村居数 |
| `herdsman_household_count` | `int(11)` | N | 牧户数 |
| `breeding_bull_count` | `int(11)` | N | 种公牛 |
| `bull_castrated_count` | `int(11)` | N | 公牛/阉畜 |
| `bull_total` | `int(11)` | N | 公牛合计（派生） |
| `cow_young_count` | `int(11)` | N | 0～1.5 岁幼畜（不含 1.5 岁） |
| `cow_adult_count` | `int(11)` | N | 1.5 岁以上成畜（含 1.5 岁） |
| `cow_total` | `int(11)` | N | 母牛合计（派生） |
| `milking_cow_count` | `int(11)` | N | 产奶母牛 |
| `fertile_cow_count` | `int(11)` | N | 能繁母牛 |
| `dairy_total` | `int(11)` | N | 奶牛合计（派生） |
| `sheep_total` | `int(11)` | N | 羊总数 |
| `milking_sheep_count` | `int(11)` | N | 产奶羊 |
| `insured_yak_count` | `int(11)` | N | 保险牦牛 |
| `yak_total` | `int(11)` | N | 牦牛总存栏（派生） |
| `grand_total` | `int(11)` | N | 总合计（派生） |
| `yak_sheep_total` | `int(11)` | N | 总量牦牛+羊（派生） |
| `create_by` / `create_time` | — | — | 审计 |
| `update_by` / `update_time` | — | — | 审计 |
| `remark` | `varchar(500)` | N | 备注 |

**说明**：本表**不使用逻辑删除**；删除为物理删除。旧四档母牛年龄字段（`cow_under_1_count` 等）已废弃，以 `cow_young_count` / `cow_adult_count` 为准。

**索引与约束**

- `UNIQUE uk_year_month_village (stat_year, stat_month, village_dept_id)`  
- `KEY idx_stat_year_month (stat_year, stat_month)`  
- `KEY idx_town_dept_id (town_dept_id)`  
- `KEY idx_village_dept_id (village_dept_id)`

**DDL 文件**：`sql/biz_yak_herd_inventory.sql`（全量覆盖建表）。已有库若仍为乡镇唯一键，执行 `sql/biz_yak_herd_inventory_village_uk.sql`（去掉 `uk_year_month_town`，改为村唯一）。

### 2.2 派生计算（`YakHerdInventoryValidation.recalculateDerived`）

```text
bull_total      = breeding_bull_count + bull_castrated_count
cow_total       = cow_young_count + cow_adult_count
dairy_total     = milking_cow_count + fertile_cow_count
yak_total       = bull_total + cow_total + insured_yak_count
grand_total     = yak_total + dairy_total + sheep_total
yak_sheep_total = bull_total + cow_total + insured_yak_count + sheep_total
```

保存与导入落库前一律重算覆盖。

### 2.3 乡镇 / 村字典

- 乡镇：`TownDeptSupport.listReportTowns()`（`parent_id=100` 且 `order_num>12`）。  
- 村树：`TownDeptSupport.buildVillageTree()` → 县→乡镇→村；父节点不 `disabled`（便于展开），前端 `disable-branch-nodes` 仅允许选村。  
- 解析：`resolveTownByName`、`resolveVillageByTownAndName`、`requireReportVillage`。

### 2.4 可选年份 / 月份

- `YakHerdInventoryYearSupport`：当前年起向前 5 年，共 6 个。  
- `YakHerdInventoryMonthSupport.resolveMonthOptions()`：1～12；`validateStatPeriod` 拒绝未来月。

---

## 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.6.1 | 村树 | GET | `/villageTree` | `list` |
| 3.7 | 年份下拉 | GET | `/yearOptions` | `list` |
| 3.7.1 | 月份下拉 | GET | `/monthOptions` | `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` / `statMonth` | int | 精确 |
| `townDeptId` | long | 乡镇 |
| `villageDeptId` | long | 村 |

**列表主要字段**：`statYear`、`statMonth`、`townName`、`villageName`、`villageCount`、`herdsmanHouseholdCount`、`yakTotal`、`bullTotal`、`cowTotal`。

#### 3.3.1 新增/修改 Body

| 字段 | 必填 | 校验 |
| --- | --- | --- |
| `statYear` / `statMonth` | Y | 合法且非未来月 |
| `villageDeptId` | Y | 合法村；反推乡镇 |
| 明细 `*Count` | N | `>= 0`；**产奶羊与羊总均非空时**：`milkingSheepCount < sheepTotal` |
| `id` | 修改必填 | — |

**唯一性**：`countByYearMonthAndVillageExcludeId(...) > 0` →「该年该月该村已填报」。

#### 3.8 导入

**请求**：`multipart/form-data`：`file` + `statYear` + `statMonth`。

**策略**（`BizYakHerdInventoryServiceImpl.importData`）：

1. 解析全部有效数据行；任一行乡镇/村无法匹配或校验失败 → 整体失败，不写库。  
2. 全部成功 → 按 `(statYear, statMonth, villageDeptId)` 查重，存在 `update`、不存在 `insert`。  
3. Excel 合计列与重算不一致 → 记入 `warnMessages`（非阻断）。

**响应**：`insertCount`、`failCount`、`failMessages`、`warnMessages`。

#### 3.8.1 Excel 列映射（0-based，`YakHerdInventoryExcelParser`）

| 列号 | Excel | 用途 |
| --- | --- | --- |
| 1 | B 乡镇 | 匹配乡镇 |
| 2 | C 村 | 匹配村 |
| 3 | D 村居 | `villageCount` |
| 4 | E 牧户 | `herdsmanHouseholdCount` |
| 5 | F 总合计 | Excel 对照 |
| 6～8 | 牦牛公母分类 | Excel 对照 |
| 9 | J 保险 | `insuredYakCount` |
| 10 | K 公牛合计 | Excel 对照 |
| 11 | L 种公牛 | `breedingBullCount` |
| 12 | M 阉畜 | `bullCastratedCount` |
| 13 | N 母牛合计 | Excel 对照 |
| 14 | O 幼畜 | `cowYoungCount` |
| 15 | P 成畜 | `cowAdultCount` |
| 16 | Q 奶牛合计 | Excel 对照 |
| 17 | R 产奶母牛 | `milkingCowCount` |
| 18 | S 能繁母牛 | `fertileCowCount` |
| 19 | T 羊 | `sheepTotal` |
| 20 | U 产奶羊 | `milkingSheepCount` |
| 21 | V 总量 | Excel 对照 |

数据起始行：`headerRow + 3`（三行表头）。

#### 3.10 按年月导出

**请求**：`GET /export?statYear=&statMonth=`。

**实现**：查询该年月全部村记录 → `YakHerdInventoryExcelExporter.export`：

1. 替换标题年、月；填表人；填表时间=**导出当日年月**  
2. 按记录写数据行（含派生合计）  
3. 末行「合计」：合并列 0～2；自列 3（村居）起对各数值列求和写入  
4. 删除合计行之后的空行  

---

## 4. 前端设计（要点）

| 项 | 说明 |
| --- | --- |
| **路径** | `ruoyi-ui/src/views/dataModel/yakHerdInventory/index.vue` |
| **API** | `ruoyi-ui/src/api/dataModel/yakHerdInventory.js` |
| **列表** | 年/月/乡镇/村筛选；列含乡镇、村、存栏等 |
| **表单** | 年/月 + Treeselect 选村；仅明细录入字段；无派生合计输入 |
| **详情** | 含派生合计只读 |
| **导入/导出** | 先选年月 |

---

## 5. 菜单与权限（示例）

同权限标识：`list|query|add|edit|remove|import|export`。菜单 SQL 见 `sql/biz_sys_menu_yak_herd_inventory.sql`。

---

## 6. 与养殖产业数据看板 / 大屏的关系

| 场景 | 口径 |
| --- | --- |
| 全县存栏合计 | `biz_yak_herd_inventory` **最近填报年月**全县 `yak_total` / `bull_total` / `cow_total` 等合计（按村汇总） |
| 年龄结构 | 两档：`cow_young_count`、`cow_adult_count`；`bandCode`=`COW_YOUNG` / `COW_ADULT` |
| 地图乡镇存栏 | 按乡镇聚合该最近年月村记录 |

---

## 7. 交付清单

- [x] `sql/biz_yak_herd_inventory.sql`、`sql/biz_yak_herd_inventory_village_uk.sql`  
- [x] Domain / Mapper / Service / Controller  
- [x] Parser / Exporter / Validation / TownDeptSupport 村树  
- [x] 前端列表/表单（Treeselect）/导入/导出  
- [x] 单元测试与 Excel 样例  

---

## 8. 修订记录

| 版本 | 说明 |
| --- | --- |
| 1.0～1.5 | 乡镇粒度、按年月、四档母牛年龄等历史版本 |
| 1.6 | 村粒度 `uk_year_month_village`；新模板字段与派生合计；导入全量覆盖；导出合计按列求和；`/villageTree`；大屏年龄两档 |
