diff --git a/sql/create_table.sql b/sql/create_table.sql index c92302c..0dd1539 100644 --- a/sql/create_table.sql +++ b/sql/create_table.sql @@ -50,10 +50,10 @@ create table if not exists picture ALTER TABLE picture -- 添加新列 - ADD COLUMN reviewStatus INT DEFAULT 0 NOT NULL COMMENT '审核状态:0-待审核; 1-通过; 2-拒绝', - ADD COLUMN reviewMessage VARCHAR(512) NULL COMMENT '审核信息', - ADD COLUMN reviewerId BIGINT NULL COMMENT '审核人 ID', - ADD COLUMN reviewTime DATETIME NULL COMMENT '审核时间'; + ADD COLUMN review_status INT DEFAULT 0 NOT NULL COMMENT '审核状态:0-待审核; 1-通过; 2-拒绝', + ADD COLUMN review_message VARCHAR(512) NULL COMMENT '审核信息', + ADD COLUMN reviewer_id BIGINT NULL COMMENT '审核人 ID', + ADD COLUMN review_time DATETIME NULL COMMENT '审核时间'; -- 创建基于 reviewStatus 列的索引 -CREATE INDEX idx_reviewStatus ON picture (reviewStatus); \ No newline at end of file +CREATE INDEX idx_reviewStatus ON picture (review_status); \ No newline at end of file diff --git a/src/main/java/edu/whut/smilepicturebackend/controller/PictureController.java b/src/main/java/edu/whut/smilepicturebackend/controller/PictureController.java index 80bc839..7a831fa 100644 --- a/src/main/java/edu/whut/smilepicturebackend/controller/PictureController.java +++ b/src/main/java/edu/whut/smilepicturebackend/controller/PictureController.java @@ -13,6 +13,7 @@ import edu.whut.smilepicturebackend.exception.ThrowUtils; import edu.whut.smilepicturebackend.model.dto.picture.*; import edu.whut.smilepicturebackend.model.entity.Picture; import edu.whut.smilepicturebackend.model.entity.User; +import edu.whut.smilepicturebackend.model.enums.PictureReviewStatusEnum; import edu.whut.smilepicturebackend.model.vo.PictureTagCategory; import edu.whut.smilepicturebackend.model.vo.PictureVO; import edu.whut.smilepicturebackend.service.PictureService; @@ -49,6 +50,19 @@ public class PictureController { return ResultUtils.success(pictureVO); } + /** + * 通过 URL 上传图片(可重新上传) + */ + @PostMapping("/upload/url") + public BaseResponse uploadPictureByUrl( + @RequestBody PictureUploadRequest pictureUploadRequest, + HttpServletRequest request) { + User loginUser = userService.getLoginUser(request); + String fileUrl = pictureUploadRequest.getFileUrl(); + PictureVO pictureVO = pictureService.uploadPicture(fileUrl, pictureUploadRequest, loginUser); + return ResultUtils.success(pictureVO); + } + /** * 删除图片 * @param deleteRequest @@ -103,6 +117,9 @@ public class PictureController { long id = pictureUpdateRequest.getId(); Picture oldPicture = pictureService.getById(id); ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR); + // 补充审核参数 + User loginUser=userService.getLoginUser(request); + pictureService.fillReviewParams(picture, loginUser); // 操作数据库 boolean result = pictureService.updateById(picture); @@ -133,6 +150,7 @@ public class PictureController { // 查询数据库 Picture picture = pictureService.getById(id); ThrowUtils.throwIf(picture == null, ErrorCode.NOT_FOUND_ERROR); + ThrowUtils.throwIf(PictureReviewStatusEnum.PASS.getValue()!=picture.getReviewStatus(),ErrorCode.NOT_FOUND_ERROR); // 获取封装类 return ResultUtils.success(pictureService.getPictureVO(picture, request)); } @@ -152,7 +170,7 @@ public class PictureController { } /** - * 分页获取图片列表(封装类,脱敏) + * 分页获取图片列表(封装类,脱敏),普通用户使用,且不能看到未过审的图片 */ @PostMapping("/list/page/vo") public BaseResponse> listPictureVOByPage(@RequestBody PictureQueryRequest pictureQueryRequest, @@ -161,6 +179,8 @@ public class PictureController { long size = pictureQueryRequest.getPageSize(); // 限制爬虫,一次不能请求超过20页 ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR); + // 普通用户默认只能看到审核通过的数据 + pictureQueryRequest.setReviewStatus(PictureReviewStatusEnum.PASS.getValue()); // 查询数据库 Page picturePage = pictureService.page(new Page<>(current, size), pictureService.getQueryWrapper(pictureQueryRequest)); diff --git a/src/main/java/edu/whut/smilepicturebackend/manager/upload/FilePictureUpload.java b/src/main/java/edu/whut/smilepicturebackend/manager/upload/FilePictureUpload.java new file mode 100644 index 0000000..e74f660 --- /dev/null +++ b/src/main/java/edu/whut/smilepicturebackend/manager/upload/FilePictureUpload.java @@ -0,0 +1,45 @@ +package edu.whut.smilepicturebackend.manager.upload; + +import cn.hutool.core.io.FileUtil; +import edu.whut.smilepicturebackend.exception.ErrorCode; +import edu.whut.smilepicturebackend.exception.ThrowUtils; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.util.Arrays; +import java.util.List; + +/** + * 文件图片上传 + */ +@Service +public class FilePictureUpload extends PictureUploadTemplate { + + @Override + protected void validPicture(Object inputSource) { + MultipartFile multipartFile = (MultipartFile) inputSource; + ThrowUtils.throwIf(multipartFile == null, ErrorCode.PARAMS_ERROR, "文件不能为空"); + // 1. 校验文件大小 + long fileSize = multipartFile.getSize(); + final long ONE_M = 1024 * 1024; + ThrowUtils.throwIf(fileSize > 2 * ONE_M, ErrorCode.PARAMS_ERROR, "文件大小不能超过 2MB"); + // 2. 校验文件后缀 + String fileSuffix = FileUtil.getSuffix(multipartFile.getOriginalFilename()); + // 允许上传的文件后缀列表(或者集合) + final List ALLOW_FORMAT_LIST = Arrays.asList("jpeg", "png", "jpg", "webp"); + ThrowUtils.throwIf(!ALLOW_FORMAT_LIST.contains(fileSuffix), ErrorCode.PARAMS_ERROR, "文件类型错误"); + } + + @Override + protected String getOriginFilename(Object inputSource) { + MultipartFile multipartFile = (MultipartFile) inputSource; + return multipartFile.getOriginalFilename(); + } + + @Override + protected void processFile(Object inputSource, File file) throws Exception { + MultipartFile multipartFile = (MultipartFile) inputSource; + multipartFile.transferTo(file); + } +} diff --git a/src/main/java/edu/whut/smilepicturebackend/manager/upload/PictureUploadTemplate.java b/src/main/java/edu/whut/smilepicturebackend/manager/upload/PictureUploadTemplate.java new file mode 100644 index 0000000..124fdc2 --- /dev/null +++ b/src/main/java/edu/whut/smilepicturebackend/manager/upload/PictureUploadTemplate.java @@ -0,0 +1,144 @@ +package edu.whut.smilepicturebackend.manager.upload; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.NumberUtil; +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 edu.whut.smilepicturebackend.config.CosClientConfig; +import edu.whut.smilepicturebackend.exception.BusinessException; +import edu.whut.smilepicturebackend.exception.ErrorCode; +import edu.whut.smilepicturebackend.manager.CosManager; +import edu.whut.smilepicturebackend.model.file.UploadPictureResult; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Resource; +import java.io.File; +import java.util.Date; + +/** + * 图片上传模板 + */ +@Slf4j +public abstract class PictureUploadTemplate { + + @Resource + private CosClientConfig cosClientConfig; + + @Resource + private CosManager cosManager; + + /** + * 上传图片 + * + * @param inputSource 文件 + * @param uploadPathPrefix 上传路径前缀 + * @return + */ + public UploadPictureResult uploadPicture(Object inputSource, String uploadPathPrefix) { + // 1. 校验图片 + validPicture(inputSource); + // 2. 图片上传地址 + String uuid = RandomUtil.randomString(16); + String originalFilename = getOriginFilename(inputSource); + // 自己拼接文件上传路径,而不是使用原始文件名称,可以增强安全性 + String uploadFilename = String.format("%s_%s.%s", DateUtil.formatDate(new Date()), uuid, + FileUtil.getSuffix(originalFilename)); + //如果多个项目共享存储桶,请在桶的根目录下以各项目名作为目录。 + String projectName="smile-picture"; + String uploadPath = String.format("/%s/%s/%s",projectName, uploadPathPrefix, uploadFilename); + File file = null; + try { + // 3. 创建临时文件,获取文件到服务器 + file = File.createTempFile(uploadPath, null); + // 处理文件来源 + processFile(inputSource, file); + // 4. 上传图片到对象存储 + PutObjectResult putObjectResult = cosManager.putPictureObject(uploadPath, file); + // 5. 获取图片信息对象,封装返回结果 + ImageInfo imageInfo = putObjectResult.getCiUploadResult().getOriginalInfo().getImageInfo(); + return buildResult(originalFilename, file, uploadPath, imageInfo); + } catch (Exception e) { + log.error("图片上传到对象存储失败", e); + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "上传失败"); + } finally { + // 6. 临时文件清理 + this.deleteTempFile(file); + } + + } + + /** + * 校验输入源(本地文件或 URL) + */ + protected abstract void validPicture(Object inputSource); + + /** + * 获取输入源的原始文件名 + */ + protected abstract String getOriginFilename(Object inputSource); + + /** + * 处理输入源并生成本地临时文件 + */ + protected abstract void processFile(Object inputSource, File file) throws Exception; + + + /** + * 封装返回结果 + * + * @param originalFilename + * @param file + * @param uploadPath + * @param imageInfo 对象存储返回的图片信息 + * @return + */ + private UploadPictureResult buildResult(String originalFilename, File file, String uploadPath, ImageInfo imageInfo) { + // 计算宽高 + int picWidth = imageInfo.getWidth(); + int picHeight = imageInfo.getHeight(); + double picScale = NumberUtil.round(picWidth * 1.0 / picHeight, 2).doubleValue(); + // 封装返回结果 + UploadPictureResult uploadPictureResult = new UploadPictureResult(); + uploadPictureResult.setUrl(cosClientConfig.getHost() + "/" + uploadPath); + uploadPictureResult.setName(FileUtil.mainName(originalFilename)); + uploadPictureResult.setPicSize(FileUtil.size(file)); + uploadPictureResult.setPicWidth(picWidth); + uploadPictureResult.setPicHeight(picHeight); + uploadPictureResult.setPicScale(picScale); + uploadPictureResult.setPicFormat(imageInfo.getFormat()); + uploadPictureResult.setPicColor(imageInfo.getAve()); + // 返回可访问的地址 + return uploadPictureResult; + } + + /** + * 清理临时文件 + * + * @param file + */ + public void deleteTempFile(File file) { + if (file == null) { + return; + } + // 删除临时文件 + boolean deleteResult = file.delete(); + if (!deleteResult) { + log.error("file delete error, filepath = {}", file.getAbsolutePath()); + } + } +} + + + + + + + + + + + + + diff --git a/src/main/java/edu/whut/smilepicturebackend/manager/upload/UrlPictureUpload.java b/src/main/java/edu/whut/smilepicturebackend/manager/upload/UrlPictureUpload.java new file mode 100644 index 0000000..c56db43 --- /dev/null +++ b/src/main/java/edu/whut/smilepicturebackend/manager/upload/UrlPictureUpload.java @@ -0,0 +1,91 @@ +package edu.whut.smilepicturebackend.manager.upload; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpStatus; +import cn.hutool.http.HttpUtil; +import cn.hutool.http.Method; +import edu.whut.smilepicturebackend.exception.BusinessException; +import edu.whut.smilepicturebackend.exception.ErrorCode; +import edu.whut.smilepicturebackend.exception.ThrowUtils; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; +import java.util.List; + +/** + * URL 图片上传 + */ +@Service +public class UrlPictureUpload extends PictureUploadTemplate { + + @Override + protected void validPicture(Object inputSource) { + String fileUrl = (String) inputSource; + // 1. 校验非空 + ThrowUtils.throwIf(StrUtil.isBlank(fileUrl), ErrorCode.PARAMS_ERROR, "文件地址为空"); + + // 2. 校验 URL 格式 + try { + new URL(fileUrl); + } catch (MalformedURLException e) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件地址格式不正确"); + } + // 3. 校验 URL 的协议 + 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; + } + // 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, "文件类型错误"); + } + // 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(); + } + } + } + + @Override + protected String getOriginFilename(Object inputSource) { + String fileUrl = (String) inputSource; + return FileUtil.mainName(fileUrl); + } + + @Override + protected void processFile(Object inputSource, File file) throws Exception { + String fileUrl = (String) inputSource; + // 下载文件到临时目录 + HttpUtil.downloadFile(fileUrl, file); + } +} diff --git a/src/main/java/edu/whut/smilepicturebackend/model/dto/picture/PictureUploadRequest.java b/src/main/java/edu/whut/smilepicturebackend/model/dto/picture/PictureUploadRequest.java index 8ea657f..1172c90 100644 --- a/src/main/java/edu/whut/smilepicturebackend/model/dto/picture/PictureUploadRequest.java +++ b/src/main/java/edu/whut/smilepicturebackend/model/dto/picture/PictureUploadRequest.java @@ -17,5 +17,10 @@ public class PictureUploadRequest implements Serializable { */ private Long id; + /** + * 文件地址 + */ + private String fileUrl; + private static final long serialVersionUID = 1L; } \ No newline at end of file diff --git a/src/main/java/edu/whut/smilepicturebackend/service/PictureService.java b/src/main/java/edu/whut/smilepicturebackend/service/PictureService.java index 0ba95d8..95d78de 100644 --- a/src/main/java/edu/whut/smilepicturebackend/service/PictureService.java +++ b/src/main/java/edu/whut/smilepicturebackend/service/PictureService.java @@ -31,12 +31,12 @@ public interface PictureService extends IService { /** * 上传图片 * - * @param multipartFile 文件输入源 + * @param inputSource 文件输入源 * @param pictureUploadRequest * @param loginUser * @return */ - PictureVO uploadPicture(MultipartFile multipartFile, PictureUploadRequest pictureUploadRequest, User loginUser); + PictureVO uploadPicture(Object inputSource, PictureUploadRequest pictureUploadRequest, User loginUser); /** * 获取查询对象 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 2f5550f..ce4d7bb 100644 --- a/src/main/java/edu/whut/smilepicturebackend/service/impl/PictureServiceImpl.java +++ b/src/main/java/edu/whut/smilepicturebackend/service/impl/PictureServiceImpl.java @@ -13,6 +13,9 @@ 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.upload.FilePictureUpload; +import edu.whut.smilepicturebackend.manager.upload.PictureUploadTemplate; +import edu.whut.smilepicturebackend.manager.upload.UrlPictureUpload; import edu.whut.smilepicturebackend.mapper.PictureMapper; import edu.whut.smilepicturebackend.model.dto.picture.PictureEditRequest; import edu.whut.smilepicturebackend.model.dto.picture.PictureQueryRequest; @@ -49,6 +52,8 @@ public class PictureServiceImpl extends ServiceImpl implements PictureService { private final FileManager fileManager; private final UserService userService; + private final FilePictureUpload filePictureUpload; + private final UrlPictureUpload urlPictureUpload; @Override public void validPicture(Picture picture) { @@ -68,7 +73,7 @@ public class PictureServiceImpl extends ServiceImpl } } @Override - public PictureVO uploadPicture(MultipartFile multipartFile, PictureUploadRequest pictureUploadRequest, User loginUser) { + public PictureVO uploadPicture(Object inputSource, PictureUploadRequest pictureUploadRequest, User loginUser) { // 校验参数 ThrowUtils.throwIf(loginUser == null, ErrorCode.NO_AUTH_ERROR); @@ -90,13 +95,19 @@ public class PictureServiceImpl extends ServiceImpl String uploadPathPrefix; //公共图库下,每个用户有自己的userid管理的文件夹。 uploadPathPrefix = String.format("public/%s", loginUser.getId()); - UploadPictureResult uploadPictureResult = fileManager.uploadPicture(multipartFile, uploadPathPrefix); + // 根据 inputSource 的类型区分上传方式 + PictureUploadTemplate pictureUploadTemplate = filePictureUpload; + if (inputSource instanceof String) { + pictureUploadTemplate = urlPictureUpload; + } + UploadPictureResult uploadPictureResult = pictureUploadTemplate.uploadPicture(inputSource, uploadPathPrefix); // 构造要入库的图片信息 Picture picture = new Picture(); // 复制同名属性(url、name、picSize、picWidth、picHeight、picScale、picFormat) BeanUtils.copyProperties(uploadPictureResult, picture); picture.setUserId(loginUser.getId()); - + // 补充审核参数 + this.fillReviewParams(picture, loginUser); // 操作数据库 // 如果 pictureId 不为空,表示更新,否则是新增 if (pictureId != null) { @@ -122,11 +133,14 @@ public class PictureServiceImpl extends ServiceImpl .like(StrUtil.isNotBlank(req.getName()), Picture::getName, req.getName()) .like(StrUtil.isNotBlank(req.getIntroduction()), Picture::getIntroduction, req.getIntroduction()) .like(StrUtil.isNotBlank(req.getPicFormat()), Picture::getPicFormat, req.getPicFormat()) + .eq(ObjUtil.isNotEmpty(req.getReviewMessage()),Picture::getReviewMessage,req.getReviewMessage()) .eq(StrUtil.isNotBlank(req.getCategory()), Picture::getCategory, req.getCategory()) .eq(ObjUtil.isNotEmpty(req.getPicWidth()), Picture::getPicWidth, req.getPicWidth()) .eq(ObjUtil.isNotEmpty(req.getPicHeight()), Picture::getPicHeight, req.getPicHeight()) .eq(ObjUtil.isNotEmpty(req.getPicSize()), Picture::getPicSize, req.getPicSize()) .eq(ObjUtil.isNotEmpty(req.getPicScale()), Picture::getPicScale, req.getPicScale()) + .eq(ObjUtil.isNotEmpty(req.getReviewStatus()),Picture::getReviewStatus,req.getReviewStatus()) + .eq(ObjUtil.isNotEmpty(req.getReviewerId()),Picture::getReviewerId,req.getReviewerId()) .ge(ObjUtil.isNotEmpty(req.getStartEditTime()), Picture::getEditTime, req.getStartEditTime()) .lt(ObjUtil.isNotEmpty(req.getEndEditTime()), Picture::getEditTime, req.getEndEditTime()); @@ -217,6 +231,8 @@ public class PictureServiceImpl extends ServiceImpl picture.setEditTime(new Date()); // 数据校验 this.validPicture(picture); + // 补充审核参数,每次编辑图片都要重新过审 + this.fillReviewParams(picture, loginUser); // 判断是否存在 long id = pictureEditRequest.getId(); Picture oldPicture = this.getById(id);