12 KiB
12 KiB
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/O(
file_get_contents,sleep) - 禁止使用全局变量 / 全局静态属性存储请求数据
- 禁止
dd()/var_dump()残留在代码中 - 禁止
SELECT *,明确列名 - 禁止在循环中执行 SQL(N+1 问题)
- 禁止在事件监听器中抛出异常阻塞主流程
- 禁止在定时任务中未加
onOneServer导致多实例重复执行