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

2
.gitignore vendored
View File

@@ -6,4 +6,4 @@
/vendor
/runtime
*.log
.env
/.env

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 = [];
}

View File

@@ -32,14 +32,14 @@
"webman/redis": "^2.1",
"illuminate/events": "^12.49",
"webman/cache": "^2.1",
"webman/console": "^2.1",
"topthink/think-validate": "^3.0",
"webman/console": "^2.2",
"webman/captcha": "^1.0",
"webman/event": "^1.0",
"vlucas/phpdotenv": "^5.6",
"workerman/crontab": "^1.0",
"webman/redis-queue": "^2.1",
"firebase/php-jwt": "^7.0"
"firebase/php-jwt": "^7.0",
"webman/validation": "^2.2"
},
"suggest": {
"ext-event": "For better performance. "

446
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ea45057d8c2734266a897db21c27e75c",
"content-hash": "c9b20b999efb47e639901ded2ade0fdb",
"packages": [
{
"name": "brick/math",
@@ -225,6 +225,150 @@
],
"time": "2025-08-10T19:31:58+00:00"
},
{
"name": "doctrine/lexer",
"version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/lexer.git",
"reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd",
"reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"doctrine/coding-standard": "^12",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.5",
"psalm/plugin-phpunit": "^0.18.3",
"vimeo/psalm": "^5.21"
},
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\Common\\Lexer\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
"description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
"homepage": "https://www.doctrine-project.org/projects/lexer.html",
"keywords": [
"annotations",
"docblock",
"lexer",
"parser",
"php"
],
"support": {
"issues": "https://github.com/doctrine/lexer/issues",
"source": "https://github.com/doctrine/lexer/tree/3.0.1"
},
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer",
"type": "tidelift"
}
],
"time": "2024-02-05T11:56:58+00:00"
},
{
"name": "egulias/email-validator",
"version": "4.0.4",
"source": {
"type": "git",
"url": "https://github.com/egulias/EmailValidator.git",
"reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa",
"reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa",
"shasum": ""
},
"require": {
"doctrine/lexer": "^2.0 || ^3.0",
"php": ">=8.1",
"symfony/polyfill-intl-idn": "^1.26"
},
"require-dev": {
"phpunit/phpunit": "^10.2",
"vimeo/psalm": "^5.12"
},
"suggest": {
"ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Egulias\\EmailValidator\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Eduardo Gulias Davis"
}
],
"description": "A library for validating emails against several RFCs",
"homepage": "https://github.com/egulias/EmailValidator",
"keywords": [
"email",
"emailvalidation",
"emailvalidator",
"validation",
"validator"
],
"support": {
"issues": "https://github.com/egulias/EmailValidator/issues",
"source": "https://github.com/egulias/EmailValidator/tree/4.0.4"
},
"funding": [
{
"url": "https://github.com/egulias",
"type": "github"
}
],
"time": "2025-03-06T22:45:56+00:00"
},
{
"name": "firebase/php-jwt",
"version": "v7.0.2",
@@ -1697,6 +1841,120 @@
},
"time": "2026-02-04T15:14:59+00:00"
},
{
"name": "illuminate/translation",
"version": "v12.52.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/translation.git",
"reference": "18aa24aba6f2ab2447b9b903ae7360725fe5bdd0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/translation/zipball/18aa24aba6f2ab2447b9b903ae7360725fe5bdd0",
"reference": "18aa24aba6f2ab2447b9b903ae7360725fe5bdd0",
"shasum": ""
},
"require": {
"illuminate/collections": "^12.0",
"illuminate/contracts": "^12.0",
"illuminate/filesystem": "^12.0",
"illuminate/macroable": "^12.0",
"illuminate/support": "^12.0",
"php": "^8.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "12.x-dev"
}
},
"autoload": {
"psr-4": {
"Illuminate\\Translation\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "The Illuminate Translation package.",
"homepage": "https://laravel.com",
"support": {
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2026-02-06T12:12:31+00:00"
},
{
"name": "illuminate/validation",
"version": "v12.52.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/validation.git",
"reference": "195b7dd66548a6a82dd93bf6280926bf9039903c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/validation/zipball/195b7dd66548a6a82dd93bf6280926bf9039903c",
"reference": "195b7dd66548a6a82dd93bf6280926bf9039903c",
"shasum": ""
},
"require": {
"brick/math": "^0.11|^0.12|^0.13|^0.14",
"egulias/email-validator": "^3.2.5|^4.0",
"ext-filter": "*",
"ext-mbstring": "*",
"illuminate/collections": "^12.0",
"illuminate/container": "^12.0",
"illuminate/contracts": "^12.0",
"illuminate/macroable": "^12.0",
"illuminate/support": "^12.0",
"illuminate/translation": "^12.0",
"php": "^8.2",
"symfony/http-foundation": "^7.2",
"symfony/mime": "^7.2",
"symfony/polyfill-php83": "^1.33"
},
"suggest": {
"illuminate/database": "Required to use the database presence verifier (^12.0).",
"ramsey/uuid": "Required to use Validator::validateUuid() (^4.7)."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "12.x-dev"
}
},
"autoload": {
"psr-4": {
"Illuminate\\Validation\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "The Illuminate Validation package.",
"homepage": "https://laravel.com",
"support": {
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2026-02-14T23:03:41+00:00"
},
{
"name": "laravel/serializable-closure",
"version": "v1.3.7",
@@ -5019,142 +5277,6 @@
],
"time": "2025-09-11T10:15:23+00:00"
},
{
"name": "topthink/think-container",
"version": "v3.0.2",
"source": {
"type": "git",
"url": "https://github.com/top-think/think-container.git",
"reference": "b2df244be1e7399ad4c8be1ccc40ed57868f730a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/top-think/think-container/zipball/b2df244be1e7399ad4c8be1ccc40ed57868f730a",
"reference": "b2df244be1e7399ad4c8be1ccc40ed57868f730a",
"shasum": ""
},
"require": {
"php": ">=8.0",
"psr/container": "^2.0",
"topthink/think-helper": "^3.1"
},
"require-dev": {
"phpunit/phpunit": "^9.5"
},
"type": "library",
"autoload": {
"files": [],
"psr-4": {
"think\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "liu21st",
"email": "liu21st@gmail.com"
}
],
"description": "PHP Container & Facade Manager",
"support": {
"issues": "https://github.com/top-think/think-container/issues",
"source": "https://github.com/top-think/think-container/tree/v3.0.2"
},
"time": "2025-04-07T03:21:51+00:00"
},
{
"name": "topthink/think-helper",
"version": "v3.1.12",
"source": {
"type": "git",
"url": "https://github.com/top-think/think-helper.git",
"reference": "fe277121112a8f1c872e169a733ca80bb11c4acb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/top-think/think-helper/zipball/fe277121112a8f1c872e169a733ca80bb11c4acb",
"reference": "fe277121112a8f1c872e169a733ca80bb11c4acb",
"shasum": ""
},
"require": {
"php": ">=7.1.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5"
},
"type": "library",
"autoload": {
"files": [
"src/helper.php"
],
"psr-4": {
"think\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "yunwuxin",
"email": "448901948@qq.com"
}
],
"description": "The ThinkPHP6 Helper Package",
"support": {
"issues": "https://github.com/top-think/think-helper/issues",
"source": "https://github.com/top-think/think-helper/tree/v3.1.12"
},
"time": "2025-12-26T09:58:29+00:00"
},
{
"name": "topthink/think-validate",
"version": "v3.0.7",
"source": {
"type": "git",
"url": "https://github.com/top-think/think-validate.git",
"reference": "85063f6d4ef8ed122f17a36179dc3e0949b30988"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/top-think/think-validate/zipball/85063f6d4ef8ed122f17a36179dc3e0949b30988",
"reference": "85063f6d4ef8ed122f17a36179dc3e0949b30988",
"shasum": ""
},
"require": {
"php": ">=8.0",
"topthink/think-container": ">=3.0"
},
"type": "library",
"autoload": {
"files": [
"src/helper.php"
],
"psr-4": {
"think\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "liu21st",
"email": "liu21st@gmail.com"
}
],
"description": "think validate",
"support": {
"issues": "https://github.com/top-think/think-validate/issues",
"source": "https://github.com/top-think/think-validate/tree/v3.0.7"
},
"time": "2025-06-11T05:51:40+00:00"
},
{
"name": "vlucas/phpdotenv",
"version": "v5.6.3",
@@ -5410,16 +5532,16 @@
},
{
"name": "webman/console",
"version": "v2.1.12",
"version": "v2.2.1",
"source": {
"type": "git",
"url": "https://github.com/webman-php/console.git",
"reference": "7f1288ace2c7b6326e5a756dba067c74bd7cd2bc"
"reference": "3c1a50296f7b3b3eff3a8fcda8906276dae3a18f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webman-php/console/zipball/7f1288ace2c7b6326e5a756dba067c74bd7cd2bc",
"reference": "7f1288ace2c7b6326e5a756dba067c74bd7cd2bc",
"url": "https://api.github.com/repos/webman-php/console/zipball/3c1a50296f7b3b3eff3a8fcda8906276dae3a18f",
"reference": "3c1a50296f7b3b3eff3a8fcda8906276dae3a18f",
"shasum": ""
},
"require": {
@@ -5460,7 +5582,7 @@
"source": "https://github.com/webman-php/console",
"wiki": "http://www.workerman.net/doc/webman"
},
"time": "2026-01-20T15:17:31+00:00"
"time": "2026-02-23T02:46:47+00:00"
},
{
"name": "webman/database",
@@ -5601,6 +5723,46 @@
},
"time": "2025-11-14T07:12:52+00:00"
},
{
"name": "webman/validation",
"version": "v2.2.0",
"source": {
"type": "git",
"url": "https://github.com/webman-php/validation.git",
"reference": "afe9c66bf612d969d5657c83617bdb8d92e62ee3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webman-php/validation/zipball/afe9c66bf612d969d5657c83617bdb8d92e62ee3",
"reference": "afe9c66bf612d969d5657c83617bdb8d92e62ee3",
"shasum": ""
},
"require": {
"illuminate/translation": "*",
"illuminate/validation": "*",
"workerman/webman-framework": "^2.1 || dev-master"
},
"require-dev": {
"phpunit/phpunit": "*"
},
"type": "library",
"autoload": {
"psr-4": {
"Webman\\Validation\\": "src",
"support\\validation\\": "src/support/validation"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Webman plugin webman/validation",
"support": {
"issues": "https://github.com/webman-php/validation/issues",
"source": "https://github.com/webman-php/validation/tree/v2.2.0"
},
"time": "2026-02-21T08:22:43+00:00"
},
{
"name": "workerman/coroutine",
"version": "v1.1.4",

View File

@@ -0,0 +1,84 @@
{
"formId": "basic-config",
"title": "基础设置",
"submitText": "保存设置",
"submitUrl": "/adminapi/system/base-config/submit/basic",
"cacheKey": "basic_config_cache",
"refreshAfterSubmit": true,
"rules": [
{
"type": "input",
"field": "site_name",
"title": "站点名称",
"value": "",
"props": {
"placeholder": "请输入站点名称"
},
"validate": [
{
"required": true,
"message": "站点名称不能为空"
}
]
},
{
"type": "textarea",
"field": "site_description",
"title": "站点描述",
"value": "",
"props": {
"placeholder": "请输入站点描述",
"autoSize": {
"minRows": 3,
"maxRows": 6
}
}
},
{
"type": "input",
"field": "site_logo",
"title": "站点Logo",
"value": "",
"props": {
"placeholder": "请输入Logo地址或上传Logo"
}
},
{
"type": "input",
"field": "icp_number",
"title": "备案号",
"value": "",
"props": {
"placeholder": "请输入ICP备案号"
}
},
{
"type": "switch",
"field": "site_status",
"title": "站点状态",
"value": true,
"props": {
"checkedText": "开启",
"uncheckedText": "关闭"
}
},
{
"type": "inputNumber",
"field": "page_size",
"title": "每页显示数量",
"value": 10,
"props": {
"min": 1,
"max": 100,
"precision": 0
},
"validate": [
{
"required": true,
"message": "每页显示数量不能为空"
}
]
}
]
}

View File

@@ -0,0 +1,112 @@
{
"formId": "email-config",
"title": "邮件设置",
"submitText": "保存设置",
"submitUrl": "/adminapi/system/base-config/submit/email",
"cacheKey": "email_config_cache",
"refreshAfterSubmit": true,
"rules": [
{
"type": "input",
"field": "smtp_host",
"title": "SMTP服务器",
"value": "",
"props": {
"placeholder": "例如smtp.qq.com"
},
"validate": [
{
"required": true,
"message": "SMTP服务器不能为空"
}
]
},
{
"type": "inputNumber",
"field": "smtp_port",
"title": "SMTP端口",
"value": 465,
"props": {
"min": 1,
"max": 65535,
"precision": 0
},
"validate": [
{
"required": true,
"message": "SMTP端口不能为空"
}
]
},
{
"type": "switch",
"field": "smtp_ssl",
"title": "启用SSL",
"value": true,
"props": {
"checkedText": "是",
"uncheckedText": "否"
}
},
{
"type": "input",
"field": "smtp_username",
"title": "SMTP用户名",
"value": "",
"props": {
"placeholder": "请输入SMTP用户名"
},
"validate": [
{
"required": true,
"message": "SMTP用户名不能为空"
}
]
},
{
"type": "input",
"field": "smtp_password",
"title": "SMTP密码",
"value": "",
"props": {
"type": "password",
"placeholder": "请输入SMTP密码或授权码"
},
"validate": [
{
"required": true,
"message": "SMTP密码不能为空"
}
]
},
{
"type": "input",
"field": "from_email",
"title": "发件人邮箱",
"value": "",
"props": {
"placeholder": "请输入发件人邮箱地址"
},
"validate": [
{
"required": true,
"message": "发件人邮箱不能为空"
},
{
"type": "email",
"message": "请输入正确的邮箱地址"
}
]
},
{
"type": "input",
"field": "from_name",
"title": "发件人名称",
"value": "",
"props": {
"placeholder": "请输入发件人名称"
}
}
]
}

View File

@@ -0,0 +1,99 @@
{
"formId": "permission-config",
"title": "权限设置",
"submitText": "保存设置",
"submitUrl": "/adminapi/system/base-config/submit/permission",
"cacheKey": "permission_config_cache",
"refreshAfterSubmit": true,
"rules": [
{
"type": "switch",
"field": "enable_permission",
"title": "启用权限控制",
"value": true,
"props": {
"checkedText": "启用",
"uncheckedText": "禁用"
}
},
{
"type": "inputNumber",
"field": "session_timeout",
"title": "会话超时时间(分钟)",
"value": 30,
"props": {
"min": 1,
"max": 1440,
"precision": 0
},
"validate": [
{
"required": true,
"message": "会话超时时间不能为空"
}
]
},
{
"type": "inputNumber",
"field": "password_min_length",
"title": "密码最小长度",
"value": 6,
"props": {
"min": 4,
"max": 32,
"precision": 0
},
"validate": [
{
"required": true,
"message": "密码最小长度不能为空"
}
]
},
{
"type": "switch",
"field": "require_strong_password",
"title": "要求强密码",
"value": false,
"props": {
"checkedText": "是",
"uncheckedText": "否"
}
},
{
"type": "inputNumber",
"field": "max_login_attempts",
"title": "最大登录尝试次数",
"value": 5,
"props": {
"min": 1,
"max": 10,
"precision": 0
},
"validate": [
{
"required": true,
"message": "最大登录尝试次数不能为空"
}
]
},
{
"type": "inputNumber",
"field": "lockout_duration",
"title": "账户锁定时长(分钟)",
"value": 30,
"props": {
"min": 1,
"max": 1440,
"precision": 0
},
"validate": [
{
"required": true,
"message": "账户锁定时长不能为空"
}
]
}
]
}

View File

@@ -0,0 +1,27 @@
[
{
"key": "basic",
"title": "基础设置",
"icon": "settings",
"description": "配置系统基础信息包括站点名称、Logo、备案号等",
"sort": 1,
"disabled": false
},
{
"key": "email",
"title": "邮件设置",
"icon": "email",
"description": "配置邮件服务器相关参数包括SMTP服务器、端口、账号密码等",
"sort": 2,
"disabled": false
},
{
"key": "permission",
"title": "权限设置",
"icon": "lock",
"description": "配置系统权限相关参数,预留权限控制功能",
"sort": 3,
"disabled": false
}
]

View File

@@ -1,48 +0,0 @@
<?php
/**
* 字典数据配置
*/
return [
[
'name' => '性别',
'code' => 'gender',
'description' => '这是一个性别字典',
'list' => [
['name' => '女', 'value' => 0],
['name' => '男', 'value' => 1],
['name' => '其它', 'value' => 2],
],
],
[
'name' => '状态',
'code' => 'status',
'description' => '状态字段可以用这个',
'list' => [
['name' => '禁用', 'value' => 0],
['name' => '启用', 'value' => 1],
],
],
[
'name' => '岗位',
'code' => 'post',
'description' => '岗位字段',
'list' => [
['name' => '总经理', 'value' => 1],
['name' => '总监', 'value' => 2],
['name' => '人事主管', 'value' => 3],
['name' => '开发部主管', 'value' => 4],
['name' => '普通职员', 'value' => 5],
['name' => '其它', 'value' => 999],
],
],
[
'name' => '任务状态',
'code' => 'taskStatus',
'description' => '任务状态字段可以用它',
'list' => [
['name' => '失败', 'value' => 0],
['name' => '成功', 'value' => 1],
],
],
];

View File

@@ -1,5 +1,8 @@
<?php
return [
// 系统配置更新后重新加载缓存
'system.config.updated' => [
[app\events\SystemConfig::class, 'reload'],
],
];

View File

@@ -0,0 +1,8 @@
<?php
use support\validation\ValidationException;
return [
'enable' => true,
'exception' => ValidationException::class,
];

View File

@@ -0,0 +1,7 @@
<?php
use Webman\Validation\Command\MakeValidatorCommand;
return [
MakeValidatorCommand::class
];

View File

@@ -0,0 +1,9 @@
<?php
use Webman\Validation\Middleware;
return [
'@' => [
Middleware::class,
],
];

View File

@@ -0,0 +1,45 @@
[
{
"name": "性别",
"code": "gender",
"description": "这是一个性别字典",
"list": [
{ "name": "女", "value": 0 },
{ "name": "男", "value": 1 },
{ "name": "其它", "value": 2 }
]
},
{
"name": "状态",
"code": "status",
"description": "状态字段可以用这个",
"list": [
{ "name": "禁用", "value": 0 },
{ "name": "启用", "value": 1 }
]
},
{
"name": "岗位",
"code": "post",
"description": "岗位字段",
"list": [
{ "name": "总经理", "value": 1 },
{ "name": "总监", "value": 2 },
{ "name": "人事主管", "value": 3 },
{ "name": "开发部主管", "value": 4 },
{ "name": "普通职员", "value": 5 },
{ "name": "其它", "value": 999 }
]
},
{
"name": "任务状态",
"code": "taskStatus",
"description": "任务状态字段可以用它",
"list": [
{ "name": "失败", "value": 0 },
{ "name": "成功", "value": 1 }
]
}
]

1059
config/system-file/menu.json Normal file

File diff suppressed because it is too large Load Diff

137
config/system-file/menu.md Normal file
View File

@@ -0,0 +1,137 @@
# 系统菜单配置
## 配置说明
系统菜单基于路由配置实现,以下是完整的菜单路由配置项说明,所有配置项均为 JSON 格式,可直接用于前端路由解析。
## 核心配置结构
```json
{
"id": "01",
"parentId": "0",
"path": "/home",
"name": "home",
"component": "home/home",
"meta": {
"title": "home",
"hide": false,
"disable": false,
"keepAlive": false,
"affix": true,
"link": "",
"iframe": false,
"isFull": false,
"roles": ["admin", "common"],
"svgIcon": "home",
"icon": "",
"sort": 1,
"type": 2
},
"children": null
}
```
## 字段详细说明
| 一级字段 | 类型 | 必填 | 说明 |
|----------|--------|------|----------------------------------------------------------------------|
| `id` | string | 是 | 路由唯一标识,建议按「层级+序号」命名(如 01=首页0201=订单管理)|
| `parentId` | string | 是 | 父路由ID顶层路由固定为 `0`,子路由对应父路由的 `id` |
| `path` | string | 是 | 路由访问路径(如 `/home``/order/order-list`|
| `name` | string | 是 | 路由名称,需与组件名/路径名保持一致,用于路由跳转标识 |
| `component` | string | 否 | 组件文件路径(基于 `src/views` 目录),如 `home/home` 对应 `src/views/home/home.vue`;目录级路由可省略 |
| `meta` | object | 是 | 路由元信息,包含菜单展示、权限、样式等核心配置 |
| `children` | array | 否 | 子路由列表,目录级路由(`type:1`)需配置,菜单级路由(`type:2`)默认为 `null` |
### `meta` 子字段说明
| 子字段 | 类型 | 必填 | 默认值 | 说明 |
|-----------|---------|------|--------|----------------------------------------------------------------------|
| `title` | string | 是 | - | 菜单显示标题:<br>1. 填写国际化key`home`),自动匹配多语言;<br>2. 无对应key则直接展示文字 |
| `hide` | boolean | 否 | false | 是否隐藏菜单:<br>✅ true = 不显示在侧边栏,但可正常访问;<br>❌ false = 正常显示 |
| `disable` | boolean | 否 | false | 是否停用路由:<br>✅ true = 不显示+不可访问;<br>❌ false = 正常可用 |
| `keepAlive` | boolean | 否 | false | 是否缓存组件:<br>✅ true = 切换路由不销毁组件;<br>❌ false = 切换后销毁 |
| `affix` | boolean | 否 | false | 是否固定在标签栏:<br>✅ true = 标签栏无关闭按钮;<br>❌ false = 可关闭 |
| `link` | string | 否 | "" | 外链地址,填写后路由跳转至该地址(优先级高于 `component`|
| `iframe` | boolean | 否 | false | 是否内嵌外链:<br>✅ true = 在页面内以iframe展示 `link` 地址;<br>❌ false = 跳转新页面 |
| `isFull` | boolean | 否 | false | 是否全屏显示:<br>✅ true = 菜单页面占满整个视口;<br>❌ false = 保留侧边栏/头部 |
| `roles` | array | 否 | [] | 路由权限角色:<br>`["admin", "common"]`,仅对应角色可访问该菜单 |
| `svgIcon` | string | 否 | "" | SVG菜单图标<br>优先级高于 `icon`,取值为 `src/assets/svgs` 目录下的SVG文件名 |
| `icon` | string | 否 | "" | 普通图标:<br>默认使用 Arco Design 图标库,填写图标名(如 `icon-file`)即可 |
| `sort` | number | 否 | 0 | 菜单排序:数值越小,展示越靠前 |
| `type` | number | 是 | - | 路由类型:<br>1 = 目录(仅作为父级,无组件);<br>2 = 菜单(可访问的页面);<br>3 = 按钮(权限控制用) |
## 配置示例
### 1. 顶层菜单(首页)
```json
{
"id": "01",
"parentId": "0",
"path": "/home",
"name": "home",
"component": "home/home",
"meta": {
"title": "平台首页",
"hide": false,
"disable": false,
"keepAlive": false,
"affix": true,
"link": "",
"iframe": false,
"isFull": false,
"roles": ["admin", "common"],
"svgIcon": "home",
"icon": "",
"sort": 1,
"type": 2
},
"children": null
}
```
### 2. 目录级路由(收款订单)
```json
{
"id": "02",
"parentId": "0",
"path": "/order",
"name": "order",
"redirect": "/order/order-list",
"meta": {
"title": "收款订单",
"hide": false,
"disable": false,
"keepAlive": true,
"affix": false,
"link": "",
"iframe": false,
"isFull": false,
"roles": ["admin", "common"],
"svgIcon": "order",
"icon": "",
"sort": 2,
"type": 1
},
"children": [
{
"id": "0201",
"parentId": "02",
"path": "/order/order-list",
"name": "order-list",
"component": "order/order-list/index",
"meta": {
"title": "订单管理",
"hide": false,
"disable": false,
"keepAlive": true,
"affix": false,
"link": "",
"iframe": false,
"isFull": false,
"roles": ["admin", "common"],
"icon": "icon-file",
"sort": 1,
"type": 2
},
"children": null
}
]
}
```

View File

@@ -0,0 +1,11 @@
-- 系统配置表
CREATE TABLE IF NOT EXISTS `ma_system_config` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`config_key` varchar(100) NOT NULL DEFAULT '' COMMENT '配置项键名(唯一标识,直接使用字段名)',
`config_value` text COMMENT '配置项值支持字符串、数字、JSON等',
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_config_key` (`config_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统配置表';

112
doc/event.md Normal file
View File

@@ -0,0 +1,112 @@
event事件处理
webman/event 提供一种精巧的事件机制可实现在不侵入代码的情况下执行一些业务逻辑实现业务模块之间的解耦。典型的场景如一个新用户注册成功时只要发布一个自定义事件如user.register各个模块遍能收到该事件执行相应的业务逻辑。
安装
composer require webman/event
订阅事件
订阅事件统一通过文件config/event.php来配置
<?php
return [
'user.register' => [
[app\event\User::class, 'register'],
// ...其它事件处理函数...
],
'user.logout' => [
[app\event\User::class, 'logout'],
// ...其它事件处理函数...
]
];
说明:
user.register user.logout 等是事件名称,字符串类型,建议小写单词并以点(.)分割
一个事件可以对应多个事件处理函数,调用顺序为配置的顺序
事件处理函数
事件处理函数可以是任意的类方法、函数、闭包函数等。
例如创建事件处理类 app/event/User.php (目录不存在请自行创建)
<?php
namespace app\event;
class User
{
function register($user)
{
var_export($user);
}
function logout($user)
{
var_export($user);
}
}
发布事件
使用 Event::dispatch($event_name, $data); 或 Event::emit($event_name, $data); 发布事件,例如
<?php
namespace app\controller;
use support\Request;
use Webman\Event\Event;
class User
{
public function register(Request $request)
{
$user = [
'name' => 'webman',
'age' => 2
];
Event::dispatch('user.register', $user);
}
}
发布事件有两个函数Event::dispatch($event_name, $data); 和 Event::emit($event_name, $data); 二者参数一样。
区别是emit内部会自动捕获异常也就是说如果一个事件有多个处理函数某个处理函数发生异常不会影响其它处理函数的执行。
而dispatch则内部不会自动捕获异常当前事件的任何一个处理函数发生异常则停止执行下一个处理函数并直接向上抛出异常。
提示
参数$data可以是任意的数据例如数组、类实例、字符串等
通配符事件监听
通配符注册监听允许您在同一个监听器上处理多个事件例如config/event.php里配置
<?php
return [
'user.*' => [
[app\event\User::class, 'deal']
],
];
我们可以通过事件处理函数第二个参数$event_data获得具体的事件名
<?php
namespace app\event;
class User
{
function deal($user, $event_name)
{
echo $event_name; // 具体的事件名,如 user.register user.logout 等
var_export($user);
}
}
停止事件广播
当我们在事件处理函数里返回false时该事件将停止广播
闭包函数处理事件
事件处理函数可以是类方法,也可以是闭包函数例如
<?php
return [
'user.login' => [
function($user){
var_dump($user);
}
]
];
查看事件及监听器
使用命令 php webman event:list 查看项目配置的所有事件及监听器
支持范围
除了主项目基础插件和应用插件同样支持event.php配置。
基础插件配置文件 config/plugin/插件厂商/插件名/event.php
应用插件配置文件 plugin/插件名/config/event.php
注意事项
event事件处理并不是异步的event不适合处理慢业务慢业务应该用消息队列处理

114
doc/exception.md Normal file
View File

@@ -0,0 +1,114 @@
异常处理
配置
config/exception.php
return [
// 这里配置异常处理类
'' => support\exception\Handler::class,
];
多应用模式时,你可以为每个应用单独配置异常处理类,参见多应用
默认异常处理类
webman中异常默认由 support\exception\Handler 类来处理。可修改配置文件config/exception.php来更改默认异常处理类。异常处理类必须实现Webman\Exception\ExceptionHandlerInterface 接口。
interface ExceptionHandlerInterface
{
/**
* 记录日志
* @param Throwable $e
* @return mixed
*/
public function report(Throwable $e);
/**
* 渲染返回
* @param Request $request
* @param Throwable $e
* @return Response
*/
public function render(Request $request, Throwable $e) : Response;
}
渲染响应
异常处理类中的render方法是用来渲染响应的。
如果配置文件config/app.php中debug值为true(以下简称app.debug=true),将返回详细的异常信息,否则将返回简略的异常信息。
如果请求期待是json返回则返回的异常信息将以json格式返回类似
{
"code": "500",
"msg": "异常信息"
}
如果app.debug=truejson数据里会额外增加一个trace字段返回详细的调用栈。
你可以编写自己的异常处理类来更改默认异常处理逻辑。
业务异常 BusinessException
有时候我们想在某个嵌套函数里终止请求并返回一个错误信息给客户端这时可以通过抛出BusinessException来做到这点。
例如:
<?php
namespace app\controller;
use support\Request;
use support\exception\BusinessException;
class FooController
{
public function index(Request $request)
{
$this->checkInput($request->post());
return response('hello index');
}
protected function checkInput($input)
{
if (!isset($input['token'])) {
throw new BusinessException('参数错误', 3000);
}
}
}
以上示例会返回一个
{"code": 3000, "msg": "参数错误"}
注意
业务异常BusinessException不需要业务try捕获框架会自动捕获并根据请求类型返回合适的输出。
自定义业务异常
如果以上响应不符合你的需求例如想把msg要改为message可以自定义一个MyBusinessException
新建 app/exception/MyBusinessException.php 内容如下
<?php
namespace app\exception;
use support\exception\BusinessException;
use Webman\Http\Request;
use Webman\Http\Response;
class MyBusinessException extends BusinessException
{
public function render(Request $request): ?Response
{
// json请求返回json数据
if ($request->expectsJson()) {
return json(['code' => $this->getCode() ?: 500, 'message' => $this->getMessage()]);
}
// 非json请求则返回一个页面
return new Response(200, [], $this->getMessage());
}
}
这样当业务调用
use app\exception\MyBusinessException;
throw new MyBusinessException('参数错误', 3000);
json请求将收到一个类似如下的json返回
{"code": 3000, "message": "参数错误"}
提示
因为BusinessException异常属于业务异常(例如用户输入参数错误),它是可预知的,所以框架并不会认为它是致命错误,并不会记录日志。
总结
在任何想中断当前请求并返回信息给客户端的时候可以考虑使用BusinessException异常。

323
doc/skill.md Normal file
View File

@@ -0,0 +1,323 @@
# MPAY V2 项目技术栈与结构文档
## 1. 项目概述
MPAY V2 是一个基于 Webman 后端框架和 Vue 3 前端框架的支付管理系统,提供完整的支付业务管理功能,包括用户认证、菜单管理、系统配置、财务管理、渠道管理和数据分析等核心模块。
## 2. 技术架构
### 2.1 后端技术栈
| 类别 | 技术/框架 | 版本 | 用途 | 来源 |
|------|-----------|------|------|------|
| 基础框架 | Webman | ^2.1 | 高性能HTTP服务框架 | composer.json:28 |
| PHP版本 | PHP | >=8.1 | 开发语言 | composer.json:27 |
| 数据库 | webman/database | ^2.1 | 数据库操作 | composer.json:31 |
| 缓存 | Redis | ^2.1 | 缓存存储 | composer.json:32 |
| 缓存 | webman/cache | ^2.1 | 缓存管理 | composer.json:34 |
| 认证 | JWT | ^7.0 | 用户认证 | composer.json:42 |
| 验证码 | webman/captcha | ^1.0 | 登录验证码 | composer.json:37 |
| 事件系统 | webman/event | ^1.0 | 事件管理 | composer.json:38 |
| 配置管理 | vlucas/phpdotenv | ^5.6 | 环境变量 | composer.json:39 |
| 定时任务 | workerman/crontab | ^1.0 | 定时任务 | composer.json:40 |
| 队列 | webman/redis-queue | ^2.1 | 消息队列 | composer.json:41 |
| 验证 | topthink/think-validate | ^3.0 | 数据验证 | composer.json:36 |
| 容器 | php-di/php-di | 7.0 | 依赖注入 | composer.json:30 |
| 日志 | monolog/monolog | ^2.0 | 日志管理 | composer.json:29 |
| 控制台 | webman/console | ^2.1 | 命令行工具 | composer.json:35 |
### 2.2 前端技术栈
| 类别 | 技术/框架 | 版本 | 用途 | 来源 |
|------|-----------|------|------|------|
| 基础框架 | Vue | ^3.5.15 | 前端框架 | package.json:61 |
| 语言 | TypeScript | ^5.2.2 | 开发语言 | package.json:103 |
| 构建工具 | Vite | ^6.3.5 | 构建工具 | package.json:107 |
| UI框架 | Arco Design | ^2.57.0 | 界面组件库 | package.json:72 |
| 状态管理 | Pinia | ^2.3.0 | 状态管理 | package.json:53 |
| 路由 | Vue Router | ^4.3.0 | 前端路由 | package.json:66 |
| HTTP客户端 | Axios | ^1.6.8 | API调用 | package.json:47 |
| 表单生成 | @form-create/arco-design | ^3.2.37 | 动态表单 | package.json:41 |
| 图表 | @visactor/vchart | ^1.11.0 | 数据可视化 | package.json:42 |
| 代码编辑器 | CodeMirror | ^6.0.1 | 代码编辑 | package.json:48 |
| 富文本编辑器 | @wangeditor/editor | ^5.1.23 | 内容编辑 | package.json:45 |
| 国际化 | vue-i18n | 10.0.0-alpha.3 | 多语言支持 | package.json:64 |
| 工具库 | @vueuse/core | ^12.4.0 | 实用工具 | package.json:44 |
| 指纹识别 | @fingerprintjs/fingerprintjs | ^4.6.2 | 设备识别 | package.json:40 |
| 二维码 | qrcode | ^1.5.4 | 二维码生成 | package.json:57 |
| 条码 | jsbarcode | ^3.11.6 | 条码生成 | package.json:51 |
| 打印 | print-js | ^1.6.0 | 页面打印 | package.json:56 |
| 进度条 | nprogress | ^0.2.0 | 加载进度 | package.json:52 |
| 中文转拼音 | pinyin-pro | ^3.26.0 | 拼音转换 | package.json:55 |
| 引导 | driver.js | ^1.3.1 | 功能引导 | package.json:49 |
## 3. 项目结构
### 3.1 后端目录结构
```
d:\phpstudy_pro\WWW\mpay\mpay_v2_webman\
├── app/ # 应用代码
│ ├── common/ # 通用代码
│ │ ├── base/ # 基础类
│ │ │ ├── BaseController.php
│ │ │ ├── BaseModel.php
│ │ │ ├── BaseRepository.php
│ │ │ └── BaseService.php
│ │ ├── constants/ # 常量
│ │ │ └── YesNo.php
│ │ ├── enums/ # 枚举
│ │ │ └── MenuType.php
│ │ ├── middleware/ # 中间件
│ │ │ ├── Cors.php
│ │ │ └── StaticFile.php
│ │ └── utils/ # 工具类
│ │ └── JwtUtil.php
│ ├── events/ # 事件
│ │ └── SystemConfig.php
│ ├── exceptions/ # 异常处理
│ │ └── ValidationException.php
│ ├── http/ # HTTP相关
│ │ ├── admin/ # 后台管理
│ │ │ ├── controller/ # 控制器
│ │ │ │ ├── AuthController.php
│ │ │ │ ├── MenuController.php
│ │ │ │ ├── SystemController.php
│ │ │ │ └── UserController.php
│ │ │ └── middleware/ # 中间件
│ │ │ └── AuthMiddleware.php
│ ├── models/ # 数据模型
│ │ ├── SystemConfig.php
│ │ └── User.php
│ ├── process/ # 进程管理
│ │ ├── Http.php
│ │ └── Monitor.php
│ ├── repositories/ # 数据仓库
│ │ ├── SystemConfigRepository.php
│ │ └── UserRepository.php
│ ├── routes/ # 路由配置
│ │ ├── admin.php
│ │ ├── api.php
│ │ └── mer.php
│ ├── services/ # 业务逻辑
│ │ ├── AuthService.php
│ │ ├── CaptchaService.php
│ │ ├── MenuService.php
│ │ ├── SystemConfigService.php
│ │ ├── SystemSettingService.php
│ │ └── UserService.php
│ └── validation/ # 数据验证
│ └── SystemConfigValidator.php
├── config/ # 配置文件
│ ├── base-config/ # 基础配置
│ │ ├── basic.json
│ │ ├── email.json
│ │ ├── permission.json
│ │ └── tabs.json
│ ├── plugin/ # 插件配置
│ │ ├── webman/
│ │ │ ├── console/
│ │ │ ├── event/
│ │ │ ├── redis-queue/
│ │ │ └── validation/
│ ├── system-file/ # 系统文件
│ │ ├── dict.json
│ │ ├── menu.json
│ │ └── menu.md
│ ├── app.php
│ ├── autoload.php
│ ├── bootstrap.php
│ ├── cache.php
│ ├── container.php
│ ├── database.php
│ ├── dependence.php
│ ├── event.php
│ ├── exception.php
│ ├── jwt.php
│ ├── log.php
│ ├── menu.php
│ ├── middleware.php
│ ├── process.php
│ ├── redis.php
│ ├── route.php
│ ├── server.php
│ ├── session.php
│ ├── static.php
│ ├── translation.php
│ └── view.php
├── database/ # 数据库文件
│ └── ma_system_config.sql
├── doc/ # 文档
│ ├── event.md
│ └── exception.md
├── public/ # 静态资源
│ └── favicon.ico
├── resource/ # 资源文件
│ └── mpay_v2_admin/ # 前端项目
├── .env # 环境变量
├── composer.json # PHP依赖
└── composer.lock # 依赖锁定
```
### 3.2 前端目录结构
```
d:\phpstudy_pro\WWW\mpay\mpay_v2_webman\resource\mpay_v2_admin\
├── src/ # 源代码
│ ├── api/ # API调用
│ ├── assets/ # 静态资源
│ ├── components/ # 组件
│ ├── config/ # 配置
│ ├── directives/ # 指令
│ ├── hooks/ # 钩子
│ ├── lang/ # 国际化
│ ├── layout/ # 布局
│ ├── mock/ # 模拟数据
│ ├── router/ # 路由
│ ├── store/ # 状态管理
│ ├── style/ # 样式
│ ├── typings/ # 类型定义
│ ├── utils/ # 工具函数
│ ├── views/ # 页面
│ ├── App.vue # 根组件
│ ├── auto-import.d.ts # 自动导入
│ ├── components.d.ts # 组件声明
│ ├── main.ts # 入口文件
│ └── style.css # 全局样式
├── build/ # 构建配置
│ ├── optimize.ts
│ └── vite-plugin.ts
├── .env # 环境变量
├── .env.development # 开发环境变量
├── .env.production # 生产环境变量
├── .env.test # 测试环境变量
├── eslint.config.js # ESLint配置
├── index.html # HTML模板
├── package.json # 前端依赖
└── vite.config.ts # Vite配置
```
## 4. 核心功能模块
### 4.1 后端核心模块
| 模块 | 主要功能 | 文件位置 | 来源 |
|------|----------|----------|------|
| 认证模块 | 用户登录、验证码生成 | app/http/admin/controller/AuthController.php | app/routes/admin.php:20-21 |
| 用户模块 | 获取用户信息 | app/http/admin/controller/UserController.php | app/routes/admin.php:26 |
| 菜单模块 | 获取路由菜单 | app/http/admin/controller/MenuController.php | app/routes/admin.php:29 |
| 系统模块 | 字典管理、配置管理 | app/http/admin/controller/SystemController.php | app/routes/admin.php:32-37 |
### 4.2 前端核心模块
| 模块 | 主要功能 | 文件位置 | 来源 |
|------|----------|----------|------|
| 布局模块 | 系统整体布局 | src/layout/ | resource/mpay_v2_admin/src/layout/ |
| 认证模块 | 登录、权限控制 | src/views/login/ | resource/mpay_v2_admin/src/views/ |
| 首页模块 | 数据概览 | src/views/home/ | resource/mpay_v2_admin/src/views/home/ |
| 财务管理 | 结算、对账、发票 | src/views/finance/ | resource/mpay_v2_admin/src/views/finance/ |
| 渠道管理 | 通道配置、支付方式 | src/views/channel/ | resource/mpay_v2_admin/src/views/channel/ |
| 数据分析 | 交易分析、商户分析 | src/views/analysis/ | resource/mpay_v2_admin/src/views/analysis/ |
| 系统设置 | 系统配置、字典管理 | src/views/system/ | resource/mpay_v2_admin/src/views/ |
## 5. API接口设计
### 5.1 认证接口
| 路径 | 方法 | 模块/文件 | 功能 | 权限 | 来源 |
|------|------|-----------|------|------|------|
| /adminapi/captcha | GET | AuthController | 获取验证码 | 无 | app/routes/admin.php:20 |
| /adminapi/login | POST | AuthController | 用户登录 | 无 | app/routes/admin.php:21 |
### 5.2 用户接口
| 路径 | 方法 | 模块/文件 | 功能 | 权限 | 来源 |
|------|------|-----------|------|------|------|
| /adminapi/user/getUserInfo | GET | UserController | 获取用户信息 | JWT | app/routes/admin.php:26 |
### 5.3 菜单接口
| 路径 | 方法 | 模块/文件 | 功能 | 权限 | 来源 |
|------|------|-----------|------|------|------|
| /adminapi/menu/getRouters | GET | MenuController | 获取路由菜单 | JWT | app/routes/admin.php:29 |
### 5.4 系统接口
| 路径 | 方法 | 模块/文件 | 功能 | 权限 | 来源 |
|------|------|-----------|------|------|------|
| /adminapi/system/getDict[/{code}] | GET | SystemController | 获取字典数据 | JWT | app/routes/admin.php:32 |
| /adminapi/system/base-config/tabs | GET | SystemController | 获取配置标签 | JWT | app/routes/admin.php:35 |
| /adminapi/system/base-config/form/{tabKey} | GET | SystemController | 获取表单配置 | JWT | app/routes/admin.php:36 |
| /adminapi/system/base-config/submit/{tabKey} | POST | SystemController | 提交配置 | JWT | app/routes/admin.php:37 |
## 6. 技术特点
### 6.1 后端特点
1. **高性能架构**:基于 Webman 框架,使用 Workerman 作为底层,支持高并发处理
2. **模块化设计**:采用分层架构,清晰分离控制器、服务、仓库和模型
3. **JWT认证**:使用 JSON Web Token 实现无状态认证
4. **中间件机制**:通过中间件实现请求拦截和权限控制
5. **Redis集成**:使用 Redis 作为缓存和队列存储
6. **事件系统**:支持事件驱动架构
7. **定时任务**:内置定时任务管理功能
8. **数据验证**:使用 think-validate 进行数据验证
9. **依赖注入**:使用 PHP-DI 实现依赖注入
10. **日志管理**:使用 Monolog 进行日志管理
### 6.2 前端特点
1. **Vue 3 + TypeScript**:使用最新的 Vue 3 组合式 API 和 TypeScript 提供类型安全
2. **Arco Design**:采用字节跳动开源的 Arco Design UI 组件库,提供美观的界面
3. **Pinia 状态管理**:使用 Pinia 替代 Vuex提供更简洁的状态管理方案
4. **Vite 构建工具**:使用 Vite 提供快速的开发体验和优化的构建输出
5. **国际化支持**:内置多语言支持,可轻松切换语言
6. **响应式设计**:适配不同屏幕尺寸的设备
7. **丰富的功能组件**:集成多种实用组件,如二维码生成、条码生成、富文本编辑等
8. **权限控制**:基于指令的权限控制机制
9. **Mock 数据**:内置 Mock 数据,方便开发和测试
## 7. 开发流程
### 7.1 后端开发
1. **环境准备**PHP 8.1+ComposerMySQLRedis
2. **依赖安装**`composer install`
3. **配置环境**:复制 `.env.example``.env` 并配置相关参数
4. **启动服务**`php start.php start`
5. **代码结构**:遵循 Webman 框架规范,按模块组织代码
### 7.2 前端开发
1. **环境准备**Node.js 18.12+PNPM 8.7+
2. **依赖安装**`pnpm install`
3. **开发模式**`pnpm dev`
4. **构建部署**`pnpm build:prod`
5. **代码结构**:遵循 Vue 3 项目规范,按功能模块组织代码
## 8. 部署与配置
### 8.1 后端部署
1. **服务器要求**Linux/Unix 系统PHP 8.1+MySQL 5.7+Redis 5.0+
2. **Nginx 配置**:配置反向代理指向 Webman 服务
3. **启动方式**
- 开发环境:`php start.php start`
- 生产环境:`php start.php start -d`
4. **监控管理**:可使用 Supervisor 管理进程
### 8.2 前端部署
1. **构建**`pnpm build:prod`
2. **部署**:将 `dist` 目录部署到 Web 服务器
3. **Nginx 配置**:配置静态文件服务和路由重写
## 9. 总结
MPAY V2 项目采用现代化的技术栈和架构设计,后端使用 Webman 框架提供高性能的 API 服务,前端使用 Vue 3 + TypeScript + Arco Design 提供美观、响应式的用户界面。项目结构清晰,模块化程度高,便于维护和扩展。
核心功能覆盖了支付管理系统的主要业务场景,包括用户认证、菜单管理、系统配置、财务管理、渠道管理和数据分析等模块,为支付业务的运营和管理提供了完整的解决方案。
技术特点包括高性能架构、模块化设计、JWT认证、Redis集成、Vue 3组合式API、TypeScript类型安全、Arco Design UI组件库、Pinia状态管理、Vite构建工具等确保了系统的稳定性、安全性和可扩展性。
该项目适合作为支付管理系统的基础框架,可根据具体业务需求进行定制和扩展。

View File

@@ -1,2 +1,38 @@
<?php
// This file is generated by Webman, please don't modify it.
use support\Container;
if (!function_exists('container_get')) {
/**
* 从容器中获取实例(单例)
*
* @param string $name 类名或接口名
* @return mixed
*
* @example
* $foo = container_get(Foo::class);
* $user_service = container_get(UserService::class);
*/
function container_get(string $name)
{
return Container::get($name);
}
}
if (!function_exists('container_make')) {
/**
* 从容器中创建新实例(每次创建新实例)
*
* @param string $name 类名或接口名
* @param array $parameters 构造函数参数
* @return mixed
*
* @example
* $log_service = container_make(LogService::class, [$path, $name]);
*/
function container_make(string $name, array $parameters = [])
{
return Container::make($name, $parameters);
}
}