基于微服务的云相册项目

项目简介

本项目是基于微服务开发的企业级项目。核心功能是用户可以上传图片到平台进行存储,类似于网盘图片存储功能。其中还用到了很多市面流行的技术,后续会对技术进行详细介绍。

项目展示

技术选型

本文介绍主要介绍后端,后端所用到的核心技术为SpringCloud+MybatisPlus+Mysql+Redis+Kafka+Minio+Nacos+libvips+Sa-Token权限认证框架

项目结构

cloud-photo-g为父工程,用于统一管理依赖

cloud-photo-api为api服务,网关转发到api服务后再调用其他微服务使用

cloud-photo-audit为图片审核服务

cloud-photo-common用于存放公共类,例如:工具类、常量类、异常类等

cloud-photo-gateway为网关服务,是整个微服务架构对外的统一入口

cloud-photo-image为图片服务

cloud-photo-trans为图片传输服务

cloud-photo-user为用户服务

技术介绍

以下工具的下载启动方式自己搜索即可,这里不做介绍。

Spring Cloud

Spring Cloud是一个开源的微服务架构工具集,它由多个子项目组成,旨在简化分布式系统的开发和管理。Spring Cloud基于Spring Boot,提供了一系列服务发现、配置管理、负载均衡、断路器等微服务所需的核心功能。

MybatisPlus

MybatisPlus(简称MP)是一个 Mybatis 的增强工具,在 Mybatis 的基础上只做增强不做改变,为简化开发、提高效率而生。关于mybatis-plus的更多介绍及特性,可以参考mybatis-plus官网。它通过已经封装好了一些crud方法,我们不需要再写xml了,直接调用这些方法就行。

Redis

Redis是一个开源的内存中数据结构存储系统,它支持多种类型的数据结构,如字符串(strings)、散列(hashes)、列表(lists)、集合(sets)和有序集合(sorted sets)。Redis以其高性能、高并发和丰富的数据结构而闻名,广泛应用于缓存、消息队列、排行榜、社交网络等多种场景。

Redis的速度非常快,因为它完全在内存中运行,并通过单线程模型处理命令,这使得它能够充分利用CPU和内存资源。Redis支持事务、持久化(RDB和AOF)、LUA脚本、LRU驱动事件、多种集群方案等高级功能,这些功能使得Redis在处理复杂数据和保证数据一致性方面表现出色。

Redis的持久化功能允许它将数据保存到磁盘上,以防止服务器意外重启导致数据丢失。此外,Redis还支持主从复制、哨兵和集群等高可用性特性,可以构建高可用和分布式的Redis架构。

Kafka

Kafka是一款分布式、支持分区的、多副本,基于zookeeper协调的分布式消息系统。最大的特性就是可以实时处理大量数据来满足需求。

kafka使用场景包括
1.日志收集:可以用kafka收集各种服务的日志 ,通过已统一接口的形式开放给各种消费者。

2.消息系统:解耦生产和消费者,缓存消息。

3.用户活动追踪:kafka可以记录webapp或app用户的各种活动,如浏览网页,点击等活动,这些活动可以发送到kafka,然后订阅者通过订阅这些消息来做监控。

4.运营指标:可以用于监控各种数据。

Minio

Minio是一个基于Apache License v2.0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。

Nacos

Nacos 是阿里巴巴推出来的一个新开源项目,这是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。

Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。

libvips

libvips是一个多线程的高性能图片处理库,运行速度快,占用的内存很少,支持C,C ++,java等。可以用来对图片做算术,直方图,卷积,形态学操作,频率滤波,颜色,重采样,统计等操作,它支持从8位int到128位complex多种数字类型。支持各种图像格式,包括JPEG,TIFF,PNG,WebP等。

Sa-Token

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权等一系列权限相关问题。详细可参考官方网站

项目介绍

(1)Sa-Token登录校验

用户进行登录后会生成一个token,每次发起请求的时候必须携带token,访问其他接口时会先进行校验是否有进行登录 。并且Sa-Token还可以自动集成redis,将token和session放入redis中。

使用步骤

在父工程导入依赖

<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot-starter</artifactId>
    <version>1.37.0</version>
</dependency>

<!--        sa-token 开启注解-->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-aop</artifactId>
    <version>1.34.0</version>
</dependency>

<!--        sa-token 集成redis-->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-dao-redis-jackson</artifactId>
    <version>1.34.0</version>
</dependency>

在application.yml编写配置

sa-token: 
    # token 名称(同时也是 cookie 名称)
    token-name: satoken
    # token 有效期(单位:秒) 默认30天,-1 代表永久有效
    timeout: 2592000
    # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
    active-timeout: -1
    # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
    is-concurrent: true
    # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
    is-share: true
    # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
    token-style: uuid
    # 是否输出操作日志 
    is-log: true

spring:
  redis:
    host: 127.0.0.1
    port: 6379

编写登录方法,调用Sa-Token提供的StpUtil中的方法,详细可以参考官方文档

    private ResultBody doLogin(UserBo bo, String phone) {
        //没有该用户就把信息新增入数据库,有该用户就把修改登录次数
        ResultBody loginResult = userFeignService.login(bo);
        if (loginResult.getCode().equals(CommonEnum.SUCCESS.getResultCode())) {
            //登陆成功 - 后台也执行登录
            StpUtil.login(phone);
            if (StpUtil.isLogin()) {
                //后台也登录成功了就返回token到前端
                //放入token请求头,方便访问后续
                String tokenValue = StpUtil.getTokenInfo().getTokenValue();
                return ResultBody.success(tokenValue);
            }
        } else {
            return loginResult;
        }
        //登陆失败 - 直接返回登录失败信息
        return loginResult;
    }

