Files
vibe_coding/.cursor/rules/references/013-backend-deep.md
2026-03-05 21:27:11 +08:00

12 KiB
Raw Blame History

013-backend.mdc (Deep Reference)

该文件为原始详细规范归档,供 Tier 3 按需读取。


🖥️ PHP Hyperf + Swoole Backend Standards

参考文档: @docs/architecture/api-contracts.md @docs/architecture/system-design.md

分层架构

Request → Middleware → Controller → Service → Repository → Model → DB
                                       ↓
                                    Event → Listener (异步)
职责 禁止
Controller 接收请求、参数验证、调用 Service、返回响应 包含业务逻辑
Service 核心业务逻辑、事务管理、调用 Repository 直接操作数据库
Repository 数据访问、查询构建、数据权限过滤 包含业务判断
Model 数据模型、关联定义、类型转换 包含业务方法

PHP 编码规范 (PSR-12)

<?php

declare(strict_types=1);

namespace App\Service\Production;

use App\Model\Production\ProductionOrder;
use App\Repository\Production\OrderRepository;
use Hyperf\Di\Annotation\Inject;
use Hyperf\DbConnection\Db;

class OrderService
{
    #[Inject]
    protected OrderRepository $orderRepository;

    public function create(array $data): ProductionOrder
    {
        return Db::transaction(function () use ($data) {
            $order = $this->orderRepository->create($data);
            // trigger event
            event(new OrderCreated($order));
            return $order;
        });
    }
}

命名规范:

类型 规范 示例
PascalCase OrderService
方法 camelCase createOrder()
变量 camelCase $orderData
常量 SCREAMING_SNAKE MAX_RETRY_COUNT
数据库字段 snake_case created_at
路由 kebab-case 复数 /admin/production-orders

API 设计

  • RESTful 命名: 复数名词 (/admin/users, /admin/production-orders)
  • 版本控制: /admin/v1/...(可选)
  • 统一响应格式:
// 成功
{ "code": 200, "message": "ok", "data": T }

// 列表(带分页)
{ "code": 200, "message": "ok", "data": { "items": [], "total": 100 } }

// 错误
{ "code": 422, "message": "Validation failed", "data": { "errors": {} } }

输入验证

// app/Request/Production/CreateOrderRequest.php
<?php

declare(strict_types=1);

namespace App\Request\Production;

use Hyperf\Validation\Request\FormRequest;

class CreateOrderRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'customer_id' => 'required|integer|exists:customers,id',
            'platform_id' => 'required|integer|exists:platforms,id',
            'order_type'  => 'required|integer|in:1,2,3',
            'remark'      => 'nullable|string|max:500',
        ];
    }
}

认证/授权

  • JWT 双 Token 机制: Access Token (2h) + Refresh Token (7d)
  • Token 存储在 Redis支持强制失效
  • 密码: password_hash() + PASSWORD_BCRYPT
  • 速率限制: #[RateLimit] 注解

中间件链

Request
  → ErrorLogMiddleware          # 异常捕获
  → AccessTokenMiddleware       # JWT 验证
  → PermissionMiddleware        # RBAC 权限
  → DataPermissionMiddleware    # 数据权限过滤
  → OperationMiddleware         # 操作日志
  → Controller

异常处理器链

异常处理器按优先级顺序注册,每个 Handler 只处理对应的异常类型:

// config/autoload/exceptions.php
return [
    'handler' => [
        'http' => [
            // Priority: higher number = higher priority
            ValidationExceptionHandler::class,    // 验证异常 → 422
            AuthExceptionHandler::class,          // 认证异常 → 401
            PermissionExceptionHandler::class,    // 权限异常 → 403
            BusinessExceptionHandler::class,      // 业务异常 → 自定义 code
            RateLimitExceptionHandler::class,     // 限流异常 → 429
            ModelNotFoundExceptionHandler::class, // 模型未找到 → 404
            QueryExceptionHandler::class,         // 数据库异常 → 500隐藏细节
            AppExceptionHandler::class,           // 兜底异常 → 500
        ],
    ],
];
// app/Exception/BusinessException.php
namespace App\Exception;

