| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548 |
- 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<InputModel>
- */
- private List<InputModel> inputModelList;
- /**
- * excel输出数据
- * @see List<OutputModel>
- */
- private List<OutputModel> outputModelList;
- /**
- * IMEI码与工单文件映射
- * 文件类型可能是PDF或者JPG
- * @see Map<String, File>
- */
- private Map<String,File> 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半]?(?<money>\\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<ButtonType> 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<OutputErrorModel> 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<OutputErrorModel> 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<OutputErrorModel> 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<InputModel>(dataList -> {
- for (InputModel inputModel : dataList) {
- log.debug("读取到一行数据:{}",inputModel);
- inputModelList.add(inputModel);
- }
- })).sheet().doRead();
- }
- }
|