更新基础架构

This commit is contained in:
技术老胡
2026-01-27 09:07:38 +08:00
parent 28f1a2855c
commit 4a34feec54
36 changed files with 1289 additions and 540 deletions

View File

@@ -1,70 +0,0 @@
<div style="padding:18px;max-width: 1024px;margin:0 auto;background-color:#fff;color:#333">
<h1>webman</h1>
基于<a href="https://www.workerman.net" target="__blank">workerman</a>开发的超高性能PHP框架
<h1>学习</h1>
<ul>
<li>
<a href="https://www.workerman.net/webman" target="__blank">主页 / Home page</a>
</li>
<li>
<a href="https://webman.workerman.net" target="__blank">文档 / Document</a>
</li>
<li>
<a href="https://www.workerman.net/doc/webman/install.html" target="__blank">安装 / Install</a>
</li>
<li>
<a href="https://www.workerman.net/questions" target="__blank">问答 / Questions</a>
</li>
<li>
<a href="https://www.workerman.net/apps" target="__blank">市场 / Apps</a>
</li>
<li>
<a href="https://www.workerman.net/sponsor" target="__blank">赞助 / Sponsors</a>
</li>
<li>
<a href="https://www.workerman.net/doc/webman/thanks.html" target="__blank">致谢 / Thanks</a>
</li>
</ul>
<div style="float:left;padding-bottom:30px;">
<h1>赞助商</h1>
<h4>特别赞助</h4>
<a href="https://www.crmeb.com/?form=workerman" target="__blank">
<img src="https://www.workerman.net/img/sponsors/6429/20230719111500.svg" width="200">
</a>
<h4>铂金赞助</h4>
<a href="https://www.fadetask.com/?from=workerman" target="__blank"><img src="https://www.workerman.net/img/sponsors/1/20230719084316.png" width="200"></a>
<a href="https://www.yilianyun.net/?from=workerman" target="__blank" style="margin-left:20px;"><img src="https://www.workerman.net/img/sponsors/6218/20230720114049.png" width="200"></a>
</div>
<div style="float:left;padding-bottom:30px;clear:both">
<h1>请作者喝咖啡</h1>
<img src="https://www.workerman.net/img/wx_donate.png" width="200">
<img src="https://www.workerman.net/img/ali_donate.png" width="200">
<br>
<b>如果您觉得webman对您有所帮助欢迎捐赠。</b>
</div>
<div style="clear: both">
<h1>LICENSE</h1>
The webman is open-sourced software licensed under the MIT.
</div>
</div>

View File

@@ -0,0 +1,42 @@
<?php
namespace app\common\base;
use support\Response;
use support\Request;
/**
* 控制器基础类
* - 提供统一的 success/fail 响应封装
*
* 约定:
* - 控制器统一通过 $this->request->* 获取请求数据
* - 为避免每个控制器构造函数重复注入 Request本类通过 __get('request') 返回当前请求对象
*/
abstract class BaseController
{
/**
* 成功响应
*/
protected function success(mixed $data = null, string $message = 'success', int $code = 200): Response
{
return json([
'code' => $code,
'message' => $message,
'data' => $data,
]);
}
/**
* 失败响应
*/
protected function fail(string $message = 'fail', int $code = 500, mixed $data = null): Response
{
return json([
'code' => $code,
'message' => $message,
'data' => $data,
]);
}
}

164
app/common/base/BaseDao.php Normal file
View File

@@ -0,0 +1,164 @@
<?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

