mirror of
https://gitee.com/technical-laohu/mpay_v2_webman.git
synced 2026-04-04 17:14:26 +08:00
更新后端基础
This commit is contained in:
14
.gitignore
vendored
14
.gitignore
vendored
@@ -1,13 +1,9 @@
|
||||
/runtime
|
||||
/.cursor
|
||||
/.idea
|
||||
/.vscode
|
||||
/vendor
|
||||
*.log
|
||||
.env
|
||||
/tests/tmp
|
||||
/tests/.phpunit.result.cache
|
||||
.kiro
|
||||
plugin
|
||||
|
||||
# 部署相关文件
|
||||
deploy.bat
|
||||
/vendor
|
||||
/runtime
|
||||
*.log
|
||||
.env
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 walkor<walkor@workerman.net> and contributors (see https://github.com/walkor/webman/contributors)
|
||||
Copyright (c) 2026 技术老胡
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -2,41 +2,62 @@
|
||||
|
||||
namespace app\common\base;
|
||||
|
||||
use support\Response;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
|
||||
/**
|
||||
* 控制器基础类
|
||||
* - 提供统一的 success/fail 响应封装
|
||||
* 控制器基础父类
|
||||
*
|
||||
* 约定:
|
||||
* - 控制器统一通过 $this->request->* 获取请求数据
|
||||
* - 为避免每个控制器构造函数重复注入 Request,本类通过 __get('request') 返回当前请求对象
|
||||
* 约定统一的 JSON 返回结构:
|
||||
* {
|
||||
* "code": 200,
|
||||
* "message": "success",
|
||||
* "data": ...
|
||||
* }
|
||||
*/
|
||||
abstract class BaseController
|
||||
class BaseController
|
||||
{
|
||||
|
||||
/**
|
||||
* 成功响应
|
||||
* 成功返回
|
||||
*/
|
||||
protected function success(mixed $data = null, string $message = 'success', int $code = 200): Response
|
||||
{
|
||||
return json([
|
||||
'code' => $code,
|
||||
'code' => $code,
|
||||
'message' => $message,
|
||||
'data' => $data,
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败响应
|
||||
* 失败返回
|
||||
*/
|
||||
protected function fail(string $message = 'fail', int $code = 500, mixed $data = null): Response
|
||||
protected function fail(string $message = 'error', int $code = 500, mixed $data = null): Response
|
||||
{
|
||||
return json([
|
||||
'code' => $code,
|
||||
'code' => $code,
|
||||
'message' => $message,
|
||||
'data' => $data,
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户的 token 载荷
|
||||
*
|
||||
* 从 AuthMiddleware 注入的用户信息中获取
|
||||
*/
|
||||
protected function currentUser(Request $request): ?array
|
||||
{
|
||||
return $request->user ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户ID
|
||||
*/
|
||||
protected function currentUserId(Request $request): int
|
||||
{
|
||||
return (int) ($request->userId ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\base;
|
||||
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use support\Db;
|
||||
|
||||
/**
|
||||
* DAO 基础类
|
||||
* - 封装数据库连接和基础 CRUD 操作
|
||||
* - 提供查询构造器访问
|
||||
*/
|
||||
abstract class BaseDao
|
||||
{
|
||||
/**
|
||||
* 数据库连接名称(子类可覆盖)
|
||||
*/
|
||||
protected string $connection = 'default';
|
||||
|
||||
/**
|
||||
* 表名(子类必须定义)
|
||||
*/
|
||||
protected string $table = '';
|
||||
|
||||
/**
|
||||
* 获取数据库连接
|
||||
*/
|
||||
protected function connection()
|
||||
{
|
||||
return Db::connection($this->connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取查询构造器
|
||||
*/
|
||||
protected function query(): Builder
|
||||
{
|
||||
return Db::connection($this->connection)->table($this->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 查找单条记录
|
||||
*/
|
||||
public function findById(int $id, array $columns = ['*']): ?array
|
||||
{
|
||||
$result = $this->query()->where('id', $id)->first($columns);
|
||||
return $result ? (array)$result : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查找单条记录
|
||||
*/
|
||||
public function findOne(array $where, array $columns = ['*']): ?array
|
||||
{
|
||||
$query = $this->query();
|
||||
foreach ($where as $key => $value) {
|
||||
$query->where($key, $value);
|
||||
}
|
||||
$result = $query->first($columns);
|
||||
return $result ? (array)$result : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查找多条记录
|
||||
*/
|
||||
public function findMany(array $where = [], array $columns = ['*'], array $orderBy = [], int $limit = 0): array
|
||||
{
|
||||
$query = $this->query();
|
||||
foreach ($where as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$query->whereIn($key, $value);
|
||||
} else {
|
||||
$query->where($key, $value);
|
||||
}
|
||||
}
|
||||
foreach ($orderBy as $column => $direction) {
|
||||
$query->orderBy($column, $direction);
|
||||
}
|
||||
if ($limit > 0) {
|
||||
$query->limit($limit);
|
||||
}
|
||||
$results = $query->get($columns);
|
||||
return array_map(fn($item) => (array)$item, $results->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入单条记录
|
||||
*/
|
||||
public function insert(array $data): int
|
||||
{
|
||||
return $this->query()->insertGetId($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入
|
||||
*/
|
||||
public function insertBatch(array $data): bool
|
||||
{
|
||||
return $this->query()->insert($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 更新记录
|
||||
*/
|
||||
public function updateById(int $id, array $data): int
|
||||
{
|
||||
return $this->query()->where('id', $id)->update($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件更新记录
|
||||
*/
|
||||
public function update(array $where, array $data): int
|
||||
{
|
||||
$query = $this->query();
|
||||
foreach ($where as $key => $value) {
|
||||
$query->where($key, $value);
|
||||
}
|
||||
return $query->update($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 删除记录
|
||||
*/
|
||||
public function deleteById(int $id): int
|
||||
{
|
||||
return $this->query()->where('id', $id)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件删除记录
|
||||
*/
|
||||
public function delete(array $where): int
|
||||
{
|
||||
$query = $this->query();
|
||||
foreach ($where as $key => $value) {
|
||||
$query->where($key, $value);
|
||||
}
|
||||
return $query->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计记录数
|
||||
*/
|
||||
public function count(array $where = []): int
|
||||
{
|
||||
$query = $this->query();
|
||||
foreach ($where as $key => $value) {
|
||||
$query->where($key, $value);
|
||||
}
|
||||
return $query->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断记录是否存在
|
||||
*/
|
||||
public function exists(array $where): bool
|
||||
{
|
||||
return $this->count($where) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,100 +2,35 @@
|
||||
|
||||
namespace app\common\base;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use support\Model;
|
||||
|
||||
/**
|
||||
* 模型基础类
|
||||
* - 统一禁用时间戳(如需要可在子类开启)
|
||||
* - 提供常用查询作用域和便捷方法
|
||||
* 所有业务模型的基础父类
|
||||
*/
|
||||
abstract class BaseModel extends Model
|
||||
class BaseModel extends Model
|
||||
{
|
||||
/**
|
||||
* 禁用时间戳(默认)
|
||||
* 约定所有主键字段名
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* 是否自动维护 created_at / updated_at
|
||||
*
|
||||
* 大部分业务表都有这两个字段,如不需要可在子类里覆盖为 false。
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* 允许批量赋值的字段(子类可覆盖)
|
||||
* 默认不禁止任何字段的批量赋值
|
||||
*
|
||||
* 建议在具体模型中按需设置 $fillable 或 $guarded。
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $guarded = [];
|
||||
|
||||
/**
|
||||
* 连接名称(默认使用配置的 default)
|
||||
*/
|
||||
protected $connection = 'default';
|
||||
|
||||
/**
|
||||
* 根据 ID 查找(返回数组格式)
|
||||
*/
|
||||
public static function findById(int $id): ?array
|
||||
{
|
||||
$model = static::find($id);
|
||||
return $model ? $model->toArray() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查找单条(返回数组格式)
|
||||
*/
|
||||
public static function findOne(array $where): ?array
|
||||
{
|
||||
$query = static::query();
|
||||
foreach ($where as $key => $value) {
|
||||
$query->where($key, $value);
|
||||
}
|
||||
$model = $query->first();
|
||||
return $model ? $model->toArray() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查找多条(返回数组格式)
|
||||
*/
|
||||
public static function findMany(array $where = [], array $orderBy = [], int $limit = 0): array
|
||||
{
|
||||
$query = static::query();
|
||||
foreach ($where as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$query->whereIn($key, $value);
|
||||
} else {
|
||||
$query->where($key, $value);
|
||||
}
|
||||
}
|
||||
foreach ($orderBy as $column => $direction) {
|
||||
$query->orderBy($column, $direction);
|
||||
}
|
||||
if ($limit > 0) {
|
||||
$query->limit($limit);
|
||||
}
|
||||
return $query->get()->map(fn($item) => $item->toArray())->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用状态作用域
|
||||
*/
|
||||
public function scopeEnabled(Builder $query): Builder
|
||||
{
|
||||
return $query->where('status', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用状态作用域
|
||||
*/
|
||||
public function scopeDisabled(Builder $query): Builder
|
||||
{
|
||||
return $query->where('status', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为数组(统一处理 null 值)
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$array = parent::toArray();
|
||||
// 将 null 值转换为空字符串(可选,根据业务需求调整)
|
||||
return array_map(fn($value) => $value === null ? '' : $value, $array);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,57 +2,73 @@
|
||||
|
||||
namespace app\common\base;
|
||||
|
||||
use support\Model;
|
||||
|
||||
/**
|
||||
* 仓库基础类
|
||||
* - 支持注入 DAO 依赖
|
||||
* - 通过魔术方法代理 DAO 的方法调用
|
||||
* - 提供通用的数据访问封装
|
||||
* 仓储层基础父类
|
||||
*
|
||||
* 封装单表常用的 CRUD / 分页操作,具体仓储继承后可扩展业务查询。
|
||||
*/
|
||||
abstract class BaseRepository
|
||||
{
|
||||
/**
|
||||
* DAO 实例(可选,子类通过构造函数注入)
|
||||
* @var Model
|
||||
*/
|
||||
protected ?BaseDao $dao = null;
|
||||
protected Model $model;
|
||||
|
||||
/**
|
||||
* 构造函数,子类可注入 DAO
|
||||
*/
|
||||
public function __construct(?BaseDao $dao = null)
|
||||
public function __construct(Model $model)
|
||||
{
|
||||
$this->dao = $dao;
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 魔术方法:代理 DAO 的方法调用
|
||||
* 如果仓库自身没有该方法,且存在 DAO 实例,则调用 DAO 的对应方法
|
||||
* 根据主键查询
|
||||
*/
|
||||
public function __call(string $method, array $arguments)
|
||||
public function find(int $id, array $columns = ['*']): ?Model
|
||||
{
|
||||
if ($this->dao && method_exists($this->dao, $method)) {
|
||||
return $this->dao->{$method}(...$arguments);
|
||||
return $this->model->newQuery()->find($id, $columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建记录
|
||||
*/
|
||||
public function create(array $data): Model
|
||||
{
|
||||
return $this->model->newQuery()->create($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按主键更新
|
||||
*/
|
||||
public function updateById(int $id, array $data): bool
|
||||
{
|
||||
return (bool) $this->model->newQuery()->whereKey($id)->update($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按主键删除
|
||||
*/
|
||||
public function deleteById(int $id): bool
|
||||
{
|
||||
return (bool) $this->model->newQuery()->whereKey($id)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单分页查询示例
|
||||
*
|
||||
* @param array $where ['字段' => 值],值为 null / '' 时会被忽略
|
||||
*/
|
||||
public function paginate(array $where = [], int $page = 1, int $pageSize = 10, array $columns = ['*'])
|
||||
{
|
||||
$query = $this->model->newQuery();
|
||||
|
||||
foreach ($where as $field => $value) {
|
||||
if ($value === null || $value === '') {
|
||||
continue;
|
||||
}
|
||||
$query->where($field, $value);
|
||||
}
|
||||
|
||||
throw new \BadMethodCallException(
|
||||
sprintf('Method %s::%s does not exist', static::class, $method)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 DAO 是否已注入
|
||||
*/
|
||||
protected function hasDao(): bool
|
||||
{
|
||||
return $this->dao !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 DAO 实例
|
||||
*/
|
||||
protected function getDao(): ?BaseDao
|
||||
{
|
||||
return $this->dao;
|
||||
return $query->paginate($pageSize, $columns, 'page', $page);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,55 +2,23 @@
|
||||
|
||||
namespace app\common\base;
|
||||
|
||||
use support\Log;
|
||||
use support\Db;
|
||||
|
||||
/**
|
||||
* 服务基础类
|
||||
* - 提供日志记录能力
|
||||
* - 预留事务、事件发布等扩展点
|
||||
* 业务服务层基础父类
|
||||
*/
|
||||
abstract class BaseService
|
||||
class BaseService
|
||||
{
|
||||
/**
|
||||
* 记录日志
|
||||
* 事务封装
|
||||
*
|
||||
* 使用方式:
|
||||
* $this->transaction(function () { ... });
|
||||
*/
|
||||
protected function log(string $level, string $message, array $context = []): void
|
||||
protected function transaction(callable $callback)
|
||||
{
|
||||
$logMessage = sprintf('[%s] %s', static::class, $message);
|
||||
Log::log($level, $logMessage, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录信息日志
|
||||
*/
|
||||
protected function info(string $message, array $context = []): void
|
||||
{
|
||||
$this->log('info', $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录警告日志
|
||||
*/
|
||||
protected function warning(string $message, array $context = []): void
|
||||
{
|
||||
$this->log('warning', $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录错误日志
|
||||
*/
|
||||
protected function error(string $message, array $context = []): void
|
||||
{
|
||||
$this->log('error', $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录调试日志
|
||||
*/
|
||||
protected function debug(string $message, array $context = []): void
|
||||
{
|
||||
$this->log('debug', $message, $context);
|
||||
return Db::connection()->transaction(function () use ($callback) {
|
||||
return $callback();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
22
app/common/constants/CommonStatus.php
Normal file
22
app/common/constants/CommonStatus.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?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;
|
||||
}
|
||||
|
||||
|
||||
32
app/common/constants/DictCode.php
Normal file
32
app/common/constants/DictCode.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?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';
|
||||
}
|
||||
|
||||
|
||||
22
app/common/constants/Permission.php
Normal file
22
app/common/constants/Permission.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?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';
|
||||
}
|
||||
|
||||
|
||||
22
app/common/constants/RoleCode.php
Normal file
22
app/common/constants/RoleCode.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\constants;
|
||||
|
||||
/**
|
||||
* 角色编码常量
|
||||
* 对应表:roles.code
|
||||
*/
|
||||
class RoleCode
|
||||
{
|
||||
/**
|
||||
* 超级管理员
|
||||
*/
|
||||
public const ADMIN = 'admin';
|
||||
|
||||
/**
|
||||
* 普通员工
|
||||
*/
|
||||
public const COMMON = 'common';
|
||||
}
|
||||
|
||||
|
||||
15
app/common/constants/YesNo.php
Normal file
15
app/common/constants/YesNo.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\constants;
|
||||
|
||||
/**
|
||||
* 通用是/否布尔枚举
|
||||
* 可复用在 is_admin 等字段
|
||||
*/
|
||||
class YesNo
|
||||
{
|
||||
public const NO = 0;
|
||||
public const YES = 1;
|
||||
}
|
||||
|
||||
|
||||
16
app/common/enums/CronLogStatus.php
Normal file
16
app/common/enums/CronLogStatus.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\enums;
|
||||
|
||||
/**
|
||||
* 定时任务日志状态
|
||||
* 对应表:cron_logs.status
|
||||
* 1 成功 0 失败
|
||||
*/
|
||||
class CronLogStatus
|
||||
{
|
||||
public const FAIL = 0;
|
||||
public const SUCCESS = 1;
|
||||
}
|
||||
|
||||
|
||||
23
app/common/enums/CronMisfirePolicy.php
Normal file
23
app/common/enums/CronMisfirePolicy.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\enums;
|
||||
|
||||
/**
|
||||
* 定时任务执行策略(misfire_policy)
|
||||
* 对应表:cron_jobs.misfire_policy
|
||||
* 1 循环执行 2 执行一次
|
||||
*/
|
||||
class CronMisfirePolicy
|
||||
{
|
||||
/**
|
||||
* 循环执行
|
||||
*/
|
||||
public const LOOP = 1;
|
||||
|
||||
/**
|
||||
* 只执行一次
|
||||
*/
|
||||
public const RUN_ONCE = 2;
|
||||
}
|
||||
|
||||
|
||||
16
app/common/enums/CronTaskType.php
Normal file
16
app/common/enums/CronTaskType.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\enums;
|
||||
|
||||
/**
|
||||
* 定时任务类型
|
||||
* 对应表:cron_jobs.task_type
|
||||
* 0 cron 表达式 1 时间间隔(秒)
|
||||
*/
|
||||
class CronTaskType
|
||||
{
|
||||
public const CRON_EXPRESSION = 0;
|
||||
public const INTERVAL_SECOND = 1;
|
||||
}
|
||||
|
||||
|
||||
28
app/common/enums/MenuType.php
Normal file
28
app/common/enums/MenuType.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\enums;
|
||||
|
||||
/**
|
||||
* 菜单类型枚举
|
||||
* 对应表:menus.type
|
||||
* 1 目录 2 菜单 3 按钮
|
||||
*/
|
||||
class MenuType
|
||||
{
|
||||
/**
|
||||
* 目录
|
||||
*/
|
||||
public const DIRECTORY = 1;
|
||||
|
||||
/**
|
||||
* 菜单
|
||||
*/
|
||||
public const MENU = 2;
|
||||
|
||||
/**
|
||||
* 按钮(权限点)
|
||||
*/
|
||||
public const BUTTON = 3;
|
||||
}
|
||||
|
||||
|
||||
27
app/common/enums/UserSex.php
Normal file
27
app/common/enums/UserSex.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\enums;
|
||||
|
||||
/**
|
||||
* 用户性别枚举
|
||||
* 对应表:users.sex 以及 gender 字典
|
||||
*/
|
||||
class UserSex
|
||||
{
|
||||
/**
|
||||
* 女
|
||||
*/
|
||||
public const FEMALE = 0;
|
||||
|
||||
/**
|
||||
* 男
|
||||
*/
|
||||
public const MALE = 1;
|
||||
|
||||
/**
|
||||
* 未知/其它
|
||||
*/
|
||||
public const UNKNOWN = 2;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,13 +22,11 @@ class Cors implements MiddlewareInterface
|
||||
{
|
||||
$response = strtoupper($request->method()) === 'OPTIONS' ? response('', 204) : $handler($request);
|
||||
|
||||
$response->withHeaders([
|
||||
return $response->withHeaders([
|
||||
'Access-Control-Allow-Credentials' => 'true',
|
||||
'Access-Control-Allow-Origin' => $request->header('origin', '*'),
|
||||
'Access-Control-Allow-Methods' => $request->header('access-control-request-method', '*'),
|
||||
'Access-Control-Allow-Headers' => $request->header('access-control-request-headers', '*'),
|
||||
]);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace app\services\auth;
|
||||
namespace app\common\utils;
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
|
||||
class JwtService
|
||||
class JwtUtil
|
||||
{
|
||||
/**
|
||||
* 生成 JWT
|
||||
@@ -47,6 +47,15 @@ class JwtService
|
||||
$config = config('jwt', []);
|
||||
return (int)($config['ttl'] ?? 7200);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存前缀
|
||||
*/
|
||||
public static function getCachePrefix(): string
|
||||
{
|
||||
$config = config('jwt', []);
|
||||
return $config['cache_prefix'] ?? 'token_';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\exceptions;
|
||||
|
||||
/**
|
||||
* 认证失败(账号或密码错误)
|
||||
*/
|
||||
class AuthFailedException extends BusinessException
|
||||
{
|
||||
public function __construct(string $message = '账号或者密码错误', int $bizCode = 400, mixed $data = null)
|
||||
{
|
||||
parent::__construct($message, $bizCode, $data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\exceptions;
|
||||
|
||||
/**
|
||||
* 业务基础异常
|
||||
*
|
||||
* 说明:
|
||||
* - 继承 webman 的 BusinessException,让框架自动捕获并渲染
|
||||
* - 重写 render() 以对齐前端期望字段:code/message/data
|
||||
*/
|
||||
class BusinessException extends \support\exception\BusinessException
|
||||
{
|
||||
public function __construct(string $message = '', int $bizCode = 500, array $data = [])
|
||||
{
|
||||
parent::__construct($message, $bizCode);
|
||||
$this->data($data);
|
||||
}
|
||||
|
||||
public function getBizCode(): int
|
||||
{
|
||||
return (int)$this->getCode();
|
||||
}
|
||||
|
||||
// 保持与 webman BusinessException 方法签名兼容
|
||||
public function getData(): array
|
||||
{
|
||||
return parent::getData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义渲染
|
||||
* - json 请求:返回 {code,message,data}
|
||||
* - 非 json:返回文本
|
||||
*/
|
||||
public function render(\Webman\Http\Request $request): ?\Webman\Http\Response
|
||||
{
|
||||
if ($request->expectsJson()) {
|
||||
return json([
|
||||
'code' => $this->getBizCode() ?: 500,
|
||||
'message' => $this->getMessage(),
|
||||
'data' => $this->getData(),
|
||||
]);
|
||||
}
|
||||
return new \Webman\Http\Response(200, [], $this->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\exceptions;
|
||||
|
||||
/**
|
||||
* 权限不足
|
||||
*/
|
||||
class ForbiddenException extends BusinessException
|
||||
{
|
||||
public function __construct(string $message = '无访问权限', int $bizCode = 403, mixed $data = null)
|
||||
{
|
||||
parent::__construct($message, $bizCode, $data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\exceptions;
|
||||
|
||||
/**
|
||||
* 未认证或登录过期
|
||||
*/
|
||||
class UnauthorizedException extends BusinessException
|
||||
{
|
||||
public function __construct(string $message = '登录状态已过期', int $bizCode = 401, mixed $data = null)
|
||||
{
|
||||
parent::__construct($message, $bizCode, $data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,15 +2,23 @@
|
||||
|
||||
namespace app\exceptions;
|
||||
|
||||
use Webman\Exception\BusinessException;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
|
||||
/**
|
||||
* 参数校验异常
|
||||
* 最常用的异常类型,用于参数验证、业务规则验证等
|
||||
*
|
||||
* 示例:
|
||||
* throw new ValidationException('优惠券和会员不可叠加使用');
|
||||
* throw new ValidationException('手机号格式不正确');
|
||||
* throw new ValidationException('金额必须大于0');
|
||||
*/
|
||||
class ValidationException extends BusinessException
|
||||
{
|
||||
public function __construct(string $message = '参数校验失败', int $bizCode = 422, mixed $data = null)
|
||||
public function __construct(string $message = '参数校验失败', int $bizCode = 422, array $data = [])
|
||||
{
|
||||
parent::__construct($message, $bizCode, $data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,46 +3,63 @@
|
||||
namespace app\http\admin\controller;
|
||||
|
||||
use app\common\base\BaseController;
|
||||
use app\services\AuthService;
|
||||
use app\services\CaptchaService;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use app\services\auth\AuthService;
|
||||
|
||||
/**
|
||||
* 认证控制器
|
||||
*
|
||||
* 处理登录、验证码等认证相关接口
|
||||
*/
|
||||
class AuthController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
protected CaptchaService $captchaService,
|
||||
protected AuthService $authService
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理后台登录
|
||||
* GET /captcha
|
||||
*
|
||||
* 生成验证码
|
||||
*/
|
||||
public function login(Request $request): Response
|
||||
public function captcha(Request $request)
|
||||
{
|
||||
$username = (string)$request->post('username', '');
|
||||
$password = (string)$request->post('password', '');
|
||||
// 前端有本地验证码,这里暂不做服务端校验,仅预留字段
|
||||
$verifyCode = $request->post('verifyCode');
|
||||
try {
|
||||
$data = $this->captchaService->generate();
|
||||
return $this->success($data);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->fail('验证码生成失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
if ($username === '' || $password === '') {
|
||||
return $this->fail('账号或密码不能为空', 400);
|
||||
/**
|
||||
* POST /login
|
||||
*
|
||||
* 用户登录
|
||||
*/
|
||||
public function login(Request $request)
|
||||
{
|
||||
$username = $request->post('username', '');
|
||||
$password = $request->post('password', '');
|
||||
$verifyCode = $request->post('verifyCode', '');
|
||||
$captchaId = $request->post('captchaId', '');
|
||||
|
||||
// 参数校验
|
||||
if (empty($username) || empty($password) || empty($verifyCode) || empty($captchaId)) {
|
||||
return $this->fail('请填写完整登录信息', 400);
|
||||
}
|
||||
|
||||
$token = $this->authService->login($username, $password, $verifyCode);
|
||||
|
||||
return $this->success(['token' => $token]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户信息
|
||||
*/
|
||||
public function getUserInfo(Request $request): Response
|
||||
{
|
||||
// 前端在 Authorization 中直接传 token
|
||||
$token = (string)$request->header('authorization', '');
|
||||
$id = $request->get('id');
|
||||
|
||||
$data = $this->authService->getUserInfo($token, $id);
|
||||
|
||||
return $this->success($data);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
53
app/http/admin/controller/MenuController.php
Normal file
53
app/http/admin/controller/MenuController.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace app\http\admin\controller;
|
||||
|
||||
use app\common\base\BaseController;
|
||||
use support\Request;
|
||||
|
||||
/**
|
||||
* 菜单控制器
|
||||
*/
|
||||
class MenuController extends BaseController
|
||||
{
|
||||
public function getRouters()
|
||||
{
|
||||
// 获取菜单数据并转换为树形结构
|
||||
$routers = $this->buildMenuTree($this->getSystemMenu());
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
46
app/http/admin/controller/SystemController.php
Normal file
46
app/http/admin/controller/SystemController.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace app\http\admin\controller;
|
||||
|
||||
use app\common\base\BaseController;
|
||||
use support\Request;
|
||||
|
||||
/**
|
||||
* 系统控制器
|
||||
*/
|
||||
class SystemController extends BaseController
|
||||
{
|
||||
/**
|
||||
* GET /system/getDict
|
||||
* GET /system/getDict/{code}
|
||||
*
|
||||
* 获取字典数据
|
||||
* 支持通过路由参数 code 查询指定字典,不传则返回所有字典
|
||||
*
|
||||
* 示例:
|
||||
* GET /adminapi/system/getDict - 返回所有字典
|
||||
* GET /adminapi/system/getDict/gender - 返回性别字典
|
||||
* GET /adminapi/system/getDict/status - 返回状态字典
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
// 返回所有字典
|
||||
return $this->success($allDicts);
|
||||
}
|
||||
}
|
||||
|
||||
44
app/http/admin/controller/UserController.php
Normal file
44
app/http/admin/controller/UserController.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace app\http\admin\controller;
|
||||
|
||||
use app\common\base\BaseController;
|
||||
use app\services\UserService;
|
||||
use support\Request;
|
||||
|
||||
/**
|
||||
* 用户接口示例控制器
|
||||
*
|
||||
* 主要用于演示 BaseController / Service / Repository / Model 的调用链路。
|
||||
*/
|
||||
class UserController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
protected UserService $userService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /user/getUserInfo
|
||||
*
|
||||
* 从 JWT token 中获取当前登录用户信息
|
||||
* 前端通过 Authorization: Bearer {token} 请求头传递 token
|
||||
*/
|
||||
public function getUserInfo(Request $request)
|
||||
{
|
||||
// 从JWT中间件注入的用户信息中获取用户ID
|
||||
$userId = $this->currentUserId($request);
|
||||
|
||||
if ($userId <= 0) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
81
app/http/admin/middleware/AuthMiddleware.php
Normal file
81
app/http/admin/middleware/AuthMiddleware.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace app\http\admin\middleware;
|
||||
|
||||
use Webman\MiddlewareInterface;
|
||||
use Webman\Http\Response;
|
||||
use Webman\Http\Request;
|
||||
use app\common\utils\JwtUtil;
|
||||
|
||||
/**
|
||||
* JWT 认证中间件
|
||||
*
|
||||
* 验证请求中的 JWT token,并将用户信息注入到请求对象中
|
||||
*/
|
||||
class AuthMiddleware implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* 处理请求
|
||||
* @param Request $request 请求对象
|
||||
* @param callable $handler 下一个中间件处理函数
|
||||
* @return Response 响应对象
|
||||
*/
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
// 从请求头中获取 token
|
||||
$auth = $request->header('Authorization', '');
|
||||
if (!$auth) {
|
||||
return $this->unauthorized('缺少认证令牌');
|
||||
}
|
||||
|
||||
// 兼容 "Bearer xxx" 或直接 "xxx"
|
||||
if (str_starts_with($auth, 'Bearer ')) {
|
||||
$token = substr($auth, 7);
|
||||
} else {
|
||||
$token = $auth;
|
||||
}
|
||||
|
||||
if (!$token) {
|
||||
return $this->unauthorized('认证令牌格式错误');
|
||||
}
|
||||
|
||||
try {
|
||||
// 解析 JWT token
|
||||
$payload = JwtUtil::parseToken($token);
|
||||
|
||||
if (empty($payload) || !isset($payload['user_id'])) {
|
||||
return $this->unauthorized('认证令牌无效');
|
||||
}
|
||||
|
||||
// 将用户信息存储到请求对象中,供控制器使用
|
||||
$request->user = $payload;
|
||||
$request->userId = (int) ($payload['user_id'] ?? 0);
|
||||
|
||||
// 继续处理请求
|
||||
return $handler($request);
|
||||
} catch (\Throwable $e) {
|
||||
// 根据异常类型返回不同的错误信息
|
||||
$message = $e->getMessage();
|
||||
if (str_contains($message, 'expired') || str_contains($message, 'Expired')) {
|
||||
return $this->unauthorized('认证令牌已过期', 401);
|
||||
} elseif (str_contains($message, 'signature') || str_contains($message, 'Signature')) {
|
||||
return $this->unauthorized('认证令牌签名无效', 401);
|
||||
} else {
|
||||
return $this->unauthorized('认证令牌验证失败:' . $message, 401);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回未授权响应
|
||||
*/
|
||||
private function unauthorized(string $message, int $code = 401): Response
|
||||
{
|
||||
return json([
|
||||
'code' => $code,
|
||||
'message' => $message,
|
||||
'data' => null,
|
||||
], $code);
|
||||
}
|
||||
}
|
||||
|
||||
30
app/models/User.php
Normal file
30
app/models/User.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace app\models;
|
||||
|
||||
use app\common\base\BaseModel;
|
||||
|
||||
/**
|
||||
* 用户模型
|
||||
*
|
||||
* 对应表:users
|
||||
*/
|
||||
class User extends BaseModel
|
||||
{
|
||||
/**
|
||||
* 表名
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'users';
|
||||
|
||||
/**
|
||||
* 关联角色(多对多)
|
||||
*/
|
||||
public function roles()
|
||||
{
|
||||
return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\repositories;
|
||||
|
||||
use app\common\base\BaseRepository;
|
||||
use app\common\base\BaseDao;
|
||||
|
||||
/**
|
||||
* 管理后台用户仓库(当前阶段使用内存数据模拟)
|
||||
*
|
||||
* 后续接入数据库时:
|
||||
* 1. 创建 AdminUserDao 继承 BaseDao
|
||||
* 2. 在构造函数中注入:public function __construct(AdminUserDao $dao) { parent::__construct($dao); }
|
||||
* 3. 将内存数据方法改为调用 $this->dao 的方法
|
||||
*/
|
||||
class AdminUserRepository extends BaseRepository
|
||||
{
|
||||
/**
|
||||
* 构造函数:支持注入 DAO(当前阶段为可选)
|
||||
*/
|
||||
public function __construct(?BaseDao $dao = null)
|
||||
{
|
||||
parent::__construct($dao);
|
||||
}
|
||||
/**
|
||||
* 模拟账户数据(对齐前端 mock accountData)
|
||||
*/
|
||||
protected function accounts(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'id' => 1,
|
||||
'deptId' => '100',
|
||||
'deptName' => '研发部门',
|
||||
'userName' => 'admin',
|
||||
'nickName' => '超级管理员',
|
||||
'email' => '2547096351@qq.com',
|
||||
'phone' => '15888888888',
|
||||
'sex' => 1,
|
||||
'avatar' => 'https://ooo.0x0.ooo/2025/04/10/O0dG7r.jpg',
|
||||
'status' => 1,
|
||||
'description' => '系统初始用户',
|
||||
'roles' => ['admin'],
|
||||
'loginIp' => '0:0:0:0:0:0:0:1',
|
||||
'loginDate' => '2025-03-31 10:30:59',
|
||||
'createBy' => 'admin',
|
||||
'createTime' => '2024-03-19 11:21:01',
|
||||
'updateBy' => null,
|
||||
'updateTime' => null,
|
||||
'admin' => true,
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'deptId' => '100010101',
|
||||
'deptName' => '研发部门',
|
||||
'userName' => 'common',
|
||||
'nickName' => '普通用户',
|
||||
'email' => '2547096351@qq.com',
|
||||
'phone' => '15222222222',
|
||||
'sex' => 0,
|
||||
'avatar' => 'https://ooo.0x0.ooo/2025/04/10/O0ddJI.jpg',
|
||||
'status' => 1,
|
||||
'description' => 'UI组用户',
|
||||
'roles' => ['common'],
|
||||
'loginIp' => '0:0:0:0:0:0:0:1',
|
||||
'loginDate' => '2025-03-31 10:30:59',
|
||||
'createBy' => 'admin',
|
||||
'createTime' => '2024-03-19 11:21:01',
|
||||
'updateBy' => null,
|
||||
'updateTime' => null,
|
||||
'admin' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟角色数据(对齐前端 mock roleData)
|
||||
*/
|
||||
protected function roles(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'id' => 1,
|
||||
'name' => '超级管理员',
|
||||
'code' => 'admin',
|
||||
'sort' => 1,
|
||||
'status' => 1,
|
||||
'admin' => true,
|
||||
'description' => '默认角色,超级管理员,上帝角色',
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'name' => '普通员工',
|
||||
'code' => 'common',
|
||||
'sort' => 2,
|
||||
'status' => 1,
|
||||
'admin' => false,
|
||||
'description' => '负责一些基础功能',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟权限数据(对齐前端 mock permissionData)
|
||||
*/
|
||||
protected function permissions(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'meta' => [
|
||||
'roles' => ['admin'],
|
||||
'permission' => 'sys:btn:add',
|
||||
],
|
||||
],
|
||||
[
|
||||
'meta' => [
|
||||
'roles' => ['admin'],
|
||||
'permission' => 'sys:btn:edit',
|
||||
],
|
||||
],
|
||||
[
|
||||
'meta' => [
|
||||
'roles' => ['admin'],
|
||||
'permission' => 'sys:btn:delete',
|
||||
],
|
||||
],
|
||||
[
|
||||
'meta' => [
|
||||
'roles' => ['admin', 'common'],
|
||||
'permission' => 'common:btn:add',
|
||||
],
|
||||
],
|
||||
[
|
||||
'meta' => [
|
||||
'roles' => ['admin', 'common'],
|
||||
'permission' => 'common:btn:edit',
|
||||
],
|
||||
],
|
||||
[
|
||||
'meta' => [
|
||||
'roles' => ['admin', 'common'],
|
||||
'permission' => 'common:btn:delete',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function findByUsername(string $username): ?array
|
||||
{
|
||||
foreach ($this->accounts() as $account) {
|
||||
if ($account['userName'] === $username) {
|
||||
return $account;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function findById(int $id): ?array
|
||||
{
|
||||
foreach ($this->accounts() as $account) {
|
||||
if ($account['id'] === $id) {
|
||||
return $account;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getRoleInfoByCodes(array $codes): array
|
||||
{
|
||||
if (!$codes) {
|
||||
return [];
|
||||
}
|
||||
$roles = [];
|
||||
foreach ($this->roles() as $role) {
|
||||
if (in_array($role['code'], $codes, true)) {
|
||||
$roles[] = $role;
|
||||
}
|
||||
}
|
||||
return $roles;
|
||||
}
|
||||
|
||||
public function getPermissionsByRoleCodes(array $codes): array
|
||||
{
|
||||
if (!$codes) {
|
||||
return [];
|
||||
}
|
||||
$permissions = [];
|
||||
foreach ($this->permissions() as $item) {
|
||||
$meta = $item['meta'] ?? [];
|
||||
$roles = $meta['roles'] ?? [];
|
||||
$permission = $meta['permission'] ?? null;
|
||||
if (!$permission) {
|
||||
continue;
|
||||
}
|
||||
foreach ($codes as $code) {
|
||||
if (in_array($code, $roles, true)) {
|
||||
$permissions[] = $permission;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 去重
|
||||
return array_values(array_unique($permissions));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
19
app/repositories/CronJobRepository.php
Normal file
19
app/repositories/CronJobRepository.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace app\repositories;
|
||||
|
||||
use app\common\base\BaseRepository;
|
||||
use app\models\CronJob;
|
||||
|
||||
/**
|
||||
* 定时任务仓储
|
||||
*/
|
||||
class CronJobRepository extends BaseRepository
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(new CronJob());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
19
app/repositories/CronLogRepository.php
Normal file
19
app/repositories/CronLogRepository.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace app\repositories;
|
||||
|
||||
use app\common\base\BaseRepository;
|
||||
use app\models\CronLog;
|
||||
|
||||
/**
|
||||
* 定时任务日志仓储
|
||||
*/
|
||||
class CronLogRepository extends BaseRepository
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(new CronLog());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
19
app/repositories/DepartmentRepository.php
Normal file
19
app/repositories/DepartmentRepository.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace app\repositories;
|
||||
|
||||
use app\common\base\BaseRepository;
|
||||
use app\models\Department;
|
||||
|
||||
/**
|
||||
* 部门仓储
|
||||
*/
|
||||
class DepartmentRepository extends BaseRepository
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(new Department());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
19
app/repositories/DictGroupRepository.php
Normal file
19
app/repositories/DictGroupRepository.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace app\repositories;
|
||||
|
||||
use app\common\base\BaseRepository;
|
||||
use app\models\DictGroup;
|
||||
|
||||
/**
|
||||
* 字典分组仓储
|
||||
*/
|
||||
class DictGroupRepository extends BaseRepository
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(new DictGroup());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
19
app/repositories/DictItemRepository.php
Normal file
19
app/repositories/DictItemRepository.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace app\repositories;
|
||||
|
||||
use app\common\base\BaseRepository;
|
||||
use app\models\DictItem;
|
||||
|
||||
/**
|
||||
* 字典项仓储
|
||||
*/
|
||||
class DictItemRepository extends BaseRepository
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(new DictItem());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
54
app/repositories/MenuRepository.php
Normal file
54
app/repositories/MenuRepository.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
19
app/repositories/RoleRepository.php
Normal file
19
app/repositories/RoleRepository.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace app\repositories;
|
||||
|
||||
use app\common\base\BaseRepository;
|
||||
use app\models\Role;
|
||||
|
||||
/**
|
||||
* 角色仓储
|
||||
*/
|
||||
class RoleRepository extends BaseRepository
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(new Role());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
47
app/repositories/UserRepository.php
Normal file
47
app/repositories/UserRepository.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace app\repositories;
|
||||
|
||||
use app\common\base\BaseRepository;
|
||||
use app\models\User;
|
||||
|
||||
/**
|
||||
* 用户仓储
|
||||
*/
|
||||
class UserRepository extends BaseRepository
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(new User());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户名查询用户
|
||||
*/
|
||||
public function findByUserName(string $userName): ?User
|
||||
{
|
||||
/** @var User|null $user */
|
||||
$user = $this->model
|
||||
->newQuery()
|
||||
->where('user_name', $userName)
|
||||
->first();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据主键查询并预加载角色
|
||||
*/
|
||||
public function findWithRoles(int $id): ?User
|
||||
{
|
||||
/** @var User|null $user */
|
||||
$user = $this->model
|
||||
->newQuery()
|
||||
->with('roles')
|
||||
->find($id);
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,33 @@
|
||||
|
||||
/**
|
||||
* 管理后台路由定义
|
||||
*
|
||||
* 接口前缀:/adminapi
|
||||
* 跨域中间件:Cors
|
||||
*/
|
||||
|
||||
use Webman\Route;
|
||||
use app\http\admin\controller\AuthController;
|
||||
use app\http\admin\controller\UserController;
|
||||
use app\http\admin\controller\MenuController;
|
||||
use app\http\admin\controller\SystemController;
|
||||
use app\common\middleware\Cors;
|
||||
use app\http\admin\middleware\AuthMiddleware;
|
||||
|
||||
Route::group('/admin', function () {
|
||||
// 登录相关
|
||||
Route::post('/mock/login', [AuthController::class, 'login']);
|
||||
Route::get('/mock/user/getUserInfo', [AuthController::class, 'getUserInfo']);
|
||||
});
|
||||
Route::group('/adminapi', function () {
|
||||
// 认证相关(无需JWT验证)
|
||||
Route::get('/captcha', [AuthController::class, 'captcha']);
|
||||
Route::post('/login', [AuthController::class, 'login']);
|
||||
|
||||
// 需要认证的路由组
|
||||
Route::group('', function () {
|
||||
// 用户相关(需要JWT验证)
|
||||
Route::get('/user/getUserInfo', [UserController::class, 'getUserInfo']);
|
||||
|
||||
// 菜单相关(需要JWT验证)
|
||||
Route::get('/menu/getRouters', [MenuController::class, 'getRouters']);
|
||||
|
||||
// 系统相关(需要JWT验证)
|
||||
Route::get('/system/getDict[/{code}]', [SystemController::class, 'getDict']);
|
||||
})->middleware([AuthMiddleware::class]);
|
||||
})->middleware([Cors::class]);
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* API 路由定义
|
||||
*/
|
||||
|
||||
use Webman\Route;
|
||||
|
||||
|
||||
143
app/services/AuthService.php
Normal file
143
app/services/AuthService.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace app\services;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\utils\JwtUtil;
|
||||
use app\repositories\UserRepository;
|
||||
use support\Cache;
|
||||
|
||||
/**
|
||||
* 认证服务
|
||||
*
|
||||
* 处理登录、token 生成等认证相关业务
|
||||
*/
|
||||
class AuthService extends BaseService
|
||||
{
|
||||
public function __construct(
|
||||
protected UserRepository $userRepository,
|
||||
protected CaptchaService $captchaService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*
|
||||
* 登录成功后返回 token,前端使用该 token 通过 Authorization 请求头访问需要认证的接口
|
||||
*
|
||||
* @param string $username 用户名
|
||||
* @param string $password 密码
|
||||
* @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);
|
||||
}
|
||||
|
||||
// 2. 查询用户
|
||||
$user = $this->userRepository->findByUserName($username);
|
||||
if (!$user) {
|
||||
throw new \RuntimeException('账号或密码错误', 401);
|
||||
}
|
||||
|
||||
// 3. 校验密码
|
||||
if (!$this->validatePassword($password, $user->password)) {
|
||||
throw new \RuntimeException('账号或密码错误', 401);
|
||||
}
|
||||
|
||||
// 4. 检查用户状态
|
||||
if ($user->status !== 1) {
|
||||
throw new \RuntimeException('账号已被禁用', 403);
|
||||
}
|
||||
|
||||
// 5. 生成 JWT token(包含用户ID、用户名、昵称等信息)
|
||||
$token = $this->generateToken($user);
|
||||
|
||||
// 6. 将 token 信息存入 Redis(用于后续刷新、黑名单等)
|
||||
$this->cacheToken($token, $user->id);
|
||||
|
||||
// 7. 更新用户最后登录信息
|
||||
$this->updateLoginInfo($user);
|
||||
|
||||
// 返回 token,前端使用该 token 访问需要认证的接口
|
||||
return [
|
||||
'token' => $token,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验密码
|
||||
*
|
||||
* @param string $password 明文密码
|
||||
* @param string|null $hash 数据库中的密码hash
|
||||
* @return bool
|
||||
*/
|
||||
private function validatePassword(string $password, ?string $hash): bool
|
||||
{
|
||||
// 如果数据库密码为空,允许使用默认密码(仅用于开发/演示)
|
||||
if ($hash === null || $hash === '') {
|
||||
// 开发环境:允许 admin/123456 和 common/123456 无密码登录
|
||||
// 生产环境应移除此逻辑
|
||||
return in_array($password, ['123456'], true);
|
||||
}
|
||||
|
||||
return password_verify($password, $hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 JWT token
|
||||
*
|
||||
* @param \app\models\User $user
|
||||
* @return string
|
||||
*/
|
||||
private function generateToken($user): string
|
||||
{
|
||||
$payload = [
|
||||
'user_id' => $user->id,
|
||||
'user_name' => $user->user_name,
|
||||
'nick_name' => $user->nick_name,
|
||||
];
|
||||
|
||||
return JwtUtil::generateToken($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 token 信息缓存到 Redis
|
||||
*
|
||||
* @param string $token
|
||||
* @param int $userId
|
||||
*/
|
||||
private function cacheToken(string $token, int $userId): void
|
||||
{
|
||||
$key = JwtUtil::getCachePrefix() . $token;
|
||||
$data = [
|
||||
'user_id' => $userId,
|
||||
'created_at' => time(),
|
||||
];
|
||||
Cache::set($key, $data, JwtUtil::getTtl());
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户登录信息
|
||||
*
|
||||
* @param \app\models\User $user
|
||||
*/
|
||||
private function updateLoginInfo($user): void
|
||||
{
|
||||
// 获取客户端真实IP(优先使用 x-real-ip,其次 x-forwarded-for,最后 remoteIp)
|
||||
$request = request();
|
||||
$ip = $request->header('x-real-ip', '')
|
||||
?: ($request->header('x-forwarded-for', '') ? explode(',', $request->header('x-forwarded-for', ''))[0] : '')
|
||||
?: $request->getRemoteIp();
|
||||
|
||||
$user->login_ip = trim($ip);
|
||||
$user->login_at = date('Y-m-d H:i:s');
|
||||
$user->save();
|
||||
}
|
||||
}
|
||||
|
||||
101
app/services/CaptchaService.php
Normal file
101
app/services/CaptchaService.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace app\services;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use support\Cache;
|
||||
use Webman\Captcha\CaptchaBuilder;
|
||||
|
||||
/**
|
||||
* 验证码服务
|
||||
*
|
||||
* 使用 Redis 缓存验证码信息,支持防重放和错误次数限制
|
||||
*/
|
||||
class CaptchaService extends BaseService
|
||||
{
|
||||
private const CACHE_PREFIX = 'captcha_';
|
||||
private const EXPIRE_SECONDS = 300; // 5 分钟
|
||||
private const MAX_ERROR_TIMES = 5;
|
||||
|
||||
/**
|
||||
* 生成验证码,返回 captchaId 和 base64 图片
|
||||
*
|
||||
* @return array ['captchaId' => string, 'image' => string]
|
||||
*/
|
||||
public function generate(): array
|
||||
{
|
||||
// 使用 webman/captcha 生成验证码图片和文本
|
||||
$builder = new CaptchaBuilder;
|
||||
// 适配前端登录表单尺寸:110x30
|
||||
$builder->build(110, 30);
|
||||
|
||||
$code = strtolower($builder->getPhrase());
|
||||
$id = bin2hex(random_bytes(16));
|
||||
|
||||
$payload = [
|
||||
'code' => $code,
|
||||
'created_at' => time(),
|
||||
'error_times' => 0,
|
||||
'used' => false,
|
||||
];
|
||||
|
||||
Cache::set($this->buildKey($id), $payload, self::EXPIRE_SECONDS);
|
||||
|
||||
// 获取图片二进制并转为 base64
|
||||
$imgContent = $builder->get();
|
||||
$base64 = base64_encode($imgContent ?: '');
|
||||
|
||||
return [
|
||||
'captchaId' => $id,
|
||||
'image' => 'data:image/jpeg;base64,' . $base64,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验验证码(基于 captchaId + code)
|
||||
*
|
||||
* @param string|null $id 验证码ID
|
||||
* @param string|null $code 用户输入的验证码
|
||||
* @return bool
|
||||
*/
|
||||
public function validate(?string $id, ?string $code): bool
|
||||
{
|
||||
if ($id === null || $id === '' || $code === null || $code === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$key = $this->buildKey($id);
|
||||
$data = Cache::get($key);
|
||||
if (!$data || !is_array($data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 已使用或错误次数过多
|
||||
if (!empty($data['used']) || ($data['error_times'] ?? 0) >= self::MAX_ERROR_TIMES) {
|
||||
Cache::delete($key);
|
||||
return false;
|
||||
}
|
||||
|
||||
$expect = (string)($data['code'] ?? '');
|
||||
if ($expect === '' || strtolower($code) !== strtolower($expect)) {
|
||||
$data['error_times'] = ($data['error_times'] ?? 0) + 1;
|
||||
Cache::set($key, $data, self::EXPIRE_SECONDS);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 标记为已使用,防重放
|
||||
$data['used'] = true;
|
||||
Cache::set($key, $data, self::EXPIRE_SECONDS);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建缓存键
|
||||
*/
|
||||
private function buildKey(string $id): string
|
||||
{
|
||||
return self::CACHE_PREFIX . $id;
|
||||
}
|
||||
}
|
||||
|
||||
43
app/services/UserService.php
Normal file
43
app/services/UserService.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace app\services;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\common\constants\RoleCode;
|
||||
use app\repositories\UserRepository;
|
||||
|
||||
/**
|
||||
* 用户相关业务服务示例
|
||||
*/
|
||||
class UserService extends BaseService
|
||||
{
|
||||
public function __construct(
|
||||
protected UserRepository $users
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 根据 ID 获取用户信息(附带角色与权限)
|
||||
*
|
||||
* 返回结构尽量与前端 mock 的 /user/getUserInfo 保持一致:
|
||||
* {
|
||||
* "user": {...}, // 用户信息,roles 字段为角色对象数组
|
||||
* "roles": ["admin"], // 角色 code 数组
|
||||
* "permissions": ["*:*:*"] // 权限标识数组
|
||||
* }
|
||||
*/
|
||||
public function getUserInfoById(int $id): array
|
||||
{
|
||||
$user = $this->users->find($id);
|
||||
if (!$user) {
|
||||
throw new \RuntimeException('用户不存在', 404);
|
||||
}
|
||||
|
||||
$userArray = $user->toArray();
|
||||
|
||||
return [
|
||||
'user' => $userArray,
|
||||
'roles' => ['admin'],
|
||||
'permissions' => ['*:*:*'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\services\auth;
|
||||
|
||||
use app\common\base\BaseService;
|
||||
use app\exceptions\AuthFailedException;
|
||||
use app\exceptions\UnauthorizedException;
|
||||
use app\repositories\AdminUserRepository;
|
||||
use support\Redis;
|
||||
|
||||
class AuthService extends BaseService
|
||||
{
|
||||
public function __construct(
|
||||
protected AdminUserRepository $userRepository
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录,返回 JWT token
|
||||
*/
|
||||
public function login(string $username, string $password, $verifyCode = null): string
|
||||
{
|
||||
$user = $this->userRepository->findByUsername($username);
|
||||
if (!$user) {
|
||||
throw new AuthFailedException();
|
||||
}
|
||||
|
||||
// 当前阶段使用明文模拟密码校验
|
||||
if ($password !== '123456') {
|
||||
throw new AuthFailedException();
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'uid' => $user['id'],
|
||||
'username' => $user['userName'],
|
||||
'roles' => $user['roles'] ?? [],
|
||||
];
|
||||
|
||||
$token = JwtService::generateToken($payload);
|
||||
|
||||
// 写入 Redis 会话,key 使用 token,方便快速失效
|
||||
$ttl = JwtService::getTtl();
|
||||
$key = 'mpay:auth:token:' . $token;
|
||||
$redis = Redis::connection('default');
|
||||
$redis->setex($key, $ttl, json_encode($payload, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 token 获取用户信息(对齐前端 mock 返回结构)
|
||||
*/
|
||||
public function getUserInfo(string $token, $id = null): array
|
||||
{
|
||||
if ($token === '') {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
$redis = Redis::connection('default');
|
||||
$key = 'mpay:auth:token:' . $token;
|
||||
$session = $redis->get($key);
|
||||
if (!$session) {
|
||||
// 尝试从 JWT 解出(例如服务重启后 Redis 丢失的情况)
|
||||
$payload = JwtService::parseToken($token);
|
||||
} else {
|
||||
$payload = json_decode($session, true) ?: [];
|
||||
}
|
||||
|
||||
if (empty($payload['uid']) && empty($payload['username'])) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
// 对齐 mock:如果有 id 参数则按 id 查,否则用 payload 中 uid 查
|
||||
if ($id !== null && $id !== '') {
|
||||
$user = $this->userRepository->findById((int)$id);
|
||||
} else {
|
||||
$user = $this->userRepository->findById((int)($payload['uid'] ?? 0));
|
||||
}
|
||||
|
||||
if (!$user) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
// 角色信息
|
||||
$roleInfo = $this->userRepository->getRoleInfoByCodes($user['roles'] ?? []);
|
||||
$user['roles'] = $roleInfo;
|
||||
$roleCodes = array_map(static fn($item) => $item['code'], $roleInfo);
|
||||
|
||||
// 权限信息
|
||||
if (in_array('admin', $roleCodes, true)) {
|
||||
$permissions = ['*:*:*'];
|
||||
} else {
|
||||
$permissions = $this->userRepository->getPermissionsByRoleCodes($roleCodes);
|
||||
}
|
||||
|
||||
return [
|
||||
'user' => $user,
|
||||
'roles' => $roleCodes,
|
||||
'permissions' => $permissions,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="shortcut icon" href="/favicon.ico"/>
|
||||
<title>webman</title>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
hello <?=htmlspecialchars($name)?>
|
||||
</body>
|
||||
</html>
|
||||
@@ -28,23 +28,18 @@
|
||||
"workerman/webman-framework": "^2.1",
|
||||
"monolog/monolog": "^2.0",
|
||||
"php-di/php-di": "7.0",
|
||||
"webman/cache": "^2.1",
|
||||
"webman/database": "^2.1",
|
||||
"webman/redis": "^2.1",
|
||||
"illuminate/events": "^11.0",
|
||||
"webman/redis-queue": "^1.3",
|
||||
"illuminate/events": "^12.49",
|
||||
"webman/cache": "^2.1",
|
||||
"webman/console": "^2.1",
|
||||
"topthink/think-validate": "^3.0",
|
||||
"webman/rate-limiter": "^1.1",
|
||||
"webman/captcha": "^1.0",
|
||||
"webman/event": "^1.0",
|
||||
"vlucas/phpdotenv": "^5.6",
|
||||
"workerman/crontab": "^1.0",
|
||||
"webman/console": "^2.1",
|
||||
"firebase/php-jwt": "^6.0",
|
||||
"illuminate/database": "^11.0",
|
||||
"guzzlehttp/guzzle": "^7.0",
|
||||
"ramsey/uuid": "^4.0",
|
||||
"nesbot/carbon": "^3.0",
|
||||
"webman/database": "^2.1"
|
||||
"webman/redis-queue": "^2.1",
|
||||
"firebase/php-jwt": "^7.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-event": "For better performance. "
|
||||
|
||||
827
composer.lock
generated
827
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@
|
||||
use support\Request;
|
||||
|
||||
return [
|
||||
'debug' => getenv('APP_DEBUG') ?? true,
|
||||
'debug' => true,
|
||||
'error_reporting' => E_ALL,
|
||||
'default_timezone' => 'Asia/Shanghai',
|
||||
'request_class' => Request::class,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
return [
|
||||
'files' => [
|
||||
base_path() . '/support/functions.php',
|
||||
base_path() . '/support/helpers.php',
|
||||
base_path() . '/support/Request.php',
|
||||
base_path() . '/support/Response.php',
|
||||
]
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
*/
|
||||
|
||||
return [
|
||||
support\bootstrap\Session::class,
|
||||
// support\bootstrap\Session::class,
|
||||
];
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
*/
|
||||
|
||||
return [
|
||||
'default' => getenv('CACHE_DRIVER') ?? 'file',
|
||||
'default' => env('CACHE_DRIVER', 'file'),
|
||||
'stores' => [
|
||||
'file' => [
|
||||
'driver' => 'file',
|
||||
@@ -22,10 +22,10 @@ return [
|
||||
],
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => 'default'
|
||||
'connection' => env('CACHE_REDIS_CONNECTION', 'default')
|
||||
],
|
||||
'array' => [
|
||||
'driver' => 'array'
|
||||
]
|
||||
]
|
||||
];
|
||||
];
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
$builder = new \DI\ContainerBuilder();
|
||||
$builder->addDefinitions(config('dependence', []));
|
||||
$builder->useAutowiring(true);
|
||||
$builder->useAttributes(true);
|
||||
return $builder->build();
|
||||
$builder = new \DI\ContainerBuilder();
|
||||
$builder->addDefinitions(config('dependence', []));
|
||||
$builder->useAutowiring(true);
|
||||
$builder->useAttributes(true);
|
||||
return $builder->build();
|
||||
@@ -1,17 +1,17 @@
|
||||
<?php
|
||||
return [
|
||||
'default' => getenv('DB_CONNECTION') ?? 'mysql',
|
||||
'default' => 'mysql',
|
||||
'connections' => [
|
||||
'mysql' => [
|
||||
'driver' => getenv('DB_DRIVER') ?? 'mysql',
|
||||
'host' => getenv('DB_HOST') ?? '127.0.0.1',
|
||||
'port' => getenv('DB_PORT') ?? '3306',
|
||||
'database' => getenv('DB_DATABASE') ?? '',
|
||||
'username' => getenv('DB_USERNAME') ?? '',
|
||||
'password' => getenv('DB_PASSWORD') ?? '',
|
||||
'driver' => 'mysql',
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', 3306),
|
||||
'database' => env('DB_DATABASE', ''),
|
||||
'username' => env('DB_USERNAME', ''),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_general_ci',
|
||||
'prefix' => getenv('DB_PREFIX') ?? '',
|
||||
'prefix' => '',
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => [
|
||||
@@ -26,4 +26,4 @@ return [
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
];
|
||||
|
||||
48
config/dict.php
Normal file
48
config/dict.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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],
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -1,12 +1,19 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JWT 配置
|
||||
*/
|
||||
return [
|
||||
// JWT 密钥,生产环境建议从 .env 读取
|
||||
'secret' => getenv('JWT_SECRET') ?: 'mpay-secret',
|
||||
// 过期时间(秒)
|
||||
'ttl' => (int)(getenv('JWT_TTL') ?: 7200),
|
||||
// 签名算法
|
||||
'alg' => getenv('JWT_ALG') ?: 'HS256',
|
||||
// JWT 密钥(请在生产环境修改为强随机字符串)
|
||||
'secret' => env('JWT_SECRET', 'mpay-admin-secret-key-change-in-production'),
|
||||
|
||||
// Token 有效期(秒),默认 2 小时
|
||||
'ttl' => (int)env('JWT_TTL', 7200),
|
||||
|
||||
// 加密算法
|
||||
'alg' => env('JWT_ALG', 'HS256'),
|
||||
|
||||
// Token 缓存前缀(用于 Redis 存储)
|
||||
'cache_prefix' => env('JWT_CACHE_PREFIX', 'token_'),
|
||||
];
|
||||
|
||||
|
||||
|
||||
1107
config/menu.php
Normal file
1107
config/menu.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
@@ -13,9 +12,4 @@
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
// 超全局中间件-覆盖插件
|
||||
'@' => [
|
||||
app\common\middleware\Cors::class, //跨域中间件
|
||||
],
|
||||
];
|
||||
return [];
|
||||
28
config/plugin/webman/console/app.php
Normal file
28
config/plugin/webman/console/app.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
return [
|
||||
'enable' => true,
|
||||
|
||||
'build_dir' => BASE_PATH . DIRECTORY_SEPARATOR . 'build',
|
||||
|
||||
'phar_filename' => 'webman.phar',
|
||||
|
||||
'phar_format' => Phar::PHAR, // Phar archive format: Phar::PHAR, Phar::TAR, Phar::ZIP
|
||||
|
||||
'phar_compression' => Phar::NONE, // Compression method for Phar archive: Phar::NONE, Phar::GZ, Phar::BZ2
|
||||
|
||||
'bin_filename' => 'webman.bin',
|
||||
|
||||
'signature_algorithm'=> Phar::SHA256, //set the signature algorithm for a phar and apply it. The signature algorithm must be one of Phar::MD5, Phar::SHA1, Phar::SHA256, Phar::SHA512, or Phar::OPENSSL.
|
||||
|
||||
'private_key_file' => '', // The file path for certificate or OpenSSL private key file.
|
||||
|
||||
'exclude_pattern' => '#^(?!.*(composer.json|/.github/|/.idea/|/.git/|/.setting/|/runtime/|/vendor-bin/|/build/|/vendor/webman/admin/))(.*)$#',
|
||||
|
||||
'exclude_files' => [
|
||||
'.env', 'LICENSE', 'composer.json', 'composer.lock', 'start.php', 'webman.phar', 'webman.bin'
|
||||
],
|
||||
|
||||
'custom_ini' => '
|
||||
memory_limit = 256M
|
||||
',
|
||||
];
|
||||
4
config/plugin/webman/event/app.php
Normal file
4
config/plugin/webman/event/app.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
return [
|
||||
'enable' => true,
|
||||
];
|
||||
17
config/plugin/webman/event/bootstrap.php
Normal file
17
config/plugin/webman/event/bootstrap.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
Webman\Event\BootStrap::class,
|
||||
];
|
||||
7
config/plugin/webman/event/command.php
Normal file
7
config/plugin/webman/event/command.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Webman\Event\EventListCommand;
|
||||
|
||||
return [
|
||||
EventListCommand::class
|
||||
];
|
||||
4
config/plugin/webman/redis-queue/app.php
Normal file
4
config/plugin/webman/redis-queue/app.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
return [
|
||||
'enable' => true,
|
||||
];
|
||||
7
config/plugin/webman/redis-queue/command.php
Normal file
7
config/plugin/webman/redis-queue/command.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Webman\RedisQueue\Command\MakeConsumerCommand;
|
||||
|
||||
return [
|
||||
MakeConsumerCommand::class
|
||||
];
|
||||
32
config/plugin/webman/redis-queue/log.php
Normal file
32
config/plugin/webman/redis-queue/log.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
'default' => [
|
||||
'handlers' => [
|
||||
[
|
||||
'class' => Monolog\Handler\RotatingFileHandler::class,
|
||||
'constructor' => [
|
||||
runtime_path() . '/logs/redis-queue/queue.log',
|
||||
7, //$maxFiles
|
||||
Monolog\Logger::DEBUG,
|
||||
],
|
||||
'formatter' => [
|
||||
'class' => Monolog\Formatter\LineFormatter::class,
|
||||
'constructor' => [null, 'Y-m-d H:i:s', true],
|
||||
],
|
||||
]
|
||||
],
|
||||
]
|
||||
];
|
||||
11
config/plugin/webman/redis-queue/process.php
Normal file
11
config/plugin/webman/redis-queue/process.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
return [
|
||||
'consumer' => [
|
||||
'handler' => Webman\RedisQueue\Process\Consumer::class,
|
||||
'count' => 8, // 可以设置多进程同时消费
|
||||
'constructor' => [
|
||||
// 消费者类目录
|
||||
'consumer_dir' => app_path() . '/jobs'
|
||||
]
|
||||
]
|
||||
];
|
||||
21
config/plugin/webman/redis-queue/redis.php
Normal file
21
config/plugin/webman/redis-queue/redis.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
return [
|
||||
'default' => [
|
||||
'host' => 'redis://' . env('REDIS_HOST', '127.0.0.1') . ':' . env('REDIS_PORT', 6379),
|
||||
'options' => [
|
||||
'auth' => env('REDIS_PASSWORD', ''),
|
||||
'db' => env('QUEUE_REDIS_DATABASE', 0),
|
||||
'prefix' => env('QUEUE_REDIS_PREFIX', 'ma:queue:'),
|
||||
'max_attempts' => 5,
|
||||
'retry_seconds' => 5,
|
||||
],
|
||||
// Connection pool, supports only Swoole or Swow drivers.
|
||||
'pool' => [
|
||||
'max_connections' => 5,
|
||||
'min_connections' => 1,
|
||||
'wait_timeout' => 3,
|
||||
'idle_timeout' => 60,
|
||||
'heartbeat_interval' => 50,
|
||||
]
|
||||
],
|
||||
];
|
||||
@@ -1,20 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* MPay V2 支付系统 - Redis配置
|
||||
* Redis 6.0+ 缓存和队列配置
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
// 默认Redis连接
|
||||
'default' => [
|
||||
'host' => getenv('REDIS_HOST') ?? '127.0.0.1',
|
||||
'password' => getenv('REDIS_PASSWORD') ?? '',
|
||||
'port' => getenv('REDIS_PORT') ?? 6379,
|
||||
'database' => getenv('REDIS_DATABASE') ?? 0,
|
||||
'password' => env('REDIS_PASSWORD', ''),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'port' => env('REDIS_PORT', 6379),
|
||||
'database' => env('REDIS_DATABASE', 0),
|
||||
'pool' => [
|
||||
'max_connections' => 20,
|
||||
'min_connections' => 5,
|
||||
'max_connections' => 5,
|
||||
'min_connections' => 1,
|
||||
'wait_timeout' => 3,
|
||||
'idle_timeout' => 60,
|
||||
'heartbeat_interval' => 50,
|
||||
],
|
||||
],
|
||||
'cache' => [
|
||||
'password' => env('REDIS_PASSWORD', ''),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'port' => env('REDIS_PORT', 6379),
|
||||
'database' => env('CACHE_REDIS_DATABASE', 1),
|
||||
'prefix' => 'ma:cache:',
|
||||
'pool' => [
|
||||
'max_connections' => 5,
|
||||
'min_connections' => 1,
|
||||
'wait_timeout' => 3,
|
||||
'idle_timeout' => 60,
|
||||
'heartbeat_interval' => 50,
|
||||
|
||||
@@ -17,26 +17,22 @@ use Webman\Route;
|
||||
use support\Response;
|
||||
use support\Request;
|
||||
|
||||
// 匹配所有options路由(CORS 预检请求)
|
||||
Route::options('[{path:.+}]', function (Request $request){
|
||||
$response = response('', 204);
|
||||
return $response->withHeaders([
|
||||
'Access-Control-Allow-Credentials' => 'true',
|
||||
'Access-Control-Allow-Origin' => $request->header('origin', '*'),
|
||||
'Access-Control-Allow-Methods' => $request->header('access-control-request-method', '*'),
|
||||
'Access-Control-Allow-Headers' => $request->header('access-control-request-headers', '*'),
|
||||
]);
|
||||
});
|
||||
|
||||
// 管理后台路由
|
||||
require_once base_path() . '/app/routes/admin.php';
|
||||
|
||||
|
||||
|
||||
// 默认路由兜底
|
||||
Route::fallback(function (Request $request) {
|
||||
// 处理预检请求
|
||||
if (strtoupper($request->method()) === 'OPTIONS') {
|
||||
$response = response('', 204);
|
||||
$response->withHeaders([
|
||||
'Access-Control-Allow-Credentials' => 'true',
|
||||
'Access-Control-Max-Age' => '86400',
|
||||
'Access-Control-Allow-Origin' => $request->header('origin', '*'),
|
||||
'Access-Control-Allow-Methods' => $request->header('access-control-request-method', '*'),
|
||||
'Access-Control-Allow-Headers' => $request->header('access-control-request-headers', '*'),
|
||||
]);
|
||||
return $response;
|
||||
}
|
||||
});
|
||||
// API 路由
|
||||
require_once base_path() . '/app/routes/api.php';
|
||||
|
||||
/**
|
||||
* 关闭默认路由
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
@@ -19,6 +18,6 @@
|
||||
return [
|
||||
'enable' => true,
|
||||
'middleware' => [ // Static file Middleware
|
||||
app\common\middleware\StaticFile::class,
|
||||
//app\middleware\StaticFile::class,
|
||||
],
|
||||
];
|
||||
];
|
||||
114
doc/异常处理.txt
114
doc/异常处理.txt
@@ -1,114 +0,0 @@
|
||||
异常处理
|
||||
配置
|
||||
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=true,json数据里会额外增加一个trace字段返回详细的调用栈。
|
||||
|
||||
你可以编写自己的异常处理类来更改默认异常处理逻辑。
|
||||
|
||||
业务异常 BusinessException
|
||||
有时候我们想在某个嵌套函数里终止请求并返回一个错误信息给客户端,这时可以通过抛出BusinessException来做到这点。
|
||||
例如:
|
||||
|
||||
<?php
|
||||
namespace app\controller;
|
||||
|
||||
use support\Request;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
class FooController
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->chackInpout($request->post());
|
||||
return response('hello index');
|
||||
}
|
||||
|
||||
protected function chackInpout($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异常。
|
||||
@@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace support\exception;
|
||||
|
||||
use Throwable;
|
||||
use Webman\Exception\ExceptionHandler;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
/**
|
||||
* 自定义异常处理器
|
||||
* 基于 webman 的 ExceptionHandler 扩展,统一处理业务异常
|
||||
*/
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
/**
|
||||
* 不需要记录日志的异常类型
|
||||
*/
|
||||
public $dontReport = [
|
||||
BusinessException::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* 报告异常(记录日志)
|
||||
*/
|
||||
public function report(Throwable $exception)
|
||||
{
|
||||
parent::report($exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染异常响应
|
||||
*/
|
||||
public function render(Request $request, Throwable $exception): Response
|
||||
{
|
||||
// 业务异常优先走异常自身的 render(参考官方文档:自定义业务异常)
|
||||
if ($exception instanceof \Webman\Exception\BusinessException) {
|
||||
if (method_exists($exception, 'render')) {
|
||||
$response = $exception->render($request);
|
||||
if ($response instanceof Response) {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 其他异常使用父类默认处理(debug=true 时带 trace,字段沿用 webman 默认)
|
||||
return parent::render($request, $exception);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Here is your custom functions.
|
||||
*/
|
||||
2
support/helpers.php
Normal file
2
support/helpers.php
Normal file
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
// This file is generated by Webman, please don't modify it.
|
||||
Reference in New Issue
Block a user