SpringBoot3 + Vue3 前后端分离项目实现文件上传下载

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 前端实现效果

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>

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
搭建Spring BootVue前后端分离项目需要以下步骤: 1. 安装环境:首先,需要安装Java JDK和Maven来支持Spring Boot的开发。可以去官方网站下载并安装。然后,安装Node.js和Vue CLI来支持Vue的开发。 2. 创建Spring Boot项目:使用Maven创建一个Spring Boot项目,可以使用宝塔面板自带的Maven插件进行构建,也可以通过命令行进入项目目录,运行`mvn clean install`来创建项目。 3. 编写后端代码:在Spring Boot项目中编写后端逻辑,包括接口的实现、数据库的操作等。可以选择使用Spring Data JPA来简化对数据库的操作。 4. 创建Vue项目:使用Vue CLI创建一个Vue项目,可以通过宝塔面板自带的Node.js插件进行创建,也可以通过命令行运行`vue create 项目名`来创建。 5. 编写前端代码:在Vue项目中编写前端逻辑,包括页面的布局、调用后台接口等。可以使用axios库来发送HTTP请求。 6. 配置跨域:由于前后端分离项目可能存在不同端口的情况,需要进行跨域配置。可以在Spring Boot项目的配置文件中添加`@CrossOrigin`注解,允许指定的域访问接口。 7. 打包部署:完成开发后,可以使用Maven将后端项目打包成一个独立的Jar文件。然后,将打包好的Jar文件上传到宝塔面板上的对应目录。将Vue项目使用`npm run build`命令打包成静态文件,然后将打包好的文件上传到宝塔面板对应的目录。 8. 配置Nginx反向代理:为了将前后端项目结合在一起,可以使用Nginx配置反向代理,将前端的请求转发给后端的接口。在宝塔面板上找到对应的站点,进行Nginx配置。 通过以上步骤,就可以搭建一个基于Spring BootVue前后端分离项目,并将项目部署到宝塔面板上进行访问和使用。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值