Browse Source

研判分析报告--step1--总体情况

master
buaixuexideshitongxue 2 weeks ago
parent
commit
4f3666c00b
  1. 12
      pom.xml
  2. 17
      src/main/java/com/biutag/supervision/constants/enums/CheckStatusEnum.java
  3. 10
      src/main/java/com/biutag/supervision/controller/work/NegativeController.java
  4. 105
      src/main/java/com/biutag/supervision/pojo/dto/report/OverviewSection.java
  5. 28
      src/main/java/com/biutag/supervision/pojo/dto/report/ReportViewModel.java
  6. 31
      src/main/java/com/biutag/supervision/pojo/enums/report/TrendEnum.java
  7. 6
      src/main/java/com/biutag/supervision/pojo/param/negativeBlame/NegativeBlameQueryParam.java
  8. 45
      src/main/java/com/biutag/supervision/pojo/request/negative/ReportGenerationRequest.java
  9. 2
      src/main/java/com/biutag/supervision/repository/negativeBlame/NegativeBlameResourceService.java
  10. 62
      src/main/java/com/biutag/supervision/service/NegativeService.java
  11. 4
      src/main/java/com/biutag/supervision/service/negativeReport/NegativeReportService.java
  12. 20
      src/main/java/com/biutag/supervision/service/negativeReport/NegativeReportServiceImpl.java
  13. 19
      src/main/java/com/biutag/supervision/service/report/ReportDataService.java
  14. 185
      src/main/java/com/biutag/supervision/service/report/ReportDataServiceImpl.java
  15. 219
      src/main/java/com/biutag/supervision/util/ChartRenderUtil.java
  16. 78
      src/main/java/com/biutag/supervision/util/DateCompareRangeUtil.java
  17. 124
      src/main/java/com/biutag/supervision/util/ReportTrendUtil.java
  18. 7
      src/main/java/com/biutag/supervision/util/TimeUtil.java
  19. BIN
      src/main/resources/static/templates/督审一体化平台研判分析报告.docx

12
pom.xml

@ -31,6 +31,18 @@
</properties>
<dependencies>
<dependency>
<groupId>org.jfree</groupId>
<artifactId>jfreechart</artifactId>
<version>1.5.4</version>
</dependency>
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.12.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>

17
src/main/java/com/biutag/supervision/constants/enums/CheckStatusEnum.java

@ -3,6 +3,8 @@ package com.biutag.supervision.constants.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
@Getter
@AllArgsConstructor
public enum CheckStatusEnum {
@ -20,4 +22,19 @@ public enum CheckStatusEnum {
private String label;
/**
* 属实
*/
public static final List<String> TRUE_LIST = List.of(TRUE.value);
/**
* 部分属实
*/
public static final List<String> PART_TRUE_LIST = List.of(PARTIALLY_TRUE.value, QTFT.value);
/**
* 不属实
*/
public static final List<String> FALSE_LIST = List.of(WFCS.value, FALSE.value);
}

10
src/main/java/com/biutag/supervision/controller/work/NegativeController.java

@ -22,15 +22,19 @@ import com.biutag.supervision.pojo.dto.flow.VerifyData;
import com.biutag.supervision.pojo.entity.*;
import com.biutag.supervision.pojo.model.UserAuth;
import com.biutag.supervision.pojo.param.NegativeQueryParam;
import com.biutag.supervision.pojo.request.negative.ReportGenerationRequest;
import com.biutag.supervision.pojo.vo.NegativeConfirmationCompletionVo;
import com.biutag.supervision.pojo.vo.NegativeQueryVo;
import com.biutag.supervision.service.*;
import com.biutag.supervision.util.JSON;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.UnsupportedEncodingException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
@ -298,4 +302,10 @@ public class NegativeController {
return Result.success();
}
@Operation(summary = "生成研判分析报告")
@PostMapping("/generateReport")
public void generateReport(@RequestBody ReportGenerationRequest request, HttpServletResponse response) throws UnsupportedEncodingException {
negativeService.generateReport(request, response);
}
}

105
src/main/java/com/biutag/supervision/pojo/dto/report/OverviewSection.java

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

28
src/main/java/com/biutag/supervision/pojo/dto/report/ReportViewModel.java

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

31
src/main/java/com/biutag/supervision/pojo/enums/report/TrendEnum.java

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

6
src/main/java/com/biutag/supervision/pojo/param/negativeBlame/NegativeBlameQueryParam.java

