# 牦牛资产档案管理 — 技术方案

> 依据：同目录 `牦牛资产档案管理功能需求.md`。档案 **列表/详情只读** + **第三方同步写入**；本期无手工增删改接口。

---

## 1. 技术架构

| 项 | 说明 |
| --- | --- |
| **后端** | RuoYi **v3.9.2**（**springboot2** 分支）：JDK 8、Spring Boot 2.x、Spring MVC、MyBatis、Druid |
| **数据库** | MySQL **5.7.39**，InnoDB，`utf8mb4` |
| **前端** | 若依 Vue2（本方案不展开） |
| **第三方** | HTTP/RPC 客户端（`RestTemplate` / `OkHttp` 等，实现期选型）；配置项：地址、鉴权、超时 |
| **关联主数据** | `biz_pasture`（牧场，可选 `pasture_id` 映射） |

**分层**：`Controller` → `LivestockFinanceMasterSyncService`（全量编排）→ 各 `*SyncService` / `*SyncTxService` → `Mapper`/XML → `domain`；IoT 数据由 `IotDataIncrementalSyncTask` 定时触发，**不**经列表页「同步」按钮。

**代码位置**：`baqing-admin` → `com.ruoyi.web.modules.industryservice`。

**业务摘要**

| 场景 | 行为 |
| --- | --- |
| 同步（手动） | `POST /dataModel/yakAsset/sync` 触发**五段**基础全量：养殖场→圈舍→生产计划→批次→入栏建档；**不含** IoT |
| IoT 同步（定时） | 每日 **凌晨 1:00** Quartz 任务；称重/定位项圈/环境监测；见 **§3.9** |
| 列表/详情 | `del_flag='0'`；排序 `status_change_date DESC, id DESC`（或与 `last_sync_time` 统一） |
| 月龄 | 列表/详情可展示库字段；同步时可写入，亦可用 `birth_date` 现场计算 |
| 删除 | 第三方下架策略**默认不物理删除**：仅更新状态或 `del_flag`（若第三方提供「离场」状态则映射为已出售/死亡等）；全量对账删除须评审 |

---

## 2. 数据库设计

### 2.1 主表 `biz_yak_asset`（牦牛资产档案）

| 字段 | 类型 | 非空 | 说明 |
| --- | --- | --- | --- |
| `id` | `bigint(20)` | Y | 主键 |
| `external_id` | `bigint(20)` | N | 第三方牛只 ID（`OpenYakEntryDto.cattleId`），**唯一** |
| `yak_no` | `varchar(64)` | Y | 场内牛只编号（`OpenYakEntryDto.cattleNo`），**唯一** |
| `ear_tag_number` | `varchar(64)` | N | 耳标编号（`OpenYakEntryDto.earTagNumber`） |
| `pasture_id` | `bigint(20)` | N | 关联 `biz_pasture.id`（映射成功时） |
| `pasture_name` | `varchar(128)` | N | 所属牧场名称（同步冗余展示） |
| `farm_external_id` | `bigint(20)` | N | 第三方养殖场 ID（`farmId`） |
| `enclosure_external_id` | `bigint(20)` | N | 第三方圈舍 ID（`farmEnclosureId`） |
| `enclosure_id` | `bigint(20)` | N | 本地圈舍 `biz_farm_enclosure.id` |
| `enclosure_name` | `varchar(128)` | N | 圈舍名称 |
| `batch_no` | `varchar(64)` | N | 批次编号；由 `batch_external_id` 关联 `biz_farming_batch.batch_no` |
| `batch_external_id` | `bigint(20)` | N | 第三方养殖批次 ID（`batchId`） |
| `batch_id` | `bigint(20)` | N | 本地养殖批次 `biz_farming_batch.id` |
| `entry_cycle` | `varchar(64)` | N | 入栏周期（`entryCycle`） |
| `gender` | `varchar(16)` | N | 性别 |
| `cattle_variety` | `varchar(128)` | N | 牛只品种 |
| `cattle_picture` | `varchar(512)` | N | 牛只图片 URL |
| `registrant` | `varchar(64)` | N | 登记人 |
| `remark` | `varchar(500)` | N | 备注 |
| `birth_date` | `date` | N | 出生日期 |
| `age_months` | `int(11)` | N | 月龄（月） |
| `entry_date` | `datetime` | N | 入栏时间（`OpenYakEntryDto.entryTime`） |
| `entry_weight_kg` | `decimal(10,2)` | N | 入栏体重 kg |
| `source` | `varchar(64)` | N | 来源 |
| `breeding_method` | `varchar(64)` | N | 养殖方式 |
| `asset_status` | `tinyint(4)` | Y | 资产状态 **§2.6** |
| `status_change_date` | `date` | N | 变更日期 |
| `status_change_reason` | `varchar(256)` | N | 状态变更原因 |
| `pen_location` | `varchar(128)` | N | 圈舍位置 |
| `expected_out_date` | `date` | N | 预计出栏日期 |
| `supplement_plan` | `varchar(512)` | N | 补饲方案 |
| `realtime_temp` | `decimal(6,2)` | N | 实时体温 ℃ |
| `realtime_steps` | `int(11)` | N | 实时运动量 |
| `env_temp` | `decimal(6,2)` | N | 环境温度 ℃（详情当前值） |
| `location` | `varchar(256)` | N | 位置 |
| `physio_collect_time` | `datetime` | N | 生理快照采集时间 |
| `father_yak_no` | `varchar(64)` | N | 父亲编号 |
| `mother_yak_no` | `varchar(64)` | N | 母亲编号 |
| `last_sync_time` | `datetime` | N | 最近同步时间 |
| `del_flag` | `char(1)` | Y | `0` 存在 `2` 删除 |
| `create_time` / `update_time` | datetime | — | 审计 |

