# 市场进栏登记 — 技术方案

> 依据：`市场进栏登记功能需求.md`（同目录）  
> 实现参照：`baqing-admin` 模块 `com.ruoyi.web.modules.trading`（`BizMarketEntry*`）

---

## 1. 技术架构

| 层级 | 说明 |
| --- | --- |
| **整体** | RuoYi **v3.9.2**（**springboot2** 分支）单体后端 + 若依 **Vue2** 前端（与分支配套） |
| **运行时** | JDK 8、Spring Boot 2.x、Spring MVC、MyBatis、Druid |
| **数据库** | MySQL **5.7.39**，存储引擎 InnoDB，字符集 **utf8mb4** |

**后端分层（与若依一致）**

- `BizMarketEntryController`：参数绑定、权限注解、返回 `AjaxResult` / `TableDataInfo`  
- `IBizMarketEntryService` / `BizMarketEntryServiceImpl`：事务、主从表保存、**R1～R6** 校验编排  
- `MarketEntryValidation`、`MarketEntryEarTagResolver`、`MarketEntryReferenceChecker`：规则与可选耳标池  
- `BizMarketEntryMapper`、`BizMarketEntryEarTagMapper` + XML：持久化  
- `domain`：`BizMarketEntry`、`BizMarketEntryEarTag`；复用 `EarTagCodec`  

**业务摘要（与功能需求对齐）**

| 场景 | 行为 |
| --- | --- |
| 新增/修改 | 校验栏位、供应商；**R1～R6**；主表落库 → 子表 `batchInsert`；修改时先按 `entry_id` 删子表再插入；`quantity = 子表行数` |
| 列表/详情 | 主表 `del_flag=0`；`supplier_name` JOIN；子表聚合 `earTags` / 组装 `earTagList` |
| 删除 | 主表 `del_flag=2`；子表保留；占用查询以主表 `del_flag` 过滤 |
| 辅助接口 | `stallOptions`；`supplierEarTags`（**A−B**，仅 `supplierId`，见 **§5**） |

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

---

## 2. 源码位置

| 类型 | 路径 |
| --- | --- |
| **Controller** | `baqing-admin/src/main/java/com/ruoyi/web/modules/trading/controller/BizMarketEntryController.java` |
| **Service** | `.../service/IBizMarketEntryService.java`、`.../service/impl/BizMarketEntryServiceImpl.java` |
| **校验/解析** | `.../support/MarketEntryValidation.java`、`MarketEntryEarTagResolver.java`、`MarketEntryRules.java` |
| **Mapper** | `.../mapper/BizMarketEntryMapper.java`、`BizMarketEntryEarTagMapper.java` |
| **XML** | `baqing-admin/src/main/resources/mapper/trading/BizMarketEntryMapper.xml`、`BizMarketEntryEarTagMapper.xml` |
| **实体** | `.../domain/BizMarketEntry.java`、`BizMarketEntryEarTag.java` |
| **VO** | `.../domain/vo/MarketStallOptionVo.java` |

---

## 3. 数据库设计

### 3.1 主表：`biz_market_entry`（进栏记录）

| 字段名 | 类型 | 非空 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `id` | `bigint(20)` | Y | 自增 | 主键 |
| `entry_time` | `datetime` | Y | — | 进栏时间 |
| `stall_id` | `bigint(20)` | Y | — | 关联 `biz_market_stall.id` |
| `stall_code` | `char(2)` | Y | — | 栏位编号冗余，列表模糊筛选 |
| `supplier_id` | `bigint(20)` | Y | — | 关联 `biz_supplier.id` |
| `quantity` | `int(11)` | Y | — | 数量（头），= 子表耳标行数 |
| `del_flag` | `char(1)` | Y | `'0'` | 删除标记，见 **3.4** |
| `create_by` / `create_time` / `update_by` / `update_time` | 若依惯例 | — | — | 审计 |
| `remark` | `varchar(500)` | N | NULL | 备注 |

