文章目录
- 图片模块
- 一、需求分析
- 二、方案设计
- 文件上传下载的实现
- 对象存储
- 创建图片的流程
- 解析图片信息
- 三、后端开发
- COS存储桶准备
- 通用类 manager - CosManager
- 文件上传
- 文件上传测试
- 文件下载
- 服务端文件下载代码demo
- picture table 逆向工程
- 图片上传
- 1.数据模型
- 2.通用文件上传服务
- 3.图片上传服务
- 接口
- 实现类
- 4.Controller
- 5.测试
- 扩展
- 图片管理
- 1.数据模型
- 2.服务开发
- 3.Controller
- 四、模块开发细节
-
上传创建图片
-
图片信息编辑(标签/分类等)
-
管理图片
-
查看 / 搜索 图片列表
-
查看图片详情
-
图片下载
-
用户上传创建图片
-
审核图片
-
导入图片(URL导入,批量抓取和创建)
-
图片查询优化
-
图片上传优化
-
图片加载优化
-
图片存储
图片模块
一、需求分析
优先确保用户能够查看图片功能的实现,上传功能暂时仅限管理员使用,保证系统的安全和稳定
管理员
-
图片上传与创建,支持本地图片上传,填写相关信息,名称、简介、标签、分类等。系统自动解析图片的基础信息,便于检索。
-
图片管理,管理员对图片信息进行编辑,例如修改名称、简洁、标签、分类等。
-
图片修改
用户
- 查看与搜索:用户可以按照关键词搜索图片,并支持按分类、标签筛选条件分页查看图片列表。
- 查看图片详情:用户点击列表中的图片后,可进入详情页,查看图片的大图及相关信息,如名称、简介、分类、标签、其他图片信息(如宽高和格式等)。
- 图片下载:用户在详情页可以点击 下载图片按钮,将图片保存本地。
二、方案设计
文件上传下载的实现
最简单的方式就是上传到后端服务器,用Java自带文件读写API就能实现。但是这种方式存在缺点:
- 不利于扩展:单个服务器的存储是有限的
- 不利于迁移
- 不安全
- 不利于管理
除了存储一些需要清理的临时文件,通常不会将用户的文件直接上传到服务器,更推荐用专业的第三方存储服务,最常用的就是对象存储。
对象存储
对象存储是一种存储海量文件的分布式存储服务,具有高扩展、低成本、可靠安全等优点。
例如Minio,AmazonS3,OSS,COS
创建图片的流程
上传图片文件+补充图片信息并保存到数据库中
- 先上传再提交数据:用户上传图片,系统生成图片存储的url;然后在用户填写其他相关信息并提交后,才保存图片记录到数据库中。
- 上传图片时保存记录:用户上传图片后,系统立即生成图片的完整数据记录,包括url和其他元数据,无需等待用户点击提交,图片信息立刻存储数据库,之后用户填写其他信息,相当于编辑了已有图片记录的信息。
第一种方法,如果用户取消提交会有存储残留。
第二种方法比较稳健,可以对图片溯源,并对用户上传做安全保障
解析图片信息
主流获取图片信息的方法主要由2种:
1.在后端服务器直接处理图片,比如Java库ImageIO、Python库pillow,还有更成熟的专业图像处理库OpenCV等。
2.通过第三方服务提取图片元数据。这里我们用腾讯的数据万象
三、后端开发
COS存储桶准备
配置
@Configuration
@ConfigurationProperties(prefix = "cos.client")
@Data
public class CosClientConfig { /** * 域名 */ private String host; /** * secretId */ private String secretId; /** * 密钥(注意不要泄露) */ private String secretKey; /** * 区域 */ private String region; /** * 桶名 */ private String bucket;@Beanpublic COSClient cosClient() {// 1 初始化用户身份信息(secretId, secretKey)。// SECRETID 和 SECRETKEY 请登录访问管理控制台 https://console.cloud.tencent.com/cam/capi 进行查看和管理COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);// 2 设置 bucket 的地域, COS 地域的简称请参见 https://cloud.tencent.com/document/product/436/6224// clientConfig 中包含了设置 region, https(默认 http), 超时, 代理等 set 方法, 使用可参见源码或者常见问题 Java SDK 部分。ClientConfig clientConfig = new ClientConfig(new Region(region));// 这里建议设置使用 https 协议// 从 5.6.54 版本开始,默认使用了 httpsclientConfig.setHttpProtocol(HttpProtocol.https);// 3 生成 cos 客户端。return new COSClient(cred, clientConfig);}
}
# 对象存储配置(需要从腾讯云获取)
cos: client: host: xxx secretId: xxx secretKey: xxx region: xxx bucket: xxx
通用类 manager - CosManager
@Component
public class CosManager { @Resource private CosClientConfig cosClientConfig; @Resource private COSClient cosClient; // ... 一些操作 COS 的方法
}
文件上传
public PutObjectResult putObject(String key,File file){PutObjectResult putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(),key,file);return cosClient.putObject(putObjectRequest);
}
/** * 上传对象(附带图片信息) * * @param key 唯一键 * @param file 文件 */
public PutObjectResult putPictureObject(String key, File file) { PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key, file); // 对图片进行处理(获取基本信息也被视作为一种处理) PicOperations picOperations = new PicOperations(); // 1 表示返回原图信息 picOperations.setIsPicInfo(1); // 构造处理参数 putObjectRequest.setPicOperations(picOperations); return cosClient.putObject(putObjectRequest);
}
文件上传测试
@PostMapping("/test/upload")
public BaseResponse<String> testUploadFile(@RequestPart("file")MutipartFile multipartFile){//dirString filename = multipart.getOriginalFilename();String filepath = String.format("/test/%s",filename);File file = null;try{file = File.createTempFile(filepath,null);mutipartFile.transferTo(file);cosManager.putObject(filepath,file);return ResultUtils.success(filepath);}catch(Exception e){throw}finally{if(file!=null){file.delete()}}}
文件下载
COS介绍了2种文件下载方式。一种是直接下载COS文件到后端服务器(适合服务端处理文件),另一种是获取文件下载输入流(适合返回给前端用户)。
还可以通过URL路径访问,适用于单一的、可公开的资源,如用户头像,公开图片。
对于安全性要求高的场景,可以先通过后端进行权限校验,然后从COS下载文件到服务器,再返回给前端,这样可以在后端限制只有登录用户才能下载。
也可以后端先进行权限校验,然后返回给前端一个临时密钥,之后前端可以凭借该密钥直接从对象存储下载,不用经过服务端中转,性能更高。
当前的图片都是公开的,直接通过URL进行访问即可。(这里的负载就交给COS了)
服务端文件下载代码demo
/** * 下载对象 * * @param key 唯一键 */
public COSObject getObject(String key) { GetObjectRequest getObjectRequest = new GetObjectRequest(cosClientConfig.getBucket(), key); return cosClient.getObject(getObjectRequest);
}
@GetMapping("/test/download")
public void testDownloadFile(String filepath,HttpServletResponse response)
{COSObjectInputStream cosObjectInput = null;try{cosManager.getObject(filepath).var;cosObjectInput = cosObject.getObjectContent();IOUtils.toByteArray(cosObjectInput).var;response.setContentType();response.setHeader();response.getOutPutStream().write(bytes);response.getOutPutStream().flush();}catch{}finally{if(cosObjectInput!=null){cosObjectInput.close();}}
}
picture table 逆向工程
优化picture 实体类
@TableName(value ="picture")
@Data
public class Picture implements Serializable { /** * id */ @TableId(type = IdType.ASSIGN_ID) private Long id; /** * 图片 url */ private String url; /** * 图片名称 */ private String name; /** * 简介 */ private String introduction; /** * 分类 */ private String category; /** * 标签(JSON 数组) */ private String tags; /** * 图片体积 */ private Long picSize; /** * 图片宽度 */ private Integer picWidth; /** * 图片高度 */ private Integer picHeight; /** * 图片宽高比例 */ private Double picScale; /** * 图片格式 */ private String picFormat; /** * 创建用户 id */ private Long userId; /** * 创建时间 */ private Date createTime; /** * 编辑时间 */ private Date editTime; /** * 更新时间 */ private Date updateTime; /** * 是否删除 */ @TableLogic private Integer isDelete; @TableField(exist = false) private static final long serialVersionUID = 1L;
}
图片上传
1.数据模型
在dto下新建用于请求参数的类。由于图片需要支持重复上传(信息不变,只改变图片文件),所以要添加图片id参数:
@Data
public class PictureUploadRequest implements Serializable { /** * 图片 id(用于修改) */ private Long id; private static final long serialVersionUID = 1L;
}
在vo下新建视图类,可以额外关联用户信息。还可以编写实体类和视图类快速转换方法,便于后续快速传值。
@Data
public class PictureVO implements Serializable { /** * id */ private Long id; /** * 图片 url */ private String url; /** * 图片名称 */ private String name; /** * 简介 */ private String introduction; /** * 标签 */ private List<String> tags; /** * 分类 */ private String category; /** * 文件体积 */ private Long picSize; /** * 图片宽度 */ private Integer picWidth; /** * 图片高度 */ private Integer picHeight; /** * 图片比例 */ private Double picScale; /** * 图片格式 */ private String picFormat; /** * 用户 id */ private Long userId; /** * 创建时间 */ private Date createTime; /** * 编辑时间 */ private Date editTime; /** * 更新时间 */ private Date updateTime; /** * 创建用户信息 */ private UserVO user; private static final long serialVersionUID = 1L; /** * 封装类转对象 */ public static Picture voToObj(PictureVO pictureVO) { if (pictureVO == null) { return null; } Picture picture = new Picture(); BeanUtils.copyProperties(pictureVO, picture); // 类型不同,需要转换 picture.setTags(JSONUtil.toJsonStr(pictureVO.getTags())); return picture; } /** * 对象转封装类 */ public static PictureVO objToVo(Picture picture) { if (picture == null) { return null; } PictureVO pictureVO = new PictureVO(); BeanUtils.copyProperties(picture, pictureVO); // 类型不同,需要转换 pictureVO.setTags(JSONUtil.toList(picture.getTags(), String.class)); return pictureVO; }
}
2.通用文件上传服务
虽然以及编写过通用对象存储操作类,但是还不够
问题:
- 图片校验
- 上传路径指定
- 解析图片:数据万象服务
FileManager / FileService
1.由于文件校验规则较复杂,单独抽象为validPicture方法,对文件大小、类型校验。
2.文件上传时,会先在本地创建临时文件,无论是否创建成功,最后都要删除临时文件,否则会导致资源泄露。
3.可以跟自己的需求定义文件上传地址,此处添加了上传日期和UUID随机数,便于了解上传时间并防止文件重复(一般处理)。预留了一个uploadPathPrefix参数,由调用方指定上传文件到哪个目录。
4.不建议存储桶共享
public class FileService{@Resourceprivate CosClientConfig cosClientConfig;@Resourceprivate CosManager cosManager;/** * 上传图片 * * @param multipartFile 文件 * @param uploadPathPrefix 上传路径前缀 * @return */
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); }
} /** * 校验文件 * * @param multipartFile multipart 文件 */
public void validPicture(MultipartFile multipartFile) { ThrowUtils.throwIf(multipartFile == null, ErrorCode.PARAMS_ERROR, "文件不能为空"); // 1. 校验文件大小 long fileSize = multipartFile.getSize(); final long ONE_M = 1024 * 1024L; ThrowUtils.throwIf(fileSize > 2 * ONE_M, ErrorCode.PARAMS_ERROR, "文件大小不能超过 2M"); // 2. 校验文件后缀 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()); }
}}
图片解析包装类:
@Data
public class UploadPictureResult { /** * 图片地址 */ private String url; /** * 图片名称 */ private String picName; /** * 文件体积 */ private Long picSize; /** * 图片宽度 */ private int picWidth; /** * 图片高度 */ private int picHeight; /** * 图片宽高比 */ private Double picScale; /** * 图片格式 */ private String picFormat; }
3.图片上传服务
接口
/** * 上传图片 * * @param multipartFile * @param pictureUploadRequest * @param loginUser * @return */
PictureVO uploadPicture(MultipartFile multipartFile, PictureUploadRequest pictureUploadRequest, User loginUser);
实现类
1.我们将所有图片都放到了public 目录下,并且为每个用户的图片存储到public/uid 的目录下
2.如果pid不为空,表示更新已有图片信息,需要判断图片是否存在,并且更新的时候要指定editTime编辑时间。可以调用Mybatis Plus 提供的saveOrUpdate方法兼容创建和更新操作
@Override
public PictureVO uploadPicture(MultipartFile multipartFile, PictureUploadRequest pictureUploadRequest, User loginUser) { ThrowUtils.throwIf(loginUser == null, ErrorCode.NO_AUTH_ERROR); Long pictureId = null;if(pictureUploadRequest!=null){pictureId=pictureUploadRequest.getId();}if(pictureId!=null){pictureMapper.lambdaQuery().eq(Picture::getId,pictureId).exists();Throwif()}String.format("public/%s",loginUser.getId()).var; // mkdir by uidfileManager.uploadPicutre(mutipartFile,uploadPrefix).var;new Picture().var;set{url,name,size,width,height,scale,format,uid}if(picutreId!=null){set{pictureid,edittime}}mapper.saveOrUpdate()ThrowIf()return PictureVO.objToVo(picture)
}
4.Controller
/** * 上传图片(可重新上传) */
@PostMapping("/upload")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<PictureVO> uploadPicture( @RequestPart("file") MultipartFile multipartFile, PictureUploadRequest pictureUploadRequest, HttpServletRequest request) { User loginUser = userService.getLoginUser(request); PictureVO pictureVO = pictureService.uploadPicture(multipartFile, pictureUploadRequest, loginUser); return ResultUtils.success(pictureVO);
}
5.测试
当上传图片过大时侯,会触发报错,SpringBoot内嵌tomcat默认限制了请求中文件上传的大小。
spring: # 开放更大的文件上传体积 servlet: multipart: max-file-size: 10MB
扩展
1.用枚举类 支持根据业务场景区分文件上传路径、校验规则等,从而复用FileManager。
2.目前文件上传的时候,会先在本地创建临时文件。如果不需要对文件进行额外处理、进一步提高性能,可以直接用流的方式将请求中的文件上传到COS。
3.补充更严格的校验,例如为支持的图片格式定义枚举,仅允许上传枚举定义的格式。
代码demo:
// 上传文件
public static String uploadToCOS(MultipartFile multipartFile, String bucketName, String key) throws Exception { // 创建 COS 客户端 COSClient cosClient = createCOSClient(); try (InputStream inputStream = multipartFile.getInputStream()) { // 元信息配置 ObjectMetadata metadata = new ObjectMetadata(); metadata.setContentLength(multipartFile.getSize()); metadata.setContentType(multipartFile.getContentType()); // 创建上传请求 PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key, inputStream, metadata); // 上传文件 cosClient.putObject(putObjectRequest); // 生成访问链接 return "https://" + bucketName + ".cos." + cosClient.getClientConfig().getRegion().getRegionName() + ".myqcloud.com/" + key; } finally { cosClient.shutdown(); }
}
图片管理
- 管理员 deleteById
- 管理员 update
- 管理员 page
- 管理员 getById
- page desen
- getById desen
- update picture
1.数据模型
每一个请求都需要一个dto
图片更新请求,给管理员使用,tags类型改为List< String>便于前端上传
@Data
public class PictureUpdateRequest implements Serializable { /** * id */ private Long id; /** * 图片名称 */ private String name; /** * 简介 */ private String introduction; /** * 分类 */ private String category; /** * 标签 */ private List<String> tags; private static final long serialVersionUID = 1L;
}
图片修改
@Data
public class PictureEditRequest implements Serializable { /** * id */ private Long id; /** * 图片名称 */ private String name; /** * 简介 */ private String introduction; /** * 分类 */ private String category; /** * 标签 */ private List<String> tags; private static final long serialVersionUID = 1L;
}
图片查询
@EqualsAndHashCode(callSuper = true)
@Data
public class PictureQueryRequest extends PageRequest implements Serializable { /** * id */ private Long id; /** * 图片名称 */ private String name; /** * 简介 */ private String introduction; /** * 分类 */ private String category; /** * 标签 */ private List<String> tags; /** * 文件体积 */ private Long picSize; /** * 图片宽度 */ private Integer picWidth; /** * 图片高度 */ private Integer picHeight; /** * 图片比例 */ private Double picScale; /** * 图片格式 */ private String picFormat; /** * 搜索词(同时搜名称、简介等) */ private String searchText; /** * 用户 id */ private Long userId; private static final long serialVersionUID = 1L;
}
2.服务开发
1.UserService编写判断用户是否为管理员的方法
/** * 是否为管理员 * * @param user * @return */
boolean isAdmin(User user);
@Override
public boolean isAdmin(User user) { return user != null && UserRoleEnum.ADMIN.getValue().equals(user.getUserRole());
}
查询包装类转换
@Override
public QueryWrapper<Picture> getQueryWrapper(PictureQueryRequest pictureQueryRequest) { QueryWrapper<Picture> queryWrapper = new QueryWrapper<>(); if (pictureQueryRequest == null) { return queryWrapper; } // 从对象中取值 Long id = pictureQueryRequest.getId(); String name = pictureQueryRequest.getName(); String introduction = pictureQueryRequest.getIntroduction(); String category = pictureQueryRequest.getCategory(); List<String> tags = pictureQueryRequest.getTags(); Long picSize = pictureQueryRequest.getPicSize(); Integer picWidth = pictureQueryRequest.getPicWidth(); Integer picHeight = pictureQueryRequest.getPicHeight(); Double picScale = pictureQueryRequest.getPicScale(); String picFormat = pictureQueryRequest.getPicFormat(); String searchText = pictureQueryRequest.getSearchText(); Long userId = pictureQueryRequest.getUserId(); String sortField = pictureQueryRequest.getSortField(); String sortOrder = pictureQueryRequest.getSortOrder(); // 从多字段中搜索 if (StrUtil.isNotBlank(searchText)) { // 需要拼接查询条件 queryWrapper.and(qw -> qw.like("name", searchText) .or() .like("introduction", searchText) ); } queryWrapper.eq(ObjUtil.isNotEmpty(id), "id", id); queryWrapper.eq(ObjUtil.isNotEmpty(userId), "userId", userId); queryWrapper.like(StrUtil.isNotBlank(name), "name", name); queryWrapper.like(StrUtil.isNotBlank(introduction), "introduction", introduction); queryWrapper.like(StrUtil.isNotBlank(picFormat), "picFormat", picFormat); queryWrapper.eq(StrUtil.isNotBlank(category), "category", category); queryWrapper.eq(ObjUtil.isNotEmpty(picWidth), "picWidth", picWidth); queryWrapper.eq(ObjUtil.isNotEmpty(picHeight), "picHeight", picHeight); queryWrapper.eq(ObjUtil.isNotEmpty(picSize), "picSize", picSize); queryWrapper.eq(ObjUtil.isNotEmpty(picScale), "picScale", picScale); // JSON 数组查询 if (CollUtil.isNotEmpty(tags)) { for (String tag : tags) { queryWrapper.like("tags", "\"" + tag + "\""); } } // 排序 queryWrapper.orderBy(StrUtil.isNotEmpty(sortField), sortOrder.equals("ascend"), sortField); return queryWrapper;
}
获取图片封装类相关方法
@Override
public PictureVO getPictureVO(Picture picture, HttpServletRequest request) { // 对象转封装类 PictureVO pictureVO = PictureVO.objToVo(picture); // 关联查询用户信息 Long userId = picture.getUserId(); if (userId != null && userId > 0) { User user = userService.getById(userId); UserVO userVO = userService.getUserVO(user); pictureVO.setUser(userVO); } return pictureVO;
}
先获取到要查询的用户id列表,只发送一次查询用户表的请求,再将查询到的值设置到图片对象中。
/** * 分页获取图片封装 */
@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()); // 1. 关联查询用户信息 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)); // 2. 填充信息 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;
}
图片数据校验
@Override
public void validPicture(Picture picture) { ThrowUtils.throwIf(picture == null, ErrorCode.PARAMS_ERROR); // 从对象中取值 Long id = picture.getId(); String url = picture.getUrl(); String introduction = picture.getIntroduction(); // 修改数据时,id 不能为空,有参数则校验 ThrowUtils.throwIf(ObjUtil.isNull(id), ErrorCode.PARAMS_ERROR, "id 不能为空"); if (StrUtil.isNotBlank(url)) { ThrowUtils.throwIf(url.length() > 1024, ErrorCode.PARAMS_ERROR, "url 过长"); } if (StrUtil.isNotBlank(introduction)) { ThrowUtils.throwIf(introduction.length() > 800, ErrorCode.PARAMS_ERROR, "简介过长"); }
}
3.Controller
/** * 删除图片 */
@PostMapping("/delete")
public BaseResponse<Boolean> deletePicture(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) { if (deleteRequest == null || deleteRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } User loginUser = userService.getLoginUser(request); long id = deleteRequest.getId(); // 判断是否存在 Picture oldPicture = pictureService.getById(id); ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR); // 仅本人或管理员可删除 if (!oldPicture.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } // 操作数据库 boolean result = pictureService.removeById(id); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); return ResultUtils.success(true);
} /** * 更新图片(仅管理员可用) */
@PostMapping("/update")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Boolean> updatePicture(@RequestBody PictureUpdateRequest pictureUpdateRequest) { if (pictureUpdateRequest == null || pictureUpdateRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } // 将实体类和 DTO 进行转换 Picture picture = new Picture(); BeanUtils.copyProperties(pictureUpdateRequest, picture); // 注意将 list 转为 string picture.setTags(JSONUtil.toJsonStr(pictureUpdateRequest.getTags())); // 数据校验 pictureService.validPicture(picture); // 判断是否存在 long id = pictureUpdateRequest.getId(); Picture oldPicture = pictureService.getById(id); ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR); // 操作数据库 boolean result = pictureService.updateById(picture); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); return ResultUtils.success(true);
} /** * 根据 id 获取图片(仅管理员可用) */
@GetMapping("/get")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Picture> getPictureById(long id, HttpServletRequest request) { ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR); // 查询数据库 Picture picture = pictureService.getById(id); ThrowUtils.throwIf(picture == null, ErrorCode.NOT_FOUND_ERROR); // 获取封装类 return ResultUtils.success(picture);
} /** * 根据 id 获取图片(封装类) */
@GetMapping("/get/vo")
public BaseResponse<PictureVO> getPictureVOById(long id, HttpServletRequest request) { ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR); // 查询数据库 Picture picture = pictureService.getById(id); ThrowUtils.throwIf(picture == null, ErrorCode.NOT_FOUND_ERROR); // 获取封装类 return ResultUtils.success(pictureService.getPictureVO(picture, request));
} /** * 分页获取图片列表(仅管理员可用) */
@PostMapping("/list/page")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Page<Picture>> listPictureByPage(@RequestBody PictureQueryRequest pictureQueryRequest) { long current = pictureQueryRequest.getCurrent(); long size = pictureQueryRequest.getPageSize(); // 查询数据库 Page<Picture> picturePage = pictureService.page(new Page<>(current, size), pictureService.getQueryWrapper(pictureQueryRequest)); return ResultUtils.success(picturePage);
} /** * 分页获取图片列表(封装类) */
@PostMapping("/list/page/vo")
public BaseResponse<Page<PictureVO>> listPictureVOByPage(@RequestBody PictureQueryRequest pictureQueryRequest, HttpServletRequest request) { long current = pictureQueryRequest.getCurrent(); long size = pictureQueryRequest.getPageSize(); // 限制爬虫 ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR); // 查询数据库 Page<Picture> picturePage = pictureService.page(new Page<>(current, size), pictureService.getQueryWrapper(pictureQueryRequest)); // 获取封装类 return ResultUtils.success(pictureService.getPictureVOPage(picturePage, request));
} /** * 编辑图片(给用户使用) */
@PostMapping("/edit")
public BaseResponse<Boolean> editPicture(@RequestBody PictureEditRequest pictureEditRequest, HttpServletRequest request) { if (pictureEditRequest == null || pictureEditRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } // 在此处将实体类和 DTO 进行转换 Picture picture = new Picture(); BeanUtils.copyProperties(pictureEditRequest, picture); // 注意将 list 转为 string picture.setTags(JSONUtil.toJsonStr(pictureEditRequest.getTags())); // 设置编辑时间 picture.setEditTime(new Date()); // 数据校验 pictureService.validPicture(picture); User loginUser = userService.getLoginUser(request); // 判断是否存在 long id = pictureEditRequest.getId(); Picture oldPicture = pictureService.getById(id); ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR); // 仅本人或管理员可编辑 if (!oldPicture.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } // 操作数据库 boolean result = pictureService.updateById(picture); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); return ResultUtils.success(true);
}
4.获取预置标签和分类
随着系统规模和数据不断扩大,可以再更改为使用配置中心或数据库动态管理这些数据,或者通过定时任务计算出热门图片分类和标签
@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);
}
