feat: 增加附件表结构与本地上传接口

This commit is contained in:
2026-05-18 21:25:57 +08:00
parent 009be37cf6
commit 18386fde63
10 changed files with 298 additions and 0 deletions

45
script/sql/attachment.sql Normal file
View File

@@ -0,0 +1,45 @@
DROP TABLE IF EXISTS sys_attachment;
CREATE TABLE sys_attachment (
id BIGSERIAL PRIMARY KEY,
source_type VARCHAR(100) NOT NULL,
source_id BIGINT,
original_name VARCHAR(255) NOT NULL,
file_name VARCHAR(255) NOT NULL,
file_suffix VARCHAR(50),
content_type VARCHAR(100),
file_size BIGINT NOT NULL DEFAULT 0,
storage_type VARCHAR(50) NOT NULL,
file_path VARCHAR(500) NOT NULL,
file_url VARCHAR(500),
version INTEGER NOT NULL DEFAULT 1,
create_time TIMESTAMP,
update_time TIMESTAMP,
remark VARCHAR(500) DEFAULT '',
create_by VARCHAR(64),
update_by VARCHAR(64)
);
CREATE INDEX idx_sys_attachment_source_type ON sys_attachment (source_type);
CREATE INDEX idx_sys_attachment_source_id ON sys_attachment (source_id);
CREATE INDEX idx_sys_attachment_storage_type ON sys_attachment (storage_type);
CREATE INDEX idx_sys_attachment_create_time ON sys_attachment (create_time);
COMMENT ON TABLE sys_attachment IS '系统附件表';
COMMENT ON COLUMN sys_attachment.id IS 'ID';
COMMENT ON COLUMN sys_attachment.source_type IS '来源业务类型';
COMMENT ON COLUMN sys_attachment.source_id IS '来源业务ID';
COMMENT ON COLUMN sys_attachment.original_name IS '原始文件名';
COMMENT ON COLUMN sys_attachment.file_name IS '存储文件名';
COMMENT ON COLUMN sys_attachment.file_suffix IS '文件后缀';
COMMENT ON COLUMN sys_attachment.content_type IS '文件MIME类型';
COMMENT ON COLUMN sys_attachment.file_size IS '文件大小(字节)';
COMMENT ON COLUMN sys_attachment.storage_type IS '存储类型';
COMMENT ON COLUMN sys_attachment.file_path IS '文件存储路径';
COMMENT ON COLUMN sys_attachment.file_url IS '文件访问地址';
COMMENT ON COLUMN sys_attachment.version IS '版本';
COMMENT ON COLUMN sys_attachment.create_time IS '创建时间';
COMMENT ON COLUMN sys_attachment.update_time IS '更新时间';
COMMENT ON COLUMN sys_attachment.remark IS '备注';
COMMENT ON COLUMN sys_attachment.create_by IS '创建者';
COMMENT ON COLUMN sys_attachment.update_by IS '更新者';

View File

@@ -0,0 +1,19 @@
package com.bruce.common.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "common.attachment")
public class AttachmentProperties {
private String basePath = "data/attachments";
public String getBasePath() {
return basePath;
}
public void setBasePath(String basePath) {
this.basePath = basePath;
}
}

View File

@@ -0,0 +1,31 @@
package com.bruce.common.controller;
import com.bruce.common.entity.SysAttachment;
import com.bruce.common.service.ISysAttachmentService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
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;
@Tag(name = "系统附件管理")
@RestController
@RequestMapping("/api/attachments")
public class SysAttachmentController {
private final ISysAttachmentService sysAttachmentService;
public SysAttachmentController(ISysAttachmentService sysAttachmentService) {
this.sysAttachmentService = sysAttachmentService;
}
@Operation(summary = "上传附件")
@PostMapping("/upload")
public SysAttachment upload(@RequestParam("file") MultipartFile file,
@RequestParam String sourceType,
@RequestParam(required = false) Long sourceId) {
return sysAttachmentService.upload(file, sourceType, sourceId);
}
}

View File

@@ -0,0 +1,59 @@
package com.bruce.common.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@TableName("sys_attachment")
@Schema(description = "系统附件")
public class SysAttachment extends BaseEntity {
@Schema(description = "来源业务类型")
@TableField("source_type")
private String sourceType;
@Schema(description = "来源业务ID")
@TableField("source_id")
private Long sourceId;
@Schema(description = "原始文件名")
@TableField("original_name")
private String originalName;
@Schema(description = "存储文件名")
@TableField("file_name")
private String fileName;
@Schema(description = "文件后缀")
@TableField("file_suffix")
private String fileSuffix;
@Schema(description = "文件MIME类型")
@TableField("content_type")
private String contentType;
@Schema(description = "文件大小(字节)")
@TableField("file_size")
private Long fileSize;
@Schema(description = "存储类型")
@TableField("storage_type")
private String storageType;
@Schema(description = "文件存储路径")
@TableField("file_path")
private String filePath;
@Schema(description = "文件访问地址")
@TableField("file_url")
private String fileUrl;
@Schema(description = "备注")
private String remark;
}

