From 7f0d4b6b6f3994615c125905d2b756e37f98b84a Mon Sep 17 00:00:00 2001 From: wenshijun Date: Sun, 28 Sep 2025 08:22:53 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=89=93=E5=8D=B0=E6=8D=AE?= =?UTF-8?q?=E7=BC=96=E5=8F=B7=E5=AD=97=E6=AE=B5=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E5=9B=BE=E7=89=87=E5=88=B0=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E7=A3=81=E7=9B=98D=E7=9B=98=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zg/framework/config/SecurityConfig.java | 1 + .../controller/MediaUploadController.java | 464 ++++++++++++++++++ .../project/wisdom/domain/dto/PkDatDTO.java | 2 + src/main/resources/application-druid.yml | 1 + src/main/resources/application.yml | 5 +- .../resources/mybatis/wisdom/PkDatMapper.xml | 2 +- 6 files changed, 472 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/zg/project/wisdom/controller/MediaUploadController.java diff --git a/src/main/java/com/zg/framework/config/SecurityConfig.java b/src/main/java/com/zg/framework/config/SecurityConfig.java index 4131fdd..5511de0 100644 --- a/src/main/java/com/zg/framework/config/SecurityConfig.java +++ b/src/main/java/com/zg/framework/config/SecurityConfig.java @@ -123,6 +123,7 @@ public class SecurityConfig "/ws/**", "/photo/**", "/wisdom/stock/**", + "/system/media/**", "/wisdom/**", "/mock/**", "/information/device/**", diff --git a/src/main/java/com/zg/project/wisdom/controller/MediaUploadController.java b/src/main/java/com/zg/project/wisdom/controller/MediaUploadController.java new file mode 100644 index 0000000..a596f23 --- /dev/null +++ b/src/main/java/com/zg/project/wisdom/controller/MediaUploadController.java @@ -0,0 +1,464 @@ +//package com.zg.project.wisdom.controller; +// +//import com.zg.framework.web.domain.AjaxResult; +//import org.apache.commons.lang3.StringUtils; +//import org.springframework.http.MediaType; +//import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +//import org.springframework.util.unit.DataSize; +//import org.springframework.web.bind.annotation.*; +//import org.springframework.web.multipart.MultipartFile; +// +//import javax.annotation.Resource; +//import java.io.IOException; +//import java.io.InputStream; +//import java.io.OutputStream; +//import java.nio.file.*; +//import java.util.*; +//import java.util.concurrent.CompletableFuture; +//import java.util.concurrent.Semaphore; +// +///** +// * 多媒体上传控制器(支持图片与视频并行落盘) +// */ +//@RestController +//@RequestMapping("/system/media") +//public class MediaUploadController { +// +// /** 根目录(如需配置化,可改为从 application.yml 读取) */ +// private static final Path ROOT_DIR = Paths.get("D:/uploads"); +// +// /** 本次上传总大小限制:100MB */ +// private static final long MAX_TOTAL_BYTES = DataSize.ofMegabytes(100).toBytes(); +// +// /** 图片/视频数量限制 */ +// private static final int MAX_IMAGE_COUNT = 100; +// private static final int MAX_VIDEO_COUNT = 5; +// +// /** 单次请求内的并发上限(建议 2~4;默认 4) */ +// private static final int PER_REQUEST_CONCURRENCY = 4; +// +// /** 注入线程池 Bean:threadPoolTaskExecutor */ +// @Resource(name = "threadPoolTaskExecutor") +// private ThreadPoolTaskExecutor executor; +// +// /** +// * 上传接口 +// */ +// @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) +// public AjaxResult upload( +// @RequestParam("username") String username, +// @RequestParam("nameBase") String nameBase, +// @RequestParam(value = "imageNames", required = false) String imageNames, +// @RequestParam(value = "videoNames", required = false) String videoNames, +// @RequestPart(value = "images", required = false) MultipartFile[] images, +// @RequestPart(value = "videos", required = false) MultipartFile[] videos +// ) { +// try { +// // 1. 基础校验 +// if (StringUtils.isBlank(username)) { +// return AjaxResult.error("username 不能为空"); +// } +// if (StringUtils.isBlank(nameBase)) { +// return AjaxResult.error("nameBase(命名信息)不能为空"); +// } +// +// int imageCount = images == null ? 0 : images.length; +// int videoCount = videos == null ? 0 : videos.length; +// +// if (imageCount == 0 && videoCount == 0) { +// return AjaxResult.error("请至少上传一张图片或一段视频"); +// } +// if (imageCount > MAX_IMAGE_COUNT) { +// return AjaxResult.error("图片最多支持上传 " + MAX_IMAGE_COUNT + " 张"); +// } +// if (videoCount > MAX_VIDEO_COUNT) { +// return AjaxResult.error("视频最多支持上传 " + MAX_VIDEO_COUNT + " 段"); +// } +// +// // 2. 总体积校验 +// long totalBytes = 0L; +// if (images != null) for (MultipartFile f : images) totalBytes += safeSize(f); +// if (videos != null) for (MultipartFile f : videos) totalBytes += safeSize(f); +// if (totalBytes > MAX_TOTAL_BYTES) { +// return AjaxResult.error("本次上传总大小超过 100 MB(约 " + (totalBytes / 1024 / 1024) + " MB)"); +// } +// +// // 3. 目标目录 +// String base = sanitize(nameBase); +// String user = sanitize(username); +// String folderName = base + "_" + user; +// Path targetDir = ROOT_DIR.resolve(folderName).normalize(); +// if (!targetDir.startsWith(ROOT_DIR)) { +// return AjaxResult.error("非法路径"); +// } +// Files.createDirectories(targetDir); +// +// // 4. 解析前端传递的文件名 +// List imageNameList = new ArrayList<>(); +// List videoNameList = new ArrayList<>(); +// +// if (StringUtils.isNotBlank(imageNames)) { +// try { +// imageNameList = Arrays.asList(imageNames.split(",")); +// } catch (Exception e) { +// // 如果解析失败,使用空列表 +// imageNameList = new ArrayList<>(); +// } +// } +// if (StringUtils.isNotBlank(videoNames)) { +// try { +// videoNameList = Arrays.asList(videoNames.split(",")); +// } catch (Exception e) { +// // 如果解析失败,使用空列表 +// videoNameList = new ArrayList<>(); +// } +// } +// +// // 5. 并发落盘任务 +// int seq = 1; +// List>> futures = new ArrayList<>(); +// Semaphore semaphore = new Semaphore(PER_REQUEST_CONCURRENCY); +// +// // 5.1 图片处理 +// if (images != null) { +// for (int i = 0; i < images.length; i++) { +// MultipartFile img = images[i]; +// if (img == null || img.isEmpty()) continue; +// +// // 使用前端传递的文件名,如果没有则从原始文件名获取扩展名 +// String originalName = i < imageNameList.size() ? imageNameList.get(i) : img.getOriginalFilename(); +// String ext = getExtension(originalName).toLowerCase(Locale.ROOT); +// if (StringUtils.isBlank(ext)) { +// ext = "jpg"; // 图片默认扩展名 +// } +// +// final String filename = String.format("%s%s%03d.%s", base, user, seq++, ext); +// final Path dest = targetDir.resolve(filename).normalize(); +// +// futures.add(CompletableFuture.supplyAsync(() -> { +// acquire(semaphore); +// try { return saveOne(img, dest, "image"); } +// finally { semaphore.release(); } +// }, executor)); +// } +// } +// +// // 5.2 视频处理 +// if (videos != null) { +// for (int i = 0; i < videos.length; i++) { +// MultipartFile vid = videos[i]; +// if (vid == null || vid.isEmpty()) continue; +// +// // 使用前端传递的文件名,如果没有则从原始文件名获取扩展名 +// String originalName = i < videoNameList.size() ? videoNameList.get(i) : vid.getOriginalFilename(); +// String ext = getExtension(originalName).toLowerCase(Locale.ROOT); +// if (StringUtils.isBlank(ext)) { +// ext = "mp4"; // 视频默认扩展名 +// } +// +// final String filename = String.format("%s%s%03d.%s", base, user, seq++, ext); +// final Path dest = targetDir.resolve(filename).normalize(); +// +// futures.add(CompletableFuture.supplyAsync(() -> { +// acquire(semaphore); +// try { return saveOne(vid, dest, "video"); } +// finally { semaphore.release(); } +// }, executor)); +// } +// } +// +// // 6. 等待完成并返回 +// List> files = new ArrayList<>(futures.size()); +// for (CompletableFuture> f : futures) { +// files.add(f.join()); +// } +// files.sort(Comparator.comparing(m -> String.valueOf(m.get("path")))); +// +// Map data = new LinkedHashMap<>(); +// data.put("folder", targetDir.toString()); +// data.put("username", username); +// data.put("nameBase", nameBase); +// data.put("totalBytes", totalBytes); +// data.put("count", files.size()); +// data.put("files", files); +// +// return AjaxResult.success("上传成功", data); +// +// } catch (Exception e) { +// return AjaxResult.error("保存文件失败:" + e.getMessage()); +// } +// } +// +// // ========================= 辅助方法 ========================= +// +// /** 安全获取文件大小,避免 NPE */ +// private static long safeSize(MultipartFile f) { +// try { return (f == null) ? 0L : f.getSize(); } +// catch (Exception ignored) { return 0L; } +// } +// +// /** 获取扩展名(无则返回空字符串) */ +// private static String getExtension(String originalName) { +// if (StringUtils.isBlank(originalName)) return ""; +// int i = originalName.lastIndexOf('.'); +// return (i >= 0 && i < originalName.length() - 1) ? originalName.substring(i + 1) : ""; +// } +// +// /** 单文件保存 */ +// private Map saveOne(MultipartFile src, Path dest, String type) { +// try { +// Path parent = dest.getParent(); +// if (parent == null) throw new IOException("父目录为空"); +// if (!dest.normalize().startsWith(parent.normalize())) throw new IOException("非法路径"); +// +// try (InputStream in = src.getInputStream(); +// OutputStream out = Files.newOutputStream(dest, +// StandardOpenOption.CREATE, +// StandardOpenOption.TRUNCATE_EXISTING, +// StandardOpenOption.WRITE)) { +// in.transferTo(out); +// } +// +// Map m = new LinkedHashMap<>(); +// m.put("type", type); +// m.put("path", dest.toString()); +// m.put("sizeBytes", src.getSize()); +// m.put("sizeMB", String.format(Locale.ROOT, "%.2f", src.getSize() / 1024.0 / 1024.0)); +// return m; +// +// } catch (Exception ex) { +// throw new RuntimeException("保存失败:" + dest.getFileName() + ",原因:" + ex.getMessage(), ex); +// } +// } +// +// /** 获取信号量许可证(受控并发) */ +// private static void acquire(Semaphore sem) { +// try { sem.acquire(); } +// catch (InterruptedException e) { +// Thread.currentThread().interrupt(); +// throw new RuntimeException("线程被中断", e); +// } +// } +// +// /** 文件名/目录名清洗 */ +// private static String sanitize(String s) { +// String cleaned = s.replaceAll("[^\\p{L}\\p{N}_-]", "_"); +// return cleaned.length() > 64 ? cleaned.substring(0, 64) : cleaned; +// } +//} + +package com.zg.project.wisdom.controller; + +import com.zg.framework.web.domain.AjaxResult; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.MediaType; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.util.unit.DataSize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.*; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Semaphore; + +/** + * 多媒体上传控制器(支持图片与视频并行落盘) + */ +@RestController +@RequestMapping("/system/media") +public class MediaUploadController { + + /** 根目录(如需配置化,可改为从 application.yml 读取) */ + private static final Path ROOT_DIR = Paths.get("D:/uploads"); + + /** 本次上传总大小限制:100MB */ + private static final long MAX_TOTAL_BYTES = DataSize.ofMegabytes(100).toBytes(); + + /** 图片/视频数量限制 */ + private static final int MAX_IMAGE_COUNT = 100; + private static final int MAX_VIDEO_COUNT = 5; + + /** 单次请求内的并发上限(建议 2~4;默认 4) */ + private static final int PER_REQUEST_CONCURRENCY = 4; + + /** 注入线程池 Bean:threadPoolTaskExecutor */ + @Resource(name = "threadPoolTaskExecutor") + private ThreadPoolTaskExecutor executor; + + /** + * 上传接口 + */ + @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public AjaxResult upload( + @RequestParam("username") String username, + @RequestParam("nameBase") String nameBase, + @RequestPart(value = "images", required = false) MultipartFile[] images, + @RequestPart(value = "videos", required = false) MultipartFile[] videos + ) { + try { + // 1. 基础校验 + if (StringUtils.isBlank(username)) { + return AjaxResult.error("username 不能为空"); + } + if (StringUtils.isBlank(nameBase)) { + return AjaxResult.error("nameBase(命名信息)不能为空"); + } + + int imageCount = images == null ? 0 : images.length; + int videoCount = videos == null ? 0 : videos.length; + + if (imageCount == 0 && videoCount == 0) { + return AjaxResult.error("请至少上传一张图片或一段视频"); + } + if (imageCount > MAX_IMAGE_COUNT) { + return AjaxResult.error("图片最多支持上传 " + MAX_IMAGE_COUNT + " 张"); + } + if (videoCount > MAX_VIDEO_COUNT) { + return AjaxResult.error("视频最多支持上传 " + MAX_VIDEO_COUNT + " 段"); + } + + // 2. 总体积校验 + long totalBytes = 0L; + if (images != null) for (MultipartFile f : images) totalBytes += safeSize(f); + if (videos != null) for (MultipartFile f : videos) totalBytes += safeSize(f); + if (totalBytes > MAX_TOTAL_BYTES) { + return AjaxResult.error("本次上传总大小超过 100 MB(约 " + (totalBytes / 1024 / 1024) + " MB)"); + } + + // 3. 目标目录 + String base = sanitize(nameBase); + String user = sanitize(username); + String folderName = base + "_" + user; + Path targetDir = ROOT_DIR.resolve(folderName).normalize(); + if (!targetDir.startsWith(ROOT_DIR)) { + return AjaxResult.error("非法路径"); + } + Files.createDirectories(targetDir); + + // 4. 并发落盘任务 + int seq = 1; + List>> futures = new ArrayList<>(); + Semaphore semaphore = new Semaphore(PER_REQUEST_CONCURRENCY); + + // 4.1 图片(不做格式校验) + if (images != null) { + for (MultipartFile img : images) { + if (img == null || img.isEmpty()) continue; + + String ext = getExtension(img.getOriginalFilename()).toLowerCase(Locale.ROOT); + final String filename = String.format( + "%s%s%03d%s", base, user, seq++, StringUtils.isBlank(ext) ? "" : "." + ext + ); + final Path dest = targetDir.resolve(filename).normalize(); + + futures.add(CompletableFuture.supplyAsync(() -> { + acquire(semaphore); + try { return saveOne(img, dest, "image"); } + finally { semaphore.release(); } + }, executor)); + } + } + + // 4.2 视频(不做格式校验) + if (videos != null) { + for (MultipartFile vid : videos) { + if (vid == null || vid.isEmpty()) continue; + + String ext = getExtension(vid.getOriginalFilename()).toLowerCase(Locale.ROOT); + final String filename = String.format( + "%s%s%03d%s", base, user, seq++, StringUtils.isBlank(ext) ? "" : "." + ext + ); + final Path dest = targetDir.resolve(filename).normalize(); + + futures.add(CompletableFuture.supplyAsync(() -> { + acquire(semaphore); + try { return saveOne(vid, dest, "video"); } + finally { semaphore.release(); } + }, executor)); + } + } + + // 5. 等待完成并返回 + List> files = new ArrayList<>(futures.size()); + for (CompletableFuture> f : futures) { + files.add(f.join()); + } + files.sort(Comparator.comparing(m -> String.valueOf(m.get("path")))); + + Map data = new LinkedHashMap<>(); + data.put("folder", targetDir.toString()); + data.put("username", username); + data.put("nameBase", nameBase); + data.put("totalBytes", totalBytes); + data.put("count", files.size()); + data.put("files", files); + + return AjaxResult.success("上传成功", data); + + } catch (Exception e) { + return AjaxResult.error("保存文件失败:" + e.getMessage()); + } + } + + // ========================= 辅助方法 ========================= + + /** 安全获取文件大小,避免 NPE */ + private static long safeSize(MultipartFile f) { + try { return (f == null) ? 0L : f.getSize(); } + catch (Exception ignored) { return 0L; } + } + + /** 获取扩展名(无则返回空字符串) */ + private static String getExtension(String originalName) { + if (StringUtils.isBlank(originalName)) return ""; + int i = originalName.lastIndexOf('.'); + return (i >= 0 && i < originalName.length() - 1) ? originalName.substring(i + 1) : ""; + } + + /** 单文件保存 */ + private Map saveOne(MultipartFile src, Path dest, String type) { + try { + Path parent = dest.getParent(); + if (parent == null) throw new IOException("父目录为空"); + if (!dest.normalize().startsWith(parent.normalize())) throw new IOException("非法路径"); + + try (InputStream in = src.getInputStream(); + OutputStream out = Files.newOutputStream(dest, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE)) { + in.transferTo(out); + } + + Map m = new LinkedHashMap<>(); + m.put("type", type); + m.put("path", dest.toString()); + m.put("sizeBytes", src.getSize()); + m.put("sizeMB", String.format(Locale.ROOT, "%.2f", src.getSize() / 1024.0 / 1024.0)); + return m; + + } catch (Exception ex) { + throw new RuntimeException("保存失败:" + dest.getFileName() + ",原因:" + ex.getMessage(), ex); + } + } + + /** 获取信号量许可证(受控并发) */ + private static void acquire(Semaphore sem) { + try { sem.acquire(); } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("线程被中断", e); + } + } + + /** 文件名/目录名清洗 */ + private static String sanitize(String s) { + String cleaned = s.replaceAll("[^\\p{L}\\p{N}_-]", "_"); + return cleaned.length() > 64 ? cleaned.substring(0, 64) : cleaned; + } +} diff --git a/src/main/java/com/zg/project/wisdom/domain/dto/PkDatDTO.java b/src/main/java/com/zg/project/wisdom/domain/dto/PkDatDTO.java index f21952c..625f39c 100644 --- a/src/main/java/com/zg/project/wisdom/domain/dto/PkDatDTO.java +++ b/src/main/java/com/zg/project/wisdom/domain/dto/PkDatDTO.java @@ -23,4 +23,6 @@ public class PkDatDTO { private String xmMs; /** 库位 */ private String pcode; + /** 打印机编号 */ + private Integer printer; } diff --git a/src/main/resources/application-druid.yml b/src/main/resources/application-druid.yml index 7fb4159..faa02e2 100644 --- a/src/main/resources/application-druid.yml +++ b/src/main/resources/application-druid.yml @@ -6,6 +6,7 @@ spring: druid: # 主库数据源 master: +# url: jdbc:mysql://101.132.133.142:3306/wisdom?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 url: jdbc:mysql://192.168.1.20:3306/wisdom?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 # url: jdbc:mysql://192.168.1.251:3306/wisdom?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 # url: jdbc:mysql://localhost:3306/wisdom?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e830924..3644ec4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -57,9 +57,9 @@ spring: servlet: multipart: # 单个文件大小 - max-file-size: 10MB + max-file-size: 100MB # 设置总上传的文件大小 - max-request-size: 20MB + max-request-size: 100MB # 服务模块 devtools: restart: @@ -68,6 +68,7 @@ spring: # redis 配置 redis: # 地址 +# host: 101.132.133.142 host: 192.168.1.20 # host: 192.168.1.251 # host: localhost diff --git a/src/main/resources/mybatis/wisdom/PkDatMapper.xml b/src/main/resources/mybatis/wisdom/PkDatMapper.xml index d06ad63..fee3de1 100644 --- a/src/main/resources/mybatis/wisdom/PkDatMapper.xml +++ b/src/main/resources/mybatis/wisdom/PkDatMapper.xml @@ -11,7 +11,7 @@ VALUES (#{item.wlNo}, #{item.realQty}, 1, 1, #{item.sapNo}, - #{item.gysMc}, #{item.remark}, #{item.xmMs}, #{item.pcode}, 1) + #{item.gysMc}, #{item.remark}, #{item.xmMs}, #{item.pcode}, #{item.printer})