# 养殖产业数据 — 技术方案

> 依据：同目录 `养殖产业数据功能需求.md`。本期为**只读看板**：全页指标由既有台账**聚合计算**，无业务台账 CRUD、无第三方同步接口。

---

## 1. 技术架构

| 项 | 说明 |
| --- | --- |
| **后端** | RuoYi **v3.9.2**（**springboot2** 分支）：JDK 8、Spring Boot 2.x、Spring MVC、MyBatis、Druid |
| **数据库** | MySQL **5.7.39**，InnoDB，`utf8mb4` |
| **前端** | 若依 Vue2；见 `养殖产业数据前端技术方案.md` |
| **时区** | `Asia/Shanghai`（自然年、统计日、预警时间范围） |

**分层**：`Controller` → `IBreedingIndustryDashboardService`（口径编排、数据权限）→ `BreedingIndustryStatsMapper`（聚合 SQL）→ 既有 `biz_*` 表。

**代码位置（建议）**：`baqing-admin` → `com.ruoyi.web.modules.industryservice`（`BreedingIndustryDashboardController`、`BreedingIndustryStatsMapper`、VO）。

| 场景 | 行为 |
| --- | --- |
| 看板加载/刷新 | 一次请求返回四块数据；`statDate` 取服务器当前日期 |
| 数据权限 | Service 注入用户可见 `pastureId` 列表；牦牛、预警按 **§2.2** 过滤；草场全县台账不按牧场过滤 |
| 月末存栏趋势 | 应用层按 1～当前月循环，每月按 **§2.2 月末存栏判定** 及需求 **§2.10** 计数（**非** `asset_status=1` 简单过滤） |
| 公/母牛 | 仅统计**当前存栏**（`asset_status=1`），与趋势图口径分离 |
| 性能 | 数据量大时启用 **§2.3** 可选快照表 + 短 TTL 缓存；本期可先实时聚合；月末 12 点目标 **≤3s**（1 万级档案，评审可调） |

**依赖模块（只读）**

| 表 | 模块 |
| --- | --- |
| `biz_pasture` | 牧场管理 |
| `biz_grassland` | 草场管理 |
| `biz_yak_asset` | 牦牛资产档案 |
| `biz_yak_disease_warning` | 牦牛疾病预警 |

---

## 2. 数据库设计

### 2.1 设计原则

本模块**不新增业务台账表**；指标均通过对上表 **SELECT 聚合** 得到，口径以功能需求 **§2** 为准。

### 2.2 数据源与字段映射

#### `biz_pasture`（牧场数量）

| 指标 | SQL 条件要点 |
| --- | --- |
| 牧场数量 | `del_flag='0'` AND `biz_status=1` AND `id IN (可见牧场ID集合)` |

#### `biz_grassland`（草场数量、面积、可用/使用中、退化占比）

| 指标 | SQL 条件要点 |
| --- | --- |
| 草场数量 | `del_flag='0'` |
| 草场面积 | `SUM(area_mu)`，`area_mu IS NOT NULL` |
| 可用草场 | `del_flag='0'` AND `degradation_level IN (1,2)`（1未退化 2轻度） |
| 使用中草场 | `del_flag='0'` AND (`utilization_type IN (2,3)` OR (`utilization_type=1` AND 统计日 NOT BETWEEN `grazing_ban_start_date` AND `grazing_ban_end_date`)) |
| 退化分组 | `GROUP BY degradation_level`（1～5） |

**枚举（与草场管理一致）**

| 字段 | 值 | 含义 |
| --- | --- | --- |
| `degradation_level` | 1～5 | 未退化、轻度、中度、重度、极重度 |
| `utilization_type` | 1/2/3 | 禁牧、轮牧、全年放牧 |

#### `biz_yak_asset`（存栏、出栏、性别、年龄、月趋势）

| 指标 | SQL 条件要点 |
| --- | --- |
| 当前存栏 | `del_flag='0'` AND `pasture_id IN (...)` AND `pasture_id IS NOT NULL` AND `asset_status=1` |
| 年出栏 | 同上牧场范围 AND `asset_status=4` AND `status_change_date` 在当年且 ≤ 统计日 AND `status_change_date IS NOT NULL` |
| 公牛/母牛 | 在存栏集合上 `gender='公'` / `gender='母'` |
| 未标注性别 | 存栏总量 − 公牛 − 母牛 |
| 年龄分档 | 在存栏集合上按 `age_months`（或 `birth_date` 计算月龄）分 0～6、7～12、≥13 |
| 月末存栏 | 对每条档案用 **§2.2 月末存栏判定**（见下），非仅 `asset_status=1` |

