19 changed files with 973 additions and 1 deletions
@ -0,0 +1,105 @@
|
||||
package com.biutag.supervision.pojo.dto.report; |
||||
|
||||
import com.biutag.supervision.pojo.enums.report.TrendEnum; |
||||
import io.swagger.v3.oas.annotations.media.Schema; |
||||
import lombok.Data; |
||||
|
||||
import java.math.BigDecimal; |
||||
|
||||
@Data |
||||
@Schema(description = "研判分析报告-总体统计数据") |
||||
public class OverviewSection { |
||||
|
||||
@Schema(description = "统计开始时间", example = "2026年3月1日") |
||||
private String periodStart; |
||||
|
||||
@Schema(description = "统计结束时间", example = "2026年3月31日") |
||||
private String periodEnd; |
||||
|
||||
@Schema(description = "全市下发督审数据总数", example = "1286") |
||||
private Integer totalIssued; |
||||
|
||||
@Schema(description = "环比趋势") |
||||
private TrendEnum momTrend; |
||||
|
||||
@Schema(description = "环比增长率", example = "12.5") |
||||
private BigDecimal momRate; |
||||
|
||||
@Schema(description = "同比趋势") |
||||
private TrendEnum yoyTrend; |
||||
|
||||
@Schema(description = "同比增长率", example = "18.2") |
||||
private BigDecimal yoyRate; |
||||
|
||||
@Schema(description = "市局层面下发数量", example = "356") |
||||
private Integer cityIssued; |
||||
|
||||
@Schema(description = "市局下发占比", example = "27.7") |
||||
private BigDecimal cityRate; |
||||
|
||||
@Schema(description = "分县市局下发数量", example = "930") |
||||
private Integer countyIssued; |
||||
|
||||
@Schema(description = "分县市局下发占比", example = "72.3") |
||||
private BigDecimal countyRate; |
||||
|
||||
@Schema(description = "已办结数据数量", example = "1100") |
||||
private Integer closedCount; |
||||
|
||||
@Schema(description = "查实数量", example = "420") |
||||
private Integer verifiedCount; |
||||
|
||||
@Schema(description = "基本属实数量", example = "380") |
||||
private Integer basicallyVerifiedCount; |
||||
|
||||
@Schema(description = "不属实数量", example = "300") |
||||
private Integer unverifiedCount; |
||||
|
||||
@Schema(description = "总体查实率", example = "72.7") |
||||
private BigDecimal verifiedRate; |
||||
|
||||
@Schema(description = "查实率环比趋势") |
||||
private TrendEnum verifiedMomTrend; |
||||
|
||||
@Schema(description = "查实率环比变化率", example = "3.2") |
||||
private BigDecimal verifiedMomRate; |
||||
|
||||
@Schema(description = "查实率同比趋势") |
||||
private TrendEnum verifiedYoyTrend; |
||||
|
||||
@Schema(description = "查实率同比变化率", example = "5.1") |
||||
private BigDecimal verifiedYoyRate; |
||||
|
||||
@Schema(description = "问责总人次", example = "58") |
||||
private Integer accountabilityTotal; |
||||
|
||||
@Schema(description = "个人问责次数", example = "45") |
||||
private Integer personalAccountability; |
||||
|
||||
@Schema(description = "单位问责次数", example = "13") |
||||
private Integer unitAccountability; |
||||
|
||||
@Schema(description = "问责环比趋势") |
||||
private TrendEnum accountabilityMomTrend; |
||||
|
||||
@Schema(description = "问责环比变化率", example = "8.6") |
||||
private BigDecimal accountabilityMomRate; |
||||
|
||||
@Schema(description = "问责同比趋势") |
||||
private TrendEnum accountabilityYoyTrend; |
||||
|
||||
@Schema(description = "问责同比变化率", example = "6.4") |
||||
private BigDecimal accountabilityYoyRate; |
||||
|
||||
/** |
||||
* @see com.biutag.supervision.pojo.enums.report.TrendEnum |
||||
*/ |
||||
@Schema(description = "整体趋势") |
||||
private TrendEnum overallTrend; |
||||
|
||||
@Schema(description = "问题集中项目", example = "工程招投标") |
||||
private String topProblemProject; |
||||
|
||||
@Schema(description = "需重点关注单位", example = "XX县局") |
||||
private String topProblemUnit; |
||||
} |
||||
@ -0,0 +1,28 @@
|
||||
package com.biutag.supervision.pojo.dto.report; |
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema; |
||||
import lombok.Getter; |
||||
import lombok.Setter; |
||||
|
||||
/** |
||||
* @ClassName ReportGenerationDTO |
||||
* @Description TODO |
||||
* @Author shihao |
||||
* @Date 2026/3/5 9:26 |
||||
*/ |
||||
@Getter |
||||
@Setter |
||||
@Schema(description = "研判分析报告DTO") |
||||
public class ReportViewModel { |
||||
|
||||
@Schema(description = "统计开始时间", example = "2026年3月1日") |
||||
private String periodStart; |
||||
|
||||
@Schema(description = "统计结束时间", example = "2026年3月31日") |
||||
private String periodEnd; |
||||
|
||||
|
||||
@Schema(description = "总体部分") |
||||
private OverviewSection overviewSection; |
||||
|
||||
} |
||||
@ -0,0 +1,31 @@
|
||||
package com.biutag.supervision.pojo.enums.report; |
||||
|
||||
import com.biutag.supervision.constants.enums.CodeEnum; |
||||
import io.swagger.v3.oas.annotations.media.Schema; |
||||
|
||||
@Schema(description = "趋势类型") |
||||
public enum TrendEnum implements CodeEnum { |
||||
UP("UP","上升"), |
||||
DOWN("DOWN","下降"), |
||||
STABLE("STABLE","持平"); |
||||
|
||||
private final String code; |
||||
private final String desc; |
||||
|
||||
|
||||
|
||||
TrendEnum(String code, String desc) { |
||||
this.code = code; |
||||
this.desc = desc; |
||||
} |
||||
|
||||
@Override |
||||
public String getCode() { |
||||
return code; |
||||
} |
||||
|
||||
@Override |
||||
public String getDesc() { |
||||
return desc; |
||||
} |
||||
} |
||||
@ -0,0 +1,45 @@
|
||||
package com.biutag.supervision.pojo.request.negative; |
||||
|
||||
import cn.hutool.core.date.DateUtil; |
||||
import cn.hutool.core.date.LocalDateTimeUtil; |
||||
import com.biutag.supervision.aop.ParamChecked; |
||||
import com.fasterxml.jackson.annotation.JsonFormat; |
||||
import io.swagger.v3.oas.annotations.media.Schema; |
||||
import jakarta.validation.ValidationException; |
||||
import lombok.Getter; |
||||
import lombok.Setter; |
||||
|
||||
import java.time.LocalDateTime; |
||||
import java.util.Date; |
||||
import java.util.Objects; |
||||
|
||||
/** |
||||
* @ClassName ReportGenerationRequest |
||||
* @Description TODO |
||||
* @Author shihao |
||||
* @Date 2026/3/4 16:38 |
||||
*/ |
||||
@Setter |
||||
@Getter |
||||
@Schema(description = "研判分析报告生成请求") |
||||
public class ReportGenerationRequest implements ParamChecked { |
||||
|
||||
@Schema(description = "开始时间") |
||||
@JsonFormat(pattern = "yyyy-MM-dd") |
||||
private Date beginTime; |
||||
|
||||
@Schema(description = "结束时间") |
||||
@JsonFormat(pattern = "yyyy-MM-dd") |
||||
private Date endTime; |
||||
|
||||
|
||||
@Override |
||||
public void check() { |
||||
if (Objects.isNull(beginTime) || Objects.isNull(endTime)){ |
||||
throw new ValidationException("必须选择时间"); |
||||
} |
||||
// 统一格式化时间
|
||||
this.beginTime = DateUtil.beginOfDay(this.beginTime); |
||||
this.endTime = DateUtil.endOfDay(this.endTime); |
||||
} |
||||
} |
||||
@ -0,0 +1,4 @@
|
||||
package com.biutag.supervision.service.negativeReport; |
||||
|
||||
public interface NegativeReportService { |
||||
} |
||||
@ -0,0 +1,20 @@
|
||||
package com.biutag.supervision.service.negativeReport; |
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema; |
||||
import lombok.RequiredArgsConstructor; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.stereotype.Service; |
||||
|
||||
/** |
||||
* @ClassName NegativeReportServiceImpl |
||||
* @Description TODO |
||||
* @Author shihao |
||||
* @Date 2026/3/4 18:34 |
||||
*/ |
||||
@Slf4j |
||||
@Service |
||||
@RequiredArgsConstructor |
||||
@Schema(description = "研判分析报告") |
||||
public class NegativeReportServiceImpl implements NegativeReportService { |
||||
|
||||
} |
||||
@ -0,0 +1,19 @@
|
||||
package com.biutag.supervision.service.report; |
||||
|
||||
|
||||
import com.biutag.supervision.pojo.dto.report.ReportViewModel; |
||||
import com.biutag.supervision.pojo.request.negative.ReportGenerationRequest; |
||||
|
||||
public interface ReportDataService { |
||||
|
||||
|
||||
/** |
||||
* 构建概要 |
||||
* @param request |
||||
* @return |
||||
*/ |
||||
ReportViewModel buildViewModel(ReportGenerationRequest request); |
||||
|
||||
|
||||
|
||||
} |
||||
@ -0,0 +1,185 @@
|
||||
package com.biutag.supervision.service.report; |
||||
|
||||
import cn.hutool.core.util.StrUtil; |
||||
import com.biutag.supervision.constants.enums.CheckStatusEnum; |
||||
import com.biutag.supervision.pojo.dto.report.OverviewSection; |
||||
import com.biutag.supervision.pojo.dto.report.ReportViewModel; |
||||
import com.biutag.supervision.pojo.entity.Negative; |
||||
import com.biutag.supervision.pojo.entity.NegativeBlame; |
||||
import com.biutag.supervision.pojo.param.NegativeQueryParam; |
||||
import com.biutag.supervision.pojo.param.negativeBlame.NegativeBlameQueryParam; |
||||
import com.biutag.supervision.pojo.request.negative.ReportGenerationRequest; |
||||
import com.biutag.supervision.repository.negative.NegativeResourceService; |
||||
import com.biutag.supervision.repository.negativeBlame.NegativeBlameResourceService; |
||||
import com.biutag.supervision.util.DateCompareRangeUtil; |
||||
import com.biutag.supervision.util.ReportTrendUtil; |
||||
import com.biutag.supervision.util.TimeUtil; |
||||
import jakarta.annotation.Resource; |
||||
import org.springframework.stereotype.Service; |
||||
|
||||
import java.math.BigDecimal; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Objects; |
||||
import java.util.stream.Stream; |
||||
|
||||
@Service |
||||
public class ReportDataServiceImpl implements ReportDataService { |
||||
|
||||
@Resource |
||||
private NegativeResourceService negativeResourceService; |
||||
|
||||
@Resource |
||||
private NegativeBlameResourceService negativeBlameResourceService; |
||||
|
||||
@Override |
||||
public ReportViewModel buildViewModel(ReportGenerationRequest request) { |
||||
String periodStart = TimeUtil.formatDate(request.getBeginTime()); |
||||
String periodEnd = TimeUtil.formatDate(request.getEndTime()); |
||||
ReportViewModel vm = new ReportViewModel(); |
||||
vm.setPeriodStart(periodStart); |
||||
vm.setPeriodEnd(periodEnd); |
||||
vm.setOverviewSection(buildOverviewSection(request, periodStart, periodEnd)); |
||||
//
|
||||
return vm; |
||||
} |
||||
|
||||
/** |
||||
* 总体情况计算 |
||||
* |
||||
* @param request |
||||
* @param periodStart |
||||
* @param periodEnd |
||||
* @return |
||||
*/ |
||||
private OverviewSection buildOverviewSection(ReportGenerationRequest request, String periodStart, String periodEnd) { |
||||
|
||||
DateCompareRangeUtil.CompareDateRange compareDateRange = DateCompareRangeUtil.buildCompareDateRange(request.getBeginTime(), request.getEndTime()); |
||||
// 总体数据
|
||||
NegativeQueryParam ztNegativeQueryParam = new NegativeQueryParam(); |
||||
ztNegativeQueryParam.setCrtTime(compareDateRange.current()); |
||||
List<Negative> ztNegativeList = negativeResourceService.query(ztNegativeQueryParam); |
||||
// 环比数据
|
||||
NegativeQueryParam hbQueryParam = new NegativeQueryParam(); |
||||
hbQueryParam.setCrtTime(compareDateRange.mom()); |
||||
List<Negative> hbNegativList = negativeResourceService.query(hbQueryParam); |
||||
// 同比数据
|
||||
NegativeQueryParam tbQueryParam = new NegativeQueryParam(); |
||||
tbQueryParam.setCrtTime(compareDateRange.yoy()); |
||||
List<Negative> tbNegativList = negativeResourceService.query(tbQueryParam); |
||||
|
||||
// 市局下发数据
|
||||
List<Negative> sjxfNegativeList = ztNegativeList.stream().filter(one -> Objects.equals(0, one.getCrtDepartLevel())).toList(); |
||||
// 分县市局下发数据
|
||||
List<Negative> fxsjxfNegativeList = ztNegativeList.stream().filter(one -> Objects.equals(2, one.getCrtDepartLevel())).toList(); |
||||
// 办结数据
|
||||
List<Negative> bjNegativeList = ztNegativeList.stream().filter(one -> !Objects.isNull(one.getCompleteDate())).toList(); |
||||
// 办结中属实数据
|
||||
List<Negative> bjssNegativeList = bjNegativeList.stream().filter(one -> CheckStatusEnum.TRUE_LIST.contains(one.getCheckStatusCode())).toList(); |
||||
// 办结中基本属实数据
|
||||
List<Negative> bjjbssNegativeList = bjNegativeList.stream().filter(one -> CheckStatusEnum.PART_TRUE_LIST.contains(one.getCheckStatusCode())).toList(); |
||||
// 办结中不属实数据
|
||||
List<Negative> bjbssNegativeList = bjNegativeList.stream().filter(one -> CheckStatusEnum.FALSE_LIST.contains(one.getCheckStatusCode())).toList(); |
||||
// 属实 基本属实中的问责数据
|
||||
List<String> ssNegativeIds = Stream.concat(bjssNegativeList.stream(), bjjbssNegativeList.stream()) |
||||
.map(Negative::getId).filter(StrUtil::isNotBlank).distinct().toList(); |
||||
NegativeBlameQueryParam negativeBlameQueryParam = new NegativeBlameQueryParam(); |
||||
negativeBlameQueryParam.setNegativeIds(ssNegativeIds); |
||||
List<NegativeBlame> negativeBlames = negativeBlameResourceService.query(negativeBlameQueryParam); |
||||
// 个人问责
|
||||
List<NegativeBlame> grwzNegativeBlames = negativeBlames.stream().filter(one -> "personal".equals(one.getType())) |
||||
.filter(one -> StrUtil.isNotBlank(one.getHandleResultName())) |
||||
.filter(one -> !"不予追责".equals(one.getHandleResultName())).toList(); |
||||
|
||||
// 单位问责
|
||||
List<NegativeBlame> dwwzNegativeBlames = negativeBlames.stream() |
||||
.filter(one -> "department".equals(one.getType())) |
||||
.filter(one -> StrUtil.isNotBlank(one.getHandleResultName())) |
||||
.filter(one -> !"不予追责".equals(one.getHandleResultName())).toList(); |
||||
// 第一段
|
||||
int current = ztNegativeList.size(); |
||||
int mom = hbNegativList.size(); |
||||
int yoy = tbNegativList.size(); |
||||
// 第二段
|
||||
int closedCount = bjNegativeList.size(); // 总办结
|
||||
int verifiedCount = bjssNegativeList.size(); // 查实
|
||||
int basicallyVerifiedCount = bjjbssNegativeList.size();// 基本属实
|
||||
int unverifiedCount = bjbssNegativeList.size(); // 不属实
|
||||
|
||||
|
||||
OverviewSection overviewSection = new OverviewSection(); |
||||
// 时间
|
||||
overviewSection.setPeriodStart(periodStart); |
||||
overviewSection.setPeriodEnd(periodEnd); |
||||
// 总条数
|
||||
overviewSection.setTotalIssued(ztNegativeList.size()); |
||||
// 总条数环比
|
||||
overviewSection.setMomRate(ReportTrendUtil.calcRate(current, mom)); |
||||
overviewSection.setMomTrend(ReportTrendUtil.calcTrend(current, mom)); |
||||
// 总条数同比
|
||||
overviewSection.setYoyRate(ReportTrendUtil.calcRate(current, yoy)); |
||||
overviewSection.setYoyTrend(ReportTrendUtil.calcTrend(current, yoy)); |
||||
// 市局下发
|
||||
overviewSection.setCityIssued(sjxfNegativeList.size()); |
||||
overviewSection.setCityRate(ReportTrendUtil.percent(sjxfNegativeList.size(), ztNegativeList.size())); |
||||
// 分县市局下发
|
||||
overviewSection.setCountyIssued(fxsjxfNegativeList.size()); |
||||
overviewSection.setCountyRate(ReportTrendUtil.percent(fxsjxfNegativeList.size(), ztNegativeList.size())); |
||||
// 办结总数据
|
||||
overviewSection.setClosedCount(closedCount); |
||||
overviewSection.setVerifiedCount(verifiedCount); |
||||
overviewSection.setBasicallyVerifiedCount(basicallyVerifiedCount); |
||||
overviewSection.setUnverifiedCount(unverifiedCount); |
||||
// 办结查实率
|
||||
// 1) 本期/环比/同比 查实率(都是 %,BigDecimal)
|
||||
BigDecimal curVerifiedRate = calcVerifiedRate(ztNegativeList); |
||||
BigDecimal momVerifiedRate = calcVerifiedRate(hbNegativList); |
||||
BigDecimal yoyVerifiedRate = calcVerifiedRate(tbNegativList); |
||||
overviewSection.setVerifiedRate(curVerifiedRate); |
||||
// 3) 查实率环比(注意:率对率)
|
||||
overviewSection.setVerifiedMomRate(ReportTrendUtil.calcRate(curVerifiedRate, momVerifiedRate)); |
||||
overviewSection.setVerifiedMomTrend(ReportTrendUtil.calcTrend(curVerifiedRate, momVerifiedRate)); |
||||
// 4) 查实率同比(注意:率对率)
|
||||
overviewSection.setVerifiedYoyRate(ReportTrendUtil.calcRate(curVerifiedRate, yoyVerifiedRate)); |
||||
overviewSection.setVerifiedYoyTrend(ReportTrendUtil.calcTrend(curVerifiedRate, yoyVerifiedRate)); |
||||
|
||||
// 集中问题
|
||||
Map.Entry<String, Long> problemEntry = ReportTrendUtil.topBy(Negative::getProblemSources, bjssNegativeList, bjjbssNegativeList); |
||||
String problemSource = problemEntry != null ? problemEntry.getKey() : null; |
||||
Long problemSourceCount = problemEntry != null ? problemEntry.getValue() : 0; |
||||
overviewSection.setTopProblemProject(problemSource); |
||||
// 重点关注单位
|
||||
Map.Entry<String, Long> departEntry = ReportTrendUtil.topBy(Negative::getInvolveDepartName, bjssNegativeList, bjjbssNegativeList); |
||||
String departName = departEntry != null ? departEntry.getKey() : null; |
||||
Long departNameCount = departEntry != null ? departEntry.getValue() : 0; |
||||
overviewSection.setTopProblemUnit(departName); |
||||
return overviewSection; |
||||
} |
||||
|
||||
|
||||
// 查实率
|
||||
private BigDecimal calcVerifiedRate(List<Negative> list) { |
||||
// 办结
|
||||
List<Negative> closed = list.stream().filter(n -> n.getCompleteDate() != null).toList(); |
||||
|
||||
int closedCount = closed.size(); |
||||
if (closedCount == 0) { |
||||
return BigDecimal.ZERO; |
||||
} |
||||
|
||||
// 属实
|
||||
int verified = (int) closed.stream() |
||||
.filter(n -> CheckStatusEnum.TRUE_LIST.contains(n.getCheckStatusCode())) |
||||
.count(); |
||||
|
||||
// 基本属实
|
||||
int partVerified = (int) closed.stream() |
||||
.filter(n -> CheckStatusEnum.PART_TRUE_LIST.contains(n.getCheckStatusCode())) |
||||
.count(); |
||||
|
||||
// 查实率 = (属实 + 基本属实) / 办结
|
||||
return ReportTrendUtil.percent(verified + partVerified, closedCount); |
||||
} |
||||
|
||||
|
||||
} |
||||
@ -0,0 +1,219 @@
|
||||
package com.biutag.supervision.util; |
||||
|
||||
import org.jfree.chart.ChartFactory; |
||||
import org.jfree.chart.ChartUtils; |
||||
import org.jfree.chart.JFreeChart; |
||||
import org.jfree.chart.labels.StandardCategoryItemLabelGenerator; |
||||
import org.jfree.chart.labels.StandardPieSectionLabelGenerator; |
||||
import org.jfree.chart.plot.CategoryPlot; |
||||
import org.jfree.chart.plot.PiePlot; |
||||
import org.jfree.chart.plot.PlotOrientation; |
||||
import org.jfree.chart.renderer.category.BarRenderer; |
||||
import org.jfree.chart.title.LegendTitle; |
||||
import org.jfree.data.category.DefaultCategoryDataset; |
||||
import org.jfree.data.general.DefaultPieDataset; |
||||
|
||||
import java.awt.*; |
||||
import java.io.ByteArrayOutputStream; |
||||
import java.text.NumberFormat; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* 图表渲染工具(PNG) |
||||
* - 适配报告输出:白底、抗锯齿、中文字体、标签更清晰、柱状图显示数值 |
||||
*/ |
||||
public class ChartRenderUtil { |
||||
|
||||
// =========================
|
||||
// 对外 API:根据数据生成图
|
||||
// =========================
|
||||
|
||||
public static byte[] piePng(Map<String, Number> data, int w, int h) { |
||||
DefaultPieDataset<String> dataset = new DefaultPieDataset<>(); |
||||
data.forEach(dataset::setValue); |
||||
|
||||
JFreeChart chart = ChartFactory.createPieChart( |
||||
"占比统计", // 你不想要标题可以传 null
|
||||
dataset, |
||||
true, |
||||
false, |
||||
false |
||||
); |
||||
|
||||
beautifyPie(chart); |
||||
return toPng(chart, w, h, "生成饼图失败"); |
||||
} |
||||
|
||||
public static byte[] barPng(Map<String, Number> data, String xLabel, String yLabel, int w, int h) { |
||||
DefaultCategoryDataset dataset = new DefaultCategoryDataset(); |
||||
// series 用一个固定值即可(yLabel 也行)
|
||||
data.forEach((k, v) -> dataset.addValue(v, yLabel, k)); |
||||
|
||||
JFreeChart chart = ChartFactory.createBarChart( |
||||
"统计分析", // 你不想要标题可以传 null
|
||||
xLabel, |
||||
yLabel, |
||||
dataset, |
||||
PlotOrientation.VERTICAL, |
||||
true, |
||||
false, |
||||
false |
||||
); |
||||
|
||||
beautifyBar(chart); |
||||
return toPng(chart, w, h, "生成柱状图失败"); |
||||
} |
||||
|
||||
// =========================
|
||||
// Demo:随便生成一个图(可留可删)
|
||||
// =========================
|
||||
|
||||
public static byte[] createPieChart() { |
||||
return piePng(Map.of("查实", 40, "基本属实", 30, "不属实", 30), 900, 520); |
||||
} |
||||
|
||||
public static byte[] createBarChart() { |
||||
return barPng(Map.of("市局", 120, "分县市局", 80, "基层单位", 50), "单位", "数量", 900, 520); |
||||
} |
||||
|
||||
// =========================
|
||||
// 美化:饼图
|
||||
// =========================
|
||||
|
||||
private static void beautifyPie(JFreeChart chart) { |
||||
chart.setAntiAlias(true); |
||||
chart.setTextAntiAlias(true); |
||||
chart.setBackgroundPaint(Color.WHITE); |
||||
chart.setBorderVisible(false); |
||||
|
||||
Font titleFont = pickFont(Font.BOLD, 16); |
||||
Font legendFont = pickFont(Font.PLAIN, 12); |
||||
Font labelFont = pickFont(Font.PLAIN, 12); |
||||
|
||||
if (chart.getTitle() != null) { |
||||
chart.getTitle().setFont(titleFont); |
||||
} |
||||
|
||||
LegendTitle legend = chart.getLegend(); |
||||
if (legend != null) { |
||||
legend.setItemFont(legendFont); |
||||
legend.setBackgroundPaint(Color.WHITE); |
||||
} |
||||
|
||||
PiePlot plot = (PiePlot) chart.getPlot(); |
||||
plot.setBackgroundPaint(Color.WHITE); |
||||
plot.setOutlineVisible(false); |
||||
|
||||
// label 样式:白底半透明,边框淡一点,去阴影
|
||||
plot.setLabelFont(labelFont); |
||||
plot.setLabelBackgroundPaint(new Color(255, 255, 255, 235)); |
||||
plot.setLabelOutlinePaint(new Color(220, 220, 220)); |
||||
plot.setLabelShadowPaint(null); |
||||
|
||||
// 标签内容:名称 + 百分比(例:查实 40.0%)
|
||||
plot.setLabelGenerator(new StandardPieSectionLabelGenerator( |
||||
"{0} {2}", |
||||
NumberFormat.getNumberInstance(), |
||||
NumberFormat.getPercentInstance() |
||||
)); |
||||
|
||||
// 让饼图别贴边
|
||||
plot.setInteriorGap(0.03); |
||||
|
||||
// 分离效果(可选:更“立体”,如果你觉得花哨可以注释)
|
||||
// plot.setExplodePercent("查实", 0.04);
|
||||
} |
||||
|
||||
// =========================
|
||||
// 美化:柱状图
|
||||
// =========================
|
||||
|
||||
private static void beautifyBar(JFreeChart chart) { |
||||
chart.setAntiAlias(true); |
||||
chart.setTextAntiAlias(true); |
||||
chart.setBackgroundPaint(Color.WHITE); |
||||
chart.setBorderVisible(false); |
||||
|
||||
Font titleFont = pickFont(Font.BOLD, 16); |
||||
Font legendFont = pickFont(Font.PLAIN, 12); |
||||
Font axisFont = pickFont(Font.PLAIN, 12); |
||||
Font valueFont = pickFont(Font.PLAIN, 12); |
||||
|
||||
if (chart.getTitle() != null) { |
||||
chart.getTitle().setFont(titleFont); |
||||
} |
||||
|
||||
LegendTitle legend = chart.getLegend(); |
||||
if (legend != null) { |
||||
legend.setItemFont(legendFont); |
||||
legend.setBackgroundPaint(Color.WHITE); |
||||
} |
||||
|
||||
CategoryPlot plot = chart.getCategoryPlot(); |
||||
plot.setBackgroundPaint(Color.WHITE); |
||||
plot.setOutlineVisible(false); |
||||
|
||||
// 网格线淡一点(更干净)
|
||||
plot.setRangeGridlinePaint(new Color(230, 230, 230)); |
||||
plot.setRangeGridlinesVisible(true); |
||||
|
||||
// 轴字体
|
||||
plot.getDomainAxis().setTickLabelFont(axisFont); |
||||
plot.getDomainAxis().setLabelFont(axisFont); |
||||
plot.getRangeAxis().setTickLabelFont(axisFont); |
||||
plot.getRangeAxis().setLabelFont(axisFont); |
||||
|
||||
// 柱体渲染:更清爽
|
||||
BarRenderer renderer = (BarRenderer) plot.getRenderer(); |
||||
renderer.setDrawBarOutline(false); |
||||
renderer.setShadowVisible(false); |
||||
renderer.setMaximumBarWidth(0.12); |
||||
renderer.setItemMargin(0.10); |
||||
|
||||
// 在柱子上方显示数值
|
||||
renderer.setDefaultItemLabelGenerator(new StandardCategoryItemLabelGenerator()); |
||||
renderer.setDefaultItemLabelsVisible(true); |
||||
renderer.setDefaultItemLabelFont(valueFont); |
||||
} |
||||
|
||||
// =========================
|
||||
// 输出 PNG
|
||||
// =========================
|
||||
|
||||
private static byte[] toPng(JFreeChart chart, int w, int h, String errMsg) { |
||||
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { |
||||
ChartUtils.writeChartAsPNG(out, chart, w, h); |
||||
return out.toByteArray(); |
||||
} catch (Exception e) { |
||||
throw new RuntimeException(errMsg, e); |
||||
} |
||||
} |
||||
|
||||
// =========================
|
||||
// 字体兜底(避免 Linux 没微软雅黑导致中文丑/方块)
|
||||
// =========================
|
||||
|
||||
private static Font pickFont(int style, int size) { |
||||
// 按优先级尝试(Windows 常见:微软雅黑;Linux 常见:Noto/文泉驿/DejaVu)
|
||||
String[] candidates = new String[]{ |
||||
"Microsoft YaHei", "微软雅黑", |
||||
"PingFang SC", "苹方-简", |
||||
"Noto Sans CJK SC", "Noto Sans SC", |
||||
"WenQuanYi Micro Hei", "文泉驿微米黑", |
||||
"SimSun", "宋体", |
||||
"Arial Unicode MS", |
||||
"SansSerif" |
||||
}; |
||||
|
||||
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); |
||||
String[] available = ge.getAvailableFontFamilyNames(); |
||||
for (String c : candidates) { |
||||
for (String a : available) { |
||||
if (a.equalsIgnoreCase(c)) { |
||||
return new Font(a, style, size); |
||||
} |
||||
} |
||||
} |
||||
return new Font("SansSerif", style, size); |
||||
} |
||||
} |
||||
@ -0,0 +1,78 @@
|
||||
package com.biutag.supervision.util; |
||||
|
||||
import java.time.*; |
||||
import java.time.temporal.ChronoUnit; |
||||
import java.util.Arrays; |
||||
import java.util.Date; |
||||
import java.util.List; |
||||
import java.util.Objects; |
||||
|
||||
public class DateCompareRangeUtil { |
||||
|
||||
private static final ZoneId ZONE = ZoneId.systemDefault(); |
||||
private static final LocalTime END_OF_DAY_MILLIS = LocalTime.of(23, 59, 59, 999_000_000); |
||||
|
||||
public record CompareDateRange(List<Date> current, List<Date> mom, List<Date> yoy) {} |
||||
|
||||
/** 本期/环比/同比(按天口径) */ |
||||
public static CompareDateRange buildCompareDateRange(Date beginTime, Date endTime) { |
||||
if (beginTime == null || endTime == null) { |
||||
throw new IllegalArgumentException("beginTime/endTime 不能为空"); |
||||
} |
||||
if (beginTime.after(endTime)) { |
||||
throw new IllegalArgumentException("beginTime 不能晚于 endTime"); |
||||
} |
||||
|
||||
LocalDate beginDate = toLocalDate(beginTime); |
||||
LocalDate endDate = toLocalDate(endTime); |
||||
|
||||
// 本期天数(包含首尾)
|
||||
long daysInclusive = ChronoUnit.DAYS.between(beginDate, endDate) + 1; |
||||
|
||||
// 本期:begin 00:00:00 ~ end 23:59:59.999
|
||||
List<Date> current = Arrays.asList( |
||||
toDate(beginDate.atStartOfDay()), |
||||
toDate(endDate.atTime(END_OF_DAY_MILLIS)) |
||||
); |
||||
|
||||
// 环比:向前推 N 天,且按你给的口径:momEnd = beginDate 当天 23:59:59.999(与本期重叠)
|
||||
LocalDate momEndDate = beginDate; |
||||
LocalDate momBeginDate = momEndDate.minusDays(daysInclusive - 1); |
||||
|
||||
List<Date> mom = Arrays.asList( |
||||
toDate(momBeginDate.atStartOfDay()), |
||||
toDate(momEndDate.atTime(END_OF_DAY_MILLIS)) |
||||
); |
||||
|
||||
// 同比:去年同期(begin/end 都减 1 年)
|
||||
LocalDate yoyBeginDate = beginDate.minusYears(1); |
||||
LocalDate yoyEndDate = endDate.minusYears(1); |
||||
|
||||
List<Date> yoy = Arrays.asList( |
||||
toDate(yoyBeginDate.atStartOfDay()), |
||||
toDate(yoyEndDate.atTime(END_OF_DAY_MILLIS)) |
||||
); |
||||
|
||||
return new CompareDateRange(current, mom, yoy); |
||||
} |
||||
|
||||
private static LocalDate toLocalDate(Date d) { |
||||
return d.toInstant().atZone(ZONE).toLocalDate(); |
||||
} |
||||
|
||||
private static Date toDate(LocalDateTime ldt) { |
||||
return Date.from(ldt.atZone(ZONE).toInstant()); |
||||
} |
||||
|
||||
// demo
|
||||
public static void main(String[] args) { |
||||
Date begin = Date.from(LocalDateTime.of(2026, 2, 13, 20, 33, 33).atZone(ZONE).toInstant()); |
||||
Date end = Date.from(LocalDateTime.of(2026, 2, 23, 20, 33, 22).atZone(ZONE).toInstant()); |
||||
|
||||
CompareDateRange r = buildCompareDateRange(begin, end); |
||||
|
||||
System.out.println("current = " + r.current()); |
||||
System.out.println("mom = " + r.mom()); |
||||
System.out.println("yoy = " + r.yoy()); |
||||
} |
||||
} |
||||
@ -0,0 +1,124 @@
|
||||
package com.biutag.supervision.util; |
||||
|
||||
import com.biutag.supervision.pojo.enums.report.TrendEnum; |
||||
import io.swagger.v3.oas.annotations.media.Schema; |
||||
|
||||
import java.math.BigDecimal; |
||||
import java.math.RoundingMode; |
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Objects; |
||||
import java.util.function.Function; |
||||
import java.util.stream.Collectors; |
||||
|
||||
/** |
||||
* @ClassName ReportTrendUtil |
||||
* @Description TODO |
||||
* @Author shihao |
||||
* @Date 2026/3/5 12:01 |
||||
*/ |
||||
@Schema(description = "比率工具类") |
||||
public class ReportTrendUtil { |
||||
|
||||
|
||||
/** |
||||
* 计算变化率 |
||||
* (current - prev) / prev * 100 |
||||
*/ |
||||
public static BigDecimal calcRate(int current, int prev) { |
||||
|
||||
if (prev == 0) { |
||||
return BigDecimal.ZERO; |
||||
} |
||||
|
||||
return BigDecimal.valueOf(current - prev) |
||||
.divide(BigDecimal.valueOf(prev), 4, RoundingMode.HALF_UP) |
||||
.multiply(BigDecimal.valueOf(100)); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* 判断趋势 |
||||
*/ |
||||
public static TrendEnum calcTrend(int current, int prev) { |
||||
|
||||
if (current > prev) { |
||||
return TrendEnum.UP; |
||||
} |
||||
|
||||
if (current < prev) { |
||||
return TrendEnum.DOWN; |
||||
} |
||||
|
||||
return TrendEnum.STABLE; |
||||
} |
||||
|
||||
/** |
||||
* 计算百分比 |
||||
*/ |
||||
public static BigDecimal percent(int part, int total) { |
||||
|
||||
if (total == 0) { |
||||
return BigDecimal.ZERO; |
||||
} |
||||
|
||||
return BigDecimal.valueOf(part) |
||||
.divide(BigDecimal.valueOf(total), 4, RoundingMode.HALF_UP) |
||||
.multiply(BigDecimal.valueOf(100)); |
||||
} |
||||
|
||||
/** |
||||
* 计算变化率(百分比对百分比) |
||||
*/ |
||||
public static BigDecimal calcRate(BigDecimal current, BigDecimal prev) { |
||||
|
||||
if (prev == null || prev.compareTo(BigDecimal.ZERO) == 0) { |
||||
return BigDecimal.ZERO; |
||||
} |
||||
|
||||
return current.subtract(prev) |
||||
.divide(prev, 4, RoundingMode.HALF_UP) |
||||
.multiply(BigDecimal.valueOf(100)); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* 判断趋势(百分比对百分比) |
||||
*/ |
||||
public static TrendEnum calcTrend(BigDecimal current, BigDecimal prev) { |
||||
|
||||
if (current == null) current = BigDecimal.ZERO; |
||||
if (prev == null) prev = BigDecimal.ZERO; |
||||
|
||||
int cmp = current.compareTo(prev); |
||||
|
||||
if (cmp > 0) { |
||||
return TrendEnum.UP; |
||||
} |
||||
|
||||
if (cmp < 0) { |
||||
return TrendEnum.DOWN; |
||||
} |
||||
|
||||
return TrendEnum.STABLE; |
||||
} |
||||
|
||||
public static <E, T> Map.Entry<T, Long> topBy(Function<E, T> mapper, List<E>... lists) { |
||||
|
||||
return Arrays.stream(lists) |
||||
.filter(Objects::nonNull) |
||||
.flatMap(List::stream) |
||||
.map(mapper) |
||||
.filter(Objects::nonNull) |
||||
.collect(Collectors.groupingBy( |
||||
Function.identity(), |
||||
Collectors.counting() |
||||
)) |
||||
.entrySet().stream() |
||||
.max(Map.Entry.comparingByValue()) |
||||
.orElse(null); |
||||
} |
||||
|
||||
|
||||
} |
||||
Binary file not shown.
Loading…
Reference in new issue