diff --git a/sql/create_table.sql b/sql/create_table.sql index 0dd1539..b1e5280 100644 --- a/sql/create_table.sql +++ b/sql/create_table.sql @@ -56,4 +56,9 @@ ALTER TABLE picture ADD COLUMN review_time DATETIME NULL COMMENT '审核时间'; -- 创建基于 reviewStatus 列的索引 -CREATE INDEX idx_reviewStatus ON picture (review_status); \ No newline at end of file +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'; \ No newline at end of file diff --git a/src/main/java/edu/whut/smilepicturebackend/common/ResultUtils.java b/src/main/java/edu/whut/smilepicturebackend/common/ResultUtils.java index 85825cf..2a1b2cc 100644 --- a/src/main/java/edu/whut/smilepicturebackend/common/ResultUtils.java +++ b/src/main/java/edu/whut/smilepicturebackend/common/ResultUtils.java @@ -14,7 +14,7 @@ public class ResultUtils { * @return 响应 */ public static BaseResponse success(T data) { - return new BaseResponse<>(200, data, "success"); + return new BaseResponse<>(0, data, "success"); } /** diff --git a/src/main/java/edu/whut/smilepicturebackend/controller/PictureController.java b/src/main/java/edu/whut/smilepicturebackend/controller/PictureController.java index 34447ed..d4c0f0f 100644 --- a/src/main/java/edu/whut/smilepicturebackend/controller/PictureController.java +++ b/src/main/java/edu/whut/smilepicturebackend/controller/PictureController.java @@ -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 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> 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 cachedPage = JSONUtil.toBean(cachedValue, Page.class); -// return ResultUtils.success(cachedPage); -// } - // 2. 本地缓存未命中,查询 Redis 分布式缓存 - ValueOperations opsForValue = stringRedisTemplate.opsForValue(); - String cachedValue = opsForValue.get(cacheKey); - if (cachedValue != null) { - Page cachedPage = JSONUtil.toBean(cachedValue, Page.class); - return ResultUtils.success(cachedPage); - } - // 3. 查询数据库 - Page picturePage = pictureService.page(new Page<>(current, size), - pictureService.getQueryWrapper(pictureQueryRequest)); - Page 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> listPictureVOByPageWithCache( + @RequestBody PictureQueryRequest pictureQueryRequest, + HttpServletRequest request) { + + Page page = pictureService + .listPictureVOByPageWithCache(pictureQueryRequest, request); + + return ResultUtils.success(page); } /** diff --git a/src/main/java/edu/whut/smilepicturebackend/manager/CosManager.java b/src/main/java/edu/whut/smilepicturebackend/manager/CosManager.java index 0a04183..27e4db3 100644 --- a/src/main/java/edu/whut/smilepicturebackend/manager/CosManager.java +++ b/src/main/java/edu/whut/smilepicturebackend/manager/CosManager.java @@ -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 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/x>(如果大于原图宽高,则不处理) + thumbnailRule.setRule(String.format("imageMogr2/thumbnail/%sx%s>", 400, 400)); + rules.add(thumbnailRule); + } // 构造处理参数 + picOperations.setRules(rules); putObjectRequest.setPicOperations(picOperations); return cosClient.putObject(putObjectRequest); } diff --git a/src/main/java/edu/whut/smilepicturebackend/manager/cache/MyCacheManager.java b/src/main/java/edu/whut/smilepicturebackend/manager/cache/MyCacheManager.java new file mode 100644 index 0000000..7558ca7 --- /dev/null +++ b/src/main/java/edu/whut/smilepicturebackend/manager/cache/MyCacheManager.java @@ -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 localCache = Caffeine.newBuilder() + .initialCapacity(1024) + .maximumSize(10_000L) // 最大 10000 条 + .expireAfterWrite(Duration.ofMinutes(5)) // 缓存 5 分钟后移除 + .build(); + + /** + * 分布式缓存 + */ + private final StringRedisTemplate stringRedisTemplate; + + public T getFromCacheOrDatabase( + String cacheKey, + Class clazz, + Supplier dbSupplier, + int redisExpireSeconds) { + + // 查询本地缓存 + String cachedValue = localCache.getIfPresent(cacheKey); + if (cachedValue != null) { + return JSONUtil.toBean(cachedValue, clazz); + } + + // 查询 Redis 缓存 + ValueOperations 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; + } +} diff --git a/src/main/java/edu/whut/smilepicturebackend/manager/upload/FilePictureUpload.java b/src/main/java/edu/whut/smilepicturebackend/manager/upload/FilePictureUpload.java index e74f660..7cbb283 100644 --- a/src/main/java/edu/whut/smilepicturebackend/manager/upload/FilePictureUpload.java +++ b/src/main/java/edu/whut/smilepicturebackend/manager/upload/FilePictureUpload.java @@ -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 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 diff --git a/src/main/java/edu/whut/smilepicturebackend/manager/upload/PictureUploadTemplate.java b/src/main/java/edu/whut/smilepicturebackend/manager/upload/PictureUploadTemplate.java index 124fdc2..dddf226 100644 --- a/src/main/java/edu/whut/smilepicturebackend/manager/upload/PictureUploadTemplate.java +++ b/src/main/java/edu/whut/smilepicturebackend/manager/upload/PictureUploadTemplate.java @@ -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 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(); diff --git a/src/main/java/edu/whut/smilepicturebackend/manager/upload/UrlPictureUpload.java b/src/main/java/edu/whut/smilepicturebackend/manager/upload/UrlPictureUpload.java index c56db43..5157ddd 100644 --- a/src/main/java/edu/whut/smilepicturebackend/manager/upload/UrlPictureUpload.java +++ b/src/main/java/edu/whut/smilepicturebackend/manager/upload/UrlPictureUpload.java @@ -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 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 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 diff --git a/src/main/java/edu/whut/smilepicturebackend/model/entity/Picture.java b/src/main/java/edu/whut/smilepicturebackend/model/entity/Picture.java index 1de1553..41c9ce2 100644 --- a/src/main/java/edu/whut/smilepicturebackend/model/entity/Picture.java +++ b/src/main/java/edu/whut/smilepicturebackend/model/entity/Picture.java @@ -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; + /** * 图片名称 */ diff --git a/src/main/java/edu/whut/smilepicturebackend/model/file/UploadPictureResult.java b/src/main/java/edu/whut/smilepicturebackend/model/file/UploadPictureResult.java index ceff098..32c04f1 100644 --- a/src/main/java/edu/whut/smilepicturebackend/model/file/UploadPictureResult.java +++ b/src/main/java/edu/whut/smilepicturebackend/model/file/UploadPictureResult.java @@ -7,6 +7,10 @@ import lombok.Data; */ @Data public class UploadPictureResult { + /** + * 原图 + */ + private String originalUrl; /** * 图片地址 diff --git a/src/main/java/edu/whut/smilepicturebackend/model/vo/PictureVO.java b/src/main/java/edu/whut/smilepicturebackend/model/vo/PictureVO.java index a125072..9b944a0 100644 --- a/src/main/java/edu/whut/smilepicturebackend/model/vo/PictureVO.java +++ b/src/main/java/edu/whut/smilepicturebackend/model/vo/PictureVO.java @@ -18,11 +18,20 @@ public class PictureVO implements Serializable { */ private Long id; + /** + * 原图 + */ + private String originalUrl; + /** * 图片 url */ private String url; + /** + * 缩略图 url + */ + private String thumbnailUrl; /** * 图片名称 diff --git a/src/main/java/edu/whut/smilepicturebackend/service/PictureService.java b/src/main/java/edu/whut/smilepicturebackend/service/PictureService.java index e600e8a..13969c9 100644 --- a/src/main/java/edu/whut/smilepicturebackend/service/PictureService.java +++ b/src/main/java/edu/whut/smilepicturebackend/service/PictureService.java @@ -94,4 +94,12 @@ public interface PictureService extends IService { */ Integer uploadPictureByBatch(PictureUploadByBatchRequest pictureUploadByBatchRequest, User loginUser); + + /** + * 缓存图片 + * @param queryRequest + * @param httpRequest + * @return + */ + Page listPictureVOByPageWithCache(PictureQueryRequest queryRequest, HttpServletRequest httpRequest); } diff --git a/src/main/java/edu/whut/smilepicturebackend/service/impl/PictureServiceImpl.java b/src/main/java/edu/whut/smilepicturebackend/service/impl/PictureServiceImpl.java index afe24da..b0cfcf1 100644 --- a/src/main/java/edu/whut/smilepicturebackend/service/impl/PictureServiceImpl.java +++ b/src/main/java/edu/whut/smilepicturebackend/service/impl/PictureServiceImpl.java @@ -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 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 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 // 遍历元素,依次处理上传图片 int uploadCount = 0; for (Element imgElement : imgElementList) { - String fileUrl = imgElement.attr("src"); - if (StrUtil.isBlank(fileUrl)) { - //并不是所有图片链接都是正确的 - log.info("当前链接为空,已跳过:{}", fileUrl); + // 找到最近的 ,它的 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 } return uploadCount; } + + /** + * 查询图片带缓存 + * @param queryRequest + * @param httpRequest + * @return + */ + @Override + public Page 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 picturePage = this.page(new Page<>(current, size), + this.getQueryWrapper(queryRequest)); + return this.getPictureVOPage(picturePage, httpRequest); + }, + expire + ); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e12c7a3..cc67e40 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,5 +1,5 @@ server: - port: 8100 + port: 8123 servlet: context-path: /api diff --git a/src/main/resources/mapper/PictureMapper.xml b/src/main/resources/mapper/PictureMapper.xml index 070a1cb..9a1c003 100644 --- a/src/main/resources/mapper/PictureMapper.xml +++ b/src/main/resources/mapper/PictureMapper.xml @@ -6,7 +6,9 @@ + + @@ -24,7 +26,7 @@ - id,url,name, + id,url,thumbnail_url,name, introduction,category,tags, pic_size,pic_width,pic_height, pic_scale,pic_format,user_id,