工具类等完整代码都可以去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.测试