View File

@@ -0,0 +1,9 @@
package com.bruce.common.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bruce.common.entity.SysAttachment;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SysAttachmentMapper extends BaseMapper<SysAttachment> {
}

View File

@@ -0,0 +1,10 @@
package com.bruce.common.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bruce.common.entity.SysAttachment;
import org.springframework.web.multipart.MultipartFile;
public interface ISysAttachmentService extends IService<SysAttachment> {
SysAttachment upload(MultipartFile file, String sourceType, Long sourceId);
}

View File

@@ -0,0 +1,70 @@
package com.bruce.common.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bruce.common.config.AttachmentProperties;
import com.bruce.common.entity.SysAttachment;
import com.bruce.common.mapper.SysAttachmentMapper;
import com.bruce.common.service.ISysAttachmentService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
@Service
public class SysAttachmentServiceImpl extends ServiceImpl<SysAttachmentMapper, SysAttachment> implements ISysAttachmentService {
private static final String STORAGE_TYPE_LOCAL = "LOCAL";
private final AttachmentProperties attachmentProperties;
public SysAttachmentServiceImpl(AttachmentProperties attachmentProperties) {
this.attachmentProperties = attachmentProperties;
}
@Override
public SysAttachment upload(MultipartFile file, String sourceType, Long sourceId) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("上传文件不能为空");
}
if (!StringUtils.hasText(sourceType)) {
throw new IllegalArgumentException("sourceType不能为空");
}
String originalName = file.getOriginalFilename();
String suffix = StringUtils.getFilenameExtension(originalName);
String storedFileName = UUID.randomUUID() + (StringUtils.hasText(suffix) ? "." + suffix : "");
String dateDirectory = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
Path rootPath = Paths.get(attachmentProperties.getBasePath()).toAbsolutePath().normalize();
Path targetDirectory = rootPath.resolve(dateDirectory);
Path targetPath = targetDirectory.resolve(storedFileName);
try {
Files.createDirectories(targetDirectory);
file.transferTo(targetPath);
} catch (IOException e) {
throw new IllegalStateException("文件上传失败", e);
}
SysAttachment attachment = new SysAttachment();
attachment.setSourceType(sourceType);
attachment.setSourceId(sourceId);
attachment.setOriginalName(originalName);
attachment.setFileName(storedFileName);
attachment.setFileSuffix(suffix);
attachment.setContentType(file.getContentType());
attachment.setFileSize(file.getSize());
attachment.setStorageType(STORAGE_TYPE_LOCAL);
attachment.setFilePath(dateDirectory + "/" + storedFileName);
attachment.setFileUrl(null);
attachment.setVersion(1);
save(attachment);
return attachment;
}
}

View File

@@ -11,3 +11,7 @@ mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
configuration:
map-underscore-to-camel-case: true
common:
attachment:
base-path: /data/common-agent/attachments

View File

@@ -4,3 +4,6 @@ spring:
profiles:
active: dev
common:
attachment:
base-path: data/attachments

View File

@@ -0,0 +1,48 @@
package com.bruce.common.attachment;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bruce.common.controller.SysAttachmentController;
import com.bruce.common.entity.SysAttachment;
import com.bruce.common.mapper.SysAttachmentMapper;
import com.bruce.common.service.ISysAttachmentService;
import com.bruce.common.service.impl.SysAttachmentServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.web.multipart.MultipartFile;
import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class SysAttachmentComponentStructureTests {
@Test
void sysAttachmentComponentsShouldReuseMybatisPlusBaseTypes() {
assertTrue(BaseMapper.class.isAssignableFrom(SysAttachmentMapper.class));
assertTrue(IService.class.isAssignableFrom(ISysAttachmentService.class));
assertTrue(ServiceImpl.class.isAssignableFrom(SysAttachmentServiceImpl.class));
assertTrue(ISysAttachmentService.class.isAssignableFrom(SysAttachmentServiceImpl.class));
assertTrue(SysAttachment.class.isAssignableFrom(SysAttachment.class));
}
@Test
void sysAttachmentShouldExposeUploadMethods() throws NoSuchMethodException {
Method serviceMethod = ISysAttachmentService.class.getMethod(
"upload",
MultipartFile.class,
String.class,
Long.class
);
Method controllerMethod = SysAttachmentController.class.getMethod(
"upload",
MultipartFile.class,
String.class,
Long.class
);
assertNotNull(serviceMethod);
assertNotNull(controllerMethod);
}
}