From 2e6830bcf59c01f29f040652b3e5209a7f21238e Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期一, 21 九月 2026 10:23:50 +0800
Subject: [PATCH] feat(投资): 投资系统导出新模板按表头解析 + 审核基准缺失保护 + 十堰源文件清理

---
 traffic-audit-server/src/main/java/com/trafficaudit/auditengine/service/AuditEngineService.java |  498 +++++++++++++++++++++++++++++++++++++++++-------------
 1 files changed, 376 insertions(+), 122 deletions(-)

diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/auditengine/service/AuditEngineService.java b/traffic-audit-server/src/main/java/com/trafficaudit/auditengine/service/AuditEngineService.java
index 8d16e0c..c2dba2b 100644
--- a/traffic-audit-server/src/main/java/com/trafficaudit/auditengine/service/AuditEngineService.java
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/auditengine/service/AuditEngineService.java
@@ -690,23 +690,30 @@
         if (sys == null) return null; // 缂哄け鐢� EXISTENCE_SYS 瑙勫垯鎻愮ず
         String field = rule.getCheckField();
         double actual;
-        double reference;
+        Double referenceObj;
         String fieldName;
         if ("startCum".equals(field)) {
             actual = nvl(monthly.getStartCum());
-            reference = nvl(sys.getStartCum());
+            referenceObj = sys.getStartCum();
             fieldName = "鑷紑濮嬪缓璁剧疮璁�";
         } else if ("yearCum".equals(field)) {
             actual = nvl(monthly.getYearCum());
-            reference = nvl(sys.getYearCum());
+            referenceObj = sys.getYearCum();
             fieldName = "鑷勾鍒濈疮璁�";
         } else if ("monthDone".equals(field)) {
             actual = nvl(monthly.getMonthDone());
-            reference = nvl(sys.getMonthDone());
+            referenceObj = sys.getMonthDone();
             fieldName = "鏈湀瀹屾垚";
         } else {
             return null;
         }
+        // 鍩哄噯缂哄け锛堝鏂版ā鏉裤��2026骞碭鏈堟姇璧勭郴缁熼」鐩槑缁嗐�嬫病鏈夈�岃嚜骞村垵绱銆嶅垪锛夆啋 涓嶅叿澶囧彲姣旀�э紝璺宠繃骞惰鏃ュ織锛�
+        // 涓嶈兘鎶� null 褰撴垚 0 鍘绘瘮瀵癸紝鍚﹀垯浼氭垚鐗囪鎶ャ��
+        if (referenceObj == null) {
+            log.warn("鎶曡祫姣斿璺宠繃锛堟姇璧勭郴缁熷熀鍑嗙己澶憋級锛歳ule={} project={} field={}", rule.getRuleCode(), projectName, fieldName);
+            return null;
+        }
+        double reference = referenceObj;
         String[] parts = rule.getThreshold().split("\\|");
         double absThreshold = parts.length > 0 ? parseDoubleSafe(parts[0], 50) : 50;
         double pctThreshold = parts.length > 1 ? parseDoubleSafe(parts[1], 5) : 5;
@@ -1259,19 +1266,17 @@
 
     public List<AuditResult> queryResults(String reportPeriod, String reportType) {
         String type = reportType == null || reportType.trim().isEmpty() ? "H2032" : reportType.trim();
+        List<AuditRule> rules = ruleMapper.selectList(new LambdaQueryWrapper<AuditRule>()
+            .eq(AuditRule::getReportType, type));
+        if (rules.isEmpty()) return new java.util.ArrayList<>();
+        List<Long> ruleIds = new ArrayList<>();
+        for (AuditRule rule : rules) ruleIds.add(rule.getId());
         List<AuditResult> results = resultMapper.selectList(new LambdaQueryWrapper<AuditResult>()
             .eq(AuditResult::getReportPeriod, reportPeriod)
+            .in(AuditResult::getRuleId, ruleIds)
             .orderByAsc(AuditResult::getId));
-        List<Long> ruleIds = new ArrayList<>();
-        for (AuditRule rule : ruleMapper.selectList(null)) {
-            if (type.equals(rule.getReportType())) ruleIds.add(rule.getId());
-        }
-        List<AuditResult> filtered = new ArrayList<>();
-        for (AuditResult result : results) {
-            if (ruleIds.contains(result.getRuleId())) filtered.add(result);
-        }
-        attachInfo(filtered);
-        return filtered;
+        attachInfo(results, type, reportPeriod);
+        return results;
     }
 
     public void reviewResult(Long id, String status, String comment) {
@@ -1283,46 +1288,56 @@
         resultMapper.updateById(result);
     }
 
