feat: 搭建后端多租户名片服务
- 初始化 Spring Boot 多模块工程与通用基础能力 - 增加租户、组织、用户、名片、文件、统计等业务模块 - 补充数据库迁移脚本与基础测试
This commit is contained in:
40
backend/easycard-module-file/pom.xml
Normal file
40
backend/easycard-module-file/pom.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.easycard</groupId>
|
||||
<artifactId>easycard-parent</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>easycard-module-file</artifactId>
|
||||
<name>easycard-module-file</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.easycard</groupId>
|
||||
<artifactId>easycard-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easycard</groupId>
|
||||
<artifactId>easycard-module-user</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.minio</groupId>
|
||||
<artifactId>minio</artifactId>
|
||||
<version>8.5.17</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.easycard.module.file.controller;
|
||||
|
||||
import com.easycard.common.api.ApiResponse;
|
||||
import com.easycard.module.file.service.FileAssetService;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
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.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/files")
|
||||
public class FileAssetController {
|
||||
|
||||
private final FileAssetService fileAssetService;
|
||||
|
||||
public FileAssetController(FileAssetService fileAssetService) {
|
||||
this.fileAssetService = fileAssetService;
|
||||
}
|
||||
|
||||
@PostMapping("/upload")
|
||||
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','TENANT_USER','PLATFORM_SUPER_ADMIN')")
|
||||
public ApiResponse<FileAssetService.FileAssetView> upload(@RequestParam("file") MultipartFile file) {
|
||||
return ApiResponse.success(fileAssetService.upload(file));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','TENANT_USER','PLATFORM_SUPER_ADMIN')")
|
||||
public ApiResponse<List<FileAssetService.FileAssetView>> list() {
|
||||
return ApiResponse.success(fileAssetService.listAssets());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.easycard.module.file.dal.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("file_asset")
|
||||
public class FileAssetDO {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private Long uploadUserId;
|
||||
private String storageProvider;
|
||||
private String bucketName;
|
||||
private String objectKey;
|
||||
private String originalName;
|
||||
private String fileExt;
|
||||
private String mimeType;
|
||||
private Long fileSize;
|
||||
private String fileHash;
|
||||
private String accessUrl;
|
||||
private String assetStatus;
|
||||
private Long createdBy;
|
||||
private LocalDateTime createdTime;
|
||||
private Long updatedBy;
|
||||
private LocalDateTime updatedTime;
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.easycard.module.file.dal.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("file_asset_usage")
|
||||
public class FileAssetUsageDO {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private Long assetId;
|
||||
private String bizType;
|
||||
private Long bizId;
|
||||
private String fieldName;
|
||||
private Long createdBy;
|
||||
private LocalDateTime createdTime;
|
||||
private Long updatedBy;
|
||||
private LocalDateTime updatedTime;
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.easycard.module.file.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.easycard.module.file.dal.entity.FileAssetDO;
|
||||
|
||||
public interface FileAssetMapper extends BaseMapper<FileAssetDO> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.easycard.module.file.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.easycard.module.file.dal.entity.FileAssetUsageDO;
|
||||
|
||||
public interface FileAssetUsageMapper extends BaseMapper<FileAssetUsageDO> {
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.easycard.module.file.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.easycard.common.auth.LoginUser;
|
||||
import com.easycard.common.auth.SecurityUtils;
|
||||
import com.easycard.common.exception.BusinessException;
|
||||
import com.easycard.common.storage.StorageUrlUtils;
|
||||
import com.easycard.module.file.dal.entity.FileAssetDO;
|
||||
import com.easycard.module.file.dal.mapper.FileAssetMapper;
|
||||
import io.minio.MinioClient;
|
||||
import io.minio.PutObjectArgs;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FileAssetService {
|
||||
|
||||
private final FileAssetMapper fileAssetMapper;
|
||||
|
||||
@Value("${easycard.storage.endpoint}")
|
||||
private String endpoint;
|
||||
|
||||
@Value("${easycard.storage.public-endpoint:${easycard.storage.endpoint}}")
|
||||
private String publicEndpoint;
|
||||
|
||||
@Value("${easycard.storage.access-key}")
|
||||
private String accessKey;
|
||||
|
||||
@Value("${easycard.storage.secret-key}")
|
||||
private String secretKey;
|
||||
|
||||
@Value("${easycard.storage.bucket}")
|
||||
private String bucket;
|
||||
|
||||
@Transactional
|
||||
public FileAssetView upload(MultipartFile file) {
|
||||
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new BusinessException("FILE_EMPTY", "上传文件不能为空");
|
||||
}
|
||||
if (file.getSize() > 5 * 1024 * 1024L) {
|
||||
throw new BusinessException("FILE_TOO_LARGE", "图片大小不能超过 5MB");
|
||||
}
|
||||
String contentType = file.getContentType() == null ? "" : file.getContentType().toLowerCase(Locale.ROOT);
|
||||
if (!contentType.startsWith("image/")) {
|
||||
throw new BusinessException("FILE_TYPE_INVALID", "仅支持图片上传");
|
||||
}
|
||||
|
||||
String extension = resolveExtension(file.getOriginalFilename());
|
||||
String objectKey = loginUser.tenantId() + "/" + UUID.randomUUID() + extension;
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
MinioClient minioClient = MinioClient.builder()
|
||||
.endpoint(endpoint)
|
||||
.credentials(accessKey, secretKey)
|
||||
.build();
|
||||
minioClient.putObject(PutObjectArgs.builder()
|
||||
.bucket(bucket)
|
||||
.object(objectKey)
|
||||
.stream(inputStream, file.getSize(), -1)
|
||||
.contentType(file.getContentType())
|
||||
.build());
|
||||
} catch (Exception exception) {
|
||||
throw new BusinessException("FILE_UPLOAD_FAILED", "上传文件失败: " + exception.getMessage());
|
||||
}
|
||||
|
||||
FileAssetDO asset = new FileAssetDO();
|
||||
asset.setTenantId(loginUser.tenantId());
|
||||
asset.setUploadUserId(loginUser.userId());
|
||||
asset.setStorageProvider("MINIO");
|
||||
asset.setBucketName(bucket);
|
||||
asset.setObjectKey(objectKey);
|
||||
asset.setOriginalName(file.getOriginalFilename());
|
||||
asset.setFileExt(extension);
|
||||
asset.setMimeType(file.getContentType());
|
||||
asset.setFileSize(file.getSize());
|
||||
asset.setAccessUrl(StorageUrlUtils.buildPublicUrl(publicEndpoint, bucket, objectKey, ""));
|
||||
asset.setAssetStatus("ACTIVE");
|
||||
asset.setCreatedBy(loginUser.userId());
|
||||
asset.setUpdatedBy(loginUser.userId());
|
||||
asset.setDeleted(0);
|
||||
fileAssetMapper.insert(asset);
|
||||
return toView(asset);
|
||||
}
|
||||
|
||||
public List<FileAssetView> listAssets() {
|
||||
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||
return fileAssetMapper.selectList(Wrappers.<FileAssetDO>lambdaQuery()
|
||||
.eq(FileAssetDO::getTenantId, loginUser.tenantId())
|
||||
.eq(FileAssetDO::getDeleted, 0)
|
||||
.orderByDesc(FileAssetDO::getId))
|
||||
.stream()
|
||||
.map(this::toView)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public FileAssetView getAsset(Long assetId) {
|
||||
FileAssetDO asset = fileAssetMapper.selectById(assetId);
|
||||
if (asset == null || Integer.valueOf(1).equals(asset.getDeleted())) {
|
||||
throw new BusinessException("ASSET_NOT_FOUND", "素材不存在");
|
||||
}
|
||||
return toView(asset);
|
||||
}
|
||||
|
||||
private FileAssetView toView(FileAssetDO asset) {
|
||||
return new FileAssetView(
|
||||
asset.getId(),
|
||||
asset.getOriginalName(),
|
||||
asset.getMimeType(),
|
||||
asset.getFileSize(),
|
||||
StorageUrlUtils.buildPublicUrl(publicEndpoint, asset.getBucketName(), asset.getObjectKey(), asset.getAccessUrl()),
|
||||
asset.getCreatedTime() == null ? LocalDateTime.now() : asset.getCreatedTime()
|
||||
);
|
||||
}
|
||||
|
||||
private String resolveExtension(String originalName) {
|
||||
if (originalName == null || !originalName.contains(".")) {
|
||||
return "";
|
||||
}
|
||||
return originalName.substring(originalName.lastIndexOf('.'));
|
||||
}
|
||||
|
||||
public record FileAssetView(Long id, String originalName, String mimeType, Long fileSize, String accessUrl, LocalDateTime createdTime) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user