# 交易订单管理 — 技术方案

> 依据：`交易订单管理功能需求.md`（同目录）  
> 关联：`../供应商结算管理/供应商结算管理-草稿.md`（结算单由本模块**完成支付**时生成）

---

## 1. 技术架构

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

**后端分层（`com.ruoyi.web.modules.trading`）**

| 组件 | 职责 |
| --- | --- |
| `BizTradeOrderController` | 订单 CRUD、完成支付、耳标候选 |
| `IBizTradeOrderService` / `BizTradeOrderServiceImpl` | 事务：保存主从表、完成支付并生成结算单 |
| `TradeOrderValidation` | R1～R8 |
| `TradeOrderEarTagResolver` | 下拉：`listSupplierEarTagOptions`（进栏子表）；保存：**（M−B）−C** `listEligible` |
| `TradeOrderNoGenerator` | 订单编号 `YYYYMMDD`+3 位序号 |
| `SettlementOnPayService` | 完成支付时写 `biz_supplier_settlement`（费用快照） |
| `BizTradeOrderMapper` / `BizTradeOrderLineMapper` | 订单持久化 |

**业务摘要**

| 场景 | 行为 |
| --- | --- |
| 新增/修改 | 仅 `order_status=0`；校验 R1～R8；生成/保留 `order_no`；主表 + 明细 `batchInsert`；修改先删明细再插 |
| 完成支付 | `@Transactional`：状态→已完成、凭证、完成时间 → **一条**结算单 → 按明细删 `biz_market_entry_ear_tag`（幂等：结算已存在则拒绝） |
| 删除 | 仅待支付；`del_flag=2` |
| 列表/详情 | `del_flag=0`；JOIN 供应商/承销商名；明细组装 `lineList` |

**初始化脚本**：`sql/biz_trade_order.sql`、`sql/biz_supplier_settlement.sql`

---

## 2. 源码位置（建议）

| 类型 | 路径 |
| --- | --- |
| Controller | `.../controller/BizTradeOrderController.java` |
| Service | `.../service/IBizTradeOrderService.java`、`.../impl/BizTradeOrderServiceImpl.java` |
| 结算写入 | `.../support/SettlementOnPayService.java` |
| 校验/耳标/等级 | `.../support/TradeOrderValidation.java`、`TradeOrderEarTagResolver.java`、`TradeOrderGradeResolver.java`、`TradeOrderRules.java` |
| Mapper | `.../mapper/BizTradeOrderMapper.java`、`BizTradeOrderLineMapper.java`、`BizGradeWeightConfigMapper.java` |
| XML | `mapper/trading/BizTradeOrderMapper.xml`、`BizTradeOrderLineMapper.xml`、`BizGradeWeightConfigMapper.xml` |
| 实体 | `.../domain/BizTradeOrder.java`、`BizTradeOrderLine.java`、`BizGradeWeightConfig.java` |

**供应商结算管理**（独立迭代，本方案仅定义表与生成逻辑）：`BizSupplierSettlementController`、`/tradeMarket/supplierSettlement`。

---

## 3. 数据库设计

### 3.1 主表：`biz_trade_order`（交易订单）

| 字段名 | 类型 | 非空 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `id` | `bigint(20)` | Y | 自增 | 主键 |
| `order_no` | `varchar(11)` | Y | — | 订单编号 `YYYYMMDD`+3 位序号，**唯一** |
| `supplier_id` | `bigint(20)` | Y | — | `biz_supplier.id` |
| `distributor_id` | `bigint(20)` | Y | — | `biz_distributor.id`（承销商） |
| `total_heads` | `int(11)` | Y | — | 交易总头数 = 明细行数 |
| `total_amount` | `decimal(12,2)` | Y | — | 交易总金额（元） |
| `order_status` | `char(1)` | Y | `'0'` | `0` 待支付 `1` 已完成 |
| `pay_voucher_url` | `varchar(512)` | N | NULL | 支付凭证 URL |
| `pay_voucher_path` | `varchar(512)` | N | NULL | 支付凭证路径 |
| `finish_time` | `datetime` | N | NULL | 订单完成时间（完成支付时写入） |
| `del_flag` | `char(1)` | Y | `'0'` | `0` 存在 `2` 逻辑删除 |
| `create_by` / `create_time` / `update_by` / `update_time` | 若依惯例 | — | — | 审计；**订单创建时间**用 `create_time` |
| `remark` | `varchar(500)` | N | NULL | 备注 |