在需要登录后才能访问的接口上加入@SaCheckLogin注解

    @SaCheckLogin
    @GetMapping("/getUserInfo")
    public ResultBody getUserInfo() {
        return loginService.getUserInfo();
    }

退出登录时要进行清理缓存的操作

    public ResultBody logout() {
        if (StpUtil.isLogin()) {
            //用户登录ID
            String loginId = StpUtil.getLoginIdAsString();
            //session
            SaSession session = StpUtil.getSessionByLoginId(loginId, true);
            //清空session
            session.logout();
            //退出登录
            StpUtil.logout();
        }
        return ResultBody.success();
    }

(2)Nacos服务注册

在父工程导入依赖

        <dependency>
            <groupId>com.alibaba.nacos</groupId>
            <artifactId>nacos-client</artifactId>
            <version>2.2.0</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

所有微服务都编写配置类,然后启动所有微服务

spring:
  cloud:
    # nacos config
    nacos:
      discovery:
        username: nacos
        password: nacos
        server-addr: 127.0.0.1:8848

下图是本项目所有微服务注册到nacos截图

(3)openfeign微服务之间调用

当我们在某个微服务中如果需要调用另外一个微服务里面的功能,这个时候要怎么办呢?SpringCloud给我们提供了一个框架openfeign,它可以帮助我们解决这个问题。

使用步骤

在父工程导入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>2.2.10.RELEASE</version>
</dependency>

在启动类添加@EnableFeignClients注解,开启openfeign并扫描feign文件

package com.cloud.photo.api;


import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;


@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = {"com.cloud.photo"})
@MapperScan(basePackages = {"com.cloud.photo.api.mapper"})
@ComponentScan({"com.cloud.photo"})
public class CloudPhotoApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(CloudPhotoApiApplication.class, args);
    }
}

编写feign接口,@FeignClient后面添加要调用的微服务name,然后直接将所需用到的接口复制到下方即可,记住需要补全路径。

package com.cloud.photo.api.feign;


import com.cloud.photo.common.bo.UserBo;
import com.cloud.photo.common.common.ResultBody;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;


@FeignClient("cloud-photo-user")
public interface UserFeignService {

    /**
     * 
     * @param phone 手机号
     * @return 用户信息
     */
    @GetMapping("/user/getUserInfo")
    ResultBody getUserInfo(@RequestParam(value = "phone") String phone);

 

}

 在需要用到该接口时注入feign服务,调用即可

package com.cloud.photo.api.service.impl;

import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.cloud.photo.api.feign.UserFeignService;
import com.cloud.photo.api.service.LoginService;
import com.cloud.photo.common.bo.UserBo;
import com.cloud.photo.common.common.CommonEnum;
import com.cloud.photo.common.common.ResultBody;
import com.cloud.photo.common.constant.CommonConstant;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.regex.Pattern;


@Service
public class LoginServiceImpl implements LoginService {

    @Resource
    private UserFeignService userFeignService;

    
    @Override
    //获得用户信息,先从缓存获取,没有则创建缓存
    public ResultBody getUserInfo() {
        if (StpUtil.isLogin()) {
            //用户登录ID
            String loginId = StpUtil.getLoginIdAsString();
            //如果session里面有信息优先从里面拿
            SaSession session = StpUtil.getSessionByLoginId(loginId);
            if (session != null) {
                String userInfo = session.getModel(CommonConstant.USER_INFO, String.class);
                if (userInfo != null) {
                    return ResultBody.success(JSON.parseObject(userInfo, UserBo.class));
                }
            }
            //否则访问user服务获取用户信息
            ResultBody userInfo = userFeignService.getUserInfo(loginId);
            if (userInfo.getCode().equals(CommonEnum.SUCCESS.getResultCode())){
                //成功拿到用户信息就存缓存里面,true是有的话就存,没有的话就新建
                SaSession saSession = StpUtil.getSessionByLoginId(loginId, true);
                saSession.set(CommonConstant.USER_INFO, JSON.toJSONString(userInfo.getData()));
            }
            return userInfo;
        }
        //未登录
        return ResultBody.error(CommonEnum.NO_LOGIN);
    }

  
}

(4)图片上传

此部分未项目重点,请做好笔记!!以下我将介绍整个图片上传流程

使用步骤

在父工程导入依赖,部分依赖上面已经导入,这里仅展示未导入的

        <!--        kafka-->
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
        </dependency>

        <!--        hutool工具-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>4.1.1</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.51</version>
        </dependency>

        <!--        上传图片需要用到-->
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk-core</artifactId>
            <version>1.12.487</version>
        </dependency>

        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk-s3</artifactId>
            <version>1.12.487</version>
        </dependency>

图片上传流程分为两步:获取上传地址和提交上传记录

获取上传地址

1.这是获取上传地址接口,这里把用户id放入FileUploadBo就进入putuploadService业务层代码。前端传过来FileUploadBo 包括category图片分类,fileName文件名,fileSize图片大小,fileMd5图片的md5值。

    @Autowired
    PutuploadService putuploadService;


    /**
     * 获取上传地址
     *
     * @return 每一张图片一个上传地址
     */
    @SaCheckLogin
    @PostMapping("getPutUploadUrl")
    public ResultBody getPutUploadUrl(@RequestBody FileUploadBo bo) {

        //从缓存拿到用户信息
        UserBo userInfo = MyUtils.getUserInfo();
        if (userInfo == null) {
            return ResultBody.error(CommonEnum.USER_IS_NULL);
        }
        //用户ID
        String userId = userInfo.getUserId();
        //判断下上传的文件是否为空
        if (bo == null) {
            return ResultBody.error(CommonEnum.FILE_LIST_IS_NULL);
        }
        //拼接用户ID
        bo.setUserId(userId);

        // 业务实现
        return putuploadService.getPutUploadUrl(bo);
    }

