用户名片 1:图片上传与压缩

前言

注册登录工作完成之后,就可以设置用户名片了,如昵称、头像、个人签名等信息。本篇带领大家实现用户名片的功能设计。

知识准备

MultipartFile

用户名片中包含了头像信息,需要用到图片上传的功能,在 Spring Boot 项目中,普遍使用 MultipartFile 实现文件上传的功能。

MultipartFile 接口在 org.springframework.web.multipart 包中,是 Spring-web 组件中的标准套件,主要用于处理 HTTP 请求中的文件流。

MultipartFile 接口继承并扩展了 InputStreamSource 接口,用于处理文件上传。主要有以下几个方法组成:

  • String getOriginalFilename():返回客户端文件系统中的原始文件名,也就是文件原本的名字
  • String getContentType():返回文件的内容类型,可以依据这个类型来判断该文件是个图片、视频、音频或者文本文件
  • long getSize():返回文件大小,如果对上传的文件大小有限制,可以以此来判断
  • void transferTo(File dest):将接收到的文件传输到给定的目标文件,这个方法比较重要,用来将文件保存下来

MultipartFile 是 Spring-web 组件中的标准套件,不需要额外引入依赖包。

Thumbnailator

Thumbnailator 是一个高质量的图片处理工具,用法简单,功能强大。提供了图片压缩、旋转、裁剪、格式转换、添加水印等操作。

1. 压缩:

  • size(最大宽度, 最大高度)
  • scale(压缩比例)
//按大小压缩
Thumbnails.of("原图.jpg").size(200, 300).toFile("缩略图.jpg");
//按比例压缩
Thumbnails.of("原图.jpg").scale(0.5f).toFile("缩略图.jpg");

2. 旋转:

  • rotate(旋转角度)
Thumbnails.of("原图.jpg").rotate(180).toFile("旋转后的图片.jpg");

3. 格式转换:

  • outputFormat(要转换的格式)
Thumbnails.of("图片.jpg").outputFormat("png").toFile("图片.png");

4. 裁剪:

  • sourceRegion(x,y,width,height)
Thumbnails.of("图片.jpg").sourceRegion(100, 100, 100, 100).toFile("裁剪后的图片.jpg");

5. 添加水印:

  • watermark(水印位置, 水印图片, 水印透明度)
Thumbnails.of("图片.jpg").watermark(Positions.CENTER, ImageIO.read(new File(result + "水印.jpg")), 0.25f).toFile("处理后的图片.jpg");

云笔记项目通过 Maven 方式引入 Thumbnailator 组件:

<dependency>
    <groupId>net.coobird</groupId>
    <artifactId>thumbnailator</artifactId>
    <version>0.4.8</version>
</dependency>

功能分析

用户名片功能中,比较难以处理的是用户头像的问题,一般来说,用户头像的存储有两种常见的方案:

  1. 系统预置若干个头像,用户在设置名片时选择一个作为自己的头像,系统中保存用户的头像 ID 即可
  2. 用户在设置名片时,自己上传一个头像,系统保存用户头像

在方案 1 中,用户设置名片时,系统保存头像 ID;用户读取头像时,系统返回头像 ID。实现比较简单,数据传输量也极小。

在方案 2 中,还可以有多种不同的实现方式。

1. 系统将头像保存为数据流,记录在数据库中;用户读取头像时需要获取到整个数据流。

  • 优点:实现简单
  • 缺点:数据库存储压力大,接口返回的数据流大、响应时间长

2. 系统将头像保存到服务器上,在数据库中保存的是头像地址;用户读取头像时,从数据库读取头像地址,从服务器获取到头像。

  • 优点:实现简单,数据库存储量少,接口响应时间短
  • 缺点:服务器存储压力大,同时要处理大量的数据流,拖慢服务器整体响应时间

3. 系统将头像保存到专用的文件服务器上,在数据库中保存头像地址;用户读取头像时,从数据库读取头像地址,从专用的文件服务器中获取到头像。

  • 优点:数据库存储量少,接口响应时间短,服务器压力小
  • 缺点:需要搭建单独的文件服务器

4. 系统将头像进行压缩,将原图和压缩后的图片都保存在专用的文件服务器上,在数据库中保存原图和缩略图的地址;用户读取头像时,从数据库读取头像地址,根据使用场景分别从专用的文件服务器上获取到原图和缩略图。

  • 优点:数据库存储量少,接口响应时间短,服务器压力小,能根据不同的使用场景获取不同大小的图片从而减少客户端及文件服务器的流量使用
  • 缺点:需要搭建单独的文件服务器,需要对图片进行压缩

