From f25afe0e9da0ba11194a3489680384e078698250 Mon Sep 17 00:00:00 2001 From: DragonWenLong <552045633@qq.com> Date: Fri, 7 Nov 2025 16:59:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(file):=20=E6=96=B0=E5=A2=9E=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=93=88=E5=B8=8C=E5=8E=BB=E9=87=8D=E4=B8=8E=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E6=8F=90=E5=8F=96=E5=8A=9F=E8=83=BD-=20=E5=9C=A8?= =?UTF-8?q?=E5=A4=9A=E4=B8=AA=E6=A8=A1=E5=9D=97=E4=B8=AD=E5=BC=95=E5=85=A5?= =?UTF-8?q?=20FileHashUtil=20=E5=B9=B6=E7=94=A8=E4=BA=8E=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=89=8D=E7=9A=84=E5=93=88=E5=B8=8C=E8=AE=A1?= =?UTF-8?q?=E7=AE=97=20-=20=E4=BC=98=E5=8C=96=E6=96=87=E4=BB=B6=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E9=80=BB=E8=BE=91=EF=BC=8C=E5=AE=9E=E7=8E=B0=E5=9F=BA?= =?UTF-8?q?=E4=BA=8E=E5=93=88=E5=B8=8C=E7=9A=84=E7=A7=92=E4=BC=A0=E6=9C=BA?= =?UTF-8?q?=E5=88=B6=20-=20=E6=96=B0=E5=A2=9E=E9=9F=B3=E9=A2=91=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E4=B8=AD=E7=9A=84=E6=96=87=E6=9C=AC=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E6=96=B9=E6=B3=95=EF=BC=8C=E6=94=AF=E6=8C=81=20txt=20=E5=92=8C?= =?UTF-8?q?=20docx=20=E6=A0=BC=E5=BC=8F=20-=20=E4=BD=BF=E7=94=A8=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=E8=A7=A3=E6=9E=90=E6=8A=80=E6=9C=AF=E5=A4=84=E7=90=86?= =?UTF-8?q?=E5=A4=A7=E6=96=87=E4=BB=B6=E5=86=85=E5=AE=B9=EF=BC=8C=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E5=86=85=E5=AD=98=E6=BA=A2=E5=87=BA=20-=E4=B8=BA=20Ap?= =?UTF-8?q?pVideoController=20=E6=B7=BB=E5=8A=A0=20/extract=20=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E7=94=A8=E4=BA=8E=E6=96=87=E6=9C=AC=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E6=8F=90=E5=8F=96=20-=20=E5=AE=8C=E5=96=84=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=93=88=E5=B8=8C=E5=B7=A5=E5=85=B7=E7=B1=BB=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E7=BA=BF=E7=A8=8B=E5=AE=89=E5=85=A8=E6=80=A7=E4=B8=8E?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86=20-=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=20SysOssService=20=E7=9A=84=20updateHash=20=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E4=BB=A5=E6=94=AF=E6=8C=81=E5=A4=8D=E7=94=A8=E9=80=BB=E8=BE=91?= =?UTF-8?q?-=20=E7=BB=9F=E4=B8=80=E6=9E=84=E5=BB=BA=20SysOssVo=20=E5=AE=9E?= =?UTF-8?q?=E4=BD=93=E6=97=B6=E7=9A=84=E5=93=88=E5=B8=8C=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/controller/AppVideoController.java | 10 +++ .../app/service/AudioProcessService.java | 80 ++++++++++++++++++- .../service/device/DeviceDebugService.java | 25 ++---- .../impl/DeviceRepairRecordsServiceImpl.java | 11 +-- .../service/impl/DeviceServiceImpl.java | 9 ++- .../equipment/utils/FileHashUtil.java | 48 ++++++++++- .../system/service/ISysOssService.java | 8 ++ .../service/impl/SysOssServiceImpl.java | 35 +++++++- 8 files changed, 187 insertions(+), 39 deletions(-) diff --git a/fys-admin/src/main/java/com/fuyuanshen/app/controller/AppVideoController.java b/fys-admin/src/main/java/com/fuyuanshen/app/controller/AppVideoController.java index 11bc754..f48a1a0 100644 --- a/fys-admin/src/main/java/com/fuyuanshen/app/controller/AppVideoController.java +++ b/fys-admin/src/main/java/com/fuyuanshen/app/controller/AppVideoController.java @@ -1,5 +1,6 @@ package com.fuyuanshen.app.controller; +import cn.dev33.satoken.annotation.SaIgnore; import com.fuyuanshen.app.service.AudioProcessService; import com.fuyuanshen.app.service.VideoProcessService; import com.fuyuanshen.common.core.domain.R; @@ -51,4 +52,13 @@ public class AppVideoController extends BaseController { public R> uploadAudioTTS(@RequestParam String text) throws IOException { return R.ok(audioProcessService.generateStandardPcmData(text)); } + + /** + * 提取文本内容(只支持txt/docx) + */ + @PostMapping(value = "/extract", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @RepeatSubmit(interval = 2, timeUnit = TimeUnit.SECONDS,message = "请勿重复提交!") + public R extract(@RequestParam("file") MultipartFile file) throws Exception { + return R.ok("Success",audioProcessService.extract(file)); + } } diff --git a/fys-admin/src/main/java/com/fuyuanshen/app/service/AudioProcessService.java b/fys-admin/src/main/java/com/fuyuanshen/app/service/AudioProcessService.java index a36e8f1..0415b19 100644 --- a/fys-admin/src/main/java/com/fuyuanshen/app/service/AudioProcessService.java +++ b/fys-admin/src/main/java/com/fuyuanshen/app/service/AudioProcessService.java @@ -7,11 +7,17 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; -import java.io.File; -import java.io.IOException; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import java.io.*; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.Arrays; import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; /** * 音频处理服务 @@ -170,5 +176,75 @@ public class AudioProcessService { } } + /** + * 提取文本 + */ + public String extract(MultipartFile file) throws Exception { + String name = file.getOriginalFilename(); + if (name == null || + (!name.endsWith(".txt") && !name.endsWith(".docx"))) { + throw new IllegalArgumentException("仅支持 .txt 或 .docx"); + } + if (file.getSize() > MAX_AUDIO_SIZE) { + throw new IllegalArgumentException("文件超过5MB"); + } + + String text; + /* 全程流式,不落地磁盘,不一次性读字节数组 */ + try (InputStream in = file.getInputStream()) { + if (name.endsWith(".txt")) { + text = readTxt(in); + } else { + text = readDocx(in); + } + } + return text; + } + + /* ---------- txt:按行读,StringBuilder 复用 ---------- */ + private String readTxt(InputStream in) throws IOException { + BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(4096); + String line; + while ((line = br.readLine()) != null) { + sb.append(line).append('\n'); + } + return sb.toString(); + } + + /* ---------- docx:ZipInputStream 只扫 document.xml ---------- */ + private String readDocx(InputStream in) throws IOException { + ZipInputStream zin = new ZipInputStream(in); + ZipEntry e; + while ((e = zin.getNextEntry()) != null) { + if ("word/document.xml".equals(e.getName())) { + return staxExtract(zin); // 流式读 XML + } + } + return ""; + } + + /* ---------- StAX 流式提取 ---------- */ + private String staxExtract(InputStream xml) throws IOException { + XMLStreamReader r = null; + StringBuilder sb = new StringBuilder(4096); + try { + //System.out.println(new String(xml.readAllBytes())); + r = XMLInputFactory.newInstance().createXMLStreamReader(xml); + while (r.hasNext()) { + if (r.next() == XMLStreamConstants.START_ELEMENT && + "t".equals(r.getLocalName())) { + String elementText = r.getElementText(); + sb.append(elementText); + } + } + } catch (XMLStreamException ex) { + throw new IOException(ex); + } finally { + if (r != null) try { r.close(); } catch (XMLStreamException ignore) {} + } + return sb.toString(); + } + } \ No newline at end of file diff --git a/fys-admin/src/main/java/com/fuyuanshen/web/service/device/DeviceDebugService.java b/fys-admin/src/main/java/com/fuyuanshen/web/service/device/DeviceDebugService.java index aa8fa6b..afea485 100644 --- a/fys-admin/src/main/java/com/fuyuanshen/web/service/device/DeviceDebugService.java +++ b/fys-admin/src/main/java/com/fuyuanshen/web/service/device/DeviceDebugService.java @@ -39,6 +39,7 @@ public class DeviceDebugService { private final IAppBusinessFileService appBusinessFileService; private final IAppOperationVideoService appOperationVideoService; private final DeviceService deviceService; + private final FileHashUtil fileHashUtil; /** * 文件上传并添加文件信息哈希去重 @@ -62,26 +63,12 @@ public class DeviceDebugService { Map hash2OssId = new LinkedHashMap<>(files.length); for (MultipartFile file : files) { // 1. 计算文件哈希 - String hash = FileHashUtil.hash(file); + String hash = fileHashUtil.hash(file); - // 2. 先根据 hash 查库(秒传) - SysOssVo exist = sysOssService.selectByHash(hash); - Long ossId; - if (exist != null) { - // 2.1 已存在,直接复用 - ossId = exist.getOssId(); - hash2OssId.putIfAbsent(hash, ossId); - } else { - // 2.2 不存在,真正上传 - SysOssVo upload = sysOssService.upload(file); - if (upload == null) { - return false; - } - ossId = upload.getOssId(); - hash2OssId.putIfAbsent(hash, ossId); - // 2.3 把 hash 写回记录(供下次去重) - sysOssService.updateHashById(ossId, hash); - } + SysOssVo exist = sysOssService.updateHash(file, hash); + // 2.1 已存在,直接复用 + long ossId = exist.getOssId(); + hash2OssId.putIfAbsent(hash, ossId); } // 4. 组装业务中间表 List bizList = new ArrayList<>(bo.getDeviceIds().length * hash2OssId.size()); diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/impl/DeviceRepairRecordsServiceImpl.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/impl/DeviceRepairRecordsServiceImpl.java index 80ff0fe..ecc7007 100644 --- a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/impl/DeviceRepairRecordsServiceImpl.java +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/service/impl/DeviceRepairRecordsServiceImpl.java @@ -50,6 +50,7 @@ public class DeviceRepairRecordsServiceImpl extends ServiceImpl impleme private final DeviceTypeGrantsMapper deviceTypeGrantsMapper; private final DeviceFenceAccessRecordMapper deviceFenceAccessRecordMapper; + private final FileHashUtil fileHashUtil; /** @@ -209,7 +211,8 @@ public class DeviceServiceImpl extends ServiceImpl impleme // 保存图片并获取URL if (deviceForm.getFile() != null) { - SysOssVo upload = ossService.upload(deviceForm.getFile()); + String fileHash = fileHashUtil.hash(deviceForm.getFile()); + SysOssVo upload = ossService.updateHash(deviceForm.getFile(),fileHash); // 设置图片路径 deviceForm.setDevicePic(upload.getUrl()); } @@ -283,8 +286,8 @@ public class DeviceServiceImpl extends ServiceImpl impleme // 处理上传的图片 if (deviceForm.getFile() != null) { - // 设置图片路径 - SysOssVo oss = ossService.upload(deviceForm.getFile()); + String fileHash = fileHashUtil.hash(deviceForm.getFile()); + SysOssVo oss = ossService.updateHash(deviceForm.getFile(),fileHash); // 强制将HTTP替换为HTTPS if (oss.getUrl() != null && oss.getUrl().startsWith("http://")) { oss.setUrl(oss.getUrl().replaceFirst("^http://", "https://")); diff --git a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/utils/FileHashUtil.java b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/utils/FileHashUtil.java index 77011b8..1e6666f 100644 --- a/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/utils/FileHashUtil.java +++ b/fys-modules/fys-equipment/src/main/java/com/fuyuanshen/equipment/utils/FileHashUtil.java @@ -1,28 +1,70 @@ package com.fuyuanshen.equipment.utils; import org.apache.commons.codec.digest.DigestUtils; +import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.io.InputStream; import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.HexFormat; /** * 文件哈希工具类 */ +@Component // 如果使用 Spring 可以注入 public class FileHashUtil { + /* 算法常量 */ private static final String ALGORITHM = "SHA-256"; + /* 缓冲区大小 8 KB */ + private static final int BUFFER_SIZE = 8192; - public static String hash(MultipartFile file) throws IOException { - MessageDigest digest = DigestUtils.getDigest(ALGORITHM); + /** + * 计算上传文件的 SHA-256 十六进制哈希 + * + * @param file 上传文件;不能为 null,且必须非空 + * @return 64 位小写十六进制字符串 + * @throws IllegalArgumentException 参数不合法 + * @throws IOException 流读取失败 + * @throws IllegalStateException 算法运行时异常(不会触发) + */ + public String hash(MultipartFile file) throws IOException { + validate(file); + + /* 每个请求新建实例,保证线程安全 */ + MessageDigest digest = newDigest(); + + /* try-with-resources 自动关闭流 */ try (InputStream in = file.getInputStream()) { - byte[] buf = new byte[8192]; + byte[] buf = new byte[BUFFER_SIZE]; int len; while ((len = in.read(buf)) != -1) { digest.update(buf, 0, len); } } + + /* JDK 17+ 的 HexFormat,比 Apache Commons 更快且无需额外依赖 */ return HexFormat.of().formatHex(digest.digest()); } + + /* -------------------- 私有辅助方法 -------------------- */ + + private static void validate(MultipartFile file) { + if (file == null) { + throw new IllegalArgumentException("MultipartFile 不能为 null"); + } + if (file.isEmpty()) { + throw new IllegalArgumentException("上传文件不能为空"); + } + } + + private static MessageDigest newDigest() { + try { + return MessageDigest.getInstance(ALGORITHM); + } catch (NoSuchAlgorithmException e) { + /* SHA-256 是 JDK 必现算法,走到这里说明 JDK 实现损坏 */ + throw new IllegalStateException("算法 " + ALGORITHM + " 不可用", e); + } + } } diff --git a/fys-modules/fys-system/src/main/java/com/fuyuanshen/system/service/ISysOssService.java b/fys-modules/fys-system/src/main/java/com/fuyuanshen/system/service/ISysOssService.java index 32a0a3e..8adfcda 100644 --- a/fys-modules/fys-system/src/main/java/com/fuyuanshen/system/service/ISysOssService.java +++ b/fys-modules/fys-system/src/main/java/com/fuyuanshen/system/service/ISysOssService.java @@ -60,6 +60,14 @@ public interface ISysOssService { */ int updateHashById(long ossId,String fileHash); + /** + * 更新文件 hash 值 + * + * @param file 文件对象 + * @return 匹配的 SysOssVo 列表 + */ + SysOssVo updateHash(MultipartFile file, String hash); + /** * 上传 MultipartFile 到对象存储服务,并保存文件信息到数据库 * diff --git a/fys-modules/fys-system/src/main/java/com/fuyuanshen/system/service/impl/SysOssServiceImpl.java b/fys-modules/fys-system/src/main/java/com/fuyuanshen/system/service/impl/SysOssServiceImpl.java index 80f1e6b..bcadabb 100644 --- a/fys-modules/fys-system/src/main/java/com/fuyuanshen/system/service/impl/SysOssServiceImpl.java +++ b/fys-modules/fys-system/src/main/java/com/fuyuanshen/system/service/impl/SysOssServiceImpl.java @@ -191,6 +191,32 @@ public class SysOssServiceImpl implements ISysOssService, OssService { storage.download(sysOss.getFileName(), response.getOutputStream(), response::setContentLengthLong); } + /** + * 上传 MultipartFile 到对象存储服务,并保存文件信息到数据库 + * + * @param file 要上传的 MultipartFile 对象 + * @return 保存到数据库的 SysOssVo 对象 + */ + @Override + public SysOssVo updateHash(MultipartFile file, String hash) { + // 2. 先根据 hash 查库(秒传) + SysOssVo exist = baseMapper.selectByHash(hash); + if (exist != null) { + return exist; + } + String originalfileName = file.getOriginalFilename(); + String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length()); + OssClient storage = OssFactory.instance(); + UploadResult uploadResult; + try { + uploadResult = storage.uploadSuffix(file.getBytes(), suffix, file.getContentType()); + } catch (IOException e) { + throw new ServiceException(e.getMessage()); + } + // 保存文件信息 + return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult,hash); + } + /** * 上传 MultipartFile 到对象存储服务,并保存文件信息到数据库 * @@ -210,7 +236,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService { throw new ServiceException(e.getMessage()); } // 保存文件信息 - return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult); + return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult,null); } /** @@ -226,7 +252,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService { OssClient storage = OssFactory.instance(); UploadResult uploadResult = storage.uploadSuffix(file, suffix); // 保存文件信息 - return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult); + return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult,null); } @@ -255,18 +281,19 @@ public class SysOssServiceImpl implements ISysOssService, OssService { uploadResult = storage.uploadSuffix(data, suffix, "image/jpeg"); // 假设是图片类型,可以根据实际需要修改 // 保存文件信息 - return buildResultEntity(fileName, suffix, storage.getConfigKey(), uploadResult); + return buildResultEntity(fileName, suffix, storage.getConfigKey(), uploadResult,null); } @NotNull - private SysOssVo buildResultEntity(String originalfileName, String suffix, String configKey, UploadResult uploadResult) { + private SysOssVo buildResultEntity(String originalfileName, String suffix, String configKey, UploadResult uploadResult, String hash) { SysOss oss = new SysOss(); oss.setUrl(uploadResult.getUrl()); oss.setFileSuffix(suffix); oss.setFileName(uploadResult.getFilename()); oss.setOriginalName(originalfileName); oss.setService(configKey); + oss.setFileHash(hash); // 设置哈希值 baseMapper.insert(oss); SysOssVo sysOssVo = MapstructUtils.convert(oss, SysOssVo.class); return this.matchingUrl(sysOssVo);