This commit is contained in:
www
2026-05-26 17:02:57 +08:00
parent 7f910ee22f
commit 0096805a49
14 changed files with 830 additions and 58 deletions

1
code/.gitignore vendored
View File

@@ -21,4 +21,5 @@ public/static/favicon/generated/*
/app/public/_admin_templates/video-metadata-missing-task-pool/*
/app/public/_admin_templates/video-metadata-missing-workbench/*
/data/seo_copy_published/*
/data/seo_resource/keyword_feedback/*
/storage/*

View File

@@ -7,6 +7,7 @@ use app\admin\controller\Index;
use app\admin\controller\Domain;
use app\admin\controller\SeoSupply;
use app\admin\controller\Novel;
use app\admin\controller\ServerNode;
use app\admin\controller\Site;
use app\admin\controller\SystemConfig;
use app\admin\controller\Video;
@@ -32,6 +33,9 @@ Route::group("/system", function () {
Route::post("/config/save", [SystemConfig::class, "saveSystemConfig"])->name("SystemConfig@saveSystemConfig");
Route::post("/config/seo-ai-rules/reset", [SystemConfig::class, "resetSeoAiRules"])->name("SystemConfig@resetSeoAiRules");
Route::get("/storage/file", [SystemConfig::class, "previewStorageFile"])->name("SystemConfig@previewStorageFile");
Route::get("/node/list", [ServerNode::class, "getServerNodeList"])->name("ServerNode@getServerNodeList");
Route::post("/node/save", [ServerNode::class, "saveServerNode"])->name("ServerNode@saveServerNode");
Route::post("/node/del", [ServerNode::class, "delServerNode"])->name("ServerNode@delServerNode");
Route::get("/cjpz/list", [SystemConfig::class, "getCaiJiPeiZhiList"])->name("SystemConfig@getCaiJiPeiZhiList");
Route::post("/cjpz/save", [SystemConfig::class, "saveCaiJiPeiZhi"])->name("SystemConfig@saveCaiJiPeiZhi");

View File

@@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
namespace app\admin\controller;
use app\admin\BaseController;
use app\model\AdminUserModel;
use app\model\ServerNodeModel;
use app\Request;
use think\Response;
class ServerNode extends BaseController
{
/**
* 前端服务器节点列表
*/
public function getServerNodeList(Request $Request, ?AdminUserModel $AdminUserModel): Response
{
$arrData = [
'page' => (int)$Request->get('page', 1),
'limit' => (int)$Request->get('limit', 10),
'key' => trim((string)$Request->get('key', '')),
];
$this->validate($arrData, [
'page' => 'require|number',
'limit' => 'require|number',
]);
$ServerNodeModel = ServerNodeModel::alias('sn')
->order('sn.sn_status', 'desc')
->order('sn.updated_at', 'desc')
->order('sn.sn_id', 'desc');
if ($arrData['key'] !== '') {
$strKey = $arrData['key'];
$ServerNodeModel->where(function ($query) use ($strKey) {
$query->whereOr([
['sn.sn_name', 'like', '%' . $strKey . '%'],
['sn.sn_code', 'like', '%' . $strKey . '%'],
['sn.sn_host', 'like', '%' . $strKey . '%'],
['sn.sn_username', 'like', '%' . $strKey . '%'],
['sn.sn_region', 'like', '%' . $strKey . '%'],
['sn.sn_purpose', 'like', '%' . $strKey . '%'],
['sn.sn_project_dir', 'like', '%' . $strKey . '%'],
['sn.sn_log_dir', 'like', '%' . $strKey . '%'],
['sn.sn_nginx_dir', 'like', '%' . $strKey . '%'],
]);
});
}
$Paginate = $ServerNodeModel->paginate([
'list_rows' => $arrData['limit'],
'page' => $arrData['page'],
]);
$arrItems = [];
foreach ($Paginate->items() as $arrItem) {
$arrItems[] = $this->serializeServerNodeRow($arrItem);
}
return $this->success([
'items' => $arrItems,
'total' => $Paginate->total(),
'current_page' => $Paginate->currentPage(),
'total_pages' => $Paginate->lastPage(),
]);
}
/**
* 保存前端服务器节点
*/
public function saveServerNode(Request $Request, ?AdminUserModel $AdminUserModel): Response
{
$arrData = [
'sn_id' => $Request->post('sn_id'),
'sn_name' => trim((string)$Request->post('sn_name', '')),
'sn_code' => $this->normalizeNullableString($Request->post('sn_code')),
'sn_host' => trim((string)$Request->post('sn_host', '')),
'sn_port' => (int)$Request->post('sn_port', 22),
'sn_username' => trim((string)$Request->post('sn_username', '')),
'sn_auth_type' => trim((string)$Request->post('sn_auth_type', 'password')),
'sn_password' => trim((string)$Request->post('sn_password', '')),
'sn_private_key' => trim((string)$Request->post('sn_private_key', '')),
'sn_passphrase' => trim((string)$Request->post('sn_passphrase', '')),
'sn_region' => $this->normalizeNullableString($Request->post('sn_region')),
'sn_purpose' => $this->normalizeNullableString($Request->post('sn_purpose')),
'sn_project_dir' => $this->normalizeNullableString($Request->post('sn_project_dir')),
'sn_log_dir' => $this->normalizeNullableString($Request->post('sn_log_dir')),
'sn_nginx_dir' => $this->normalizeNullableString($Request->post('sn_nginx_dir')),
'sn_note' => $this->normalizeNullableString($Request->post('sn_note')),
'sn_status' => (int)$Request->post('sn_status', 1),
];
$this->validate($arrData, [
'sn_name' => 'require|max:255',
'sn_host' => 'require|max:255',
'sn_port' => 'require|number|between:1,65535',
'sn_username' => 'require|max:100',
'sn_auth_type' => 'require|in:password,private_key',
'sn_status' => 'require|in:0,1',
]);
$boolIsCreate = empty($arrData['sn_id']);
if ($boolIsCreate) {
$ServerNodeModel = new ServerNodeModel();
} else {
$ServerNodeModel = ServerNodeModel::where('sn_id', (int)$arrData['sn_id'])->find();
if (empty($ServerNodeModel)) {
return $this->error('9993', '服务器节点不存在');
}
}
if ($arrData['sn_code'] !== null) {
$DuplicateModel = ServerNodeModel::where('sn_code', $arrData['sn_code']);
if (!$boolIsCreate) {
$DuplicateModel->where('sn_id', '<>', (int)$arrData['sn_id']);
}
if ($DuplicateModel->find() !== null) {
return $this->error('9993', '节点编码已存在,请更换后重试');
}
}
$strCurrentAuthType = trim((string)($ServerNodeModel->sn_auth_type ?? ''));
$boolHasExistingPassword = trim((string)($ServerNodeModel->sn_password ?? '')) !== '';
$boolHasExistingPrivateKey = trim((string)($ServerNodeModel->sn_private_key ?? '')) !== '';
if ($arrData['sn_auth_type'] === 'password') {
if ($arrData['sn_password'] === '') {
if ($boolIsCreate || $strCurrentAuthType !== 'password' || !$boolHasExistingPassword) {
return $this->error('9993', '当前登录方式为密码,请填写 SSH 密码');
}
unset($arrData['sn_password']);
}
$arrData['sn_private_key'] = '';
$arrData['sn_passphrase'] = '';
} else {
if ($arrData['sn_private_key'] === '') {
if ($boolIsCreate || $strCurrentAuthType !== 'private_key' || !$boolHasExistingPrivateKey) {
return $this->error('9993', '当前登录方式为私钥,请填写 SSH 私钥');
}
unset($arrData['sn_private_key']);
if ($arrData['sn_passphrase'] === '') {
unset($arrData['sn_passphrase']);
}
}
$arrData['sn_password'] = '';
}
$ServerNodeModel->fill($arrData);
$ServerNodeModel->save();
return $this->success([
'sn_id' => (int)$ServerNodeModel->sn_id,
], $boolIsCreate ? '服务器节点新增成功' : '服务器节点保存成功');
}
/**
* 删除前端服务器节点
*/
public function delServerNode(Request $Request, ?AdminUserModel $AdminUserModel): Response
{
$arrData = [
'sn_id' => trim((string)$Request->post('sn_id', '')),
];
$this->validate($arrData, [
'sn_id' => 'require',
]);
$arrIds = array_values(array_unique(array_filter(array_map('intval', explode(',', $arrData['sn_id'])), static function (int $intId): bool {
return $intId > 0;
})));
if ($arrIds === []) {
return $this->error('9993', '未识别到可删除的服务器节点');
}
ServerNodeModel::whereIn('sn_id', $arrIds)->delete();
return $this->success();
}
/**
* 屏蔽列表接口里的敏感 SSH 凭证,只返回配置状态。
*/
protected function serializeServerNodeRow($arrItem): array
{
$arrRow = is_array($arrItem) ? $arrItem : $arrItem->toArray();
$arrRow['sn_port'] = (int)($arrRow['sn_port'] ?? 22);
$arrRow['sn_status'] = (int)($arrRow['sn_status'] ?? 1);
$arrRow['sn_password_configured'] = trim((string)($arrRow['sn_password'] ?? '')) !== '';
$arrRow['sn_private_key_configured'] = trim((string)($arrRow['sn_private_key'] ?? '')) !== '';
$arrRow['sn_passphrase_configured'] = trim((string)($arrRow['sn_passphrase'] ?? '')) !== '';
unset($arrRow['sn_password'], $arrRow['sn_private_key'], $arrRow['sn_passphrase']);
return $arrRow;
}
protected function normalizeNullableString($value): ?string
{
$strValue = trim((string)$value);
return $strValue === '' ? null : $strValue;
}
}

