diff --git a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/TmsPriceBillDemo.java b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/TmsPriceBillDemo.java new file mode 100644 index 00000000..20f39456 --- /dev/null +++ b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/TmsPriceBillDemo.java @@ -0,0 +1,288 @@ +package com.ruoyi.web.controller.tool; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamReader; + +/** + * BMS TMS 计费接口 demo(单文件,纯 JDK,无第三方依赖) + * + * 功能:读取 D:\code\view\orders_10000.xlsx 的每一行(跳过表头), + * 组装为 JSON 后 POST 到本地 BMS 计费接口。 + * + * 接口:POST http://127.0.0.1:8080/api/tms/price/bill + * 鉴权:Header X-Api-Code + * + * 编译运行: + * javac TmsPriceBillDemo.java + * java TmsPriceBillDemo + */ +public class TmsPriceBillDemo { + + // ===== 可配置项 ===== + private static final String BASE_URL = "http://127.0.0.1:8080"; + private static final String BILL_PATH = "/api/tms/price/bill"; + private static final String API_CODE = "BMS.TMS.ZYaOvlQzpcfau8TUaCFfO3Hp8ieHIqGw"; + private static final String EXCEL_PATH = "D:\\code\\view\\orders_10000.xlsx"; + private static final int CONNECT_TIMEOUT_MS = 10_000; + private static final int READ_TIMEOUT_MS = 30_000; + /** 每多少条打印一次进度 */ + private static final int PROGRESS_STEP = 200; + + /** 数值字段:JSON 中不加引号,直接输出 Excel 中的原始数字字符串 */ + private static final Set NUMERIC_FIELDS = new HashSet<>(Arrays.asList( + "weight", "pieces", "actualWeight", "volume", "volumeWeight", + "pkgLength", "pkgWidth", "pkgHeight", "tmsId" + )); + + public static void main(String[] args) throws Exception { + // 可通过命令行参数覆盖 Excel 路径 + String excel = (args.length > 0 && !args[0].isEmpty()) ? args[0] : EXCEL_PATH; + System.out.println("[config] base=" + BASE_URL + BILL_PATH); + System.out.println("[config] xApiCode=" + API_CODE); + System.out.println("[config] excel=" + excel); + + long start = System.currentTimeMillis(); + int ok = 0, fail = 0, rowIdx = 0; + + try (ZipFile zf = new ZipFile(excel)) { + List sst = readSharedStrings(zf); + System.out.println("[read] sharedStrings uniqueCount=" + sst.size()); + + SheetReader sr = new SheetReader(zf, sst); + String[] headers = null; + List row; + + while ((row = sr.nextRow()) != null) { + rowIdx++; + if (headers == null) { + headers = row.toArray(new String[0]); + System.out.println("[read] headers=" + row); + continue; + } + String json = buildJson(row, headers); + String resp; + try { + resp = postJson(BASE_URL + BILL_PATH, API_CODE, json); + } catch (Exception e) { + resp = "ERR: " + e.getMessage(); + } + if (resp.contains("\"code\":200") || resp.contains("\"code\": 200")) { + ok++; + } else { + fail++; + } + if (rowIdx % PROGRESS_STEP == 0 || rowIdx == 1) { + String snippet = resp.length() > 120 ? resp.substring(0, 120) + "..." : resp; + System.out.printf("[progress] %d ok=%d fail=%d last=%s%n", rowIdx, ok, fail, snippet); + } + } + System.out.printf("[done] rows=%d ok=%d fail=%d cost=%dms%n", + rowIdx, ok, fail, System.currentTimeMillis() - start); + } + } + + // ===== JSON 构造 ===== + + private static String buildJson(List row, String[] headers) { + StringBuilder sb = new StringBuilder(256); + sb.append('{'); + for (int i = 0; i < headers.length; i++) { + if (i > 0) sb.append(','); + String k = headers[i]; + String v = (i < row.size()) ? row.get(i) : ""; + sb.append('"').append(escape(k)).append("\":"); + boolean isNumeric = NUMERIC_FIELDS.contains(k) + && v != null && !v.isEmpty() && !"null".equals(v); + if (isNumeric) { + sb.append(v); + } else if (v == null || v.isEmpty() || "null".equals(v)) { + sb.append("null"); + } else { + sb.append('"').append(escape(v)).append('"'); + } + } + sb.append('}'); + return sb.toString(); + } + + private static String escape(String s) { + if (s == null) return ""; + StringBuilder sb = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '"': sb.append("\\\""); break; + case '\\': sb.append("\\\\"); break; + case '\n': sb.append("\\n"); break; + case '\r': sb.append("\\r"); break; + case '\t': sb.append("\\t"); break; + default: + if (c < 0x20) sb.append(String.format("\\u%04x", (int) c)); + else sb.append(c); + } + } + return sb.toString(); + } + + // ===== HTTP ===== + + private static String postJson(String url, String apiCode, String json) throws Exception { + HttpURLConnection conn = null; + try { + conn = (HttpURLConnection) new URL(url).openConnection(); + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + conn.setConnectTimeout(CONNECT_TIMEOUT_MS); + conn.setReadTimeout(READ_TIMEOUT_MS); + conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8"); + conn.setRequestProperty("X-Api-Code", apiCode); + conn.setRequestProperty("Accept", "application/json"); + + byte[] body = json.getBytes(StandardCharsets.UTF_8); + conn.setRequestProperty("Content-Length", String.valueOf(body.length)); + try (OutputStream os = conn.getOutputStream()) { + os.write(body); + os.flush(); + } + + int code = conn.getResponseCode(); + InputStream is = (code >= 200 && code < 300) ? conn.getInputStream() : conn.getErrorStream(); + String respBody = readAll(is); + return "HTTP " + code + " " + respBody; + } finally { + if (conn != null) conn.disconnect(); + } + } + + private static String readAll(InputStream is) throws Exception { + if (is == null) return ""; + StringBuilder sb = new StringBuilder(); + try (BufferedReader r = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + String line; + while ((line = r.readLine()) != null) { + if (sb.length() > 0) sb.append('\n'); + sb.append(line); + } + } + return sb.toString(); + } + + // ===== XLSX 解析(纯 JDK) ===== + + /** 读取 xl/sharedStrings.xml,返回共享字符串表 */ + private static List readSharedStrings(ZipFile zf) throws Exception { + ZipEntry e = zf.getEntry("xl/sharedStrings.xml"); + if (e == null) return Collections.emptyList(); + List out = new ArrayList<>(); + try (InputStream in = zf.getInputStream(e)) { + XMLInputFactory f = XMLInputFactory.newInstance(); + // 禁用 DTD 以避免 XXE + f.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); + f.setProperty(XMLInputFactory.SUPPORT_DTD, false); + XMLStreamReader r = f.createXMLStreamReader(in); + StringBuilder text = new StringBuilder(); + boolean inT = false; + while (r.hasNext()) { + int ev = r.next(); + if (ev == XMLStreamConstants.START_ELEMENT) { + if ("t".equals(r.getLocalName())) { + inT = true; + text.setLength(0); + } + } else if (ev == XMLStreamConstants.CHARACTERS) { + if (inT) text.append(r.getText()); + } else if (ev == XMLStreamConstants.END_ELEMENT) { + String ln = r.getLocalName(); + if ("t".equals(ln)) { + inT = false; + } else if ("si".equals(ln)) { + out.add(text.toString()); + text.setLength(0); + } + } + } + r.close(); + } + return out; + } + + /** 流式读取 xl/worksheets/sheet1.xml,nextRow() 逐行返回 */ + static class SheetReader { + private final XMLStreamReader r; + private final List sst; + private final InputStream stream; + + SheetReader(ZipFile zf, List sst) throws Exception { + ZipEntry e = zf.getEntry("xl/worksheets/sheet1.xml"); + this.stream = zf.getInputStream(e); + this.sst = sst; + XMLInputFactory f = XMLInputFactory.newInstance(); + f.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); + f.setProperty(XMLInputFactory.SUPPORT_DTD, false); + this.r = f.createXMLStreamReader(stream); + } + + List nextRow() throws Exception { + List row = null; + String curType = null; + StringBuilder curVal = new StringBuilder(); + boolean inV = false; + while (r.hasNext()) { + int ev = r.next(); + if (ev == XMLStreamConstants.START_ELEMENT) { + String ln = r.getLocalName(); + if ("row".equals(ln)) { + row = new ArrayList<>(); + } else if ("c".equals(ln) && row != null) { + curType = r.getAttributeValue(null, "t"); + curVal.setLength(0); + } else if ("v".equals(ln) && row != null) { + inV = true; + } else if ("t".equals(ln) && row != null) { + inV = true; // inlineStr 的 + } + } else if (ev == XMLStreamConstants.CHARACTERS) { + if (inV && row != null) curVal.append(r.getText()); + } else if (ev == XMLStreamConstants.END_ELEMENT) { + String ln = r.getLocalName(); + if ("v".equals(ln) || "t".equals(ln)) { + inV = false; + } else if ("c".equals(ln) && row != null) { + String v = curVal.toString(); + String val; + if ("s".equals(curType)) { + int idx = Integer.parseInt(v.trim()); + val = idx < sst.size() ? sst.get(idx) : ""; + } else if ("b".equals(curType)) { + val = "1".equals(v.trim()) ? "true" : "false"; + } else { + // 数值、str、inlineStr、空等,直接取原值 + val = v; + } + row.add(val); + } else if ("row".equals(ln) && row != null) { + return row; + } + } + } + try { r.close(); stream.close(); } catch (Exception ignored) {} + return null; + } + } +}