经过以上分析,结合本项目的实际情况,并考虑到当前主流的使用方案,决定采用最后一种方式实现头像的存储与获取。

本篇将实现原图与缩略图的存储,下一篇实现 Nginx 搭建文件服务器及头像的访问。

流程设计

这里以用户头像上传功能的流程为例,流程图如下:

上传头像流程

总体方案
  1. 客户端上传 JWT 及要设置的名片内容(接口 2001),服务端校验用户身份及名片内容后设置
  2. 客户端上传 JWT 及头像(接口 2003),服务端校验用户身份及头像格式后对图片进行压缩
    • 将压缩好的图片和原图分别保存在文件服务器
    • 保存成功后将原图和缩略图的地址保存在用户名片表 user_card 中
  3. 客户端根据 JWT 获取用户名片(接口 2002),服务端校验用户身份后返回名片信息
  4. 客户端根据业务场景从文件服务器中获取到原图或缩略图

代码实现

代码结构

本篇涉及到的代码结构如下:

  • common.util
    • RequestUtils.java:请求工具类,负责请求相关的操作,如:获取请求头、请求参数、解析出请求的目标地址等
      • String getUrl(Map<String,String> params,HttpServletRequest request):从请求中解析出目标地址,会从请求中的 JWT 中解析出 UserId 并拼接在请求参数中
  • JwtUtils.java:JWT 工具类,负责 JWT 的生成和解析操作
    • Integer getUserId(String token):从 JWT 中解析出 UserId
    • FileUtils.java:文件工具类,负责文件的相关操作,如:获取文件名、保存图片、获取文件访问地址等
      • String getSuffix(String fileName):获取文件名的后缀名
      • String getImgPath(String fileName):获取文件的保存路径
      • String getImgUrl(String fileName):获取文件的访问地址
      • String saveImgAndThum (MultipartFile file,String name):保存图片及缩略图
  • entity
    • UserCard.java:用户名片实体类
  • mapper
    • UserCardMapper.java:用户名片持久层
  • service
    • UserCardService.java:用户名片逻辑处理类
  • controller
    • UserCardController.java:用户名片接口处理类
    • UploadController.java:上传接口处理类
服务端配置

打开项目的配置文件 application.properties,根据实际添加如下配置:

# nginx 文件目录,这个目录为电脑本地目录
nginx.path = /Users/yangkaile/Projects/nginx/keller/
# Nginx 服务器访问地址,这个地址为 Nginx 的访问地址:可以暂时空着,等下一篇 Nginx 配置好后修改;也可以先写好,下一篇按照这个配置 Nginx 
nginx.url = http://localhost:8081/keller/
# 原图存放的相对目录
img.path = image/img/
# 缩略图存放的相对目录
thum.path = image/thum/

同时将其添加到 CommonConfig.java 和PublicConstant.java 中供后续使用。

校验用户身份

在前面的内容里我们使用 jjwt 生成了 JWT,并完成了对其的解析,接下来我们在请求中添加对用户身份的验证。

在实际的项目中,有的接口需要验证用户身份,有的接口不需要。使用 JWT 做用户身份的验证可以有多种方案:

  1. 需要用户身份验证的接口一般都需要获取到 UserId 参数来区分不同用户,因此可以在接口访问前解析请求中的 JWT,获取到 UserId 并将其添加到请求参数中,并且过滤掉请求自带的 UserId 参数。工作量小,控制灵活,容易遗漏,适合小型项目。
  2. 每个接口在定义时同时声明其访问权限(通过自定义注解、继承自定义接口等方式),在 JWT 中存储用户的访问权限,用户访问接口时,根据请求中的 JWT 判断其访问权限。工作量适中,控制灵活,可以做到接口、权限的可视化,方便生成文档。
  3. 搭建完整的权限管理系统,适工作量大,控制严格,用于规模大,安全性高的系统。

结合项目实际情况,本项目将采用第 1 种方案实现用户身份的校验。

首先,在 JwtUtils.java 中添加方法 getUserId(String jwtString) 以从 JWT 中读取到用户 Id,代码如下:

public static Integer getUserId(String jwtString){
    try {
        Claims claims = Jwts.parser()
                      //设置数字签名
                .setSigningKey(PublicConstant.JWT_SIGN_KEY)
                .parseClaimsJws(jwtString)
                .getBody();
        int id = Integer.parseInt(claims.getId());
        String subject = claims.getSubject();
        //校验应用名
        if(!subject.equals(PublicConstant.appName)){
            return null;
        }
        return id;
    }catch (Exception e){
        e.printStackTrace();
        Console.error("checkJwt","JWT 解析失败",jwtString,e.getMessage());
        return null;
    }
}

