第五部分:业务逻辑层与服务
5.1 用户服务
// app/Services/UserService.php
<?php
namespace App\Services;
use App\Models\User;
use App\Validators\UserValidator;
use App\Exceptions\ValidationException;
use App\Exceptions\BusinessException;
use Intervention\Image\ImageManager;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
class UserService
{
private Logger $logger;
private ImageManager $imageManager;
public function __construct()
{
$this->logger = new Logger('user-service');
$this->logger->pushHandler(new StreamHandler(STORAGE_PATH . '/logs/user.log', Logger::INFO));
$this->imageManager = new ImageManager(['driver' => 'gd']);
}
/**
* 注册新用户
*/
public function register(array $data): array
{
// 验证数据
$validator = new UserValidator($data);
if (!$validator->validate()) {
throw new ValidationException($validator->getErrors());
}
// 检查邮箱是否已存在
if (User::where('email', '=', $data['email'])->first()) {
throw new BusinessException('邮箱已被注册');
}
// 检查用户名是否已存在
if (User::where('username', '=', $data['username'])->first()) {
throw new BusinessException('用户名已被占用');
}
// 创建用户
$user = User::create($data);
if (!$user) {
throw new BusinessException('注册失败,请稍后重试');
}
// 记录日志
$this->logger->info('用户注册成功', [
'user_id' => $user->id,
'username' => $user->username,
'email' => $user->email,
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown'
]);
// 发送欢迎邮件(异步)
$this->sendWelcomeEmail($user);
// 生成API Token
$token = $user->generateApiToken();
return [
'user' => $user->toArray(),
'token' => $token,
'expires_in' => 86400 // 24小时
];
}
/**
* 用户登录
*/
public function login(string $login, string $password, string $ip): array
{
// 验证用户
$user = User::authenticate($login, $password);
if (!$user) {
$this->logger->warning('登录失败', [
'login' => $login,
'ip' => $ip,
'reason' => 'invalid_credentials'
]);
throw new BusinessException('用户名或密码错误');
}
// 检查用户状态
if ($user->status != 1) {
throw new BusinessException('账户已被禁用,请联系管理员');
}
// 更新登录信息
$user->updateLoginInfo($ip);
// 记录日志
$this->logger->info('用户登录成功', [
'user_id' => $user->id,
'username' => $user->username,
'ip' => $ip
]);
// 生成Token
$token = $user->generateApiToken();
return [
'user' => $user->toArray(),
'token' => $token,
'expires_in' => 86400
];
}
/**
* 获取用户列表(分页)
*/
public function getUserList(array $filters, int $page = 1, int $perPage = 15): array
{
$query = User::where('deleted_at', 'IS', null);
// 应用过滤条件
if (!empty($filters['keyword'])) {
$keyword = '%' . $filters['keyword'] . '%';
$query->where(function($q) use ($keyword) {
$q->where('username', 'LIKE', $keyword)
->orWhere('email', 'LIKE', $keyword);
});
}
if (!empty($filters['role_id'])) {
$query->where('role_id', '=', $filters['role_id']);
}
if (isset($filters['status']) && $filters['status'] !== '') {
$query->where('status', '=', (int) $filters['status']);
}
// 排序
$orderBy = $filters['order_by'] ?? 'created_at';
$orderDir = $filters['order_dir'] ?? 'DESC';
$query->orderBy($orderBy, $orderDir);
return $query->paginate($perPage, $page);
}
/**
* 更新用户信息
*/
public function updateUser(int $userId, array $data): User
{
$user = User::find($userId);
if (!$user) {
throw new BusinessException('用户不存在');
}
// 验证邮箱唯一性
if (isset($data['email']) && $data['email'] !== $user->email) {
$exists = User::where('email', '=', $data['email'])->first();
if ($exists) {
throw new BusinessException('邮箱已被其他用户使用');
}
$user->email = $data['email'];
}
// 验证用户名唯一性
if (isset($data['username']) && $data['username'] !== $user->username) {
$exists = User::where('username', '=', $data['username'])->first();
if ($exists) {
throw new BusinessException('用户名已被占用');
}
$user->username = $data['username'];
}
// 更新字段
$updatableFields = ['phone', 'avatar', 'role_id', 'status'];
foreach ($updatableFields as $field) {
if (isset($data[$field])) {
$user->$field = $data[$field];
}
}
// 更新密码
if (!empty($data['password'])) {
$user->password = HashService::make($data['password']);
}
$user->save();
$this->logger->info('用户信息已更新', [
'user_id' => $userId,
'updated_by' => $_SESSION['user_id'] ?? 'system'
]);
return $user;
}
/**
* 上传用户头像
*/
public function uploadAvatar(int $userId, array $file): string
{
// 验证文件
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new BusinessException('文件上传失败');
}
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($file['type'], $allowedTypes)) {
throw new BusinessException('只支持JPEG、PNG、GIF、WEBP格式的图片');
}
if ($file['size'] > 5 * 1024 * 1024) {
throw new BusinessException('图片大小不能超过5MB');
}
// 处理图片
$image = $this->imageManager->make($file['tmp_name']);
// 调整尺寸(300x300)
$image->fit(300, 300);
// 生成文件名
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = 'avatar_' . $userId . '_' . time() . '.' . $extension;
$path = UPLOAD_PATH . '/avatars/' . $filename;
// 保存图片
$image->save($path);
// 更新用户头像
$user = User::find($userId);
if ($user) {
// 删除旧头像
if ($user->avatar && file_exists(UPLOAD_PATH . '/avatars/' . $user->avatar)) {
unlink(UPLOAD_PATH . '/avatars/' . $user->avatar);
}
$user->avatar = $filename;
$user->save();
}
return $filename;
}
/**
* 重置密码
*/
public function resetPassword(string $email, string $token, string $newPassword): bool
{
// 验证token
$reset = PasswordReset::where('email', '=', $email)
->where('token', '=', $token)
->where('expires_at', '>', date('Y-m-d H:i:s'))
->first();
if (!$reset) {
throw new BusinessException('重置链接无效或已过期');
}
// 更新密码
$user = User::where('email', '=', $email)->first();
if (!$user) {
throw new BusinessException('用户不存在');
}
$user->password = HashService::make($newPassword);
$user->save();
// 删除重置记录
$reset->delete();
$this->logger->info('密码重置成功', [
'user_id' => $user->id,
'email' => $email
]);
return true;
}
/**
* 发送欢迎邮件
*/
private function sendWelcomeEmail(User $user): void
{
// 异步发送邮件
// 实际项目中可以使用消息队列(Redis、RabbitMQ)
// 这里简化处理
$mailService = new MailService();
$mailService->sendWelcome($user->email, $user->username);
}
}
5.2 权限服务
// app/Services/AuthService.php
<?php
namespace App\Services;
use App\Models\User;
use App\Models\Role;
class AuthService
{
private ?User $currentUser = null;
/**
* 设置当前用户
*/
public function setUser(?User $user): void
{
$this->currentUser = $user;
}
/**
* 获取当前用户
*/
public function getUser(): ?User
{
return $this->currentUser;
}
/**
* 检查是否已登录
*/
public function isLoggedIn(): bool
{
return $this->currentUser !== null;
}
/**
* 检查是否为超级管理员
*/
public function isSuperAdmin(): bool
{
if (!$this->currentUser) {
return false;
}
$role = $this->currentUser->getRole();
return $role && $role->slug === 'super_admin';
}
/**
* 检查是否有权限
*/
public function can(string $permission): bool
{
if (!$this->currentUser) {
return false;
}
// 超级管理员拥有所有权限
if ($this->isSuperAdmin()) {
return true;
}
return $this->currentUser->hasPermission($permission);
}
/**
* 检查是否有任一权限
*/
public function canAny(array $permissions): bool
{
foreach ($permissions as $permission) {
if ($this->can($permission)) {
return true;
}
}
return false;
}
/**
* 检查是否有所有权限
*/
public function canAll(array $permissions): bool
{
foreach ($permissions as $permission) {
if (!$this->can($permission)) {
return false;
}
}
return true;
}
/**
* 检查是否有角色
*/
public function hasRole(string $roleSlug): bool
{
if (!$this->currentUser) {
return false;
}
$role = $this->currentUser->getRole();
return $role && $role->slug === $roleSlug;
}
/**
* 验证API Token
*/
public function authenticateByToken(string $token): ?User
{
try {
$payload = JWTService::verify($token);
if (!$payload || empty($payload['user_id'])) {
return null;
}
$user = User::find($payload['user_id']);
// 检查用户状态
if ($user && $user->status != 1) {
return null;
}
return $user;
} catch (\Exception $e) {
return null;
}
}
}
第六部分:控制器与路由
6.1 基础控制器
// app/Controllers/Controller.php
<?php
namespace App\Controllers;
use App\Services\AuthService;
abstract class Controller
{
protected AuthService $auth;
public function __construct()
{
$this->auth = new AuthService();
// 从Token或Session获取用户
$this->authenticate();
}
protected function authenticate(): void
{
// 从Authorization头获取Token
$headers = getallheaders();
$token = $headers['Authorization'] ?? $_SESSION['api_token'] ?? null;
if ($token) {
$token = str_replace('Bearer ', '', $token);
$user = $this->auth->authenticateByToken($token);
$this->auth->setUser($user);
}
}
protected function json($data, int $statusCode = 200): void
{
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
protected function success($data = null, string $message = 'success'): void
{
$this->json([
'code' => 200,
'message' => $message,
'data' => $data,
'timestamp' => time()
]);
}
protected function error(string $message, int $code = 400, $data = null): void
{
$this->json([
'code' => $code,
'message' => $message,
'data' => $data,
'timestamp' => time()
], $code >= 400 ? $code : 400);
}
protected function validate(array $data, array $rules): array
{
$errors = [];
foreach ($rules as $field => $rule) {
$value = $data[$field] ?? null;
// 必填验证
if (strpos($rule, 'required') !== false && empty($value)) {
$errors[$field] = "{$field}字段不能为空";
continue;
}
if (!empty($value)) {
// 邮箱验证
if (strpos($rule, 'email') !== false && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
$errors[$field] = "{$field}格式不正确";
}
// 最小长度验证
if (preg_match('/min:(\d+)/', $rule, $matches)) {
if (strlen($value) < $matches[1]) {
$errors[$field] = "{$field}长度不能小于{$matches[1]}";
}
}
// 最大长度验证
if (preg_match('/max:(\d+)/', $rule, $matches)) {
if (strlen($value) > $matches[1]) {
$errors[$field] = "{$field}长度不能大于{$matches[1]}";
}
}
}
}
if (!empty($errors)) {
throw new \InvalidArgumentException(json_encode($errors));
}
return $data;
}
}
6.2 用户控制器
// app/Controllers/UserController.php
<?php
namespace App\Controllers;
use App\Services\UserService;
use App\Services\AuthService;
use App\Exceptions\BusinessException;
class UserController extends Controller
{
private UserService $userService;
public function __construct()
{
parent::__construct();
$this->userService = new UserService();
}
/**
* 用户注册
* POST /api/register
*/
public function register(): void
{
try {
$data = json_decode(file_get_contents('php://input'), true);
// 验证输入
$this->validate($data, [
'username' => 'required|min:3|max:50',
'email' => 'required|email',
'password' => 'required|min:6|max:20',
'confirm_password' => 'required'
]);
// 验证密码一致性
if ($data['password'] !== $data['confirm_password']) {
throw new BusinessException('两次输入的密码不一致');
}
$result = $this->userService->register($data);
$this->success($result, '注册成功');
} catch (BusinessException $e) {
$this->error($e->getMessage(), 400);
} catch (\InvalidArgumentException $e) {
$this->error(json_decode($e->getMessage(), true), 422);
} catch (\Exception $e) {
$this->error('服务器错误', 500);
}
}
/**
* 用户登录
* POST /api/login
*/
public function login(): void
{
try {
$data = json_decode(file_get_contents('php://input'), true);
$this->validate($data, [
'login' => 'required',
'password' => 'required'
]);
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$result = $this->userService->login($data['login'], $data['password'], $ip);
// 保存Token到Session
$_SESSION['api_token'] = $result['token'];
$_SESSION['user_id'] = $result['user']['id'];
$this->success($result, '登录成功');
} catch (BusinessException $e) {
$this->error($e->getMessage(), 401);
} catch (\Exception $e) {
$this->error('服务器错误', 500);
}
}
/**
* 获取当前用户信息
* GET /api/user/profile
*/
public function profile(): void
{
if (!$this->auth->isLoggedIn()) {
$this->error('未登录', 401);
return;
}
$user = $this->auth->getUser();
$this->success($user->toArray());
}
/**
* 获取用户列表(需要权限)
* GET /api/users
*/
public function list(): void
{
if (!$this->auth->can('user.view')) {
$this->error('无权限访问', 403);
return;
}
$page = (int) ($_GET['page'] ?? 1);
$perPage = (int) ($_GET['per_page'] ?? 15);
$filters = [
'keyword' => $_GET['keyword'] ?? '',
'role_id' => $_GET['role_id'] ?? '',
'status' => $_GET['status'] ?? '',
'order_by' => $_GET['order_by'] ?? 'created_at',
'order_dir' => $_GET['order_dir'] ?? 'DESC'
];
$result = $this->userService->getUserList($filters, $page, $perPage);
$this->success($result);
}
/**
* 获取用户详情
* GET /api/users/{id}
*/
public function show(int $id): void
{
if (!$this->auth->can('user.view')) {
$this->error('无权限访问', 403);
return;
}
$user = User::find($id);
if (!$user) {
$this->error('用户不存在', 404);
return;
}
$this->success($user->toArray());
}
/**
* 创建用户(管理员)
* POST /api/users
*/
public function create(): void
{
if (!$this->auth->can('user.create')) {
$this->error('无权限操作', 403);
return;
}
try {
$data = json_decode(file_get_contents('php://input'), true);
$this->validate($data, [
'username' => 'required|min:3|max:50',
'email' => 'required|email',
'password' => 'required|min:6',
'role_id' => 'required|integer'
]);
$user = User::create($data);
$this->success($user->toArray(), '创建成功', 201);
} catch (BusinessException $e) {
$this->error($e->getMessage(), 400);
} catch (\Exception $e) {
$this->error('创建失败', 500);
}
}
/**
* 更新用户
* PUT /api/users/{id}
*/
public function update(int $id): void
{
if (!$this->auth->can('user.edit')) {
$this->error('无权限操作', 403);
return;
}
try {
$data = json_decode(file_get_contents('php://input'), true);
$user = $this->userService->updateUser($id, $data);
$this->success($user->toArray(), '更新成功');
} catch (BusinessException $e) {
$this->error($e->getMessage(), 400);
} catch (\Exception $e) {
$this->error('更新失败', 500);
}
}
/**
* 删除用户
* DELETE /api/users/{id}
*/
public function delete(int $id): void
{
if (!$this->auth->can('user.delete')) {
$this->error('无权限操作', 403);
return;
}
$user = User::find($id);
if (!$user) {
$this->error('用户不存在', 404);
return;
}
// 不能删除自己
if ($this->auth->getUser() && $this->auth->getUser()->id == $id) {
$this->error('不能删除自己的账号', 400);
return;
}
$user->softDelete();
$this->success(null, '删除成功');
}
/**
* 上传头像
* POST /api/user/avatar
*/
public function uploadAvatar(): void
{
if (!$this->auth->isLoggedIn()) {
$this->error('未登录', 401);
return;
}
try {
if (empty($_FILES['avatar'])) {
throw new BusinessException('请选择要上传的图片');
}
$filename = $this->userService->uploadAvatar($this->auth->getUser()->id, $_FILES['avatar']);
$this->success(['avatar' => $filename], '上传成功');
} catch (BusinessException $e) {
$this->error($e->getMessage(), 400);
} catch (\Exception $e) {
$this->error('上传失败', 500);
}
}
/**
* 退出登录
* POST /api/logout
*/
public function logout(): void
{
unset($_SESSION['api_token']);
unset($_SESSION['user_id']);
session_destroy();
$this->success(null, '已退出登录');
}
}
6.3 路由配置
// routes/web.php
<?php
use App\Controllers\UserController;
use App\Controllers\RoleController;
use App\Middleware\AuthMiddleware;
use App\Middleware\PermissionMiddleware;
// 简单路由实现
class Router
{
private array $routes = [];
public function get(string $path, $handler): void
{
$this->addRoute('GET', $path, $handler);
}
public function post(string $path, $handler): void
{
$this->addRoute('POST', $path, $handler);
}
public function put(string $path, $handler): void
{
$this->addRoute('PUT', $path, $handler);
}
public function delete(string $path, $handler): void
{
$this->addRoute('DELETE', $path, $handler);
}
private function addRoute(string $method, string $path, $handler): void
{
$this->routes[] = [
'method' => $method,
'path' => $path,
'handler' => $handler,
'middleware' => []
];
}
public function middleware(string $middleware): self
{
$lastRoute = &$this->routes[count($this->routes) - 1];
$lastRoute['middleware'][] = $middleware;
return $this;
}
public function group(array $options, callable $callback): void
{
// 实现路由分组
}
public function dispatch(string $method, string $uri): void
{
// 移除查询参数
$uri = parse_url($uri, PHP_URL_PATH);
foreach ($this->routes as $route) {
if ($route['method'] !== $method) {
continue;
}
// 匹配路径并提取参数
$pattern = preg_replace('/\{(\w+)\}/', '(?P<$1>[^/]+)', $route['path']);
$pattern = '#^' . $pattern . '$#';
if (preg_match($pattern, $uri, $matches)) {
// 提取参数
$params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
// 执行中间件
foreach ($route['middleware'] as $middlewareClass) {
$middleware = new $middlewareClass();
$middleware->handle();
}
// 执行控制器
if (is_array($route['handler'])) {
$controller = new $route['handler'][0]();
$method = $route['handler'][1];
call_user_func_array([$controller, $method], $params);
} else {
call_user_func_array($route['handler'], $params);
}
return;
}
}
// 404 Not Found
http_response_code(404);
echo json_encode(['code' => 404, 'message' => 'Not Found']);
}
}
$router = new Router();
// 公开路由
$router->post('/api/register', [UserController::class, 'register']);
$router->post('/api/login', [UserController::class, 'login']);
// 需要认证的路由
$router->get('/api/user/profile', [UserController::class, 'profile'])->middleware(AuthMiddleware::class);
$router->post('/api/user/avatar', [UserController::class, 'uploadAvatar'])->middleware(AuthMiddleware::class);
$router->post('/api/logout', [UserController::class, 'logout'])->middleware(AuthMiddleware::class);
// 需要权限的路由
$router->get('/api/users', [UserController::class, 'list'])
->middleware(AuthMiddleware::class)
->middleware(PermissionMiddleware::class);
$router->get('/api/users/{id}', [UserController::class, 'show'])
->middleware(AuthMiddleware::class)
->middleware(PermissionMiddleware::class);
$router->post('/api/users', [UserController::class, 'create'])
->middleware(AuthMiddleware::class)
->middleware(PermissionMiddleware::class);
$router->put('/api/users/{id}', [UserController::class, 'update'])
->middleware(AuthMiddleware::class)
->middleware(PermissionMiddleware::class);
$router->delete('/api/users/{id}', [UserController::class, 'delete'])
->middleware(AuthMiddleware::class)
->middleware(PermissionMiddleware::class);