diff --git a/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/java/apijson/ExcelUtil.java b/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/java/apijson/ExcelUtil.java index 545a2e68..3a8fa1f1 100644 --- a/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/java/apijson/ExcelUtil.java +++ b/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/java/apijson/ExcelUtil.java @@ -329,7 +329,7 @@ private static List> prepareData(List detailItems, int detailRow.add(item.getCorrectCount()); detailRow.add(item.getFalsePositiveCount()); // G-M: 公式计算列 (注意所有列号都已更新) - detailRow.add(createFormulaCell(String.format("D%d-E%d", currentRow, currentRow))); // G: 漏检数 + detailRow.add(createFormulaCell(String.format("C%d-D%d", currentRow, currentRow))); // G: 漏检数 // detailRow.add(createFormulaCell(String.format("IFERROR(E%d/D%d,0)", currentRow, currentRow))); // H: 召回率 // detailRow.add(createFormulaCell(String.format("IFERROR(E%d/(E%d+F%d),0)", currentRow, currentRow, currentRow))); // I: 精准率 // detailRow.add(createFormulaCell(String.format("IFERROR(2*I%d*H%d/(I%d+H%d),0)", currentRow, currentRow, currentRow, currentRow))); // J: F1 Score diff --git a/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/java/apijson/boot/FileController.java b/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/java/apijson/boot/FileController.java index 7c15ea5f..a4a1df20 100755 --- a/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/java/apijson/boot/FileController.java +++ b/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/java/apijson/boot/FileController.java @@ -1,13 +1,14 @@ package apijson.boot; import java.io.*; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; +import java.net.URLEncoder; +import java.text.DateFormat; +import java.util.*; //import javax.annotation.PostConstruct; +import apijson.ExcelUtil; +import apijson.StringUtil; import org.apache.commons.io.FileUtils; import org.springframework.core.io.InputStreamResource; import org.springframework.http.HttpHeaders; @@ -25,6 +26,8 @@ import apijson.demo.DemoParser; +import static com.google.common.io.Files.getFileExtension; + /**文件相关的控制器,包括上传、下载、浏览等 * @author : ramostear * @modifier : Lemon @@ -104,18 +107,21 @@ public boolean accept(File file) { @ResponseBody public JSONObject upload(@RequestParam("file") MultipartFile file) { try { - File convertFile = new File(fileUploadRootDir + file.getOriginalFilename()); + String name = file.getOriginalFilename(); + name = (StringUtil.isEmpty(name) ? DateFormat.getDateInstance().format(new Date()) : name) + .replaceAll("[^a-zA-Z0-9._-]", String.valueOf(Math.round(1100*Math.random()))); + File convertFile = new File(fileUploadRootDir + name); FileOutputStream fileOutputStream; fileOutputStream = new FileOutputStream(convertFile); fileOutputStream.write(file.getBytes()); fileOutputStream.close(); if (fileNames != null && ! fileNames.isEmpty()) { - fileNames.add(file.getOriginalFilename()); + fileNames.add(name); } JSONObject res = new JSONObject(); - res.put("path", "/download/" + file.getOriginalFilename()); + res.put("path", "/download/" + name); res.put("size", file.getBytes().length); return new DemoParser().extendSuccessResult(res); } @@ -132,26 +138,62 @@ public ResponseEntity download(@PathVariable(name = "fileName") String f File file = new File(fileUploadRootDir + fileName); InputStreamResource resource = new InputStreamResource(new FileInputStream(file)); + String encodedFileName = fileName; + try { + encodedFileName = URLEncoder.encode(fileName, StringUtil.UTF_8); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + HttpHeaders headers = new HttpHeaders(); - headers.add("Content-Disposition", String.format("attachment;filename=\"%s", fileName)); - headers.add("Cache-Control", "no-cache,no-store,must-revalidate"); - headers.add("Pragma", "no-cache"); - headers.add("Expires", "0"); + headers.add("Content-Disposition", String.format("attachment;filename=\"%s;filename*=UTF_8''%s", fileName, encodedFileName)); + headers.add("Cache-Control", "public, max-age=86400"); +// headers.add("Cache-Control", "no-cache,no-store,must-revalidate"); +// headers.add("Pragma", "no-cache"); +// headers.add("Expires", "0"); ResponseEntity responseEntity = ResponseEntity.ok() .headers(headers) .contentLength(file.length()) - .contentType(MediaType.parseMediaType("application/txt")) + .contentType(determineContentType(fileName)) .body(resource); return responseEntity; } + private MediaType determineContentType(String fileName) { + String extension = getFileExtension(fileName).toLowerCase(); + switch (extension) { + case "jpg": + case "jpeg": + return MediaType.IMAGE_JPEG; + case "png": + return MediaType.IMAGE_PNG; + case "gif": + return MediaType.IMAGE_GIF; + case "pdf": + return MediaType.APPLICATION_PDF; + case "json": + return MediaType.APPLICATION_JSON; + case "xml": + return MediaType.APPLICATION_XML; + case "txt": + return MediaType.TEXT_PLAIN; + case "css": + return MediaType.valueOf("text/css"); + case "js": + return MediaType.valueOf("application/javascript"); + default: + return MediaType.APPLICATION_OCTET_STREAM; + } + } + @GetMapping("/download/report/{reportId}/cv") @ResponseBody public ResponseEntity downloadCVReport(@PathVariable(name = "reportId") String reportId) throws FileNotFoundException, IOException { String name = "CVAuto_report_" + reportId + ".xlsx"; - File file = new File(fileUploadRootDir + name); + String path = fileUploadRootDir + name; + File file = new File(path); long size = file.exists() ? file.length() : 0; if (size < 10*1024) { file.delete(); @@ -159,13 +201,13 @@ public ResponseEntity downloadCVReport(@PathVariable(name = "reportId") if ((file.exists() ? file.length() : 0) < 10*1024) { String filePath = ExcelUtil.newCVAutoReportWithTemplate(fileUploadRootDir, name); - if (! Objects.equals(filePath, fileUploadRootDir + name)) { + if (! Objects.equals(filePath, path)) { try { File sourceFile = new File(filePath); - File destFile = new File(fileUploadRootDir + name); + File destFile = new File(path); if (! destFile.getAbsolutePath().equals(sourceFile.getAbsolutePath())) { FileUtils.copyFile(sourceFile, destFile); - System.out.println("文件复制完成 (Commons IO): " + filePath + " -> " + fileUploadRootDir + name); + System.out.println("文件复制完成 (Commons IO): " + filePath + " -> " + path); } } catch (IOException e) { e.printStackTrace(); diff --git a/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/README.md b/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/README.md index ad6515f9..7539f3fc 100755 --- a/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/README.md +++ b/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/README.md @@ -2,7 +2,7 @@ CVAuto -

👁 零代码零标注 CV AI 自动化测试平台 🚀
零代码快速自动化测试 CV 计算机视觉 AI 人工智能图像识别算法的功能、效果、性能
适合 算法测试 工程师/专家、AI/机器学习/算法 工程师/专家/研究员/科学家、算法 应用/开发 工程师/专家 等

+

👁 零代码零标注 CV AI 自动化测试平台 🚀
零代码快速自动化测试 CV 计算机视觉 AI 人工智能图像识别算法的功能、效果、性能
适合 算法 应用/开发 工程师/专家、算法测试 工程师/专家、AI/机器学习/算法 工程师/专家/研究员/科学家 等

English @@ -212,27 +212,23 @@ https://github.com/TommyLemon/APIAuto/issues

-### 其它项目 +### 生态项目 -[APIJSON](https://github.com/Tencent/APIJSON) 🚀 腾讯零代码、全功能、强安全 ORM 库 🏆 后端接口和文档零代码,前端(客户端) 定制返回 JSON 的数据和结构 +[APIJSON](https://github.com/Tencent/APIJSON) 🏆 腾讯实时 零代码、全功能、强安全 ORM 库 🚀 后端接口和文档零代码,前端(客户端) 定制返回 JSON 的数据和结构 -[APIAuto](https://github.com/TommyLemon/APIAuto) 敏捷开发最强大易用的 HTTP 接口工具,机器学习零代码测试、生成代码与静态检查、生成文档与光标悬浮注释,集 文档、测试、Mock、调试、管理 于一体的一站式体验 +[APIAuto](https://github.com/TommyLemon/APIAuto) ☔ 敏捷开发最强大易用的接口工具,零代码测试与 AI 问答、生成代码与静态检查、生成文档与光标悬浮注释,腾讯、华为、SHEIN、传音、工行等使用 -[UnitAuto](https://github.com/TommyLemon/UnitAuto) 机器学习单元测试平台,零代码、全方位、自动化 测试 方法/函数 的正确性和可用性 +[UnitAuto](https://github.com/TommyLemon/UnitAuto) ☀️ 最先进、最省事、ROI 最高的单元测试,零代码、全方位、自动化 测试 方法/函数,用户包含腾讯、快手、某 500 强巨头等 -[SQLAuto](https://github.com/TommyLemon/SQLAuto) 智能零代码自动化测试 SQL 语句执行结果的数据库工具,任意增删改查、任意 SQL 模板变量、一键批量生成参数组合、快速构造大量测试数据 - -[UIGO](https://github.com/TommyLemon/UIGO) 📱 零代码快准稳 UI 智能录制回放平台 🚀 自动兼容任意宽高比分辨率屏幕,自动精准等待网络请求,录制回放快、准、稳! +[SQLAuto](https://github.com/TommyLemon/SQLAuto) 智能零代码自动化测试 SQL 数据库工具,任意增删改查、任意 SQL 模板变量、一键批量生成参数组合、快速构造大量测试数据 +[UIGO](https://github.com/TommyLemon/UIGO) 📱 零代码快准稳 UI 智能录制回放平台 🚀 3 像素内自动精准定位,2 毫秒内自动精准等待,用户包含腾讯,微信团队邀请分享 ### 持续更新 https://github.com/TommyLemon/CVAuto/commits - ### 我要赞赏 -**创作不易、坚持更难,右上角点亮 ⭐ Star 支持/收藏下本项目吧,谢谢 ^_^**
+**创作不易、坚持更难,右上角点亮 ⭐ Star 收藏/支持下本项目吧,谢谢 ^_^**
https://github.com/TommyLemon/CVAuto - - diff --git a/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/apijson/FileUtil.js b/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/apijson/FileUtil.js new file mode 100644 index 00000000..ec59031c --- /dev/null +++ b/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/apijson/FileUtil.js @@ -0,0 +1,49 @@ +/*Copyright ©2025 TommyLemon(https://github.com/TommyLemon/CVAuto) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use FileUtil file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License.*/ + + +/**util for string + * @author Lemon + */ +var FileUtil = { + TAG: 'FileUtil', + + /** + * 抽取某一帧,输出 Blob + */ + captureFrame: function(video, time) { + return new Promise((resolve) => { + const v = document.createElement("video"); + v.src = video.src; + v.currentTime = time; + + v.onseeked = () => { + const canvas = document.createElement("canvas"); + const w = video.videoWidth; + const h = video.videoHeight; + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext("2d"); + ctx.drawImage(v, 0, 0, w, h); + canvas.toBlob((blob) => resolve(blob), "image/jpeg", 0.8); + }; + }); + }, +}; + +if (typeof module == 'object') { + module.exports = FileUtil; +} + +//校正(自动补全等)字符串>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> diff --git a/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/debug.log b/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/debug.log deleted file mode 100644 index 3439ed6f..00000000 --- a/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/debug.log +++ /dev/null @@ -1,6 +0,0 @@ -[0218/150154:ERROR:tcp_listen_socket.cc(76)] Could not bind socket to 127.0.0.1:6004 -[0218/150154:ERROR:node_debugger.cc(86)] Cannot start debugger server -[0221/153307:ERROR:tcp_listen_socket.cc(76)] Could not bind socket to 127.0.0.1:6004 -[0221/153307:ERROR:node_debugger.cc(86)] Cannot start debugger server -[0223/144750:ERROR:tcp_listen_socket.cc(76)] Could not bind socket to 127.0.0.1:6004 -[0223/144751:ERROR:node_debugger.cc(86)] Cannot start debugger server diff --git a/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/index.html b/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/index.html index 434f3770..9cfa4fd4 100644 --- a/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/index.html +++ b/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/index.html @@ -89,7 +89,7 @@ 2s - + @@ -509,7 +509,7 @@

  • - {{ Math.round(100*((item.Random || {}).size || 0)/8/1024/1024)/100 + 'M ' + ((item.Random || {}).width || '') + "x" + ((item.Random || {}).height || '') }} + {{ Math.round(100*((item.Random || {}).size || 0)/1024/1024)/100 + 'M ' + ((item.Random || {}).width || '') + "x" + ((item.Random || {}).height || '') }}
    @@ -1080,6 +1080,7 @@ + diff --git a/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/js/main.js b/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/js/main.js index 26a66182..af51eec5 100644 --- a/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/js/main.js +++ b/APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/js/main.js @@ -47,6 +47,7 @@ var Vue = require('./vue.min'); // 某些版本不兼容 require('vue'); var StringUtil = require('../apijson/StringUtil'); var CodeUtil = require('../apijson/CodeUtil'); + var FileUtil = require('../apijson/FileUtil'); var JSONObject = require('../apijson/JSONObject'); var JSONResponse = require('../apijson/JSONResponse'); var JSONRequest = require('../apijson/JSONRequest'); @@ -6784,6 +6785,8 @@ https://github.com/Tencent/APIJSON/issues const doc = (this.currentRemoteItem || {}).Document || {} const random = cri.Random || {} const ind = isSub && this.currentRandomSubIndex != null ? this.currentRandomSubIndex : this.currentRandomIndex; + const index = ind != null && ind >= 0 ? ind : -1; // items.length; + const server = this.server var selectedFiles = Array.from(event.target.files); // const previewList = document.getElementById('previewList'); @@ -6791,90 +6794,155 @@ https://github.com/Tencent/APIJSON/issues selectedFiles.forEach((file, i) => { const reader = new FileReader(); - reader.onload = (e) => { + reader.onload = (evt) => { // const img = document.createElement('img'); - // img.src = e.target.result; + // img.src = evt.target.result; // img.style.height = '100%'; // img.style.margin = '1px'; // previewList.appendChild(img); - const index = ind != null && ind >= 0 ? ind : -1; // items.length; - var item = JSONResponse.deepMerge({ - Random: { - id: -(index || 0) - 1, //表示未上传 - toId: random.id, - userId: random.userId || doc.userId, - documentId: random.documentId || doc.id, - count: 1, - name: '分析位于 ' + index + ' 的这张图片', - img: e.target.result, - config: '' + function callback(file, img, name, size, width, height, index, rank) { + + var item = JSONResponse.deepMerge({ + Random: { + id: -(index || 0) - 1, //表示未上传 + toId: random.id, + userId: random.userId || doc.userId, + documentId: random.documentId || doc.id, + count: 1, + name: '分析位于 ' + index + ' 的这张图片', + img: img, + config: '' + } + }, items[index] || {}); + item.status = 'uploading'; + + const r = item.Random || {}; + r.name = r.file = name; + r.size = size; + r.width = width; + r.height = height; + r.rank = rank; + + if (index < 0) { // || r.id == null || r.id <= 0) { + items.unshift(item); + } else { + items[index] = item; } - }, items[index] || {}); - item.status = 'uploading'; - const r = item.Random || {}; - r.name = r.file = file.name; - r.size = file.size; - r.width = file.width; - r.height = file.height; - - if (index < 0) { // || r.id == null || r.id <= 0) { - items.unshift(item); - } else { - items[index] = item; - } + if (isSub) { + App.randomSubs = items; + } else { + App.randoms = items; + } - if (isSub) { - this.randomSubs = items; - } - else { - this.randoms = items; - } + try { + Vue.set(items, index < 0 ? 0 : index, item); + } catch (e) { + console.error(e) + } - try { - Vue.set(items, index < 0 ? 0 : index, item); - } catch (e) { - console.error(e) - } + const formData = new FormData(); + formData.append('file', file); - const formData = new FormData(); - formData.append('file', file); + fetch(server + '/upload', { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + var path = data.path; + if (StringUtil.isEmpty(path, true) || data.size == null) { + throw new Error('上传失败!' + JSON.stringify(data || {})); + } - fetch(this.server + '/upload', { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - var path = data.path; - if (StringUtil.isEmpty(path, true) || data.size == null) { - throw new Error('上传失败!' + JSON.stringify(data || {})); - } + console.log('Upload successful:', data); + item.status = 'done'; + if (!(server.includes('localhost') || server.includes('127.0.0.1'))) { + r.img = (path.startsWith('/') ? server + path : path) || r.img; + } - console.log('Upload successful:', data); - item.status = 'done'; - if (! (App.server.includes('localhost') || App.server.includes('127.0.0.1'))) { - r.img = (path.startsWith('/') ? App.server + path : path) || r.img; - } + try { + Vue.set(items, index < 0 ? 0 : index, item); + } catch (e) { + console.error(e) + } - try { - Vue.set(items, index < 0 ? 0 : index, item); - } catch (e) { - console.error(e) - } + App.updateRandom(r) + }) + .catch(error => { + console.error('Upload failed:', error); + item.status = 'failed'; + }); + } + + if (file.type && file.type.startsWith("video/")) { + // === 视频处理 === + this.extractFramesAndUpload(file, 5, async (frameBlob, rank, totalFrames) => { + const reader2 = new FileReader(); + reader2.onload = (evt) => { // TODO 传 toId,视频作为分组,次数作为抽取帧数 + var fn = file.name + '-' + rank + '.jpg' + callback(new File([frameBlob], fn), reader2.result, fn, frameBlob.size, frameBlob.width, frameBlob.height, -1, rank) + } + reader2.readAsDataURL(frameBlob); + }); + } else { + callback(file, reader.result, file.name, file.size, file.width, file.height, index) + } - App.updateRandom(r) - }) - .catch(error => { - console.error('Upload failed:', error); - item.status = 'failed'; - }); }; reader.readAsDataURL(file); }); }, + /** + * 从视频抽帧(倒序),支持并发上传 + * @param {File} file - 视频文件 + * @param {number} concurrency - 最大并发数 + * @param {function} onFrameReady - 回调(frameBlob, rank, totalFrames) + */ + extractFramesAndUpload: function(file, concurrency, onFrameReady) { + const url = URL.createObjectURL(file); + const video = document.createElement("video"); + video.src = url; + video.preload = "metadata"; + + video.onloadedmetadata = async () => { + const duration = video.duration; + let timestamps = []; + + if (duration <= 100) { + for (let t = Math.floor(duration); t >= 0; t--) timestamps.push(t); + } else { + const step = duration / 100; + for (let i = 100; i >= 0; i--) timestamps.push(i * step); + } + + const totalFrames = timestamps.length; + let index = 0; + + async function worker() { + while (index < totalFrames) { + const i = index++; + const time = timestamps[i]; + const frameBlob = await FileUtil.captureFrame(video, time); + // rank 按倒序:0 表示最新帧,totalFrames-1 表示最早帧 + const rank = i; + onFrameReady(frameBlob, rank, totalFrames); + } + } + + // 并发执行 + const workers = []; + for (let i = 0; i < concurrency; i++) { + workers.push(worker()); + } + await Promise.all(workers); + + URL.revokeObjectURL(url); + }; + }, + uploadImage: function(randomIndex, randomSubIndex) { const isSub = randomSubIndex != null; const items = (isSub ? this.randomSubs : this.randoms) || [];