paopao/docs/tasks/任务9-Sprint-2- 文件上传.md
2024-05-22 14:16:51 +08:00

11 KiB
Raw Permalink Blame History

任务名称: 文件上传

目标:

  • 掌握框架中文件上传的实现
  • 了解MapStruct的自定义映射

预备知识:

操作步骤

对于文件上传,服务器端需要做:

  • 文件上传的接收
  • 文件存储

存储方案有: 文件系统、云存储如Amazon S3、阿里云OSS、数据库对于小文件等。

本实现使用文件系统存储。

1. 撰写附件上传接口的API文档

2. 上传相关的自定义配置

application.yml中添加以下配置:

app:
  upload:
    path: /home/whz/tmp/upload # 文件上传的存储路径
    sub-path-name-length: 2 # 子路径名称的长度
    base-url: http://localhost:8080/upload # 文件访问的URL

实现一个对应的配置类,目的是为了后续的AttachmentMapper中获取配置

package com.lk.paopao.conf;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "app.upload")
public class AppUploadProperties {
    String baseUrl;
}

3. 数据结构

实现附件上传的实体类Attachment

package com.lk.paopao.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.Comment;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

@Getter
@Setter
@Comment("附件")
@Entity
@Table(name = "p_attachment", indexes = {
        @Index(name = "idx_attachment_user", columnList = "user_id")
})
@EntityListeners(AuditingEntityListener.class)
public class Attachment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "file_size", nullable = false)
    private Long fileSize = 0L;

    @ColumnDefault("0")
    @Column(name = "img_width", nullable = false)
    private Integer imgWidth = 0;

    @ColumnDefault("0")
    @Column(name = "img_height", nullable = false)
    private Integer imgHeight = 0;

    @Comment("1图片2视频3其他附件")
    @Column(name = "type", nullable = false)
    private Byte type = 1;

    @Column(name = "content", nullable = false)
    private String content;

    @Comment("创建时间")
    @CreatedDate
    @Column(name = "created_on", nullable = false)
    private Long createdOn;

    @Comment("修改时间")
    @LastModifiedDate
    @Column(name = "modified_on", nullable = false)
    private Long modifiedOn;

    @Comment("删除时间")
    @Column()
    private Long deletedOn = 0L;

    @Comment("是否删除 0 为未删除、1 为已删除")
    @Column(name = "is_del", nullable = false)
    private byte isDel = 0;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

}

实现对应的DTO类AttachmentDo

package com.lk.paopao.dto;

import lombok.Value;

import java.io.Serializable;

@Value
public class AttachmentDto implements Serializable {
    Long id;
    Long fileSize;
    Integer imgWidth;
    Integer imgHeight;
    Byte type;
    String content;
    Long createdOn;
    Long userId;
}

实现对应的MapStruct映射

package com.lk.paopao.mapper;

import com.lk.paopao.conf.AppUploadProperties;
import com.lk.paopao.dto.AttachmentDto;
import com.lk.paopao.entity.Attachment;
import org.mapstruct.*;
import org.springframework.beans.factory.annotation.Autowired;

@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE, componentModel = MappingConstants.ComponentModel.SPRING)
public abstract class AttachmentMapper {

    @Autowired
    AppUploadProperties appUploadProperties ;

    @Mapping(source = "user.id", target = "userId")
    @Mapping(target = "content", expression = "java(addHttpPrefix(attachment.getContent()))")
    public abstract AttachmentDto toDto(Attachment attachment);

    @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
    public abstract  Attachment partialUpdate(AttachmentDto attachmentDto, @MappingTarget Attachment attachment);

    String addHttpPrefix(String content) {
        if (content != null && !content.startsWith("http://") && !content.startsWith("https://")) {
            // 添加HTTP前缀并去掉"//"
            return appUploadProperties.getBaseUrl() + content;
        }
        return content;
    }
}

注意

  • addHttpPrefix 方法用于将上传的文件路径转换为完整的URL。
  • 使用了抽象类AttachmentMapper,目的是能够使用@Autowired注解注入AppUploadProperties对象。
  • @Mapping 注解用于指定映射规则。
  • @BeanMapping 注解用于指定部分属性的更新策略。

4. 创建JPA接口 AttachmentRepository

package com.lk.paopao.repository;

import com.lk.paopao.entity.Attachment;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AttachmentRepository extends JpaRepository<Attachment, Long> {
}

5. 创建对应的Service类AttachService

AttachService完成附件上传的业务逻辑。

package com.lk.paopao.service;

import com.lk.paopao.dto.AttachmentDto;
import com.lk.paopao.entity.Attachment;
import com.lk.paopao.entity.User;
import com.lk.paopao.mapper.AttachmentMapper;
import com.lk.paopao.repository.AttachmentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@Service
public class AttachService {