然后在 RequestUtils.java 中的 getUrl 方法中添加对 JWT 的处理,代码如下:

public static String getUrl(Map<String,String> params,HttpServletRequest request){
    //从 JWT 中解析出 UserId
    Integer userId = JwtUtils.getUserId(request.getHeader(RequestConfig.TOKEN));
    ... ...
    //请求参数中添加 userId
builder.append(PublicConstant.USER_ID_KEY).append("=").append(userId).append("&");
    if(params == null){
        return builder.toString();
    }
     for(String key :params.keySet()){
            //过滤校请求中的 userId
            if(PublicConstant.USER_ID_KEY.equals(key)){
                continue;
            }
           ... ...
        }
    ... ...
}

这样就实现了所有走代理类 ApiController.java 和 FormController.java 访问的请求都会从请求中获取到 UserId 并添加到请求接口的参数中。

创建实体类和数据表

用户名片的实体类根据数据库设计中的 user_card 表的设计进行设计,并使用通用 Mapper 生成数据表。

用户名片实体类 UserCard.java 代码如下:

@TableAttribute(name = "user_card",comment = "用户名片表")
public class UserCard extends BaseEntity {

    @FieldAttribute("用户ID")
    @KeyAttribute
    private int userId;

    @FieldAttribute(value = "昵称",length = 50)
    private String nickName;

    @FieldAttribute(value = "名片上要展示的邮箱地址",length = 100)
    private String email;

    @FieldAttribute(value = "用户头像最初的文件名",length = 100)
    private String portraitOriginName;

    @FieldAttribute(value = "用户头像名称",length = 100)
    private String portraitName;

        ... ...

    private String protraitUrl;

    private String protraitThumUrl;
      ... ...
}

数据持久层直接继承 BaseMapper 接口就可以,如:

@Mapper
public interface UserCardMapper extends BaseMapper<UserCard> {
}

新建测试类,调用通用 Mapper 中的 baseCreate 方法即可生成 user_card 表:

@Resource
private UserCardMapper userCardMapper;

@Test
public void createUserCardTable(){
    userCardMapper.baseCreate(new UserCard());
}
设置名片

根据接口文档中接口 2001 的说明,设置名片的接口做如下设计:

接口类 UserCardControler.java 主要负责接收用户请求,对请求参数进行格式校验,校验完毕后交由业务逻辑类 UserCardService.java 进行处理,代码如下:

@RestController
@RequestMapping("/userCard")
public class UserCardController {
    @Resource
    private UserCardService service;
      /**
       * 设置用户名片接口 
       */
    @PostMapping
    public ResponseEntity setUserCard(Integer kellerUserId,String nickName,String email,String label){
        if(kellerUserId != null && StringUtils.hasValue(nickName,email,label)){
            return Response.ok(
                    service.setUserCard(kellerUserId,nickName,email,label));
        }
        return Response.badRequest();
    }
}

业务逻辑类 UserCardService.java,负责实际业务处理,代码如下:

@Service
public class UserCardService {
    @Resource
    private UserCardMapper mapper;

    /**
     * 设置用户名片,只设置有值的字段,当用户名片不存在时创建名片
     */
    public ResultData setUserCard(int userId,String nickName,String email,String label){
        UserCard userCard = new UserCard(userId);
        UserCard result = mapper.baseSelectByKey(userCard);
        //标示是否是更新操作
        boolean update = true;
        //如果没查到用户名片,新建一个用户名片
        if(result == null){
            result = userCard;
            update = false;
        }
        result.setNickName(nickName);
        result.setEmail(email);
        result.setLabel(label);
        if(update){
            mapper.baseUpdateByKey(result);
        }else {
            mapper.baseInsert(result);
        }
        return ResultData.success();
    }
}

在用户名片设置时要注意:

  • 假设规则:用户可以选择设置名片中的一个信息或多个信息;用户不可以将其中一个信息修改为空。
  • 因为通用 Mapper 的 baseUpateByKey 和 baseUpdateById 方法已经实现了空值检测,在 UserCardService 类中的 setUserCard 方法中在更新时就可以不用再依次判断每一项是否为空了。这种用法在项目中会普遍使用,以后就不做特殊说明了。
上传头像

