3.14 图片查询二级缓存实现 图片优化、压缩格式、缩略图
This commit is contained in:
parent
144a55c1cf
commit
6fcfd80e51
@ -56,4 +56,9 @@ ALTER TABLE picture
|
||||
ADD COLUMN review_time DATETIME NULL COMMENT '审核时间';
|
||||
|
||||
-- 创建基于 reviewStatus 列的索引
|
||||
CREATE INDEX idx_reviewStatus ON picture (review_status);
|
||||
CREATE INDEX idx_reviewStatus ON picture (review_status);
|
||||
|
||||
ALTER TABLE picture
|
||||
-- 添加新列
|
||||
ADD COLUMN original_url varchar(512) NULL COMMENT '原图 url',
|
||||
ADD COLUMN thumbnail_url varchar(512) NULL COMMENT '缩略图 url';
|
@ -14,7 +14,7 @@ public class ResultUtils {
|
||||
* @return 响应
|
||||
*/
|
||||
public static <T> BaseResponse<T> success(T data) {
|
||||
return new BaseResponse<>(200, data, "success");
|
||||
return new BaseResponse<>(0, data, "success");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,10 +1,6 @@
|
||||
package edu.whut.smilepicturebackend.controller;
|
||||
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import edu.whut.smilepicturebackend.annotation.AuthCheck;
|
||||
import edu.whut.smilepicturebackend.common.BaseResponse;
|
||||
import edu.whut.smilepicturebackend.common.DeleteRequest;
|
||||
@ -24,17 +20,14 @@ import edu.whut.smilepicturebackend.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.core.ValueOperations;
|
||||
import org.springframework.util.DigestUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@ -43,16 +36,8 @@ import java.util.concurrent.TimeUnit;
|
||||
public class PictureController {
|
||||
private final UserService userService;
|
||||
private final PictureService pictureService;
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
/**
|
||||
* 本地缓存
|
||||
*/
|
||||
private final Cache<String, String> LOCAL_CACHE = Caffeine.newBuilder()
|
||||
.initialCapacity(1024)
|
||||
.maximumSize(10_000L) // 最大 10000 条
|
||||
.expireAfterWrite(Duration.ofMinutes(5)) // 缓存 5 分钟后移除
|
||||
.build();
|
||||
|
||||
|
||||
/**
|
||||
* 上传图片(可重新上传)
|
||||
@ -211,45 +196,14 @@ public class PictureController {
|
||||
*/
|
||||
@Deprecated
|
||||
@PostMapping("/list/page/vo/cache")
|
||||
public BaseResponse<Page<PictureVO>> listPictureVOByPageWithCache(@RequestBody PictureQueryRequest pictureQueryRequest,
|
||||
HttpServletRequest request) {
|
||||
long current = pictureQueryRequest.getCurrent();
|
||||
long size = pictureQueryRequest.getPageSize();
|
||||
// 限制爬虫
|
||||
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
|
||||
// 普通用户默认只能看到审核通过的数据
|
||||
pictureQueryRequest.setReviewStatus(PictureReviewStatusEnum.PASS.getValue());
|
||||
// 查询缓存,缓存中没有,再查询数据库
|
||||
// 构建缓存的 key,根据查询条件来构建->json
|
||||
String queryCondition = JSONUtil.toJsonStr(pictureQueryRequest); //json->md5,更轻量化,去除括号,双引号
|
||||
String hashKey = DigestUtils.md5DigestAsHex(queryCondition.getBytes());
|
||||
String cacheKey = String.format("smilepicture:listPictureVOByPage:%s", hashKey);
|
||||
// // 1. 先从本地缓存中查询
|
||||
// String cachedValue = LOCAL_CACHE.getIfPresent(cacheKey);
|
||||
// if (cachedValue != null) {
|
||||
// // 如果缓存命中,返回结果
|
||||
// Page<PictureVO> cachedPage = JSONUtil.toBean(cachedValue, Page.class);
|
||||
// return ResultUtils.success(cachedPage);
|
||||
// }
|
||||
// 2. 本地缓存未命中,查询 Redis 分布式缓存
|
||||
ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
|
||||
String cachedValue = opsForValue.get(cacheKey);
|
||||
if (cachedValue != null) {
|
||||
Page<PictureVO> cachedPage = JSONUtil.toBean(cachedValue, Page.class);
|
||||
return ResultUtils.success(cachedPage);
|
||||
}
|
||||
// 3. 查询数据库
|
||||
Page<Picture> picturePage = pictureService.page(new Page<>(current, size),
|
||||
pictureService.getQueryWrapper(pictureQueryRequest));
|
||||
Page<PictureVO> pictureVOPage = pictureService.getPictureVOPage(picturePage, request);
|
||||
// 4. 更新缓存
|
||||
// 更新 Redis 缓存
|
||||
String cacheValue = JSONUtil.toJsonStr(pictureVOPage);
|
||||
// 设置缓存的过期时间,5 - 10 分钟过期,防止缓存雪崩!
|
||||
int cacheExpireTime = 300 + RandomUtil.randomInt(0, 300);
|
||||
opsForValue.set(cacheKey, cacheValue, cacheExpireTime, TimeUnit.SECONDS);
|
||||
// 获取封装类
|
||||
return ResultUtils.success(pictureVOPage);
|
||||
public BaseResponse<Page<PictureVO>> listPictureVOByPageWithCache(
|
||||
@RequestBody PictureQueryRequest pictureQueryRequest,
|
||||
HttpServletRequest request) {
|
||||
|
||||
Page<PictureVO> page = pictureService
|
||||
.listPictureVOByPageWithCache(pictureQueryRequest, request);
|
||||
|
||||
return ResultUtils.success(page);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,26 +1,31 @@
|
||||
package edu.whut.smilepicturebackend.manager;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import com.qcloud.cos.COSClient;
|
||||
import com.qcloud.cos.model.*;
|
||||
import com.qcloud.cos.model.ciModel.persistence.PicOperations;
|
||||
import edu.whut.smilepicturebackend.config.CosClientConfig;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* 和业务没有关系,通用的文件上传下载
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CosManager {
|
||||
|
||||
@Resource
|
||||
private CosClientConfig cosClientConfig;
|
||||
private final CosClientConfig cosClientConfig;
|
||||
|
||||
@Resource
|
||||
private COSClient cosClient;
|
||||
private final COSClient cosClient;
|
||||
|
||||
final long THUMBNAIL_THRESHOLD = 20 * 1024; // 20 KB
|
||||
|
||||
/**
|
||||
* 上传对象
|
||||
@ -47,7 +52,28 @@ public class CosManager {
|
||||
PicOperations picOperations = new PicOperations();
|
||||
// 1 表示返回原图信息
|
||||
picOperations.setIsPicInfo(1);
|
||||
// 图片处理规则列表
|
||||
List<PicOperations.Rule> rules = new ArrayList<>();
|
||||
// 1. 图片压缩(转成 webp 格式) https://cloud.tencent.com/document/product/436/42215
|
||||
String webpKey = FileUtil.mainName(key) + ".webp";
|
||||
PicOperations.Rule compressRule = new PicOperations.Rule();
|
||||
compressRule.setFileId(webpKey);
|
||||
compressRule.setBucket(cosClientConfig.getBucket());
|
||||
compressRule.setRule("imageMogr2/format/webp");
|
||||
rules.add(compressRule);
|
||||
// 2. 缩略图处理,仅对 > 20 KB 的图片生成缩略图 https://cloud.tencent.com/document/product/436/113295
|
||||
if (file.length() > THUMBNAIL_THRESHOLD) {
|
||||
PicOperations.Rule thumbnailRule = new PicOperations.Rule();
|
||||
// 拼接缩略图的路径
|
||||
String thumbnailKey = FileUtil.mainName(key) + "_thumbnail." + FileUtil.getSuffix(key);
|
||||
thumbnailRule.setFileId(thumbnailKey);
|
||||
thumbnailRule.setBucket(cosClientConfig.getBucket());
|
||||
// 缩放规则 /thumbnail/<Width>x<Height>>(如果大于原图宽高,则不处理)
|
||||
thumbnailRule.setRule(String.format("imageMogr2/thumbnail/%sx%s>", 400, 400));
|
||||
rules.add(thumbnailRule);
|
||||
}
|
||||
// 构造处理参数
|
||||
picOperations.setRules(rules);
|
||||
putObjectRequest.setPicOperations(picOperations);
|
||||
return cosClient.putObject(putObjectRequest);
|
||||
}
|
||||
|
63
src/main/java/edu/whut/smilepicturebackend/manager/cache/MyCacheManager.java
vendored
Normal file
63
src/main/java/edu/whut/smilepicturebackend/manager/cache/MyCacheManager.java
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
package edu.whut.smilepicturebackend.manager.cache;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.core.ValueOperations;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MyCacheManager {
|
||||
|
||||
/**
|
||||
* 本地缓存
|
||||
*/
|
||||
private final Cache<String, String> localCache = Caffeine.newBuilder()
|
||||
.initialCapacity(1024)
|
||||
.maximumSize(10_000L) // 最大 10000 条
|
||||
.expireAfterWrite(Duration.ofMinutes(5)) // 缓存 5 分钟后移除
|
||||
.build();
|
||||
|
||||
/**
|
||||
* 分布式缓存
|
||||
*/
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
public <T> T getFromCacheOrDatabase(
|
||||
String cacheKey,
|
||||
Class<T> clazz,
|
||||
Supplier<T> dbSupplier,
|
||||
int redisExpireSeconds) {
|
||||
|
||||
// 查询本地缓存
|
||||
String cachedValue = localCache.getIfPresent(cacheKey);
|
||||
if (cachedValue != null) {
|
||||
return JSONUtil.toBean(cachedValue, clazz);
|
||||
}
|
||||
|
||||
// 查询 Redis 缓存
|
||||
ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
|
||||
cachedValue = opsForValue.get(cacheKey);
|
||||
if (cachedValue != null) {
|
||||
localCache.put(cacheKey, cachedValue);
|
||||
return JSONUtil.toBean(cachedValue, clazz);
|
||||
}
|
||||
|
||||
// 执行数据库查询
|
||||
T dbValue = dbSupplier.get();
|
||||
|
||||
// 更新 Redis 缓存
|
||||
cachedValue = JSONUtil.toJsonStr(dbValue);
|
||||
opsForValue.set(cacheKey, cachedValue, redisExpireSeconds, TimeUnit.SECONDS);
|
||||
// 写入本地缓存
|
||||
localCache.put(cacheKey, cachedValue);
|
||||
return dbValue;
|
||||
}
|
||||
}
|
@ -26,9 +26,13 @@ public class FilePictureUpload extends PictureUploadTemplate {
|
||||
ThrowUtils.throwIf(fileSize > 2 * ONE_M, ErrorCode.PARAMS_ERROR, "文件大小不能超过 2MB");
|
||||
// 2. 校验文件后缀
|
||||
String fileSuffix = FileUtil.getSuffix(multipartFile.getOriginalFilename());
|
||||
ThrowUtils.throwIf(fileSuffix == null, ErrorCode.PARAMS_ERROR, "文件后缀缺失");
|
||||
// 允许上传的文件后缀列表(或者集合)
|
||||
final List<String> ALLOW_FORMAT_LIST = Arrays.asList("jpeg", "png", "jpg", "webp");
|
||||
ThrowUtils.throwIf(!ALLOW_FORMAT_LIST.contains(fileSuffix), ErrorCode.PARAMS_ERROR, "文件类型错误");
|
||||
// 统一转成小写再比较
|
||||
ThrowUtils.throwIf(
|
||||
!ALLOW_FORMAT_LIST.contains(fileSuffix.toLowerCase()),
|
||||
ErrorCode.PARAMS_ERROR, "文件类型错误");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,4 +1,5 @@
|
||||
package edu.whut.smilepicturebackend.manager.upload;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
@ -6,6 +7,7 @@ import cn.hutool.core.util.RandomUtil;
|
||||
import com.qcloud.cos.model.PutObjectResult;
|
||||
import com.qcloud.cos.model.ciModel.persistence.CIObject;
|
||||
import com.qcloud.cos.model.ciModel.persistence.ImageInfo;
|
||||
import com.qcloud.cos.model.ciModel.persistence.ProcessResults;
|
||||
import edu.whut.smilepicturebackend.config.CosClientConfig;
|
||||
import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
@ -16,6 +18,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import javax.annotation.Resource;
|
||||
import java.io.File;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 图片上传模板
|
||||
@ -42,9 +45,13 @@ public abstract class PictureUploadTemplate {
|
||||
// 2. 图片上传地址
|
||||
String uuid = RandomUtil.randomString(16);
|
||||
String originalFilename = getOriginFilename(inputSource);
|
||||
// extName 直接取扩展名,不含点
|
||||
String extension = FileUtil.extName(originalFilename); // "png" 或 "jpg"
|
||||
// 自己拼接文件上传路径,而不是使用原始文件名称,可以增强安全性
|
||||
String uploadFilename = String.format("%s_%s.%s", DateUtil.formatDate(new Date()), uuid,
|
||||
FileUtil.getSuffix(originalFilename));
|
||||
String uploadFilename = String.format("%s_%s.%s",
|
||||
DateUtil.formatDate(new Date()),
|
||||
uuid,
|
||||
extension);
|
||||
//如果多个项目共享存储桶,请在桶的根目录下以各项目名作为目录。
|
||||
String projectName="smile-picture";
|
||||
String uploadPath = String.format("/%s/%s/%s",projectName, uploadPathPrefix, uploadFilename);
|
||||
@ -58,6 +65,21 @@ public abstract class PictureUploadTemplate {
|
||||
PutObjectResult putObjectResult = cosManager.putPictureObject(uploadPath, file);
|
||||
// 5. 获取图片信息对象,封装返回结果
|
||||
ImageInfo imageInfo = putObjectResult.getCiUploadResult().getOriginalInfo().getImageInfo();
|
||||
// 获取到图片处理结果
|
||||
ProcessResults processResults = putObjectResult.getCiUploadResult().getProcessResults();
|
||||
List<CIObject> objectList = processResults.getObjectList();
|
||||
if (CollUtil.isNotEmpty(objectList)) {
|
||||
// 获取压缩之后得到的文件信息
|
||||
CIObject compressedCiObject = objectList.get(0); //第一个是压缩后的
|
||||
// 缩略图默认等于压缩图,压缩图是必有的
|
||||
CIObject thumbnailCiObject = compressedCiObject;
|
||||
// 有生成缩略图,才获取缩略图
|
||||
if (objectList.size() > 1) { //第二个是缩略图
|
||||
thumbnailCiObject = objectList.get(1);
|
||||
}
|
||||
// 封装压缩图的返回结果
|
||||
return buildResult(originalFilename, compressedCiObject, thumbnailCiObject, imageInfo,uploadPath);
|
||||
}
|
||||
return buildResult(originalFilename, file, uploadPath, imageInfo);
|
||||
} catch (Exception e) {
|
||||
log.error("图片上传到对象存储失败", e);
|
||||
@ -66,7 +88,6 @@ public abstract class PictureUploadTemplate {
|
||||
// 6. 临时文件清理
|
||||
this.deleteTempFile(file);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -85,6 +106,38 @@ public abstract class PictureUploadTemplate {
|
||||
protected abstract void processFile(Object inputSource, File file) throws Exception;
|
||||
|
||||
|
||||
/**
|
||||
* 封装返回结果
|
||||
*
|
||||
* @param originalFilename 原始文件名
|
||||
* @param compressedCiObject 压缩后的对象
|
||||
* @param thumbnailCiObject 缩略图对象
|
||||
* @param imageInfo 图片信息
|
||||
* @return
|
||||
*/
|
||||
private UploadPictureResult buildResult(String originalFilename, CIObject compressedCiObject, CIObject thumbnailCiObject,
|
||||
ImageInfo imageInfo,String uploadPath) {
|
||||
// 计算宽高
|
||||
int picWidth = compressedCiObject.getWidth();
|
||||
int picHeight = compressedCiObject.getHeight();
|
||||
double picScale = NumberUtil.round(picWidth * 1.0 / picHeight, 2).doubleValue();
|
||||
// 封装返回结果
|
||||
UploadPictureResult uploadPictureResult = new UploadPictureResult();
|
||||
// 设置压缩后的原图地址
|
||||
uploadPictureResult.setUrl(cosClientConfig.getHost() + "/" + compressedCiObject.getKey());
|
||||
uploadPictureResult.setName(FileUtil.mainName(originalFilename));
|
||||
uploadPictureResult.setPicSize(compressedCiObject.getSize().longValue());
|
||||
uploadPictureResult.setPicWidth(picWidth);
|
||||
uploadPictureResult.setPicHeight(picHeight);
|
||||
uploadPictureResult.setPicScale(picScale);
|
||||
uploadPictureResult.setPicFormat(compressedCiObject.getFormat());
|
||||
uploadPictureResult.setPicColor(imageInfo.getAve());
|
||||
// 设置缩略图地址
|
||||
uploadPictureResult.setThumbnailUrl(cosClientConfig.getHost() + "/" + thumbnailCiObject.getKey());
|
||||
uploadPictureResult.setOriginalUrl(cosClientConfig.getHost() + "/" + uploadPath);
|
||||
// 返回可访问的地址
|
||||
return uploadPictureResult;
|
||||
}
|
||||
/**
|
||||
* 封装返回结果
|
||||
*
|
||||
@ -94,6 +147,7 @@ public abstract class PictureUploadTemplate {
|
||||
* @param imageInfo 对象存储返回的图片信息
|
||||
* @return
|
||||
*/
|
||||
|
||||
private UploadPictureResult buildResult(String originalFilename, File file, String uploadPath, ImageInfo imageInfo) {
|
||||
// 计算宽高
|
||||
int picWidth = imageInfo.getWidth();
|
||||
|
@ -39,47 +39,72 @@ public class UrlPictureUpload extends PictureUploadTemplate {
|
||||
ThrowUtils.throwIf(!fileUrl.startsWith("http://") && !fileUrl.startsWith("https://"),
|
||||
ErrorCode.PARAMS_ERROR, "仅支持 HTTP 或 HTTPS 协议的文件地址"
|
||||
);
|
||||
// 4. 发送 HEAD 请求验证文件是否存在
|
||||
HttpResponse httpResponse = null;
|
||||
try {
|
||||
httpResponse = HttpUtil.createRequest(Method.HEAD, fileUrl)
|
||||
.execute();
|
||||
// 未正常返回,无需执行其他判断
|
||||
if (httpResponse.getStatus() != HttpStatus.HTTP_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 只发一次 HEAD,就做远程校验 + 提取扩展名
|
||||
* @return 不含点的扩展名,比如 "jpg","png",取不出时返回空串
|
||||
*/
|
||||
protected String fetchAndValidateExtension(String fileUrl) {
|
||||
try (HttpResponse resp = HttpUtil
|
||||
.createRequest(Method.HEAD, fileUrl)
|
||||
.execute()) {
|
||||
if (resp.getStatus() != HttpStatus.HTTP_OK) {
|
||||
throw new BusinessException(
|
||||
ErrorCode.OPERATION_ERROR, "文件不存在或不可访问");
|
||||
}
|
||||
// 5. 文件存在,文件类型校验
|
||||
String contentType = httpResponse.header("Content-Type");
|
||||
// 不为空,才校验是否合法,这样校验规则相对宽松
|
||||
if (StrUtil.isNotBlank(contentType)) {
|
||||
// 允许的图片类型
|
||||
final List<String> ALLOW_CONTENT_TYPES = Arrays.asList("image/jpeg", "image/jpg", "image/png", "image/webp");
|
||||
ThrowUtils.throwIf(!ALLOW_CONTENT_TYPES.contains(contentType.toLowerCase()),
|
||||
ErrorCode.PARAMS_ERROR, "文件类型错误");
|
||||
|
||||
// 1) Content-Type 验证 & 提取扩展名
|
||||
String ct = resp.header("Content-Type");
|
||||
String ext = "";
|
||||
if (StrUtil.isNotBlank(ct) && ct.contains("/")) {
|
||||
// e.g. "image/jpeg"
|
||||
String raw = StrUtil.subAfter(ct, "/", true).toLowerCase();
|
||||
ext = raw.equals("jpeg") ? "jpg" : raw;
|
||||
// 验证白名单
|
||||
List<String> allow = Arrays.asList(
|
||||
"image/jpeg", "image/jpg", "image/png", "image/webp"
|
||||
);
|
||||
ThrowUtils.throwIf(
|
||||
!allow.contains(ct.toLowerCase()),
|
||||
ErrorCode.PARAMS_ERROR,
|
||||
"不支持的图片类型:" + ct
|
||||
);
|
||||
}
|
||||
// 6. 文件存在,文件大小校验
|
||||
String contentLengthStr = httpResponse.header("Content-Length");
|
||||
if (StrUtil.isNotBlank(contentLengthStr)) {
|
||||
try {
|
||||
long contentLength = Long.parseLong(contentLengthStr);
|
||||
final long ONE_M = 1024 * 1024;
|
||||
ThrowUtils.throwIf(contentLength > 2 * ONE_M, ErrorCode.PARAMS_ERROR, "文件大小不能超过 2MB");
|
||||
} catch (NumberFormatException e) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件大小格式异常");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// 记得释放资源
|
||||
if (httpResponse != null) {
|
||||
httpResponse.close();
|
||||
|
||||
// 2) Content-Length 验证
|
||||
String len = resp.header("Content-Length");
|
||||
if (StrUtil.isNotBlank(len)) {
|
||||
long size = Long.parseLong(len);
|
||||
long max = 2L * 1024 * 1024;
|
||||
ThrowUtils.throwIf(
|
||||
size > max,
|
||||
ErrorCode.PARAMS_ERROR,
|
||||
"文件大小不能超过 2MB"
|
||||
);
|
||||
}
|
||||
|
||||
return ext;
|
||||
} catch (NumberFormatException e) {
|
||||
throw new BusinessException(
|
||||
ErrorCode.PARAMS_ERROR, "文件大小格式异常");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected String getOriginFilename(Object inputSource) {
|
||||
String fileUrl = (String) inputSource;
|
||||
return FileUtil.mainName(fileUrl);
|
||||
// 1) HEAD 验证并拿扩展名(只这一处会发 HEAD)
|
||||
String ext = fetchAndValidateExtension(fileUrl);
|
||||
// 2) fallback:若服务器没返回类型,再从 URL 中简单截取
|
||||
if (StrUtil.isBlank(ext)) {
|
||||
ext = FileUtil.extName(fileUrl);
|
||||
}
|
||||
ThrowUtils.throwIf(ext==null,ErrorCode.PARAMS_ERROR,"不正确的图片格式");
|
||||
// 3) 拿到 baseName,然后拼回去
|
||||
String base = FileUtil.mainName(FileUtil.getName(fileUrl));
|
||||
return StrUtil.isNotBlank(ext) ? base + "." + ext : base;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -19,11 +19,21 @@ public class Picture implements Serializable {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 原图
|
||||
*/
|
||||
private String originalUrl;
|
||||
|
||||
/**
|
||||
* 图片 url
|
||||
*/
|
||||
private String url;
|
||||
|
||||
/**
|
||||
* 缩略图 url
|
||||
*/
|
||||
private String thumbnailUrl;
|
||||
|
||||
/**
|
||||
* 图片名称
|
||||
*/
|
||||
|
@ -7,6 +7,10 @@ import lombok.Data;
|
||||
*/
|
||||
@Data
|
||||
public class UploadPictureResult {
|
||||
/**
|
||||
* 原图
|
||||
*/
|
||||
private String originalUrl;
|
||||
|
||||
/**
|
||||
* 图片地址
|
||||
|
@ -18,11 +18,20 @@ public class PictureVO implements Serializable {
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 原图
|
||||
*/
|
||||
private String originalUrl;
|
||||
|
||||
/**
|
||||
* 图片 url
|
||||
*/
|
||||
private String url;
|
||||
|
||||
/**
|
||||
* 缩略图 url
|
||||
*/
|
||||
private String thumbnailUrl;
|
||||
|
||||
/**
|
||||
* 图片名称
|
||||
|
@ -94,4 +94,12 @@ public interface PictureService extends IService<Picture> {
|
||||
*/
|
||||
Integer uploadPictureByBatch(PictureUploadByBatchRequest pictureUploadByBatchRequest,
|
||||
User loginUser);
|
||||
|
||||
/**
|
||||
* 缓存图片
|
||||
* @param queryRequest
|
||||
* @param httpRequest
|
||||
* @return
|
||||
*/
|
||||
Page<PictureVO> listPictureVOByPageWithCache(PictureQueryRequest queryRequest, HttpServletRequest httpRequest);
|
||||
}
|
||||
|
@ -3,7 +3,10 @@ package edu.whut.smilepicturebackend.service.impl;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSON;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
@ -13,6 +16,7 @@ import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import edu.whut.smilepicturebackend.exception.ThrowUtils;
|
||||
import edu.whut.smilepicturebackend.manager.FileManager;
|
||||
import edu.whut.smilepicturebackend.manager.cache.MyCacheManager;
|
||||
import edu.whut.smilepicturebackend.manager.upload.FilePictureUpload;
|
||||
import edu.whut.smilepicturebackend.manager.upload.PictureUploadTemplate;
|
||||
import edu.whut.smilepicturebackend.manager.upload.UrlPictureUpload;
|
||||
@ -34,6 +38,7 @@ import org.jsoup.nodes.Element;
|
||||
import org.jsoup.select.Elements;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.DigestUtils;
|
||||
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@ -58,7 +63,7 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
||||
private final UserService userService;
|
||||
private final FilePictureUpload filePictureUpload;
|
||||
private final UrlPictureUpload urlPictureUpload;
|
||||
|
||||
private final MyCacheManager cacheManager;
|
||||
@Override
|
||||
public void validPicture(Picture picture) {
|
||||
ThrowUtils.throwIf(picture == null, ErrorCode.PARAMS_ERROR);
|
||||
@ -99,7 +104,7 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
||||
String uploadPathPrefix;
|
||||
//公共图库下,每个用户有自己的userid管理的文件夹。
|
||||
uploadPathPrefix = String.format("public/%s", loginUser.getId());
|
||||
// 根据 inputSource 的类型区分上传方式
|
||||
// 根据 inputSource 的类型区分上传方式!!
|
||||
PictureUploadTemplate pictureUploadTemplate = filePictureUpload;
|
||||
if (inputSource instanceof String) {
|
||||
pictureUploadTemplate = urlPictureUpload;
|
||||
@ -335,23 +340,27 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
||||
// 遍历元素,依次处理上传图片
|
||||
int uploadCount = 0;
|
||||
for (Element imgElement : imgElementList) {
|
||||
String fileUrl = imgElement.attr("src");
|
||||
if (StrUtil.isBlank(fileUrl)) {
|
||||
//并不是所有图片链接都是正确的
|
||||
log.info("当前链接为空,已跳过:{}", fileUrl);
|
||||
// 找到最近的 <a class="iusc">,它的 m 属性里有完整的 JSON
|
||||
Element a = imgElement.closest("a.iusc");
|
||||
if (a == null || a.attr("m").isEmpty()) {
|
||||
log.info("没找到 m 属性,跳过该图片");
|
||||
continue;
|
||||
}
|
||||
// 处理图片的地址,防止转义或者和对象存储冲突的问题
|
||||
// codefather.cn?yupi=dog,应该只保留 codefather.cn
|
||||
int questionMarkIndex = fileUrl.indexOf("?");
|
||||
if (questionMarkIndex > -1) {
|
||||
fileUrl = fileUrl.substring(0, questionMarkIndex);
|
||||
// 用 Hutool 解析 JSON,取出 murl
|
||||
JSONObject mObj = JSONUtil.parseObj(a.attr("m"));
|
||||
String fileUrl = mObj.getStr("murl"); // murl是带 .jpg/.png 的原图 , src是缩略图,
|
||||
|
||||
// 可选:去掉 URL 后面的 ? 及参数
|
||||
int qm = fileUrl.indexOf("?");
|
||||
if (qm > 0) {
|
||||
fileUrl = fileUrl.substring(0, qm);
|
||||
}
|
||||
// 上传图片
|
||||
PictureUploadRequest pictureUploadRequest = new PictureUploadRequest();
|
||||
pictureUploadRequest.setFileUrl(fileUrl);
|
||||
pictureUploadRequest.setPicName(namePrefix + (uploadCount + 1));
|
||||
try {
|
||||
log.info("爬取图片url:"+fileUrl);
|
||||
PictureVO pictureVO = this.uploadPicture(fileUrl, pictureUploadRequest, loginUser);
|
||||
log.info("图片上传成功,id = {}", pictureVO.getId());
|
||||
uploadCount++;
|
||||
@ -365,6 +374,42 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
||||
}
|
||||
return uploadCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询图片带缓存
|
||||
* @param queryRequest
|
||||
* @param httpRequest
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public Page<PictureVO> listPictureVOByPageWithCache(PictureQueryRequest queryRequest, HttpServletRequest httpRequest) {
|
||||
long current = queryRequest.getCurrent();
|
||||
long size = queryRequest.getPageSize();
|
||||
|
||||
// 参数校验
|
||||
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
|
||||
queryRequest.setReviewStatus(PictureReviewStatusEnum.PASS.getValue());
|
||||
|
||||
// 构造 cacheKey
|
||||
String condJson = JSONUtil.toJsonStr(queryRequest);
|
||||
String hash = DigestUtils.md5DigestAsHex(condJson.getBytes());
|
||||
String cacheKey = "smilepicture:listPictureVOByPage:" + hash;
|
||||
|
||||
// 随机过期:300–600s
|
||||
int expire = 300 + RandomUtil.randomInt(0, 300);
|
||||
|
||||
// 调用通用缓存方法
|
||||
return cacheManager.getFromCacheOrDatabase(
|
||||
cacheKey,
|
||||
Page.class,
|
||||
() -> {
|
||||
Page<Picture> picturePage = this.page(new Page<>(current, size),
|
||||
this.getQueryWrapper(queryRequest));
|
||||
return this.getPictureVOPage(picturePage, httpRequest);
|
||||
},
|
||||
expire
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
server:
|
||||
port: 8100
|
||||
port: 8123
|
||||
servlet:
|
||||
context-path: /api
|
||||
|
||||
|
@ -6,7 +6,9 @@
|
||||
|
||||
<resultMap id="BaseResultMap" type="edu.whut.smilepicturebackend.model.entity.Picture">
|
||||
<id property="id" column="id" jdbcType="BIGINT"/>
|
||||
<result property="originalUrl" column="original_url" jdbcType="VARCHAR"/>
|
||||
<result property="url" column="url" jdbcType="VARCHAR"/>
|
||||
<result property="thumbnailUrl" column="thumbnail_url" jdbcType="VARCHAR"/>
|
||||
<result property="name" column="name" jdbcType="VARCHAR"/>
|
||||
<result property="introduction" column="introduction" jdbcType="VARCHAR"/>
|
||||
<result property="category" column="category" jdbcType="VARCHAR"/>
|
||||
@ -24,7 +26,7 @@
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id,url,name,
|
||||
id,url,thumbnail_url,name,
|
||||
introduction,category,tags,
|
||||
pic_size,pic_width,pic_height,
|
||||
pic_scale,pic_format,user_id,
|
||||
|
Loading…
x
Reference in New Issue
Block a user