From 9ecf371710c1128205a793624fcf7f92e8bc575c Mon Sep 17 00:00:00 2001 From: yangjun <1173114630@qq.com> Date: Fri, 19 Dec 2025 08:39:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jeecg/config/shiro/ShiroConfig.java | 1 + .../service/impl/CameraInfoServiceImpl.java | 28 +- .../care/order/job/CareSubDownTplinkJob.java | 1 + .../controller/CommonVideoController.java | 281 ++++++++++++++++++ 4 files changed, 303 insertions(+), 8 deletions(-) create mode 100644 nursing-unit-system/nu-system-biz/src/main/java/org/jeecg/modules/system/controller/CommonVideoController.java diff --git a/nursing-unit-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java b/nursing-unit-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java index 62263175..871a7a63 100644 --- a/nursing-unit-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java +++ b/nursing-unit-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java @@ -117,6 +117,7 @@ public class ShiroConfig { filterChainDefinitionMap.put("/sys/user/passwordChange", "anon");//用户更改密码 filterChainDefinitionMap.put("/auth/2step-code", "anon");//登录验证码 filterChainDefinitionMap.put("/sys/common/static/**", "anon");//图片预览 &下载文件不限制token + filterChainDefinitionMap.put("/sys/commonVideo/staticVideo/**", "anon");//视频预览 &下载文件不限制token filterChainDefinitionMap.put("/sys/common/pdf/**", "anon");//pdf预览 filterChainDefinitionMap.put("/sys/dict/getDictItems/**", "anon");//获取字典数据 //filterChainDefinitionMap.put("/sys/common/view/**", "anon");//图片预览不限制token diff --git a/nursing-unit-iot/nu-iot-biz/src/main/java/com/nu/modules/tplink/camera/service/impl/CameraInfoServiceImpl.java b/nursing-unit-iot/nu-iot-biz/src/main/java/com/nu/modules/tplink/camera/service/impl/CameraInfoServiceImpl.java index 2aed0f12..27c4fde5 100644 --- a/nursing-unit-iot/nu-iot-biz/src/main/java/com/nu/modules/tplink/camera/service/impl/CameraInfoServiceImpl.java +++ b/nursing-unit-iot/nu-iot-biz/src/main/java/com/nu/modules/tplink/camera/service/impl/CameraInfoServiceImpl.java @@ -29,6 +29,8 @@ import org.springframework.stereotype.Service; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.extern.slf4j.Slf4j; import java.math.BigDecimal; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.*; import static java.lang.Thread.sleep; @@ -2720,7 +2722,7 @@ public class CameraInfoServiceImpl extends ServiceImpl queryWrapper = new QueryWrapper<>(); queryWrapper.isNull("tplink_path"); + queryWrapper.eq("iz_finish","Y"); List careOrdersSubList = careOrdersSubService.list(queryWrapper); for (CareOrdersSub careOrdersSubEntity : careOrdersSubList){ try { diff --git a/nursing-unit-system/nu-system-biz/src/main/java/org/jeecg/modules/system/controller/CommonVideoController.java b/nursing-unit-system/nu-system-biz/src/main/java/org/jeecg/modules/system/controller/CommonVideoController.java new file mode 100644 index 00000000..d8be7b6d --- /dev/null +++ b/nursing-unit-system/nu-system-biz/src/main/java/org/jeecg/modules/system/controller/CommonVideoController.java @@ -0,0 +1,281 @@ +package org.jeecg.modules.system.controller; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.nu.entity.SysUploadPreEntity; +import com.nu.modules.sysconfig.ISysConfigApi; +import com.nu.utils.HttpRequestUtil; +import com.nu.utils.SafetyUtil; +import lombok.extern.slf4j.Slf4j; +import org.jeecg.common.api.vo.Result; +import org.jeecg.common.constant.CommonConstant; +import org.jeecg.common.constant.SymbolConstant; +import org.jeecg.common.exception.JeecgBootException; +import org.jeecg.common.util.CommonUtils; +import org.jeecg.common.util.filter.SsrfFileTypeFilter; +import org.jeecg.common.util.oConvertUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + *

+ * 用户表 前端控制器 + *

+ * + * @Author scott + * @since 2018-12-20 + */ +@Slf4j +@RestController +@RequestMapping("/sys/commonVideo") +public class CommonVideoController { + + @Value(value = "${jeecg.path.upload}") + private String uploadpath; + + /** + * 本地:local minio:minio 阿里:alioss + */ + @Value(value = "${jeecg.uploadType}") + private String uploadType; + + @Autowired + private ISysConfigApi sysConfigApi; + + /** + * @return + * @Author 政辉 + */ + @GetMapping("/403") + public Result noauth() { + return Result.error("没有权限,请联系管理员分配权限!"); + } + + + + + /** + * 把指定URL后的字符串全部截断当成参数 + * 这么做是为了防止URL中包含中文或者特殊字符(/等)时,匹配不了的问题 + * + * @param request + * @return + */ + private static String extractPathFromPattern(final HttpServletRequest request) { + String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); + String bestMatchPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); + return new AntPathMatcher().extractPathWithinPattern(bestMatchPattern, path); + } + + @GetMapping(value = "/staticVideo/**") + public void staticVideo(HttpServletRequest request, HttpServletResponse response) { + String filePath = extractPathFromPattern(request); + if (StringUtils.isEmpty(filePath) || "null".equals(filePath)) { + response.setStatus(HttpStatus.BAD_REQUEST.value()); + return; + } + + try { + // 安全过滤 + filePath = filePath.replace("..", "").replace("../", ""); + if (filePath.endsWith(",")) { + filePath = filePath.substring(0, filePath.length() - 1); + } + + // 安全检查 + SsrfFileTypeFilter.checkDownloadFileType(filePath); + + // 获取完整文件路径 + String fullPath = uploadpath + File.separator + filePath; + File file = new File(fullPath); + + if (!file.exists()) { + response.setStatus(HttpStatus.NOT_FOUND.value()); + log.error("文件[" + filePath + "]不存在"); + return; + } + + // 获取请求头信息 + String rangeHeader = request.getHeader("Range"); + long fileLength = file.length(); + + // 根据文件扩展名设置 Content-Type + String contentType = getContentType(filePath); + response.setContentType(contentType); + + // 如果是视频文件,支持范围请求 + if (isVideoFile(filePath) && rangeHeader != null && rangeHeader.startsWith("bytes=")) { + // 处理视频流范围请求 + handleRangeRequest(file, rangeHeader, fileLength, response); + } else { + // 普通文件下载(支持音频、图片等) + response.setContentType(contentType); + response.setHeader("Content-Disposition", "inline; filename=\"" + + new String(file.getName().getBytes("UTF-8"), "ISO-8859-1") + "\""); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Content-Length", String.valueOf(fileLength)); + + try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); + OutputStream outputStream = response.getOutputStream()) { + + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + } + } + + } catch (IOException e) { + log.error("处理文件失败: " + filePath, e); + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); + } catch (Exception e) { + log.error("预览文件失败", e); + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); + } + } + + /** + * 处理视频范围请求(支持断点续传) + */ + private void handleRangeRequest(File file, String rangeHeader, long fileLength, + HttpServletResponse response) throws IOException { + + // 解析范围请求 + String[] ranges = rangeHeader.substring(6).split("-"); + long start = Long.parseLong(ranges[0]); + long end = (ranges.length > 1 && !ranges[1].isEmpty()) ? + Long.parseLong(ranges[1]) : fileLength - 1; + + // 确保范围有效 + if (start < 0) start = 0; + if (end >= fileLength) end = fileLength - 1; + + long contentLength = end - start + 1; + + // 设置响应头 + response.setStatus(HttpStatus.PARTIAL_CONTENT.value()); + response.setHeader("Content-Range", String.format("bytes %d-%d/%d", start, end, fileLength)); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Content-Length", String.valueOf(contentLength)); + + try (RandomAccessFile raf = new RandomAccessFile(file, "r"); + OutputStream outputStream = response.getOutputStream()) { + + byte[] buffer = new byte[8192]; + raf.seek(start); + + long bytesToRead = contentLength; + int bytesRead; + + while (bytesToRead > 0 && + (bytesRead = raf.read(buffer, 0, (int) Math.min(buffer.length, bytesToRead))) != -1) { + outputStream.write(buffer, 0, bytesRead); + bytesToRead -= bytesRead; + } + } + } + + /** + * 根据文件扩展名获取 Content-Type + */ + private String getContentType(String filePath) { + String extension = getFileExtension(filePath).toLowerCase(); + + Map contentTypeMap = new HashMap<>(); + contentTypeMap.put("mp4", "video/mp4"); + contentTypeMap.put("webm", "video/webm"); + contentTypeMap.put("ogg", "video/ogg"); + contentTypeMap.put("avi", "video/x-msvideo"); + contentTypeMap.put("mov", "video/quicktime"); + contentTypeMap.put("wmv", "video/x-ms-wmv"); + contentTypeMap.put("flv", "video/x-flv"); + contentTypeMap.put("mkv", "video/x-matroska"); + + // 音频文件 + contentTypeMap.put("mp3", "audio/mpeg"); + contentTypeMap.put("wav", "audio/wav"); + contentTypeMap.put("ogg", "audio/ogg"); + contentTypeMap.put("aac", "audio/aac"); + contentTypeMap.put("flac", "audio/flac"); + + // 图片文件 + contentTypeMap.put("jpg", "image/jpeg"); + contentTypeMap.put("jpeg", "image/jpeg"); + contentTypeMap.put("png", "image/png"); + contentTypeMap.put("gif", "image/gif"); + contentTypeMap.put("bmp", "image/bmp"); + contentTypeMap.put("webp", "image/webp"); + contentTypeMap.put("svg", "image/svg+xml"); + + // 文档文件 + contentTypeMap.put("pdf", "application/pdf"); + contentTypeMap.put("txt", "text/plain"); + contentTypeMap.put("html", "text/html"); + + return contentTypeMap.getOrDefault(extension, "application/octet-stream"); + } + + /** + * 判断是否为视频文件 + */ + private boolean isVideoFile(String filePath) { + String extension = getFileExtension(filePath).toLowerCase(); + Set videoExtensions = Set.of( + "mp4", "webm", "ogg", "avi", "mov", + "wmv", "flv", "mkv", "m3u8", "ts" + ); + return videoExtensions.contains(extension); + } + + /** + * 判断是否为音频文件 + */ + private boolean isAudioFile(String filePath) { + String extension = getFileExtension(filePath).toLowerCase(); + Set audioExtensions = Set.of( + "mp3", "wav", "ogg", "aac", "flac", + "m4a", "wma", "opus" + ); + return audioExtensions.contains(extension); + } + + /** + * 判断是否为图片文件 + */ + private boolean isImageFile(String filePath) { + String extension = getFileExtension(filePath).toLowerCase(); + Set imageExtensions = Set.of( + "jpg", "jpeg", "png", "gif", "bmp", + "webp", "svg", "ico", "tiff" + ); + return imageExtensions.contains(extension); + } + + /** + * 获取文件扩展名 + */ + private String getFileExtension(String filePath) { + int dotIndex = filePath.lastIndexOf('.'); + if (dotIndex > 0 && dotIndex < filePath.length() - 1) { + return filePath.substring(dotIndex + 1); + } + return ""; + } +}