2.我们先看trans服务里的代码,因为putuploadService需要远程调用它。首先根据图片的md5值来查询是否有相同的图片已经上传过,这样的话就可以节省资源。如果图片已存在则称为秒传。如果图片存在的话我们直接返回storageObjectId(图片的存储id,这是数据库的主键)即可,因为图片已经上传过了,已经不需要拿他的上传地址了。如果图片不存在则需要进行图片上传,调用S3Util来进行上传,然后返回S3Util生成的返回值,重点是图片上传地址。

***注意,秒传和非秒传返回值是不同的***

    
     @Autowired
    IFileMd5Service iFileService;
        
    public String getPutUploadUrl(String fileName, String fileMd5, Long fileSize) {
        //文件已存在 进行秒传 直接将存储id返回
        FileMd5 fileMd5Entity = iFileService.getOne(new QueryWrapper<FileMd5>().eq("md5", fileMd5));
        if (fileMd5Entity != null) {
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("storageObjectId", fileMd5Entity.getStorageObjectId());
            return jsonObject.toJSONString();
        }

        //文件不存在
        String suffixName = "";
        if (StringUtils.isNotBlank(fileName)) {//检查fileName为不为空
            suffixName = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length());
        }
        return S3Util.getPutUploadUrl(suffixName, fileMd5);//上传图片,返回上传信息
    }

3.然后我们看看S3Util的代码。主要就是把图片上传到Minio,也可以选择服务商的桶服务存储,原理是差不多的,我选择的是Minio。原理就是先建立连接然后将文件的md5转换为base64上传到Minio存储,然后返回上传信息。这里的代码会用即可。

package com.cloud.photo.common.util;

import cn.hutool.core.codec.Base64;
import com.alibaba.fastjson.JSONObject;
import com.amazonaws.HttpMethod;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.auth.profile.ProfileCredentialsProvider;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.*;
import io.micrometer.core.instrument.util.StringUtils;
import org.apache.tomcat.util.buf.HexUtils;

import java.io.*;
import java.net.URL;
import java.net.URLEncoder;
import java.util.*;

public class S3Util {


    private static String accessKey = "minioadmin";
    private static String secretKey = "minioadmin";
    private static String bucketName = "cloud-photo";
    private static String serviceEndpoint = "http://127.0.0.1:9000";


    private static String containerId = "10001";

    /**
     * 获取上传地址
     * @param suffixName
     * @param fileMd5
     * @return
     */
    public static String getPutUploadUrl(String suffixName,String fileMd5) {

        //链接过期时间
        Date expiration = new Date();
        long expTimeMillis = expiration.getTime();
        expTimeMillis += 1000 * 60 * 10;
        expiration.setTime(expTimeMillis);
        String objectId = UUID.randomUUID().toString().replaceAll("-","");
        String base64Md5 = "";
        if(StringUtils.isNotBlank(suffixName)){
            objectId = objectId +"." + suffixName;
        }
        //建立S3客户端,获取上传地址
        AmazonS3 s3Client = getAmazonS3Client();
        GeneratePresignedUrlRequest generatePresignedUrlRequest =
                new GeneratePresignedUrlRequest(bucketName, objectId)
                        .withMethod(HttpMethod.PUT)
                        .withExpiration(expiration);
        if(StringUtils.isNotBlank(fileMd5)){
            base64Md5= Base64.encode(HexUtils.fromHexString(fileMd5));
            generatePresignedUrlRequest = generatePresignedUrlRequest.withContentMd5(base64Md5);
        }
        URL url = s3Client.generatePresignedUrl(generatePresignedUrlRequest);

        JSONObject jsonObject =new JSONObject();
        jsonObject.put("objectId",objectId);
        jsonObject.put("url",url);
        jsonObject.put("containerId",containerId);
        jsonObject.put("base64Md5",base64Md5);
        return jsonObject.toJSONString();
    }
}

4.这是第1中调用的putuploadService,首先需要通过Feign远程调用trans服务,2.3中已经介绍了,返回值可能有两个信息storageObjectId(秒传)和图片上传信息(非秒传)。然后根据是否为秒传设置fileUploadBo的属性, fileUploadBo的传输状态设置为传送中,然后存入redis中,后续可以查看传输进度。

    @Autowired
    private CloudPhotoTransService cloudPhotoTransService;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public ResultBody getPutUploadUrl(FileUploadBo fileUploadBo) {

        ResultBody resultBody = cloudPhotoTransService.getPutUploadUrl(fileUploadBo.getUserId(),
                fileUploadBo.getFileSize(),
                fileUploadBo.getFileMd5(),
                fileUploadBo.getFileName());

        //存进缓存的Key
        String key = fileUploadBo.getUserId() + ":" + fileUploadBo.getFileMd5();

        if (resultBody.getCode().equals(CommonEnum.SUCCESS.getResultCode())) {
            //成功
            JSONObject obj = JSONUtil.parseObj(resultBody.getData());
            if (obj.containsKey("storageObjectId")) {
                //是秒传
                fileUploadBo.setStorageObjectId(obj.getStr("storageObjectId"));
            } else {
                //不是秒传
                fileUploadBo.setContainerId(obj.getStr("containerId"));
                fileUploadBo.setObjectId(obj.getStr("objectId"));
                fileUploadBo.setUploadUrl(obj.getStr("url"));
                fileUploadBo.setBase64Md5(obj.getStr("base64Md5"));
            }

            //成功了状态是传输中
            fileUploadBo.setStatus(CommonConstant.FILE_UPLOAD_ING);
            fileUploadBo.setUploadTime(DateUtil.now());
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(fileUploadBo), 1, TimeUnit.DAYS);
        } else {
            //失败了状态是传输失败
            fileUploadBo.setStatus(CommonConstant.FILE_UPLOAD_FAIL);
            fileUploadBo.setUploadTime(DateUtil.now());
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(fileUploadBo), 1, TimeUnit.DAYS);
        }

        return ResultBody.success(fileUploadBo);
    }

