3.17 图片存储优化,redis保存session信息,实现持久化登录状态

This commit is contained in:
zhangsan 2025-03-17 19:42:59 +08:00
parent 0fd9ef2e83
commit 1026b764dd
8 changed files with 109 additions and 38 deletions

View File

@ -74,6 +74,11 @@
<artifactId>caffeine</artifactId> <artifactId>caffeine</artifactId>
<version>3.1.8</version> <version>3.1.8</version>
</dependency> </dependency>
<!-- Spring Session + Redis -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId> <artifactId>spring-boot-configuration-processor</artifactId>

View File

@ -61,4 +61,4 @@ CREATE INDEX idx_reviewStatus ON picture (review_status);
ALTER TABLE picture ALTER TABLE picture
-- 添加新列 -- 添加新列
ADD COLUMN original_url varchar(512) NULL COMMENT '原图 url', ADD COLUMN original_url varchar(512) NULL COMMENT '原图 url',
ADD COLUMN thumbnail_url varchar(512) NULL COMMENT '缩略图 url'; ADD COLUMN thumbnail_url varchar(512) NULL COMMENT '缩略图 url';

View File

@ -40,7 +40,8 @@ public class PictureController {
/** /**
* 上传图片可重新上传 * 上传图片可重新上传前端选中图片就会调用该接口无需前端点'创建'按钮
*
*/ */
@PostMapping("/upload") @PostMapping("/upload")
// @AuthCheck(mustRole = UserConstant.ADMIN_ROLE) // @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
@ -79,20 +80,7 @@ public class PictureController {
throw new BusinessException(ErrorCode.PARAMS_ERROR); throw new BusinessException(ErrorCode.PARAMS_ERROR);
} }
User loginUser = userService.getLoginUser(request); User loginUser = userService.getLoginUser(request);
Long id = deleteRequest.getId(); pictureService.deletePicture(deleteRequest.getId(), loginUser);
//判断用户是否存在
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);
//清理图片资源
pictureService.clearPictureFile(oldPicture);
return ResultUtils.success(true); return ResultUtils.success(true);
} }
@ -209,7 +197,7 @@ public class PictureController {
} }
/** /**
* 编辑图片给用户使用 * 编辑图片给用户使用或创建图片时编辑标签分类的时候
*/ */
@PostMapping("/edit") @PostMapping("/edit")
public BaseResponse<Boolean> editPicture(@RequestBody PictureEditRequest pictureEditRequest, HttpServletRequest request) { public BaseResponse<Boolean> editPicture(@RequestBody PictureEditRequest pictureEditRequest, HttpServletRequest request) {

View File

@ -1,15 +1,19 @@
package edu.whut.smilepicturebackend.manager; package edu.whut.smilepicturebackend.manager;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.qcloud.cos.COSClient; import com.qcloud.cos.COSClient;
import com.qcloud.cos.model.*; import com.qcloud.cos.model.*;
import com.qcloud.cos.model.ciModel.persistence.PicOperations; import com.qcloud.cos.model.ciModel.persistence.PicOperations;
import edu.whut.smilepicturebackend.config.CosClientConfig; import edu.whut.smilepicturebackend.config.CosClientConfig;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.io.File; import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -19,6 +23,7 @@ import java.util.List;
*/ */
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j
public class CosManager { public class CosManager {
private final CosClientConfig cosClientConfig; private final CosClientConfig cosClientConfig;
@ -155,11 +160,45 @@ public class CosManager {
/** /**
* 删除对象 * 删除对象 https://cloud.tencent.com/document/product/436/65939
* * @param url
* @param key 唯一键
*/ */
public void deleteObject(String key) { public void deleteIfNotBlank(String url) {
cosClient.deleteObject(cosClientConfig.getBucket(), key); if (StrUtil.isBlank(url)) {
return;
}
try {
// 1. 解析 URL 得到对象 key
String key = extractUploadPath(url);
if (StrUtil.isBlank(key)) {
log.warn("无法从 URL 解析出 COS 对象 key跳过删除: {}", url);
return;
}
// 2. 调用 COS SDK 删除
log.info("清理图片中... {}", key);
cosClient.deleteObject(cosClientConfig.getBucket(), key);
} catch (IllegalArgumentException e) {
// URL 格式非法或解析失败时给出警告不中断业务
log.warn("删除图片失败URL 解析异常url={}", url, e);
}
} }
/**
* 从完整 COS URL 中解析出对象 key uploadPath
*
* @param originalUrl 形如 https://bucket.cos.xxx/my/prefix/file.png
* @return my/prefix/file.png
*/
public static String extractUploadPath(String originalUrl) {
try {
// 1. 直接拿 URI path 部分例如 "//smile-picture/public/.../xxx.png"
String path = new URI(originalUrl).getPath();
// 2. 去掉所有前导斜杠得到真正的 COS 对象 key
return path.replaceFirst("^/+", ""); // => smile-picture/public/.../xxx.png
} catch (URISyntaxException e) {
throw new IllegalArgumentException("非法的 COS URL: " + originalUrl, e);
}
}
} }

View File

@ -135,6 +135,7 @@ public abstract class PictureUploadTemplate {
uploadPictureResult.setPicColor(imageInfo.getAve()); uploadPictureResult.setPicColor(imageInfo.getAve());
// 设置缩略图地址 // 设置缩略图地址
uploadPictureResult.setThumbnailUrl(cosClientConfig.getHost() + "/" + thumbnailCiObject.getKey()); uploadPictureResult.setThumbnailUrl(cosClientConfig.getHost() + "/" + thumbnailCiObject.getKey());
//设置原图地址
uploadPictureResult.setOriginalUrl(cosClientConfig.getHost() + "/" + uploadPath); uploadPictureResult.setOriginalUrl(cosClientConfig.getHost() + "/" + uploadPath);
// 返回可访问的地址 // 返回可访问的地址
return uploadPictureResult; return uploadPictureResult;

View File

@ -43,6 +43,14 @@ public interface PictureService extends IService<Picture> {
*/ */
LambdaQueryWrapper<Picture> getQueryWrapper(PictureQueryRequest pictureQueryRequest); LambdaQueryWrapper<Picture> getQueryWrapper(PictureQueryRequest pictureQueryRequest);
/**
* 删除图片
*
* @param pictureId
* @param loginUser
*/
void deletePicture(long pictureId, User loginUser);
/** /**
* 获取图片包装类单条 * 获取图片包装类单条
* *

View File

@ -40,6 +40,7 @@ import org.jsoup.select.Elements;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils; import org.springframework.util.DigestUtils;
@ -84,19 +85,19 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
ThrowUtils.throwIf(introduction.length() > 800, ErrorCode.PARAMS_ERROR, "简介过长"); ThrowUtils.throwIf(introduction.length() > 800, ErrorCode.PARAMS_ERROR, "简介过长");
} }
} }
@Override @Override
public PictureVO uploadPicture(Object inputSource, PictureUploadRequest pictureUploadRequest, User loginUser) { public PictureVO uploadPicture(Object inputSource, PictureUploadRequest pictureUploadRequest, User loginUser) {
// 校验参数 // 校验参数
ThrowUtils.throwIf(loginUser == null, ErrorCode.NO_AUTH_ERROR); ThrowUtils.throwIf(loginUser == null, ErrorCode.NO_AUTH_ERROR);
// 判断是新增还是删除 // 判断是创建还是替换
Long pictureId = null; Long pictureId = pictureUploadRequest == null ? null : pictureUploadRequest.getId();
if (pictureUploadRequest != null) { Picture oldPicture = null;
pictureId = pictureUploadRequest.getId();
}
// 如果是更新判断图片是否存在 // 如果是更新判断图片是否存在
if (pictureId != null) { if (pictureId != null) {
Picture oldPicture = this.getById(pictureId); oldPicture = this.getById(pictureId);
ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR, "图片不存在"); ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR, "图片不存在");
// 仅本人或管理员可编辑图片 // 仅本人或管理员可编辑图片
if (!oldPicture.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) { if (!oldPicture.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {
@ -112,8 +113,9 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
if (inputSource instanceof String) { if (inputSource instanceof String) {
pictureUploadTemplate = urlPictureUpload; pictureUploadTemplate = urlPictureUpload;
} }
//上传到腾讯云COS上
UploadPictureResult uploadPictureResult = pictureUploadTemplate.uploadPicture(inputSource, uploadPathPrefix); UploadPictureResult uploadPictureResult = pictureUploadTemplate.uploadPicture(inputSource, uploadPathPrefix);
// 构造要入库的图片信息 // 构造要入库的图片信息将图片信息存入数据库中
Picture picture = new Picture(); Picture picture = new Picture();
// 复制同名属性urlnamepicSizepicWidthpicHeightpicScalepicFormat // 复制同名属性urlnamepicSizepicWidthpicHeightpicScalepicFormat
BeanUtils.copyProperties(uploadPictureResult, picture); BeanUtils.copyProperties(uploadPictureResult, picture);
@ -135,8 +137,11 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
picture.setEditTime(new Date()); picture.setEditTime(new Date());
} }
boolean result = this.saveOrUpdate(picture); boolean result = this.saveOrUpdate(picture);
//todo:如果是更新清理旧的图片
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR, "图片上传失败,数据库操作失败"); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR, "图片上传失败,数据库操作失败");
//如果是更新清理旧的图片
if (oldPicture != null) {
this.clearPictureFile(oldPicture);
}
return PictureVO.objToVo(picture); return PictureVO.objToVo(picture);
} }
@ -190,6 +195,22 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
return qw; return qw;
} }
@Override
public void deletePicture(long pictureId, User loginUser) {
Picture oldPicture = this.getById(pictureId);
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 = this.removeById(pictureId);
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
//清理图片资源
this.clearPictureFile(oldPicture);
}
@Override @Override
public PictureVO getPictureVO(Picture picture, HttpServletRequest request) { public PictureVO getPictureVO(Picture picture, HttpServletRequest request) {
// 对象转封装类 // 对象转封装类
@ -418,7 +439,7 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
@Async //异步执行 @Async //异步执行
@Override @Override
public void clearPictureFile(Picture oldPicture) { public void clearPictureFile(Picture oldPicture) {
// 判断图片是否被多条记录使用(图片秒传的情况可能一个cos地址对应多个数据库url) // 判断图片是否被多条记录使用(图片秒传的情况可能一个cos地址对应多个数据库url)
String pictureUrl = oldPicture.getUrl(); String pictureUrl = oldPicture.getUrl();
long count = this.lambdaQuery() long count = this.lambdaQuery()
.eq(Picture::getUrl, pictureUrl) .eq(Picture::getUrl, pictureUrl)
@ -428,18 +449,18 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
return; return;
} }
// 删除图片 // 删除图片
cosManager.deleteObject(pictureUrl); cosManager.deleteIfNotBlank(pictureUrl);
//删除原图 //删除原图
String originalUrl=oldPicture.getOriginalUrl(); String originalUrl=oldPicture.getOriginalUrl();
if (StrUtil.isNotBlank(originalUrl)) { cosManager.deleteIfNotBlank(originalUrl);
cosManager.deleteObject(originalUrl);
}
// 删除缩略图 // 删除缩略图
String thumbnailUrl = oldPicture.getThumbnailUrl(); String thumbnailUrl = oldPicture.getThumbnailUrl();
if (StrUtil.isNotBlank(thumbnailUrl)) { cosManager.deleteIfNotBlank(thumbnailUrl);
cosManager.deleteObject(thumbnailUrl);
}
} }
} }

View File

@ -2,6 +2,10 @@ server:
port: 8123 port: 8123
servlet: servlet:
context-path: /api context-path: /api
# cookie 30 天过期
session:
cookie:
max-age: 2592000
spring: spring:
profiles: profiles:
@ -21,6 +25,11 @@ spring:
port: 6379 port: 6379
password: 123456 password: 123456
timeout: 5000 timeout: 5000
# Session 配置
session:
store-type: redis
# session 30 天后过期,单位是秒
timeout: 2592000
servlet: servlet:
multipart: multipart:
max-file-size: 10MB max-file-size: 10MB