App.java 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. package space.anyi.amountVerification;
  2. import com.alibaba.excel.EasyExcel;
  3. import com.alibaba.excel.ExcelWriter;
  4. import com.alibaba.excel.read.listener.PageReadListener;
  5. import com.alibaba.excel.write.metadata.WriteSheet;
  6. import io.github.mymonstercat.Model;
  7. import io.github.mymonstercat.ocr.InferenceEngine;
  8. import javafx.application.Application;
  9. import javafx.application.Platform;
  10. import javafx.geometry.Insets;
  11. import javafx.geometry.Pos;
  12. import javafx.scene.Scene;
  13. import javafx.scene.control.*;
  14. import javafx.scene.image.Image;
  15. import javafx.scene.layout.BorderPane;
  16. import javafx.scene.layout.GridPane;
  17. import javafx.scene.layout.VBox;
  18. import javafx.stage.DirectoryChooser;
  19. import javafx.stage.FileChooser;
  20. import javafx.stage.Stage;
  21. import org.apache.pdfbox.Loader;
  22. import org.apache.pdfbox.pdmodel.PDDocument;
  23. import org.apache.pdfbox.text.PDFTextStripper;
  24. import org.slf4j.Logger;
  25. import org.slf4j.LoggerFactory;
  26. import space.anyi.amountVerification.model.InputModel;
  27. import space.anyi.amountVerification.model.OutputErrorModel;
  28. import space.anyi.amountVerification.model.OutputModel;
  29. import java.awt.image.BufferedImage;
  30. import java.io.File;
  31. import java.io.IOException;
  32. import java.math.BigDecimal;
  33. import java.util.*;
  34. import java.util.regex.Matcher;
  35. import java.util.regex.Pattern;
  36. /**
  37. * @ProjectName: amount-verification
  38. * @FileName: Application
  39. * @Author: 杨逸
  40. * @Data:2025/11/27 18:33
  41. * @Description:
  42. */
  43. public class App extends Application {
  44. public static final Logger log = LoggerFactory.getLogger(App.class);
  45. /**
  46. * 主窗口
  47. * @see Stage
  48. */
  49. private Stage primaryStage;
  50. /**
  51. * 开始按钮
  52. * @see Button
  53. */
  54. private Button startButton;
  55. /**
  56. * 文件
  57. * @see TextField
  58. */
  59. private TextField fileField;
  60. /**文件夹
  61. * @see TextField
  62. */
  63. private TextField folderField;
  64. /**
  65. * excel输入数据
  66. * @see List<InputModel>
  67. */
  68. private List<InputModel> inputModelList;
  69. /**
  70. * excel输出数据
  71. * @see List<OutputModel>
  72. */
  73. private List<OutputModel> outputModelList;
  74. /**
  75. * IMEI码与工单文件映射
  76. * 文件类型可能是PDF或者JPG
  77. * @see Map<String, File>
  78. */
  79. private Map<String,File> workOrderFileMap;
  80. /**
  81. * 进度条控件
  82. * @see ProgressBar
  83. */
  84. private ProgressBar progressBar;
  85. /**
  86. * 进度标签
  87. * @see Label
  88. */
  89. private Label progressLabel;
  90. /**
  91. * 匹配金额使用的正则表达式对象
  92. * @see Pattern
  93. */
  94. private final Pattern pattern;
  95. /**
  96. * OCR使用的对象
  97. * @see InferenceEngine
  98. */
  99. private final InferenceEngine engineOCR;
  100. public App() {
  101. //配置临时目录,用于存放OCR模型
  102. System.setProperty("java.io.tmpdir", System.getenv("user.dir") + "\\temp");
  103. //初始化OCR使用的对象
  104. this.engineOCR = InferenceEngine.getInstance(Model.ONNX_PPOCR_V4);
  105. //初始化匹配金额的正则表达式对象
  106. String regex = "理赔金额合计[^\\d]{1,2}[¥¥#Y半]?(?<money>\\d+\\.\\d{2})";
  107. this.pattern = Pattern.compile(regex);
  108. }
  109. @Override
  110. public void start(Stage primaryStage) throws Exception {
  111. this.primaryStage = primaryStage;
  112. primaryStage.setTitle("金额验证工具");
  113. primaryStage.setResizable(false);
  114. primaryStage.getIcons().add(new Image(getClass().getResourceAsStream("/logo.jpg")));
  115. // 设置窗口关闭事件处理
  116. primaryStage.setOnCloseRequest(event -> {
  117. event.consume(); // 阻止默认关闭行为
  118. showCloseConfirmation(primaryStage);
  119. });
  120. // 创建主布局
  121. BorderPane mainLayout = new BorderPane();
  122. // 创建菜单栏
  123. MenuBar menuBar = createMenuBar();
  124. mainLayout.setTop(menuBar);
  125. // 创建内容区域
  126. VBox contentLayout = new VBox(20);
  127. contentLayout.setPadding(new Insets(20));
  128. contentLayout.setAlignment(Pos.CENTER);
  129. // 创建网格布局用于表单
  130. GridPane gridPane = new GridPane();
  131. gridPane.setHgap(10);
  132. gridPane.setVgap(10);
  133. gridPane.setAlignment(Pos.CENTER);
  134. // 文件选择组件
  135. Label fileLabel = new Label("选择文件:");
  136. fileField = new TextField();
  137. fileField.setPromptText("请选择文件...");
  138. fileField.setPrefWidth(300);
  139. Button fileButton = new Button("浏览");
  140. // 文件夹选择组件
  141. Label folderLabel = new Label("选择文件夹:");
  142. folderField = new TextField();
  143. folderField.setPromptText("请选择文件夹...");
  144. folderField.setPrefWidth(300);
  145. Button folderButton = new Button("浏览");
  146. // 进度条组件
  147. progressBar = new ProgressBar(0);
  148. progressBar.setPrefWidth(300);
  149. progressBar.setVisible(false);
  150. // 进度标签
  151. progressLabel = new Label("准备开始...");
  152. progressLabel.setVisible(false);
  153. // 开始按钮
  154. startButton = new Button("开始验证");
  155. startButton.setPrefWidth(150);
  156. startButton.setPrefHeight(40);
  157. // 设置按钮事件
  158. fileButton.setOnAction(e -> chooseFile());
  159. folderButton.setOnAction(e -> chooseFolder());
  160. startButton.setOnAction(e -> startVerification());
  161. // 添加到网格布局
  162. gridPane.add(fileLabel, 0, 0);
  163. gridPane.add(fileField, 1, 0);
  164. gridPane.add(fileButton, 2, 0);
  165. gridPane.add(folderLabel, 0, 1);
  166. gridPane.add(folderField, 1, 1);
  167. gridPane.add(folderButton, 2, 1);
  168. // 添加到内容布局
  169. contentLayout.getChildren().addAll(gridPane, progressLabel, progressBar, startButton);
  170. mainLayout.setCenter(contentLayout);
  171. // 创建场景并显示
  172. Scene scene = new Scene(mainLayout, 500, 400);
  173. primaryStage.setScene(scene);
  174. primaryStage.show();
  175. }
  176. private MenuBar createMenuBar() {
  177. MenuBar menuBar = new MenuBar();
  178. // 创建帮助菜单
  179. Menu helpMenu = new Menu("帮助");
  180. // 创建关于菜单项
  181. MenuItem aboutMenuItem = new MenuItem("关于");
  182. aboutMenuItem.setOnAction(e -> showAboutDialog());
  183. helpMenu.getItems().add(aboutMenuItem);
  184. menuBar.getMenus().add(helpMenu);
  185. return menuBar;
  186. }
  187. private void showAboutDialog() {
  188. Alert alert = new Alert(Alert.AlertType.INFORMATION);
  189. alert.setTitle("关于");
  190. alert.setHeaderText("金额验证工具");
  191. alert.setContentText("作者: 杨逸\n" +
  192. "联系方式: huanglve2004@163.com\n\n" +
  193. "版本: 1.0.2\n" +
  194. "描述: 用于金额验证的桌面应用程序");
  195. alert.showAndWait();
  196. }
  197. private void showCloseConfirmation(Stage stage) {
  198. Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
  199. alert.setTitle("确认关闭");
  200. alert.setHeaderText("您确定要关闭应用程序吗?");
  201. alert.setContentText("如果正在执行验证操作,关闭将中断该过程。");
  202. Optional<ButtonType> result = alert.showAndWait();
  203. if (result.isPresent() && result.get() == ButtonType.OK) {
  204. stage.close();
  205. }
  206. }
  207. private void chooseFile() {
  208. FileChooser fileChooser = new FileChooser();
  209. fileChooser.setTitle("选择文件");
  210. // 添加文件过滤器,只允许选择xlsx文件
  211. FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter(
  212. "Excel文件 (*.xlsx)", "*.xlsx");
  213. fileChooser.getExtensionFilters().add(extFilter);
  214. File selectedFile = fileChooser.showOpenDialog(null);
  215. if (selectedFile != null) {
  216. log.info("成功选择文件:{}",selectedFile.getAbsolutePath());
  217. fileField.setText(selectedFile.getAbsolutePath());
  218. }
  219. }
  220. private void chooseFolder() {
  221. DirectoryChooser directoryChooser = new DirectoryChooser();
  222. directoryChooser.setTitle("选择文件夹");
  223. File selectedFolder = directoryChooser.showDialog(null);
  224. if (selectedFolder != null) {
  225. log.info("成功选择文件夹:{}",selectedFolder.getAbsolutePath());
  226. folderField.setText(selectedFolder.getAbsolutePath());
  227. }
  228. }
  229. /**
  230. * 更新进度条和进度标签
  231. * @param message 进度消息
  232. * @param progress 进度值 (0.0 - 1.0)
  233. */
  234. private void updateProgress(String message, double progress) {
  235. Platform.runLater(() -> {
  236. progressLabel.setText(message);
  237. progressBar.setProgress(progress);
  238. });
  239. }
  240. private void startVerification() {
  241. //如果当前时间大于2025-11-28,则关闭程序
  242. //if (System.currentTimeMillis() > 1764341281000L) {
  243. // new Alert(Alert.AlertType.ERROR, "软件已过期,请联系作者获取最新版本").showAndWait();
  244. // Platform.exit();
  245. //}
  246. startButton.setDisable(true);
  247. String filePath = fileField.getText();
  248. String folderPath = folderField.getText();
  249. if (filePath.isEmpty() || folderPath.isEmpty()) {
  250. log.warn("未选择文件和文件夹");
  251. Alert alert = new Alert(Alert.AlertType.ERROR);
  252. alert.setHeaderText("请选择文件和文件夹");
  253. alert.showAndWait();
  254. return;
  255. }
  256. //开启一个新的线程处理
  257. Thread thread = new Thread(() -> {
  258. try {
  259. log.info("开始验证");
  260. // 初始化进度条
  261. progressBar.setProgress(0);
  262. progressBar.setVisible(true);
  263. progressLabel.setVisible(true);
  264. updateProgress("准备开始验证...", 0);
  265. inputModelList = new ArrayList<>();
  266. outputModelList = new ArrayList<>();
  267. workOrderFileMap = new HashMap<>();
  268. List<OutputErrorModel> errorImeiList = new ArrayList<>();
  269. // 读取excel
  270. log.info("开始读取Excel文件");
  271. updateProgress("正在读取Excel文件...", 0.1);
  272. readExcel(filePath);
  273. log.info("读取Excel文件结束");
  274. updateProgress("Excel文件读取完成,正在扫描文件夹...", 0.25);
  275. //读取文件夹
  276. log.info("开始读取文件夹");
  277. readFolder(folderPath);
  278. log.info("读取文件夹结束");
  279. updateProgress("文件夹扫描完成,开始解析工单文件...", 0.5);
  280. // 解析工单文件
  281. log.info("开始解析工单文件");
  282. handler(errorImeiList);
  283. log.info("解析PDF工单结束");
  284. updateProgress("工单文件解析完成,正在生成输出文件...", 0.9);
  285. //输出最终文件
  286. log.info("开始输出结果文件");
  287. String outputFileName = filePath.substring(0, filePath.lastIndexOf(".")) + "_" + System.currentTimeMillis() + "_output.xlsx";
  288. outputFileHandler(outputFileName, errorImeiList);
  289. updateProgress("验证完成!输出文件: " + outputFileName, 1.0);
  290. // 显示完成信息
  291. Platform.runLater(() -> {
  292. Alert alert = new Alert(Alert.AlertType.INFORMATION);
  293. alert.setTitle("验证完成");
  294. alert.setHeaderText("金额验证已完成");
  295. alert.setContentText("输出文件已生成: " + outputFileName +
  296. (errorImeiList.isEmpty() ? "" : "\n\nIMEI处理失败数量: " + errorImeiList.size()));
  297. alert.showAndWait();
  298. });
  299. } finally {
  300. // 隐藏进度条
  301. progressBar.setVisible(false);
  302. progressLabel.setVisible(false);
  303. // 恢复按钮状态
  304. startButton.setDisable(false);
  305. // 清空输入框
  306. fileField.clear();
  307. folderField.clear();
  308. //资源清理
  309. inputModelList = null;
  310. outputModelList = null;
  311. workOrderFileMap = null;
  312. }
  313. });
  314. thread.setDaemon(true);
  315. thread.start();
  316. }
  317. private void outputFileHandler(String outputFileName,List<OutputErrorModel> errorImeiList) {
  318. try (ExcelWriter excelWriter = EasyExcel.write(outputFileName).build()) {
  319. //正常的sheet
  320. WriteSheet writeSheet = EasyExcel.writerSheet("正常").head(OutputModel.class).build();
  321. excelWriter.write(outputModelList, writeSheet);
  322. //错误的sheet
  323. WriteSheet writeSheet1 = EasyExcel.writerSheet("错误").head(OutputErrorModel.class).build();
  324. excelWriter.write(errorImeiList, writeSheet1);
  325. }
  326. }
  327. private void handler(List<OutputErrorModel> errorImeiList) {
  328. int totalWorkOrders = inputModelList.size();
  329. log.info("待处理对账工单数:{}",totalWorkOrders);
  330. int processedWorkOrders = 0;
  331. for (InputModel inputModel : inputModelList) {
  332. OutputModel outputModel = new OutputModel(inputModel);
  333. String imei = inputModel.getImei().trim();
  334. String workOrderNumber = inputModel.getWorkOrderNumber().trim();
  335. String fileKey = workOrderNumber + "-" + imei;
  336. fileKey = fileKey.toUpperCase(Locale.ROOT);
  337. log.info("开始对账工单:{}",fileKey);
  338. //找到对应工单文件的路径
  339. File workOrderFile = workOrderFileMap.getOrDefault(fileKey, null);
  340. if (Objects.isNull(workOrderFile)) {
  341. //拼接字符可能是 _
  342. log.warn("未找到{}对应的工单文件",fileKey);
  343. log.info("开始对账工单:{}",fileKey);
  344. fileKey = workOrderNumber + "_" + imei;
  345. fileKey = fileKey.toUpperCase(Locale.ROOT);
  346. workOrderFile = workOrderFileMap.getOrDefault(fileKey, null);
  347. }
  348. if (Objects.isNull(workOrderFile)) {
  349. log.warn("未找到{}对应的工单文件",fileKey);
  350. errorImeiList.add(new OutputErrorModel(imei,workOrderNumber));
  351. processedWorkOrders++;
  352. double progress = 0.5 + (0.4 * processedWorkOrders / totalWorkOrders);
  353. updateProgress("解析工单文件: " + processedWorkOrders + "/" + totalWorkOrders + " (工单文件未找到: " + fileKey + ")", progress);
  354. continue;
  355. }
  356. log.info("找到{}对应的工单文件:{}",fileKey,workOrderFile.getName());
  357. //解析工单文件
  358. log.info("开始解析工单文件:{}",workOrderFile.getName());
  359. String text = null;
  360. if (workOrderFile.getName().endsWith(".pdf")) {
  361. //pdf
  362. text = pdfHandler(workOrderFile);
  363. }else if (workOrderFile.getName().endsWith(".jpg") || workOrderFile.getName().endsWith(".png") || workOrderFile.getName().endsWith(".jpeg")) {
  364. //图片
  365. text = imageHandler(workOrderFile);
  366. }
  367. //解析pdf
  368. if (Objects.isNull(text)) {
  369. //没有文本
  370. log.warn("解析IMEI:{}对应的工单文件: {} 失败",imei,workOrderFile.getName());
  371. errorImeiList.add(new OutputErrorModel(imei,workOrderNumber));
  372. }else{
  373. Matcher matcher = pattern.matcher(text);
  374. if (matcher.find()) {
  375. String money = matcher.group("money"); // 获取命名分组 "money"
  376. log.info("IMEI:{},匹配到金额:{}",imei,money);
  377. BigDecimal amount1 = new BigDecimal(inputModel.getAmount());
  378. BigDecimal amount2 = new BigDecimal(money);
  379. outputModel.setPdfAmount(amount2.doubleValue());
  380. outputModel.setMargin(amount1.subtract(amount2).doubleValue());
  381. } else {
  382. log.warn("IMEI:{}未匹配到金额",imei);
  383. errorImeiList.add(new OutputErrorModel(imei,workOrderNumber));
  384. }
  385. }
  386. log.info("解析IMEI:{}对应的工单文件: {} 完成",imei,workOrderFile.getName());
  387. outputModelList.add(outputModel);
  388. processedWorkOrders++;
  389. double progress = 0.5 + (0.4 * processedWorkOrders / totalWorkOrders);
  390. log.info("进度:{}",progress);
  391. updateProgress("解析工单文件: " + processedWorkOrders + "/" + totalWorkOrders, progress);
  392. }
  393. }
  394. /**
  395. * 图片转文本,通过OCR实现
  396. * @param workOrderFile
  397. * @return {@code String }
  398. * @description:
  399. * @author: 杨逸
  400. * @data:2025/12/09 14:25:00
  401. * @since 1.0.0
  402. */
  403. private String imageHandler(File workOrderFile) {
  404. String text = null;
  405. //OCR识别
  406. BufferedImage bufferedImage = null;
  407. try {
  408. //使用RapidOCR进行识别
  409. text = engineOCR.runOcr(workOrderFile.getAbsolutePath()).getStrRes().trim();
  410. //开始识别整张图片中的文字
  411. //bufferedImage = ImageIO.read(workOrderFile);
  412. //直接处理二进制数据,避免因为文件扩展名错误导致的处理失败
  413. //text = tesseract.doOCR(bufferedImage);
  414. log.debug("OCR解析{}到{}",workOrderFile.getAbsolutePath(),text);
  415. } catch (Exception e) {
  416. log.warn("OCR解析{}失败,case:{}",workOrderFile.getAbsolutePath(),e.getMessage());
  417. e.printStackTrace();
  418. } finally {
  419. if (!Objects.isNull(bufferedImage)) {
  420. //释放资源
  421. bufferedImage.flush();
  422. bufferedImage = null;
  423. }
  424. }
  425. return text;
  426. }
  427. /**
  428. * PDF转文本
  429. * @param workOrderFile 文件对象
  430. * @return {@code String }
  431. * @description:
  432. * @author: 杨逸
  433. * @data:2025/12/09 14:24:17
  434. * @since 1.0.0
  435. */
  436. private String pdfHandler(File workOrderFile) {
  437. String text = null;
  438. try (PDDocument pdDocument = Loader.loadPDF(workOrderFile)) {
  439. PDFTextStripper textStripper = new PDFTextStripper();
  440. //提取文本
  441. text = textStripper.getText(pdDocument);
  442. log.debug("PDF解析{}到{}",workOrderFile.getAbsolutePath(),text);
  443. }catch (IOException e) {
  444. log.warn("PDF解析{}失败,case:{}",workOrderFile.getAbsolutePath(),e.getMessage());
  445. e.printStackTrace();
  446. }
  447. return text;
  448. }
  449. private void readFolder(String folderPath) {
  450. File folder = new File(folderPath);
  451. File[] files = folder.listFiles();
  452. // 扫描文件夹并建立工单号+IMEI=工单文件映射
  453. int totalFolders = files.length;
  454. int processedFolders = 0;
  455. for (File file : files) {
  456. if (file.isFile()) {
  457. continue;
  458. }
  459. log.info("正在扫描文件夹,查找工单文件: {}", file.getName());
  460. File[] childrenFiles = file.listFiles();
  461. String fileKey = file.getName().replaceAll(" ","").toUpperCase(Locale.ROOT);
  462. log.debug("fileKey: {}", fileKey);
  463. //拿到文件夹里的工单文件(文件类型可能是PDF或JPG)
  464. for (File workOrderFile : childrenFiles) {
  465. if (workOrderFile.isDirectory()) {
  466. continue;
  467. }
  468. if (workOrderFile.getName().contains("工单照片") || workOrderFile.getName().contains("工单资料")) {
  469. if (workOrderFileMap.containsKey(fileKey)) {
  470. log.error("出现重复的工单文件夹: {}", fileKey);
  471. //出现重复的imei
  472. Platform.runLater(()->{
  473. Alert alert = new Alert(Alert.AlertType.ERROR);
  474. alert.setHeaderText("出现重复的工单文件夹");
  475. alert.setContentText("请检查文件夹中是否出现重复的工单文件夹:" + fileKey);
  476. alert.showAndWait();
  477. });
  478. return;
  479. }
  480. log.info("找到工单文件: {}",workOrderFile.getName());
  481. workOrderFileMap.put(fileKey, workOrderFile);
  482. break;
  483. }
  484. }
  485. processedFolders++;
  486. double progress = 0.25 + (0.25 * processedFolders / totalFolders);
  487. updateProgress("正在扫描文件夹: " + processedFolders + "/" + totalFolders, progress);
  488. }
  489. }
  490. private void readExcel(String filePath) {
  491. EasyExcel.read(filePath, InputModel.class, new PageReadListener<InputModel>(dataList -> {
  492. for (InputModel inputModel : dataList) {
  493. log.debug("读取到一行数据:{}",inputModel);
  494. inputModelList.add(inputModel);
  495. }
  496. })).sheet().doRead();
  497. }
  498. }