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