446 lines
12 KiB
Markdown
446 lines
12 KiB
Markdown
# 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
|
||
<?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/...`(可选)
|
||
- 统一响应格式:
|
||
|
||
```php
|
||
// 成功
|
||
{ "code": 200, "message": "ok", "data": T }
|
||
|
||
// 列表(带分页)
|
||
{ "code": 200, "message": "ok", "data": { "items": [], "total": 100 } }
|
||
|
||
// 错误
|
||
{ "code": 422, "message": "Validation failed", "data": { "errors": {} } }
|
||
```
|
||
|
||
## 输入验证
|
||
|
||
```php
|
||
// 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 只处理对应的异常类型:
|
||
|
||
```php
|
||
// 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
|
||
],
|
||
],
|
||
];
|
||
```
|
||
|
||
```php
|
||
// 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` | 缓存失效 | 预热缓存、记录日志 |
|
||
|
||
```php
|
||
// 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` | 仅自己创建的数据 |
|
||
|
||
```php
|
||
// 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 分布式锁防止并发操作导致数据不一致:
|
||
|
||
```php
|
||
// 订单锁定场景
|
||
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`) |
|
||
|
||
```php
|
||
// 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;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
## 定时任务
|
||
|
||
```php
|
||
// 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` — 多实例部署时只在一台执行
|
||
|
||
## 请求签名验证
|
||
|
||
前端签名 + 后端验签,防止请求篡改:
|
||
|
||
```php
|
||
// 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);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 依赖注入
|
||
|
||
```php
|
||
// ✅ 构造函数注入(推荐)
|
||
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` 导致多实例重复执行
|