获取上传地址这部分流程就结束了

提交上传记录

因为上一步我们只是将图片上传了,但相应记录还并未在数据库进行保存,所以这部分就是来解决这个问题的。

1.这是提交记录的接口,接受上一步中的FileUploadBo ,调用业务层commitUpload代码


    /**
     * 提交
     *
     * @return 结果
     */
    @SaCheckLogin
    @PostMapping("commitUpload")
    //将图片入数据库
    public ResultBody commitUpload(@RequestBody FileUploadBo bo) {
        //从缓存拿到用户信息
        UserBo userInfo = MyUtils.getUserInfo();
        if (userInfo == null) {
            return ResultBody.error(CommonEnum.USER_IS_NULL);
        }
        //用户ID
        String userId = userInfo.getUserId();
        //判断下上传的文件是否为空
        if (ObjectUtil.isEmpty(bo)) {
            return ResultBody.error(CommonEnum.FILE_LIST_IS_NULL);
        }
        //拼接用户ID
        bo.setUserId(userId);

        // 业务实现
        return putuploadService.commitUpload(bo);
    }

2.这是commitUpload代码,远程调用trans服务commit接口,然后将传输状态设置一下返回。接下来重点看trans服务commit接口实现了什么。

    @Override
    public ResultBody commitUpload(FileUploadBo bo) {

        //存进缓存的Key
        String key = bo.getUserId() + ":" + bo.getFileMd5();

        ResultBody commitResultBody = cloudPhotoTransService.commit(bo);

        if (commitResultBody.getCode().equals(CommonEnum.SUCCESS.getResultCode())) {
            //成功提交 - 状态更新为传输成功
            bo.setStatus(CommonConstant.FILE_UPLOAD_SUCCESS);
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(bo), 1, TimeUnit.DAYS);
        } else {
            //提及失败 - 状态更新为传输失败
            bo.setStatus(CommonConstant.FILE_UPLOAD_FAIL);
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(bo), 1, TimeUnit.DAYS);
        }

        return commitResultBody;
    }

3.这是trans服务commit接口,判断是否为秒传来决定调用那个方法,因为上面介绍过了秒传的返回值FileUploadBo是有StorageObjectId的,非秒传是没有的,所以根据这点很容易判断出是否为秒传。

@RequestMapping("/commit")
    public ResultBody commit(HttpServletRequest request, HttpServletResponse response,
                             @RequestBody FileUploadBo bo) {
        //打印请求日志
        String requestId = RequestUtil.getRequestId(request);
        RequestUtil.printQequestInfo(request);

        //返回值
        CommonEnum result;

        //判断文件是否秒传,如果有StorageObjectId则说明是秒传
        if (StringUtils.isBlank(bo.getStorageObjectId())) {
            //处理非秒传
            result = putuploadService.commit(bo);
        } else {
            result = putuploadService.commitTransSecond(bo);
        }

        log.info("getPutUploadUrl() userId =" + bo.getUserId() + ",result=" + result);
        if (StringUtils.equals(result.getResultMsg(), CommonEnum.SUCCESS.getResultMsg())) {
            return ResultBody.success(CommonEnum.SUCCESS.getResultMsg(), requestId);
        } else {
            return ResultBody.error(result.getResultCode(), result.getResultMsg(), requestId);
        }

    }

4.先看是非秒传调用的commit方法,这里的代码并不复杂,就是将各种信息存储到数据库里面。

    /**
     * 处理非妙传的
     * @param bo
     * @return
     */
    public CommonEnum commit(FileUploadBo bo) {
        //获取文件资源池储存信息
        S3ObjectSummary s3ObjectSummary = S3Util.getObjectInfo(bo.getObjectId());//非秒传的必须有ObjectId

        //文件未上传
        if (s3ObjectSummary == null) {
            return CommonEnum.FILE_NOT_UPLOADED;
        }

        //文件上传错误
        if (!bo.getFileSize().equals(s3ObjectSummary.getSize()) || !StringUtils.equalsIgnoreCase(s3ObjectSummary.getETag(), bo.getFileMd5())) {
            return CommonEnum.FILE_UPLOADED_ERROR;
        }

        //文件上传成功 - 文件存储信息入库
        StorageObject storageObject = new StorageObject("minio", bo.getContainerId(), bo.getObjectId(), bo.getFileMd5(), bo.getFileSize());
        iStorageObjectService.save(storageObject);

        //文件md5入库,秒传用
        FileMd5 fileMd5 = new FileMd5(bo.getFileMd5(), bo.getFileSize(), storageObject.getStorageObjectId());
        iFileMd5Service.save(fileMd5);

        //文件入库 - 用户文件列表 - 发送到审核、图片 kafka列表
        bo.setStorageObjectId(storageObject.getStorageObjectId());
        iUserFileService.saveAndFileDeal(bo);

        return CommonEnum.SUCCESS;
    }

