# 入场隔离登记 — 技术方案

> 依据：同目录 `入场隔离登记功能需求.md`。主表 + **分圈明细子表**；保存时校验 **R1～R5**；检疫证文件走若依上传；**逻辑删除**。

---

## 1. 技术架构

| 项 | 说明 |
| --- | --- |
| **后端** | RuoYi **v3.9.2**（**springboot2** 分支）：JDK 8、Spring Boot 2.x、Spring MVC、MyBatis、Druid |
| **数据库** | MySQL **5.7.39**，InnoDB，`utf8mb4` |
| **前端** | 若依 Vue2；字段 **小驼峰**（页面见前端技术方案，本文不展开） |
| **文件** | 复用 `/common/upload`；存 `cert_file_url`、`cert_file_path`（`/profile/...`） |

**分层**：`Controller` → `Service`（`EntryIsolationValidation`、`EarTagCodec`、`ReleaseDateCalculator`、`EntryIsolationReferenceChecker`）→ `Mapper`/XML → `domain`。

**代码包（建议）**：`com.ruoyi.web.modules.trading`

**初始化脚本**：`sql/biz_entry_isolation.sql`

**业务摘要**

| 场景 | 行为 |
| --- | --- |
| 新增/修改 | 校验供应商、圈位存在且未删；解析耳标；校验 **R1～R5**；计算 `expected_release_date`；主表 + 子表**同事务**保存 |
| 修改分圈 | 先删该登记下全部子表行，再批量插入新分圈明细 |
| 列表/详情 | 主表 `del_flag=0`；详情带子表 `penAllocList` |
| 列表筛选 | 进场日期区间、检疫证号模糊、`supplier_id` 精确 |
| 删除 | `del_flag=2`；下游引用时拒绝 |

**预计解除日期**：`expected_release_date = DATE(entry_time) + isolation_days`（自然日相加，与功能需求默认一致）。

---

## 2. 数据库设计

### 2.1 表 `biz_entry_isolation`（入场隔离登记主表）

| 字段 | 类型 | 非空 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `id` | `bigint(20)` | Y | 自增 | 主键 |
| `entry_time` | `datetime` | Y | — | 进场时间 |
| `supplier_id` | `bigint(20)` | Y | — | 关联 `biz_supplier.id` |
| `cert_file_url` | `varchar(512)` | N | NULL | 检疫证访问 URL |
| `cert_file_path` | `varchar(512)` | N | NULL | 检疫证存储路径 |
| `cert_no` | `varchar(50)` | Y | — | 检疫证号 |
| `quantity` | `int(11)` | Y | — | 总数量（头），正整数 |
| `ear_tags` | `text` | Y | — | 主表耳标，逗号分隔入库 |
| `isolation_days` | `int(11)` | Y | — | 隔离天数，1～3650 |
| `expected_release_date` | `date` | Y | — | 预计解除日期（服务端计算） |
| `del_flag` | `char(1)` | Y | `'0'` | `0` 存在 `2` 删除 |
| `create_by` / `create_time` / `update_by` / `update_time` | 若依惯例 | — | — | 审计 |
| `remark` | `varchar(500)` | N | NULL | 备注 |

**索引**：`PRIMARY KEY (id)`；`KEY idx_entry_time (entry_time)`；`KEY idx_cert_no (cert_no)`；`KEY idx_supplier (supplier_id)`；`KEY idx_del_flag (del_flag)`。

### 2.2 表 `biz_entry_isolation_alloc`（分圈明细）

| 字段 | 类型 | 非空 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `id` | `bigint(20)` | Y | 自增 | 主键 |
| `entry_id` | `bigint(20)` | Y | — | 关联主表 `id` |
| `pen_id` | `bigint(20)` | Y | — | 关联 `biz_isolation_pen.id` |
| `quantity` | `int(11)` | Y | — | 本圈数量（头） |
| `ear_tags` | `text` | Y | — | 本圈耳标，逗号分隔 |
| `sort_order` | `int(11)` | Y | `0` | 展示排序 |

**索引**：`PRIMARY KEY (id)`；`KEY idx_entry_id (entry_id)`；`UNIQUE KEY uk_entry_pen (entry_id, pen_id)`（同一登记内圈位不可重复）。

> **说明**：`pen_id` 冗余存储便于关联；列表/详情通过 JOIN 带出 `pen_code` 展示。

### 2.3 DDL（MySQL 5.7）

