项目初始化
This commit is contained in:
79
app/common/controller/Api.php
Normal file
79
app/common/controller/Api.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\controller;
|
||||
|
||||
use app\support\BaseController;
|
||||
use support\Response;
|
||||
use support\think\Db;
|
||||
use think\db\exception\PDOException;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
/**
|
||||
* API 控制器基类
|
||||
* 迁移自 app/common/controller/Api.php,适配 Webman
|
||||
*/
|
||||
class Api extends BaseController
|
||||
{
|
||||
/**
|
||||
* 默认响应输出类型
|
||||
*/
|
||||
protected string $responseType = 'json';
|
||||
|
||||
/**
|
||||
* 是否开启系统站点配置(数据库检查、ip_check、set_timezone)
|
||||
*/
|
||||
protected bool $useSystemSettings = true;
|
||||
|
||||
/**
|
||||
* API 初始化(需在控制器方法开头调用)
|
||||
* @param WebmanRequest $request
|
||||
* @return Response|null 若需直接返回(如 ip 禁止、数据库错误)则返回 Response,否则 null
|
||||
*/
|
||||
public function initializeApi(WebmanRequest $request): ?Response
|
||||
{
|
||||
$this->setRequest($request);
|
||||
|
||||
if ($this->useSystemSettings) {
|
||||
// 检查数据库连接
|
||||
try {
|
||||
Db::execute('SELECT 1');
|
||||
} catch (PDOException $e) {
|
||||
return $this->error(mb_convert_encoding($e->getMessage(), 'UTF-8', 'UTF-8,GBK,GB2312,BIG5'));
|
||||
}
|
||||
|
||||
// IP 检查
|
||||
$ipCheckResponse = ip_check(null, $request);
|
||||
if ($ipCheckResponse !== null) {
|
||||
return $ipCheckResponse;
|
||||
}
|
||||
|
||||
// 时区设定
|
||||
set_timezone();
|
||||
}
|
||||
|
||||
// 语言包由 LoadLangPack 中间件加载
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Request 或路由解析控制器路径(如 user/user)
|
||||
* 优先从 $request->controller 解析,否则从 path 解析
|
||||
*/
|
||||
protected function getControllerPath(WebmanRequest $request): ?string
|
||||
{
|
||||
return get_controller_path($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Request 解析应用名(api 或 admin)
|
||||
*/
|
||||
protected function getAppFromRequest(WebmanRequest $request): string
|
||||
{
|
||||
$path = trim($request->path(), '/');
|
||||
$parts = explode('/', $path);
|
||||
return $parts[0] ?? 'api';
|
||||
}
|
||||
}
|
||||
435
app/common/controller/Backend.php
Normal file
435
app/common/controller/Backend.php
Normal file
@@ -0,0 +1,435 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\controller;
|
||||
|
||||
use Throwable;
|
||||
use app\admin\library\Auth;
|
||||
use app\common\library\token\TokenExpirationException;
|
||||
use app\admin\library\traits\Backend as BackendTrait;
|
||||
use support\Response;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
/**
|
||||
* 后台控制器基类
|
||||
* 迁移 Auth 鉴权、权限校验、CRUD trait
|
||||
*/
|
||||
class Backend extends Api
|
||||
{
|
||||
use BackendTrait;
|
||||
/**
|
||||
* 无需登录的方法
|
||||
*/
|
||||
protected array $noNeedLogin = [];
|
||||
|
||||
/**
|
||||
* 无需鉴权的方法
|
||||
*/
|
||||
protected array $noNeedPermission = [];
|
||||
|
||||
/**
|
||||
* 权限类实例
|
||||
* @var Auth|null
|
||||
*/
|
||||
protected ?Auth $auth = null;
|
||||
|
||||
/**
|
||||
* 模型类实例(子类设置)
|
||||
* @var object|null
|
||||
*/
|
||||
protected ?object $model = null;
|
||||
|
||||
/**
|
||||
* 新增/编辑时排除字段
|
||||
*/
|
||||
protected array|string $preExcludeFields = [];
|
||||
|
||||
/**
|
||||
* 权重字段
|
||||
*/
|
||||
protected string $weighField = 'weigh';
|
||||
|
||||
/**
|
||||
* 默认排序
|
||||
*/
|
||||
protected string|array $defaultSortField = [];
|
||||
|
||||
/**
|
||||
* 有序保证
|
||||
*/
|
||||
protected string|array $orderGuarantee = [];
|
||||
|
||||
/**
|
||||
* 快速搜索字段
|
||||
*/
|
||||
protected string|array $quickSearchField = 'id';
|
||||
|
||||
/**
|
||||
* 数据限制
|
||||
*/
|
||||
protected bool|string|int $dataLimit = false;
|
||||
|
||||
/**
|
||||
* 数据限制字段
|
||||
*/
|
||||
protected string $dataLimitField = 'admin_id';
|
||||
|
||||
/**
|
||||
* 数据限制开启时自动填充
|
||||
*/
|
||||
protected bool $dataLimitFieldAutoFill = true;
|
||||
|
||||
/**
|
||||
* 查看请求返回的主表字段
|
||||
*/
|
||||
protected string|array $indexField = ['*'];
|
||||
|
||||
/**
|
||||
* 是否开启模型验证
|
||||
*/
|
||||
protected bool $modelValidate = true;
|
||||
|
||||
/**
|
||||
* 是否开启模型场景验证
|
||||
*/
|
||||
protected bool $modelSceneValidate = false;
|
||||
|
||||
/**
|
||||
* 关联查询方法名
|
||||
*/
|
||||
protected array $withJoinTable = [];
|
||||
|
||||
/**
|
||||
* 关联查询 JOIN 方式
|
||||
*/
|
||||
protected string $withJoinType = 'LEFT';
|
||||
|
||||
/**
|
||||
* 输入过滤函数名(如 clean_xss,CRUD 含 editor 字段时自动设置)
|
||||
*/
|
||||
protected string $inputFilter = '';
|
||||
|
||||
/**
|
||||
* 后台初始化(需在控制器方法开头调用,在 initializeApi 之后)
|
||||
* @return Response|null 需直接返回时返回 Response,否则 null
|
||||
*/
|
||||
public function initializeBackend(WebmanRequest $request): ?Response
|
||||
{
|
||||
$response = $this->initializeApi($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
// 调用子类 initialize(CRUD 生成的控制器在此设置 model)
|
||||
$this->initialize();
|
||||
|
||||
$action = $this->getActionFromPath($request->path());
|
||||
$needLogin = !action_in_arr($this->noNeedLogin, $action);
|
||||
$needPermission = !action_in_arr($this->noNeedPermission, $action);
|
||||
|
||||
try {
|
||||
$this->auth = Auth::instance();
|
||||
$token = get_auth_token(['ba', 'token'], $request);
|
||||
if ($token) {
|
||||
$this->auth->init($token);
|
||||
}
|
||||
} catch (TokenExpirationException) {
|
||||
if ($needLogin) {
|
||||
return $this->error(__('Token expiration'), [], 409);
|
||||
}
|
||||
}
|
||||
|
||||
if ($needLogin) {
|
||||
if (!$this->auth->isLogin()) {
|
||||
return $this->error(__('Please login first'), [
|
||||
'type' => Auth::NEED_LOGIN,
|
||||
], 0, ['statusCode' => Auth::LOGIN_RESPONSE_CODE]);
|
||||
}
|
||||
if ($needPermission) {
|
||||
$controllerPath = $this->getControllerPath($request);
|
||||
$routePath = $controllerPath . '/' . $action;
|
||||
if (!$this->auth->check($routePath)) {
|
||||
return $this->error(__('You have no permission'), [], 401);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event_trigger('backendInit', $this->auth);
|
||||
|
||||
if (method_exists($this, 'initController')) {
|
||||
$initResp = $this->initController($request);
|
||||
if ($initResp !== null) {
|
||||
return $initResp;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 子类可覆盖,用于初始化 model 等(替代原 initialize)
|
||||
* @return Response|null 需直接返回时返回 Response,否则 null
|
||||
*/
|
||||
protected function initController(WebmanRequest $request): ?Response
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function index(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
return $this->_index();
|
||||
}
|
||||
|
||||
public function add(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
return $this->_add();
|
||||
}
|
||||
|
||||
public function edit(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
return $this->_edit();
|
||||
}
|
||||
|
||||
public function del(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
return $this->_del();
|
||||
}
|
||||
|
||||
public function sortable(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
return $this->_sortable();
|
||||
}
|
||||
|
||||
public function select(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) return $response;
|
||||
$this->_select();
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询参数构建器
|
||||
*/
|
||||
public function queryBuilder(): array
|
||||
{
|
||||
if (empty($this->model) || !$this->request) {
|
||||
return [[], [], 10, []];
|
||||
}
|
||||
$pk = $this->model->getPk();
|
||||
$quickSearch = $this->request->get('quickSearch', '');
|
||||
$limit = $this->request->get('limit', 10);
|
||||
$limit = is_numeric($limit) ? intval($limit) : 10;
|
||||
$search = $this->request->get('search', []);
|
||||
$search = is_array($search) ? $search : [];
|
||||
$initKey = $this->request->get('initKey', $pk);
|
||||
$initValue = $this->request->get('initValue', '');
|
||||
$initOperator = $this->request->get('initOperator', 'in');
|
||||
|
||||
$where = [];
|
||||
$modelTable = strtolower($this->model->getTable());
|
||||
$alias = [];
|
||||
$alias[$modelTable] = parse_name(basename(str_replace('\\', '/', get_class($this->model))));
|
||||
$mainTableAlias = $alias[$modelTable] . '.';
|
||||
|
||||
if ($quickSearch) {
|
||||
$quickSearchArr = is_array($this->quickSearchField) ? $this->quickSearchField : explode(',', $this->quickSearchField);
|
||||
foreach ($quickSearchArr as $k => $v) {
|
||||
$quickSearchArr[$k] = str_contains($v, '.') ? $v : $mainTableAlias . $v;
|
||||
}
|
||||
$where[] = [implode('|', $quickSearchArr), 'LIKE', '%' . str_replace('%', '\%', $quickSearch) . '%'];
|
||||
}
|
||||
if ($initValue) {
|
||||
$where[] = [$initKey, $initOperator, $initValue];
|
||||
$limit = 999999;
|
||||
}
|
||||
|
||||
foreach ($search as $field) {
|
||||
if (!is_array($field) || !isset($field['operator']) || !isset($field['field']) || !isset($field['val'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$field['operator'] = $this->getOperatorByAlias($field['operator']);
|
||||
|
||||
if (str_contains($field['field'], '.')) {
|
||||
$fieldNameParts = explode('.', $field['field']);
|
||||
$fieldNamePartsLastKey = array_key_last($fieldNameParts);
|
||||
foreach ($fieldNameParts as $fieldNamePartsKey => $fieldNamePart) {
|
||||
if ($fieldNamePartsKey !== $fieldNamePartsLastKey) {
|
||||
$fieldNameParts[$fieldNamePartsKey] = parse_name($fieldNamePart);
|
||||
}
|
||||
}
|
||||
$fieldName = implode('.', $fieldNameParts);
|
||||
} else {
|
||||
$fieldName = $mainTableAlias . $field['field'];
|
||||
}
|
||||
|
||||
if (isset($field['render']) && $field['render'] == 'datetime') {
|
||||
if ($field['operator'] == 'RANGE') {
|
||||
$datetimeArr = explode(',', $field['val']);
|
||||
if (!isset($datetimeArr[1])) {
|
||||
continue;
|
||||
}
|
||||
$datetimeArr = array_filter(array_map('strtotime', $datetimeArr));
|
||||
$where[] = [$fieldName, str_replace('RANGE', 'BETWEEN', $field['operator']), $datetimeArr];
|
||||
continue;
|
||||
}
|
||||
$where[] = [$fieldName, '=', strtotime($field['val'])];
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($field['operator'] == 'RANGE' || $field['operator'] == 'NOT RANGE') {
|
||||
$arr = explode(',', $field['val']);
|
||||
if (!isset($arr[0]) || $arr[0] === '') {
|
||||
$operator = $field['operator'] == 'RANGE' ? '<=' : '>';
|
||||
$arr = $arr[1];
|
||||
} elseif (!isset($arr[1]) || $arr[1] === '') {
|
||||
$operator = $field['operator'] == 'RANGE' ? '>=' : '<';
|
||||
$arr = $arr[0];
|
||||
} else {
|
||||
$operator = str_replace('RANGE', 'BETWEEN', $field['operator']);
|
||||
}
|
||||
$where[] = [$fieldName, $operator, $arr];
|
||||
continue;
|
||||
}
|
||||
|
||||
switch ($field['operator']) {
|
||||
case '=':
|
||||
case '<>':
|
||||
$where[] = [$fieldName, $field['operator'], strval($field['val'])];
|
||||
break;
|
||||
case 'LIKE':
|
||||
case 'NOT LIKE':
|
||||
$where[] = [$fieldName, $field['operator'], '%' . str_replace('%', '\%', $field['val']) . '%'];
|
||||
break;
|
||||
case '>':
|
||||
case '>=':
|
||||
case '<':
|
||||
case '<=':
|
||||
$where[] = [$fieldName, $field['operator'], intval($field['val'])];
|
||||
break;
|
||||
case 'FIND_IN_SET':
|
||||
if (is_array($field['val'])) {
|
||||
foreach ($field['val'] as $val) {
|
||||
$where[] = [$fieldName, 'find in set', $val];
|
||||
}
|
||||
} else {
|
||||
$where[] = [$fieldName, 'find in set', $field['val']];
|
||||
}
|
||||
break;
|
||||
case 'IN':
|
||||
case 'NOT IN':
|
||||
$where[] = [$fieldName, $field['operator'], is_array($field['val']) ? $field['val'] : explode(',', $field['val'])];
|
||||
break;
|
||||
case 'NULL':
|
||||
case 'NOT NULL':
|
||||
$where[] = [$fieldName, strtolower($field['operator']), ''];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$dataLimitAdminIds = $this->getDataLimitAdminIds();
|
||||
if ($dataLimitAdminIds) {
|
||||
$where[] = [$mainTableAlias . $this->dataLimitField, 'in', $dataLimitAdminIds];
|
||||
}
|
||||
|
||||
return [$where, $alias, $limit, $this->queryOrderBuilder()];
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询的排序参数构建器
|
||||
*/
|
||||
public function queryOrderBuilder(): array
|
||||
{
|
||||
$pk = $this->model->getPk();
|
||||
$order = $this->request ? $this->request->get('order') : null;
|
||||
$order = $order ?: $this->defaultSortField;
|
||||
|
||||
if ($order && is_string($order)) {
|
||||
$order = explode(',', $order);
|
||||
$order = [$order[0] => $order[1] ?? 'asc'];
|
||||
}
|
||||
if (!$this->orderGuarantee) {
|
||||
$this->orderGuarantee = [$pk => 'desc'];
|
||||
} elseif (is_string($this->orderGuarantee)) {
|
||||
$orderParts = explode(',', $this->orderGuarantee);
|
||||
$this->orderGuarantee = [$orderParts[0] => $orderParts[1] ?? 'asc'];
|
||||
}
|
||||
$orderGuaranteeKey = array_key_first($this->orderGuarantee);
|
||||
if (!is_array($order) || !array_key_exists($orderGuaranteeKey, $order)) {
|
||||
$order[$orderGuaranteeKey] = $this->orderGuarantee[$orderGuaranteeKey];
|
||||
}
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据权限控制 - 获取有权限访问的管理员 IDs
|
||||
*/
|
||||
protected function getDataLimitAdminIds(): array
|
||||
{
|
||||
if (!$this->dataLimit || !$this->auth || $this->auth->isSuperAdmin()) {
|
||||
return [];
|
||||
}
|
||||
$adminIds = [];
|
||||
if ($this->dataLimit == 'parent') {
|
||||
$parentGroups = $this->auth->getAdminChildGroups();
|
||||
if ($parentGroups) {
|
||||
$adminIds = $this->auth->getGroupAdmins($parentGroups);
|
||||
}
|
||||
} elseif (is_numeric($this->dataLimit) && $this->dataLimit > 0) {
|
||||
$adminIds = $this->auth->getGroupAdmins([$this->dataLimit]);
|
||||
return in_array($this->auth->id, $adminIds) ? [] : [$this->auth->id];
|
||||
} elseif ($this->dataLimit == 'allAuth' || $this->dataLimit == 'allAuthAndOthers') {
|
||||
$allAuthGroups = $this->auth->getAllAuthGroups($this->dataLimit);
|
||||
$adminIds = $this->auth->getGroupAdmins($allAuthGroups);
|
||||
}
|
||||
$adminIds[] = $this->auth->id;
|
||||
return array_unique($adminIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从别名获取原始的逻辑运算符
|
||||
*/
|
||||
protected function getOperatorByAlias(string $operator): string
|
||||
{
|
||||
$alias = [
|
||||
'ne' => '<>',
|
||||
'eq' => '=',
|
||||
'gt' => '>',
|
||||
'egt' => '>=',
|
||||
'lt' => '<',
|
||||
'elt' => '<=',
|
||||
];
|
||||
return $alias[$operator] ?? $operator;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 path 解析 action
|
||||
*/
|
||||
protected function getActionFromPath(string $path): string
|
||||
{
|
||||
$parts = explode('/', trim($path, '/'));
|
||||
return $parts[array_key_last($parts)] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Request 或路由解析控制器路径(如 auth/admin)
|
||||
* 优先从 $request->controller(Webman 路由匹配时设置)解析,否则从 path 解析
|
||||
*/
|
||||
protected function getControllerPath(WebmanRequest $request): string
|
||||
{
|
||||
return get_controller_path($request);
|
||||
}
|
||||
}
|
||||
60
app/common/controller/Frontend.php
Normal file
60
app/common/controller/Frontend.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\controller;
|
||||
|
||||
use app\common\library\Auth;
|
||||
use app\common\library\token\TokenExpirationException;
|
||||
use support\Response;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
/**
|
||||
* 前台/会员中心控制器基类
|
||||
* 继承 Api,增加会员鉴权
|
||||
*/
|
||||
class Frontend extends Api
|
||||
{
|
||||
protected array $noNeedLogin = [];
|
||||
protected array $noNeedPermission = [];
|
||||
protected ?Auth $auth = null;
|
||||
|
||||
/**
|
||||
* 前台初始化(需在控制器方法开头调用)
|
||||
* @return Response|null 若需直接返回则返回 Response,否则 null
|
||||
*/
|
||||
public function initializeFrontend(WebmanRequest $request): ?Response
|
||||
{
|
||||
$response = $this->initializeApi($request);
|
||||
if ($response !== null) return $response;
|
||||
|
||||
$this->setRequest($request);
|
||||
$path = trim($request->path(), '/');
|
||||
$parts = explode('/', $path);
|
||||
$action = $parts[array_key_last($parts)] ?? '';
|
||||
$needLogin = !action_in_arr($this->noNeedLogin, $action);
|
||||
|
||||
try {
|
||||
$this->auth = Auth::instance(['request' => $request]);
|
||||
$token = get_auth_token(['ba', 'user', 'token'], $request);
|
||||
if ($token) $this->auth->init($token);
|
||||
} catch (TokenExpirationException) {
|
||||
if ($needLogin) return $this->error(__('Token expiration'), [], 409);
|
||||
}
|
||||
|
||||
if ($needLogin) {
|
||||
if (!$this->auth->isLogin()) {
|
||||
return $this->error(__('Please login first'), ['type' => Auth::NEED_LOGIN], Auth::LOGIN_RESPONSE_CODE);
|
||||
}
|
||||
if (!action_in_arr($this->noNeedPermission, $action)) {
|
||||
$routePath = get_controller_path($request) . '/' . $action;
|
||||
if (!$this->auth->check($routePath)) {
|
||||
return $this->error(__('You have no permission'), [], 401);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event_trigger('frontendInit', $this->auth);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user