内容平台只要开放了用户上传图片的功能,就必然会面对图像审核的问题。靠人工逐张检查显然不现实,接入成熟的图像审核接口是大多数团队的选择。这类接口通常基于深度学习模型,能够识别色情、暴恐、政治敏感、广告二维码等多种违规类型,并返回对应的置信度分数,由业务侧决定拦截或放行。本文将围绕 Spring Boot 项目,完整讲解如何封装一个图像审核服务,从接口调用、结果解析到异步落库,给出可直接复用的实现方案。

一、整体设计思路与依赖准备
在动手写代码之前,先明确审核流程的位置。常见做法是在图片上传成功后、对外可见之前插入一个审核环节:上传接口先把图片写入对象存储,拿到访问 URL,然后调用审核接口判定内容是否合规。审核结果分为三种情况:通过、拒绝、待人工复核。前两种可以直接走自动化处理,第三种则进入后台审核队列。
审核调用有两种模式:同步调用和异步回调。同步模式适合小流量场景,上传请求直接等待审核结果返回;异步模式下,审核服务处理完成后会回调你的接口,适合大并发场景。实际项目中推荐异步模式,因为它不会阻塞上传主流程,接口超时也不会影响用户体验。下面的示例会同时覆盖两种模式。
依赖方面,除了 Spring Boot 本体,还需要用到 HTTP 客户端和 JSON 解析。这里以 RestTemplate 加 Jackson 为例,也可以替换成 WebClient 或 OkHttp:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true<//optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>二、封装图像审核服务
先定义审核结果的统一模型。不同厂商返回的字段差异较大,建议在业务层做一层抽象,屏蔽掉厂商细节,方便后续更换审核服务商。核心字段包括:图片唯一标识、审核结论、命中的违规类型、置信度分数以及原始返回报文。
@Data
@Builder
public class AuditResult {
private String imageId;
private String imageUrl;
/** pass / block / review */
private String conclusion;
/** 命中的违规类型,如 porn、terrorism、ad */
private String label;
/** 置信度 0-100 */
private Double confidence;
/** 原始返回,便于排查问题 */
private String rawResponse;
}接着封装审核客户端。下面的示例模拟了一个通用的 HTTP 审核 API,通过图片 URL 发起检测。注意签名算法各厂商不同,这里用 Hutool 的 SecureUtil 简化处理。生产环境中务必设置合理的连接超时和读取超时,避免审核服务抖动拖垮上传接口。
@Service
@Slf4j
public class ImageAuditService {
@Value("${audit.api-url}")
private String apiUrl;
@Value("${audit.access-key}")
private String accessKey;
@Value("${audit.secret-key}")
private String secretKey;
private final RestTemplate restTemplate;
public ImageAuditService(RestTemplateBuilder builder) {
this.restTemplate = builder
.setConnectTimeout(Duration.ofSeconds(3))
.setReadTimeout(Duration.ofSeconds(5))
.build();
}
public AuditResult audit(String imageId, String imageUrl) {
long ts = System.currentTimeMillis();
String sign = SecureUtil.md5(accessKey + secretKey + ts);
Map<String, Object> params = new HashMap<>();
params.put("accessKey", accessKey);
params.put("timestamp", ts);
params.put("sign", sign);
params.put("imageUrl", imageUrl);
try {
String body = restTemplate.postForObject(apiUrl, params, String.class);
return parseResult(imageId, imageUrl, body);
} catch (RestClientException e) {
log.error("审核接口调用失败, imageId={}", imageId, e);
// 审核失败时的兜底策略:转人工复核,宁可慢不可漏
return AuditResult.builder()
.imageId(imageId)
.imageUrl(imageUrl)
.conclusion("review")
.rawResponse(e.getMessage())
.build();
}
}
private AuditResult parseResult(String imageId, String imageUrl, String body) {
JSONObject json = JSONUtil.parseObj(body);
JSONArray scenes = json.getJSONArray("scenes");
String worstLabel = "normal";
double maxScore = 0;
for (Object o : scenes) {
JSONObject scene = (JSONObject) o;
if (scene.getDouble("score") > maxScore) {
maxScore = scene.getDouble("score");
worstLabel = scene.getStr("label");
}
}
String conclusion = maxScore >= 90 ? "block"
: maxScore >= 75 ? "review" : "pass";
return AuditResult.builder()
.imageId(imageId)
.imageUrl(imageUrl)
.conclusion(conclusion)
.label(worstLabel)
.confidence(maxScore)
.rawResponse(body)
.build();
}
}这里有一个关键细节:置信度阈值的设定。阈值定得太高会有漏网之鱼,定得太低则误杀率上升,用户上传的正常图片被拦截会严重伤害体验。建议将阈值做成配置项而不是硬编码,上线后根据实际误判数据持续调优。分数落在灰色区间的图片一律转人工复核,这是行业通行做法。
三、异步化处理与回调接收
图片上传高峰期,如果同步等待审核结果,接口响应时间会明显变长。更合理的架构是:上传接口只负责存储并返回,审核动作丢到消息队列或线程池中异步执行。Spring 提供的 @Async 注解配合自定义线程池即可满足大多数场景。
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("auditExecutor")
public Executor auditExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("audit-");
// 队列满时由调用线程执行,保证任务不丢
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
@Service
@Slf4j
public class AuditDispatchService {
@Autowired
private ImageAuditService auditService;
@Autowired
private ImageAuditRecordMapper recordMapper;
@Async("auditExecutor")
public void dispatch(String imageId, String imageUrl) {
AuditResult result = auditService.audit(imageId, imageUrl);
// 审核结果落库,供后续追溯和统计
recordMapper.insert(result);
if ("block".equals(result.getConclusion())) {
// 命中拦截,通知业务侧下架或标记
log.warn("图片审核不通过, imageId={}, label={}", imageId, result.getLabel());
}
}
}如果选择异步回调模式,还需要提供一个对外暴露的回调接口。回调接口要做好两件事:一是验签,确认请求确实来自审核服务商,防止恶意伪造回调绕过审核;二是幂等处理,厂商重试机制可能导致同一张图片的回调被推送多次,直接以最新一次结果覆盖即可。
@RestController
@RequestMapping("/callback/audit")
public class AuditCallbackController {
@Autowired
private ImageAuditRecordMapper recordMapper;
@PostMapping("/image")
public Map<String, Object> onImageAuditResult(@RequestBody String body,
@RequestHeader("X-Audit-Sign") String sign) {
// 验签:签名不匹配直接拒绝
if (!SignUtil.verify(body, sign)) {
return Map.of("code", 403, "msg", "invalid sign");
}
JSONObject json = JSONUtil.parseObj(body);
AuditResult result = AuditResult.builder()
.imageId(json.getStr("imageId"))
.imageUrl(json.getStr("imageUrl"))
.conclusion(json.getStr("conclusion"))
.label(json.getStr("label"))
.confidence(json.getDouble("score"))
.rawResponse(body)
.build();
recordMapper.upsert(result); // 幂等写入
return Map.of("code", 0, "msg", "success");
}
}四、数据表设计与上线注意事项
审核记录表建议至少包含这些字段:主键、图片 ID、图片 URL、审核结论、违规类型、置信度、原始报文、审核时间、审核渠道。保留原始报文非常重要,一旦出现误判投诉或需要复盘,原始数据是唯一可信的依据。同时建议按审核时间建立索引,方便统计每日拦截量和误判率。
上线前还有几个容易踩的坑值得提醒。第一,审核接口要有降级方案,当审核服务整体不可用时,新上传的图片可以先标记为待审核状态,暂不对外展示,等审核服务恢复后批量补审。第二,注意图片格式兼容性,部分厂商对 WebP、HEIC 等格式的支持程度不同,必要时在上传阶段统一转码为 JPEG。第三,审核不是一锤子买卖,模型会迭代,历史图片也可能因为政策变化需要重新审核,所以审核记录表要支持按批次补审的查询条件。
最后一点建议:把审核能力沉淀成独立的 starter 模块,通过配置切换不同厂商实现,业务代码只依赖统一的 ImageAuditService 接口。这样未来更换审核服务商时,业务层代码几乎零改动,也方便在测试环境接入模拟审核服务,提升联调效率。一套设计良好的图像审核链路,不仅保障了平台合规,也为后续扩展视频审核、文本审核打下架构基础。
Spring Boot图像审核内容过滤修改时间:2026-09-13 21:27:09