5.再看commitTransSecond代码,这里只需要保存一到一个表里即可,因为秒传部分数据已经会存在数据库里面了。

    /**
     * 处理妙传的
     * @param bo
     * @return
     */
    public CommonEnum commitTransSecond(FileUploadBo bo) {

        //检验储存的ID是否正确
        StorageObject storageObject = iStorageObjectService.getById(bo.getStorageObjectId());//秒传的必须有StorageObjectId

        if (storageObject == null) {
            return CommonEnum.FILE_UPLOADED_ERROR;
        }

        //检查秒传文件大小
        if (!storageObject.getObjectSize().equals(bo.getFileSize()) || !StringUtils.equalsIgnoreCase(bo.getFileMd5(), storageObject.getMd5())) {
            return CommonEnum.FILE_UPLOADED_ERROR;
        }

        //保存文件入库 - 用户文件列表 - 发送到审核、图片 kafka列表
        boolean result = iUserFileService.saveAndFileDeal(bo);

        if (result) {
            return CommonEnum.SUCCESS;
        } else {
            return CommonEnum.FILE_UPLOADED_ERROR;
        }
    }

6.秒传和秒传都调用了saveAndFileDeal方法,我们看一下它的代码。除了将数据保存到数据库,这里还通过kafka发送了两条消息,这里下面将进行介绍。

    @Override
    public boolean saveAndFileDeal(FileUploadBo bo) {
        UserFile userFile = new UserFile();
        userFile.setUserId(bo.getUserId());
        userFile.setFileStatus(CommonConstant.FILE_STATUS_NORMA);//文件正常
        userFile.setCreateTime(LocalDateTime.now());
        userFile.setFileName(bo.getFileName());
        userFile.setIsFolder(CommonConstant.FILE_IS_FOLDER_NO);
        userFile.setAuditStatus(CommonConstant.FILE_AUDIT_ACCESS);
        userFile.setFileSize(bo.getFileSize());
        userFile.setModifyTime(LocalDateTime.now());
        userFile.setStorageObjectId(bo.getStorageObjectId());
        userFile.setCategory(bo.getCategory());
        Boolean result = this.save(userFile);

        //文件审核处理
        kafkaTemplate.send(CommonConstant.FILE_AUDIT_TOPIC, JSONObject.toJSONString(userFile));

        //图片处理-格式分析
        kafkaTemplate.send(CommonConstant.FILE_IMAGE_TOPIC, JSONObject.toJSONString(userFile));

        return result;
    }

(5)消息监听

上面最后通过kafka向两个topic(FILE_AUDIT_TOPIC和FILE_ IMAGE_TOPIC)各发送了一条消息,这里就介绍这里的流程。

FILE_AUDIT_TOPIC

这部分是有关文件审核处理的

AuditConsumer 用于消费file_audit_topic里面的消息,上面往file_audit_topic发送消息后,这个类监听到后获取图片的md5值,根据图片md5查询审核表之前是否有审核记录,没有审核记录的话就添加进审核列表,有审核记录并且之前已经通过的不需要再次审核,有审核记录但之前未通过的=就直接设置审核失败。

package com.cloud.photo.audit.consumer;


import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.cloud.photo.audit.entity.FileAudit;
import com.cloud.photo.audit.service.IFileAuditService;
import com.cloud.photo.common.bo.StorageObjectBo;
import com.cloud.photo.common.bo.UserFileBo;
import com.cloud.photo.common.constant.CommonConstant;
import com.cloud.photo.common.feign.CloudPhotoTransService;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Component
public class AuditConsumer {

    @Autowired
    IFileAuditService iFileAuditService;
    @Autowired
    CloudPhotoTransService cloudPhotoTransService;

    // 消费监听
    @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 fileName = jsonObject.getString("fileName");
        Integer fileSize = jsonObject.getInteger("fileSize");
        String storageObjectId = jsonObject.getString("storageObjectId");

        StorageObjectBo storageObject = cloudPhotoTransService.getStorageObjectById(storageObjectId);
        String fileMd5=storageObject.getMd5();

        //根据文件MD5查看之前是否有审核过相同的照片   读取审核状态  相同文件只需要审核一次
        FileAudit fileAudit = iFileAuditService.getOne(new QueryWrapper<FileAudit>().eq("md5", fileMd5), false);

        //之前没有审核过此照片   未人工审核  加入审核列表
        if(fileAudit == null){
            //未审核  插入审核列表
            fileAudit=new FileAudit();
            fileAudit.setAuditStatus(0);
            fileAudit.setMd5(fileMd5);
            fileAudit.setUserFileId(userFileId);
            fileAudit.setFileName(fileName);
            fileAudit.setFileSize(fileSize);
            fileAudit.setCreateTime(LocalDateTime.now());
            fileAudit.setStorageObjectId(storageObjectId);
            iFileAuditService.save(fileAudit);

        }else if(fileAudit.getAuditStatus().equals(CommonConstant.FILE_AUDIT_ACCESS)){//1
            //之前已经审核过此照片   文件审核状态默认为已通过审核  无需修改文件状态
        }else if(fileAudit.getAuditStatus().equals(CommonConstant.FILE_AUDIT_FAIL)){//2
            //之前已经审核过此照片   文件审核状态默认为未通过  审核失败  更新文件审核状态
            UserFileBo userFileBo=new UserFileBo();
            userFileBo.setUserFileId(userFileId);
            userFileBo.setAuditStatus(CommonConstant.FILE_AUDIT_FAIL);

            List<UserFileBo> userFileBoList=new ArrayList<>();
            userFileBoList.add(userFileBo);
            cloudPhotoTransService.updateUserFile(userFileBoList);
        }
    }
}