-    private void attachInfo(List<AuditResult> results) {
+    private void attachInfo(List<AuditResult> results, String type, String period) {
         if (results.isEmpty()) return;
         Map<Long, AuditRule> ruleMap = new HashMap<>();
-        for (AuditRule rule : ruleMapper.selectList(null)) ruleMap.put(rule.getId(), rule);
-        Map<Long, String> h2032Name = new HashMap<>();
-        Map<Long, String> h2032Explain = new HashMap<>();
-        Map<Long, String> passengerName = new HashMap<>();
-        Map<Long, String> passengerExplain = new HashMap<>();
-        Map<Long, String> energyName = new HashMap<>();
-        Map<Long, String> investName = new HashMap<>();
-        Map<Long, String> busName = new HashMap<>();
-        Map<Long, String> busExplain = new HashMap<>();
-        Map<Long, String> taxiName = new HashMap<>();
-        Map<Long, String> taxiExplain = new HashMap<>();
-        for (H2032EnterpriseMonthly r : h2032Mapper.selectList(null)) {
-            h2032Name.putIfAbsent(r.getId(), r.getEnterpriseName());
-            h2032Explain.putIfAbsent(r.getId(), r.getVerifyExplanation());
+        for (AuditRule rule : ruleMapper.selectList(new LambdaQueryWrapper<AuditRule>()
+                .eq(AuditRule::getReportType, type))) {
+            ruleMap.put(rule.getId(), rule);
         }
-        for (PassengerEnterpriseMonthly r : passengerMapper.selectList(null)) {
-            passengerName.putIfAbsent(r.getId(), r.getEnterpriseName());
-            passengerExplain.putIfAbsent(r.getId(), r.getVerifyExplanation());
-        }
-        for (EnergyVehicleQuarterly r : energyMapper.selectList(null)) {
-            energyName.putIfAbsent(r.getId(), r.getEnterpriseName());
-        }
-        for (CityBusMonthly r : cityBusMapper.selectList(null)) {
-            busName.putIfAbsent(r.getId(), r.getEnterpriseName());
-            busExplain.putIfAbsent(r.getId(), r.getVerifyExplanation());
-        }
-        for (CityTaxiMonthly r : cityTaxiMapper.selectList(null)) {
-            taxiName.putIfAbsent(r.getId(), r.getCity());
-            taxiExplain.putIfAbsent(r.getId(), r.getVerifyExplanation());
-        }
-        Map<Long, Long> investProjectId = new HashMap<>();
-        for (InvestmentMonthly r : investMonthlyMapper.selectList(null)) {
-            investProjectId.putIfAbsent(r.getId(), r.getProjectId());
-        }
-        for (InvestmentProject p : investProjectMapper.selectList(null)) {
-            for (Map.Entry<Long, Long> e : investProjectId.entrySet()) {
-                if (e.getValue().equals(p.getId())) investName.putIfAbsent(e.getKey(), p.getProjectName());
+        Map<Long, String> nameMap = new HashMap<>();
+        Map<Long, String> explainMap = new HashMap<>();
+        if ("INVEST".equals(type)) {
+            Map<Long, Long> monthlyProject = new HashMap<>();
+            for (InvestmentMonthly m : investMonthlyMapper.selectList(new LambdaQueryWrapper<InvestmentMonthly>()
+                    .eq(InvestmentMonthly::getReportPeriod, period))) {
+                monthlyProject.put(m.getId(), m.getProjectId());
+            }
+            for (InvestmentProject p : investProjectMapper.selectList(null)) {
+                for (Map.Entry<Long, Long> e : monthlyProject.entrySet()) {
+                    if (e.getValue() != null && e.getValue().equals(p.getId())) {
+                        nameMap.putIfAbsent(e.getKey(), p.getProjectName());
+                    }
+                }
+            }
+        } else if ("H204".equals(type)) {
+            for (EnergyVehicleQuarterly m : energyMapper.selectList(new LambdaQueryWrapper<EnergyVehicleQuarterly>()
+                    .eq(EnergyVehicleQuarterly::getReportPeriod, period))) {
+                nameMap.putIfAbsent(m.getId(), m.getEnterpriseName());
+            }
+        } else if ("H2031".equals(type)) {
+            for (PassengerEnterpriseMonthly m : passengerMapper.selectList(new LambdaQueryWrapper<PassengerEnterpriseMonthly>()
+                    .eq(PassengerEnterpriseMonthly::getReportPeriod, period))) {
+                nameMap.putIfAbsent(m.getId(), m.getEnterpriseName());
+                explainMap.putIfAbsent(m.getId(), m.getVerifyExplanation());
+            }
+        } else if ("CITY_BUS".equals(type)) {
+            for (CityBusMonthly m : cityBusMapper.selectList(new LambdaQueryWrapper<CityBusMonthly>()
+                    .eq(CityBusMonthly::getReportPeriod, period))) {
+                nameMap.putIfAbsent(m.getId(), m.getEnterpriseName());
+                explainMap.putIfAbsent(m.getId(), m.getVerifyExplanation());
+            }
+        } else if ("CITY_TAXI".equals(type)) {
+            for (CityTaxiMonthly m : cityTaxiMapper.selectList(new LambdaQueryWrapper<CityTaxiMonthly>()
+                    .eq(CityTaxiMonthly::getReportPeriod, period))) {
+                nameMap.putIfAbsent(m.getId(), m.getCity());
+                explainMap.putIfAbsent(m.getId(), m.getVerifyExplanation());
+            }
+        } else {
+            for (H2032EnterpriseMonthly m : h2032Mapper.selectList(new LambdaQueryWrapper<H2032EnterpriseMonthly>()
+                    .eq(H2032EnterpriseMonthly::getReportPeriod, period))) {
+                nameMap.putIfAbsent(m.getId(), m.getEnterpriseName());
+                explainMap.putIfAbsent(m.getId(), m.getVerifyExplanation());
             }
         }
         for (AuditResult result : results) {
@@ -1332,69 +1347,49 @@
                 result.setRuleCode(rule.getRuleCode());
                 result.setAlertLevel(rule.getAlertLevel());
             }
-            // 鍚勬姤琛ㄨ〃 id 鍚勮嚜鐙珛鑷锛岄渶鎸夎鍒欐墍灞炴姤琛ㄧ被鍨嬪垎琛ㄥ洖濉紒涓氬悕绉�
-            String type = rule == null ? null : rule.getReportType();
-            Map<Long, String> nameMap = "INVEST".equals(type) ? investName
-                : "H204".equals(type) ? energyName
-                : "H2031".equals(type) ? passengerName
-                : "CITY_BUS".equals(type) ? busName
-                : "CITY_TAXI".equals(type) ? taxiName : h2032Name;
-            Map<Long, String> explainMap = "H2031".equals(type) ? passengerExplain
-                : "CITY_BUS".equals(type) ? busExplain
-                : "CITY_TAXI".equals(type) ? taxiExplain : h2032Explain;
             String enterpriseName = nameMap.get(result.getReportId());
             if (enterpriseName == null) {
-                // 涓婃姤鏁版嵁琚噸鏂板鍏ュ悗鏃у鏍哥粨鏋滃凡澶辨晥锛岀粰鍑烘彁绀鸿�岄潪绌虹櫧
-                enterpriseName = "\uFF08\u539F\u6570\u636E\u5DF2\u91CD\u65B0\u5BFC\u5165\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C\u5BA1\u6838\uFF09";
+                enterpriseName = "锛堝師鏁版嵁宸查噸鏂板鍏ワ紝璇烽噸鏂版墽琛屽鏍革級";
             }
             result.setEnterpriseName(enterpriseName);
             result.setVerifyExplanation(explainMap.get(result.getReportId()));
         }
     }
-
     /** 鏈�杩戞湁鏁版嵁鐨勬姤琛ㄦ湡锛堢敤浜庡墠绔粯璁ら�変腑锛� */
     public String latestPeriod(String reportType) {
         String type = reportType == null || reportType.trim().isEmpty() ? "H2032" : reportType.trim();
-        String max = null;
         switch (type) {
             case "H2031":
-                for (PassengerEnterpriseMonthly m : passengerMapper.selectList(
-                        new LambdaQueryWrapper<PassengerEnterpriseMonthly>().select(PassengerEnterpriseMonthly::getReportPeriod))) {
-                    max = maxPeriod(max, m.getReportPeriod());
-                }
-                break;
+                return firstOf(passengerMapper.selectObjs(new LambdaQueryWrapper<PassengerEnterpriseMonthly>()
+                    .select(PassengerEnterpriseMonthly::getReportPeriod)
+                    .orderByDesc(PassengerEnterpriseMonthly::getReportPeriod).last("LIMIT 1")));
             case "H204":
-                for (EnergyVehicleQuarterly m : energyMapper.selectList(
-                        new LambdaQueryWrapper<EnergyVehicleQuarterly>().select(EnergyVehicleQuarterly::getReportPeriod))) {
-                    max = maxPeriod(max, m.getReportPeriod());
-                }
-                break;
+                return firstOf(energyMapper.selectObjs(new LambdaQueryWrapper<EnergyVehicleQuarterly>()
+                    .select(EnergyVehicleQuarterly::getReportPeriod)
+                    .orderByDesc(EnergyVehicleQuarterly::getReportPeriod).last("LIMIT 1")));
             case "INVEST":
-                for (InvestmentMonthly m : investMonthlyMapper.selectList(
-                        new LambdaQueryWrapper<InvestmentMonthly>().select(InvestmentMonthly::getReportPeriod))) {
-                    max = maxPeriod(max, m.getReportPeriod());
-                }
-                break;
+                return firstOf(investMonthlyMapper.selectObjs(new LambdaQueryWrapper<InvestmentMonthly>()
+                    .select(InvestmentMonthly::getReportPeriod)
+                    .orderByDesc(InvestmentMonthly::getReportPeriod).last("LIMIT 1")));
             case "CITY_BUS":
-                for (CityBusMonthly m : cityBusMapper.selectList(
-                        new LambdaQueryWrapper<CityBusMonthly>().select(CityBusMonthly::getReportPeriod))) {
-                    max = maxPeriod(max, m.getReportPeriod());
-                }
-                break;
+                return firstOf(cityBusMapper.selectObjs(new LambdaQueryWrapper<CityBusMonthly>()
+                    .select(CityBusMonthly::getReportPeriod)
+                    .orderByDesc(CityBusMonthly::getReportPeriod).last("LIMIT 1")));
             case "CITY_TAXI":
-                for (CityTaxiMonthly m : cityTaxiMapper.selectList(
-                        new LambdaQueryWrapper<CityTaxiMonthly>().select(CityTaxiMonthly::getReportPeriod))) {
-                    max = maxPeriod(max, m.getReportPeriod());
-                }
-                break;
+                return firstOf(cityTaxiMapper.selectObjs(new LambdaQueryWrapper<CityTaxiMonthly>()
+                    .select(CityTaxiMonthly::getReportPeriod)
+                    .orderByDesc(CityTaxiMonthly::getReportPeriod).last("LIMIT 1")));
             default:
-                for (H2032EnterpriseMonthly m : h2032Mapper.selectList(
-                        new LambdaQueryWrapper<H2032EnterpriseMonthly>().select(H2032EnterpriseMonthly::getReportPeriod))) {
-                    max = maxPeriod(max, m.getReportPeriod());
-                }
-                break;
+                return firstOf(h2032Mapper.selectObjs(new LambdaQueryWrapper<H2032EnterpriseMonthly>()
+                    .select(H2032EnterpriseMonthly::getReportPeriod)
+                    .orderByDesc(H2032EnterpriseMonthly::getReportPeriod).last("LIMIT 1")));
         }
-        return max;
+    }
+
+    private String firstOf(List<Object> objs) {
+        if (objs == null || objs.isEmpty() || objs.get(0) == null) return null;
+        String v = String.valueOf(objs.get(0));
+        return v.trim().isEmpty() ? null : v.trim();
     }
 
     private String maxPeriod(String a, String b) {
@@ -1403,24 +1398,29 @@
         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 鐜瘮鎻愰啋锛夛紝鍙繚鐣欐牳瀹炲瀷闂锛泂tatus=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()));
@@ -1437,10 +1437,154 @@
             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 name = valueOf(res.getRuleName());
+        String act = valueOf(res.getActualValue());
+        if ("RULE_001".equals(code) || "RULE_002".equals(code)) {
+            return "浼佷笟涓婃姤杞﹁締/鍚ㄤ綅涓庤繍鏀挎暟鎹姣斿瓨鍦ㄥ樊寮傦紙" + act + "锛夛紝璇锋牳瀹�";
+        }
+        if ("RULE_003".equals(code)) {
+            return "涓婃姤鍛ㄨ浆閲忎笌杞ㄨ抗娴嬬畻鍛ㄨ浆閲忓樊寮傛槑鏄撅紙" + act + "锛夛紝璇峰弬鑰冭建杩规牳瀹炰紒涓氬懆杞噺";
+        }
+        if ("RULE_004".equals(code)) {
+            return "璐ц繍绫诲瀷鐜瘮鍙戠敓鍙樺寲" + (act.isEmpty() ? "" : "锛�" + act + "锛�") + "锛岃鍏虫敞鏄惁濉姤姝e父";
+        }
+        if ("RULE_005".equals(code)) {
+            return "鏃犻泦瑁呯杞︿絾瀛樺湪闆嗚绠辫揣杩愰噺锛�" + act + "锛夛紝璇锋牳瀹炶溅杈嗙被鍨嬩笌璐ц繍閲忓~鎶�";
+        }
+        if ("RULE_006".equals(code)) {
+            return "鏁磋溅骞冲潎鍚ㄤ綅瓒呭嚭鍚堢悊鑼冨洿锛�" + act + "锛夛紝璇锋牳瀹炲钩鍧囧惃浣嶅~鎶�";
+        }
+        if (code != null && code.startsWith("RULE_P")) {
+            String special = passengerIssueSentence(res);
+            if (special != null) return special;
+        }
+        return name + "寮傚父锛�" + act + "锛夛紝璇锋牳瀹�";
+    }
+
+    /** 瀹㈣繍锛圚2031锛夎鍒欓棶棰樿瘽鏈細璐村悎妯℃澘绀轰緥鈥滄牳瀹炲紓甯革紙涓婃湡鎸囨爣=锛屾湰鏈熸寚鏍�=锛夎鏍稿疄鈥� */
+    private String passengerIssueSentence(AuditResult res) {
+        String code = res.getRuleCode();
+        String act = valueOf(res.getActualValue());
+        if ("RULE_P003".equals(code) || "RULE_P004".equals(code)) {
+            boolean turnover = "RULE_P004".equals(code);
+            String inner = act;
+            if (inner.startsWith("鏈湀涓庝笂鏈堢浉鍚�: ")) {
+                inner = inner.substring("鏈湀涓庝笂鏈堢浉鍚�: ".length());
+            }
+            java.util.List<String> clauses = new java.util.ArrayList<>();
+            for (String item : inner.split("銆�")) {
+                int eq = item.indexOf('=');
+                if (eq <= 0) { clauses.add(item); continue; }
+                String label = item.substring(0, eq).trim();
+                String v = item.substring(eq + 1).trim();
+                clauses.add("涓婃湀" + (turnover ? "鍛ㄨ浆閲�" : "瀹㈣繍閲�") + "=" + v + "锛屾湰鏈�" + label + "=" + v);
+            }
+            return valueOf(res.getRuleName()) + "寮傚父锛�" + String.join("锛�", clauses) + "锛夎鏍稿疄";
+        }
+        if ("RULE_P005".equals(code)) {
+            String pct = extractPct(valueOf(res.getThresholdValue()));
+            String prefix = pct.isEmpty() ? "鐝嚎骞冲潎杩愯窛鐜瘮瓒呴檺" : "鐝嚎骞冲潎杩愯窛鐜瘮瓒吢�" + pct + "%";
+            return prefix + "锛屾甯哥彮绾胯繍璺濆湪绾胯矾鏈彂鐢熼噸澶у彉鍖栧墠杈冧负绋冲畾锛�" + act + "锛夛紝璇锋牳瀹�";
+        }
+        if ("RULE_P006".equals(code)) {
+            String inner = act;
+            if (inner.startsWith("绛変簬涓婃湀: ")) {
+                inner = inner.substring("绛変簬涓婃湀: ".length());
+            }
+            java.util.List<String> clauses = new java.util.ArrayList<>();
+            for (String item : inner.split("銆�")) {
+                int eq = item.indexOf('=');
+                if (eq <= 0) { clauses.add(item); continue; }
+                String v = item.substring(eq + 1).trim();
+                clauses.add("涓婃湀杩愯窛=" + v + "锛屾湰鏈堣繍璺�=" + v);
+            }
+            return "鍖呰溅骞冲潎杩愯窛绛変簬涓婃湀锛�" + String.join("锛�", clauses)
+                + "锛夎鏍稿疄浼佷笟鏄惁鏄浐瀹氱嚎璺梾娓告垨鑰呴�氬嫟涓氬姟";
+        }
+        return null;
+    }
+
+    private String extractPct(String thresholdText) {
+        if (thresholdText == null || thresholdText.isEmpty()) return "";
+        int idx = thresholdText.indexOf('\u00b1');
+        if (idx < 0) return "";
+        StringBuilder num = new StringBuilder();
+        for (int i = idx + 1; i < thresholdText.length(); i++) {
+            char c = thresholdText.charAt(i);
+            if (Character.isDigit(c) || c == '.') num.append(c);
+            else if (!Character.isWhitespace(c)) break;
+        }
+        return num.toString();
     }
 
     private String auditStatusCn(String status) {
@@ -1569,6 +1713,8 @@
             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":
@@ -1611,6 +1757,86 @@
         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,
@@ -1645,26 +1871,34 @@
         return null;
     }
 
-    /** 璐ц繍绫诲瀷鐜瘮锛氭湰鏈堟柊澧烇紙涓婃湀涓�0銆佹湰鏈�>0锛夐渶鏍稿疄锛屽皯鐨勪笉绠� */
+    /** 璐ц繍绫诲瀷鐜瘮锛圧ULE_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(",");
@@ -1679,15 +1913,29 @@
         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;
@@ -1782,6 +2030,10 @@
         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;
     }
@@ -1805,18 +2057,20 @@
     }
 
     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 鏃呭瀹℃牳 ==========

--
Gitblit v1.9.1