3.11 图片审核完善 用户url传图 抽取模板方法实现图片上传

This commit is contained in:
zhangsan 2025-03-10 18:06:07 +08:00
parent 260e082db7
commit 2513d314b9
8 changed files with 332 additions and 11 deletions

View File

@ -50,10 +50,10 @@ create table if not exists picture
ALTER TABLE picture ALTER TABLE picture
-- 添加新列 -- 添加新列
ADD COLUMN reviewStatus INT DEFAULT 0 NOT NULL COMMENT '审核状态0-待审核; 1-通过; 2-拒绝', ADD COLUMN review_status INT DEFAULT 0 NOT NULL COMMENT '审核状态0-待审核; 1-通过; 2-拒绝',
ADD COLUMN reviewMessage VARCHAR(512) NULL COMMENT '审核信息', ADD COLUMN review_message VARCHAR(512) NULL COMMENT '审核信息',
ADD COLUMN reviewerId BIGINT NULL COMMENT '审核人 ID', ADD COLUMN reviewer_id BIGINT NULL COMMENT '审核人 ID',
ADD COLUMN reviewTime DATETIME NULL COMMENT '审核时间'; ADD COLUMN review_time DATETIME NULL COMMENT '审核时间';
-- 创建基于 reviewStatus 列的索引 -- 创建基于 reviewStatus 列的索引
CREATE INDEX idx_reviewStatus ON picture (reviewStatus); CREATE INDEX idx_reviewStatus ON picture (review_status);

View File

@ -13,6 +13,7 @@ import edu.whut.smilepicturebackend.exception.ThrowUtils;
import edu.whut.smilepicturebackend.model.dto.picture.*; import edu.whut.smilepicturebackend.model.dto.picture.*;
import edu.whut.smilepicturebackend.model.entity.Picture; import edu.whut.smilepicturebackend.model.entity.Picture;
import edu.whut.smilepicturebackend.model.entity.User; 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.PictureTagCategory;
import edu.whut.smilepicturebackend.model.vo.PictureVO; import edu.whut.smilepicturebackend.model.vo.PictureVO;
import edu.whut.smilepicturebackend.service.PictureService; import edu.whut.smilepicturebackend.service.PictureService;
@ -49,6 +50,19 @@ public class PictureController {
return ResultUtils.success(pictureVO); return ResultUtils.success(pictureVO);
} }
/**
* 通过 URL 上传图片可重新上传
*/
@PostMapping("/upload/url")
public BaseResponse<PictureVO> 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 * @param deleteRequest
@ -103,6 +117,9 @@ public class PictureController {
long id = pictureUpdateRequest.getId(); long id = pictureUpdateRequest.getId();
Picture oldPicture = pictureService.getById(id); Picture oldPicture = pictureService.getById(id);
ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR); ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR);
// 补充审核参数
User loginUser=userService.getLoginUser(request);
pictureService.fillReviewParams(picture, loginUser);
// 操作数据库 // 操作数据库
boolean result = pictureService.updateById(picture); boolean result = pictureService.updateById(picture);
@ -133,6 +150,7 @@ public class PictureController {
// 查询数据库 // 查询数据库
Picture picture = pictureService.getById(id); Picture picture = pictureService.getById(id);
ThrowUtils.throwIf(picture == null, ErrorCode.NOT_FOUND_ERROR); 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)); return ResultUtils.success(pictureService.getPictureVO(picture, request));
} }
@ -152,7 +170,7 @@ public class PictureController {
} }
/** /**
* 分页获取图片列表封装类脱敏 * 分页获取图片列表封装类脱敏,普通用户使用且不能看到未过审的图片
*/ */
@PostMapping("/list/page/vo") @PostMapping("/list/page/vo")
public BaseResponse<Page<PictureVO>> listPictureVOByPage(@RequestBody PictureQueryRequest pictureQueryRequest, public BaseResponse<Page<PictureVO>> listPictureVOByPage(@RequestBody PictureQueryRequest pictureQueryRequest,
@ -161,6 +179,8 @@ public class PictureController {
long size = pictureQueryRequest.getPageSize(); long size = pictureQueryRequest.getPageSize();
// 限制爬虫一次不能请求超过20页 // 限制爬虫一次不能请求超过20页
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR); ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
// 普通用户默认只能看到审核通过的数据
pictureQueryRequest.setReviewStatus(PictureReviewStatusEnum.PASS.getValue());
// 查询数据库 // 查询数据库
Page<Picture> picturePage = pictureService.page(new Page<>(current, size), Page<Picture> picturePage = pictureService.page(new Page<>(current, size),
pictureService.getQueryWrapper(pictureQueryRequest)); pictureService.getQueryWrapper(pictureQueryRequest));

View File

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

View File

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

View File

@ -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<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, "文件类型错误");
}
// 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);
}
}

View File

@ -17,5 +17,10 @@ public class PictureUploadRequest implements Serializable {
*/ */
private Long id; private Long id;
/**
* 文件地址
*/
private String fileUrl;
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
} }

View File