FILE_ IMAGE_TOPIC

这部分是有关图片处理,格式分析的       

ImageConsumer 用于消费file_image_topic里面的消息,接收到消息调用iFileResizeIconService里的imageThumbnailAndMediaInfo方法。我们重点看它里面的代码。

package com.cloud.photo.image.consumer;


import com.alibaba.fastjson.JSONObject;
import com.cloud.photo.image.service.IFileResizeIconService;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

@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);
    }
}

这是iFileResizeIconService代码,主要用于生成图片的缩略图,这里是核心。上面先调用了里面的imageThumbnailAndMediaInfo方法,这个方法首先根据图片存储id来查询是否有200*200和600*600尺寸的缩略图,然后再查询这张图片是否已经分析过格式。 若都满足则可以结束,缩略图不存在的话则需要先把原图下载。调用这个类中的downloadImage方法,里面的DownloadFileUtil会先将原图下载在本地定义好的地址。然后就可以调用imageThumbnailSave方法来根据下载下来的原图生成缩略图,里面还包括了上传到Minio和数据库,其中缩略图的生成需要用到VipsUtil这个工具类。最后还要用PicUtils这个工具类来对图片格式分析后保存到数据库。

package com.cloud.photo.image.service.impl;

import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.cloud.photo.common.bo.StorageObjectBo;
import com.cloud.photo.common.bo.UserFileBo;
import com.cloud.photo.common.common.ResultBody;
import com.cloud.photo.common.constant.CommonConstant;
import com.cloud.photo.common.feign.CloudPhotoTransService;
import com.cloud.photo.image.entity.FileResizeIcon;
import com.cloud.photo.image.entity.MediaInfo;
import com.cloud.photo.image.mapper.FileResizeIconMapper;
import com.cloud.photo.image.service.IFileResizeIconService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cloud.photo.image.service.IMediaInfoService;
import com.cloud.photo.image.util.DownloadFileUtil;
import com.cloud.photo.image.util.PicUtils;
import com.cloud.photo.image.util.UploadFileUtil;
import com.cloud.photo.image.util.VipsUtil;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.ResourceUtils;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.HashMap;
import java.util.UUID;

/**
 * <p>
 * 图片缩略图 服务实现类
 * </p>
 *
 * @author wfc
 * @since 2023-07-13
 */
@Service
public class FileResizeIconServiceImpl extends ServiceImpl<FileResizeIconMapper, FileResizeIcon> implements IFileResizeIconService {

    @Autowired
    CloudPhotoTransService cloudPhotoTransService;
    @Autowired
    IMediaInfoService iMediaInfoService;

    @Override
    public String getIconUrl(String userId, String fileId, String iconCode) {

        //查询文件信息
        UserFileBo userFile=cloudPhotoTransService.getUserFileById(fileId);
        String storangeObject=userFile.getStorageObjectId();
        String fileName=userFile.getFileName();
        String suffixName=fileName.substring(fileName.lastIndexOf(".")+1,fileName.length());

        //获取文件存储信息
        StorageObjectBo storageObjectBo=cloudPhotoTransService.getStorageObjectById(storangeObject);
        //查询缩略图信息
        FileResizeIcon fileResizeIcon=getFileResizeIcon(userFile.getStorageObjectId(),iconCode);

        String objectId;
        String containerId;
        //缩略图不存在   生成缩略图
        if(fileResizeIcon==null){
            //获取原图,传递给libvips的原文件的地址
            String srcFileName=downloadImage(storageObjectBo.getContainerId(),storageObjectBo.getObjectId(),suffixName);
            if(StringUtils.isBlank(srcFileName)){
                return null;
            }
            //先生成一张缩略图,通过libvips工具,然后用http请求访问srcFileName,并上传一个文件体
            FileResizeIcon newFileResizeIcon=this.imageThumbnailSave(iconCode,suffixName,srcFileName,storangeObject,fileName);
            if(newFileResizeIcon==null){
                return null;
            }
            objectId= newFileResizeIcon.getObjectId();
            containerId= newFileResizeIcon.getContainerId();
        }else {
            objectId=fileResizeIcon.getObjectId();
            containerId=fileResizeIcon.getContainerId();
        }

        //生成缩略图下载地址
        ResultBody iconUrlResponse=cloudPhotoTransService.getDownloadUrl(containerId,objectId);
        return iconUrlResponse.getData().toString();
    }



    /**
     * 图片处理  1、生成 200_200、600_600尺寸缩略图  2、分析图片格式 宽高等信息
     * @param storageObjectId
     * @param fileName
     */
    @Override
    public void imageThumbnailAndMediaInfo(String storageObjectId, String fileName) {
        String iconCode200 = "200_200";
        String iconCode600 = "600_600";

        //查询尺寸200和尺寸600缩略图 是否存在  - 同一张缩略图无需重复生成
        FileResizeIcon fileResizeIcon200 = getFileResizeIcon(storageObjectId,iconCode200);
        FileResizeIcon fileResizeIcon600 = 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 = cloudPhotoTransService.getStorageObjectById(storageObjectId);
        String srcFileName = downloadImage(storageObject.getContainerId(), storageObject.getObjectId(), suffixName);

        //原图下载失败
        if(StringUtils.isBlank(srcFileName)){
            log.error("downloadResult error!");
            return;
        }

        //生成缩略图 保存入库
        if(fileResizeIcon200==null){
            this.imageThumbnailSave(iconCode200,suffixName,srcFileName,storageObjectId,fileName);
        }

        if(fileResizeIcon600==null){
            this.imageThumbnailSave(iconCode600,suffixName,srcFileName,storageObjectId,fileName);
        }

        //图片格式分析&入库
        MediaInfo mediaInfo1=PicUtils.analyzePicture(new File(srcFileName));
        mediaInfo1.setStorageObjectId(storageObjectId);
        if(StringUtils.isBlank(mediaInfo1.getShootingTime())){
            mediaInfo1.setShootingTime(DateUtil.now());
        }
        iMediaInfoService.save(mediaInfo1);
    }

