1、数据库表设计
CREATE TABLE `files` (
`id` varchar(40) NOT NULL COMMENT '主键',
`name` varchar(255) DEFAULT NULL COMMENT '文件名',
`url` varchar(255) DEFAULT NULL COMMENT '下载URL',
`type` varchar(40) DEFAULT NULL COMMENT '文件类型',
`size` bigint DEFAULT NULL COMMENT '文件大小(KB)',
`md5` varchar(255) DEFAULT NULL COMMENT '文件唯一标识',
`creator_id` varchar(40) DEFAULT NULL COMMENT '创建人ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`ts` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_flag` int NOT NULL DEFAULT '0' COMMENT '逻辑删除字段(0:正常,1:删除)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='文件表';
2、Java 后端实现
2.1 application.yml 指定文件上传下载目录
server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/xxx
username: xxx
password: xxx
# application.yml 指定文件上传下载目录
files:
upload:
path: D:/files/
2.2 Files-实体类
package com.dragon.springboot3vue3.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* <p>
* 文件表
* </p>
*/
@Data
@Schema(name = "Files", description = "文件表")
public class Files implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "主键")
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
@Schema(description = "文件名")
private String name;
@Schema(description = "下载URL")
private String url;
@Schema(description = "文件类型")
private String type;
@Schema(description = "文件大小")
private long size;
@Schema(description = "文件唯一标识")
private String md5;
@Schema(description = "创建人ID")
@TableField(fill = FieldFill.INSERT)
private String creatorId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime ts;
@Schema(description = "逻辑删除字段(0:正常,1:删除)")
@TableLogic
private Integer deleteFlag;
}
2.3 FilesController (注意:要放行 "/files/{fileName}" 接口)
package com.dragon.springboot3vue3.controller;
import cn.dev33.satoken.util.SaResult;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.dragon.springboot3vue3.controller.dto.pageDto.FilesPageDto;
import com.dragon.springboot3vue3.entity.Files;
import com.dragon.springboot3vue3.service.IFilesService;
import com.dragon.springboot3vue3.utils.StringIdsDTO;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* <p>
* 文件表 前端控制器
* </p>
*
* @author dragon
* @since 2024-05-25
*/
@Tag(name = "文件上传下载接口")
@RestController
@RequestMapping("/files")
public class FilesController {
@Autowired
private IFilesService filesService;
@Value("${files.upload.path}")
private String path;
/**
* 文件上传
* @param file
* @return
* @throws IOException
*/
@Operation(summary = "文件上传")
@PostMapping("/upload")
public SaResult upload(@RequestParam MultipartFile file) throws IOException {
String originalFilename = file.getOriginalFilename();
String type = FileUtil.extName(originalFilename);
long size = file.getSize();
// 如果文件目录不存在,则新建
File uploadParentFile = new File(path);
if(!uploadParentFile.exists()){
uploadParentFile.mkdirs();
}
// 保证存储的文件名唯一
String fileName=IdUtil.fastSimpleUUID() + StrUtil.DOT + type;
File uploadFile = new File(path + fileName);
// 设置下载的url
String url = "http://localhost:8080/files/"+fileName;
// 将文件存储到磁盘
file.transferTo(uploadFile);
// 生成文件唯一标识 md5,保证不会在磁盘存储重复的文件
String md5 = SecureUtil.md5(uploadFile);
List<Files> list = filesService.lambdaQuery().eq(Files::getMd5, md5).list();
if(CollectionUtil.isNotEmpty(list)){
url=list.getFirst().getUrl();
uploadFile.delete();
}
// 文件信息存储到数据库
Files saveFile = new Files();
saveFile.setName(originalFilename);
saveFile.setType(type);
// 单位转换 B -> KB
saveFile.setSize(size/1024);
saveFile.setUrl(url);
saveFile.setMd5(md5);
filesService.save(saveFile);
return SaResult.ok().setData(url);
}
@Operation(summary = "文件下载")
@GetMapping("/{fileName}")
public void download(@PathVariable String fileName, HttpServletResponse response) throws IOException {
// 在指定目录下,根据文件名查找文件
File file = new File(path + fileName);
ServletOutputStream outputStream = response.getOutputStream();
// 设置输出流格式
response.addHeader("Content-Disposition","attachment;filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));
response.setContentType("application/octet-stream");
// 读取文件字节流
outputStream.write(FileUtil.readBytes(file));
outputStream.flush();
outputStream.close();
}
@Operation(summary = "分页列表")
@PostMapping("/list")
public SaResult list(@RequestBody FilesPageDto pageDto){
// 创建分页对象
Page<Files> page=new Page<>(pageDto.getCurrentPage(), pageDto.getPageSize());
// 构造询条件
MPJLambdaWrapper<Files> qw=new MPJLambdaWrapper<Files>()
.like(StringUtils.isNotBlank(pageDto.getName()),Files::getName, pageDto.getName())
.like(StringUtils.isNotBlank(pageDto.getType()),Files::getType, pageDto.getType())
.orderByDesc(Files::getCreateTime);
// 根据查询条件,将结果封装到分页对象
Page<Files> response = filesService.page(page, qw);
return SaResult.ok().setData(response);
}
@Operation(summary = "删除")
@DeleteMapping("/remove")
public SaResult remove(@RequestBody @Validated StringIdsDTO stringIdsDTO){
filesService.removeByIds(stringIdsDTO.getIds());
return SaResult.ok();
}
}
2.4 IFilesService
package com.dragon.springboot3vue3.service;
import com.dragon.springboot3vue3.entity.Files;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 文件表 服务类
* </p>
*
*/
public interface IFilesService extends IService<Files> {
}
2.5 FilesServiceImpl
package com.dragon.springboot3vue3.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.dragon.springboot3vue3.entity.Files;
import com.dragon.springboot3vue3.mapper.FilesMapper;
import com.dragon.springboot3vue3.service.IFilesService;
import org.springframework.stereotype.Service;
/**
* <p>
* 文件表 服务实现类
* </p>
*
*/
@Service
public class FilesServiceImpl extends ServiceImpl<FilesMapper, Files> implements IFilesService {
}
3、Vue 3 前端实现
3.1 前端实现效果
![](https://img-blog.csdnimg.cn/direct/d32e38152d7b44bd9f20240bdc1f9686.png)
3.2 前端实现文件上传下载、图片预览
<template>
<el-card class="container">
<template #header>
<div class="header">
<el-breadcrumb :separator-icon="ArrowRight">
<el-breadcrumb-item :to="{ path: '/home/index' }" class="title">首页</el-breadcrumb-item>
<el-breadcrumb-item class="title">文件管理</el-breadcrumb-item>
</el-breadcrumb>
<div class="right">
<el-upload :action="url" :show-file-list="false" :on-success="handleUploadSuccess" :on-error="handleUploadError">
<el-button :icon="Upload" type="primary">上传文件</el-button>
</el-upload>
<el-button class="batchRemove" type="danger" @click="batchRemove">批量删除</el-button>
</div>
</div>
</template>
<!-- 搜索表单 -->
<el-form inline>
<el-form-item label="文件名">
<el-input v-model="searchModel.name" placeholder="请输入文件名" style="width: 150px" clearable></el-input>
</el-form-item>
<el-form-item label="文件类型">
<el-input v-model="searchModel.type" placeholder="请输入文件类型" style="width: 150px" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getFilesList">搜索</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- 列表 -->
<el-table :data="filesList" border stripe style="width: 100%" height="550" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" />
<el-table-column label="文件">
<template #default="{ row }">
<el-image class="image" v-if="row.type=='jpg' || row.type=='png' || row.type=='jpeg'" :src="row.url" fit="fill" @click="showDialog(row)" />
<el-tag v-else>{{ row.type }} 文件,请下载查看</el-tag>
</template>
</el-table-column>
<el-table-column label="文件名" prop="name"></el-table-column>
<el-table-column label="文件类型" prop="type"></el-table-column>
<el-table-column label="文件大小(KB)" prop="size"> </el-table-column>
<el-table-column label="创建时间" prop="createTime"></el-table-column>
<el-table-column label="更新时间" prop="ts"> </el-table-column>
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-tooltip effect="dark" placement="top" content="下载">
<el-button :icon="Download" circle plain type="primary" @click="download(row)"></el-button>
</el-tooltip>
<el-tooltip effect="dark" placement="top" content="删除">
<el-button :icon="Delete" circle plain type="danger" @click="remove(row)"></el-button>
</el-tooltip>
</template>
</el-table-column>
<template #empty>
<el-empty description="没有数据" />
</template>
</el-table>
<!-- 图片预览 -->
<el-dialog v-model="dialogVisible" width="50%">
<div class="previewBox">
<el-image :src="imageUrl" :preview-src-list="[imageUrl]" fit="fill" />
</div>
</el-dialog>
<!-- 分页 -->
<el-pagination
v-model:current-page="searchModel.currentPage"
v-model:page-size="searchModel.pageSize"
:page-sizes="[10, 30, 50, 100]"
layout="jumper, total, sizes, prev, pager, next"
:total="searchModel.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
style="margin: 10px 0; justify-content: flex-end"
/>
</el-card>
</template>
<script setup lang="ts">
import { ref,reactive,onMounted } from 'vue'
import { Delete,ArrowRight,Upload,Download } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import filesApi from '@/api/files'
const filesList=ref()
// 批量删除的 id
const ids=ref<string[]>([])
// 文件上传的url
const url=ref('')
// 图片预览的url
const imageUrl = ref('')
const dialogVisible = ref(false)
// 分页&搜索模型
const searchModel=reactive({
currentPage:1,
pageSize:10,
total:0,
name:'',
type:''
})
const initSearchModel={ ...searchModel }
// pageSize 变化时触发
const handleSizeChange = (val: number) => {
searchModel.pageSize=val;
getFilesList();
}
// currentPage 变化时触发
const handleCurrentChange = (val: number) => {
searchModel.currentPage=val;
getFilesList();
}
// 菜单列表
const getFilesList= async()=>{
const response= await filesApi.list(searchModel);
filesList.value=response.data.records;
searchModel.currentPage=response.data.current;
searchModel.pageSize=response.data.size;
searchModel.total=response.data.total;
}
// 重置搜索表单
const reset= ()=>{
Object.assign(searchModel, initSearchModel);
getFilesList();
}
// 批量删除选择
const handleSelectionChange = (rows: any) => {
ids.value = rows.map((item:any) => item.id);
}
// 批量删除
const batchRemove= ()=>{
if(ids.value.length > 0){
ElMessageBox.confirm(
`是否批量删除?`,
'温馨提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async() => {
await filesApi.remove(ids.value);
ElMessage({ type: 'success',message: '删除成功' });
getFilesList();
})
}else{
ElMessage.warning('请选择批量删除项');
}
}
// 单条删除
const remove= async(row:any)=>{
ElMessageBox.confirm(
`是否删除 [ ${row.name} ] 文件?`,
'温馨提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async() => {
ids.value.push(row.id);
await filesApi.remove(ids.value);
ElMessage({ type: 'success', message: '删除成功' });
getFilesList();
})
}
// 下载
const download= (row:any)=>{
window.open(row.url);
}
// 上传成功处理的事件
const handleUploadSuccess = () => {
ElMessage.success("上传成功");
getFilesList();
}
// 上传失败处理的事件
const handleUploadError = () => {
ElMessage.error("上传失败");
}
// 获取图片上传的url
const getUrl= ()=>{
url.value=filesApi.url();
}
// 图片预览
const showDialog = (row:any) => {
dialogVisible.value = true;
imageUrl.value=row.url;
};
onMounted(()=>{
getFilesList();
getUrl();
})
</script>
<style scoped lang="less">
.container{
height: 100%;
box-sizing: border-box;
}
.header{
display: flex;
align-items: center;
justify-content: space-between;
}
.right{
display: flex;
}
.batchRemove{
margin-left: 10px;
}
.title{
font-size: large;
font-weight: 600;
}
.image{
height: 100px;
}
.previewBox {
display: flex;
justify-content: center;
align-items: center;
}
</style>