基于SpringBoot+MySQL+Vue實(shí)現(xiàn)文件共享系統(tǒng)
一、為什么要做這個(gè)系統(tǒng)?
我們每天要傳海報(bào)、視頻、文案,以前靠微信群、U盤、郵箱來回傳,問題一大堆。 老板找我:“你搞個(gè)系統(tǒng),把文件管起來。” 于是,我用SpringBoot+MySQL+Vue搞了個(gè)文件共享系統(tǒng),同時(shí)加了用戶空間限額。
二、界面效果



三、數(shù)據(jù)庫(kù)表設(shè)計(jì)
一共三張表,簡(jiǎn)單清晰。
1. 用戶表
CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL COMMENT '用戶名', password VARCHAR(100) NOT NULL COMMENT '密碼', role VARCHAR(20) DEFAULT 'user' COMMENT '角色: user, designer, admin', create_time DATETIME DEFAULT CURRENT_TIMESTAMP );
2. 用戶空間配額表
CREATE TABLE user_quota ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT UNIQUE NOT NULL COMMENT '用戶ID', total_quota BIGINT DEFAULT 5368709120 COMMENT '總空間(字節(jié)),默認(rèn)5G', used_quota BIGINT DEFAULT 0 COMMENT '已用空間(字節(jié))', FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE );
注:5368709120 = 5 * 1024 * 1024 * 1024(5G)
3. 文件信息表
CREATE TABLE file_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, filename VARCHAR(200) NOT NULL COMMENT '原始文件名', path VARCHAR(500) NOT NULL COMMENT '服務(wù)器存儲(chǔ)路徑', file_size BIGINT NOT NULL COMMENT '文件大?。ㄗ止?jié))', user_id BIGINT NOT NULL COMMENT '上傳者ID', folder_id BIGINT DEFAULT 0 COMMENT '所屬文件夾', upload_time DATETIME DEFAULT CURRENT_TIMESTAMP, download_count INT DEFAULT 0 COMMENT '下載次數(shù)', remark VARCHAR(200) COMMENT '備注', FOREIGN KEY (user_id) REFERENCES user(id) );
四、后端實(shí)現(xiàn)(主要代碼流程)
1. 上傳文件:先檢查空間
@PostMapping("/upload")
public Result upload(@RequestParam("file") MultipartFile file,
@RequestHeader("UserId") Long userId) {
// 1. 獲取用戶空間配額
UserQuota quota = userQuotaMapper.findByUserId(userId);
long fileSize = file.getSize();
// 2. 檢查空間是否足夠
if (quota.getUsedQuota() + fileSize > quota.getTotalQuota()) {
return Result.error("空間不足!當(dāng)前可用:" +
formatSize(quota.getTotalQuota() - quota.getUsedQuota()));
}
// 3. 保存文件到磁盤
String uploadDir = "D:/uploads/" + userId + "/";
File dir = new File(uploadDir);
if (!dir.exists()) dir.mkdirs();
String filePath = uploadDir + file.getOriginalFilename();
File dest = new File(filePath);
file.transferTo(dest);
// 4. 更新已用空間
quota.setUsedQuota(quota.getUsedQuota() + fileSize);
userQuotaMapper.update(quota);
// 5. 記錄文件信息
FileInfo fileInfo = new FileInfo();
fileInfo.setFilename(file.getOriginalFilename());
fileInfo.setPath(filePath);
fileInfo.setFileSize(fileSize);
fileInfo.setUserId(userId);
fileInfo.setUploadTime(new Date());
fileInfoMapper.insert(fileInfo);
return Result.success("上傳成功");
}
2. 刪除文件:記得退回空間
@DeleteMapping("/delete/{id}")
public Result delete(@PathVariable Long id, @RequestHeader("UserId") Long userId) {
FileInfo file = fileInfoMapper.findById(id);
if (file == null || !file.getUserId().equals(userId)) {
return Result.error("文件不存在或無權(quán)限");
}
// 刪除文件
new File(file.getPath()).delete();
// 退回空間
UserQuota quota = userQuotaMapper.findByUserId(userId);
quota.setUsedQuota(quota.getUsedQuota() - file.getFileSize());
userQuotaMapper.update(quota);
// 刪除數(shù)據(jù)庫(kù)記錄
fileInfoMapper.delete(id);
return Result.success("刪除成功");
}
3. 工具方法:字節(jié)轉(zhuǎn)可讀大小
public static String formatSize(long bytes) {
if (bytes < 1024) return bytes + " B";
else if (bytes < 1048576) return String.format("%.2f KB", bytes / 1024.0);
else if (bytes < 1073741824) return String.format("%.2f MB", bytes / 1048576.0);
else return String.format("%.2f GB", bytes / 1073741824.0);
}
五、前端實(shí)現(xiàn)(Vue3+Element UI)
前端全部代碼
<template>
<div class="file-system-container">
<!-- 頂部信息欄 -->
<div class="top-info-bar">
<div class="user-greeting">
<el-avatar :size="32" :src="userAvatar">{{ userInitial }}</el-avatar>
<span class="greeting-text">你好,{{ username }}</span>
</div>
<div class="space-usage" :class="{ 'warning': spacePercentage > 80, 'danger': spacePercentage > 95 }">
<div class="progress-text">
<span>{{ spacePercentage }}%</span>
<span>{{ formatSize(usedSpace) }} / {{ formatSize(totalSpace) }}</span>
</div>
<el-progress
:percentage="spacePercentage"
:stroke-width="12"
:color="progressColor"
/>
</div>
<div class="action-buttons">
<el-button type="primary" @click="showUploadDialog">
<el-icon><Upload /></el-icon>上傳文件
</el-button>
<el-button v-if="isAdmin" type="success" @click="showQuotaDialog">
<el-icon><SetUp /></el-icon>管理配額
</el-button>
</div>
</div>
<!-- 主內(nèi)容區(qū) -->
<div class="main-content">
<!-- 左側(cè)文件夾樹 -->
<div class="folder-tree">
<h3>文件夾</h3>
<el-tree
:data="folders"
:props="defaultProps"
@node-click="handleFolderSelect"
highlight-current
:expand-on-click-node="false"
default-expand-all
>
<template #default="{ node, data }">
<div class="folder-node">
<el-icon><Folder /></el-icon>
<span>{{ node.label }}</span>
<span class="folder-count">({{ data.fileCount || 0 }})</span>
</div>
</template>
</el-tree>
</div>
<!-- 右側(cè)文件列表 -->
<div class="file-list">
<div class="file-list-header">
<h3>{{ currentFolder.name || '全部文件' }}</h3>
<div class="file-search">
<el-input
v-model="searchQuery"
placeholder="搜索文件..."
prefix-icon="Search"
clearable
/>
</div>
</div>
<el-table
:data="filteredFiles"
style="width: 100%"
v-loading="loading"
:empty-text="emptyText"
>
<el-table-column label="文件名" min-width="240">
<template #default="scope">
<div class="file-name-cell">
<el-icon :size="20" class="file-icon">
<component :is="getFileIcon(scope.row.filename)"/>
</el-icon>
<span>{{ scope.row.filename }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="uploadTime" label="上傳時(shí)間" width="180" />
<el-table-column prop="fileSize" label="大小" width="120">
<template #default="scope">
{{ formatSize(scope.row.fileSize) }}
</template>
</el-table-column>
<el-table-column prop="downloadCount" label="下載次數(shù)" width="100" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="scope">
<el-button size="small" @click="downloadFile(scope.row)">
<el-icon><Download /></el-icon>下載
</el-button>
<el-button size="small" type="danger" @click="confirmDelete(scope.row)">
<el-icon><Delete /></el-icon>刪除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="totalFiles"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</div>
<!-- 上傳文件對(duì)話框 -->
<el-dialog
v-model="uploadDialogVisible"
title="上傳文件"
width="500px"
>
<el-form :model="uploadForm" label-width="80px">
<el-form-item label="文件夾">
<el-select v-model="uploadForm.folderId" placeholder="選擇文件夾">
<el-option
v-for="folder in flatFolders"
:key="folder.id"
:label="folder.name"
:value="folder.id"
/>
</el-select>
</el-form-item>
<el-form-item label="文件">
<el-upload
class="upload-demo"
drag
action="#"
:http-request="customUpload"
:before-upload="beforeUpload"
:file-list="uploadForm.fileList"
multiple
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
拖拽文件到此處或 <em>點(diǎn)擊上傳</em>
</div>
<template #tip>
<div class="el-upload__tip">
可用空間: {{ formatSize(availableSpace) }}
</div>
</template>
</el-upload>
</el-form-item>
<el-form-item label="備注">
<el-input v-model="uploadForm.remark" type="textarea" :rows="2" placeholder="可選備注信息" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="uploadDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitUpload" :loading="uploading">
上傳
</el-button>
</span>
</template>
</el-dialog>
<!-- 配額管理對(duì)話框 -->
<el-dialog
v-model="quotaDialogVisible"
title="空間配額管理"
width="600px"
>
<el-table :data="userQuotas" style="width: 100%">
<el-table-column prop="username" label="用戶名" />
<el-table-column label="已用空間">
<template #default="scope">
{{ formatSize(scope.row.usedQuota) }}
</template>
</el-table-column>
<el-table-column label="總空間">
<template #default="scope">
<el-input-number
v-model="scope.row.totalQuotaGB"
:min="1"
:max="100"
size="small"
@change="updateQuota(scope.row)"
style="width: 90px;"
/>
GB
</template>
</el-table-column>
<el-table-column label="使用率">
<template #default="scope">
<el-progress
:percentage="Math.round((scope.row.usedQuota / (scope.row.totalQuota || 1)) * 100)"
:color="getQuotaColor(scope.row.usedQuota, scope.row.totalQuota)"
/>
</template>
</el-table-column>
</el-table>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted, reactive } from 'vue'
import {
Folder, Document, Picture, VideoPlay, Download,
Upload, Delete, Search, SetUp, UploadFilled
} from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
// 用戶信息
const username = ref('小王')
const userAvatar = ref('')
const userInitial = computed(() => username.value.charAt(0))
const isAdmin = ref(true)
// 空間使用情況
const totalSpace = ref(10 * 1024 * 1024 * 1024) // 10GB
const usedSpace = ref(9.2 * 1024 * 1024 * 1024) // 9.2GB
const availableSpace = computed(() => totalSpace.value - usedSpace.value)
const spacePercentage = computed(() => Math.round((usedSpace.value / totalSpace.value) * 100))
const progressColor = computed(() => {
if (spacePercentage.value > 95) return '#F56C6C'
if (spacePercentage.value > 80) return '#E6A23C'
return '#67C23A'
})
// 文件夾數(shù)據(jù)
const folders = ref([
{
id: 1,
label: '我的文件',
fileCount: 12,
children: [
{ id: 11, label: '設(shè)計(jì)稿', fileCount: 5 },
{ id: 12, label: '文檔', fileCount: 7 }
]
},
{
id: 2,
label: '共享文件',
fileCount: 8,
children: [
{ id: 21, label: '項(xiàng)目資料', fileCount: 3 },
{ id: 22, label: '薪資', fileCount: 5 }
]
}
])
const flatFolders = computed(() => {
const result = []
const flatten = (items, prefix = '') => {
items.forEach(item => {
result.push({
id: item.id,
name: prefix + item.label
})
if (item.children) {
flatten(item.children, prefix + item.label + '/')
}
})
}
flatten(folders.value)
return result
})
const defaultProps = {
children: 'children',
label: 'label'
}
// 當(dāng)前選中的文件夾
const currentFolder = ref({})
// 文件列表
const files = ref([
{ id: 1, filename: '設(shè)計(jì)方案.docx', uploadTime: '2025-08-20 14:30', fileSize: 2.5 * 1024 * 1024, downloadCount: 5, folderId: 11 },
{ id: 2, filename: '產(chǎn)品海報(bào).png', uploadTime: '2025-08-21 09:15', fileSize: 8.7 * 1024 * 1024, downloadCount: 12, folderId: 11 },
{ id: 3, filename: '宣傳視頻.mp4', uploadTime: '2025-08-22 16:45', fileSize: 256 * 1024 * 1024, downloadCount: 8, folderId: 11 },
{ id: 4, filename: '會(huì)議紀(jì)要.pdf', uploadTime: '2025-08-23 11:20', fileSize: 1.2 * 1024 * 1024, downloadCount: 15, folderId: 12 },
{ id: 5, filename: '8月工資條.xlsx', uploadTime: '2025-08-15 10:00', fileSize: 0.5 * 1024 * 1024, downloadCount: 25, folderId: 22 }
])
// 分頁
const currentPage = ref(1)
const pageSize = ref(10)
const totalFiles = ref(files.value.length)
// 搜索
const searchQuery = ref('')
const loading = ref(false)
const emptyText = ref('暫無文件')
// 過濾后的文件列表
const filteredFiles = computed(() => {
let result = files.value
// 按文件夾過濾
if (currentFolder.value.id) {
result = result.filter(file => file.folderId === currentFolder.value.id)
}
// 按搜索關(guān)鍵詞過濾
if (searchQuery.value) {
result = result.filter(file =>
file.filename.toLowerCase().includes(searchQuery.value.toLowerCase())
)
}
return result
})
// 上傳相關(guān)
const uploadDialogVisible = ref(false)
const uploading = ref(false)
const uploadForm = reactive({
folderId: null,
fileList: [],
remark: ''
})
// 配額管理
const quotaDialogVisible = ref(false)
const userQuotas = ref([
{ userId: 1, username: '小王', usedQuota: 9.2 * 1024 * 1024 * 1024, totalQuota: 10 * 1024 * 1024 * 1024, totalQuotaGB: 10 },
{ userId: 2, username: '小李', usedQuota: 1.2 * 1024 * 1024 * 1024, totalQuota: 5 * 1024 * 1024 * 1024, totalQuotaGB: 5 },
{ userId: 3, username: '小張', usedQuota: 3.8 * 1024 * 1024 * 1024, totalQuota: 5 * 1024 * 1024 * 1024, totalQuotaGB: 5 }
])
// 方法
const handleFolderSelect = (data) => {
currentFolder.value = {
id: data.id,
name: data.label
}
}
const handleSizeChange = (val) => {
pageSize.value = val
}
const handleCurrentChange = (val) => {
currentPage.value = val
}
const getFileIcon = (filename) => {
const ext = filename.split('.').pop().toLowerCase()
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(ext)) {
return Picture
} else if (['mp4', 'avi', 'mov', 'wmv', 'flv'].includes(ext)) {
return VideoPlay
} else {
return Document
}
}
const formatSize = (bytes) => {
if (bytes < 1024) return bytes + ' B'
else if (bytes < 1048576) return (bytes / 1024).toFixed(2) + ' KB'
else if (bytes < 1073741824) return (bytes / 1048576).toFixed(2) + ' MB'
else return (bytes / 1073741824).toFixed(2) + ' GB'
}
const showUploadDialog = () => {
uploadDialogVisible.value = true
uploadForm.folderId = currentFolder.value.id || null
}
const beforeUpload = (file) => {
// 檢查文件大小是否超過可用空間
if (file.size > availableSpace.value) {
ElMessage.error(`文件大小超過可用空間!當(dāng)前可用:${formatSize(availableSpace.value)}`)
return false
}
return true
}
const customUpload = ({ file }) => {
// 這里只是模擬添加到上傳列表,實(shí)際項(xiàng)目中會(huì)發(fā)送到服務(wù)器
uploadForm.fileList.push(file)
}
const submitUpload = () => {
if (uploadForm.fileList.length === 0) {
ElMessage.warning('請(qǐng)選擇要上傳的文件')
return
}
uploading.value = true
// 模擬上傳過程
setTimeout(() => {
// 模擬添加文件到列表
const newFiles = uploadForm.fileList.map((file, index) => {
return {
id: files.value.length + index + 1,
filename: file.name,
uploadTime: new Date().toLocaleString(),
fileSize: file.size,
downloadCount: 0,
folderId: uploadForm.folderId
}
})
files.value = [...files.value, ...newFiles]
// 更新已用空間
const totalUploadSize = uploadForm.fileList.reduce((sum, file) => sum + file.size, 0)
usedSpace.value += totalUploadSize
// 重置表單
uploadForm.fileList = []
uploadForm.remark = ''
uploadDialogVisible.value = false
uploading.value = false
ElMessage.success(`成功上傳 ${newFiles.length} 個(gè)文件`)
}, 1500)
}
const downloadFile = (file) => {
// 模擬下載過程
ElMessage.success(`開始下載: ${file.filename}`)
// 更新下載次數(shù)
const index = files.value.findIndex(f => f.id === file.id)
if (index !== -1) {
files.value[index].downloadCount++
}
}
const confirmDelete = (file) => {
ElMessageBox.confirm(
`確定要?jiǎng)h除文件 "${file.filename}" 嗎?`,
'刪除確認(rèn)',
{
confirmButtonText: '確定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
// 刪除文件
const index = files.value.findIndex(f => f.id === file.id)
if (index !== -1) {
// 更新可用空間
usedSpace.value -= files.value[index].fileSize
// 從列表中移除
files.value.splice(index, 1)
ElMessage.success('文件已刪除')
}
}).catch(() => {
// 取消刪除
})
}
const showQuotaDialog = () => {
quotaDialogVisible.value = true
}
const updateQuota = (user) => {
// 轉(zhuǎn)換GB到字節(jié)
user.totalQuota = user.totalQuotaGB * 1024 * 1024 * 1024
ElMessage.success(`已更新 ${user.username} 的空間配額為 ${user.totalQuotaGB}GB`)
}
const getQuotaColor = (used, total) => {
const percentage = (used / total) * 100
if (percentage > 95) return '#F56C6C'
if (percentage > 80) return '#E6A23C'
return '#67C23A'
}
onMounted(() => {
// 模擬加載數(shù)據(jù)
loading.value = true
setTimeout(() => {
loading.value = false
}, 500)
})
</script>
<style scoped>
.file-system-container {
display: flex;
flex-direction: column;
height: 100%;
background-color: #f5f7fa;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 20px;
}
.top-info-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background-color: #fff;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
.user-greeting {
display: flex;
align-items: center;
gap: 10px;
}
.greeting-text {
font-size: 16px;
font-weight: 500;
}
.space-usage {
flex: 1;
margin: 0 30px;
max-width: 400px;
}
.progress-text {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-size: 14px;
color: #606266;
}
.space-usage.warning :deep(.el-progress-bar__inner) {
background-color: #E6A23C;
}
.space-usage.danger :deep(.el-progress-bar__inner) {
background-color: #F56C6C;
}
.main-content {
display: flex;
flex: 1;
gap: 20px;
overflow: hidden;
}
.folder-tree {
width: 250px;
background-color: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
overflow-y: auto;
}
.folder-tree h3 {
margin-top: 0;
margin-bottom: 16px;
padding-bottom: 10px;
border-bottom: 1px solid #ebeef5;
}
.folder-node {
display: flex;
align-items: center;
gap: 5px;
}
.folder-count {
font-size: 12px;
color: #909399;
margin-left: 5px;
}
.file-list {
flex: 1;
background-color: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
overflow: hidden;
}
.file-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 10px;
border-bottom: 1px solid #ebeef5;
}
.file-list-header h3 {
margin: 0;
}
.file-search {
width: 250px;
}
.file-name-cell {
display: flex;
align-items: center;
gap: 8px;
}
.file-icon {
color: #909399;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
:deep(.el-upload-dragger) {
width: 100%;
}
:deep(.el-upload__tip) {
color: #67C23A;
font-weight: bold;
}
</style>
完成!
六、總結(jié)
這個(gè)案例可以解決團(tuán)隊(duì)三大痛點(diǎn):
- 文件不丟不亂:有目錄、有記錄
- 權(quán)限清晰:誰傳的、誰下載的,一目了然
- 空間可控:不會(huì)因?yàn)橐粋€(gè)人傳大文件,影響所有人
如果你團(tuán)隊(duì)也有文件管理的煩惱,不妨試試這個(gè)方案。
以上就是基于SpringBoot+MySQL+Vue實(shí)現(xiàn)文件共享系統(tǒng)的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot Vue文件共享的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java?正則表達(dá)式高級(jí)用法實(shí)例詳解
Java?中的正則表達(dá)式是強(qiáng)大的文本處理工具,可以用于搜索、匹配、替換和分割字符串,下面給大家介紹Java?正則表達(dá)式高級(jí)用法,感興趣的朋友一起看看吧2025-06-06
不到十行實(shí)現(xiàn)javaCV圖片OCR文字識(shí)別
識(shí)別圖片中的文字,會(huì)省很多時(shí)間,本文介紹了javaCV圖片OCR文字識(shí)別,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05
基于springboot實(shí)現(xiàn)redis分布式鎖的方法
這篇文章主要介紹了基于springboot實(shí)現(xiàn)redis分布式鎖的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11
Shiro + JWT + SpringBoot應(yīng)用示例代碼詳解
這篇文章主要介紹了Shiro (Shiro + JWT + SpringBoot應(yīng)用),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-06-06
spring定時(shí)器定時(shí)任務(wù)到時(shí)間未執(zhí)行問題的解決
這篇文章主要介紹了spring定時(shí)器定時(shí)任務(wù)到時(shí)間未執(zhí)行問題的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11

