feat(bid): 新增历史报价参考功能并修复多租户与数据库约束问题
1. 为比价功能新增历史报价查询逻辑,当物料无当前RFQ报价时补充同物料其他RFQ的最近报价 2. 修复BizComparisonController的多租户ID获取逻辑,兼容无租户场景 3. 扩展报价单状态范围,将draft状态纳入有效报价统计 4. 新增发货单相关数据库表与修复脚本,修正biz_delivery_order的非空约束问题 5. 优化前端比价页面布局,移除旧的卡片式报价展示,统一使用表格展示 6. 修复类型转换与空指针风险,完善工具类方法的兼容性处理 7. 优化评分排序与徽章标记逻辑,避免覆盖自定义徽章
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,7 @@ import org.apache.ibatis.annotations.Param;
|
||||
public interface BizComparisonMapper {
|
||||
List<Map<String, Object>> selectComparisonData(@Param("rfqId") Long rfqId);
|
||||
Map<String, Object> selectRfqInfo(@Param("rfqId") Long rfqId);
|
||||
List<Map<String, Object>> selectHistoricalPrices(@Param("materialName") String materialName,
|
||||
@Param("excludeRfqId") Long excludeRfqId,
|
||||
@Param("limit") int limit);
|
||||
}
|
||||
|
||||
@@ -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<Map<String, Object>> histRows = mapper.selectHistoricalPrices(materialName, rfqId, 3);
|
||||
if (histRows == null || histRows.isEmpty()) continue;
|
||||
|
||||
List<BizComparisonVO.SupplierPrice> histPrices = new ArrayList<>();
|
||||
for (Map<String, Object> 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<BizComparisonVO.SupplierPrice> 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; }
|
||||
|
||||
@@ -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}
|
||||
</select>
|
||||
|
||||
<!-- 历史报价参考:当前RFQ无报价时,查询同物料在其他RFQ中的最近报价 -->
|
||||
<select id="selectHistoricalPrices" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
qi.unit_price AS unitPrice,
|
||||
qi.total_price AS totalPrice,
|
||||
qi.delivery_days AS deliveryDays,
|
||||
q.quotation_id AS quotationId,
|
||||
q.quote_no AS quoteNo,
|
||||
q.supplier_id AS supplierId,
|
||||
s.supplier_name AS supplierName,
|
||||
r.rfq_no AS rfqNo,
|
||||
COALESCE(ev.avg_quality, 3.0) AS avgQuality,
|
||||
COALESCE(ev.avg_service, 3.0) AS avgService,
|
||||
COALESCE(ev.eval_count, 0) AS historyCount
|
||||
FROM biz_quotation_item qi
|
||||
JOIN biz_quotation q ON qi.quotation_id = q.quotation_id
|
||||
AND q.status IN ('submitted','accepted')
|
||||
JOIN biz_supplier s ON s.supplier_id = q.supplier_id
|
||||
LEFT JOIN (
|
||||
SELECT supplier_id,
|
||||
AVG(quality_score) AS avg_quality,
|
||||
AVG(service_score) AS avg_service,
|
||||
COUNT(*) AS eval_count
|
||||
FROM biz_supplier_evaluation
|
||||
GROUP BY supplier_id
|
||||
) ev ON ev.supplier_id = q.supplier_id
|
||||
LEFT JOIN biz_rfq r ON q.rfq_id = r.rfq_id
|
||||
WHERE qi.material_name = #{materialName}
|
||||
AND q.rfq_id != #{excludeRfqId}
|
||||
ORDER BY q.create_time DESC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -28,14 +28,6 @@
|
||||
|
||||
<div v-if="!item.prices || !item.prices.length" class="no-quote">暂无供应商报价</div>
|
||||
<div v-else>
|
||||
<div class="supplier-cards">
|
||||
<div v-for="(sp, si) in item.prices" :key="sp.supplierId"
|
||||
class="supplier-card">
|
||||
<div class="card-supplier">{{ sp.supplierName }}</div>
|
||||
<div class="card-price">¥{{ sp.unitPrice }} / {{ item.unit }}</div>
|
||||
<div class="card-meta">交货 {{ sp.deliveryDays }} 天<span v-if="sp.historyCount > 0"> · 历史 {{ sp.historyCount }} 次</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="item.prices" border size="small" class="detail-table">
|
||||
<el-table-column label="供应商" prop="supplierName" min-width="140" />
|
||||
<el-table-column label="报价单号" prop="quoteNo" width="130" />
|
||||
@@ -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; }
|
||||
|
||||
@@ -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(),'','',''),
|
||||
|
||||
69
sql/fix_delivery_order_rfq_null.sql
Normal file
69
sql/fix_delivery_order_rfq_null.sql
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user