**索引**：`PRIMARY KEY (id)`；`UNIQUE KEY uk_order_no (order_no)`；`KEY idx_supplier (supplier_id)`；`KEY idx_distributor (distributor_id)`；`KEY idx_order_status (order_status)`；`KEY idx_create_time (create_time)`；`KEY idx_del_flag (del_flag)`。

### 3.2 子表：`biz_trade_order_line`（采购明细）

| 字段名 | 类型 | 非空 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `id` | `bigint(20)` | Y | 自增 | 主键 |
| `order_id` | `bigint(20)` | Y | — | 主表 ID |
| `supplier_id` | `bigint(20)` | Y | — | 供应商 ID（与主表一致，冗余） |
| `ear_tag` | `varchar(32)` | Y | — | 耳标号 |
| `weight` | `decimal(12,2)` | Y | — | 重量 kg，>0 |
| `amount` | `decimal(12,2)` | Y | — | 金额 元，>0 |
| `grade_code` | `char(1)` | N | NULL | 品质等级编码；保存时由 `TradeOrderGradeResolver` 按 `biz_grade_weight_config` 匹配后写入 |
| `grade_name` | `varchar(32)` | N | NULL | 品质等级名称快照；与 `grade_code` 同时写入 |

**索引**：`PRIMARY KEY (id)`；`UNIQUE KEY uk_order_ear (order_id, ear_tag)`；`KEY idx_order_id (order_id)`；`KEY idx_supplier (supplier_id)`；`KEY idx_ear_tag (ear_tag)`。

**维护**：新增/修改主表成功后批量插入；`supplier_id` 由服务端从主表带出；修改时 `DELETE ... WHERE order_id=?` 后重插。

### 3.3 关联表：`biz_supplier_settlement`（供应商结算，本模块写入）

> 结算模块查询/完成结算在 **`/tradeMarket/supplierSettlement`** 实现；**仅**在订单完成支付时由 `SettlementOnPayService` 插入一条。

| 字段名 | 类型 | 非空 | 说明 |
| --- | --- | --- | --- |
| `id` | `bigint(20)` | Y | 主键 |
| `settlement_no` | `varchar(14)` | Y | `JSD`+`YYYYMMDD`+3 位序号，**唯一** |
| `order_id` | `bigint(20)` | Y | 关联订单，**唯一**（一对一） |
| `order_no` | `varchar(11)` | Y | 冗余，列表按关联订单号模糊 |
| `supplier_id` | `bigint(20)` | Y | 冗余 |
| `total_heads` | `int(11)` | Y | 来自订单 |
| `total_amount` | `decimal(12,2)` | Y | 来自订单 |
| `fee_method` | `tinyint(4)` | Y | 快照：生成时供应商收费方式 |
| `fee_standard` | `decimal(12,2)` | N | 快照：收费标准（元/头） |
| `transaction_fee_rate` | `decimal(5,2)` | N | 快照：交易费率 % |
| `management_fee_rate` | `decimal(5,2)` | N | 快照：管理费率 % |
| `service_fee_amount` | `decimal(12,2)` | Y | 服务费（元），生成时计算固化 |
| `payable_amount` | `decimal(12,2)` | Y | 实际应付 = 总金额 − 服务费 |
| `settlement_status` | `char(1)` | Y | `0` 待结算 `1` 已完成 |
| `settle_voucher_url` / `settle_voucher_path` | `varchar(512)` | N | 结算凭证（结算模块完成时写入） |
| `settle_finish_time` | `datetime` | N | 结算完成时间 |
| `del_flag` | `char(1)` | Y | `0`/`2` |
| 审计字段 | — | — | 若依惯例 |

**索引**：`UNIQUE KEY uk_order_id (order_id)`；`UNIQUE KEY uk_settlement_no (settlement_no)`；`KEY idx_order_no (order_no)`；`KEY idx_settlement_no (settlement_no)`；`KEY idx_supplier`；`KEY idx_settlement_status`；`KEY idx_del_flag`。

### 3.4 只读关联表

| 表 | 用途 |
| --- | --- |
| `biz_supplier` | 供应商校验、收费快照来源 |
| `biz_distributor` | 承销商校验 |
| `biz_market_entry` + `biz_market_entry_ear_tag` | 耳标池 **M** |
| `biz_isolation_patrol` | 耳标池 **B**（`selectSickDeadEarTagRows`） |
| `biz_grade_weight_config` | 等级重量区间；保存订单明细时匹配 `grade_code` |

### 3.5 枚举

