文章只提供了部分代码,每一天对应着分支工具类等完整代码都可以去github上获取,地址:https://github.com/kkoneone11/cloud-photo/
生成缩略图功能
流程图
预览功能
文件上传功能![](https://i-blog.csdnimg.cn/blog_migrate/fcf2dc0cf33d1326f5997130378ac256.png)
流程解释(粗体的为大致要实现的接口):首先加载图片列表会先需要请求获得缩略图,服务器收到该信息之后先判断有没有该缩略图,如果
- 没有的话则需要通过请求照片的信息去资源池下载原图,再根据原图生成生成缩略图的地址,然后下载下来再显示缩略图。最后将缩略图上传到资源池,再将缩略图进行入库
- 是存在缩略图的话则直接获得缩略图地址然后进行返回结果
数据库
图片缩略图表:tb_file_resize_icon 图片格式分析表:tb_media_info
都是通过存储id(StorageObjectId)去对应,所以通过存储id去映射着两个表。且StorageObjectId对应着一张原图、一张200x200的缩略图和一张600x600的缩略图
分析:
开发过程:
1.基础配置
1.1创建Image子项目,配置配置类,改端口为9007,改application的name
1.2pom中引入common项目,导入以下pom坐标
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>cloud-photo-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.drewnoakes</groupId>
<artifactId>metadata-extractor</artifactId>
<version>2.18.0</version>
</dependency>
</dependencies>
1.3启动类
- @ComponentScan注解用于指定要扫描的包或类的位置。它会自动扫描指定包及其子包下的所有类,并将其注册为Spring Bean。这个注解通常用于配置类上,以便Spring Boot可以自动扫描并注册所有需要的组件
1.4在common类的utils导入工具类
- 要先下载原图,所以有一个下载工具类S3Util 里面也要记得修改到自己具体的minio的配置
- 获得图片宽高等工具
- 生成缩略图后上传到存储池上传工具
- vips工具生成缩略图 记得要修改里面的下载好的工具类的地址
1.5导入代码生成工具类,根据要操作的数据库"tb_file_resize_icon","tb_media_info"
生成对应的mvc,实体类里的主键记得添加上@TableId(type = IdType.ASSIGN_UUID)
图片预览功能流程图:
2.接口开发
2.1查看缩略图信息getFileResizeIcon()
2.1.1参数分析:首先肯定需要先查询是否存储在数据库里(storageObjectId )和哪一种缩略图(iconCode)。返回值为FileResizeIcon,返回的是缩略图的信息
2.1.2这个方法只需要在service里实现,是getIconUrl()会调用到。需要通过storageObjectId和iconCode在file_resize_icon表里进行唯一查询
getOne方法的时候注意后面要加个false,不然存入多个相同的照片的时候会出错
2.2下载原图downloadImage()
2.2.1参数分析:String containerId , String objectId , String suffixName。返回原图的下载地址
2.2.2这个方法需要找到资源池里的原图然后获取其下载地址并下载到源文件目录里。填写地址的时候记得后面还有一个斜杠。
2.2.3是通过trans里的方法进行下载原图,而项目直接的方法调用就得用到fegin,因此在common类里面我们写一个Cloud2TransService,用来作为中间对象方便调用Trans里的方法。通过fegin调用trans服务,fegin类放在common包里方便管理。同时记得在启动类上加上
@EnableFeignClients(basePackages = {"com.cloud.photo.common.fegin"})扫描到对应包
package com.cloud.photo.common.fegin;
import com.cloud.photo.common.bo.AlbumPageBo;
import com.cloud.photo.common.bo.FileUploadBo;
import com.cloud.photo.common.bo.StorageObjectBo;
import com.cloud.photo.common.bo.UserFileBo;
import com.cloud.photo.common.common.ResultBody;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
/**
* @Author:kkoneone11
* @name:Image2TransService
* @Date:2023/7/15 13:20
*/
@Service
@FeignClient("cloud-photo-trans")
public interface Cloud2TransService {
/**
* 获得图片下载地址
* @param containerId
* @param objectId
* @return
*/
@GetMapping("/trans/getDownloadUrl")
ResultBody getDownloadUrl(@RequestParam(value = "containerId") String containerId,
@RequestParam(value = "objectId") String objectId);
/**
* 上传文件
* @param userId
* @param fileName
* @param fileMd5
* @param fileSize
* @return
*/
@GetMapping("/trans/getPutUploadUrl")
ResultBody getPutUploadUrl(@RequestParam(value = "userId",required = false) String userId,
@RequestParam(value = "fileName") String fileName,
@RequestParam(value = "fileMd5") String fileMd5,
@RequestParam(value = "fileSize") Long fileSize);
/**
* 更新文件审核状态
* @param userFileList
* @return
*/
@RequestMapping("/trans/updateUserFile")
Boolean updateUserFile(@RequestBody List<UserFileBo> userFileList);
/**
* 提交上传
* @param bo
* @return
*/
@RequestMapping("/trans/commit")
ResultBody commit(@RequestBody FileUploadBo bo);
@RequestMapping("/trans/userFilelist")
ResultBody userFilelist(@RequestBody AlbumPageBo pageBo);
@RequestMapping("/trans/getUserFileById")
UserFileBo getUserFileById(@RequestParam("fileId") String fileId);
@RequestMapping("/trans/getStorageObjectById")
StorageObjectBo getStorageObjectById(@RequestParam("storageObjectId") String storageObjectId);
}
2.2.4而在trans项目里的DownloadController我们应该再写一个含containerId,objectId两个参数的getDownloadUrl()方法。然后在service里直接调用S3工具里的getDownloadUrl()方法即可。
/**
*从资源池下载原图
* @param containerId
* @param objectId
* @param suffixName 文件后缀名
* @return
*/
@Override
public String downloadImage(String containerId, String objectId, String suffixName) {
//将文件下载到该地址
String srcFileDirName = "D:/cloudImage/";
//获取原图的下载地址
ResultBody url = cloud2TransService.getDownloadUrl(containerId, objectId);
//组装文件名
String srcFileName = srcFileDirName + UUID.randomUUID().toString() + "." + suffixName;
//根据文件路径创建一个file类
File dir = new File(srcFileDirName);
//判断该路径下的文件存不存在,不存在则创建
if(!dir.exists()){
dir.mkdir();
}
//将文件下载到该文件路径
Boolean result = DownloadFileUtil.downloadFile(url.getData().toString(), srcFileName);
if(!result){
return null;
}
//返回文件名
return srcFileName;
}
2.3.原图生成缩略图并入库imageThumbnailSave()
2.3.1参数分析:根据原图然后生成缩略图(suffixName,srcFileName)、图相关属性因为会有200和600的尺寸(iconCode)、获得上传地址(fileName)、唯一识别缩略图连接两个表且需要当做信息存入到图片缩略图表(storageObjectId)
Vips工具生成缩略图的方法参数
2.3.2要先找到原图位置,然后构造一下缩略图的源文件名,将文件的信息分析出来,然后再用vips生成缩略图,然后将调用uploadIcon()方法缩略图上传到(存储池)资源池。
2.4上传缩略图uploadIcon()
2.4.1参数分析:要入库到缩略图的数据库里,要有的参数有userId,storageObjectId, ,fileName,
同时还需要调用getPutUploadUrl()获得上传缩略图的地址,所以需要fileName,useId
然后通过调用UploadFileUtil里的文件上传方法将文件上传到地址所以需要iconFile
2.4.2通过fegin调用传输服务的上传地址方法去上传图片。看源码就知道不需要md5,传null也能生成一个地址。而缩略图不需要上传提交,因为提交是上传到文件列表,而缩略图不需要上传到文件列表 ,直接保存入库即可。如果报错返回空即可
**
* 上传缩略图并入库
* @param userId
* @param storageObjectId
* @param iconCode
* @param iconFile
* @param fileName
* @return
*/
@Override
public FileResizeIcon uploadIcon(String userId,String storageObjectId ,String iconCode, File iconFile,String fileName){
//获得上传地址
ResultBody putUploadUrl = cloud2TransService.getPutUploadUrl(userId, fileName, null, null);
//转化成JSON格式并把参数提取出来用作上传到资源池和入库
JSONObject jsonObject = JSONObject.parseObject(putUploadUrl.getData().toString());
String objectId = jsonObject.getString("objectId");
String uploadUrl = jsonObject.getString("url");
String containerId= jsonObject.getString("containerId");
//上传到资源池
UploadFileUtil.uploadSinglePart(iconFile,uploadUrl);
//入库 file_resize_iocn表
FileResizeIcon fileResizeIcon = new FileResizeIcon(storageObjectId,iconCode,containerId,objectId);
iFileResizeIconService.save(fileResizeIcon);
return fileResizeIcon;
}
FileResizeIcon()的时候记得要创建一个无参的构造函数,因为有了一个有参的构造函数
2.5. 接受消息处理缩略图imageThumbnailAndMediaInfo()
2.5.1参数分析:String storageObjectId, String fileName
2.5.2因为已经之前上传完原图已经上传了一条消息到kfaka,所以现在要写一个类去消费kafka的消息。并且生成 200_200、600_600尺寸缩略图 再分析图片格式 宽高等信息并信息入库。在image下创建一个consumer包,然后创建一个ImageConsumer类,记得在类上备注@Component,然后监听消费file_image_topic的信息
@Component
public class ImageConsumer {
@Autowired
IFileResizeIconService iFileResizeIconService;
// 消费监听
@KafkaListener(topics = {"file_image_topic"})
public void onMessage1(ConsumerRecord<String, Object> record){
// 消费的哪个topic、partition的消息,打印出消息内容
System.out.println("消费:"+record.topic()+"-"+record.partition()+"-"+record.value());
Object value = record.value();
JSONObject jsonObject = JSONObject.parseObject(value.toString());
String userFileId = jsonObject.getString("userFileId");
String storageObjectId = jsonObject.getString("storageObjectId");
String fileName = jsonObject.getString("fileName");
iFileResizeIconService.imageThumbnailAndMediaInfo(storageObjectId,fileName);
}
}
2.5.3先用getFileResizeIcon()方法看一下缩略图是否已经生成,都已经生成且存在数据库里了则不用再生成,否则通过downloadImage()方法下载原图然后再分别生成两张缩略图
再在image创建一个consumer的包,包里创建一个类,并在类名上注释@Component,把kafka里的消息读出来然后进行处理,处理的内容主要是生成缩略图,一张200x200,一张600x600的图和然后处理格式分析。先到数据库查是否有对应的尺寸,分别查,然后再查是否有格式分析属性,如果没生成图片和没有格式分析再处理。
/**
* 调用imageThumbnailSave方法分别生成200和600的缩略图并将缩略图信息入库medi_info表
* @param storageObjectId
* @param fileName
*/
@Override
public void imageThumbnailAndMediaInfo(String storageObjectId, String fileName){
String iconCode200 = "200_200";
String iconCode600 = "600_600";
//查询一下200和600的缩略图是否存在
FileResizeIcon fileResizeIcon200 = iFileResizeIconService.getFileResizeIcon(storageObjectId, iconCode200);
FileResizeIcon fileResizeIcon600 = iFileResizeIconService.getFileResizeIcon(storageObjectId, iconCode600);
MediaInfo mediaInfo = iMediaInfoService.getOne(new QueryWrapper<MediaInfo>().eq("storage_Object_Id", storageObjectId) ,false);
//如果都不为空则已经存在缩略图
if(fileResizeIcon200!=null&&fileResizeIcon600!=null&&mediaInfo!=null){
return ;
}
//缩略图存在的话则下载一下
String suffixName = fileName.substring(fileName.lastIndexOf(".")+1,fileName.length());
StorageObjectBo storageObject = cloud2TransService.getStorageObjectById(storageObjectId);
String srcFileName = iFileResizeIconService.downloadImage(storageObject.getContainerId(), storageObject.getObjectId(), suffixName);
if(StringUtils.isBlank(srcFileName)){
log.error("downloadImage error!");
return;
}
//生成缩略图并保存入库
if(fileResizeIcon200 == null){
iFileResizeIconService.imageThumbnailSave(iconCode200,suffixName,srcFileName,storageObjectId,fileName);
}
if(fileResizeIcon600 == null){
iFileResizeIconService.imageThumbnailSave(iconCode600,suffixName,srcFileName,storageObjectId,fileName);
}
//使用分析图片并入mediaInfo库
MediaInfo newmediaInfo = PicUtils.analyzePicture(new File(srcFileName));
//组装一下mediaInfo
newmediaInfo.setStorageObjectId(storageObjectId);
if(StringUtils.isBlank(newmediaInfo.getShootingTime())){
newmediaInfo.setShootingTime(DateUtil.now());
}
iMediaInfoService.save(newmediaInfo);
}
2.6.获取缩略图预览地址接口 getIconUrl()
2.6.1参数分析:用户(userId)、图片尺寸(iconCode)、文件id(fileId)
因为这个接口是每个用户自己的缩略图,所以需要一个userId确定是哪个用户的缩略图,同时如果要找到缩略图的话需要storageObjectId和iconCode唯一确定,而storageObjectId是原图的,要拿到这个可以根据fileId和userId来唯一确定对应的那张图片。用一个fileResizeIconBo封装
2.6.2先在FileResizeIconController里写一个/image/getIconUrl方法,然后具体业务在service层实现。首先根据fileId在UserFile表获得userFile,然后通过auditStatus看审核状态如果审核失败的就调用getAuditFailIconUrl()审核通过的则拿到storageObjectId就通过getFileResizeIcon()先去查看是否有缩略图,如果没有就需要调用downloadImage()方法,而里面又需要传入containerId和objectId,因此就需要通过getStorageObjectById()方法拿到里面的storageObject。然后最后再将
最终的 containerId和objectId赋值然后再调用getDownloadUrl()方法生成缩略图下载地址。
/**
* 获得缩略图生成地址
* @param userId
* @param fileId
* @param iconCode
* @return
*/
@Override
public String getIconUrl(String userId, String fileId, String iconCode){
//根据fileId先把userFile拿出来,为了拿到里面的storageObjectId
UserFileBo userFile = cloud2TransService.getUserFileById(fileId);
String storageObjectId = userFile.getStorageObjectId();
String fileName = userFile.getFileName();
String suffixName = fileName.substring(fileName.lastIndexOf("."));
//查看审核状态
if(userFile.getAuditStatus().equals(CommonConstant.FILE_AUDIT_FAIL)){
return getAuditFailIconUrl();
}
//查询缩略图是否存在
FileResizeIcon fileResizeIcon = iFileResizeIconService.getFileResizeIcon(storageObjectId, iconCode);
//不存在则要生成缩略图的时候需要containerId和objectId,所以根据storageObject拿到storageObject取出里面的元素
StorageObjectBo storageObject = cloud2TransService.getStorageObjectById(storageObjectId);
String containerId;
String objectId ;
//不存在则生成缩略图
//1.下载原图
if(fileResizeIcon == null){
String srcFileName = iFileResizeIconService.downloadImage(storageObject.getContainerId(),storageObject.getObjectId(),suffixName);
//原图下载失败
if(StringUtils.isBlank(suffixName)){
log.error("downloadResult error!");
return null;
}
//生成缩略图
FileResizeIcon newFileResizeIcon = iFileResizeIconService.imageThumbnailSave(iconCode, suffixName, srcFileName, storageObjectId, fileName);
//文件为空或者文件生成失败
if(newFileResizeIcon == null){
log.error("imageThumbnailSave() error!");
return null;
}
objectId = newFileResizeIcon.getObjectId();
containerId = newFileResizeIcon.getContainerId();
}else{
//存在则直接生成缩略图下载地址
objectId = fileResizeIcon.getObjectId();
containerId = fileResizeIcon.getContainerId();
}
ResultBody iconUrlResponse = cloud2TransService.getDownloadUrl(containerId, objectId);
return iconUrlResponse.getData().toString();
}
2.7getAuditFailIconUrl() 获取审核失败的缩略图地址
2.7.1参数分析:无
2.7.2首先从常量池CommonConstant获取一个ICON_STORAGE_OBJECT_ID的审核不通过的常量id,先去查询是否存在这个storageObject
- 如果没有,则先下载已经准备好的缩略图然后uploadIcon()方法进行上传然后取出containerId和objectId去获得下载的地址
- 如果有则直接取出containerId和objectId去获得下载的地址
/**
* 获取审核失败的照片图
* @return
*/
@Override
public String getAuditFailIconUrl() {
//查询默认图是否存在存储池
String iconStorageObjectId = CommonConstant.ICON_STORAGE_OBJECT_ID;
StorageObjectBo iconStorageObject = cloud2TransService.getStorageObjectById(iconStorageObjectId);
String containerId = "";
String objectId = "";
String srcFileName = "";
//不存在则从文件中获取违规图上传到存储池
if(iconStorageObject == null){
File file = null;
try{
file = ResourceUtils.getFile("classpath:static/auditFail.jpg");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
FileResizeIcon newFileResizeIcon = iFileResizeIconService.uploadIcon(null, iconStorageObjectId, "200_200", file, "auditFail.jpg");
containerId = newFileResizeIcon.getContainerId();
objectId = newFileResizeIcon.getObjectId();
}else{
containerId = iconStorageObject.getContainerId();
objectId = iconStorageObject.getObjectId();
}
//生成缩略图下载地址
ResultBody iconUrlResponse = cloud2TransService.getDownloadUrl(containerId,objectId);
return iconUrlResponse.getData().toString();
}
审核功能
流程图
数据库
开发流程
1.基础配置
1.1新生成一个audit的项目,配置application,修改名字,端口为9001,
1.2pom文件导入
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>cloud-photo-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
</dependencies>
1.3用代码生成工具根据tb_file_audit表生成基本类
1.4配置启动类,加上注解,实体表的主键上也加上@TableId(type = IdType.ASSIGN_UUID)
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients(basePackages = {"com.cloud.photo"})
@MapperScan(basePackages = {"com.cloud.photo.audit.mapper"})
@ComponentScan({"com.cloud.photo"})
2.接口开发
2.1.kfkaf消费审核列表
因为是从kfkaf里拿消息所以肯定是先创建一个AuditConsumer类负责消费审核列表的消息,再进行业务逻辑处理。在类上注解配上@Component
先拿出file_audit_topic主题里的审核消息,取出userFileId、fileMd5、fileName、fileSize、storageObjectId属性,相同文件只需要判断一次,因此根据md5判断是否审核过
- 如果没审核则组装一下然后入库加入人工审核列表,管理员则通过待审核列表去更新审核列表
- 如果已经过了审核则不需要做什么
- 如果没有过审核的则需要在userFile里修改审核状态然后执行updateUserFile()方法
/**
* @Author:kkoneone11
* @name:AuditConsumer
* @Date:2023/7/23 10:51
*/
@Component
public class AuditConsumer {
@Autowired
IFileAuditService iFileAuditService;
@Autowired
Cloud2TransService cloud2TransService;
@KafkaListener(topics = {"file_audit_topic"})
public void onMessage(ConsumerRecord<String,Object> record){
// 消费的哪个topic、partition的消息,打印出消息内容
System.out.println("消费:"+record.topic()+"-"+record.partition()+"-"+record.value());
//组装一下需要的信息
Object value = record.value();
JSONObject jsonObject = JSONObject.parseObject(value.toString());
String userFileId = jsonObject.getString("userFileId");
String fileMd5 = jsonObject.getString("fileMd5");
String fileName = jsonObject.getString("fileName");
Integer fileSize = jsonObject.getInteger("fileSize");
String storageObjectId = jsonObject.getString("storageObjectId");
//根据md5查询是否在审核数据库里
FileAudit fileAudit = iFileAuditService.getOne(new QueryWrapper<FileAudit>().eq("md5", fileMd5), false);
//1.未审核,不存在数据库中则入库
if(fileAudit == null){
//插入审核列表
FileAudit newFileAudit =new FileAudit();
newFileAudit.setAuditStatus(CommonConstant.FILE_AUDIT);
newFileAudit.setFileName(fileName);
newFileAudit.setMd5(fileMd5);
newFileAudit.setFileSize(fileSize);
newFileAudit.setCreateTime(LocalDateTime.now());
newFileAudit.setUserFileId(userFileId);
newFileAudit.setStorageObjectId(storageObjectId);
iFileAuditService.save(newFileAudit);
//2.已经审核过了
}else if(fileAudit.getFileAuditId().equals(CommonConstant.FILE_AUDIT_ACCESS)){
//3.审核过且不通过,则修改状态并更新到数据库中
}else if(fileAudit.getAuditStatus().equals(CommonConstant.FILE_AUDIT_FAIL)){
//组装
UserFileBo userFileBo = new UserFileBo();
userFileBo.setUserFileId(userFileId);
userFileBo.setAuditStatus(CommonConstant.FILE_AUDIT_FAIL);
List<UserFileBo> userFileList = new ArrayList<>();
userFileList.add(userFileBo);
cloud2TransService.updateUserFile(userFileList);
}
}
}
2.2获得审核列表 /audit/getAuditLis Post
2.2.1参数分析:查询列表当然需要(current、pageSize),然后要需要展现一个待审核的列表(AuditStatusList)。
2.2.2因为未处理的审核列表和已经处理的审核列表用的都是同一个方法,因此先看pageBo里的AuditStatusList是否为空
- 如果不为空则表明要查询的是已经审核的列表,设置的查询为in
- 如果为空则表明要查询的是待审核的,返回查询待审核的列表即可,设置查询为eq
最后面设置page然后进行查询即可
2.3.更新审核状态 /audit/updateAuditStatus Post
2.3.1参数分析:待更改的对象列表idsList、要更改的状态auditStatus
2.3.2写一个updateAuditStatus()方法,然后具体处理业务放在service里,先从pageBo类拿出auditStatus和idsList。然后通过idsList查出fileAuditList列表,通过一个for循环给里面的fileAud对象标记上已审核的标记,然后再组装一个userFileBo。再后面对fileAuditList和userFileBoList进行更新。
**
* <p>
* 文件审核列表 服务实现类
* </p>
*
* @author kkoneone11
* @since 2023-07-23
*/
@Service
public class FileAuditServiceImpl extends ServiceImpl<FileAuditMapper, FileAudit> implements IFileAuditService {
@Autowired
private IFileAuditService iFileAuditService;
@Autowired
private Cloud2TransService cloud2TransService;
@Override
public Boolean updateAuditStatus(AuditPageBo auditPageBo) {
Integer auditStatus = auditPageBo.getAuditStatus();
List<String> idsList = auditPageBo.getFileAuditIds();
List<UserFileBo> userFileBoList = new ArrayList<>();
List<FileAudit> fileAuditList = iFileAuditService.listByIds(idsList);
for(FileAudit fileAudit : fileAuditList){
fileAudit.setAuditStatus(auditStatus);
//组装UserFileBo去更新状态
UserFileBo userFileBo = new UserFileBo();
userFileBo.setAuditStatus(auditStatus);
userFileBo.setStorageObjectId(fileAudit.getStorageObjectId());
userFileBoList.add(userFileBo);
}
Boolean updateResult = iFileAuditService.updateBatchById(fileAuditList);
//审核不通过的才去更新更新文件列表审核状态
if(auditStatus.equals(CommonConstant.FILE_AUDIT_FAIL)){
updateResult = cloud2TransService.updateUserFile(userFileBoList);
}
return updateResult;
}
}