zhizhijie
2026-09-20 35d098a89613c96179391178b7f8143dec393ada
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/controller/ReportExportController.java
@@ -2,17 +2,21 @@
import com.trafficaudit.common.Result;
import com.trafficaudit.reportexport.service.ReportExportService;
import com.trafficaudit.reportexport.service.ReportExportTaskService;
import com.trafficaudit.system.service.OperationLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.net.URLEncoder;
import java.util.List;
import java.nio.charset.StandardCharsets;
@Slf4j
@@ -22,32 +26,330 @@
    @Resource
    private ReportExportService reportService;
    @Resource
    private ReportExportTaskService exportTaskService;
    @Resource
    private OperationLogService operationLogService;
    /** 生成_货运量分市州明细.xlsx */
    @GetMapping("/export/cityDetail")
    public ResponseEntity<byte[]> exportCityDetail(@RequestParam("period") String period) throws Exception {
        return buildResponse(reportService.exportCityDetail(period), "生成_货运量分市州明细.xlsx");
    public ResponseEntity<byte[]> exportCityDetail(@RequestParam("period") String period,
                                                    @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExport("生成_货运量分市州明细.xlsx", period);
        return buildResponse(reportService.exportCityDetail(period, mode), "生成_货运量分市州明细.xlsx");
    }
    /** 生成_货运量排名.xlsx */
    @GetMapping("/export/freightRank")
    public ResponseEntity<byte[]> exportFreightRank(@RequestParam("period") String period) throws Exception {
        return buildResponse(reportService.exportFreightRank(period), "生成_货运量排名.xlsx");
    public ResponseEntity<byte[]> exportFreightRank(@RequestParam("period") String period,
                                                     @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExport("生成_货运量排名.xlsx", period);
        return buildResponse(reportService.exportFreightRank(period, mode), "生成_货运量排名.xlsx");
    }
    /** 生成_周转量排名.xlsx */
    @GetMapping("/export/turnoverRank")
    public ResponseEntity<byte[]> exportTurnoverRank(@RequestParam("period") String period) throws Exception {
        return buildResponse(reportService.exportTurnoverRank(period), "生成_周转量排名.xlsx");
    public ResponseEntity<byte[]> exportTurnoverRank(@RequestParam("period") String period,
                                                      @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExport("生成_周转量排名.xlsx", period);
        return buildResponse(reportService.exportTurnoverRank(period, mode), "生成_周转量排名.xlsx");
    }
    /** 生成_公路旅客分市州明细.xlsx(month: 报表期 yyyy-MM;year: 年份 yyyy) */
    @GetMapping("/export/passengerCityDetail")
    public ResponseEntity<byte[]> exportPassengerCityDetail(@RequestParam("period") String period,
                                                            @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExport("生成_公路旅客分市州明细.xlsx", period);
        return buildResponse(reportService.exportPassengerCityDetail(period, mode), "生成_公路旅客分市州明细.xlsx");
    }
    /** 生成_公路旅客中口径明细.xlsx */
    @GetMapping("/export/passengerMidDetail")
    public ResponseEntity<byte[]> exportPassengerMidDetail(@RequestParam("period") String period,
                                                           @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExport("生成_公路旅客中口径明细.xlsx", period);
        return buildResponse(reportService.exportPassengerMidDetail(period, mode), "生成_公路旅客中口径明细.xlsx");
    }
    /** 生成_公路旅客中口径排名.xlsx */
    @GetMapping("/export/passengerMidRank")
    public ResponseEntity<byte[]> exportPassengerMidRank(@RequestParam("period") String period,
                                                         @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExport("生成_公路旅客中口径排名.xlsx", period);
        return buildResponse(reportService.exportPassengerMidRank(period, mode), "生成_公路旅客中口径排名.xlsx");
    }
    /** 生成_公路旅客中口径分析.xlsx */
    @GetMapping("/export/passengerMidAnalysis")
    public ResponseEntity<byte[]> exportPassengerMidAnalysis(@RequestParam("period") String period,
                                                             @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExport("生成_公路旅客中口径分析.xlsx", period);
        return buildResponse(reportService.exportPassengerMidAnalysis(period, mode), "生成_公路旅客中口径分析.xlsx");
    }
    /** 生成_道路运输量汇总表.xlsx(汇总工作簿 P0-P1 骨架:整本复制母版,页签/公式/2025 缓存保留) */
    @GetMapping("/export/summaryWorkbook")
    public ResponseEntity<byte[]> exportSummaryWorkbook(@RequestParam("period") String period,
                                                        @RequestParam(value = "mode", defaultValue = "month") String mode,
                                                        @RequestParam(value = "toMonth", required = false) Integer toMonth) throws Exception {
        logExport("道路运输量汇总表.xlsx", period);
        return buildResponse(reportService.exportSummaryWorkbook(period, mode, toMonth), String.format("生成_道路运输量汇总表_%s.xlsx", period));
    }
    /**
     * 汇总大表异步导出:立即返回任务号,前端轮询进度、完成后再下载文件。
     * 生成一次约 10~20 秒,走异步可避免点击后长时间无响应;同报表期 10 分钟内的结果直接复用(期间有导入则自动失效),
     * refresh=true 可强制重新生成。
     */
    @GetMapping("/exportTask/summaryWorkbook")
    public Result<java.util.Map<String, Object>> submitSummaryWorkbookTask(@RequestParam("period") String period,
                                                                          @RequestParam(value = "mode", defaultValue = "month") String mode,
                                                                          @RequestParam(value = "toMonth", required = false) Integer toMonth,
                                                                          @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) {
        ReportExportTaskService.Task task = exportTaskService.submit(
                ReportExportTaskService.TYPE_SUMMARY_WORKBOOK, period, mode, toMonth, refresh);
        if (!task.isFromCache()) {
            logExport("道路运输量汇总表.xlsx", period);
        }
        return Result.ok(task.toView());
    }
    /** 查询异步导出任务进度(结果保留 30 分钟) */
    @GetMapping("/exportTask/{taskId}")
    public Result<java.util.Map<String, Object>> exportTaskStatus(@PathVariable("taskId") String taskId) {
        ReportExportTaskService.Task task = exportTaskService.get(taskId);
        if (task == null) {
            return Result.error("导出任务不存在或已过期(生成结果保留 30 分钟)");
        }
        return Result.ok(task.toView());
    }
    /** 下载异步导出任务已生成的文件 */
    @GetMapping("/exportTask/{taskId}/file")
    public ResponseEntity<byte[]> exportTaskFile(@PathVariable("taskId") String taskId) throws Exception {
        ReportExportTaskService.Task task = exportTaskService.get(taskId);
        if (task == null) {
            throw new RuntimeException("导出任务不存在或已过期(生成结果保留 30 分钟)");
        }
        if (!task.isDone()) {
            throw new RuntimeException("导出任务尚未完成(当前状态:" + task.getStatus() + "),请稍后再试");
        }
        return buildResponse(task.getData(), task.getFileName());
    }
    /** 生成_网约车客运量拆分表.xlsx(月度:订单附件1 + 全省 pin + 巡游出租月报) */
    @GetMapping("/export/wycSplit")
    public ResponseEntity<byte[]> exportWycSplit(@RequestParam("period") String period,
                                                 @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExport("生成_网约车客运量拆分表.xlsx", period);
        return buildResponse(reportService.exportWycSplit(period, mode), String.format("生成_网约车客运量拆分表_%s.xlsx", period));
    }
    /** 生成_能运汇总表.xlsx(H204,全省 + 各市州 sheet) */
    @GetMapping("/export/energySummary")
    public ResponseEntity<byte[]> exportEnergySummary(@RequestParam("period") String period) throws Exception {
        logExport("生成_能运汇总表.xlsx", period);
        return buildResponse(reportService.exportEnergySummary(period), "生成_能运汇总表.xlsx");
    }
    /** 生成_城市公交客运量分市州明细.xlsx(城市客运第1步,公交) */
    @GetMapping("/export/cityBusDetail")
    public ResponseEntity<byte[]> exportCityBusDetail(@RequestParam("period") String period,
                                                       @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExport("生成_城市公交客运量分市州明细.xlsx", period);
        return buildResponse(reportService.exportCityBusDetail(period, mode), "生成_城市公交客运量分市州明细.xlsx");
    }
    /** 生成_巡游出租客运量分市州明细.xlsx(城市客运第2步,巡游出租) */
    @GetMapping("/export/cityTaxiDetail")
    public ResponseEntity<byte[]> exportCityTaxiDetail(@RequestParam("period") String period,
                                                        @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExport("生成_巡游出租客运量分市州明细.xlsx", period);
        return buildResponse(reportService.exportCityTaxiDetail(period, mode), "生成_巡游出租客运量分市州明细.xlsx");
    }
    /** 生成_2026年全省预安排计划进度情况汇总.xlsx(投资模块表1) */
    @GetMapping("/export/investPlan")
    public ResponseEntity<byte[]> exportInvestPlan(@RequestParam("period") String period,
                                                    @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExport("生成_全省预安排计划进度情况汇总.xls", period);
        return buildResponse(reportService.investPlan(period, mode), "生成_全省预安排计划进度情况汇总.xls");
    }
    /** 生成_"十五五"规划物流项目进展情况.xlsx(投资模块表2) */
    @GetMapping("/export/investFiveYearLogistics")
    public ResponseEntity<byte[]> exportInvestFiveYearLogistics(@RequestParam("period") String period,
                                                                 @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExport("生成_十五五规划物流项目进展情况.xls", period);
        return buildResponse(reportService.investFiveYearLogistics(period, mode), "生成_十五五规划物流项目进展情况.xls");
    }
    /** 生成_湖北省(客货站场)亿元投资项目.xls(投资模块表3,投资总表+规上项目表两sheet) */
    @GetMapping("/export/investBillion")
    public ResponseEntity<byte[]> exportInvestBillion(@RequestParam("period") String period,
                                                       @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExport("生成_湖北省(客货站场)亿元投资项目.xls", period);
        return buildResponse(reportService.investBillion(period, mode), "生成_湖北省(客货站场)亿元投资项目.xls");
    }
    /** 生成_(经济强县项目)交通物流基础设施投资统计报表.xlsx(投资模块表4) */
    @GetMapping("/export/investCounty")
    public ResponseEntity<byte[]> exportInvestCounty(@RequestParam("period") String period,
                                                      @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExport("生成_经济强县交通物流基础设施投资统计报表.xls", period);
        return buildResponse(reportService.investCounty(period, mode), "生成_经济强县交通物流基础设施投资统计报表.xls");
    }
    /** 生成_X月客运站投资明细表.xlsx(投资模块明细表1:客运站场) */
    @GetMapping("/export/investPassengerDetail")
    public ResponseEntity<byte[]> exportInvestPassengerDetail(@RequestParam("period") String period,
                                                              @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExport("生成_X月客运站投资明细表.xlsx", period);
        return buildResponse(reportService.exportInvestPassengerDetail(period, mode), "生成_客运站投资明细表.xlsx");
    }
    /** 生成_X月物流站场投资明细.xls(投资模块明细表2:物流园区) */
    @GetMapping("/export/investLogisticsDetail")
    public ResponseEntity<byte[]> exportInvestLogisticsDetail(@RequestParam("period") String period,
                                                              @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExport("生成_X月物流站场投资明细.xls", period);
        return buildResponse(reportService.exportInvestLogisticsDetail(period, mode), "生成_物流站场投资明细.xls");
    }
    /** 生成_轨道轮渡客运量分市州明细.xlsx(城市客运,轨道/轮渡取自公交源文件) */
    @GetMapping("/export/cityRailFerryDetail")
    public ResponseEntity<byte[]> exportCityRailFerryDetail(@RequestParam("period") String period,
                                                             @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExport("生成_轨道轮渡客运量分市州明细.xlsx", period);
        return buildResponse(reportService.exportCityRailFerryDetail(period, mode), "生成_轨道轮渡客运量分市州明细.xlsx");
    }
    /** 生成_城市客运客运量分市州累计.xlsx(城市分市州_模板底稿) */
    @GetMapping("/export/cityPassengerCitySum")
    public ResponseEntity<byte[]> exportCityPassengerCitySum(@RequestParam("period") String period,
                                                              @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExport("生成_城市客运客运量分市州累计.xlsx", period);
        return buildResponse(reportService.exportCityPassengerCitySum("生成_城市客运客运量分市州累计.xlsx", period, mode), "生成_城市客运客运量分市州累计.xlsx");
    }
    /** 生成_城市客运各市州明细表.xlsx(城市客运各市州明细表_模板底稿) */
    @GetMapping("/export/cityPassengerDetailSum")
    public ResponseEntity<byte[]> exportCityPassengerDetailSum(@RequestParam("period") String period,
                                                                @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExport("生成_城市客运各市州明细表.xlsx", period);
        return buildResponse(reportService.exportCityPassengerCitySum("生成_城市客运各市州明细表.xlsx", period, mode), "生成_城市客运各市州明细表.xlsx");
    }
    /** 生成_城市客运汇总.xlsx(城市汇总_模板底稿) */
    @GetMapping("/export/cityPassengerSummary")
    public ResponseEntity<byte[]> exportCityPassengerSummary(@RequestParam("period") String period,
                                                              @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExport("生成_城市客运汇总.xlsx", period);
        return buildResponse(reportService.exportCityPassengerSummary(period, mode), "生成_城市客运汇总.xlsx");
    }
    /** 生成_城市客运报表系列.zip(一键打包全部:公交/出租/轨道轮渡明细 + 分市州累计 + 各市州明细表 + 汇总) */
    @GetMapping("/export/cityPassengerSeries")
    public ResponseEntity<byte[]> exportCityPassengerSeries(@RequestParam("period") String period,
                                                             @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExportZip("生成_城市客运报表系列.zip", period);
        String fileName = String.format("生成_城市客运报表系列_%s.zip", period);
        String encoded = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20");
        return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded)
            .contentType(MediaType.parseMediaType("application/zip"))
            .body(reportService.exportCityPassengerSeries(period, mode));
    }
    /** 生成_货运报表系列.zip(一键打包:明细+排名) */
    @GetMapping("/export/freightSeries")
    public ResponseEntity<byte[]> exportFreightSeries(@RequestParam("period") String period,
                                                       @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExportZip("生成_货运报表系列.zip", period);
        return zipResponse(reportService.exportFreightSeries(period, mode), String.format("生成_货运报表系列_%s.zip", period));
    }
    /** 生成_公路旅客报表系列.zip(一键打包:分市州明细+中口径明细/排名/分析) */
    @GetMapping("/export/passengerSeries")
    public ResponseEntity<byte[]> exportPassengerSeries(@RequestParam("period") String period,
                                                        @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExportZip("生成_公路旅客报表系列.zip", period);
        return zipResponse(reportService.exportPassengerSeries(period, mode), String.format("生成_公路旅客报表系列_%s.zip", period));
    }
    /** 生成_投资报表系列.zip(一键打包:预安排/十五五/亿元/经济强县) */
    @GetMapping("/export/investSeries")
    public ResponseEntity<byte[]> exportInvestSeries(@RequestParam("period") String period,
                                                      @RequestParam(value = "mode", defaultValue = "month") String mode) throws Exception {
        logExportZip("生成_投资报表系列.zip", period);
        return zipResponse(reportService.exportInvestSeries(period, mode), String.format("生成_投资报表系列_%s.zip", period));
    }
    /** 生成_所选报表.zip:types 逗号分隔(前端"生成选中"多选) */
    @GetMapping("/export/selected")
    public ResponseEntity<byte[]> exportSelected(@RequestParam("period") String period,
                                                 @RequestParam(value = "mode", defaultValue = "month") String mode,
                                                 @RequestParam("types") String types) throws Exception {
        List<String> typeList = new java.util.ArrayList<>();
        for (String t : types.split(",")) {
            String s = t.trim();
            if (!s.isEmpty()) typeList.add(s);
        }
        logExportZip("生成_所选报表.zip", period);
        return zipResponse(reportService.exportSelectedSeries(period, mode, typeList),
                String.format("生成_所选报表_%s.zip", period));
    }
    /** 导出前数据就绪校验:返回每张报表依赖的数据行数/缺失提示(types 逗号分隔) */
    @GetMapping("/ready")
    public Result<java.util.Map<String, Object>> exportReady(@RequestParam("period") String period,
                                                              @RequestParam(value = "mode", defaultValue = "month") String mode,
                                                              @RequestParam("types") String types) {
        List<String> typeList = new java.util.ArrayList<>();
        for (String t : types.split(",")) {
            String s = t.trim();
            if (!s.isEmpty()) typeList.add(s);
        }
        return Result.ok(reportService.checkExportReady(period, mode, typeList));
    }
    /** 报表数据预览(轻量版):就绪 + 月度覆盖 + 核心指标摘要 + 问题清单(types 逗号分隔) */
    @GetMapping("/preview")
    public Result<java.util.Map<String, Object>> reportPreview(@RequestParam("period") String period,
                                                                @RequestParam(value = "mode", defaultValue = "month") String mode,
                                                                @RequestParam("types") String types) {
        List<String> typeList = new java.util.ArrayList<>();
        for (String t : types.split(",")) {
            String s = t.trim();
            if (!s.isEmpty()) typeList.add(s);
        }
        return Result.ok(reportService.reportPreview(period, mode, typeList));
    }
    private void logExport(String reportName, String period) {
        operationLogService.record("报表生成", "导出报表", reportName + "(报表期:" + period + ")");
    }
    private void logExportZip(String zipName, String period) {
        operationLogService.record("报表生成", "导出报表系列", zipName + "(报表期:" + period + ")");
    }
    private ResponseEntity<byte[]> zipResponse(byte[] data, String fileName) throws Exception {
        String encoded = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20");
        return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded)
            .contentType(MediaType.parseMediaType("application/zip"))
            .body(data);
    }
    private ResponseEntity<byte[]> buildResponse(byte[] data, String fileName) throws Exception {
        String encoded = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20");
        String type = fileName.toLowerCase().endsWith(".xls")
            ? "application/vnd.ms-excel"
            : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
        return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded)
            .contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
            .contentType(MediaType.parseMediaType(type))
            .body(data);
    }
}