UGC 内容审核系统技术方案(Hyperf 框架)
基于 Hyperf 协程框架的高并发内容审核系统设计方案
技术栈: Hyperf 3.1 + PHP 8.2 + MySQL 8.0 + Redis 7.0 + Swoole 5.0
一、设计规范与原则
1.1 数据库设计规范
字段类型规范
- ✅ 日期时间:统一使用
datetime类型,不使用timestamp - ✅ 状态字段:统一使用
tinyint数字类型,禁止使用ENUM - ✅ 状态说明:在注释中明确说明状态数字对应含义
- ✅ 主键:统一使用
bigint unsigned auto_increment - ✅ 字符集:统一使用
utf8mb4
状态码设计示例
-- ✅ 正确示例
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-待审核 1-已通过 2-已拒绝 3-人工审核中'
-- ❌ 错误示例
`status` ENUM('pending', 'approved', 'rejected') DEFAULT 'pending'
1.2 代码架构规范
分层架构
Controller (控制器层)
↓ 调用
Service (业务逻辑层)
↓ 调用
Dao (数据访问层)
↓ 调用
Model (模型层)
层级职责
- Controller:接收请求、参数验证、返回响应
- Service:业务逻辑处理、事务控制
- Dao:数据库操作封装、SQL 组装
- Model:数据模型定义、关联关系
开发流程
- 接到需求后,先查找
Dao包是否存在满足的方法 - 如果存在,直接调用;如果不存在,才新建方法
- Dao 方法要具有通用性,便于复用
1.3 API 响应规范
统一响应格式
{
"code": 0,
"message": "success",
"data": {
"list": [],
"total": 100,
"page": 1,
"page_size": 20
}
}
响应字段说明
code:业务状态码(0 表示成功)message:提示信息data:业务数据(外层套字段接收,方便扩展)list:列表数据total:总数page:当前页码page_size:每页数量
1.4 命名规范
变量命名
- ✅ 变量名:使用小驼峰式
$userId,$postList - ✅ 类名:使用大驼峰式
PostService,UserDao - ✅ 常量:使用全大写下划线
MAX_RETRY_COUNT - ✅ 数据库字段:使用下划线
user_id,created_at - ✅ API 参数:使用下划线
post_id,page_size
// ✅ 正确示例
public function getUserById(int $userId): array
{
$postList = $this->postDao->findByUserId($userId);
return ['list' => $postList];
}
// ❌ 错误示例
public function get_user_by_id(int $user_id): array
{
$post_list = $this->postDao->find_by_user_id($user_id);
return $post_list;
}
1.5 性能规范
事务处理规范
// ✅ 正确:先准备数据,再开启事务
$userData = $this->prepareUserData($userId);
$emailContent = $this->generateEmail($userData); // IO 操作在事务外
Db::transaction(function () use ($userId, $userData) {
$this->userDao->update($userId, $userData);
$this->logDao->insert(['user_id' => $userId]);
});
// 事务完成后再执行 IO 操作
$this->emailService->send($emailContent);
// ❌ 错误:事务中包含 IO 操作
Db::transaction(function () use ($userId) {
$this->userDao->update($userId, ['status' => 1]);
$this->emailService->send('...'); // ❌ 耗时 IO 操作
$this->apiClient->notify('...'); // ❌ 网络请求
});
批量查询规范
// ✅ 正确:批量查询
$userIds = [1, 2, 3, 4, 5];
$users = $this->userDao->findByIds($userIds); // 一次 SQL
// ❌ 错误:循环查询
$users = [];
foreach ($userIds as $userId) {
$users[] = $this->userDao->findById($userId); // N 次 SQL
}
协程并发规范
use Hyperf\Coroutine\Parallel;
// ✅ 正确:使用协程并发处理 IO 操作
$parallel = new Parallel(10);
foreach ($postIds as $postId) {
$parallel->add(function () use ($postId) {
return $this->aiService->moderate($postId); // IO 操作
});
}
$results = $parallel->wait();
1.6 代码质量规范
必须遵守
- ✅ 单一职责原则:一个方法只做一件事
- ✅ 依赖注入:使用
#[Inject]注入依赖 - ✅ 异常处理:捕获异常并记录日志
- ✅ 参数验证:使用
FormRequest验证请求参数 - ✅ 代码注释:关键逻辑必须添加注释
禁止事项
- ❌ 禁止在循环中执行 SQL 查询
- ❌ 禁止在事务中执行 IO 操作(网络请求、文件操作等)
- ❌ 禁止硬编码:状态码、配置项等必须定义常量
- ❌ 禁止使用
die(),exit(),var_dump()等调试函数 - ❌ 禁止直接使用
$_GET,$_POST,统一使用 Request 对象
二、系统架构设计
1.1 整体架构图
┌─────────────┐
│ 用户端 │ (提交帖子)
└──────┬──────┘
│ HTTP POST /api/v1/posts
↓
┌──────────────────────────────────┐
│ Hyperf API Gateway │
│ - 幂等性校验 (Idempotency-Key) │
│ - 参数验证 │
│ - 限流控制 │
└──────┬───────────────────────────┘
│
↓
┌──────────────────────────────────┐
│ PostService (核心业务) │
│ - 创建帖子记录 │
│ - 推送审核任务到队列 │
└──────┬───────────────────────────┘
│
↓
┌──────────────────────────────────┐
│ Redis 异步队列 (async-queue) │
│ - 规则审核任务 │
│ - AI 审核任务 │
└──────┬───────────────────────────┘
│
↓
┌──────────────────────────────────┐
│ 审核流水线 (Pipeline) │
│ ┌────────────────────────────┐ │
│ │ 1. 规则审核 (RuleCheck) │ │
│ │ - 敏感词过滤 │ │
│ │ - 正则匹配 │ │
│ │ - 策略:拒绝/替换/放行 │ │
│ └──────────┬─────────────────┘ │
│ ↓ │
│ ┌────────────────────────────┐ │
│ │ 2. AI 审核 (AICheck) │ │
│ │ - 调用 OpenAI API │ │
│ │ - 风险评分 │ │
│ │ - 超时重试熔断 │ │
│ └──────────┬─────────────────┘ │
│ ↓ │
│ ┌────────────────────────────┐ │
│ │ 3. AI 改写建议 (Rewrite) │ │
│ │ - 可选改写 │ │
│ │ - 人工确认 │ │
│ └──────────┬─────────────────┘ │
│ ↓ │
│ ┌────────────────────────────┐ │
│ │ 4. 决策引擎 (Decision) │ │
│ │ - 自动放行 │ │
│ │ - 转人工审核 │ │
│ └────────────────────────────┘ │
└───────────────────────────────────┘
│
↓
┌──────────────────────────────────┐
│ 后台管理系统 (Admin) │
│ - 人工审核工作台 │
│ - 审核历史记录 │
│ - 敏感词规则管理 │
│ - 权限控制 (RBAC) │
└───────────────────────────────────┘
1.2 时序图:同步 vs 异步流程
同步流程(用户提交)
用户端 → API → PostService
↓
创建 Post 记录 (状态: pending)
↓
创建 ModerationTask
↓
推送到异步队列
↓
立即返回 post_id
响应时间:< 50ms
异步流程(审核流水线)
队列消费者 → 规则审核 (100ms)
↓
通过? 是 → AI 审核 (500-1500ms)
↓
风险低? 是 → 自动放行 (status: approved)
↓
风险中 → AI 改写建议 → 转人工审核
↓
风险高 → 自动拒绝 (status: rejected)
总耗时:T90 ~ 3s(符合要求)
三、数据库设计
3.1 状态码常量定义
<?php
namespace App\Constants;
class PostStatus
{
const PENDING = 0; // 待审核
const APPROVED = 1; // 已通过
const REJECTED = 2; // 已拒绝
const MANUAL_REVIEW = 3; // 人工审核中
}
class VisibleType
{
const AUTHOR_ONLY = 0; // 仅作者可见
const PUBLIC = 1; // 公开可见
}
class TaskState
{
const PENDING = 0; // 待处理
const PROCESSING = 1; // 处理中
const COMPLETED = 2; // 已完成
const FAILED = 3; // 失败
}
class TaskSource
{
const RULE = 1; // 规则审核
const AI = 2; // AI 审核
const MANUAL = 3; // 人工审核
}
class DecisionAction
{
const ALLOW = 1; // 允许
const REJECT = 2; // 拒绝
const REWRITE = 3; // 需要改写
const MANUAL = 4; // 转人工审核
}
class DecidedBy
{
const AI = 1; // AI 决策
const HUMAN = 2; // 人工决策
}
3.2 核心表结构
posts 表(帖子表)
CREATE TABLE `posts` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '帖子ID',
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
`content_raw` TEXT NOT NULL COMMENT '原始内容',
`content_sanitized` TEXT NULL COMMENT '清洗后内容(替换敏感词等)',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-待审核 1-已通过 2-已拒绝 3-人工审核中',
`visible_to` TINYINT NOT NULL DEFAULT 0 COMMENT '可见性:0-仅作者可见 1-公开可见',
`idempotency_key` VARCHAR(64) NULL COMMENT '幂等键',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY uk_idempotency_key (`idempotency_key`),
INDEX idx_user_id (`user_id`),
INDEX idx_status (`status`),
INDEX idx_created_at (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='帖子表';
moderation_tasks 表(审核任务表)
CREATE TABLE `moderation_tasks` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '任务ID',
`post_id` BIGINT UNSIGNED NOT NULL COMMENT '帖子ID',
`state` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-待处理 1-处理中 2-已完成 3-失败',
`source` TINYINT NOT NULL COMMENT '审核来源:1-规则审核 2-AI审核 3-人工审核',
`priority` TINYINT NOT NULL DEFAULT 5 COMMENT '优先级:0-9,数字越大优先级越高',
`retry_count` TINYINT NOT NULL DEFAULT 0 COMMENT '重试次数',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_post_id (`post_id`),
INDEX idx_state_priority (`state`, `priority`),
INDEX idx_created_at (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审核任务表';
moderation_decisions 表(审核决策表)
CREATE TABLE `moderation_decisions` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '决策ID',
`task_id` BIGINT UNSIGNED NOT NULL COMMENT '任务ID',
`post_id` BIGINT UNSIGNED NOT NULL COMMENT '帖子ID',
`action` TINYINT NOT NULL COMMENT '决策动作:1-允许 2-拒绝 3-需改写 4-转人工审核',
`labels` JSON NULL COMMENT '标签列表(如:["spam", "violence"])',
`score` DECIMAL(5,4) NULL COMMENT '风险评分 0-1',
`model_version` VARCHAR(50) NULL COMMENT 'AI 模型版本',
`decided_by` TINYINT NOT NULL COMMENT '决策者:1-AI 2-人工',
`operator_id` BIGINT UNSIGNED NULL COMMENT '操作员ID(人工审核时)',
`reason` TEXT NULL COMMENT '决策原因/备注',
`decided_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '决策时间',
INDEX idx_task_id (`task_id`),
INDEX idx_post_id (`post_id`),
INDEX idx_decided_at (`decided_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审核决策表';
rewrite_suggestions 表(改写建议表)
CREATE TABLE `rewrite_suggestions` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '建议ID',
`post_id` BIGINT UNSIGNED NOT NULL COMMENT '帖子ID',
`original_text` TEXT NOT NULL COMMENT '原文',
`suggested_text` TEXT NOT NULL COMMENT '建议改写',
`diff` JSON NULL COMMENT '差异对比',
`applied` TINYINT NOT NULL DEFAULT 0 COMMENT '是否已应用:0-否 1-是',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
INDEX idx_post_id (`post_id`),
INDEX idx_applied (`applied`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='改写建议表';
sensitive_rules 表(敏感词规则表)
CREATE TABLE `sensitive_rules` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '规则ID',
`pattern` VARCHAR(255) NOT NULL COMMENT '匹配模式(正则或关键词)',
`pattern_type` TINYINT NOT NULL DEFAULT 1 COMMENT '模式类型:1-关键词 2-正则表达式',
`strategy` TINYINT NOT NULL DEFAULT 2 COMMENT '处理策略:1-放行 2-拒绝 3-替换 4-转人工',
`replacement` VARCHAR(255) NULL COMMENT '替换文本(策略为替换时使用)',
`locale` VARCHAR(10) NOT NULL DEFAULT 'zh-CN' COMMENT '语言区域',
`enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用:0-禁用 1-启用',
`priority` INT NOT NULL DEFAULT 0 COMMENT '优先级(数字越大越先执行)',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_enabled_priority (`enabled`, `priority`),
INDEX idx_locale (`locale`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='敏感词规则表';
四、API 接口设计
4.1 响应格式规范
成功响应
{
"code": 0,
"message": "success",
"data": {
"info": {},
"list": [],
"total": 0
}
}
失败响应
{
"code": 400,
"message": "参数错误",
"data": null
}
业务错误码定义
<?php
namespace App\Constants;
class ErrorCode
{
const SUCCESS = 0; // 成功
const PARAM_ERROR = 400; // 参数错误
const UNAUTHORIZED = 401; // 未授权
const FORBIDDEN = 403; // 禁止访问
const NOT_FOUND = 404; // 资源不存在
const DUPLICATE_REQUEST = 409; // 重复请求
const TOO_MANY_REQUESTS = 429; // 请求过于频繁
const SERVER_ERROR = 500; // 服务器错误
}
4.2 帖子提交接口
请求
POST /api/v1/posts
Content-Type: application/json
Idempotency-Key: uuid-xxx-xxx
{
"content": "这是一篇用户发布的帖子内容..."
}
响应(成功)
{
"code": 0,
"message": "success",
"data": {
"info": {
"post_id": 123456,
"status": 0,
"status_text": "待审核",
"visible_to": 0,
"visible_to_text": "仅作者可见",
"estimated_review_time": 3
}
}
}
响应(幂等重复提交)
{
"code": 0,
"message": "already exists",
"data": {
"info": {
"post_id": 123456,
"status": 1,
"status_text": "已通过"
}
}
}
4.3 帖子详情接口
请求
GET /api/v1/posts/{post_id}
响应
{
"code": 0,
"message": "success",
"data": {
"info": {
"post_id": 123456,
"user_id": 10001,
"content": "这是一篇帖子...",
"status": 1,
"status_text": "已通过",
"moderation": {
"labels": ["safe"],
"score": 0.05,
"reviewed_at": "2025-10-31 10:30:00"
},
"rewrite_suggestion": null,
"created_at": "2025-10-31 10:25:00"
}
}
}
4.4 帖子列表接口
请求
GET /api/v1/posts?page=1&page_size=20&status=1
响应
{
"code": 0,
"message": "success",
"data": {
"list": [
{
"post_id": 123456,
"content": "...",
"status": 1,
"created_at": "2025-10-31 10:25:00"
}
],
"total": 100,
"page": 1,
"page_size": 20
}
}
4.5 人工审核决策接口(后台)
请求
POST /api/v1/admin/moderations/{task_id}/decision
Content-Type: application/json
Authorization: Bearer <admin_token>
{
"action": 1, // 1-允许 2-拒绝 3-改写
"note": "内容符合社区规范"
}
响应
{
"code": 0,
"message": "审核完成",
"data": {
"info": {
"post_id": 123456,
"status": 1,
"status_text": "已通过"
}
}
}
五、核心代码实现
5.1 目录结构(按规范架构)
app/
├── Controller/ # 控制器层
│ ├── PostController.php
│ └── Admin/
│ ├── ModerationController.php
│ └── RuleController.php
├── Service/ # 业务逻辑层
│ ├── PostService.php
│ ├── ModerationService.php
│ ├── AIService.php
│ └── Pipeline/
│ ├── RuleCheckPipeline.php
│ ├── AICheckPipeline.php
│ └── RewritePipeline.php
├── Dao/ # 数据访问层(新增)
│ ├── PostDao.php
│ ├── ModerationTaskDao.php
│ ├── ModerationDecisionDao.php
│ ├── RewriteSuggestionDao.php
│ └── SensitiveRuleDao.php
├── Model/ # 模型层
│ ├── Post.php
│ ├── ModerationTask.php
│ ├── ModerationDecision.php
│ ├── RewriteSuggestion.php
│ └── SensitiveRule.php
├── Constants/ # 常量定义
│ ├── PostStatus.php
│ ├── ErrorCode.php
│ └── TaskSource.php
├── Job/ # 异步任务
│ └── ModerationJob.php
├── Request/ # 请求验证
│ └── PostRequest.php
└── Middleware/ # 中间件
├── IdempotencyMiddleware.php
└── RateLimitMiddleware.php
5.2 Controller 层(控制器)
PostController(帖子控制器)
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Request\PostRequest;
use App\Service\PostService;
use App\Constants\ErrorCode;
use App\Constants\PostStatus;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\PostMapping;
use Hyperf\HttpServer\Annotation\GetMapping;
use Hyperf\HttpServer\Contract\RequestInterface;
use Psr\Http\Message\ResponseInterface;
#[Controller(prefix: '/api/v1/posts')]
class PostController extends AbstractController
{
#[Inject]
private PostService $postService;
/**
* 创建帖子
*/
#[PostMapping(path: '')]
public function create(PostRequest $request): array
{
$content = $request->input('content');
$userId = $this->getUserId();
$idempotencyKey = $request->header('Idempotency-Key');
$postInfo = $this->postService->createPost($userId, $content, $idempotencyKey);
return $this->success([
'info' => [
'post_id' => $postInfo['postId'],
'status' => $postInfo['status'],
'status_text' => $this->getStatusText($postInfo['status']),
'visible_to' => $postInfo['visibleTo'],
'visible_to_text' => $this->getVisibleText($postInfo['visibleTo']),
'estimated_review_time' => 3,
],
]);
}
/**
* 获取帖子详情
*/
#[GetMapping(path: '{post_id}')]
public function show(int $post_id): array
{
$postInfo = $this->postService->getPostDetail($post_id);
if (!$postInfo) {
return $this->error(ErrorCode::NOT_FOUND, '帖子不存在');
}
return $this->success([
'info' => $postInfo,
]);
}
/**
* 获取帖子列表
*/
#[GetMapping(path: '')]
public function list(RequestInterface $request): array
{
$page = (int) $request->input('page', 1);
$pageSize = (int) $request->input('page_size', 20);
$status = $request->input('status');
$result = $this->postService->getPostList($page, $pageSize, $status);
return $this->success([
'list' => $result['list'],
'total' => $result['total'],
'page' => $page,
'page_size' => $pageSize,
]);
}
/**
* 获取用户ID(从上下文)
*/
private function getUserId(): int
{
return \Hyperf\Context\Context::get('user_id', 0);
}
/**
* 获取状态文本
*/
private function getStatusText(int $status): string
{
return match ($status) {
PostStatus::PENDING => '待审核',
PostStatus::APPROVED => '已通过',
PostStatus::REJECTED => '已拒绝',
PostStatus::MANUAL_REVIEW => '人工审核中',
default => '未知',
};
}
/**
* 获取可见性文本
*/
private function getVisibleText(int $visibleTo): string
{
return $visibleTo === 0 ? '仅作者可见' : '公开可见';
}
}
AbstractController(基础控制器)
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Constants\ErrorCode;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Psr\Container\ContainerInterface;
abstract class AbstractController
{
#[Inject]
protected ContainerInterface $container;
#[Inject]
protected RequestInterface $request;
#[Inject]
protected ResponseInterface $response;
/**
* 成功响应
*/
protected function success(array $data = [], string $message = 'success'): array
{
return [
'code' => ErrorCode::SUCCESS,
'message' => $message,
'data' => $data,
];
}
/**
* 失败响应
*/
protected function error(int $code, string $message, $data = null): array
{
return [
'code' => $code,
'message' => $message,
'data' => $data,
];
}
}
4.3 PostService(帖子服务)
<?php
namespace App\Service;
use App\Model\Post;
use App\Model\ModerationTask;
use App\Job\ModerationJob;
use Hyperf\AsyncQueue\Driver\DriverFactory;
use Hyperf\DbConnection\Db;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Redis\Redis;
class PostService
{
#[Inject]
private DriverFactory $queueFactory;
#[Inject]
private Redis $redis;
/**
* 创建帖子
*/
public function createPost(int $userId, string $content, ?string $idempotencyKey = null): array
{
// 1. 幂等性检查
if ($idempotencyKey) {
$cachedPostId = $this->redis->get("idempotency:{$idempotencyKey}");
if ($cachedPostId) {
$post = Post::find($cachedPostId);
return [
'post_id' => $post->id,
'moderation_status' => $post->status,
'is_duplicate' => true,
];
}
}
// 2. 使用数据库事务创建帖子和审核任务
$post = Db::transaction(function () use ($userId, $content, $idempotencyKey) {
// 创建帖子记录
$post = Post::create([
'user_id' => $userId,
'content_raw' => $content,
'status' => 'pending',
'visible_to' => 'author_only', // 审核前仅作者可见
'idempotency_key' => $idempotencyKey,
]);
// 创建审核任务
$task = ModerationTask::create([
'post_id' => $post->id,
'state' => 'pending',
'source' => 'RULE',
'priority' => 5,
]);
return $post;
});
// 3. 推送到异步队列
$queue = $this->queueFactory->get('default');
$queue->push(new ModerationJob($post->id), 0);
// 4. 缓存幂等键
if ($idempotencyKey) {
$this->redis->setex("idempotency:{$idempotencyKey}", 86400, $post->id);
}
return [
'post_id' => $post->id,
'moderation_status' => $post->status,
'visible_to' => $post->visible_to,
'estimated_review_time' => 3, // 预估审核时间(秒)
];
}
/**
* 获取帖子详情(包含审核信息)
*/
public function getPostDetail(int $postId): array
{
$post = Post::findOrFail($postId);
// 获取最新的审核决策
$decision = $post->decisions()->latest()->first();
// 获取改写建议(如果有)
$rewriteSuggestion = $post->rewriteSuggestions()
->where('applied', 0)
->latest()
->first();
return [
'id' => $post->id,
'user_id' => $post->user_id,
'content' => $post->content_sanitized ?: $post->content_raw,
'status' => $post->status,
'moderation' => $decision ? [
'labels' => $decision->labels,
'score' => $decision->score,
'reviewed_at' => $decision->decided_at,
] : null,
'rewrite_suggestion' => $rewriteSuggestion ? [
'id' => $rewriteSuggestion->id,
'suggested_text' => $rewriteSuggestion->suggested_text,
'diff' => $rewriteSuggestion->diff,
] : null,
'created_at' => $post->created_at,
];
}
}
4.4 ModerationJob(审核队列任务)
<?php
namespace App\Job;
use App\Service\ModerationService;
use Hyperf\AsyncQueue\Job;
use Hyperf\Context\ApplicationContext;
class ModerationJob extends Job
{
public int $postId;
public function __construct(int $postId)
{
$this->postId = $postId;
}
public function handle()
{
$container = ApplicationContext::getContainer();
$moderationService = $container->get(ModerationService::class);
try {
// 执行审核流水线
$moderationService->processModerationPipeline($this->postId);
} catch (\Exception $e) {
// 记录日志
logger('moderation')->error('审核任务失败', [
'post_id' => $this->postId,
'error' => $e->getMessage(),
]);
// 重新抛出异常,触发队列重试机制
throw $e;
}
}
}
4.5 ModerationService(审核服务 - 核心流水线)
<?php
namespace App\Service;
use App\Model\Post;
use App\Model\ModerationTask;
use App\Model\ModerationDecision;
use App\Service\Pipeline\RuleCheckPipeline;
use App\Service\Pipeline\AICheckPipeline;
use App\Service\Pipeline\RewritePipeline;
use Hyperf\Di\Annotation\Inject;
class ModerationService
{
#[Inject]
private RuleCheckPipeline $ruleCheckPipeline;
#[Inject]
private AICheckPipeline $aiCheckPipeline;
#[Inject]
private RewritePipeline $rewritePipeline;
/**
* 执行审核流水线
*/
public function processModerationPipeline(int $postId): void
{
$post = Post::findOrFail($postId);
$content = $post->content_raw;
// ========== 第 1 步:规则审核 ==========
$ruleResult = $this->ruleCheckPipeline->check($content);
if ($ruleResult['action'] === 'reject') {
// 直接拒绝
$this->rejectPost($post, 'RULE', $ruleResult['reason']);
return;
}
if ($ruleResult['action'] === 'replace') {
// 替换敏感词
$post->content_sanitized = $ruleResult['sanitized_content'];
$post->save();
$content = $ruleResult['sanitized_content'];
}
// ========== 第 2 步:AI 审核 ==========
$aiResult = $this->aiCheckPipeline->check($content, $post->id);
// 保存 AI 决策记录
$this->saveDecision($post->id, [
'action' => $aiResult['action'],
'labels' => $aiResult['labels'],
'score' => $aiResult['score'],
'decided_by' => 'AI',
'model_version' => $aiResult['model_version'] ?? 'gpt-4',
]);
// 根据风险评分决策
if ($aiResult['score'] < 0.3) {
// 低风险:自动放行
$this->approvePost($post);
return;
}
if ($aiResult['score'] > 0.7) {
// 高风险:直接拒绝
$this->rejectPost($post, 'AI', $aiResult['reason']);
return;
}
// ========== 第 3 步:中风险 - AI 改写建议 ==========
if ($aiResult['score'] >= 0.3 && $aiResult['score'] <= 0.7) {
$this->rewritePipeline->generateSuggestion($post);
// 转人工审核
$post->status = 'manual_review';
$post->save();
return;
}
}
/**
* 批准帖子
*/
private function approvePost(Post $post): void
{
$post->status = 'approved';
$post->visible_to = 'public'; // 审核通过后公开可见
$post->save();
}
/**
* 拒绝帖子
*/
private function rejectPost(Post $post, string $source, string $reason): void
{
$post->status = 'rejected';
$post->save();
$this->saveDecision($post->id, [
'action' => 'REJECT',
'decided_by' => $source === 'RULE' ? 'AI' : 'AI',
'reason' => $reason,
]);
}
/**
* 保存审核决策
*/
private function saveDecision(int $postId, array $data): void
{
$task = ModerationTask::where('post_id', $postId)->latest()->first();
ModerationDecision::create([
'task_id' => $task->id,
'post_id' => $postId,
'action' => $data['action'] ?? 'MANUAL',
'labels' => json_encode($data['labels'] ?? []),
'score' => $data['score'] ?? null,
'model_version' => $data['model_version'] ?? null,
'decided_by' => $data['decided_by'] ?? 'AI',
'reason' => $data['reason'] ?? null,
]);
}
}
4.6 RuleCheckPipeline(规则审核)
<?php
namespace App\Service\Pipeline;
use App\Model\SensitiveRule;
use Hyperf\Cache\Annotation\Cacheable;
class RuleCheckPipeline
{
/**
* 规则检查
*/
public function check(string $content): array
{
$rules = $this->getSensitiveRules();
$sanitizedContent = $content;
$matchedRules = [];
foreach ($rules as $rule) {
$pattern = $rule->pattern;
$patternType = $rule->pattern_type;
// 关键词匹配
if ($patternType === 'keyword') {
if (mb_strpos($content, $pattern) !== false) {
$matchedRules[] = $rule;
if ($rule->strategy === 'reject') {
return [
'action' => 'reject',
'reason' => "包含敏感词:{$pattern}",
];
}
if ($rule->strategy === 'replace') {
$replacement = $rule->replacement ?: str_repeat('*', mb_strlen($pattern));
$sanitizedContent = str_replace($pattern, $replacement, $sanitizedContent);
}
if ($rule->strategy === 'manual') {
return [
'action' => 'manual',
'reason' => "触发人工审核规则:{$pattern}",
];
}
}
}
// 正则匹配
if ($patternType === 'regex') {
if (preg_match($pattern, $content)) {
$matchedRules[] = $rule;
if ($rule->strategy === 'reject') {
return [
'action' => 'reject',
'reason' => "匹配敏感规则:{$pattern}",
];
}
if ($rule->strategy === 'replace') {
$sanitizedContent = preg_replace($pattern, $rule->replacement ?: '***', $sanitizedContent);
}
}
}
}
// 没有匹配到拒绝规则
if ($sanitizedContent !== $content) {
return [
'action' => 'replace',
'sanitized_content' => $sanitizedContent,
'matched_rules' => $matchedRules,
];
}
return [
'action' => 'pass',
];
}
/**
* 获取敏感词规则(缓存 1 小时)
*/
#[Cacheable(prefix: 'sensitive_rules', ttl: 3600)]
private function getSensitiveRules(): array
{
return SensitiveRule::where('enabled', 1)
->orderBy('priority', 'desc')
->get()
->toArray();
}
}
4.7 AICheckPipeline(AI 审核)
<?php
namespace App\Service\Pipeline;
use App\Service\AIService;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Retry\Annotation\Retry;
class AICheckPipeline
{
#[Inject]
private AIService $aiService;
/**
* AI 审核(带超时重试)
*/
#[Retry(maxAttempts: 3, base: 1000, strategy: 'backoff')]
public function check(string $content, int $postId): array
{
try {
// 调用 AI 服务(OpenAI API)
$result = $this->aiService->moderateContent($content);
return [
'action' => $this->mapAction($result['category']),
'labels' => $result['categories'] ?? [],
'score' => $result['score'] ?? 0,
'model_version' => $result['model'] ?? 'gpt-4',
'reason' => $result['reason'] ?? null,
];
} catch (\Exception $e) {
// 超时或错误:降级策略
logger('ai')->error('AI 审核失败', [
'post_id' => $postId,
'error' => $e->getMessage(),
]);
// 熔断降级:转人工审核
return [
'action' => 'MANUAL',
'labels' => ['ai_error'],
'score' => 0.5,
'reason' => 'AI 服务异常,转人工审核',
];
}
}
/**
* 映射 AI 返回的分类到动作
*/
private function mapAction(string $category): string
{
return match ($category) {
'safe' => 'ALLOW',
'spam', 'violence', 'porn' => 'REJECT',
default => 'MANUAL',
};
}
}
4.8 AIService(AI 服务抽象层)
<?php
namespace App\Service;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Hyperf\Guzzle\ClientFactory;
use Hyperf\Di\Annotation\Inject;
use Psr\SimpleCache\CacheInterface;
class AIService
{
#[Inject]
private ClientFactory $clientFactory;
#[Inject]
private CacheInterface $cache;
private Client $httpClient;
private string $apiKey;
private string $baseUrl = 'https://api.openai.com/v1';
public function __construct()
{
$this->apiKey = config('ai.openai_api_key');
$this->httpClient = $this->clientFactory->create([
'timeout' => 10, // 10 秒超时
]);
}
/**
* 内容审核(OpenAI Moderation API)
*/
public function moderateContent(string $content): array
{
// 缓存相同内容的审核结果(1小时)
$cacheKey = 'ai_moderate:' . md5($content);
$cached = $this->cache->get($cacheKey);
if ($cached) {
return json_decode($cached, true);
}
try {
$response = $this->httpClient->post("{$this->baseUrl}/moderations", [
'headers' => [
'Authorization' => "Bearer {$this->apiKey}",
'Content-Type' => 'application/json',
],
'json' => [
'input' => $content,
],
]);
$result = json_decode($response->getBody()->getContents(), true);
$moderation = $result['results'][0] ?? [];
$parsed = [
'categories' => $moderation['categories'] ?? [],
'category' => $this->getPrimaryCategory($moderation),
'score' => $this->calculateRiskScore($moderation),
'model' => 'text-moderation-latest',
];
// 缓存结果
$this->cache->set($cacheKey, json_encode($parsed), 3600);
return $parsed;
} catch (GuzzleException $e) {
throw new \RuntimeException("AI API 调用失败: {$e->getMessage()}");
}
}
/**
* 获取主要风险分类
*/
private function getPrimaryCategory(array $moderation): string
{
$categories = $moderation['categories'] ?? [];
if ($categories['sexual'] ?? false) return 'porn';
if ($categories['violence'] ?? false) return 'violence';
if ($categories['hate'] ?? false) return 'hate';
if ($categories['self-harm'] ?? false) return 'self-harm';
return 'safe';
}
/**
* 计算综合风险评分(0-1)
*/
private function calculateRiskScore(array $moderation): float
{
$scores = $moderation['category_scores'] ?? [];
return max([
$scores['sexual'] ?? 0,
$scores['violence'] ?? 0,
$scores['hate'] ?? 0,
$scores['self-harm'] ?? 0,
]);
}
/**
* AI 改写建议(OpenAI Chat API)
*/
public function rewriteSuggestion(string $content, array $issues): string
{
$prompt = "以下内容存在问题:" . implode('、', $issues) .
"\n\n原文:{$content}\n\n请改写为符合社区规范的版本:";
try {
$response = $this->httpClient->post("{$this->baseUrl}/chat/completions", [
'headers' => [
'Authorization' => "Bearer {$this->apiKey}",
'Content-Type' => 'application/json',
],
'json' => [
'model' => 'gpt-4',
'messages' => [
['role' => 'system', 'content' => '你是内容审核助手,擅长改写不当内容。'],
['role' => 'user', 'content' => $prompt],
],
'temperature' => 0.7,
'max_tokens' => 500,
],
]);
$result = json_decode($response->getBody()->getContents(), true);
return $result['choices'][0]['message']['content'] ?? '';
} catch (GuzzleException $e) {
throw new \RuntimeException("AI 改写失败: {$e->getMessage()}");
}
}
}
4.9 RewritePipeline(改写建议)
<?php
namespace App\Service\Pipeline;
use App\Model\Post;
use App\Model\RewriteSuggestion;
use App\Service\AIService;
use Hyperf\Di\Annotation\Inject;
class RewritePipeline
{
#[Inject]
private AIService $aiService;
/**
* 生成改写建议
*/
public function generateSuggestion(Post $post): void
{
try {
$suggestedText = $this->aiService->rewriteSuggestion(
$post->content_raw,
['不当表述', '可能引起争议']
);
RewriteSuggestion::create([
'post_id' => $post->id,
'original_text' => $post->content_raw,
'suggested_text' => $suggestedText,
'diff' => $this->generateDiff($post->content_raw, $suggestedText),
]);
} catch (\Exception $e) {
logger('rewrite')->error('改写建议生成失败', [
'post_id' => $post->id,
'error' => $e->getMessage(),
]);
}
}
/**
* 生成 diff 对比
*/
private function generateDiff(string $original, string $suggested): array
{
return [
'original_length' => mb_strlen($original),
'suggested_length' => mb_strlen($suggested),
'changes' => [
// 简化 diff,实际可使用专门的 diff 库
'type' => 'text_replace',
],
];
}
}
五、非功能性设计
5.1 幂等性保障
实现方式:Idempotency-Key 请求头 + Redis 缓存
<?php
namespace App\Middleware;
use Hyperf\Redis\Redis;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class IdempotencyMiddleware implements MiddlewareInterface
{
public function __construct(private Redis $redis)
{
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$idempotencyKey = $request->getHeaderLine('Idempotency-Key');
if ($idempotencyKey) {
// 检查是否正在处理
$lockKey = "idempotency:lock:{$idempotencyKey}";
$locked = $this->redis->set($lockKey, '1', ['NX', 'EX' => 60]);
if (!$locked) {
// 正在处理,返回 409 Conflict
return response()->json([
'code' => 409,
'message' => 'Request is being processed',
], 409);
}
}
$response = $handler->handle($request);
// 请求完成后删除锁
if ($idempotencyKey) {
$this->redis->del("idempotency:lock:{$idempotencyKey}");
}
return $response;
}
}
5.2 限流控制
实现方式:Redis + 滑动窗口算法
<?php
namespace App\Middleware;
use Hyperf\Redis\Redis;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class RateLimitMiddleware implements MiddlewareInterface
{
private const LIMIT = 200; // 200 req/s
private const WINDOW = 1; // 1 秒窗口
public function __construct(private Redis $redis)
{
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$ip = $this->getClientIp($request);
$key = "rate_limit:{$ip}";
$now = time();
// 滑动窗口计数
$this->redis->zRemRangeByScore($key, 0, $now - self::WINDOW);
$count = $this->redis->zCard($key);
if ($count >= self::LIMIT) {
return response()->json([
'code' => 429,
'message' => 'Too Many Requests',
], 429);
}
$this->redis->zAdd($key, $now, uniqid());
$this->redis->expire($key, self::WINDOW + 1);
return $handler->handle($request);
}
private function getClientIp(ServerRequestInterface $request): string
{
return $request->getHeaderLine('X-Real-IP') ?: '127.0.0.1';
}
}
5.3 超时控制
配置队列超时
// config/autoload/async_queue.php
return [
'default' => [
'driver' => Hyperf\AsyncQueue\Driver\RedisDriver::class,
'channel' => 'queue',
'timeout' => 10, // 消费超时时间 10 秒
'retry_seconds' => 5, // 失败后重试间隔
'handle_timeout' => 30, // 任务执行超时时间 30 秒
'processes' => 4, // 消费进程数
],
];
AI API 超时控制
// config/autoload/ai.php
return [
'openai_api_key' => env('OPENAI_API_KEY'),
'timeout' => 10, // 10 秒超时
'retry' => 3, // 重试 3 次
];
5.4 熔断降级
使用 Hyperf Circuit Breaker
<?php
namespace App\Service;
use Hyperf\CircuitBreaker\Annotation\CircuitBreaker;
class AIService
{
#[CircuitBreaker(
timeout: 10,
failCounter: 5,
successCounter: 2,
fallback: 'App\Service\AIService::fallback'
)]
public function moderateContent(string $content): array
{
// AI 调用...
}
/**
* 熔断降级处理
*/
public function fallback(string $content): array
{
// 降级:转人工审核
logger('ai')->warning('AI 服务熔断,启用降级策略');
return [
'action' => 'MANUAL',
'labels' => ['fallback'],
'score' => 0.5,
'reason' => 'AI 服务暂时不可用,已转人工审核',
];
}
}
5.5 日志与监控
结构化日志
<?php
namespace App\Aspect;
use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Hyperf\Logger\LoggerFactory;
#[Aspect]
class ModerationLogAspect extends AbstractAspect
{
public array $classes = [
'App\Service\ModerationService::processModerationPipeline',
];
public function __construct(private LoggerFactory $loggerFactory)
{
}
public function process(ProceedingJoinPoint $proceedingJoinPoint)
{
$startTime = microtime(true);
$postId = $proceedingJoinPoint->arguments['keys']['postId'] ?? 0;
try {
$result = $proceedingJoinPoint->process();
$duration = (microtime(true) - $startTime) * 1000;
$this->loggerFactory->get('moderation')->info('审核完成', [
'post_id' => $postId,
'duration_ms' => round($duration, 2),
'status' => 'success',
]);
return $result;
} catch (\Exception $e) {
$this->loggerFactory->get('moderation')->error('审核失败', [
'post_id' => $postId,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
throw $e;
}
}
}
5.6 可观测性
Prometheus 指标
<?php
namespace App\Service;
use Prometheus\CollectorRegistry;
use Prometheus\Storage\Redis;
class MetricsService
{
private CollectorRegistry $registry;
public function __construct()
{
$this->registry = new CollectorRegistry(new Redis());
}
public function recordModerationDuration(float $duration, string $status): void
{
$histogram = $this->registry->getOrRegisterHistogram(
'app',
'moderation_duration_seconds',
'审核耗时',
['status']
);
$histogram->observe($duration, [$status]);
}
public function incrementModerationCounter(string $action): void
{
$counter = $this->registry->getOrRegisterCounter(
'app',
'moderation_total',
'审核总数',
['action']
);
$counter->inc(['action' => $action]);
}
}
六、后台管理系统
6.1 人工审核工作台
接口:获取待审核列表
<?php
namespace App\Controller\Admin;
use App\Model\Post;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\GetMapping;
#[Controller(prefix: '/api/v1/admin/moderations')]
class ModerationController
{
#[GetMapping(path: 'pending')]
public function getPendingList()
{
$posts = Post::where('status', 'manual_review')
->with(['tasks', 'decisions'])
->orderBy('created_at', 'asc')
->paginate(20);
return [
'code' => 0,
'data' => $posts,
];
}
#[PostMapping(path: '{taskId}/decision')]
public function makeDecision(int $taskId, ModerationDecisionRequest $request)
{
$action = $request->input('action'); // ALLOW | REJECT | REWRITE
$note = $request->input('note');
$operatorId = $this->getAdminId();
$task = ModerationTask::findOrFail($taskId);
$post = Post::find($task->post_id);
// 记录决策
ModerationDecision::create([
'task_id' => $taskId,
'post_id' => $task->post_id,
'action' => $action,
'decided_by' => 'HUMAN',
'operator_id' => $operatorId,
'reason' => $note,
]);
// 更新帖子状态
$post->status = $action === 'ALLOW' ? 'approved' : 'rejected';
$post->visible_to = $action === 'ALLOW' ? 'public' : 'author_only';
$post->save();
return [
'code' => 0,
'message' => '审核完成',
];
}
}
6.2 敏感词规则管理
接口:规则列表与更新
<?php
namespace App\Controller\Admin;
use App\Model\SensitiveRule;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\GetMapping;
use Hyperf\HttpServer\Annotation\PostMapping;
use Hyperf\HttpServer\Annotation\PutMapping;
#[Controller(prefix: '/api/v1/admin/rules')]
class RuleController
{
#[GetMapping(path: '')]
public function list()
{
$rules = SensitiveRule::orderBy('priority', 'desc')->get();
return [
'code' => 0,
'data' => $rules,
];
}
#[PostMapping(path: '')]
public function create(RuleRequest $request)
{
$rule = SensitiveRule::create($request->validated());
// 清除缓存
cache()->delete('sensitive_rules');
return [
'code' => 0,
'data' => $rule,
];
}
#[PutMapping(path: '{id}')]
public function update(int $id, RuleRequest $request)
{
$rule = SensitiveRule::findOrFail($id);
$rule->update($request->validated());
// 清除缓存
cache()->delete('sensitive_rules');
return [
'code' => 0,
'data' => $rule,
];
}
}
6.3 规则命中审计
功能说明:追踪每个规则的实际命中情况,分析规则有效性,发现并处理误报。
数据库设计
-- 规则命中记录表
CREATE TABLE `rule_hits` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '记录ID',
`rule_id` INT UNSIGNED NOT NULL COMMENT '规则ID',
`post_id` BIGINT UNSIGNED NOT NULL COMMENT '帖子ID',
`matched_text` VARCHAR(500) NULL COMMENT '命中的文本片段',
`action_taken` TINYINT NOT NULL COMMENT '采取的动作:1-放行 2-拒绝 3-替换 4-人工',
`final_status` TINYINT NULL COMMENT '帖子最终状态:1-通过 2-拒绝',
`is_effective` TINYINT NULL COMMENT '是否有效:0-误报 1-有效 NULL-待确认',
`feedback_reason` VARCHAR(200) NULL COMMENT '反馈原因(误报时填写)',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
INDEX idx_rule_id (`rule_id`),
INDEX idx_post_id (`post_id`),
INDEX idx_is_effective (`is_effective`),
INDEX idx_created_at (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规则命中记录表';
-- 规则命中统计表(每日汇总)
CREATE TABLE `rule_hit_stats` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`rule_id` INT UNSIGNED NOT NULL COMMENT '规则ID',
`hit_count` INT NOT NULL DEFAULT 0 COMMENT '命中次数',
`effective_count` INT NOT NULL DEFAULT 0 COMMENT '有效次数',
`false_positive_count` INT NOT NULL DEFAULT 0 COMMENT '误报次数',
`date` DATE NOT NULL COMMENT '统计日期',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_rule_date (`rule_id`, `date`),
INDEX idx_date (`date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规则命中统计表';
核心功能代码
<?php
namespace App\Service;
use App\Dao\RuleHitDao;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Redis\Redis;
use Hyperf\DbConnection\Db;
class RuleAuditService
{
#[Inject]
private RuleHitDao $ruleHitDao;
#[Inject]
private Redis $redis;
/**
* 记录规则命中(在规则审核时调用)
*
* @param int $ruleId 规则ID
* @param int $postId 帖子ID
* @param string $matchedText 命中文本
* @param int $actionTaken 采取的动作
*/
public function recordRuleHit(int $ruleId, int $postId, string $matchedText, int $actionTaken): void
{
// 异步记录,避免影响审核流程性能
$this->ruleHitDao->create([
'rule_id' => $ruleId,
'post_id' => $postId,
'matched_text' => mb_substr($matchedText, 0, 500), // 限制长度
'action_taken' => $actionTaken,
]);
// 更新实时统计(Redis 计数器)
$today = date('Y-m-d');
$this->redis->hincrby("rule_hits:{$today}", $ruleId, 1);
}
/**
* 分析规则有效性
*
* @param int $ruleId 规则ID
* @param string $startDate 开始日期
* @param string $endDate 结束日期
* @return array
*/
public function analyzeRuleEffectiveness(int $ruleId, string $startDate, string $endDate): array
{
// 统计数据
$stats = Db::table('rule_hits')
->where('rule_id', $ruleId)
->whereBetween('created_at', [$startDate, $endDate])
->selectRaw('
COUNT(*) as total,
SUM(CASE WHEN is_effective = 1 THEN 1 ELSE 0 END) as effective_count,
SUM(CASE WHEN is_effective = 0 THEN 1 ELSE 0 END) as false_positive_count
')
->first();
// 误报原因 TOP5
$topReasons = Db::table('rule_hits')
->where('rule_id', $ruleId)
->where('is_effective', 0)
->whereBetween('created_at', [$startDate, $endDate])
->selectRaw('feedback_reason, COUNT(*) as count')
->whereNotNull('feedback_reason')
->groupBy('feedback_reason')
->orderByDesc('count')
->limit(5)
->get();
$total = $stats->total ?? 0;
$effective = $stats->effective_count ?? 0;
$falsePositive = $stats->false_positive_count ?? 0;
return [
'total_hits' => $total,
'effective_count' => $effective,
'false_positive_count' => $falsePositive,
'effectiveness_rate' => $total > 0 ? ($effective / $total) * 100 : 0,
'false_positive_rate' => $total > 0 ? ($falsePositive / $total) * 100 : 0,
'top_false_reasons' => $topReasons->toArray(),
];
}
/**
* 生成规则优化建议
*
* @param int $ruleId
* @return array
*/
public function generateOptimizationSuggestions(int $ruleId): array
{
$analysis = $this->analyzeRuleEffectiveness($ruleId, date('Y-m-d', strtotime('-30 days')), date('Y-m-d'));
$suggestions = [];
// 建议1:误报率过高
if ($analysis['false_positive_rate'] > 10) {
$suggestions[] = [
'type' => 'high_false_positive',
'severity' => 'high',
'message' => "误报率 {$analysis['false_positive_rate']}% 过高,建议优化规则",
'actions' => [
'调整匹配模式,增加上下文判断',
'添加品牌白名单',
'降低规则优先级',
],
];
}
// 建议2:命中次数过少
if ($analysis['total_hits'] < 10) {
$suggestions[] = [
'type' => 'low_hits',
'severity' => 'medium',
'message' => '命中次数较少,规则可能过于严格或不常见',
'actions' => [
'考虑禁用此规则',
'放宽匹配条件',
'合并到其他规则',
],
];
}
// 建议3:完全有效
if ($analysis['effectiveness_rate'] >= 99 && $analysis['total_hits'] > 100) {
$suggestions[] = [
'type' => 'highly_effective',
'severity' => 'info',
'message' => '规则效果优秀,建议提高优先级',
'actions' => [
'提高优先级,优先执行',
'扩展到其他语言',
],
];
}
return $suggestions;
}
/**
* 获取规则命中排行
*/
public function getRuleHitRanking(string $startDate, string $endDate, int $limit = 20): array
{
return Db::table('rule_hits as h')
->join('sensitive_rules as r', 'h.rule_id', '=', 'r.id')
->selectRaw('
h.rule_id,
r.pattern,
r.strategy,
COUNT(*) as hit_count,
SUM(CASE WHEN h.is_effective = 1 THEN 1 ELSE 0 END) as effective_count,
SUM(CASE WHEN h.is_effective = 0 THEN 1 ELSE 0 END) as false_positive_count
')
->whereBetween('h.created_at', [$startDate, $endDate])
->groupBy('h.rule_id', 'r.pattern', 'r.strategy')
->orderByDesc('hit_count')
->limit($limit)
->get()
->toArray();
}
}
命中审计 API 接口
<?php
namespace App\Controller\Admin;
use App\Service\RuleAuditService;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\GetMapping;
use Hyperf\HttpServer\Annotation\PostMapping;
use Hyperf\Di\Annotation\Inject;
#[Controller(prefix: '/api/v1/admin/rule-audit')]
class RuleAuditController extends AbstractController
{
#[Inject]
private RuleAuditService $ruleAuditService;
/**
* 获取规则命中详情
*/
#[GetMapping(path: 'rules/{rule_id}/hits')]
public function getHitDetails(int $rule_id): array
{
$page = (int) $this->request->input('page', 1);
$pageSize = (int) $this->request->input('page_size', 20);
$startDate = $this->request->input('start_date', date('Y-m-d', strtotime('-7 days')));
$endDate = $this->request->input('end_date', date('Y-m-d'));
// 命中记录列表
$hitList = $this->ruleAuditService->getRuleHitList($rule_id, $page, $pageSize, $startDate, $endDate);
// 效果分析
$analysis = $this->ruleAuditService->analyzeRuleEffectiveness($rule_id, $startDate, $endDate);
// 优化建议
$suggestions = $this->ruleAuditService->generateOptimizationSuggestions($rule_id);
return $this->success([
'list' => $hitList['list'],
'total' => $hitList['total'],
'page' => $page,
'page_size' => $pageSize,
'analysis' => $analysis,
'suggestions' => $suggestions,
]);
}
/**
* 标记命中为误报
*/
#[PostMapping(path: 'hits/{hit_id}/mark-false-positive')]
public function markFalsePositive(int $hit_id): array
{
$reason = $this->request->input('reason');
$this->ruleAuditService->markAsFalsePositive($hit_id, $reason);
return $this->success(['message' => '已标记为误报']);
}
/**
* 获取规则命中排行
*/
#[GetMapping(path: 'ranking')]
public function ranking(): array
{
$startDate = $this->request->input('start_date', date('Y-m-d', strtotime('-30 days')));
$endDate = $this->request->input('end_date', date('Y-m-d'));
$ranking = $this->ruleAuditService->getRuleHitRanking($startDate, $endDate);
return $this->success([
'list' => $ranking,
]);
}
}
6.4 策略灰度与功能开关
功能说明:支持新功能的灰度发布、AB 测试、紧急回滚,确保系统稳定性。
数据库设计
-- 功能开关配置表
CREATE TABLE `feature_flags` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`feature_key` VARCHAR(50) NOT NULL COMMENT '功能标识(如:ai_moderation)',
`feature_name` VARCHAR(100) NOT NULL COMMENT '功能名称',
`description` TEXT NULL COMMENT '功能描述',
`enabled` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-关闭 1-开启 2-灰度中',
`rollout_percentage` INT NOT NULL DEFAULT 0 COMMENT '灰度比例:0-100',
`rollout_strategy` TINYINT NOT NULL DEFAULT 1 COMMENT '灰度策略:1-随机 2-用户ID哈希 3-地区 4-白名单',
`whitelist` JSON NULL COMMENT '白名单用户ID列表',
`blacklist` JSON NULL COMMENT '黑名单用户ID列表',
`config` JSON NULL COMMENT '额外配置参数',
`created_by` BIGINT UNSIGNED NULL COMMENT '创建人',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_feature_key (`feature_key`),
INDEX idx_enabled (`enabled`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='功能开关配置表';
-- AB 测试配置表
CREATE TABLE `ab_experiments` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`experiment_key` VARCHAR(50) NOT NULL COMMENT '实验标识',
`experiment_name` VARCHAR(100) NOT NULL COMMENT '实验名称',
`description` TEXT NULL COMMENT '实验描述',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-草稿 1-进行中 2-已结束 3-已发布',
`traffic_split` JSON NOT NULL COMMENT '流量分配:{"control": 50, "variant": 50}',
`control_config` JSON NULL COMMENT '对照组配置',
`variant_config` JSON NOT NULL COMMENT '实验组配置',
`start_at` DATETIME NULL COMMENT '开始时间',
`end_at` DATETIME NULL COMMENT '结束时间',
`created_by` BIGINT UNSIGNED NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_experiment_key (`experiment_key`),
INDEX idx_status (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AB测试配置表';
-- AB 测试指标表
CREATE TABLE `ab_metrics` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`experiment_id` INT UNSIGNED NOT NULL,
`group_type` TINYINT NOT NULL COMMENT '组别:1-对照组 2-实验组',
`user_id` BIGINT UNSIGNED NOT NULL,
`post_id` BIGINT UNSIGNED NULL,
`metric_type` VARCHAR(50) NOT NULL COMMENT '指标类型:manual_review_rate, false_positive_rate等',
`metric_value` DECIMAL(10,4) NOT NULL COMMENT '指标值',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_experiment_group (`experiment_id`, `group_type`),
INDEX idx_metric_type (`metric_type`),
INDEX idx_created_at (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AB测试指标表';
功能开关服务
<?php
namespace App\Service;
use App\Dao\FeatureFlagDao;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Redis\Redis;
class FeatureFlagService
{
#[Inject]
private FeatureFlagDao $flagDao;
#[Inject]
private Redis $redis;
/**
* 检查功能是否对用户启用(支持灰度)
*
* @param string $featureKey 功能标识
* @param int $userId 用户ID
* @return bool
*/
public function isEnabled(string $featureKey, int $userId): bool
{
// 1. 从缓存获取配置(5分钟缓存)
$cacheKey = "feature_flag:{$featureKey}";
$cached = $this->redis->get($cacheKey);
if ($cached) {
$flag = json_decode($cached, true);
} else {
$flag = $this->flagDao->findByKey($featureKey);
if (!$flag) {
return false; // 功能不存在,默认关闭
}
$this->redis->setex($cacheKey, 300, json_encode($flag));
}
// 2. 完全关闭
if ($flag['enabled'] === 0) {
return false;
}
// 3. 完全开启
if ($flag['enabled'] === 1) {
return true;
}
// 4. 灰度模式 (enabled === 2)
return $this->checkRollout($userId, $flag);
}
/**
* 灰度策略检查
*/
private function checkRollout(int $userId, array $flag): bool
{
// 检查白名单(最高优先级)
$whitelist = json_decode($flag['whitelist'] ?? '[]', true);
if (in_array($userId, $whitelist)) {
return true;
}
// 检查黑名单
$blacklist = json_decode($flag['blacklist'] ?? '[]', true);
if (in_array($userId, $blacklist)) {
return false;
}
$percentage = $flag['rollout_percentage'];
$strategy = $flag['rollout_strategy'];
switch ($strategy) {
case 1: // 随机灰度(每次请求随机)
return (mt_rand(1, 100) <= $percentage);
case 2: // 用户ID哈希(同一用户结果稳定)
$hash = crc32($userId) % 100;
return ($hash < $percentage);
case 3: // 地区灰度
$userRegion = $this->getUserRegion($userId);
$config = json_decode($flag['config'] ?? '{}', true);
$allowedRegions = $config['regions'] ?? [];
return in_array($userRegion, $allowedRegions);
case 4: // 仅白名单
return false;
default:
return false;
}
}
/**
* 调整灰度比例
*/
public function adjustRollout(string $featureKey, int $percentage): void
{
if ($percentage < 0 || $percentage > 100) {
throw new \InvalidArgumentException('灰度比例必须在 0-100 之间');
}
$enabled = 2; // 灰度中
if ($percentage === 0) {
$enabled = 0; // 关闭
} elseif ($percentage === 100) {
$enabled = 1; // 完全开启
}
$this->flagDao->updateByKey($featureKey, [
'rollout_percentage' => $percentage,
'enabled' => $enabled,
]);
// 清除缓存
$this->redis->del("feature_flag:{$featureKey}");
// 记录操作日志
logger('feature_flag')->info('调整灰度比例', [
'feature_key' => $featureKey,
'percentage' => $percentage,
]);
}
/**
* 全量发布
*/
public function publishFull(string $featureKey): void
{
$this->adjustRollout($featureKey, 100);
}
/**
* 紧急回滚
*/
public function rollback(string $featureKey): void
{
$this->flagDao->updateByKey($featureKey, [
'enabled' => 0,
'rollout_percentage' => 0,
]);
$this->redis->del("feature_flag:{$featureKey}");
logger('feature_flag')->warning('功能已紧急回滚', [
'feature_key' => $featureKey,
]);
}
}
AB 测试服务
<?php
namespace App\Service;
use App\Dao\ABExperimentDao;
use App\Dao\ABMetricDao;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Redis\Redis;
use Hyperf\DbConnection\Db;
class ABTestService
{
#[Inject]
private ABExperimentDao $experimentDao;
#[Inject]
private ABMetricDao $metricDao;
#[Inject]
private Redis $redis;
/**
* 分配用户到实验组(保证一致性)
*
* @param string $experimentKey 实验标识
* @param int $userId 用户ID
* @return string 'control' | 'variant'
*/
public function assignGroup(string $experimentKey, int $userId): string
{
// 1. 检查缓存(保证同一用户分组一致)
$cacheKey = "ab_group:{$experimentKey}:{$userId}";
$cached = $this->redis->get($cacheKey);
if ($cached) {
return $cached;
}
// 2. 获取实验配置
$experiment = $this->experimentDao->findByKey($experimentKey);
if (!$experiment || $experiment['status'] !== 1) {
return 'control'; // 实验未进行,默认对照组
}
// 3. 一致性哈希分配
$trafficSplit = json_decode($experiment['traffic_split'], true);
$controlPercentage = $trafficSplit['control'] ?? 50;
$hash = crc32("{$experimentKey}:{$userId}") % 100;
$group = ($hash < $controlPercentage) ? 'control' : 'variant';
// 4. 缓存分组结果(实验期间保持一致)
$expireAt = strtotime($experiment['end_at']) - time();
$this->redis->setex($cacheKey, max($expireAt, 86400), $group);
return $group;
}
/**
* 记录实验指标
*/
public function recordMetric(string $experimentKey, int $userId, ?int $postId, string $metricType, float $value): void
{
$experiment = $this->experimentDao->findByKey($experimentKey);
if (!$experiment) {
return;
}
$group = $this->assignGroup($experimentKey, $userId);
$groupType = ($group === 'control') ? 1 : 2;
$this->metricDao->create([
'experiment_id' => $experiment['id'],
'group_type' => $groupType,
'user_id' => $userId,
'post_id' => $postId,
'metric_type' => $metricType,
'metric_value' => $value,
]);
}
/**
* 获取实验结果对比
*/
public function getExperimentResults(int $experimentId): array
{
// 对照组统计
$controlStats = $this->getGroupStatistics($experimentId, 1);
// 实验组统计
$variantStats = $this->getGroupStatistics($experimentId, 2);
// 计算提升度
$improvements = $this->calculateImprovements($controlStats, $variantStats);
return [
'control' => $controlStats,
'variant' => $variantStats,
'improvements' => $improvements,
'recommendation' => $this->generateRecommendation($improvements),
];
}
/**
* 获取分组统计数据
*/
private function getGroupStatistics(int $experimentId, int $groupType): array
{
return Db::table('ab_metrics')
->where('experiment_id', $experimentId)
->where('group_type', $groupType)
->selectRaw('
metric_type,
COUNT(DISTINCT user_id) as user_count,
COUNT(*) as sample_count,
AVG(metric_value) as avg_value,
STDDEV(metric_value) as stddev_value,
MIN(metric_value) as min_value,
MAX(metric_value) as max_value
')
->groupBy('metric_type')
->get()
->keyBy('metric_type')
->toArray();
}
/**
* 计算提升度
*/
private function calculateImprovements(array $controlStats, array $variantStats): array
{
$improvements = [];
foreach ($controlStats as $metricType => $control) {
if (isset($variantStats[$metricType])) {
$variant = $variantStats[$metricType];
$controlAvg = $control['avg_value'];
$variantAvg = $variant['avg_value'];
if ($controlAvg != 0) {
$improvement = (($variantAvg - $controlAvg) / $controlAvg) * 100;
$improvements[$metricType] = round($improvement, 2);
}
}
}
return $improvements;
}
/**
* 生成发布建议
*/
private function generateRecommendation(array $improvements): array
{
// 关键指标:误报率(越低越好)
$falsePositiveImprovement = $improvements['false_positive_rate'] ?? 0;
if ($falsePositiveImprovement < -20) {
return [
'action' => 'publish_variant',
'reason' => '实验组误报率显著降低,建议全量发布',
'confidence' => 'high',
];
}
if ($falsePositiveImprovement > 10) {
return [
'action' => 'keep_control',
'reason' => '实验组误报率升高,建议保持对照组配置',
'confidence' => 'high',
];
}
return [
'action' => 'extend_experiment',
'reason' => '数据差异不显著,建议延长实验时间收集更多数据',
'confidence' => 'medium',
];
}
}
功能开关 API 接口
<?php
namespace App\Controller\Admin;
use App\Service\FeatureFlagService;
use App\Service\ABTestService;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\GetMapping;
use Hyperf\HttpServer\Annotation\PostMapping;
use Hyperf\HttpServer\Annotation\PutMapping;
use Hyperf\Di\Annotation\Inject;
#[Controller(prefix: '/api/v1/admin/feature-flags')]
class FeatureFlagController extends AbstractController
{
#[Inject]
private FeatureFlagService $flagService;
/**
* 功能开关列表
*/
#[GetMapping(path: '')]
public function list(): array
{
$flags = $this->flagService->getAllFlags();
return $this->success([
'list' => $flags,
]);
}
/**
* 创建功能开关
*/
#[PostMapping(path: '')]
public function create(): array
{
$data = [
'feature_key' => $this->request->input('feature_key'),
'feature_name' => $this->request->input('feature_name'),
'description' => $this->request->input('description'),
'enabled' => 0, // 默认关闭
'rollout_percentage' => 0,
];
$flagId = $this->flagService->createFlag($data);
return $this->success([
'info' => ['flag_id' => $flagId],
]);
}
/**
* 调整灰度比例
*/
#[PostMapping(path: '{feature_key}/rollout/{percentage}')]
public function adjustRollout(string $feature_key, int $percentage): array
{
$this->flagService->adjustRollout($feature_key, $percentage);
return $this->success([
'info' => [
'feature_key' => $feature_key,
'new_percentage' => $percentage,
],
], '灰度比例已调整');
}
/**
* 全量发布
*/
#[PostMapping(path: '{feature_key}/publish')]
public function publish(string $feature_key): array
{
$this->flagService->publishFull($feature_key);
return $this->success(['message' => '已全量发布']);
}
/**
* 紧急回滚
*/
#[PostMapping(path: '{feature_key}/rollback')]
public function rollback(string $feature_key): array
{
$reason = $this->request->input('reason', '紧急回滚');
$this->flagService->rollback($feature_key);
// 发送告警通知
event(new FeatureRollback($feature_key, $reason));
return $this->success(['message' => '已紧急回滚']);
}
/**
* 获取灰度数据
*/
#[GetMapping(path: '{feature_key}/metrics')]
public function getMetrics(string $feature_key): array
{
$metrics = $this->flagService->getFeatureMetrics($feature_key);
return $this->success([
'info' => $metrics,
]);
}
}
/**
* AB 测试控制器
*/
#[Controller(prefix: '/api/v1/admin/experiments')]
class ExperimentController extends AbstractController
{
#[Inject]
private ABTestService $abTestService;
/**
* 实验列表
*/
#[GetMapping(path: '')]
public function list(): array
{
$experiments = $this->abTestService->getAllExperiments();
return $this->success([
'list' => $experiments,
]);
}
/**
* 创建实验
*/
#[PostMapping(path: '')]
public function create(): array
{
$data = [
'experiment_key' => $this->request->input('experiment_key'),
'experiment_name' => $this->request->input('experiment_name'),
'description' => $this->request->input('description'),
'traffic_split' => $this->request->input('traffic_split'),
'variant_config' => $this->request->input('variant_config'),
];
$experimentId = $this->abTestService->createExperiment($data);
return $this->success([
'info' => ['experiment_id' => $experimentId],
]);
}
/**
* 启动实验
*/
#[PostMapping(path: '{experiment_id}/start')]
public function start(int $experiment_id): array
{
$this->abTestService->startExperiment($experiment_id);
return $this->success(['message' => '实验已启动']);
}
/**
* 停止实验
*/
#[PostMapping(path: '{experiment_id}/stop')]
public function stop(int $experiment_id): array
{
$this->abTestService->stopExperiment($experiment_id);
return $this->success(['message' => '实验已停止']);
}
/**
* 获取实验结果
*/
#[GetMapping(path: '{experiment_id}/results')]
public function getResults(int $experiment_id): array
{
$results = $this->abTestService->getExperimentResults($experiment_id);
return $this->success([
'info' => [
'control_stats' => $results['control'],
'variant_stats' => $results['variant'],
'improvements' => $results['improvements'],
'recommendation' => $results['recommendation'],
],
]);
}
/**
* 全量发布获胜组
*/
#[PostMapping(path: '{experiment_id}/publish')]
public function publish(int $experiment_id): array
{
$winningGroup = $this->request->input('winning_group'); // 'control' | 'variant'
$this->abTestService->publishWinningGroup($experiment_id, $winningGroup);
return $this->success(['message' => '实验结果已全量发布']);
}
}
在审核流水线中集成功能开关
<?php
namespace App\Service;
class ModerationService
{
#[Inject]
private FeatureFlagService $featureFlagService;
#[Inject]
private ABTestService $abTestService;
public function processModerationPipeline(int $postId): void
{
$post = $this->postDao->findById($postId);
$userId = $post['user_id'];
$content = $post['content_raw'];
// 第1步:规则审核(始终执行)
$ruleResult = $this->ruleCheckPipeline->check($content);
if ($ruleResult['action'] === 'reject') {
$this->rejectPost($post, $ruleResult['reason']);
return;
}
// 第2步:AI 审核(功能开关控制)
if ($this->featureFlagService->isEnabled('ai_moderation', $userId)) {
// AB 测试:不同的评分阈值
$group = $this->abTestService->assignGroup('ai_threshold_test', $userId);
if ($group === 'control') {
// 对照组:原阈值 0.3 / 0.7
$lowThreshold = 0.3;
$highThreshold = 0.7;
} else {
// 实验组:新阈值 0.2 / 0.6
$lowThreshold = 0.2;
$highThreshold = 0.6;
}
$aiResult = $this->aiCheckPipeline->check($content, $postId);
// 记录 AB 测试指标
$this->abTestService->recordMetric(
'ai_threshold_test',
$userId,
$postId,
'ai_score',
$aiResult['score']
);
// 使用对应阈值判断
if ($aiResult['score'] < $lowThreshold) {
$this->approvePost($post);
$this->abTestService->recordMetric(
'ai_threshold_test',
$userId,
$postId,
'auto_approved',
1
);
return;
}
if ($aiResult['score'] > $highThreshold) {
$this->rejectPost($post, $aiResult['reason']);
$this->abTestService->recordMetric(
'ai_threshold_test',
$userId,
$postId,
'auto_rejected',
1
);
return;
}
}
// 第3步:改写建议(灰度功能)
if ($this->featureFlagService->isEnabled('ai_rewrite', $userId)) {
$this->rewritePipeline->generateSuggestion($post);
}
// 转人工审核
$this->postDao->update($post['id'], [
'status' => PostStatus::MANUAL_REVIEW,
]);
// 记录转人工指标
$this->abTestService->recordMetric(
'ai_threshold_test',
$userId,
$postId,
'manual_review',
1
);
}
}
6.5 权限控制(RBAC)
中间件:权限校验
<?php
namespace App\Middleware;
use Hyperf\Context\Context;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class AdminAuthMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$token = $request->getHeaderLine('Authorization');
if (!$token) {
return response()->json(['code' => 401, 'message' => 'Unauthorized'], 401);
}
// 验证 Token(JWT)
$adminId = $this->validateToken($token);
if (!$adminId) {
return response()->json(['code' => 401, 'message' => 'Invalid token'], 401);
}
// 检查权限
if (!$this->checkPermission($adminId, $request->getUri()->getPath())) {
return response()->json(['code' => 403, 'message' => 'Forbidden'], 403);
}
// 存储到上下文
Context::set('admin_id', $adminId);
return $handler->handle($request);
}
private function validateToken(string $token): ?int
{
// JWT 验证逻辑
return 1; // 示例返回管理员 ID
}
private function checkPermission(int $adminId, string $path): bool
{
// 权限校验逻辑(RBAC)
return true;
}
}
七、性能优化
7.1 协程并发优化
并发调用多个 API
<?php
use Hyperf\Coroutine\Parallel;
$parallel = new Parallel(5); // 最多 5 个并发
// 同时调用多个 AI API
$parallel->add(function () use ($content) {
return $this->aiService->moderateContent($content);
});
$parallel->add(function () use ($content) {
return $this->aiService->analyzeEmotion($content);
});
$parallel->add(function () use ($userId) {
return $this->userService->getUserHistory($userId);
});
$results = $parallel->wait(); // 等待所有协程完成
7.2 数据库连接池优化
// config/autoload/databases.php
return [
'default' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'database' => env('DB_DATABASE', 'ugc'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'pool' => [
'min_connections' => 10, // 最小连接数
'max_connections' => 50, // 最大连接数
'connect_timeout' => 10.0, // 连接超时
'wait_timeout' => 3.0, // 等待超时
'heartbeat' => 60, // 心跳间隔
],
],
];
7.3 Redis 连接池优化
// config/autoload/redis.php
return [
'default' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => (int) env('REDIS_PORT', 6379),
'pool' => [
'min_connections' => 5,
'max_connections' => 20,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => 30,
],
],
];
7.4 缓存策略
多级缓存
<?php
namespace App\Service;
use Hyperf\Cache\Annotation\Cacheable;
class PostService
{
/**
* 缓存帖子详情(本地内存 + Redis)
*/
#[Cacheable(prefix: 'post_detail', ttl: 600, value: '#{id}')]
public function getPostDetail(int $id): array
{
// 查询数据库
return Post::find($id)->toArray();
}
}
缓存预热
<?php
namespace App\Crontab;
use Hyperf\Crontab\Annotation\Crontab;
#[Crontab(rule: '*/5 * * * *', name: 'CacheWarmup')]
class CacheWarmupTask
{
public function execute()
{
// 预热热门帖子缓存
$hotPosts = Post::where('views', '>', 10000)->pluck('id');
foreach ($hotPosts as $postId) {
$this->postService->getPostDetail($postId);
}
}
}