UGC 内容审核系统技术方案(Hyperf 框架)

51 阅读13分钟

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:数据模型定义、关联关系
开发流程
  1. 接到需求后,先查找 Dao 包是否存在满足的方法
  2. 如果存在,直接调用;如果不存在,才新建方法
  3. 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);
        }
    }
}