**索引**：`PRIMARY KEY (id)`；`KEY idx_entry_time`；`KEY idx_stall_code`；`KEY idx_supplier`；`KEY idx_del_flag`。

> 主表**不存** `ear_tags` 文本字段；耳标明细见 **3.2**。

### 3.2 子表：`biz_market_entry_ear_tag`（耳标明细）

| 字段名 | 类型 | 非空 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `id` | `bigint(20)` | Y | 自增 | 主键 |
| `entry_id` | `bigint(20)` | Y | — | 关联主表 `biz_market_entry.id` |
| `supplier_id` | `bigint(20)` | Y | — | 供应商 ID（与主表一致，便于占用查询） |
| `ear_tag` | `varchar(32)` | Y | — | 耳标号，单段 1～32 字 |

**索引与约束**

- `PRIMARY KEY (id)`  
- `UNIQUE KEY uk_entry_ear (entry_id, ear_tag)` — 同单内耳标不重复  
- `KEY idx_entry_id (entry_id)`、`KEY idx_supplier_id (supplier_id)`、`KEY idx_ear_tag (ear_tag)`  

**维护策略**：新增/修改主表成功后批量插入子表；修改时 `DELETE FROM biz_market_entry_ear_tag WHERE entry_id=?` 后重插。

### 3.3 关联表（只读，本模块不建）

| 表 | 用途 |
| --- | --- |
| `biz_market_stall` | 栏位下拉、`stall_code` 冗余写入 |
| `biz_supplier` | 供应商校验、列表 `supplier_name` |
| `biz_entry_isolation` | 可选耳标池 **A**（主表 `ear_tags`，隔离已结束） |
| `biz_entry_isolation_alloc` | 入场隔离分圈（本模块耳标池 B **不**使用） |
| `biz_isolation_patrol` | 可选耳标池 **B**（全表病死病害耳标，`selectSickDeadEarTagRows`） |

### 3.4 删除标记 `del_flag`（主表）

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

### 3.5 DDL（MySQL 5.7）

完整脚本见 `sql/biz_market_entry.sql`。核心结构如下：

