254 lines
8.4 KiB
PHP
254 lines
8.4 KiB
PHP
<?php
|
||
// +----------------------------------------------------------------------
|
||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||
// +----------------------------------------------------------------------
|
||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||
// +----------------------------------------------------------------------
|
||
// | Author: ywxapp<admin@ywxapp.cn>
|
||
// +----------------------------------------------------------------------
|
||
declare (strict_types = 1);
|
||
|
||
namespace ywxapp\controller;
|
||
|
||
use think\App;
|
||
use think\exception\ValidateException;
|
||
use think\Validate;
|
||
use ywxapp\library\Result;
|
||
use think\facade\Lang;
|
||
use think\facade\View;
|
||
|
||
/**
|
||
* 控制器基础类
|
||
*/
|
||
abstract class BaseController
|
||
{
|
||
/**
|
||
* 应用实例
|
||
* @var \think\App
|
||
*/
|
||
protected $app;
|
||
|
||
/**
|
||
* Request实例
|
||
* @var \think\Request
|
||
*/
|
||
protected $request;
|
||
|
||
/**
|
||
* 应用模块名称
|
||
* @var string
|
||
*/
|
||
protected $module = '';
|
||
|
||
/**
|
||
* 是否批量验证
|
||
* @var bool
|
||
*/
|
||
protected $batchValidate = false;
|
||
|
||
/**
|
||
* 控制器中间件
|
||
* @var array
|
||
*/
|
||
protected $middleware = [];
|
||
|
||
/**
|
||
* 控制器模型
|
||
* @var \think\Model
|
||
*/
|
||
protected $model = null;
|
||
|
||
/**
|
||
* 响应实例
|
||
* @var \ywxapp\library\Result
|
||
*/
|
||
protected $result;
|
||
|
||
/**
|
||
* 用户实例
|
||
* @var \ywxapp\library\Auth
|
||
*/
|
||
protected $auth;
|
||
|
||
/**
|
||
* Summary of needLogin
|
||
* @var array
|
||
*/
|
||
protected $noNeedLogin = [];
|
||
/**
|
||
* Summary of needRight
|
||
* @var array
|
||
*/
|
||
protected $noNeedVerify = [];
|
||
|
||
/**
|
||
* 构造方法
|
||
* @access public
|
||
* @param App $app 应用对象
|
||
*/
|
||
public function __construct(App $app)
|
||
{
|
||
$this->app = $app;
|
||
// 安全的CORS配置(必须在 $this->app 赋值后再调用,使用容器 Env 而非全局 env() 助手)
|
||
$this->setCorsHeaders();
|
||
|
||
$this->module = $app->http->getName();
|
||
$this->result = $app->result;
|
||
$this->request = $app->request;
|
||
$this->auth = $app->auth;
|
||
// 尝试用 token 还原登录态:有 token 就解析并初始化用户,没有则跳过(不报错、不影响后续)
|
||
$this->auth->tryInitByToken();
|
||
// 登录验证(需要登录的 action 未登录返回 401;已登录则校验权限)
|
||
|
||
// 加载当前控制器语言包
|
||
$this->loadlang($this->request->controller());
|
||
$this->_initialize();
|
||
$this->assignSite();
|
||
}
|
||
|
||
/**
|
||
* 设置安全的CORS头部
|
||
*/
|
||
protected function setCorsHeaders(): void
|
||
{
|
||
// 从配置或环境变量获取允许的来源(使用容器 Env,避免依赖全局 env() 助手在插件/CLI 上下文未注册)
|
||
$allowedOrigins = $this->app->env->get('CORS_ALLOWED_ORIGINS', 'http://localhost:3000,http://localhost:8080');
|
||
$allowedOriginsArray = array_filter(array_map('trim', explode(',', $allowedOrigins)));
|
||
|
||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||
|
||
// 检查来源是否在白名单中
|
||
if (in_array($origin, $allowedOriginsArray) || empty($origin)) {
|
||
if (!empty($origin)) {
|
||
header('Access-Control-Allow-Origin: ' . $origin);
|
||
header('Access-Control-Allow-Credentials: true');
|
||
}
|
||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
|
||
header('Access-Control-Max-Age: 86400'); // 24小时预检缓存
|
||
}
|
||
|
||
// 处理OPTIONS预检请求
|
||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||
http_response_code(200);
|
||
exit();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 向模板注入当前应用的基础路径信息。
|
||
*
|
||
* 关键:对外 URL 的模块段必须是 app_map 的「虚拟名」(admin/user/home),
|
||
* 而非 http->getName() 返回的「真实目录名」(backend/member/frontend),
|
||
* 否则拼接出的 URL 会与路由不匹配(重复 / 错位模块名)。
|
||
* 虚拟名直接取自 request->root() 中解析出的那段。
|
||
*/
|
||
protected function assignSite(): void
|
||
{
|
||
$root = $this->app->request->root(); // 如 /admin、/user、默认应用为空
|
||
$appRoot = ltrim($root, '/'); // 虚拟名:admin/user/home,默认应用为 ''
|
||
// 部署根(去掉模块段):/sub/admin -> /sub;/admin -> ''(网站根目录)
|
||
$deployRoot = preg_replace('#^/[^/]+#', '', $root);
|
||
|
||
View::assign('site', [
|
||
'root' => $deployRoot === '' ? '' : rtrim($deployRoot, '/'),
|
||
'module' => $appRoot, // 对外模块段(虚拟名,URL 用):admin/user/home
|
||
'app' => $this->module, // 真实应用目录名(静态资源目录用):backend/member/frontend/home
|
||
'controller' => $this->request->controller(),
|
||
'action' => $this->request->action(),
|
||
'devToken' => config('ywxapp.developer_token'),
|
||
'devAddon' => config('ywxapp.addon_developer')
|
||
]);
|
||
|
||
// 后端生成一个「带占位符的基准 URL」,供 JS(route.js) 做片段替换,
|
||
// 从而与后端路由规则(app_map/别名/后缀)完全对齐。
|
||
// 例如当前应用在 backend 时生成: /admin/{__CTRL__}/{__ACT__}
|
||
// 注意:不可用 Route::buildUrl('{__CTRL__}/{__ACT__}') —— 它把占位符当真实
|
||
// 路由解析,在某些路由配置下会误生成 /menu/{__CTRL__} 之类的异常路径。
|
||
// 改为按 app_map 反查当前应用(真实目录名 $this->module)的虚拟前缀手动拼。
|
||
$appMap = (array) config('app.app_map');
|
||
$appPrefix = array_search($this->module, $appMap, true);
|
||
if (! $appPrefix) {
|
||
$appPrefix = $this->module; // 无映射时直接用真实目录名
|
||
}
|
||
$base = $deployRoot === '' ? '' : rtrim($deployRoot, '/');
|
||
$routeBase = $base . '/' . $appPrefix . '/{__CTRL__}/{__ACT__}';
|
||
View::assign('route_base', $routeBase);
|
||
}
|
||
|
||
/**
|
||
* 控制器初始化 _initialize
|
||
* @return void
|
||
*/
|
||
protected function _initialize()
|
||
{}
|
||
|
||
/**
|
||
* 加载语言文件.
|
||
*
|
||
* @param string $name
|
||
*/
|
||
protected function loadlang($name = '')
|
||
{
|
||
$name = $name ?: $this->request->controller();
|
||
if (strpos($name, '.')) {
|
||
$_arr = explode('.', $name);
|
||
if (count($_arr) == 2) {
|
||
$path = $_arr[0] . '/' . strtolower($_arr[1]);
|
||
} else {
|
||
$path = strtolower($name);
|
||
}
|
||
} else {
|
||
$path = strtolower($name);
|
||
}
|
||
Lang::load($this->app->getAppPath() . '/lang/' . Lang::getLangset() . '/' . $path . '.php');
|
||
}
|
||
/**
|
||
* 验证数据
|
||
* @access protected
|
||
* @param array $data 数据
|
||
* @param string|array $validate 验证器名或者验证规则数组
|
||
* @param array $message 提示信息
|
||
* @param bool $batch 是否批量验证
|
||
* @return array|string|true
|
||
* @throws ValidateException
|
||
*/
|
||
protected function validate(array $data, string | array $validate, array $message = [], bool $batch = false)
|
||
{
|
||
if (is_array($validate)) {
|
||
$v = new Validate();
|
||
$v->rule($validate);
|
||
} else {
|
||
if (strpos($validate, '.')) {
|
||
[$validate, $scene] = explode('.', $validate);
|
||
}
|
||
$class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate);
|
||
$v = new $class();
|
||
if (! empty($scene)) {
|
||
$v->scene($scene);
|
||
}
|
||
}
|
||
$v->message($message);
|
||
if ($batch || $this->batchValidate) {
|
||
$v->batch(true);
|
||
}
|
||
return $v->failException(true)->check($data);
|
||
}
|
||
|
||
/**
|
||
* 成功响应(转发到 Result).
|
||
*/
|
||
public function success($data = null, $message = 'success', int $code = 0)
|
||
{
|
||
return $this->result->success($data, $message, $code);
|
||
}
|
||
|
||
/**
|
||
* 失败响应(转发到 Result).
|
||
*/
|
||
public function error($message = 'Error', int $code = 1, $data = null)
|
||
{
|
||
return $this->result->error($message, $code, $data);
|
||
}
|
||
}
|