use Hyperf\Server\Exception\ServerException;

class BusinessException extends ServerException
{
    public function __construct(
        int $code = 500,
        string $message = '',
        ?\Throwable $previous = null,
    ) {
        parent::__construct($message, $code, $previous);
    }
}

// app/Exception/Handler/BusinessExceptionHandler.php
class BusinessExceptionHandler extends ExceptionHandler
{
    public function handle(Throwable $throwable, ResponseInterface $response): ResponseInterface
    {
        $this->stopPropagation();

        return $response->withStatus(200)->withBody(new SwooleStream(json_encode([
            'code'    => $throwable->getCode(),
            'message' => $throwable->getMessage(),
            'data'    => null,
        ], JSON_UNESCAPED_UNICODE)));
    }

    public function isValid(Throwable $throwable): bool
    {
        return $throwable instanceof BusinessException;
    }
}

事件系统

Event + Listener 解耦异步逻辑,避免 Service 膨胀:

事件 触发时机 监听器
OrderCreated 订单创建后 发送通知、记录日志、同步第三方
OrderStatusChanged 状态变更后 更新统计、通知相关人
PaymentReceived 收款确认后 更新订单状态、触发生产
UserLoggedIn 登录成功 记录登录日志、更新最后登录时间
UserRegistered 注册成功 发送欢迎邮件、初始化默认数据
WorkflowApproved 审批通过 推进流程、通知下一节点
WorkflowRejected 审批驳回 通知发起人、记录原因
FileUploaded 文件上传完成 生成缩略图、同步到 OSS
NotificationSent 通知发出 WebSocket 推送、记录日志
CacheInvalidated 缓存失效 预热缓存、记录日志
// app/Event/OrderCreated.php
class OrderCreated
{
    public function __construct(public readonly ProductionOrder $order) {}
}

// app/Listener/SendOrderNotificationListener.php
#[Listener]
class SendOrderNotificationListener implements ListenerInterface
{
    public function listen(): array
    {
        return [OrderCreated::class];
    }

    public function process(object $event): void
    {
        /** @var OrderCreated $event */
        $this->notificationService->send(
            userId: $event->order->created_by,
            type: 'order_created',
            data: ['order_no' => $event->order->order_no],
        );
    }
}

数据权限过滤

5 级 DATA_SCOPE 控制用户可见数据范围,在 Repository 层自动过滤:

级别 常量 说明
全部 DATA_SCOPE_ALL 管理员,无限制
自定义 DATA_SCOPE_CUSTOM 指定部门集合
本部门 DATA_SCOPE_DEPT 仅本部门数据
本部门及下级 DATA_SCOPE_DEPT_AND_CHILD 本部门 + 子部门
仅本人 DATA_SCOPE_SELF 仅自己创建的数据
// app/Repository/Concern/DataPermissionTrait.php
trait DataPermissionTrait
{
    protected function applyDataScope(Builder $query): Builder
    {
        $user = Context::get('current_user');
        if (!$user) return $query;

        return match ($user->data_scope) {
            DataScope::ALL => $query,
            DataScope::CUSTOM => $query->whereIn('dept_id', $user->custom_dept_ids),
            DataScope::DEPT => $query->where('dept_id', $user->dept_id),
            DataScope::DEPT_AND_CHILD => $query->whereIn(
                'dept_id',
                $this->deptService->getChildDeptIds($user->dept_id),
            ),
            DataScope::SELF => $query->where('created_by', $user->id),
        };
    }
}

分布式锁模式

使用 Redis 分布式锁防止并发操作导致数据不一致:

// 订单锁定场景
class OrderLockService
{
    private const LOCK_PREFIX = 'order_lock:';
    private const LOCK_TTL = 30;

    private const LOCK_TYPES = [
        'edit'             => '编辑锁',
        'process'          => '处理锁',
        'payment_pending'  => '待收款锁',
        'payment_missing'  => '缺款锁',
    ];

