# 交易订单管理 — 技术方案 > 依据:`交易订单管理功能需求.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` 一并写入 |