```sql
CREATE TABLE `biz_market_entry` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `entry_time` datetime NOT NULL,
  `stall_id` bigint(20) NOT NULL,
  `stall_code` char(2) NOT NULL,
  `supplier_id` bigint(20) NOT NULL,
  `quantity` int(11) NOT NULL,
  `del_flag` char(1) NOT NULL DEFAULT '0',
  `create_by` varchar(64) DEFAULT '',
  `create_time` datetime DEFAULT NULL,
  `update_by` varchar(64) DEFAULT '',
  `update_time` datetime DEFAULT NULL,
  `remark` varchar(500) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_entry_time` (`entry_time`),
  KEY `idx_stall_code` (`stall_code`),
  KEY `idx_supplier` (`supplier_id`),
  KEY `idx_del_flag` (`del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='市场进栏登记';

CREATE TABLE `biz_market_entry_ear_tag` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `entry_id` bigint(20) NOT NULL,
  `supplier_id` bigint(20) NOT NULL,
  `ear_tag` varchar(32) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_entry_ear` (`entry_id`,`ear_tag`),
  KEY `idx_entry_id` (`entry_id`),
  KEY `idx_supplier_id` (`supplier_id`),
  KEY `idx_ear_tag` (`ear_tag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='市场进栏登记耳标明细';
```

---

## 4. 字段命名约定（接口 JSON）

与前端、持久化 **小驼峰（camelCase）** 对齐；库表为下划线。

| 前端 / JSON（camelCase） | 库表字段 | 说明 |
| --- | --- | --- |
| `id` | `id` | 主键 |
| `entryTime` | `entry_time` | 进栏时间 |
| `stallId` | `stall_id` | 市场栏位 ID |
| `stallCode` | `stall_code` | 栏位编号（服务端冗余写入） |
| `supplierId` | `supplier_id` | 供应商 ID |
| `supplierName` | — | 列表/详情 JOIN 展示，非主表列 |
| `quantity` | `quantity` | 头数 |
| `earTagList` | 子表 `ear_tag` | 请求/响应数组；入库拆为多行 |
| `earTags` | 子表聚合 | 列表/详情逗号拼接展示用 |
| `entryDateStart` / `entryDateEnd` | — | 列表筛选，映射 `DATE(entry_time)` |
| `earTag` | 子表 `ear_tag` | 列表筛选，子表 `LIKE` |
| `remark` | `remark` | 备注 |

子表行（内部/扩展）：`entryId`、`supplierId`、`earTag`。

---

## 5. 可选耳标池（`MarketEntryEarTagResolver`）

与功能需求 **§4** 一致。耳标字段均为逗号分隔文本，经 `EarTagCodec.parse` 拆分去重。

| 集合 | 规则 | 实现要点 |
| --- | --- | --- |
| **A** | 入场隔离**主表**耳标；`supplier_id`；`del_flag=0`；**隔离已结束** | `BizEntryIsolationMapper.selectEndedMainEarTagRowsBySupplier`；`expected_release_date <= CURDATE()` |
| **B** | **全部**巡查；`del_flag=0`；`sick_dead_qty>0` 且耳标非空 | `BizIsolationPatrolMapper.selectSickDeadEarTagRows`（**无** `supplierId` 条件） |
| **C** | 他单进栏占用 | `BizMarketEntryEarTagMapper.selectOccupiedEarTags`；主表 `del_flag=0`；`entry_id <> excludeEntryId` |

**隔离已结束**：当前日期 **≥** 预计解除日期。

### 5.1 下拉接口 `listSupplierEarTagOptions`（`GET /supplierEarTags`）

- 入参：仅 **`supplierId`**（必填）；校验供应商存在且未删除。  
- 返回：**A − B**，升序 `string[]`。  
- **不含 C**；已被他单占用的耳标仍可能出现在下拉中。

### 5.2 保存校验 `listEligible`（新增/修改 R4）

- 返回：**(A − B) − C**。  
- `excludeEntryId`：新增为 `null`；修改传当前主表 `id`，释放本条已选耳标占用。

---

## 6. 业务校验与事务

**`MarketEntryValidation`（保存时 R1～R6）**

| 规则 | 实现要点 |
| --- | --- |
| R1 | 进栏时间、栏位、供应商、至少 1 个耳标 |
| R2 | `entry_time <= now()` |
| R3 | `biz_market_stall`、`biz_supplier` 存在且 `del_flag=0` |
| R4 | 每个耳标 ∈ `listEligible(supplierId, excludeEntryId)` |
| R5 | `EarTagCodec` 去重，单段 1～32 字 |
| R6 | `quantity = earTagList.size()` |

**事务**：`insert` / `update` 使用 `@Transactional`；主表与子表在同一事务内提交。

**删除**：`MarketEntryReferenceChecker.hasReference(id)` 为 true 时拒绝；否则主表 `del_flag=2`。

---

## 7. 接口设计

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

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

**Base Path**：`/tradeMarket/marketEntry`

> 路由注册顺序：`/stallOptions`、`/supplierEarTags` 须在 `/{id}` **之前**。

| 接口说明 | HTTP | URI（相对 Base Path） | 权限 |
| --- | --- | --- | --- |
| 分页列表 | GET | `/list` | `list` |
| 详情 | GET | `/{id}` | `query` |
| 新增 | POST | `/` | `add` |
| 修改 | PUT | `/` | `edit` |
| 删除 | DELETE | `/{ids}` | `remove` |
| 栏位下拉 | GET | `/stallOptions` | `list` 或 `query` |
| 可选耳标 | GET | `/supplierEarTags` | `list` 或 `query` |

### 7.1 列表 `GET /list`

| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `pageNum` / `pageSize` | int | N | 默认 `1` / `20` |
| `entryDateStart` / `entryDateEnd` | string | N | `yyyy-MM-dd`；闭区间；均填时须 `start <= end` |
| `stallCode` | string | N | `stall_code LIKE %stallCode%` |
| `supplierId` | long | N | 精确 |
| `earTag` | string | N | 子表 `ear_tag LIKE %earTag%`（`EXISTS`） |

**条件**：`del_flag='0'`。**排序**：`entry_time DESC, id DESC`。

**行字段**：`id`、`entryTime`、`stallId`、`stallCode`、`supplierId`、`supplierName`、`earTags`、`earTagList`（可选）、`quantity`、审计字段。

### 7.2 详情 `GET /{id}`

**`data`**：主表全字段 + `earTagList` + `supplierName`（子表组装）。

### 7.3 新增 `POST /`

| Body 字段 | 必填 | 说明 |
| --- | --- | --- |
| `entryTime` | Y | `yyyy-MM-dd HH:mm:ss`，≤ 当前时间 |
| `stallId` | Y | 服务端写入 `stallCode` |
| `supplierId` | Y | — |
| `earTagList` | Y | ≥1；或与 `earTags` 逗号串二选一，经 `EarTagCodec` 解析 |
| `quantity` | N | 服务端以耳标数为准覆盖 |
| `remark` | N | ≤500 字 |

### 7.4 修改 `PUT /`

同 **7.3**；`id` 必填；保存时服务端以 `id` 作为 `excludeEntryId` 参与 **§5.2** 占用校验（与 `supplierEarTags` 无关）。

### 7.5 删除 `DELETE /{ids}`

单条可只传一个 id；更新主表 `del_flag='2'`；有下游引用返回「存在关联业务，无法删除」。

### 7.6 栏位下拉 `GET /stallOptions`

**`data`**：`[{ stallId, stallCode }]`，`biz_market_stall` 且 `del_flag=0`，`stall_code ASC`。

### 7.7 可选耳标 `GET /supplierEarTags`

| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `supplierId` | long | Y | 供应商 |

**`data`**：`string[]`（**A−B**，见 **§5.1**）。供应商无效时业务异常。

**计算步骤（与需求 §4.1、§4.2 一致）**

1. 按 `supplierId` 查 `biz_entry_isolation`：`del_flag=0`、`expected_release_date <= CURDATE()`，汇总主表 `ear_tags`（逗号拆分去重）→ **A**  
2. 查 `biz_isolation_patrol`：`del_flag=0`、`sick_dead_qty>0`、耳标非空（**全表，不按 supplierId**），汇总 `ear_tags` → **B**  
3. 返回 **A − B**（升序）。**不**剔除他单进栏占用 **C**（保存时见 **§5.2**、**§6** R4）

### 7.8 供应商搜索（前端联调）

登记表单模糊搜索：复用 `/tradeMarket/supplier/list`（`supplierName` 模糊）；列表筛选使用本模块 `supplierId` 精确条件。

---

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

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

组件路径（前端）：`tradeMarket/marketEntry/index`。

---

## 9. 交付清单

- [x] `sql/biz_market_entry.sql`（主表 + 子表）
- [x] `BizMarketEntry`、`BizMarketEntryEarTag`、Mapper/XML、Service、Controller
- [x] `MarketEntryValidation`、`MarketEntryEarTagResolver`、`MarketEntryRules`
- [x] 单元测试 / MockMvc（`com.ruoyi.web.modules.trading.**.*MarketEntry*`）

---

## 10. 修订记录

| 日期 | 说明 |
| --- | --- |
| — | 初稿：单表 `ear_tags` 文本存储 |
| — | v2.0：耳标池 A−B−C；关联入场隔离、隔离巡查 |
| — | v2.1：主从表 `biz_market_entry` + `biz_market_entry_ear_tag` |
| — | 版式对齐《养殖标准管理技术方案》《养殖标准管理技术前端方案》；接口/库表/源码路径分章 |
| — | v2.2：`supplierEarTags` 仅 `supplierId`，返回 A−B；B 为全表巡查病死耳标；保存 (A−B)−C |
