奇麟大數據:前端大文件上傳解決方案

在奇麟大數據業務系統的開發及使用過程中,例如OBS對象存儲文件管理、流計算DSC依賴管理,經常會遇到上傳文件這樣的基礎需求,一般情況下,前端上傳文件就是new FormData,然后把文件 append 進去,然后post發送給后端就完事了,但是文件越大,上傳的文件也就越長,在上傳過程中,經常會遇到請求超時或者阻礙用戶繼續操作的情況,十分影響用戶體驗,因為把該文件直接在一個請求體中提交,會出現一些問題,以nginx為例:

  • 其默認允許1MB以內的文件

  • 超過1MB的文件,需要設置?client_max_body_size放開體積限制

一味地只放寬Nginx的限制,會出現一些問題,最明顯的問題是服務器的存儲和網絡帶寬壓力都會非常大。

以奇麟云存儲OBS對象存儲文件管理中使用的常規上傳解決方案為例展示

// 將目標文件轉為FormData數據流function?uploadRequest?(params)?{? ? ?const?file =?params.file? ? ?const?form =?new?FormData()? ? ?form.append('file', file)? ? ?const?config = {? ? ? ? ? ?headers: {? ? ? ? ? ? ??'Content-Type':?'multipart/form-data',? ? ? ? ? ?},? ? ? ? ? ?onUploadProgress: e => {? ? ? ? ? ? ??// 上傳進度百分比? ? ? ? ? ? ??if?(e.total >?0) {? ? ? ? ? ? ? ? ? e.percent = e.loaded / e.total *?100? ? ? ? ? ? ? }? ? ? ? ? ? ??params.onProgress(e)? ? ? ? ? ?},? ? ? ? ? ?timeout:?0,?// 上傳文件不設置超時時間? ? ?}? ? ?await?request({'XXXX','post', form, config}? ? ? ? ?.then(res => {? ? ? ? ??// 上傳成功? ? ? ? ? ? ?params.onSuccess(res)? ? ? ? ? })? ? ? ? ? ? .catch(err => {? ? ? ? ? ? ??params.onError(err)? ? ? ? ? ? })? ? ? ? ? ?.finally(() => {})}

上傳文件過大的情況,很容易出現網絡超時導致接口報錯的情況,且用戶在當前操作頁面無法終止,只能持續等待直至上傳操作完成

為了解決上述業務痛點,這里就需要尋求大文件上傳的解決方案,核心是利用?Blob.prototype.slice?方法,和數組的 slice 方法相似,文件的 slice 方法可以返回?原文件的某個切片

提前定義好單個切片大小,將大文件切分為一個個小的切片,然后借助 http 的可并發性,同時上傳多個切片。這樣從原本傳一個大文件,變成了并發傳多個小的文件切片,可以大大減少上傳時間

因為由于是并發請求傳輸,所以傳輸到服務端的切片文件順序可能會發生變化,因此我們還需要給每個切片記錄順序

1、前端實現部分

前端使用 Vue 作為開發框架,組件庫使用 Element-Plus 作為 UI 框架

1.1 上傳控件

首先創建選擇文件的控件并監聽 change 事件,另外就是上傳按鈕