    // 从配置中获取上传路径
    @Value("${app.upload.path}")
    String UPLOAD_DIR;

    // 从配置中获取子路径名长度,用于文件分目录存储
    @Value("${app.upload.sub-path-name-length}")
    int SUB_PATH_LENGTH = 1;

    @Autowired
    AuthService authService;
    @Autowired
    AttachmentRepository attachmentRepository;
    @Autowired
    AttachmentMapper attachmentMapper;


    /**
     * 上传附件
     * @param file  文件
     * @return AttachmentDto
     * @throws IOException 输入输出异常
     */
    public AttachmentDto upload(MultipartFile file) throws IOException {
        // 获取原始文件名
        String originFileName = file.getOriginalFilename();
        if(originFileName == null){
            throw new IllegalArgumentException("没有原始文件名");
        }

        InputStream inputStream = file.getInputStream();
        // 获取当前用户
        User currentUser = authService.getCurrentUser();
        // 上传文件并获取相对URL
        String relativeUrl = save(originFileName,inputStream);
        Attachment attachment = new Attachment();
        // 设置附件类型
        attachment.setType(getType(file.getContentType()));
        // 设置附件内容即存储的URL
        attachment.setContent(relativeUrl);
        // 关联用户
        attachment.setUser(currentUser);
        // 设置文件大小
        attachment.setFileSize(file.getSize());
        // 保存附件
        Attachment savedAttachment = attachmentRepository.save(attachment);
        // 转换并返回附件DTO
        return attachmentMapper.toDto(savedAttachment);
    }

    /**
     * 保存文件到上传目录中
     * @param fileName 原文件名
     * @param fileStream 文件流
     * @return 保存后的文件访问路径
     * @throws IOException 文件操作异常
     */
    public String save(String fileName, InputStream fileStream) throws IOException {
        // 生成新的文件名以避免重复
        String rename = UUID.randomUUID().toString();
        int dotIndex = fileName.lastIndexOf('.');
        // 新文件名,包含扩展名
        String newFile = rename+((dotIndex > 0) ? "."+fileName.substring(dotIndex + 1) : "");

        // 根据子路径长度计算文件保存的子目录
        String subPath = rename.substring(0,SUB_PATH_LENGTH);
        // 完整的保存路径
        String uploadPath = UPLOAD_DIR+"/"+subPath;

        // 确保目录存在,不存在则创建
        Path directory = Paths.get(uploadPath);
        if (!Files.exists(directory)) {
            Files.createDirectories(directory);
        }
        // 文件的最终保存位置
        Path targetLocation = Paths.get(uploadPath, newFile);
        // 复制文件流到目标位置
        Files.copy(fileStream, targetLocation);
        // 返回文件的访问路径
        return "/"+subPath+"/"+newFile;
    }

    /**
     * 根据文件的MIME类型返回文件类型标识
     * @param contentType 文件的MIME类型
     * @return 文件类型的标识字节0表示未知类型
     */
    public static byte getType(String contentType) {

        if(contentType == null){
            return 0;
        }
        if(contentType.startsWith("image/")){
            return 1;
        }
        if(contentType.startsWith("video/")){
            return 2;
        }
        if(contentType.startsWith("application/")){
            return 3;
        }
        if(contentType.startsWith("text/")){
            return 4;
        }
        return 0;
    }
}

6. 创建控制器AttachmentController

package com.lk.paopao.controller;


import com.lk.paopao.dto.AttachmentDto;
import com.lk.paopao.dto.rest.response.DataResult;
import com.lk.paopao.dto.rest.response.ResultUtil;
import com.lk.paopao.service.AttachService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

@RestController
@RequestMapping("${app.version}/attachment")
public class AttachmentController {
    @Autowired
    AttachService attachService;

    @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public DataResult<AttachmentDto> upload(@RequestParam("file")  MultipartFile file) throws IOException {

        AttachmentDto dto = attachService.upload(file);
        return ResultUtil.ok(dto);
    }
}

说明:

  • @RequestParam("file"): 表示请求参数中的文件

技术/工具需求:

  • [列出完成任务所需的技术栈、工具、软件版本等。]

成功标准:

  • [明确完成任务的评判标准,如代码功能实现、性能指标、测试通过条件等。]

扩展学习(可选):

  • [提供一些额外学习资源或挑战性任务,鼓励学有余力的学生进一步探索。]

评估与反馈:

  • [说明如何提交作业、代码审查的标准、或任何反馈收集机制。]

时间估算:

  • [给出预计完成该任务所需的时间,帮助学生合理安排学习计划。]