# 牦牛资产档案管理 — 技术方案 > 依据:同目录 `牦牛资产档案管理功能需求.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` | | 客户端 | `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(*)` 判断 |