diff --git a/docs/api doc/附件上传接口.md b/docs/api doc/附件上传接口.md new file mode 100644 index 0000000..d73def3 --- /dev/null +++ b/docs/api doc/附件上传接口.md @@ -0,0 +1,99 @@ +## 1. 附件上传 + +### 1.1 接口描述 +上传图片、视频、压缩文件等。 +### 1.2 请求URL +`/v1/post` + +### 1.3 请求方式 +**POST** + +multipart/form-data方式上传文件 + +### 1.4 请求头示例 +```http +Content-Type: multipart/form-data; boundary=----12256474795412178794287418104 +``` + +### 1.6 请求体示例 +```http +-----------------------------12256474795412178794287418104 +Content-Disposition: form-data; name="type" + +public/image +-----------------------------12256474795412178794287418104 +Content-Disposition: form-data; name="file"; filename="2024-03-18_22-06.png" +Content-Type: image/png +这里是文件内容... +-----------------------------12256474795412178794287418104 + +``` +```http +-----------------------------35576727471291609851653063070 +Content-Disposition: form-data; name="type" +attachment +-----------------------------35576727471291609851653063070 +Content-Disposition: form-data; name="file"; filename="paopao.zip" +Content-Type: application/zip +这里是文件内容... +-----------------------------35576727471291609851653063070 +``` + +### 1.7 返回参数说明 +| 参数名称 | 必填 | 数据类型 | 约束条件 | 描述 | +|------| ---- | -------- | -- |-----------| +| code | 是 | 整型 | | 错误码,0表示成功 | +| msg | 否 | 字符串 | | 错误信息描述 | +| data | 否 | json | | 具体业务数据 | + + +data结构说明: + +| 参数名称 | 必填 | 数据类型 | 约束条件 | 描述 | +|-------| ---- | -------- |------|-----------| +| | | | | | + +### 1.8 响应示例(成功) + +```json +{ + "code": 0, + "msg": "success", + "data": { + "user_id": 100066, + "file_size": 249763, + "img_width": 0, + "img_height": 0, + "type": 1, + "content": "http://60.204.241.255:8008/oss/paopao/public/image/4f/77/ec/5f/85ad-4189-887d-785cac178e21.png" + } +} +``` +### 1.9 响应示例(错误) +```json +{ "code": 500, "msg": "失败" } +``` + +### 1.10 错误响应码参考 +更多响应错误码及含义,请参阅[API响应码表](URL/for/api/responseCode/table)。 + +### 1.11 安全性与认证 + + +### 1.12 测试环境 +访问测试环境以进行接口调试: +[https://test.apiAddress.com](https://test.apiAddress.com) + +### 1.13 版本管理 +本API通过URI路径进行版本控制。请在请求URL中包含`/v1`以使用当前版本。 + +### 1.14 更新记录 +- **2024-04-29**:发布文档初始版本。 + + +### 1.15 联系支持 +如需帮助或对API有任何疑问,请通过电子邮件与我们联系:[support@api.com](mailto:support@api.com)。 + +### 1.16 反馈与建议 +发现文档问题或有改进建议?请填写[反馈表单](https://forms.api.com/feedback)与我们分享。 + diff --git a/docs/guides/form-data.md b/docs/guides/form-data.md new file mode 100644 index 0000000..36f10f7 --- /dev/null +++ b/docs/guides/form-data.md @@ -0,0 +1,46 @@ + +## multipart/form-data + +`multipart/form-data` 是一种在HTTP请求中用于上传文件和提交表单数据的编码类型。它主要用于POST请求,允许在同一个请求中发送多个不同类型的数据部分,比如文本字段、文件、二进制数据等。这种格式特别适合于文件上传场景,因为它可以处理大体积数据,并且能够维持数据的边界和类型区分。以下是关于`multipart/form-data`的一些关键点介绍: + +### 基本概念 + +- **多部分**: 表示请求体由多个部分组成,每个部分对应表单中的一个字段或一个文件。 +- **边界**: `multipart/form-data`请求体中,各部分之间由特定的边界字符串分隔。这个边界字符串是唯一的,通常由客户端生成并在Content-Type头中声明,以确保服务器能够正确解析各个部分。 + +### 使用场景 + +- **文件上传**: 当需要通过表单上传文件时,必须使用`multipart/form-data`。这是因为普通的表单编码(如`application/x-www-form-urlencoded`)不支持二进制数据。 +- **大体积或复杂数据**: 对于包含大量文本或需要保持数据原始格式(如富文本编辑器的内容)的情况,使用`multipart/form-data`也是合适的。 + +### 请求示例 + +一个典型的`multipart/form-data`请求看起来像这样(简化版,实际请求会包含更多HTTP头部信息): + +``` +POST /upload HTTP/1.1 +Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW + +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="text" + +Hello, World! +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="file"; filename="example.txt" +Content-Type: text/plain + +This is an example file content. +------WebKitFormBoundary7MA4YWxkTrZu0gW-- +``` + +### 关键部分说明 + +- **Content-Type**: 包含了`boundary`参数,定义了分隔各个部分的边界字符串。 +- **Content-Disposition**: 指定了该部分的名称(如果是表单字段)或文件名(如果是文件上传),以及它是表单数据的一部分还是文件。 +- **实际数据**: 在每个部分的头部信息之后,紧接着就是该部分的实际内容,可以是文本、图片、视频等任何形式的数据。 + +### 处理`multipart/form-data` + +服务器端处理这类请求时,需要解析请求体,识别出各个部分,并根据Content-Disposition头部确定每个部分的类型和处理方式。大多数现代Web框架(如Spring Boot、Django、Express.js等)都提供了内置的支持来简化这一过程。 + +总之,`multipart/form-data`是一种在HTTP请求中携带复杂数据(特别是文件)的标准格式,广泛应用于Web表单提交和API接口设计中。 \ No newline at end of file diff --git a/docs/tasks/任务9-Sprint-2- 文件上传.md b/docs/tasks/任务9-Sprint-2- 文件上传.md new file mode 100644 index 0000000..4ebe6d6 --- /dev/null +++ b/docs/tasks/任务9-Sprint-2- 文件上传.md @@ -0,0 +1,397 @@ +## 任务名称: 文件上传 +### 目标: +- 掌握框架中文件上传的实现 +- 了解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) { + // 存储MIME类型与文件类型标识的映射 + Map map = new HashMap<>(); + + // 初始化映射,定义各类文件的类型标识 + map.put("image/jpeg", (byte)1); + map.put("image/png", (byte)1); + map.put("image/gif", (byte)1); + map.put("image/bmp", (byte)1); + map.put("image/webp", (byte)1); + map.put("image/svg+xml", (byte)1); + map.put("image/tiff", (byte)1); + map.put("image/x-icon", (byte)1); + map.put("image/vnd.microsoft.icon", (byte)1); + map.put("video/mp4", (byte)2); + map.put("video/x-msvideo", (byte)2); + map.put("video/x-ms-wmv", (byte)2); + map.put("video/x-flv", (byte)2); + map.put("video/quicktime", (byte)2); + map.put("video/x-matroska", (byte)2); + map.put("video/webm", (byte)2); + map.put("video/ogg", (byte)2); + map.put("application/zip", (byte)3); + map.put("application/x-7z-compressed", (byte)3); + map.put("application/x-rar-compressed", (byte)3); + map.put("application/x-tar", (byte)3); + map.put("application/x-gzip", (byte)3); + map.put("application/x-bzip2", (byte)3); + map.put("application/x-xz", (byte)3); + map.put("application/pdf", (byte)4); + map.put("application/msword", (byte)4); + // 获取文件类型标识 + Byte type = map.get(contentType); + //获取不到type,则返回0 + return type==null?0:type; + } +} +``` +#### 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"): 表示请求参数中的文件 + +### 技术/工具需求: +- [列出完成任务所需的技术栈、工具、软件版本等。] + +### 成功标准: +- [明确完成任务的评判标准,如代码功能实现、性能指标、测试通过条件等。] + +### 扩展学习(可选): +- [提供一些额外学习资源或挑战性任务,鼓励学有余力的学生进一步探索。] + +### 评估与反馈: +- [说明如何提交作业、代码审查的标准、或任何反馈收集机制。] + +### 时间估算: +- [给出预计完成该任务所需的时间,帮助学生合理安排学习计划。] \ No newline at end of file