**索引**：`UNIQUE uk_external_id (external_id)`；`UNIQUE uk_yak_no (yak_no)`；`KEY idx_asset_status`；`KEY idx_pasture_id`；`KEY idx_status_change_date`；`KEY idx_last_sync_time`；`KEY idx_del_flag`。

#### 2.1.1 第三方入栏建档字段映射（`OpenYakEntryDto` → `biz_yak_asset`）

| OpenAPI 字段 | 库字段 | 说明 |
| --- | --- | --- |
| `cattleId` | `external_id` | 同步 upsert 主键 |
| `cattleNo` | `yak_no` | 场内牛只编号 |
| `earTagNumber` | `ear_tag_number` | 耳标编号 |
| `farmId` | `farm_external_id` / `pasture_id` | 前者直存；后者经 `biz_pasture.external_id` 解析 |
| `farmName` | `pasture_name` | 冗余展示 |
| `farmEnclosureId` | `enclosure_external_id` / `enclosure_id` | 后者经 `biz_farm_enclosure.external_id` 解析 |
| `farmEnclosureName` | `enclosure_name` / `pen_location` | 兼容保留 `pen_location` |
| `batchId` | `batch_external_id` / `batch_id` / `batch_no` | `batch_id`、`batch_no` 经 `biz_farming_batch.external_id` 解析 |
| `entryCycle` | `entry_cycle` | |
| `farmLocation` | `location` | |
| `cattleSex` | `gender` | 归一公/母 |
| `cattleSource` | `source` | |
| `cattleVariety` | `cattle_variety` | |
| `cattlePicture` | `cattle_picture` | |
| `registrant` | `registrant` | |
| `fatherNumber` | `father_yak_no` | |
| `motherNumber` | `mother_yak_no` | |
| `weight` | `entry_weight_kg` | 当前体重 |
| `entryTime` | `entry_date` | |
| `birthDate` | `birth_date` | 月龄由同步时计算写入 `age_months` |
| `remark` | `remark` | |

列表接口未返回的字段（`asset_status`、实时体温/运动量等）保持库内原值或默认「正常」。

### 2.2 子表 `biz_yak_physio_series`（生理时序，曲线图）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `id` | `bigint(20)` | 主键 |
| `yak_asset_id` | `bigint(20)` | 关联主表 |
| `collect_time` | `datetime` | 采集时间 |
| `body_temp` | `decimal(6,2)` | 体温 ℃ |
| `steps` | `int(11)` | 运动量 |
| `env_temp` | `decimal(6,2)` | 环境温度 ℃ |

**索引**：`KEY idx_yak_collect (yak_asset_id, collect_time)`。

### 2.3 子表 `biz_yak_growth_record`（生长性状）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `id` | `bigint(20)` | 主键 |
| `yak_asset_id` | `bigint(20)` | 关联主表 |
| `day_age` | `int(11)` | 日龄（天） |
| `weight_kg` | `decimal(10,2)` | 体重 |
| `height_cm` | `decimal(10,2)` | 体高 |
| `chest_cm` | `decimal(10,2)` | 胸围 |
| `length_cm` | `decimal(10,2)` | 体长 |
| `collect_time` | `datetime` | 采集时间 |
| `data_kind` | `tinyint(4)` | `1` 实测 `2` 预测 |

**索引**：`KEY idx_yak_growth (yak_asset_id, collect_time)`。

