本部分内容主要来源于鱼皮智能协图云图库部分,并在笔者个人项目学习的基础上进行扩展衍生。
本地存储
直接将文件保存在本地服务器的磁盘目录中
java
package com.itheima.controller;
import com.itheima.pojo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.util.UUID;
@Slf4j
@RestController
public class UploadController {
private static final String UPLOAD_DIR = "D:/images/";
/**
* 上传文件 - 参数名file
*/
@PostMapping("/upload")
public Result upload(MultipartFile file) throws Exception {
log.info("上传文件:{}, {}, {}", username, age, file);
if (!file.isEmpty()) {
// 生成唯一文件名
String originalFilename = file.getOriginalFilename();
//获取扩展名
String extName = originalFilename.substring(originalFilename.lastIndexOf("."));
String uniqueFileName = UUID.randomUUID().toString().replace("-", "") + extName;
// 拼接完整的文件路径
File targetFile = new File(UPLOAD_DIR + uniqueFileName);
// 如果目标目录不存在,则创建它
if (!targetFile.getParentFile().exists()) {
targetFile.getParentFile().mkdirs();
}
// 保存文件
file.transferTo(targetFile);
}
return Result.success();
}
}
要点:
MultipartFile,用来接收上传的文件
MultipartFile 常见方法:
-
String getOriginalFilename();//获取原始文件名 -
void transferTo(File dest);``//将接收的文件转存到磁盘文件中 -
long getSize();//获取文件的大小,单位:字节 -
byte[] getBytes();``//获取文件内容的字节数组 -
InputStream getInputStream();//获取接收到的文件内容的输入流
originalFilename.lastIndexOf("."):找到原始文件名中最后一个点(.)的位置
改变 Tomcat 服务器默认限制的文件上传大小:
java
spring:
servlet:
multipart:
max-file-size: 10MB
优缺点:
优点:方便快捷
缺点:
-
不安全:磁盘如果损坏,所有的文件就会丢失1
-
容量有限:如果存储大量的图片,磁盘空间有限(磁盘不可能无限制扩容)
-
无法直接访问
自己搭建存储服务器
使用fastDFS 、MinIO等。等我后续使用了再补充。
使用现成云服务
在黑马javaweb中使用了阿里云oss,而鱼皮这里使用的是腾讯云。实际上操作都大同小异,因此这里只详细讲解使用腾讯云的过程。
使用流程
新建腾讯云账号--开通腾讯云对象存储--创建存储桶(Bucket)--开通数据万象(后续解析图片)--后端代码操作对象(SDK或API)
后端代码操作对象
引入依赖
bash
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
<version>5.6.227</version>
</dependency>
CosClientConfig 类
java
@Configuration
@ConfigurationProperties(prefix = "cos.client")
@Data
public class CosClientConfig {
private String host;
private String secretId;
private String secretKey;
private String region;
private String bucket;
@Bean
public COSClient cosClient() {
COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);
ClientConfig clientConfig = new ClientConfig(new Region(region));
return new COSClient(cred, clientConfig);
}
}
读取配置,创建COS客户端Bean类
@Value适合单个零散配置,@ConfigurationProperties适合批量绑定一组前缀相同的配置,更适合配置类场景。都是用来读取.yml文件的配置信息。
配置文件填写
java
cos:
client:
host: xxx
secretId: xxx
secretKey: xxx
region: xxx
bucket: xxx
保存在local配置文件中,在.gitignore中忽略该文件的提交
编写通用能力类
创建CosManager类,编写文件的上传与下载操作
文件上传
java
@Component
public class CosManager {
@Resource
private CosClientConfig cosClientConfig;
@Resource
private COSClient cosClient;
public PutObjectResult putObject(String key, File file) {
PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key,
file);
return cosClient.putObject(putObjectRequest);
}
}
文件上传接口
java
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
@PostMapping("/test/upload")
public BaseResponse<String> testUploadFile(@RequestPart("file") MultipartFile multipartFile) {
String filename = multipartFile.getOriginalFilename();
String filepath = String.format("/test/%s", filename);
File file = null;
try {
file = File.createTempFile(filepath, null);
multipartFile.transferTo(file);
cosManager.putObject(filepath, file);
return ResultUtils.success(filepath);
} catch (Exception e) {
log.error("file upload error, filepath = " + filepath, e);
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "上传失败");
} finally {
if (file != null) {
boolean delete = file.delete();
if (!delete) {
log.error("file delete error, filepath = {}", filepath);
}
}
}
}
要点:
1.创建本地空文件 createTempFile(filepath, null)
2.目标文件转存本地 transferTo(file)
3.传入路径与文件,上传云端
4.删除本地文件
文件下载
1.下载到后端进行处理
2.获取文件下载输入流传给前端
3.获取URL路径访问传给前端
方式1:
java
public COSObject getObject(String key) {
GetObjectRequest getObjectRequest = new GetObjectRequest(cosClientConfig.getBucket(), key);
return cosClient.getObject(getObjectRequest);
}
文件下载接口
java
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
@GetMapping("/test/download/")
public void testDownloadFile(String filepath, HttpServletResponse response) throws IOException {
COSObjectInputStream cosObjectInput = null;
try {
COSObject cosObject = cosManager.getObject(filepath);
cosObjectInput = cosObject.getObjectContent();
byte[] bytes = IOUtils.toByteArray(cosObjectInput);
response.setContentType("application/octet-stream;charset=UTF-8");
response.setHeader("Content-Disposition", "attachment; filename=" + filepath);
response.getOutputStream().write(bytes);
response.getOutputStream().flush();
} catch (Exception e) {
log.error("file download error, filepath = " + filepath, e);
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "下载失败");
} finally {
if (cosObjectInput != null) {
cosObjectInput.close();
}
}
}
要点:
1.通过路径获取文件内容访问入口CosObject
2.获取文件输入流cosObjectInput
3.将文件输入流转换为字节数组
4.通过response的输出流getOutputStream传给前端
5.刷新流,关闭流
文件解析
通过数据万象进行解析,在CosManager添加解析方法
java
public PutObjectResult putPictureObject(String key, File file) {
PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key,
file);
PicOperations picOperations = new PicOperations();
picOperations.setIsPicInfo(1);
putObjectRequest.setPicOperations(picOperations);
return cosClient.putObject(putObjectRequest);
}
不用记,直接看文档
以上即为云存储的基本工作流程。接下来详细讲解鱼皮智能协图云库项目要点。
智能协图云图库
设计库表
sql
create table if not exists picture
(
id bigint auto_increment comment 'id' primary key,
url varchar(512) not null comment '图片 url',
name varchar(128) not null comment '图片名称',
introduction varchar(512) null comment '简介',
category varchar(64) null comment '分类',
tags varchar(512) null comment '标签(JSON 数组)',
picSize bigint null comment '图片体积',
picWidth int null comment '图片宽度',
picHeight int null comment '图片高度',
picScale double null comment '图片宽高比例',
picFormat varchar(32) null comment '图片格式',
userId bigint not null comment '创建用户 id',
createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',
editTime datetime default CURRENT_TIMESTAMP not null comment '编辑时间',
updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
isDelete tinyint default 0 not null comment '是否删除',
INDEX idx_name (name),
INDEX idx_introduction (introduction),
INDEX idx_category (category),
INDEX idx_tags (tags),
INDEX idx_userId (userId)
) comment '图片' collate = utf8mb4_unicode_ci;
生成基础代码
1.通过MybatisX插件生成实体类,Mapper,Service等基础代码。
2.优化基础代码:主键自增->雪花算法(IdType.ASSIGN_ID),逻辑删除(@TableLogic)
接下来遵循一个请求接口对应一个DTO类,对应一个返回VO类进行设计增删改查的过程。
图片上传
1.数据模型,dto与vo设计
dto:PictureUploadRequest包含id,支持重复上传同一id图片的修改上传
vo:PictureVO包含vo转obj与obj转vo的方法。
要点:
简单属性复制--BeanUtils.copyProperties()
复杂属性--picture.setTags(JSONUtil.toJsonStr(pictureVO.getTags()));与 pictureVO.setTags(JSONUtil.toList(picture.getTags(), String.class));
Picture 是 "和数据库打交道的工具",数据库能存什么类型,它的字段就是什么类型;
PictureVO 是 "和前端打交道的工具",前端需要什么类型,它的字段就是什么类型;
数据库无法存储 List 集合,JSON 字符串是数组型标签的最优存储格式(兼顾可读性、查询精准性);
2.编写FileManager
java
public UploadPictureResult uploadPicture(MultipartFile multipartFile, String uploadPathPrefix) {
validPicture(multipartFile);
String uuid = RandomUtil.randomString(16);
String originFilename = multipartFile.getOriginalFilename();
String uploadFilename = String.format("%s_%s.%s", DateUtil.formatDate(new Date()), uuid,
FileUtil.getSuffix(originFilename));
String uploadPath = String.format("/%s/%s", uploadPathPrefix, uploadFilename);
File file = null;
try {
file = File.createTempFile(uploadPath, null);
multipartFile.transferTo(file);
PutObjectResult putObjectResult = cosManager.putPictureObject(uploadPath, file);
ImageInfo imageInfo = putObjectResult.getCiUploadResult().getOriginalInfo().getImageInfo();
UploadPictureResult uploadPictureResult = new UploadPictureResult();
int picWidth = imageInfo.getWidth();
int picHeight = imageInfo.getHeight();
double picScale = NumberUtil.round(picWidth * 1.0 / picHeight, 2).doubleValue();
uploadPictureResult.setPicName(FileUtil.mainName(originFilename));
uploadPictureResult.setPicWidth(picWidth);
uploadPictureResult.setPicHeight(picHeight);
uploadPictureResult.setPicScale(picScale);
uploadPictureResult.setPicFormat(imageInfo.getFormat());
uploadPictureResult.setPicSize(FileUtil.size(file));
uploadPictureResult.setUrl(cosClientConfig.getHost() + "/" + uploadPath);
return uploadPictureResult;
} catch (Exception e) {
log.error("图片上传到对象存储失败", e);
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "上传失败");
} finally {
this.deleteTempFile(file);
}
}
public void validPicture(MultipartFile multipartFile) {
ThrowUtils.throwIf(multipartFile == null, ErrorCode.PARAMS_ERROR, "文件不能为空");
long fileSize = multipartFile.getSize();
final long ONE_M = 1024 * 1024L;
ThrowUtils.throwIf(fileSize > 2 * ONE_M, ErrorCode.PARAMS_ERROR, "文件大小不能超过 2M");
String fileSuffix = FileUtil.getSuffix(multipartFile.getOriginalFilename());
final List<String> ALLOW_FORMAT_LIST = Arrays.asList("jpeg", "jpg", "png", "webp");
ThrowUtils.throwIf(!ALLOW_FORMAT_LIST.contains(fileSuffix), ErrorCode.PARAMS_ERROR, "文件类型错误");
}
public void deleteTempFile(File file) {
if (file == null) {
return;
}
boolean deleteResult = file.delete();
if (!deleteResult) {
log.error("file delete error, filepath = {}", file.getAbsolutePath());
}
}
要点:
1.校验图片是否合规validPicture()
2.获取上传的文件名与路径,getSuffix()获取后缀,
3.上传文件并解析
4.删除上传时生成的本地文件
服务开发
Controller->service->serviceimpl
图片增删改查
编写通过PictureQueryRequest构建QueryWrapper的方法
没有太大难度,这里依旧详细讲解一下分页查询的要点
java
@Override
public Page<PictureVO> getPictureVOPage(Page<Picture> picturePage, HttpServletRequest request) {
List<Picture> pictureList = picturePage.getRecords();
Page<PictureVO> pictureVOPage = new Page<>(picturePage.getCurrent(), picturePage.getSize(), picturePage.getTotal());
if (CollUtil.isEmpty(pictureList)) {
return pictureVOPage;
}
List<PictureVO> pictureVOList = pictureList.stream().map(PictureVO::objToVo).collect(Collectors.toList());
Set<Long> userIdSet = pictureList.stream().map(Picture::getUserId).collect(Collectors.toSet());
Map<Long, List<User>> userIdUserListMap = userService.listByIds(userIdSet).stream()
.collect(Collectors.groupingBy(User::getId));
pictureVOList.forEach(pictureVO -> {
Long userId = pictureVO.getUserId();
User user = null;
if (userIdUserListMap.containsKey(userId)) {
user = userIdUserListMap.get(userId).get(0);
}
pictureVO.setUser(userService.getUserVO(user));
});
pictureVOPage.setRecords(pictureVOList);
return pictureVOPage;
}
1.获取分页对象,对象实体+元信息pictureService.page(),返回Page<T>类型。
2.分页对象转VO对象
(1)通过分页对象获取对象实体getRecords(),返回List<T>。
(2)构建空的Page<T>VO对象,只包含多少条数据页数等元信息
(3)处理对象实体,这里利用set避免重复查询
(4)对象实体添加到空的Page<T>VO对象里
3.返回VO对象
获取预置标签和分类
java
@GetMapping("/tag_category")
public BaseResponse<PictureTagCategory> listPictureTagCategory() {
PictureTagCategory pictureTagCategory = new PictureTagCategory();
List<String> tagList = Arrays.asList("热门", "搞笑", "生活", "高清", "艺术", "校园", "背景", "简历", "创意");
List<String> categoryList = Arrays.asList("模板", "电商", "表情包", "素材", "海报");
pictureTagCategory.setTagList(tagList);
pictureTagCategory.setCategoryList(categoryList);
return ResultUtils.success(pictureTagCategory);
}