feat(bid): 新增投标报表统计分析模块
本次提交新增了完整的投标报表统计分析功能,包括: 添加用于数据检查与菜单初始化的 SQL 脚本 实现采购概览仪表板、采购成本分析及供应商绩效报告的后端服务、Mapper、Controller 及 VO 类 添加前端 API、路由配置以及使用 ECharts 可视化图表的页面组件 为仪表板添加通用的 KPI 卡片组件
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
package com.ruoyi.system.domain.bid;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 采购成本分析 VO
|
||||
*/
|
||||
public class ReportCostVO {
|
||||
|
||||
/** 顶部汇总 */
|
||||
private CostSummary summary;
|
||||
|
||||
/** 月度成本趋势 */
|
||||
private List<CostTrend> costTrend;
|
||||
|
||||
/** 品类采购分布 */
|
||||
private List<CategoryDist> categoryDist;
|
||||
|
||||
/** RFQ 比价明细 */
|
||||
private List<RfqCompareDetail> rfqDetails;
|
||||
|
||||
// ===== getters / setters =====
|
||||
|
||||
public CostSummary getSummary() { return summary; }
|
||||
public void setSummary(CostSummary v) { summary = v; }
|
||||
|
||||
public List<CostTrend> getCostTrend() { return costTrend; }
|
||||
public void setCostTrend(List<CostTrend> v) { costTrend = v; }
|
||||
|
||||
public List<CategoryDist> getCategoryDist() { return categoryDist; }
|
||||
public void setCategoryDist(List<CategoryDist> v) { categoryDist = v; }
|
||||
|
||||
public List<RfqCompareDetail> getRfqDetails() { return rfqDetails; }
|
||||
public void setRfqDetails(List<RfqCompareDetail> v) { rfqDetails = v; }
|
||||
|
||||
// ===== 内部类 =====
|
||||
|
||||
/** 成本汇总 */
|
||||
public static class CostSummary {
|
||||
private BigDecimal totalExpected;
|
||||
private BigDecimal totalActual;
|
||||
private BigDecimal savedAmount;
|
||||
private double savedRate;
|
||||
|
||||
public BigDecimal getTotalExpected() { return totalExpected; }
|
||||
public void setTotalExpected(BigDecimal v) { totalExpected = v; }
|
||||
public BigDecimal getTotalActual() { return totalActual; }
|
||||
public void setTotalActual(BigDecimal v) { totalActual = v; }
|
||||
public BigDecimal getSavedAmount() { return savedAmount; }
|
||||
public void setSavedAmount(BigDecimal v) { savedAmount = v; }
|
||||
public double getSavedRate() { return savedRate; }
|
||||
public void setSavedRate(double v) { savedRate = v; }
|
||||
}
|
||||
|
||||
/** 月度成本趋势 */
|
||||
public static class CostTrend {
|
||||
private String month;
|
||||
private BigDecimal expectedAmount;
|
||||
private BigDecimal actualAmount;
|
||||
private BigDecimal savedAmount;
|
||||
|
||||
public String getMonth() { return month; }
|
||||
public void setMonth(String v) { month = v; }
|
||||
public BigDecimal getExpectedAmount() { return expectedAmount; }
|
||||
public void setExpectedAmount(BigDecimal v) { expectedAmount = v; }
|
||||
public BigDecimal getActualAmount() { return actualAmount; }
|
||||
public void setActualAmount(BigDecimal v) { actualAmount = v; }
|
||||
public BigDecimal getSavedAmount() { return savedAmount; }
|
||||
public void setSavedAmount(BigDecimal v) { savedAmount = v; }
|
||||
}
|
||||
|
||||
/** 品类分布 */
|
||||
public static class CategoryDist {
|
||||
private String categoryName;
|
||||
private BigDecimal amount;
|
||||
private int materialCount;
|
||||
private double percent;
|
||||
|
||||
public String getCategoryName() { return categoryName; }
|
||||
public void setCategoryName(String v) { categoryName = v; }
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
public void setAmount(BigDecimal v) { amount = v; }
|
||||
public int getMaterialCount() { return materialCount; }
|
||||
public void setMaterialCount(int v) { materialCount = v; }
|
||||
public double getPercent() { return percent; }
|
||||
public void setPercent(double v) { percent = v; }
|
||||
}
|
||||
|
||||
/** RFQ 比价明细 */
|
||||
public static class RfqCompareDetail {
|
||||
private Long rfqId;
|
||||
private String rfqNo;
|
||||
private String rfqTitle;
|
||||
private BigDecimal expectedTotal;
|
||||
private BigDecimal lowestQuote;
|
||||
private BigDecimal acceptedQuote;
|
||||
private BigDecimal savedAmount;
|
||||
private int supplierCount;
|
||||
|
||||
public Long getRfqId() { return rfqId; }
|
||||
public void setRfqId(Long v) { rfqId = v; }
|
||||
public String getRfqNo() { return rfqNo; }
|
||||
public void setRfqNo(String v) { rfqNo = v; }
|
||||
public String getRfqTitle() { return rfqTitle; }
|
||||
public void setRfqTitle(String v) { rfqTitle = v; }
|
||||
public BigDecimal getExpectedTotal() { return expectedTotal; }
|
||||
public void setExpectedTotal(BigDecimal v) { expectedTotal = v; }
|
||||
public BigDecimal getLowestQuote() { return lowestQuote; }
|
||||
public void setLowestQuote(BigDecimal v) { lowestQuote = v; }
|
||||
public BigDecimal getAcceptedQuote() { return acceptedQuote; }
|
||||
public void setAcceptedQuote(BigDecimal v) { acceptedQuote = v; }
|
||||
public BigDecimal getSavedAmount() { return savedAmount; }
|
||||
public void setSavedAmount(BigDecimal v) { savedAmount = v; }
|
||||
public int getSupplierCount() { return supplierCount; }
|
||||
public void setSupplierCount(int v) { supplierCount = v; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.ruoyi.system.domain.bid;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 采购总览看板 VO
|
||||
*/
|
||||
public class ReportDashboardVO {
|
||||
|
||||
/** KPI 卡片 */
|
||||
private KpiCard totalPurchaseAmount;
|
||||
private KpiCard totalRfqCount;
|
||||
private KpiCard totalPoCount;
|
||||
private KpiCard activeSupplierCount;
|
||||
|
||||
/** 月度采购趋势 */
|
||||
private List<MonthTrend> monthlyTrend;
|
||||
|
||||
/** RFQ 状态分布 */
|
||||
private List<StatusDist> rfqStatusDist;
|
||||
|
||||
/** Top 供应商排名 */
|
||||
private List<SupplierRank> topSuppliers;
|
||||
|
||||
/** 最近动态 */
|
||||
private List<RecentActivity> recentActivities;
|
||||
|
||||
// ===== getters / setters =====
|
||||
|
||||
public KpiCard getTotalPurchaseAmount() { return totalPurchaseAmount; }
|
||||
public void setTotalPurchaseAmount(KpiCard v) { totalPurchaseAmount = v; }
|
||||
|
||||
public KpiCard getTotalRfqCount() { return totalRfqCount; }
|
||||
public void setTotalRfqCount(KpiCard v) { totalRfqCount = v; }
|
||||
|
||||
public KpiCard getTotalPoCount() { return totalPoCount; }
|
||||
public void setTotalPoCount(KpiCard v) { totalPoCount = v; }
|
||||
|
||||
public KpiCard getActiveSupplierCount() { return activeSupplierCount; }
|
||||
public void setActiveSupplierCount(KpiCard v) { activeSupplierCount = v; }
|
||||
|
||||
public List<MonthTrend> getMonthlyTrend() { return monthlyTrend; }
|
||||
public void setMonthlyTrend(List<MonthTrend> v) { monthlyTrend = v; }
|
||||
|
||||
public List<StatusDist> getRfqStatusDist() { return rfqStatusDist; }
|
||||
public void setRfqStatusDist(List<StatusDist> v) { rfqStatusDist = v; }
|
||||
|
||||
public List<SupplierRank> getTopSuppliers() { return topSuppliers; }
|
||||
public void setTopSuppliers(List<SupplierRank> v) { topSuppliers = v; }
|
||||
|
||||
public List<RecentActivity> getRecentActivities() { return recentActivities; }
|
||||
public void setRecentActivities(List<RecentActivity> v) { recentActivities = v; }
|
||||
|
||||
// ===== 内部类 =====
|
||||
|
||||
/** KPI 指标卡 */
|
||||
public static class KpiCard {
|
||||
private String label;
|
||||
private BigDecimal value;
|
||||
private double changeRate;
|
||||
private String unit;
|
||||
private String trend; // up / down
|
||||
|
||||
public String getLabel() { return label; }
|
||||
public void setLabel(String v) { label = v; }
|
||||
public BigDecimal getValue() { return value; }
|
||||
public void setValue(BigDecimal v) { value = v; }
|
||||
public double getChangeRate() { return changeRate; }
|
||||
public void setChangeRate(double v) { changeRate = v; }
|
||||
public String getUnit() { return unit; }
|
||||
public void setUnit(String v) { unit = v; }
|
||||
public String getTrend() { return trend; }
|
||||
public void setTrend(String v) { trend = v; }
|
||||
}
|
||||
|
||||
/** 月度趋势 */
|
||||
public static class MonthTrend {
|
||||
private String month;
|
||||
private BigDecimal amount;
|
||||
private int count;
|
||||
|
||||
public String getMonth() { return month; }
|
||||
public void setMonth(String v) { month = v; }
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
public void setAmount(BigDecimal v) { amount = v; }
|
||||
public int getCount() { return count; }
|
||||
public void setCount(int v) { count = v; }
|
||||
}
|
||||
|
||||
/** 状态分布 */
|
||||
public static class StatusDist {
|
||||
private String status;
|
||||
private String statusLabel;
|
||||
private int count;
|
||||
private double percent;
|
||||
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String v) { status = v; }
|
||||
public String getStatusLabel() { return statusLabel; }
|
||||
public void setStatusLabel(String v) { statusLabel = v; }
|
||||
public int getCount() { return count; }
|
||||
public void setCount(int v) { count = v; }
|
||||
public double getPercent() { return percent; }
|
||||
public void setPercent(double v) { percent = v; }
|
||||
}
|
||||
|
||||
/** 供应商排名 */
|
||||
public static class SupplierRank {
|
||||
private Long supplierId;
|
||||
private String supplierName;
|
||||
private BigDecimal totalAmount;
|
||||
private int poCount;
|
||||
private double avgScore;
|
||||
|
||||
public Long getSupplierId() { return supplierId; }
|
||||
public void setSupplierId(Long v) { supplierId = v; }
|
||||
public String getSupplierName() { return supplierName; }
|
||||
public void setSupplierName(String v) { supplierName = v; }
|
||||
public BigDecimal getTotalAmount() { return totalAmount; }
|
||||
public void setTotalAmount(BigDecimal v) { totalAmount = v; }
|
||||
public int getPoCount() { return poCount; }
|
||||
public void setPoCount(int v) { poCount = v; }
|
||||
public double getAvgScore() { return avgScore; }
|
||||
public void setAvgScore(double v) { avgScore = v; }
|
||||
}
|
||||
|
||||
/** 最近动态 */
|
||||
public static class RecentActivity {
|
||||
private String time;
|
||||
private String type;
|
||||
private String desc;
|
||||
private String linkId;
|
||||
|
||||
public String getTime() { return time; }
|
||||
public void setTime(String v) { time = v; }
|
||||
public String getType() { return type; }
|
||||
public void setType(String v) { type = v; }
|
||||
public String getDesc() { return desc; }
|
||||
public void setDesc(String v) { desc = v; }
|
||||
public String getLinkId() { return linkId; }
|
||||
public void setLinkId(String v) { linkId = v; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.ruoyi.system.domain.bid;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 供应商绩效 VO
|
||||
*/
|
||||
public class ReportSupplierVO {
|
||||
|
||||
/** 评分排名 */
|
||||
private List<SupplierScore> rankings;
|
||||
|
||||
/** 雷达图数据 */
|
||||
private List<RadarData> radarData;
|
||||
|
||||
/** 中标率 */
|
||||
private List<WinRateData> winRateData;
|
||||
|
||||
/** 异议统计 */
|
||||
private List<ObjectionStat> objectionStats;
|
||||
|
||||
// ===== getters / setters =====
|
||||
|
||||
public List<SupplierScore> getRankings() { return rankings; }
|
||||
public void setRankings(List<SupplierScore> v) { rankings = v; }
|
||||
|
||||
public List<RadarData> getRadarData() { return radarData; }
|
||||
public void setRadarData(List<RadarData> v) { radarData = v; }
|
||||
|
||||
public List<WinRateData> getWinRateData() { return winRateData; }
|
||||
public void setWinRateData(List<WinRateData> v) { winRateData = v; }
|
||||
|
||||
public List<ObjectionStat> getObjectionStats() { return objectionStats; }
|
||||
public void setObjectionStats(List<ObjectionStat> v) { objectionStats = v; }
|
||||
|
||||
// ===== 内部类 =====
|
||||
|
||||
/** 供应商评分 */
|
||||
public static class SupplierScore {
|
||||
private Long supplierId;
|
||||
private String supplierName;
|
||||
private int evalCount;
|
||||
private double qualityAvg;
|
||||
private double deliveryAvg;
|
||||
private double serviceAvg;
|
||||
private double priceAvg;
|
||||
private double totalAvg;
|
||||
private int poCount;
|
||||
private BigDecimal poAmount;
|
||||
|
||||
public Long getSupplierId() { return supplierId; }
|
||||
public void setSupplierId(Long v) { supplierId = v; }
|
||||
public String getSupplierName() { return supplierName; }
|
||||
public void setSupplierName(String v) { supplierName = v; }
|
||||
public int getEvalCount() { return evalCount; }
|
||||
public void setEvalCount(int v) { evalCount = v; }
|
||||
public double getQualityAvg() { return qualityAvg; }
|
||||
public void setQualityAvg(double v) { qualityAvg = v; }
|
||||
public double getDeliveryAvg() { return deliveryAvg; }
|
||||
public void setDeliveryAvg(double v) { deliveryAvg = v; }
|
||||
public double getServiceAvg() { return serviceAvg; }
|
||||
public void setServiceAvg(double v) { serviceAvg = v; }
|
||||
public double getPriceAvg() { return priceAvg; }
|
||||
public void setPriceAvg(double v) { priceAvg = v; }
|
||||
public double getTotalAvg() { return totalAvg; }
|
||||
public void setTotalAvg(double v) { totalAvg = v; }
|
||||
public int getPoCount() { return poCount; }
|
||||
public void setPoCount(int v) { poCount = v; }
|
||||
public BigDecimal getPoAmount() { return poAmount; }
|
||||
public void setPoAmount(BigDecimal v) { poAmount = v; }
|
||||
}
|
||||
|
||||
/** 雷达图 */
|
||||
public static class RadarData {
|
||||
private String supplierName;
|
||||
private double quality;
|
||||
private double delivery;
|
||||
private double service;
|
||||
private double price;
|
||||
|
||||
public String getSupplierName() { return supplierName; }
|
||||
public void setSupplierName(String v) { supplierName = v; }
|
||||
public double getQuality() { return quality; }
|
||||
public void setQuality(double v) { quality = v; }
|
||||
public double getDelivery() { return delivery; }
|
||||
public void setDelivery(double v) { delivery = v; }
|
||||
public double getService() { return service; }
|
||||
public void setService(double v) { service = v; }
|
||||
public double getPrice() { return price; }
|
||||
public void setPrice(double v) { price = v; }
|
||||
}
|
||||
|
||||
/** 中标率 */
|
||||
public static class WinRateData {
|
||||
private Long supplierId;
|
||||
private String supplierName;
|
||||
private int totalQuotes;
|
||||
private int winCount;
|
||||
private double winRate;
|
||||
|
||||
public Long getSupplierId() { return supplierId; }
|
||||
public void setSupplierId(Long v) { supplierId = v; }
|
||||
public String getSupplierName() { return supplierName; }
|
||||
public void setSupplierName(String v) { supplierName = v; }
|
||||
public int getTotalQuotes() { return totalQuotes; }
|
||||
public void setTotalQuotes(int v) { totalQuotes = v; }
|
||||
public int getWinCount() { return winCount; }
|
||||
public void setWinCount(int v) { winCount = v; }
|
||||
public double getWinRate() { return winRate; }
|
||||
public void setWinRate(double v) { winRate = v; }
|
||||
}
|
||||
|
||||
/** 异议统计 */
|
||||
public static class ObjectionStat {
|
||||
private Long supplierId;
|
||||
private String supplierName;
|
||||
private int objectionCount;
|
||||
private int resolvedCount;
|
||||
private String topReason;
|
||||
|
||||
public Long getSupplierId() { return supplierId; }
|
||||
public void setSupplierId(Long v) { supplierId = v; }
|
||||
public String getSupplierName() { return supplierName; }
|
||||
public void setSupplierName(String v) { supplierName = v; }
|
||||
public int getObjectionCount() { return objectionCount; }
|
||||
public void setObjectionCount(int v) { objectionCount = v; }
|
||||
public int getResolvedCount() { return resolvedCount; }
|
||||
public void setResolvedCount(int v) { resolvedCount = v; }
|
||||
public String getTopReason() { return topReason; }
|
||||
public void setTopReason(String v) { topReason = v; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.ruoyi.system.mapper.bid;
|
||||
|
||||
import com.ruoyi.system.domain.bid.ReportDashboardVO;
|
||||
import com.ruoyi.system.domain.bid.ReportCostVO;
|
||||
import com.ruoyi.system.domain.bid.ReportSupplierVO;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 报表统计 Mapper
|
||||
*/
|
||||
public interface BizReportMapper {
|
||||
|
||||
// ========== 看板 ==========
|
||||
|
||||
/** 本月/上月采购总额 */
|
||||
Map<String, Object> selectPurchaseAmount(@Param("month") String month);
|
||||
|
||||
/** 本月/上月 RFQ 数量 */
|
||||
Map<String, Object> selectRfqCount(@Param("month") String month);
|
||||
|
||||
/** 本月/上月 PO 数量 */
|
||||
Map<String, Object> selectPoCount(@Param("month") String month);
|
||||
|
||||
/** 活跃供应商数 */
|
||||
Map<String, Object> selectActiveSupplierCount();
|
||||
|
||||
/** 月度采购趋势(近12月) */
|
||||
List<ReportDashboardVO.MonthTrend> selectMonthlyTrend();
|
||||
|
||||
/** RFQ 状态分布 */
|
||||
List<ReportDashboardVO.StatusDist> selectRfqStatusDist();
|
||||
|
||||
/** Top 供应商排名 */
|
||||
List<ReportDashboardVO.SupplierRank> selectTopSuppliers();
|
||||
|
||||
/** 最近动态 */
|
||||
List<ReportDashboardVO.RecentActivity> selectRecentActivities();
|
||||
|
||||
// ========== 成本分析 ==========
|
||||
|
||||
/** 预算总额(所有已完成RFQ的期望价) */
|
||||
Map<String, Object> selectTotalExpected();
|
||||
|
||||
/** 实际采购总额 */
|
||||
Map<String, Object> selectTotalActual();
|
||||
|
||||
/** 月度成本趋势 */
|
||||
List<ReportCostVO.CostTrend> selectCostTrend(@Param("startMonth") String startMonth,
|
||||
@Param("endMonth") String endMonth);
|
||||
|
||||
/** 品类采购分布 */
|
||||
List<ReportCostVO.CategoryDist> selectCategoryDist();
|
||||
|
||||
/** RFQ 比价明细 */
|
||||
List<ReportCostVO.RfqCompareDetail> selectRfqCompareDetails();
|
||||
|
||||
// ========== 供应商绩效 ==========
|
||||
|
||||
/** 供应商评分排名 */
|
||||
List<ReportSupplierVO.SupplierScore> selectSupplierScores();
|
||||
|
||||
/** 供应商中标率 */
|
||||
List<ReportSupplierVO.WinRateData> selectWinRate();
|
||||
|
||||
/** 供应商雷达图数据 */
|
||||
List<ReportSupplierVO.RadarData> selectRadarData();
|
||||
|
||||
/** 异议统计 */
|
||||
List<ReportSupplierVO.ObjectionStat> selectObjectionStats();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ruoyi.system.service.bid;
|
||||
|
||||
import com.ruoyi.system.domain.bid.ReportDashboardVO;
|
||||
import com.ruoyi.system.domain.bid.ReportCostVO;
|
||||
import com.ruoyi.system.domain.bid.ReportSupplierVO;
|
||||
|
||||
/**
|
||||
* 报表统计 Service 接口
|
||||
*/
|
||||
public interface IBizReportService {
|
||||
|
||||
/** 获取采购总览看板数据 */
|
||||
ReportDashboardVO getDashboard();
|
||||
|
||||
/** 获取采购成本分析数据 */
|
||||
ReportCostVO getCostAnalysis(String startMonth, String endMonth);
|
||||
|
||||
/** 获取供应商绩效数据 */
|
||||
ReportSupplierVO getSupplierPerformance();
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package com.ruoyi.system.service.bid.impl;
|
||||
|
||||
import com.ruoyi.system.domain.bid.ReportDashboardVO;
|
||||
import com.ruoyi.system.domain.bid.ReportCostVO;
|
||||
import com.ruoyi.system.domain.bid.ReportSupplierVO;
|
||||
import com.ruoyi.system.mapper.bid.BizReportMapper;
|
||||
import com.ruoyi.system.service.bid.IBizReportService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 报表统计 Service 实现
|
||||
*/
|
||||
@Service
|
||||
public class BizReportServiceImpl implements IBizReportService {
|
||||
|
||||
@Autowired
|
||||
private BizReportMapper mapper;
|
||||
|
||||
private static final DateTimeFormatter MONTH_FMT = DateTimeFormatter.ofPattern("yyyy-MM");
|
||||
|
||||
@Override
|
||||
public ReportDashboardVO getDashboard() {
|
||||
ReportDashboardVO vo = new ReportDashboardVO();
|
||||
|
||||
// 当前月 & 上月
|
||||
String curMonth = LocalDate.now().format(MONTH_FMT);
|
||||
String lastMonth = LocalDate.now().minusMonths(1).format(MONTH_FMT);
|
||||
|
||||
// 1) KPI 卡片
|
||||
vo.setTotalPurchaseAmount(buildKpi("采购总额",
|
||||
selectAmount("totalAmount", curMonth),
|
||||
selectAmount("totalAmount", lastMonth), "元"));
|
||||
|
||||
vo.setTotalRfqCount(buildKpi("RFQ总数",
|
||||
selectCount("rfq", curMonth),
|
||||
selectCount("rfq", lastMonth), "单"));
|
||||
|
||||
vo.setTotalPoCount(buildKpi("采购单数",
|
||||
selectCount("po", curMonth),
|
||||
selectCount("po", lastMonth), "单"));
|
||||
|
||||
Map<String, Object> act = mapper.selectActiveSupplierCount();
|
||||
BigDecimal activeVal = toBD(act.get("totalCount"));
|
||||
vo.setActiveSupplierCount(buildKpi("活跃供应商", activeVal, null, "家"));
|
||||
|
||||
// 2) 月度趋势
|
||||
vo.setMonthlyTrend(mapper.selectMonthlyTrend());
|
||||
|
||||
// 3) RFQ 状态分布
|
||||
List<ReportDashboardVO.StatusDist> dist = mapper.selectRfqStatusDist();
|
||||
// 补充中文标签
|
||||
for (ReportDashboardVO.StatusDist d : dist) {
|
||||
if (d.getStatusLabel() == null) {
|
||||
d.setStatusLabel(d.getStatus());
|
||||
}
|
||||
}
|
||||
vo.setRfqStatusDist(dist);
|
||||
|
||||
// 4) Top 供应商
|
||||
vo.setTopSuppliers(mapper.selectTopSuppliers());
|
||||
|
||||
// 5) 最近动态
|
||||
vo.setRecentActivities(mapper.selectRecentActivities());
|
||||
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReportCostVO getCostAnalysis(String startMonth, String endMonth) {
|
||||
ReportCostVO vo = new ReportCostVO();
|
||||
|
||||
// 汇总
|
||||
ReportCostVO.CostSummary summary = new ReportCostVO.CostSummary();
|
||||
BigDecimal expected = toBD(mapper.selectTotalExpected().get("totalExpected"));
|
||||
BigDecimal actual = toBD(mapper.selectTotalActual().get("totalActual"));
|
||||
summary.setTotalExpected(expected);
|
||||
summary.setTotalActual(actual);
|
||||
summary.setSavedAmount(expected.subtract(actual).max(BigDecimal.ZERO));
|
||||
summary.setSavedRate(expected.compareTo(BigDecimal.ZERO) > 0
|
||||
? summary.getSavedAmount().divide(expected, 4, RoundingMode.HALF_UP)
|
||||
.multiply(new BigDecimal("100")).setScale(1, RoundingMode.HALF_UP).doubleValue()
|
||||
: 0);
|
||||
vo.setSummary(summary);
|
||||
|
||||
// 月度趋势
|
||||
vo.setCostTrend(mapper.selectCostTrend(startMonth, endMonth));
|
||||
|
||||
// 品类分布
|
||||
List<ReportCostVO.CategoryDist> catDist = mapper.selectCategoryDist();
|
||||
BigDecimal total = catDist.stream()
|
||||
.map(ReportCostVO.CategoryDist::getAmount)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
for (ReportCostVO.CategoryDist d : catDist) {
|
||||
if (total.compareTo(BigDecimal.ZERO) > 0) {
|
||||
d.setPercent(d.getAmount().divide(total, 4, RoundingMode.HALF_UP)
|
||||
.multiply(new BigDecimal("100")).setScale(1, RoundingMode.HALF_UP).doubleValue());
|
||||
}
|
||||
}
|
||||
vo.setCategoryDist(catDist);
|
||||
|
||||
// RFQ 比价明细
|
||||
vo.setRfqDetails(mapper.selectRfqCompareDetails());
|
||||
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReportSupplierVO getSupplierPerformance() {
|
||||
ReportSupplierVO vo = new ReportSupplierVO();
|
||||
|
||||
vo.setRankings(mapper.selectSupplierScores());
|
||||
vo.setWinRateData(mapper.selectWinRate());
|
||||
vo.setRadarData(mapper.selectRadarData());
|
||||
vo.setObjectionStats(mapper.selectObjectionStats());
|
||||
|
||||
return vo;
|
||||
}
|
||||
|
||||
// ===== 私有工具方法 =====
|
||||
|
||||
private ReportDashboardVO.KpiCard buildKpi(String label, BigDecimal curVal, BigDecimal lastVal, String unit) {
|
||||
ReportDashboardVO.KpiCard card = new ReportDashboardVO.KpiCard();
|
||||
card.setLabel(label);
|
||||
card.setValue(curVal);
|
||||
card.setUnit(unit);
|
||||
|
||||
if (lastVal != null && lastVal.compareTo(BigDecimal.ZERO) > 0) {
|
||||
double rate = curVal.subtract(lastVal)
|
||||
.divide(lastVal, 4, RoundingMode.HALF_UP)
|
||||
.multiply(new BigDecimal("100"))
|
||||
.setScale(1, RoundingMode.HALF_UP)
|
||||
.doubleValue();
|
||||
card.setChangeRate(Math.abs(rate));
|
||||
card.setTrend(rate >= 0 ? "up" : "down");
|
||||
} else {
|
||||
card.setChangeRate(0);
|
||||
card.setTrend("up");
|
||||
}
|
||||
return card;
|
||||
}
|
||||
|
||||
private BigDecimal selectAmount(String column, String month) {
|
||||
Map<String, Object> map = mapper.selectPurchaseAmount(month);
|
||||
return toBD(map.get(column));
|
||||
}
|
||||
|
||||
private BigDecimal selectCount(String type, String month) {
|
||||
Map<String, Object> map;
|
||||
switch (type) {
|
||||
case "rfq": map = mapper.selectRfqCount(month); break;
|
||||
case "po": map = mapper.selectPoCount(month); break;
|
||||
default: return BigDecimal.ZERO;
|
||||
}
|
||||
return toBD(map.get("totalCount"));
|
||||
}
|
||||
|
||||
private BigDecimal toBD(Object v) {
|
||||
if (v == null) return BigDecimal.ZERO;
|
||||
if (v instanceof BigDecimal) return (BigDecimal) v;
|
||||
if (v instanceof Number) return BigDecimal.valueOf(((Number) v).doubleValue());
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user