### 2.4 子表 `biz_yak_reproduction_record`（繁殖性能）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `id` | `bigint(20)` | 主键 |
| `yak_asset_id` | `bigint(20)` | 关联主表 |
| `day_age` | `int(11)` | 日龄 |
| `parity` | `int(11)` | 胎次 |
| `calving_interval_days` | `int(11)` | 产犊间隔（天） |
| `calf_count` | `int(11)` | 产犊数量（头） |
| `calf_survival_rate` | `decimal(6,4)` | 犊牛成活率 0～1 |
| `calf_birth_weight_kg` | `decimal(10,2)` | 犊牛出生体重 |
| `dystocia_flag` | `char(1)` | `0` 否 `1` 是 |

**索引**：`KEY idx_yak_repro (yak_asset_id, parity)`。

### 2.5 子表 `biz_yak_feeding_record`（饲喂数据）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `id` | `bigint(20)` | 主键 |
| `yak_asset_id` | `bigint(20)` | 关联主表 |
| `start_day_age` | `int(11)` | 开始日龄 |
| `end_day_age` | `int(11)` | 结束日龄 |
| `daily_supplement_kg` | `decimal(10,2)` | 日补饲量 kg |
| `feed_type` | `varchar(64)` | 饲料类型 |

**索引**：`KEY idx_yak_feeding (yak_asset_id)`。

### 2.6 子表 `biz_yak_pen_rel` / `biz_yak_batch_rel`

圈舍、批次信息已合并入主表字段 `pen_location`、`batch_no`；上述关联子表已废弃删除。

### 2.7 IoT 落库表（定时同步，非手动「同步」按钮）

| 表名 | 第三方接口 | 唯一键 | DDL |
| --- | --- | --- | --- |
| `biz_yak_weighing_record` | `GET /open-api/v1/iot/weighings` | `external_id` | `sql/biz_yak_weighing_record.sql` |

`biz_yak_weighing_record` 字段映射：`earTagNumber`→`ear_tag_number`，`cattleNo` 参与 `yak_no` 与资产关联解析；`cattleId`→`cattle_external_id`，`farmId`/`enclosureId`/`weight`/`weighingTime` 等按接口落库。
| `biz_yak_ear_tag_record` | `GET /open-api/v1/iot/ear-tags` | `external_id` | `sql/biz_yak_ear_tag_record.sql` |

`biz_yak_ear_tag_record` 字段映射：`earTagNumber`→`ear_tag_number`，`cattleNo` 参与 `yak_no` 与资产关联解析；`actualTemperature`/`temperature`/`steps`/`reportTime` 同名落库。
| `biz_yak_collar_location` | `GET /open-api/v1/iot/collar-locations` | `(yak_no, report_time)` | `sql/biz_yak_collar_location.sql` |
| `biz_env_monitoring_record` | `GET /open-api/v1/iot/env-monitoring` | `external_id` | `sql/biz_env_monitoring_record.sql` |

关联解析：称重/项圈可关联 `biz_yak_asset`（优先 `cattleId`→`external_id`，其次 `earTagNumber`→`ear_tag_number`，再次 `cattleNo`→`yak_no`）；环境/称重可关联 `biz_pasture`、`biz_farm_enclosure`。

### 2.8 枚举 `asset_status`

| 值 | 含义 |
| --- | --- |
| `1` | 正常 |
| `2` | 死淘 |
| `3` | 丢失 |
| `4` | 出栏 |

第三方旧码（如在养/死亡/已出售等）由 `YakAssetStatusMapper` 归一；存量库执行 `sql/biz_yak_asset_migrate_v2.sql`。

`data_kind`（生长）：`1` 实测，`2` 预测。

### 2.8 DDL（MySQL 5.7，节选）

