feat(bid): 新增历史报价参考功能并修复多租户与数据库约束问题

1.  为比价功能新增历史报价查询逻辑,当物料无当前RFQ报价时补充同物料其他RFQ的最近报价
2.  修复BizComparisonController的多租户ID获取逻辑,兼容无租户场景
3.  扩展报价单状态范围,将draft状态纳入有效报价统计
4.  新增发货单相关数据库表与修复脚本,修正biz_delivery_order的非空约束问题
5.  优化前端比价页面布局,移除旧的卡片式报价展示,统一使用表格展示
6.  修复类型转换与空指针风险,完善工具类方法的兼容性处理
7.  优化评分排序与徽章标记逻辑,避免覆盖自定义徽章
This commit is contained in:
2026-06-17 02:54:35 +08:00
parent f5b91c3bd0
commit 38f6246090
7 changed files with 204 additions and 21 deletions

View File

@@ -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);
}

View File

@@ -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; }

View File

@@ -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>