| 字段 | 值 | 含义 |
| --- | --- | --- |
| `order_status` | `0` / `1` | 待支付 / 已完成 |
| `settlement_status` | `0` / `1` | 待结算 / 已完成 |
| `del_flag` | `0` / `2` | 正常 / 已删 |
| `fee_method` | `1` / `2` / `3` | 按户 / 按头 / 按额（同供应商表） |

### 3.6 DDL

见 `sql/biz_trade_order.sql`、`sql/biz_supplier_settlement.sql`、`sql/biz_grade_weight_config.sql`。

---

## 4. 字段命名（JSON 小驼峰）

| JSON | 库表 | 说明 |
| --- | --- | --- |
| `id` | `id` | 主键 |
| `orderNo` | `order_no` | 订单编号（新增响应可返回） |
| `supplierId` | `supplier_id` | |
| `supplierName` | — | 列表/详情 JOIN |
| `distributorId` | `distributor_id` | 承销商 |
| `distributorName` | — | JOIN |
| `totalHeads` | `total_heads` | |
| `totalAmount` | `total_amount` | |
| `orderStatus` | `order_status` | `0`/`1` |
| `orderStatusName` | — | 展示：待支付/已完成 |
| `payVoucherUrl` / `payVoucherPath` | 支付凭证 | 完成支付入参 |
| `finishTime` | `finish_time` | 订单完成时间 |
| `earTag` | `ear_tag` | 明细耳标 |
| `weight` | `weight` | 明细重量 kg |
| `amount` | `amount` | 明细金额 元 |
| `gradeCode` | `grade_code` | 明细等级编码；**响应**返回；**请求体不传**，保存时服务端按重量匹配 |
| `gradeName` | `grade_name` | 明细等级名称快照；**响应**返回；**请求体不传**，与 `gradeCode` 同时写入 |
| `lineList` | 子表 | `[{ earTag, weight, amount }]`（`supplierId`、`gradeCode`、`gradeName` 服务端写入，请求体不传） |
| `orderNo`（筛） | `order_no` | 列表模糊 `LIKE` |

---

## 5. 核心业务实现

### 5.1 订单编号 `TradeOrderNoGenerator`

- 规则：`yyyyMMdd` + 3 位序号（`001`～`999`）。  
- 查询当日 `del_flag=0` 最大序号 +1；冲突重试。  
- 超 999 抛业务异常。

### 5.2 耳标候选 `TradeOrderEarTagResolver`

与功能需求 §4.4 一致，**下拉与保存分流**：

| 方法 | 用途 | SQL / 逻辑 |
| --- | --- | --- |
| `listSupplierEarTagOptions(supplierId)` | `GET /supplierEarTags` | `BizMarketEntryEarTagMapper.selectEarTagsBySupplierIdOnly`：`biz_market_entry_ear_tag` 按 `supplier_id`，`distinct ear_tag` |
| `listEligible(supplierId, excludeOrderId)` | 保存 R3、`completePay` 前复检 | **(M−B)−C** |

| 集合 | SQL 要点（仅 `listEligible`） |
| --- | --- |
| **M** | `biz_market_entry` `del_flag=0` + `supplier_id` → `selectDistinctEarTagsBySupplierId` |
| **B** | `biz_isolation_patrol`：`del_flag=0`、`sick_dead_qty>0`（`selectSickDeadEarTagRows`） |
| **C** | `biz_trade_order_line` JOIN `biz_trade_order`：`del_flag=0`、`order_status in (0,1)`，`order_id <> excludeOrderId` |

`BizTradeOrderServiceImpl.releaseMarketEntryEarTags`：支付成功后 `deleteBySupplierIdAndEarTag`（明细 `supplier_id` + `ear_tag`）。

### 5.2.1 等级匹配 `TradeOrderGradeResolver`

- 配置表：`biz_grade_weight_config`（`sql/biz_grade_weight_config.sql`）。  
- 保存/修改订单、`completePay` 前复检时，在 `TradeOrderValidation.prepareAndValidateForSave` 内对每行明细：  
  - 读取全部配置（`sort_order` 升序）；  
  - 若 `min_weight ≤ weight ≤ max_weight`（闭区间，`BigDecimal.compareTo`），写入 `grade_code`；  
  - 无匹配或配置为空 → 业务异常（`MSG_GRADE_NOT_MATCHED` / `MSG_GRADE_CONFIG_EMPTY`）。  
- 请求体中的 `gradeCode` **忽略**，以服务端匹配结果为准。

### 5.3 保存校验 `TradeOrderValidation`

