## 任务名称: 文件上传 ### 目标: - 掌握框架中文件上传的实现 - 了解MapStruct的自定义映射 - ### 预备知识: - [multipart/form-data](../guides/form-data.md) ### 操作步骤 对于文件上传,服务器端需要做: - **文件上传的接收** - **文件存储** 存储方案有: 文件系统、云存储(如Amazon S3、阿里云OSS)、数据库(对于小文件)等。 本实现使用文件系统存储。 #### 1. 撰写附件上传接口的API文档 #### 2. 上传相关的自定义配置 在`application.yml`中添加以下配置: ```yaml app: upload: path: /home/whz/tmp/upload # 文件上传的存储路径 sub-path-name-length: 2 # 子路径名称的长度 base-url: http://localhost:8080/upload # 文件访问的URL ``` 实现一个对应的配置类,目的是为了后续的AttachmentMapper中获取配置: ```java 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** ```java 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** ```java 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映射** ```java 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 ```java package com.lk.paopao.repository; import com.lk.paopao.entity.Attachment; import org.springframework.data.jpa.repository.JpaRepository; public interface AttachmentRepository extends JpaRepository { } ``` #### 5. 创建对应的Service类AttachService AttachService完成附件上传的业务逻辑。 ```java 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 ```java 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 upload(@RequestParam("file") MultipartFile file) throws IOException { AttachmentDto dto = attachService.upload(file); return ResultUtil.ok(dto); } } ``` 说明: - @RequestParam("file"): 表示请求参数中的文件 ### 技术/工具需求: - [列出完成任务所需的技术栈、工具、软件版本等。] ### 成功标准: - [明确完成任务的评判标准,如代码功能实现、性能指标、测试通过条件等。] ### 扩展学习(可选): - [提供一些额外学习资源或挑战性任务,鼓励学有余力的学生进一步探索。] ### 评估与反馈: - [说明如何提交作业、代码审查的标准、或任何反馈收集机制。] ### 时间估算: - [给出预计完成该任务所需的时间,帮助学生合理安排学习计划。]