工具类等完整代码都可以去github上获取,地址:https://github.com/kkoneone11/cloud-photo
图片上传流程图
流程解释(粗体的都是要写的大致接口方法):
用户要上传文件,则先写一个接口用来获得上传地址,然后再通过一个Md5工具类根据文件的属性信息进行生成一个唯一的Md5,然后判断是否是需要秒传,如果是,即存在数据库中,则返回秒传标志(文件),如果不是的话则证明此文件之前没有上传过,即不存在数据库则根据Md5和地址工具类生成一个唯一的文件上传地址(专门供给这个照片的)供用户上传文件到,将上传照片资源到资源池minio,最后再写一个接口用来提交上传,即将存储文件相关信息存储到到数据库。同时无论是否是秒传都要将将消息入库到kafka队列,一个是存放图片缩略图方便管理员展示的时候使用。另一个队列是审核队列用来审核图片是否可以观看
会操作到的数据库表
- 用户文件表:保存这个用户和其存储了的文件的相关信息,文件信息(文件名、文件大小、分类等信息),查询用户文件列表
- 文件存储信息表:文件保存在哪,即资源池存储信息,通过桶Id和存储池文件id可以唯一识别该文件,通过存储信息可以生成下载地址,通过storage_object_ID、文件表和文件MD5关联
- 文件MD5表:保存了文件的相关属性,保存文件MD5,校验文件秒传用。
开发大体步骤 :
1.基础配置
1.1相关配置
先创建一个common项目,然后里面导入相关的工具包,代码我已经放到了github上。可以自取。然后再创建一个trans子项目在pom里导入common子项目。接下来的接口都在trans上进行开发
注意!!!:使用getById方法的时候。在表生成的实体类上主键要加上以下代码,否则会报错
@TableId(type = IdType.ASSIGN_UUID)
1.2
新建启动类,启动类扫描上配置Mapper文件 @MapperScan(basePackages = {"com.cloud.photo.trans.mapper"})
1.3
用代码生成工具类根据数据库表一键生成代码 "tb_user_file","tb_storage_object","tb_file_md5",记得根据自己的实际情况修改配置
1.4
配置一下application.yml文件。trans的端口是9006
2.接口开发
2.1接口1:获得上传地址 /trans/getPutUploadUrl Get
1.参数分析:需要根据文件属性生成唯一地址(fileName、fileSize、fileMd5)(fileSize、fileMd5非必要,其实fileName也非必要,因为传进去只是为了方便拿到后缀名)、且需要知道是哪个用户生成的(userId,非必要)、用来判断是否已经生成过而进行秒传(fileMd5,非必要)。都是非必要的原因是地址的生成是根据objectId,而其又是UUID,随机且唯一,基本不会冲突。因此地址都是唯一
2.先写一个PutUploadUrlController,然后写获得上传地址方法getPutUploadUrl(),然后业务逻辑是要根据Md5的值判断,所以创建Service类进行逻辑判断并传入文件相关的信息,其中Service类要调用FileMd5接口根据Md5查询数据库中是否存在这个文件。判断的时候又得根据getOne方法去获得,如果没有则传入fileName然后调用S3工具类生成一个上传地址并返回
3.测试:trans/getPutUploadUrl。
测试阶段filesize、md5要用工具生成。现在这是一个未上传过的照片所以肯定是返回地址。然后返回的uri就是我们所需要上传文件的地址,base64就是服务端生成的,后面这两个需要用到!!
package cloud.photo.common.utils;
import org.apache.commons.codec.digest.DigestUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class Md5Util {
public static void main(String[] args) {
String filePath = "C:\\Users\\admin\\Postman\\files\\test6.png";
String md5 = getFileMd5(filePath);
System.out.println(md5);
System.out.println(new File(filePath).length());
}
public static String getFileMd5(String filePath){
String md5 = null;
try {
md5 = DigestUtils.md5Hex(new FileInputStream(filePath));
} catch (IOException e) {
e.printStackTrace();
}
return md5;
}
}
测试:上传资源到资源池
1.在接口1我们获得到了一个地址然后我们复制截取uri的部分,往里发一个post请求,相当于传入文件。这里是往minio或者华为云obs中存入文件
2.选择二进制文件,上传当时文件里已经计算好了的那张照片。(注意!!!这里要用PUT请求而不是POST,虽然POST也能提交成功但返回值有问题)
再将接口1中返回的base64Md5的值按照以下的形式填入到请求头里面
可以看到已经有了一条信息
2.2接口2:提交存储文件 文件入库、发送审核、图片消息到Kafka /trans/commit Post
1.参数分析:首先要知道谁传入了这个文件(userId),文件的信息(fileName fileSize fileMd5)、传去哪个资源池(containerId)、在资源池中哪个是它(objectId)、存储的地址方便拿到数据(uploadUrl)、文件或者照片是什么类型(category)、存储的时间(uploadTime)、文件的状态正常或者删除(statue)、base64文件md5(base64Md5)、storageObjectId是用来连接用户文件表和文件存储信息表且用来判断入库(storageObjectId)。这里参数比较多因此我们封装成一个请求体FileUpLoadBo类
2.这一步是根据获得上传地址接口再继续往下判断的,根据StoreObjectId判断是否秒传,因为已经存在了的话肯定会已经入库,而不根据md5是因为有可能生成了但还没入库,如果
- 是秒传(数据库中存在)的话则也要通过storageObjectId检查到底是否上传的是同一份文件,然后也要判断秒传文件大小是否相同(storage_object_id字段是已经传过才会生成的)最后再将传入一条图片处理消息和一条审核处理消息到kafka
- 非秒传的话(数据库中不存在)则通过s3工具通过ObjectId校验已经上传和上传文件是否相同。最后上传成功的文件要分别在FileMd5数据库和StrorageObject表进行入库
3.继续在PutUploadUrlController中写一个commit接口,接口里能处理一个秒传和一个非秒传的操作,这里的逻辑判断部分同样是在PutUploadUrlService中实现,然后Controller部分调用即可
4.在判断完是否需要秒传之后,分别在在PutUploadUrlService中实现一个commit和一个commitTransSecond方法,用来非秒传和秒传。在非秒传和秒传里的业务逻辑还需要判断
- 秒传:1.根据StorageObjectId判断上传的和数据库数据是否一致 2.根据fileMd5和size判断上传文件是否一致
- 非秒传:1.根据objectId判断有没有上传 2.根据fileMd5和size判断上传文件是否一致
5.最后面都分别对UserFile表、MD5表、StorageObject表入库即可。而UserFile入库的时候注意还需要对Kafka分别发送一条审核消息和一条生成缩略图消息供后续处理,因此需要写一个saveAndFileDeal方法独立处理
注意在创建StorageObject和FileMd5的时候要加一个无参的构造方法,不然会报错
package cloud.photo.common.bo;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
/**
* 上传文件信息体
* @author linzsh
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class FileUploadBo {
/**
* 文件名
*/
private String fileName;
/**
* 文件大小
*/
private Long fileSize;
/**
* 文件MD5
*/
private String fileMd5;
/**
* 后面拼接用户ID
*/
private String userId;
/**
* 对象ID
*/
private String objectId;
/**
* 资源池ID
*/
private String containerId;
/**
* 对象ID(秒传用)
*/
private String storageObjectId;
/**
* 上传地址
*/
private String uploadUrl;
/**
* 分类
*/
private Integer category;
/**
* base64文件md5
*/
private String base64Md5;
/**
* 上传状态
*/
private String status;
/**
* 上传时间
*/
private String uploadTime;
}
这里其实Getmapping也可以的,因为GetMapping = RequestMapping+Get,而HttpServlet也不是必须的只是为了方便打印请求的id和传回的请求体
6.测试:/trans/commit
2.3接口3:文件下载接口 /trans/getDownloadUrlByFileId Get
1.参数分析:是通过UserFile表去查相关文件信息,因此UserId和fileId可以唯一确定一个文件。因为可以看S3工具类里的getDownloadUrl方法,封装好了一个containerId和一个objectId,因此我们只需要在业务层通过fileId在UserFile表拿到StorageObjectId然后去这张表里拿到这两个参数进行查询地址返回回来即可。
2.写一个DownloadController,在里面写getDownloadUrlByFileId方法,同样也要编写IDownloadService和DownloadServiceImpl方法,在里面实现逻辑判断
3.测试:
2.4接口4:文件列表查询接口 /trans/userFilelist
1.参数分析:因为是查询的文件列表,所以肯定需要当前用户是谁(userId)然后在UserFile表里就能根据userId查到其对应下的文件即可然后还需要当前页(current)、每一页展现的个数(PageSize)、同时还需要一个分类(category),因此我们封装一个AlbumPageBo类
2.在UserFileController写一个userFilelist方法
package com.cloud.photo.trans.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.cloud.photo.common.bo.AlbumPageBo;
import com.cloud.photo.common.common.ResultBody;
import com.cloud.photo.trans.entity.UserFile;
import com.cloud.photo.trans.service.IUserFileService;
import org.springframework.beans.factory.annotation.Autowired;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.stereotype.Controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
/**
* <p>
* 前端控制器
* </p>
*
* @author kkoneone11
* @since 2023-07-20
*/
@Controller
@RequestMapping("/trans")
public class UserFileController {
@Autowired
IUserFileService iUserFileService;
@RequestMapping("/userFilelist")
public ResultBody userFilelist(HttpServletRequest request , HttpServletResponse response,
@RequestBody AlbumPageBo albumPageBo){
//设置QueryWrapper
QueryWrapper<UserFile> qw = new QueryWrapper<>();
//通过HashMap组装多个条件Wrapper
HashMap<String,Object> hm = new HashMap<>();
if(albumPageBo.getCategory()!=null){
hm.put("category" , albumPageBo.getCategory());
}
hm.put("userId", albumPageBo.getUserId());
qw.allEq(hm);
Integer pageSize = albumPageBo.getPageSize();
Integer current = albumPageBo.getCurrent();
if(current == null) current = 1;
if(pageSize == null ) pageSize = 20;
//组装一下page
Page<UserFile> page = new Page<UserFile>(current, pageSize);
IPage<UserFile> userFilePage = iUserFileService.page(page, qw.orderByDesc("user_id", "create_time"));
return ResultBody.success(userFilePage);
}
}
3.测试
2.5接口5:更新文件审核状态接口 /trans/updateUserFile
1.参数分析:更新文件审核这个业务很明显需要操作的字段有状态(status)、根据存储池信息(storageObjectId)字段进行查询然后更新状态
2.在UserFileController写updateUserFile方法,可以分别根据StorageObjectId和userfileid来执行更新语句,用updateWrapper
@RequestMapping("/updateUserFile")
public Boolean updateUserFile(HttpServletRequest request, HttpServletResponse response,
@RequestBody List<UserFile> userFileBoList){
//打印这次请求信息
String requestId = RequestUtil.getRequestId(request);
RequestUtil.printQequestInfo(request);
//取出每个userFile分别进行更新
for(UserFile userFile : userFileBoList){
UpdateWrapper<UserFile> updateWrapper = new UpdateWrapper<>();
//根据StorageObjectId条件更新审核
if(StringUtils.isNotBlank(userFile.getStorageObjectId())){
updateWrapper.eq("storage_object_id",userFile.getStorageObjectId());
}
//添加userfileid条件更新审核
if(StringUtils.isNotBlank(userFile.getUserFileId())){
updateWrapper.eq("user_file_id",userFile.getUserFileId());
}
//设置更新的状态
updateWrapper.set("audit_status",userFile.getAuditStatus());
//执行pdateWrapper
iUserFileService.update(updateWrapper);
}
return true;
}
3.测试