    public FileResizeIcon getFileResizeIcon(String storageObjectId, String iconCode) {

        //1.设置查询Mapper
        QueryWrapper<FileResizeIcon> qw = new QueryWrapper<>();
        //2.组装查询条件
        HashMap<String, Object> param = new HashMap<>();
        param.put("storage_object_id",storageObjectId);
        param.put("icon_code",iconCode);
        qw.allEq(param);
        return this.getOne(qw,false);
    }

    private String downloadImage(String containerId, String objectId, String suffixName) {
        //获取下载地址
        String srcFileDirName = "D:\\cloudiconphoto\\img\\";
        ResultBody baseResponse = cloudPhotoTransService.getDownloadUrl(containerId,objectId);
        String url = baseResponse.getData().toString();

        String srcFileName  =  srcFileDirName + UUID.randomUUID().toString() +"." +suffixName;
        File dir = new File(srcFileDirName);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        Boolean downloadResult = DownloadFileUtil.downloadFile(url, srcFileName);
        if(!downloadResult){
            return null;
        }
        return srcFileName;
    }


    //先生成缩略图到本机,再把缩略图上传到minio中
    private FileResizeIcon imageThumbnailSave(String iconCode,String suffixName,String srcFileName,
                                              String storageObjectId,String fileName) {

        //文件路径
        String srcFileDirName = "D:\\cloudiconphoto\\img\\";

        //生成缩略图
        String iconFileName  =  srcFileDirName + UUID.randomUUID().toString()+"." + suffixName;
        int width = Integer.parseInt(iconCode.split("_")[0]);
        int height = Integer.parseInt(iconCode.split("_")[1]);
        VipsUtil.thumbnail(srcFileName,iconFileName,width,height,"70");

        //文件为空或者截图失败
        if(StringUtils.isBlank(iconFileName) || !new File(iconFileName).exists()){
            return null;
        }

        //上传缩略图 & 入库,生成了一个缩略图的图片,为上传做准备
        FileResizeIcon fileResizeIcon = this.uploadIcon(null,storageObjectId ,iconCode, new File(iconFileName),fileName);
        return fileResizeIcon;
    }


    private FileResizeIcon uploadIcon(String userId,String storageObjectId ,String iconCode, File iconFile,String fileName) {
        //上传缩略图
        //获得了上传地址
        ResultBody uploadUrlResponse = cloudPhotoTransService.getPutUploadUrl(userId,null,null,fileName);
        JSONObject jsonObject =JSONObject.parseObject(uploadUrlResponse.getData().toString());
        String objectId = jsonObject.getString("objectId");
        String uploadUrl = jsonObject.getString("url");
        String containerId= jsonObject.getString("containerId");

        //上传文件到存储池
        UploadFileUtil.uploadSinglePart(iconFile,uploadUrl);

        //保存入库
        FileResizeIcon newFileResizeIcon = new FileResizeIcon(storageObjectId ,iconCode ,containerId,objectId);
        this.save(newFileResizeIcon);
        return newFileResizeIcon;
    }


    public String getAuditFailIconUrl() {

        //查询默认图是否存在存储池  不存在 上传到存储池
        String iconStorageObjectId = CommonConstant.ICON_STORAGE_OBJECT_ID;
        StorageObjectBo iconStorageObject = cloudPhotoTransService.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 =  this.uploadIcon(null,iconStorageObjectId ,"200_200", file,"auditFail.jpg");
            containerId = newFileResizeIcon.getContainerId();
            objectId = newFileResizeIcon.getObjectId();
        }else{
            containerId = iconStorageObject.getContainerId();
            objectId = iconStorageObject.getObjectId();
        }
        //生成缩略图下载地址
        ResultBody iconUrlResponse = cloudPhotoTransService.getDownloadUrl(containerId,objectId);
        return iconUrlResponse.getData().toString();
    }
}

DownloadFileUtil代码

package com.cloud.photo.image.util;

import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpConnectionParams;

import java.io.*;

@Slf4j
public class DownloadFileUtil {


	//下载原图
	public static Boolean downloadFile(String url, String fileName) {
		Boolean result = false;

		log.info("downloadFile() - fileName=" + fileName + ", url=" + url);

		BufferedOutputStream bos = null;
		FileOutputStream fileOutputStream = null;
		try {
			fileOutputStream = new FileOutputStream(new File(fileName));
			bos = new BufferedOutputStream(fileOutputStream);
		} catch (Exception e) {
			log.warn("downloadFile() - fileName=" + fileName + ", url=" + url + ", FileOutputStream error. ");
			return result;
		}
		HttpClient client = getHttpClient();

		HttpGet get = null;
		InputStream is = null;
		int k = 0;

		while (k < 3) {
			log.info("test - k = "+k);
			try {
				
				get = new HttpGet(url);
				
				HttpResponse resp = client.execute(get);

				int status = resp.getStatusLine().getStatusCode();

				if (status == HttpStatus.SC_OK) {
					is = resp.getEntity().getContent();

					byte[] k8 = new byte[8 * 1024];

					int i = 0;

					while ((i = is.read(k8)) != -1) {
						bos.write(k8, 0, i);
					}
					bos.flush();
					result = true;
					break;
				}

			} catch (ClientProtocolException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				try {
					if (bos != null) {
						bos.close();
					}
					if (fileOutputStream != null) {
						fileOutputStream.close();
					}
					if(is != null){
						is.close();
					}
					if (get != null) {
						get.abort();
					}
				} catch (Exception e) {
				}
			}
			try {
				Thread.sleep(100);
			} catch (Exception e) {

			}
			k++;
		}

		log.info("downloadFile() - fileName=" + fileName + ", url=" + url + ", result=" + result);

		return result;

	}