@@ -0,0 +1,101 @@
<?php
namespace app\common\base;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
/**
* 模型基础类
* - 统一禁用时间戳(如需要可在子类开启)
* - 提供常用查询作用域和便捷方法
*/
abstract class BaseModel extends Model
{
/**
* 禁用时间戳(默认)
*/
public $timestamps = false;
/**
* 允许批量赋值的字段(子类可覆盖)
*/
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

@@ -0,0 +1,58 @@
<?php
namespace app\common\base;
/**
* 仓库基础类
* - 支持注入 DAO 依赖
* - 通过魔术方法代理 DAO 的方法调用
* - 提供通用的数据访问封装
*/
abstract class BaseRepository
{
/**
* DAO 实例(可选,子类通过构造函数注入)
*/
protected ?BaseDao $dao = null;
/**
* 构造函数,子类可注入 DAO
*/
public function __construct(?BaseDao $dao = null)
{
$this->dao = $dao;
}
/**
* 魔术方法:代理 DAO 的方法调用
* 如果仓库自身没有该方法,且存在 DAO 实例,则调用 DAO 的对应方法
*/
public function __call(string $method, array $arguments)
{
if ($this->dao && method_exists($this->dao, $method)) {
return $this->dao->{$method}(...$arguments);
}
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;
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace app\common\base;
use support\Log;
/**
* 服务基础类
* - 提供日志记录能力
* - 预留事务、事件发布等扩展点
*/
abstract class BaseService
{
/**
* 记录日志
*/
protected function log(string $level, string $message, array $context = []): void
{
$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);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace app\common\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
/**
* 全局跨域中间件
* 处理前后端分离项目中的跨域请求问题
*/
class Cors implements MiddlewareInterface
{
/**
* 处理请求
* @param Request $request 请求对象
* @param callable $handler 下一个中间件处理函数
* @return Response 响应对象
*/
public function process(Request $request, callable $handler): Response
{
$response = strtoupper($request->method()) === 'OPTIONS' ? response('', 204) : $handler($request);
$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

@@ -12,7 +12,7 @@
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace app\middleware;
namespace app\common\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;

View File

@@ -1,42 +0,0 @@
<?php
namespace app\controller;
use support\Request;
class IndexController
{
public function index(Request $request)
{
return <<<EOF
<style>
* {
padding: 0;
margin: 0;
}
iframe {
border: none;
overflow: scroll;
}
</style>
<iframe
src="https://www.workerman.net/wellcome"
width="100%"
height="100%"
allow="clipboard-write"
sandbox="allow-scripts allow-same-origin allow-popups"
></iframe>
EOF;
}
public function view(Request $request)
{
return view('index/view', ['name' => 'webman']);
}
public function json(Request $request)
{
return json(['code' => 0, 'msg' => 'ok']);
}
}

View File

@@ -0,0 +1,16 @@
<?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

@@ -0,0 +1,49 @@
<?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

@@ -0,0 +1,16 @@
<?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

@@ -0,0 +1,16 @@
<?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

@@ -0,0 +1,16 @@
<?php
namespace app\exceptions;
/**
* 参数校验异常
*/
class ValidationException extends BusinessException
{
public function __construct(string $message = '参数校验失败', int $bizCode = 422, mixed $data = null)
{
parent::__construct($message, $bizCode, $data);
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace app\http\admin\controller;
use app\common\base\BaseController;
use support\Request;
use support\Response;
use app\services\auth\AuthService;
class AuthController extends BaseController
{
public function __construct(
protected AuthService $authService
) {}
/**
* 管理后台登录
*/
public function login(Request $request): Response
{
$username = (string)$request->post('username', '');
$password = (string)$request->post('password', '');
// 前端有本地验证码,这里暂不做服务端校验,仅预留字段
$verifyCode = $request->post('verifyCode');
if ($username === '' || $password === '') {
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);
}
}

View File

@@ -1,29 +0,0 @@
<?php
namespace app\model;
use support\Model;
class Test extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'test';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
}

View File

@@ -0,0 +1,207 @@
<?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));
}
}

14
app/routes/admin.php Normal file
View File

@@ -0,0 +1,14 @@
<?php
/**
* 管理后台路由定义
*/
use Webman\Route;
use app\http\admin\controller\AuthController;
Route::group('/admin', function () {
// 登录相关
Route::post('/mock/login', [AuthController::class, 'login']);
Route::get('/mock/user/getUserInfo', [AuthController::class, 'getUserInfo']);
});

0
app/routes/api.php Normal file
View File

0
app/routes/user.php Normal file
View File

View File

@@ -0,0 +1,104 @@
<?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

@@ -0,0 +1,52 @@
<?php
namespace app\services\auth;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class JwtService
{
/**
* 生成 JWT
*/
public static function generateToken(array $payloadBase): string
{
$config = config('jwt', []);
$secret = $config['secret'] ?? 'mpay-secret';
$ttl = (int)($config['ttl'] ?? 7200);
$alg = $config['alg'] ?? 'HS256';
$now = time();
$payload = array_merge($payloadBase, [
'iat' => $now,
'exp' => $now + $ttl,
]);
return JWT::encode($payload, $secret, $alg);
}
/**
* 解析 JWT
*/
public static function parseToken(string $token): array
{
$config = config('jwt', []);
$secret = $config['secret'] ?? 'mpay-secret';
$alg = $config['alg'] ?? 'HS256';
$decoded = JWT::decode($token, new Key($secret, $alg));
return json_decode(json_encode($decoded, JSON_UNESCAPED_UNICODE), true) ?: [];
}
/**
* 获取 ttl
*/
public static function getTtl(): int
{
$config = config('jwt', []);
return (int)($config['ttl'] ?? 7200);
}
}

View File

@@ -44,7 +44,7 @@
"guzzlehttp/guzzle": "^7.0",
"ramsey/uuid": "^4.0",
"nesbot/carbon": "^3.0",
"webman/admin": "~2.0"
"webman/database": "^2.1"
},
"suggest": {
"ext-event": "For better performance. "

287
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "39121444a6b84a7e26f3813db45c8cb1",
"content-hash": "abca1c99f7511639d3548201fd459bd5",
"packages": [
{
"name": "brick/math",
@@ -1384,56 +1384,6 @@
},
"time": "2024-06-28T20:10:30+00:00"
},
{
"name": "illuminate/pagination",
"version": "v11.47.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/pagination.git",
"reference": "2c4970463f7ede55be207fbdc27608b4c4178380"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/pagination/zipball/2c4970463f7ede55be207fbdc27608b4c4178380",
"reference": "2c4970463f7ede55be207fbdc27608b4c4178380",
"shasum": ""
},
"require": {
"ext-filter": "*",
"illuminate/collections": "^11.0",
"illuminate/contracts": "^11.0",
"illuminate/support": "^11.0",
"php": "^8.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "11.x-dev"
}
},
"autoload": {
"psr-4": {
"Illuminate\\Pagination\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "The Illuminate Pagination package.",
"homepage": "https://laravel.com",
"support": {
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2025-02-14T15:33:00+00:00"
},
{
"name": "illuminate/pipeline",
"version": "v11.47.0",
@@ -1670,90 +1620,6 @@
},
"time": "2025-11-27T16:16:32+00:00"
},
{
"name": "intervention/image",
"version": "2.7.2",
"source": {
"type": "git",
"url": "https://github.com/Intervention/image.git",
"reference": "04be355f8d6734c826045d02a1079ad658322dad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Intervention/image/zipball/04be355f8d6734c826045d02a1079ad658322dad",
"reference": "04be355f8d6734c826045d02a1079ad658322dad",
"shasum": ""
},
"require": {
"ext-fileinfo": "*",
"guzzlehttp/psr7": "~1.1 || ^2.0",
"php": ">=5.4.0"
},
"require-dev": {
"mockery/mockery": "~0.9.2",
"phpunit/phpunit": "^4.8 || ^5.7 || ^7.5.15"
},
"suggest": {
"ext-gd": "to use GD library based image processing.",
"ext-imagick": "to use Imagick based image processing.",
"intervention/imagecache": "Caching extension for the Intervention Image library"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Image": "Intervention\\Image\\Facades\\Image"
},
"providers": [
"Intervention\\Image\\ImageServiceProvider"
]
},
"branch-alias": {
"dev-master": "2.4-dev"
}
},
"autoload": {
"psr-4": {
"Intervention\\Image\\": "src/Intervention/Image"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Oliver Vogel",
"email": "oliver@intervention.io",
"homepage": "https://intervention.io/"
}
],
"description": "Image handling and manipulation library with support for Laravel integration",
"homepage": "http://image.intervention.io/",
"keywords": [
"gd",
"image",
"imagick",
"laravel",
"thumbnail",
"watermark"
],
"support": {
"issues": "https://github.com/Intervention/image/issues",
"source": "https://github.com/Intervention/image/tree/2.7.2"
},
"funding": [
{
"url": "https://paypal.me/interventionio",
"type": "custom"
},
{
"url": "https://github.com/Intervention",
"type": "github"
}
],
"time": "2022-05-21T17:30:32+00:00"
},
{
"name": "laravel/serializable-closure",
"version": "v1.3.7",
@@ -3359,16 +3225,16 @@
},
{
"name": "symfony/error-handler",
"version": "v7.4.0",
"version": "v7.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/error-handler.git",
"reference": "48be2b0653594eea32dcef130cca1c811dcf25c2"
"reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/error-handler/zipball/48be2b0653594eea32dcef130cca1c811dcf25c2",
"reference": "48be2b0653594eea32dcef130cca1c811dcf25c2",
"url": "https://api.github.com/repos/symfony/error-handler/zipball/8da531f364ddfee53e36092a7eebbbd0b775f6b8",
"reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8",
"shasum": ""
},
"require": {
@@ -3417,7 +3283,7 @@
"description": "Provides tools to manage errors and ease debugging PHP code",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/error-handler/tree/v7.4.0"
"source": "https://github.com/symfony/error-handler/tree/v7.4.4"
},
"funding": [
{
@@ -3437,20 +3303,20 @@
"type": "tidelift"
}
],
"time": "2025-11-05T14:29:59+00:00"
"time": "2026-01-20T16:42:42+00:00"
},
{
"name": "symfony/event-dispatcher",
"version": "v7.4.0",
"version": "v7.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
"reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d"
"reference": "dc2c0eba1af673e736bb851d747d266108aea746"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9dddcddff1ef974ad87b3708e4b442dc38b2261d",
"reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/dc2c0eba1af673e736bb851d747d266108aea746",
"reference": "dc2c0eba1af673e736bb851d747d266108aea746",
"shasum": ""
},
"require": {
@@ -3502,7 +3368,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/event-dispatcher/tree/v7.4.0"
"source": "https://github.com/symfony/event-dispatcher/tree/v7.4.4"
},
"funding": [
{
@@ -3522,7 +3388,7 @@
"type": "tidelift"
}
],
"time": "2025-10-28T09:38:46+00:00"
"time": "2026-01-05T11:45:34+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
@@ -3602,16 +3468,16 @@
},
{
"name": "symfony/finder",
"version": "v7.4.3",
"version": "v7.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "fffe05569336549b20a1be64250b40516d6e8d06"
"reference": "01b24a145bbeaa7141e75887ec904c34a6728a5f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/fffe05569336549b20a1be64250b40516d6e8d06",
"reference": "fffe05569336549b20a1be64250b40516d6e8d06",
"url": "https://api.github.com/repos/symfony/finder/zipball/01b24a145bbeaa7141e75887ec904c34a6728a5f",
"reference": "01b24a145bbeaa7141e75887ec904c34a6728a5f",
"shasum": ""
},
"require": {
@@ -3646,7 +3512,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v7.4.3"
"source": "https://github.com/symfony/finder/tree/v7.4.4"
},
"funding": [
{
@@ -3666,20 +3532,20 @@
"type": "tidelift"
}
],
"time": "2025-12-23T14:50:43+00:00"
"time": "2026-01-12T12:19:02+00:00"
},
{
"name": "symfony/http-foundation",
"version": "v7.4.3",
"version": "v7.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "a70c745d4cea48dbd609f4075e5f5cbce453bd52"
"reference": "977a554a34cf8edc95ca351fbecb1bb1ad05cc94"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/a70c745d4cea48dbd609f4075e5f5cbce453bd52",
"reference": "a70c745d4cea48dbd609f4075e5f5cbce453bd52",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/977a554a34cf8edc95ca351fbecb1bb1ad05cc94",
"reference": "977a554a34cf8edc95ca351fbecb1bb1ad05cc94",
"shasum": ""
},
"require": {
@@ -3728,7 +3594,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-foundation/tree/v7.4.3"
"source": "https://github.com/symfony/http-foundation/tree/v7.4.4"
},
"funding": [
{
@@ -3748,20 +3614,20 @@
"type": "tidelift"
}
],
"time": "2025-12-23T14:23:49+00:00"
"time": "2026-01-09T12:14:21+00:00"
},
{
"name": "symfony/http-kernel",
"version": "v7.4.3",
"version": "v7.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
"reference": "885211d4bed3f857b8c964011923528a55702aa5"
"reference": "48b067768859f7b68acf41dfb857a5a4be00acdd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/885211d4bed3f857b8c964011923528a55702aa5",
"reference": "885211d4bed3f857b8c964011923528a55702aa5",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/48b067768859f7b68acf41dfb857a5a4be00acdd",
"reference": "48b067768859f7b68acf41dfb857a5a4be00acdd",
"shasum": ""
},
"require": {
@@ -3847,7 +3713,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-kernel/tree/v7.4.3"
"source": "https://github.com/symfony/http-kernel/tree/v7.4.4"
},
"funding": [
{
@@ -3867,20 +3733,20 @@
"type": "tidelift"
}
],
"time": "2025-12-31T08:43:57+00:00"
"time": "2026-01-24T22:13:01+00:00"
},
{
"name": "symfony/mime",
"version": "v7.4.0",
"version": "v7.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a"
"reference": "40945014c0a9471ccfe19673c54738fa19367a3c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/bdb02729471be5d047a3ac4a69068748f1a6be7a",
"reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a",
"url": "https://api.github.com/repos/symfony/mime/zipball/40945014c0a9471ccfe19673c54738fa19367a3c",
"reference": "40945014c0a9471ccfe19673c54738fa19367a3c",
"shasum": ""
},
"require": {
@@ -3936,7 +3802,7 @@
"mime-type"
],
"support": {
"source": "https://github.com/symfony/mime/tree/v7.4.0"
"source": "https://github.com/symfony/mime/tree/v7.4.4"
},
"funding": [
{
@@ -3956,7 +3822,7 @@
"type": "tidelift"
}
],
"time": "2025-11-16T10:14:42+00:00"
"time": "2026-01-08T16:12:55+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -4713,16 +4579,16 @@
},
{
"name": "symfony/string",
"version": "v7.4.0",
"version": "v7.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003"
"reference": "1c4b10461bf2ec27537b5f36105337262f5f5d6f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/d50e862cb0a0e0886f73ca1f31b865efbb795003",
"reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003",
"url": "https://api.github.com/repos/symfony/string/zipball/1c4b10461bf2ec27537b5f36105337262f5f5d6f",
"reference": "1c4b10461bf2ec27537b5f36105337262f5f5d6f",
"shasum": ""
},
"require": {
@@ -4780,7 +4646,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v7.4.0"
"source": "https://github.com/symfony/string/tree/v7.4.4"
},
"funding": [
{
@@ -4800,20 +4666,20 @@
"type": "tidelift"
}
],
"time": "2025-11-27T13:27:24+00:00"
"time": "2026-01-12T10:54:30+00:00"
},
{
"name": "symfony/translation",
"version": "v7.4.3",
"version": "v7.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "7ef27c65d78886f7599fdd5c93d12c9243ecf44d"
"reference": "bfde13711f53f549e73b06d27b35a55207528877"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/7ef27c65d78886f7599fdd5c93d12c9243ecf44d",
"reference": "7ef27c65d78886f7599fdd5c93d12c9243ecf44d",
"url": "https://api.github.com/repos/symfony/translation/zipball/bfde13711f53f549e73b06d27b35a55207528877",
"reference": "bfde13711f53f549e73b06d27b35a55207528877",
"shasum": ""
},
"require": {
@@ -4880,7 +4746,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/translation/tree/v7.4.3"
"source": "https://github.com/symfony/translation/tree/v7.4.4"
},
"funding": [
{
@@ -4900,7 +4766,7 @@
"type": "tidelift"
}
],
"time": "2025-12-29T09:31:36+00:00"
"time": "2026-01-13T10:40:19+00:00"
},
{
"name": "symfony/translation-contracts",
@@ -4986,16 +4852,16 @@
},
{
"name": "symfony/var-dumper",
"version": "v7.4.3",
"version": "v7.4.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "7e99bebcb3f90d8721890f2963463280848cba92"
"reference": "0e4769b46a0c3c62390d124635ce59f66874b282"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/7e99bebcb3f90d8721890f2963463280848cba92",
"reference": "7e99bebcb3f90d8721890f2963463280848cba92",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/0e4769b46a0c3c62390d124635ce59f66874b282",
"reference": "0e4769b46a0c3c62390d124635ce59f66874b282",
"shasum": ""
},
"require": {
@@ -5049,7 +4915,7 @@
"dump"
],
"support": {
"source": "https://github.com/symfony/var-dumper/tree/v7.4.3"
"source": "https://github.com/symfony/var-dumper/tree/v7.4.4"
},
"funding": [
{
@@ -5069,7 +4935,7 @@
"type": "tidelift"
}
],
"time": "2025-12-18T07:04:31+00:00"
"time": "2026-01-01T22:13:48+00:00"
},
{
"name": "symfony/var-exporter",
@@ -5446,49 +5312,6 @@
],
"time": "2024-11-21T01:49:47+00:00"
},
{
"name": "webman/admin",
"version": "v2.1.7",
"source": {
"type": "git",
"url": "https://github.com/webman-php/admin.git",
"reference": "44a6158fbac2475edca92d3fc0108221a93373b5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webman-php/admin/zipball/44a6158fbac2475edca92d3fc0108221a93373b5",
"reference": "44a6158fbac2475edca92d3fc0108221a93373b5",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/guzzle": "^7.5",
"illuminate/events": ">=7.30",
"illuminate/pagination": ">=7.30",
"intervention/image": "^2.7",
"laravel/serializable-closure": "^1.0",
"webman/captcha": "^1.0.0",
"webman/database": ">=2.1",
"webman/event": "^1.0",
"workerman/webman-framework": ">=2.1"
},
"type": "project",
"autoload": {
"psr-4": {
"Webman\\Admin\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Webman Admin",
"support": {
"issues": "https://github.com/webman-php/admin/issues",
"source": "https://github.com/webman-php/admin/tree/v2.1.7"
},
"time": "2025-05-07T14:58:24+00:00"
},
{
"name": "webman/cache",
"version": "v2.1.3",

View File

@@ -15,7 +15,7 @@
use support\Request;
return [
'debug' => true,
'debug' => getenv('APP_DEBUG') ?? true,
'error_reporting' => E_ALL,
'default_timezone' => 'Asia/Shanghai',
'request_class' => Request::class,

View File

@@ -14,7 +14,7 @@
return [
'files' => [
base_path() . '/app/functions.php',
base_path() . '/support/functions.php',
base_path() . '/support/Request.php',
base_path() . '/support/Response.php',
]

View File

@@ -14,7 +14,7 @@
*/
return [
'default' => 'file',
'default' => getenv('CACHE_DRIVER') ?? 'file',
'stores' => [
'file' => [
'driver' => 'file',

View File

@@ -1,84 +1,25 @@
<?php
/**
* MPay V2 支付系统 - 数据库配置
* 基于webman标准配置格式支持MySQL 5.7+
*
* 配置说明:
* - 字符集utf8mb4支持完整的UTF-8字符集包括emoji
* - 排序规则utf8mb4_unicode_ciUnicode标准排序
* - 连接池支持连接池管理提高性能仅支持swoole/swow驱动
* - 读写分离:支持主从数据库配置
*/
return [
// 默认数据库连接
'default' => env('DB_CONNECTION', 'mysql'),
// 数据库连接配置
return [
'default' => getenv('DB_CONNECTION') ?? 'mysql',
'connections' => [
// 主数据库配置
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '192.168.31.200'),
'port' => env('DB_PORT', 3306),
'database' => env('DB_DATABASE', 'mpay_v2'),
'username' => env('DB_USERNAME', 'mpay_v2'),
'password' => env('DB_PASSWORD', 'pXfNWELALrwAAt88'),
'unix_socket' => '',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => env('DB_PREFIX', ''),
'strict' => true,
'engine' => 'InnoDB',
'pool' => [
'max_connections' => env('DB_POOL_MAX_CONNECTIONS', 20),
'min_connections' => env('DB_POOL_MIN_CONNECTIONS', 3),
'wait_timeout' => env('DB_POOL_WAIT_TIMEOUT', 3),
'idle_timeout' => env('DB_POOL_IDLE_TIMEOUT', 60),
'heartbeat_interval' => env('DB_POOL_HEARTBEAT_INTERVAL', 50),
'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') ?? '',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_general_ci',
'prefix' => getenv('DB_PREFIX') ?? '',
'strict' => true,
'engine' => null,
'options' => [
PDO::ATTR_EMULATE_PREPARES => false, // Must be false for Swoole and Swow drivers.
],
],
// 读库配置(主从分离)
'mysql_read' => [
'driver' => 'mysql',
'host' => env('DB_READ_HOST', env('DB_HOST', '192.168.31.200')),
'port' => env('DB_READ_PORT', env('DB_PORT', 3306)),
'database' => env('DB_READ_DATABASE', env('DB_DATABASE', 'mpay_v2')),
'username' => env('DB_READ_USERNAME', env('DB_USERNAME', 'mpay_v2')),
'password' => env('DB_READ_PASSWORD', env('DB_PASSWORD', 'pXfNWELALrwAAt88')),
'unix_socket' => '',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => env('DB_PREFIX', ''),
'strict' => true,
'engine' => 'InnoDB',
'pool' => [
'max_connections' => 15,
'min_connections' => 2,
'wait_timeout' => 3,
'idle_timeout' => 60,
'heartbeat_interval' => 50,
],
],
// 写库配置(主从分离)
'mysql_write' => [
'driver' => 'mysql',
'host' => env('DB_WRITE_HOST', env('DB_HOST', '192.168.31.200')),
'port' => env('DB_WRITE_PORT', env('DB_PORT', 3306)),
'database' => env('DB_WRITE_DATABASE', env('DB_DATABASE', 'mpay_v2')),
'username' => env('DB_WRITE_USERNAME', env('DB_USERNAME', 'mpay_v2')),
'password' => env('DB_WRITE_PASSWORD', env('DB_PASSWORD', 'pXfNWELALrwAAt88')),
'unix_socket' => '',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => env('DB_PREFIX', ''),
'strict' => true,
'engine' => 'InnoDB',
'pool' => [
'max_connections' => 10,
'min_connections' => 2,
'max_connections' => 5,
'min_connections' => 1,
'wait_timeout' => 3,
'idle_timeout' => 60,
'heartbeat_interval' => 50,

12
config/jwt.php Normal file
View File

@@ -0,0 +1,12 @@
<?php
return [
// JWT 密钥,生产环境建议从 .env 读取
'secret' => getenv('JWT_SECRET') ?: 'mpay-secret',
// 过期时间(秒)
'ttl' => (int)(getenv('JWT_TTL') ?: 7200),
// 签名算法
'alg' => getenv('JWT_ALG') ?: 'HS256',
];

View File

@@ -1,4 +1,5 @@
<?php
/**
* This file is part of webman.
*
@@ -12,4 +13,9 @@
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [];
return [
// 超全局中间件-覆盖插件
'@' => [
app\common\middleware\Cors::class, //跨域中间件
],
];

View File

@@ -1,4 +1,5 @@
<?php
/**
* MPay V2 支付系统 - Redis配置
* Redis 6.0+ 缓存和队列配置
@@ -7,91 +8,16 @@
return [
// 默认Redis连接
'default' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', ''),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_DATABASE', 0),
'host' => getenv('REDIS_HOST') ?? '127.0.0.1',
'password' => getenv('REDIS_PASSWORD') ?? '',
'port' => getenv('REDIS_PORT') ?? 6379,
'database' => getenv('REDIS_DATABASE') ?? 0,
'pool' => [
'max_connections' => env('REDIS_POOL_MAX_CONNECTIONS', 20),
'min_connections' => env('REDIS_POOL_MIN_CONNECTIONS', 5),
'wait_timeout' => env('REDIS_POOL_WAIT_TIMEOUT', 3),
'idle_timeout' => env('REDIS_POOL_IDLE_TIMEOUT', 60),
'heartbeat_interval' => env('REDIS_POOL_HEARTBEAT_INTERVAL', 50),
],
'options' => [
'prefix' => env('CACHE_PREFIX', 'mpay_v2_'),
],
],
// 缓存专用Redis连接
'cache' => [
'host' => env('REDIS_CACHE_HOST', env('REDIS_HOST', '127.0.0.1')),
'password' => env('REDIS_CACHE_PASSWORD', env('REDIS_PASSWORD', '')),
'port' => env('REDIS_CACHE_PORT', env('REDIS_PORT', 6379)),
'database' => env('REDIS_CACHE_DATABASE', 1),
'pool' => [
'max_connections' => 15,
'min_connections' => 3,
'max_connections' => 20,
'min_connections' => 5,
'wait_timeout' => 3,
'idle_timeout' => 60,
'heartbeat_interval' => 50,
],
'options' => [
'prefix' => env('CACHE_PREFIX', 'mpay_v2_cache_'),
],
],
// 队列专用Redis连接
'queue' => [
'host' => env('REDIS_QUEUE_HOST', env('REDIS_HOST', '127.0.0.1')),
'password' => env('REDIS_QUEUE_PASSWORD', env('REDIS_PASSWORD', '')),
'port' => env('REDIS_QUEUE_PORT', env('REDIS_PORT', 6379)),
'database' => env('REDIS_QUEUE_DATABASE', 2),
'pool' => [
'max_connections' => 10,
'min_connections' => 2,
'wait_timeout' => 3,
'idle_timeout' => 60,
'heartbeat_interval' => 50,
],
'options' => [
'prefix' => env('QUEUE_PREFIX', 'mpay_v2_queue_'),
],
],
// 会话存储Redis连接
'session' => [
'host' => env('REDIS_SESSION_HOST', env('REDIS_HOST', '127.0.0.1')),
'password' => env('REDIS_SESSION_PASSWORD', env('REDIS_PASSWORD', '')),
'port' => env('REDIS_SESSION_PORT', env('REDIS_PORT', 6379)),
'database' => env('REDIS_SESSION_DATABASE', 3),
'pool' => [
'max_connections' => 10,
'min_connections' => 2,
'wait_timeout' => 3,
'idle_timeout' => 60,
'heartbeat_interval' => 50,
],
'options' => [
'prefix' => 'mpay_v2_session_',
],
],
// JWT黑名单Redis连接
'jwt_blacklist' => [
'host' => env('REDIS_JWT_HOST', env('REDIS_HOST', '127.0.0.1')),
'password' => env('REDIS_JWT_PASSWORD', env('REDIS_PASSWORD', '')),
'port' => env('REDIS_JWT_PORT', env('REDIS_PORT', 6379)),
'database' => env('REDIS_JWT_DATABASE', 4),
'pool' => [
'max_connections' => 5,
'min_connections' => 1,
'wait_timeout' => 3,
'idle_timeout' => 60,
'heartbeat_interval' => 50,
],
'options' => [
'prefix' => 'mpay_v2_jwt_blacklist_',
],
],
];

View File

@@ -1,4 +1,5 @@
<?php
/**
* This file is part of webman.
*
@@ -13,9 +14,31 @@
*/
use Webman\Route;
use support\Response;
use support\Request;
// 管理后台路由
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;
}
});
/**
* 关闭默认路由
*/
Route::disableDefaultRoute();

View File

@@ -1,4 +1,5 @@
<?php
/**
* This file is part of webman.
*
@@ -18,6 +19,6 @@
return [
'enable' => true,
'middleware' => [ // Static file Middleware
//app\middleware\StaticFile::class,
app\common\middleware\StaticFile::class,
],
];
];

114
doc/异常处理.txt Normal file
View File

@@ -0,0 +1,114 @@
异常处理
配置
config/exception.php
return [
// 这里配置异常处理类
'' => support\exception\Handler::class,
];
多应用模式时,你可以为每个应用单独配置异常处理类,参见多应用
默认异常处理类
webman中异常默认由 support\exception\Handler 类来处理。可修改配置文件config/exception.php来更改默认异常处理类。异常处理类必须实现Webman\Exception\ExceptionHandlerInterface 接口。
interface ExceptionHandlerInterface
{
/**
* 记录日志
* @param Throwable $e
* @return mixed
*/
public function report(Throwable $e);
/**
* 渲染返回
* @param Request $request
* @param Throwable $e
* @return Response
*/
public function render(Request $request, Throwable $e) : Response;
}
渲染响应
异常处理类中的render方法是用来渲染响应的。
如果配置文件config/app.php中debug值为true(以下简称app.debug=true),将返回详细的异常信息,否则将返回简略的异常信息。
如果请求期待是json返回则返回的异常信息将以json格式返回类似
{
"code": "500",
"msg": "异常信息"
}
如果app.debug=truejson数据里会额外增加一个trace字段返回详细的调用栈。
你可以编写自己的异常处理类来更改默认异常处理逻辑。
业务异常 BusinessException
有时候我们想在某个嵌套函数里终止请求并返回一个错误信息给客户端这时可以通过抛出BusinessException来做到这点。
例如:
<?php
namespace app\controller;
use support\Request;
use support\exception\BusinessException;
class FooController
{
public function index(Request $request)
{
$this->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

@@ -0,0 +1,51 @@
<?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);
}
}