<template>? ?<div?class="upload-container">? ??<input?type="file"?@change="handleFileChange"?/>? ??<el-button?@click="handleUpload">upload</el-button>??</div></template><script?setup>import?{ ref }?from?'vue'const?container =?ref({??file:?null})function?handleFileChange(e) {? ? ?const?[file] = e.target.files;? ? ?if?(!file)?return;? ? ?container.value.file?= file;}async?handleUpload() {??// ...}</script>

1.2 請求邏輯

request({? ? ?url,? ? ?method =?"post",? ? ?data,? ? ?headers = {},? ? ?requestList? ?}) {? ? ?return?new?Promise(resolve?=>?{? ? ? ?const?xhr =?new?XMLHttpRequest();? ? ? ?xhr.open(method, url);? ? ? ?Object.keys(headers).forEach(key?=>? ? ? ? ?xhr.setRequestHeader(key, headers[key])? ? ? ?);? ? ? ?xhr.send(data);? ? ? ?xhr.onload?=?e?=>?{? ? ? ? ?resolve({? ? ? ? ? ?data: e.target.response? ? ? ? ?});? ? ? ?};? ? ?});}

1.3 上傳切片

接著就要實現核心的上傳功能,上傳需要做兩件事

  • 對文件進行切片

  • 將切片傳輸給服務端

<template>??<div>? ??<input?type="file"?@change="handleFileChange"?/>? ??<el-button?@click="handleUpload">上傳</el-button>??</div></template><script?setup>import?{ ref }?from?'vue';// 設置默認切片大小 10MB?const?SIZE?=?10?*?1024?*?1024;?const?data =?ref([])const?container =?ref({??file:?null})function?request() {? ?// ****}function?handleFileChange() {? ?// ***}??// 生成文件切片??function?createFileChunk(file, size = SIZE) {? ?const?fileChunkList = [];? ??let?cur =?0;? ??while?(cur < file.size) {? ? ? fileChunkList.push({?file: file.slice(cur, cur + size) });? ? ? cur += size;? ? }? ??return?fileChunkList;? }??// 上傳切片??async?function?uploadChunks() {? ??const?requestList = data.value? ? ? .map(({ chunk,hash }) =>?{? ? ? ??const?formData =?new?FormData();? ? ? ? formData.append("chunk", chunk);? ? ? ? formData.append("hash", hash);? ? ? ? formData.append("filename", container.value.file.name);? ? ? ??return?{ formData };? ? ? })? ? ? .map(({ formData }) =>? ? ? ??request({? ? ? ? ??url:?"http://localhost:8888",? ? ? ? ??data: formData? ? ? ? })? ? ? );? ? ?// 并發請求? ??await?Promise.all(requestList);?? }??async?function?handleUpload() {? ??if?(!container.value.file)?return;? ??const?fileChunkList =?createFileChunk(container.value.file);? ? data.value?= fileChunkList.map(({ file },index) =>?({? ? ??chunk: file,? ? ? ??// 文件名 + 數組下標? ? ??hash: container.value.file.name?+?"-"?+ index? ? }));? ??await?uploadChunks();? }</script>

用戶點擊上傳按鈕時,調用 createFileChunk方法 將文件切片,切片數量通過文件大小控制,這里設置 10MB,也就是說一個 100 MB 的文件會被分成 10 個 10MB 的切片
createFileChunk 內使用 while 循環和 slice 方法將切片放入 fileChunkList 數組中返回
在生成文件切片時,需要給每個切片一個標識作為 hash,這里暫時使用文件名 + 下標的標記方式,這樣后端可以知道當前切片是第幾個切片,用于之后的合并切片
隨后調用 uploadChunks 上傳所有的文件切片,將文件切片,切片 hash,以及文件名放入 formData 中,再調用上一步的 request 函數返回一個 proimise,最后調用 Promise.all 并發上傳所有的切片

1.4 發送合并請求

使用整體思路中提到的第二種合并切片的方式,即前端主動通知服務端進行合并

前端發送額外的合并請求,服務端接受到請求時合并切片

<template>??<div>? ??<input?type="file"?@change="handleFileChange"?/>? ??<el-button?@click="handleUpload">upload</el-button>??</div></template><script?setup>import?{ ref }?from?'vue';// 設置默認切片大小 10MB?const?SIZE?=?10?*?1024?*?1024;?const?data =?ref([])const?container =?ref({??file:?null})function?request() {??// ****}function?handleFileChange() {??// ***}// 生成文件切片function?createFileChunk(file, size = SIZE) {??const?fileChunkList = [];? ?let?cur =?0;? ?while?(cur < file.size) {? ? ?fileChunkList.push({?file: file.slice(cur, cur + size) });? ? ?cur += size;? ?}? ?return?fileChunkList;}// 上傳切片async?function?uploadChunks() {? ?const?requestList = data.value? ? ?.map(({ chunk,hash }) =>?{? ? ? ?const?formData =?new?FormData();? ? ? ?formData.append("chunk", chunk);? ? ? ?formData.append("hash", hash);? ? ? ?formData.append("filename", container.value.file.name);? ? ? ?return?{ formData };? ? ?})? ? ?.map(({ formData }) =>? ? ? ?request({? ? ? ? ?url:?"http://localhost:8888",? ? ? ? ?data: formData? ? ? ?})? ? ?);? ??// 并發請求? ?await?Promise.all(requestList)??// 合并切片? ?await?mergeRequest()}async?function?handleUpload() {? ?if?(!container.value.file)?return;? ?const?fileChunkList =?createFileChunk(container.value.file);? ?data.value?= fileChunkList.map(({ file },index) =>?({? ? ?chunk: file,? ? ??// 文件名 + 數組下標? ? ?hash: container.value.file.name?+?"-"?+ index? ?}));??await?uploadChunks()??async?function?mergeRequest() {? ? ?await?request({? ? ? ?url:?"http://localhost:8888/mergeFile",? ? ? ?headers: {? ? ? ? ?"content-type":?"application/json"? ? ? ?},? ? ? ?data:?JSON.stringify({? ? ? ? ?filename: container.value.file.name? ? ? ?})? ? ?})? ?}}</script>

2、服務端部分,以NodeJs為例

服務端負責接受前端傳輸的切片,并在接收到所有切片后合并所有切片
這里又引伸出兩個問題

  • 服務端合并切片的時機,即切片什么時候傳輸完成?

  • 如何進行切片合并,合并的規則是什么?

第一個問題需要前端配合,前端在每個切片中都攜 帶切片最大數量的信息,當服務端接受到這個數量的切片時自動合并。或者也可以額外發一個請求,主動通知服務端進行切片的合并
第二個問題,具體如何合并切片呢?這里可以使用 Nodejs 的 讀寫流(readStream/writeStream),將所有切片的流傳輸到最終文件的流里

使用 http 模塊搭建一個簡單服務端

const?http =?require("http");const?server = http.createServer();server.on("request",?async?(req, res) => {? res.setHeader("Access-Control-Allow-Origin",?"*");? res.setHeader("Access-Control-Allow-Headers",?"*");??if?(req.method?===?"OPTIONS") {? ? res.status?=?200;? ? res.end();? ??return;? }});server.listen(8888,?() =>?console.log("listening port 8888"));

2.1 接受切片

使用?multiparty?處理前端傳來的 formData
在 multiparty.parse 的回調中,files 參數保存了 formData 中文件,fields 參數保存了 formData 中非文件的字段

const?http =?require("http");const?path =?require("path");const?fse =?require("fs-extra");const?multiparty =?require("multiparty");const?server = http.createServer();// 大文件上傳的臨時存儲目錄const?UPLOAD_DIR_NAME?= path.resolve(__dirname,?"..",?"target");server.on("request",?async?(req, res) => {? res.setHeader("Access-Control-Allow-Origin",?"*");? res.setHeader("Access-Control-Allow-Headers",?"*");??if?(req.method?===?"OPTIONS") {? ? res.status?=?200;? ? res.end();? ??return;? }??const?multipart =?new?multiparty.Form();? multipart.parse(req,?async?(err, fields, files) => {? ??if?(err) {? ? ??return;? ? }? ??const?[chunk] = files.chunk;? ??const?[hash] = fields.hash;? ??const?[filename] = fields.filename;? ??// 創建臨時文件夾用于臨時存儲 chunk? ??// 添加 chunkDir 前綴與文件名做區分? ??const?chunkDir = path.resolve(UPLOAD_DIR_NAME,?'chunkDir'? filename);? ??if?(!fse.existsSync(chunkDir)) {? ? ??await?fse.mkdirs(chunkDir);? ? }? ??// fs-extra 的 rename 方法 windows 平臺會有權限問題? ??// 參考: https://github.com/meteor/meteor/issues/7852#issuecomment-255767835? ??await?fse.move(chunk.path,?`${chunkDir}/${hash}`);? ? res.end("received file chunks");? });});server.listen(8888,?() =>?console.log("listening port 8888"));

查看 multiparty 處理后的 chunk 對象,path 是存儲臨時文件的路徑,size 是臨時文件大小,在 multiparty 文檔中提到可以使用 fs.rename(這里換成了 fs.remove, 因為 fs-extra 的 rename 方法在 windows 平臺存在權限問題)
在接受文件切片時,需要先創建臨時存儲切片的文件夾,以 chunkDir 作為前綴,文件名作為后綴
由于前端在發送每個切片時額外攜帶了唯一值 hash,所以以 hash 作為文件名,將切片從臨時路徑移動切片文件夾中,最后的結果如下

2.2 合并切片

在接收到前端發送的合并請求后,服務端將文件夾下的所有切片進行合并

const?http =?require("http");const?path =?require("path");const?fse =?require("fs-extra");const?server = http.createServer();const?UPLOAD_DIR_NAME?= path.resolve(__dirname,?"..",?"target");+?const?resolvePost?= req =>+ ??new?Promise(resolve?=>?{+ ? ??let?chunk =?"";+ ? ? req.on("data",?data?=>?{+ ? ? ? chunk += data;+ ? ? });+ ? ? req.on("end",?() =>?{+ ? ? ??resolve(JSON.parse(chunk));+ ? ? });+ ? });+?// 寫入文件流+?const?pipeStream?= (path, writeStream) =>+ ?new?Promise(resolve?=>?{+ ? ?const?readStream = fse.createReadStream(path);+ ? ?readStream.on("end",?() =>?{+ ? ? ?fse.unlinkSync(path);+ ? ? ?resolve();+ ? ?});+ ? ?readStream.pipe(writeStream);+ ?});// 合并切片+?const?mergeFileChunk?=?async?(filePath, filename, size) => {+ ??const?chunkDir = path.resolve(UPLOAD_DIR_NAME,?'chunkDir'?+ filename);+ ??const?chunkPaths =?await?fse.readdir(chunkDir);+ ??// 根據切片下標進行排序+ ??// 否則直接讀取目錄的獲得的順序會錯亂+ ? chunkPaths.sort((a, b) =>?a.split("-")[1] - b.split("-")[1]);+ ??// 并發寫入文件+ ??await?Promise.all(+ ? ? chunkPaths.map((chunkPath, index) =>+ ? ? ??pipeStream(+ ? ? ? ? path.resolve(chunkDir, chunkPath),+ ? ? ? ??// 根據 size 在指定位置創建可寫流+ ? ? ? ? fse.createWriteStream(filePath, {+ ? ? ? ? ??start: index * size,+ ? ? ? ? })+ ? ? ? )+ ? ? )+ ?);+ ?// 合并后刪除保存切片的目錄+ ?fse.rmdirSync(chunkDir);+};server.on("request",?async?(req, res) => {? res.setHeader("Access-Control-Allow-Origin",?"*");? res.setHeader("Access-Control-Allow-Headers",?"*");??if?(req.method?===?"OPTIONS") {? ? res.status?=?200;? ? res.end();? ??return;? }+ ??if?(req.url?===?"/mergeFile") {+ ? ??const?data =?await?resolvePost(req);+ ? ??const?{ filename,size } = data;+ ? ??const?filePath = path.resolve(UPLOAD_DIR_NAME,?`${filename}`);+ ? ??await?mergeFileChunk(filePath, filename);+ ? ? res.end(+ ? ? ??JSON.stringify({+ ? ? ? ??code:?0,+ ? ? ? ??message:?"file merged success"+ ? ? ? })+ ? ? );+ ? }});server.listen(8888,?() =>?console.log("listening port 8888"));

由于前端在發送合并請求時會攜帶文件名,服務端根據文件名可以找到上一步創建的切片文件夾,接著使用 fs.createWriteStream 創建一個可寫流,可寫流文件名就是上傳時的文件名,隨后遍歷整個切片文件夾,將切片通過 fs.createReadStream 創建可讀流,傳輸合并到目標文件中。
值得注意的是每次可讀流都會傳輸到可寫流的指定位置,這是通過 createWriteStream 的第二個參數 start 控制的,目的是能夠并發合并多個可讀流至可寫流中,這樣即使并發時流的順序不同,也能傳輸到正確的位置。所以還需要讓前端在請求的時候提供之前設定好的 size 給服務端,服務端根據 size 指定可讀流的起始位置

async?mergeRequest() {? ? ??await?request({? ? ? ??url:?"http://localhost:8888/mergeFile",? ? ? ??headers: {? ? ? ? ??"content-type":?"application/json"? ? ? ? },? ? ? ??data:?JSON.stringify({+ ? ? ? ??size:?SIZE,? ? ? ? ??filename: container.value.file.name? ? ? ? })? ? ? });? ? }

其實也可以等上一個切片合并完后再合并下個切片,這樣就不需要指定位置,但傳輸速度會降低,所以使用了并發合并的手段

接著只要保證每次合并完成后刪除這個切片,等所有切片都合并完畢后最后刪除切片文件夾即可

至此一個簡單的大文件上傳就完成了,接下來我們在此基礎上擴展一些額外的功能

2.3 總進度條

將每個切片已上傳的部分累加,除以整個文件的大小,就能得出當前文件的上傳進度,所以這里使用 Vue 的計算屬性

computed: {? ? ? ?uploadPercentage() {? ? ? ? ??if?(!container.value.file?|| !data.value.length)?return?0;? ? ? ? ??const?loaded = data.value? ? ? ? ? ? .map(item?=>?item.size?* item.percentage)? ? ? ? ? ? .reduce((acc, cur) =>?acc + cur);? ? ? ? ??return?parseInt((loaded / container.value.file.size).toFixed(2));? ? ? ? }?}

經過切片邏輯改造之后,切片的每塊文件都能分別統計到上傳進度,如下圖所示:

改造之后的大文件上傳效果展示:

3. 總結

大文件上傳

  • 前端上傳大文件時使用 Blob.prototype.slice 將文件切片,并發上傳多個切片,最后發送一個合并的請求通知服務端合并切片

  • 服務端接收切片并存儲,收到合并請求后使用流將切片合并到最終文件

  • 原生 XMLHttpRequest 的 upload.onprogress 對切片上傳進度的監聽

  • 使用 Vue 計算屬性根據每個切片的進度算出整個文件的上傳進度

  • 用戶等待上傳時間大大縮減,改善用戶的體驗


更多技術干貨,

請關注“360智匯云開發者”👇

360智匯云官網:https://zyun.360.cn(復制在瀏覽器中打開)

更多好用又便宜的云產品,歡迎試用體驗~

添加工作人員企業微信👇,get更快審核通道+試用包哦~

圖片

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/91951.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/91951.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/91951.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

立創EDA中雙層PCB疊層分析

立創EDA中雙層PCB疊層分析 結論&#xff1a;立創EDA中的雙層 PCB 疊層視圖相比傳統視圖&#xff0c;多出一個焊盤層&#xff08;博主命名&#xff09;&#xff1b; 1. 傳統雙層 PCB 疊層示意圖 絲印層 印刷元件標識、極性標記及廠商信息 輔助組裝與后期維護 阻焊層 覆蓋銅層表…

深入理解進程:從底層原理到硬件系統實戰

深入理解進程&#xff1a;從底層原理到嵌入式實戰&#xff08;3-4 萬字詳解&#xff09; 前言&#xff1a;為什么硬件開發者必須吃透進程&#xff1f; 作為嵌入式開發者&#xff0c;你可能會說&#xff1a;“我平時用的 RTOS 里只有任務&#xff08;Task&#xff09;&#xff0…

Elasticsearch 簡化指南:GCP Google Compute Engine

作者&#xff1a;來自 Elastic Eduard Martin 系列內容的一部分&#xff1a;開始使用 Elasticsearch&#xff1a;GCP 想獲得 Elastic 認證&#xff1f;看看下一期 Elasticsearch Engineer 培訓什么時候開始&#xff01; Elasticsearch 擁有豐富的新功能&#xff0c;幫助你根據…

STM32的定時器輸入捕獲-超聲波測距案例

STM32的定時器輸入捕獲-超聲波測距案例 gitee代碼輸入捕獲硬件電路案例說明主函數代碼 gitee代碼 https://gitee.com/xiaolixi/l-stm32/tree/master/STM32F103C8T6/2-1tem-ld-timer-input-pluse 輸入捕獲硬件電路 超聲波測距案例說明 使用超聲波測距傳感器使用tim1的輸入捕獲…

[特殊字符] Spring Boot 常用注解全解析:20 個高頻注解 + 使用場景實例

一文掌握 Spring Boot 中最常用的 20 個注解&#xff0c;涵蓋開發、配置、Web、數據庫、測試等場景&#xff0c;配合示例講解&#xff0c;一站式掌握&#xff01;&#x1f4cc; 一、核心配置類注解 1. SpringBootApplication 作用&#xff1a;標記為 Spring Boot 應用的入口類&…

【工具變量】地級市城市包容性綠色增長數據(2011-2023年)

城市包容性綠色增長是指在推動城市經濟增長的過程中&#xff0c;兼顧環境可持續性、社會公平和包容性發展的理念與實踐。它強調在實現綠色轉型和低碳發展的同時&#xff0c;保障社會各群體&#xff0c;特別是弱勢群體的利益與參與權利&#xff0c;確保增長成果能夠公平共享 本…

深入理解React Hooks:從使用到原理

4. 源碼解析類:《深入理解React Hooks:從使用到原理》 # 深入理解React Hooks:從使用到原理?? **背景**: - Hooks解決了Class組件的哪些問題? - 為什么不能在循環/條件中調用Hooks??? **核心原理**:### 1. Hooks鏈表 React內部維護一個單向鏈表:fiber.memoizedSta…

【云原生】Docker 部署 Elasticsearch 9 操作詳解

目錄 一、前言 二、Elasticsearch 9 新特性介紹 2.1 基于 Lucene 10 重大升級 2.2 Better Binary Quantization(BBQ) 2.3 Elastic Distributions of OpenTelemetry(EDOT) 2.4 LLM 可觀測性 2.5 攻擊發現與自動導入 2.6 ES|QL 增強 2.7 語義檢索 三、基于Docker部署…

uview-ui使用u-search搜索框

1、效果圖 2、帶地址搜索框&#xff0c;在微信小程序線上需要開啟地圖定位接口&#xff0c;若沒有權限則顯示不了城市名&#xff0c;注意事項參考uniapp相關地圖 API調用-CSDN博客 <template><view><u-sticky offset-top"-1"><u-search v-mode…

Elasticsearch+Logstash+Kibana部署

目錄 一、實驗準備 1.下載安裝 2.下載java 2.同步主機系統時間 二、部署 1.部署elasticsearch 修改 /etc/elasticsearch/elasticsearch.yml 配置文件 修改 /etc/hosts/ 文件 啟動elasticsearch 查看是否啟動進程netstat -antptu | grep java 2.部署logstash 進入/et…

TEngine學習

關于靜態類中的靜態變量賦值&#xff1a; public static class ActorEventDefine{public static readonly int ScoreChange RuntimeId.ToRuntimeId("ActorEventDefine.ScoreChange");public static readonly int GameOver RuntimeId.ToRuntimeId("ActorEventD…

獵板:在 5G 與 AI 時代,印制線路板如何滿足高性能需求

5G 與 AI 技術的深度融合&#xff0c;推動電子設備向高速傳輸、高算力、高集成方向發展&#xff0c;印制線路板&#xff08;PCB&#xff09;作為核心載體&#xff0c;其性能直接決定終端設備的運行效率與可靠性。獵板 PCB 聚焦 5G 通信的高頻需求與 AI 算力的密集需求&#xff…

教你如何借助AI精讀文獻

目錄1. 原文2. 對文獻有一個快速的理解3. 專業術語解讀4. 解答疑問5. 借助AI翻譯摘要和引言部分5.1 **摘要 (Abstract)**5.2 **引言 (Introduction)**6. 介紹論文中的“Stack-Propagation”7. 查閱論文里的參考文獻&#xff0c;看看他是如何在Introduction中引述研究進展文獻&a…

FastAdmin框架超級管理員密碼重置與常規admin安全機制解析-卓伊凡|大東家

FastAdmin框架超級管理員密碼重置與常規admin安全機制解析-卓伊凡|大東家我們可以看到admin賬戶是不允許直接修改的&#xff0c;這也是目前fastadmin 框架不允許的&#xff0c;那么如何處理一、FastAdmin超級管理員密碼重置方法當FastAdmin的超級管理員密碼忘記或需要重置時&am…

我做的基礎服務項目,是如何實現 API 安全與限流的(短信、郵件、文件上傳、釘釘通知)

我做的基礎服務項目&#xff0c;是如何實現 API 安全與限流的&#xff08;短信、郵件、文件上傳、釘釘通知&#xff09;一、背景 最近我做了一個基礎服務項目&#xff0c;主要對外提供短信、郵件、文件上傳和釘釘通知等基礎功能。這些接口是多個業務系統都要調用的&#xff0c;…

(Python)類和類的方法(基礎教程介紹)(Python基礎教程)

源代碼&#xff1a;class Students:stats"大學"def __init__(self,name,age,sex,credit):self.namenameself.ageageself.sexsexself.creditcreditdef tell(self):return f"{self.name}說&#xff1a;你好"class Teachers(Students):stats"教師"d…

網絡智能體研究綜述

網絡智能體研究綜述1.什么是網絡智能體1.1.核心特征1.2.分類方式1.2.1.按功能定位1.2.2. 按網絡結構1.2.3.按應用場景1.3.典型應用場景1.4.技術基礎1.5.發展趨勢與挑戰1.5.1.發展趨勢1.5.2.核心挑戰2.網絡智能體盤點3.阿里的WebSailor3.1.WebSailor的主要功能和技術特點3.2.技術…

git 介紹與使用教程

Git 是一個 分布式版本控制系統&#xff0c;每個開發者都有一個完整的本地倉庫&#xff08;包含完整歷史記錄&#xff09;&#xff0c;而遠程倉庫&#xff08;如 GitHub、GitLab、Gitee&#xff09;是團隊共享的中央倉庫。它們的關系如下&#xff1a;本地倉庫&#xff08;Local…

[AI風堇]基于ChatGPT3.5+科大訊飛錄音轉文字API+GPT-SOVITS的模擬情感實時語音對話項目

[AI風堇]最近利用工作日的晚間和周末時間&#xff0c;我完成了一個有趣的Python編程小項目。這個項目的靈感來源于上個月在B站看到的"科技怪咖"UP主分享的一個視頻&#xff0c;視頻中展示了一個名為"DataMagic"的自動化數據處理工具&#xff0c;能夠智能分…

物聯網-規則引擎的定義

構建物聯網系統中的規則引擎是一個系統性的工程&#xff0c;它需要處理來自海量設備的實時數據流&#xff0c;并根據預定義的邏輯觸發動作。以下是構建一個高效、可靠、可擴展的物聯網規則引擎的關鍵步驟和考慮因素&#xff1a; 核心目標 實時性&#xff1a; 快速處理設備事件并…