文件上传 分片上传

分片上传则是将一个大文件分割成多个小块分别上传,最后再由服务器合并成完整的文件。这种做法的好处是可以并行处理多个小文件,提高上传效率;同时,如果某一部分上传失败,只需要重传这一部分,不影响其他部分。

初步实现

后端代码

/*** 分片上传** @param file 上传的文件* @param start 文件开始上传的位置* @param fileName 文件名称* @return  上传结果*/
@PostMapping("/fragmentUpload")
@ResponseBody
public AjaxResult fragmentUpload(@RequestParam("file") MultipartFile file, @RequestParam("start") long start, @RequestParam("fileName") String fileName) {try {// 检查上传目录是否存在,如果不存在则创建File directory = new File(uploadPath);if (!directory.exists()) {directory.mkdirs();}// 设置上传文件的目标路径File targetFile = new File(uploadPath +File.separator+ fileName);// 创建 RandomAccessFile 对象以便进行文件的随机读写操作RandomAccessFile randomAccessFile = new RandomAccessFile(targetFile, "rw");// 获取 RandomAccessFile 对应的 FileChannelFileChannel channel = randomAccessFile.getChannel();// 设置文件通道的位置,即从哪里开始写入文件内容channel.position(start);// 从 MultipartFile 对象的资源通道中读取文件内容,并写入到指定位置channel.transferFrom(file.getResource().readableChannel(), start, file.getSize());// 关闭文件通道和 RandomAccessFile 对象channel.close();randomAccessFile.close();// 返回上传成功的响应return AjaxResult.success("上传成功");} catch (Exception e) {// 捕获异常并返回上传失败的响应return AjaxResult.error("上传失败");}
}/*** 检测文件是否存在* 如果文件存在,则返回已经存在的文件大小。* 如果文件不存在,则返回 0,表示前端从头开始上传该文件。* @param filename* @return*/
@GetMapping("/checkFile")
@ResponseBody
public AjaxResult checkFile(@RequestParam("filename") String filename) {File file = new File(uploadPath+File.separator + filename);if (file.exists()) {return AjaxResult.success(file.length());} else {return AjaxResult.success(0L);}
}

前端

var prefix = ctx + "/kuroshiro/file-upload";// 每次上传大小
const chunkSize = 1 * 1024 * 1024;/*** 开始上传*/
function startUpload(type) {const fileInput = document.getElementById('fileInput');const file = fileInput.files[0];if (!file) {alert("请选择文件");return;}if(type == 1){checkFile(filename).then(start => {uploadFile(file, start,Math.min(start + chunkSize, file.size));})}
}/*** 检查是否上传过* @param filename* @returns {Promise<unknown>}*/
function checkFile(filename) {return $fetch(prefix+`/checkFile?filename=${filename}`);
}/*** 开始分片上传* @param file 文件* @param start 开始位置* @param end 结束位置*/
function uploadFile(file, start,end) {if(start < end){const chunk = file.slice(start, end);const formData = new FormData();formData.append('file', chunk);formData.append('start', start);formData.append('fileName', file.name);$fetch(prefix+'/fragmentUpload', {method: 'POST',body: formData}).then(response => {console.log(`分片 ${start} - ${end} 上传成功`);// 递归调用uploadFile(file,end,Math.min(end + chunkSize, file.size))})}}function $fetch(url,requestInit){return new Promise((resolve, reject) => {fetch(url,requestInit).then(response => {if (!response.ok) {throw new Error('请求失败');}return response.json();}).then(data => {if (data.code === 0) {resolve(data.data);} else {console.error(data.msg);reject(data.msg)}}).catch(error => {console.error(error);reject(error)});});}

以上虽然实现的分片上传,但是它是某种意义上来说还是与整体上传差不多,它是一段一段的上传,某段上传失败后,后续的就不会再继续上传;不过比起整体上传来说,它会保存之前上传的内容,下一个上传时,从之前上传的位置接着上传。不用整体上传。下面进行优化。

优化

首先,之前的分片上传,后端是直接写入了一个文件中了,所以只能顺序的上传写入,虽然可以保存上传出错之前的内容,但是整体上看来是速度也不行。
优化逻辑:把分片按顺序单独保存下来,等到所有分片都上传成功后,把所有分片合并成文件。这样上传的时候就不用等着上一个上传成功才上传下一个了。

后端代码

/**
* 分片上传* @param file 文件* @param chunkIndex 分片下标*/
@PostMapping("/uploadChunk")
@ResponseBody
public AjaxResult uploadChunk(@RequestParam("file") MultipartFile file, @RequestParam("chunkIndex") int chunkIndex,@RequestParam("fileName") String fileName) {String uploadDirectory = chunkUploadPath+File.separator+fileName;File directory = new File(uploadDirectory);if (!directory.exists()||directory.isFile()) {directory.mkdirs();}String filePath = uploadDirectory + File.separator + fileName+ "_" + chunkIndex;try (OutputStream os = new FileOutputStream(filePath)) {os.write(file.getBytes());return AjaxResult.success("分片"+(chunkIndex+1)+"上传成功");}catch (Exception e){// 保存失败后如果文件建立了就删除,下次上传时重新保存,避免文件内容错误File chunkFile = new File(filePath);if(chunkFile.exists()) chunkFile.delete();e.printStackTrace();return AjaxResult.error("分片"+(chunkIndex+1)+"上传失败");}}/*** 检测分片是否存在* 如果文件存在,则返回已经存在的分片下标集合。存在的就不上传* 如果文件不存在,则返回空集合,表示前端从头开始上传该文件* @param fileName* @return*/
@GetMapping("/checkChunk")
@ResponseBody
public AjaxResult checkChunk(@RequestParam("fileName") String fileName) {String uploadDirectory = chunkUploadPath+File.separator+fileName;List<Integer> list = new ArrayList<>();File file = new File(uploadDirectory);// 文件目录不存在if(!file.exists()||file.isFile()) return AjaxResult.success(list);File[] files = file.listFiles();// 文件目录下没有分片文件if(files == null) return AjaxResult.success(list);// 返回存在分片下标集合return AjaxResult.success(Arrays.stream(files).map(item->Integer.valueOf(item.getName().substring(item.getName().lastIndexOf("_")+1))).collect(Collectors.toList()));
}// 合并文件分片@PostMapping("/mergeChunks")@ResponseBodypublic AjaxResult mergeChunks(@RequestParam("fileName") String fileName, @RequestParam("totalChunks") int totalChunks) {String uploadDirectory = chunkUploadPath+File.separator+fileName;String mergedFilePath = uploadPath +File.separator+ fileName;try (OutputStream os = new FileOutputStream(mergedFilePath, true)) {for (int i = 0; i < totalChunks; i++) {Path chunkFilePath = Paths.get(uploadDirectory +File.separator+ fileName + "_" + i);Files.copy(chunkFilePath, os);Files.delete(chunkFilePath);}return AjaxResult.success();}catch (Exception e){e.printStackTrace();return AjaxResult.error(e.getMessage());}}

前端代码

/*** 开始上传*/
function startUpload(type) {const fileInput = document.getElementById('fileInput');const file = fileInput.files[0];if (!file) {alert("请选择文件");return;}const filename = file.name;if(type == 1){checkFile(filename).then(start => {uploadFile(file, start,Math.min(start + chunkSize, file.size));})}if(type == 2){checkChunk(filename).then(arr => {uploadChunk(file, arr);})}
}/**
* 切割文件为多个分片
* @param file
* @returns {*[]}
*/
function sliceFile(file) {const chunks = [];let offset = 0;while (offset < file.size) {const chunk = file.slice(offset, offset + chunkSize);chunks.push(chunk);offset += chunkSize;}return chunks;
}
/**
* 检查是否上传过
* @param filename
* @returns {Promise<unknown>}
*/
function checkChunk(filename) {return $fetch(prefix+`/checkChunk?fileName=${filename}`);
}/**
* 开始分片上传
* @param file 文件
* @param exists 存在的分片下标
*/
function uploadChunk(file,exists) {const chunkArr = sliceFile(file);Promise.all(chunkArr.map((chunk, index) => {if(!exists.includes(index)){const formData = new FormData();formData.append('file', chunk);formData.append('fileName', file.name);formData.append('chunkIndex', index);return $fetch(prefix+'/uploadChunk', {method: 'POST',body: formData});}})).then(uploadRes=> {// 合并分片const formData = new FormData();formData.append('fileName', file.name);formData.append('totalChunks', chunkArr.length);$fetch(prefix + '/mergeChunks', {method: 'POST',body:formData,}).then(mergeRes=>{console.log("合并成功")});});
}

以上优化后所有分片可以同时上传,所有分片上传都成功后进行合并。

最后是完整代码

@Controller()
@RequestMapping("/kuroshiro/file-upload")
public class FileUploadController {private String prefix = "kuroshiro/fragmentUpload";// 文件保存目录private final String uploadPath = RuoYiConfig.getUploadPath();// 分片保存目录private final String chunkUploadPath = uploadPath+File.separator+"chunks";/*** demo* @return*/@GetMapping("/demo")public String demo() {return prefix+"/demo";}/*** 分片上传** @param file 上传的文件* @param start 文件开始上传的位置* @param fileName 文件名称* @return  上传结果*/@PostMapping("/fragmentUpload")@ResponseBodypublic AjaxResult fragmentUpload(@RequestParam("file") MultipartFile file, @RequestParam("start") long start, @RequestParam("fileName") String fileName) {try {// 检查上传目录是否存在,如果不存在则创建File directory = new File(uploadPath);if (!directory.exists()) {directory.mkdirs();}// 设置上传文件的目标路径File targetFile = new File(uploadPath +File.separator+ fileName);// 创建 RandomAccessFile 对象以便进行文件的随机读写操作RandomAccessFile randomAccessFile = new RandomAccessFile(targetFile, "rw");// 获取 RandomAccessFile 对应的 FileChannelFileChannel channel = randomAccessFile.getChannel();// 设置文件通道的位置,即从哪里开始写入文件内容channel.position(start);// 从 MultipartFile 对象的资源通道中读取文件内容,并写入到指定位置channel.transferFrom(file.getResource().readableChannel(), start, file.getSize());// 关闭文件通道和 RandomAccessFile 对象channel.close();randomAccessFile.close();// 返回上传成功的响应return AjaxResult.success("上传成功");} catch (Exception e) {// 捕获异常并返回上传失败的响应return AjaxResult.error("上传失败");}}/*** 检测文件是否存在* 如果文件存在,则返回已经存在的文件大小。* 如果文件不存在,则返回 0,表示前端从头开始上传该文件。* @param filename* @return*/@GetMapping("/checkFile")@ResponseBodypublic AjaxResult checkFile(@RequestParam("filename") String filename) {File file = new File(uploadPath+File.separator + filename);if (file.exists()) {return AjaxResult.success(file.length());} else {return AjaxResult.success(0L);}}/*** 分片上传* @param file 文件* @param chunkIndex 分片下标*/@PostMapping("/uploadChunk")@ResponseBodypublic AjaxResult uploadChunk(@RequestParam("file") MultipartFile file, @RequestParam("chunkIndex") int chunkIndex,@RequestParam("fileName") String fileName) {String uploadDirectory = chunkUploadPath+File.separator+fileName;File directory = new File(uploadDirectory);if (!directory.exists()||directory.isFile()) {directory.mkdirs();}String filePath = uploadDirectory + File.separator + fileName+ "_" + chunkIndex;try (OutputStream os = new FileOutputStream(filePath)) {os.write(file.getBytes());return AjaxResult.success("分片"+(chunkIndex+1)+"上传成功");}catch (Exception e){// 保存失败后如果文件建立了就删除,下次上传时重新保存,避免文件内容错误File chunkFile = new File(filePath);if(chunkFile.exists()) chunkFile.delete();e.printStackTrace();return AjaxResult.error("分片"+(chunkIndex+1)+"上传失败");}}/*** 检测分片是否存在* 如果文件存在,则返回已经存在的分片下标集合。存在的就不上传* 如果文件不存在,则返回空集合,表示前端从头开始上传该文件* @param fileName* @return*/@GetMapping("/checkChunk")@ResponseBodypublic AjaxResult checkChunk(@RequestParam("fileName") String fileName) {String uploadDirectory = chunkUploadPath+File.separator+fileName;List<Integer> list = new ArrayList<>();File file = new File(uploadDirectory);// 文件目录不存在if(!file.exists()||file.isFile()) return AjaxResult.success(list);File[] files = file.listFiles();// 文件目录下没有分片文件if(files == null) return AjaxResult.success(list);// 返回存在分片下标集合return AjaxResult.success(Arrays.stream(files).map(item->Integer.valueOf(item.getName().substring(item.getName().lastIndexOf("_")+1))).collect(Collectors.toList()));}// 合并文件分片@PostMapping("/mergeChunks")@ResponseBodypublic AjaxResult mergeChunks(@RequestParam("fileName") String fileName, @RequestParam("totalChunks") int totalChunks) {String uploadDirectory = chunkUploadPath+File.separator+fileName;String mergedFilePath = uploadPath +File.separator+ fileName;try (OutputStream os = new FileOutputStream(mergedFilePath, true)) {for (int i = 0; i < totalChunks; i++) {Path chunkFilePath = Paths.get(uploadDirectory +File.separator+ fileName + "_" + i);Files.copy(chunkFilePath, os);Files.delete(chunkFilePath);}File chunkDir = new File(uploadDirectory);if (chunkDir.exists()) chunkDir.delete();return AjaxResult.success();}catch (Exception e){e.printStackTrace();return AjaxResult.error(e.getMessage());}}}
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head><th:block th:include="include :: header('分片上传')" />
</head>
<body class="gray-bg">
<div class="container-div" id="chunk-div"><div class="row"><div class="col-sm-12 search-collapse"><form id="formId"><div class="select-list"><ul><li><label>选择文件:</label><input type="file" id="fileInput"/></li><li><a class="btn btn-primary btn-rounded btn-sm" @click="startUpload(1)"><i class="fa fa-upload"></i>&nbsp;开始上传1</a><a class="btn btn-primary btn-rounded btn-sm" @click="startUpload(2)"><i class="fa fa-upload"></i>&nbsp;开始上传2</a></li></ul></div></form></div><div class="col-sm-12" style="padding-left: 0;"><div class="ibox"><div class="ibox-content"><h3>上传进度</h3><ul class="sortable-list connectList agile-list" v-if="uploadMsg && uploadMsg.length>0"><li v-for="item in uploadMsg" :class="item.status+'-element'">{{item.title}}</li></ul></div></div></div></div>
</div>
<th:block th:include="include :: footer" />
<script th:inline="javascript">var prefix = ctx + "/kuroshiro/file-upload";new Vue({el: '#chunk-div',data: {// 每次上传大小chunkSize: 1 * 1024 * 1024,uploadMsg:[],},methods: {/*** 开始上传*/startUpload: function(type){const fileInput = document.getElementById('fileInput');const file = fileInput.files[0];if (!file) {alert("请选择文件");return;}const filename = file.name;this.uploadMsg = [];if(type == 1){this.checkFile(filename).then(start => {this.uploadFile(file, start,Math.min(start + this.chunkSize, file.size));},err => {this.uploadMsg.push({title:`文件检测失败失败:${err}`,status:"danger"});})}if(type == 2){this.checkChunk(filename).then(arr => {this.uploadChunk(file, arr);},err => {this.uploadMsg.push({title:`文件检测失败失败:${err}`,status:"danger"});})}},/*** 检查是否上传过* @param filename* @returns {Promise<unknown>}*/checkFile: function(filename) {return this.$fetch(prefix+`/checkFile?filename=${filename}`);},/*** 开始分片上传* @param file 文件* @param start 开始位置* @param end 结束位置*/uploadFile: function(file, start,end) {if(start < end){const chunk = file.slice(start, end);const formData = new FormData();formData.append('file', chunk);formData.append('start', start);formData.append('fileName', file.name);this.$fetch(prefix+'/fragmentUpload', {method: 'POST',body: formData}).then(response => {this.uploadMsg.push({title:`分片 ${start} - ${end} 上传成功`,status:"info"});// 递归调用this.uploadFile(file,end,Math.min(end + this.chunkSize, file.size))},err=>{this.uploadMsg.push({title:`分片 ${start} - ${end} 上传失败:${err}`,status:"danger"});})}else{this.uploadMsg.push({title:`文件已上传`,status:"info"});}},/*** 切割文件为多个分片* @param file* @returns {*[]}*/sliceFile: function(file) {const chunks = [];let offset = 0;while (offset < file.size) {const chunk = file.slice(offset, offset + this.chunkSize);chunks.push(chunk);offset += this.chunkSize;}return chunks;},/*** 检查是否上传过* @param filename* @returns {Promise<unknown>}*/checkChunk: function(filename) {return this.$fetch(prefix+`/checkChunk?fileName=${filename}`);},/*** 开始分片上传* @param file 文件* @param exists 存在的分片下标*/uploadChunk: function(file,exists) {const chunkArr = this.sliceFile(file);Promise.all(chunkArr.map((chunk, index) => {if(!exists.includes(index)){const formData = new FormData();formData.append('file', chunk);formData.append('fileName', file.name);formData.append('chunkIndex', index);return new Promise((resolve, reject) => {this.$fetch(prefix+'/uploadChunk', {method: 'POST',body: formData}).then(res => {resolve(res)this.uploadMsg.push({title:`分片 ${index+1} 上传成功`,status:"info"});},err => {reject(err)this.uploadMsg.push({title:`分片 ${index+1} 上传失败:${err}`,status:"danger"});});})}})).then(uploadRes=> {// 合并分片const formData = new FormData();formData.append('fileName', file.name);formData.append('totalChunks', chunkArr.length);this.$fetch(prefix + '/mergeChunks', {method: 'POST',body:formData,}).then(mergeRes=>{this.uploadMsg.push({title:`合并成功`,status:"info"});},err => {this.uploadMsg.push({title:`合并失败:${err}`,status:"danger"});});});},$fetch: function(url,requestInit){return new Promise((resolve, reject) => {fetch(url,requestInit).then(response => {if (!response.ok) {throw new Error('请求失败');}return response.json();}).then(data => {if (data.code === 0) {resolve(data.data);} else {reject(data.msg)}}).catch(error => {reject(error)});});},}});</script>
</body>
</html>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/66118.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

js解决 Number失精度问题

const updatePromises adinfo.rows.map(async item > {const cwf await uniCloud.httpclient.request("https://api.oceanengine.com/open_api/v3.0/project/list/", {method: GET,data: {advertiser_id: item.account_id},// 1. 指定text数据格式dataType: tex…

实力认证 | 海云安入选《信创安全产品及服务购买决策参考》

近日&#xff0c;国内知名安全调研机构GoUpSec发布了2024年中国网络安全行业《信创安全产品及服务购买决策参考》&#xff0c;报告从产品特点、产品优势、成功案例、安全策略等维度对各厂商信创安全产品及服务进行调研了解。 海云安凭借AI大模型技术在信创安全领域中的创新应用…

windows系统“GameInputRedist.dll”文件丢失或错误导致游戏运行异常如何解决?windows系统DLL文件修复方法

GameInputRedist.dll是存放在windows系统中的一个重要dll文件&#xff0c;缺少它可能会造成部分游戏不能正常运行。当你的电脑弹出提示“无法找到GameInputRedist.dll”或“计算机缺少GameInputRedist.dll”等错误问题&#xff0c;请不用担心&#xff0c;我们将深入解析DLL文件…

概率论考前一天

判断是不是分布函数&#xff1a;单调不减&#xff0c;右连续&#xff0c;F负无穷为0&#xff0c; F正无穷为1 判断是不是密度函数&#xff1a;非负性&#xff08;函数任意地方都大于0&#xff09;&#xff0c;规范&#xff1a;积分为1

FPGA工程师成长四阶段

朋友&#xff0c;你有入行三年、五年、十年的职业规划吗&#xff1f;你知道你所做的岗位未来该如何成长吗&#xff1f; FPGA行业的发展近几年是蓬勃发展&#xff0c;有越来越多的人才想要或已经踏进了FPGA行业的大门。很多同学在入行FPGA之前&#xff0c;都会抱着满腹对职业发…

springCloudGateway+nacos自定义负载均衡-通过IP隔离开发环境

先说一下想法&#xff0c;小公司开发项目&#xff0c;参考若依框架使用的spring-cloud-starter-gateway和spring-cloud-starter-alibaba-nacos, 用到了nacos的配置中心和注册中心&#xff0c;有多个模块&#xff08;每个模块都是一个服务&#xff09;。 想本地开发&#xff0c;…

向量数据库如何助力Text2SQL处理高基数类别数据

01. 导语 Agent工作流和 LLMs &#xff08;大语言模型&#xff09;的出现&#xff0c;让我们能够以自然语言交互的模式执行复杂的SQL查询&#xff0c;并彻底改变Text2SQL系统的运行方式。其典型代表是如何处理High-Cardinality Categorical Data &#xff08;高基数类别数据&am…

qBittorent访问webui时提示unauthorized解决方法

现象描述 QNAP使用Container Station运行容器&#xff0c;使用Docker封装qBittorrent时&#xff0c;访问IP:PORT的方式后无法访问到webui&#xff0c;而是提示unauthorized&#xff0c;如图&#xff1a; 原因分析 此时通常是由于设备IP与qBittorrent的ip地址不在同一个网段导致…

工程水印相机结合图纸,真实现场时间地点,如何使用水印相机,超简单方法只教一次!

在工程管理领域&#xff0c;精准记录现场信息至关重要。水印相机拍照功能&#xff0c;为工程人员提供了强大的现场信息记录工具&#xff0c;助力工程管理和统计工程量&#xff0c;更可以将图片分享到电脑、分享给同事&#xff0c;协同工作。 一、打开图纸 打开手机版CAD快速看图…

GO语言实现KMP算法

前言 本文结合朱战立教授编著的《数据结构—使用c语言&#xff08;第五版&#xff09;》&#xff08;以下简称为《数据结构&#xff08;第五版&#xff09;朱站立》&#xff09;中4.4.2章节内容编写&#xff0c;KMP的相关概念可参考此书4.4.2章节内容。原文中代码是C语言&…

LeetCode 热题 100_从前序与中序遍历序列构造二叉树(47_105_中等_C++)(二叉树;递归)

LeetCode 热题 100_从前序与中序遍历序列构造二叉树&#xff08;47_105&#xff09; 题目描述&#xff1a;输入输出样例&#xff1a;题解&#xff1a;解题思路&#xff1a;思路一&#xff08;递归&#xff09;&#xff1a; 代码实现代码实现&#xff08;思路一&#xff08;递归…

文档智能:OCR+Rocketqa+layoutxlm <Rocketqa>

此次梳理Rocketqa&#xff0c;个人认为该篇文件讲述的是段落搜索的改进点&#xff0c;关于其框架&#xff1a;粗检索 重排序----&#xff08;dual-encoder architecture&#xff09;&#xff0c;讲诉不多&#xff0c;那是另外的文章&#xff1b; 之前根据文档智能功能&#x…

ubuntu官方软件包网站 字体设置

在https://ubuntu.pkgs.org/22.04/ubuntu-universe-amd64/xl2tpd_1.3.16-1_amd64.deb.html搜索找到需要的软件后&#xff0c;点击&#xff0c;下滑&#xff0c; 即可在Links和Download找到相关链接&#xff0c;下载即可&#xff0c; 但是找不到ros的安装包&#xff0c; 字体设…

使用 WPF 和 C# 绘制覆盖网格的 3D 表面

此示例展示了如何使用 C# 代码和 XAML 绘制覆盖有网格的 3D 表面。示例使用 WPF 和 C# 将纹理应用于三角形展示了如何将纹理应用于三角形。此示例只是使用该技术将包含大网格的位图应用于表面。 在类级别&#xff0c;程序使用以下代码来定义将点的 X 和 Z 坐标映射到 0.0 - 1.…

[Do374]Ansible一键搭建sftp实现用户批量增删

[Do374]Ansible一键搭建sftp实现用户批量增删 1. 前言2. 思路3. sftp搭建及用户批量新增3.1 配置文件内容3.2 执行测试3.3 登录测试3.4 确认sftp服务器配置文件 4. 测试删除用户 1. 前言 最近准备搞一下RHCA LV V,外加2.9之后的ansible有较大变化于是练习下Do374的课程内容. 工…

【IDEA 2024】学习笔记--文件选项卡

在我们项目的开发过程中&#xff0c;由于项目涉及的类过多&#xff0c;以至于我们会打开很多的窗口。使用IDEA默认的配置&#xff0c;个人觉得十分不便。 目录 一、设置多个文件选项卡按照文件字母顺序排列 二、设置多个文件选项卡分行显示 一、设置多个文件选项卡按照文件字…

Docker save load 镜像 tag 为 <none>

一、场景分析 我从 docker hub 上拉了这么一个镜像。 docker pull tomcat:8.5-jre8-alpine 我用 docker save 命令想把它导出成 tar 文件以便拷贝到内网机器上使用。 docker save -o tomcat-8.5-jre8-alpine.tar.gz 镜像ID 当我把这个镜像传到别的机器&#xff0c;并用 dock…

O2O同城系统架构与功能分析

2015工作至今&#xff0c;10年资深全栈工程师&#xff0c;CTO&#xff0c;擅长带团队、攻克各种技术难题、研发各类软件产品&#xff0c;我的代码态度&#xff1a;代码虐我千百遍&#xff0c;我待代码如初恋&#xff0c;我的工作态度&#xff1a;极致&#xff0c;责任&#xff…

走出实验室的人形机器人,将复刻ChatGPT之路?

1月7日&#xff0c;在2025年CES电子展现场&#xff0c;黄仁勋不仅展示了他全新的皮衣和采用Blackwell架构的RTX 50系列显卡&#xff0c;更进一步展现了他对于机器人技术领域&#xff0c;特别是人形机器人和通用机器人技术的笃信。黄仁勋认为机器人即将迎来ChatGPT般的突破&…

EF Core执行原生SQL语句

目录 EFCore执行非查询原生SQL语句 为什么要写原生SQL语句 执行非查询SQL语句 有SQL注入漏洞 ExecuteSqlInterpolatedAsync 其他方法 执行实体相关查询原生SQL语句 FromSqlInterpolated 局限性 执行任意原生SQL查询语句 什么时候用ADO.NET 执行任意SQL Dapper 总…