diff --git a/src/main/java/com/lk/paopao/conf/AppUploadProperties.java b/src/main/java/com/lk/paopao/conf/AppUploadProperties.java new file mode 100644 index 0000000..8c111ab --- /dev/null +++ b/src/main/java/com/lk/paopao/conf/AppUploadProperties.java @@ -0,0 +1,14 @@ +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; +} diff --git a/src/main/java/com/lk/paopao/controller/AttachmentController.java b/src/main/java/com/lk/paopao/controller/AttachmentController.java new file mode 100644 index 0000000..07bf2ca --- /dev/null +++ b/src/main/java/com/lk/paopao/controller/AttachmentController.java @@ -0,0 +1,30 @@ +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); + } +} diff --git a/src/main/java/com/lk/paopao/dto/AttachmentDto.java b/src/main/java/com/lk/paopao/dto/AttachmentDto.java new file mode 100644 index 0000000..048ed47 --- /dev/null +++ b/src/main/java/com/lk/paopao/dto/AttachmentDto.java @@ -0,0 +1,20 @@ +package com.lk.paopao.dto; + +import lombok.Value; + +import java.io.Serializable; + +/** + * DTO for {@link com.lk.paopao.entity.Attachment} + */ +@Value +public class AttachmentDto implements Serializable { + Long id; + Long fileSize; + Integer imgWidth; + Integer imgHeight; + Byte type; + String content; + Long createdOn; + Long userId; +} \ No newline at end of file diff --git a/src/main/java/com/lk/paopao/entity/Attachment.java b/src/main/java/com/lk/paopao/entity/Attachment.java new file mode 100644 index 0000000..cbc26a2 --- /dev/null +++ b/src/main/java/com/lk/paopao/entity/Attachment.java @@ -0,0 +1,65 @@ +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; + +} \ No newline at end of file diff --git a/src/main/java/com/lk/paopao/mapper/AttachmentMapper.java b/src/main/java/com/lk/paopao/mapper/AttachmentMapper.java new file mode 100644 index 0000000..681fbe1 --- /dev/null +++ b/src/main/java/com/lk/paopao/mapper/AttachmentMapper.java @@ -0,0 +1,30 @@ +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; + } + +} \ No newline at end of file diff --git a/src/main/java/com/lk/paopao/repository/AttachmentRepository.java b/src/main/java/com/lk/paopao/repository/AttachmentRepository.java new file mode 100644 index 0000000..7a4c287 --- /dev/null +++ b/src/main/java/com/lk/paopao/repository/AttachmentRepository.java @@ -0,0 +1,7 @@ +package com.lk.paopao.repository; + +import com.lk.paopao.entity.Attachment; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AttachmentRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/lk/paopao/service/AttachService.java b/src/main/java/com/lk/paopao/service/AttachService.java new file mode 100644 index 0000000..4a6e5ca --- /dev/null +++ b/src/main/java/com/lk/paopao/service/AttachService.java @@ -0,0 +1,151 @@ +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 文件 + * @param attachType 附件类型 + * @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; + } + +} + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index fb55ab6..b2ebc94 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -25,11 +25,14 @@ spring: defer-datasource-initialization: true database-platform: org.hibernate.dialect.H2Dialect hibernate: - ddl-auto: create-drop + ddl-auto: update # 可选值:create-drop,create,update,none. create-drop:每次启动项目都会删除表,然后重新创建表,适合开发环境;create:每次启动项目都会创建表,适合开发环境;update:每次启动项目都会更新表,适合开发环境;none:不执行任何操作,适合生产环境。 properties: hibernate: format_sql: true + # 静态资源目录配置 + resources: + static-locations: classpath:/static/,file:/home/whz/tmp/ # 指定静态资源的位置 app: version: v1 @@ -39,7 +42,7 @@ app: jwt: secret-key: uYuVw0mke38MfLhO19wUQyRgwrmYo89ibpQTXPHi4vg= expiration: 36000000 - white-list: + white-list: # 白名单 - path: "/api-docs/**" methods: ["GET","PUT","POST"] - path: "/swagger-ui/**" @@ -48,5 +51,11 @@ app: methods: [ "GET" ] - path: "/v1/auth/**" methods: [ "GET","PUT","POST"] - allowed-origins: - - "http://localhost:5173" \ No newline at end of file + - path: "/upload/**" + methods: [ "GET" ] + allowed-origins: # 允许跨域的域名 + - "http://localhost:5173" + upload: + path: /home/whz/tmp/upload # 上传文件路径 + sub-path-name-length: 2 # 上传文件路径子目录长度 + base-url: http://localhost:8080/upload # 上传文件路径前缀 \ No newline at end of file