更新基础架构

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

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

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

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