@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
import java.util.Set;
/**
@ -16,6 +17,11 @@ import java.util.Set;
@Setter
public class NegativeBlameQueryParam {
@Schema(description = "问题id")
private String negativeId;
@Schema(description = "问题Ids")
private List<String> negativeIds;
@Schema(description = "涉及人员禁闭处罚id")
private String confinementId;

45
src/main/java/com/biutag/supervision/pojo/request/negative/ReportGenerationRequest.java

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

2
src/main/java/com/biutag/supervision/repository/negativeBlame/NegativeBlameResourceService.java

@ -28,7 +28,7 @@ public class NegativeBlameResourceService extends BaseDAO {
LambdaQueryWrapper<NegativeBlame> queryWrapper = new LambdaQueryWrapper<>();
setBatchQuery(param.getConfinementId(), param.getConfinementIds(), queryWrapper, NegativeBlame::getConfinementId);
setBatchQuery(param.getLeadConfinementId(), param.getLeadConfinementIds(), queryWrapper, NegativeBlame::getLeadConfinementId);
setBatchQuery(param.getNegativeId(), param.getNegativeIds(), queryWrapper, NegativeBlame::getNegativeId);
// 防御
if (queryWrapper.getExpression() == null || queryWrapper.getExpression().getSqlSegment().isEmpty()) {
return Collections.emptyList();

62
src/main/java/com/biutag/supervision/service/NegativeService.java

@ -27,17 +27,31 @@ import com.biutag.supervision.pojo.dto.NegativeDto;
import com.biutag.supervision.pojo.dto.flow.FirstDistributeData;
import com.biutag.supervision.pojo.dto.flow.VerifyData;
import com.biutag.supervision.pojo.dto.jwdc.NegativeApiDto;
import com.biutag.supervision.pojo.dto.report.ReportViewModel;
import com.biutag.supervision.pojo.entity.*;
import com.biutag.supervision.pojo.model.UserAuth;
import com.biutag.supervision.pojo.request.negative.ReportGenerationRequest;
import com.biutag.supervision.pojo.vo.NegativeFileVo;
import com.biutag.supervision.service.report.ReportDataService;
import com.biutag.supervision.util.ChartRenderUtil;
import com.biutag.supervision.util.SpringUtil;
import com.biutag.supervision.util.TimeUtil;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.data.Pictures;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StreamUtils;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
@ -80,6 +94,8 @@ public class NegativeService extends ServiceImpl<NegativeMapper, Negative> {
private final NegativeCountersignApplyService countersignApplyService;
private final ReportDataService reportDataService;
public NegativeDetail get(String id, Integer workId) {
Negative negative = getById(id);
List<NegativeHistory> flows = negativeHistoryService.listByNegativeId(id);
@ -476,4 +492,50 @@ public class NegativeService extends ServiceImpl<NegativeMapper, Negative> {
}
}
public void generateReport(ReportGenerationRequest request, HttpServletResponse response) {
// 你要返回的模板路径
String templatePath = "static/templates/督审一体化平台研判分析报告.docx";
ClassPathResource resource = new ClassPathResource(templatePath);
if (!resource.exists()) {
// 资源不存在,返回 404
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.setContentType("application/json;charset=UTF-8");
try {
response.getWriter().write("{\"message\":\"模板文件不存在: " + templatePath + "\"}");
} catch (IOException ignored) {}
return;
}
// 1) 准备模板变量(含图表)
ReportViewModel vm = reportDataService.buildViewModel(request);
// 2) 响应头(docx 正确类型)
String downloadName = "督审一体化平台研判分析报告.docx";
String encoded = URLEncoder.encode(downloadName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
String contentDisposition = "attachment; filename=\"" + encoded + "\"; filename*=UTF-8''" + encoded;
response.reset();
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Disposition", contentDisposition);
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
// 3) 关键:compile + render + write(不要 StreamUtils.copy)
try (InputStream in = resource.getInputStream();
XWPFTemplate template = XWPFTemplate.compile(in).render(vm);
ServletOutputStream out = response.getOutputStream()) {
template.write(out);
out.flush();
} catch (Exception e) {
log.error("生成/下载报告失败", e);
// 如果你们有统一异常处理,可以直接抛出去
// throw new RuntimeException("生成报告失败", e);
}
}
}

4
src/main/java/com/biutag/supervision/service/negativeReport/NegativeReportService.java

@ -0,0 +1,4 @@
package com.biutag.supervision.service.negativeReport;
public interface NegativeReportService {
}

20
src/main/java/com/biutag/supervision/service/negativeReport/NegativeReportServiceImpl.java

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

19
src/main/java/com/biutag/supervision/service/report/ReportDataService.java

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

185
src/main/java/com/biutag/supervision/service/report/ReportDataServiceImpl.java

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

219
src/main/java/com/biutag/supervision/util/ChartRenderUtil.java

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

78
src/main/java/com/biutag/supervision/util/DateCompareRangeUtil.java

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

124
src/main/java/com/biutag/supervision/util/ReportTrendUtil.java

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

7
src/main/java/com/biutag/supervision/util/TimeUtil.java

@ -6,9 +6,11 @@ import cn.hutool.core.util.StrUtil;
import com.biutag.supervision.constants.enums.FlowNodeEnum;
import com.biutag.supervision.service.HolidayService;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Objects;
public class TimeUtil {
@ -102,4 +104,9 @@ public class TimeUtil {
}
return getRemainingDuration(beginTime, maxHandleDuration, extensionDurationDays);
}
public static String formatDate(Date date) {
return new SimpleDateFormat("yyyy年MM月dd日").format(date);
}
}

BIN
src/main/resources/static/templates/督审一体化平台研判分析报告.docx

Binary file not shown.
Loading…
Cancel
Save