```sql
CREATE TABLE `biz_entry_isolation` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `entry_time` datetime NOT NULL COMMENT '进场时间',
  `supplier_id` bigint(20) NOT NULL COMMENT '供应商ID',
  `cert_file_url` varchar(512) DEFAULT NULL COMMENT '检疫证URL',
  `cert_file_path` varchar(512) DEFAULT NULL COMMENT '检疫证路径',
  `cert_no` varchar(50) NOT NULL COMMENT '检疫证号',
  `quantity` int(11) NOT NULL COMMENT '总数量头',
  `ear_tags` text NOT NULL COMMENT '耳标号逗号分隔',
  `isolation_days` int(11) NOT NULL COMMENT '隔离天数',
  `expected_release_date` date NOT NULL COMMENT '预计解除日期',
  `del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '0存在 2删除',
  `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(64) DEFAULT '' COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  KEY `idx_entry_time` (`entry_time`),
  KEY `idx_cert_no` (`cert_no`),
  KEY `idx_supplier` (`supplier_id`),
  KEY `idx_del_flag` (`del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入场隔离登记';

CREATE TABLE `biz_entry_isolation_alloc` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `entry_id` bigint(20) NOT NULL COMMENT '登记主表ID',
  `pen_id` bigint(20) NOT NULL COMMENT '圈位ID',
  `quantity` int(11) NOT NULL COMMENT '本圈数量头',
  `ear_tags` text NOT NULL COMMENT '本圈耳标逗号分隔',
  `sort_order` int(11) NOT NULL DEFAULT '0' COMMENT '排序',
  PRIMARY KEY (`id`),
  KEY `idx_entry_id` (`entry_id`),
  UNIQUE KEY `uk_entry_pen` (`entry_id`,`pen_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入场隔离分圈明细';
```

### 2.4 关联与枚举

| 关联 | 说明 |
| --- | --- |
| `supplier_id` | `biz_supplier`，`del_flag=0` 方可选用 |
| `pen_id` | `biz_isolation_pen`，`del_flag=0` 方可选用 |

**删除标记 `del_flag`（主表）**

| 值 | 含义 |
| --- | --- |
| 0 | 正常 |
| 2 | 已逻辑删除 |

### 2.5 耳标存储约定（`EarTagCodec`）

- 入库：列表 → 英文逗号拼接 `ear_tags`；去空白、去重、过滤空段。  
- 出库：详情/列表分圈 → `earTagList`（`string[]`）；主表同时返回 `earTags`（原文）与 `earTagList`。  
- 单段长度 **1～32**；主表耳标个数 = `quantity`（**R1**）。

### 2.6 一致性校验（`EntryIsolationValidation`）

| 规则 | 实现要点 |
| --- | --- |
| R1 | `quantity == earTagList.size()` |
| R2 | `sum(alloc.quantity) == quantity` |
| R3 | 分圈耳标并集 = 主表耳标集合 |
| R4 | 分圈耳标无交集重复 |
| R5 | 每条 `alloc.quantity == alloc.earTagList.size()` |
| 其他 | 分圈 ≥1 条；`pen_id` 不重复；供应商/圈位有效；检疫证后缀与 30MB（上传阶段校验） |

---

## 3. 接口设计

**统一响应**：`AjaxResult` / `TableDataInfo`（`code`、`msg`、`data` / `rows`、`total`）。

**权限标识**：`tradeMarket:entryIsolation:list|query|add|edit|remove`

**Base Path**：`/tradeMarket/entryIsolation`

| # | 说明 | Method | URI | 权限 |
| --- | --- | --- | --- | --- |
| 3.1 | 分页列表 | GET | `/tradeMarket/entryIsolation/list` | `list` |
| 3.2 | 详情 | GET | `/tradeMarket/entryIsolation/{id}` | `query` |
| 3.3 | 新增 | POST | `/tradeMarket/entryIsolation` | `add` |
| 3.4 | 修改 | PUT | `/tradeMarket/entryIsolation` | `edit` |
| 3.5 | 删除 | DELETE | `/tradeMarket/entryIsolation/{ids}` | `remove` |

### 3.1 列表

**Query（驼峰）**

| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `pageNum` / `pageSize` | int | N | 默认 `1` / `20` |
| `entryDateStart` | string | N | `yyyy-MM-dd`，进场日期 ≥ |
| `entryDateEnd` | string | N | `yyyy-MM-dd`，进场日期 ≤ |
| `certNo` | string | N | 检疫证号**模糊** `LIKE %certNo%` |
| `supplierId` | long | N | 供应商 **精确** |

**条件**：`del_flag='0'`；日期对 `DATE(entry_time)` 过滤。

**排序**：`entry_time DESC, id DESC`。

**行字段（驼峰）**：`id`、`entryTime`、`supplierId`、`supplierName`、`certNo`、`quantity`、`isolationDays`、`expectedReleaseDate`、`penAllocSummary`（可选，如「3 个圈位」）+ 审计字段。

### 3.2 详情 `data`

主表全字段 + `supplierName` + `earTagList` + `penAllocList`：

| `penAllocList[]` | 类型 | 说明 |
| --- | --- | --- |
| `id` | long | 子表主键（修改时前端可不带，以全量覆盖为准） |
| `penId` | long | 圈位 ID |
| `penCode` | string | 圈位编号 |
| `quantity` | int | 本圈数量 |
| `earTagList` | string[] | 本圈耳标 |
| `earTags` | string | 逗号拼接（展示用） |

### 3.3 新增 / 修改 Body（驼峰）

| 字段 | 新增 | 修改 | 说明 |
| --- | --- | --- | --- |
| `id` | — | Y | 主键 |
| `entryTime` | Y | Y | `yyyy-MM-dd HH:mm:ss` |
| `supplierId` | Y | Y | 有效供应商 |
| `certFileUrl` / `certFilePath` | N | N | 检疫证；有路径时校验后缀 |
| `certNo` | Y | Y | 1～50 字，trim |
| `quantity` | Y | Y | 正整数 |
| `earTagList` | Y | Y | 耳标数组（优先）；或与 `earTags` 二选一，服务端统一解析 |
| `earTags` | N | N | 逗号分隔字符串（兼容） |
| `isolationDays` | Y | Y | 1～3650 |
| `penAllocList` | Y | Y | 至少 1 条，见下表 |
| `remark` | N | N | ≤500 字 |

**`penAllocList[]`**

| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `penId` | Y | 圈位 ID，同登记内不可重复 |
| `earTagList` | Y | 本圈耳标，≥1；须属于主表耳标集合 |

> `quantity` 由服务端按 `earTagList.size()` 写入子表，可不传或传了也以服务端为准（**R5**）。

**服务端流程**

1. `EarTagCodec` 规范化主表耳标 → 写 `ear_tags`。  
2. 校验 **R1～R5** 及外键。  
3. `ReleaseDateCalculator` 写 `expected_release_date`。  
4. 插入/更新主表；**删除**原 `entry_id` 子表行后 **批量插入** 新分圈。

### 3.4 删除

- `DELETE /tradeMarket/entryIsolation/{ids}`，单条传一个 id。  
- `EntryIsolationReferenceChecker.hasReference(id)` 为 true 时失败。  
- 主表 `del_flag='2'`；子表可保留（查询带主表 `del_flag`）或同步逻辑删除（**默认保留子表**，仅主表删）。

### 3.5 校验与错误（摘要）

| 项 | 规则 |
| --- | --- |
| 供应商 | 存在且 `biz_supplier.del_flag=0` |
| 圈位 | 每条 `pen_id` 存在且 `biz_isolation_pen.del_flag=0` |
| 检疫证文件 | 后缀 `pdf,md,doc,docx,xls,xlsx,ppt,pptx`；路径 `/profile` 前缀 |
| 修改/删除 | 主表存在且 `del_flag=0` |
| 失败文案 | 与功能需求 **§8** 对齐，如「总数量须与耳标个数一致」 |

### 3.6 本期不提供

- 检疫证 OCR 识别接口。  
- 分圈明细独立 CRUD（仅随主表全量保存）。

---

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

| 类型 | 名称 | 权限标识 |
| --- | --- | --- |
| 菜单 | 入场隔离登记 | `tradeMarket:entryIsolation:list` |
| 按钮 | 查询 | `tradeMarket:entryIsolation:query` |
| 按钮 | 新增 | `tradeMarket:entryIsolation:add` |
| 按钮 | 修改 | `tradeMarket:entryIsolation:edit` |
| 按钮 | 删除 | `tradeMarket:entryIsolation:remove` |

组件路径：`tradeMarket/entryIsolation/index`（「交易市场平台」目录下）。

---

## 5. 交付清单

- [ ] `sql/biz_entry_isolation.sql`、菜单权限 SQL  
- [ ] Domain / Mapper / Service / Controller（`trading` 包）  
- [ ] `EntryIsolationValidation`、`EarTagCodec`、`ReleaseDateCalculator`  
- [ ] 单元测试：R1～R5、日期计算、供应商/圈位外键、逻辑删除  

---

## 6. 修订记录

| 版本 | 说明 |
| --- | --- |
| 1.0 | 初稿：主表+分圈子表、R1～R5、接口 CRUD；对齐功能需求与 RuoYi/MySQL 5.7 |
