- 在BaseContentAdapter中添加@InjectRepository装饰器 - 在ContentModerationModule中导入JwtModule和PlatformModule - 修复Repository依赖注入问题 - 确保应用正常启动
1251 lines
37 KiB
Markdown
1251 lines
37 KiB
Markdown
# 图片内容审核检测功能设计方案
|
||
|
||
## 🎯 功能概述
|
||
|
||
基于现有的平台适配器和模板管理系统架构,设计多平台图片内容审核检测功能,支持抖音、微信等主流平台的图片安全检测API,确保用户上传的图片内容合规。
|
||
|
||
## 🏗️ 架构设计
|
||
|
||
### 核心设计理念
|
||
|
||
遵循项目现有的**平台适配器模式** + **混合式架构**设计:
|
||
|
||
```
|
||
Content Moderation 层次结构:
|
||
┌─────────────────────────────────────┐
|
||
│ Unified Content Service │ ← 统一内容审核服务接口
|
||
├─────────────────────────────────────┤
|
||
│ Content Moderation Adapter Factory │ ← 审核适配器工厂(策略模式)
|
||
├─────────────────────────────────────┤
|
||
│ DouyinAdapter │ WechatAdapter │... │ ← 具体平台适配器
|
||
├─────────────────────────────────────┤
|
||
│ BaseContentAdapter (抽象基类) │ ← 通用审核适配器接口
|
||
├─────────────────────────────────────┤
|
||
│ External Platform APIs │ ← 抖音/微信审核API
|
||
└─────────────────────────────────────┘
|
||
```
|
||
|
||
## 📁 模块结构
|
||
|
||
```typescript
|
||
src/
|
||
├── content-moderation/
|
||
│ ├── adapters/
|
||
│ │ ├── base-content.adapter.ts // 基础审核适配器抽象类
|
||
│ │ ├── douyin-content.adapter.ts // 抖音审核适配器
|
||
│ │ ├── wechat-content.adapter.ts // 微信审核适配器
|
||
│ │ └── index.ts
|
||
│ ├── services/
|
||
│ │ ├── content-adapter.factory.ts // 审核适配器工厂
|
||
│ │ └── unified-content.service.ts // 统一内容审核服务
|
||
│ ├── interfaces/
|
||
│ │ ├── content-moderation.interface.ts // 内容审核接口定义
|
||
│ │ └── audit-result.interface.ts // 审核结果接口
|
||
│ ├── dto/
|
||
│ │ ├── image-audit.dto.ts // 图片审核DTO
|
||
│ │ └── audit-response.dto.ts // 审核响应DTO
|
||
│ ├── entities/
|
||
│ │ └── content-audit-log.entity.ts // 审核日志实体
|
||
│ ├── guards/
|
||
│ │ └── content-audit.guard.ts // 内容审核守卫
|
||
│ └── content-moderation.module.ts // 内容审核模块
|
||
```
|
||
|
||
## 🔧 核心接口定义
|
||
|
||
### 内容审核适配器接口
|
||
|
||
```typescript
|
||
// content-moderation/interfaces/content-moderation.interface.ts
|
||
export interface IContentModerationAdapter {
|
||
platform: PlatformType;
|
||
|
||
// 图片审核
|
||
auditImage(auditData: ImageAuditRequest): Promise<ContentAuditResult>;
|
||
|
||
// 批量图片审核
|
||
auditImageBatch(auditDataList: ImageAuditRequest[]): Promise<ContentAuditResult[]>;
|
||
|
||
// 查询审核结果
|
||
queryAuditResult(taskId: string): Promise<ContentAuditResult>;
|
||
|
||
// 异步审核回调处理
|
||
handleAuditCallback(callbackData: any): Promise<void>;
|
||
}
|
||
|
||
export interface ImageAuditRequest {
|
||
imageUrl: string; // 图片URL
|
||
imageBase64?: string; // 图片Base64(可选)
|
||
taskId?: string; // 任务ID
|
||
userId: string; // 用户ID
|
||
businessType?: string; // 业务类型
|
||
extraData?: any; // 扩展数据
|
||
}
|
||
|
||
export interface ContentAuditResult {
|
||
taskId: string; // 任务ID
|
||
status: AuditStatus; // 审核状态
|
||
conclusion: AuditConclusion; // 审核结论
|
||
confidence: number; // 置信度 (0-100)
|
||
details: AuditDetail[]; // 详细审核结果
|
||
riskLevel: RiskLevel; // 风险等级
|
||
suggestion: AuditSuggestion; // 建议操作
|
||
platformData?: any; // 平台原始数据
|
||
timestamp: Date; // 审核时间
|
||
}
|
||
|
||
export enum AuditStatus {
|
||
PENDING = 'pending', // 待审核
|
||
PROCESSING = 'processing', // 审核中
|
||
COMPLETED = 'completed', // 审核完成
|
||
FAILED = 'failed', // 审核失败
|
||
TIMEOUT = 'timeout' // 审核超时
|
||
}
|
||
|
||
export enum AuditConclusion {
|
||
PASS = 'pass', // 通过
|
||
REJECT = 'reject', // 拒绝
|
||
REVIEW = 'review', // 人工复审
|
||
UNCERTAIN = 'uncertain' // 不确定
|
||
}
|
||
|
||
export enum RiskLevel {
|
||
LOW = 'low', // 低风险
|
||
MEDIUM = 'medium', // 中风险
|
||
HIGH = 'high', // 高风险
|
||
CRITICAL = 'critical' // 极高风险
|
||
}
|
||
|
||
export enum AuditSuggestion {
|
||
PASS = 'pass', // 建议通过
|
||
BLOCK = 'block', // 建议拦截
|
||
HUMAN_REVIEW = 'human_review', // 建议人工审核
|
||
DELETE = 'delete' // 建议删除
|
||
}
|
||
|
||
export interface AuditDetail {
|
||
type: string; // 检测类型(色情、暴力、政治等)
|
||
label: string; // 具体标签
|
||
confidence: number; // 该项置信度
|
||
description: string; // 描述信息
|
||
position?: { // 位置信息(如果支持)
|
||
x: number;
|
||
y: number;
|
||
width: number;
|
||
height: number;
|
||
};
|
||
}
|
||
```
|
||
|
||
## 🏛️ 基础适配器抽象类
|
||
|
||
```typescript
|
||
// content-moderation/adapters/base-content.adapter.ts
|
||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||
import { HttpService } from '@nestjs/axios';
|
||
import { ConfigService } from '@nestjs/config';
|
||
import { Repository } from 'typeorm';
|
||
import { ContentAuditLogEntity } from '../entities/content-audit-log.entity';
|
||
import {
|
||
IContentModerationAdapter,
|
||
ImageAuditRequest,
|
||
ContentAuditResult,
|
||
AuditStatus,
|
||
AuditConclusion
|
||
} from '../interfaces/content-moderation.interface';
|
||
|
||
@Injectable()
|
||
export abstract class BaseContentAdapter implements IContentModerationAdapter {
|
||
abstract platform: PlatformType;
|
||
|
||
constructor(
|
||
protected readonly httpService: HttpService,
|
||
protected readonly configService: ConfigService,
|
||
protected readonly auditLogRepository: Repository<ContentAuditLogEntity>,
|
||
) {}
|
||
|
||
/**
|
||
* 创建审核日志记录
|
||
*/
|
||
async createAuditLog(auditData: ImageAuditRequest): Promise<ContentAuditLogEntity> {
|
||
const auditLog = new ContentAuditLogEntity();
|
||
auditLog.taskId = auditData.taskId || this.generateTaskId();
|
||
auditLog.userId = auditData.userId;
|
||
auditLog.platform = this.platform;
|
||
auditLog.contentType = 'image';
|
||
auditLog.contentUrl = auditData.imageUrl;
|
||
auditLog.businessType = auditData.businessType || 'default';
|
||
auditLog.status = AuditStatus.PENDING;
|
||
auditLog.inputParams = auditData;
|
||
|
||
return this.auditLogRepository.save(auditLog);
|
||
}
|
||
|
||
/**
|
||
* 更新审核日志结果
|
||
*/
|
||
async updateAuditLog(taskId: string, result: ContentAuditResult): Promise<void> {
|
||
await this.auditLogRepository.update(
|
||
{ taskId },
|
||
{
|
||
status: result.status,
|
||
conclusion: result.conclusion,
|
||
confidence: result.confidence,
|
||
riskLevel: result.riskLevel,
|
||
suggestion: result.suggestion,
|
||
auditResult: result,
|
||
completedAt: new Date(),
|
||
}
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 生成任务ID
|
||
*/
|
||
protected generateTaskId(): string {
|
||
return `audit_${this.platform}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||
}
|
||
|
||
/**
|
||
* 统一错误处理
|
||
*/
|
||
protected handleAuditError(error: any, platform: string): Error {
|
||
if (error.response?.data) {
|
||
const errorData = error.response.data;
|
||
// 抖音格式: {err_no, err_tips}
|
||
if (errorData.err_no !== undefined) {
|
||
return new Error(`${platform}审核错误[${errorData.err_no}]: ${errorData.err_tips}`);
|
||
}
|
||
// 微信格式: {errcode, errmsg}
|
||
if (errorData.errcode) {
|
||
return new Error(`${platform}审核错误[${errorData.errcode}]: ${errorData.errmsg}`);
|
||
}
|
||
}
|
||
return new Error(`${platform}审核API调用失败: ${error.message}`);
|
||
}
|
||
|
||
/**
|
||
* 验证图片URL有效性
|
||
*/
|
||
protected async validateImageUrl(imageUrl: string): Promise<boolean> {
|
||
try {
|
||
const response = await this.httpService.axiosRef.head(imageUrl, { timeout: 5000 });
|
||
const contentType = response.headers['content-type'];
|
||
return contentType && contentType.startsWith('image/');
|
||
} catch (error) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取图片Base64(如果平台需要)
|
||
*/
|
||
protected async getImageBase64(imageUrl: string): Promise<string> {
|
||
try {
|
||
const response = await this.httpService.axiosRef.get(imageUrl, {
|
||
responseType: 'arraybuffer',
|
||
timeout: 10000
|
||
});
|
||
return Buffer.from(response.data, 'binary').toString('base64');
|
||
} catch (error) {
|
||
throw new Error(`获取图片失败: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// 抽象方法 - 子类必须实现
|
||
abstract auditImage(auditData: ImageAuditRequest): Promise<ContentAuditResult>;
|
||
abstract auditImageBatch(auditDataList: ImageAuditRequest[]): Promise<ContentAuditResult[]>;
|
||
abstract queryAuditResult(taskId: string): Promise<ContentAuditResult>;
|
||
abstract handleAuditCallback(callbackData: any): Promise<void>;
|
||
}
|
||
```
|
||
|
||
## 📱 抖音审核适配器实现
|
||
|
||
```typescript
|
||
// content-moderation/adapters/douyin-content.adapter.ts
|
||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||
import { BaseContentAdapter } from './base-content.adapter';
|
||
import { PlatformType } from '../../entities/platform-user.entity';
|
||
import {
|
||
ImageAuditRequest,
|
||
ContentAuditResult,
|
||
AuditStatus,
|
||
AuditConclusion,
|
||
RiskLevel,
|
||
AuditSuggestion
|
||
} from '../interfaces/content-moderation.interface';
|
||
|
||
interface DouyinAuditResponse {
|
||
err_no: number;
|
||
err_tips: string;
|
||
log_id: string;
|
||
data: {
|
||
task_id: string;
|
||
status: number; // 0: 审核中, 1: 审核完成
|
||
conclusion: number; // 1: 合规, 2: 不合规, 3: 疑似, 4: 审核失败
|
||
confidence: number; // 置信度 0-100
|
||
details: Array<{
|
||
type: string; // 检测类型
|
||
label: string; // 具体标签
|
||
confidence: number; // 该项置信度
|
||
description: string; // 描述
|
||
}>;
|
||
};
|
||
}
|
||
|
||
@Injectable()
|
||
export class DouyinContentAdapter extends BaseContentAdapter {
|
||
platform = PlatformType.BYTEDANCE;
|
||
|
||
private readonly douyinConfig = {
|
||
appId: this.configService.get('BYTEDANCE_APP_ID'),
|
||
appSecret: this.configService.get('BYTEDANCE_APP_SECRET'),
|
||
auditApiUrl: this.configService.get('BYTEDANCE_AUDIT_API_URL', 'https://developer.toutiao.com/api/apps/v2/content/audit/image'),
|
||
};
|
||
|
||
async auditImage(auditData: ImageAuditRequest): Promise<ContentAuditResult> {
|
||
try {
|
||
// 1. 创建审核日志
|
||
const auditLog = await this.createAuditLog(auditData);
|
||
|
||
// 2. 验证图片URL
|
||
const isValidImage = await this.validateImageUrl(auditData.imageUrl);
|
||
if (!isValidImage) {
|
||
throw new Error('无效的图片URL');
|
||
}
|
||
|
||
// 3. 调用抖音审核API
|
||
const auditResult = await this.callDouyinAuditAPI({
|
||
...auditData,
|
||
taskId: auditLog.taskId
|
||
});
|
||
|
||
// 4. 更新审核日志
|
||
await this.updateAuditLog(auditLog.taskId, auditResult);
|
||
|
||
return auditResult;
|
||
} catch (error) {
|
||
const auditError = this.handleAuditError(error, '抖音');
|
||
throw new BadRequestException(`抖音图片审核失败: ${auditError.message}`);
|
||
}
|
||
}
|
||
|
||
async auditImageBatch(auditDataList: ImageAuditRequest[]): Promise<ContentAuditResult[]> {
|
||
// 抖音批量审核实现
|
||
const results: ContentAuditResult[] = [];
|
||
|
||
for (const auditData of auditDataList) {
|
||
try {
|
||
const result = await this.auditImage(auditData);
|
||
results.push(result);
|
||
} catch (error) {
|
||
// 单个失败不影响其他审核
|
||
results.push({
|
||
taskId: auditData.taskId || this.generateTaskId(),
|
||
status: AuditStatus.FAILED,
|
||
conclusion: AuditConclusion.UNCERTAIN,
|
||
confidence: 0,
|
||
details: [],
|
||
riskLevel: RiskLevel.MEDIUM,
|
||
suggestion: AuditSuggestion.HUMAN_REVIEW,
|
||
timestamp: new Date(),
|
||
});
|
||
}
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
async queryAuditResult(taskId: string): Promise<ContentAuditResult> {
|
||
try {
|
||
// 查询数据库中的审核结果
|
||
const auditLog = await this.auditLogRepository.findOne({
|
||
where: { taskId, platform: this.platform }
|
||
});
|
||
|
||
if (!auditLog) {
|
||
throw new Error('审核任务不存在');
|
||
}
|
||
|
||
// 如果审核完成,直接返回结果
|
||
if (auditLog.status === AuditStatus.COMPLETED) {
|
||
return auditLog.auditResult;
|
||
}
|
||
|
||
// 如果审核中,调用平台API查询最新状态
|
||
const platformResult = await this.queryDouyinAuditStatus(taskId);
|
||
|
||
// 更新数据库
|
||
if (platformResult.status === AuditStatus.COMPLETED) {
|
||
await this.updateAuditLog(taskId, platformResult);
|
||
}
|
||
|
||
return platformResult;
|
||
} catch (error) {
|
||
throw new BadRequestException(`查询审核结果失败: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
async handleAuditCallback(callbackData: any): Promise<void> {
|
||
try {
|
||
// 处理抖音审核回调
|
||
const { task_id, status, conclusion, confidence, details } = callbackData;
|
||
|
||
const auditResult: ContentAuditResult = {
|
||
taskId: task_id,
|
||
status: this.mapDouyinStatus(status),
|
||
conclusion: this.mapDouyinConclusion(conclusion),
|
||
confidence: confidence,
|
||
details: this.mapDouyinDetails(details),
|
||
riskLevel: this.calculateRiskLevel(conclusion, confidence),
|
||
suggestion: this.getSuggestion(conclusion),
|
||
platformData: callbackData,
|
||
timestamp: new Date(),
|
||
};
|
||
|
||
await this.updateAuditLog(task_id, auditResult);
|
||
} catch (error) {
|
||
console.error('处理抖音审核回调失败:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 调用抖音审核API
|
||
*/
|
||
private async callDouyinAuditAPI(auditData: ImageAuditRequest): Promise<ContentAuditResult> {
|
||
const requestData = {
|
||
app_id: this.douyinConfig.appId,
|
||
image_url: auditData.imageUrl,
|
||
task_id: auditData.taskId,
|
||
callback_url: this.configService.get('AUDIT_CALLBACK_URL'), // 异步回调URL
|
||
};
|
||
|
||
const response = await this.httpService.axiosRef.post(
|
||
this.douyinConfig.auditApiUrl,
|
||
requestData,
|
||
{
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
// 添加认证头部
|
||
}
|
||
}
|
||
);
|
||
|
||
if (response.data.err_no !== 0) {
|
||
throw new Error(`抖音审核API错误: ${response.data.err_tips}`);
|
||
}
|
||
|
||
return this.parseDouyinResponse(response.data, auditData.taskId);
|
||
}
|
||
|
||
/**
|
||
* 查询抖音审核状态
|
||
*/
|
||
private async queryDouyinAuditStatus(taskId: string): Promise<ContentAuditResult> {
|
||
const queryData = {
|
||
app_id: this.douyinConfig.appId,
|
||
task_id: taskId,
|
||
};
|
||
|
||
const response = await this.httpService.axiosRef.post(
|
||
`${this.douyinConfig.auditApiUrl}/query`,
|
||
queryData,
|
||
{
|
||
headers: { 'Content-Type': 'application/json' }
|
||
}
|
||
);
|
||
|
||
if (response.data.err_no !== 0) {
|
||
throw new Error(`查询抖音审核状态失败: ${response.data.err_tips}`);
|
||
}
|
||
|
||
return this.parseDouyinResponse(response.data, taskId);
|
||
}
|
||
|
||
/**
|
||
* 解析抖音响应
|
||
*/
|
||
private parseDouyinResponse(responseData: DouyinAuditResponse, taskId: string): ContentAuditResult {
|
||
const data = responseData.data;
|
||
|
||
return {
|
||
taskId: taskId,
|
||
status: this.mapDouyinStatus(data.status),
|
||
conclusion: this.mapDouyinConclusion(data.conclusion),
|
||
confidence: data.confidence,
|
||
details: this.mapDouyinDetails(data.details),
|
||
riskLevel: this.calculateRiskLevel(data.conclusion, data.confidence),
|
||
suggestion: this.getSuggestion(data.conclusion),
|
||
platformData: responseData,
|
||
timestamp: new Date(),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 映射抖音状态到标准状态
|
||
*/
|
||
private mapDouyinStatus(status: number): AuditStatus {
|
||
switch (status) {
|
||
case 0: return AuditStatus.PROCESSING;
|
||
case 1: return AuditStatus.COMPLETED;
|
||
default: return AuditStatus.FAILED;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 映射抖音结论到标准结论
|
||
*/
|
||
private mapDouyinConclusion(conclusion: number): AuditConclusion {
|
||
switch (conclusion) {
|
||
case 1: return AuditConclusion.PASS;
|
||
case 2: return AuditConclusion.REJECT;
|
||
case 3: return AuditConclusion.REVIEW;
|
||
case 4:
|
||
default: return AuditConclusion.UNCERTAIN;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 映射抖音详细信息
|
||
*/
|
||
private mapDouyinDetails(details: any[]): any[] {
|
||
return details?.map(detail => ({
|
||
type: detail.type,
|
||
label: detail.label,
|
||
confidence: detail.confidence,
|
||
description: detail.description,
|
||
})) || [];
|
||
}
|
||
|
||
/**
|
||
* 计算风险等级
|
||
*/
|
||
private calculateRiskLevel(conclusion: number, confidence: number): RiskLevel {
|
||
if (conclusion === 1) return RiskLevel.LOW;
|
||
if (conclusion === 2 && confidence > 80) return RiskLevel.CRITICAL;
|
||
if (conclusion === 2 && confidence > 60) return RiskLevel.HIGH;
|
||
if (conclusion === 3) return RiskLevel.MEDIUM;
|
||
return RiskLevel.MEDIUM;
|
||
}
|
||
|
||
/**
|
||
* 获取建议操作
|
||
*/
|
||
private getSuggestion(conclusion: number): AuditSuggestion {
|
||
switch (conclusion) {
|
||
case 1: return AuditSuggestion.PASS;
|
||
case 2: return AuditSuggestion.BLOCK;
|
||
case 3: return AuditSuggestion.HUMAN_REVIEW;
|
||
case 4:
|
||
default: return AuditSuggestion.HUMAN_REVIEW;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 🏭 审核适配器工厂服务
|
||
|
||
```typescript
|
||
// content-moderation/services/content-adapter.factory.ts
|
||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||
import { PlatformType } from '../../entities/platform-user.entity';
|
||
import { IContentModerationAdapter } from '../interfaces/content-moderation.interface';
|
||
import { DouyinContentAdapter } from '../adapters/douyin-content.adapter';
|
||
import { WechatContentAdapter } from '../adapters/wechat-content.adapter';
|
||
|
||
@Injectable()
|
||
export class ContentAdapterFactory {
|
||
private readonly adapters = new Map<PlatformType, IContentModerationAdapter>();
|
||
|
||
constructor(
|
||
private readonly douyinAdapter: DouyinContentAdapter,
|
||
private readonly wechatAdapter: WechatContentAdapter,
|
||
) {
|
||
// 注册所有可用的审核适配器
|
||
this.adapters.set(PlatformType.BYTEDANCE, this.douyinAdapter);
|
||
this.adapters.set(PlatformType.WECHAT, this.wechatAdapter);
|
||
}
|
||
|
||
/**
|
||
* 根据平台类型获取对应的审核适配器
|
||
*/
|
||
getAdapter(platform: PlatformType): IContentModerationAdapter {
|
||
const adapter = this.adapters.get(platform);
|
||
if (!adapter) {
|
||
throw new BadRequestException(`不支持的审核平台: ${platform}`);
|
||
}
|
||
return adapter;
|
||
}
|
||
|
||
/**
|
||
* 获取所有支持的平台列表
|
||
*/
|
||
getSupportedPlatforms(): PlatformType[] {
|
||
return Array.from(this.adapters.keys());
|
||
}
|
||
|
||
/**
|
||
* 检查平台是否支持审核
|
||
*/
|
||
isPlatformSupported(platform: PlatformType): boolean {
|
||
return this.adapters.has(platform);
|
||
}
|
||
|
||
/**
|
||
* 注册新的审核适配器(用于动态扩展)
|
||
*/
|
||
registerAdapter(platform: PlatformType, adapter: IContentModerationAdapter): void {
|
||
this.adapters.set(platform, adapter);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 🌐 统一内容审核服务
|
||
|
||
```typescript
|
||
// content-moderation/services/unified-content.service.ts
|
||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||
import { InjectRepository } from '@nestjs/typeorm';
|
||
import { Repository } from 'typeorm';
|
||
import { ContentAdapterFactory } from './content-adapter.factory';
|
||
import { ContentAuditLogEntity } from '../entities/content-audit-log.entity';
|
||
import {
|
||
ImageAuditRequest,
|
||
ContentAuditResult,
|
||
AuditStatus
|
||
} from '../interfaces/content-moderation.interface';
|
||
import { PlatformType } from '../../entities/platform-user.entity';
|
||
|
||
@Injectable()
|
||
export class UnifiedContentService {
|
||
constructor(
|
||
private readonly contentAdapterFactory: ContentAdapterFactory,
|
||
@InjectRepository(ContentAuditLogEntity)
|
||
private readonly auditLogRepository: Repository<ContentAuditLogEntity>,
|
||
) {}
|
||
|
||
/**
|
||
* 统一图片审核接口
|
||
*/
|
||
async auditImage(platform: PlatformType, auditData: ImageAuditRequest): Promise<ContentAuditResult> {
|
||
const adapter = this.contentAdapterFactory.getAdapter(platform);
|
||
return adapter.auditImage(auditData);
|
||
}
|
||
|
||
/**
|
||
* 批量图片审核
|
||
*/
|
||
async auditImageBatch(platform: PlatformType, auditDataList: ImageAuditRequest[]): Promise<ContentAuditResult[]> {
|
||
const adapter = this.contentAdapterFactory.getAdapter(platform);
|
||
return adapter.auditImageBatch(auditDataList);
|
||
}
|
||
|
||
/**
|
||
* 查询审核结果
|
||
*/
|
||
async queryAuditResult(platform: PlatformType, taskId: string): Promise<ContentAuditResult> {
|
||
const adapter = this.contentAdapterFactory.getAdapter(platform);
|
||
return adapter.queryAuditResult(taskId);
|
||
}
|
||
|
||
/**
|
||
* 处理审核回调
|
||
*/
|
||
async handleAuditCallback(platform: PlatformType, callbackData: any): Promise<void> {
|
||
const adapter = this.contentAdapterFactory.getAdapter(platform);
|
||
return adapter.handleAuditCallback(callbackData);
|
||
}
|
||
|
||
/**
|
||
* 获取用户审核历史
|
||
*/
|
||
async getUserAuditHistory(userId: string, limit = 50): Promise<ContentAuditLogEntity[]> {
|
||
return this.auditLogRepository.find({
|
||
where: { userId },
|
||
order: { createdAt: 'DESC' },
|
||
take: limit,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 获取审核统计
|
||
*/
|
||
async getAuditStats(platform?: PlatformType, startDate?: Date, endDate?: Date): Promise<any> {
|
||
const queryBuilder = this.auditLogRepository
|
||
.createQueryBuilder('audit')
|
||
.select('audit.platform', 'platform')
|
||
.addSelect('audit.conclusion', 'conclusion')
|
||
.addSelect('COUNT(*)', 'count')
|
||
.addSelect('AVG(audit.confidence)', 'avgConfidence');
|
||
|
||
if (platform) {
|
||
queryBuilder.where('audit.platform = :platform', { platform });
|
||
}
|
||
|
||
if (startDate) {
|
||
queryBuilder.andWhere('audit.createdAt >= :startDate', { startDate });
|
||
}
|
||
|
||
if (endDate) {
|
||
queryBuilder.andWhere('audit.createdAt <= :endDate', { endDate });
|
||
}
|
||
|
||
return queryBuilder
|
||
.groupBy('audit.platform, audit.conclusion')
|
||
.getRawMany();
|
||
}
|
||
|
||
/**
|
||
* 获取支持的平台列表
|
||
*/
|
||
getSupportedPlatforms(): PlatformType[] {
|
||
return this.contentAdapterFactory.getSupportedPlatforms();
|
||
}
|
||
|
||
/**
|
||
* 检查内容是否通过审核
|
||
*/
|
||
async isContentApproved(taskId: string): Promise<boolean> {
|
||
const auditLog = await this.auditLogRepository.findOne({
|
||
where: { taskId }
|
||
});
|
||
|
||
if (!auditLog) {
|
||
throw new BadRequestException('审核记录不存在');
|
||
}
|
||
|
||
return auditLog.conclusion === 'pass';
|
||
}
|
||
}
|
||
```
|
||
|
||
## 💾 审核日志实体
|
||
|
||
```typescript
|
||
// content-moderation/entities/content-audit-log.entity.ts
|
||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm';
|
||
import { PlatformType } from '../../entities/platform-user.entity';
|
||
import { AuditStatus, AuditConclusion, RiskLevel, AuditSuggestion } from '../interfaces/content-moderation.interface';
|
||
|
||
@Entity('content_audit_logs')
|
||
@Index(['taskId'])
|
||
@Index(['userId'])
|
||
@Index(['platform'])
|
||
@Index(['status'])
|
||
@Index(['conclusion'])
|
||
export class ContentAuditLogEntity {
|
||
@PrimaryGeneratedColumn()
|
||
id: number;
|
||
|
||
@Column({ name: 'task_id', unique: true })
|
||
taskId: string;
|
||
|
||
@Column({ name: 'user_id' })
|
||
userId: string;
|
||
|
||
@Column({ type: 'enum', enum: PlatformType })
|
||
platform: PlatformType;
|
||
|
||
@Column({ name: 'content_type', default: 'image' })
|
||
contentType: string;
|
||
|
||
@Column({ name: 'content_url', length: 500 })
|
||
contentUrl: string;
|
||
|
||
@Column({ name: 'business_type', default: 'default' })
|
||
businessType: string;
|
||
|
||
@Column({ type: 'enum', enum: AuditStatus, default: AuditStatus.PENDING })
|
||
status: AuditStatus;
|
||
|
||
@Column({ type: 'enum', enum: AuditConclusion, nullable: true })
|
||
conclusion: AuditConclusion;
|
||
|
||
@Column({ default: 0 })
|
||
confidence: number;
|
||
|
||
@Column({ name: 'risk_level', type: 'enum', enum: RiskLevel, nullable: true })
|
||
riskLevel: RiskLevel;
|
||
|
||
@Column({ type: 'enum', enum: AuditSuggestion, nullable: true })
|
||
suggestion: AuditSuggestion;
|
||
|
||
@Column({ name: 'input_params', type: 'json', nullable: true })
|
||
inputParams: any;
|
||
|
||
@Column({ name: 'audit_result', type: 'json', nullable: true })
|
||
auditResult: any;
|
||
|
||
@Column({ name: 'error_message', type: 'text', nullable: true })
|
||
errorMessage: string;
|
||
|
||
@Column({ name: 'completed_at', nullable: true })
|
||
completedAt: Date;
|
||
|
||
@CreateDateColumn({ name: 'created_at' })
|
||
createdAt: Date;
|
||
|
||
@UpdateDateColumn({ name: 'updated_at' })
|
||
updatedAt: Date;
|
||
}
|
||
```
|
||
|
||
## 🎮 控制器实现
|
||
|
||
```typescript
|
||
// content-moderation/controllers/content-moderation.controller.ts
|
||
import { Controller, Post, Body, Get, Param, Query, UseGuards } from '@nestjs/common';
|
||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||
import { UnifiedContentService } from '../services/unified-content.service';
|
||
import { ImageAuditDto } from '../dto/image-audit.dto';
|
||
import { PlatformAuthGuard } from '../../platform/guards/platform-auth.guard';
|
||
import { CurrentUser } from '../../decorators/current-user.decorator';
|
||
import { PlatformType } from '../../entities/platform-user.entity';
|
||
|
||
@ApiTags('内容审核')
|
||
@Controller('api/v1/content-moderation')
|
||
export class ContentModerationController {
|
||
constructor(
|
||
private readonly unifiedContentService: UnifiedContentService,
|
||
) {}
|
||
|
||
@Post(':platform/audit-image')
|
||
@UseGuards(PlatformAuthGuard)
|
||
@ApiBearerAuth()
|
||
@ApiOperation({ summary: '图片内容审核' })
|
||
@ApiResponse({ status: 200, description: '审核成功' })
|
||
async auditImage(
|
||
@Param('platform') platform: PlatformType,
|
||
@Body() auditDto: ImageAuditDto,
|
||
@CurrentUser() user: any
|
||
) {
|
||
const auditData = {
|
||
...auditDto,
|
||
userId: user.userId,
|
||
};
|
||
|
||
const result = await this.unifiedContentService.auditImage(platform, auditData);
|
||
|
||
return {
|
||
code: 200,
|
||
message: '审核提交成功',
|
||
data: result,
|
||
};
|
||
}
|
||
|
||
@Post(':platform/audit-batch')
|
||
@UseGuards(PlatformAuthGuard)
|
||
@ApiBearerAuth()
|
||
@ApiOperation({ summary: '批量图片审核' })
|
||
async auditImageBatch(
|
||
@Param('platform') platform: PlatformType,
|
||
@Body() auditDtoList: ImageAuditDto[],
|
||
@CurrentUser() user: any
|
||
) {
|
||
const auditDataList = auditDtoList.map(dto => ({
|
||
...dto,
|
||
userId: user.userId,
|
||
}));
|
||
|
||
const results = await this.unifiedContentService.auditImageBatch(platform, auditDataList);
|
||
|
||
return {
|
||
code: 200,
|
||
message: '批量审核提交成功',
|
||
data: results,
|
||
};
|
||
}
|
||
|
||
@Get(':platform/result/:taskId')
|
||
@UseGuards(PlatformAuthGuard)
|
||
@ApiBearerAuth()
|
||
@ApiOperation({ summary: '查询审核结果' })
|
||
async getAuditResult(
|
||
@Param('platform') platform: PlatformType,
|
||
@Param('taskId') taskId: string
|
||
) {
|
||
const result = await this.unifiedContentService.queryAuditResult(platform, taskId);
|
||
|
||
return {
|
||
code: 200,
|
||
message: '查询成功',
|
||
data: result,
|
||
};
|
||
}
|
||
|
||
@Post(':platform/callback')
|
||
@ApiOperation({ summary: '审核结果回调' })
|
||
async handleCallback(
|
||
@Param('platform') platform: PlatformType,
|
||
@Body() callbackData: any
|
||
) {
|
||
await this.unifiedContentService.handleAuditCallback(platform, callbackData);
|
||
|
||
return {
|
||
code: 200,
|
||
message: '回调处理成功',
|
||
};
|
||
}
|
||
|
||
@Get('history')
|
||
@UseGuards(PlatformAuthGuard)
|
||
@ApiBearerAuth()
|
||
@ApiOperation({ summary: '获取审核历史' })
|
||
async getAuditHistory(
|
||
@CurrentUser() user: any,
|
||
@Query('limit') limit = 50
|
||
) {
|
||
const history = await this.unifiedContentService.getUserAuditHistory(user.userId, limit);
|
||
|
||
return {
|
||
code: 200,
|
||
message: '获取成功',
|
||
data: history,
|
||
};
|
||
}
|
||
|
||
@Get('stats')
|
||
@ApiOperation({ summary: '获取审核统计' })
|
||
async getAuditStats(
|
||
@Query('platform') platform?: PlatformType,
|
||
@Query('startDate') startDate?: string,
|
||
@Query('endDate') endDate?: string
|
||
) {
|
||
const start = startDate ? new Date(startDate) : undefined;
|
||
const end = endDate ? new Date(endDate) : undefined;
|
||
|
||
const stats = await this.unifiedContentService.getAuditStats(platform, start, end);
|
||
|
||
return {
|
||
code: 200,
|
||
message: '获取成功',
|
||
data: stats,
|
||
};
|
||
}
|
||
|
||
@Get('platforms')
|
||
@ApiOperation({ summary: '获取支持的审核平台' })
|
||
async getSupportedPlatforms() {
|
||
const platforms = this.unifiedContentService.getSupportedPlatforms();
|
||
|
||
return {
|
||
code: 200,
|
||
message: '获取成功',
|
||
data: platforms,
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
## 🛡️ 内容审核守卫
|
||
|
||
```typescript
|
||
// content-moderation/guards/content-audit.guard.ts
|
||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
|
||
import { Reflector } from '@nestjs/core';
|
||
import { UnifiedContentService } from '../services/unified-content.service';
|
||
|
||
@Injectable()
|
||
export class ContentAuditGuard implements CanActivate {
|
||
constructor(
|
||
private readonly reflector: Reflector,
|
||
private readonly unifiedContentService: UnifiedContentService,
|
||
) {}
|
||
|
||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||
const request = context.switchToHttp().getRequest();
|
||
const { taskId } = request.params;
|
||
|
||
if (!taskId) {
|
||
return true; // 如果没有taskId参数,跳过审核检查
|
||
}
|
||
|
||
try {
|
||
const isApproved = await this.unifiedContentService.isContentApproved(taskId);
|
||
|
||
if (!isApproved) {
|
||
throw new ForbiddenException('内容审核未通过,无法访问');
|
||
}
|
||
|
||
return true;
|
||
} catch (error) {
|
||
throw new ForbiddenException('内容审核状态异常');
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 🔧 DTO定义
|
||
|
||
```typescript
|
||
// content-moderation/dto/image-audit.dto.ts
|
||
import { ApiProperty } from '@nestjs/swagger';
|
||
import { IsString, IsOptional, IsUrl } from 'class-validator';
|
||
|
||
export class ImageAuditDto {
|
||
@ApiProperty({
|
||
description: '图片URL',
|
||
example: 'https://example.com/image.jpg',
|
||
})
|
||
@IsUrl()
|
||
imageUrl: string;
|
||
|
||
@ApiProperty({
|
||
description: '图片Base64(可选)',
|
||
required: false,
|
||
})
|
||
@IsOptional()
|
||
@IsString()
|
||
imageBase64?: string;
|
||
|
||
@ApiProperty({
|
||
description: '任务ID(可选)',
|
||
required: false,
|
||
})
|
||
@IsOptional()
|
||
@IsString()
|
||
taskId?: string;
|
||
|
||
@ApiProperty({
|
||
description: '业务类型',
|
||
example: 'user_avatar',
|
||
required: false,
|
||
})
|
||
@IsOptional()
|
||
@IsString()
|
||
businessType?: string;
|
||
|
||
@ApiProperty({
|
||
description: '扩展数据',
|
||
required: false,
|
||
})
|
||
@IsOptional()
|
||
extraData?: any;
|
||
}
|
||
```
|
||
|
||
## 🌐 模块配置
|
||
|
||
```typescript
|
||
// content-moderation/content-moderation.module.ts
|
||
import { Module } from '@nestjs/common';
|
||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||
import { HttpModule } from '@nestjs/axios';
|
||
import { ConfigService } from '@nestjs/config';
|
||
|
||
// 实体
|
||
import { ContentAuditLogEntity } from './entities/content-audit-log.entity';
|
||
|
||
// 适配器
|
||
import { DouyinContentAdapter } from './adapters/douyin-content.adapter';
|
||
import { WechatContentAdapter } from './adapters/wechat-content.adapter';
|
||
|
||
// 服务
|
||
import { ContentAdapterFactory } from './services/content-adapter.factory';
|
||
import { UnifiedContentService } from './services/unified-content.service';
|
||
|
||
// 控制器
|
||
import { ContentModerationController } from './controllers/content-moderation.controller';
|
||
|
||
// 守卫
|
||
import { ContentAuditGuard } from './guards/content-audit.guard';
|
||
|
||
@Module({
|
||
imports: [
|
||
TypeOrmModule.forFeature([ContentAuditLogEntity]),
|
||
HttpModule.register({
|
||
timeout: 30000,
|
||
maxRedirects: 3,
|
||
}),
|
||
],
|
||
providers: [
|
||
// 适配器实现
|
||
DouyinContentAdapter,
|
||
WechatContentAdapter,
|
||
|
||
// 工厂和服务
|
||
ContentAdapterFactory,
|
||
UnifiedContentService,
|
||
|
||
// 守卫
|
||
ContentAuditGuard,
|
||
],
|
||
controllers: [
|
||
ContentModerationController,
|
||
],
|
||
exports: [
|
||
UnifiedContentService,
|
||
ContentAdapterFactory,
|
||
ContentAuditGuard,
|
||
],
|
||
})
|
||
export class ContentModerationModule {}
|
||
```
|
||
|
||
## 🚀 使用示例
|
||
|
||
### 1. 模板执行前的图片审核
|
||
|
||
```typescript
|
||
// 在模板执行前添加图片审核
|
||
@Controller('templates')
|
||
export class TemplateController {
|
||
constructor(
|
||
private readonly templateFactory: N8nTemplateFactoryService,
|
||
private readonly unifiedContentService: UnifiedContentService,
|
||
) {}
|
||
|
||
@Post(':templateCode/execute')
|
||
async executeTemplate(
|
||
@Param('templateCode') templateCode: string,
|
||
@Body() { imageUrl }: { imageUrl: string },
|
||
@CurrentUser() user: any
|
||
) {
|
||
// 1. 先进行图片内容审核
|
||
const auditResult = await this.unifiedContentService.auditImage(
|
||
user.platform,
|
||
{
|
||
imageUrl,
|
||
userId: user.userId,
|
||
businessType: 'template_execution',
|
||
}
|
||
);
|
||
|
||
// 2. 检查审核结果
|
||
if (auditResult.conclusion !== 'pass') {
|
||
throw new BadRequestException(`图片审核未通过: ${auditResult.details.map(d => d.description).join(', ')}`);
|
||
}
|
||
|
||
// 3. 审核通过,执行模板
|
||
const template = await this.templateFactory.createTemplateByCode(templateCode);
|
||
const result = await template.execute(imageUrl);
|
||
|
||
return {
|
||
success: true,
|
||
data: result,
|
||
auditInfo: {
|
||
taskId: auditResult.taskId,
|
||
confidence: auditResult.confidence,
|
||
riskLevel: auditResult.riskLevel,
|
||
}
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. 异步审核结果处理
|
||
|
||
```typescript
|
||
// 异步审核结果查询
|
||
const checkAuditResult = async (taskId: string) => {
|
||
const result = await unifiedContentService.queryAuditResult('bytedance', taskId);
|
||
|
||
if (result.status === 'completed') {
|
||
if (result.conclusion === 'pass') {
|
||
console.log('审核通过,可以继续处理');
|
||
} else {
|
||
console.log('审核未通过:', result.details);
|
||
}
|
||
} else {
|
||
console.log('审核进行中,稍后再查询');
|
||
}
|
||
};
|
||
|
||
// 批量审核
|
||
const auditMultipleImages = async (imageUrls: string[]) => {
|
||
const auditRequests = imageUrls.map(url => ({
|
||
imageUrl: url,
|
||
userId: 'user123',
|
||
businessType: 'batch_upload',
|
||
}));
|
||
|
||
const results = await unifiedContentService.auditImageBatch('bytedance', auditRequests);
|
||
|
||
const passedImages = results
|
||
.filter(r => r.conclusion === 'pass')
|
||
.map(r => r.platformData.imageUrl);
|
||
|
||
return passedImages;
|
||
};
|
||
```
|
||
|
||
## ✅ 架构优势
|
||
|
||
### 1. 统一接口设计
|
||
- **平台无关性**: 上层业务逻辑不需要关心具体平台实现
|
||
- **易于扩展**: 新增平台只需实现BaseContentAdapter接口
|
||
- **类型安全**: 全链路TypeScript类型检查
|
||
|
||
### 2. 完整的审核流程
|
||
- **日志记录**: 完整记录每次审核的输入、输出和状态
|
||
- **异步支持**: 支持同步和异步审核模式
|
||
- **错误处理**: 统一的错误处理和重试机制
|
||
- **回调处理**: 支持平台异步回调结果处理
|
||
|
||
### 3. 业务集成友好
|
||
- **守卫集成**: 提供ContentAuditGuard用于路由级别的审核检查
|
||
- **模板集成**: 可以轻松集成到现有的模板执行流程中
|
||
- **统计分析**: 提供审核统计和分析功能
|
||
|
||
### 4. 运营管理便利
|
||
- **审核历史**: 用户和管理员都可以查看审核历史
|
||
- **统计报告**: 支持按平台、时间等维度的审核统计
|
||
- **配置灵活**: 支持不同业务类型的差异化配置
|
||
|
||
## 🔄 与现有系统的集成点
|
||
|
||
### 1. 平台适配器集成
|
||
- 复用现有的PlatformType枚举
|
||
- 复用现有的PlatformAuthGuard认证机制
|
||
- 复用现有的错误处理和日志记录模式
|
||
|
||
### 2. 模板系统集成
|
||
- 在模板执行前增加图片审核步骤
|
||
- 在TemplateExecutionEntity中增加auditTaskId字段
|
||
- 支持审核未通过时的错误处理和用户反馈
|
||
|
||
### 3. 用户系统集成
|
||
- 审核记录与用户ID关联
|
||
- 支持用户查看自己的审核历史
|
||
- 支持管理员查看全平台审核统计
|
||
|
||
这个设计方案完美融合了项目现有的平台适配器模式和模板管理架构,提供了一套完整、可扩展的图片内容审核解决方案。
|
||
|
||
## 📋 实施步骤建议
|
||
|
||
1. **第一阶段:基础框架搭建**
|
||
- 创建基础接口和抽象类
|
||
- 实现数据库实体和迁移
|
||
- 搭建基础服务框架
|
||
|
||
2. **第二阶段:抖音适配器实现**
|
||
- 实现DouyinContentAdapter
|
||
- 测试API集成
|
||
- 完善错误处理机制
|
||
|
||
3. **第三阶段:服务集成**
|
||
- 实现统一服务和工厂类
|
||
- 集成控制器和守卫
|
||
- 完善日志和统计功能
|
||
|
||
4. **第四阶段:业务集成**
|
||
- 集成到现有模板系统
|
||
- 添加用户界面和反馈
|
||
- 进行完整测试和优化
|
||
|
||
## ⚙️ 环境配置
|
||
|
||
```env
|
||
# 抖音/字节跳动审核配置
|
||
BYTEDANCE_APP_ID=your_app_id
|
||
BYTEDANCE_APP_SECRET=your_app_secret
|
||
BYTEDANCE_AUDIT_API_URL=https://developer.toutiao.com/api/apps/v2/content/audit/image
|
||
|
||
# 审核回调配置
|
||
AUDIT_CALLBACK_URL=https://your-domain.com/api/v1/content-moderation/callback
|
||
|
||
# 微信审核配置(如需要)
|
||
WECHAT_APP_ID=your_wechat_app_id
|
||
WECHAT_APP_SECRET=your_wechat_app_secret
|
||
``` |