更新后端基础

This commit is contained in:
技术老胡
2026-02-23 11:33:27 +08:00
parent 4a34feec54
commit d29751cce8
75 changed files with 2978 additions and 1489 deletions

14
.gitignore vendored
View File

@@ -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

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
});
}
}

View 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;
}

View 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';
}

View 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';
}

View File

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

View File

@@ -0,0 +1,15 @@
<?php
namespace app\common\constants;
/**
* 通用是/否布尔枚举
* 可复用在 is_admin 等字段
*/
class YesNo
{
public const NO = 0;
public const YES = 1;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View File

@@ -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;
}
}

View File

@@ -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_';
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View 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;
}
}

View 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);
}
}

View 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);
}
}
}

View 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
View 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');
}
}

View File

@@ -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));
}
}

View 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());
}
}

View 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());
}
}

View 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());
}
}

View 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());
}
}

View 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());
}
}

View 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();
}
}

View 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());
}
}

View 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;
}
}

View File

@@ -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]);

View File

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

View File

View 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();
}
}

View 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;
}
}

View 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' => ['*:*:*'],
];
}
}

View File

@@ -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,
];
}
}

View File

@@ -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>

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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,

View File

@@ -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',
]

View File

@@ -13,5 +13,5 @@
*/
return [
support\bootstrap\Session::class,
// support\bootstrap\Session::class,
];

View File

@@ -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'
]
]
];
];

View File

@@ -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();

View File

@@ -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
View 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],
],
],
];

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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 [];

View 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
',
];

View File

@@ -0,0 +1,4 @@
<?php
return [
'enable' => true,
];

View 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,
];

View File

@@ -0,0 +1,7 @@
<?php
use Webman\Event\EventListCommand;
return [
EventListCommand::class
];

View File

@@ -0,0 +1,4 @@
<?php
return [
'enable' => true,
];

View File

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

View 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],
],
]
],
]
];

View File

@@ -0,0 +1,11 @@
<?php
return [
'consumer' => [
'handler' => Webman\RedisQueue\Process\Consumer::class,
'count' => 8, // 可以设置多进程同时消费
'constructor' => [
// 消费者类目录
'consumer_dir' => app_path() . '/jobs'
]
]
];

View 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,
]
],
];

View File

@@ -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,

View File

@@ -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';
/**
* 关闭默认路由

View File

@@ -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,
],
];
];

View File

@@ -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=truejson数据里会额外增加一个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异常。

View File

@@ -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);
}
}

View File

@@ -1,4 +0,0 @@
<?php
/**
* Here is your custom functions.
*/

2
support/helpers.php Normal file
View File

@@ -0,0 +1,2 @@
<?php
// This file is generated by Webman, please don't modify it.