1. 调整异常处理类

2. 统一职责分工
3. 清除多余代码
This commit is contained in:
技术老胡
2026-02-24 13:37:35 +08:00
parent d29751cce8
commit d5a134d3a8
59 changed files with 3370 additions and 646 deletions

22
app/command/Test.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
namespace app\command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand('test', 'test')]
class Test extends Command
{
protected function configure(): void
{
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('<info>Hello</info> <comment>' . $this->getName() . '</comment>');
return self::SUCCESS;
}
}

View File

@@ -24,7 +24,7 @@ class BaseController
{
return json([
'code' => $code,
'message' => $message,
'msg' => $message,
'data' => $data,
]);
}
@@ -36,7 +36,7 @@ class BaseController
{
return json([
'code' => $code,
'message' => $message,
'msg' => $message,
'data' => $data,
]);
}
@@ -59,5 +59,3 @@ class BaseController
return (int) ($request->userId ?? 0);
}
}

View File

@@ -1,22 +0,0 @@
<?php
namespace app\common\constants;
/**
* 通用启用/禁用状态
* 多个表 status 字段复用users.status, departments.status, roles.status, cron_jobs.status 等
*/
class CommonStatus
{
/**
* 禁用 / 停用
*/
public const DISABLED = 0;
/**
* 启用
*/
public const ENABLED = 1;
}

View File

@@ -1,32 +0,0 @@
<?php
namespace app\common\constants;
/**
* 数据字典编码常量
* 对应表dict_groups.code
*/
class DictCode
{
/**
* 性别字典
*/
public const GENDER = 'gender';
/**
* 通用启用/禁用状态字典
*/
public const STATUS = 'status';
/**
* 岗位字典
*/
public const POST = 'post';
/**
* 任务状态字典
*/
public const TASK_STATUS = 'taskStatus';
}

View File

@@ -1,22 +0,0 @@
<?php
namespace app\common\constants;
/**
* 权限标识常量
* 对应表menus.permission 以及前端 permissionData.meta.permission
*/
class Permission
{
// 系统按钮权限(示例)
public const SYS_BTN_ADD = 'sys:btn:add';
public const SYS_BTN_EDIT = 'sys:btn:edit';
public const SYS_BTN_DELETE = 'sys:btn:delete';
// 通用按钮权限(示例)
public const COMMON_BTN_ADD = 'common:btn:add';
public const COMMON_BTN_EDIT = 'common:btn:edit';
public const COMMON_BTN_DELETE = 'common:btn:delete';
}

View File

@@ -1,22 +0,0 @@
<?php
namespace app\common\constants;
/**
* 角色编码常量
* 对应表roles.code
*/
class RoleCode
{
/**
* 超级管理员
*/
public const ADMIN = 'admin';
/**
* 普通员工
*/
public const COMMON = 'common';
}

View File

@@ -1,16 +0,0 @@
<?php
namespace app\common\enums;
/**
* 定时任务日志状态
* 对应表cron_logs.status
* 1 成功 0 失败
*/
class CronLogStatus
{
public const FAIL = 0;
public const SUCCESS = 1;
}

View File

@@ -1,23 +0,0 @@
<?php
namespace app\common\enums;
/**
* 定时任务执行策略misfire_policy
* 对应表cron_jobs.misfire_policy
* 1 循环执行 2 执行一次
*/
class CronMisfirePolicy
{
/**
* 循环执行
*/
public const LOOP = 1;
/**
* 只执行一次
*/
public const RUN_ONCE = 2;
}

View File

@@ -1,16 +0,0 @@
<?php
namespace app\common\enums;
/**
* 定时任务类型
* 对应表cron_jobs.task_type
* 0 cron 表达式 1 时间间隔(秒)
*/
class CronTaskType
{
public const CRON_EXPRESSION = 0;
public const INTERVAL_SECOND = 1;
}

View File

@@ -1,27 +0,0 @@
<?php
namespace app\common\enums;
/**
* 用户性别枚举
* 对应表users.sex 以及 gender 字典
*/
class UserSex
{
/**
* 女
*/
public const FEMALE = 0;
/**
* 男
*/
public const MALE = 1;
/**
* 未知/其它
*/
public const UNKNOWN = 2;
}