**`asset_status`**：`1` 正常 `2` 死淘 `3` 丢失 `4` 出栏。

**月末时点计入存栏（单头判定，记月末日为 `M`）**

```text
计入 ⇔ del_flag='0'
      AND pasture_id IN (可见牧场)
      AND pasture_id IS NOT NULL
      AND NOT (asset_status IN (2,3,4) AND status_change_date IS NOT NULL AND status_change_date <= M)
      AND asset_status 可识别（1 或 2/3/4 且 change_date > M）
```

当前月：`M` 替换为 **统计日**。

#### `biz_yak_disease_warning`（预警牦牛数量）

```sql
-- 口径示意：当年 1/1 0:00 ～ 统计日 23:59:59，去重 yak_no，牦牛须在纳入集合内
SELECT COUNT(DISTINCT w.yak_no)
FROM biz_yak_disease_warning w
INNER JOIN biz_yak_asset y ON y.yak_no = w.yak_no AND y.del_flag = '0'
WHERE w.alert_time >= ? AND w.alert_time <= ?
  AND y.pasture_id IN (...)
  AND y.pasture_id IS NOT NULL;
```

### 2.3 可选表 `biz_breeding_dashboard_cache`（性能优化）

数据量较大或刷新慢时启用；**非本期必建**。

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `id` | `bigint(20)` | 主键 |
| `scope_key` | `varchar(64)` | 权限范围键，如 `all` 或 `user:{userId}` |
| `stat_date` | `date` | 统计日 |
| `payload_json` | `mediumtext` | 看板完整 JSON（与 **§3.2** `data` 结构一致） |
| `expire_time` | `datetime` | 过期时间 |
| `create_time` | `datetime` | 写入时间 |

**索引**：`UNIQUE uk_scope_stat (scope_key, stat_date)`。

刷新时：先查未过期缓存命中则直接返回；否则实时计算后 UPSERT。TTL 建议 5～15 分钟（可配置）。

### 2.4 索引建议（既有表）

若统计偏慢，确认已存在（`sql/biz_yak_asset.sql` 等已含大部分）：

| 表 | 建议索引 |
| --- | --- |
| `biz_yak_asset` | `(pasture_id, del_flag, asset_status)`、`status_change_date` |
| `biz_yak_disease_warning` | `(alert_time)`、`(yak_no, alert_time)` |
| `biz_grassland` | `(del_flag, degradation_level)`、`(del_flag, utilization_type)` |
| `biz_pasture` | `(del_flag, biz_status)` |

### 2.5 可选缓存表 DDL

