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