unify controller dir: migrate api/v1 to Api/V1, remove PSR-4 workaround
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\V1;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
use App\Middleware\PermissionMiddleware;
|
||||
use App\Model\ApiKey;
|
||||
use Hyperf\HttpServer\Annotation\Controller;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Hyperf\HttpServer\Annotation\Middleware;
|
||||
use Hyperf\HttpServer\Annotation\RequestMapping;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\Tag(name: 'Admin API Keys', description: '管理员 API Key 管理')]
|
||||
#[Controller(prefix: "/api/v1/admin/api-keys")]
|
||||
class AdminApiKeyController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* 管理员列出所有 API Keys
|
||||
*
|
||||
* 支持按 user_id、enabled 筛选,关联用户信息
|
||||
*/
|
||||
#[OA\Get(
|
||||
path: '/admin/api-keys',
|
||||
summary: '管理员列出所有 API Keys',
|
||||
description: '分页列出所有用户的 API Keys,支持按 user_id、enabled 筛选,关联用户基本信息',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Admin API Keys'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'page', in: 'query', required: false, description: '页码,默认 1', schema: new OA\Schema(type: 'integer', default: 1)),
|
||||
new OA\Parameter(name: 'per_page', in: 'query', required: false, description: '每页条数,默认 15,最大 100', schema: new OA\Schema(type: 'integer', default: 15)),
|
||||
new OA\Parameter(name: 'user_id', in: 'query', required: false, description: '按用户 ID 筛选', schema: new OA\Schema(type: 'integer')),
|
||||
new OA\Parameter(name: 'enabled', in: 'query', required: false, description: '按启用状态筛选(0/1)', schema: new OA\Schema(type: 'integer', enum: [0, 1])),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '获取成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '获取成功'),
|
||||
new OA\Property(property: 'data', properties: [
|
||||
new OA\Property(property: 'items', type: 'array', items: new OA\Items(properties: [
|
||||
new OA\Property(property: 'id', type: 'integer'),
|
||||
new OA\Property(property: 'user_id', type: 'integer'),
|
||||
new OA\Property(property: 'name', type: 'string'),
|
||||
new OA\Property(property: 'key_prefix', type: 'string'),
|
||||
new OA\Property(property: 'last_used_at', type: 'string', format: 'date-time', nullable: true),
|
||||
new OA\Property(property: 'expires_at', type: 'string', format: 'date-time', nullable: true),
|
||||
new OA\Property(property: 'enabled', type: 'boolean'),
|
||||
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||
new OA\Property(property: 'user', properties: [
|
||||
new OA\Property(property: 'id', type: 'integer'),
|
||||
new OA\Property(property: 'username', type: 'string'),
|
||||
new OA\Property(property: 'api_key_enabled', type: 'boolean'),
|
||||
], type: 'object'),
|
||||
])),
|
||||
new OA\Property(property: 'total', type: 'integer'),
|
||||
new OA\Property(property: 'page', type: 'integer'),
|
||||
new OA\Property(property: 'per_page', type: 'integer'),
|
||||
], type: 'object'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 403, description: '无权限', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "", methods: "GET")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function index(): array
|
||||
{
|
||||
$page = (int) $this->request->input('page', 1);
|
||||
$per_page = min((int) $this->request->input('per_page', 15), 100);
|
||||
|
||||
$query = ApiKey::query()->with('user:id,username,api_key_enabled');
|
||||
|
||||
// 按用户 ID 筛选
|
||||
$user_id = $this->request->input('user_id');
|
||||
if ($user_id !== null && $user_id !== '') {
|
||||
$query->where('user_id', (int) $user_id);
|
||||
}
|
||||
|
||||
// 按启用状态筛选
|
||||
$enabled = $this->request->input('enabled');
|
||||
if ($enabled !== null && $enabled !== '') {
|
||||
$query->where('enabled', (bool) (int) $enabled);
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$items = $query->orderBy('created_at', 'desc')
|
||||
->offset(($page - 1) * $per_page)
|
||||
->limit($per_page)
|
||||
->get();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '获取成功',
|
||||
'data' => [
|
||||
'items' => $items,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员启用/禁用指定 API Key
|
||||
*/
|
||||
#[OA\Patch(
|
||||
path: '/admin/api-keys/{id}/toggle',
|
||||
summary: '启用/禁用指定 API Key',
|
||||
description: '管理员切换任意 API Key 的启用状态',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Admin API Keys'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'id', in: 'path', required: true, description: 'API Key ID', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['enabled'],
|
||||
properties: [
|
||||
new OA\Property(property: 'enabled', type: 'boolean', description: '是否启用'),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '状态更新成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '状态更新成功'),
|
||||
new OA\Property(property: 'data', properties: [
|
||||
new OA\Property(property: 'id', type: 'integer'),
|
||||
new OA\Property(property: 'name', type: 'string'),
|
||||
new OA\Property(property: 'key_prefix', type: 'string'),
|
||||
new OA\Property(property: 'enabled', type: 'boolean'),
|
||||
new OA\Property(property: 'user_id', type: 'integer'),
|
||||
], type: 'object'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 403, description: '无权限', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 404, description: 'API Key 不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "{id}/toggle", methods: "PATCH")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function toggle(int $id): ResponseInterface|array
|
||||
{
|
||||
$api_key = ApiKey::query()->find($id);
|
||||
|
||||
if (!$api_key) {
|
||||
return $this->response->json([
|
||||
'code' => 404,
|
||||
'message' => 'API Key 不存在',
|
||||
])->withStatus(404);
|
||||
}
|
||||
|
||||
$enabled = filter_var($this->request->input('enabled'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||
if ($enabled === null) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'enabled 参数不能为空或格式不正确',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$api_key->enabled = $enabled;
|
||||
$api_key->save();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '状态更新成功',
|
||||
'data' => $api_key,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员删除指定 API Key
|
||||
*/
|
||||
#[OA\Delete(
|
||||
path: '/admin/api-keys/{id}',
|
||||
summary: '删除指定 API Key',
|
||||
description: '管理员删除任意 API Key(硬删除,不可恢复)',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Admin API Keys'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'id', in: 'path', required: true, description: 'API Key ID', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '删除成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '删除成功'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 403, description: '无权限', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 404, description: 'API Key 不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "{id}", methods: "DELETE")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function destroy(int $id): ResponseInterface|array
|
||||
{
|
||||
$api_key = ApiKey::query()->find($id);
|
||||
|
||||
if (!$api_key) {
|
||||
return $this->response->json([
|
||||
'code' => 404,
|
||||
'message' => 'API Key 不存在',
|
||||
])->withStatus(404);
|
||||
}
|
||||
|
||||
$api_key->delete();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '删除成功',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\V1;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
use App\Model\ApiKey;
|
||||
use App\Model\User;
|
||||
use Hyperf\HttpServer\Annotation\Controller;
|
||||
use Hyperf\HttpServer\Annotation\Middleware;
|
||||
use Hyperf\HttpServer\Annotation\RequestMapping;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Qbhy\HyperfAuth\AuthManager;
|
||||
|
||||
#[OA\Tag(name: 'API Keys', description: 'API Key 管理')]
|
||||
#[Controller(prefix: "/api/v1/me/api-keys")]
|
||||
class ApiKeyController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* 生成 API Key
|
||||
*
|
||||
* 需要用户已启用 api_key_enabled,明文仅在生成时返回一次
|
||||
*/
|
||||
#[OA\Post(
|
||||
path: '/me/api-keys',
|
||||
summary: '生成 API Key',
|
||||
description: '生成新的 API Key,需用户已启用 api_key_enabled。明文仅在生成时返回一次',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['API Keys'],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['name'],
|
||||
properties: [
|
||||
new OA\Property(property: 'name', type: 'string', maxLength: 100, example: 'Production Key'),
|
||||
new OA\Property(property: 'expires_at', type: 'string', format: 'date-time', nullable: true, description: '过期时间,不传则永不过期'),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '生成成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '生成成功'),
|
||||
new OA\Property(property: 'data', properties: [
|
||||
new OA\Property(property: 'plain_key', type: 'string', description: '明文 Key,仅此次可见'),
|
||||
new OA\Property(property: 'api_key', properties: [
|
||||
new OA\Property(property: 'id', type: 'integer'),
|
||||
new OA\Property(property: 'name', type: 'string'),
|
||||
new OA\Property(property: 'key_prefix', type: 'string'),
|
||||
new OA\Property(property: 'expires_at', type: 'string', format: 'date-time', nullable: true),
|
||||
new OA\Property(property: 'enabled', type: 'boolean'),
|
||||
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||
], type: 'object'),
|
||||
], type: 'object'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 400, description: '参数校验失败', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 403, description: '未启用 API Key 功能', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "", methods: "POST")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
public function store(AuthManager $auth): \Psr\Http\Message\ResponseInterface|array
|
||||
{
|
||||
$user = $auth->guard('jwt')->user();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return $this->response->json([
|
||||
'code' => 401,
|
||||
'message' => '未授权',
|
||||
])->withStatus(401);
|
||||
}
|
||||
|
||||
if (!$user->api_key_enabled) {
|
||||
return $this->response->json([
|
||||
'code' => 403,
|
||||
'message' => '未启用 API Key 功能,请联系管理员开启',
|
||||
])->withStatus(403);
|
||||
}
|
||||
|
||||
$name = $this->request->input('name');
|
||||
$expires_at = $this->request->input('expires_at');
|
||||
|
||||
if (!is_string($name) || trim($name) === '') {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'API Key 名称不能为空',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$name = trim($name);
|
||||
if (strlen($name) > 100) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'API Key 名称不能超过 100 个字符',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
// 同用户内 Key 名称唯一
|
||||
$name_exists = ApiKey::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('name', $name)
|
||||
->exists();
|
||||
if ($name_exists) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '已存在同名的 API Key,请使用不同的名称',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
// 每用户最多 10 个 Key
|
||||
$key_count = ApiKey::query()->where('user_id', $user->id)->count();
|
||||
if ($key_count >= 10) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '每个用户最多创建 10 个 API Key',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
// 校验过期时间格式
|
||||
if ($expires_at !== null && $expires_at !== '') {
|
||||
try {
|
||||
$expires_at = \Carbon\Carbon::parse($expires_at)->toDateTimeString();
|
||||
} catch (\Throwable $e) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '过期时间格式不正确',
|
||||
])->withStatus(400);
|
||||
}
|
||||
} else {
|
||||
$expires_at = null;
|
||||
}
|
||||
|
||||
$result = ApiKey::generate($user->id, $name, $expires_at);
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '生成成功',
|
||||
'data' => [
|
||||
'plain_key' => $result['plain_key'],
|
||||
'api_key' => $result['api_key'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* API Key 列表
|
||||
*
|
||||
* 列出当前用户的所有 API Keys(不含哈希)
|
||||
*/
|
||||
#[OA\Get(
|
||||
path: '/me/api-keys',
|
||||
summary: 'API Key 列表',
|
||||
description: '列出当前用户的所有 API Keys',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['API Keys'],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '获取成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '获取成功'),
|
||||
new OA\Property(property: 'data', type: 'array', items: new OA\Items(properties: [
|
||||
new OA\Property(property: 'id', type: 'integer'),
|
||||
new OA\Property(property: 'name', type: 'string'),
|
||||
new OA\Property(property: 'key_prefix', type: 'string'),
|
||||
new OA\Property(property: 'last_used_at', type: 'string', format: 'date-time', nullable: true),
|
||||
new OA\Property(property: 'expires_at', type: 'string', format: 'date-time', nullable: true),
|
||||
new OA\Property(property: 'enabled', type: 'boolean'),
|
||||
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||
])),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "", methods: "GET")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
public function index(AuthManager $auth): \Psr\Http\Message\ResponseInterface|array
|
||||
{
|
||||
$user = $auth->guard('jwt')->user();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return $this->response->json([
|
||||
'code' => 401,
|
||||
'message' => '未授权',
|
||||
])->withStatus(401);
|
||||
}
|
||||
|
||||
$keys = ApiKey::query()
|
||||
->where('user_id', $user->id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '获取成功',
|
||||
'data' => $keys,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 API Key
|
||||
*/
|
||||
#[OA\Delete(
|
||||
path: '/me/api-keys/{id}',
|
||||
summary: '删除 API Key',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['API Keys'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'id', in: 'path', required: true, description: 'API Key ID', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '删除成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '删除成功'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 404, description: 'API Key 不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "{id}", methods: "DELETE")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
public function destroy(int $id, AuthManager $auth): \Psr\Http\Message\ResponseInterface|array
|
||||
{
|
||||
$user = $auth->guard('jwt')->user();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return $this->response->json([
|
||||
'code' => 401,
|
||||
'message' => '未授权',
|
||||
])->withStatus(401);
|
||||
}
|
||||
|
||||
$api_key = ApiKey::query()
|
||||
->where('id', $id)
|
||||
->where('user_id', $user->id)
|
||||
->first();
|
||||
|
||||
if (!$api_key) {
|
||||
return $this->response->json([
|
||||
'code' => 404,
|
||||
'message' => 'API Key 不存在',
|
||||
])->withStatus(404);
|
||||
}
|
||||
|
||||
$api_key->delete();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '删除成功',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用自己的 API Key
|
||||
*/
|
||||
#[OA\Patch(
|
||||
path: '/me/api-keys/{id}/toggle',
|
||||
summary: '启用/禁用自己的 API Key',
|
||||
description: '用户切换自己 API Key 的启用状态',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['API Keys'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'id', in: 'path', required: true, description: 'API Key ID', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['enabled'],
|
||||
properties: [
|
||||
new OA\Property(property: 'enabled', type: 'boolean', description: '是否启用'),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '状态更新成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '状态更新成功'),
|
||||
new OA\Property(property: 'data', properties: [
|
||||
new OA\Property(property: 'id', type: 'integer'),
|
||||
new OA\Property(property: 'name', type: 'string'),
|
||||
new OA\Property(property: 'key_prefix', type: 'string'),
|
||||
new OA\Property(property: 'enabled', type: 'boolean'),
|
||||
new OA\Property(property: 'last_used_at', type: 'string', format: 'date-time', nullable: true),
|
||||
new OA\Property(property: 'expires_at', type: 'string', format: 'date-time', nullable: true),
|
||||
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||
], type: 'object'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 400, description: '参数错误', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 404, description: 'API Key 不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "{id}/toggle", methods: "PATCH")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
public function toggle(int $id, AuthManager $auth): \Psr\Http\Message\ResponseInterface|array
|
||||
{
|
||||
$user = $auth->guard('jwt')->user();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return $this->response->json([
|
||||
'code' => 401,
|
||||
'message' => '未授权',
|
||||
])->withStatus(401);
|
||||
}
|
||||
|
||||
$api_key = ApiKey::query()
|
||||
->where('id', $id)
|
||||
->where('user_id', $user->id)
|
||||
->first();
|
||||
|
||||
if (!$api_key) {
|
||||
return $this->response->json([
|
||||
'code' => 404,
|
||||
'message' => 'API Key 不存在',
|
||||
])->withStatus(404);
|
||||
}
|
||||
|
||||
$enabled = filter_var($this->request->input('enabled'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||
if ($enabled === null) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'enabled 参数不能为空或格式不正确',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$api_key->enabled = $enabled;
|
||||
$api_key->save();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '状态更新成功',
|
||||
'data' => $api_key,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,718 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\V1;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
use App\Model\User;
|
||||
use App\Service\OperationLogService;
|
||||
use App\Utils\RequestHelper;
|
||||
use Carbon\Carbon;
|
||||
use Hyperf\HttpServer\Annotation\Controller;
|
||||
use Hyperf\HttpServer\Annotation\Middleware;
|
||||
use Hyperf\HttpServer\Annotation\RequestMapping;
|
||||
use Hyperf\HttpServer\Contract\RequestInterface;
|
||||
use Hyperf\HttpServer\Contract\ResponseInterface;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Qbhy\HyperfAuth\AuthManager;
|
||||
|
||||
#[OA\Tag(name: 'Auth', description: '认证与个人信息')]
|
||||
#[Controller(prefix: "/api/v1")]
|
||||
class AuthController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
#[OA\Post(
|
||||
path: '/register',
|
||||
summary: '用户注册',
|
||||
description: '注册新用户,需提供用户名、密码和邮箱',
|
||||
tags: ['Auth'],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['username', 'password', 'email'],
|
||||
properties: [
|
||||
new OA\Property(property: 'username', type: 'string', minLength: 3, maxLength: 20, example: 'new_user'),
|
||||
new OA\Property(property: 'password', type: 'string', minLength: 6, maxLength: 32, example: 'Pass_1234'),
|
||||
new OA\Property(property: 'email', type: 'string', format: 'email', maxLength: 100, example: 'user@example.com'),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '注册成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '注册成功'),
|
||||
new OA\Property(property: 'data', properties: [
|
||||
new OA\Property(property: 'id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'username', type: 'string', example: 'new_user'),
|
||||
new OA\Property(property: 'email', type: 'string', example: 'user@example.com'),
|
||||
], type: 'object'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 400, description: '参数校验失败或唯一性冲突', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "register", methods: "POST")]
|
||||
public function register(RequestInterface $request, ResponseInterface $response): \Psr\Http\Message\ResponseInterface|array
|
||||
{
|
||||
$username = $request->input('username');
|
||||
$password = $request->input('password');
|
||||
$email = $request->input('email');
|
||||
|
||||
// 校验 username
|
||||
if (!is_string($username) || trim($username) === '') {
|
||||
return $response->json([
|
||||
'code' => 400,
|
||||
'message' => '用户名不能为空',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$username = trim($username);
|
||||
$username_length = strlen($username);
|
||||
if ($username_length < 3 || $username_length > 20) {
|
||||
return $response->json([
|
||||
'code' => 400,
|
||||
'message' => '用户名长度需在 3-20 个字符',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
// 校验 password
|
||||
if (!is_string($password) || $password === '') {
|
||||
return $response->json([
|
||||
'code' => 400,
|
||||
'message' => '密码不能为空',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$password_length = strlen($password);
|
||||
if ($password_length < 6 || $password_length > 32) {
|
||||
return $response->json([
|
||||
'code' => 400,
|
||||
'message' => '密码长度需在 6-32 个字符',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
// 校验 email
|
||||
if (!is_string($email) || trim($email) === '') {
|
||||
return $response->json([
|
||||
'code' => 400,
|
||||
'message' => '邮箱不能为空',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$email = trim($email);
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return $response->json([
|
||||
'code' => 400,
|
||||
'message' => '邮箱格式不正确',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
if (strlen($email) > 100) {
|
||||
return $response->json([
|
||||
'code' => 400,
|
||||
'message' => '邮箱长度不能超过 100 个字符',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
// 唯一性检查
|
||||
if (User::query()->where('username', $username)->exists()) {
|
||||
return $response->json([
|
||||
'code' => 400,
|
||||
'message' => '用户名已存在',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
if (User::query()->where('email', $email)->exists()) {
|
||||
return $response->json([
|
||||
'code' => 400,
|
||||
'message' => '邮箱已被注册',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
$user = User::create([
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'email' => $email,
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '注册成功',
|
||||
'data' => [
|
||||
'id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'email' => $user->email,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
#[OA\Post(
|
||||
path: '/login',
|
||||
summary: '用户登录',
|
||||
description: '使用用户名和密码登录,返回 access_token 和 refresh_token',
|
||||
tags: ['Auth'],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['username', 'password'],
|
||||
properties: [
|
||||
new OA\Property(property: 'username', type: 'string', example: 'admin'),
|
||||
new OA\Property(property: 'password', type: 'string', example: 'Pass_1234'),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '登录成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '登录成功'),
|
||||
new OA\Property(property: 'data', properties: [
|
||||
new OA\Property(property: 'access_token', type: 'string'),
|
||||
new OA\Property(property: 'refresh_token', type: 'string'),
|
||||
new OA\Property(property: 'token_type', type: 'string', example: 'Bearer'),
|
||||
new OA\Property(property: 'expires_in', type: 'integer', example: 7200),
|
||||
new OA\Property(property: 'user', properties: [
|
||||
new OA\Property(property: 'id', type: 'integer'),
|
||||
new OA\Property(property: 'username', type: 'string'),
|
||||
new OA\Property(property: 'email', type: 'string'),
|
||||
new OA\Property(property: 'role', type: 'string', example: 'administrator'),
|
||||
], type: 'object'),
|
||||
], type: 'object'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 400, description: '参数校验失败', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 401, description: '用户名或密码错误', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 403, description: '账号已被禁用', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "login", methods: "POST")]
|
||||
public function login(RequestInterface $request, ResponseInterface $response, AuthManager $auth): \Psr\Http\Message\ResponseInterface|array
|
||||
{
|
||||
$username = $request->input('username');
|
||||
$password = $request->input('password');
|
||||
|
||||
// 校验参数
|
||||
if (!is_string($username) || trim($username) === '') {
|
||||
return $response->json([
|
||||
'code' => 400,
|
||||
'message' => '用户名不能为空',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
if (!is_string($password) || $password === '') {
|
||||
return $response->json([
|
||||
'code' => 400,
|
||||
'message' => '密码不能为空',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
$user = User::query()->where('username', trim($username))->first();
|
||||
|
||||
if (!$user) {
|
||||
return $response->json([
|
||||
'code' => 401,
|
||||
'message' => '用户名或密码错误',
|
||||
])->withStatus(401);
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if (!$user->verifyPassword($password)) {
|
||||
return $response->json([
|
||||
'code' => 401,
|
||||
'message' => '用户名或密码错误',
|
||||
])->withStatus(401);
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if ($user->status !== 1) {
|
||||
return $response->json([
|
||||
'code' => 403,
|
||||
'message' => '账号已被禁用',
|
||||
])->withStatus(403);
|
||||
}
|
||||
|
||||
$user->load('role');
|
||||
|
||||
// 生成 Access Token(注入 role 到 JWT payload,前端从 JWT 解码 role 作为可信权限来源)
|
||||
$token = $auth->guard('jwt')->login($user, ['role' => $user->role?->name ?? 'accessor']);
|
||||
|
||||
// 生成 Refresh Token
|
||||
$refreshToken = bin2hex(random_bytes(32));
|
||||
$user->refresh_token = $refreshToken;
|
||||
$user->refresh_token_expires_at = Carbon::now()->addDays(30);
|
||||
$user->save();
|
||||
|
||||
OperationLogService::log(
|
||||
user_id: $user->id,
|
||||
action: 'auth.login',
|
||||
target_type: 'user',
|
||||
target_id: $user->id,
|
||||
description: "用户 {$user->username} 登录",
|
||||
ip: RequestHelper::getClientIp($this->request),
|
||||
);
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '登录成功',
|
||||
'data' => [
|
||||
'access_token' => $token,
|
||||
'refresh_token' => $refreshToken,
|
||||
'token_type' => 'Bearer',
|
||||
'expires_in' => 7200,
|
||||
'user' => [
|
||||
'id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'email' => $user->email,
|
||||
'role' => $user->role?->name ?? 'accessor',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新 Access Token
|
||||
*/
|
||||
#[OA\Get(
|
||||
path: '/refresh',
|
||||
summary: '刷新 Access Token',
|
||||
description: '使用 refresh_token 获取新的 access_token,同时轮换 refresh_token',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Auth'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'refresh_token', in: 'query', required: true, description: 'Refresh Token', schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Token 刷新成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: 'Token 刷新成功'),
|
||||
new OA\Property(property: 'data', properties: [
|
||||
new OA\Property(property: 'access_token', type: 'string'),
|
||||
new OA\Property(property: 'refresh_token', type: 'string'),
|
||||
new OA\Property(property: 'token_type', type: 'string', example: 'Bearer'),
|
||||
new OA\Property(property: 'expires_in', type: 'integer', example: 7200),
|
||||
], type: 'object'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 400, description: '缺少 refresh_token 参数', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 401, description: 'refresh_token 无效或已过期', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 403, description: '账号已被禁用', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "refresh", methods: "GET")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
public function refresh(RequestInterface $request, ResponseInterface $response, AuthManager $auth): \Psr\Http\Message\ResponseInterface|array
|
||||
{
|
||||
$refreshToken = $request->input('refresh_token');
|
||||
|
||||
if (!is_string($refreshToken) || $refreshToken === '') {
|
||||
return $response->json([
|
||||
'code' => 400,
|
||||
'message' => '缺少 refresh_token 参数',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
$user = User::query()->where('refresh_token', $refreshToken)->first();
|
||||
|
||||
if (!$user) {
|
||||
return $response->json([
|
||||
'code' => 401,
|
||||
'message' => '无效的 refresh_token',
|
||||
])->withStatus(401);
|
||||
}
|
||||
|
||||
// 验证 refresh token 是否过期
|
||||
if (!$user->isRefreshTokenValid()) {
|
||||
return $response->json([
|
||||
'code' => 401,
|
||||
'message' => 'refresh_token 已过期,请重新登录',
|
||||
])->withStatus(401);
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if ($user->status !== 1) {
|
||||
return $response->json([
|
||||
'code' => 403,
|
||||
'message' => '账号已被禁用',
|
||||
])->withStatus(403);
|
||||
}
|
||||
|
||||
// 生成新的 Access Token(注入 role 到 JWT payload)
|
||||
$user->load('role');
|
||||
$token = $auth->guard('jwt')->login($user, ['role' => $user->role?->name ?? 'accessor']);
|
||||
|
||||
// 生成新的 Refresh Token(轮换以提升安全性)
|
||||
$newRefreshToken = bin2hex(random_bytes(32));
|
||||
$user->refresh_token = $newRefreshToken;
|
||||
$user->refresh_token_expires_at = Carbon::now()->addDays(30);
|
||||
$user->save();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => 'Token 刷新成功',
|
||||
'data' => [
|
||||
'access_token' => $token,
|
||||
'refresh_token' => $newRefreshToken,
|
||||
'token_type' => 'Bearer',
|
||||
'expires_in' => 7200,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
#[OA\Get(
|
||||
path: '/me',
|
||||
summary: '获取当前用户信息',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Auth'],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '获取成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '获取成功'),
|
||||
new OA\Property(property: 'data', properties: [
|
||||
new OA\Property(property: 'id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'username', type: 'string', example: 'admin'),
|
||||
new OA\Property(property: 'email', type: 'string', example: 'admin@example.com'),
|
||||
new OA\Property(property: 'status', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'api_key_enabled', type: 'boolean', example: true, description: 'API Key 功能是否启用'),
|
||||
new OA\Property(property: 'ext', type: 'object', nullable: true),
|
||||
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||
], type: 'object'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "me", methods: "GET")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
public function me(AuthManager $auth, ResponseInterface $response): \Psr\Http\Message\ResponseInterface|array
|
||||
{
|
||||
$user = $auth->guard('jwt')->user();
|
||||
|
||||
if (!$user) {
|
||||
return $response->json([
|
||||
'code' => 401,
|
||||
'message' => '未授权',
|
||||
])->withStatus(401);
|
||||
}
|
||||
|
||||
$user->load('role');
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '获取成功',
|
||||
'data' => [
|
||||
'id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'email' => $user->email,
|
||||
'role' => $user->role?->name ?? 'accessor',
|
||||
'status' => $user->status,
|
||||
'api_key_enabled' => $user->api_key_enabled,
|
||||
'ext' => $user->ext,
|
||||
'created_at' => $user->created_at->toDateTimeString(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新个人信息
|
||||
*
|
||||
* 当前用户更新自己的 email 和 ext 字段
|
||||
*/
|
||||
#[OA\Put(
|
||||
path: '/me/profile',
|
||||
summary: '更新个人信息',
|
||||
description: '当前用户更新自己的 email 和 ext 字段',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Auth'],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'email', type: 'string', format: 'email', maxLength: 100, example: 'new@example.com'),
|
||||
new OA\Property(property: 'ext', type: 'object', nullable: true, example: ['nickname' => 'user']),
|
||||
])
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '更新成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '个人信息更新成功'),
|
||||
new OA\Property(property: 'data', properties: [
|
||||
new OA\Property(property: 'id', type: 'integer'),
|
||||
new OA\Property(property: 'username', type: 'string'),
|
||||
new OA\Property(property: 'email', type: 'string'),
|
||||
new OA\Property(property: 'status', type: 'integer'),
|
||||
new OA\Property(property: 'ext', type: 'object', nullable: true),
|
||||
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||
], type: 'object'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 400, description: '参数校验失败或唯一性冲突', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "me/profile", methods: "PUT")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
public function updateProfile(RequestInterface $request, ResponseInterface $response, AuthManager $auth): \Psr\Http\Message\ResponseInterface|array
|
||||
{
|
||||
$user = $auth->guard('jwt')->user();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return $response->json([
|
||||
'code' => 401,
|
||||
'message' => '未授权',
|
||||
])->withStatus(401);
|
||||
}
|
||||
|
||||
$email = $request->input('email');
|
||||
$ext = $request->input('ext');
|
||||
$updates = [];
|
||||
|
||||
if ($email !== null) {
|
||||
if (!is_string($email) || trim($email) === '') {
|
||||
return $response->json([
|
||||
'code' => 400,
|
||||
'message' => '邮箱不能为空',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$email = trim($email);
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return $response->json([
|
||||
'code' => 400,
|
||||
'message' => '邮箱格式不正确',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
if (strlen($email) > 100) {
|
||||
return $response->json([
|
||||
'code' => 400,
|
||||
'message' => '邮箱长度不能超过 100 个字符',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
if (User::query()->where('email', $email)->where('id', '!=', $user->id)->exists()) {
|
||||
return $response->json([
|
||||
'code' => 400,
|
||||
'message' => '邮箱已被注册',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$updates['email'] = $email;
|
||||
}
|
||||
|
||||
if ($ext !== null) {
|
||||
if (!is_array($ext)) {
|
||||
return $response->json([
|
||||
'code' => 400,
|
||||
'message' => 'ext 必须为对象',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$updates['ext'] = $ext;
|
||||
}
|
||||
|
||||
if ($updates === []) {
|
||||
return $response->json([
|
||||
'code' => 400,
|
||||
'message' => '缺少可更新字段',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$user->fill($updates);
|
||||
$user->save();
|
||||
$user->refresh();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '个人信息更新成功',
|
||||
'data' => [
|
||||
'id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'email' => $user->email,
|
||||
'status' => $user->status,
|
||||
'ext' => $user->ext,
|
||||
'created_at' => $user->created_at->toDateTimeString(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*
|
||||
* 需验证旧密码,修改成功后清除 refresh_token 强制重新登录
|
||||
*/
|
||||
#[OA\Put(
|
||||
path: '/me/password',
|
||||
summary: '修改密码',
|
||||
description: '修改当前用户密码,需验证旧密码。修改成功后清除 refresh_token,需重新登录',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Auth'],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['old_password', 'new_password'],
|
||||
properties: [
|
||||
new OA\Property(property: 'old_password', type: 'string', example: 'OldPass_1234'),
|
||||
new OA\Property(property: 'new_password', type: 'string', minLength: 6, maxLength: 32, example: 'NewPass_5678'),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '密码修改成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '密码修改成功,请重新登录'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 400, description: '参数校验失败或旧密码不正确', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "me/password", methods: "PUT")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
public function changePassword(RequestInterface $request, ResponseInterface $response, AuthManager $auth): \Psr\Http\Message\ResponseInterface|array
|
||||
{
|
||||
$user = $auth->guard('jwt')->user();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return $response->json([
|
||||
'code' => 401,
|
||||
'message' => '未授权',
|
||||
])->withStatus(401);
|
||||
}
|
||||
|
||||
$old_password = $request->input('old_password');
|
||||
$new_password = $request->input('new_password');
|
||||
|
||||
// 验证旧密码
|
||||
if (!is_string($old_password) || $old_password === '') {
|
||||
return $response->json([
|
||||
'code' => 400,
|
||||
'message' => '旧密码不能为空',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
if (!$user->verifyPassword($old_password)) {
|
||||
return $response->json([
|
||||
'code' => 400,
|
||||
'message' => '旧密码不正确',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
// 验证新密码
|
||||
if (!is_string($new_password) || $new_password === '') {
|
||||
return $response->json([
|
||||
'code' => 400,
|
||||
'message' => '新密码不能为空',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$new_password_length = strlen($new_password);
|
||||
if ($new_password_length < 6 || $new_password_length > 32) {
|
||||
return $response->json([
|
||||
'code' => 400,
|
||||
'message' => '新密码长度需在 6-32 个字符',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
// 修改密码并清除 refresh_token
|
||||
$user->password = $new_password;
|
||||
$user->refresh_token = null;
|
||||
$user->refresh_token_expires_at = null;
|
||||
$user->save();
|
||||
|
||||
OperationLogService::log(
|
||||
user_id: $user->id,
|
||||
action: 'user.password_change',
|
||||
target_type: 'user',
|
||||
target_id: $user->id,
|
||||
description: "用户 {$user->username} 修改密码",
|
||||
ip: RequestHelper::getClientIp($this->request),
|
||||
);
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '密码修改成功,请重新登录',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
#[OA\Get(
|
||||
path: '/logout',
|
||||
summary: '退出登录',
|
||||
description: '退出登录,清除 refresh_token 并注销当前 JWT token',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Auth'],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '退出成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '退出成功'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "logout", methods: "GET")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
public function logout(AuthManager $auth): array
|
||||
{
|
||||
$user = $auth->guard('jwt')->user();
|
||||
|
||||
if ($user instanceof User) {
|
||||
OperationLogService::log(
|
||||
user_id: $user->id,
|
||||
action: 'auth.logout',
|
||||
target_type: 'user',
|
||||
target_id: $user->id,
|
||||
description: "用户 {$user->username} 退出登录",
|
||||
ip: RequestHelper::getClientIp($this->request),
|
||||
);
|
||||
|
||||
// 清除 refresh token
|
||||
$user->refresh_token = null;
|
||||
$user->refresh_token_expires_at = null;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
// 注销当前 token
|
||||
$auth->guard('jwt')->logout();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '退出成功',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\V1;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
use App\Middleware\PermissionMiddleware;
|
||||
use App\Model\Company;
|
||||
use App\Model\Store;
|
||||
use Hyperf\HttpServer\Annotation\Controller;
|
||||
use Hyperf\HttpServer\Annotation\Middleware;
|
||||
use Hyperf\HttpServer\Annotation\RequestMapping;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\Tag(name: 'Companies', description: '公司管理')]
|
||||
#[Controller(prefix: "/api/v1/companies")]
|
||||
class CompanyController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* 公司列表(受 scope 过滤)
|
||||
*
|
||||
* administrator 可见全部,developer/accessor 仅可见权限范围内的公司
|
||||
*/
|
||||
#[OA\Get(
|
||||
path: '/companies',
|
||||
summary: '公司列表',
|
||||
description: '获取公司列表,支持按 name/label 模糊搜索,受 scope 过滤',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Companies'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'name', in: 'query', required: false, description: '公司名称模糊搜索(匹配 name 或 label)', schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '获取成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '获取成功'),
|
||||
new OA\Property(property: 'data', type: 'array', items: new OA\Items(properties: [
|
||||
new OA\Property(property: 'id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'name', type: 'string', example: 'acme'),
|
||||
new OA\Property(property: 'label', type: 'string', example: '阿克米公司'),
|
||||
new OA\Property(property: 'enabled', type: 'boolean', example: true),
|
||||
new OA\Property(property: 'ext', type: 'object', nullable: true),
|
||||
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||
new OA\Property(property: 'updated_at', type: 'string', format: 'date-time'),
|
||||
])),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "", methods: "GET")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function index(): array
|
||||
{
|
||||
$scope_type = $this->request->getAttribute('scope_type');
|
||||
$scope_ids = $this->request->getAttribute('scope_ids', []);
|
||||
|
||||
$query = Company::query();
|
||||
|
||||
// scope 过滤
|
||||
if ($scope_type === 'store') {
|
||||
// 从 store_ids 反查 company_ids
|
||||
$company_ids = Store::query()
|
||||
->whereIn('id', $scope_ids)
|
||||
->distinct()
|
||||
->pluck('company_id')
|
||||
->toArray();
|
||||
$query->whereIn('id', $company_ids);
|
||||
} elseif ($scope_type === 'platform') {
|
||||
$company_ids = Store::query()
|
||||
->whereIn('platform_id', $scope_ids)
|
||||
->distinct()
|
||||
->pluck('company_id')
|
||||
->toArray();
|
||||
$query->whereIn('id', $company_ids);
|
||||
}
|
||||
// 'all' → 不附加条件
|
||||
|
||||
// 按 name 模糊搜索
|
||||
$name = $this->request->input('name');
|
||||
if ($name !== null && $name !== '') {
|
||||
$query->where(function ($q) use ($name): void {
|
||||
$q->where('name', 'ilike', "%{$name}%")
|
||||
->orWhere('label', 'ilike', "%{$name}%");
|
||||
});
|
||||
}
|
||||
|
||||
$companies = $query->orderBy('id')->get();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '获取成功',
|
||||
'data' => $companies,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\V1;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
use App\Middleware\PermissionMiddleware;
|
||||
use App\Model\Company;
|
||||
use App\Service\OperationLogService;
|
||||
use App\Utils\RequestHelper;
|
||||
use App\Model\Platform;
|
||||
use App\Model\Store;
|
||||
use App\Model\User;
|
||||
use App\Model\UserDataScope;
|
||||
use App\Service\ScopeBitmapService;
|
||||
use App\Service\ScopeTableManager;
|
||||
use Hyperf\DbConnection\Db;
|
||||
use Hyperf\HttpServer\Annotation\Controller;
|
||||
use Hyperf\HttpServer\Annotation\Middleware;
|
||||
use Hyperf\HttpServer\Annotation\RequestMapping;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
#[OA\Tag(name: 'Data Scope', description: '用户数据范围管理')]
|
||||
#[Controller(prefix: "/api/v1/users")]
|
||||
class DataScopeController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
protected readonly ScopeTableManager $scopeTableManager,
|
||||
protected readonly ScopeBitmapService $bitmapService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看用户数据权限
|
||||
*
|
||||
* 返回用户的 scope 列表(含实体名称)和解析后的 store_ids
|
||||
*
|
||||
* @param int $id 用户 ID
|
||||
*/
|
||||
#[OA\Get(
|
||||
path: '/users/{id}/data-scope',
|
||||
summary: '查看用户数据权限',
|
||||
description: '返回用户的 scope 列表(含实体名称)和解析后的 store_ids',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Data Scope'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'id', in: 'path', required: true, description: '用户 ID', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '获取成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '获取成功'),
|
||||
new OA\Property(property: 'data', properties: [
|
||||
new OA\Property(property: 'user_id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'role', type: 'string', example: 'admin'),
|
||||
new OA\Property(
|
||||
property: 'scopes',
|
||||
type: 'array',
|
||||
items: new OA\Items(properties: [
|
||||
new OA\Property(property: 'scope_type', type: 'string', example: 'company'),
|
||||
new OA\Property(property: 'scope_id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'name', type: 'string', example: '示例公司'),
|
||||
], type: 'object')
|
||||
),
|
||||
new OA\Property(property: 'resolved_store_ids', type: 'array', items: new OA\Items(type: 'integer'), example: [1, 2, 3]),
|
||||
], type: 'object'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 404, description: '用户不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "{id}/data-scope", methods: "GET")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function show(int $id): ResponseInterface|array
|
||||
{
|
||||
$user = User::query()->with('role')->find($id);
|
||||
|
||||
if (!$user) {
|
||||
return $this->response->json([
|
||||
'code' => 404,
|
||||
'message' => '用户不存在',
|
||||
])->withStatus(404);
|
||||
}
|
||||
|
||||
$scopes = UserDataScope::query()->where('user_id', $id)->get();
|
||||
|
||||
// 批量查询实体名称,避免 N+1
|
||||
$scope_data = $this->enrichScopes($scopes->toArray());
|
||||
|
||||
// 解析最终 store_ids
|
||||
$resolved_store_ids = [];
|
||||
if ($user->role && $user->role->name !== 'administrator') {
|
||||
$bitmap = $this->bitmapService->buildForUser($id);
|
||||
$resolved_store_ids = $this->bitmapService->decode($bitmap);
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '获取成功',
|
||||
'data' => [
|
||||
'user_id' => $id,
|
||||
'role' => $user->role?->name,
|
||||
'scopes' => $scope_data,
|
||||
'resolved_store_ids' => $resolved_store_ids,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户数据权限
|
||||
*
|
||||
* 全量替换用户的 scope 绑定,并重建 bitmap
|
||||
*
|
||||
* @param int $id 用户 ID
|
||||
*/
|
||||
#[OA\Put(
|
||||
path: '/users/{id}/data-scope',
|
||||
summary: '设置用户数据权限',
|
||||
description: '全量替换用户的 scope 绑定,并重建 bitmap',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Data Scope'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'id', in: 'path', required: true, description: '用户 ID', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['scopes'],
|
||||
properties: [
|
||||
new OA\Property(
|
||||
property: 'scopes',
|
||||
type: 'array',
|
||||
items: new OA\Items(
|
||||
required: ['scope_type', 'scope_id'],
|
||||
properties: [
|
||||
new OA\Property(property: 'scope_type', type: 'string', enum: ['company', 'platform', 'store'], example: 'company'),
|
||||
new OA\Property(property: 'scope_id', type: 'integer', example: 1),
|
||||
],
|
||||
type: 'object'
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '数据权限更新成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '数据权限更新成功'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 400, description: '参数校验失败(无效 scope_type 等)', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 404, description: '用户不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "{id}/data-scope", methods: "PUT")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function update(int $id): ResponseInterface|array
|
||||
{
|
||||
$user = User::query()->with('role')->find($id);
|
||||
|
||||
if (!$user) {
|
||||
return $this->response->json([
|
||||
'code' => 404,
|
||||
'message' => '用户不存在',
|
||||
])->withStatus(404);
|
||||
}
|
||||
|
||||
$body = $this->request->getParsedBody();
|
||||
|
||||
if (!array_key_exists('scopes', $body)) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '缺少 scopes 参数',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$scopes = $body['scopes'];
|
||||
|
||||
if (!is_array($scopes)) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'scopes 必须为数组',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
// 校验每条 scope
|
||||
$valid_scope_types = ['company', 'platform', 'store'];
|
||||
$records = [];
|
||||
foreach ($scopes as $item) {
|
||||
if (!is_array($item) || !isset($item['scope_type']) || !isset($item['scope_id'])) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '每条 scope 必须包含 scope_type 和 scope_id',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
if (!in_array($item['scope_type'], $valid_scope_types, true)) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => "scope_type 必须为 company、platform 或 store",
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$records[] = [
|
||||
'user_id' => $id,
|
||||
'scope_type' => $item['scope_type'],
|
||||
'scope_id' => (int) $item['scope_id'],
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
|
||||
// 事务内全量替换
|
||||
Db::beginTransaction();
|
||||
try {
|
||||
UserDataScope::query()->where('user_id', $id)->delete();
|
||||
|
||||
if (!empty($records)) {
|
||||
UserDataScope::query()->insert($records);
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollBack();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// 重建 bitmap 并更新 Swoole\Table
|
||||
$this->scopeTableManager->rebuildUserScope($id);
|
||||
|
||||
OperationLogService::log(
|
||||
user_id: OperationLogService::getCurrentUserId(),
|
||||
action: 'scope.update',
|
||||
target_type: 'user',
|
||||
target_id: $id,
|
||||
description: "更新用户 #{$id} 数据权限",
|
||||
detail: ['scopes' => $scopes],
|
||||
ip: RequestHelper::getClientIp($this->request),
|
||||
);
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '数据权限更新成功',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量查询 scope 关联的实体名称
|
||||
*
|
||||
* @param array $scopes scope 记录数组
|
||||
* @return array 带名称的 scope 列表
|
||||
*/
|
||||
protected function enrichScopes(array $scopes): array
|
||||
{
|
||||
if (empty($scopes)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 按 scope_type 分组收集 IDs
|
||||
$company_ids = [];
|
||||
$platform_ids = [];
|
||||
$store_ids = [];
|
||||
|
||||
foreach ($scopes as $scope) {
|
||||
match ($scope['scope_type']) {
|
||||
'company' => $company_ids[] = $scope['scope_id'],
|
||||
'platform' => $platform_ids[] = $scope['scope_id'],
|
||||
'store' => $store_ids[] = $scope['scope_id'],
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
// 批量查询名称
|
||||
$company_names = !empty($company_ids)
|
||||
? Company::query()->whereIn('id', $company_ids)->pluck('label', 'id')->toArray()
|
||||
: [];
|
||||
$platform_names = !empty($platform_ids)
|
||||
? Platform::query()->whereIn('id', $platform_ids)->pluck('name', 'id')->toArray()
|
||||
: [];
|
||||
$store_names = !empty($store_ids)
|
||||
? Store::query()->whereIn('id', $store_ids)->pluck('label', 'id')->toArray()
|
||||
: [];
|
||||
|
||||
// 组装结果
|
||||
return array_map(function (array $scope) use ($company_names, $platform_names, $store_names): array {
|
||||
$name = match ($scope['scope_type']) {
|
||||
'company' => $company_names[$scope['scope_id']] ?? null,
|
||||
'platform' => $platform_names[$scope['scope_id']] ?? null,
|
||||
'store' => $store_names[$scope['scope_id']] ?? null,
|
||||
default => null,
|
||||
};
|
||||
|
||||
return [
|
||||
'scope_type' => $scope['scope_type'],
|
||||
'scope_id' => $scope['scope_id'],
|
||||
'name' => $name,
|
||||
];
|
||||
}, $scopes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\V1;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
use App\Middleware\PermissionMiddleware;
|
||||
use App\Model\Platform;
|
||||
use Hyperf\HttpServer\Annotation\Controller;
|
||||
use Hyperf\HttpServer\Annotation\Middleware;
|
||||
use Hyperf\HttpServer\Annotation\RequestMapping;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\Tag(name: 'Platforms', description: '平台管理')]
|
||||
#[Controller(prefix: "/api/v1/platforms")]
|
||||
class PlatformController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* 平台列表(全局数据,不过滤)
|
||||
*/
|
||||
#[OA\Get(
|
||||
path: '/platforms',
|
||||
summary: '平台列表',
|
||||
description: '获取全部平台列表(全局数据,不过滤)',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Platforms'],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '获取成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '获取成功'),
|
||||
new OA\Property(property: 'data', type: 'array', items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'developer_id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||
new OA\Property(property: 'updated_at', type: 'string', format: 'date-time'),
|
||||
],
|
||||
type: 'object'
|
||||
)),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "", methods: "GET")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function index(): array
|
||||
{
|
||||
$platforms = Platform::query()->orderBy('id')->get();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '获取成功',
|
||||
'data' => $platforms,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,586 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\V1;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
use App\Middleware\PermissionMiddleware;
|
||||
use App\Model\Role;
|
||||
use App\Service\OperationLogService;
|
||||
use App\Utils\RequestHelper;
|
||||
use App\Model\RoleRouteOverride;
|
||||
use App\Model\Route;
|
||||
use App\Model\RouteGroup;
|
||||
use App\Model\User;
|
||||
use App\Service\ScopeTableManager;
|
||||
use Hyperf\DbConnection\Db;
|
||||
use Hyperf\HttpServer\Annotation\Controller;
|
||||
use Hyperf\HttpServer\Annotation\Middleware;
|
||||
use Hyperf\HttpServer\Annotation\RequestMapping;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Qbhy\HyperfAuth\AuthManager;
|
||||
|
||||
#[OA\Tag(name: 'Roles', description: '角色与授权管理')]
|
||||
#[Controller(prefix: "/api/v1/roles")]
|
||||
class RoleController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
protected readonly ScopeTableManager $scopeTableManager,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色列表
|
||||
*/
|
||||
#[OA\Get(
|
||||
path: '/roles',
|
||||
summary: '角色列表',
|
||||
description: '获取所有角色,包含每个角色的用户数和路由组数',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Roles'],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '获取成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '获取成功'),
|
||||
new OA\Property(property: 'data', type: 'array', items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'name', type: 'string', example: 'editor'),
|
||||
new OA\Property(property: 'label', type: 'string', example: '编辑者'),
|
||||
new OA\Property(property: 'description', type: 'string', example: '可以编辑内容'),
|
||||
new OA\Property(property: 'users_count', type: 'integer', example: 5),
|
||||
new OA\Property(property: 'route_groups_count', type: 'integer', example: 3),
|
||||
],
|
||||
type: 'object'
|
||||
)),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "", methods: "GET")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function index(): array
|
||||
{
|
||||
$roles = Role::query()
|
||||
->withCount(['users', 'routeGroups'])
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '获取成功',
|
||||
'data' => $roles,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配用户角色
|
||||
*
|
||||
* administrator 不允许降级自己的角色(防止锁死)
|
||||
* 分配角色后触发 bitmap 重建
|
||||
*
|
||||
* @param int $id 用户 ID
|
||||
*/
|
||||
#[OA\Put(
|
||||
path: '/users/{id}/role',
|
||||
summary: '分配用户角色',
|
||||
description: '为指定用户分配角色,administrator 不允许降级自己的角色',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Roles'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'id', in: 'path', required: true, description: '用户 ID', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['role_id'],
|
||||
properties: [
|
||||
new OA\Property(property: 'role_id', type: 'integer', description: '角色 ID', example: 2),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '角色分配成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '角色分配成功'),
|
||||
new OA\Property(property: 'data', ref: '#/components/schemas/User'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 400, description: '不允许降级自己的管理员角色', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 404, description: '用户或角色不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "/api/v1/users/{id}/role", methods: "PUT")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function assignRole(int $id, AuthManager $auth): ResponseInterface|array
|
||||
{
|
||||
$target_user = User::query()->with('role')->find($id);
|
||||
|
||||
if (!$target_user) {
|
||||
return $this->response->json([
|
||||
'code' => 404,
|
||||
'message' => '用户不存在',
|
||||
])->withStatus(404);
|
||||
}
|
||||
|
||||
$body = $this->request->getParsedBody();
|
||||
|
||||
if (!isset($body['role_id'])) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '缺少 role_id 参数',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$role_id = (int) $body['role_id'];
|
||||
$new_role = Role::query()->find($role_id);
|
||||
|
||||
if (!$new_role) {
|
||||
return $this->response->json([
|
||||
'code' => 404,
|
||||
'message' => '角色不存在',
|
||||
])->withStatus(404);
|
||||
}
|
||||
|
||||
// 防止 administrator 降级自己
|
||||
$current_user = $auth->guard('jwt')->user();
|
||||
if ($current_user instanceof User
|
||||
&& $current_user->id === $id
|
||||
&& $target_user->isAdministrator()
|
||||
&& $new_role->name !== 'administrator'
|
||||
) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '不允许降级自己的管理员角色',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$target_user->role_id = $role_id;
|
||||
$target_user->save();
|
||||
|
||||
// 角色变化影响 scope 计算,重建 bitmap
|
||||
$this->scopeTableManager->rebuildUserScope($id);
|
||||
|
||||
$target_user->refresh();
|
||||
$target_user->load('role');
|
||||
|
||||
OperationLogService::log(
|
||||
user_id: OperationLogService::getCurrentUserId(),
|
||||
action: 'role.update',
|
||||
target_type: 'user',
|
||||
target_id: $id,
|
||||
description: "用户 #{$id} 角色变更为 {$new_role->name}",
|
||||
detail: ['role_id' => $role_id, 'role_name' => $new_role->name],
|
||||
ip: RequestHelper::getClientIp($this->request),
|
||||
);
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '角色分配成功',
|
||||
'data' => $target_user,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看角色已授权的路由组
|
||||
*
|
||||
* @param int $id 角色 ID
|
||||
*/
|
||||
#[OA\Get(
|
||||
path: '/roles/{id}/route-groups',
|
||||
summary: '查看角色已授权的路由组',
|
||||
description: '获取指定角色已授权的路由组列表',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Roles'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'id', in: 'path', required: true, description: '角色 ID', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '获取成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '获取成功'),
|
||||
new OA\Property(property: 'data', type: 'array', items: new OA\Items(type: 'object')),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 404, description: '角色不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "{id}/route-groups", methods: "GET")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function getRouteGroups(int $id): ResponseInterface|array
|
||||
{
|
||||
$role = Role::query()->find($id);
|
||||
|
||||
if (!$role) {
|
||||
return $this->response->json([
|
||||
'code' => 404,
|
||||
'message' => '角色不存在',
|
||||
])->withStatus(404);
|
||||
}
|
||||
|
||||
$groups = $role->routeGroups()->withCount('routes')->orderBy('sort_order')->get();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '获取成功',
|
||||
'data' => $groups,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置角色的路由组授权(全量替换)
|
||||
*
|
||||
* 请求体:{ "group_ids": [1, 3, 5] }
|
||||
*
|
||||
* @param int $id 角色 ID
|
||||
*/
|
||||
#[OA\Put(
|
||||
path: '/roles/{id}/route-groups',
|
||||
summary: '设置角色的路由组授权',
|
||||
description: '全量替换指定角色的路由组授权,administrator 角色不允许修改',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Roles'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'id', in: 'path', required: true, description: '角色 ID', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['group_ids'],
|
||||
properties: [
|
||||
new OA\Property(property: 'group_ids', type: 'array', items: new OA\Items(type: 'integer'), example: [1, 3, 5]),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '路由组授权更新成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '路由组授权更新成功'),
|
||||
new OA\Property(property: 'data', type: 'array', items: new OA\Items(type: 'object')),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 400, description: 'administrator 角色不允许修改或参数错误', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 404, description: '角色不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "{id}/route-groups", methods: "PUT")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function setRouteGroups(int $id): ResponseInterface|array
|
||||
{
|
||||
$role = Role::query()->find($id);
|
||||
|
||||
if (!$role) {
|
||||
return $this->response->json([
|
||||
'code' => 404,
|
||||
'message' => '角色不存在',
|
||||
])->withStatus(404);
|
||||
}
|
||||
|
||||
// 不允许修改 administrator 角色的路由授权(administrator 拥有全部权限)
|
||||
if ($role->name === 'administrator') {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'administrator 角色拥有全部权限,无需设置路由组授权',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$body = $this->request->getParsedBody();
|
||||
|
||||
if (!array_key_exists('group_ids', $body)) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '缺少 group_ids 参数',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$group_ids = $body['group_ids'];
|
||||
|
||||
if (!is_array($group_ids)) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'group_ids 必须为数组',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
// 过滤并校验 group_ids
|
||||
$group_ids = array_map('intval', $group_ids);
|
||||
$group_ids = array_unique($group_ids);
|
||||
|
||||
// 校验 group_ids 都存在
|
||||
if (!empty($group_ids)) {
|
||||
$existing_count = RouteGroup::query()->whereIn('id', $group_ids)->count();
|
||||
if ($existing_count !== count($group_ids)) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '包含不存在的 group_id',
|
||||
])->withStatus(400);
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 sync 全量替换
|
||||
$role->routeGroups()->sync($group_ids);
|
||||
|
||||
// 返回更新后的路由组列表
|
||||
$groups = $role->routeGroups()->withCount('routes')->orderBy('sort_order')->get();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '路由组授权更新成功',
|
||||
'data' => $groups,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看角色的单条路由覆盖
|
||||
*
|
||||
* @param int $id 角色 ID
|
||||
*/
|
||||
#[OA\Get(
|
||||
path: '/roles/{id}/route-overrides',
|
||||
summary: '查看角色的路由覆盖',
|
||||
description: '获取指定角色的路由覆盖列表,包含路由详情',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Roles'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'id', in: 'path', required: true, description: '角色 ID', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '获取成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '获取成功'),
|
||||
new OA\Property(property: 'data', type: 'array', items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'id', type: 'integer'),
|
||||
new OA\Property(property: 'role_id', type: 'integer'),
|
||||
new OA\Property(property: 'route_id', type: 'integer'),
|
||||
new OA\Property(property: 'allowed', type: 'boolean'),
|
||||
new OA\Property(property: 'route', type: 'object'),
|
||||
],
|
||||
type: 'object'
|
||||
)),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 404, description: '角色不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "{id}/route-overrides", methods: "GET")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function getRouteOverrides(int $id): ResponseInterface|array
|
||||
{
|
||||
$role = Role::query()->find($id);
|
||||
|
||||
if (!$role) {
|
||||
return $this->response->json([
|
||||
'code' => 404,
|
||||
'message' => '角色不存在',
|
||||
])->withStatus(404);
|
||||
}
|
||||
|
||||
$overrides = RoleRouteOverride::query()
|
||||
->where('role_id', $id)
|
||||
->with('route')
|
||||
->get();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '获取成功',
|
||||
'data' => $overrides,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置角色的单条路由覆盖(全量替换)
|
||||
*
|
||||
* 请求体:{ "overrides": [{ "route_id": 12, "allowed": false }, ...] }
|
||||
*
|
||||
* @param int $id 角色 ID
|
||||
*/
|
||||
#[OA\Put(
|
||||
path: '/roles/{id}/route-overrides',
|
||||
summary: '设置角色的路由覆盖',
|
||||
description: '全量替换指定角色的路由覆盖规则,administrator 角色不允许修改',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Roles'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'id', in: 'path', required: true, description: '角色 ID', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['overrides'],
|
||||
properties: [
|
||||
new OA\Property(
|
||||
property: 'overrides',
|
||||
type: 'array',
|
||||
items: new OA\Items(
|
||||
required: ['route_id', 'allowed'],
|
||||
properties: [
|
||||
new OA\Property(property: 'route_id', type: 'integer', description: '路由 ID', example: 12),
|
||||
new OA\Property(property: 'allowed', type: 'boolean', description: '是否允许', example: false),
|
||||
],
|
||||
type: 'object'
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '路由覆盖更新成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '路由覆盖更新成功'),
|
||||
new OA\Property(property: 'data', type: 'array', items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'id', type: 'integer'),
|
||||
new OA\Property(property: 'role_id', type: 'integer'),
|
||||
new OA\Property(property: 'route_id', type: 'integer'),
|
||||
new OA\Property(property: 'allowed', type: 'boolean'),
|
||||
new OA\Property(property: 'route', type: 'object'),
|
||||
],
|
||||
type: 'object'
|
||||
)),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 400, description: 'route_id 重复、不存在或 administrator 角色不允许修改', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 404, description: '角色不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "{id}/route-overrides", methods: "PUT")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function setRouteOverrides(int $id): ResponseInterface|array
|
||||
{
|
||||
$role = Role::query()->find($id);
|
||||
|
||||
if (!$role) {
|
||||
return $this->response->json([
|
||||
'code' => 404,
|
||||
'message' => '角色不存在',
|
||||
])->withStatus(404);
|
||||
}
|
||||
|
||||
// 不允许修改 administrator 角色的路由覆盖
|
||||
if ($role->name === 'administrator') {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'administrator 角色拥有全部权限,无需设置路由覆盖',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$body = $this->request->getParsedBody();
|
||||
|
||||
if (!array_key_exists('overrides', $body)) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '缺少 overrides 参数',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$overrides = $body['overrides'];
|
||||
|
||||
if (!is_array($overrides)) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'overrides 必须为数组',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
// 校验每条 override 格式
|
||||
$records = [];
|
||||
$seen_route_ids = [];
|
||||
foreach ($overrides as $item) {
|
||||
if (!is_array($item) || !isset($item['route_id']) || !isset($item['allowed'])) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '每条覆盖规则必须包含 route_id 和 allowed 字段',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$route_id = (int) $item['route_id'];
|
||||
|
||||
// 检查重复
|
||||
if (in_array($route_id, $seen_route_ids, true)) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => "route_id {$route_id} 重复",
|
||||
])->withStatus(400);
|
||||
}
|
||||
$seen_route_ids[] = $route_id;
|
||||
|
||||
$records[] = [
|
||||
'role_id' => $id,
|
||||
'route_id' => $route_id,
|
||||
'allowed' => (bool) $item['allowed'],
|
||||
];
|
||||
}
|
||||
|
||||
// 校验 route_ids 都存在
|
||||
if (!empty($seen_route_ids)) {
|
||||
$existing_count = Route::query()->whereIn('id', $seen_route_ids)->count();
|
||||
if ($existing_count !== count($seen_route_ids)) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '包含不存在的 route_id',
|
||||
])->withStatus(400);
|
||||
}
|
||||
}
|
||||
|
||||
// 事务内全量替换
|
||||
Db::beginTransaction();
|
||||
try {
|
||||
RoleRouteOverride::query()->where('role_id', $id)->delete();
|
||||
|
||||
if (!empty($records)) {
|
||||
RoleRouteOverride::query()->insert($records);
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollBack();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// 返回更新后的覆盖列表
|
||||
$result = RoleRouteOverride::query()
|
||||
->where('role_id', $id)
|
||||
->with('route')
|
||||
->get();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '路由覆盖更新成功',
|
||||
'data' => $result,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,687 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\V1;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
use App\Middleware\PermissionMiddleware;
|
||||
use App\Model\Route;
|
||||
use App\Model\RouteGroup;
|
||||
use App\Service\RouteSyncService;
|
||||
use Hyperf\DbConnection\Db;
|
||||
use Hyperf\HttpServer\Annotation\Controller;
|
||||
use Hyperf\HttpServer\Annotation\Middleware;
|
||||
use Hyperf\HttpServer\Annotation\RequestMapping;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
#[OA\Tag(name: 'Route Groups', description: '路由组管理')]
|
||||
#[Controller(prefix: "/api/v1/route-groups")]
|
||||
class RouteGroupController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* 路由组列表
|
||||
*
|
||||
* 返回所有路由组,包含每组的路由数量
|
||||
*/
|
||||
#[OA\Get(
|
||||
path: '/route-groups',
|
||||
summary: '路由组列表',
|
||||
description: '返回所有路由组,包含每组的路由数量',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Route Groups'],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '获取成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '获取成功'),
|
||||
new OA\Property(property: 'data', type: 'array', items: new OA\Items(properties: [
|
||||
new OA\Property(property: 'id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'name', type: 'string', example: 'user-management'),
|
||||
new OA\Property(property: 'label', type: 'string', nullable: true, example: '用户管理'),
|
||||
new OA\Property(property: 'description', type: 'string', nullable: true, example: '用户相关路由'),
|
||||
new OA\Property(property: 'sort_order', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'routes_count', type: 'integer', example: 5),
|
||||
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||
])),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "", methods: "GET")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function index(): array
|
||||
{
|
||||
$groups = RouteGroup::query()
|
||||
->withCount('routes')
|
||||
->orderBy('sort_order')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '获取成功',
|
||||
'data' => $groups,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建路由组
|
||||
*/
|
||||
#[OA\Post(
|
||||
path: '/route-groups',
|
||||
summary: '创建路由组',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Route Groups'],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['name'],
|
||||
properties: [
|
||||
new OA\Property(property: 'name', type: 'string', maxLength: 100, example: 'user-management'),
|
||||
new OA\Property(property: 'label', type: 'string', maxLength: 200, nullable: true, example: '用户管理'),
|
||||
new OA\Property(property: 'description', type: 'string', nullable: true, example: '用户相关路由'),
|
||||
new OA\Property(property: 'sort_order', type: 'integer', default: 0, example: 0),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '创建成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '创建成功'),
|
||||
new OA\Property(property: 'data', properties: [
|
||||
new OA\Property(property: 'id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'name', type: 'string', example: 'user-management'),
|
||||
new OA\Property(property: 'label', type: 'string', nullable: true, example: '用户管理'),
|
||||
new OA\Property(property: 'description', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'sort_order', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||
], type: 'object'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 400, description: '参数校验失败或名称重复', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "", methods: "POST")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function store(): ResponseInterface|array
|
||||
{
|
||||
$name = $this->request->input('name');
|
||||
$label = $this->request->input('label');
|
||||
$description = $this->request->input('description');
|
||||
$sort_order = $this->request->input('sort_order');
|
||||
|
||||
// 校验 name
|
||||
if (!is_string($name) || trim($name) === '') {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'name 不能为空',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$name = trim($name);
|
||||
if (strlen($name) > 100) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'name 长度不能超过 100 个字符',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
if (RouteGroup::query()->where('name', $name)->exists()) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '路由组名称已存在',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
// 校验 label
|
||||
if ($label !== null && $label !== '') {
|
||||
if (!is_string($label)) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'label 必须为字符串',
|
||||
])->withStatus(400);
|
||||
}
|
||||
$label = trim($label);
|
||||
if (strlen($label) > 200) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'label 长度不能超过 200 个字符',
|
||||
])->withStatus(400);
|
||||
}
|
||||
} else {
|
||||
$label = null;
|
||||
}
|
||||
|
||||
// 校验 sort_order
|
||||
if ($sort_order !== null && $sort_order !== '') {
|
||||
if (!is_numeric($sort_order)) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'sort_order 必须为整数',
|
||||
])->withStatus(400);
|
||||
}
|
||||
$sort_order = (int) $sort_order;
|
||||
} else {
|
||||
$sort_order = 0;
|
||||
}
|
||||
|
||||
$group = RouteGroup::query()->create([
|
||||
'name' => $name,
|
||||
'label' => $label,
|
||||
'description' => is_string($description) ? trim($description) : null,
|
||||
'sort_order' => $sort_order,
|
||||
]);
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '创建成功',
|
||||
'data' => $group,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新路由组
|
||||
*
|
||||
* @param int $id 路由组 ID
|
||||
*/
|
||||
#[OA\Put(
|
||||
path: '/route-groups/{id}',
|
||||
summary: '更新路由组',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Route Groups'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'id', in: 'path', required: true, description: '路由组 ID', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'name', type: 'string', maxLength: 100, example: 'user-management'),
|
||||
new OA\Property(property: 'label', type: 'string', maxLength: 200, nullable: true, example: '用户管理'),
|
||||
new OA\Property(property: 'description', type: 'string', nullable: true, example: '用户相关路由'),
|
||||
new OA\Property(property: 'sort_order', type: 'integer', example: 0),
|
||||
])
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '更新成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '更新成功'),
|
||||
new OA\Property(property: 'data', properties: [
|
||||
new OA\Property(property: 'id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'name', type: 'string', example: 'user-management'),
|
||||
new OA\Property(property: 'label', type: 'string', nullable: true, example: '用户管理'),
|
||||
new OA\Property(property: 'description', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'sort_order', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||
], type: 'object'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 400, description: '参数校验失败或名称重复', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 404, description: '路由组不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "{id}", methods: "PUT")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function update(int $id): ResponseInterface|array
|
||||
{
|
||||
$group = RouteGroup::query()->find($id);
|
||||
|
||||
if (!$group) {
|
||||
return $this->response->json([
|
||||
'code' => 404,
|
||||
'message' => '路由组不存在',
|
||||
])->withStatus(404);
|
||||
}
|
||||
|
||||
$name = $this->request->input('name');
|
||||
$label = $this->request->input('label');
|
||||
$description = $this->request->input('description');
|
||||
$sort_order = $this->request->input('sort_order');
|
||||
$updates = [];
|
||||
|
||||
if ($name !== null) {
|
||||
if (!is_string($name) || trim($name) === '') {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'name 不能为空',
|
||||
])->withStatus(400);
|
||||
}
|
||||
$name = trim($name);
|
||||
if (strlen($name) > 100) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'name 长度不能超过 100 个字符',
|
||||
])->withStatus(400);
|
||||
}
|
||||
if (RouteGroup::query()->where('name', $name)->where('id', '!=', $group->id)->exists()) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '路由组名称已存在',
|
||||
])->withStatus(400);
|
||||
}
|
||||
$updates['name'] = $name;
|
||||
}
|
||||
|
||||
if ($label !== null) {
|
||||
if (!is_string($label)) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'label 必须为字符串',
|
||||
])->withStatus(400);
|
||||
}
|
||||
$updates['label'] = trim($label);
|
||||
}
|
||||
|
||||
if ($description !== null) {
|
||||
$updates['description'] = is_string($description) ? trim($description) : null;
|
||||
}
|
||||
|
||||
if ($sort_order !== null) {
|
||||
if (!is_numeric($sort_order)) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'sort_order 必须为整数',
|
||||
])->withStatus(400);
|
||||
}
|
||||
$updates['sort_order'] = (int) $sort_order;
|
||||
}
|
||||
|
||||
if ($updates === []) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '缺少可更新字段',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$group->fill($updates);
|
||||
$group->save();
|
||||
$group->refresh();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '更新成功',
|
||||
'data' => $group,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除路由组
|
||||
*
|
||||
* 组内路由 group_id 自动设为 NULL(ON DELETE SET NULL)
|
||||
* role_route_groups 关联自动级联删除(ON DELETE CASCADE)
|
||||
*
|
||||
* @param int $id 路由组 ID
|
||||
*/
|
||||
#[OA\Delete(
|
||||
path: '/route-groups/{id}',
|
||||
summary: '删除路由组',
|
||||
description: '删除路由组,组内路由 group_id 自动设为 NULL,role_route_groups 关联自动级联删除',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Route Groups'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'id', in: 'path', required: true, description: '路由组 ID', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '删除成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '删除成功'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 404, description: '路由组不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "{id}", methods: "DELETE")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function destroy(int $id): ResponseInterface|array
|
||||
{
|
||||
$group = RouteGroup::query()->find($id);
|
||||
|
||||
if (!$group) {
|
||||
return $this->response->json([
|
||||
'code' => 404,
|
||||
'message' => '路由组不存在',
|
||||
])->withStatus(404);
|
||||
}
|
||||
|
||||
$group->delete();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '删除成功',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 全部路由列表
|
||||
*
|
||||
* 含分组信息,支持按 group_id 筛选(传 0 或 "ungrouped" 筛选未分组路由)
|
||||
*/
|
||||
#[OA\Get(
|
||||
path: '/routes',
|
||||
summary: '路由列表',
|
||||
description: '获取全部路由列表,含分组信息,支持按 group_id、method、path 筛选',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Route Groups'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'group_id', in: 'query', required: false, description: '路由组 ID,传 0 或 "ungrouped" 筛选未分组路由', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'method', in: 'query', required: false, description: 'HTTP 方法筛选', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'path', in: 'query', required: false, description: '路径模糊搜索', schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '获取成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '获取成功'),
|
||||
new OA\Property(property: 'data', type: 'array', items: new OA\Items(properties: [
|
||||
new OA\Property(property: 'id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'method', type: 'string', example: 'GET'),
|
||||
new OA\Property(property: 'path', type: 'string', example: '/api/v1/users'),
|
||||
new OA\Property(property: 'group_id', type: 'integer', nullable: true, example: 1),
|
||||
new OA\Property(property: 'group', nullable: true, properties: [
|
||||
new OA\Property(property: 'id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'name', type: 'string', example: 'user-management'),
|
||||
new OA\Property(property: 'label', type: 'string', nullable: true, example: '用户管理'),
|
||||
], type: 'object'),
|
||||
])),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "/api/v1/routes", methods: "GET")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function routes(): array
|
||||
{
|
||||
$query = Route::query()->with('group');
|
||||
|
||||
// 按 group_id 筛选
|
||||
$group_id = $this->request->input('group_id');
|
||||
if ($group_id !== null && $group_id !== '') {
|
||||
if ($group_id === '0' || $group_id === 'ungrouped') {
|
||||
// 未分组路由
|
||||
$query->whereNull('group_id');
|
||||
} else {
|
||||
$query->where('group_id', (int) $group_id);
|
||||
}
|
||||
}
|
||||
|
||||
// 按 method 筛选
|
||||
$method = $this->request->input('method');
|
||||
if ($method !== null && $method !== '') {
|
||||
$query->where('method', strtoupper($method));
|
||||
}
|
||||
|
||||
// 按 path 模糊搜索
|
||||
$path = $this->request->input('path');
|
||||
if ($path !== null && $path !== '') {
|
||||
$query->where('path', 'like', '%' . $path . '%');
|
||||
}
|
||||
|
||||
$routes = $query->orderBy('path')->orderBy('method')->get();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '获取成功',
|
||||
'data' => $routes,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 将路由分配到路由组
|
||||
*
|
||||
* 传 group_id=null 表示从分组中移出
|
||||
*
|
||||
* @param int $id 路由 ID
|
||||
*/
|
||||
#[OA\Put(
|
||||
path: '/routes/{id}/group',
|
||||
summary: '分配路由到路由组',
|
||||
description: '将路由分配到指定路由组,传 group_id=null 表示从分组中移出',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Route Groups'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'id', in: 'path', required: true, description: '路由 ID', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['group_id'],
|
||||
properties: [
|
||||
new OA\Property(property: 'group_id', type: 'integer', nullable: true, description: '路由组 ID,传 null 移出分组', example: 1),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '分配成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '分配成功'),
|
||||
new OA\Property(property: 'data', properties: [
|
||||
new OA\Property(property: 'id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'method', type: 'string', example: 'GET'),
|
||||
new OA\Property(property: 'path', type: 'string', example: '/api/v1/users'),
|
||||
new OA\Property(property: 'group_id', type: 'integer', nullable: true, example: 1),
|
||||
new OA\Property(property: 'group', nullable: true, properties: [
|
||||
new OA\Property(property: 'id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'name', type: 'string', example: 'user-management'),
|
||||
new OA\Property(property: 'label', type: 'string', nullable: true, example: '用户管理'),
|
||||
], type: 'object'),
|
||||
], type: 'object'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 400, description: '参数校验失败', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 404, description: '路由或目标路由组不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "/api/v1/routes/{id}/group", methods: "PUT")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function assignRouteGroup(int $id): ResponseInterface|array
|
||||
{
|
||||
$route = Route::query()->find($id);
|
||||
|
||||
if (!$route) {
|
||||
return $this->response->json([
|
||||
'code' => 404,
|
||||
'message' => '路由不存在',
|
||||
])->withStatus(404);
|
||||
}
|
||||
|
||||
// group_id 可以为 null(移出分组)或整数(分配到指定分组)
|
||||
$body = $this->request->getParsedBody();
|
||||
if (!array_key_exists('group_id', $body)) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '缺少 group_id 参数',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$group_id = $body['group_id'];
|
||||
|
||||
if ($group_id !== null) {
|
||||
if (!is_numeric($group_id)) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'group_id 必须为整数或 null',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$group_id = (int) $group_id;
|
||||
|
||||
// 校验目标路由组存在
|
||||
if (!RouteGroup::query()->where('id', $group_id)->exists()) {
|
||||
return $this->response->json([
|
||||
'code' => 404,
|
||||
'message' => '目标路由组不存在',
|
||||
])->withStatus(404);
|
||||
}
|
||||
}
|
||||
|
||||
$route->group_id = $group_id;
|
||||
$route->save();
|
||||
$route->load('group');
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => $group_id === null ? '已从分组中移出' : '分配成功',
|
||||
'data' => $route,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量同步路由到路由组
|
||||
*
|
||||
* 替换式同步:先清空旧归属,再设置新归属,事务保证原子性
|
||||
*
|
||||
* @param int $id 路由组 ID
|
||||
*/
|
||||
#[OA\Put(
|
||||
path: '/route-groups/{id}/routes',
|
||||
summary: '批量同步路由到路由组',
|
||||
description: '替换式同步:提交 route_ids 数组,组内路由将被完全替换为指定路由。提交空数组清空组内所有路由。若指定路由已属于其他组,将自动从原组移出。',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Route Groups'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'id', in: 'path', required: true, description: '路由组 ID', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['route_ids'],
|
||||
properties: [
|
||||
new OA\Property(property: 'route_ids', type: 'array', items: new OA\Items(type: 'integer'), description: '路由 ID 数组,空数组表示清空', example: [1, 2, 3]),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '同步成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '同步成功'),
|
||||
new OA\Property(property: 'data', properties: [
|
||||
new OA\Property(property: 'id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'name', type: 'string', example: 'user-management'),
|
||||
new OA\Property(property: 'label', type: 'string', nullable: true, example: '用户管理'),
|
||||
new OA\Property(property: 'routes_count', type: 'integer', example: 5),
|
||||
], type: 'object'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 404, description: '路由组不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 422, description: '参数校验失败', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "{id}/routes", methods: "PUT")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function syncRoutes(int $id): ResponseInterface|array
|
||||
{
|
||||
$group = RouteGroup::query()->find($id);
|
||||
|
||||
if (!$group) {
|
||||
return $this->response->json([
|
||||
'code' => 404,
|
||||
'message' => '路由组不存在',
|
||||
])->withStatus(404);
|
||||
}
|
||||
|
||||
$body = $this->request->getParsedBody();
|
||||
if (!array_key_exists('route_ids', $body) || !is_array($body['route_ids'])) {
|
||||
return $this->response->json([
|
||||
'code' => 422,
|
||||
'message' => 'route_ids 必须为数组',
|
||||
])->withStatus(422);
|
||||
}
|
||||
|
||||
$route_ids = array_values(array_map('intval', array_filter($body['route_ids'], 'is_numeric')));
|
||||
|
||||
Db::transaction(function () use ($id, $route_ids): void {
|
||||
// 清空旧归属
|
||||
Route::query()->where('group_id', $id)->update(['group_id' => null]);
|
||||
|
||||
// 设置新归属
|
||||
if ($route_ids !== []) {
|
||||
Route::query()->whereIn('id', $route_ids)->update(['group_id' => $id]);
|
||||
}
|
||||
});
|
||||
|
||||
$group->loadCount('routes');
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '同步成功',
|
||||
'data' => $group,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步注解路由到数据库
|
||||
*
|
||||
* 将 Hyperf 注解定义的 API 路由同步到 routes 表,管理员专用
|
||||
*/
|
||||
#[OA\Post(
|
||||
path: '/routes/sync',
|
||||
summary: '同步注解路由到数据库',
|
||||
description: '将 Hyperf 注解定义的 API 路由同步到 routes 表,支持重复调用(幂等)',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Route Groups'],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '同步成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '同步成功'),
|
||||
new OA\Property(property: 'data', properties: [
|
||||
new OA\Property(property: 'synced', type: 'integer', description: '同步的路由数量', example: 50),
|
||||
new OA\Property(property: 'total', type: 'integer', description: '总路由数量', example: 50),
|
||||
], type: 'object'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 403, description: '无权限', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "/api/v1/routes/sync", methods: "POST")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function sync(): array
|
||||
{
|
||||
$result = $this->container->get(RouteSyncService::class)->sync();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '同步成功',
|
||||
'data' => $result,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\V1;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
use App\Middleware\PermissionMiddleware;
|
||||
use App\Model\Store;
|
||||
use Hyperf\HttpServer\Annotation\Controller;
|
||||
use Hyperf\HttpServer\Annotation\Middleware;
|
||||
use Hyperf\HttpServer\Annotation\RequestMapping;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\Tag(name: 'Stores', description: '店铺管理')]
|
||||
#[Controller(prefix: "/api/v1/stores")]
|
||||
class StoreController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* 店铺列表(受 scope 过滤,支持 company_id/platform_id 筛选)
|
||||
*
|
||||
* administrator 可见全部,developer/accessor 仅可见权限范围内的店铺
|
||||
*/
|
||||
#[OA\Get(
|
||||
path: '/stores',
|
||||
summary: '店铺列表',
|
||||
description: '获取店铺列表,受 scope 过滤,支持 company_id/platform_id 筛选及 name 模糊搜索',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Stores'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'company_id', in: 'query', required: false, description: '按公司 ID 筛选', schema: new OA\Schema(type: 'integer')),
|
||||
new OA\Parameter(name: 'platform_id', in: 'query', required: false, description: '按平台 ID 筛选', schema: new OA\Schema(type: 'integer')),
|
||||
new OA\Parameter(name: 'name', in: 'query', required: false, description: '按店铺名称模糊搜索', schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '获取成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '获取成功'),
|
||||
new OA\Property(property: 'data', type: 'array', items: new OA\Items(properties: [
|
||||
new OA\Property(property: 'id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'company_id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'platform_id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'platform_store_id', type: 'string', example: 'SHOP-001'),
|
||||
new OA\Property(property: 'name', type: 'string', example: 'my-store'),
|
||||
new OA\Property(property: 'label', type: 'string', example: '我的店铺'),
|
||||
new OA\Property(property: 'enabled', type: 'boolean', example: true),
|
||||
new OA\Property(property: 'warehouse_id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'currency_id', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'timezone', type: 'integer', example: 8),
|
||||
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||
new OA\Property(property: 'updated_at', type: 'string', format: 'date-time'),
|
||||
], type: 'object')),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "", methods: "GET")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function index(): array
|
||||
{
|
||||
$scope_type = $this->request->getAttribute('scope_type');
|
||||
$scope_ids = $this->request->getAttribute('scope_ids', []);
|
||||
|
||||
$query = Store::query();
|
||||
|
||||
// scope 过滤
|
||||
if ($scope_type === 'store') {
|
||||
$query->whereIn('id', $scope_ids);
|
||||
} elseif ($scope_type === 'platform') {
|
||||
$query->whereIn('platform_id', $scope_ids);
|
||||
}
|
||||
// 'all' → 不附加条件
|
||||
|
||||
// 按 company_id 筛选
|
||||
$company_id = $this->request->input('company_id');
|
||||
if ($company_id !== null && $company_id !== '') {
|
||||
$query->where('company_id', (int) $company_id);
|
||||
}
|
||||
|
||||
// 按 platform_id 筛选
|
||||
$platform_id = $this->request->input('platform_id');
|
||||
if ($platform_id !== null && $platform_id !== '') {
|
||||
$query->where('platform_id', (int) $platform_id);
|
||||
}
|
||||
|
||||
// 按 name 模糊搜索
|
||||
$name = $this->request->input('name');
|
||||
if ($name !== null && $name !== '') {
|
||||
$query->where(function ($q) use ($name): void {
|
||||
$q->where('name', 'ilike', "%{$name}%")
|
||||
->orWhere('label', 'ilike', "%{$name}%");
|
||||
});
|
||||
}
|
||||
|
||||
$stores = $query->orderBy('id')->get();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '获取成功',
|
||||
'data' => $stores,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,634 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\V1;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
use App\Middleware\PermissionMiddleware;
|
||||
use App\Model\User;
|
||||
use App\Service\OperationLogService;
|
||||
use App\Utils\RequestHelper;
|
||||
use Hyperf\HttpServer\Annotation\Controller;
|
||||
use Hyperf\HttpServer\Annotation\Middleware;
|
||||
use Hyperf\HttpServer\Annotation\RequestMapping;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\Tag(name: 'Users', description: '用户管理')]
|
||||
#[Controller(prefix: "/api/v1/users")]
|
||||
class UserController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* 用户列表
|
||||
*
|
||||
* 支持分页、按 username/email 模糊搜索、按 status/role_id 精确筛选
|
||||
*/
|
||||
#[OA\Get(
|
||||
path: '/users',
|
||||
summary: '用户列表',
|
||||
description: '获取用户列表,支持分页、按 username/email 模糊搜索、按 status/role_id 精确筛选',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Users'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
|
||||
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15, maximum: 100)),
|
||||
new OA\Parameter(name: 'username', in: 'query', required: false, description: '用户名模糊搜索', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'email', in: 'query', required: false, description: '邮箱模糊搜索', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'status', in: 'query', required: false, description: '状态筛选(0=禁用,1=启用)', schema: new OA\Schema(type: 'integer', enum: [0, 1])),
|
||||
new OA\Parameter(name: 'role_id', in: 'query', required: false, description: '按角色筛选', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '获取成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '获取成功'),
|
||||
new OA\Property(property: 'data', properties: [
|
||||
new OA\Property(property: 'items', type: 'array', items: new OA\Items(ref: '#/components/schemas/User')),
|
||||
new OA\Property(property: 'total', type: 'integer', example: 100),
|
||||
new OA\Property(property: 'page', type: 'integer', example: 1),
|
||||
new OA\Property(property: 'per_page', type: 'integer', example: 15),
|
||||
], type: 'object'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "", methods: "GET")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function index(): array
|
||||
{
|
||||
$page = max(1, (int) $this->request->input('page', 1));
|
||||
$per_page = min(100, max(1, (int) $this->request->input('per_page', 15)));
|
||||
|
||||
$query = User::query()->with('role');
|
||||
|
||||
// 按 username 模糊搜索
|
||||
$username = $this->request->input('username');
|
||||
if ($username !== null && $username !== '') {
|
||||
$query->where('username', 'like', '%' . $username . '%');
|
||||
}
|
||||
|
||||
// 按 email 模糊搜索
|
||||
$email = $this->request->input('email');
|
||||
if ($email !== null && $email !== '') {
|
||||
$query->where('email', 'like', '%' . $email . '%');
|
||||
}
|
||||
|
||||
// 按 status 精确筛选
|
||||
$status = $this->request->input('status');
|
||||
if ($status !== null && $status !== '') {
|
||||
$query->where('status', (int) $status);
|
||||
}
|
||||
|
||||
// 按 role_id 精确筛选
|
||||
$role_id = $this->request->input('role_id');
|
||||
if ($role_id !== null && $role_id !== '') {
|
||||
$query->where('role_id', (int) $role_id);
|
||||
}
|
||||
|
||||
// 按 created_at 降序排序
|
||||
$query->orderBy('created_at', 'desc');
|
||||
|
||||
$total = $query->count();
|
||||
$items = $query->offset(($page - 1) * $per_page)
|
||||
->limit($per_page)
|
||||
->get();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '获取成功',
|
||||
'data' => [
|
||||
'items' => $items,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户
|
||||
*/
|
||||
#[OA\Post(
|
||||
path: '/users',
|
||||
summary: '创建用户',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Users'],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['username', 'password', 'email'],
|
||||
properties: [
|
||||
new OA\Property(property: 'username', type: 'string', minLength: 3, maxLength: 20, example: 'new_user'),
|
||||
new OA\Property(property: 'password', type: 'string', minLength: 6, maxLength: 32, example: 'Pass_1234'),
|
||||
new OA\Property(property: 'email', type: 'string', format: 'email', maxLength: 100, example: 'new@example.com'),
|
||||
new OA\Property(property: 'status', type: 'integer', enum: [0, 1], default: 1),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '创建成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '创建成功'),
|
||||
new OA\Property(property: 'data', ref: '#/components/schemas/User'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 400, description: '参数校验失败或唯一性冲突', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "", methods: "POST")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function store(): \Psr\Http\Message\ResponseInterface|array
|
||||
{
|
||||
$username = $this->request->input('username');
|
||||
$password = $this->request->input('password');
|
||||
$email = $this->request->input('email');
|
||||
$status_input = $this->request->input('status');
|
||||
|
||||
if (!is_string($username) || trim($username) === '') {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '用户名不能为空',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$username = trim($username);
|
||||
$username_length = strlen($username);
|
||||
if ($username_length < 3 || $username_length > 20) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '用户名长度需在 3-20 个字符',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
if (!is_string($password) || $password === '') {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '密码不能为空',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$password_length = strlen($password);
|
||||
if ($password_length < 6 || $password_length > 32) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '密码长度需在 6-32 个字符',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
if (!is_string($email) || trim($email) === '') {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '邮箱不能为空',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$email = trim($email);
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '邮箱格式不正确',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
if (strlen($email) > 100) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '邮箱长度不能超过 100 个字符',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
if ($status_input === null || $status_input === '') {
|
||||
$status = 1;
|
||||
} elseif (!is_numeric($status_input) || !in_array((int) $status_input, [0, 1], true)) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'status 参数必须为 0 或 1',
|
||||
])->withStatus(400);
|
||||
} else {
|
||||
$status = (int) $status_input;
|
||||
}
|
||||
|
||||
if (User::query()->where('username', $username)->exists()) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '用户名已存在',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
if (User::query()->where('email', $email)->exists()) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '邮箱已被注册',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$user = User::query()->create([
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'email' => $email,
|
||||
'status' => $status,
|
||||
]);
|
||||
|
||||
OperationLogService::log(
|
||||
user_id: OperationLogService::getCurrentUserId(),
|
||||
action: 'user.create',
|
||||
target_type: 'user',
|
||||
target_id: $user->id,
|
||||
description: "创建用户 {$username}",
|
||||
detail: ['email' => $email, 'status' => $status],
|
||||
ip: RequestHelper::getClientIp($this->request),
|
||||
);
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '创建成功',
|
||||
'data' => $user,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户详情
|
||||
*
|
||||
* @param int $id 用户 ID
|
||||
*/
|
||||
#[OA\Get(
|
||||
path: '/users/{id}',
|
||||
summary: '用户详情',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Users'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'id', in: 'path', required: true, description: '用户 ID', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '获取成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '获取成功'),
|
||||
new OA\Property(property: 'data', ref: '#/components/schemas/User'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 404, description: '用户不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "{id}", methods: "GET")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function show(int $id): \Psr\Http\Message\ResponseInterface|array
|
||||
{
|
||||
$user = User::query()->with('role')->find($id);
|
||||
|
||||
if (!$user) {
|
||||
return $this->response->json([
|
||||
'code' => 404,
|
||||
'message' => '用户不存在',
|
||||
])->withStatus(404);
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '获取成功',
|
||||
'data' => $user,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*
|
||||
* @param int $id 用户 ID
|
||||
*/
|
||||
#[OA\Put(
|
||||
path: '/users/{id}',
|
||||
summary: '更新用户信息',
|
||||
description: '更新用户的 username、email 或 ext 字段,不支持修改密码',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Users'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'id', in: 'path', required: true, description: '用户 ID', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'username', type: 'string', minLength: 3, maxLength: 20, example: 'updated_user'),
|
||||
new OA\Property(property: 'email', type: 'string', format: 'email', maxLength: 100, example: 'updated@example.com'),
|
||||
new OA\Property(property: 'ext', type: 'object', nullable: true, example: ['nickname' => 'Tester']),
|
||||
])
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '更新成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '更新成功'),
|
||||
new OA\Property(property: 'data', ref: '#/components/schemas/User'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 400, description: '参数校验失败或唯一性冲突', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 404, description: '用户不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "{id}", methods: "PUT")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function update(int $id): \Psr\Http\Message\ResponseInterface|array
|
||||
{
|
||||
$user = User::query()->find($id);
|
||||
|
||||
if (!$user) {
|
||||
return $this->response->json([
|
||||
'code' => 404,
|
||||
'message' => '用户不存在',
|
||||
])->withStatus(404);
|
||||
}
|
||||
|
||||
if ($this->request->input('password') !== null) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '更新接口不支持修改密码',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$username = $this->request->input('username');
|
||||
$email = $this->request->input('email');
|
||||
$ext = $this->request->input('ext');
|
||||
$updates = [];
|
||||
|
||||
if ($username !== null) {
|
||||
if (!is_string($username) || trim($username) === '') {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '用户名不能为空',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$username = trim($username);
|
||||
$username_length = strlen($username);
|
||||
if ($username_length < 3 || $username_length > 20) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '用户名长度需在 3-20 个字符',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
if (User::query()->where('username', $username)->where('id', '!=', $user->id)->exists()) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '用户名已存在',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$updates['username'] = $username;
|
||||
}
|
||||
|
||||
if ($email !== null) {
|
||||
if (!is_string($email) || trim($email) === '') {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '邮箱不能为空',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$email = trim($email);
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '邮箱格式不正确',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
if (strlen($email) > 100) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '邮箱长度不能超过 100 个字符',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
if (User::query()->where('email', $email)->where('id', '!=', $user->id)->exists()) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '邮箱已被注册',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$updates['email'] = $email;
|
||||
}
|
||||
|
||||
if ($ext !== null) {
|
||||
if (!is_array($ext)) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'ext 必须为对象',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$updates['ext'] = $ext;
|
||||
}
|
||||
|
||||
if ($updates === []) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '缺少可更新字段',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$user->fill($updates);
|
||||
$user->save();
|
||||
$user->refresh();
|
||||
|
||||
OperationLogService::log(
|
||||
user_id: OperationLogService::getCurrentUserId(),
|
||||
action: 'user.update',
|
||||
target_type: 'user',
|
||||
target_id: $user->id,
|
||||
description: "更新用户 {$user->username} 信息",
|
||||
detail: $updates,
|
||||
ip: RequestHelper::getClientIp($this->request),
|
||||
);
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '更新成功',
|
||||
'data' => $user,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户状态
|
||||
*
|
||||
* @param int $id 用户 ID
|
||||
*/
|
||||
#[OA\Patch(
|
||||
path: '/users/{id}/status',
|
||||
summary: '更新用户状态',
|
||||
description: '启用或禁用用户',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Users'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'id', in: 'path', required: true, description: '用户 ID', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['status'],
|
||||
properties: [
|
||||
new OA\Property(property: 'status', type: 'integer', enum: [0, 1], description: '0=禁用,1=启用'),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: '状态更新成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: '状态更新成功'),
|
||||
new OA\Property(property: 'data', ref: '#/components/schemas/User'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 400, description: '参数校验失败', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 404, description: '用户不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "{id}/status", methods: "PATCH")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function updateStatus(int $id): \Psr\Http\Message\ResponseInterface|array
|
||||
{
|
||||
$user = User::query()->find($id);
|
||||
|
||||
if (!$user) {
|
||||
return $this->response->json([
|
||||
'code' => 404,
|
||||
'message' => '用户不存在',
|
||||
])->withStatus(404);
|
||||
}
|
||||
|
||||
$status_input = $this->request->input('status');
|
||||
if ($status_input === null || $status_input === '') {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => '缺少 status 参数',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
if (!is_numeric($status_input) || !in_array((int) $status_input, [0, 1], true)) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'status 参数必须为 0 或 1',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$old_status = $user->status;
|
||||
$user->status = (int) $status_input;
|
||||
$user->save();
|
||||
$user->refresh();
|
||||
|
||||
OperationLogService::log(
|
||||
user_id: OperationLogService::getCurrentUserId(),
|
||||
action: 'user.status_change',
|
||||
target_type: 'user',
|
||||
target_id: $user->id,
|
||||
description: "用户 {$user->username} 状态变更",
|
||||
detail: ['old_status' => $old_status, 'new_status' => $user->status],
|
||||
ip: RequestHelper::getClientIp($this->request),
|
||||
);
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '状态更新成功',
|
||||
'data' => $user,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员切换用户 API Key 权限
|
||||
*/
|
||||
#[OA\Patch(
|
||||
path: '/users/{id}/api-key-enabled',
|
||||
summary: '切换用户 API Key 权限',
|
||||
description: '管理员切换指定用户的 api_key_enabled 全局开关。关闭后该用户所有 Key 无法认证,重新开启后自动恢复',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Users'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'id', in: 'path', required: true, description: '用户 ID', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['api_key_enabled'],
|
||||
properties: [
|
||||
new OA\Property(property: 'api_key_enabled', type: 'boolean', description: '是否启用 API Key 功能'),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'API Key 权限更新成功',
|
||||
content: new OA\JsonContent(properties: [
|
||||
new OA\Property(property: 'code', type: 'integer', example: 0),
|
||||
new OA\Property(property: 'message', type: 'string', example: 'API Key 权限更新成功'),
|
||||
new OA\Property(property: 'data', properties: [
|
||||
new OA\Property(property: 'id', type: 'integer'),
|
||||
new OA\Property(property: 'username', type: 'string'),
|
||||
new OA\Property(property: 'api_key_enabled', type: 'boolean'),
|
||||
], type: 'object'),
|
||||
])
|
||||
),
|
||||
new OA\Response(response: 400, description: '参数错误', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 403, description: '无权限', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
new OA\Response(response: 404, description: '用户不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
|
||||
]
|
||||
)]
|
||||
#[RequestMapping(path: "{id}/api-key-enabled", methods: "PATCH")]
|
||||
#[Middleware(AuthMiddleware::class)]
|
||||
#[Middleware(PermissionMiddleware::class)]
|
||||
public function updateApiKeyEnabled(int $id): \Psr\Http\Message\ResponseInterface|array
|
||||
{
|
||||
$user = User::query()->find($id);
|
||||
|
||||
if (!$user) {
|
||||
return $this->response->json([
|
||||
'code' => 404,
|
||||
'message' => '用户不存在',
|
||||
])->withStatus(404);
|
||||
}
|
||||
|
||||
$api_key_enabled = filter_var($this->request->input('api_key_enabled'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||
if ($api_key_enabled === null) {
|
||||
return $this->response->json([
|
||||
'code' => 400,
|
||||
'message' => 'api_key_enabled 参数不能为空或格式不正确',
|
||||
])->withStatus(400);
|
||||
}
|
||||
|
||||
$user->api_key_enabled = $api_key_enabled;
|
||||
$user->save();
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => 'API Key 权限更新成功',
|
||||
'data' => [
|
||||
'id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'api_key_enabled' => $user->api_key_enabled,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user