diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/config/FileConfig.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/config/FileConfig.java index c003a2e5..4a1eadda 100644 --- a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/config/FileConfig.java +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/config/FileConfig.java @@ -19,14 +19,14 @@ public class FileConfig { private String staticPath; public String getCommonPath() { - return PathUtils.joinFilePath(commonDir, commonDir); + return PathUtils.joinFilePath(baseDir, commonDir); } public String getContainerPath() { - return PathUtils.joinFilePath(commonDir, containerDir); + return PathUtils.joinFilePath(baseDir, containerDir); } public String getTempPath() { - return PathUtils.joinFilePath(commonDir, tempDir); + return PathUtils.joinFilePath(baseDir, tempDir); } } diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/config/WebMvcConfig.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/config/WebMvcConfig.java index be598ebd..8b0e3bd2 100644 --- a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/config/WebMvcConfig.java +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/config/WebMvcConfig.java @@ -50,6 +50,6 @@ public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(fileConfig.getStaticPath() + "/**") - .addResourceLocations("file:" + fileConfig.getUploadDir() + "/"); + .addResourceLocations("file:" + fileConfig.getBaseDir() + "/"); } } \ No newline at end of file diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/FileController.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/FileController.java index f6f483d5..f2909ac6 100644 --- a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/FileController.java +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/FileController.java @@ -14,10 +14,7 @@ import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.io.File; import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; @RestController @RequestMapping("/file") @@ -25,8 +22,6 @@ public class FileController { @Resource private DbUtils dbUtils; @Resource - private FileUploadUtils fileUploadUtils; - @Resource private FileConfig fileConfig; /** @@ -34,9 +29,9 @@ public class FileController { */ @PostMapping("/uploadTemp") public Result> uploadTemp( - @RequestParam("file") MultipartFile file, - @RequestParam(value = "fileName", required = false) String fileName) - throws Exception{ + @RequestParam("file") MultipartFile file, + @RequestParam(value = "fileName", required = false) String fileName) + throws Exception { // 1. 校验 FileUtils.checkFile(file); @@ -46,6 +41,7 @@ public class FileController { // 3. 物理路径 String basePath = fileConfig.getTempPath(); // D:/data/file/temp/ + System.out.println(basePath); // 4. 保存 FileUtils.saveFile(file, basePath, uuidName); @@ -69,10 +65,7 @@ public class FileController { @Transactional(rollbackFor = Exception.class) public Result> uploadCommon( @RequestParam("file") MultipartFile file, - @RequestParam("moduleId") String moduleId, - @RequestParam("father") Long father, - @RequestParam("fileName") String fileName, - @RequestParam(value = "mxXh", required = false) Integer mxXh + @RequestParam("fileName") String fileName ) throws Exception { // 1. 校验 FileUtils.checkFile(file); @@ -97,22 +90,6 @@ public class FileController { uuidName ); - // 保存到数据库 - String fileSuffix = FileUtils.getSuffix(fileName); - long fileSize = FileUtils.getFileSize(file); - - Map insertMap = new HashMap<>(); - insertMap.put("subid", UniqueId.nextId()); - insertMap.put("father", father); - insertMap.put("mx_moduleid", moduleId); - insertMap.put("mx_xh", Optional.ofNullable(mxXh).orElse(0)); - insertMap.put("mx_filename", fileName); - insertMap.put("mx_filesize", fileSize); - insertMap.put("mx_fileext", fileSuffix); - insertMap.put("mx_mapfilename", relativePath); - - dbUtils.insert("bf_file", "subid", insertMap); - // 7. URL String url = PathUtils.joinUrl( fileConfig.getServerUrl(), @@ -129,127 +106,192 @@ public class FileController { return Result.ok(result); } +// @PostMapping("/uploadContainer") +// @Transactional(rollbackFor = Exception.class) +// public Result> uploadContainer( +// @RequestParam("file") MultipartFile file, +// @RequestParam("containerNo") String containerNo, +// @RequestParam("fileName") String fileName, +// @RequestParam("jobId") String jobId +// ) throws Exception { +// // 1. 校验 +// FileUtils.checkFile(file); +// +// // 2. 日期路径 +// String datePath = TimeUtils.todayPath(); // 2026/03/30/ +// +// // 3. 根路径 +// String basePath = fileConfig.getContainerPath(); +// +// // 4. 构建目录:日期 / 箱号 / jobId +// String fullDirPath = PathUtils.joinFilePath( +// basePath, +// datePath, +// containerNo, +// jobId +// ); +// +// // 5. 创建目录(幂等) +// File dir = new File(fullDirPath); +// if (!dir.exists()) { +// dir.mkdirs(); +// } +// +// // 6. 文件后缀 +// String suffix = FileUtils.getSuffix(fileName); +// +// long nextId = UniqueId.nextId(); +// +// // 7. 使用雪花ID作为文件名(核心优化) +// String newFileName = nextId + suffix; +// +// // 8. 保存文件 +// FileUtils.saveFile(file, fullDirPath, newFileName); +// +// // 9. 相对路径 +// String relativePath = PathUtils.joinFilePath( +// fileConfig.getContainerDir(), +// datePath, +// containerNo, +// jobId, +// newFileName +// ); +// +// // 10. URL +// String url = PathUtils.joinUrl( +// fileConfig.getServerUrl(), +// fileConfig.getStaticPath(), +// relativePath +// ); +// +// // 11. 返回 +// Map result = new HashMap<>(); +// result.put("url", url); +// result.put("path", relativePath); +// result.put("uid", nextId); +// result.put("fileName", newFileName); +// +// return Result.ok(result); +// } + @PostMapping("/uploadContainer") @Transactional(rollbackFor = Exception.class) public Result> uploadContainer( @RequestParam("file") MultipartFile file, - @RequestParam("containerNo") String containerNo + @RequestParam("fileName") String fileName ) throws Exception { // 1. 校验 FileUtils.checkFile(file); // 2. 日期路径 - String datePath = TimeUtils.todayPath(); // 2026/03/29/ + String datePath = TimeUtils.todayPath(); // 2026/03/30/ // 3. 根路径 String basePath = fileConfig.getContainerPath(); - String dayPath = PathUtils.joinFilePath(basePath, datePath); - String relativePath; - String fileName; - String containerDirName; + // 4. 雪花 ID(唯一) + long nextId = UniqueId.nextId(); - // 🔥 4. 并发锁(同一天 + container) - String lockKey = (datePath + containerNo).intern(); + // 5. 分桶(0-ff) + String bucket = String.format("%02x", nextId & 0xff); - synchronized (lockKey) { + // 6. 构建目录:日期 + 分桶 + String fullDirPath = PathUtils.joinFilePath( + basePath, + datePath, + bucket + ); - // 4.1 获取 container 目录(防重复) - containerDirName = getAvailableContainerDir(dayPath, containerNo); - - String fullDirPath = PathUtils.joinFilePath(dayPath, containerDirName); - - // 4.2 获取下一个序号(不补位) - int index = getNextIndex(fullDirPath); - - // 4.3 后缀 - String suffix = FileUtils.getSuffix(file.getOriginalFilename()); - - // 4.4 文件名(核心规则) - fileName = containerNo + "_" + String.format("%02d", index) + suffix; - - // 4.5 保存 - FileUtils.saveFile(file, fullDirPath, fileName); - - // 4.6 相对路径 - relativePath = PathUtils.joinFilePath( - fileConfig.getContainerDir(), - datePath, - containerDirName, - fileName - ); + // 7. 创建目录(幂等) + File dir = new File(fullDirPath); + if (!dir.exists()) { + dir.mkdirs(); } - // 5. URL + // 8. 文件后缀 + String suffix = FileUtils.getSuffix(fileName); + + String newFileName = nextId + suffix; + + // 9. 保存文件 + FileUtils.saveFile(file, fullDirPath, newFileName); + + // 10. 相对路径 + String relativePath = PathUtils.joinFilePath( + fileConfig.getContainerDir(), + datePath, + bucket, + newFileName + ); + + // 11. URL String url = PathUtils.joinUrl( fileConfig.getServerUrl(), fileConfig.getStaticPath(), relativePath ); - // 6. 返回 + // 12. 返回 Map result = new HashMap<>(); result.put("url", url); result.put("path", relativePath); - result.put("fileName", fileName); - result.put("containerDir", containerDirName); + result.put("uid", nextId); + result.put("fileName", newFileName); + result.put("bucket", bucket); return Result.ok(result); } - public static int getNextIndex(String dirPath) { - File dir = new File(dirPath); + @PostMapping("/delete") + @Transactional(rollbackFor = Exception.class) + public Result deleteFile(@RequestBody Map params) { + try { + // 1. 参数校验 + ValidationUtils.checkRequiredFields(params, "subid"); - if (!dir.exists()) { - return 1; - } + Long subid = Long.valueOf(params.get("subid").toString()); - File[] files = dir.listFiles(); - if (files == null || files.length == 0) { - return 1; - } + // 2. 查询文件记录 + List> maps = dbUtils.loadData( + "SELECT * FROM bf_file WHERE subid = ?", subid + ); - int max = 0; - - for (File f : files) { - String name = f.getName(); - - // 解析 xxx_01.jpg - int underscore = name.lastIndexOf("_"); - int dot = name.lastIndexOf("."); - - if (underscore > -1 && dot > underscore) { - try { - int num = Integer.parseInt(name.substring(underscore + 1, dot)); - max = Math.max(max, num); - } catch (Exception ignored) {} + if (maps.isEmpty()) { + return Result.ok(); } - } - return max + 1; - } + Map objectMap = maps.get(0); - public static String getAvailableContainerDir(String basePath, String containerNo) { - File baseDir = new File(basePath); + // 3. 获取文件路径 + String mxMapfilename = objectMap.get("mx_mapfilename") == null + ? "" + : objectMap.get("mx_mapfilename").toString(); - if (!baseDir.exists()) { - baseDir.mkdirs(); - } - - File dir = new File(baseDir, containerNo); - if (!dir.exists()) { - return containerNo; - } - - int i = 1; - while (true) { - String newName = containerNo + "_" + String.format("%02d", i); - File newDir = new File(baseDir, newName); - if (!newDir.exists()) { - return newName; + if (StringUtils.isBlank(mxMapfilename)) { + return Result.fail("文件路径为空"); } - i++; + + // 4. 拼接真实路径 + String filePath = PathUtils.joinFilePath( + fileConfig.getBaseDir(), + mxMapfilename + ); + + // 5. 删除本地文件 + boolean deleted = FileUtils.deleteFileByPath(filePath); + if (!deleted) { + System.out.println("文件删除失败或不存在: " + filePath); + } + + // 6. 删除数据库 + dbUtils.deleteById("bf_file", "subid", subid); + + return Result.ok(); + + } catch (Exception e) { + e.printStackTrace(); + return Result.fail("删除失败: " + e.getMessage()); } } } diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/OcrController.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/OcrController.java index 6a1caf7d..c30644e5 100644 --- a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/OcrController.java +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/OcrController.java @@ -68,6 +68,7 @@ public class OcrController { return Result.ok(result); } catch (Exception e){ + e.printStackTrace(); return Result.fail("OCR 识别失败,请联系管理员"); } } diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/OssController.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/OssController.java index f0c91fa6..e7bacf74 100644 --- a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/OssController.java +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/controller/OssController.java @@ -41,67 +41,78 @@ public class OssController { ValidationUtils.checkRequiredFields(params, "father", "mx_xh", "mx_filename", "mx_filesize", "mx_mapfilename", "mx_moduleid"); String userId = UserContext.get().getUserId(); + Object subIdObj = params.get("subid"); - long nextId = UniqueId.nextId(); + Long subid = null; - params.put("subid", nextId); + if (subIdObj != null) { + subid = Long.valueOf(subIdObj.toString()); + } + +// 如果 subid 为空,生成新ID + if (subid == null) { + long nextId = UniqueId.nextId(); + subid = nextId; + } + + params.put("subid", subid); params.put("mx_inputuser_id",userId); params.put("mx_inputdatetime", TimeUtils.getDateTime()); dbUtils.insert("bf_file","subid",params); - return Result.ok(nextId); + return Result.ok(subid); } - @PostMapping("/delete") - @Transactional(rollbackFor = Exception.class) - public Result deleteFile(@RequestBody Map params) { - ValidationUtils.checkRequiredFields(params, "subid", "tableName", "bucketName"); - - String tableName = params.get("tableName").toString(); - String bucketName = params.get("bucketName").toString(); - String id = params.get("subid").toString(); - - // 查询数据 - List> maps = dbUtils.loadData("SELECT * FROM " + tableName + " WHERE subid = ?", id); - - // 如果没有数据,直接返回删除成功 - if (ObjectUtils.isEmpty(maps)) { - return Result.ok("删除成功"); - } - - String mxMapfilename = maps.get(0).get("mx_mapfilename").toString(); - String orgId = UserContext.get().getOrgId(); - int index = mxMapfilename.indexOf(orgId + "/"); - - // 如果没找到,就转换orgId为大写 - if (index < 0) { - orgId = orgId.toUpperCase(); - index = mxMapfilename.indexOf(orgId + "/"); - } - - String objectName = mxMapfilename.substring(index); - - // 删除记录 - Map deleteParams = new HashMap<>(); - deleteParams.put("subid", id); - int deleteCount = dbUtils.delete(tableName, deleteParams); - - // 如果删除成功,发送删除请求 - if (deleteCount > 0) { - if(StringUtils.isBlank(objectName)){ - return Result.fail("删除失败,文件格式有误"); - } - String requestUrl = String.format("http://g3oss.g3soft.cn:8082/delete?bucketName=%s&objectName=%s", bucketName, objectName); - try { - HttpUtils.get(requestUrl); - } catch (Exception e) { - throw new RuntimeException("删除文件失败",e); - } - } - - return Result.ok(); - } +// @PostMapping("/delete") +// @Transactional(rollbackFor = Exception.class) +// public Result deleteFile(@RequestBody Map params) { +// ValidationUtils.checkRequiredFields(params, "subid", "tableName", "bucketName"); +// +// String tableName = params.get("tableName").toString(); +// String bucketName = params.get("bucketName").toString(); +// String id = params.get("subid").toString(); +// +// // 查询数据 +// List> maps = dbUtils.loadData("SELECT * FROM " + tableName + " WHERE subid = ?", id); +// +// // 如果没有数据,直接返回删除成功 +// if (ObjectUtils.isEmpty(maps)) { +// return Result.ok("删除成功"); +// } +// +// String mxMapfilename = maps.get(0).get("mx_mapfilename").toString(); +// String orgId = UserContext.get().getOrgId(); +// int index = mxMapfilename.indexOf(orgId + "/"); +// +// // 如果没找到,就转换orgId为大写 +// if (index < 0) { +// orgId = orgId.toUpperCase(); +// index = mxMapfilename.indexOf(orgId + "/"); +// } +// +// String objectName = mxMapfilename.substring(index); +// +// // 删除记录 +// Map deleteParams = new HashMap<>(); +// deleteParams.put("subid", id); +// int deleteCount = dbUtils.delete(tableName, deleteParams); +// +// // 如果删除成功,发送删除请求 +// if (deleteCount > 0) { +// if(StringUtils.isBlank(objectName)){ +// return Result.fail("删除失败,文件格式有误"); +// } +// String requestUrl = String.format("http://g3oss.g3soft.cn:8082/delete?bucketName=%s&objectName=%s", bucketName, objectName); +// try { +// HttpUtils.get(requestUrl); +// } catch (Exception e) { +// throw new RuntimeException("删除文件失败",e); +// } +// } +// +// return Result.ok(); +// } @PostMapping("/deleteAliyunFile") @Transactional(rollbackFor = Exception.class) diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/FileContainerUtils.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/FileContainerUtils.java deleted file mode 100644 index 811f0c0e..00000000 --- a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/FileContainerUtils.java +++ /dev/null @@ -1,99 +0,0 @@ -package cn.g3soft.utils; - -import cn.g3soft.config.FileConfig; -import org.springframework.stereotype.Component; -import org.springframework.web.multipart.MultipartFile; - -import javax.annotation.Resource; -import java.io.File; -import java.io.IOException; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; - -/** - * 通用文件上传工具类 - * 规则: - * - 文件名保持原始或指定 - * - 同一天同箱号重复 -> 自动生成 _1/_2/... 文件夹 - * - 使用 FileUtils 拼接路径 - */ -@Component -public class FileContainerUtils { - - @Resource - private FileConfig fileConfig; - - /** - * 单文件上传 - */ - public String upload(MultipartFile file, String containerNo, String fileName) throws IOException { - if (file == null || file.isEmpty()) { - throw new RuntimeException("文件不能为空"); - } - - // 1. 日期路径 yyyy/MM/dd - String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); - - // 2. 基础目录:上传目录 + 日期 - String basePath = PathUtils.joinFilePath(fileConfig.getContainerPath(), datePath); - - // 3. 计算最终目录(箱号 + 重复批次) - File finalDir = resolveContainerDir(basePath, containerNo); - - if (!finalDir.exists()) { - finalDir.mkdirs(); - } - - // 4. 最终文件 - File dest = new File(finalDir, fileName); - file.transferTo(dest); - - // 5. 相对路径,用于生成URL - String relativePath = PathUtils.joinFilePath( - datePath, - finalDir.getName(), - fileName - ); - - return fileConfig.buildFileUrl(relativePath); - } - - /** - * 多文件上传 - */ - public List uploadBatch(MultipartFile[] files, String containerNo, String[] fileNames) throws IOException { - if (files == null || files.length == 0) { - return new ArrayList<>(); - } - List urls = new ArrayList<>(); - for (int i = 0; i < files.length; i++) { - String name = (fileNames != null && i < fileNames.length) ? fileNames[i] : files[i].getOriginalFilename(); - urls.add(upload(files[i], containerNo, name)); - } - return urls; - } - - /** - * 解析容器目录 - * - 如果同一天已存在该箱号目录 -> 自动加 _1/_2/... 避免覆盖 - */ - private File resolveContainerDir(String basePath, String containerNo) { - File containerDir = new File(PathUtils.joinFilePath(basePath, containerNo)); - - if (!containerDir.exists()) { - return containerDir; - } - - // 已存在 -> 找下一个可用序号 - int i = 1; - while (true) { - File dir = new File(PathUtils.joinFilePath(basePath, containerNo + "_" + i)); - if (!dir.exists()) { - return dir; - } - i++; - } - } -} \ No newline at end of file diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/FileUploadUtils.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/FileUploadUtils.java deleted file mode 100644 index c51b2182..00000000 --- a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/FileUploadUtils.java +++ /dev/null @@ -1,87 +0,0 @@ -package cn.g3soft.utils; - -import cn.g3soft.config.FileConfig; -import org.springframework.stereotype.Component; -import org.springframework.web.multipart.MultipartFile; - -import javax.annotation.Resource; -import java.io.File; -import java.io.IOException; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.UUID; - -@Component -public class FileUploadUtils { - - @Resource - private FileConfig fileConfig; - - /** - * 上传文件到临时路径 - * @param file 上传的文件 - * @param fileName 如果前端传递了文件名,使用传递的文件名 - * @return 临时文件的URL - */ - public String uploadTemp(MultipartFile file, String fileName) throws IOException { - // 1. 生成最终文件名 - String finalFileName = generateTempFileName(file, fileName); - - // 2. 临时目录路径 - File tempDir = new File(fileConfig.getTempPath()); - - // 如果临时目录不存在,创建目录 - if (!tempDir.exists()) { - tempDir.mkdirs(); - } - - // 3. 保存文件到临时目录 - File tempFile = new File(tempDir, finalFileName); - file.transferTo(tempFile); - - // 4. 返回临时文件的 URL(供 OCR 使用) - return fileConfig.buildTempUrl(finalFileName); - } - - /** - * 根据文件名生成临时文件名(带时间戳 + UUID + 文件后缀) - * @param file 上传的文件 - * @param fileName 传递的文件名(可选) - * @return 生成的最终文件名 - */ - private String generateTempFileName(MultipartFile file, String fileName) { - // 优先使用前端传递的 fileName - String originalFilename = (fileName != null && !fileName.isEmpty()) - ? fileName - : file.getOriginalFilename(); - - // 如果没有获取到文件名,设置一个默认文件名 - if (originalFilename == null) { - originalFilename = "file"; - } - - // 获取文件后缀 - String extension = ""; - if (originalFilename.contains(".")) { - extension = originalFilename.substring(originalFilename.lastIndexOf(".")); - } - - // 生成带时间戳和 UUID 的文件名 - return LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + "_" - + UUID.randomUUID().toString().replace("-", "") + extension; - } - - /** - * 删除临时文件(成功识别后删除) - * @param tempFileName 临时文件的文件名 - */ - public void deleteTempFile(String tempFileName) { - String filePath = PathUtils.joinFilePath(fileConfig.getBaseDir(), fileConfig.getTempDir(), tempFileName); - - File tempFile = new File(tempFileName); - if (tempFile.exists()) { - tempFile.delete(); - } - } - -} \ No newline at end of file diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/FileUtils.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/FileUtils.java index 217350b9..688a728a 100644 --- a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/FileUtils.java +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/FileUtils.java @@ -114,4 +114,63 @@ public class FileUtils { throw new RuntimeException("上传文件不能为空"); } } + + + public static boolean deleteFileByPath(String fullPath) { + if (fullPath == null || fullPath.trim().isEmpty()) { + throw new RuntimeException("路径不能为空"); + } + + File file = new File(fullPath); + + // 防止误删 + if (!file.exists()) { + return false; + } + + if (!file.isFile()) { + throw new RuntimeException("不是文件: " + fullPath); + } + + return file.delete(); + } + + public static boolean deleteDirectory(String dirPath) { + if (dirPath == null || dirPath.trim().isEmpty()) { + throw new RuntimeException("路径不能为空"); + } + + File dir = new File(dirPath); + + if (!dir.exists()) { + return false; + } + + if (!dir.isDirectory()) { + throw new RuntimeException("不是文件夹: " + dirPath); + } + + return deleteDirRecursively(dir); + } + + private static boolean deleteDirRecursively(File dir) { + File[] files = dir.listFiles(); + + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + // 递归删除子目录 + deleteDirRecursively(file); + } else { + // 删除文件 + if (!file.delete()) { + throw new RuntimeException("文件删除失败: " + file.getAbsolutePath()); + } + } + } + } + + // 删除当前目录 + return dir.delete(); + } } \ No newline at end of file diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/PathUtils.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/PathUtils.java index f8c5010b..7f9d416f 100644 --- a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/PathUtils.java +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/PathUtils.java @@ -14,6 +14,9 @@ public class PathUtils { part = part.trim(); + // 替换 Windows 路径分隔符为 URL 的 '/' + part = part.replace("\\", "/"); + if (sb.length() == 0) { // 第一段:保留协议(http://) sb.append(part.replaceAll("/+$", "")); diff --git a/code/container-ocr/ocr-uniapp/env/.env b/code/container-ocr/ocr-uniapp/env/.env index f56bdca3..bbcb1fbb 100644 --- a/code/container-ocr/ocr-uniapp/env/.env +++ b/code/container-ocr/ocr-uniapp/env/.env @@ -1,3 +1,3 @@ -VITE_APP_PORT = 8080 +VITE_APP_PORT = 8082 VITE_ALIYUN_BUCKET = 'g3hd' \ No newline at end of file diff --git a/code/container-ocr/ocr-uniapp/src/pages-sub/upload/edit.vue b/code/container-ocr/ocr-uniapp/src/pages-sub/upload/edit.vue new file mode 100644 index 00000000..00e18ab6 --- /dev/null +++ b/code/container-ocr/ocr-uniapp/src/pages-sub/upload/edit.vue @@ -0,0 +1,468 @@ + + + + + diff --git a/code/container-ocr/ocr-uniapp/src/pages-sub/upload/index.vue b/code/container-ocr/ocr-uniapp/src/pages-sub/upload/index.vue index 686e11cf..1fb301de 100644 --- a/code/container-ocr/ocr-uniapp/src/pages-sub/upload/index.vue +++ b/code/container-ocr/ocr-uniapp/src/pages-sub/upload/index.vue @@ -1,9 +1,8 @@ @@ -96,13 +95,13 @@ function handleToFile(itemData) { v-for="(item, index) in dataList" :key="index" class="list-item" + @click="handleToDetail(item)" > - @@ -195,6 +194,12 @@ page { position: relative; padding: 16rpx; background-color: #fff; + transition: background-color 0.2s; + + &:active { + background-color: #f5f5f5; + } + &.list-item--desktop { .list-item__header { max-width: 1000rpx; diff --git a/code/container-ocr/ocr-uniapp/src/request/api.ts b/code/container-ocr/ocr-uniapp/src/request/api.ts index 414533a5..1b0aac73 100644 --- a/code/container-ocr/ocr-uniapp/src/request/api.ts +++ b/code/container-ocr/ocr-uniapp/src/request/api.ts @@ -45,8 +45,6 @@ export function recognizeOcrBoxApi(params: OcrBoxRequest) { export function deleteFileApi(subid) { return httpPost(`file/delete`, { subid, - tableName: 'bf_file', - bucketName: aliyunBucket, }) } @@ -76,8 +74,8 @@ export function getUniqueIdApi() { interface LoadDataParams { viewName: string searchCondition: string - orderField: string - searchColumns: string[] + orderField?: string + searchColumns?: string[] } export function loadDataApi(params: LoadDataParams) { @@ -109,6 +107,28 @@ export function loadPageDataApi(params: PageParams) { ) } -export function uploadTempFileApi(filePath: string, fileName: string) { - return httpUpload('/file/uploadTemp', filePath, fileName) +interface UpdateTempFileParams { + fileName: string +} + +export function uploadTempFileApi(filePath: string, params: UpdateTempFileParams) { + return httpUpload<{ + url: string + path: string + }>('/file/uploadTemp', filePath, params) +} + +interface UpdateContainerFileParams { + jobId: string | number + containerNo: string + fileName: string +} + +export function uploadContainerFileApi(filePath: string, params: UpdateContainerFileParams) { + return httpUpload<{ + url: string + path: string + uid: number + fileName: string + }>('/file/uploadContainer', filePath, params) } diff --git a/code/container-ocr/ocr-uniapp/src/request/index.ts b/code/container-ocr/ocr-uniapp/src/request/index.ts index 44426d4c..fea8ae83 100644 --- a/code/container-ocr/ocr-uniapp/src/request/index.ts +++ b/code/container-ocr/ocr-uniapp/src/request/index.ts @@ -3,6 +3,7 @@ import { toast } from 'sard-uniapp' import * as dbJson from '@/static/db.json' import { useUserStore } from '@/store' import { joinUrl } from '@/utils/common' + import { isH5 } from '@/utils/platform' /** @@ -27,6 +28,48 @@ function readDbJson(): Promise> { }) } +/** + * 根据机构码获取原始 baseUrl(不拼接) + * @param orgid 机构码 + * @returns Promise + */ +export async function getOrgBaseUrl(orgid: string): Promise { + if (!orgid) { + return '' + } + + let dbRes: Record = {} + + // 读取配置(和你原逻辑保持一致) + if (isH5) { + const timestamp = Date.now() + dbRes = await new Promise((resolve, reject) => { + uni.request({ + url: `/g3ocr-app/static/db.json?t=${timestamp}`, + method: 'GET', + success: res => resolve(res.data as Record), + fail: err => reject(err), + }) + }) + } + else { + dbRes = dbJson as Record + } + + if (!dbRes || Object.keys(dbRes).length === 0) { + throw new Error('机构配置获取失败') + } + + const baseUrl = dbRes[orgid] + + if (!baseUrl) { + throw new Error('机构码无效') + } + + // 👉 关键:只返回原始 JSON 里的值 + return baseUrl +} + /** * ====================== * 获取 baseUrl + headers @@ -200,7 +243,6 @@ function request( export function httpUpload( url: string, filePath: string, - fileName: string, formData?: Record, option?: UniApp.UploadFileOption, ) { @@ -217,17 +259,12 @@ export function httpUpload( const fullUrl = joinUrl(baseUrl, url) - const mergedFormData = { - ...formData, - fileName, - } - return new Promise>((resolve, reject) => { task = uni.uploadFile({ url: fullUrl, filePath, name: 'file', - formData: mergedFormData, + formData, header: { ...headers, ...(option?.header || {}), diff --git a/code/container-ocr/ocr-uniapp/src/utils/file.ts b/code/container-ocr/ocr-uniapp/src/utils/file.ts index 9884c509..e74ec005 100644 --- a/code/container-ocr/ocr-uniapp/src/utils/file.ts +++ b/code/container-ocr/ocr-uniapp/src/utils/file.ts @@ -1,7 +1,10 @@ import { toast } from 'sard-uniapp' +import { getOrgBaseUrl } from '@/request' +import { useUserStore } from '@/store' import { getOssKey } from './oss' import { isApp, isH5, isMpWeixin } from './platform' +const userStore = useUserStore() const aliyunBucket = import.meta.env.VITE_ALIYUN_BUCKET /** @@ -115,8 +118,6 @@ export function formatFileSize(size: number): string { else return `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB` } - - /** * 上传文件到阿里云 OSS(优化版) */ @@ -359,3 +360,39 @@ function downloadAppMedia(fileUrl: string, type: 'image' | 'video') { } }) } + +export function buildPubicUrl(orgUrl, path: string) { + if (!path) + return '' + + return joinUrl(orgUrl, '/static/file', path) +} + +export function joinUrl(...parts: (string | undefined | null)[]): string { + if (!parts || parts.length === 0) + return '' + + const sb: string[] = [] + + for (let part of parts) { + if (!part || !part.trim()) + continue + + part = part.trim() + + // 替换 Windows 路径分隔符 + part = part.replace(/\\/g, '/') + + if (sb.length === 0) { + // 第一段:保留协议,去掉末尾 / + sb.push(part.replace(/\/+$/, '')) + } + else { + // 后续段:去掉前后 / + sb.push(part.replace(/^\/+|\/+$/g, '')) + } + } + + // 中间用 / 拼接 + return sb.join('/') +} diff --git a/code/container-ocr/ocr-vue/.env b/code/container-ocr/ocr-vue/.env index 1898271d..0291d152 100644 --- a/code/container-ocr/ocr-vue/.env +++ b/code/container-ocr/ocr-vue/.env @@ -10,6 +10,7 @@ PUBLIC_SERVER_PORT=8080 # API # 后端 API 基础地址 -PUBLIC_API_BASE_URL=http://ser369.g3soft.cn:80/api +# PUBLIC_API_BASE_URL=http://ser369.g3soft.cn:80/api +PUBLIC_API_BASE_URL=http://192.168.1.137:80/api PUBLIC_ALIYUN_BUCKET = g3hd diff --git a/code/container-ocr/ocr-vue/components.d.ts b/code/container-ocr/ocr-vue/components.d.ts index ab8d863d..0882878f 100644 --- a/code/container-ocr/ocr-vue/components.d.ts +++ b/code/container-ocr/ocr-vue/components.d.ts @@ -39,6 +39,7 @@ declare module 'vue' { ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTree: typeof import('element-plus/es')['ElTree'] G3Column: typeof import('./src/components/g3-table/g3-column.vue')['default'] + G3ContainerFile: typeof import('./src/components/g3-container-file/index.vue')['default'] G3Detail: typeof import('./src/components/g3-detail/index.vue')['default'] G3Error: typeof import('./src/components/g3-error/index.vue')['default'] G3File: typeof import('./src/components/g3-file/index.vue')['default'] diff --git a/code/container-ocr/ocr-vue/src/components/g3-container-file/index.vue b/code/container-ocr/ocr-vue/src/components/g3-container-file/index.vue new file mode 100644 index 00000000..af963801 --- /dev/null +++ b/code/container-ocr/ocr-vue/src/components/g3-container-file/index.vue @@ -0,0 +1,1281 @@ + + + + + + + diff --git a/code/container-ocr/ocr-vue/src/components/g3-file/index.vue b/code/container-ocr/ocr-vue/src/components/g3-file/index.vue index bbea2dfc..7e935bd7 100644 --- a/code/container-ocr/ocr-vue/src/components/g3-file/index.vue +++ b/code/container-ocr/ocr-vue/src/components/g3-file/index.vue @@ -27,8 +27,15 @@ import { getAliyunFileBlob, getFileExtension, uploadFile, + buildPublicUrl, } from "@/utils/file"; -import { loadDataApi, saveObjectKeysApi, deleteFileApi } from "@/request/api"; +import { + loadDataApi, + insertFileApi, + saveObjectApi, + deleteFileApi, + uploadCommonFileApi, +} from "@/request/api"; const props = defineProps({ father: { @@ -158,7 +165,7 @@ async function handleConfirmUpdate() { ]; try { - const response = await saveObjectKeysApi(requestData); + const response = await saveObjectApi(requestData); if (response.success) { ElMessage.success("文件名更新成功"); @@ -239,8 +246,15 @@ async function loadFiles() { const res = await loadDataApi("bf_file", searchCondition, "mx_xh"); if (res.success) { - fileList.value = res.data; - oldFileList.value = JSON.parse(JSON.stringify(res.data)); + let datas = res.data.map((item) => { + return { + ...item, + mx_mapfilename: buildPublicUrl(item.mx_mapfilename), + }; + }); + + fileList.value = datas; + oldFileList.value = JSON.parse(JSON.stringify(datas)); loading.value = false; } } catch (error) { @@ -588,45 +602,69 @@ async function deleteFiles(files) { }); } +// 保存 // 保存 async function handleSave() { loading.value = true; loadingTip.value = "正在保存中 . . ."; - const { insertdata, updatedata, deletedata, hasChanges } = compareAndClassifyData( - fileList.value, - oldFileList.value, - "subid", - ["_content", "_status", "checked"], - ); + try { + const { insertdata, updatedata, deletedata, hasChanges } = compareAndClassifyData( + fileList.value, + oldFileList.value, + "subid", + ["_content", "_status", "checked"], + ); - // 处理新增的文件 - for (const item of insertdata) { - try { - await uploadFile( - item._content, - item.mx_filename, - item.mx_fileext, - item.mx_filesize, - item.mx_xh, - props.moduleId, - props.father, - ); - console.log(`文件 ${item.mx_filename} 上传成功`); - item._status = ""; - } catch (error) { - console.error(`文件 ${item.mx_filename} 上传失败:`, error); - ElMessage.error(`文件 ${item.mx_filename} 上传失败,请重试`); + // 没有变化直接结束 + if (!hasChanges) { + ElMessage.info("没有数据变更"); + return; } + + const mxXh = + fileList.value.length > 0 ? Math.max(...fileList.value.map((item) => item.mx_xh ?? 0)) : 2; + + // 处理新增 + for (const item of insertdata) { + try { + const uploadRes = await uploadCommonFileApi(item.mx_mapfilename, { + fileName: item.mx_filename, + }); + + if (!uploadRes.success) { + throw new Error("上传失败"); + } + + const insertRes = await insertFileApi({ + father: props.father, + mx_xh: mxXh + 1, + mx_filename: item.mx_filename, + mx_filesize: item.mx_filesize, + mx_fileext: item.mx_fileext, + mx_mapfilename: uploadRes.data.path, + mx_moduleid: props.moduleId, + mx_cate_id: props.cateId, + }); + + if (!insertRes.success) { + throw new Error("入库失败"); + } + + console.log(`文件 ${item.mx_filename} 上传成功`); + item._status = ""; + } catch (error) { + console.error(`文件 ${item.mx_filename} 处理失败:`, error); + ElMessage.error(`文件 ${item.mx_filename} 上传失败,请重试`); + } + } + + ElMessage.success("保存成功"); + } finally { + loading.value = false; + loadFiles(); } - - ElMessage.success("上传成功"); - - loading.value = false; - - loadFiles(); } - async function handleDownloadZip() { if (checkedFiles.value.length < 2) { ElMessage.warning("打包文件数量不得少于2"); diff --git a/code/container-ocr/ocr-vue/src/request/api.js b/code/container-ocr/ocr-vue/src/request/api.js index 6a70c529..0232f4b4 100644 --- a/code/container-ocr/ocr-vue/src/request/api.js +++ b/code/container-ocr/ocr-vue/src/request/api.js @@ -1,7 +1,5 @@ import { httpPost, httpUpload } from "."; - - // 登录 export function loginApi(orgid, userid, password) { return httpPost(`/auth/login`, { @@ -190,11 +188,31 @@ export function insertFileApi(params) { return httpPost("/file/insert", params); } +// export function deleteFileApi(subid) { +// return httpPost("/file/delete", { +// subid, +// tableName: "bf_file", +// bucketName: import.meta.env.PUBLIC_ALIYUN_BUCKET, +// }); +// } + export function deleteFileApi(subid) { return httpPost("/file/delete", { subid, - tableName: "bf_file", - bucketName: import.meta.env.PUBLIC_ALIYUN_BUCKET, + }); +} + +export function uploadContainerFileApi(filePath, { jobId, containerNo, fileName }) { + return httpUpload("/file/uploadContainer", filePath, { + jobId, + containerNo, + fileName, + }); +} + +export function uploadCommonFileApi(filePath, { fileName }) { + return httpUpload("/file/uploadCommon", filePath, { + fileName, }); } // ============================= file ============================= diff --git a/code/container-ocr/ocr-vue/src/request/index.js b/code/container-ocr/ocr-vue/src/request/index.js index e871c70e..f68291d5 100644 --- a/code/container-ocr/ocr-vue/src/request/index.js +++ b/code/container-ocr/ocr-vue/src/request/index.js @@ -120,19 +120,17 @@ export function httpPost(url, data, config) { * @param {Object} config - axios 配置(可选) * @returns {Promise} */ -export function httpUpload(url, file, extraData = {}, config = {}) { +export async function httpUpload(url, file, extraData = {}, config = {}) { const formData = new FormData(); - // 兼容单文件 & 多文件 if (Array.isArray(file)) { - file.forEach((f) => { - formData.append("file", f); - }); + for (const f of file) { + await appendFile(formData, f); + } } else { - formData.append("file", file); + await appendFile(formData, file); } - // 追加额外参数 Object.keys(extraData).forEach((key) => { formData.append(key, extraData[key]); }); @@ -147,6 +145,24 @@ export function httpUpload(url, file, extraData = {}, config = {}) { .then((res) => res.data); } +async function appendFile(formData, file) { + // ✅ 已经是 File / Blob + if (file instanceof File || file instanceof Blob) { + formData.append("file", file); + return; + } + + // ✅ 处理 blob:http://xxx + if (typeof file === "string" && file.startsWith("blob:")) { + const blob = await fetch(file).then((res) => res.blob()); + + formData.append("file", blob); + return; + } + + throw new Error("不支持的文件类型:" + typeof file); +} + export function reset401Flag() { hasShown401 = false; } diff --git a/code/container-ocr/ocr-vue/src/utils/file.js b/code/container-ocr/ocr-vue/src/utils/file.js index 8b05fa72..1d0d3ea2 100644 --- a/code/container-ocr/ocr-vue/src/utils/file.js +++ b/code/container-ocr/ocr-vue/src/utils/file.js @@ -203,7 +203,8 @@ export function getFileExtension(fileName) { return fileExt; } -export function downloadZip(files, fileName = "文件合集", callback) { + +export function downloadOssZip(files, fileName = "文件合集", callback) { const zip = new JSZip(); const addedFiles = new Set(); @@ -415,3 +416,78 @@ export function getAliyunFileBlob(url) { } // ============================================ 阿里云上传和下载 ============================================ + +// ============================================ 本地文件上传和下载 ========================================== +export function buildPublicUrl(filePath) { + const baseUrl = import.meta.env.PUBLIC_API_BASE_URL || ""; + + return joinUrl(baseUrl, "/static/file", filePath); +} + +export function getFileBlob(path) { + const fullUrl = buildPublicUrl(path) + + return fetch(fullUrl).then((response) => { + if (!response.ok) { + throw new Error(`获取文件失败,状态码:${response.status}`); + } + return response.blob(); + }); +} + +export function downloadZip(files, fileName = "文件合集", callback) { + const zip = new JSZip(); + const addedFiles = new Set(); + + const tasks = files.map((item) => { + return getFileBlob(item.mx_mapfilename) + .then((res) => { + const uniqueFileName = getUniqueFileName(item.mx_filename, addedFiles); + zip.file(uniqueFileName, res, { binary: true }); + addedFiles.add(uniqueFileName); + }) + .catch((err) => { + // 捕获单个文件下载失败的错误,不影响其他文件下载 + console.error(`Failed to download file: ${item.mx_filename}`, err); + }); + }); + + // 关键:使用 Promise.all 确保所有任务完成 + return Promise.all(tasks) + .then(() => zip.generateAsync({ type: "blob" })) + .then((res) => { + saveAs(res, `${fileName}.zip`); + callback && callback(); + }) + .catch((err) => { + callback && callback(); + throw err; + }); +} +// ============================================ 本地文件上传和下载 ========================================== + +export function joinUrl(...parts) { + if (!parts || parts.length === 0) return ""; + + const sb = []; + + for (let part of parts) { + if (!part) continue; + + part = String(part).trim(); + if (!part) continue; + + // 替换 Windows 路径分隔符 + part = part.replace(/\\/g, "/"); + + if (sb.length === 0) { + // 第一段:保留协议(http:// https://),去掉末尾 / + sb.push(part.replace(/\/+$/, "")); + } else { + // 后续段:去掉前后 / + sb.push(part.replace(/^\/+|\/+$/g, "")); + } + } + + return sb.join("/"); +} diff --git a/code/container-ocr/ocr-vue/src/views/container/high-download-dialog.vue b/code/container-ocr/ocr-vue/src/views/container/high-download-dialog.vue index f946fc97..72ee8194 100644 --- a/code/container-ocr/ocr-vue/src/views/container/high-download-dialog.vue +++ b/code/container-ocr/ocr-vue/src/views/container/high-download-dialog.vue @@ -4,7 +4,7 @@ import { useThrottle } from "@/utils/throttle"; import { ElMessage } from "element-plus"; import { saveAs } from "file-saver"; import JSZip from "jszip"; -import { getAliyunFileBlob } from "@/utils/file"; +import { getFileBlob } from "@/utils/file"; import { formatToDate } from "@/utils/time"; const viewName = "v_container"; @@ -189,28 +189,55 @@ async function downloadAll() { let finished = 0; let totalFiles = 0; - // 1. 收集所有下载任务 const allTasks = []; + // ✅ 关键:箱号计数器 + const containerCountMap = new Map(); + for (const item of records) { const fileRes = await loadDataApi("bf_file", `father='${item.b_id}'`, "mx_xh asc"); + const files = (fileRes.success && fileRes.data) || []; totalFiles += files.length; - // 按 年/月/日/集装箱号 生成层级文件夹 - const ymdFolderName = formatToDate(item.b_inputdatetime, "YYYY/MM/DD"); // 2026/03/27 - const containerFolderName = item.b_container_no; - let folder = zip.folder(ymdFolderName); - folder = folder ? folder.folder(containerFolderName) : zip.folder(containerFolderName); + // ========================= + // ✅ 生成“箱号文件夹名称” + // ========================= + const containerNo = item.b_container_no || "未知箱号"; + const count = containerCountMap.get(containerNo) || 0; + + let folderName = containerNo; + + if (count === 1) { + folderName = `${containerNo}_01`; + } else if (count > 1) { + folderName = `${containerNo}_${String(count - 1).padStart(2, "0")}`; + } + + containerCountMap.set(containerNo, count + 1); + + // ========================= + // 生成日期目录 + // ========================= + const ymdFolderName = formatToDate(item.b_inputdatetime, "YYYY/MM/DD"); + + let folder = zip.folder(ymdFolderName); + folder = folder ? folder.folder(folderName) : zip.folder(folderName); + + // ========================= + // 收集下载任务 + // ========================= files.forEach((file, idx) => { allTasks.push(async () => { try { - const blob = await getAliyunFileBlob(file.mx_mapfilename); - const fileName = buildFileName(item.b_container_no, idx + 1, file.mx_filename) || "未命名文件"; + const blob = await getFileBlob(file.mx_mapfilename); + + const fileName = buildFileName(containerNo, idx + 1, file.mx_filename) || "未命名文件"; + folder.file(fileName, blob); } catch (e) { - console.error(item.b_container_no + "下载失败:", file.mx_mapfilename, e); + console.error(`${containerNo} 下载失败:`, file.mx_mapfilename, e); } finally { finished++; loading.setText(`正在下载 ${finished}/${totalFiles} 个文件...`); @@ -219,18 +246,21 @@ async function downloadAll() { }); } - // 2. 并发执行下载任务,限制同时 3 个 + // ========================= + // 并发执行(限制3个) + // ========================= await runWithLimit(allTasks, 3); loading.setText("正在生成压缩包..."); + const content = await zip.generateAsync({ type: "blob" }); - // 3. 保存压缩包 saveAs(content, buildZipName("ALL")); ElMessage.success("下载完成"); } catch (e) { console.error(e); + ElMessage.error("下载失败"); } finally { loading.close(); } @@ -246,6 +276,7 @@ async function downloadSlice() { let start = Math.max(1, Math.min(sliceModeForm.value.startPage || 1, allTotal.value)); let end = Math.max(1, Math.min(sliceModeForm.value.endPage || 1, allTotal.value)); if (start > end) [start, end] = [end, start]; + sliceModeForm.value.startPage = start; sliceModeForm.value.endPage = end; @@ -268,28 +299,51 @@ async function downloadSlice() { let finished = 0; const allTasks = []; + // ✅ 箱号计数 + const containerCountMap = new Map(); + records.forEach((item) => { const ymdFolderName = formatToDate(item.b_inputdatetime, "YYYY/MM/DD"); - const containerFolderName = item.b_container_no; + + const containerNo = item.b_container_no || "未知箱号"; + + // ===== 关键:生成文件夹名 ===== + const count = containerCountMap.get(containerNo) || 0; + + let containerFolderName = containerNo; + + if (count === 1) { + containerFolderName = `${containerNo}_01`; + } else if (count > 1) { + containerFolderName = `${containerNo}_${String(count - 1).padStart(2, "0")}`; + } + + containerCountMap.set(containerNo, count + 1); + const folderPromise = loadDataApi("bf_file", `father='${item.b_id}'`, "mx_xh asc"); allTasks.push(async () => { const fileRes = await folderPromise; + if (!fileRes.success) { - console.error("获取附件失败:", item.b_container_no); + console.error("获取附件失败:", containerNo); return; } - const folder = zip.folder(ymdFolderName)?.folder(containerFolderName) || zip.folder(containerFolderName); + let folder = zip.folder(ymdFolderName); + folder = folder ? folder.folder(containerFolderName) : zip.folder(containerFolderName); const files = fileRes.data || []; + const fileTasks = files.map((file, idx) => async () => { try { - const blob = await getAliyunFileBlob(file.mx_mapfilename); - const fileName = buildFileName(item.b_container_no, idx + 1, file.mx_filename) || "未命名文件"; + const blob = await getFileBlob(file.mx_mapfilename); + + const fileName = buildFileName(containerNo, idx + 1, file.mx_filename) || "未命名文件"; + folder.file(fileName, blob); } catch (e) { - console.error(item.b_container_no + "下载失败:", file.mx_mapfilename, e); + console.error(containerNo + "下载失败:", file.mx_mapfilename, e); } finally { finished++; loading.setText(`正在下载 ${finished} / ${records.length} 个文件...`); @@ -304,6 +358,7 @@ async function downloadSlice() { loading.setText("正在生成压缩包..."); const content = await zip.generateAsync({ type: "blob" }); + saveAs(content, buildZipName("SLICE")); ElMessage.success("下载完成"); } catch (e) { @@ -322,6 +377,7 @@ async function downloadChunk() { try { const records = await fetchDatas(); + if (!records?.length) { ElMessage.warning("无有效数据"); return; @@ -334,33 +390,60 @@ async function downloadChunk() { const allPageTasks = []; + // ✅ 箱号计数(关键) + const containerCountMap = new Map(); + for (let pageIndex = 0; pageIndex < totalPages; pageIndex++) { const startIndex = pageIndex * pageSize; const endIndex = Math.min((pageIndex + 1) * pageSize, records.length); const pageRecords = records.slice(startIndex, endIndex); + const pageFolderName = `${startIndex + 1}-${endIndex}`; const pageFolder = zip.folder(pageFolderName); pageRecords.forEach((item) => { allPageTasks.push(async () => { const fileRes = await loadDataApi("bf_file", `father='${item.b_id}'`, "mx_xh asc"); + if (!fileRes.success) { console.error("获取附件失败:", item.b_container_no); return; } + const containerNo = item.b_container_no || "未知箱号"; + + // ===== 生成箱号文件夹名 ===== + const count = containerCountMap.get(containerNo) || 0; + + let containerFolderName = containerNo; + + if (count === 1) { + containerFolderName = `${containerNo}_01`; + } else if (count > 1) { + containerFolderName = `${containerNo}_${String(count - 1).padStart(2, "0")}`; + } + + containerCountMap.set(containerNo, count + 1); + const ymdFolderName = formatToDate(item.b_inputdatetime, "YYYY/MM/DD"); - const containerFolderName = item.b_container_no; - const folder = pageFolder.folder(ymdFolderName)?.folder(containerFolderName) || pageFolder.folder(containerFolderName); + + let folder = pageFolder.folder(ymdFolderName); + folder = folder + ? folder.folder(containerFolderName) + : pageFolder.folder(containerFolderName); const files = fileRes.data || []; + const fileTasks = files.map((file, idx) => async () => { try { - const blob = await getAliyunFileBlob(file.mx_mapfilename); - const fileName = buildFileName(item.b_container_no, idx + 1, file.mx_filename) || "未命名文件"; + const blob = await getFileBlob(file.mx_mapfilename); + + const fileName = + buildFileName(containerNo, idx + 1, file.mx_filename) || "未命名文件"; + folder.file(fileName, blob); } catch (e) { - console.error(item.b_container_no + "下载失败:", file.mx_mapfilename, e); + console.error(containerNo + "下载失败:", file.mx_mapfilename, e); } finally { finished++; loading.setText(`正在下载 ${finished} 个文件...`); @@ -376,6 +459,7 @@ async function downloadChunk() { loading.setText("正在生成压缩包..."); const content = await zip.generateAsync({ type: "blob" }); + saveAs(content, buildZipName("CHUNK")); ElMessage.success("下载完成"); } catch (e) { @@ -385,7 +469,6 @@ async function downloadChunk() { loading.close(); } } - function buildFileName(containerNo, index, fileName) { const formattedIndex = String(index).padStart(2, "0"); diff --git a/code/container-ocr/ocr-vue/src/views/container/index.vue b/code/container-ocr/ocr-vue/src/views/container/index.vue index 9f143711..7c5e0355 100644 --- a/code/container-ocr/ocr-vue/src/views/container/index.vue +++ b/code/container-ocr/ocr-vue/src/views/container/index.vue @@ -1,4 +1,8 @@ + + + +