2025-11-10 10:45:43 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
2026-03-06 15:29:04 +08:00
|
|
|
|
namespace App\Controller\Api\V1;
|
2025-11-10 10:45:43 +08:00
|
|
|
|
|
2026-03-06 15:29:04 +08:00
|
|
|
|
use App\Controller\AbstractController;
|
2026-03-09 10:13:08 +08:00
|
|
|
|
use App\Middleware\AuthMiddleware;
|
2025-11-10 10:45:43 +08:00
|
|
|
|
use App\Model\User;
|
2026-03-17 15:54:23 +08:00
|
|
|
|
use App\Service\OperationLogService;
|
2026-03-18 08:49:10 +08:00
|
|
|
|
use App\Utils\RequestHelper;
|
2025-11-10 10:45:43 +08:00
|
|
|
|
use Carbon\Carbon;
|
2025-11-10 15:27:00 +08:00
|
|
|
|
use Hyperf\HttpServer\Annotation\Controller;
|
|
|
|
|
|
use Hyperf\HttpServer\Annotation\Middleware;
|
2026-03-09 10:13:08 +08:00
|
|
|
|
use Hyperf\HttpServer\Annotation\RequestMapping;
|
|
|
|
|
|
use Hyperf\HttpServer\Contract\RequestInterface;
|
|
|
|
|
|
use Hyperf\HttpServer\Contract\ResponseInterface;
|
|
|
|
|
|
use OpenApi\Attributes as OA;
|
|
|
|
|
|
use Qbhy\HyperfAuth\AuthManager;
|
2025-11-10 15:27:00 +08:00
|
|
|
|
|
2026-03-09 10:13:08 +08:00
|
|
|
|
#[OA\Tag(name: 'Auth', description: '认证与个人信息')]
|
|
|
|
|
|
#[Controller(prefix: "/api/v1")]
|
2025-11-10 10:45:43 +08:00
|
|
|
|
class AuthController extends AbstractController
|
|
|
|
|
|
{
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 用户注册
|
|
|
|
|
|
*/
|
2026-03-09 10:13:08 +08:00
|
|
|
|
#[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
|
2025-11-10 10:45:43 +08:00
|
|
|
|
{
|
|
|
|
|
|
$username = $request->input('username');
|
|
|
|
|
|
$password = $request->input('password');
|
|
|
|
|
|
$email = $request->input('email');
|
|
|
|
|
|
|
2026-03-09 10:13:08 +08:00
|
|
|
|
// 校验 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 唯一性检查
|
2025-11-10 10:45:43 +08:00
|
|
|
|
if (User::query()->where('username', $username)->exists()) {
|
|
|
|
|
|
return $response->json([
|
|
|
|
|
|
'code' => 400,
|
|
|
|
|
|
'message' => '用户名已存在',
|
2026-03-09 10:13:08 +08:00
|
|
|
|
])->withStatus(400);
|
2025-11-10 10:45:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (User::query()->where('email', $email)->exists()) {
|
|
|
|
|
|
return $response->json([
|
|
|
|
|
|
'code' => 400,
|
|
|
|
|
|
'message' => '邮箱已被注册',
|
2026-03-09 10:13:08 +08:00
|
|
|
|
])->withStatus(400);
|
2025-11-10 10:45:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建用户
|
|
|
|
|
|
$user = User::create([
|
|
|
|
|
|
'username' => $username,
|
2026-03-09 10:13:08 +08:00
|
|
|
|
'password' => $password,
|
2025-11-10 10:45:43 +08:00
|
|
|
|
'email' => $email,
|
|
|
|
|
|
'status' => 1,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2026-03-09 10:13:08 +08:00
|
|
|
|
return [
|
2025-11-10 10:45:43 +08:00
|
|
|
|
'code' => 0,
|
|
|
|
|
|
'message' => '注册成功',
|
|
|
|
|
|
'data' => [
|
|
|
|
|
|
'id' => $user->id,
|
|
|
|
|
|
'username' => $user->username,
|
|
|
|
|
|
'email' => $user->email,
|
|
|
|
|
|
],
|
2026-03-09 10:13:08 +08:00
|
|
|
|
];
|
2025-11-10 10:45:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 用户登录
|
|
|
|
|
|
*/
|
2026-03-09 10:13:08 +08:00
|
|
|
|
#[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'),
|
2026-03-19 09:13:20 +08:00
|
|
|
|
new OA\Property(property: 'role', type: 'string', example: 'administrator'),
|
2026-03-09 10:13:08 +08:00
|
|
|
|
], 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
|
2025-11-10 10:45:43 +08:00
|
|
|
|
{
|
|
|
|
|
|
$username = $request->input('username');
|
|
|
|
|
|
$password = $request->input('password');
|
|
|
|
|
|
|
2026-03-09 10:13:08 +08:00
|
|
|
|
// 校验参数
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-10 10:45:43 +08:00
|
|
|
|
// 查找用户
|
2026-03-09 10:13:08 +08:00
|
|
|
|
$user = User::query()->where('username', trim($username))->first();
|
2025-11-10 10:45:43 +08:00
|
|
|
|
|
|
|
|
|
|
if (!$user) {
|
|
|
|
|
|
return $response->json([
|
|
|
|
|
|
'code' => 401,
|
|
|
|
|
|
'message' => '用户名或密码错误',
|
2026-03-09 10:13:08 +08:00
|
|
|
|
])->withStatus(401);
|
2025-11-10 10:45:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证密码
|
|
|
|
|
|
if (!$user->verifyPassword($password)) {
|
|
|
|
|
|
return $response->json([
|
|
|
|
|
|
'code' => 401,
|
|
|
|
|
|
'message' => '用户名或密码错误',
|
2026-03-09 10:13:08 +08:00
|
|
|
|
])->withStatus(401);
|
2025-11-10 10:45:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查用户状态
|
|
|
|
|
|
if ($user->status !== 1) {
|
|
|
|
|
|
return $response->json([
|
|
|
|
|
|
'code' => 403,
|
|
|
|
|
|
'message' => '账号已被禁用',
|
2026-03-09 10:13:08 +08:00
|
|
|
|
])->withStatus(403);
|
2025-11-10 10:45:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 15:11:27 +08:00
|
|
|
|
$user->load('role');
|
|
|
|
|
|
|
|
|
|
|
|
// 生成 Access Token(注入 role 到 JWT payload,前端从 JWT 解码 role 作为可信权限来源)
|
|
|
|
|
|
$token = $auth->guard('jwt')->login($user, ['role' => $user->role?->name ?? 'accessor']);
|
2025-11-10 10:45:43 +08:00
|
|
|
|
|
|
|
|
|
|
// 生成 Refresh Token
|
|
|
|
|
|
$refreshToken = bin2hex(random_bytes(32));
|
|
|
|
|
|
$user->refresh_token = $refreshToken;
|
|
|
|
|
|
$user->refresh_token_expires_at = Carbon::now()->addDays(30);
|
|
|
|
|
|
$user->save();
|
|
|
|
|
|
|
2026-03-17 15:54:23 +08:00
|
|
|
|
OperationLogService::log(
|
|
|
|
|
|
user_id: $user->id,
|
|
|
|
|
|
action: 'auth.login',
|
|
|
|
|
|
target_type: 'user',
|
|
|
|
|
|
target_id: $user->id,
|
|
|
|
|
|
description: "用户 {$user->username} 登录",
|
2026-03-18 08:49:10 +08:00
|
|
|
|
ip: RequestHelper::getClientIp($this->request),
|
2026-03-17 15:54:23 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
2026-03-09 10:13:08 +08:00
|
|
|
|
return [
|
2025-11-10 10:45:43 +08:00
|
|
|
|
'code' => 0,
|
|
|
|
|
|
'message' => '登录成功',
|
|
|
|
|
|
'data' => [
|
|
|
|
|
|
'access_token' => $token,
|
|
|
|
|
|
'refresh_token' => $refreshToken,
|
|
|
|
|
|
'token_type' => 'Bearer',
|
2026-03-09 10:13:08 +08:00
|
|
|
|
'expires_in' => 7200,
|
2025-11-10 10:45:43 +08:00
|
|
|
|
'user' => [
|
|
|
|
|
|
'id' => $user->id,
|
|
|
|
|
|
'username' => $user->username,
|
|
|
|
|
|
'email' => $user->email,
|
2026-03-19 09:13:20 +08:00
|
|
|
|
'role' => $user->role?->name ?? 'accessor',
|
2025-11-10 10:45:43 +08:00
|
|
|
|
],
|
|
|
|
|
|
],
|
2026-03-09 10:13:08 +08:00
|
|
|
|
];
|
2025-11-10 10:45:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 刷新 Access Token
|
|
|
|
|
|
*/
|
2026-03-09 10:13:08 +08:00
|
|
|
|
#[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")]
|
2025-11-10 15:27:00 +08:00
|
|
|
|
#[Middleware(AuthMiddleware::class)]
|
2026-03-09 10:13:08 +08:00
|
|
|
|
public function refresh(RequestInterface $request, ResponseInterface $response, AuthManager $auth): \Psr\Http\Message\ResponseInterface|array
|
2025-11-10 10:45:43 +08:00
|
|
|
|
{
|
|
|
|
|
|
$refreshToken = $request->input('refresh_token');
|
|
|
|
|
|
|
2026-03-09 10:13:08 +08:00
|
|
|
|
if (!is_string($refreshToken) || $refreshToken === '') {
|
2025-11-10 10:45:43 +08:00
|
|
|
|
return $response->json([
|
|
|
|
|
|
'code' => 400,
|
|
|
|
|
|
'message' => '缺少 refresh_token 参数',
|
2026-03-09 10:13:08 +08:00
|
|
|
|
])->withStatus(400);
|
2025-11-10 10:45:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查找用户
|
|
|
|
|
|
$user = User::query()->where('refresh_token', $refreshToken)->first();
|
|
|
|
|
|
|
|
|
|
|
|
if (!$user) {
|
|
|
|
|
|
return $response->json([
|
|
|
|
|
|
'code' => 401,
|
|
|
|
|
|
'message' => '无效的 refresh_token',
|
2026-03-09 10:13:08 +08:00
|
|
|
|
])->withStatus(401);
|
2025-11-10 10:45:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证 refresh token 是否过期
|
|
|
|
|
|
if (!$user->isRefreshTokenValid()) {
|
|
|
|
|
|
return $response->json([
|
|
|
|
|
|
'code' => 401,
|
|
|
|
|
|
'message' => 'refresh_token 已过期,请重新登录',
|
2026-03-09 10:13:08 +08:00
|
|
|
|
])->withStatus(401);
|
2025-11-10 10:45:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查用户状态
|
|
|
|
|
|
if ($user->status !== 1) {
|
|
|
|
|
|
return $response->json([
|
|
|
|
|
|
'code' => 403,
|
|
|
|
|
|
'message' => '账号已被禁用',
|
2026-03-09 10:13:08 +08:00
|
|
|
|
])->withStatus(403);
|
2025-11-10 10:45:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 15:11:27 +08:00
|
|
|
|
// 生成新的 Access Token(注入 role 到 JWT payload)
|
|
|
|
|
|
$user->load('role');
|
|
|
|
|
|
$token = $auth->guard('jwt')->login($user, ['role' => $user->role?->name ?? 'accessor']);
|
2025-11-10 10:45:43 +08:00
|
|
|
|
|
2026-03-09 10:13:08 +08:00
|
|
|
|
// 生成新的 Refresh Token(轮换以提升安全性)
|
2025-11-10 10:45:43 +08:00
|
|
|
|
$newRefreshToken = bin2hex(random_bytes(32));
|
|
|
|
|
|
$user->refresh_token = $newRefreshToken;
|
|
|
|
|
|
$user->refresh_token_expires_at = Carbon::now()->addDays(30);
|
|
|
|
|
|
$user->save();
|
|
|
|
|
|
|
2026-03-09 10:13:08 +08:00
|
|
|
|
return [
|
2025-11-10 10:45:43 +08:00
|
|
|
|
'code' => 0,
|
|
|
|
|
|
'message' => 'Token 刷新成功',
|
|
|
|
|
|
'data' => [
|
|
|
|
|
|
'access_token' => $token,
|
|
|
|
|
|
'refresh_token' => $newRefreshToken,
|
|
|
|
|
|
'token_type' => 'Bearer',
|
|
|
|
|
|
'expires_in' => 7200,
|
|
|
|
|
|
],
|
2026-03-09 10:13:08 +08:00
|
|
|
|
];
|
2025-11-10 10:45:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取当前用户信息
|
|
|
|
|
|
*/
|
2026-03-09 10:13:08 +08:00
|
|
|
|
#[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),
|
2026-04-02 10:40:47 +08:00
|
|
|
|
new OA\Property(property: 'api_key_enabled', type: 'boolean', example: true, description: 'API Key 功能是否启用'),
|
2026-03-09 10:13:08 +08:00
|
|
|
|
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")]
|
2025-11-10 15:27:00 +08:00
|
|
|
|
#[Middleware(AuthMiddleware::class)]
|
2026-03-09 10:13:08 +08:00
|
|
|
|
public function me(AuthManager $auth, ResponseInterface $response): \Psr\Http\Message\ResponseInterface|array
|
2025-11-10 10:45:43 +08:00
|
|
|
|
{
|
2026-04-16 13:16:43 +08:00
|
|
|
|
$user = $this->getAuthUser();
|
2025-11-10 10:45:43 +08:00
|
|
|
|
|
|
|
|
|
|
if (!$user) {
|
|
|
|
|
|
return $response->json([
|
|
|
|
|
|
'code' => 401,
|
|
|
|
|
|
'message' => '未授权',
|
2026-03-09 10:13:08 +08:00
|
|
|
|
])->withStatus(401);
|
2025-11-10 10:45:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 08:45:32 +08:00
|
|
|
|
$user->load('role');
|
|
|
|
|
|
|
2026-03-09 10:13:08 +08:00
|
|
|
|
return [
|
2025-11-10 10:45:43 +08:00
|
|
|
|
'code' => 0,
|
|
|
|
|
|
'message' => '获取成功',
|
|
|
|
|
|
'data' => [
|
|
|
|
|
|
'id' => $user->id,
|
|
|
|
|
|
'username' => $user->username,
|
|
|
|
|
|
'email' => $user->email,
|
2026-03-19 08:45:32 +08:00
|
|
|
|
'role' => $user->role?->name ?? 'accessor',
|
2025-11-10 10:45:43 +08:00
|
|
|
|
'status' => $user->status,
|
2026-04-02 10:40:47 +08:00
|
|
|
|
'api_key_enabled' => $user->api_key_enabled,
|
2025-11-10 10:45:43 +08:00
|
|
|
|
'ext' => $user->ext,
|
|
|
|
|
|
'created_at' => $user->created_at->toDateTimeString(),
|
|
|
|
|
|
],
|
2026-03-09 10:13:08 +08:00
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 更新个人信息
|
|
|
|
|
|
*
|
|
|
|
|
|
* 当前用户更新自己的 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();
|
|
|
|
|
|
|
2026-03-17 15:54:23 +08:00
|
|
|
|
OperationLogService::log(
|
|
|
|
|
|
user_id: $user->id,
|
|
|
|
|
|
action: 'user.password_change',
|
|
|
|
|
|
target_type: 'user',
|
|
|
|
|
|
target_id: $user->id,
|
|
|
|
|
|
description: "用户 {$user->username} 修改密码",
|
2026-03-18 08:49:10 +08:00
|
|
|
|
ip: RequestHelper::getClientIp($this->request),
|
2026-03-17 15:54:23 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
2026-03-09 10:13:08 +08:00
|
|
|
|
return [
|
|
|
|
|
|
'code' => 0,
|
|
|
|
|
|
'message' => '密码修改成功,请重新登录',
|
|
|
|
|
|
];
|
2025-11-10 10:45:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 退出登录
|
|
|
|
|
|
*/
|
2026-03-09 10:13:08 +08:00
|
|
|
|
#[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")]
|
2025-11-10 15:27:00 +08:00
|
|
|
|
#[Middleware(AuthMiddleware::class)]
|
2026-03-09 10:13:08 +08:00
|
|
|
|
public function logout(AuthManager $auth): array
|
2025-11-10 10:45:43 +08:00
|
|
|
|
{
|
|
|
|
|
|
$user = $auth->guard('jwt')->user();
|
|
|
|
|
|
|
|
|
|
|
|
if ($user instanceof User) {
|
2026-03-17 15:54:23 +08:00
|
|
|
|
OperationLogService::log(
|
|
|
|
|
|
user_id: $user->id,
|
|
|
|
|
|
action: 'auth.logout',
|
|
|
|
|
|
target_type: 'user',
|
|
|
|
|
|
target_id: $user->id,
|
|
|
|
|
|
description: "用户 {$user->username} 退出登录",
|
2026-03-18 08:49:10 +08:00
|
|
|
|
ip: RequestHelper::getClientIp($this->request),
|
2026-03-17 15:54:23 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
2025-11-10 10:45:43 +08:00
|
|
|
|
// 清除 refresh token
|
|
|
|
|
|
$user->refresh_token = null;
|
|
|
|
|
|
$user->refresh_token_expires_at = null;
|
|
|
|
|
|
$user->save();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 注销当前 token
|
|
|
|
|
|
$auth->guard('jwt')->logout();
|
|
|
|
|
|
|
2026-03-09 10:13:08 +08:00
|
|
|
|
return [
|
2025-11-10 10:45:43 +08:00
|
|
|
|
'code' => 0,
|
|
|
|
|
|
'message' => '退出成功',
|
2026-03-09 10:13:08 +08:00
|
|
|
|
];
|
2025-11-10 10:45:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|