	/**
	 *
	 * @Title: getHttpClient
	 * @Description: 获取Httpclient
	 * @return    设定文件
	 * @return DefaultHttpClient    返回类型
	 * @throws
	 */
	public static DefaultHttpClient getHttpClient( ) {
		DefaultHttpClient httpclient = new DefaultHttpClient();
		httpclient.getParams().setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_1);
		httpclient.getParams().setParameter(CoreProtocolPNames.USE_EXPECT_CONTINUE, Boolean.FALSE);
		httpclient.getParams().setParameter(CoreProtocolPNames.HTTP_CONTENT_CHARSET,"utf-8");

		httpclient.getParams().setIntParameter(HttpConnectionParams.SO_TIMEOUT, 30 * 1000);
		httpclient.getParams().setIntParameter(HttpConnectionParams.CONNECTION_TIMEOUT, 30 * 1000);

		DefaultHttpRequestRetryHandler retryHandler = new DefaultHttpRequestRetryHandler(3, true);// 重试3次

		httpclient.setHttpRequestRetryHandler(retryHandler);

		return httpclient;
	}
}

这是VipsUtil,是通过上面吧提到的libvips工具来实现生成缩略图的

package com.cloud.photo.image.util;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.PumpStreamHandler;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

@Slf4j
public class VipsUtil {
    //生成缩略图
    static String windowsCommand = "D:\\vips-dev-8.14\\bin\\vipsthumbnail.exe";
    static String linuxCommand = "/usr/local/bin/vipsthumbnail";

    //生成缩略图
    public static Boolean thumbnail(String srcPath, String desPath, int tarWidth, int tarHight, String quality) {

        String size_param = "";

        //原图片宽高均小于目前宽高,则直接用原图宽高
        //获取图片宽高
        size_param = tarWidth + "x" + tarHight;

        ByteArrayOutputStream stdout = new ByteArrayOutputStream();
        PumpStreamHandler psh = new PumpStreamHandler(stdout);
        log.info("thumbnail() prepare tools info ");

        //加入参数-t表示自动旋转
        String command = "";
        if (desPath.endsWith(".png")) {
            command = String.format("%s -t %s -s %s -o %s[Q=%s,strip]", windowsCommand, srcPath, size_param, desPath, quality);
        } else {
            command = String.format("%s -t %s -s %s -o %s[Q=%s,optimize_coding,strip]", windowsCommand, srcPath, size_param, desPath, quality);
        }

        CommandLine cl = CommandLine.parse(command);
        log.info("thumbnail() command = " + command);

        DefaultExecutor exec = new DefaultExecutor();
        exec.setStreamHandler(psh);
        try {
            System.out.println(command);
            exec.execute(cl);
        } catch (IOException e) {
            log.warn("thumbnail() exec command failed!");
        }
        return true;
    }

}

这是PicUtils,用于图片格式分析。使用exif解析图片信息。

package com.cloud.photo.image.util;

import cn.hutool.core.date.DateUtil;
import com.cloud.photo.image.entity.MediaInfo;
import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.Tag;

import java.io.File;
import java.io.IOException;

/**
 * 图片格式分析工具
 * @author linzsh
 */
public class PicUtils {

    /**
     * 使用exif解析图片所有信息
     */
    public static MediaInfo analyzePicture(File file){

        Metadata metadata = null;
        try {
            metadata = ImageMetadataReader.readMetadata(file);
        } catch (ImageProcessingException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        MediaInfo mediaInfo = new MediaInfo();
        for(Directory directory : metadata.getDirectories()){
            for(Tag tag : directory.getTags()){
                //if (tag.getTagName().equalsIgnoreCase("Data Precision")) mediaInfo.setDataPrecision(tag.getDescription());
                if (tag.getTagName().equalsIgnoreCase("Image Height")) mediaInfo.setHeight(Integer.parseInt(tag.getDescription().replaceAll(" pixels","")));
                if (tag.getTagName().equalsIgnoreCase("Image Width")) mediaInfo.setWidth(Integer.parseInt(tag.getDescription().replaceAll(" pixels","")));
                if (tag.getTagName().equalsIgnoreCase("Date/Time Original")){
                    mediaInfo.setShootingTime(DateUtil.parse(tag.getDescription(), "yyyy:MM:dd HH:mm:ss").toString());
                }
                if (tag.getTagName().equalsIgnoreCase("GPS Latitude")) mediaInfo.setLatitude(tag.getDescription());
                if (tag.getTagName().equalsIgnoreCase("GPS Longitude")) mediaInfo.setLongitude(tag.getDescription());
            }
        }

        return mediaInfo;
    }
}

本项目的核心业务至此已经介绍完毕,如有疑问请在评论区交流。

  • 38
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值