507 lines
17 KiB
PHP
507 lines
17 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\index\controller;
|
||
|
||
use app\model\Banner;
|
||
use app\index\BaseController;
|
||
use app\model\Cms\FrontMenu;
|
||
use app\model\Cms\OnePage;
|
||
use app\model\System\SystemSiteSetting;
|
||
use app\service\ThemeService;
|
||
use think\db\exception\DbException;
|
||
use think\facade\Env;
|
||
use think\facade\Request;
|
||
use app\model\Cms\TemplateSiteConfig;
|
||
use app\model\Cms\Friendlink;
|
||
use app\model\Tenant\Tenant;
|
||
|
||
class Index extends BaseController
|
||
{
|
||
private ThemeService $themeService;
|
||
|
||
public function __construct()
|
||
{
|
||
$this->themeService = new ThemeService();
|
||
}
|
||
|
||
public function index()
|
||
{
|
||
// 获取请求对象
|
||
$request = $this->request ?? Request::instance();
|
||
|
||
// 优先从请求对象获取中间件设置的租户ID(通过域名解析)
|
||
$tid = $request->tenantId ?? 0;
|
||
|
||
// 兼容:从header获取
|
||
if ($tid == 0) {
|
||
$headerTid = $request->header('X-Tenant-Id');
|
||
$tid = $headerTid ? (int) $headerTid : 0;
|
||
}
|
||
|
||
// 调试:检查tid
|
||
if ($tid === 0) {
|
||
die('tenantId未获取到,请检查域名解析');
|
||
}
|
||
|
||
// 获取租户选择的模板
|
||
$themeKey = 'default';
|
||
if ($tid > 0) {
|
||
// 不使用软删除过滤
|
||
$config = TemplateSiteConfig::where('tid', $tid)
|
||
->where('key', 'current_theme')
|
||
->withoutField('delete_time')
|
||
->find();
|
||
$themeKey = $config['value'] ?? 'default';
|
||
}
|
||
|
||
// 模板路径:/themes/{theme_key}/,优先使用index.php,其次index.html
|
||
$themeBasePath = root_path() . 'public' . DIRECTORY_SEPARATOR . 'themes' . DIRECTORY_SEPARATOR . $themeKey;
|
||
$themeUrlPath = '/themes/' . $themeKey . '/';
|
||
|
||
if (is_file($themeBasePath . DIRECTORY_SEPARATOR . 'index.php')) {
|
||
// 渲染PHP模板(直接include)
|
||
return $this->renderPhpTemplate($themeBasePath . DIRECTORY_SEPARATOR . 'index.php', $themeUrlPath);
|
||
} elseif (is_file($themeBasePath . DIRECTORY_SEPARATOR . 'index.html')) {
|
||
// 读取HTML模板内容
|
||
$content = file_get_contents($themeBasePath . DIRECTORY_SEPARATOR . 'index.html');
|
||
// 修复资源路径:将相对路径转换为绝对路径
|
||
$content = $this->fixTemplateAssets($content, $themeUrlPath);
|
||
return response($content, 200, ['Content-Type' => 'text/html; charset=utf-8']);
|
||
}
|
||
|
||
// 模板不存在,返回默认
|
||
return view('index/index');
|
||
}
|
||
|
||
/**
|
||
* 通用页面渲染
|
||
* 根据页面名称(如 about, blog, portfolio 等)渲染对应模板
|
||
* @param string $page 页面名称
|
||
* @return \think\response|\think\response\View
|
||
*/
|
||
public function page(string $page = 'index')
|
||
{
|
||
$request = $this->request ?? Request::instance();
|
||
|
||
// 获取租户ID
|
||
$tid = $request->tenantId ?? 0;
|
||
if ($tid == 0) {
|
||
$headerTid = $request->header('X-Tenant-Id');
|
||
$tid = $headerTid ? (int) $headerTid : 0;
|
||
}
|
||
|
||
if ($tid === 0) {
|
||
die('tenantId未获取到,请检查域名解析');
|
||
}
|
||
|
||
// 获取租户选择的模板
|
||
$themeKey = 'default';
|
||
if ($tid > 0) {
|
||
$config = TemplateSiteConfig::where('tid', $tid)
|
||
->where('key', 'current_theme')
|
||
->withoutField('delete_time')
|
||
->find();
|
||
$themeKey = $config['value'] ?? 'default';
|
||
}
|
||
|
||
// 模板路径
|
||
$themeBasePath = root_path() . 'public' . DIRECTORY_SEPARATOR . 'themes' . DIRECTORY_SEPARATOR . $themeKey;
|
||
$themeUrlPath = '/themes/' . $themeKey . '/';
|
||
|
||
// 安全检查:只允许字母、数字、中划线
|
||
$page = preg_replace('/[^a-zA-Z0-9\-]/', '', $page);
|
||
if (empty($page)) {
|
||
$page = 'index';
|
||
}
|
||
|
||
// 查找 PHP 模板(优先)或 HTML 模板
|
||
$templateFile = $themeBasePath . DIRECTORY_SEPARATOR . $page . '.php';
|
||
$templateHtmlFile = $themeBasePath . DIRECTORY_SEPARATOR . $page . '.html';
|
||
|
||
if (is_file($templateFile)) {
|
||
return $this->renderPhpTemplate($templateFile, $themeUrlPath);
|
||
} elseif (is_file($templateHtmlFile)) {
|
||
$content = file_get_contents($templateHtmlFile);
|
||
$content = $this->fixTemplateAssets($content, $themeUrlPath);
|
||
return response($content, 200, ['Content-Type' => 'text/html; charset=utf-8']);
|
||
}
|
||
|
||
// 模板不存在
|
||
return response('页面不存在', 404, ['Content-Type' => 'text/html; charset=utf-8']);
|
||
}
|
||
|
||
/**
|
||
* 文章详情页
|
||
* @param int $id 文章ID
|
||
* @return \think\response
|
||
*/
|
||
public function articleDetail(int $id = 0)
|
||
{
|
||
$request = $this->request ?? Request::instance();
|
||
|
||
// 获取租户ID
|
||
$tid = $request->tenantId ?? 0;
|
||
if ($tid == 0) {
|
||
$headerTid = $request->header('X-Tenant-Id');
|
||
$tid = $headerTid ? (int) $headerTid : 0;
|
||
}
|
||
|
||
if ($tid === 0) {
|
||
die('tenantId未获取到,请检查域名解析');
|
||
}
|
||
|
||
// 获取租户选择的模板
|
||
$themeKey = 'default';
|
||
if ($tid > 0) {
|
||
$config = TemplateSiteConfig::where('tid', $tid)
|
||
->where('key', 'current_theme')
|
||
->withoutField('delete_time')
|
||
->find();
|
||
$themeKey = $config['value'] ?? 'default';
|
||
}
|
||
|
||
// 模板路径
|
||
$themeBasePath = root_path() . 'public' . DIRECTORY_SEPARATOR . 'themes' . DIRECTORY_SEPARATOR . $themeKey;
|
||
$themeUrlPath = '/themes/' . $themeKey . '/';
|
||
|
||
// 查找文章详情模板
|
||
$templateFile = $themeBasePath . DIRECTORY_SEPARATOR . 'article_detail.php';
|
||
$templateHtmlFile = $themeBasePath . DIRECTORY_SEPARATOR . 'article_detail.html';
|
||
$blogDetailsFile = $themeBasePath . DIRECTORY_SEPARATOR . 'blog-details.php';
|
||
$blogDetailsHtmlFile = $themeBasePath . DIRECTORY_SEPARATOR . 'blog-details.html';
|
||
|
||
// 优先使用 article_detail 模板
|
||
if (is_file($templateFile)) {
|
||
return $this->renderPhpTemplate($templateFile, $themeUrlPath);
|
||
} elseif (is_file($templateHtmlFile)) {
|
||
$content = file_get_contents($templateHtmlFile);
|
||
$content = $this->fixTemplateAssets($content, $themeUrlPath);
|
||
return response($content, 200, ['Content-Type' => 'text/html; charset=utf-8']);
|
||
} elseif (is_file($blogDetailsFile)) {
|
||
return $this->renderPhpTemplate($blogDetailsFile, $themeUrlPath);
|
||
} elseif (is_file($blogDetailsHtmlFile)) {
|
||
$content = file_get_contents($blogDetailsHtmlFile);
|
||
$content = $this->fixTemplateAssets($content, $themeUrlPath);
|
||
return response($content, 200, ['Content-Type' => 'text/html; charset=utf-8']);
|
||
}
|
||
|
||
return response('文章不存在', 404, ['Content-Type' => 'text/html; charset=utf-8']);
|
||
}
|
||
|
||
/**
|
||
* 修复模板中的资源路径
|
||
* 将相对路径 (assets/, css/, js/, images/) 转换为绝对路径 (/themes/xxx/)
|
||
*/
|
||
private function fixTemplateAssets(string $content, string $themeUrlPath): string
|
||
{
|
||
// 需要修复的资源目录
|
||
$assetsDirs = ['assets', 'css', 'js', 'images', 'img', 'fonts', 'plugins', 'libs'];
|
||
|
||
foreach ($assetsDirs as $dir) {
|
||
// 匹配 src="assets/..." 或 href="css/..." 等相对路径
|
||
// 不匹配已以 / 开头的绝对路径
|
||
$content = preg_replace(
|
||
'/(src|href)=["\'](' . $dir . '\/[^"\']*)["\']/',
|
||
'$1="' . $themeUrlPath . '$2"',
|
||
$content
|
||
);
|
||
// 匹配 src='assets/...'
|
||
$content = preg_replace(
|
||
'/(src|href)=[\']([^\/][^\'\"]*\/[^\'\"]*)[\']/',
|
||
'$1="' . $themeUrlPath . '$2"',
|
||
$content
|
||
);
|
||
}
|
||
|
||
return $content;
|
||
}
|
||
|
||
/**
|
||
* 渲染PHP模板文件
|
||
*/
|
||
private function renderPhpTemplate(string $templateFile, string $themeUrlPath = '')
|
||
{
|
||
// 定义模板基础URL常量,供模板使用
|
||
if (!defined('THEME_URL')) {
|
||
define('THEME_URL', $themeUrlPath);
|
||
}
|
||
|
||
ob_start();
|
||
// 获取URL参数
|
||
$getParams = Request::get();
|
||
extract($getParams, EXTR_OVERWRITE);
|
||
include $templateFile;
|
||
$content = ob_get_clean();
|
||
|
||
// 修复PHP模板输出中的资源路径
|
||
$content = $this->fixTemplateAssets($content, $themeUrlPath);
|
||
|
||
return response($content, 200, ['Content-Type' => 'text/html; charset=utf-8']);
|
||
}
|
||
|
||
/**
|
||
* 前端初始化接口 - 返回当前模板和填充数据
|
||
* @return \think\response\Json
|
||
*/
|
||
public function init()
|
||
{
|
||
// 优先从请求对象获取中间件设置的租户ID(通过域名解析)
|
||
$tid = $this->request->tenantId ?? 0;
|
||
|
||
// 兼容:URL参数传递的tid作为备用
|
||
$tid = $tid > 0 ? $tid : Request::param('tid', 0, 'int');
|
||
|
||
// 从TemplateSiteConfig获取配置(根据租户ID)
|
||
$config = null;
|
||
if ($tid > 0) {
|
||
$config = TemplateSiteConfig::where('tid', $tid)
|
||
->where('key', 'current_theme')
|
||
->find();
|
||
}
|
||
|
||
$themeKey = $config['value'] ?? 'default';
|
||
|
||
// 模板路径:/themes/{theme_key}/,优先使用index.php,其次index.html
|
||
$themeBasePath = root_path() . 'public' . DIRECTORY_SEPARATOR . 'themes' . DIRECTORY_SEPARATOR . $themeKey;
|
||
|
||
if (is_file($themeBasePath . DIRECTORY_SEPARATOR . 'index.php')) {
|
||
$themePath = '/themes/' . $themeKey . '/index.php';
|
||
} else {
|
||
$themePath = '/themes/' . $themeKey . '/index.html';
|
||
}
|
||
|
||
return json([
|
||
'code' => 200,
|
||
'msg' => 'success',
|
||
'data' => [
|
||
'theme_key' => $themeKey,
|
||
'theme_path' => $themePath
|
||
]
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 获取日志列表
|
||
*/
|
||
public function getLogs()
|
||
{
|
||
$logPath = Env::get('runtime_path') . 'log/';
|
||
$logs = [];
|
||
|
||
// 读取最近的日志文件
|
||
$files = glob($logPath . '*.log');
|
||
if ($files) {
|
||
// 按修改时间排序,最新的在前
|
||
usort($files, function ($a, $b) {
|
||
return filemtime($b) - filemtime($a);
|
||
});
|
||
|
||
// 读取最新的日志文件
|
||
$latestFile = reset($files);
|
||
if (file_exists($latestFile)) {
|
||
$content = file_get_contents($latestFile);
|
||
$lines = explode("\n", $content);
|
||
|
||
// 倒序遍历,只取最近的100条
|
||
$count = 0;
|
||
for ($i = count($lines) - 1; $i >= 0 && $count < 100; $i--) {
|
||
if (!empty(trim($lines[$i]))) {
|
||
$logs[] = $lines[$i];
|
||
$count++;
|
||
}
|
||
}
|
||
|
||
// 再倒序回来,使最新的日志在最后
|
||
$logs = array_reverse($logs);
|
||
}
|
||
}
|
||
|
||
return json([
|
||
'code' => 200,
|
||
'msg' => 'success',
|
||
'data' => $logs
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 获取前端导航接口
|
||
* @return \think\response\Json
|
||
*/
|
||
public function getHeadMenu()
|
||
{
|
||
try {
|
||
// 获取所有未删除的菜单
|
||
$frontMenus = FrontMenu::where('delete_time', null)
|
||
->field('id,pid,title,type,image,path,component_path,sort,desc')
|
||
->order('sort', 'desc')
|
||
->select()
|
||
->toArray(); // 转换为数组
|
||
|
||
// 使用 buildFrontMenuTree 构建树形结构
|
||
$treeFrontMenus = $this->buildFrontMenuTree($frontMenus);
|
||
|
||
return json([
|
||
'code' => 200,
|
||
'msg' => 'success',
|
||
'data' => $treeFrontMenus
|
||
]);
|
||
} catch (DbException $e) {
|
||
return json([
|
||
'code' => 500,
|
||
'msg' => 'fail:' . $e->getMessage(),
|
||
'data' => $e->getTraceAsString()
|
||
]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 构建前端导航树形结构
|
||
* @param array $frontMenus 前端导航列表
|
||
* @param int $pid 父前端导航ID
|
||
* @return array
|
||
*/
|
||
private function buildFrontMenuTree(array $frontMenus, int $pid = 0): array
|
||
{
|
||
$tree = [];
|
||
|
||
foreach ($frontMenus as $frontMenu) {
|
||
// 将null的pid视为0处理
|
||
$menuPid = is_null($frontMenu['pid']) ? 0 : $frontMenu['pid'];
|
||
if ($menuPid == $pid) {
|
||
$children = $this->buildFrontMenuTree($frontMenus, $frontMenu['id']);
|
||
if (!empty($children)) {
|
||
$frontMenu['children'] = $children;
|
||
}
|
||
$tree[] = $frontMenu;
|
||
}
|
||
}
|
||
|
||
// 按 sort 字段降序排序
|
||
usort($tree, function ($a, $b) {
|
||
$sortA = $a['sort'] ?? 0;
|
||
$sortB = $b['sort'] ?? 0;
|
||
return $sortB - $sortA;
|
||
});
|
||
|
||
return $tree;
|
||
}
|
||
|
||
/**
|
||
* 根据路径获取单页内容
|
||
* @param string $path 路由路径
|
||
* @return \think\response\Json
|
||
*/
|
||
public function getOnePageByPath($path = '')
|
||
{
|
||
try {
|
||
// 从路由参数获取路径
|
||
if (empty($path)) {
|
||
// 如果没有路由参数,尝试从 pathinfo 获取
|
||
$requestPath = $this->request->pathinfo();
|
||
$path = str_replace('onepage/', '', $requestPath);
|
||
}
|
||
|
||
// 确保路径以 / 开头
|
||
if (empty($path) || $path[0] !== '/') {
|
||
$path = '/' . $path;
|
||
}
|
||
|
||
// 解码路径
|
||
$path = urldecode($path);
|
||
|
||
// 查找对应路径的单页
|
||
$onePage = OnePage::where('path', $path)
|
||
->where('status', 1)
|
||
->where('delete_time', null)
|
||
->find();
|
||
|
||
if (!$onePage) {
|
||
return json([
|
||
'code' => 404,
|
||
'msg' => '单页不存在或已禁用',
|
||
'data' => null
|
||
]);
|
||
}
|
||
|
||
return json([
|
||
'code' => 200,
|
||
'msg' => 'success',
|
||
'data' => $onePage->toArray()
|
||
]);
|
||
} catch (DbException $e) {
|
||
return json([
|
||
'code' => 500,
|
||
'msg' => 'fail:' . $e->getMessage(),
|
||
'data' => null
|
||
]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取首页综合数据
|
||
* @return \think\response\Json
|
||
*/
|
||
public function getHomeData()
|
||
{
|
||
$baseUrl = Request::param('baseUrl', '');
|
||
|
||
if (empty($baseUrl)) {
|
||
return json(['code' => 400, 'msg' => '缺少 baseUrl 参数']);
|
||
}
|
||
|
||
try {
|
||
// 1. 通过域名获取租户ID
|
||
$tid = BaseController::getTenantIdByDomain($baseUrl);
|
||
|
||
if (empty($tid)) {
|
||
return json([
|
||
'code' => 400,
|
||
'msg' => '无法识别租户信息',
|
||
'data' => []
|
||
]);
|
||
}
|
||
|
||
// 2. 获取站点基础信息 (normalinfos)
|
||
$normalInfos = SystemSiteSetting::where('tid', $tid)
|
||
->field('sitename,logo,logow,ico,description,copyright,companyname,icp,companyintroduction')
|
||
->find();
|
||
|
||
// 3. 获取友情链接列表
|
||
$friendlinkList = Friendlink::where('delete_time', null)
|
||
->where('tid', $tid)
|
||
->where('status', 1)
|
||
->order('sort', 'asc')
|
||
->field('id,link_name,link_url,link_logo')
|
||
->select()
|
||
->toArray();
|
||
|
||
// 获取联系方式
|
||
$contact = Tenant::where('id', $tid)
|
||
->where('status', 1)
|
||
->where('delete_time', null)
|
||
->field('contact_phone,contact_email,address,worktime')
|
||
->find();
|
||
|
||
// 4. 合并返回
|
||
return json([
|
||
'code' => 200,
|
||
'msg' => 'success',
|
||
'data' => [
|
||
'normal' => $normalInfos ?: (object) [],
|
||
'contact' => $contact ?: (object) [],
|
||
'links' => $friendlinkList
|
||
]
|
||
]);
|
||
|
||
} catch (\Exception $e) {
|
||
return json([
|
||
'code' => 500,
|
||
'msg' => '服务器错误:' . $e->getMessage(),
|
||
'data' => null
|
||
]);
|
||
}
|
||
}
|
||
}
|