View File

@@ -0,0 +1,28 @@
<?php
namespace app\events;
use app\repositories\SystemConfigRepository;
/**
* 系统配置相关事件处理
*
* 负责在配置更新后重新从数据库加载缓存
*/
class SystemConfig
{
/**
* 重新加载系统配置缓存
*
* @param mixed $data 事件数据(此处用不到)
* @return void
*/
public function reload($data = null): void
{
// 通过仓储重新加载缓存
$repository = new SystemConfigRepository();
$repository->reloadCache();
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace app\exceptions;
use Webman\Exception\BusinessException;
/**
* 请求参数错误异常
* 用于请求参数格式错误、验证码错误等情况
*
* 示例:
* throw new BadRequestException('验证码错误或已失效');
* throw new BadRequestException('请求参数格式错误');
*/
class BadRequestException extends BusinessException
{
public function __construct(string $message = '请求参数错误', int $bizCode = 400, array $data = [])
{
parent::__construct($message, $bizCode);
if (!empty($data)) {
$this->data($data);
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace app\exceptions;
use Webman\Exception\BusinessException;
/**
* 禁止访问异常
* 用于无权限、账号被禁用等情况
*
* 示例:
* throw new ForbiddenException('账号已被禁用');
* throw new ForbiddenException('无权限访问该资源');
*/
class ForbiddenException extends BusinessException
{
public function __construct(string $message = '禁止访问', int $bizCode = 403, array $data = [])
{
parent::__construct($message, $bizCode);
if (!empty($data)) {
$this->data($data);
}
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace app\exceptions;
use Webman\Exception\BusinessException;
/**
* 系统内部错误异常
* 用于配置文件错误、系统错误等不可预期的错误
*
* 示例:
* throw new InternalServerException('字典配置文件不存在');
* throw new InternalServerException('配置文件格式错误:' . json_last_error_msg());
* throw new InternalServerException('保存失败');
*/
class InternalServerException extends BusinessException
{
public function __construct(string $message = '系统内部错误', int $bizCode = 500, array $data = [])
{
parent::__construct($message, $bizCode);
if (!empty($data)) {
$this->data($data);
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace app\exceptions;
use Webman\Exception\BusinessException;
/**
* 资源不存在异常
* 用于资源未找到的情况
*
* 示例:
* throw new NotFoundException('用户不存在');
* throw new NotFoundException('未找到指定的字典:' . $code);
*/
class NotFoundException extends BusinessException
{
public function __construct(string $message = '资源不存在', int $bizCode = 404, array $data = [])
{
parent::__construct($message, $bizCode);
if (!empty($data)) {
$this->data($data);
}
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace app\exceptions;
use Webman\Exception\BusinessException;
/**
* 未授权异常
* 用于认证失败、token无效等情况
*
* 示例:
* throw new UnauthorizedException('账号或密码错误');
* throw new UnauthorizedException('认证令牌已过期');
* throw new UnauthorizedException('认证令牌无效');
*/
class UnauthorizedException extends BusinessException
{
public function __construct(string $message = '未授权', int $bizCode = 401, array $data = [])
{
parent::__construct($message, $bizCode);
if (!empty($data)) {
$this->data($data);
}
}
}

View File

@@ -3,8 +3,6 @@
namespace app\exceptions;
use Webman\Exception\BusinessException;
use Webman\Http\Request;
use Webman\Http\Response;
/**
* 参数校验异常
@@ -19,6 +17,9 @@ class ValidationException extends BusinessException
{
public function __construct(string $message = '参数校验失败', int $bizCode = 422, array $data = [])
{
parent::__construct($message, $bizCode, $data);
parent::__construct($message, $bizCode);
if (!empty($data)) {
$this->data($data);
}
}
}

View File

@@ -27,12 +27,8 @@ class AuthController extends BaseController
*/
public function captcha(Request $request)
{
try {
$data = $this->captchaService->generate();
return $this->success($data);
} catch (\Throwable $e) {
return $this->fail('验证码生成失败:' . $e->getMessage(), 500);
}
$data = $this->captchaService->generate();
return $this->success($data);
}
/**
@@ -52,14 +48,8 @@ class AuthController extends BaseController
return $this->fail('请填写完整登录信息', 400);
}
try {
$data = $this->authService->login($username, $password, $verifyCode, $captchaId);
return $this->success($data);
} catch (\RuntimeException $e) {
return $this->fail($e->getMessage(), $e->getCode() ?: 500);
} catch (\Throwable $e) {
return $this->fail('登录失败:' . $e->getMessage(), 500);
}
$data = $this->authService->login($username, $password, $verifyCode, $captchaId);
return $this->success($data);
}
}

View File

@@ -3,6 +3,7 @@
namespace app\http\admin\controller;
use app\common\base\BaseController;
use app\services\MenuService;
use support\Request;
/**
@@ -10,44 +11,15 @@ use support\Request;
*/
class MenuController extends BaseController
{
public function __construct(
protected MenuService $menuService
) {
}
public function getRouters()
{
// 获取菜单数据并转换为树形结构
$routers = $this->buildMenuTree($this->getSystemMenu());
$routers = $this->menuService->getRouters();
return $this->success($routers);
}
/**
* 获取系统菜单数据
* 从配置文件读取
*/
private function getSystemMenu(): array
{
return config('menu', []);
}
/**
* 构建菜单树形结构
*/
private function buildMenuTree(array $menus, string $parentId = '0'): array
{
$tree = [];
foreach ($menus as $menu) {
if (($menu['parentId'] ?? '0') === $parentId) {
$children = $this->buildMenuTree($menus, $menu['id']);
$menu['children'] = !empty($children) ? $children : null;
$tree[] = $menu;
}
}
// 按 sort 排序
usort($tree, function ($a, $b) {
return ($a['meta']['sort'] ?? 0) <=> ($b['meta']['sort'] ?? 0);
});
return $tree;
}
}

View File

@@ -3,6 +3,7 @@
namespace app\http\admin\controller;
use app\common\base\BaseController;
use app\services\SystemSettingService;
use support\Request;
/**
@@ -10,6 +11,10 @@ use support\Request;
*/
class SystemController extends BaseController
{
public function __construct(
protected SystemSettingService $settingService
) {
}
/**
* GET /system/getDict
* GET /system/getDict/{code}
@@ -24,23 +29,50 @@ class SystemController extends BaseController
*/
public function getDict(Request $request, string $code = '')
{
// 获取所有字典数据
$allDicts = config('dict', []);
// 如果指定了 code则只返回对应的字典
if (!empty($code)) {
// 将数组转换为以 code 为键的关联数组,便于快速查找
$dictsByCode = array_column($allDicts, null, 'code');
$dict = $dictsByCode[$code] ?? null;
if ($dict === null) {
return $this->fail('未找到指定的字典:' . $code, 404);
}
return $this->success($dict);
$data = $this->settingService->getDict($code);
return $this->success($data);
}
/**
* GET /system/base-config/tabs
*
* 获取所有Tab配置
* 由 SystemSettingService 负责读取配置和缓存
*/
public function getTabsConfig()
{
$tabs = $this->settingService->getTabs();
return $this->success($tabs);
}
/**
* GET /system/base-config/form/{tabKey}
*
* 获取指定Tab的表单配置
* 从 SystemSettingService 获取合并后的配置
*/
public function getFormConfig(Request $request, string $tabKey)
{
$formConfig = $this->settingService->getFormConfig($tabKey);
return $this->success($formConfig);
}
/**
* POST /system/base-config/submit/{tabKey}
*
* 提交表单数据
* 接收表单数据直接使用字段名fieldName作为 config_key 保存到数据库
*/
public function submitConfig(Request $request, string $tabKey)
{
$formData = $request->post();
if (empty($formData)) {
return $this->fail('提交数据不能为空', 400);
}
// 返回所有字典
return $this->success($allDicts);
$this->settingService->saveFormConfig($tabKey, $formData);
return $this->success(null, '保存成功');
}
}

View File

@@ -33,12 +33,8 @@ class UserController extends BaseController
return $this->fail('未获取到用户信息,请先登录', 401);
}
try {
$data = $this->userService->getUserInfoById($userId);
return $this->success($data);
} catch (\RuntimeException $e) {
return $this->fail($e->getMessage(), $e->getCode() ?: 500);
}
$data = $this->userService->getUserInfoById($userId);
return $this->success($data);
}
}

View File

@@ -3,9 +3,10 @@
namespace app\http\admin\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
use Webman\Http\Response;
use app\common\utils\JwtUtil;
use app\exceptions\UnauthorizedException;
/**
* JWT 认证中间件
@@ -25,7 +26,7 @@ class AuthMiddleware implements MiddlewareInterface
// 从请求头中获取 token
$auth = $request->header('Authorization', '');
if (!$auth) {
return $this->unauthorized('缺少认证令牌');
throw new UnauthorizedException('缺少认证令牌');
}
// 兼容 "Bearer xxx" 或直接 "xxx"
@@ -36,7 +37,7 @@ class AuthMiddleware implements MiddlewareInterface
}
if (!$token) {
return $this->unauthorized('认证令牌格式错误');
throw new UnauthorizedException('认证令牌格式错误');
}
try {
@@ -44,7 +45,7 @@ class AuthMiddleware implements MiddlewareInterface
$payload = JwtUtil::parseToken($token);
if (empty($payload) || !isset($payload['user_id'])) {
return $this->unauthorized('认证令牌无效');
throw new UnauthorizedException('认证令牌无效');
}
// 将用户信息存储到请求对象中,供控制器使用
@@ -53,29 +54,20 @@ class AuthMiddleware implements MiddlewareInterface
// 继续处理请求
return $handler($request);
} catch (UnauthorizedException $e) {
// 重新抛出业务异常,让框架处理
throw $e;
} catch (\Throwable $e) {
// 根据异常类型返回不同的错误信息
$message = $e->getMessage();
if (str_contains($message, 'expired') || str_contains($message, 'Expired')) {
return $this->unauthorized('认证令牌已过期', 401);
throw new UnauthorizedException('认证令牌已过期');
} elseif (str_contains($message, 'signature') || str_contains($message, 'Signature')) {
return $this->unauthorized('认证令牌签名无效', 401);
throw new UnauthorizedException('认证令牌签名无效');
} else {
return $this->unauthorized('认证令牌验证失败:' . $message, 401);
throw new UnauthorizedException('认证令牌验证失败:' . $message);
}
}
}
/**
* 返回未授权响应
*/
private function unauthorized(string $message, int $code = 401): Response
{
return json([
'code' => $code,
'message' => $message,
'data' => null,
], $code);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace app\models;
use app\common\base\BaseModel;
/**
* 系统配置模型
*
* 对应表ma_system_config
*/
class SystemConfig extends BaseModel
{
/**
* 启用自动维护 created_at / updated_at
*
* @var bool
*/
public $timestamps = true;
/**
* 表名
*
* @var string
*/
protected $table = 'ma_system_config';
/**
* 允许批量赋值的字段
*
* @var array
*/
protected $fillable = [
'config_key',
'config_value',
];
}

View File

@@ -1,19 +0,0 @@
<?php
namespace app\repositories;
use app\common\base\BaseRepository;
use app\models\CronJob;
/**
* 定时任务仓储
*/
class CronJobRepository extends BaseRepository
{
public function __construct()
{
parent::__construct(new CronJob());
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace app\repositories;
use app\common\base\BaseRepository;
use app\models\CronLog;
/**
* 定时任务日志仓储
*/
class CronLogRepository extends BaseRepository
{
public function __construct()
{
parent::__construct(new CronLog());
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace app\repositories;
use app\common\base\BaseRepository;
use app\models\Department;
/**
* 部门仓储
*/
class DepartmentRepository extends BaseRepository
{
public function __construct()
{
parent::__construct(new Department());
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace app\repositories;
use app\common\base\BaseRepository;
use app\models\DictGroup;
/**
* 字典分组仓储
*/
class DictGroupRepository extends BaseRepository
{
public function __construct()
{
parent::__construct(new DictGroup());
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace app\repositories;
use app\common\base\BaseRepository;
use app\models\DictItem;
/**
* 字典项仓储
*/
class DictItemRepository extends BaseRepository
{
public function __construct()
{
parent::__construct(new DictItem());
}
}

View File

@@ -1,54 +0,0 @@
<?php
namespace app\repositories;
use app\common\base\BaseRepository;
use app\models\Menu;
/**
* 菜单 / 权限仓储
*/
class MenuRepository extends BaseRepository
{
public function __construct()
{
parent::__construct(new Menu());
}
/**
* 获取所有启用的菜单(仅目录和菜单类型,排除按钮)
*/
public function getAllEnabledMenus(): array
{
return $this->model
->newQuery()
->whereIn('type', [1, 2]) // 1目录 2菜单排除3按钮
->where('status', 1) // 只获取启用的菜单
->orderBy('sort', 'asc')
->orderBy('id', 'asc')
->get()
->toArray();
}
/**
* 根据菜单ID列表获取启用的菜单仅目录和菜单类型排除按钮
*/
public function getMenusByIds(array $menuIds): array
{
if (empty($menuIds)) {
return [];
}
return $this->model
->newQuery()
->whereIn('id', $menuIds)
->whereIn('type', [1, 2]) // 1目录 2菜单排除3按钮
->where('status', 1) // 只获取启用的菜单
->orderBy('sort', 'asc')
->orderBy('id', 'asc')
->get()
->toArray();
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace app\repositories;
use app\common\base\BaseRepository;
use app\models\Role;
/**
* 角色仓储
*/
class RoleRepository extends BaseRepository
{
public function __construct()
{
parent::__construct(new Role());
}
}

View File

@@ -0,0 +1,157 @@
<?php
namespace app\repositories;
use app\common\base\BaseRepository;
use app\models\SystemConfig;
use support\Cache;
use Webman\Event\Event;
/**
* 系统配置仓储
*/
class SystemConfigRepository extends BaseRepository
{
/**
* 缓存键:全部系统配置
*/
private const CACHE_KEY_ALL_CONFIG = 'system_config_all';
public function __construct()
{
parent::__construct(new SystemConfig());
}
/**
* 从数据库加载所有配置到缓存
*
* @return array<string, string>
*/
protected function loadAllToCache(): array
{
// 优先从 webman/cache 获取
$cached = Cache::get(self::CACHE_KEY_ALL_CONFIG);
if (is_array($cached)) {
return $cached;
}
// 缓存不存在时从数据库加载
$configs = $this->model
->newQuery()
->get(['config_key', 'config_value']);
$result = [];
foreach ($configs as $config) {
$result[$config->config_key] = $config->config_value;
}
// 写入缓存(不过期,除非显式清理)
Cache::set(self::CACHE_KEY_ALL_CONFIG, $result);
return $result;
}
/**
* 清空缓存(供事件调用)
*/
public static function clearCache(): void
{
Cache::delete(self::CACHE_KEY_ALL_CONFIG);
}
/**
* 重新从数据库加载缓存(供事件调用)
*/
public function reloadCache(): void
{
Cache::delete(self::CACHE_KEY_ALL_CONFIG);
$this->loadAllToCache();
}
/**
* 根据配置键名查询配置值
*
* @param string $configKey
* @return string|null
*/
public function getValueByKey(string $configKey): ?string
{
$all = $this->loadAllToCache();
return $all[$configKey] ?? null;
}
/**
* 根据配置键名数组批量查询配置
*
* @param array $configKeys
* @return array 返回 ['config_key' => 'config_value'] 格式的数组
*/
public function getValuesByKeys(array $configKeys): array
{
if (empty($configKeys)) {
return [];
}
$all = $this->loadAllToCache();
$result = [];
foreach ($configKeys as $key) {
if (array_key_exists($key, $all)) {
$result[$key] = $all[$key];
}
}
return $result;
}
/**
* 根据配置键名更新或创建配置
*
* @param string $configKey
* @param string $configValue
* @return bool
*/
public function updateOrCreate(string $configKey, string $configValue): bool
{
$this->model
->newQuery()
->updateOrCreate(
['config_key' => $configKey],
['config_value' => $configValue]
);
// 通过事件通知重新加载缓存
Event::emit('system.config.updated', null);
return true;
}
/**
* 批量更新或创建配置
*
* @param array $configs 格式:['config_key' => 'config_value']
* @return bool
*/
public function batchUpdateOrCreate(array $configs): bool
{
if (empty($configs)) {
return true;
}
foreach ($configs as $configKey => $configValue) {
$this->model
->newQuery()
->updateOrCreate(
['config_key' => $configKey],
['config_value' => $configValue]
);
}
// 批量更新后只触发一次事件,通知重新加载缓存
Event::emit('system.config.updated', null);
return true;
}
}

View File

@@ -30,5 +30,10 @@ Route::group('/adminapi', function () {
// 系统相关需要JWT验证
Route::get('/system/getDict[/{code}]', [SystemController::class, 'getDict']);
// 系统配置相关需要JWT验证
Route::get('/system/base-config/tabs', [SystemController::class, 'getTabsConfig']);
Route::get('/system/base-config/form/{tabKey}', [SystemController::class, 'getFormConfig']);
Route::post('/system/base-config/submit/{tabKey}', [SystemController::class, 'submitConfig']);
})->middleware([AuthMiddleware::class]);
})->middleware([Cors::class]);

8
app/routes/mer.php Normal file
View File

@@ -0,0 +1,8 @@
<?php
/**
* API 路由定义
*/
use Webman\Route;

View File

@@ -4,6 +4,7 @@ namespace app\services;
use app\common\base\BaseService;
use app\common\utils\JwtUtil;
use app\exceptions\{BadRequestException, ForbiddenException, UnauthorizedException};
use app\repositories\UserRepository;
use support\Cache;
@@ -30,29 +31,28 @@ class AuthService extends BaseService
* @param string $verifyCode 验证码
* @param string $captchaId 验证码ID
* @return array ['token' => string]
* @throws \RuntimeException
*/
public function login(string $username, string $password, string $verifyCode, string $captchaId): array
{
// 1. 校验验证码
if (!$this->captchaService->validate($captchaId, $verifyCode)) {
throw new \RuntimeException('验证码错误或已失效', 400);
throw new BadRequestException('验证码错误或已失效');
}
// 2. 查询用户
$user = $this->userRepository->findByUserName($username);
if (!$user) {
throw new \RuntimeException('账号或密码错误', 401);
throw new UnauthorizedException('账号或密码错误');
}
// 3. 校验密码
if (!$this->validatePassword($password, $user->password)) {
throw new \RuntimeException('账号或密码错误', 401);
throw new UnauthorizedException('账号或密码错误');
}
// 4. 检查用户状态
if ($user->status !== 1) {
throw new \RuntimeException('账号已被禁用', 403);
throw new ForbiddenException('账号已被禁用');
}
// 5. 生成 JWT token包含用户ID、用户名、昵称等信息

View File

@@ -0,0 +1,89 @@
<?php
namespace app\services;
use app\common\base\BaseService;
use app\exceptions\InternalServerException;
use support\Cache;
/**
* 菜单相关业务服务
*
* 负责:
* - 从 JSON / 配置文件读取系统菜单
* - 使用 webman/cache 缓存菜单数据
* - 构建路由树结构
*/
class MenuService extends BaseService
{
/**
* 缓存键:系统菜单
* 注意webman/cache (symfony/cache) 不允许 key 中包含 : 等特殊字符
*/
private const CACHE_KEY_MENU = 'system_menu_all';
/**
* 获取前端路由(树形结构)
*
* @return array
*/
public function getRouters(): array
{
$menus = $this->getSystemMenu();
return $this->buildMenuTree($menus);
}
/**
* 获取系统菜单数据
* 仅从 JSON 文件 + 缓存中读取
*/
protected function getSystemMenu(): array
{
$menus = Cache::get(self::CACHE_KEY_MENU);
if (!is_array($menus)) {
// 优先读取 JSON 文件
$jsonPath = config_path('system-file/menu.json');
if (!file_exists($jsonPath)) {
throw new InternalServerException('菜单配置文件不存在');
}
$jsonContent = file_get_contents($jsonPath);
$data = json_decode($jsonContent, true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
throw new InternalServerException('菜单配置文件格式错误:' . json_last_error_msg());
}
$menus = $data;
Cache::set(self::CACHE_KEY_MENU, $menus);
}
return $menus;
}
/**
* 构建菜单树形结构
*/
protected function buildMenuTree(array $menus, string $parentId = '0'): array
{
$tree = [];
foreach ($menus as $menu) {
if (($menu['parentId'] ?? '0') === $parentId) {
$children = $this->buildMenuTree($menus, $menu['id']);
$menu['children'] = !empty($children) ? $children : null;
$tree[] = $menu;
}
}
// 按 sort 排序
usort($tree, function ($a, $b) {
return ($a['meta']['sort'] ?? 0) <=> ($b['meta']['sort'] ?? 0);
});
return $tree;
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace app\services;
use app\common\base\BaseService;
use app\repositories\SystemConfigRepository;
/**
* 系统配置服务
*/
class SystemConfigService extends BaseService
{
public function __construct(
protected SystemConfigRepository $configRepository
) {
}
/**
* 根据配置键名获取配置值
*
* @param string $configKey
* @param mixed $default 默认值
* @return mixed
*/
public function getValue(string $configKey, $default = null)
{
$value = $this->configRepository->getValueByKey($configKey);
return $value !== null ? $value : $default;
}
/**
* 根据配置键名数组批量获取配置值
*
* @param array $configKeys
* @return array 返回 ['config_key' => 'config_value'] 格式的数组
*/
public function getValues(array $configKeys): array
{
return $this->configRepository->getValuesByKeys($configKeys);
}
/**
* 保存配置值
*
* @param string $configKey
* @param mixed $configValue
* @return bool
*/
public function setValue(string $configKey, $configValue): bool
{
// 如果是数组或对象转换为JSON字符串
if (is_array($configValue) || is_object($configValue)) {
$configValue = json_encode($configValue, JSON_UNESCAPED_UNICODE);
} else {
$configValue = (string) $configValue;
}
return $this->configRepository->updateOrCreate($configKey, $configValue);
}
/**
* 批量保存配置值
*
* @param array $configs 格式:['config_key' => 'config_value']
* @return bool
*/
public function setValues(array $configs): bool
{
// 处理数组和对象类型的值
$processedConfigs = [];
foreach ($configs as $key => $value) {
if (is_array($value) || is_object($value)) {
$processedConfigs[$key] = json_encode($value, JSON_UNESCAPED_UNICODE);
} else {
$processedConfigs[$key] = (string) $value;
}
}
return $this->configRepository->batchUpdateOrCreate($processedConfigs);
}
}

View File

@@ -0,0 +1,211 @@
<?php
namespace app\services;
use app\common\base\BaseService;
use app\exceptions\{InternalServerException, NotFoundException};
use support\Cache;
/**
* 系统设置相关业务服务
*
* 负责:
* - 字典配置dict.json + 缓存)
* - 系统设置 Tab 配置tabs.json + 缓存)
* - 表单配置({tabKey}.json + 数据库配置值 + 缓存)
*/
class SystemSettingService extends BaseService
{
/**
* 缓存键系统设置Tab
*/
private const CACHE_KEY_TABS = 'system_base_config_tabs';
/**
* 缓存键前缀:系统设置表单配置
*/
private const CACHE_KEY_FORM_PREFIX = 'system_base_config_form_';
/**
* 缓存键:所有字典
*/
private const CACHE_KEY_DICT = 'system_dict_all';
public function __construct(
protected SystemConfigService $configService
) {
}
/**
* 获取字典数据
*
* @param string $code 字典编码,不传返回全部
* @return array
*/
public function getDict(string $code = ''): array
{
$allDicts = Cache::get(self::CACHE_KEY_DICT);
if (!is_array($allDicts)) {
$jsonPath = config_path('system-file/dict.json');
if (!file_exists($jsonPath)) {
throw new InternalServerException('字典配置文件不存在');
}
$jsonContent = file_get_contents($jsonPath);
$data = json_decode($jsonContent, true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
throw new InternalServerException('字典配置文件格式错误:' . json_last_error_msg());
}
$allDicts = $data;
Cache::set(self::CACHE_KEY_DICT, $allDicts);
}
if ($code === '') {
return $allDicts;
}
$dictsByCode = array_column($allDicts, null, 'code');
$dict = $dictsByCode[$code] ?? null;
if ($dict === null) {
throw new NotFoundException('未找到指定的字典:' . $code);
}
return $dict;
}
/**
* 获取所有系统设置 Tab 配置
*
* @return array
*/
public function getTabs(): array
{
$cached = Cache::get(self::CACHE_KEY_TABS);
if (is_array($cached)) {
return $cached;
}
$configPath = config_path('base-config/tabs.json');
if (!file_exists($configPath)) {
throw new NotFoundException('Tab配置文件不存在');
}
$jsonContent = file_get_contents($configPath);
$tabs = json_decode($jsonContent, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new InternalServerException('Tab配置文件格式错误' . json_last_error_msg());
}
usort($tabs, function ($a, $b) {
$sortA = $a['sort'] ?? 0;
$sortB = $b['sort'] ?? 0;
return $sortA <=> $sortB;
});
Cache::set(self::CACHE_KEY_TABS, $tabs);
return $tabs;
}
/**
* 获取指定 Tab 的表单配置(合并数据库值)
*
* @param string $tabKey
* @return array
*/
public function getFormConfig(string $tabKey): array
{
$cacheKey = self::CACHE_KEY_FORM_PREFIX . $tabKey;
$formConfig = Cache::get($cacheKey);
if (!is_array($formConfig)) {
$configPath = config_path("base-config/{$tabKey}.json");
if (!file_exists($configPath)) {
throw new NotFoundException("表单配置文件不存在:{$tabKey}");
}
$jsonContent = file_get_contents($configPath);
$formConfig = json_decode($jsonContent, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new InternalServerException('表单配置文件格式错误:' . json_last_error_msg());
}
Cache::set($cacheKey, $formConfig);
}
// 合并数据库配置值
if (isset($formConfig['rules']) && is_array($formConfig['rules'])) {
$fieldNames = [];
foreach ($formConfig['rules'] as $rule) {
if (isset($rule['field'])) {
$fieldNames[] = $rule['field'];
}
}
if (!empty($fieldNames)) {
$dbValues = $this->configService->getValues($fieldNames);
foreach ($formConfig['rules'] as &$rule) {
if (isset($rule['field']) && isset($dbValues[$rule['field']])) {
$value = $dbValues[$rule['field']];
$decoded = json_decode($value, true);
if (json_last_error() === JSON_ERROR_NONE) {
$rule['value'] = $decoded;
} else {
if (isset($rule['type'])) {
switch ($rule['type']) {
case 'inputNumber':
$rule['value'] = is_numeric($value) ? (float) $value : ($rule['value'] ?? 0);
break;
case 'switch':
$rule['value'] = in_array(strtolower($value), ['1', 'true', 'yes', 'on'], true);
break;
default:
$rule['value'] = $value;
}
} else {
$rule['value'] = $value;
}
}
}
}
unset($rule);
}
}
Cache::set($cacheKey, $formConfig);
return $formConfig;
}
/**
* 保存表单配置
*
* @param string $tabKey
* @param array $formData
* @return void
*/
public function saveFormConfig(string $tabKey, array $formData): void
{
$result = $this->configService->setValues($formData);
if (!$result) {
throw new InternalServerException('保存失败');
}
// 清理对应表单缓存
$cacheKey = self::CACHE_KEY_FORM_PREFIX . $tabKey;
Cache::delete($cacheKey);
}
}

View File

@@ -4,6 +4,7 @@ namespace app\services;
use app\common\base\BaseService;
use app\common\constants\RoleCode;
use app\exceptions\NotFoundException;
use app\repositories\UserRepository;
/**
@@ -29,7 +30,7 @@ class UserService extends BaseService
{
$user = $this->users->find($id);
if (!$user) {
throw new \RuntimeException('用户不存在', 404);
throw new NotFoundException('用户不存在');
}
$userArray = $user->toArray();

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace app\validation;
use support\validation\Validator;
class SystemConfigValidator extends Validator
{
protected array $rules = [
'config_key' => 'string|max:100',
'config_value' => 'nullable|string',
];
protected array $messages = [];
protected array $attributes = [
'config_key' => '配置项键名',
'config_value' => '配置项值',
];
protected array $scenes = [];
}