From a08c70db67e49bf6cd125df04ebaf68c26f0d8dd Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期日, 20 九月 2026 16:19:16 +0800
Subject: [PATCH] feat(投资): 市州月报统一填报模板 + 县(市、区)列解析 + 表头行判定收紧

---
 traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java |  292 ++++++++++++++++++++++++++++++++++++++++++++++++++-------
 1 files changed, 253 insertions(+), 39 deletions(-)

diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java b/traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java
index 1f6ca46..c694e75 100644
--- a/traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java
@@ -60,6 +60,10 @@
 import org.apache.poi.xwpf.usermodel.XWPFParagraph;
 import org.apache.poi.xwpf.usermodel.XWPFTable;
 import org.apache.poi.xwpf.usermodel.XWPFTableRow;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
 import cn.hutool.core.io.IoUtil;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -2461,10 +2465,10 @@
             }
             if (name == null || name.trim().isEmpty()) continue;
             String nt = name.trim().replaceAll("[\r\n]+", "");
-            if (nt.contains("鍚堣") || nt.contains("鎬昏")) continue;
+            String ntFlat = nt.replaceAll("\\s+", "");
+            if (ntFlat.contains("鍚堣") || ntFlat.contains("鎬昏") || ntFlat.contains("灏忚")) continue;
             if (nt.matches("^[涓�浜屼笁鍥涗簲鍏竷鍏節鍗乚+銆�.*")) continue;
             if (nt.matches("^锛圼涓�浜屼笁鍥涗簲鍏竷鍏節鍗乚+锛�.*")) continue;