@ -31,12 +31,12 @@ public interface PictureService extends IService<Picture> {
/** /**
* 上传图片 * 上传图片
* *
* @param multipartFile 文件输入源 * @param inputSource 文件输入源
* @param pictureUploadRequest * @param pictureUploadRequest
* @param loginUser * @param loginUser
* @return * @return
*/ */
PictureVO uploadPicture(MultipartFile multipartFile, PictureUploadRequest pictureUploadRequest, User loginUser); PictureVO uploadPicture(Object inputSource, PictureUploadRequest pictureUploadRequest, User loginUser);
/** /**
* 获取查询对象 * 获取查询对象

View File

@ -13,6 +13,9 @@ import edu.whut.smilepicturebackend.exception.BusinessException;
import edu.whut.smilepicturebackend.exception.ErrorCode; import edu.whut.smilepicturebackend.exception.ErrorCode;
import edu.whut.smilepicturebackend.exception.ThrowUtils; import edu.whut.smilepicturebackend.exception.ThrowUtils;
import edu.whut.smilepicturebackend.manager.FileManager; 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.mapper.PictureMapper;
import edu.whut.smilepicturebackend.model.dto.picture.PictureEditRequest; import edu.whut.smilepicturebackend.model.dto.picture.PictureEditRequest;
import edu.whut.smilepicturebackend.model.dto.picture.PictureQueryRequest; import edu.whut.smilepicturebackend.model.dto.picture.PictureQueryRequest;
@ -49,6 +52,8 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
implements PictureService { implements PictureService {
private final FileManager fileManager; private final FileManager fileManager;
private final UserService userService; private final UserService userService;
private final FilePictureUpload filePictureUpload;
private final UrlPictureUpload urlPictureUpload;
@Override @Override
public void validPicture(Picture picture) { public void validPicture(Picture picture) {
@ -68,7 +73,7 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
} }
} }
@Override @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); ThrowUtils.throwIf(loginUser == null, ErrorCode.NO_AUTH_ERROR);
@ -90,13 +95,19 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
String uploadPathPrefix; String uploadPathPrefix;
//公共图库下每个用户有自己的userid管理的文件夹 //公共图库下每个用户有自己的userid管理的文件夹
uploadPathPrefix = String.format("public/%s", loginUser.getId()); 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(); Picture picture = new Picture();
// 复制同名属性urlnamepicSizepicWidthpicHeightpicScalepicFormat // 复制同名属性urlnamepicSizepicWidthpicHeightpicScalepicFormat
BeanUtils.copyProperties(uploadPictureResult, picture); BeanUtils.copyProperties(uploadPictureResult, picture);
picture.setUserId(loginUser.getId()); picture.setUserId(loginUser.getId());
// 补充审核参数
this.fillReviewParams(picture, loginUser);
// 操作数据库 // 操作数据库
// 如果 pictureId 不为空表示更新否则是新增 // 如果 pictureId 不为空表示更新否则是新增
if (pictureId != null) { if (pictureId != null) {
@ -122,11 +133,14 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
.like(StrUtil.isNotBlank(req.getName()), Picture::getName, req.getName()) .like(StrUtil.isNotBlank(req.getName()), Picture::getName, req.getName())
.like(StrUtil.isNotBlank(req.getIntroduction()), Picture::getIntroduction, req.getIntroduction()) .like(StrUtil.isNotBlank(req.getIntroduction()), Picture::getIntroduction, req.getIntroduction())
.like(StrUtil.isNotBlank(req.getPicFormat()), Picture::getPicFormat, req.getPicFormat()) .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(StrUtil.isNotBlank(req.getCategory()), Picture::getCategory, req.getCategory())
.eq(ObjUtil.isNotEmpty(req.getPicWidth()), Picture::getPicWidth, req.getPicWidth()) .eq(ObjUtil.isNotEmpty(req.getPicWidth()), Picture::getPicWidth, req.getPicWidth())
.eq(ObjUtil.isNotEmpty(req.getPicHeight()), Picture::getPicHeight, req.getPicHeight()) .eq(ObjUtil.isNotEmpty(req.getPicHeight()), Picture::getPicHeight, req.getPicHeight())
.eq(ObjUtil.isNotEmpty(req.getPicSize()), Picture::getPicSize, req.getPicSize()) .eq(ObjUtil.isNotEmpty(req.getPicSize()), Picture::getPicSize, req.getPicSize())
.eq(ObjUtil.isNotEmpty(req.getPicScale()), Picture::getPicScale, req.getPicScale()) .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()) .ge(ObjUtil.isNotEmpty(req.getStartEditTime()), Picture::getEditTime, req.getStartEditTime())
.lt(ObjUtil.isNotEmpty(req.getEndEditTime()), Picture::getEditTime, req.getEndEditTime()); .lt(ObjUtil.isNotEmpty(req.getEndEditTime()), Picture::getEditTime, req.getEndEditTime());
@ -217,6 +231,8 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
picture.setEditTime(new Date()); picture.setEditTime(new Date());
// 数据校验 // 数据校验
this.validPicture(picture); this.validPicture(picture);
// 补充审核参数,每次编辑图片都要重新过审
this.fillReviewParams(picture, loginUser);
// 判断是否存在 // 判断是否存在
long id = pictureEditRequest.getId(); long id = pictureEditRequest.getId();
Picture oldPicture = this.getById(id); Picture oldPicture = this.getById(id);