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

380 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 任务名称: 文件上传
### 目标:
- 掌握框架中文件上传的实现
- 了解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<Attachment, Long> {
}
```
#### 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<AttachmentDto> upload(@RequestParam("file") MultipartFile file) throws IOException {
AttachmentDto dto = attachService.upload(file);
return ResultUtil.ok(dto);
}
}
```
说明:
- @RequestParam("file"): 表示请求参数中的文件
### 技术/工具需求:
- [列出完成任务所需的技术栈、工具、软件版本等。]
### 成功标准:
- [明确完成任务的评判标准,如代码功能实现、性能指标、测试通过条件等。]
### 扩展学习(可选):
- [提供一些额外学习资源或挑战性任务,鼓励学有余力的学生进一步探索。]
### 评估与反馈:
- [说明如何提交作业、代码审查的标准、或任何反馈收集机制。]
### 时间估算:
- [给出预计完成该任务所需的时间,帮助学生合理安排学习计划。]