```sql
CREATE TABLE `biz_yak_asset` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `yak_no` varchar(64) NOT NULL COMMENT '牦牛编号',
  `pasture_id` bigint(20) DEFAULT NULL,
  `pasture_name` varchar(128) DEFAULT NULL,
  `batch_no` varchar(64) DEFAULT NULL,
  `gender` varchar(16) DEFAULT NULL,
  `birth_date` date DEFAULT NULL,
  `age_months` int(11) DEFAULT NULL,
  `entry_date` datetime DEFAULT NULL,
  `entry_weight_kg` decimal(10,2) DEFAULT NULL,
  `source` varchar(64) DEFAULT NULL,
  `breeding_method` varchar(64) DEFAULT NULL,
  `asset_status` tinyint(4) NOT NULL DEFAULT '1',
  `status_change_date` date DEFAULT NULL,
  `realtime_temp` decimal(6,2) DEFAULT NULL,
  `realtime_steps` int(11) DEFAULT NULL,
  `env_temp` decimal(6,2) DEFAULT NULL,
  `location` varchar(256) DEFAULT NULL,
  `physio_collect_time` datetime DEFAULT NULL,
  `father_yak_no` varchar(64) DEFAULT NULL,
  `mother_yak_no` varchar(64) DEFAULT NULL,
  `last_sync_time` datetime DEFAULT NULL,
  `del_flag` char(1) NOT NULL DEFAULT '0',
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_yak_no` (`yak_no`),
  KEY `idx_asset_status` (`asset_status`),
  KEY `idx_status_change_date` (`status_change_date`),
  KEY `idx_del_flag` (`del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='牦牛资产档案';

-- 子表及迁移见 sql/biz_yak_asset.sql、sql/biz_yak_asset_migrate_v2.sql
```

---

## 3. 接口设计

**统一响应**：`AjaxResult` / `TableDataInfo`。

**权限**：`dataModel:yakAsset:list|query|sync`

**Base Path**：`/dataModel/yakAsset`

| # | 说明 | Method | URI | 权限 | 要点 |
| --- | --- | --- | --- | --- | --- |
| 3.1 | 分页列表 | GET | `/dataModel/yakAsset/list` | `list` | Query **3.1.1** |
| 3.2 | 档案详情 | GET | `/dataModel/yakAsset/{id}` | `query` | 主表 + 六类子表列表 |
| 3.3 | 按编号查详情 | GET | `/dataModel/yakAsset/byNo/{yakNo}` | `query` | 系谱跳转父/母 |
| 3.4 | 生理曲线 | GET | `/dataModel/yakAsset/{id}/physioChart` | `query` | Query `days` 默认 30 |
| 3.5 | 生长曲线 | GET | `/dataModel/yakAsset/{id}/growthChart` | `query` | 实测+预测分 `dataKind` |
| 3.6 | 第三方全量同步 | POST | `/dataModel/yakAsset/sync` | `sync` | 返回 **3.6.1**（五段统计；顶层 `insertCount` 兼容旧前端，取自入栏建档） |

本期**不提供** `POST/PUT/DELETE` 档案维护接口。

#### 3.1.1 列表 Query

| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `pageNum` / `pageSize` | int | N | 默认 `1` / `20` |
| `keyword` | string | N | `yak_no` **模糊** |
| `assetStatus` | int | N | `1`～`4` **精确** |

#### 3.2.1 详情响应结构（`data`）

| 节点 | 内容 |
| --- | --- |
| `asset` | 主表字段（驼峰，含 `yakNo`、`pastureName`、`assetStatus` 等） |
| `physioSeries` | 生理时序数组（近一月可由独立接口或详情内截断） |
| `growthRecords` | 生长列表（含 `dataKind`） |
| `reproductionRecords` | 繁殖列表 |
| `feedingRecords` | 饲喂列表 |
| `penRecords` | 圈舍列表 |
| `batchRecords` | 批次列表 |

#### 3.4.1 生理曲线 Query

| 参数 | 说明 |
| --- | --- |
| `days` | 默认 `30`；按 `collect_time` 倒序取最近 N 天 |

返回序列：`[{ collectTime, bodyTemp, steps, envTemp }, ...]`，供 ECharts 多折线。

#### 3.5.1 生长曲线 Query

| 参数 | 说明 |
| --- | --- |
| `metric` | 可选 `weight`/`height`/`chest`/`length`，默认 `weight` |

返回：`{ actual: [...], forecast: [...] }`，按 `data_kind` 分组。

#### 3.6.1 全量同步响应（`data`）

编排类：`LivestockFinanceMasterSyncService.syncAll()`。

| 字段 | 说明 |
| --- | --- |
| `pasture` / `enclosure` / `plan` / `batch` / `yakAsset` | 各步骤 `{ insertCount, updateCount, failCount, failMessages?, syncTime }` |
| `insertCount` / `updateCount` / `failCount` | **兼容旧前端**：取值自 `yakAsset` 步骤（入栏建档头数） |
| `syncTime` | 本次全量同步开始时间 |

**同步顺序**：`biz_pasture` → `biz_farm_enclosure` → `biz_farming_plan` → `biz_farming_batch` → `biz_yak_asset`。

IoT 三类数据**不在此接口**内执行，见 **§3.9**。

### 3.7 第三方对接 — 入栏建档（实现要点）

| 项 | 说明 |
| --- | --- |
| 配置 | `application.yml`：`third-party.farming`（与牧场管理同源） |
| 接口 | `GET /open-api/v1/farming/entry-filings`；分页 `OpenApiPageResult<OpenYakEntryDto>` |
| 客户端 | `ThirdPartyYakClientImpl` → `YakEntryOpenApiMapper.toBundles()` → `YakAssetBundleSyncTxService` |
| 映射 | 字段对照 **§2.1.1**；`YakAssetStatusMapper` 供后续含状态字段的接口 |
| 牧场 | `farmId` → `biz_pasture.external_id`；失败则仅写 `farmName` |
| 事务 | 每头牦牛独立事务（`REQUIRES_NEW`）；子表仅当 bundle 携带对应列表时覆盖 |
| Stub | `thirdparty/stub-yak-entries.json`（OpenAPI 记录样例） |
| 并发 | 全量编排 `syncLock`；各子步骤 `AtomicBoolean` 防重入 |

### 3.8 查询约定

- 列表、详情：`del_flag = '0'`。  
- 列表排序：`status_change_date DESC, id DESC`。  
- 子表列表：`collect_time` 或业务字段倒序。

### 3.9 IoT 数据定时同步

| 项 | 说明 |
| --- | --- |
| **触发** | Quartz：`iotDataIncrementalSyncTask.syncIncremental` |
| **Cron** | `0 0 1 * * ?`（每天凌晨 **1:00**） |
| **初始化 SQL** | `sql/biz_sys_job_iot_data_incremental_sync.sql`（`sys_job` + `sys_config` 水印） |
| **编排** | `IotDataIncrementalSyncService` → `YakWeighingSyncService` / `YakEarTagSyncService` / `YakCollarLocationSyncService` / `EnvMonitoringSyncService` |
| **配置** | 同源 `third-party.farming`（`base-url`、`app-key`、`app-secret`、`mode=http\|stub`） |
| **水印** | `sys_config.third_party.iot.last_sync_time`；增量时作为 `updatedAfter`；任务成功后更新 |

**全量 / 增量策略（按表独立）**

| 本地表行数 `count(*)` | 模式 | HTTP 查询参数 |
| --- | --- | --- |
| **= 0**（表已清空） | **全量** | 不传 `startTime` / `endTime` / `updatedAfter`，分页拉取全部 |
| **> 0** | **增量** | `startTime` = 最大事件时间 − 5 分钟；`endTime` = 当前；`updatedAfter` = 配置水印 |

行数统计**不按** `del_flag` 过滤；仅 `TRUNCATE`/`DELETE` 清空整表后才会对该表走全量。

**Stub 样例**：`thirdparty/stub-iot-weighings.json`、`stub-iot-ear-tags.json`、`stub-iot-collar-locations.json`、`stub-iot-env-monitoring.json`。

**日志**：任务开始/结束写入应用日志；结束行输出 `IotDataIncrementalSyncResultVo` JSON。

---

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

| 类型 | 名称 | 权限标识 |
| --- | --- | --- |
| 菜单 | 牦牛资产档案 | `dataModel:yakAsset:list` |
| 按钮 | 查询 | `dataModel:yakAsset:query` |
| 按钮 | 同步 | `dataModel:yakAsset:sync` |

组件路径：`livestockFinance/yakAsset/index`（挂载「牧业金融 · 生物资产」目录下）。

---

## 5. 交付清单

- [ ] `sql/biz_yak_asset.sql`（主表 + 4 子表 + 可选同步日志表）  
- [ ] `LivestockFinanceMasterSyncService`、IoT `IotDataIncrementalSyncTask`  
- [ ] `sql/biz_yak_weighing_record.sql` 等 IoT 三表 + `sql/biz_sys_job_iot_data_incremental_sync.sql`  
- [ ] `ThirdPartyYakClient`、`YakAssetSyncService`  
- [ ] `BizYakAsset` Domain / Mapper / `IYakAssetService` / `BizYakAssetController`  
- [ ] 单元测试（同步合并、状态映射、曲线查询）  
- [ ] MockMvc：list、detail、sync（Mock 第三方）  

---

## 6. 修订记录

| 版本 | 说明 |
| --- | --- |
| 1.0 | 初稿：主表+四子表；只读查询+同步接口；生长实测/预测分 `data_kind`；对接可配置 |
| 1.1 | 对齐入栏建档 OpenAPI：`OpenYakEntryDto`、`external_id`、分页拉取、字段映射 **§2.1.1** |
| 1.2 | 全量同步扩展为五段编排；IoT 三表独立定时同步 **§2.7、§3.9**；全量/增量按表 `count(*)` 判断 |