| 规则 | 实现 |
| --- | --- |
| R1～R2 | 非空 + `biz_supplier` / `biz_distributor` 存在且 `del_flag=0` |
| R3～R4 | 耳标池 + `EarTagCodec` 去重；`weight`/`amount` `> 0` |
| R5～R6 | `totalHeads = lineList.size()`；`totalAmount = sum(amount)` 保留 2 位小数 |
| R7 | `order_status=1` 拒绝 update |
| R8 | `order_no` 唯一 |

### 5.4 完成支付 `completePay`

**接口**：`PUT /tradeMarket/tradeOrder/completePay`（见 §6.8）。

**事务步骤**：

1. 锁订单行或校验 `order_status=0`、`del_flag=0`；  
2. 校验支付凭证后缀/路径（`jpg|jpeg|png`，≤10MB 由上传接口保证）；  
3. 再校验 R3～R6、供应商 `fee_method` 已配置；  
4. `UPDATE` 订单：`order_status=1`，`finish_time=now()`，凭证字段；  
5. 若 `biz_supplier_settlement` 已存在 `order_id` → 抛错（幂等）；  
6. `SettlementOnPayService.createFromOrder(order)`：生成 `settlement_no`，写快照与 `service_fee_amount`、`payable_amount`（`HALF_UP` 2 位小数）。  
7. 按订单明细 `supplier_id` + `ear_tag` 删除 `biz_market_entry_ear_tag` 对应行（释放进栏耳标池，与 `/supplierEarTags` 数据源一致）。

**服务费计算**（`BigDecimal`）：

```text
fee_method=1 → serviceFee = 0
fee_method=2 → serviceFee = totalHeads × feeStandard
fee_method=3 → serviceFee = totalAmount × (transactionFeeRate + managementFeeRate) / 100
payableAmount = totalAmount − serviceFee
```

### 5.5 删除

- 仅 `order_status=0`；`del_flag=2`；  
- **已完成**拒绝；可选：存在结算单时亦拒绝（已完成必存在）。

---

## 6. 接口设计

**Base Path**：`/tradeMarket/tradeOrder`  
**权限**：`tradeMarket:tradeOrder:list|query|add|edit|remove|pay`  
**响应**：`AjaxResult` / `TableDataInfo`

> `/supplierEarTags` 须在 `/{id}` **之前**注册。

| 说明 | HTTP | URI | 权限 |
| --- | --- | --- | --- |
| 分页列表 | GET | `/list` | `list` |
| 详情 | GET | `/{id}` | `query` |
| 新增 | POST | `/` | `add` |
| 修改 | PUT | `/` | `edit` |
| 删除 | DELETE | `/{ids}` | `remove` |
| 耳标候选 | GET | `/supplierEarTags` | `list` 或 `query` |
| 完成支付 | PUT | `/completePay` | `pay` |

### 6.1 列表 `GET /list`

| 参数 | 类型 | 说明 |
| --- | --- | --- |
| `pageNum` / `pageSize` | int | 默认 `1` / `20` |
| `orderNo` | string | 模糊 |
| `supplierId` | long | 精确 |
| `distributorId` | long | 精确 |

- 条件：`del_flag=0`；排序 `create_time DESC, id DESC`。  
- 行字段：§4 所列 + `lineSummary`（可选，首耳标+等 N 头）。

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

- `data`：主表 + `lineList` + 双方名称 + 状态中文。

### 6.3 新增 `POST /`

| Body | 必填 | 说明 |
| --- | --- | --- |
| `supplierId` | Y | |
| `distributorId` | Y | |
| `lineList` | Y | ≥1 行 |
| `totalHeads` / `totalAmount` | N | 服务端覆盖 |
| `remark` | N | |

- 生成 `orderNo`；`order_status=0`；写主表+明细。

### 6.4 修改 `PUT /`

- `id` 必填；仅 `order_status=0`；Body 同新增；`orderNo` 不可改。

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

- 仅待支付；逻辑删除。

### 6.6 耳标候选 `GET /supplierEarTags`

| 参数 | 必填 | 说明 |
| --- | --- | --- |
| `supplierId` | Y | |

- `data`：`string[]`；**仅**按 `supplierId` 查询 `biz_market_entry_ear_tag`（`distinct ear_tag`），不剔除巡查病死、不剔除他单占用。
- 保存时服务端仍执行 **（M−B）−C**（`TradeOrderValidation` / `listEligible`）。

### 6.7 完成支付 `PUT /completePay`

| Body | 必填 | 说明 |
| --- | --- | --- |
| `id` | Y | 订单主键 |
| `payVoucherUrl` | Y | 先 `/common/upload` |
| `payVoucherPath` | Y | |

