traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java
@@ -2,7 +2,9 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.trafficaudit.auditengine.entity.AuditResult;
import com.trafficaudit.auditengine.entity.AuditRun;
import com.trafficaudit.auditengine.mapper.AuditResultMapper;
import com.trafficaudit.auditengine.mapper.AuditRunMapper;
import com.trafficaudit.common.util.RegionUtil;
import com.trafficaudit.dataimport.entity.EnergyAuthVehicle;
import com.trafficaudit.dataimport.entity.EnergyVehicleQuarterly;
@@ -54,8 +56,13 @@
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import cn.hutool.core.io.IoUtil;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
@@ -142,6 +149,8 @@
    private FreightTurnoverImportMapper freightTurnoverMapper;
    @Resource
    private AuditResultMapper auditResultMapper;
    @Resource
    private AuditRunMapper auditRunMapper;
    @Resource
    private PassengerEnterpriseMonthlyMapper passengerMapper;
    @Resource
@@ -558,24 +567,30 @@
        }
    }
    /** 公路旅客运政车辆数(独立模板:企业代码/企业名称/车辆数/载客位数) */
    /** 公路旅客运政车辆数(兼容两种表头:① 企业代码/企业名称/车辆数/载客位数;② 运政库导出 OWNERNAME/总车辆数/客位数,无企业代码列) */
    public ImportResult importPassengerAuth(MultipartFile file, String period) throws Exception {
        List<PassengerAuthVehicle> authList = new ArrayList<>();
        try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
            Sheet sheet = wb.getSheetAt(0);
            Map<String, Integer> colMap = buildHeaderMap(sheet.getRow(0));
            requireHeaderCols(sheet, "旅客运政车辆数", "企业名称", "车辆数", "载客位数");
            Map<String, Integer> colMap = buildHeaderMapCompat(sheet.getRow(0));
            Integer nameCol = firstColIdx(colMap, "企业名称", "OWNERNAME");
            Integer vehicleCol = firstColIdx(colMap, "车辆数", "总车辆数");
            Integer seatCol = firstColIdx(colMap, "载客位数", "客位数");
            Integer codeCol = firstColIdx(colMap, "企业代码");
            if (nameCol == null || vehicleCol == null || seatCol == null) {
                throw new RuntimeException("所选数据类型【旅客运政车辆数】与文件内容不符:表头需含【企业名称/车辆数/载客位数】或【OWNERNAME/总车辆数/客位数】,请确认是否选错了数据类型或使用了旧模板");
            }
            for (int r = 1; r <= sheet.getLastRowNum(); r++) {
                Row row = sheet.getRow(r);
                if (row == null) continue;
                String name = gs(row, colMap, "企业名称");
                String name = getString(row, nameCol);
                if (name == null || name.trim().isEmpty()) continue;
                PassengerAuthVehicle auth = new PassengerAuthVehicle();
                auth.setReportPeriod(period);
                auth.setEnterpriseCode(gs(row, colMap, "企业代码"));
                auth.setEnterpriseCode(codeCol == null ? null : getString(row, codeCol));
                auth.setEnterpriseName(name.trim());
                auth.setVehicleCount(gi(row, colMap, "车辆数"));
                auth.setSeatCount(gi(row, colMap, "载客位数"));
                auth.setVehicleCount(getInt(row, vehicleCol));
                auth.setSeatCount(getInt(row, seatCol));
                authList.add(auth);
            }
        } catch (Exception e) {
@@ -1306,6 +1321,275 @@
        }
    }
    /** 支持“按类型清空”的数据类型 -> 数据表操作键(同表同类别归并,避免重复删除) */
    private static final Map<String, String> CLEAR_TYPE_TO_OP = new LinkedHashMap<>();
    /** 数据类型 -> 关联审核规则类型(同步清除审核结果与已审核标记) */
    private static final Map<String, String[]> CLEAR_TYPE_TO_AUDIT = new HashMap<>();
    /** 数据类型 -> 关联导入批次 importType(同步清除导入批次记录) */
    private static final Map<String, String[]> CLEAR_TYPE_TO_BATCH = new HashMap<>();
    static {
        CLEAR_TYPE_TO_OP.put("h2032", "H2032");
        CLEAR_TYPE_TO_OP.put("transportAuth", "TRANSPORT_AUTH");
        CLEAR_TYPE_TO_OP.put("trackMileage", "TRACK_MILEAGE");
        CLEAR_TYPE_TO_OP.put("scaleSplit", "SCALE_SPLIT");
        CLEAR_TYPE_TO_OP.put("freightTurnover", "FREIGHT_TURNOVER");
        CLEAR_TYPE_TO_OP.put("passengerMonthly", "H2031");
        CLEAR_TYPE_TO_OP.put("passengerAuth", "PASSENGER_AUTH");
        CLEAR_TYPE_TO_OP.put("passengerIndividual", "PASSENGER_INDIVIDUAL");
        CLEAR_TYPE_TO_OP.put("energyMonthly", "H204");
        CLEAR_TYPE_TO_OP.put("energyAuth", "ENERGY_AUTH");
        CLEAR_TYPE_TO_OP.put("investment", "INVEST_MONTHLY_PASSENGER");
        CLEAR_TYPE_TO_OP.put("investCityPassenger", "INVEST_MONTHLY_PASSENGER");
        CLEAR_TYPE_TO_OP.put("investmentLogistics", "INVEST_MONTHLY_LOGISTICS");
        CLEAR_TYPE_TO_OP.put("investCityLogistics", "INVEST_MONTHLY_LOGISTICS");
        CLEAR_TYPE_TO_OP.put("investmentSystem", "INVEST_SYSTEM");
        CLEAR_TYPE_TO_OP.put("cityBus", "CITY_BUS");
        CLEAR_TYPE_TO_OP.put("cityTaxi", "CITY_TAXI");
        CLEAR_TYPE_TO_OP.put("cityTaxiAuth", "CITY_TAXI_AUTH");
        CLEAR_TYPE_TO_OP.put("wycTotal", "WYC_TOTAL");
        CLEAR_TYPE_TO_OP.put("wycOrder", "WYC_ORDER");
        CLEAR_TYPE_TO_OP.put("wycOrderReport", "WYC_ORDER");
        CLEAR_TYPE_TO_AUDIT.put("h2032", new String[]{"H2032"});
        CLEAR_TYPE_TO_AUDIT.put("transportAuth", new String[]{"H2032"});
        CLEAR_TYPE_TO_AUDIT.put("trackMileage", new String[]{"H2032"});
        CLEAR_TYPE_TO_AUDIT.put("scaleSplit", new String[]{"H2032"});
        CLEAR_TYPE_TO_AUDIT.put("freightTurnover", new String[]{"H2032"});
        CLEAR_TYPE_TO_AUDIT.put("passengerMonthly", new String[]{"H2031"});
        CLEAR_TYPE_TO_AUDIT.put("passengerAuth", new String[]{"H2031"});
        CLEAR_TYPE_TO_AUDIT.put("passengerIndividual", new String[]{"H2031"});
        CLEAR_TYPE_TO_AUDIT.put("energyMonthly", new String[]{"H204"});
        CLEAR_TYPE_TO_AUDIT.put("energyAuth", new String[]{"H204"});
        CLEAR_TYPE_TO_AUDIT.put("investment", new String[]{"INVEST"});
        CLEAR_TYPE_TO_AUDIT.put("investmentLogistics", new String[]{"INVEST"});
        CLEAR_TYPE_TO_AUDIT.put("investCityPassenger", new String[]{"INVEST"});
        CLEAR_TYPE_TO_AUDIT.put("investCityLogistics", new String[]{"INVEST"});
        CLEAR_TYPE_TO_AUDIT.put("investmentSystem", new String[]{"INVEST"});
        CLEAR_TYPE_TO_AUDIT.put("cityBus", new String[]{"CITY_BUS"});
        CLEAR_TYPE_TO_AUDIT.put("cityTaxi", new String[]{"CITY_TAXI"});
        CLEAR_TYPE_TO_AUDIT.put("cityTaxiAuth", new String[]{"CITY_TAXI"});
        String[] investPassengerBatch = {"INVEST_客运站场", "INVEST_客运站场_CITY"};
        String[] investLogisticsBatch = {"INVEST_物流园区", "INVEST_物流园区_CITY"};
        CLEAR_TYPE_TO_BATCH.put("h2032", new String[]{"H2032"});
        CLEAR_TYPE_TO_BATCH.put("transportAuth", new String[]{"TRANSPORT_AUTH"});
        CLEAR_TYPE_TO_BATCH.put("trackMileage", new String[]{"TRACK_MILEAGE"});
        CLEAR_TYPE_TO_BATCH.put("scaleSplit", new String[]{"SCALE_SPLIT"});
        CLEAR_TYPE_TO_BATCH.put("freightTurnover", new String[]{"FREIGHT_TURNOVER"});
        CLEAR_TYPE_TO_BATCH.put("passengerMonthly", new String[]{"H2031"});
        CLEAR_TYPE_TO_BATCH.put("passengerAuth", new String[]{"PASSENGER_AUTH"});
        CLEAR_TYPE_TO_BATCH.put("passengerIndividual", new String[]{"PASSENGER_INDIVIDUAL"});
        CLEAR_TYPE_TO_BATCH.put("energyMonthly", new String[]{"H204"});
        CLEAR_TYPE_TO_BATCH.put("energyAuth", new String[]{"ENERGY_AUTH"});
        CLEAR_TYPE_TO_BATCH.put("investment", investPassengerBatch);
        CLEAR_TYPE_TO_BATCH.put("investCityPassenger", investPassengerBatch);
        CLEAR_TYPE_TO_BATCH.put("investmentLogistics", investLogisticsBatch);
        CLEAR_TYPE_TO_BATCH.put("investCityLogistics", investLogisticsBatch);
        CLEAR_TYPE_TO_BATCH.put("investmentSystem", new String[]{"INVEST_SYSTEM"});
        CLEAR_TYPE_TO_BATCH.put("cityBus", new String[]{"CITY_BUS"});
        CLEAR_TYPE_TO_BATCH.put("cityTaxi", new String[]{"CITY_TAXI"});
        CLEAR_TYPE_TO_BATCH.put("cityTaxiAuth", new String[]{"CITY_TAXI_AUTH"});
        CLEAR_TYPE_TO_BATCH.put("wycTotal", new String[]{"WYC_TOTAL"});
        CLEAR_TYPE_TO_BATCH.put("wycOrder", new String[]{"WYC_ORDER", "WYC_ORDER_REPORT"});
        CLEAR_TYPE_TO_BATCH.put("wycOrderReport", new String[]{"WYC_ORDER_REPORT", "WYC_ORDER"});
    }
    /**
     * 清空指定报表期已导入数据与审核结果,用于整月重导;投资项目主档保留。
     *
     * @param types 需清空的数据类型(前端类型键,如 h2032/cityBus);为空则清空该报表期全部类型
     */
    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> clearPeriod(String period, List<String> types) {
        if (period == null || !period.matches("\\d{4}-\\d{2}")) {
            throw new IllegalArgumentException("报表期格式不正确,应为 yyyy-MM");
        }
        Set<String> selected = new LinkedHashSet<>();
        if (types != null) {
            for (String t : types) {
                if (t != null && !t.trim().isEmpty()) selected.add(t.trim());
            }
        }
        boolean clearAll = selected.isEmpty();
        int dataRows = 0;
        int auditRows = 0;
        int auditRunRows = 0;
        int batchRows = 0;
        if (clearAll) {
        dataRows += h2032Mapper.delete(new LambdaQueryWrapper<H2032EnterpriseMonthly>()
            .eq(H2032EnterpriseMonthly::getReportPeriod, period));
        dataRows += transportAuthMapper.delete(new LambdaQueryWrapper<TransportAuthVehicle>()
            .eq(TransportAuthVehicle::getReportPeriod, period));
        dataRows += trackMileageMapper.delete(new LambdaQueryWrapper<VehicleTrackMileage>()
            .eq(VehicleTrackMileage::getReportPeriod, period));
        dataRows += scaleSplitMapper.delete(new LambdaQueryWrapper<ScaleSplitTransport>()
            .eq(ScaleSplitTransport::getReportPeriod, period));
        dataRows += freightTurnoverMapper.delete(new LambdaQueryWrapper<FreightTurnoverImport>()
            .eq(FreightTurnoverImport::getReportPeriod, period));
        dataRows += passengerMapper.delete(new LambdaQueryWrapper<PassengerEnterpriseMonthly>()
            .eq(PassengerEnterpriseMonthly::getReportPeriod, period));
        dataRows += passengerIndividualMapper.delete(new LambdaQueryWrapper<PassengerIndividualMonthly>()
            .eq(PassengerIndividualMonthly::getReportPeriod, period));
        dataRows += passengerAuthMapper.delete(new LambdaQueryWrapper<PassengerAuthVehicle>()
            .eq(PassengerAuthVehicle::getReportPeriod, period));
        dataRows += energyMapper.delete(new LambdaQueryWrapper<EnergyVehicleQuarterly>()
            .eq(EnergyVehicleQuarterly::getReportPeriod, period));
        dataRows += energyAuthMapper.delete(new LambdaQueryWrapper<EnergyAuthVehicle>()
            .eq(EnergyAuthVehicle::getReportPeriod, period));
        dataRows += investMonthlyMapper.delete(new LambdaQueryWrapper<InvestmentMonthly>()
            .eq(InvestmentMonthly::getReportPeriod, period));
        dataRows += investSystemMapper.delete(new LambdaQueryWrapper<InvestmentSystem>()
            .eq(InvestmentSystem::getReportPeriod, period));
        dataRows += cityBusMapper.delete(new LambdaQueryWrapper<CityBusMonthly>()
            .eq(CityBusMonthly::getReportPeriod, period));
        dataRows += cityTaxiMapper.delete(new LambdaQueryWrapper<CityTaxiMonthly>()
            .eq(CityTaxiMonthly::getReportPeriod, period));
        dataRows += cityTaxiAuthMapper.delete(new LambdaQueryWrapper<CityTaxiAuth>()
            .eq(CityTaxiAuth::getReportPeriod, period));
        dataRows += wycTotalMapper.delete(new LambdaQueryWrapper<WycTotalMonthly>()
            .eq(WycTotalMonthly::getReportPeriod, period));
        dataRows += wycOrderMapper.delete(new LambdaQueryWrapper<WycOrderMonthly>()
            .eq(WycOrderMonthly::getReportPeriod, period));
        auditRows = auditResultMapper.delete(new LambdaQueryWrapper<AuditResult>()
            .eq(AuditResult::getReportPeriod, period));
        auditRunRows = auditRunMapper.delete(new LambdaQueryWrapper<AuditRun>()
            .eq(AuditRun::getReportPeriod, period));
        batchRows = importBatchMapper.delete(new LambdaQueryWrapper<ImportBatch>()
            .eq(ImportBatch::getReportPeriod, period));
        } else {
            Set<String> ops = new LinkedHashSet<>();
            Set<String> auditTypes = new LinkedHashSet<>();
            Set<String> batchTypes = new LinkedHashSet<>();
            for (String t : selected) {
                String op = CLEAR_TYPE_TO_OP.get(t);
                if (op == null) {
                    log.warn("清空数据:未知数据类型 {},已忽略", t);
                    continue;
                }
                ops.add(op);
                String[] at = CLEAR_TYPE_TO_AUDIT.get(t);
                if (at != null) java.util.Collections.addAll(auditTypes, at);
                String[] bt = CLEAR_TYPE_TO_BATCH.get(t);
                if (bt != null) java.util.Collections.addAll(batchTypes, bt);
            }
            if (ops.isEmpty()) {
                throw new IllegalArgumentException("未指定有效的数据类型");
            }
            for (String op : ops) {
                dataRows += clearTypeData(op, period);
            }
            if (!auditTypes.isEmpty()) {
                List<Long> ruleIds = new ArrayList<>();
                for (com.trafficaudit.rulemanage.entity.AuditRule rule : ruleMapper.selectList(null)) {
                    if (rule.getReportType() != null && auditTypes.contains(rule.getReportType())) {
                        ruleIds.add(rule.getId());
                    }
                }
                if (!ruleIds.isEmpty()) {
                    auditRows = auditResultMapper.delete(new LambdaQueryWrapper<AuditResult>()
                        .eq(AuditResult::getReportPeriod, period)
                        .in(AuditResult::getRuleId, ruleIds));
                }
                for (String rt : auditTypes) {
                    auditRunRows += auditRunMapper.delete(new LambdaQueryWrapper<AuditRun>()
                        .eq(AuditRun::getReportPeriod, period)
                        .eq(AuditRun::getReportType, rt));
                }
            }
            if (!batchTypes.isEmpty()) {
                batchRows = importBatchMapper.delete(new LambdaQueryWrapper<ImportBatch>()
                    .eq(ImportBatch::getReportPeriod, period)
                    .in(ImportBatch::getImportType, batchTypes));
            }
        }
        Map<String, Object> stats = new LinkedHashMap<>();
        stats.put("period", period);
        stats.put("scope", clearAll ? "all" : "types");
        if (!clearAll) stats.put("types", new ArrayList<>(selected));
        stats.put("dataRows", dataRows);
        stats.put("auditRows", auditRows);
        stats.put("auditRunRows", auditRunRows);
        stats.put("batchRows", batchRows);
        log.info("清空报表期 {}({})完成:业务数据 {} 行、审核结果 {} 行、审核标记 {} 行、导入批次 {} 行",
            period, clearAll ? "全部类型" : String.join(",", selected), dataRows, auditRows, auditRunRows, batchRows);
        return stats;
    }
    /** 按“数据表操作键”清空对应业务表(投资月度按项目类别过滤),返回删除行数 */
    private int clearTypeData(String op, String period) {
        switch (op) {
            case "H2032":
                return h2032Mapper.delete(new LambdaQueryWrapper<H2032EnterpriseMonthly>()
                    .eq(H2032EnterpriseMonthly::getReportPeriod, period));
            case "TRANSPORT_AUTH":
                return transportAuthMapper.delete(new LambdaQueryWrapper<TransportAuthVehicle>()
                    .eq(TransportAuthVehicle::getReportPeriod, period));
            case "TRACK_MILEAGE":
                return trackMileageMapper.delete(new LambdaQueryWrapper<VehicleTrackMileage>()
                    .eq(VehicleTrackMileage::getReportPeriod, period));
            case "SCALE_SPLIT":
                return scaleSplitMapper.delete(new LambdaQueryWrapper<ScaleSplitTransport>()
                    .eq(ScaleSplitTransport::getReportPeriod, period));
            case "FREIGHT_TURNOVER":
                return freightTurnoverMapper.delete(new LambdaQueryWrapper<FreightTurnoverImport>()
                    .eq(FreightTurnoverImport::getReportPeriod, period));
            case "H2031":
                return passengerMapper.delete(new LambdaQueryWrapper<PassengerEnterpriseMonthly>()
                    .eq(PassengerEnterpriseMonthly::getReportPeriod, period));
            case "PASSENGER_AUTH":
                return passengerAuthMapper.delete(new LambdaQueryWrapper<PassengerAuthVehicle>()
                    .eq(PassengerAuthVehicle::getReportPeriod, period));
            case "PASSENGER_INDIVIDUAL":
                return passengerIndividualMapper.delete(new LambdaQueryWrapper<PassengerIndividualMonthly>()
                    .eq(PassengerIndividualMonthly::getReportPeriod, period));
            case "H204":
                return energyMapper.delete(new LambdaQueryWrapper<EnergyVehicleQuarterly>()
                    .eq(EnergyVehicleQuarterly::getReportPeriod, period));
            case "ENERGY_AUTH":
                return energyAuthMapper.delete(new LambdaQueryWrapper<EnergyAuthVehicle>()
                    .eq(EnergyAuthVehicle::getReportPeriod, period));
            case "INVEST_MONTHLY_PASSENGER":
                return deleteInvestmentMonthly(period, "客运站场");
            case "INVEST_MONTHLY_LOGISTICS":
                return deleteInvestmentMonthly(period, "物流园区");
            case "INVEST_SYSTEM":
                return investSystemMapper.delete(new LambdaQueryWrapper<InvestmentSystem>()
                    .eq(InvestmentSystem::getReportPeriod, period));
            case "CITY_BUS":
                return cityBusMapper.delete(new LambdaQueryWrapper<CityBusMonthly>()
                    .eq(CityBusMonthly::getReportPeriod, period));
            case "CITY_TAXI":
                return cityTaxiMapper.delete(new LambdaQueryWrapper<CityTaxiMonthly>()
                    .eq(CityTaxiMonthly::getReportPeriod, period));
            case "CITY_TAXI_AUTH":
                return cityTaxiAuthMapper.delete(new LambdaQueryWrapper<CityTaxiAuth>()
                    .eq(CityTaxiAuth::getReportPeriod, period));
            case "WYC_TOTAL":
                return wycTotalMapper.delete(new LambdaQueryWrapper<WycTotalMonthly>()
                    .eq(WycTotalMonthly::getReportPeriod, period));
            case "WYC_ORDER":
                return wycOrderMapper.delete(new LambdaQueryWrapper<WycOrderMonthly>()
                    .eq(WycOrderMonthly::getReportPeriod, period));
            default:
                log.warn("清空数据:未知数据表操作 {}", op);
                return 0;
        }
    }
    /** 投资月度按项目类别清除(投资项目主档保留) */
    private int deleteInvestmentMonthly(String period, String category) {
        List<Long> projectIds = new ArrayList<>();
        for (InvestmentProject p : investProjectMapper.selectList(new LambdaQueryWrapper<InvestmentProject>()
                .eq(InvestmentProject::getCategory, category))) {
            projectIds.add(p.getId());
        }
        if (projectIds.isEmpty()) return 0;
        return investMonthlyMapper.delete(new LambdaQueryWrapper<InvestmentMonthly>()
            .eq(InvestmentMonthly::getReportPeriod, period)
            .in(InvestmentMonthly::getProjectId, projectIds));
    }
    /** 数据变化后删除指定报表类型指定报表期的审核结果(需重新执行审核) */
    private void deleteRuleResults(String reportType, String period) {
        List<Long> ruleIds = new ArrayList<>();
@@ -1394,6 +1678,33 @@
            }
        }
        return colMap;
    }
    /** 兼容表头映射:去引号/去空白;纯英文表头统一大写(如 'OWNERNAME' → OWNERNAME),便于新旧模板混用 */
    private Map<String, Integer> buildHeaderMapCompat(Row header) {
        Map<String, Integer> colMap = new HashMap<>();
        if (header == null) return colMap;
        for (Cell cell : header) {
            if (cell == null) continue;
            String h = FORMATTER.formatCellValue(cell).replace("'", "").replace("\"", "").trim();
            if (h.isEmpty()) continue;
            boolean ascii = true;
            for (int i = 0; i < h.length(); i++) {
                if (h.charAt(i) > 127) { ascii = false; break; }
            }
            if (ascii) h = h.toUpperCase();
            if (!colMap.containsKey(h)) colMap.put(h, cell.getColumnIndex());
        }
        return colMap;
    }
    /** 按别名顺序取第一个存在的列号,全部缺失返回 null */
    private Integer firstColIdx(Map<String, Integer> colMap, String... aliases) {
        for (String a : aliases) {
            Integer idx = colMap.get(a);
            if (idx != null) return idx;
        }
        return null;
    }
    private int colIdx(Map<String, Integer> colMap, String header) {
@@ -2839,6 +3150,209 @@
        return ok;
    }
    // ========== 网约车订单运行监测分析报告(docx)导入 ==========
    /** 网约车订单报告导入结果(报表期/订单合计/覆盖市州数/缺失市州) */
    public static class WycOrderReportResult {
        private final String period;
        private final double orderSum;
        private final int cityCount;
        private final List<String> missingCities;
        public WycOrderReportResult(String period, double orderSum, int cityCount, List<String> missingCities) {
            this.period = period;
            this.orderSum = orderSum;
            this.cityCount = cityCount;
            this.missingCities = missingCities;
        }
        public String getPeriod() { return period; }
        public double getOrderSum() { return orderSum; }
        public int getCityCount() { return cityCount; }
        public List<String> getMissingCities() { return missingCities; }
    }
    /**
     * 《网约车行业YYYY年M月运行监测情况分析报告》(docx)导入:
     * 解析报告附件中「地市名称 | … | 订单数总数」表的 17 市州订单数,写入 wyc_order_monthly 的
     * F 订单合计与 G-W 17 市州订单列;全省 pin(B-E)沿用该期已有行,若无该期行则从
     * wyc_total_monthly 同报表期复制。报表期优先取报告标题年月,取不到时用页面所选报表期。
     */
    public synchronized WycOrderReportResult importWycOrderReport(MultipartFile file, String period) throws Exception {
        Map<String, Double> orders = new LinkedHashMap<>();
        double[] sumHolder = new double[]{0.0};
        String docPeriod;
        try (java.io.InputStream in = file.getInputStream();
             XWPFDocument doc = new XWPFDocument(in)) {
            docPeriod = periodOfDocx(doc);
            parseWycOrderReportTables(doc, orders, sumHolder);
        } catch (Exception e) {
            throw new RuntimeException("无法读取 Word 文档,请上传 .docx 格式的《网约车行业YYYY年M月运行监测情况分析报告》;"
                    + "若上传的其实是 Excel/PDF,请改用对应模板导入。(原始错误:" + e.getMessage() + ")");
        }
        if (orders.isEmpty()) {
            throw new RuntimeException("未解析到任何市州订单数,请确认上传的是《网约车行业YYYY年M月运行监测情况分析报告》");
        }
        String rp = null;
        if (docPeriod != null && docPeriod.matches("\\d{4}-\\d{2}")) rp = docPeriod;
        else if (period != null && period.matches("\\d{4}-\\d{2}")) rp = period;
        if (rp == null) {
            throw new RuntimeException("无法确定报表期:报告标题未含「YYYY年M月」,且页面未选择报表期");
        }
        List<String> missing = new ArrayList<>();
        double citySum = 0.0;
        for (String city : RegionUtil.cityList()) {
            Double v = orders.get(city);
            if (v == null) missing.add(city);
            else citySum += v;
        }
        double sum = sumHolder[0] > 0 ? sumHolder[0] : citySum;
        WycOrderMonthly it = wycOrderMapper.selectOne(new LambdaQueryWrapper<WycOrderMonthly>()
                .eq(WycOrderMonthly::getReportPeriod, rp).last("LIMIT 1"));
        if (it == null) it = new WycOrderMonthly();
        it.setReportPeriod(rp);
        it.setOrderSum(sum > 0 ? sum : null);
        it.setOrderWuhan(orderOf(orders, "武汉市"));
        it.setOrderHuangshi(orderOf(orders, "黄石市"));
        it.setOrderShiyan(orderOf(orders, "十堰市"));
        it.setOrderYichang(orderOf(orders, "宜昌市"));
        it.setOrderXiangyang(orderOf(orders, "襄阳市"));
        it.setOrderEzhou(orderOf(orders, "鄂州市"));
        it.setOrderJingmen(orderOf(orders, "荆门市"));
        it.setOrderXiaogan(orderOf(orders, "孝感市"));
        it.setOrderJingzhou(orderOf(orders, "荆州市"));
        it.setOrderHuanggang(orderOf(orders, "黄冈市"));
        it.setOrderXianning(orderOf(orders, "咸宁市"));
        it.setOrderSuizhou(orderOf(orders, "随州市"));
        it.setOrderEnshi(orderOf(orders, "恩施州"));
        it.setOrderXiantao(orderOf(orders, "仙桃市"));
        it.setOrderQianjiang(orderOf(orders, "潜江市"));
        it.setOrderTianmen(orderOf(orders, "天门市"));
        it.setOrderShennong(orderOf(orders, "神农架林区"));
        if (it.getPinK() == null || it.getPinCK() == null || it.getPinZ() == null || it.getPinCZ() == null) {
            WycTotalMonthly tot = wycTotalMapper.selectOne(new LambdaQueryWrapper<WycTotalMonthly>()
                    .eq(WycTotalMonthly::getReportPeriod, rp).last("LIMIT 1"));
            if (tot != null) {
                if (it.getPinK() == null) it.setPinK(tot.getPinK());
                if (it.getPinCK() == null) it.setPinCK(tot.getPinCK());
                if (it.getPinZ() == null) it.setPinZ(tot.getPinZ());
                if (it.getPinCZ() == null) it.setPinCZ(tot.getPinCZ());
            }
        }
        wycOrderMapper.delete(new LambdaQueryWrapper<WycOrderMonthly>().eq(WycOrderMonthly::getReportPeriod, rp));
        wycOrderMapper.insert(it);
        recordBatch(file.getOriginalFilename(), "WYC_ORDER_REPORT", rp, 1, 1, 0, null);
        log.info("WycOrderReport imported: period={} sum={} cities={} missing={}", rp, sum, orders.size(), missing);
        return new WycOrderReportResult(rp, sum, orders.size(), missing);
    }
    /** 报告标题中的报表期:网约车行业2026年7月 -> 2026-07;取不到返回 null */
    private String periodOfDocx(XWPFDocument doc) {
        java.util.regex.Pattern titlePat = java.util.regex.Pattern.compile("网约车行业\\s*(\\d{4})\\s*年\\s*(\\d{1,2})\\s*月");
        java.util.regex.Pattern anyPat = java.util.regex.Pattern.compile("(\\d{4})\\s*年\\s*(\\d{1,2})\\s*月");
        String fallback = null;
        int scanned = 0;
        for (XWPFParagraph para : doc.getParagraphs()) {
            String text = para.getText();
            if (text == null) continue;
            String flat = text.replaceAll("\\s+", "");
            if (flat.isEmpty()) continue;
            java.util.regex.Matcher m = titlePat.matcher(flat);
            if (m.find()) return ymd(m.group(1), m.group(2));
            if (fallback == null && scanned < 10) {
                java.util.regex.Matcher m2 = anyPat.matcher(flat);
                if (m2.find()) fallback = ymd(m2.group(1), m2.group(2));
            }
            scanned++;
        }
        return fallback;
    }
    private String ymd(String y, String mo) {
        try {
            int month = Integer.parseInt(mo);
            if (month < 1 || month > 12) return null;
            return String.format("%04d-%02d", Integer.parseInt(y), month);
        } catch (NumberFormatException e) {
            return null;
        }
    }
    /** 解析报告中的「地市名称 | … | 订单数总数」表,orders=市州订单,sumHolder[0]=合计行订单数 */
    private void parseWycOrderReportTables(XWPFDocument doc, Map<String, Double> orders, double[] sumHolder) {
        for (XWPFTable table : doc.getTables()) {
            List<XWPFTableRow> rows = table.getRows();
            if (rows.size() < 3) continue;
            int headerRow = -1;
            int orderCol = -1;
            for (int r = 0; r < Math.min(rows.size(), 3); r++) {
                List<org.apache.poi.xwpf.usermodel.XWPFTableCell> cells = rows.get(r).getTableCells();
                for (int c = 0; c < cells.size(); c++) {
                    String norm = flat(cells.get(c).getText());
                    if (norm.contains("订单数总数") || "订单数".equals(norm)) {
                        headerRow = r;
                        orderCol = c;
                        break;
                    }
                }
                if (headerRow >= 0) break;
            }
            if (headerRow < 0 || orderCol < 0) continue;
            Map<String, Double> found = new LinkedHashMap<>();
            double citySum = 0.0;
            boolean hasTotal = false;
            for (int r = headerRow + 1; r < rows.size(); r++) {
                List<org.apache.poi.xwpf.usermodel.XWPFTableCell> cells = rows.get(r).getTableCells();
                if (orderCol >= cells.size()) continue;
                String name = flat(cells.get(0).getText());
                if (name.isEmpty()) continue;
                Double val = parsePlainNumber(cells.get(orderCol).getText());
                if (val == null) continue;
                String city = RegionUtil.normalizeCityName(name);
                if (city != null && RegionUtil.cityList().contains(city)) {
                    found.put(city, val);
                    citySum += val;
                } else if (name.contains("合计") || name.contains("总计") || name.contains("全省") || name.contains("湖北")) {
                    hasTotal = true;
                    sumHolder[0] = val;
                }
            }
            if (!found.isEmpty()) {
                orders.clear();
                orders.putAll(found);
                if (!hasTotal) sumHolder[0] = citySum;
                return;
            }
        }
    }
    private String flat(String s) {
        return s == null ? "" : s.replaceAll("\\s+", "").trim();
    }
    /** 「11,183,267 单」这类文本取数值 */
    private Double parsePlainNumber(String s) {
        if (s == null) return null;
        String t = s.replaceAll("[,\\s]", "").trim();
        if (t.isEmpty()) return null;
        java.util.regex.Matcher m = java.util.regex.Pattern.compile("-?\\d+(?:\\.\\d+)?").matcher(t);
        if (!m.find()) return null;
        try {
            return Double.parseDouble(m.group());
        } catch (NumberFormatException e) {
            return null;
        }
    }
    private Double orderOf(Map<String, Double> orders, String city) {
        Double v = orders.get(city);
        return (v == null || v <= 0) ? null : v;
    }
    private String periodOfCell(Cell c) {
        if (c == null) return null;
        if (c.getCellType() == CellType.NUMERIC) {
@@ -3208,6 +3722,7 @@
            case "scaleSplit": return new ImportResult(importScaleSplit(file, period), 0, new ArrayList<>());
            case "wycTotal": return new ImportResult(importWycTotal(file, period), 0, new ArrayList<>());
            case "wycOrder": return new ImportResult(importWycOrder(file, period), 0, new ArrayList<>());
            case "wycOrderReport": return new ImportResult(importWycOrderReport(file, period).getCityCount(), 0, new ArrayList<>());
            default: throw new RuntimeException("不支持的导入类型:" + type);
        }
    }