当前位置: 首页 > news >正文

大文件分片上传-续传-秒传(详解)

前言

前面记录过使用库实现的大文件的分片上传
基于WebUploader实现大文件分片上传
基于vue-simple-uploader 实现大文件分片上传

前面记录过基于库实现的大文件的分片上传,那如果不使用库,
文件分片是怎么实现的,该怎么做到呢?
一起看看吧

思路

1、文件分片、
2、每个文件标识、
3、并发上传、
4、合并组装
5、上传前查询是否存在

实现

读取文件

通过监听 input 的 change 事件,当选取了本地文件后,可以在回调函数中拿到对应的文件:

const handleUpload = (e: Event) => {const files = (e.target as HTMLInputElement).filesif (!files) {return}// 读取选择的文件console.log(files[0]);
}

文件分片

核心是用Blob 对象的 slice 方法,用法如下:

let blob = instanceOfBlob.slice([start [, end [, contentType]]]};
start 和 end 代表 Blob 里的下标,表示被拷贝进新的 Blob 的字节的起始位置和结束位置。
contentType 会给新的 Blob 赋予一个新的文档类型,在这里我们用不到。

使用slice方法来实现下对文件的分片,获取分片的文件列表

const createFileChunks = (file: File) => {const fileChunkList = []let cur = 0while (cur < file.size) {fileChunkList.push({file: file.slice(cur, cur + CHUNK_SIZE),})cur += CHUNK_SIZE // CHUNK_SIZE为分片的大小}return fileChunkList
}

hash 计算

怎么区分每一个文件呢?
1、根据文件名去区分,不可以,因为文件名我们可以是随便修改的;
2、我们见过用 webpack 打包出来的文件的文件名,会有一串不一样的字符串,这个字符串就是根据文件的内容生成的 hash 值,文件内容变化,hash 值就会跟着发生变化。
3、而且妙传实现也是基于此:
服务器在处理上传文件的请求的时候,要先判断下对应文件的 hash 值有没有记录,如果 A 和 B 先后上传一份内容相同的文件,
所以这两份文件的 hash 值是一样的。当 A 上传的时候会根据文件内容生成一个对应的 hash 值,然后在服务器上就会有一个对应的文件,B 再上传的时候,服务器就会发现这个文件的 hash 值之前已经有记录了,说明之前
已经上传过相同内容的文件了,所以就不用处理 B 的这个上传请求了,给用户的感觉就像是实现了秒传

spark-md5

我们得先安装spark-md5。我们就可以用文件的所有切片来算该文件的hash 值,
但是如果一个文件特别大,每个切片的所有内容都参与计算的话会很耗时间,所有我们可以采取以下策略:
1、第一个和最后一个切片的内容全部参与计算;
2、中间剩余的切片我们分别在前面、后面和中间取 2 个字节参与计算;
3、既能保证所有的切片参与了计算,也能保证不耗费很长的时间

安装使用

npm install spark-md5
npm install @types/spark-md5 -Dimport SparkMD5 from 'spark-md5'
/*** 计算文件的hash值,计算的时候并不是根据所用的切片的内容去计算的,那样会很耗时间,我们采取下面的策略去计算:* 1. 第一个和最后一个切片的内容全部参与计算* 2. 中间剩余的切片我们分别在前面、后面和中间取2个字节参与计算* 这样做会节省计算hash的时间*/
const calculateHash = async (fileChunks: Array<{file: Blob}>) => {return new Promise(resolve => {const spark = new sparkMD5.ArrayBuffer()const chunks: Blob[] = []fileChunks.forEach((chunk, index) => {if (index === 0 || index === fileChunks.length - 1) {// 1. 第一个和最后一个切片的内容全部参与计算chunks.push(chunk.file)} else {// 2. 中间剩余的切片我们分别在前面、后面和中间取2个字节参与计算// 前面的2字节chunks.push(chunk.file.slice(0, 2))// 中间的2字节chunks.push(chunk.file.slice(CHUNK_SIZE / 2, CHUNK_SIZE / 2 + 2))// 后面的2字节chunks.push(chunk.file.slice(CHUNK_SIZE - 2, CHUNK_SIZE))}})const reader = new FileReader()reader.readAsArrayBuffer(new Blob(chunks))reader.onload = (e: Event) => {spark.append(e?.target?.result as ArrayBuffer)resolve(spark.end())}})
}

文件上传前端实现

const uploadChunks = async (fileChunks: Array<{ file: Blob }>) => {const data = fileChunks.map(({ file }, index) => ({fileHash: fileHash.value,index,chunkHash: `${fileHash.value}-${index}`,chunk: file,size: file.size,}))const formDatas = data.map(({ chunk, chunkHash }) => {const formData = new FormData()// 切片文件formData.append('chunk', chunk)// 切片文件hashformData.append('chunkHash', chunkHash)// 大文件的文件名formData.append('fileName', fileName.value)// 大文件hashformData.append('fileHash', fileHash.value)return formData})let index = 0const max = 6 // 并发请求数量const taskPool: any = [] // 请求队列while (index < formDatas.length) {const task = fetch('http://127.0.0.1:3000/upload', {method: 'POST',body: formDatas[index],})task.then(() => {taskPool.splice(taskPool.findIndex((item: any) => item === task))})taskPool.push(task)if (taskPool.length === max) {// 当请求队列中的请求数达到最大并行请求数的时候,得等之前的请求完成再循环下一个await Promise.race(taskPool)}index++percentage.value = ((index / formDatas.length) * 100).toFixed(0)}await Promise.all(taskPool)
}

文件上传后端实现

后端 express 框架,用到的工具包:multiparty、fs-extra、cors、body-parser、nodemon后端我们处理文件时需要用到 multiparty 这个工具,所以也是得先安装,然后再引入它。
我们在处理每个上传的分片的时候,应该先将它们临时存放到服务器的一个地方,方便我们合并的时候再去读
取。为了区分不同文件的分片,我们就用文件对应的那个 hash 为文件夹的名称,将这个文件的所有分片放到这
个文件夹中。
// 所有上传的文件存放到该目录下
const UPLOAD_DIR = path.resolve(__dirname, 'uploads')// 处理上传的分片
app.post('/upload', async (req, res) => {const form = new multiparty.Form()form.parse(req, async function (err, fields, files) {if (err) {res.status(401).json({ok: false,msg: '上传失败',})}const chunkHash = fields['chunkHash'][0]const fileName = fields['fileName'][0]const fileHash = fields['fileHash'][0]// 存储切片的临时文件夹const chunkDir = path.resolve(UPLOAD_DIR, fileHash)// 切片目录不存在,则创建切片目录if (!fse.existsSync(chunkDir)) {await fse.mkdirs(chunkDir)}const oldPath = files.chunk[0].path// 把文件切片移动到我们的切片文件夹中await fse.move(oldPath, path.resolve(chunkDir, chunkHash))res.status(200).json({ok: true,msg: 'received file chunk',})})
})
写完前后端代码后就可以来试下看看文件能不能实现切片的上传,如果没有错误的话,我们的 uploads 文件
夹下应该就会多一个文件夹,这个文件夹里面就是存储的所有文件的分片了。

文件合并前端实现

核心:切片合并
前端只需要向服务器发送一个合并的请求,并且为了区分要合并的文件,需要将文件的 hash 值给传过去
/*** 发请求通知服务器,合并切片*/
const mergeRequest = () => {// 发送合并请求fetch('http://127.0.0.1:3000/merge', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify({size: CHUNK_SIZE,fileHash: fileHash.value,fileName: fileName.value,}),}).then((response) => response.json()).then(() => {alert('上传成功')})
}

文件合并后端实现

之前已经将所有的切片上传到服务器并存储到对应的目录里面去了,
合并的时候需要从对应的文件夹中获取所有的切片,然后利用文件的读写操作,实现文件的合并了。
合并完成之后,我们将生成的文件以 hash 值命名存放到对应的位置就可以了
// 提取文件后缀名
const extractExt = (filename) => {return filename.slice(filename.lastIndexOf('.'), filename.length)
}/*** 读的内容写到writeStream中*/
const pipeStream = (path, writeStream) => {return new Promise((resolve, reject) => {// 创建可读流const readStream = fse.createReadStream(path)readStream.on('end', async () => {fse.unlinkSync(path)resolve()})readStream.pipe(writeStream)})
}/*** 合并文件夹中的切片,生成一个完整的文件*/
async function mergeFileChunk(filePath, fileHash, size) {const chunkDir = path.resolve(UPLOAD_DIR, fileHash)const chunkPaths = await fse.readdir(chunkDir)// 根据切片下标进行排序// 否则直接读取目录的获得的顺序可能会错乱chunkPaths.sort((a, b) => {return a.split('-')[1] - b.split('-')[1]})const list = chunkPaths.map((chunkPath, index) => {return pipeStream(path.resolve(chunkDir, chunkPath),fse.createWriteStream(filePath, {start: index * size,end: (index + 1) * size,}),)})await Promise.all(list)// 文件合并后删除保存切片的目录fse.rmdirSync(chunkDir)
}// 合并文件
app.post('/merge', async (req, res) => {const { fileHash, fileName, size } = req.bodyconst filePath = path.resolve(UPLOAD_DIR, `${fileHash}${extractExt(fileName)}`)// 如果大文件已经存在,则直接返回if (fse.existsSync(filePath)) {res.status(200).json({ok: true,msg: '合并成功',})return}const chunkDir = path.resolve(UPLOAD_DIR, fileHash)// 切片目录不存在,则无法合并切片,报异常if (!fse.existsSync(chunkDir)) {res.status(200).json({ok: false,msg: '合并失败,请重新上传',})return}await mergeFileChunk(filePath, fileHash, size)res.status(200).json({ok: true,msg: '合并成功',})
})

文件秒传&断点续传

服务器上给上传的文件命名的时候就是用对应的 hash 值命名的,
所以在上传之前判断有对应的这个文件,就不用再重复上传了,
直接告诉用户上传成功,给用户的感觉就像是实现了秒传。
文件秒传-前端
前端在上传之前,需要将对应文件的 hash 值告诉服务器,看看服务器上有没有对应的这个文件,
如果有,就直接返回,不执行上传分片的操作了
/*** 验证该文件是否需要上传,文件通过hash生成唯一,改名后也是不需要再上传的,也就相当于秒传*/
const verifyUpload = async () => {return fetch('http://127.0.0.1:3000/verify', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify({fileName: fileName.value,fileHash: fileHash.value,}),}).then((response) => response.json()).then((data) => {return data // data中包含对应的表示服务器上有没有该文件的查询结果})
}// 点击上传事件
const handleUpload = async (e: Event) => {// ...// uploadedList已上传的切片的切片文件名称const res = await verifyUpload()const { shouldUpload } = res.dataif (!shouldUpload) {// 服务器上已经有该文件,不需要上传alert('秒传:上传成功')return}// 服务器上不存在该文件,继续上传uploadChunks(fileChunks)
}
文件秒传-后端
// 根据文件hash验证文件有没有上传过
app.post('/verify', async (req, res) => {const { fileHash, fileName } = req.bodyconst filePath = path.resolve(UPLOAD_DIR, `${fileHash}${extractExt(fileName)}`)if (fse.existsSync(filePath)) {// 文件存在服务器中,不需要再上传了res.status(200).json({ok: true,data: {shouldUpload: false,},})} else {// 文件不在服务器中,就需要上传res.status(200).json({ok: true,data: {shouldUpload: true,},})}
})
文件断点续传-前端
如果我们之前已经上传了一部分分片了,我们只需要再上传之前拿到这部分分片,
然后再过滤掉是不是就可以避免去重复上传这些分片了,也就是只需要上传那些上传失败的分片,
所以,再上传之前还得加一个判断。
我们还是在那个 verify 的接口中去获取已经上传成功的分片,然后在上传分片前进行一个过滤
const uploadChunks = async (fileChunks: Array<{ file: Blob }>, uploadedList: Array<string>) => {const formDatas = fileChunks.filter((chunk, index) => {// 过滤服务器上已经有的切片return !uploadedList.includes(`${fileHash.value}-${index}`)}).map(({ file }, index) => {const formData = new FormData()// 切片文件formData.append('file', file)// 切片文件hashformData.append('chunkHash', `${fileHash.value}-${index}`)// 大文件的文件名formData.append('fileName', fileName.value)// 大文件hashformData.append('fileHash', fileHash.value)return formData})// ...
}
文件断点续传-后端
只需在 /verify 这个接口中加上已经上传成功的所有切片的名称就可以,
因为所有的切片都存放在以文件的 hash 值命名的那个文件夹,
所以需要读取这个文件夹中所有的切片的名称就可以。
/*** 返回已经上传切片名* @param {*} fileHash* @returns*/
const createUploadedList = async (fileHash) => {return fse.existsSync(path.resolve(UPLOAD_DIR, fileHash))? await fse.readdir(path.resolve(UPLOAD_DIR, fileHash)) // 读取该文件夹下所有的文件的名称: []
}// 根据文件hash验证文件有没有上传过
app.post('/verify', async (req, res) => {const { fileHash, fileName } = req.bodyconst filePath = path.resolve(UPLOAD_DIR, `${fileHash}${extractExt(fileName)}`)if (fse.existsSync(filePath)) {// 文件存在服务器中,不需要再上传了res.status(200).json({ok: true,data: {shouldUpload: false,},})} else {// 文件不在服务器中,就需要上传,并且返回服务器上已经存在的切片res.status(200).json({ok: true,data: {shouldUpload: true,uploadedList: await createUploadedList(fileHash),},})}
})

相关文章:

大文件分片上传-续传-秒传(详解)

前言 前面记录过使用库实现的大文件的分片上传 基于WebUploader实现大文件分片上传 基于vue-simple-uploader 实现大文件分片上传 前面记录过基于库实现的大文件的分片上传&#xff0c;那如果不使用库&#xff0c; 文件分片是怎么实现的&#xff0c;该怎么做到呢&#xff1f;…...

CE-LVD证书跟CE-EMC证书有什么区别?

CE-LVD证书跟CE-EMC证书有什么区别&#xff1f; CE-LVD证书跟CE-EMC证书有什么区别&#xff1f; 近日&#xff0c;TEMU平台电器需提交CE-LVD证书&#xff0c;不再接受EMC证书---玩具产品需提交满足玩具法规的CE证书&#xff0c;法规总是多变的&#xff0c;卖家也是很苦恼&…...

使用Mapster实现双向映射,解放搬砖体力活

经常会有对象属性互相赋值的操作&#xff0c;但是频繁的写实在是搬运工一样&#xff0c;比较难受比如下面两个类 public class AgencyBdm {public new int Id { set; get; }public string AgencyId { set; get; }public string AgencyName { set; get; }public string Region {…...

一种基于屏幕分辨率的RTSP主子码流切换的多路视频监控的播放方案

技术背景&#xff1a; 用户场景下&#xff0c;存在多个监控场所的100路监控摄像头&#xff0c;例如&#xff1a;大华、海康、宇视、杭州宇泛的枪机、球机、半球、NVR、DVR等不同类型的监控设备&#xff0c;通过视频监控平台进行设备的管理&#xff0c;通过RTSP拉流的方案管理监…...

SpringBoot日志+SpringMVC+UUID重命名文件+Idea热部署

目录 【SpringBoot日志】 什么是日志&#xff0c;日志的作用 关于日志的基本信息&#xff0c;又有哪些呢&#xff1f; 关于日志的级别 Springboot内置SLF4J【门面模式】 和 logback【日志框架】 在配置文件中可以设置日志级别【以.yml为例】 SpringBoot 持久化的保存日…...

向日葵远程控制中的键盘异常问题

本文记录的是ubuntu 20.04 上&#xff0c; 向日葵的最高版本目前只有V 11.0.1.44968&#xff08;2022.02&#xff09; 我的被控制和 控制端都是上述环境&#xff1b; 起因&#xff0c;由于我昨天在控制端按下了 win/ 或者是其他的组合键 &#xff08;具体哪个键盘确实没有注…...

【iOS免越狱】利用IOS自动化web-driver-agent_appium-实现自动点击+滑动屏幕

1.目标 在做饭、锻炼等无法腾出双手的场景中&#xff0c;想刷刷抖音 刷抖音的时候有太多的广告 如何解决痛点 抖音自动播放下一个视频 iOS系统高版本无法 越狱 安装插件 2.操作环境 MAC一台&#xff0c;安装 Xcode iPhone一台&#xff0c;16 系统以上最佳 3.流程 下载最…...

聊聊“JVM 调优JVM 性能优化”是怎么个事?

所谓“调优”就是一个诊断和处理手段&#xff0c;最终的目标是让系统的处理能力&#xff0c;也就是“性能”达到最优化。 计算机系统中&#xff0c;性能相关的资源主要分为这几类&#xff1a; CPU&#xff1a;CPU 是系统最关键的计算资源&#xff0c;在单位时间内有限&#xf…...

再获Gartner认可!持安科技获评ZTNA领域代表供应商

近日&#xff0c;全球权威市场研究与咨询机构Gartner发布了《Hype Cycle for Security in China, 2023&#xff08;2023中国安全技术成熟度曲线&#xff09;》报告&#xff0c;对2023年的20个中国安全技术领域的现状与发展趋势进行了详细的分析与解读。 其中&#xff0c;持安科…...

微服务-Feign

文章目录 Feign介绍Feign的基本使用自定义Feign的配置Feign性能优化Feign最佳实践 Feign介绍 RestTemplate远程调用存在的问题&#xff1a;代码可读性差&#xff0c;java代码中夹杂url&#xff1b;参数复杂很难维护 String url "http://userservice/user/" order.g…...

jsp获取数据 jsp直接获取后端数据 获取input选中的值 单选 没 checked属性

let str0${showList}; let str1${showList}; 然后可以通过JSON.parse() 转 获取input选中的值 //goodsType 按类别 goods按货品var oneType $("input[ namecriteria1 ] ").val();//count按数量 totalprice按费用var twoType $("input[ namecriteria2 ] &q…...

React 中 keys 的作用是什么?

目录 前言&#xff1a;React 中的 Keys 的重要性 为什么 Keys 重要&#xff1f; 详解&#xff1a;key 属性的基本概念 用法&#xff1a;key 属性的示例 解析&#xff1a;key 属性的优势和局限性 优势&#xff1a; 局限性&#xff1a; key 属性的最佳实践 稳定的唯一标…...

代码随想录 | Day46

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 今日学习目标一、算法题1.完全背包问题2.零钱兑换 II3.组合总和 Ⅳ 学习及参考书籍 今日学习目标 完全背包问题 零钱兑换 II&#xff08;518&#xff09; 组合总和…...

word行内插入mathtype 公式后行距变大解决办法

现象 word行内插入mathtype 公式后行距变大 解决方法 选中要进行操作的那些行&#xff0c;依次单击菜单命令“格式→段落”&#xff0c;打开“段落”对话框&#xff1b;单击“缩进和间距”选项卡&#xff0c;将间距的“段前”和“段后”都调整为“0行”&#xff1b;将“如果…...

直播预告 | YashanDB 2023年度发布会正式定档11月2日,邀您共同见证国产数据库发展实践!

11月2日&#xff0c;YashanDB 2023年度发布会将于云端直播开启&#xff0c;发布会以 「惟实励新」 为主题&#xff0c;邀请企业用户、合作伙伴、广大开发者共同见证全新产品与解决方案。届时发布会将在墨天轮社区同步进行&#xff0c;欢迎大家报名&#xff01; 惟实求真。Yasha…...

一文读懂WebClient和RestTemplate的差异

自 Spring 5 以来&#xff0c;WebClient已成为Spring WebFlux的一部分&#xff0c;并且是发出 HTTP 请求的首选方式。它是经典RestTemplate的首选替代方案&#xff0c;后者自 Spring 5.0 以来一直处于维护模式。 本文将讨论 Spring WebClient和RestTemplate类之间的主要区别。…...

如何使用SpringBoot处理全局异常

如何使用SpringBoot处理全局异常 使用ControllerAdvice 和 ExceptionHandler处理全局异常 参考&#xff1a; ControllerAdvice ResponseBody Slf4j public class ExceptionHandler {ResponseStatus(HttpStatus.OK)org.springframework.web.bind.annotation.ExceptionHandler…...

【2023CANN训练营第二季】——通过一份入门级算子开发代码了解Ascend C算子开发流程

本次博客讲解的代码是Gitee代码仓的Ascend C加法算子开发代码&#xff0c;代码地址为&#xff1a; quick-start 打开Add文件&#xff0c;可以看到文件结构如下&#xff1a; 其中add_custom.cpp是算子开发的核心文件&#xff0c;包括了核函数的实现&#xff0c;展示了如何在Asc…...

建模仿真软件 Comsol Multiphysics mac中文版软件介绍

COMSOL Multiphysics mac是一款全球通用的基于高级数值方法和模拟物理场问题的通用软件&#xff0c;拥有、网格划分、研究和优化、求解器、可视化和后处理、仿真 App等相关功能&#xff0c;轻松实现各个环节的流畅进行&#xff0c;它能够解释耦合或多物理现象。 附加产品扩展了…...

深入理解强化学习——强化学习的历史:近代强化学习的发展

分类目录&#xff1a;《深入理解强化学习》总目录 在《深入理解强化学习——强化学习的历史》前面的文章中我们讨论了最优控制和试错学习学习的思想&#xff0c;接下来&#xff0c;我们将讨论一些在20世纪60年代和70年代&#xff0c;在试错学习计算和理论研究被相对忽视的时候&…...

数字人开发新范式:Fay-UE5虚拟交互引擎零基础实战指南

数字人开发新范式&#xff1a;Fay-UE5虚拟交互引擎零基础实战指南 【免费下载链接】fay-ue5 项目地址: https://gitcode.com/gh_mirrors/fa/fay-ue5 在数字内容创作与智能交互需求爆发的当下&#xff0c;开发者面临三大核心挑战&#xff1a;如何快速构建高逼真度虚拟形…...

数字孪生:从制造到城市,虚拟照进现实的系统工程

数字孪生已从概念走向规模化落地&#xff0c;其核心价值在于“以虚控实”。对软件测试从业者而言&#xff0c;这不仅是新场景的拓展&#xff0c;更是一场测试范式的革命——测试对象从单一软件系统&#xff0c;升级为“物理实体数字模型数据流控制闭环”的复杂异构系统。本文将…...

Flowise语音交互扩展:Whisper+TTS构建全模态助手

Flowise语音交互扩展&#xff1a;WhisperTTS构建全模态助手 1. 引言&#xff1a;为什么需要语音交互&#xff1f; 想象一下这样的场景&#xff1a;你正在厨房做饭&#xff0c;手上沾满了面粉&#xff0c;突然想到一个技术问题需要查询。这时候如果还要打字输入&#xff0c;简…...

【实战篇】Nginx核心配置与性能优化全攻略

1. Nginx基础配置快速上手 第一次接触Nginx时&#xff0c;我被它简洁的配置文件结构惊艳到了。相比其他Web服务器动辄几百行的配置&#xff0c;Nginx的配置文件就像一份精心设计的菜谱&#xff0c;每个指令都恰到好处。先带大家看看最基本的配置结构&#xff1a; # 全局块 user…...

Qwen3-14B WebUI定制教程:更换主题、添加历史记录、导出对话功能

Qwen3-14B WebUI定制教程&#xff1a;更换主题、添加历史记录、导出对话功能 1. 准备工作与环境检查 在开始定制Qwen3-14B的WebUI之前&#xff0c;我们需要确保环境已经正确配置并运行。以下是准备工作步骤&#xff1a; 1.1 确认镜像版本与硬件配置 首先检查您的环境是否符…...

快速搭建视觉定位服务:Chord(Qwen2.5-VL)一键部署与使用

快速搭建视觉定位服务&#xff1a;Chord&#xff08;Qwen2.5-VL&#xff09;一键部署与使用 1. 项目概述 Chord是基于Qwen2.5-VL多模态大模型的视觉定位服务&#xff0c;能够通过自然语言描述在图像中精确定位目标对象。想象一下&#xff0c;你只需要说"找到图里的白色花…...

3步让旧款iOS设备重获新生:Legacy-iOS-Kit性能拯救全指南

3步让旧款iOS设备重获新生&#xff1a;Legacy-iOS-Kit性能拯救全指南 【免费下载链接】Legacy-iOS-Kit An all-in-one tool to restore/downgrade, save SHSH blobs, jailbreak legacy iOS devices, and more 项目地址: https://gitcode.com/gh_mirrors/le/Legacy-iOS-Kit …...

C语言宏定义:嵌入式开发中的高效利器与避坑指南

1. C语言宏定义的基础与陷阱在嵌入式开发中&#xff0c;宏定义是C语言最强大的特性之一&#xff0c;但也是最容易踩坑的特性。让我们从一个简单的需求开始&#xff1a;如何用宏实现两个数的比较并返回较小值&#xff1f;初学者最常见的写法是这样的&#xff1a;#define MIN(a,b…...

2026软考高项论文题目预测!十大管理+绩效域双押题(附答题思路)

备考软考高项的同学都知道&#xff0c;论文是决定成败的关键一科。随着2025年绩效域全面上位&#xff0c;论文考核方式已从“单一知识点”升级为“绩效域协同五大过程组联动可量化测量指标”的实战型命题。2026年考什么&#xff1f;如何准备&#xff1f;本文基于近3年命题规律&…...

个人创作者利器:AI净界RMBG-1.4,3秒完成以往30分钟的手动精修

个人创作者利器&#xff1a;AI净界RMBG-1.4&#xff0c;3秒完成以往30分钟的手动精修 1. 为什么你需要AI净界RMBG-1.4&#xff1f; 作为一名内容创作者&#xff0c;你是否经常遇到这些困扰&#xff1a; 拍摄的产品照片背景杂乱&#xff0c;需要花费大量时间手动抠图精心设计…...