# 市场进栏登记 — 技术方案 > 依据:`市场进栏登记功能需求.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 |