package space.anyi.amountVerification; import com.alibaba.excel.EasyExcel; import com.alibaba.excel.ExcelWriter; import com.alibaba.excel.read.listener.PageReadListener; import com.alibaba.excel.write.metadata.WriteSheet; import io.github.mymonstercat.Model; import io.github.mymonstercat.ocr.InferenceEngine; import javafx.application.Application; import javafx.application.Platform; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.layout.BorderPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.VBox; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import javafx.stage.Stage; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.text.PDFTextStripper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import space.anyi.amountVerification.model.InputModel; import space.anyi.amountVerification.model.OutputErrorModel; import space.anyi.amountVerification.model.OutputModel; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.math.BigDecimal; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @ProjectName: amount-verification * @FileName: Application * @Author: 杨逸 * @Data:2025/11/27 18:33 * @Description: */ public class App extends Application { public static final Logger log = LoggerFactory.getLogger(App.class); /** * 主窗口 * @see Stage */ private Stage primaryStage; /** * 开始按钮 * @see Button */ private Button startButton; /** * 文件 * @see TextField */ private TextField fileField; /**文件夹 * @see TextField */ private TextField folderField; /** * excel输入数据 * @see List */ private List inputModelList; /** * excel输出数据 * @see List */ private List outputModelList; /** * IMEI码与工单文件映射 * 文件类型可能是PDF或者JPG * @see Map */ private Map workOrderFileMap; /** * 进度条控件 * @see ProgressBar */ private ProgressBar progressBar; /** * 进度标签 * @see Label */ private Label progressLabel; /** * 匹配金额使用的正则表达式对象 * @see Pattern */ private final Pattern pattern; /** * OCR使用的对象 * @see InferenceEngine */ private final InferenceEngine engineOCR; public App() { //配置临时目录,用于存放OCR模型 System.setProperty("java.io.tmpdir", System.getenv("user.dir") + "\\temp"); //初始化OCR使用的对象 this.engineOCR = InferenceEngine.getInstance(Model.ONNX_PPOCR_V4); //初始化匹配金额的正则表达式对象 String regex = "理赔金额合计[^\\d]{1,2}[¥¥#Y半]?(?\\d+\\.\\d{2})"; this.pattern = Pattern.compile(regex); } @Override public void start(Stage primaryStage) throws Exception { this.primaryStage = primaryStage; primaryStage.setTitle("金额验证工具"); primaryStage.setResizable(false); primaryStage.getIcons().add(new Image(getClass().getResourceAsStream("/logo.jpg"))); // 设置窗口关闭事件处理 primaryStage.setOnCloseRequest(event -> { event.consume(); // 阻止默认关闭行为 showCloseConfirmation(primaryStage); }); // 创建主布局 BorderPane mainLayout = new BorderPane(); // 创建菜单栏 MenuBar menuBar = createMenuBar(); mainLayout.setTop(menuBar); // 创建内容区域 VBox contentLayout = new VBox(20); contentLayout.setPadding(new Insets(20)); contentLayout.setAlignment(Pos.CENTER); // 创建网格布局用于表单 GridPane gridPane = new GridPane(); gridPane.setHgap(10); gridPane.setVgap(10); gridPane.setAlignment(Pos.CENTER); // 文件选择组件 Label fileLabel = new Label("选择文件:"); fileField = new TextField(); fileField.setPromptText("请选择文件..."); fileField.setPrefWidth(300); Button fileButton = new Button("浏览"); // 文件夹选择组件 Label folderLabel = new Label("选择文件夹:"); folderField = new TextField(); folderField.setPromptText("请选择文件夹..."); folderField.setPrefWidth(300); Button folderButton = new Button("浏览"); // 进度条组件 progressBar = new ProgressBar(0); progressBar.setPrefWidth(300); progressBar.setVisible(false); // 进度标签 progressLabel = new Label("准备开始..."); progressLabel.setVisible(false); // 开始按钮 startButton = new Button("开始验证"); startButton.setPrefWidth(150); startButton.setPrefHeight(40); // 设置按钮事件 fileButton.setOnAction(e -> chooseFile()); folderButton.setOnAction(e -> chooseFolder()); startButton.setOnAction(e -> startVerification()); // 添加到网格布局 gridPane.add(fileLabel, 0, 0); gridPane.add(fileField, 1, 0); gridPane.add(fileButton, 2, 0); gridPane.add(folderLabel, 0, 1); gridPane.add(folderField, 1, 1); gridPane.add(folderButton, 2, 1); // 添加到内容布局 contentLayout.getChildren().addAll(gridPane, progressLabel, progressBar, startButton); mainLayout.setCenter(contentLayout); // 创建场景并显示 Scene scene = new Scene(mainLayout, 500, 400); primaryStage.setScene(scene); primaryStage.show(); } private MenuBar createMenuBar() { MenuBar menuBar = new MenuBar(); // 创建帮助菜单 Menu helpMenu = new Menu("帮助"); // 创建关于菜单项 MenuItem aboutMenuItem = new MenuItem("关于"); aboutMenuItem.setOnAction(e -> showAboutDialog()); helpMenu.getItems().add(aboutMenuItem); menuBar.getMenus().add(helpMenu); return menuBar; } private void showAboutDialog() { Alert alert = new Alert(Alert.AlertType.INFORMATION); alert.setTitle("关于"); alert.setHeaderText("金额验证工具"); alert.setContentText("作者: 杨逸\n" + "联系方式: huanglve2004@163.com\n\n" + "版本: 1.0.2\n" + "描述: 用于金额验证的桌面应用程序"); alert.showAndWait(); } private void showCloseConfirmation(Stage stage) { Alert alert = new Alert(Alert.AlertType.CONFIRMATION); alert.setTitle("确认关闭"); alert.setHeaderText("您确定要关闭应用程序吗?"); alert.setContentText("如果正在执行验证操作,关闭将中断该过程。"); Optional result = alert.showAndWait(); if (result.isPresent() && result.get() == ButtonType.OK) { stage.close(); } } private void chooseFile() { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("选择文件"); // 添加文件过滤器,只允许选择xlsx文件 FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter( "Excel文件 (*.xlsx)", "*.xlsx"); fileChooser.getExtensionFilters().add(extFilter); File selectedFile = fileChooser.showOpenDialog(null); if (selectedFile != null) { log.info("成功选择文件:{}",selectedFile.getAbsolutePath()); fileField.setText(selectedFile.getAbsolutePath()); } } private void chooseFolder() { DirectoryChooser directoryChooser = new DirectoryChooser(); directoryChooser.setTitle("选择文件夹"); File selectedFolder = directoryChooser.showDialog(null); if (selectedFolder != null) { log.info("成功选择文件夹:{}",selectedFolder.getAbsolutePath()); folderField.setText(selectedFolder.getAbsolutePath()); } } /** * 更新进度条和进度标签 * @param message 进度消息 * @param progress 进度值 (0.0 - 1.0) */ private void updateProgress(String message, double progress) { Platform.runLater(() -> { progressLabel.setText(message); progressBar.setProgress(progress); }); } private void startVerification() { //如果当前时间大于2025-11-28,则关闭程序 //if (System.currentTimeMillis() > 1764341281000L) { // new Alert(Alert.AlertType.ERROR, "软件已过期,请联系作者获取最新版本").showAndWait(); // Platform.exit(); //} startButton.setDisable(true); String filePath = fileField.getText(); String folderPath = folderField.getText(); if (filePath.isEmpty() || folderPath.isEmpty()) { log.warn("未选择文件和文件夹"); Alert alert = new Alert(Alert.AlertType.ERROR); alert.setHeaderText("请选择文件和文件夹"); alert.showAndWait(); return; } //开启一个新的线程处理 Thread thread = new Thread(() -> { try { log.info("开始验证"); // 初始化进度条 progressBar.setProgress(0); progressBar.setVisible(true); progressLabel.setVisible(true); updateProgress("准备开始验证...", 0); inputModelList = new ArrayList<>(); outputModelList = new ArrayList<>(); workOrderFileMap = new HashMap<>(); List errorImeiList = new ArrayList<>(); // 读取excel log.info("开始读取Excel文件"); updateProgress("正在读取Excel文件...", 0.1); readExcel(filePath); log.info("读取Excel文件结束"); updateProgress("Excel文件读取完成,正在扫描文件夹...", 0.25); //读取文件夹 log.info("开始读取文件夹"); readFolder(folderPath); log.info("读取文件夹结束"); updateProgress("文件夹扫描完成,开始解析工单文件...", 0.5); // 解析工单文件 log.info("开始解析工单文件"); handler(errorImeiList); log.info("解析PDF工单结束"); updateProgress("工单文件解析完成,正在生成输出文件...", 0.9); //输出最终文件 log.info("开始输出结果文件"); String outputFileName = filePath.substring(0, filePath.lastIndexOf(".")) + "_" + System.currentTimeMillis() + "_output.xlsx"; outputFileHandler(outputFileName, errorImeiList); updateProgress("验证完成!输出文件: " + outputFileName, 1.0); // 显示完成信息 Platform.runLater(() -> { Alert alert = new Alert(Alert.AlertType.INFORMATION); alert.setTitle("验证完成"); alert.setHeaderText("金额验证已完成"); alert.setContentText("输出文件已生成: " + outputFileName + (errorImeiList.isEmpty() ? "" : "\n\nIMEI处理失败数量: " + errorImeiList.size())); alert.showAndWait(); }); } finally { // 隐藏进度条 progressBar.setVisible(false); progressLabel.setVisible(false); // 恢复按钮状态 startButton.setDisable(false); // 清空输入框 fileField.clear(); folderField.clear(); //资源清理 inputModelList = null; outputModelList = null; workOrderFileMap = null; } }); thread.setDaemon(true); thread.start(); } private void outputFileHandler(String outputFileName,List errorImeiList) { try (ExcelWriter excelWriter = EasyExcel.write(outputFileName).build()) { //正常的sheet WriteSheet writeSheet = EasyExcel.writerSheet("正常").head(OutputModel.class).build(); excelWriter.write(outputModelList, writeSheet); //错误的sheet WriteSheet writeSheet1 = EasyExcel.writerSheet("错误").head(OutputErrorModel.class).build(); excelWriter.write(errorImeiList, writeSheet1); } } private void handler(List errorImeiList) { int totalWorkOrders = inputModelList.size(); log.info("待处理对账工单数:{}",totalWorkOrders); int processedWorkOrders = 0; for (InputModel inputModel : inputModelList) { OutputModel outputModel = new OutputModel(inputModel); String imei = inputModel.getImei().trim(); String workOrderNumber = inputModel.getWorkOrderNumber().trim(); String fileKey = workOrderNumber + "-" + imei; fileKey = fileKey.toUpperCase(Locale.ROOT); log.info("开始对账工单:{}",fileKey); //找到对应工单文件的路径 File workOrderFile = workOrderFileMap.getOrDefault(fileKey, null); if (Objects.isNull(workOrderFile)) { //拼接字符可能是 _ log.warn("未找到{}对应的工单文件",fileKey); log.info("开始对账工单:{}",fileKey); fileKey = workOrderNumber + "_" + imei; fileKey = fileKey.toUpperCase(Locale.ROOT); workOrderFile = workOrderFileMap.getOrDefault(fileKey, null); } if (Objects.isNull(workOrderFile)) { log.warn("未找到{}对应的工单文件",fileKey); errorImeiList.add(new OutputErrorModel(imei,workOrderNumber)); processedWorkOrders++; double progress = 0.5 + (0.4 * processedWorkOrders / totalWorkOrders); updateProgress("解析工单文件: " + processedWorkOrders + "/" + totalWorkOrders + " (工单文件未找到: " + fileKey + ")", progress); continue; } log.info("找到{}对应的工单文件:{}",fileKey,workOrderFile.getName()); //解析工单文件 log.info("开始解析工单文件:{}",workOrderFile.getName()); String text = null; if (workOrderFile.getName().endsWith(".pdf")) { //pdf text = pdfHandler(workOrderFile); }else if (workOrderFile.getName().endsWith(".jpg") || workOrderFile.getName().endsWith(".png") || workOrderFile.getName().endsWith(".jpeg")) { //图片 text = imageHandler(workOrderFile); } //解析pdf if (Objects.isNull(text)) { //没有文本 log.warn("解析IMEI:{}对应的工单文件: {} 失败",imei,workOrderFile.getName()); errorImeiList.add(new OutputErrorModel(imei,workOrderNumber)); }else{ Matcher matcher = pattern.matcher(text); if (matcher.find()) { String money = matcher.group("money"); // 获取命名分组 "money" log.info("IMEI:{},匹配到金额:{}",imei,money); BigDecimal amount1 = new BigDecimal(inputModel.getAmount()); BigDecimal amount2 = new BigDecimal(money); outputModel.setPdfAmount(amount2.doubleValue()); outputModel.setMargin(amount1.subtract(amount2).doubleValue()); } else { log.warn("IMEI:{}未匹配到金额",imei); errorImeiList.add(new OutputErrorModel(imei,workOrderNumber)); } } log.info("解析IMEI:{}对应的工单文件: {} 完成",imei,workOrderFile.getName()); outputModelList.add(outputModel); processedWorkOrders++; double progress = 0.5 + (0.4 * processedWorkOrders / totalWorkOrders); log.info("进度:{}",progress); updateProgress("解析工单文件: " + processedWorkOrders + "/" + totalWorkOrders, progress); } } /** * 图片转文本,通过OCR实现 * @param workOrderFile * @return {@code String } * @description: * @author: 杨逸 * @data:2025/12/09 14:25:00 * @since 1.0.0 */ private String imageHandler(File workOrderFile) { String text = null; //OCR识别 BufferedImage bufferedImage = null; try { //使用RapidOCR进行识别 text = engineOCR.runOcr(workOrderFile.getAbsolutePath()).getStrRes().trim(); //开始识别整张图片中的文字 //bufferedImage = ImageIO.read(workOrderFile); //直接处理二进制数据,避免因为文件扩展名错误导致的处理失败 //text = tesseract.doOCR(bufferedImage); log.debug("OCR解析{}到{}",workOrderFile.getAbsolutePath(),text); } catch (Exception e) { log.warn("OCR解析{}失败,case:{}",workOrderFile.getAbsolutePath(),e.getMessage()); e.printStackTrace(); } finally { if (!Objects.isNull(bufferedImage)) { //释放资源 bufferedImage.flush(); bufferedImage = null; } } return text; } /** * PDF转文本 * @param workOrderFile 文件对象 * @return {@code String } * @description: * @author: 杨逸 * @data:2025/12/09 14:24:17 * @since 1.0.0 */ private String pdfHandler(File workOrderFile) { String text = null; try (PDDocument pdDocument = Loader.loadPDF(workOrderFile)) { PDFTextStripper textStripper = new PDFTextStripper(); //提取文本 text = textStripper.getText(pdDocument); log.debug("PDF解析{}到{}",workOrderFile.getAbsolutePath(),text); }catch (IOException e) { log.warn("PDF解析{}失败,case:{}",workOrderFile.getAbsolutePath(),e.getMessage()); e.printStackTrace(); } return text; } private void readFolder(String folderPath) { File folder = new File(folderPath); File[] files = folder.listFiles(); // 扫描文件夹并建立工单号+IMEI=工单文件映射 int totalFolders = files.length; int processedFolders = 0; for (File file : files) { if (file.isFile()) { continue; } log.info("正在扫描文件夹,查找工单文件: {}", file.getName()); File[] childrenFiles = file.listFiles(); String fileKey = file.getName().replaceAll(" ","").toUpperCase(Locale.ROOT); log.debug("fileKey: {}", fileKey); //拿到文件夹里的工单文件(文件类型可能是PDF或JPG) for (File workOrderFile : childrenFiles) { if (workOrderFile.isDirectory()) { continue; } if (workOrderFile.getName().contains("工单照片") || workOrderFile.getName().contains("工单资料")) { if (workOrderFileMap.containsKey(fileKey)) { log.error("出现重复的工单文件夹: {}", fileKey); //出现重复的imei Platform.runLater(()->{ Alert alert = new Alert(Alert.AlertType.ERROR); alert.setHeaderText("出现重复的工单文件夹"); alert.setContentText("请检查文件夹中是否出现重复的工单文件夹:" + fileKey); alert.showAndWait(); }); return; } log.info("找到工单文件: {}",workOrderFile.getName()); workOrderFileMap.put(fileKey, workOrderFile); break; } } processedFolders++; double progress = 0.25 + (0.25 * processedFolders / totalFolders); updateProgress("正在扫描文件夹: " + processedFolders + "/" + totalFolders, progress); } } private void readExcel(String filePath) { EasyExcel.read(filePath, InputModel.class, new PageReadListener(dataList -> { for (InputModel inputModel : dataList) { log.debug("读取到一行数据:{}",inputModel); inputModelList.add(inputModel); } })).sheet().doRead(); } }