解决抖音图片审核"无效图片URL"问题,并设计统一异步架构方案: ## 核心修复 - 增强 validateImageUrl 方法,支持文件扩展名 + Content-Type 双重验证 - 修复抖音审核失败问题:binary/octet-stream 响应头导致的验证失败 ## 架构设计 - 设计统一异步内容审核架构,抹平平台差异(同步/异步) - 创建 EnhancedBaseContentAdapter 统一接口 - 实现平台适配器模式,WeChat同步转异步,Douyin原生异步 - 添加回调驱动的模板执行流程 ## 数据库增强 - 新增 taskId 字段关联审核任务和模板执行 - 添加 PENDING_AUDIT 状态支持异步审核流程 - 创建 MySQL 优化的数据库迁移脚本 ## 文档完善 - 更新内容审核设计文档,包含详细序列图 - 创建 5 阶段升级文档,含 MySQL 专项优化 - 提供完整的向后兼容迁移方案 ## 测试改进 - 更新单元测试,修复 validateStatus 参数 - 增强错误处理和边界情况测试覆盖
1769 lines
52 KiB
Markdown
1769 lines
52 KiB
Markdown
# 图片内容审核检测功能设计方案 (统一异步架构)
|
||
|
||
## 🎯 功能概述
|
||
|
||
基于现有的平台适配器和模板管理系统架构,设计多平台图片内容审核检测功能,支持抖音、微信等主流平台的图片安全检测API。
|
||
|
||
**🚀 核心创新**:采用**统一异步架构**,通过平台差异抹平技术,将同步平台(如微信)适配为异步模式,实现所有平台的一致异步体验。
|
||
|
||
## 🏗️ 架构设计
|
||
|
||
### 核心设计理念
|
||
|
||
**统一异步 + 平台差异抹平**:
|
||
|
||
```
|
||
Enhanced Content Moderation 架构:
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Template Execution Controller │ ← 模板执行控制器
|
||
│ (统一异步模式) │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ Unified Content Service │ ← 统一内容审核服务
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ Content Moderation Adapter Factory │ ← 审核适配器工厂
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ Enhanced Base Content Adapter │ ← 增强基础适配器
|
||
│ (平台差异抹平层) │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ Enhanced │ Enhanced │ Douyin │ ← 平台适配器层
|
||
│ Wechat │ Future │ Adapter │
|
||
│ Adapter │ Platform │ (原生异步) │
|
||
│ (同步→异步) │ Adapters │ │
|
||
├─────────────────────────────────────────────────────────┤
|
||
│ 微信同步API │ 其他平台API │ 抖音异步API │ ← 外部平台API
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 🎯 统一异步执行流程
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant U as 用户
|
||
participant TC as Template Controller
|
||
participant CS as Content Service
|
||
participant WA as Wechat Adapter
|
||
participant DA as Douyin Adapter
|
||
participant DB as Database
|
||
|
||
U->>TC: POST /templates/code/xxx/execute
|
||
TC->>CS: auditImage()
|
||
CS->>WA: auditImage() (同步平台)
|
||
CS->>DA: auditImage() (异步平台)
|
||
|
||
Note over WA,DA: 🎯 统一返回 PROCESSING 状态
|
||
WA-->>CS: {status: PROCESSING}
|
||
DA-->>CS: {status: PROCESSING}
|
||
CS-->>TC: {status: PROCESSING}
|
||
|
||
TC->>DB: 创建执行记录 (PENDING_AUDIT)
|
||
TC-->>U: 返回 executionId 和状态
|
||
|
||
Note over WA: 同步平台立即触发回调
|
||
WA->>WA: setImmediate(handleCallback)
|
||
WA->>TC: handleAuditComplete()
|
||
|
||
Note over DA: 异步平台通过外部回调
|
||
DA-->>TC: 外部回调触发 handleAuditComplete()
|
||
|
||
TC->>DB: 更新状态并执行模板
|
||
|
||
U->>TC: 轮询 /executions/{id}/status
|
||
TC-->>U: 返回最新状态
|
||
```
|
||
|
||
## 📁 模块结构 (增强版)
|
||
|
||
```typescript
|
||
src/
|
||
├── content-moderation/
|
||
│ ├── adapters/
|
||
│ │ ├── base-content.adapter.ts // 原始基础适配器
|
||
│ │ ├── enhanced-base-content.adapter.ts // 🆕 增强基础适配器(平台差异抹平)
|
||
│ │ ├── douyin-content.adapter.ts // 抖音审核适配器(原生异步)
|
||
│ │ ├── wechat-content.adapter.ts // 微信审核适配器(原始同步版本)
|
||
│ │ ├── enhanced-wechat-content.adapter.ts // 🆕 增强微信适配器(同步→异步)
|
||
│ │ └── index.ts
|
||
│ ├── services/
|
||
│ │ ├── content-adapter.factory.ts // 审核适配器工厂
|
||
│ │ └── unified-content.service.ts // 统一内容审核服务
|
||
│ ├── interfaces/
|
||
│ │ ├── content-moderation.interface.ts // 内容审核接口定义
|
||
│ │ └── audit-result.interface.ts // 审核结果接口
|
||
│ ├── controllers/
|
||
│ │ └── content-moderation.controller.ts // 内容审核控制器
|
||
│ ├── dto/
|
||
│ │ ├── image-audit.dto.ts // 图片审核DTO
|
||
│ │ └── audit-response.dto.ts // 审核响应DTO
|
||
│ ├── entities/
|
||
│ │ └── content-audit-log.entity.ts // 审核日志实体
|
||
│ └── guards/
|
||
│ └── content-audit.guard.ts // 内容审核守卫
|
||
├── controllers/
|
||
│ ├── template.controller.ts // 原始模板控制器
|
||
│ └── enhanced-template.controller.ts // 🆕 增强模板控制器(统一异步)
|
||
└── entities/
|
||
└── template-execution.entity.ts // 🔄 模板执行实体(扩展状态枚举)
|
||
│ └── content-moderation.module.ts // 内容审核模块
|
||
```
|
||
|
||
## 🔧 核心接口定义
|
||
|
||
### 🆕 增强执行状态枚举
|
||
|
||
```typescript
|
||
// 扩展原有的 ExecutionStatus 枚举
|
||
enum ExecutionStatus {
|
||
PENDING = 'pending', // 等待中
|
||
PENDING_AUDIT = 'pending_audit', // 🆕 待审核(关键新状态)
|
||
AUDIT_FAILED = 'audit_failed', // 🆕 审核失败
|
||
PROCESSING = 'processing', // 执行中
|
||
COMPLETED = 'completed', // 已完成
|
||
FAILED = 'failed', // 执行失败
|
||
}
|
||
```
|
||
|
||
### 🆕 增强模板执行实体
|
||
|
||
```typescript
|
||
// 扩展 TemplateExecutionEntity
|
||
@Entity('template_executions')
|
||
export class TemplateExecutionEntity {
|
||
// ... 原有字段
|
||
|
||
@Column({ name: 'audit_task_id', nullable: true })
|
||
auditTaskId?: string; // 🆕 关联审核任务ID
|
||
|
||
@Column({
|
||
type: 'enum',
|
||
enum: ExecutionStatus,
|
||
default: ExecutionStatus.PENDING_AUDIT // 🆕 默认状态改为待审核
|
||
})
|
||
status: ExecutionStatus;
|
||
|
||
// ... 其他字段
|
||
}
|
||
```
|
||
|
||
### 🎯 平台差异抹平接口
|
||
|
||
```typescript
|
||
/**
|
||
* 增强基础适配器接口
|
||
*/
|
||
export abstract class EnhancedBaseContentAdapter {
|
||
/** 标识平台特性 */
|
||
abstract readonly isSyncPlatform: boolean;
|
||
|
||
/** 统一异步审核接口 - 所有平台都返回 PROCESSING 状态 */
|
||
async auditImage(auditData: ImageAuditRequest): Promise<ContentAuditResult> {
|
||
// 1. 调用平台API
|
||
const platformResult = await this.callPlatformAuditAPI(auditData);
|
||
|
||
// 2. 统一返回处理中状态
|
||
const unifiedResult = {
|
||
taskId: auditData.taskId,
|
||
status: AuditStatus.PROCESSING, // 🎯 关键:统一异步状态
|
||
conclusion: AuditConclusion.UNCERTAIN,
|
||
// ...
|
||
};
|
||
|
||
// 3. 同步平台立即触发回调
|
||
if (this.isSyncPlatform) {
|
||
setImmediate(() => this.simulateCallback(platformResult));
|
||
}
|
||
|
||
return unifiedResult;
|
||
}
|
||
|
||
/** 平台特定API调用 */
|
||
protected abstract callPlatformAuditAPI(auditData: ImageAuditRequest): Promise<any>;
|
||
|
||
/** 格式化回调数据 */
|
||
protected abstract formatCallbackData(platformResult: any): any;
|
||
}
|
||
```
|
||
|
||
### 内容审核适配器接口
|
||
|
||
```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;
|
||
};
|
||
}
|
||
```
|
||
|
||
## 🚀 统一异步架构核心设计
|
||
|
||
### 🎯 设计原则
|
||
|
||
1. **平台无关性**:业务逻辑不关心底层平台是同步还是异步
|
||
2. **统一体验**:所有平台都提供一致的异步执行体验
|
||
3. **状态驱动**:通过状态管理驱动整个执行流程
|
||
4. **回调集成**:审核完成后自动触发模板执行
|
||
|
||
### 🔄 关键执行流程
|
||
|
||
```typescript
|
||
// 🎯 统一异步执行模式
|
||
class EnhancedTemplateController {
|
||
async executeTemplateByCode(code: string, body: { imageUrl: string }) {
|
||
// 1. 提交审核(所有平台都返回 PROCESSING)
|
||
const auditResult = await this.contentService.auditImage(platform, auditData);
|
||
|
||
// 2. 创建执行记录(PENDING_AUDIT 状态)
|
||
const execution = await this.createExecution({
|
||
auditTaskId: auditResult.taskId,
|
||
status: ExecutionStatus.PENDING_AUDIT, // 🎯 关键状态
|
||
// ...
|
||
});
|
||
|
||
// 3. 立即返回,不等待审核结果
|
||
return { executionId: execution.id, status: 'pending_audit' };
|
||
}
|
||
|
||
// 🎯 审核完成回调 - 无论同步异步平台都会调用
|
||
async handleAuditComplete(auditTaskId: string, auditResult: ContentAuditResult) {
|
||
const execution = await this.findByAuditTaskId(auditTaskId);
|
||
|
||
if (auditResult.conclusion === AuditConclusion.PASS) {
|
||
// 审核通过,开始执行模板
|
||
await this.startTemplateExecution(execution);
|
||
} else {
|
||
// 审核失败,更新状态
|
||
await this.updateStatus(execution.id, ExecutionStatus.AUDIT_FAILED);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 🔧 平台差异抹平机制
|
||
|
||
```typescript
|
||
// 🎯 微信适配器:同步 → 异步
|
||
class EnhancedWechatAdapter extends EnhancedBaseContentAdapter {
|
||
readonly isSyncPlatform = true; // 标识同步平台
|
||
|
||
async auditImage(auditData: ImageAuditRequest): Promise<ContentAuditResult> {
|
||
// 1. 调用微信同步API
|
||
const syncResult = await this.callWechatSyncAPI(auditData);
|
||
|
||
// 2. 统一返回 PROCESSING 状态
|
||
const processingResult = {
|
||
taskId: auditData.taskId,
|
||
status: AuditStatus.PROCESSING, // 🎯 伪装成异步
|
||
conclusion: AuditConclusion.UNCERTAIN,
|
||
};
|
||
|
||
// 3. 立即触发"伪异步"回调
|
||
setImmediate(async () => {
|
||
const callbackData = this.formatCallbackData(syncResult);
|
||
await this.handleAuditCallback(callbackData);
|
||
});
|
||
|
||
return processingResult;
|
||
}
|
||
}
|
||
|
||
// 🎯 抖音适配器:原生异步
|
||
class DouyinAdapter extends BaseContentAdapter {
|
||
async auditImage(auditData: ImageAuditRequest): Promise<ContentAuditResult> {
|
||
// 直接调用异步API,通过外部回调处理结果
|
||
const response = await this.callDouyinAsyncAPI(auditData);
|
||
return {
|
||
taskId: response.task_id,
|
||
status: AuditStatus.PROCESSING, // 原生异步状态
|
||
conclusion: AuditConclusion.UNCERTAIN,
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
### 📊 状态流转图
|
||
|
||
```
|
||
用户提交
|
||
↓
|
||
PENDING_AUDIT (待审核)
|
||
↓
|
||
[审核处理中...]
|
||
↓
|
||
审核完成回调
|
||
├─ PASS → PROCESSING (开始执行模板)
|
||
│ ↓
|
||
│ COMPLETED (执行完成)
|
||
│
|
||
└─ REJECT → AUDIT_FAILED (审核失败)
|
||
```
|
||
|
||
### 🎨 前端集成示例
|
||
|
||
```javascript
|
||
// 前端统一异步体验
|
||
async function executeTemplate(code, imageUrl) {
|
||
// 1. 提交任务
|
||
const response = await api.post(`/enhanced/templates/code/${code}/execute`, { imageUrl });
|
||
const { executionId } = response.data;
|
||
|
||
// 2. 轮询状态
|
||
return new Promise((resolve, reject) => {
|
||
const poll = async () => {
|
||
const status = await api.get(`/enhanced/templates/executions/${executionId}/status`);
|
||
|
||
switch (status.data.status) {
|
||
case 'pending_audit':
|
||
setTimeout(poll, 2000); // 继续轮询
|
||
break;
|
||
case 'audit_failed':
|
||
reject(new Error(status.data.errorMessage));
|
||
break;
|
||
case 'processing':
|
||
setTimeout(poll, 3000); // 继续轮询
|
||
break;
|
||
case 'completed':
|
||
resolve(status.data);
|
||
break;
|
||
case 'failed':
|
||
reject(new Error(status.data.errorMessage));
|
||
break;
|
||
}
|
||
};
|
||
|
||
poll();
|
||
});
|
||
}
|
||
```
|
||
|
||
## 🏛️ 增强基础适配器实现
|
||
|
||
```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;
|
||
}
|
||
```
|
||
|
||
## 🌐 环境变量配置 (更新版)
|
||
|
||
### 📋 必需环境变量
|
||
|
||
```bash
|
||
# .env 文件配置
|
||
|
||
# 数据库配置
|
||
DB_HOST=mysql-server-host
|
||
DB_PORT=3306
|
||
DB_USERNAME=username
|
||
DB_PASSWORD=password
|
||
DB_DATABASE=database_name
|
||
|
||
# 应用配置
|
||
NODE_ENV=development
|
||
PORT=3002
|
||
|
||
# JWT配置
|
||
JWT_SECRET=your_jwt_secret_key
|
||
|
||
# 微信小程序配置
|
||
WECHAT_APP_ID=wxb51f0b0c3aad7cdf
|
||
WECHAT_APP_SECRET=your_wechat_app_secret
|
||
|
||
# 抖音小程序配置
|
||
BYTEDANCE_APP_ID=ttbfd9c96420ec8f8201
|
||
BYTEDANCE_APP_SECRET=04a026867fbd1ba52174c7c21d94cbc7361ec378
|
||
|
||
# 🆕 内容审核配置(关键配置)
|
||
# 图片审核回调地址(重要:抖音审核完成后会调用此接口)
|
||
AUDIT_CALLBACK_URL=https://api.bowongai.com/api/v1/content-moderation/bytedance/callback
|
||
|
||
# 抖音内容审核API地址
|
||
BYTEDANCE_AUDIT_API_URL=https://developer.toutiao.com/api/apps/v2/content/audit/image
|
||
|
||
# 微信内容审核API地址
|
||
WECHAT_AUDIT_API_URL=https://api.weixin.qq.com/wxa/img_sec_check
|
||
|
||
# N8N配置
|
||
N8N_WEBHOOK_URL=https://n8n.bowongai.com/webhook/f1487ca8-bc49-4994-ba82-e2bcb95931f9
|
||
```
|
||
|
||
### 🔧 配置验证
|
||
|
||
```typescript
|
||
// config/content-moderation.config.ts
|
||
export const contentModerationConfig = () => ({
|
||
audit: {
|
||
callback: {
|
||
url: process.env.AUDIT_CALLBACK_URL,
|
||
timeout: parseInt(process.env.AUDIT_CALLBACK_TIMEOUT) || 30000,
|
||
},
|
||
bytedance: {
|
||
appId: process.env.BYTEDANCE_APP_ID,
|
||
appSecret: process.env.BYTEDANCE_APP_SECRET,
|
||
apiUrl: process.env.BYTEDANCE_AUDIT_API_URL ||
|
||
'https://developer.toutiao.com/api/apps/v2/content/audit/image',
|
||
},
|
||
wechat: {
|
||
appId: process.env.WECHAT_APP_ID,
|
||
appSecret: process.env.WECHAT_APP_SECRET,
|
||
apiUrl: process.env.WECHAT_AUDIT_API_URL ||
|
||
'https://api.weixin.qq.com/wxa/img_sec_check',
|
||
},
|
||
},
|
||
});
|
||
|
||
// 配置验证
|
||
export function validateConfig() {
|
||
const required = [
|
||
'BYTEDANCE_APP_ID',
|
||
'BYTEDANCE_APP_SECRET',
|
||
'AUDIT_CALLBACK_URL',
|
||
'WECHAT_APP_ID',
|
||
'WECHAT_APP_SECRET',
|
||
];
|
||
|
||
const missing = required.filter(key => !process.env[key]);
|
||
if (missing.length > 0) {
|
||
throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 🌐 模块配置
|
||
|
||
```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
|
||
```
|
||
|
||
## 🚀 部署指南
|
||
|
||
### 📋 部署清单
|
||
|
||
1. **环境变量配置**:
|
||
- ✅ 配置所有必需的环境变量
|
||
- ✅ 确保回调URL可被外部访问
|
||
- ✅ 验证API密钥的有效性
|
||
|
||
2. **数据库迁移**:
|
||
```bash
|
||
# 运行数据库迁移
|
||
npm run migration:run
|
||
|
||
# 或手动执行SQL
|
||
ALTER TABLE template_executions ADD COLUMN audit_task_id VARCHAR(255);
|
||
ALTER TABLE template_executions MODIFY COLUMN status ENUM('pending','pending_audit','audit_failed','processing','completed','failed') DEFAULT 'pending_audit';
|
||
```
|
||
|
||
3. **SSL证书配置**:
|
||
- 确保回调URL使用HTTPS
|
||
- 配置有效的SSL证书
|
||
|
||
4. **防火墙配置**:
|
||
- 开放回调接口端口
|
||
- 允许抖音服务器IP访问
|
||
|
||
### 🔧 启动步骤
|
||
|
||
```bash
|
||
# 1. 安装依赖
|
||
npm install
|
||
|
||
# 2. 配置环境变量
|
||
cp .env.example .env
|
||
# 编辑 .env 文件
|
||
|
||
# 3. 数据库迁移
|
||
npm run migration:run
|
||
|
||
# 4. 启动应用
|
||
npm run start:prod
|
||
```
|
||
|
||
### 🧪 测试验证
|
||
|
||
```bash
|
||
# 1. 测试抖音审核适配器
|
||
curl -X POST https://your-domain.com/api/v1/content-moderation/bytedance/audit-image \
|
||
-H "Content-Type: application/json" \
|
||
-H "Authorization: Bearer your-token" \
|
||
-d '{"imageUrl": "https://example.com/test.jpg"}'
|
||
|
||
# 2. 测试统一异步模板执行
|
||
curl -X POST https://your-domain.com/enhanced/templates/code/photo_restore_v1/execute \
|
||
-H "Content-Type: application/json" \
|
||
-H "Authorization: Bearer your-token" \
|
||
-d '{"imageUrl": "https://example.com/test.jpg"}'
|
||
|
||
# 3. 查询执行状态
|
||
curl -X GET https://your-domain.com/enhanced/templates/executions/123/status \
|
||
-H "Authorization: Bearer your-token"
|
||
```
|
||
|
||
## 📊 监控和告警
|
||
|
||
### 🔍 关键指标
|
||
|
||
1. **审核性能指标**:
|
||
- 审核响应时间
|
||
- 审核成功率
|
||
- 回调处理成功率
|
||
|
||
2. **执行状态分布**:
|
||
- PENDING_AUDIT 数量
|
||
- AUDIT_FAILED 比例
|
||
- 整体执行成功率
|
||
|
||
3. **错误监控**:
|
||
- API调用失败
|
||
- 回调处理异常
|
||
- 数据库连接异常
|
||
|
||
### 📈 监控配置
|
||
|
||
```typescript
|
||
// metrics/audit.metrics.ts
|
||
import { Injectable } from '@nestjs/common';
|
||
import { InjectMetric } from '@nestjs/prometheus';
|
||
import { Counter, Histogram, Gauge } from 'prom-client';
|
||
|
||
@Injectable()
|
||
export class AuditMetrics {
|
||
constructor(
|
||
@InjectMetric('audit_requests_total')
|
||
private auditCounter: Counter<string>,
|
||
|
||
@InjectMetric('audit_duration_seconds')
|
||
private auditDuration: Histogram<string>,
|
||
|
||
@InjectMetric('pending_audits_total')
|
||
private pendingGauge: Gauge<string>,
|
||
) {}
|
||
|
||
recordAuditRequest(platform: string, result: string) {
|
||
this.auditCounter.labels(platform, result).inc();
|
||
}
|
||
|
||
recordAuditDuration(platform: string, duration: number) {
|
||
this.auditDuration.labels(platform).observe(duration);
|
||
}
|
||
|
||
updatePendingAudits(count: number) {
|
||
this.pendingGauge.set(count);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 🎯 总结
|
||
|
||
### ✨ 核心创新点
|
||
|
||
1. **🚀 统一异步架构**:
|
||
- 彻底解决同步/异步平台差异问题
|
||
- 提供一致的用户体验
|
||
- 支持轻松扩展新平台
|
||
|
||
2. **🔧 平台差异抹平**:
|
||
- 微信同步API → 异步模式适配
|
||
- 抖音原生异步 → 保持不变
|
||
- 业务层完全无感知
|
||
|
||
3. **📊 状态驱动设计**:
|
||
- 清晰的状态流转
|
||
- 完整的执行链路追踪
|
||
- 易于监控和调试
|
||
|
||
4. **🛡️ 安全可靠**:
|
||
- 图片URL验证增强
|
||
- 完整的错误处理
|
||
- 审核日志记录
|
||
|
||
### 🎉 架构优势
|
||
|
||
- **扩展性**:新增平台只需实现适配器
|
||
- **维护性**:统一的接口和状态管理
|
||
- **性能**:异步非阻塞处理
|
||
- **用户体验**:响应迅速,状态透明
|
||
- **监控**:完整的指标和日志
|
||
|
||
### 🔮 未来扩展
|
||
|
||
1. **智能重试机制**:审核失败自动重试
|
||
2. **缓存优化**:相同图片审核结果缓存
|
||
3. **批量处理**:支持大规模批量审核
|
||
4. **AI增强**:结合自研AI模型预检测
|
||
5. **实时通知**:WebSocket推送状态更新
|
||
|
||
---
|
||
|
||
**🎯 这套统一异步架构完美解决了原始的同步审核阻塞异步模板执行的问题,为多平台内容审核提供了优雅的解决方案。** |