| | |
| | | .eq(InvestmentSystem::getReportPeriod, reportPeriod))) { |
| | | sysMap.computeIfAbsent(normInvestName(s.getProjectName()), k -> new ArrayList<>()).add(s); |
| | | } |
| | | // 同报表期投资系统基准整期缺失时,不做"项目缺失核实"(缺失=基准未导入,不等于项目不存在),避免整表误报 |
| | | boolean benchmarkMissing = sysMap.isEmpty(); |
| | | |
| | | // 幂等:只清除 INVEST 规则的旧审核结果 |
| | | List<Long> ruleIds = new ArrayList<>(); |
| | |
| | | String projectName = project == null ? "未知项目" : project.getProjectName(); |
| | | InvestmentSystem sys = findInvestSystem(sysMap, projectName); |
| | | for (AuditRule rule : rules) { |
| | | if (benchmarkMissing && "EXISTENCE_SYS".equals(rule.getCompareType())) continue; |
| | | AuditResult result = checkInvestRule(rule, monthly, projectName, sys); |
| | | if (result != null) { |
| | | result.setRuleName(rule.getRuleName()); |
| | |
| | | return a; |
| | | } |
| | | |
| | | /** 审核结果导出 Excel(含企业名/规则名/状态/审核意见) */ |
| | | /** 审核结果导出 Excel(两 Sheet,按业务模板):Sheet1 审核结果(逐条明细);Sheet2 企业合并(按企业汇总 + 问题列) */ |
| | | public byte[] exportResults(String reportPeriod, String reportType) throws Exception { |
| | | return exportResults(reportPeriod, reportType, true, "all"); |
| | | } |
| | | |
| | | /** onlyVerify=true 时剔除“提醒型”规则(如 RULE_004 环比提醒),只保留核实型问题;status=all/pending/handled 与界面“状态”筛选一致 */ |
| | | public byte[] exportResults(String reportPeriod, String reportType, boolean onlyVerify, String status) throws Exception { |
| | | List<AuditResult> results = queryResults(reportPeriod, reportType); |
| | | java.util.List<AuditResult> filtered = new java.util.ArrayList<>(); |
| | | String st = status == null ? "all" : status.trim(); |
| | | for (AuditResult res : results) { |
| | | if (onlyVerify && isReminderRule(res.getRuleCode())) continue; |
| | | if ("pending".equals(st) && !"PENDING".equals(res.getStatus())) continue; |
| | | if ("handled".equals(st) && (res.getStatus() == null || "PENDING".equals(res.getStatus()))) continue; |
| | | filtered.add(res); |
| | | } |
| | | try (XSSFWorkbook wb = new XSSFWorkbook()) { |
| | | // ---------- Sheet1 审核结果(逐条,列与业务模板一致) ---------- |
| | | Sheet sheet = wb.createSheet("审核结果"); |
| | | String[] headers = {"报表期", "企业/市州", "规则编码", "审核规则", "等级", "实际值", "阈值/基准", "偏差说明", "状态", "审核意见"}; |
| | | Row head = sheet.createRow(0); |
| | | for (int i = 0; i < headers.length; i++) { |
| | | Cell cell = head.createCell(i); |
| | | cell.setCellValue(headers[i]); |
| | | org.apache.poi.ss.usermodel.CellStyle style = wb.createCellStyle(); |
| | | org.apache.poi.ss.usermodel.Font font = wb.createFont(); |
| | | font.setBold(true); |
| | | style.setFont(font); |
| | | cell.setCellStyle(style); |
| | | } |
| | | writeAuditHeader(sheet, headers); |
| | | int r = 1; |
| | | for (AuditResult res : results) { |
| | | for (AuditResult res : filtered) { |
| | | Row row = sheet.createRow(r++); |
| | | row.createCell(0).setCellValue(valueOf(res.getReportPeriod())); |
| | | row.createCell(1).setCellValue(valueOf(res.getEnterpriseName())); |
| | |
| | | for (int i = 0; i < widths.length; i++) { |
| | | sheet.setColumnWidth(i, widths[i] * 256); |
| | | } |
| | | |
| | | // ---------- Sheet2 企业合并(同一企业多条规则并排列出 + 问题汇总列) ---------- |
| | | Sheet ms = wb.createSheet("企业合并"); |
| | | String[] h2 = {"报表期", "企业/市州", "审核规则", "实际值", "阈值/基准", "偏差说明", "问题"}; |
| | | writeAuditHeader(ms, h2); |
| | | java.util.LinkedHashMap<String, java.util.List<AuditResult>> groups = new java.util.LinkedHashMap<>(); |
| | | for (AuditResult res : filtered) { |
| | | String key = valueOf(res.getEnterpriseName()); |
| | | if (key.isEmpty()) key = valueOf(res.getReportPeriod()) + "#" + valueOf(res.getActualValue()); |
| | | groups.computeIfAbsent(key, k -> new java.util.ArrayList<>()).add(res); |
| | | } |
| | | int rowCursor = 1; |
| | | for (java.util.Map.Entry<String, java.util.List<AuditResult>> e : groups.entrySet()) { |
| | | java.util.List<AuditResult> items = e.getValue(); |
| | | int groupStart = rowCursor; |
| | | StringBuilder problems = new StringBuilder(); |
| | | int seq = 0; |
| | | for (AuditResult it : items) { |
| | | Row row = ms.createRow(rowCursor++); |
| | | if (seq == 0) { |
| | | row.createCell(0).setCellValue(valueOf(it.getReportPeriod())); |
| | | row.createCell(1).setCellValue(valueOf(it.getEnterpriseName()).isEmpty() ? e.getKey() : valueOf(it.getEnterpriseName())); |
| | | } |
| | | row.createCell(2).setCellValue(valueOf(it.getRuleName())); |
| | | row.createCell(3).setCellValue(valueOf(it.getActualValue())); |
| | | row.createCell(4).setCellValue(valueOf(it.getThresholdValue())); |
| | | row.createCell(5).setCellValue(valueOf(it.getDeviation())); |
| | | seq++; |
| | | if (problems.length() > 0) problems.append("\n"); |
| | | problems.append(seq).append("、").append(issueSentence(it)); |
| | | } |
| | | if (rowCursor - groupStart > 1) { |
| | | ms.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(groupStart, rowCursor - 1, 1, 1)); |
| | | ms.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(groupStart, rowCursor - 1, 6, 6)); |
| | | } |
| | | Row first = ms.getRow(groupStart); |
| | | if (first.getCell(6) == null) first.createCell(6); |
| | | first.getCell(6).setCellValue(problems.toString()); |
| | | } |
| | | int[] widths2 = {12, 30, 22, 34, 22, 34, 60}; |
| | | for (int i = 0; i < widths2.length; i++) { |
| | | ms.setColumnWidth(i, widths2[i] * 256); |
| | | } |
| | | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| | | wb.write(out); |
| | | return out.toByteArray(); |
| | | } |
| | | } |
| | | |
| | | private void writeAuditHeader(Sheet sheet, String[] headers) { |
| | | Row head = sheet.createRow(0); |
| | | for (int i = 0; i < headers.length; i++) { |
| | | Cell cell = head.createCell(i); |
| | | cell.setCellValue(headers[i]); |
| | | org.apache.poi.ss.usermodel.CellStyle style = sheet.getWorkbook().createCellStyle(); |
| | | org.apache.poi.ss.usermodel.Font font = sheet.getWorkbook().createFont(); |
| | | font.setBold(true); |
| | | style.setFont(font); |
| | | cell.setCellStyle(style); |
| | | } |
| | | } |
| | | |
| | | /** 提醒型规则:仅提示变化、不需人工核实(如 RULE_004 货运类型环比提醒) */ |
| | | private boolean isReminderRule(String ruleCode) { |
| | | return "RULE_004".equals(ruleCode); |
| | | } |
| | | |
| | | /** 企业合并“问题”列话术:按规则类型生成可读的核实意见 */ |
| | | private String issueSentence(AuditResult res) { |
| | | String code = res.getRuleCode(); |
| | | String dev = valueOf(res.getDeviation()); |
| | | String act = valueOf(res.getActualValue()); |
| | | String thr = valueOf(res.getThresholdValue()); |
| | | if ("RULE_001".equals(code) || "RULE_002".equals(code)) { |
| | | return "企业上报车辆/吨位与运政数据对比存在差异(" + dev + "),请核实"; |
| | | } |
| | | if ("RULE_003".equals(code)) { |
| | | return "上报周转量与轨迹测算周转量差异明显(" + dev + "),请参考轨迹核实企业周转量"; |
| | | } |
| | | if ("RULE_004".equals(code)) { |
| | | return "货运类型环比发生变化,请核实"; |
| | | } |
| | | if ("RULE_005".equals(code)) { |
| | | return "无集装箱车但存在集装箱货运量,请核实车辆类型与货运量填报"; |
| | | } |
| | | if ("RULE_006".equals(code)) { |
| | | return "整车吨位超出 4~40 吨合理范围,请核实平均吨位填报"; |
| | | } |
| | | return valueOf(res.getRuleName()) + "异常(实际:" + act + ";基准:" + thr + ";" + dev + "),请核实"; |
| | | } |
| | | |
| | | private String auditStatusCn(String status) { |
| | |
| | | case "RULE_I001": |
| | | case "RULE_I002": |
| | | case "RULE_I003": |
| | | case "RULE_I004": |
| | | return investSystemMapper.selectCount(new LambdaQueryWrapper<InvestmentSystem>() |
| | | .eq(InvestmentSystem::getReportPeriod, period)) == 0; |
| | | default: |
| | |
| | | switch (rule.getCompareType()) { |
| | | case "DIFF": |
| | | return checkDiff(rule, report, authMap); |
| | | case "DIFF_FIELDS": |
| | | return checkDiffFields(rule, report, authMap); |
| | | case "RATIO": |
| | | return checkRatio(rule, report, authMap, trackMap); |
| | | case "MOM": |
| | |
| | | return null; |
| | | } |
| | | |
| | | /** RULE_001 分项运政对比:牵引车/挂车/整车车辆数与挂车吨位/整车吨位/总吨位分别与运政对比,任一分项超限即提醒 */ |
| | | private AuditResult checkDiffFields(AuditRule rule, H2032EnterpriseMonthly report, |
| | | Map<String, TransportAuthVehicle> authMap) { |
| | | TransportAuthVehicle auth = findAuth(authMap, report.getEnterpriseName()); |
| | | if (auth == null) return null; |
| | | String[] fields = rule.getCheckField() == null ? new String[0] : rule.getCheckField().split(","); |
| | | String[] limits = rule.getThreshold() == null ? new String[0] : rule.getThreshold().split("[,;|]"); |
| | | if (fields.length == 0 || limits.length != fields.length) return null; |
| | | java.util.List<String> hits = new java.util.ArrayList<>(); |
| | | double maxAbs = 0; |
| | | for (int i = 0; i < fields.length; i++) { |
| | | String field = fields[i].trim(); |
| | | double limit; |
| | | try { |
| | | limit = Double.parseDouble(limits[i].trim()); |
| | | } catch (Exception e) { |
| | | return null; |
| | | } |
| | | double actual; |
| | | double reference; |
| | | String unit; |
| | | switch (field) { |
| | | case "vehicleTractor": |
| | | actual = num(report.getVehicleTractor()); |
| | | reference = num(auth.getTractorCount()); |
| | | unit = "台"; |
| | | break; |
| | | case "vehicleTrailer": |
| | | actual = num(report.getVehicleTrailer()); |
| | | reference = num(auth.getTrailerCount()); |
| | | unit = "台"; |
| | | break; |
| | | case "tonsTrailer": |
| | | actual = nvl(report.getTonsTrailer()); |
| | | reference = nvl(auth.getTrailerTons()); |
| | | unit = "吨"; |
| | | break; |
| | | case "vehicleWhole": |
| | | actual = num(report.getVehicleWhole()); |
| | | reference = num(auth.getOtherCount()); |
| | | unit = "台"; |
| | | break; |
| | | case "tonsWhole": |
| | | actual = nvl(report.getTonsWhole()); |
| | | reference = nvl(auth.getOtherTons()); |
| | | unit = "吨"; |
| | | break; |
| | | case "tonsTotal": |
| | | actual = nvl(report.getTonsTotal()); |
| | | reference = nvl(auth.getTrailerTons()) + nvl(auth.getOtherTons()); |
| | | unit = "吨"; |
| | | break; |
| | | default: |
| | | return null; |
| | | } |
| | | double diff = Math.abs(actual - reference); |
| | | maxAbs = Math.max(maxAbs, diff); |
| | | if (diff > limit) { |
| | | hits.add(h2032FieldLabel(field) + ":上报=" + fmt(actual) + " " + unit + ",运政=" + fmt(reference) + " " + unit |
| | | + ",差" + fmt(diff) + " " + unit + "(限" + fmt(limit) + " " + unit + ")"); |
| | | } |
| | | } |
| | | if (hits.isEmpty()) return null; |
| | | StringBuilder detail = new StringBuilder(); |
| | | for (int i = 0; i < hits.size(); i++) { |
| | | String one = hits.get(i); |
| | | if (detail.length() == 0) { |
| | | detail.append(one); |
| | | } else if (detail.length() + one.length() + 1 <= 180) { |
| | | detail.append(";").append(one); |
| | | } else { |
| | | detail.append(";等").append(hits.size() - i).append("项超限"); |
| | | break; |
| | | } |
| | | } |
| | | return buildResult(rule, report, detail.toString(), |
| | | "牵引车/挂车/整车车辆差≤5台;挂车吨位/整车吨位差≤50吨;总吨位差≤50吨", |
| | | "超出项数=" + hits.size() + ",最大差=" + fmt(maxAbs)); |
| | | } |
| | | |
| | | /** 上报周转量与轨迹测算周转量对比,高出阈值比例核实 */ |
| | | private AuditResult checkRatio(AuditRule rule, H2032EnterpriseMonthly report, |
| | | Map<String, TransportAuthVehicle> authMap, |
| | |
| | | return null; |
| | | } |
| | | |
| | | /** 货运类型环比:本月新增(上月为0、本月>0)需核实,少的不管 */ |
| | | /** 货运类型环比(RULE_004):上月无→本月新增 或 上月有→本月无,均提醒核实 */ |
| | | private AuditResult checkMom(AuditRule rule, H2032EnterpriseMonthly report, |
| | | Map<String, H2032EnterpriseMonthly> lastMonthMap) { |
| | | H2032EnterpriseMonthly lastMonth = lastMonthMap.get(key(report.getEnterpriseCode())); |
| | | if (lastMonth == null) return null; |
| | | List<String> hits = new ArrayList<>(); |
| | | List<String> addHits = new ArrayList<>(); |
| | | List<String> delHits = new ArrayList<>(); |
| | | for (String field : rule.getCheckField().split(",")) { |
| | | double current = getH2032FieldValue(report, field); |
| | | double last = getH2032FieldValue(lastMonth, field); |
| | | if (current > 0 && last == 0) { |
| | | hits.add(h2032FieldLabel(field) + "=" + fmt(current)); |
| | | addHits.add(h2032FieldLabel(field) + "=" + fmt(current)); |
| | | } else if (last > 0 && current == 0) { |
| | | delHits.add(h2032FieldLabel(field) + "(上月=" + fmt(last) + ",本月无)"); |
| | | } |
| | | } |
| | | if (!hits.isEmpty()) { |
| | | return buildResult(rule, report, "本月新增: " + String.join("、", hits), |
| | | "上月该类型为0", "本月新增货运类型(上月无)"); |
| | | if (!addHits.isEmpty() || !delHits.isEmpty()) { |
| | | StringBuilder msg = new StringBuilder(); |
| | | if (!addHits.isEmpty()) msg.append("本月新增: ").append(String.join("、", addHits)); |
| | | if (!delHits.isEmpty()) { |
| | | if (msg.length() > 0) msg.append(";"); |
| | | msg.append("本月消失: ").append(String.join("、", delHits)); |
| | | } |
| | | return buildResult(rule, report, msg.toString(), |
| | | "上月无→本月新增 或 上月有→本月无", "货运类型环比新增/消失需核实"); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** 有货运量但对应车辆为0 需核实(如 有集装箱货运量无集装箱车辆) */ |
| | | private AuditResult checkNull(AuditRule rule, H2032EnterpriseMonthly report) { |
| | | String[] fields = rule.getCheckField().split(","); |
| | |
| | | return null; |
| | | } |
| | | |
| | | /** 数值范围检查(如 整车吨位 4≤且≤40) */ |
| | | /** 数值范围检查;整车吨位按“平均吨位=整车吨位/整车数”口径判定 4~40 */ |
| | | private AuditResult checkOutRange(AuditRule rule, H2032EnterpriseMonthly report) { |
| | | String[] bounds = rule.getThreshold().split(","); |
| | | if (bounds.length < 2) return null; |
| | | double value = getH2032FieldValue(report, rule.getCheckField()); |
| | | String field = rule.getCheckField(); |
| | | String label = h2032FieldLabel(field); |
| | | double value; |
| | | if ("tonsWhole".equals(field)) { |
| | | double vehicles = report.getVehicleWhole() == null ? 0.0 : report.getVehicleWhole().doubleValue(); |
| | | double tons = nvl(report.getTonsWhole()); |
| | | if (vehicles > 0) { |
| | | value = tons / vehicles; |
| | | label = "整车平均吨位(整车吨位/整车数)"; |
| | | } else { |
| | | value = tons; // 无整车数但有吨位时仍按原吨位判定 |
| | | } |
| | | } else { |
| | | value = getH2032FieldValue(report, field); |
| | | } |
| | | double low = Double.parseDouble(bounds[0].trim()); |
| | | double high = Double.parseDouble(bounds[1].trim()); |
| | | if (value > 0 && (value < low || value > high)) { |
| | | return buildResult(rule, report, h2032FieldLabel(rule.getCheckField()) + "=" + fmt(value), |
| | | return buildResult(rule, report, label + "=" + fmt(value), |
| | | "范围 " + fmt(low) + "~" + fmt(high), "超出范围"); |
| | | } |
| | | return null; |
| | |
| | | labels.put("tonsWhole", "整车吨位"); |
| | | labels.put("turnoverTotal", "周转量"); |
| | | labels.put("vehicleWhole", "整车车辆数"); |
| | | labels.put("vehicleTractor", "牵引车数"); |
| | | labels.put("vehicleTrailer", "挂车数"); |
| | | labels.put("tonsTrailer", "挂车吨位"); |
| | | labels.put("tonsWhole", "整车吨位"); |
| | | String v = labels.get(field); |
| | | return v == null ? field : v; |
| | | } |
| | |
| | | } |
| | | |
| | | private String fmt(double v) { |
| | | if (v == Math.floor(v) && !Double.isInfinite(v)) { |
| | | if (v == Math.floor(v) && !Double.isInfinite(v) && Math.abs(v) < 1e15) { |
| | | return String.valueOf((long) v); |
| | | } |
| | | return String.valueOf(Math.round(v * 100.0) / 100.0); |
| | | return java.math.BigDecimal.valueOf(v).setScale(2, java.math.RoundingMode.HALF_UP) |
| | | .stripTrailingZeros().toPlainString(); |
| | | } |
| | | |
| | | /** 占比/小数值:保留 4 位小数 */ |
| | | private String fmt4(double v) { |
| | | if (v == Math.floor(v) && !Double.isInfinite(v)) { |
| | | if (v == Math.floor(v) && !Double.isInfinite(v) && Math.abs(v) < 1e15) { |
| | | return String.valueOf((long) v); |
| | | } |
| | | return String.valueOf(Math.round(v * 10000.0) / 10000.0); |
| | | return java.math.BigDecimal.valueOf(v).setScale(4, java.math.RoundingMode.HALF_UP) |
| | | .stripTrailingZeros().toPlainString(); |
| | | } |
| | | |
| | | // ========== H2031 旅客审核 ========== |
| | |
| | | result.setStatus("PENDING"); |
| | | return result; |
| | | } |
| | | } |
| | | } |