```sql
CREATE TABLE IF NOT EXISTS `biz_breeding_dashboard_cache` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `scope_key` varchar(64) NOT NULL COMMENT '权限范围键',
  `stat_date` date NOT NULL COMMENT '统计日',
  `payload_json` mediumtext NOT NULL COMMENT '看板JSON',
  `expire_time` datetime NOT NULL COMMENT '过期时间',
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_scope_stat` (`scope_key`,`stat_date`),
  KEY `idx_expire_time` (`expire_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='养殖产业看板缓存(可选)';
```

### 2.6 数据权限实现

与「牦牛疾病预警」列表一致：聚合前解析**可见牧场 ID 集合** `allowedPastureIds`，传入 `BreedingIndustryStatsMapper` 的 `IN (...)` 条件（牦牛、预警、牧场数量）；**草场**统计不加牧场条件。

**建议类**：`com.ruoyi.web.modules.industryservice.support.BreedingIndustryPastureScope`（或复用后续统一的 `IndustryPastureScopeHelper`）。

| 步骤 | 说明 |
| --- | --- |
| 1 | 查询 `biz_pasture`：`del_flag='0'` AND `biz_status=1`，得到候选 ID 列表 |
| 2 | 按当前登录用户应用项目**数据权限**（部门/角色自定义范围，与预警 `allowedPastureIds` 同源）；县级全量角色得到**全部**候选 ID |
| 3 | 将结果写入统计上下文 `BreedingIndustryStatsContext.allowedPastureIds` |

**三种语义（实现须区分）**

| `allowedPastureIds` | 牦牛 / 预警 / 牧场数 | 草场 |
| --- | --- | --- |
| `null`（未限制，全量管理员） | 候选列表 = 步骤 1 全部正常牧场 ID | 全县 `del_flag=0` |
| 非空列表 | `pasture_id IN (...)` 且 `pasture_id IS NOT NULL` | 不变 |
| **空列表** `[]`（有权限框架但无可见牧场） | 牦牛类、预警、牧场数 **均为 0** | 仍全县统计 |

**Controller 示例**

```java
List<Long> allowed = pastureScope.resolveAllowedPastureIds();
return success(dashboardService.loadDashboard(allowed, forceRefresh));
```

**Mapper 约定**：与 `BizYakDiseaseWarningMapper.xml` 相同写法——仅当 `allowedPastureIds != null && size > 0` 时追加 `pasture_id in (...)`；全量管理员传**完整 ID 列表**（勿传 `null` 跳过牦牛过滤，以免误统计 `pasture_id` 为空的档案）。

**预警**：`biz_yak_disease_warning` 与 `biz_yak_asset` 按 `yak_no` 关联后，再限制 `y.pasture_id IN (...)`（见 **§2.2** SQL）。

---

## 3. 接口设计

**统一响应**：`AjaxResult`（`code` / `msg` / `data`）。

**权限**：`dataModel:breedingIndustry:query`（查看与刷新共用）。

**Base Path**：`/dataModel/breedingIndustry`

本期**仅 1 个**业务接口；**不提供** POST/PUT/DELETE 维护接口。

| # | 说明 | Method | URI | 权限 |
| --- | --- | --- | --- | --- |
| 3.1 | 看板全量数据 | GET | `/dashboard` | `query` |

前端进入页与点击「刷新」均调用 **3.1**。

#### 3.1.1 Query

| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `forceRefresh` | boolean | N | 默认 `false`；`true` 时跳过 **§2.3** 缓存 |

#### 3.1.2 响应 `data` 结构

| 节点 | 类型 | 说明 |
| --- | --- | --- |
| `statDate` | string | 统计日 `yyyy-MM-dd` |
| `statYear` | int | 当前自然年，如 `2026` |
| `overview` | object | 产业总览 **§3.1.3** |
| `inventoryChange` | object | 牦牛存栏变动 **§3.1.4** |
| `ageStructure` | array | 年龄结构 **§3.1.5** |
| `grassland` | object | 草场类型占比 **§3.1.6** |

#### 3.1.3 `overview`

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `inventoryTotal` | int | 牦牛存栏总量（头） |
| `annualOutbound` | int | 牦牛年出栏量（头） |
| `warningYakCount` | int | 预警牦牛数量（头） |
| `pastureCount` | int | 牧场数量（个） |
| `grasslandCount` | int | 草场数量（个） |
| `grasslandAreaMu` | decimal | 草场面积（亩） |

#### 3.1.4 `inventoryChange`

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `bullCount` | int | 公牛数量（头） |
| `cowCount` | int | 母牛数量（头） |
| `unknownGenderCount` | int | 未标注性别（头）；为 0 时前端可不展示 |
| `monthlyTrend` | array | 按月存栏，仅含 1～当前月 |

`monthlyTrend[]` 元素：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `month` | int | 1～12 |
| `inventoryCount` | int | 该月还原存栏总量（头） |

#### 3.1.5 `ageStructure[]`

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `bandCode` | string | `M1_6` / `M7_12` / `M13_PLUS` |
| `bandLabel` | string | 展示名：`1-6月龄` 等 |
| `count` | int | 头数 |
| `ratio` | decimal | 占比 0～1 或前端转 %（约定返回 **0～100** 数值，保留 1 位小数，如 `37.5` 表示 37.5%） |

三档固定返回（count 为 0 也返回）。

#### 3.1.6 `grassland`

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `availableCount` | int | 可用草场（个） |
| `inUseCount` | int | 使用中草场（个） |
| `degradationStats` | array | 退化等级统计 |

`degradationStats[]` 元素：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `degradationLevel` | int | 1～5 |
| `degradationLabel` | string | 未退化、轻度… |
| `count` | int | 个数 |
| `ratio` | decimal | 占比 %（同 `ageStructure`） |

五档固定返回；`count` 之和应等于 `overview.grasslandCount`。

#### 3.1.7 响应示例（节选）

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "statDate": "2026-05-20",
    "statYear": 2026,
    "overview": {
      "inventoryTotal": 120,
      "annualOutbound": 8,
      "warningYakCount": 5,
      "pastureCount": 12,
      "grasslandCount": 80,
      "grasslandAreaMu": 12500.50
    },
    "inventoryChange": {
      "bullCount": 45,
      "cowCount": 60,
      "unknownGenderCount": 15,
      "monthlyTrend": [
        { "month": 1, "inventoryCount": 110 },
        { "month": 5, "inventoryCount": 120 }
      ]
    },
    "ageStructure": [
      { "bandCode": "M1_6", "bandLabel": "1-6月龄", "count": 30, "ratio": 25.0 }
    ],
    "grassland": {
      "availableCount": 50,
      "inUseCount": 40,
      "degradationStats": [
        { "degradationLevel": 1, "degradationLabel": "未退化", "count": 30, "ratio": 37.5 }
      ]
    }
  }
}
```

#### 3.1.8 异常与校验

| 场景 | 处理 |
| --- | --- |
| 无查询权限 | 403 或 `AjaxResult.error` 无权限 |
| 聚合异常 | `AjaxResult.error("加载失败，请稍后重试")`，`data` 为空 |
| 可见牧场为空 | 牦牛类指标均为 0；草场类仍按全县统计 |
| 刷新中重复点击 | 前端防重；后端可选短锁（与资产同步类似） |

---

## 4. 实现要点（摘要）

| 项 | 说明 |
| --- | --- |
| **月龄** | `COALESCE(age_months, TIMESTAMPDIFF(MONTH, birth_date, statDate))`，`birth_date` 空则 0 |
| **性别** | 入库值 `trim` 后精确匹配 `公`/`母`；第三方若写「公牛」「母牛」等，Service 层归一后再统计 |
| **公母 vs 趋势** | `bullCount`/`cowCount`/`inventoryTotal`/`ageStructure` 用 **§2.3**（`asset_status=1`）；`monthlyTrend` 用 **§2.10** 月末判定 |
| **一致性** | `monthlyTrend` 最后一月 `inventoryCount` 应等于 `overview.inventoryTotal`（同统计日）；实现后 UT 断言 |
| **比率** | `ratio = count * 100.0 / NULLIF(total, 0)`，分母为 0 时 ratio=0 |
| **月末性能** | 优先 1 条 SQL + `CASE WHEN` 按月末分组，或 Java 循环 12 次；超过 3s 启用 **§2.3** 缓存 |
| **日志** | 看板接口可 `@Log(title="养殖产业数据", businessType=OTHER)` 或仅记录慢查询 |

**前端**：见同目录 `养殖产业数据前端技术方案.md`。

---

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

| 类型 | 名称 | 权限标识 |
| --- | --- | --- |
| 目录/菜单 | 养殖产业数据 | `dataModel:breedingIndustry:query` |
| 按钮 | 刷新（与查看共用） | `dataModel:breedingIndustry:query` |

组件路径：`livestockIndustry/breedingIndustry/index`（挂载「产业数据模型」或同级目录，与菜单配置一致）。

**`sys_menu` 示例（`menu_id` 按环境递增）**

```sql
-- 父菜单 parent_id 替换为「产业数据模型」实际 ID
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
VALUES ('养殖产业数据', {parentId}, 10, 'breedingIndustry', 'livestockIndustry/breedingIndustry/index', 1, 0, 'C', '0', '0', 'dataModel:breedingIndustry:query', 'chart', 'admin', sysdate(), '养殖产业看板');
```

本期**无** `add`/`edit`/`remove`/`sync` 按钮权限。

---

## 6. 需求追溯

| 功能需求 | 技术落点 |
| --- | --- |
| §5.2 产业总览 | `overview` + **§2.2** SQL |
| §5.3 存栏变动 | `inventoryChange` + **§2.2** 月末判定 |
| §5.4 年龄结构 | `ageStructure` |
| §5.5 草场占比 | `grassland` |
| §5.1 刷新 | **3.1** `forceRefresh` |
| §2.9 数据权限 | **§2.6** `BreedingIndustryPastureScope` |
| §2.10 月末趋势 | **§2.2** 月末判定 + `monthlyTrend` |
| §2.5 预警 | **§2.2** warning SQL |