-            if (nt.replace(" ", "").matches(".*灏廫\s*璁�.*")) continue;
             if (nt.startsWith("濉姤") || nt.startsWith("鍗曚綅璐熻矗浜�") || nt.matches("^\\d+銆�.*")) continue;
             if (hasSeq) {
                 if (seq == null || seq.trim().isEmpty()) {
@@ -2485,6 +2489,8 @@
             project.setProjectName(nt);
             project.setCategory(category);
             project.setCity(currentCity);
+            String countyCell = cellStr(row, col.get("county"));
+            if (countyCell != null && !countyCell.trim().isEmpty()) project.setCounty(countyCell.trim());
             project.setBuilderName(cellStr(row, col.get("builder")));
             project.setConstructNature(cellStr(row, col.get("nature")));
             project.setStartTime(investTime(cellStr(row, col.get("start"))));
@@ -2520,13 +2526,22 @@
             Row row = sh.getRow(r);
             if (row == null) continue;
             boolean hasName = false;
+            int headerCells = 0;
             for (int c = 0; c <= row.getLastCellNum(); c++) {
                 String v = cellStr(row, c);
-                if (v != null && v.contains("椤圭洰鍚嶇О")) { hasName = true; break; }
+                if (v == null) continue;
+                String t = v.trim();
+                if (t.isEmpty() || t.length() > 20) continue;   // 璇存槑鎬ч暱鍙ヤ笉绠楄〃澶村瓧娈�
+                String k = investHeaderKey(t);
+                if (k == null) continue;
+                headerCells++;
+                if ("name".equals(k)) hasName = true;
             }
-            if (!hasName) continue;
+            // 鐪熻〃澶磋鑷冲皯鏈� 2 涓煭琛ㄥご瀛楁锛涘彧鏈変竴鏍奸暱鍙ュ惈銆岄」鐩悕绉般�嶇殑璇存槑椤典細琚帓闄�
+            if (!hasName || headerCells < 2) continue;
             Map<Integer, String> colTexts = new HashMap<>();
-            for (int rr = r; rr <= Math.min(r + 3, sh.getLastRowNum()); rr++) {
+            int scanEnd = investHeaderScanEnd(sh, r, row);
+            for (int rr = r; rr <= scanEnd; rr++) {
                 Row rr2 = sh.getRow(rr);
                 if (rr2 == null) continue;
                 for (int c = 0; c <= rr2.getLastCellNum(); c++) {
@@ -2537,23 +2552,8 @@
             }
             Map<String, Integer> col = new HashMap<>();
             for (Map.Entry<Integer, String> e : colTexts.entrySet()) {
-                int c = e.getKey();
-                String v = e.getValue();
-                if (v.contains("椤圭洰鍚嶇О")) col.putIfAbsent("name", c);
-                else if (v.contains("鎶曡祫寤鸿") || v.contains("寤鸿鍗曚綅") || v.contains("椤圭洰涓氫富")) col.putIfAbsent("builder", c);
-                else if (v.contains("寤鸿鎬ц川")) col.putIfAbsent("nature", c);
-                else if (v.contains("寮�宸�")) col.putIfAbsent("start", c);
-                else if (v.contains("绔e伐") || v.contains("寤烘垚")) col.putIfAbsent("end", c);
-                else if ((v.contains("璁″垝鎬绘姇璧�") || v.contains("鎬绘姇璧�")) && !v.contains("绱")) col.putIfAbsent("total", c);
-                else if (v.contains("鑷紑濮嬪缓璁�")) col.putIfAbsent("startcum", c);
-                else if (v.contains("鏈勾璁″垝")) col.putIfAbsent("yearplan", c);
-                else if (v.contains("鑷勾鍒濈疮璁�")) col.putIfAbsent("yearcum", c);
-                else if (v.contains("鏈湀瀹屾垚")) col.putIfAbsent("monthdone", c);
-                else if (v.contains("寤鸿闃舵")) col.putIfAbsent("stage", c);
-                else if (v.contains("褰㈣薄杩涘害")) col.putIfAbsent("desc", c);
-                else if (v.contains("宸ュ彲") && !v.contains("椤圭洰鍚嶇О")) col.putIfAbsent("gk", c);
-                else if (v.contains("鍒濊") && !v.contains("椤圭洰鍚嶇О")) col.putIfAbsent("cs", c);
-                else if (v.contains("寤虹瓚闈㈢Н") && !v.contains("鏂板")) col.putIfAbsent("area", c);
+                String k = investHeaderKey(e.getValue());
+                if (k != null) col.putIfAbsent(k, e.getKey());
             }
             if (!col.containsKey("name")) continue;
             for (int c = 0; c <= row.getLastCellNum(); c++) {
@@ -2570,14 +2570,85 @@
                 int md = col.get("yearcum") + 1;
                 if (md <= row.getLastCellNum()) col.put("monthdone", md);
             }
+            // desc 缂哄け鏃舵寜 stage+1 鍏滃簳锛氱渷鏍囥�岄」鐩缓璁鹃樁娈点�佽繘搴︽儏鍐点�嶆槸 L/M 鍚堝苟琛ㄥご锛�
+            // 瀛濇劅/鎭╂柦/鏋楀尯/闅忓窞绛夊競宸炲彧鍦� L 鍐欍�屽缓璁鹃樁娈点�嶃�丮 鐩存帴鍐欒繘搴︽枃瀛楄�屼笉鍐欐爣棰�
+            if (!col.containsKey("desc") && col.containsKey("stage") && col.get("stage") >= 0) {
+                int dc = col.get("stage") + 1;
+                if (dc <= row.getLastCellNum()) col.put("desc", dc);
+            }
             for (String k : new String[]{"builder", "nature", "start", "end", "total", "startcum",
-                "yearplan", "yearcum", "monthdone", "stage", "desc", "gk", "cs", "area"}) {
+                "yearplan", "yearcum", "monthdone", "stage", "desc", "gk", "cs", "area", "county"}) {
                 col.putIfAbsent(k, -1);
             }
             col.put("headerRow", r);
             return col;
         }
         return null;
+    }
+
+    /** 鍗曚釜琛ㄥご鍗曞厓鏍� -> 瀛楁閿紙null 琛ㄧず涓嶆槸琛ㄥご瀛楁锛夛紱渚涜〃澶磋鍒ゅ畾涓庡垪鏄犲皠鍏辩敤 */
+    private String investHeaderKey(String v) {
+        if (v == null) return null;
+        String flat = v.replace(" ", "");
+        if (v.contains("椤圭洰鍚嶇О")) return "name";
+        // 銆屽幙锛堝競銆佸尯锛夈�嶇殑鍚勭鍐欐硶锛氬崐/鍏ㄨ鎷彿銆侀】鍙�/閫楀彿銆佹湁鏃犳嫭鍙凤紝缁熶竴褰掍竴鍖栧悗鍐嶅垽瀹�
+        String flatCounty = flat.replace("锛�", "(").replace("锛�", ")").replace("銆�", "").replace("锛�", "").replace(",", "");
+        if (flatCounty.contains("鍘�(甯傚尯)") || flatCounty.contains("鍘垮競鍖�") || flatCounty.contains("鍖哄幙")) return "county";
+        if (v.contains("鎶曡祫寤鸿") || v.contains("寤鸿鍗曚綅") || v.contains("椤圭洰涓氫富")) return "builder";
+        if (v.contains("寤鸿鎬ц川")) return "nature";
+        if (v.contains("寮�宸�")) return "start";
+        if (v.contains("绔e伐") || v.contains("寤烘垚")) return "end";
+        if ((v.contains("璁″垝鎬绘姇璧�") || v.contains("鎬绘姇璧�")) && !v.contains("绱")) return "total";
+        if (v.contains("鑷紑濮嬪缓璁�")) return "startcum";
+        if (v.contains("鏈勾璁″垝")) return "yearplan";
+        if (v.contains("鑷勾鍒濈疮璁�")) return "yearcum";
+        if (v.contains("鏈湀瀹屾垚")) return "monthdone";
+        if (v.contains("寤鸿闃舵")) return "stage";
+        if (v.contains("褰㈣薄杩涘害") || v.contains("寤鸿杩涘睍")) return "desc";
+        if (v.contains("宸ュ彲") && !v.contains("椤圭洰鍚嶇О")) return "gk";
+        if (v.contains("鍒濊") && !v.contains("椤圭洰鍚嶇О")) return "cs";
+        if (v.contains("寤虹瓚闈㈢Н") && !v.contains("鏂板")) return "area";
+        return null;
+    }
+
+    /** 琛ㄥご鏂囨湰鎵弿涓嬬晫锛氶粯璁よ〃澶磋 +3锛涜嫢銆岄」鐩悕绉般�嶅垪棣栦釜鏁版嵁琛屾洿闈犱笅锛屽垯鎵╁睍鍒版暟鎹鍓嶄竴琛岋紙涓婇檺 +12锛夈��
+     *  鑳屾櫙锛氬捀瀹�/鍗佸牥绛夊競宸炴妸銆屽缓璁鹃樁娈�/褰㈣薄杩涘害銆嶆爣绛惧啓鍦ㄨ〃澶翠笅绗� 4~8 琛岋紝鍥哄畾 +3 浼氭紡鍒楋紙褰㈣薄杩涘害璇讳笉鍒帮級銆� */
+    private int investHeaderScanEnd(Sheet sh, int headerRow, Row headerRowObj) {
+        int hardEnd = Math.min(headerRow + 12, sh.getLastRowNum());
+        int nameCol = -1;
+        if (headerRowObj != null) {
+            for (int c = 0; c <= headerRowObj.getLastCellNum(); c++) {
+                String v = cellStr(headerRowObj, c);
+                if (v != null && v.contains("椤圭洰鍚嶇О")) { nameCol = c; break; }
+            }
+        }
+        int firstData = findFirstInvestDataRow(sh, headerRow, nameCol);
+        int scanEnd = (firstData > headerRow)
+            ? Math.min(hardEnd, firstData - 1)
+            : Math.min(headerRow + 3, sh.getLastRowNum());
+        if (scanEnd < headerRow) scanEnd = headerRow;
+        return scanEnd;
+    }
+
+    /** 銆岄」鐩悕绉般�嶅垪棣栦釜鏁版嵁琛屼笅鏍囷紙璺宠繃琛ㄥご缁涓庣┖琛岋級锛涙壘涓嶅埌杩斿洖 -1 */
+    private int findFirstInvestDataRow(Sheet sh, int headerRow, int nameCol) {
+        if (nameCol < 0) return -1;
+        for (int r = headerRow + 1; r <= sh.getLastRowNum(); r++) {
+            Row row = sh.getRow(r);
+            if (row == null) continue;
+            String name = cellStr(row, nameCol);
+            if (name == null || name.trim().isEmpty()) continue;
+            String nt = name.trim().replaceAll("[\r\n]+", "");
+            if (nt.contains("椤圭洰鍚嶇О")) continue;
+            String flat = nt.replaceAll("\\s+", "");
+            if (flat.contains("鍚堣") || flat.contains("鎬昏") || flat.contains("灏忚")) continue;
+            if (nt.matches("^[涓�浜屼笁鍥涗簲鍏竷鍏節鍗乚+銆�.*")) continue;
+            if (nt.matches("^锛圼涓�浜屼笁鍥涗簲鍏竷鍏節鍗乚+锛�.*")) continue;
+            if (nt.startsWith("濉姤") || nt.startsWith("鍗曚綅璐熻矗浜�") || nt.matches("^\\d+銆�.*")) continue;
+            if (investmentCity(nt) != null) continue;
+            return r;
+        }
+        return -1;
     }
 
     /** 缁熻 sheet 鍐呴」鐩暟鎹鏁帮紙鐢ㄤ簬澶� sheet 鍘嗗彶鏂囦欢閫夊綋鏈� sheet锛� */
@@ -2592,9 +2663,9 @@
             String name = cellStr(row, nameCol);
             if (name == null || name.trim().isEmpty()) continue;
             String nt = name.trim().replaceAll("[\r\n]+", "");
-            if (nt.contains("鍚堣") || nt.contains("鎬昏")) continue;
+            String ntFlat = nt.replaceAll("\\s+", "");
+            if (ntFlat.contains("鍚堣") || ntFlat.contains("鎬昏") || ntFlat.contains("灏忚")) continue;
             if (nt.matches("^[涓�浜屼笁鍥涗簲鍏竷鍏節鍗乚+銆�.*")) continue;
-            if (nt.replace(" ", "").matches(".*灏廫\s*璁�.*")) continue;
             if (nt.startsWith("濉姤") || nt.startsWith("鍗曚綅璐熻矗浜�") || nt.matches("^\\d+銆�.*")) continue;
             if (!hasSeq) {
                 if (nt.length() > 4) count++;
@@ -3319,10 +3390,36 @@
         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);
+        byte[] bytes;
+        try (java.io.InputStream in = file.getInputStream()) {
+            bytes = IoUtil.readBytes(in);
+        }
+        boolean isZip = bytes.length > 3 && (bytes[0] & 0xFF) == 0x50 && (bytes[1] & 0xFF) == 0x4B;
+        boolean isWordMl = !isZip && looksLikeWordMl(bytes);
+        try {
+            if (isZip && !isWordPackage(bytes)) {
+                throw new RuntimeException("杩欎笉鏄� Word 鏂囨。锛堝帇缂╁寘閲屾病鏈� word/document.xml锛夛紝"
+                        + "澶氬崐鏄� Excel 鎴栧叾瀹冩牸寮忋�傝涓婁紶缃戠害杞﹁鍗曞垎鏋愭姤鍛婄殑 .docx锛堟垨 Word 2003 XML锛�");
+            }
+            if (isZip) {
+                try (XWPFDocument doc = new XWPFDocument(new java.io.ByteArrayInputStream(bytes))) {
+                    docPeriod = periodOfParagraphs(paragraphsOfDocx(doc));
+                    parseWycOrderReportTables(doc, orders, sumHolder);
+                }
+            } else if (isWordMl) {
+                // 缃戠害杞︾洃绠″钩鍙颁笅鍙戠殑鎶ュ憡甯歌 WPS/Word 瀛樻垚銆學ord 2003 XML锛堝钩閾� XML锛夈�嶅悗鍐嶆敼鍚� .docx锛�
+                // 杩欑鏂囦欢涓嶆槸 zip 鍖咃紝XWPFDocument 鎵撲笉寮�锛堟姤 "File is not a zip file"锛夛紝闇�鎸� XML 鐩存帴瑙f瀽銆�
+                String xml = new String(bytes, java.nio.charset.StandardCharsets.UTF_8);
+                Document dom = parseXmlSafely(xml);
+                docPeriod = periodOfParagraphs(paragraphsOfDocxXml(dom));
+                parseWycOrderReportTablesXml(dom, orders, sumHolder);
+            } else if (isOle2(bytes)) {
+                throw new RuntimeException("杩欐槸 Word 97-2003 鑰佹牸寮忥紙.doc锛夛紝璇风敤 WPS/Word 鎵撳紑鍚庛�屽彟瀛樹负銆峝ocx 鏍煎紡鍐嶅鍏�");
+            } else {
+                throw new RuntimeException("鏃犳硶璇嗗埆鐨勬枃浠舵牸寮忥細鏃笉鏄� docx锛坺ip 鍖咃級锛屼篃涓嶆槸 Word 2003 XML 鎴� .doc");
+            }
+        } catch (RuntimeException e) {
+            throw e;
         } catch (Exception e) {
             throw new RuntimeException("鏃犳硶璇诲彇 Word 鏂囨。锛岃涓婁紶 .docx 鏍煎紡鐨勩�婄綉绾﹁溅琛屼笟YYYY骞碝鏈堣繍琛岀洃娴嬫儏鍐靛垎鏋愭姤鍛娿�嬶紱"
                     + "鑻ヤ笂浼犵殑鍏跺疄鏄� Excel/PDF锛岃鏀圭敤瀵瑰簲妯℃澘瀵煎叆銆傦紙鍘熷閿欒锛�" + e.getMessage() + "锛�");
@@ -3388,13 +3485,12 @@
     }
 
     /** 鎶ュ憡鏍囬涓殑鎶ヨ〃鏈燂細缃戠害杞﹁涓�2026骞�7鏈� -> 2026-07锛涘彇涓嶅埌杩斿洖 null */
-    private String periodOfDocx(XWPFDocument doc) {
+    private String periodOfParagraphs(List<String> paragraphs) {
         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();
+        for (String text : paragraphs) {
             if (text == null) continue;
             String flat = text.replaceAll("\\s+", "");
             if (flat.isEmpty()) continue;
@@ -3409,6 +3505,92 @@
         return fallback;
     }
 
+    /** docx 娈佃惤鏂囨湰锛堜繚鎸佸師濮嬮『搴忥級 */
+    private List<String> paragraphsOfDocx(XWPFDocument doc) {
+        List<String> out = new ArrayList<>();
+        for (XWPFParagraph para : doc.getParagraphs()) out.add(para.getText());
+        return out;
+    }
+
+    /** Word 2003 XML锛圵ordML锛夋钀芥枃鏈細<w:p> 涓嬫墍鏈� <w:t> 鎷兼帴鍚庡帇鎺夌┖鐧� */
+    private List<String> paragraphsOfDocxXml(Document dom) {
+        List<String> out = new ArrayList<>();
+        for (Element p : elementsByLocalName(dom, "p")) out.add(textOfXmlElement(p));
+        return out;
+    }
+
+    /** 鏄惁涓� Word 2003 XML锛圵ordML 骞抽摵 XML锛夆�斺�旇繖绉嶆枃浠跺父琚敼鍚嶆垚 .docx */
+    private boolean looksLikeWordMl(byte[] bytes) {
+        int n = Math.min(bytes.length, 4096);
+        String head = new String(bytes, 0, n, java.nio.charset.StandardCharsets.UTF_8);
+        return head.contains("wordml") && head.contains("<w:wordDocument");
+    }
+
+    /** zip 鍖呭唴鏄惁鍚� word/document.xml锛堢敤浜庡尯鍒� docx 涓� xlsx/鍏跺畠 OPC 鍖咃級 */
+    private boolean isWordPackage(byte[] bytes) {
+        try (java.util.zip.ZipInputStream zin =
+                     new java.util.zip.ZipInputStream(new java.io.ByteArrayInputStream(bytes))) {
+            java.util.zip.ZipEntry e;
+            while ((e = zin.getNextEntry()) != null) {
+                String n = e.getName();
+                if ("word/document.xml".equals(n)) return true;
+            }
+        } catch (Exception ignore) {
+            return false;
+        }
+        return false;
+    }
+
+    /** 鏄惁涓� OLE2 澶嶅悎鏂囨。锛圵ord 97-2003 .doc锛� */
+    private boolean isOle2(byte[] b) {
+        return b.length > 8 && (b[0] & 0xFF) == 0xD0 && (b[1] & 0xFF) == 0xCF
+                && (b[2] & 0xFF) == 0x11 && (b[3] & 0xFF) == 0xE0;
+    }
+
+    /** 瀹夊叏瑙f瀽 XML锛氱鐢ㄥ閮ㄥ疄浣�/DTD锛堟姤鍛婃潵鑷閮ㄥ钩鍙帮紝闃� XXE锛� */
+    private Document parseXmlSafely(String xml) throws Exception {
+        javax.xml.parsers.DocumentBuilderFactory f = javax.xml.parsers.DocumentBuilderFactory.newInstance();
+        f.setNamespaceAware(true);
+        try { f.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); } catch (Exception ignore) { }
+        try { f.setFeature("http://xml.org/sax/features/external-general-entities", false); } catch (Exception ignore) { }
+        try { f.setFeature("http://xml.org/sax/features/external-parameter-entities", false); } catch (Exception ignore) { }
+        javax.xml.parsers.DocumentBuilder b = f.newDocumentBuilder();
+        b.setEntityResolver(new org.xml.sax.EntityResolver() {
+            @Override
+            public org.xml.sax.InputSource resolveEntity(String publicId, String systemId) {
+                return new org.xml.sax.InputSource(new java.io.StringReader(""));
+            }
+        });
+        return b.parse(new org.xml.sax.InputSource(new java.io.StringReader(xml)));
+    }
+
+    /** 鎸� localName 鍙栧叏閮ㄥ悗浠e厓绱狅紙鍛藉悕绌洪棿鏃犲叧锛屽吋瀹� WordML 涓� docx 鐨� w: 鍓嶇紑锛� */
+    private List<Element> elementsByLocalName(Document dom, String localName) {
+        List<Element> out = new ArrayList<>();
+        NodeList list = dom.getElementsByTagNameNS("*", localName);
+        for (int i = 0; i < list.getLength(); i++) out.add((Element) list.item(i));
+        return out;
+    }
+
+    /** 鍙彇鐩存帴瀛愬厓绱狅紙閬垮厤宓屽琛ㄦ牸/宓屽灞炴�у厓绱犺绠楄繘鏉ワ級 */
+    private List<Element> childElementsByLocalName(Element parent, String localName) {
+        List<Element> out = new ArrayList<>();
+        NodeList kids = parent.getChildNodes();
+        for (int i = 0; i < kids.getLength(); i++) {
+            Node n = kids.item(i);
+            if (n.getNodeType() == Node.ELEMENT_NODE && localName.equals(n.getLocalName())) out.add((Element) n);
+        }
+        return out;
+    }
+
+    /** 鍏冪礌鍐呮墍鏈� <w:t> 鏂囨湰鎷兼帴骞跺帇鎺夌┖鐧� */
+    private String textOfXmlElement(Element el) {
+        StringBuilder sb = new StringBuilder();
+        NodeList ts = el.getElementsByTagNameNS("*", "t");
+        for (int i = 0; i < ts.getLength(); i++) sb.append(ts.item(i).getTextContent());
+        return flat(sb.toString());
+    }
+
     private String ymd(String y, String mo) {
         try {
             int month = Integer.parseInt(mo);
@@ -3421,15 +3603,47 @@
 
     /** 瑙f瀽鎶ュ憡涓殑銆屽湴甯傚悕绉� | 鈥� | 璁㈠崟鏁版�绘暟銆嶈〃锛宱rders=甯傚窞璁㈠崟锛宻umHolder[0]=鍚堣琛岃鍗曟暟 */
     private void parseWycOrderReportTables(XWPFDocument doc, Map<String, Double> orders, double[] sumHolder) {
+        List<List<List<String>>> tables = new ArrayList<>();
         for (XWPFTable table : doc.getTables()) {
-            List<XWPFTableRow> rows = table.getRows();
+            List<List<String>> rows = new ArrayList<>();
+            for (XWPFTableRow row : table.getRows()) {
+                List<String> cells = new ArrayList<>();
+                for (org.apache.poi.xwpf.usermodel.XWPFTableCell c : row.getTableCells()) cells.add(flat(c.getText()));
+                rows.add(cells);
+            }
+            tables.add(rows);
+        }
+        matchWycOrderTable(tables, orders, sumHolder);
+    }
+
+    /** Word 2003 XML锛圵ordML锛夊垎鏀細<w:tbl>/<w:tr>/<w:tc> 鎶芥垚鏂囨湰琛ㄥ悗璧板悓涓�濂楀尮閰嶉�昏緫 */
+    private void parseWycOrderReportTablesXml(Document dom, Map<String, Double> orders, double[] sumHolder) {
+        List<List<List<String>>> tables = new ArrayList<>();
+        for (Element tbl : elementsByLocalName(dom, "tbl")) {
+            List<List<String>> rows = new ArrayList<>();
+            for (Element tr : childElementsByLocalName(tbl, "tr")) {
+                List<String> cells = new ArrayList<>();
+                for (Element tc : childElementsByLocalName(tr, "tc")) cells.add(textOfXmlElement(tc));
+                rows.add(cells);
+            }
+            tables.add(rows);
+        }
+        matchWycOrderTable(tables, orders, sumHolder);
+    }
+
+    /**
+     * 鍦ㄣ�屾枃鏈寲琛ㄦ牸銆嶉噷鎵鹃檮浠�1锛堣〃澶村惈銆岃鍗曟暟鎬绘暟銆�/銆岃鍗曟暟銆嶏級骞跺彇 17 甯傚窞璁㈠崟鏁颁笌鍚堣銆�
+     * 鍏ュ弬 tables = 琛� -> 琛� -> 鍗曞厓鏍兼枃鏈紙宸插帇绌虹櫧锛夛紝涓庢潵婧愭牸寮忔棤鍏炽��
+     */
+    private void matchWycOrderTable(List<List<List<String>>> tables, Map<String, Double> orders, double[] sumHolder) {
+        for (List<List<String>> rows : tables) {
             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();
+                List<String> cells = rows.get(r);
                 for (int c = 0; c < cells.size(); c++) {
-                    String norm = flat(cells.get(c).getText());
+                    String norm = cells.get(c);
                     if (norm.contains("璁㈠崟鏁版�绘暟") || "璁㈠崟鏁�".equals(norm)) {
                         headerRow = r;
                         orderCol = c;
@@ -3444,11 +3658,11 @@
             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());
+                List<String> cells = rows.get(r);
+                if (cells.isEmpty() || orderCol >= cells.size()) continue;
+                String name = cells.get(0);
                 if (name.isEmpty()) continue;
-                Double val = parsePlainNumber(cells.get(orderCol).getText());
+                Double val = parsePlainNumber(cells.get(orderCol));
                 if (val == null) continue;
                 String city = RegionUtil.normalizeCityName(name);
                 if (city != null && RegionUtil.cityList().contains(city)) {

--
Gitblit v1.9.1