    public function acquireLock(int $orderId, string $type, int $userId): bool
    {
        $key = self::LOCK_PREFIX . "{$type}:{$orderId}";
        return $this->redis->set($key, $userId, ['NX', 'EX' => self::LOCK_TTL]);
    }

    public function releaseLock(int $orderId, string $type, int $userId): bool
    {
        $key = self::LOCK_PREFIX . "{$type}:{$orderId}";
        // Lua script for atomic check-and-delete
        $script = <<<'LUA'
            if redis.call("get", KEYS[1]) == ARGV[1] then
                return redis.call("del", KEYS[1])
            else
                return 0
            end
        LUA;
        return (bool) $this->redis->eval($script, [$key, (string) $userId], 1);
    }
}

Model Traits

常用 Model Trait 复用数据库行为:

Trait 职责
HasCreator 自动填充 created_by / updated_by
SoftDeletes 软删除 (deleted_at)
HasOperationLog 操作日志记录
HasDataPermission 查询自动加数据权限条件
HasSortable 拖拽排序 (sort_order)
// app/Model/Concern/HasCreator.php
trait HasCreator
{
    public static function bootHasCreator(): void
    {
        static::creating(function (Model $model) {
            $user = Context::get('current_user');
            if ($user) {
                $model->created_by = $user->id;
                $model->updated_by = $user->id;
            }
        });

        static::updating(function (Model $model) {
            $user = Context::get('current_user');
            if ($user) {
                $model->updated_by = $user->id;
            }
        });
    }
}

定时任务

// app/Crontab/DailyStatisticsCrontab.php
#[Crontab(
    name: 'daily_statistics',
    rule: '0 2 * * *',
    singleton: true,
    onOneServer: true,
    memo: '每日凌晨 2 点统计报表'
)]
class DailyStatisticsCrontab
{
    #[Inject]
    protected StatisticsService $statisticsService;

    public function execute(): void
    {
        $this->statisticsService->generateDailyReport(
            Carbon::yesterday()
        );
    }
}

注解说明:

  • singleton: true — 同一进程内不重复执行
  • onOneServer: true — 多实例部署时只在一台执行

请求签名验证

前端签名 + 后端验签,防止请求篡改:

// app/Middleware/RequestSignMiddleware.php
class RequestSignMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $timestamp = $request->getHeaderLine('X-Timestamp');
        $sign      = $request->getHeaderLine('X-Sign');
        $nonce     = $request->getHeaderLine('X-Nonce');

        // Step 1: 时间窗口校验5 分钟内有效)
        if (abs(time() - (int) $timestamp) > 300) {
            throw new BusinessException(403, 'Request expired');
        }

        // Step 2: Nonce 防重放
        if (!$this->redis->set("nonce:{$nonce}", 1, ['NX', 'EX' => 300])) {
            throw new BusinessException(403, 'Duplicate request');
        }

        // Step 3: 签名校验
        $body = (string) $request->getBody();
        $expectedSign = hash_hmac('sha256', "{$timestamp}{$nonce}{$body}", $this->appSecret);

        if (!hash_equals($expectedSign, $sign)) {
            throw new BusinessException(403, 'Invalid signature');
        }

        return $handler->handle($request);
    }
}

依赖注入

// ✅ 构造函数注入(推荐)
public function __construct(
    protected readonly OrderRepository $orderRepo,
    protected readonly CacheInterface $cache,
) {}

// ✅ 属性注入
#[Inject]
protected OrderService $orderService;

// ❌ 禁止手动 new 或 make()
$service = new OrderService(); // WRONG

禁止事项

  • 禁止在 Controller 中直接操作数据库
  • 禁止在 Swoole 环境使用阻塞 I/Ofile_get_contents, sleep
  • 禁止使用全局变量 / 全局静态属性存储请求数据
  • 禁止 dd() / var_dump() 残留在代码中
  • 禁止 SELECT *,明确列名
  • 禁止在循环中执行 SQLN+1 问题)
  • 禁止在事件监听器中抛出异常阻塞主流程
  • 禁止在定时任务中未加 onOneServer 导致多实例重复执行