diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/bid/BizComparisonController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/bid/BizComparisonController.java index 08e4536c..f43d8bf2 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/bid/BizComparisonController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/bid/BizComparisonController.java @@ -17,6 +17,8 @@ public class BizComparisonController extends BaseController { @PreAuthorize("@ss.hasPermi('bid:comparison:list')") @GetMapping("/rfq/{rfqId}") public AjaxResult compareRfq(@PathVariable Long rfqId) { - return success(service.compareRfq(rfqId, 1L)); + Long tenantId = getDeptId(); + if (tenantId == null) tenantId = 1L; + return success(service.compareRfq(rfqId, tenantId)); } } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/bid/BizComparisonMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/bid/BizComparisonMapper.java index 82ec96e9..ae8c927a 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/bid/BizComparisonMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/bid/BizComparisonMapper.java @@ -7,4 +7,7 @@ import org.apache.ibatis.annotations.Param; public interface BizComparisonMapper { List> selectComparisonData(@Param("rfqId") Long rfqId); Map selectRfqInfo(@Param("rfqId") Long rfqId); + List> selectHistoricalPrices(@Param("materialName") String materialName, + @Param("excludeRfqId") Long excludeRfqId, + @Param("limit") int limit); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/bid/impl/BizComparisonServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/bid/impl/BizComparisonServiceImpl.java index 5bfea4da..e780acca 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/bid/impl/BizComparisonServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/bid/impl/BizComparisonServiceImpl.java @@ -73,6 +73,37 @@ public class BizComparisonServiceImpl implements IBizComparisonService { } } + // ── Step 1.5: 对无报价的物料,从其他RFQ的历史报价中补充参考价 ────── + for (BizComparisonVO vo : itemMap.values()) { + if (vo.getPrices() != null && !vo.getPrices().isEmpty()) continue; + String materialName = vo.getMaterialName(); + if (materialName == null || materialName.isEmpty()) continue; + + List> histRows = mapper.selectHistoricalPrices(materialName, rfqId, 3); + if (histRows == null || histRows.isEmpty()) continue; + + List histPrices = new ArrayList<>(); + for (Map row : histRows) { + BizComparisonVO.SupplierPrice sp = new BizComparisonVO.SupplierPrice(); + sp.setSupplierId(toLong(row.get("supplierId"))); + sp.setSupplierName(str(row.get("supplierName"))); + sp.setQuotationId(toLong(row.get("quotationId"))); + sp.setQuoteNo(str(row.get("quoteNo"))); + sp.setUnitPrice(toBD(row.get("unitPrice"))); + sp.setTotalPrice(toBD(row.get("totalPrice"))); + sp.setDeliveryDays(row.get("deliveryDays") == null ? 30 + : ((Number) row.get("deliveryDays")).intValue()); + sp.setEvalAvgScore(toDouble(row.get("avgQuality"))); + sp.setHistoryCount(row.get("historyCount") == null ? 0 + : ((Number) row.get("historyCount")).intValue()); + sp.setQualityScore(toDouble(row.get("avgQuality"))); + sp.setServiceScore(toDouble(row.get("avgService"))); + sp.setRankBadge("历史参考"); + histPrices.add(sp); + } + vo.setPrices(histPrices); + } + // ── Step 2: 多维归一化评分 ─────────────────────────────────────────── for (BizComparisonVO vo : itemMap.values()) { List prices = vo.getPrices(); @@ -115,15 +146,15 @@ public class BizComparisonServiceImpl implements IBizComparisonService { + W_QUALITY * qualityS + W_SERVICE * serviceS; sp.setCompositeScore(round2(composite)); - // 最低价标记 - sp.setLowestPrice(minPrice.compareTo(sp.getUnitPrice()) == 0); + // 最低价标记 (unitPrice 可能为空,避免 NPE) + sp.setLowestPrice(sp.getUnitPrice() != null && minPrice.compareTo(sp.getUnitPrice()) == 0); } // ── 排序 + 标记徽章 ── prices.sort(Comparator.comparingDouble(BizComparisonVO.SupplierPrice::getCompositeScore).reversed()); if (!prices.isEmpty()) { BizComparisonVO.SupplierPrice best = prices.get(0); - best.setRankBadge("综合最优"); + if (best.getRankBadge() == null) best.setRankBadge("综合最优"); } // 价格最低标记 prices.stream().filter(BizComparisonVO.SupplierPrice::isLowestPrice) @@ -211,7 +242,12 @@ public class BizComparisonServiceImpl implements IBizComparisonService { // ── Utilities ───────────────────────────────────────────────────────────── private Long toLong(Object v) { return v == null ? null : ((Number)v).longValue(); } - private BigDecimal toBD(Object v) { return v == null ? null : new BigDecimal(v.toString()); } + private BigDecimal toBD(Object v) { + if (v == null) return null; + if (v instanceof BigDecimal) return (BigDecimal) v; + if (v instanceof Number) return BigDecimal.valueOf(((Number) v).doubleValue()); + return new BigDecimal(v.toString()); + } private String str(Object v) { return v == null ? "" : v.toString(); } private double toDouble(Object v) { return v == null ? 3.0 : ((Number)v).doubleValue(); } private double round2(double v) { return Math.round(v * 100) / 100.0; } diff --git a/ruoyi-system/src/main/resources/mapper/bid/BizComparisonMapper.xml b/ruoyi-system/src/main/resources/mapper/bid/BizComparisonMapper.xml index 1b51e51d..46ef173b 100644 --- a/ruoyi-system/src/main/resources/mapper/bid/BizComparisonMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/bid/BizComparisonMapper.xml @@ -25,7 +25,7 @@ FROM biz_rfq_item ri LEFT JOIN biz_quotation_item qi ON qi.rfq_item_id = ri.item_id LEFT JOIN biz_quotation q ON q.quotation_id = qi.quotation_id - AND q.status IN ('submitted','accepted') + AND q.status IN ('draft','submitted','accepted') LEFT JOIN biz_supplier s ON s.supplier_id = q.supplier_id LEFT JOIN ( SELECT supplier_id, @@ -45,4 +45,37 @@ FROM biz_rfq WHERE rfq_id = #{rfqId} + + + diff --git a/ruoyi-ui/src/views/bid/comparison/detail.vue b/ruoyi-ui/src/views/bid/comparison/detail.vue index 573451cc..caf2f189 100644 --- a/ruoyi-ui/src/views/bid/comparison/detail.vue +++ b/ruoyi-ui/src/views/bid/comparison/detail.vue @@ -28,14 +28,6 @@
暂无供应商报价
-
-
-
{{ sp.supplierName }}
-
¥{{ sp.unitPrice }} / {{ item.unit }}
-
交货 {{ sp.deliveryDays }} 天 · 历史 {{ sp.historyCount }} 次
-
-
@@ -169,12 +161,24 @@ export default { }; }, created() { - const rfqId = this.$route.query.rfqId; - if (rfqId) this.loadData(rfqId); + this.loadByRoute(); + }, + watch: { + /** 路由 query 变化时重新加载(不同 RFQ 切换时组件复用,created 不会再次触发) */ + '$route.query.rfqId': function(newId) { + if (newId) this.loadData(newId); + } }, methods: { + loadByRoute() { + const rfqId = this.$route.query.rfqId; + if (rfqId) this.loadData(rfqId); + }, loadData(rfqId) { this.loading = true; + this.result = null; + this.planSelected = {}; + this.selectedRows = {}; compareRfq(rfqId).then(r => { this.result = r.data; // init selection = all items @@ -253,11 +257,6 @@ export default { .item-spec { color:rgba(255,255,255,.8); font-size:13px; } .item-qty { margin-left:auto; font-size:12px; color:rgba(255,255,255,.8); } .no-quote { padding:20px; color:#909399; background:#fafafa; border:1px dashed #e4e7ed; border-radius:4px; text-align:center; } -.supplier-cards { display:flex; gap:12px; padding:16px 0; overflow-x:auto; } -.supplier-card { flex:0 0 200px; border:1px solid #e4e7ed; border-radius:8px; padding:14px; background:#fff; &:hover { box-shadow:0 4px 16px rgba(0,0,0,.1); } } -.card-supplier { font-size:13px; font-weight:600; color:#303133; margin-bottom:6px; } -.card-price { font-size:13px; color:#e4393c; font-weight:600; margin-bottom:10px; } -.card-meta { font-size:11px; color:#c0c4cc; margin-top:8px; } .price-low { color:#67c23a; font-weight:700; } .plan-card { border:1px solid #e4e7ed; border-radius:8px; margin-bottom:24px; overflow:hidden; background:#fff; position:relative; } .plan-header { display:flex; align-items:center; justify-content:space-between; padding:14px 20px; background:linear-gradient(90deg,#fafafa,#fff); border-bottom:1px solid #e4e7ed; flex-wrap:wrap; gap:10px; } diff --git a/sql/bid_tables.sql b/sql/bid_tables.sql index 2059fcc2..6a9a9498 100644 --- a/sql/bid_tables.sql +++ b/sql/bid_tables.sql @@ -222,6 +222,47 @@ CREATE TABLE IF NOT EXISTS biz_transaction ( PRIMARY KEY (tx_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易记录'; +-- ═══════════════════════════════════════════════════════════════ +-- 发货单(注:该表需手动创建,此前未包含在 DDL 中) +-- ═══════════════════════════════════════════════════════════════ +CREATE TABLE IF NOT EXISTS biz_delivery_order ( + do_id BIGINT NOT NULL AUTO_INCREMENT, + tenant_id BIGINT NOT NULL DEFAULT 1, + do_no VARCHAR(50) NOT NULL COMMENT '发货单号', + type VARCHAR(20) DEFAULT NULL COMMENT '类型(supplier/client)', + rfq_id BIGINT DEFAULT NULL COMMENT '关联RFQ ID(client类型可为空)', + quotation_id BIGINT DEFAULT NULL COMMENT '关联报价单ID', + client_quote_id BIGINT DEFAULT NULL COMMENT '关联甲方报价单ID', + supplier_id BIGINT DEFAULT NULL COMMENT '供应商ID(client类型可为空)', + total_amount DECIMAL(15,4) DEFAULT 0 COMMENT '总金额', + currency VARCHAR(10) DEFAULT 'CNY', + delivery_date DATE DEFAULT NULL COMMENT '交货日期', + delay_date DATE DEFAULT NULL COMMENT '延期日期', + actual_close_date DATE DEFAULT NULL COMMENT '实际结单日期', + close_date_set_by VARCHAR(64) DEFAULT '' COMMENT '结单设置人', + delivery_status VARCHAR(20) DEFAULT 'pending' COMMENT '状态(pending/transit/history/confirmed/rejected)', + remark VARCHAR(500) DEFAULT NULL, + create_by VARCHAR(64) DEFAULT '', + create_time DATETIME, + update_by VARCHAR(64) DEFAULT '', + update_time DATETIME, + PRIMARY KEY (do_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='发货单'; + +CREATE TABLE IF NOT EXISTS biz_delivery_order_item ( + item_id BIGINT NOT NULL AUTO_INCREMENT, + do_id BIGINT NOT NULL COMMENT '发货单ID', + material_id BIGINT DEFAULT 0, + material_name VARCHAR(200) DEFAULT '', + spec VARCHAR(500) DEFAULT '', + unit VARCHAR(50) DEFAULT '', + quantity DECIMAL(15,4) DEFAULT 0, + unit_price DECIMAL(15,4) DEFAULT 0, + total_price DECIMAL(15,4) DEFAULT 0, + remark VARCHAR(500) DEFAULT '', + PRIMARY KEY (item_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='发货单明细'; + INSERT IGNORE INTO sys_menu(menu_id,menu_name,parent_id,order_num,path,component,query,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,update_by,update_time,remark) VALUES (2000,'智慧报价',0,5,'bid',NULL,NULL,1,0,'M','0','0','','#','admin',NOW(),'','',''), diff --git a/sql/fix_delivery_order_rfq_null.sql b/sql/fix_delivery_order_rfq_null.sql new file mode 100644 index 00000000..3cd1f7f3 --- /dev/null +++ b/sql/fix_delivery_order_rfq_null.sql @@ -0,0 +1,69 @@ +-- ═══════════════════════════════════════════════════════════ +-- Fix: 发货单(biz_delivery_order) rfq_id 非空约束修复 +-- +-- 问题描述: +-- client 类型发货单不关联 RFQ(询价单),rfq_id 应为 NULL +-- 若表存在 NOT NULL 约束,会导致 SQLIntegrityConstraintViolationException +-- +-- 修复: +-- 1) 确保 rfq_id 列允许 NULL +-- 2) 补充缺失的 DDL(该表在项目中无建表脚本) +-- ═══════════════════════════════════════════════════════════ + +SET NAMES utf8mb4; + +-- 1. 检查并移除 rfq_id 上的 NOT NULL 约束 +-- (MySQL 中通过 MODIFY COLUMN 去掉 NOT NULL) +DROP PROCEDURE IF EXISTS fix_rfq_nullable; +DELIMITER // +CREATE PROCEDURE fix_rfq_nullable() +BEGIN + DECLARE col_is_nullable VARCHAR(3); + SELECT IS_NULLABLE INTO col_is_nullable + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'biz_delivery_order' + AND COLUMN_NAME = 'rfq_id'; + + IF col_is_nullable = 'NO' THEN + ALTER TABLE biz_delivery_order MODIFY COLUMN rfq_id BIGINT DEFAULT NULL COMMENT '关联RFQ ID(client类型可为空)'; + SELECT 'rfq_id 已从 NOT NULL 改为 NULL' AS info; + ELSE + SELECT 'rfq_id 已经是可空状态' AS info; + END IF; +END // +DELIMITER ; +CALL fix_rfq_nullable(); +DROP PROCEDURE IF EXISTS fix_rfq_nullable; + +-- 2. 同理确保 supplier_id 允许 NULL(client 类型没有供应商) +DROP PROCEDURE IF EXISTS fix_supplier_nullable; +DELIMITER // +CREATE PROCEDURE fix_supplier_nullable() +BEGIN + DECLARE col_is_nullable VARCHAR(3); + SELECT IS_NULLABLE INTO col_is_nullable + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'biz_delivery_order' + AND COLUMN_NAME = 'supplier_id'; + + IF col_is_nullable = 'NO' THEN + ALTER TABLE biz_delivery_order MODIFY COLUMN supplier_id BIGINT DEFAULT NULL COMMENT '供应商ID(client类型可为空)'; + SELECT 'supplier_id 已从 NOT NULL 改为 NULL' AS info; + ELSE + SELECT 'supplier_id 已经是可空状态' AS info; + END IF; +END // +DELIMITER ; +CALL fix_supplier_nullable(); +DROP PROCEDURE IF EXISTS fix_supplier_nullable; + +-- 3. 验证结果 +SELECT COLUMN_NAME, IS_NULLABLE, COLUMN_TYPE, COLUMN_COMMENT +FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'biz_delivery_order' + AND COLUMN_NAME IN ('rfq_id', 'supplier_id', 'quotation_id', 'client_quote_id'); + +SELECT '修复完成' AS result;