1. 图片审核与批量抓取系统设计
作为一个长期从事内容管理系统开发的工程师,我深知图片审核和批量处理在实际业务中的重要性。最近在开发智能协图云图库项目时,我实现了一套完整的图片审核流程和批量抓取功能,这套方案在多个项目中都得到了验证。
图片审核系统需要解决的核心问题是:如何在保证用户体验的同时,有效控制平台内容质量。而批量抓取功能则要解决内容快速填充的需求。这两个看似独立的功能,在实际业务中往往是紧密关联的。
2. 图片审核系统实现
2.1 审核流程设计
审核系统的核心在于状态管理。我们设计了三种审核状态:
- 待审核(0):用户刚上传时的默认状态
- 通过(1):管理员审核通过
- 拒绝(2):管理员审核不通过
这种三元状态设计覆盖了所有可能的审核场景,同时为后续的统计分析提供了基础。
数据库层面,我们在原有picture表上增加了四个关键字段:
sql复制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 '审核时间';
提示:为reviewStatus字段添加索引可以显著提升按状态查询的效率,特别是在图片数量大的情况下。
2.2 状态管理实现
在Java代码中,我们使用枚举类来管理审核状态:
java复制@Getter
public enum PictureReviewStatusEnum {
REVIEWING("待审核", 0),
PASS("通过", 1),
REJECT("拒绝", 2);
private final String text;
private final int value;
PictureReviewStatusEnum(String text, int value) {
this.text = text;
this.value = value;
}
public static PictureReviewStatusEnum getEnumByValue(Integer value) {
if (ObjUtil.isEmpty(value)) {
return null;
}
for (PictureReviewStatusEnum pictureReviewStatusEnum : PictureReviewStatusEnum.values()) {
if (pictureReviewStatusEnum.value == value) {
return pictureReviewStatusEnum;
}
}
return null;
}
}
这种枚举设计的好处是:
- 状态值集中管理,避免魔法数字
- 提供了状态值和描述文本的映射
- 支持通过值获取枚举实例
2.3 权限与自动审核
在实际业务中,管理员上传的图片应该自动通过审核。我们通过fillReviewParams方法实现这一逻辑:
java复制@Override
public void fillReviewParams(Picture picture, User loginUser) {
if (userService.isAdmin(loginUser)) {
// 管理员自动过审
picture.setReviewStatus(PictureReviewStatusEnum.PASS.getValue());
picture.setReviewerId(loginUser.getId());
picture.setReviewMessage("管理员自动过审");
picture.setReviewTime(new Date());
} else {
// 普通用户需要审核
picture.setReviewStatus(PictureReviewStatusEnum.REVIEWING.getValue());
}
}
这个方法会在以下操作中被调用:
- 用户上传图片
- 用户编辑图片信息
- 管理员更新图片信息
3. URL图片导入实现
3.1 整体流程
通过URL导入图片的流程分为三步:
- 下载图片:从指定URL下载到临时存储
- 校验图片:检查格式、大小等
- 上传图片:保存到对象存储
核心方法如下:
java复制public UploadPictureResult uploadPictureByUrl(String fileUrl, String uploadPathPrefix) {
// 校验URL
validPicture(fileUrl);
// 生成唯一文件名
String uuid = RandomUtil.randomString(16);
String originFilename = FileUtil.mainName(fileUrl);
String uploadFilename = String.format("%s_%s.%s", DateUtil.formatDate(new Date()), uuid,
FileUtil.getSuffix(originFilename));
String uploadPath = String.format("/%s/%s", uploadPathPrefix, uploadFilename);
File file = null;
try {
// 创建临时文件
file = File.createTempFile(uploadPath, null);
// 下载文件
HttpUtil.downloadFile(fileUrl, file);
// 上传到对象存储...
} finally {
this.deleteTempFile(file);
}
}
3.2 URL校验细节
URL校验是保证系统安全的重要环节,我们实现了全面的校验逻辑:
java复制private void validPicture(String fileUrl) {
// 基础校验
ThrowUtils.throwIf(StrUtil.isBlank(fileUrl), ErrorCode.PARAMS_ERROR, "文件地址不能为空");
// URL格式校验
try {
new URL(fileUrl);
} catch (MalformedURLException e) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件地址格式不正确");
}
// 协议校验
ThrowUtils.throwIf(!(fileUrl.startsWith("http://") || fileUrl.startsWith("https://")),
ErrorCode.PARAMS_ERROR, "仅支持 HTTP 或 HTTPS 协议的文件地址");
// 通过HEAD请求获取文件信息
HttpResponse response = null;
try {
response = HttpUtil.createRequest(Method.HEAD, fileUrl).execute();
// 状态码校验
if (response.getStatus() != HttpStatus.HTTP_OK) {
return;
}
// 文件类型校验
String contentType = response.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, "文件类型错误");
}
// 文件大小校验
String contentLengthStr = response.header("Content-Length");
if (StrUtil.isNotBlank(contentLengthStr)) {
try {
long contentLength = Long.parseLong(contentLengthStr);
final long TWO_MB = 2 * 1024 * 1024L;
ThrowUtils.throwIf(contentLength > TWO_MB, ErrorCode.PARAMS_ERROR, "文件大小不能超过 2M");
} catch (NumberFormatException e) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件大小格式错误");
}
}
} finally {
if (response != null) {
response.close();
}
}
}
注意:使用HEAD方法而不是GET方法进行校验,可以避免下载整个文件,显著提升校验效率。
4. 批量图片抓取实现
4.1 抓取流程设计
批量抓取功能主要解决内容快速填充的需求。我们选择从Bing图片库抓取,流程如下:
- 构造搜索URL
- 获取页面HTML
- 解析图片元素
- 下载并保存图片
4.2 核心实现代码
java复制@Override
public int uploadPictureByBatch(PictureUploadByBatchRequest pictureUploadByBatchRequest, User loginUser) {
String searchText = pictureUploadByBatchRequest.getSearchText();
Integer count = pictureUploadByBatchRequest.getCount();
ThrowUtils.throwIf(count > 30, ErrorCode.PARAMS_ERROR, "最多 30 条");
// 构造请求URL
String fetchUrl = String.format("https://cn.bing.com/images/async?q=%s&mmasync=1", searchText);
// 获取页面
Document document;
try {
document = Jsoup.connect(fetchUrl).get();
} catch (IOException e) {
log.error("获取页面失败", e);
throw new BusinessException(ErrorCode.OPERATION_ERROR, "获取页面失败");
}
// 解析图片元素
Element div = document.getElementsByClass("dgControl").first();
if (ObjUtil.isNull(div)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "获取元素失败");
}
Elements imgElementList = div.select("img.mimg");
int uploadCount = 0;
// 遍历下载图片
for (Element imgElement : imgElementList) {
String fileUrl = imgElement.attr("src");
if (StrUtil.isBlank(fileUrl)) {
log.info("当前链接为空,已跳过: {}", fileUrl);
continue;
}
// 清理URL参数
int questionMarkIndex = fileUrl.indexOf("?");
if (questionMarkIndex > -1) {
fileUrl = fileUrl.substring(0, questionMarkIndex);
}
// 上传图片
PictureUploadRequest pictureUploadRequest = new PictureUploadRequest();
try {
PictureVO pictureVO = this.uploadPicture(fileUrl, pictureUploadRequest, loginUser);
log.info("图片上传成功, id = {}", pictureVO.getId());
uploadCount++;
} catch (Exception e) {
log.error("图片上传失败", e);
continue;
}
if (uploadCount >= count) {
break;
}
}
return uploadCount;
}
4.3 关键点解析
-
URL构造:使用String.format动态生成搜索URL,其中%s会被搜索关键词替换
-
HTML解析:使用Jsoup库解析HTML文档
- document.getElementsByClass()获取特定class的元素
- div.select()使用CSS选择器查找图片元素
-
URL处理:清理图片URL中的查询参数,避免潜在的非法字符问题
-
异常处理:单个图片下载失败不影响整体流程,记录日志后继续处理下一个
5. 实战经验与优化建议
5.1 审核系统优化
-
批量审核:实现批量选择图片并一键审核的功能,提升管理员效率
-
敏感词过滤:在图片描述等文本字段添加敏感词过滤,自动标记可疑内容
-
审核日志:记录完整的审核操作日志,便于追溯和统计
5.2 批量抓取优化
-
异步处理:对于大量图片抓取,可以改为异步任务处理,避免请求超时
-
代理支持:添加代理设置,防止IP被目标网站封禁
-
去重机制:基于图片hash值实现去重,避免重复存储相同图片
5.3 性能优化
-
缓存策略:对频繁访问的已审核图片添加缓存
-
连接池:使用HTTP连接池管理外部请求
-
压缩处理:对大图片进行适当压缩后再存储
在实际项目中,这套系统每天处理数万张图片的审核和数百次的批量抓取操作,运行稳定。关键在于合理的状态设计和完善的异常处理机制。