diff --git a/pom.xml b/pom.xml index c8aa6dc..9f0b2b1 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,18 @@ + + org.jfree + jfreechart + 1.5.4 + + + + com.deepoove + poi-tl + 1.12.2 + + org.projectlombok lombok diff --git a/src/main/java/com/biutag/supervision/constants/enums/CheckStatusEnum.java b/src/main/java/com/biutag/supervision/constants/enums/CheckStatusEnum.java index 666129d..835c4fe 100644 --- a/src/main/java/com/biutag/supervision/constants/enums/CheckStatusEnum.java +++ b/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 TRUE_LIST = List.of(TRUE.value); + + /** + * 部分属实 + */ + public static final List PART_TRUE_LIST = List.of(PARTIALLY_TRUE.value, QTFT.value); + + /** + * 不属实 + */ + public static final List FALSE_LIST = List.of(WFCS.value, FALSE.value); + } diff --git a/src/main/java/com/biutag/supervision/controller/work/NegativeController.java b/src/main/java/com/biutag/supervision/controller/work/NegativeController.java index f8cd869..98f0362 100644 --- a/src/main/java/com/biutag/supervision/controller/work/NegativeController.java +++ b/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); + } + } diff --git a/src/main/java/com/biutag/supervision/pojo/dto/report/OverviewSection.java b/src/main/java/com/biutag/supervision/pojo/dto/report/OverviewSection.java new file mode 100644 index 0000000..e08c281 --- /dev/null +++ b/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; +} \ No newline at end of file diff --git a/src/main/java/com/biutag/supervision/pojo/dto/report/ReportViewModel.java b/src/main/java/com/biutag/supervision/pojo/dto/report/ReportViewModel.java new file mode 100644 index 0000000..b0ebde6 --- /dev/null +++ b/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; + +} diff --git a/src/main/java/com/biutag/supervision/pojo/enums/report/TrendEnum.java b/src/main/java/com/biutag/supervision/pojo/enums/report/TrendEnum.java new file mode 100644 index 0000000..0f45730 --- /dev/null +++ b/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; + } +} diff --git a/src/main/java/com/biutag/supervision/pojo/param/negativeBlame/NegativeBlameQueryParam.java b/src/main/java/com/biutag/supervision/pojo/param/negativeBlame/NegativeBlameQueryParam.java index 852af3b..ea40a20 100644 --- a/src/main/java/com/biutag/supervision/pojo/param/negativeBlame/NegativeBlameQueryParam.java +++ b/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 negativeIds; @Schema(description = "涉及人员禁闭处罚id") private String confinementId; diff --git a/src/main/java/com/biutag/supervision/pojo/request/negative/ReportGenerationRequest.java b/src/main/java/com/biutag/supervision/pojo/request/negative/ReportGenerationRequest.java new file mode 100644 index 0000000..5bd044d --- /dev/null +++ b/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); + } +} \ No newline at end of file diff --git a/src/main/java/com/biutag/supervision/repository/negativeBlame/NegativeBlameResourceService.java b/src/main/java/com/biutag/supervision/repository/negativeBlame/NegativeBlameResourceService.java index 4ddb5fa..c150049 100644 --- a/src/main/java/com/biutag/supervision/repository/negativeBlame/NegativeBlameResourceService.java +++ b/src/main/java/com/biutag/supervision/repository/negativeBlame/NegativeBlameResourceService.java @@ -28,7 +28,7 @@ public class NegativeBlameResourceService extends BaseDAO { LambdaQueryWrapper 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(); diff --git a/src/main/java/com/biutag/supervision/service/NegativeService.java b/src/main/java/com/biutag/supervision/service/NegativeService.java index 2787e9f..6340b1f 100644 --- a/src/main/java/com/biutag/supervision/service/NegativeService.java +++ b/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 { private final NegativeCountersignApplyService countersignApplyService; + private final ReportDataService reportDataService; + public NegativeDetail get(String id, Integer workId) { Negative negative = getById(id); List flows = negativeHistoryService.listByNegativeId(id); @@ -476,4 +492,50 @@ public class NegativeService extends ServiceImpl { } } + + + + 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); + } + } } diff --git a/src/main/java/com/biutag/supervision/service/negativeReport/NegativeReportService.java b/src/main/java/com/biutag/supervision/service/negativeReport/NegativeReportService.java new file mode 100644 index 0000000..d4a9876 --- /dev/null +++ b/src/main/java/com/biutag/supervision/service/negativeReport/NegativeReportService.java @@ -0,0 +1,4 @@ +package com.biutag.supervision.service.negativeReport; + +public interface NegativeReportService { +} diff --git a/src/main/java/com/biutag/supervision/service/negativeReport/NegativeReportServiceImpl.java b/src/main/java/com/biutag/supervision/service/negativeReport/NegativeReportServiceImpl.java new file mode 100644 index 0000000..7f8fc7d --- /dev/null +++ b/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 { + +} diff --git a/src/main/java/com/biutag/supervision/service/report/ReportDataService.java b/src/main/java/com/biutag/supervision/service/report/ReportDataService.java new file mode 100644 index 0000000..d31c12b --- /dev/null +++ b/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); + + + +} diff --git a/src/main/java/com/biutag/supervision/service/report/ReportDataServiceImpl.java b/src/main/java/com/biutag/supervision/service/report/ReportDataServiceImpl.java new file mode 100644 index 0000000..acd9ba9 --- /dev/null +++ b/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 ztNegativeList = negativeResourceService.query(ztNegativeQueryParam); + // 环比数据 + NegativeQueryParam hbQueryParam = new NegativeQueryParam(); + hbQueryParam.setCrtTime(compareDateRange.mom()); + List hbNegativList = negativeResourceService.query(hbQueryParam); + // 同比数据 + NegativeQueryParam tbQueryParam = new NegativeQueryParam(); + tbQueryParam.setCrtTime(compareDateRange.yoy()); + List tbNegativList = negativeResourceService.query(tbQueryParam); + + // 市局下发数据 + List sjxfNegativeList = ztNegativeList.stream().filter(one -> Objects.equals(0, one.getCrtDepartLevel())).toList(); + // 分县市局下发数据 + List fxsjxfNegativeList = ztNegativeList.stream().filter(one -> Objects.equals(2, one.getCrtDepartLevel())).toList(); + // 办结数据 + List bjNegativeList = ztNegativeList.stream().filter(one -> !Objects.isNull(one.getCompleteDate())).toList(); + // 办结中属实数据 + List bjssNegativeList = bjNegativeList.stream().filter(one -> CheckStatusEnum.TRUE_LIST.contains(one.getCheckStatusCode())).toList(); + // 办结中基本属实数据 + List bjjbssNegativeList = bjNegativeList.stream().filter(one -> CheckStatusEnum.PART_TRUE_LIST.contains(one.getCheckStatusCode())).toList(); + // 办结中不属实数据 + List bjbssNegativeList = bjNegativeList.stream().filter(one -> CheckStatusEnum.FALSE_LIST.contains(one.getCheckStatusCode())).toList(); + // 属实 基本属实中的问责数据 + List ssNegativeIds = Stream.concat(bjssNegativeList.stream(), bjjbssNegativeList.stream()) + .map(Negative::getId).filter(StrUtil::isNotBlank).distinct().toList(); + NegativeBlameQueryParam negativeBlameQueryParam = new NegativeBlameQueryParam(); + negativeBlameQueryParam.setNegativeIds(ssNegativeIds); + List negativeBlames = negativeBlameResourceService.query(negativeBlameQueryParam); + // 个人问责 + List grwzNegativeBlames = negativeBlames.stream().filter(one -> "personal".equals(one.getType())) + .filter(one -> StrUtil.isNotBlank(one.getHandleResultName())) + .filter(one -> !"不予追责".equals(one.getHandleResultName())).toList(); + + // 单位问责 + List 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 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 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 list) { + // 办结 + List 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); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/biutag/supervision/util/ChartRenderUtil.java b/src/main/java/com/biutag/supervision/util/ChartRenderUtil.java new file mode 100644 index 0000000..a4aaded --- /dev/null +++ b/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 data, int w, int h) { + DefaultPieDataset 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 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); + } +} \ No newline at end of file diff --git a/src/main/java/com/biutag/supervision/util/DateCompareRangeUtil.java b/src/main/java/com/biutag/supervision/util/DateCompareRangeUtil.java new file mode 100644 index 0000000..200dad2 --- /dev/null +++ b/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 current, List mom, List 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 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 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 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()); + } +} \ No newline at end of file diff --git a/src/main/java/com/biutag/supervision/util/ReportTrendUtil.java b/src/main/java/com/biutag/supervision/util/ReportTrendUtil.java new file mode 100644 index 0000000..adc5cee --- /dev/null +++ b/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 Map.Entry topBy(Function mapper, List... 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); + } + + +} diff --git a/src/main/java/com/biutag/supervision/util/TimeUtil.java b/src/main/java/com/biutag/supervision/util/TimeUtil.java index d08a03f..2a0019d 100644 --- a/src/main/java/com/biutag/supervision/util/TimeUtil.java +++ b/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); + } } diff --git a/src/main/resources/static/templates/督审一体化平台研判分析报告.docx b/src/main/resources/static/templates/督审一体化平台研判分析报告.docx new file mode 100644 index 0000000..d13e753 Binary files /dev/null and b/src/main/resources/static/templates/督审一体化平台研判分析报告.docx differ