👉 這是一個或許對你有用的社群
《專案實戰(影片)》:從書中學,往事上“練” 《網際網路高頻面試題》:面朝簡歷學習,春暖花開 《架構 x 系統設計》:摧枯拉朽,掌控面試高頻場景題 《精進 Java 學習指南》:系統學習,網際網路主流技術棧 《必讀 Java 原始碼專欄》:知其然,知其所以然

👉這是一個或許對你有用的開源專案國產 Star 破 10w+ 的開源專案,前端包括管理後臺 + 微信小程式,後端支援單體和微服務架構。功能涵蓋 RBAC 許可權、SaaS 多租戶、資料許可權、商城、支付、工作流、大屏報表、微信公眾號、ERP、CRM、AI 大模型等等功能:
Boot 多模組架構:https://gitee.com/zhijiantianya/ruoyi-vue-pro Cloud 微服務架構:https://gitee.com/zhijiantianya/yudao-cloud 影片教程:https://doc.iocoder.cn 【國內首批】支援 JDK 17/21 + SpringBoot 3.3、JDK 8/11 + Spring Boot 2.7 雙版本

一 檔案上傳的常見場景
基於 Spring Boot + MyBatis Plus + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
專案地址:https://github.com/YunaiV/ruoyi-vue-pro 影片教程:https://doc.iocoder.cn/video/
二 秒傳、斷點上傳與分片上傳
秒傳
斷點續傳
分片上傳
基於 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
專案地址:https://github.com/YunaiV/yudao-cloud 影片教程:https://doc.iocoder.cn/video/
三 秒傳實戰
後端實現
MessageDigest
類來計算檔案的 MD5 值,然後檢查資料庫中是否存在該檔案。@RestController
@RequestMapping
(
"/file"
)
publicclassFileController
{
@Autowired
FileService fileService;
@PostMapping
(
"/upload1"
)
public ResponseEntity<String> secondUpload(@RequestParam(value = "file",required = false) MultipartFile file,@RequestParam(required = false,value = "md5") String md5)
{
try
{
// 檢查資料庫中是否已存在該檔案
if
(fileService.existsByMd5(md5)) {
return
ResponseEntity.ok(
"檔案已存在"
);
}
// 儲存檔案到伺服器
file.transferTo(
new
File(
"/path/to/save/"
+ file.getOriginalFilename()));
// 儲存檔案資訊到資料庫
fileService.save(
new
FileInfo(file.getOriginalFilename(), DigestUtils.md5DigestAsHex(file.getInputStream())));
return
ResponseEntity.ok(
"上傳成功"
);
}
catch
(Exception e) {
return
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(
"上傳失敗"
);
}
}
}
前端呼叫
<!DOCTYPE html>
<htmllang="en">
<head>
<metacharset="UTF-8">
<title>
秒傳
</title>
<scriptsrc="spark-md5.js"></script>
</head>
<body>
<inputtype="file"id="fileInput" />
<buttononclick="startUpload()">
開始上傳
</button>
<hr>
<script>
asyncfunctionstartUpload()
{
const
fileInput =
document
.getElementById(
'fileInput'
);
const
file = fileInput.files[
0
];
if
(!file) {
alert(
"請選擇檔案"
);
return
;
}
const
md5 =
await
calculateMd5(file);
const
formData =
new
FormData();
formData.append(
'md5'
, md5);
const
response =
await
fetch(
'/file/upload1'
, {
method
:
'POST'
,
body
: formData
});
const
result =
await
response.text();
if
(response.ok) {
if
(result !=
"檔案已存在"
) {
// 開始上傳檔案
}
}
else
{
console
.error(
"上傳失敗: "
+ result);
}
}
functioncalculateMd5(file)
{
returnnewPromise
(
(resolve, reject) =>
{
const
reader =
new
FileReader();
reader.onloadend =
() =>
{
const
spark =
new
SparkMD5.ArrayBuffer();
spark.append(reader.result);
resolve(spark.end());
};
reader.onerror =
() =>
reject(reader.error);
reader.readAsArrayBuffer(file);
});
}
</script>
</body>
</html>
-
計算檔案的 MD5 值,計算之後傳送給服務端確定檔案是否存在。 -
如果檔案已經存在,則不需要繼續上傳檔案;如果檔案不存在,則開始上傳檔案,上傳檔案和 MD5 校驗請求類似,上面的案例程式碼中我就沒有重複演示了,松哥在書裡和之前的課程裡都多次講過檔案上傳,這裡不再囉嗦。
四 分片上傳實戰
-
第 0 片,從 0 開始,一共是 1024*1024
個位元組。 -
第 1 片,從 1024*1024
開始,一共是1024*1024
個位元組。 -
第 2 片…
後端實現
privatestaticfinal
String UPLOAD_DIR = System.getProperty(
"user.home"
) +
"/uploads/"
;
/**
* 上傳檔案到指定位置
*
*
@param
file 上傳的檔案
*
@param
start 檔案開始上傳的位置
*
@return
ResponseEntity<String> 上傳結果
*/
@PostMapping
(
"/upload2"
)
public ResponseEntity<String> resumeUpload(@RequestParam("file") MultipartFile file, @RequestParam("start")long start,@RequestParam("fileName") String fileName)
{
try
{
File directory =
new
File(UPLOAD_DIR);
if
(!directory.exists()) {
directory.mkdirs();
}
File targetFile =
new
File(UPLOAD_DIR + fileName);
RandomAccessFile randomAccessFile =
new
RandomAccessFile(targetFile,
"rw"
);
FileChannel channel = randomAccessFile.getChannel();
channel.position(start);
channel.transferFrom(file.getResource().readableChannel(), start, file.getSize());
channel.close();
randomAccessFile.close();
return
ResponseEntity.ok(
"上傳成功"
);
}
catch
(Exception e) {
System.out.println(
"上傳失敗: "
+e.getMessage());
return
ResponseEntity.status(
500
).body(
"上傳失敗"
);
}
}
前端呼叫
<!DOCTYPE html>
<htmllang="en">
<head>
<metacharset="UTF-8">
<metaname="viewport"content="width=device-width, initial-scale=1.0">
<title>
分片示例
</title>
</head>
<body>
<inputtype="file"id="fileInput" />
<buttononclick="startUpload()">
開始上傳
</button>
<script>
asyncfunctionstartUpload()
{
const
fileInput =
document
.getElementById(
'fileInput'
);
const
file = fileInput.files[
0
];
if
(!file) {
alert(
"請選擇檔案"
);
return
;
}
const
filename = file.name;
let
start =
0
;
uploadFile(file, start);
}
asyncfunctionuploadFile(file, start)
{
const
chunkSize =
1024
*
1024
;
// 每個分片1MB
const
total =
Math
.ceil(file.size / chunkSize);
for
(
let
i =
0
; i < total; i++) {
const
chunkStart = start + i * chunkSize;
const
chunkEnd =
Math
.min(chunkStart + chunkSize, file.size);
const
chunk = file.slice(chunkStart, chunkEnd);
const
formData =
new
FormData();
formData.append(
'file'
, chunk);
formData.append(
'start'
, chunkStart);
formData.append(
'fileName'
, file.name);
const
response =
await
fetch(
'/file/upload2'
, {
method
:
'POST'
,
body
: formData
});
const
result =
await
response.text();
if
(response.ok) {
console
.log(
`分片 ${i + 1}/${total} 上傳成功`
);
}
else
{
console
.error(
`分片 ${i + 1}/${total} 上傳失敗: ${result}`
);
break
;
}
}
}
</script>
</body>
</html>
五 斷點續傳實戰
-
前端先發送一個請求,檢查要上傳的檔案在服務端是否已經存在,如果存在,目前大小是多少。 -
前端根據已經存在的大小,繼續上傳檔案即可。
後端案例
@GetMapping
(
"/check"
)
public ResponseEntity<Long> checkFile(@RequestParam("filename") String filename)
{
File file =
new
File(UPLOAD_DIR + filename);
if
(file.exists()) {
return
ResponseEntity.ok(file.length());
}
else
{
return
ResponseEntity.ok(
0L
);
}
}
前端呼叫
<!DOCTYPE html>
<htmllang="en">
<head>
<metacharset="UTF-8">
<metaname="viewport"content="width=device-width, initial-scale=1.0">
<title>
斷點續傳示例
</title>
</head>
<body>
<inputtype="file"id="fileInput"/>
<buttononclick="startUpload()">
開始上傳
</button>
<script>
asyncfunctionstartUpload()
{
const
fileInput =
document
.getElementById(
'fileInput'
);
const
file = fileInput.files[
0
];
if
(!file) {
alert(
"請選擇檔案"
);
return
;
}
const
filename = file.name;
let
start =
await
checkFile(filename);
uploadFile(file, start);
}
asyncfunctioncheckFile(filename)
{
const
response =
await
fetch(
`/file/check?filename=${filename}`
);
const
start =
await
response.json();
return
start;
}
asyncfunctionuploadFile(file, start)
{
const
chunkSize =
1024
*
1024
;
// 每個分片1MB
const
total =
Math
.ceil((file.size - start) / chunkSize);
for
(
let
i =
0
; i < total; i++) {
const
chunkStart = start + i * chunkSize;
const
chunkEnd =
Math
.min(chunkStart + chunkSize, file.size);
const
chunk = file.slice(chunkStart, chunkEnd);
const
formData =
new
FormData();
formData.append(
'file'
, chunk);
formData.append(
'start'
, chunkStart);
formData.append(
'fileName'
, file.name);
const
response =
await
fetch(
'/file/upload2'
, {
method
:
'POST'
,
body
: formData
});
const
result =
await
response.text();
if
(response.ok) {
console
.log(
`分片 ${i + 1}/${total} 上傳成功`
);
}
else
{
console
.error(
`分片 ${i + 1}/${total} 上傳失敗: ${result}`
);
break
;
}
}
}
</script>
</body>
</html>
六 總結