设置好用户昵称、个性签名等信息后,就差上传用户头像了。上传图片、文件等操作和普通的 GET、POST 请求不同点在于:需要处理的数据是文件流。新建一个接口类 UploadController.java 专门处理文件上传操作:

@RestController
@RequestMapping("/upload")
@CrossOrigin(origins = "*",allowedHeaders="*", maxAge = 3600)
public class UploadController {

    @Resource
    private UserCardService userCardService;

      //上传头像
    @PostMapping
    public ResponseEntity upload(MultipartFile file, HttpServletRequest request){
          //如果读取不到文件流,返回 400
        if(file == null){
            return Response.badRequest();
        }
          // 获取 JWT
        String token = request.getHeader(RequestConfig.TOKEN);
          //解析出 userId
        Integer userId = JwtUtils.getUserId(token);
        if(userId == null){
            return Response.unauthorized();
        }
        ResultData resultData = userCardService.setPortrait(file,userId);
        return Response.ok(resultData);
    }
}

UserCardService.java 中处理头像信息,处理逻辑为:

  • 获取到原文件名和文件后缀名
  • 获取当前时间戳作为新的文件名
  • 保存原图和缩略图,格式分别为:文件名.后缀名thum 文件名.后缀名
  • 将原文件名和新的文件名都保存在数据库中

实现代码如下:

public ResultData setPortrait(MultipartFile file,int userId){
      //获取时间戳
    String timeMask = DateUtils.getTimeMask();
      //获取原始文件名
    String originalFilename = file.getOriginalFilename();
    String fileName;
      //保存原图和缩略图,保存成功后返回新的文件名
    try {
        fileName = UploadUtils.saveImgAndThum(file,timeMask);
    }catch (Exception e){
        e.printStackTrace();
        return ResultData.error("头像保存失败");
    }
    UserCard userCard = new UserCard(userId);
    userCard.setPortraitOriginName(originalFilename);
    userCard.setPortraitName(fileName);
      //修改用户名片
    int result = mapper.baseUpdateByKey(userCard);
    if(result > 0){
        return ResultData.success();
    }
    return ResultData.error("用户名片不存在");
}

其中用到的保存原图和缩略图的代码如下:

public static String saveImgAndThum (MultipartFile file,String name) throws IOException{
    //解析文件后缀名
    String suffix = FileUtils.getSuffixWithSpilt(file.getOriginalFilename());
    //构建原图保存路径
    String fileName = name + suffix;
    //保存原图
    File img = new File(FileUtils.getImgPath(fileName));
    file.transferTo(img);

    //保存缩略图
    File thum = new File(FileUtils.getThumPath(fileName));
    Thumbnails.of(img).size(PublicConstant.THUM_MAX_WIDTH,PublicConstant.THUM_MAX_HEIGHT).toFile(thum);

    return fileName;
}
获取名片

UserCardController:

@GetMapping
public ResponseEntity getUserCard(Integer kellerUserId){
    if(kellerUserId != null){
        return Response.ok(service.getUserCard(kellerUserId));
    }
    return Response.badRequest();
}

UserCardService.java:

public ResultData getUserCard(int userId){
    UserCard userCard = new UserCard(userId);
    userCard = mapper.baseSelectByKey(userCard);
    if(userCard == null){
        return ResultData.error("用户名片不存在");
    }
    String imgName = userCard.getPortraitName();
    if(StringUtils.isNotEmpty(imgName)){
        userCard.setProtraitUrl(FileUtils.getImgUrl(imgName));
        userCard.setProtraitThumUrl(FileUtils.getThumUrl(imgName));
    }

    return ResultData.success(userCard);
}

效果测试

设置用户名片,需要先登录,获取到 JWT:

设置用户名片Header

在 Body 中传递参数,选择 JSON 格式:

设置用户名片Body

获取用户名片,也需要登录,返回的数据中有原图和缩略图的访问地址:

获取用户名片

源码地址

本篇完整的源码地址如下:

https://github.com/tianlanlandelan/KellerNotes/tree/master/15.用户名片1

小结

本篇从用户名片功能功能分析开始,带领大家完成流程分析、方案设计、功能实现。同时结合了前面用到的 JWT 实现了用户身份的校验,使用 Thumbnailator 完成了图片的压缩功能。

建议大家在学习完本篇之后,深入思考一下本篇内容还有什么可以优化的,如:更换头像时删除原来的头像、MD5 实现图片去重等。本项目只用到了 Thumbnailator 的图片压缩功能,大家可以写几个例子体验一下 Thumbnailator 的旋转、裁剪、转码、加水印等其他功能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值