3.14 图片查询二级缓存实现 图片优化、压缩格式、缩略图

This commit is contained in:
zhangsan 2025-03-14 16:55:32 +08:00
parent 144a55c1cf
commit 6fcfd80e51
15 changed files with 320 additions and 111 deletions

View File

@ -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';

View File

@ -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");
}
/**

View File

@ -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);
}
/**

View File

@ -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);
}

View 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;
}
}

View File

@ -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

View File

@ -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();

View File

@ -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

View File

@ -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;
/**
* 图片名称
*/

View File

@ -7,6 +7,10 @@ import lombok.Data;
*/
@Data
public class UploadPictureResult {
/**
* 原图
*/
private String originalUrl;
/**
* 图片地址

View File

@ -18,11 +18,20 @@ public class PictureVO implements Serializable {
*/
private Long id;
/**
* 原图
*/
private String originalUrl;
/**
* 图片 url
*/
private String url;
/**
* 缩略图 url
*/
private String thumbnailUrl;
/**
* 图片名称

View File

@ -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);
}

View File

@ -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;
// 随机过期300600s
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
);
}
}

View File

@ -1,5 +1,5 @@
server:
port: 8100
port: 8123
servlet:
context-path: /api

View File

@ -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,