图片上传服务业务流程

接口信息如下

路径地址 http://localhost:63040/media/upload/coursefile
请求方式 POST
请求参数 MultipartFile
返回结果 UploadFileResultVO

返回结果VO

CourseCategoryTreeVO
@Data
@EqualsAndHashCode(callSuper = true)
public class UploadFileResultVO extends MediaFiles {

}

参数DTO

UploadFileParamDTO
@Data
public class UploadFileParamDTO implements Serializable {

private static final long serialVersionUID = 1L;

/**
* 文件名称
*/
private String filename;

/**
* 文件类型(图片、文档,视频)
*/
private String fileType;

/**
* 文件大小
*/
private Long fileSize;

/**
* 标签
*/
private String tags;

/**
* 上传人
*/
private String username;

/**
* 备注
*/
private String remark;
}

定义Service

文件存储操作

定义上传媒体文件的方法,找到com.swx.media.service包下的FileStorageService接口,定义如下方法:

FileStorageService
/**
* 上传文件
*
* @param prefix 文件前缀
* @param filename 文件名
* @param inputStream 文件流
* @return 桶和文件全路径
*/
public Map<String, String> uploadMediaFile(String prefix, String filename, String mineType, InputStream inputStream);

实现该方法

MinIOFileStorageService
private final MinioClient minioClient;
private final MinIOConfigProperties minIOConfigProperties;

public MinIOFileStorageService(MinioClient minioClient, MinIOConfigProperties minIOConfigProperties) {
this.minioClient = minioClient;
this.minIOConfigProperties = minIOConfigProperties;
}
/**
* 上传图片文件
*
* @param prefix 文件前缀
* @param filename 文件名
* @param inputStream 文件流
* @return 文件全路径
*/
@Override
public Map<String, String> uploadMediaFile(String prefix, String filename, String mineType, InputStream inputStream) {
String filepath = builderFilePath(prefix, filename);
String bucket = minIOConfigProperties.getBucket().get("files");
try {
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.object(filepath)
.contentType(mineType)
.bucket(bucket)
.stream(inputStream, inputStream.available(), -1)
.build();
minioClient.putObject(putObjectArgs);
Map<String, String> map = new HashMap<>();
map.put("path", filepath);
map.put("bucket", bucket);
return map;
} catch (Exception ex) {
log.error("minio put file error.", ex);
throw new RuntimeException("上传文件失败");
}
}

文件上传方法

定义上传文件的方法,找到com.swx.media.service包下的MediaFilesService接口,定义如下方法:

MediaFilesService
public interface MediaFilesService extends IService<MediaFiles> {
/**
* 上传文件
*
* @param companyId 机构ID
* @param dto 文件信息
* @param multipartFile 文件信息
* @return UploadFileResultVO
*/
public UploadFileResultVO uploadFile(Long companyId, UploadFileParamDTO dto, MultipartFile multipartFile);

/**
* 从dto保存到数据库
*
* @param dto 文件信息
* @return 保是否保存成功
*/
/**
* 上传到MinIO之后保存到数据库
*
* @param dto 文件信息
* @param companyId 机构ID
* @param prefix 前缀
* @param bucket 桶
* @param path 文件路径
* @return 保是否保存成功
*/
public MediaFiles saveAfterStore(UploadFileParamDTO dto, Long companyId, String prefix, String bucket, String path);
}

实现该方法:

/**
* <p>
* 媒资信息 服务实现类
* </p>
*
* @author sw-code
* @since 2023-08-21
*/
@Slf4j
@Service
public class MediaFilesServiceImpl extends ServiceImpl<MediaFilesMapper, MediaFiles> implements MediaFilesService {

private final FileStorageService fileStorageService;
private final TransactionTemplate transactionTemplate;

public MediaFilesServiceImpl(FileStorageService fileStorageService, TransactionTemplate transactionTemplate) {
this.fileStorageService = fileStorageService;
this.transactionTemplate = transactionTemplate;
}

/**
* 上传文件
*
* @param companyId 机构ID
* @param dto 文件信息
* @param multipartFile 文件信息
* @return UploadFileResultVO
*/
@Override
public UploadFileResultVO uploadFile(Long companyId, UploadFileParamDTO dto, MultipartFile multipartFile) {

String filename = dto.getFilename();
String suffix = filename.substring(filename.lastIndexOf("."));
try {
String prefix = getFileMd5(multipartFile.getInputStream());
// 已存在数据库,直接返回
MediaFiles dbMediaFile = getById(prefix);
if (dbMediaFile != null) {
UploadFileResultVO resultVO = new UploadFileResultVO();
BeanUtils.copyProperties(dbMediaFile, resultVO);
return resultVO;
}
// 上传到MinIO
String mineType = getMineType(suffix);
Map<String, String> result = fileStorageService.uploadMediaFile("", prefix + suffix, mineType, multipartFile.getInputStream());
String bucket = result.get("bucket");
String path = result.get("path");

// 保存到数据库,使用编程式事务
MediaFiles mediaFiles = transactionTemplate.execute(transactionStatus -> {
try {
return saveAfterStore(dto, companyId, prefix, bucket, path);
} catch (Exception e) {
log.error("文件入库失败: ", e);
transactionStatus.setRollbackOnly();
}
return null;
});

// 返回数据
UploadFileResultVO resultVO = new UploadFileResultVO();
if (mediaFiles == null) return resultVO;
BeanUtils.copyProperties(mediaFiles, resultVO);
return resultVO;
} catch (IOException e) {
log.info("MediaFilesServiceImpl-上传文件失败", e);
throw new BizException("文件上传失败");
}
}

/**
* 从dto保存到数据库
*
* @param dto 文件信息
* @return 保是否保存成功
*/
@Override
public MediaFiles saveAfterStore(UploadFileParamDTO dto, Long companyId, String fileMd5, String bucket, String path) {
// 构造参数
MediaFiles mediaFiles = new MediaFiles();
BeanUtils.copyProperties(dto, mediaFiles);
mediaFiles.setId(fileMd5);
mediaFiles.setCompanyId(companyId);
mediaFiles.setBucket(bucket);
mediaFiles.setFilePath(path);
mediaFiles.setFileId(fileMd5);
mediaFiles.setUrl("/" + bucket + "/" + path);
mediaFiles.setCreateDate(LocalDateTime.now());
mediaFiles.setStatus(MediaFileStatusEnum.NORMAL.status());
mediaFiles.setAuditStatus("002003");
boolean save = save(mediaFiles);
if (!save) {
log.error("想数据库保存文件失败,bucket: {},文件名: {}", bucket, path);
return null;
}
return mediaFiles;
}

private String getMineType(String suffix) {
if (suffix == null) suffix = "";
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(suffix);
String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
if (extensionMatch != null) {
mimeType = extensionMatch.getMimeType();
}
return mimeType;
}

/**
* 获得文件Md5值
*
* @param inputStream 文件
* @return Md5值
*/
private String getFileMd5(InputStream inputStream) {
try {
return DigestUtils.md5DigestAsHex(inputStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

什么情况Spring事务会失效?

  • 在方法中捕获异常没有抛出
  • 非事务方法调用事务方法
  • @Transactional标记的方法不是public
  • 抛出的异常与rollbackFor指定的异常不匹配,默认rollbackFor指定的异常为RuntimeException
  • 数据库不支持事务,比如MySQL的MyISAM
  • Spring的传播行为导致事务失效,比如:PROPAGATION_NEVER、PROPAGATION_NOT_SUPPORTED
PROPAGATION_REQUIRED -- 支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
PROPAGATION_SUPPORT -- 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY -- 支持当前事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW -- 新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED -- 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER -- 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED -- 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则与PROPAGATION_REQUIRED类似的操作。

分块文件清理问题?
上传一个文件进行分块上传,上传一半不传了,之前上传到minio的分块文件要清理吗? 怎么做的?

1、在数据库中有一张文件表记录minio中存储的文件信息。
2、文件开始上传时会亏入文件表,状志为上传中,上传完成会更新状态为上传成。
3、当一个文件传了一半不再上传了说明该文件没有上传完成,会有定时任务去查询文件表中的记录,如果文件末上传完成则删除minio中没有上传成功的文件目录。

定义Controller

MediaFilesUploadController
/**
* <p>
* 媒资上传 前端控制器
* </p>
*
* @author sw-code
* @since 2023-08-21
*/
@Api(value = "媒资文件上传管理接口", tags = "媒资文件上传管理接口")
@RestController
@ResponseResult
@RequestMapping("/upload")
public class MediaFilesUploadController {

private final MediaFilesService mediaFilesService;

public MediaFilesUploadController(MediaFilesService mediaFilesService) {
this.mediaFilesService = mediaFilesService;
}

@ApiOperation("上传文件")
@PostMapping(value = "/coursefile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public UploadFileResultVO uploadImage(@RequestPart("filedata") MultipartFile filedata) {
Long companyId = 1232141425L;
UploadFileParamDTO dto = new UploadFileParamDTO();
dto.setFilename(filedata.getOriginalFilename());
dto.setFileSize(filedata.getSize());
dto.setFileType("001001");
return mediaFilesService.uploadFile(companyId, dto, filedata);
}
}

Push到Git

commit "完成图片上传功能"