View File

@@ -16,7 +16,7 @@ class DomainExternalSeoSnapshotAnalysisHelper
$arrProviders = [];
$arrHosts = [];
$arrLatestIndexByHost = [];
$arrLatestSignalsByHost = [];
$intKeywordSnapshots = (int)SeoExternalSnapshotModel::where('metric_date', '>=', $strStartDate)
->where('scope', 'keyword')
->count();
@@ -28,9 +28,10 @@ class DomainExternalSeoSnapshotAnalysisHelper
self::eachSnapshotRow(
SeoExternalSnapshotModel::where('metric_date', '>=', $strStartDate)
->where('scope', 'host'),
['id', 'provider', 'host', 'scope', 'metric_date', 'status', 'indexed_status', 'baidu_pc_ip_range', 'baidu_mobile_ip_range', 'pc_keyword_count', 'mobile_keyword_count', 'queried_at'],
static function (array $arrItem) use (&$arrProviders, &$arrHosts, &$arrLatestIndexByHost, &$strLatestMetricDate): void {
['id', 'provider', 'snapshot_type', 'host', 'scope', 'metric_date', 'status', 'indexed_status', 'result_count_text', 'baidu_pc_ip_range', 'baidu_mobile_ip_range', 'pc_keyword_count', 'mobile_keyword_count', 'queried_at'],
static function (array $arrItem) use (&$arrProviders, &$arrHosts, &$arrLatestSignalsByHost, &$strLatestMetricDate): void {
$strProvider = trim((string)($arrItem['provider'] ?? ''));
$strSnapshotType = trim((string)($arrItem['snapshot_type'] ?? ''));
$strHost = trim((string)($arrItem['host'] ?? ''));
$strMetricDate = trim((string)($arrItem['metric_date'] ?? ''));
$strStatus = trim((string)($arrItem['status'] ?? ''));
@@ -48,34 +49,33 @@ class DomainExternalSeoSnapshotAnalysisHelper
}
if ($strHost !== '') {
$strHostKey = $strHost;
$arrCurrent = $arrLatestIndexByHost[$strHostKey] ?? null;
$boolShouldReplace = false;
if (!$arrCurrent) {
$boolShouldReplace = true;
} else {
$strCurrentDate = (string)($arrCurrent['metric_date'] ?? '');
$intCurrentQueriedAt = (int)($arrCurrent['queried_at'] ?? 0);
if (strcmp($strCurrentDate, $strMetricDate) < 0) {
$boolShouldReplace = true;
} elseif ($strCurrentDate === $strMetricDate && $intCurrentQueriedAt < $intQueriedAt) {
$boolShouldReplace = true;
}
$arrCandidate = [
'host' => $strHost,
'provider' => $strProvider,
'snapshot_type' => $strSnapshotType,
'metric_date' => $strMetricDate,
'queried_at' => $intQueriedAt,
'status' => $strStatus,
'indexed_status' => $strIndexedStatus,
'result_count_text' => trim((string)($arrItem['result_count_text'] ?? '')),
'baidu_pc_ip_range' => trim((string)($arrItem['baidu_pc_ip_range'] ?? '')),
'baidu_mobile_ip_range' => trim((string)($arrItem['baidu_mobile_ip_range'] ?? '')),
'pc_keyword_count' => (int)($arrItem['pc_keyword_count'] ?? 0),
'mobile_keyword_count' => (int)($arrItem['mobile_keyword_count'] ?? 0),
];
if (self::shouldReplaceSnapshot($arrLatestSignalsByHost[$strHost]['display'] ?? null, $strMetricDate, $intQueriedAt)) {
$arrLatestSignalsByHost[$strHost]['display'] = $arrCandidate;
}
if ($boolShouldReplace) {
$arrLatestIndexByHost[$strHostKey] = [
'host' => $strHost,
'provider' => $strProvider,
'metric_date' => $strMetricDate,
'queried_at' => $intQueriedAt,
'status' => $strStatus,
'indexed_status' => $strIndexedStatus,
'baidu_pc_ip_range' => trim((string)($arrItem['baidu_pc_ip_range'] ?? '')),
'baidu_mobile_ip_range' => trim((string)($arrItem['baidu_mobile_ip_range'] ?? '')),
'pc_keyword_count' => (int)($arrItem['pc_keyword_count'] ?? 0),
'mobile_keyword_count' => (int)($arrItem['mobile_keyword_count'] ?? 0),
];
if ($strSnapshotType === 'site_query'
&& self::shouldReplaceSnapshot($arrLatestSignalsByHost[$strHost]['index'] ?? null, $strMetricDate, $intQueriedAt)) {
$arrLatestSignalsByHost[$strHost]['index'] = $arrCandidate;
}
if ($strSnapshotType === 'aizhan_summary'
&& self::shouldReplaceSnapshot($arrLatestSignalsByHost[$strHost]['keyword'] ?? null, $strMetricDate, $intQueriedAt)) {
$arrLatestSignalsByHost[$strHost]['keyword'] = $arrCandidate;
}
}
},
@@ -85,13 +85,44 @@ class DomainExternalSeoSnapshotAnalysisHelper
$intIndexedLikeCount = 0;
$intKeywordReadyCount = 0;
$intTrafficReadyCount = 0;
$arrHostRows = array_values($arrLatestIndexByHost);
$arrHostRows = [];
foreach ($arrLatestSignalsByHost as $strHost => $arrSignals) {
$arrDisplay = (array)($arrSignals['display'] ?? []);
$arrIndex = (array)($arrSignals['index'] ?? []);
$arrKeyword = (array)($arrSignals['keyword'] ?? []);
$arrProvidersForHost = [];
foreach ([$arrIndex, $arrKeyword, $arrDisplay] as $arrSignalRow) {
$strSignalProvider = trim((string)($arrSignalRow['provider'] ?? ''));
if ($strSignalProvider !== '') {
$arrProvidersForHost[$strSignalProvider] = true;
}
}
$arrHostRows[] = [
'host' => $strHost,
'provider' => implode(',', array_keys($arrProvidersForHost)),
'index_provider' => (string)($arrIndex['provider'] ?? ''),
'keyword_provider' => (string)($arrKeyword['provider'] ?? ''),
'metric_date' => max((string)($arrDisplay['metric_date'] ?? ''), (string)($arrIndex['metric_date'] ?? ''), (string)($arrKeyword['metric_date'] ?? '')),
'queried_at' => max((int)($arrDisplay['queried_at'] ?? 0), (int)($arrIndex['queried_at'] ?? 0), (int)($arrKeyword['queried_at'] ?? 0)),
'status' => (string)($arrDisplay['status'] ?? ''),
'indexed_status' => (string)($arrIndex['indexed_status'] ?? ''),
'result_count_text' => (string)($arrIndex['result_count_text'] ?? ''),
'baidu_pc_ip_range' => (string)($arrKeyword['baidu_pc_ip_range'] ?? ''),
'baidu_mobile_ip_range' => (string)($arrKeyword['baidu_mobile_ip_range'] ?? ''),
'pc_keyword_count' => (int)($arrKeyword['pc_keyword_count'] ?? 0),
'mobile_keyword_count' => (int)($arrKeyword['mobile_keyword_count'] ?? 0),
];
}
foreach ($arrHostRows as &$arrRow) {
$boolIndexedLike = (string)($arrRow['indexed_status'] ?? '') === 'indexed_like';
$boolIndexedLike = self::normalizeIndexState($arrRow) === 'indexed_like';
$boolKeywordReady = (int)($arrRow['pc_keyword_count'] ?? 0) > 0 || (int)($arrRow['mobile_keyword_count'] ?? 0) > 0;
$boolTrafficReady = !self::isZeroRange((string)($arrRow['baidu_pc_ip_range'] ?? ''))
|| !self::isZeroRange((string)($arrRow['baidu_mobile_ip_range'] ?? ''));
$arrRow['indexed_like'] = $boolIndexedLike;
$arrRow['keyword_ready'] = $boolKeywordReady;
$arrRow['traffic_ready'] = $boolTrafficReady;
@@ -113,6 +144,11 @@ class DomainExternalSeoSnapshotAnalysisHelper
if ($intLeftKeyword !== $intRightKeyword) {
return $intRightKeyword <=> $intLeftKeyword;
}
$intLeftIndexed = !empty($arrLeft['indexed_like']) ? 1 : 0;
$intRightIndexed = !empty($arrRight['indexed_like']) ? 1 : 0;
if ($intLeftIndexed !== $intRightIndexed) {
return $intRightIndexed <=> $intLeftIndexed;
}
return strcmp((string)($arrLeft['host'] ?? ''), (string)($arrRight['host'] ?? ''));
});
@@ -402,6 +438,19 @@ class DomainExternalSeoSnapshotAnalysisHelper
return $strRange === '' || $strRange === '0 ~ 0' || $strRange === '0~0';
}
protected static function shouldReplaceSnapshot(?array $arrCurrent, string $strMetricDate, int $intQueriedAt): bool
{
if (!$arrCurrent) {
return true;
}
$strCurrentDate = (string)($arrCurrent['metric_date'] ?? '');
$intCurrentQueriedAt = (int)($arrCurrent['queried_at'] ?? 0);
return strcmp($strCurrentDate, $strMetricDate) < 0
|| ($strCurrentDate === $strMetricDate && $intCurrentQueriedAt < $intQueriedAt);
}
protected static function normalizeIndexState(array $arrRow): string
{
$strIndexedStatus = trim((string)($arrRow['indexed_status'] ?? ''));

View File

@@ -27,14 +27,6 @@ class JsBuilder
@mkdir($targetDir, 0755, true);
}
// 输出文件名(域名专属缓存)
$targetFile = $targetDir . "{$staticHash}.js";
// 如果已经生成过,直接返回
if (file_exists($targetFile)) {
return "/static/js/compiled/{$staticHash}.js";
}
// 你可以像 CSS 一样在这里挂一个“全站基础 JS”
// 注意:如果没有这个文件,就别写入,否则会被 skip
$listJs = [
@@ -46,9 +38,46 @@ class JsBuilder
$finalJsFiles = array_merge($listJs, $jsFiles);
$finalJsFiles = array_values(array_unique(array_filter($finalJsFiles)));
$signatureParts = [
'static_hash' => $staticHash,
'dom_prefix' => $domPrefix,
'builder' => is_file(__FILE__) ? sha1_file(__FILE__) : (string)@filemtime(__FILE__),
'files' => [],
];
// 如果 compiled 已存在且比所有源文件都新,直接返回;否则自动重建。
$intLatestSourceTime = @filemtime(__FILE__) ?: 0;
foreach ($finalJsFiles as $file) {
$file = ltrim((string)$file, '/');
$path = $baseDir . $file;
if (is_file($path)) {
$intLatestSourceTime = max($intLatestSourceTime, (int)@filemtime($path));
$signatureParts['files'][] = [
'file' => $file,
'mtime' => (int)@filemtime($path),
'size' => (int)@filesize($path),
'sha1' => sha1_file($path),
];
} else {
$signatureParts['files'][] = [
'file' => $file,
'missing' => true,
];
}
}
// 前端代理的静态缓存按 path 命中query version 无法兜底;文件名必须跟随源码变化。
$sourceHash = substr(sha1((string)json_encode($signatureParts, JSON_UNESCAPED_SLASHES)), 0, 10);
$targetName = "{$staticHash}-{$sourceHash}.js";
$targetFile = $targetDir . $targetName;
if (file_exists($targetFile) && (int)@filemtime($targetFile) >= $intLatestSourceTime) {
return "/static/js/compiled/{$targetName}";
}
// 合并 JS 文件内容
$allJs = "";
$allJs .= "/*! compiled: {$staticHash}.js */\n";
$allJs .= "/*! compiled: {$targetName} */\n";
$allJs .= "(function(){\n'use strict';\n";
foreach ($finalJsFiles as $file) {
@@ -82,6 +111,6 @@ class JsBuilder
file_put_contents($targetFile, $allJs);
// 返回前端可访问路径
return "/static/js/compiled/{$staticHash}.js";
return "/static/js/compiled/{$targetName}";
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace app\model;
/**
* 前端服务器节点模型
*
* @mixin \think\Model
*/
class ServerNodeModel extends BaseModel
{
protected $name = 'server_node';
protected $pk = 'sn_id';
protected $createTime = 'created_at';
protected $updateTime = 'updated_at';
}

View File

@@ -0,0 +1,88 @@
<?php
use think\migration\Migrator;
use think\migration\db\Column;
class CreateServerNodeTable extends Migrator
{
public function up()
{
if (!$this->hasTable('server_node')) {
$this->table('server_node', ['id' => false])
->addColumn(Column::integer('sn_id')->setUnsigned()->setIdentity(true)->setComment('节点ID'))
->addColumn(Column::string('sn_name', 255)->setDefault('')->setComment('节点名称'))
->addColumn(Column::string('sn_code', 100)->setNullable(true)->setComment('节点编码'))
->addColumn(Column::string('sn_host', 255)->setDefault('')->setComment('SSH 主机/IP'))
->addColumn(Column::smallInteger('sn_port')->setUnsigned()->setDefault(22)->setComment('SSH 端口'))
->addColumn(Column::string('sn_username', 100)->setDefault('')->setComment('SSH 用户名'))
->addColumn(Column::string('sn_auth_type', 20)->setDefault('password')->setComment('认证方式: password/private_key'))
->addColumn(Column::text('sn_password')->setNullable(true)->setComment('SSH 密码'))
->addColumn(Column::mediumText('sn_private_key')->setNullable(true)->setComment('SSH 私钥'))
->addColumn(Column::text('sn_passphrase')->setNullable(true)->setComment('私钥口令'))
->addColumn(Column::string('sn_region', 100)->setNullable(true)->setComment('区域/环境'))
->addColumn(Column::string('sn_purpose', 255)->setNullable(true)->setComment('机器用途'))
->addColumn(Column::string('sn_project_dir', 500)->setNullable(true)->setComment('项目目录'))
->addColumn(Column::string('sn_log_dir', 500)->setNullable(true)->setComment('日志目录'))
->addColumn(Column::string('sn_nginx_dir', 500)->setNullable(true)->setComment('Nginx 配置目录'))
->addColumn(Column::text('sn_note')->setNullable(true)->setComment('备注'))
->addColumn(Column::tinyInteger('sn_status')->setUnsigned()->setDefault(1)->setComment('状态:1启用,0禁用'))
->addTimestamps('created_at', 'updated_at')
->addIndex(['sn_id'], ['type' => 'primary'])
->addIndex(['sn_code'], ['unique' => true])
->addIndex(['sn_status'])
->addIndex(['sn_host'])
->setComment('前端服务器节点')
->create();
return;
}
$Table = $this->table('server_node');
$this->addColumnIfMissing($Table, 'sn_name', Column::string('sn_name', 255)->setDefault('')->setComment('节点名称'));
$this->addColumnIfMissing($Table, 'sn_code', Column::string('sn_code', 100)->setNullable(true)->setComment('节点编码'));
$this->addColumnIfMissing($Table, 'sn_host', Column::string('sn_host', 255)->setDefault('')->setComment('SSH 主机/IP'));
$this->addColumnIfMissing($Table, 'sn_port', Column::smallInteger('sn_port')->setUnsigned()->setDefault(22)->setComment('SSH 端口'));
$this->addColumnIfMissing($Table, 'sn_username', Column::string('sn_username', 100)->setDefault('')->setComment('SSH 用户名'));
$this->addColumnIfMissing($Table, 'sn_auth_type', Column::string('sn_auth_type', 20)->setDefault('password')->setComment('认证方式: password/private_key'));
$this->addColumnIfMissing($Table, 'sn_password', Column::text('sn_password')->setNullable(true)->setComment('SSH 密码'));
$this->addColumnIfMissing($Table, 'sn_private_key', Column::mediumText('sn_private_key')->setNullable(true)->setComment('SSH 私钥'));
$this->addColumnIfMissing($Table, 'sn_passphrase', Column::text('sn_passphrase')->setNullable(true)->setComment('私钥口令'));
$this->addColumnIfMissing($Table, 'sn_region', Column::string('sn_region', 100)->setNullable(true)->setComment('区域/环境'));
$this->addColumnIfMissing($Table, 'sn_purpose', Column::string('sn_purpose', 255)->setNullable(true)->setComment('机器用途'));
$this->addColumnIfMissing($Table, 'sn_project_dir', Column::string('sn_project_dir', 500)->setNullable(true)->setComment('项目目录'));
$this->addColumnIfMissing($Table, 'sn_log_dir', Column::string('sn_log_dir', 500)->setNullable(true)->setComment('日志目录'));
$this->addColumnIfMissing($Table, 'sn_nginx_dir', Column::string('sn_nginx_dir', 500)->setNullable(true)->setComment('Nginx 配置目录'));
$this->addColumnIfMissing($Table, 'sn_note', Column::text('sn_note')->setNullable(true)->setComment('备注'));
$this->addColumnIfMissing($Table, 'sn_status', Column::tinyInteger('sn_status')->setUnsigned()->setDefault(1)->setComment('状态:1启用,0禁用'));
$this->addColumnIfMissing($Table, 'created_at', Column::timestamp('created_at')->setDefault('CURRENT_TIMESTAMP'));
$this->addColumnIfMissing($Table, 'updated_at', Column::timestamp('updated_at')->setDefault('CURRENT_TIMESTAMP')->setUpdate('CURRENT_TIMESTAMP'));
$this->addIndexIfMissing($Table, ['sn_code'], ['unique' => true]);
$this->addIndexIfMissing($Table, ['sn_status']);
$this->addIndexIfMissing($Table, ['sn_host']);
$Table->update();
}
public function down()
{
if ($this->hasTable('server_node')) {
$this->table('server_node')->drop()->save();
}
}
protected function addColumnIfMissing($table, string $columnName, Column $column): void
{
if (!$table->hasColumn($columnName)) {
$table->addColumn($column);
}
}
protected function addIndexIfMissing($table, array $columns, array $options = []): void
{
if (!$table->hasIndex($columns)) {
$table->addIndex($columns, $options);
}
}
}

View File

@@ -179,6 +179,40 @@ CREATE TABLE `plan_task` (
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `server_node`
--
DROP TABLE IF EXISTS `server_node`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `server_node` (
`sn_id` int unsigned NOT NULL AUTO_INCREMENT,
`sn_name` varchar(255) NOT NULL DEFAULT '' COMMENT '节点名称',
`sn_code` varchar(100) DEFAULT NULL COMMENT '节点编码',
`sn_host` varchar(255) NOT NULL DEFAULT '' COMMENT 'SSH 主机/IP',
`sn_port` smallint unsigned NOT NULL DEFAULT '22' COMMENT 'SSH 端口',
`sn_username` varchar(100) NOT NULL DEFAULT '' COMMENT 'SSH 用户名',
`sn_auth_type` varchar(20) NOT NULL DEFAULT 'password' COMMENT '认证方式: password/private_key',
`sn_password` text COMMENT 'SSH 密码',
`sn_private_key` mediumtext COMMENT 'SSH 私钥',
`sn_passphrase` text COMMENT '私钥口令',
`sn_region` varchar(100) DEFAULT NULL COMMENT '区域/环境',
`sn_purpose` varchar(255) DEFAULT NULL COMMENT '机器用途',
`sn_project_dir` varchar(500) DEFAULT NULL COMMENT '项目目录',
`sn_log_dir` varchar(500) DEFAULT NULL COMMENT '日志目录',
`sn_nginx_dir` varchar(500) DEFAULT NULL COMMENT 'Nginx 配置目录',
`sn_note` text COMMENT '备注',
`sn_status` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '状态:1启用,0禁用',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`sn_id`) USING BTREE,
UNIQUE KEY `sn_code` (`sn_code`) USING BTREE,
KEY `sn_status` (`sn_status`) USING BTREE,
KEY `sn_host` (`sn_host`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='前端服务器节点';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `subject_fomart`
--

View File

@@ -5,7 +5,7 @@
* -----------------------------------------------------
* - 不使用全局变量
* - 支持一个页面多个播放器
* - 初始化 data-engine="dplayer" 的模块
* - 优先初始化 data-engine="dplayer" 的模块,兼容旧模板里的 #dplayer
* =====================================================
*/
@@ -22,8 +22,16 @@
}
function resolveContainer(root) {
if (root && root.querySelector) {
return root.querySelector('.dplayer, [id="dplayer"]');
if (root) {
if (root.matches && (root.matches('.dplayer') || root.id === 'dplayer')) {
return root;
}
if (root.querySelector) {
var scoped = root.querySelector('.dplayer, [id="dplayer"]');
if (scoped) {
return scoped;
}
}
}
return document.getElementById('dplayer') || document.querySelector('.dplayer');
@@ -35,6 +43,18 @@
showError(root, '播放器容器不存在');
return null;
}
if (!strPlayUrl) {
showError(root, '播放地址不存在');
return null;
}
if (typeof DPlayer === "undefined") {
showError(root, '播放器脚本加载失败');
return null;
}
if (DomDPlayer.dataset.dplayerReady === '1') {
return null;
}
DomDPlayer.dataset.dplayerReady = '1';
// ---- 创建 DPlayer 实例 ----
try {
@@ -54,7 +74,11 @@
},
});
videoPlayer.play()
window.videoPlayer = videoPlayer;
var playResult = videoPlayer.play()
if (playResult && playResult.catch) {
playResult.catch(function () {});
}
// ---- 错误处理(兜底)----
videoPlayer.on('error', function () {
@@ -64,7 +88,8 @@
return videoPlayer;
} catch (e) {
console.log(e)
console.warn(e);
DomDPlayer.dataset.dplayerReady = '0';
showError(root, '播放器初始化失败');
return null;
}
@@ -77,10 +102,11 @@
}
ensurePrefix(root);
var err = root.querySelector('.' + root.dataset.prefix + '-player-status');
var errClass = root.dataset.prefix ? root.dataset.prefix + '-player-status' : 'player-status';
var err = root.querySelector('.' + errClass);
if (!err) {
err = document.createElement('div');
err.className = root.dataset.prefix + '-player-status is-error';
err.className = errClass + ' is-error';
root.appendChild(err);
}
err.innerText = msg;
@@ -95,6 +121,20 @@
});
}
function resolveRoot() {
var root = document.querySelector('[data-engine="dplayer"]');
if (root) {
return root;
}
var container = document.getElementById('dplayer') || document.querySelector('.dplayer');
if (!container) {
return null;
}
return container.closest ? (container.closest('[data-engine="dplayer"]') || container) : container;
}
// 获取第一个线路 key带容错
function getDefaultLine() {
if (typeof arrPlayUrl === "undefined" || !arrPlayUrl || typeof arrPlayUrl !== "object") {
@@ -120,23 +160,36 @@
return arrPlayUrl[playLine][0].url || null;
}
function getIndexedPlayUrl(playLine, playIndex) {
if (!arrPlayUrl || !arrPlayUrl[playLine]) {
console.warn("播放线路不存在:", playLine, arrPlayUrl);
return null;
}
var intIndex = Math.max(1, parseInt(playIndex, 10) || 1);
var activeUrl = arrPlayUrl[playLine][intIndex - 1] || arrPlayUrl[playLine][0];
return activeUrl ? (activeUrl.url || null) : null;
}
// DOM Ready
document.addEventListener("DOMContentLoaded", function () {
try {
if (typeof strVideoId == "undefined") return;
var root = document.querySelector('[data-engine="dplayer"]');
var root = resolveRoot();
if (!root) return;
boot();
ensurePrefix(root);
console.log(strPlayType)
console.log(boolIsPlayPage)
if (typeof strPlayType !== "undefined" && boolIsPlayPage) {
// 用户指定线路
if (strPlayType !== "default") {
const activeUrl = arrPlayUrl[strPlayType][intPlayUrlIndex - 1];
strPlayUrl = activeUrl.url;
strPlayUrl = getIndexedPlayUrl(strPlayType, intPlayUrlIndex);
if (!strPlayUrl) {
const defaultLine = getDefaultLine();
strPlayUrl = getFirstPlayUrl(defaultLine);
}
// checkLine(strPlayType);
// 默认播放第一个线路的第一个 URL
@@ -145,7 +198,6 @@
strPlayUrl = getFirstPlayUrl(defaultLine);
// checkLine(defaultLine);
}
console.log('strPlayUrlstrPlayUrl')
initDPlayer(root, strPlayUrl);
} else if (strPlayType == "default" && !boolIsPlayPage) {
@@ -157,9 +209,7 @@
initDPlayer(root, strPlayUrl);
}
} catch (error) {
console.log(error);
console.warn(error);
}
})
})();