- 成功：`code=200`；订单已完成；结算单已生成（响应可带 `settlementId` / `settlementNo` 便于跳转）。  
- 失败：订单仍为待支付。

### 6.8 主数据联调（前端）

| 用途 | 接口 |
| --- | --- |
| 供应商搜索 | `GET /tradeMarket/supplier/list`（`supplierName` 模糊） |
| 承销商搜索 | `GET /tradeMarket/distributor/list`（`distributorName` 模糊） |
| 凭证上传 | `POST /common/upload` |

---

## 7. 关联方案：供应商结算管理

> 结算模块完整设计见 **`../供应商结算管理/供应商结算管理技术方案.md`**（v1.0）。

### 7.1 模块边界

| 能力 | 交易订单 | 供应商结算 |
| --- | --- | --- |
| 表 | `biz_trade_order`(+line) | `biz_supplier_settlement` |
| 创建结算单 | `completePay` 写入 | — |
| 列表/完成结算/打印凭证 | — | 独立 Controller |
| 删除订单 | 仅待支付 | 不删结算单 |

### 7.2 结算模块接口（建议，供联调）

**Base Path**：`/tradeMarket/supplierSettlement`  
**权限**：`tradeMarket:supplierSettlement:list|query|settle`

| 说明 | HTTP | URI |
| --- | --- | --- |
| 列表 | GET | `/list` |
| 详情 | GET | `/{id}` |
| 完成结算 | PUT | `/completeSettle` |
| 打印凭证 | 前端 | 详情数据 |

**列表参数**：`settlementNo`、`orderNo` 模糊；`del_flag=0`；排序 `create_time DESC`。

**完成结算 Body**：`id`、`settleVoucherUrl`、`settleVoucherPath`（规则同订单支付凭证）→ `settlement_status=1`，`settle_finish_time=now()`。

### 7.3 状态与一致性

```text
订单 order_status: 0 → 1
结算 settlement_status: (无) → 0 → 1
```

- `uk_order_id` 保证一笔订单一条结算单。  
- 订单已完成后**不回写**订单状态。

### 7.4 打印（前端）

| 功能 | 数据来源 |
| --- | --- |
| 打印小票 | `GET /tradeOrder/{id}`，`orderStatus=0` |
| 打印凭证（订单） | 同上，`orderStatus=1`，含 `payVoucherUrl` |
| 打印凭证（结算） | `GET /supplierSettlement/{id}` |

---

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

| 类型 | 名称 | 权限标识 |
| --- | --- | --- |
| 菜单 | 交易订单管理 | `tradeMarket:tradeOrder:list` |
| 按钮 | 查询 | `tradeMarket:tradeOrder:query` |
| 按钮 | 新增 | `tradeMarket:tradeOrder:add` |
| 按钮 | 修改 | `tradeMarket:tradeOrder:edit` |
| 按钮 | 删除 | `tradeMarket:tradeOrder:remove` |
| 按钮 | 完成支付 | `tradeMarket:tradeOrder:pay` |
| 菜单 | 供应商结算管理 | `tradeMarket:supplierSettlement:list` |

组件路径：`tradeMarket/tradeOrder/index`、`tradeMarket/supplierSettlement/index`。

---

## 9. 交付清单

- [ ] `sql/biz_trade_order.sql`
- [ ] `sql/biz_supplier_settlement.sql`
- [ ] `BizTradeOrder*`、`TradeOrderValidation`、`TradeOrderEarTagResolver`、`SettlementOnPayService`
- [ ] `BizSupplierSettlement*`（结算查询与完成结算，可二期与订单并行）
- [ ] 单元测试 / MockMvc

---

## 10. 修订记录

| 版本 | 说明 |
| --- | --- |
| 1.0 | 初稿：订单主从表、完成支付生成结算、耳标 M−B−C、接口与结算关联方案 |
| 1.1 | `biz_trade_order_line`：`weight_kg`→`weight`，`amount_yuan`→`amount` |
| 1.2 | `biz_trade_order_line` 增加 `supplier_id` |
| 1.3 | 完成支付后按 `supplier_id`+`ear_tag` 删除 `biz_market_entry_ear_tag` |
| 1.4 | 与实现对齐：`/supplierEarTags` 仅子表；§5.2 下拉/保存分流；功能需求 v1.2 |
| 1.5 | §7 引用《供应商结算管理技术方案》v1.0 |
| 1.6 | `biz_trade_order_line.grade_code`；`TradeOrderGradeResolver` + `biz_grade_weight_config` |
| 1.7 | `biz_trade_order_line.grade_name`；保存时与 `grade_code` 一并写入 |
