Compare commits
80 Commits
7fff1a9944
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0096805a49 | ||
|
|
7f910ee22f | ||
|
|
97aefcf86b | ||
|
|
b764e7555d | ||
|
|
04242363a8 | ||
|
|
c1ac050c6a | ||
|
|
b943ea2ab7 | ||
|
|
e568577ce0 | ||
|
|
66a8086a17 | ||
|
|
328946f1a5 | ||
|
|
2b2b822631 | ||
|
|
566e966582 | ||
|
|
53a1d7e4fd | ||
|
|
d469c4b93d | ||
|
|
fccd2bfe5b | ||
|
|
e8688443e5 | ||
|
|
da09dbd64d | ||
|
|
23b9a719d8 | ||
|
|
cec1fdbc69 | ||
|
|
2ea87050a5 | ||
|
|
02a65d97ee | ||
|
|
2a5685605b | ||
|
|
a87a4a41cc | ||
|
|
5cb11b4476 | ||
|
|
c8fb6956ef | ||
|
|
4dc78e3366 | ||
|
|
309ef80383 | ||
|
|
eb33bb7d91 | ||
|
|
6d73d61e49 | ||
|
|
186dd80dd0 | ||
|
|
84f1d18526 | ||
|
|
aa406cc35c | ||
|
|
8297cf342c | ||
|
|
b49d67df52 | ||
|
|
db249204fb | ||
|
|
5aaa44f7a2 | ||
|
|
87b1caaedb | ||
|
|
56ab9a95a6 | ||
|
|
62e3eed6f4 | ||
|
|
9a7cbfa3b7 | ||
|
|
16962332fd | ||
|
|
586ca4d5a2 | ||
|
|
f9fc2b658a | ||
|
|
875c106908 | ||
|
|
49d8e39773 | ||
|
|
72cc7cb504 | ||
|
|
3c6a5f6a20 | ||
|
|
57730ec6b0 | ||
|
|
008f27a4c0 | ||
|
|
00f9a0d97b | ||
|
|
101084ff80 | ||
|
|
b6c36bc914 | ||
|
|
178ec7ea6f | ||
|
|
cab7d979af | ||
|
|
a3d732b9ea | ||
|
|
7aaeb9d462 | ||
|
|
ee21d21a8a | ||
|
|
3513dbe089 | ||
|
|
570f36e2c2 | ||
|
|
8a6ae1bf66 | ||
|
|
ebb6ed4bb9 | ||
|
|
b3cfbad62c | ||
|
|
35b59d68ec | ||
|
|
2fb8896268 | ||
|
|
598b42e4e4 | ||
|
|
e5228a1734 | ||
|
|
28e2a93563 | ||
|
|
0b9a45e5c4 | ||
|
|
0c534d2903 | ||
|
|
56644e7f50 | ||
|
|
dd3cbf09cc | ||
|
|
fc61444993 | ||
|
|
2a905bbf0f | ||
|
|
09b2137fa6 | ||
|
|
fe374bbbc9 | ||
|
|
82d2d1691d | ||
|
|
af0b09daee | ||
|
|
327c5f506c | ||
|
|
71b986a4e0 | ||
|
|
b17bce24e9 |
5
code/.gitignore
vendored
5
code/.gitignore
vendored
@@ -17,4 +17,9 @@ Thumbs.db
|
|||||||
public/static/css/compiled/*
|
public/static/css/compiled/*
|
||||||
public/static/js/compiled/*
|
public/static/js/compiled/*
|
||||||
public/static/favicon/generated/*
|
public/static/favicon/generated/*
|
||||||
|
/public/_seo_copy_release/*
|
||||||
|
/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/*
|
/storage/*
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use think\exception\Handle;
|
|||||||
use think\exception\HttpException;
|
use think\exception\HttpException;
|
||||||
use think\exception\HttpResponseException;
|
use think\exception\HttpResponseException;
|
||||||
use think\exception\ValidateException;
|
use think\exception\ValidateException;
|
||||||
|
use think\facade\Log;
|
||||||
use think\Response;
|
use think\Response;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
@@ -36,6 +37,8 @@ class ExceptionHandle extends Handle
|
|||||||
*/
|
*/
|
||||||
public function report(Throwable $exception): void
|
public function report(Throwable $exception): void
|
||||||
{
|
{
|
||||||
|
$this->reportFrontendDetailException($exception);
|
||||||
|
|
||||||
// 使用内置的方式记录异常日志
|
// 使用内置的方式记录异常日志
|
||||||
parent::report($exception);
|
parent::report($exception);
|
||||||
}
|
}
|
||||||
@@ -55,4 +58,63 @@ class ExceptionHandle extends Handle
|
|||||||
// 其他错误交给系统处理
|
// 其他错误交给系统处理
|
||||||
return parent::render($request, $e);
|
return parent::render($request, $e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function reportFrontendDetailException(Throwable $exception): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (!function_exists('request')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$request = request();
|
||||||
|
if (!$request) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = '/' . ltrim((string)$request->pathinfo(), '/');
|
||||||
|
$normalizedPath = strtolower($path);
|
||||||
|
$isDetailLike = str_starts_with($normalizedPath, '/detail/')
|
||||||
|
|| str_starts_with($normalizedPath, '/voddetail/')
|
||||||
|
|| str_starts_with($normalizedPath, '/video-detail/')
|
||||||
|
|| str_starts_with($normalizedPath, '/vodinfo/')
|
||||||
|
|| str_starts_with($normalizedPath, '/video-info/')
|
||||||
|
|| str_starts_with($normalizedPath, '/film/')
|
||||||
|
|| str_starts_with($normalizedPath, '/movie/')
|
||||||
|
|| str_starts_with($normalizedPath, '/player/')
|
||||||
|
|| str_starts_with($normalizedPath, '/vodplay/')
|
||||||
|
|| str_starts_with($normalizedPath, '/video-bofang/')
|
||||||
|
|| str_starts_with($normalizedPath, '/bf-');
|
||||||
|
|
||||||
|
if (!$isDetailLike) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$route = method_exists($request, 'route') ? $request->route() : null;
|
||||||
|
$routeVars = [];
|
||||||
|
foreach (['strPinyin', 'intVId', 'intVForgeId', 'strPlayType', 'intPlayIndex'] as $key) {
|
||||||
|
try {
|
||||||
|
$routeVars[$key] = $route ? $route->getRuleParam($key) : null;
|
||||||
|
} catch (Throwable) {
|
||||||
|
$routeVars[$key] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::error('frontend_detail_exception', [
|
||||||
|
'host' => (string)$request->host(true),
|
||||||
|
'method' => (string)$request->method(),
|
||||||
|
'url' => (string)$request->url(true),
|
||||||
|
'path' => $path,
|
||||||
|
'ip' => (string)$request->ip(),
|
||||||
|
'user_agent' => (string)$request->server('HTTP_USER_AGENT', ''),
|
||||||
|
'query' => $request->get(),
|
||||||
|
'route' => $routeVars,
|
||||||
|
'exception_class' => get_class($exception),
|
||||||
|
'exception_message' => $exception->getMessage(),
|
||||||
|
'exception_file' => $exception->getFile(),
|
||||||
|
'exception_line' => $exception->getLine(),
|
||||||
|
]);
|
||||||
|
} catch (Throwable) {
|
||||||
|
// 避免异常上报链路再抛错,影响原始异常处理。
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,19 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use app\admin\controller\AdminUser;
|
use app\admin\controller\AdminUser;
|
||||||
|
use app\admin\controller\Index;
|
||||||
use app\admin\controller\Domain;
|
use app\admin\controller\Domain;
|
||||||
use app\admin\controller\SeoSupply;
|
use app\admin\controller\SeoSupply;
|
||||||
use app\admin\controller\Novel;
|
use app\admin\controller\Novel;
|
||||||
|
use app\admin\controller\ServerNode;
|
||||||
use app\admin\controller\Site;
|
use app\admin\controller\Site;
|
||||||
use app\admin\controller\SystemConfig;
|
use app\admin\controller\SystemConfig;
|
||||||
use app\admin\controller\Video;
|
use app\admin\controller\Video;
|
||||||
use app\admin\middleware\AdminAuth;
|
use app\admin\middleware\AdminAuth;
|
||||||
use think\facade\Route;
|
use think\facade\Route;
|
||||||
|
|
||||||
|
Route::get("/", [Index::class, "index"])->name("Index@index")->middleware(AdminAuth::class);
|
||||||
|
|
||||||
Route::group("/guest", function () {
|
Route::group("/guest", function () {
|
||||||
Route::post("/login", "Guest/Login")->name("Guest@Login");
|
Route::post("/login", "Guest/Login")->name("Guest@Login");
|
||||||
Route::post("/logout", "Guest/Logout")->name("Guest@Logout");
|
Route::post("/logout", "Guest/Logout")->name("Guest@Logout");
|
||||||
@@ -27,6 +31,11 @@ Route::group("/system", function () {
|
|||||||
|
|
||||||
Route::get("/config/list", [SystemConfig::class, "getSystemConfigList"])->name("SystemConfig@getSystemConfigList");
|
Route::get("/config/list", [SystemConfig::class, "getSystemConfigList"])->name("SystemConfig@getSystemConfigList");
|
||||||
Route::post("/config/save", [SystemConfig::class, "saveSystemConfig"])->name("SystemConfig@saveSystemConfig");
|
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::get("/cjpz/list", [SystemConfig::class, "getCaiJiPeiZhiList"])->name("SystemConfig@getCaiJiPeiZhiList");
|
||||||
Route::post("/cjpz/save", [SystemConfig::class, "saveCaiJiPeiZhi"])->name("SystemConfig@saveCaiJiPeiZhi");
|
Route::post("/cjpz/save", [SystemConfig::class, "saveCaiJiPeiZhi"])->name("SystemConfig@saveCaiJiPeiZhi");
|
||||||
@@ -49,6 +58,20 @@ Route::group("/video", function () {
|
|||||||
Route::post("/level/set", [Video::class, "updateVideoLevel"])->name("Video@updateVideoLevel");
|
Route::post("/level/set", [Video::class, "updateVideoLevel"])->name("Video@updateVideoLevel");
|
||||||
Route::post("/boxoffice/set", [Video::class, "updateBoxOffice"])->name("Video@updateBoxOffice");
|
Route::post("/boxoffice/set", [Video::class, "updateBoxOffice"])->name("Video@updateBoxOffice");
|
||||||
Route::get("/category/list", [Video::class, "getCategory"])->name("Video@getCategory");
|
Route::get("/category/list", [Video::class, "getCategory"])->name("Video@getCategory");
|
||||||
|
Route::get("/metadata/missing/workbench", [Video::class, "getMetadataMissingWorkbenchSummary"])->name("Video@getMetadataMissingWorkbenchSummary");
|
||||||
|
Route::get("/metadata/missing/workbench/runs", [Video::class, "getMetadataMissingWorkbenchRuns"])->name("Video@getMetadataMissingWorkbenchRuns");
|
||||||
|
Route::get("/metadata/missing/prompt", [Video::class, "getMetadataMissingCodexPrompt"])->name("Video@getMetadataMissingCodexPrompt");
|
||||||
|
Route::post("/metadata/missing/workbench/run", [Video::class, "runMetadataMissingWorkbench"])->name("Video@runMetadataMissingWorkbench");
|
||||||
|
Route::get("/metadata/missing/task-pool", [Video::class, "getMetadataMissingTaskPool"])->name("Video@getMetadataMissingTaskPool");
|
||||||
|
Route::get("/metadata/missing/task-pool/status", [Video::class, "getMetadataMissingTaskPoolStatus"])->name("Video@getMetadataMissingTaskPoolStatus");
|
||||||
|
Route::get("/metadata/missing/task-pool/ops", [Video::class, "getMetadataMissingTaskPoolOps"])->name("Video@getMetadataMissingTaskPoolOps");
|
||||||
|
Route::get("/metadata/missing/task-pool/plan-task/status", [Video::class, "getMetadataMissingTaskPoolPlanTaskStatus"])->name("Video@getMetadataMissingTaskPoolPlanTaskStatus");
|
||||||
|
Route::get("/metadata/missing/task-pool/batch/detail", [Video::class, "getMetadataMissingTaskPoolBatchDetail"])->name("Video@getMetadataMissingTaskPoolBatchDetail");
|
||||||
|
Route::get("/metadata/missing/task-pool/batch/prompt", [Video::class, "getMetadataMissingTaskPoolBatchPrompt"])->name("Video@getMetadataMissingTaskPoolBatchPrompt");
|
||||||
|
Route::get("/metadata/missing/task-pool/batch/history", [Video::class, "getMetadataMissingTaskPoolBatchHistory"])->name("Video@getMetadataMissingTaskPoolBatchHistory");
|
||||||
|
Route::post("/metadata/missing/task-pool/run", [Video::class, "runMetadataMissingTaskPool"])->name("Video@runMetadataMissingTaskPool");
|
||||||
|
Route::post("/metadata/missing/task-pool/batch/state/save", [Video::class, "saveMetadataMissingTaskPoolBatchState"])->name("Video@saveMetadataMissingTaskPoolBatchState");
|
||||||
|
Route::post("/metadata/missing/task-pool/plan-task/status/save", [Video::class, "saveMetadataMissingTaskPoolPlanTaskStatus"])->name("Video@saveMetadataMissingTaskPoolPlanTaskStatus");
|
||||||
})->middleware(AdminAuth::class);
|
})->middleware(AdminAuth::class);
|
||||||
|
|
||||||
# 广告
|
# 广告
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
declare (strict_types = 1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace app\admin\controller;
|
namespace app\admin\controller;
|
||||||
|
|
||||||
class Index
|
class Index
|
||||||
{
|
{
|
||||||
public function index()
|
public function index(): string
|
||||||
{
|
{
|
||||||
return '您好!这是一个[admin]示例应用';
|
return 'SEONexus Admin frontend has been migrated to the Vue console. Please open the Vue admin entry instead of this PHP page.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
210
code/app/admin/controller/ServerNode.php
Normal file
210
code/app/admin/controller/ServerNode.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ use app\common\helper\DomainBatchImportTemplateHelper;
|
|||||||
use app\common\helper\DomainImportProbeRunHelper;
|
use app\common\helper\DomainImportProbeRunHelper;
|
||||||
use app\common\helper\DomainImportAsyncJobHelper;
|
use app\common\helper\DomainImportAsyncJobHelper;
|
||||||
use app\common\helper\DomainImportFailedQueueHelper;
|
use app\common\helper\DomainImportFailedQueueHelper;
|
||||||
|
use app\common\helper\DomainSitemapGenerationHelper;
|
||||||
use app\common\helper\DomainSpiderMdRunHelper;
|
use app\common\helper\DomainSpiderMdRunHelper;
|
||||||
use app\common\helper\DomainImportHealthTrendHelper;
|
use app\common\helper\DomainImportHealthTrendHelper;
|
||||||
use app\common\helper\DomainImportRunIndexHelper;
|
use app\common\helper\DomainImportRunIndexHelper;
|
||||||
@@ -62,6 +63,7 @@ use app\common\helper\DomainExternalSeoCollectorStatusHelper;
|
|||||||
use app\common\helper\DomainSupplyBatchHelper;
|
use app\common\helper\DomainSupplyBatchHelper;
|
||||||
use app\common\helper\DomainSupplyRunIndexHelper;
|
use app\common\helper\DomainSupplyRunIndexHelper;
|
||||||
use app\common\helper\DomainSupplyBatchTemplateHelper;
|
use app\common\helper\DomainSupplyBatchTemplateHelper;
|
||||||
|
use app\common\helper\DomainSeoNamingHelper;
|
||||||
use app\common\helper\DomainBootstrapEnvTemplateHelper;
|
use app\common\helper\DomainBootstrapEnvTemplateHelper;
|
||||||
use app\common\helper\DomainSpiderCrawlWorkbenchHelper;
|
use app\common\helper\DomainSpiderCrawlWorkbenchHelper;
|
||||||
use app\common\helper\DomainTrajectoryProbeHelper;
|
use app\common\helper\DomainTrajectoryProbeHelper;
|
||||||
@@ -1055,7 +1057,7 @@ class Site extends BaseController
|
|||||||
|
|
||||||
protected function domainExternalSeoSummaryRoot(): string
|
protected function domainExternalSeoSummaryRoot(): string
|
||||||
{
|
{
|
||||||
return $this->bootstrapCodeRoot() . '/public/_admin_templates/domain-seo-external/latest';
|
return $this->bootstrapCodeRoot() . '/storage/external-seo/latest';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function domainExternalSeoManualImportRoot(): string
|
protected function domainExternalSeoManualImportRoot(): string
|
||||||
@@ -1065,12 +1067,12 @@ class Site extends BaseController
|
|||||||
|
|
||||||
protected function domainExternalSeoSnapshotRunRoot(): string
|
protected function domainExternalSeoSnapshotRunRoot(): string
|
||||||
{
|
{
|
||||||
return $this->bootstrapCodeRoot() . '/public/_admin_templates/domain-seo-external/snapshot-runs';
|
return $this->bootstrapCodeRoot() . '/storage/external-seo/snapshot-runs';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function domainExternalSeoSnapshotValidateRunRoot(): string
|
protected function domainExternalSeoSnapshotValidateRunRoot(): string
|
||||||
{
|
{
|
||||||
return $this->bootstrapCodeRoot() . '/public/_admin_templates/domain-seo-external/snapshot-validate-runs';
|
return $this->bootstrapCodeRoot() . '/storage/external-seo/snapshot-validate-runs';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function domainExternalSeoStorageRoot(): string
|
protected function domainExternalSeoStorageRoot(): string
|
||||||
@@ -1120,6 +1122,18 @@ class Site extends BaseController
|
|||||||
return $strPath;
|
return $strPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function storageRelativePath(string $strPath): string
|
||||||
|
{
|
||||||
|
$strStorageRoot = rtrim($this->bootstrapCodeRoot() . '/storage/', '/');
|
||||||
|
$strPath = str_replace('\\', '/', $strPath);
|
||||||
|
$strStorageRoot = str_replace('\\', '/', $strStorageRoot);
|
||||||
|
if (str_starts_with($strPath, $strStorageRoot . '/')) {
|
||||||
|
return substr($strPath, strlen($strStorageRoot . '/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $strPath;
|
||||||
|
}
|
||||||
|
|
||||||
protected function bootstrapTemplateKey(?string $strTemplate): string
|
protected function bootstrapTemplateKey(?string $strTemplate): string
|
||||||
{
|
{
|
||||||
$strTemplate = strtolower(trim((string)$strTemplate));
|
$strTemplate = strtolower(trim((string)$strTemplate));
|
||||||
@@ -1230,9 +1244,14 @@ class Site extends BaseController
|
|||||||
|
|
||||||
$Spreadsheet = IOFactory::load($FilePath);
|
$Spreadsheet = IOFactory::load($FilePath);
|
||||||
$Sheet = $Spreadsheet->getActiveSheet();
|
$Sheet = $Spreadsheet->getActiveSheet();
|
||||||
$arrData = $Sheet->toArray();
|
$arrData = $Sheet->toArray(null, true, true, false);
|
||||||
|
|
||||||
array_shift($arrData);
|
$arrHeaders = array_map(
|
||||||
|
static function ($strHeader): string {
|
||||||
|
return DomainBatchImportTemplateHelper::normalizeHeader(trim((string)$strHeader));
|
||||||
|
},
|
||||||
|
(array)array_shift($arrData)
|
||||||
|
);
|
||||||
|
|
||||||
$strRunRoot = DomainImportProbeRunHelper::createRunRoot($this->domainImportRunRoot(), 'excel_import');
|
$strRunRoot = DomainImportProbeRunHelper::createRunRoot($this->domainImportRunRoot(), 'excel_import');
|
||||||
$strUploadedExcelPath = $strRunRoot . '/uploaded.' . $strExt;
|
$strUploadedExcelPath = $strRunRoot . '/uploaded.' . $strExt;
|
||||||
@@ -1252,34 +1271,74 @@ class Site extends BaseController
|
|||||||
|
|
||||||
foreach ($arrData as $arrRows) {
|
foreach ($arrData as $arrRows) {
|
||||||
$intRowsTotal++;
|
$intRowsTotal++;
|
||||||
$strStrategyProfile = $this->normalizeStrategyProfile($arrRows[19] ?? DomainModel::SEO_STRATEGY_PROFILE_STANDARD);
|
$arrRowAssoc = [];
|
||||||
$strTkdMode = DomainModel::normalizeTkdMode((string)($arrRows[20] ?? ''));
|
foreach ($arrHeaders as $index => $strHeader) {
|
||||||
$strTkdProvider = DomainModel::normalizeTkdProvider((string)($arrRows[21] ?? DomainModel::TKD_PROVIDER_LOCAL));
|
if ($strHeader === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$arrRowAssoc[$strHeader] = trim((string)($arrRows[$index] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty(array_filter($arrRowAssoc, static fn($value): bool => trim((string)$value) !== ''))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$strStrategyProfile = $this->normalizeStrategyProfile($arrRowAssoc['strategy_profile'] ?? DomainModel::SEO_STRATEGY_PROFILE_STANDARD);
|
||||||
|
$strRawTkdMode = trim((string)($arrRowAssoc['tkd_mode'] ?? ''));
|
||||||
|
$strTkdProvider = DomainModel::normalizeTkdProvider((string)($arrRowAssoc['tkd_provider'] ?? DomainModel::TKD_PROVIDER_LOCAL));
|
||||||
|
$strTkdMode = $strRawTkdMode !== ''
|
||||||
|
? DomainModel::normalizeTkdMode($strRawTkdMode)
|
||||||
|
: '';
|
||||||
|
if ($strTkdMode === '' && $strTkdProvider === DomainModel::TKD_PROVIDER_OPENAI) {
|
||||||
|
$strTkdMode = DomainModel::TKD_MODE_AI_OPTIMIZE;
|
||||||
|
}
|
||||||
$arrDomain = [
|
$arrDomain = [
|
||||||
'd_domain' => $this->normalizeStoredDomain($arrRows[0] ?? '', $arrRows[14] ?? DomainModel::MATCH_TYPE_EXACT),
|
'd_domain' => $this->normalizeStoredDomain($arrRowAssoc['d_domain'] ?? '', $arrRowAssoc['d_match_type'] ?? DomainModel::MATCH_TYPE_EXACT),
|
||||||
't_id' => $arrRows[1],
|
't_id' => $arrRowAssoc['t_id'] ?? 1,
|
||||||
'd_name' => $arrRows[2],
|
'd_name' => $arrRowAssoc['d_name'] ?? '',
|
||||||
'd_keywords' => $arrRows[3],
|
'd_keywords' => $arrRowAssoc['d_keywords'] ?? '',
|
||||||
'd_index_title' => $arrRows[4],
|
'd_index_title' => $arrRowAssoc['d_index_title'] ?? '',
|
||||||
'd_index_keywords' => $arrRows[5],
|
'd_index_keywords' => $arrRowAssoc['d_index_keywords'] ?? '',
|
||||||
'd_index_description' => $arrRows[6],
|
'd_index_description' => $arrRowAssoc['d_index_description'] ?? '',
|
||||||
'd_description' => $arrRows[7],
|
'd_description' => $arrRowAssoc['d_description'] ?? '',
|
||||||
'd_statis' => $arrRows[8],
|
'd_statis' => $arrRowAssoc['d_statis'] ?? '',
|
||||||
'd_logo_type' => (int)$arrRows[9],
|
'd_logo_type' => (int)($arrRowAssoc['d_logo_type'] ?? 0),
|
||||||
'd_text_logo' => $arrRows[10],
|
'd_text_logo' => $arrRowAssoc['d_text_logo'] ?? '',
|
||||||
'd_img_logo' => $arrRows[11],
|
'd_img_logo' => $arrRowAssoc['d_img_logo'] ?? '',
|
||||||
'd_content_encode' => $arrRows[12],
|
'd_content_encode' => $arrRowAssoc['d_content_encode'] ?? 0,
|
||||||
'info_id' => $arrRows[13],
|
'info_id' => $arrRowAssoc['info_id'] ?? 0,
|
||||||
'd_match_type' => $this->normalizeMatchType($arrRows[14] ?? DomainModel::MATCH_TYPE_EXACT),
|
'd_match_type' => $this->normalizeMatchType($arrRowAssoc['d_match_type'] ?? DomainModel::MATCH_TYPE_EXACT),
|
||||||
'd_parent_domain' => $this->normalizeParentDomain($arrRows[15] ?? ''),
|
'd_parent_domain' => $this->normalizeParentDomain($arrRowAssoc['d_parent_domain'] ?? ''),
|
||||||
'd_seed_scope' => $this->normalizeSeedScope($arrRows[16] ?? DomainModel::SEED_SCOPE_DOMAIN),
|
'd_seed_scope' => $this->normalizeSeedScope($arrRowAssoc['d_seed_scope'] ?? DomainModel::SEED_SCOPE_DOMAIN),
|
||||||
'd_baidu_token' => $arrRows[17] ?? null,
|
'd_baidu_token' => $arrRowAssoc['d_baidu_token'] ?? null,
|
||||||
];
|
];
|
||||||
$arrDomain['d_seo_cfg'] = $this->applySeoStrategyProfileForDomainData($arrRows[18] ?? null, $arrDomain, $strStrategyProfile);
|
if (DomainSeoNamingHelper::shouldOptimizeForOpenAi($strTkdProvider, $strTkdMode)) {
|
||||||
|
$arrSeoDefaults = DomainSeoNamingHelper::buildSeoDefaults(
|
||||||
|
(string)($arrDomain['d_name'] ?? ''),
|
||||||
|
(string)($arrDomain['d_domain'] ?? ''),
|
||||||
|
(string)($arrDomain['d_index_title'] ?? ''),
|
||||||
|
(string)($arrDomain['d_index_keywords'] ?? ''),
|
||||||
|
(string)($arrDomain['d_index_description'] ?? '')
|
||||||
|
);
|
||||||
|
$arrDomain['d_name'] = $arrSeoDefaults['site_name'];
|
||||||
|
$arrDomain['d_index_title'] = $arrSeoDefaults['index_title'];
|
||||||
|
$arrDomain['d_index_keywords'] = $arrSeoDefaults['index_keywords'];
|
||||||
|
$arrDomain['d_index_description'] = $arrSeoDefaults['index_description'];
|
||||||
|
if (trim((string)($arrDomain['d_keywords'] ?? '')) === '') {
|
||||||
|
$arrDomain['d_keywords'] = $arrSeoDefaults['index_keywords'];
|
||||||
|
}
|
||||||
|
if (trim((string)($arrDomain['d_description'] ?? '')) === '') {
|
||||||
|
$arrDomain['d_description'] = $arrSeoDefaults['index_description'];
|
||||||
|
}
|
||||||
|
if (trim((string)($arrDomain['d_text_logo'] ?? '')) === '') {
|
||||||
|
$arrDomain['d_text_logo'] = $arrSeoDefaults['site_name'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$arrDomain['d_seo_cfg'] = $this->applySeoStrategyProfileForDomainData($arrRowAssoc['d_seo_cfg'] ?? null, $arrDomain, $strStrategyProfile);
|
||||||
$arrDomain['d_seo_cfg']['tkd'] = array_merge(
|
$arrDomain['d_seo_cfg']['tkd'] = array_merge(
|
||||||
(array)($arrDomain['d_seo_cfg']['tkd'] ?? []),
|
(array)($arrDomain['d_seo_cfg']['tkd'] ?? []),
|
||||||
[
|
[
|
||||||
'mode' => $strTkdMode !== '' ? $strTkdMode : ((trim((string)($arrRows[4] ?? '')) !== '' || trim((string)($arrRows[5] ?? '')) !== '' || trim((string)($arrRows[6] ?? '')) !== '') ? DomainModel::TKD_MODE_FORCE_IMPORT : DomainModel::TKD_MODE_AUTO_GENERATE),
|
'mode' => $strTkdMode !== '' ? $strTkdMode : ((trim((string)($arrRowAssoc['d_index_title'] ?? '')) !== '' || trim((string)($arrRowAssoc['d_index_keywords'] ?? '')) !== '' || trim((string)($arrRowAssoc['d_index_description'] ?? '')) !== '') ? DomainModel::TKD_MODE_FORCE_IMPORT : DomainModel::TKD_MODE_AUTO_GENERATE),
|
||||||
'provider' => $strTkdProvider,
|
'provider' => $strTkdProvider,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -1331,11 +1390,16 @@ class Site extends BaseController
|
|||||||
$DomainModel->save();
|
$DomainModel->save();
|
||||||
SeoCopyGenerationHelper::initializeForDomain($DomainModel);
|
SeoCopyGenerationHelper::initializeForDomain($DomainModel);
|
||||||
SeoResourcePoolHelper::initSitePositioning((string)$DomainModel->d_domain, 1, true);
|
SeoResourcePoolHelper::initSitePositioning((string)$DomainModel->d_domain, 1, true);
|
||||||
|
DomainSitemapGenerationHelper::queueForDomains([(string)$DomainModel->d_domain]);
|
||||||
$intInsertedCount++;
|
$intInsertedCount++;
|
||||||
$arrInsertedDomains[] = [
|
$arrInsertedDomains[] = [
|
||||||
'd_id' => (int)$DomainModel->d_id,
|
'd_id' => (int)$DomainModel->d_id,
|
||||||
'd_domain' => (string)$DomainModel->d_domain,
|
'd_domain' => (string)$DomainModel->d_domain,
|
||||||
|
'd_name' => (string)$DomainModel->d_name,
|
||||||
|
't_id' => (int)$DomainModel->t_id,
|
||||||
'strategy_profile' => $strStrategyProfile,
|
'strategy_profile' => $strStrategyProfile,
|
||||||
|
'tkd_provider' => $strTkdProvider,
|
||||||
|
'tkd_mode' => (string)($arrDomain['d_seo_cfg']['tkd']['mode'] ?? ''),
|
||||||
];
|
];
|
||||||
$arrInsertedHosts[] = (string)$DomainModel->d_domain;
|
$arrInsertedHosts[] = (string)$DomainModel->d_domain;
|
||||||
$arrStrategyProfileCounts[$strStrategyProfile] = (int)($arrStrategyProfileCounts[$strStrategyProfile] ?? 0) + 1;
|
$arrStrategyProfileCounts[$strStrategyProfile] = (int)($arrStrategyProfileCounts[$strStrategyProfile] ?? 0) + 1;
|
||||||
@@ -1356,6 +1420,8 @@ class Site extends BaseController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$arrPostImportWorkflow = $this->buildPostImportWorkflow($arrInsertedDomains, $arrImportProbeSummary);
|
||||||
|
|
||||||
return $this->success([
|
return $this->success([
|
||||||
'rows_total' => $intRowsTotal,
|
'rows_total' => $intRowsTotal,
|
||||||
'inserted_count' => $intInsertedCount,
|
'inserted_count' => $intInsertedCount,
|
||||||
@@ -1374,6 +1440,9 @@ class Site extends BaseController
|
|||||||
'probe_failed_count' => (int)($arrImportProbeSummary['failed_count'] ?? 0),
|
'probe_failed_count' => (int)($arrImportProbeSummary['failed_count'] ?? 0),
|
||||||
'summary_json_path' => $this->publicRelativePath((string)($arrImportProbeSummary['summary_json_path'] ?? '')),
|
'summary_json_path' => $this->publicRelativePath((string)($arrImportProbeSummary['summary_json_path'] ?? '')),
|
||||||
'summary_html_path' => $this->publicRelativePath((string)($arrImportProbeSummary['summary_html_path'] ?? '')),
|
'summary_html_path' => $this->publicRelativePath((string)($arrImportProbeSummary['summary_html_path'] ?? '')),
|
||||||
|
'post_import_workflow' => $arrPostImportWorkflow,
|
||||||
|
'next_step_notice' => array_values((array)($arrPostImportWorkflow['notices'] ?? [])),
|
||||||
|
'codex_continue_prompt' => (string)($arrPostImportWorkflow['codex']['prompt'] ?? ''),
|
||||||
]);
|
]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->error("9999", '导入失败: ' . $e->getMessage());
|
return $this->error("9999", '导入失败: ' . $e->getMessage());
|
||||||
@@ -1385,11 +1454,15 @@ class Site extends BaseController
|
|||||||
try {
|
try {
|
||||||
$arrTemplate = DomainBatchImportTemplateHelper::writeTemplate($this->domainImportTemplateRoot());
|
$arrTemplate = DomainBatchImportTemplateHelper::writeTemplate($this->domainImportTemplateRoot());
|
||||||
$strRelativePath = str_replace($this->bootstrapCodeRoot() . '/public/', '', $arrTemplate['file_path']);
|
$strRelativePath = str_replace($this->bootstrapCodeRoot() . '/public/', '', $arrTemplate['file_path']);
|
||||||
|
$arrSummary = (array)($arrTemplate['summary'] ?? []);
|
||||||
|
|
||||||
return $this->success([
|
return $this->success([
|
||||||
'excel_template_path' => $strRelativePath,
|
'excel_template_path' => $strRelativePath,
|
||||||
'template_file_name' => $arrTemplate['file_name'],
|
'template_file_name' => $arrTemplate['file_name'],
|
||||||
'template_summary' => $arrTemplate['summary'],
|
'template_summary' => $arrSummary,
|
||||||
|
'template_display_headers' => array_values((array)($arrSummary['display_headers'] ?? [])),
|
||||||
|
'template_operator_notice' => array_values((array)($arrSummary['operator_notice'] ?? [])),
|
||||||
|
'template_filling_guidance' => (array)($arrSummary['filling_guidance'] ?? []),
|
||||||
]);
|
]);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
return $this->error('9999', $Throwable->getMessage());
|
return $this->error('9999', $Throwable->getMessage());
|
||||||
@@ -1401,11 +1474,15 @@ class Site extends BaseController
|
|||||||
try {
|
try {
|
||||||
$arrTemplate = DomainSupplyBatchTemplateHelper::writeTemplate($this->domainSupplyTemplateRoot());
|
$arrTemplate = DomainSupplyBatchTemplateHelper::writeTemplate($this->domainSupplyTemplateRoot());
|
||||||
$strRelativePath = $this->publicRelativePath((string)$arrTemplate['file_path']);
|
$strRelativePath = $this->publicRelativePath((string)$arrTemplate['file_path']);
|
||||||
|
$arrSummary = (array)($arrTemplate['summary'] ?? []);
|
||||||
|
|
||||||
return $this->success([
|
return $this->success([
|
||||||
'excel_template_path' => $strRelativePath,
|
'excel_template_path' => $strRelativePath,
|
||||||
'template_file_name' => $arrTemplate['file_name'],
|
'template_file_name' => $arrTemplate['file_name'],
|
||||||
'template_summary' => $arrTemplate['summary'],
|
'template_summary' => $arrSummary,
|
||||||
|
'template_display_headers' => array_values((array)($arrSummary['display_headers'] ?? [])),
|
||||||
|
'template_operator_notice' => array_values((array)($arrSummary['operator_notice'] ?? [])),
|
||||||
|
'template_filling_guidance' => (array)($arrSummary['filling_guidance'] ?? []),
|
||||||
]);
|
]);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
return $this->error('9999', '生成一体化供给模板失败:' . $Throwable->getMessage());
|
return $this->error('9999', '生成一体化供给模板失败:' . $Throwable->getMessage());
|
||||||
@@ -1519,9 +1596,17 @@ class Site extends BaseController
|
|||||||
$intWindowHours,
|
$intWindowHours,
|
||||||
$strBotScope,
|
$strBotScope,
|
||||||
$mixSelectedBots
|
$mixSelectedBots
|
||||||
));
|
))->header([
|
||||||
|
'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0',
|
||||||
|
'Pragma' => 'no-cache',
|
||||||
|
'Expires' => '0',
|
||||||
|
]);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
return $this->error('9999', '读取蜘蛛抓取概览失败:' . $Throwable->getMessage());
|
return $this->error('9999', '读取蜘蛛抓取概览失败:' . $Throwable->getMessage())->header([
|
||||||
|
'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0',
|
||||||
|
'Pragma' => 'no-cache',
|
||||||
|
'Expires' => '0',
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2107,8 +2192,8 @@ class Site extends BaseController
|
|||||||
$intLimit
|
$intLimit
|
||||||
);
|
);
|
||||||
$arrSummary = DomainExternalSeoSummaryHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSummary);
|
$arrSummary = DomainExternalSeoSummaryHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSummary);
|
||||||
$arrSummary['summary_json_path'] = $this->publicRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
$arrSummary['summary_json_path'] = $this->storageRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
||||||
$arrSummary['summary_html_path'] = $this->publicRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
$arrSummary['summary_html_path'] = $this->storageRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
||||||
|
|
||||||
return $this->success($arrSummary);
|
return $this->success($arrSummary);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
@@ -2142,8 +2227,8 @@ class Site extends BaseController
|
|||||||
$intLimit
|
$intLimit
|
||||||
);
|
);
|
||||||
$arrSummary = DomainExternalSeoTrendHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSummary);
|
$arrSummary = DomainExternalSeoTrendHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSummary);
|
||||||
$arrSummary['summary_json_path'] = $this->publicRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
$arrSummary['summary_json_path'] = $this->storageRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
||||||
$arrSummary['summary_html_path'] = $this->publicRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
$arrSummary['summary_html_path'] = $this->storageRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
||||||
|
|
||||||
return $this->success($arrSummary);
|
return $this->success($arrSummary);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
@@ -2217,11 +2302,11 @@ class Site extends BaseController
|
|||||||
public function getDomainExternalSeoSearchConsolePropertyMapTemplate(Request $Request, ?AdminUserModel $AdminUserModel)
|
public function getDomainExternalSeoSearchConsolePropertyMapTemplate(Request $Request, ?AdminUserModel $AdminUserModel)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$strOutputDir = $this->bootstrapCodeRoot() . '/public/_admin_templates/domain-seo-external/search-console';
|
$strOutputDir = $this->bootstrapCodeRoot() . '/storage/external-seo/search-console';
|
||||||
$arrTemplate = DomainExternalSeoSearchConsoleConfigHelper::writeTemplate($strOutputDir);
|
$arrTemplate = DomainExternalSeoSearchConsoleConfigHelper::writeTemplate($strOutputDir);
|
||||||
return $this->success([
|
return $this->success([
|
||||||
'file_name' => (string)($arrTemplate['file_name'] ?? ''),
|
'file_name' => (string)($arrTemplate['file_name'] ?? ''),
|
||||||
'file_path' => $this->publicRelativePath((string)($arrTemplate['file_path'] ?? '')),
|
'file_path' => $this->storageRelativePath((string)($arrTemplate['file_path'] ?? '')),
|
||||||
'summary' => (array)($arrTemplate['summary'] ?? []),
|
'summary' => (array)($arrTemplate['summary'] ?? []),
|
||||||
]);
|
]);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
@@ -2261,8 +2346,8 @@ class Site extends BaseController
|
|||||||
'mapped_hosts_count' => (int)($arrResult['mapped_hosts_count'] ?? 0),
|
'mapped_hosts_count' => (int)($arrResult['mapped_hosts_count'] ?? 0),
|
||||||
'provider_summary' => $arrProviderSummary,
|
'provider_summary' => $arrProviderSummary,
|
||||||
'real_summary_status' => (string)($arrRealSummary['status'] ?? ''),
|
'real_summary_status' => (string)($arrRealSummary['status'] ?? ''),
|
||||||
'summary_json_path' => $this->publicRelativePath((string)($arrRealSummary['summary_json_path'] ?? '')),
|
'summary_json_path' => $this->storageRelativePath((string)($arrRealSummary['summary_json_path'] ?? '')),
|
||||||
'summary_html_path' => $this->publicRelativePath((string)($arrRealSummary['summary_html_path'] ?? '')),
|
'summary_html_path' => $this->storageRelativePath((string)($arrRealSummary['summary_html_path'] ?? '')),
|
||||||
]);
|
]);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
return $this->error('9999', '上传 Search Console property map 失败:' . $Throwable->getMessage());
|
return $this->error('9999', '上传 Search Console property map 失败:' . $Throwable->getMessage());
|
||||||
@@ -2275,8 +2360,8 @@ class Site extends BaseController
|
|||||||
$intDays = max(1, min(30, (int)$Request->get('days', 7)));
|
$intDays = max(1, min(30, (int)$Request->get('days', 7)));
|
||||||
$arrSummary = DomainExternalSeoSearchConsoleProviderPlanHelper::buildSummary($this->bootstrapCodeRoot(), $intDays);
|
$arrSummary = DomainExternalSeoSearchConsoleProviderPlanHelper::buildSummary($this->bootstrapCodeRoot(), $intDays);
|
||||||
$arrSummary = DomainExternalSeoSearchConsoleProviderPlanHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSummary);
|
$arrSummary = DomainExternalSeoSearchConsoleProviderPlanHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSummary);
|
||||||
$arrSummary['summary_json_path'] = $this->publicRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
$arrSummary['summary_json_path'] = $this->storageRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
||||||
$arrSummary['summary_html_path'] = $this->publicRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
$arrSummary['summary_html_path'] = $this->storageRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
||||||
return $this->success($arrSummary);
|
return $this->success($arrSummary);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
return $this->error('9999', '读取 Search Console provider 计划失败:' . $Throwable->getMessage());
|
return $this->error('9999', '读取 Search Console provider 计划失败:' . $Throwable->getMessage());
|
||||||
@@ -2289,8 +2374,8 @@ class Site extends BaseController
|
|||||||
$intDays = max(1, min(30, (int)$Request->get('days', 7)));
|
$intDays = max(1, min(30, (int)$Request->get('days', 7)));
|
||||||
$arrSummary = DomainExternalSeoSearchConsoleFetchHelper::buildSummary($this->bootstrapCodeRoot(), $intDays);
|
$arrSummary = DomainExternalSeoSearchConsoleFetchHelper::buildSummary($this->bootstrapCodeRoot(), $intDays);
|
||||||
$arrSummary = DomainExternalSeoSearchConsoleFetchHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSummary);
|
$arrSummary = DomainExternalSeoSearchConsoleFetchHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSummary);
|
||||||
$arrSummary['summary_json_path'] = $this->publicRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
$arrSummary['summary_json_path'] = $this->storageRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
||||||
$arrSummary['summary_html_path'] = $this->publicRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
$arrSummary['summary_html_path'] = $this->storageRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
||||||
return $this->success($arrSummary);
|
return $this->success($arrSummary);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
return $this->error('9999', '读取 Search Console fetch 摘要失败:' . $Throwable->getMessage());
|
return $this->error('9999', '读取 Search Console fetch 摘要失败:' . $Throwable->getMessage());
|
||||||
@@ -2302,8 +2387,8 @@ class Site extends BaseController
|
|||||||
try {
|
try {
|
||||||
$arrSummary = DomainExternalSeoBaiduProviderPlanHelper::buildSummary($this->bootstrapCodeRoot());
|
$arrSummary = DomainExternalSeoBaiduProviderPlanHelper::buildSummary($this->bootstrapCodeRoot());
|
||||||
$arrSummary = DomainExternalSeoBaiduProviderPlanHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSummary);
|
$arrSummary = DomainExternalSeoBaiduProviderPlanHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSummary);
|
||||||
$arrSummary['summary_json_path'] = $this->publicRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
$arrSummary['summary_json_path'] = $this->storageRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
||||||
$arrSummary['summary_html_path'] = $this->publicRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
$arrSummary['summary_html_path'] = $this->storageRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
||||||
return $this->success($arrSummary);
|
return $this->success($arrSummary);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
return $this->error('9999', '读取百度 provider 计划失败:' . $Throwable->getMessage());
|
return $this->error('9999', '读取百度 provider 计划失败:' . $Throwable->getMessage());
|
||||||
@@ -2316,8 +2401,8 @@ class Site extends BaseController
|
|||||||
$intLimit = max(1, min(200, (int)$Request->get('limit', 50)));
|
$intLimit = max(1, min(200, (int)$Request->get('limit', 50)));
|
||||||
$arrSummary = DomainExternalSeoBaiduPushSummaryHelper::buildSummary($this->bootstrapCodeRoot(), $intLimit);
|
$arrSummary = DomainExternalSeoBaiduPushSummaryHelper::buildSummary($this->bootstrapCodeRoot(), $intLimit);
|
||||||
$arrSummary = DomainExternalSeoBaiduPushSummaryHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSummary);
|
$arrSummary = DomainExternalSeoBaiduPushSummaryHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSummary);
|
||||||
$arrSummary['summary_json_path'] = $this->publicRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
$arrSummary['summary_json_path'] = $this->storageRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
||||||
$arrSummary['summary_html_path'] = $this->publicRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
$arrSummary['summary_html_path'] = $this->storageRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
||||||
return $this->success($arrSummary);
|
return $this->success($arrSummary);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
return $this->error('9999', '读取百度推送观察摘要失败:' . $Throwable->getMessage());
|
return $this->error('9999', '读取百度推送观察摘要失败:' . $Throwable->getMessage());
|
||||||
@@ -2366,8 +2451,8 @@ class Site extends BaseController
|
|||||||
$intLimit = max(1, min(300, (int)$Request->get('limit', 100)));
|
$intLimit = max(1, min(300, (int)$Request->get('limit', 100)));
|
||||||
$arrQueue = DomainExternalSeoBaiduPushAttentionQueueHelper::buildQueue($this->bootstrapCodeRoot(), $intLimit);
|
$arrQueue = DomainExternalSeoBaiduPushAttentionQueueHelper::buildQueue($this->bootstrapCodeRoot(), $intLimit);
|
||||||
$arrQueue = DomainExternalSeoBaiduPushAttentionQueueHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrQueue);
|
$arrQueue = DomainExternalSeoBaiduPushAttentionQueueHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrQueue);
|
||||||
$arrQueue['summary_json_path'] = $this->publicRelativePath((string)($arrQueue['summary_json_path'] ?? ''));
|
$arrQueue['summary_json_path'] = $this->storageRelativePath((string)($arrQueue['summary_json_path'] ?? ''));
|
||||||
$arrQueue['summary_html_path'] = $this->publicRelativePath((string)($arrQueue['summary_html_path'] ?? ''));
|
$arrQueue['summary_html_path'] = $this->storageRelativePath((string)($arrQueue['summary_html_path'] ?? ''));
|
||||||
return $this->success($arrQueue);
|
return $this->success($arrQueue);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
return $this->error('9999', '读取百度推送异常队列失败:' . $Throwable->getMessage());
|
return $this->error('9999', '读取百度推送异常队列失败:' . $Throwable->getMessage());
|
||||||
@@ -2380,8 +2465,8 @@ class Site extends BaseController
|
|||||||
$intLimit = max(1, min(100, (int)$Request->get('limit', 20)));
|
$intLimit = max(1, min(100, (int)$Request->get('limit', 20)));
|
||||||
$arrSummary = DomainExternalSeoBaiduPushTrendHelper::buildSummary($this->bootstrapCodeRoot(), $intLimit);
|
$arrSummary = DomainExternalSeoBaiduPushTrendHelper::buildSummary($this->bootstrapCodeRoot(), $intLimit);
|
||||||
$arrSummary = DomainExternalSeoBaiduPushTrendHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSummary);
|
$arrSummary = DomainExternalSeoBaiduPushTrendHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSummary);
|
||||||
$arrSummary['summary_json_path'] = $this->publicRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
$arrSummary['summary_json_path'] = $this->storageRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
||||||
$arrSummary['summary_html_path'] = $this->publicRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
$arrSummary['summary_html_path'] = $this->storageRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
||||||
return $this->success($arrSummary);
|
return $this->success($arrSummary);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
return $this->error('9999', '读取百度推送趋势摘要失败:' . $Throwable->getMessage());
|
return $this->error('9999', '读取百度推送趋势摘要失败:' . $Throwable->getMessage());
|
||||||
@@ -2394,8 +2479,8 @@ class Site extends BaseController
|
|||||||
$intLimit = max(1, min(100, (int)$Request->get('limit', 20)));
|
$intLimit = max(1, min(100, (int)$Request->get('limit', 20)));
|
||||||
$arrSummary = DomainExternalSeoBaiduFeedbackSummaryHelper::buildSummary($this->bootstrapCodeRoot(), $intLimit);
|
$arrSummary = DomainExternalSeoBaiduFeedbackSummaryHelper::buildSummary($this->bootstrapCodeRoot(), $intLimit);
|
||||||
$arrSummary = DomainExternalSeoBaiduFeedbackSummaryHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSummary);
|
$arrSummary = DomainExternalSeoBaiduFeedbackSummaryHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSummary);
|
||||||
$arrSummary['summary_json_path'] = $this->publicRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
$arrSummary['summary_json_path'] = $this->storageRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
||||||
$arrSummary['summary_html_path'] = $this->publicRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
$arrSummary['summary_html_path'] = $this->storageRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
||||||
return $this->success($arrSummary);
|
return $this->success($arrSummary);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
return $this->error('9999', '读取百度反馈摘要失败:' . $Throwable->getMessage());
|
return $this->error('9999', '读取百度反馈摘要失败:' . $Throwable->getMessage());
|
||||||
@@ -2408,8 +2493,8 @@ class Site extends BaseController
|
|||||||
$intLimit = max(1, min(100, (int)$Request->get('limit', 20)));
|
$intLimit = max(1, min(100, (int)$Request->get('limit', 20)));
|
||||||
$arrSummary = DomainExternalSeoSiteQueryProbeHelper::buildSummary($this->bootstrapCodeRoot(), $intLimit);
|
$arrSummary = DomainExternalSeoSiteQueryProbeHelper::buildSummary($this->bootstrapCodeRoot(), $intLimit);
|
||||||
$arrSummary = DomainExternalSeoSiteQueryProbeHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSummary);
|
$arrSummary = DomainExternalSeoSiteQueryProbeHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSummary);
|
||||||
$arrSummary['summary_json_path'] = $this->publicRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
$arrSummary['summary_json_path'] = $this->storageRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
||||||
$arrSummary['summary_html_path'] = $this->publicRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
$arrSummary['summary_html_path'] = $this->storageRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
||||||
return $this->success($arrSummary);
|
return $this->success($arrSummary);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
return $this->error('9999', '读取百度 site: 收录探测失败:' . $Throwable->getMessage());
|
return $this->error('9999', '读取百度 site: 收录探测失败:' . $Throwable->getMessage());
|
||||||
@@ -2436,8 +2521,8 @@ class Site extends BaseController
|
|||||||
$intLimit = max(1, min(300, (int)$Request->get('limit', 100)));
|
$intLimit = max(1, min(300, (int)$Request->get('limit', 100)));
|
||||||
$arrQueue = DomainExternalSeoAizhanKeywordAttentionQueueHelper::buildQueue($this->bootstrapCodeRoot(), $intLimit);
|
$arrQueue = DomainExternalSeoAizhanKeywordAttentionQueueHelper::buildQueue($this->bootstrapCodeRoot(), $intLimit);
|
||||||
$arrQueue = DomainExternalSeoAizhanKeywordAttentionQueueHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrQueue);
|
$arrQueue = DomainExternalSeoAizhanKeywordAttentionQueueHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrQueue);
|
||||||
$arrQueue['summary_json_path'] = $this->publicRelativePath((string)($arrQueue['summary_json_path'] ?? ''));
|
$arrQueue['summary_json_path'] = $this->storageRelativePath((string)($arrQueue['summary_json_path'] ?? ''));
|
||||||
$arrQueue['summary_html_path'] = $this->publicRelativePath((string)($arrQueue['summary_html_path'] ?? ''));
|
$arrQueue['summary_html_path'] = $this->storageRelativePath((string)($arrQueue['summary_html_path'] ?? ''));
|
||||||
return $this->success($arrQueue);
|
return $this->success($arrQueue);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
return $this->error('9999', '读取爱站关键词异常队列失败:' . $Throwable->getMessage());
|
return $this->error('9999', '读取爱站关键词异常队列失败:' . $Throwable->getMessage());
|
||||||
@@ -2450,8 +2535,8 @@ class Site extends BaseController
|
|||||||
$intLimit = max(1, min(100, (int)$Request->get('limit', 20)));
|
$intLimit = max(1, min(100, (int)$Request->get('limit', 20)));
|
||||||
$arrSummary = DomainExternalSeoAizhanKeywordTrendHelper::buildSummary($this->bootstrapCodeRoot(), $intLimit);
|
$arrSummary = DomainExternalSeoAizhanKeywordTrendHelper::buildSummary($this->bootstrapCodeRoot(), $intLimit);
|
||||||
$arrSummary = DomainExternalSeoAizhanKeywordTrendHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSummary);
|
$arrSummary = DomainExternalSeoAizhanKeywordTrendHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSummary);
|
||||||
$arrSummary['summary_json_path'] = $this->publicRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
$arrSummary['summary_json_path'] = $this->storageRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
||||||
$arrSummary['summary_html_path'] = $this->publicRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
$arrSummary['summary_html_path'] = $this->storageRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
||||||
return $this->success($arrSummary);
|
return $this->success($arrSummary);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
return $this->error('9999', '读取爱站关键词趋势摘要失败:' . $Throwable->getMessage());
|
return $this->error('9999', '读取爱站关键词趋势摘要失败:' . $Throwable->getMessage());
|
||||||
@@ -2495,40 +2580,40 @@ class Site extends BaseController
|
|||||||
$arrResult = DomainExternalSeoSnapshotHelper::ingestSnapshots((array)$arrSnapshots, $strSource);
|
$arrResult = DomainExternalSeoSnapshotHelper::ingestSnapshots((array)$arrSnapshots, $strSource);
|
||||||
$strRunRoot = DomainExternalSeoSnapshotRunHelper::createRunRoot($this->domainExternalSeoSnapshotRunRoot(), 'snapshot_push');
|
$strRunRoot = DomainExternalSeoSnapshotRunHelper::createRunRoot($this->domainExternalSeoSnapshotRunRoot(), 'snapshot_push');
|
||||||
$arrResult = DomainExternalSeoSnapshotRunHelper::writeRunArtifacts(
|
$arrResult = DomainExternalSeoSnapshotRunHelper::writeRunArtifacts(
|
||||||
$this->bootstrapCodeRoot() . '/public',
|
$this->bootstrapCodeRoot() . '/storage',
|
||||||
$strRunRoot,
|
$strRunRoot,
|
||||||
$arrResult,
|
$arrResult,
|
||||||
(array)$arrSnapshots
|
(array)$arrSnapshots
|
||||||
);
|
);
|
||||||
$arrResult['run_root'] = $this->publicRelativePath($strRunRoot);
|
$arrResult['run_root'] = $this->storageRelativePath($strRunRoot);
|
||||||
$arrResult['summary_json_path'] = $this->publicRelativePath((string)($arrResult['summary_json_path'] ?? ''));
|
$arrResult['summary_json_path'] = $this->storageRelativePath((string)($arrResult['summary_json_path'] ?? ''));
|
||||||
$arrResult['summary_html_path'] = $this->publicRelativePath((string)($arrResult['summary_html_path'] ?? ''));
|
$arrResult['summary_html_path'] = $this->storageRelativePath((string)($arrResult['summary_html_path'] ?? ''));
|
||||||
$arrResult['payload_json_path'] = $this->publicRelativePath((string)($arrResult['payload_json_path'] ?? ''));
|
$arrResult['payload_json_path'] = $this->storageRelativePath((string)($arrResult['payload_json_path'] ?? ''));
|
||||||
$arrOverview = DomainExternalSeoCnOverviewHelper::buildOverview(14, 10);
|
$arrOverview = DomainExternalSeoCnOverviewHelper::buildOverview(14, 10);
|
||||||
$arrOverview = DomainExternalSeoCnOverviewHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrOverview);
|
$arrOverview = DomainExternalSeoCnOverviewHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrOverview);
|
||||||
$arrOverview['summary_json_path'] = $this->publicRelativePath((string)($arrOverview['summary_json_path'] ?? ''));
|
$arrOverview['summary_json_path'] = $this->storageRelativePath((string)($arrOverview['summary_json_path'] ?? ''));
|
||||||
$arrOverview['summary_html_path'] = $this->publicRelativePath((string)($arrOverview['summary_html_path'] ?? ''));
|
$arrOverview['summary_html_path'] = $this->storageRelativePath((string)($arrOverview['summary_html_path'] ?? ''));
|
||||||
|
|
||||||
$arrSnapshotTrend = DomainExternalSeoSnapshotTrendHelper::buildSummary($this->domainExternalSeoSnapshotRunRoot(), 10);
|
$arrSnapshotTrend = DomainExternalSeoSnapshotTrendHelper::buildSummary($this->domainExternalSeoSnapshotRunRoot(), 10);
|
||||||
$arrSnapshotTrend = DomainExternalSeoSnapshotTrendHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSnapshotTrend);
|
$arrSnapshotTrend = DomainExternalSeoSnapshotTrendHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSnapshotTrend);
|
||||||
$arrSnapshotTrend['summary_json_path'] = $this->publicRelativePath((string)($arrSnapshotTrend['summary_json_path'] ?? ''));
|
$arrSnapshotTrend['summary_json_path'] = $this->storageRelativePath((string)($arrSnapshotTrend['summary_json_path'] ?? ''));
|
||||||
$arrSnapshotTrend['summary_html_path'] = $this->publicRelativePath((string)($arrSnapshotTrend['summary_html_path'] ?? ''));
|
$arrSnapshotTrend['summary_html_path'] = $this->storageRelativePath((string)($arrSnapshotTrend['summary_html_path'] ?? ''));
|
||||||
|
|
||||||
$arrResult['post_ingest_overview'] = $arrOverview;
|
$arrResult['post_ingest_overview'] = $arrOverview;
|
||||||
$arrResult['post_ingest_snapshot_trend'] = $arrSnapshotTrend;
|
$arrResult['post_ingest_snapshot_trend'] = $arrSnapshotTrend;
|
||||||
$arrResult = DomainExternalSeoSnapshotRunHelper::writeRunArtifacts(
|
$arrResult = DomainExternalSeoSnapshotRunHelper::writeRunArtifacts(
|
||||||
$this->bootstrapCodeRoot() . '/public',
|
$this->bootstrapCodeRoot() . '/storage',
|
||||||
$strRunRoot,
|
$strRunRoot,
|
||||||
$arrResult,
|
$arrResult,
|
||||||
(array)$arrSnapshots
|
(array)$arrSnapshots
|
||||||
);
|
);
|
||||||
$arrResult['summary_json_path'] = $this->publicRelativePath((string)($arrResult['summary_json_path'] ?? ''));
|
$arrResult['summary_json_path'] = $this->storageRelativePath((string)($arrResult['summary_json_path'] ?? ''));
|
||||||
$arrResult['summary_html_path'] = $this->publicRelativePath((string)($arrResult['summary_html_path'] ?? ''));
|
$arrResult['summary_html_path'] = $this->storageRelativePath((string)($arrResult['summary_html_path'] ?? ''));
|
||||||
$arrResult['payload_json_path'] = $this->publicRelativePath((string)($arrResult['payload_json_path'] ?? ''));
|
$arrResult['payload_json_path'] = $this->storageRelativePath((string)($arrResult['payload_json_path'] ?? ''));
|
||||||
$arrSnapshotTrend = DomainExternalSeoSnapshotTrendHelper::buildSummary($this->domainExternalSeoSnapshotRunRoot(), 10);
|
$arrSnapshotTrend = DomainExternalSeoSnapshotTrendHelper::buildSummary($this->domainExternalSeoSnapshotRunRoot(), 10);
|
||||||
$arrSnapshotTrend = DomainExternalSeoSnapshotTrendHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSnapshotTrend);
|
$arrSnapshotTrend = DomainExternalSeoSnapshotTrendHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSnapshotTrend);
|
||||||
$arrSnapshotTrend['summary_json_path'] = $this->publicRelativePath((string)($arrSnapshotTrend['summary_json_path'] ?? ''));
|
$arrSnapshotTrend['summary_json_path'] = $this->storageRelativePath((string)($arrSnapshotTrend['summary_json_path'] ?? ''));
|
||||||
$arrSnapshotTrend['summary_html_path'] = $this->publicRelativePath((string)($arrSnapshotTrend['summary_html_path'] ?? ''));
|
$arrSnapshotTrend['summary_html_path'] = $this->storageRelativePath((string)($arrSnapshotTrend['summary_html_path'] ?? ''));
|
||||||
$arrResult['post_ingest_snapshot_trend'] = $arrSnapshotTrend;
|
$arrResult['post_ingest_snapshot_trend'] = $arrSnapshotTrend;
|
||||||
return $this->success($arrResult);
|
return $this->success($arrResult);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
@@ -2578,15 +2663,15 @@ class Site extends BaseController
|
|||||||
$arrResult = DomainExternalSeoSnapshotSchemaHelper::validateSnapshots((array)$arrSnapshots);
|
$arrResult = DomainExternalSeoSnapshotSchemaHelper::validateSnapshots((array)$arrSnapshots);
|
||||||
$strRunRoot = DomainExternalSeoSnapshotValidateRunHelper::createRunRoot($this->domainExternalSeoSnapshotValidateRunRoot(), 'snapshot_validate');
|
$strRunRoot = DomainExternalSeoSnapshotValidateRunHelper::createRunRoot($this->domainExternalSeoSnapshotValidateRunRoot(), 'snapshot_validate');
|
||||||
$arrResult = DomainExternalSeoSnapshotValidateRunHelper::writeRunArtifacts(
|
$arrResult = DomainExternalSeoSnapshotValidateRunHelper::writeRunArtifacts(
|
||||||
$this->bootstrapCodeRoot() . '/public',
|
$this->bootstrapCodeRoot() . '/storage',
|
||||||
$strRunRoot,
|
$strRunRoot,
|
||||||
$arrResult,
|
$arrResult,
|
||||||
(array)$arrSnapshots
|
(array)$arrSnapshots
|
||||||
);
|
);
|
||||||
$arrResult['run_root'] = $this->publicRelativePath($strRunRoot);
|
$arrResult['run_root'] = $this->storageRelativePath($strRunRoot);
|
||||||
$arrResult['summary_json_path'] = $this->publicRelativePath((string)($arrResult['summary_json_path'] ?? ''));
|
$arrResult['summary_json_path'] = $this->storageRelativePath((string)($arrResult['summary_json_path'] ?? ''));
|
||||||
$arrResult['summary_html_path'] = $this->publicRelativePath((string)($arrResult['summary_html_path'] ?? ''));
|
$arrResult['summary_html_path'] = $this->storageRelativePath((string)($arrResult['summary_html_path'] ?? ''));
|
||||||
$arrResult['payload_json_path'] = $this->publicRelativePath((string)($arrResult['payload_json_path'] ?? ''));
|
$arrResult['payload_json_path'] = $this->storageRelativePath((string)($arrResult['payload_json_path'] ?? ''));
|
||||||
|
|
||||||
return $this->success($arrResult);
|
return $this->success($arrResult);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
@@ -2871,8 +2956,8 @@ class Site extends BaseController
|
|||||||
$intDays = max(1, min(30, (int)$Request->get('days', 7)));
|
$intDays = max(1, min(30, (int)$Request->get('days', 7)));
|
||||||
$arrSummary = DomainExternalSeoRealSummaryHelper::buildSummary($this->bootstrapCodeRoot(), $intDays);
|
$arrSummary = DomainExternalSeoRealSummaryHelper::buildSummary($this->bootstrapCodeRoot(), $intDays);
|
||||||
$arrSummary = DomainExternalSeoRealSummaryHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSummary);
|
$arrSummary = DomainExternalSeoRealSummaryHelper::writeArtifacts($this->domainExternalSeoSummaryRoot(), $arrSummary);
|
||||||
$arrSummary['summary_json_path'] = $this->publicRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
$arrSummary['summary_json_path'] = $this->storageRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
||||||
$arrSummary['summary_html_path'] = $this->publicRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
$arrSummary['summary_html_path'] = $this->storageRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
||||||
return $this->success($arrSummary);
|
return $this->success($arrSummary);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
return $this->error('9999', '读取真实站外 summary 骨架失败:' . $Throwable->getMessage());
|
return $this->error('9999', '读取真实站外 summary 骨架失败:' . $Throwable->getMessage());
|
||||||
@@ -2885,7 +2970,7 @@ class Site extends BaseController
|
|||||||
$arrTemplate = DomainExternalSeoManualImportHelper::writeTemplate($this->domainExternalSeoManualImportRoot());
|
$arrTemplate = DomainExternalSeoManualImportHelper::writeTemplate($this->domainExternalSeoManualImportRoot());
|
||||||
return $this->success([
|
return $this->success([
|
||||||
'file_name' => (string)($arrTemplate['file_name'] ?? ''),
|
'file_name' => (string)($arrTemplate['file_name'] ?? ''),
|
||||||
'file_path' => $this->publicRelativePath((string)($arrTemplate['file_path'] ?? '')),
|
'file_path' => $this->storageRelativePath((string)($arrTemplate['file_path'] ?? '')),
|
||||||
'summary' => $arrTemplate['summary'] ?? [],
|
'summary' => $arrTemplate['summary'] ?? [],
|
||||||
]);
|
]);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
@@ -2936,20 +3021,20 @@ class Site extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
return $this->success([
|
return $this->success([
|
||||||
'uploaded_excel_path' => $this->publicRelativePath($strUploadedPath),
|
'uploaded_excel_path' => $this->storageRelativePath($strUploadedPath),
|
||||||
'uploaded_report_path' => $this->publicRelativePath($strUploadedPath),
|
'uploaded_report_path' => $this->storageRelativePath($strUploadedPath),
|
||||||
'csv_path' => $this->publicRelativePath((string)($arrResult['csv_path'] ?? '')),
|
'csv_path' => $this->storageRelativePath((string)($arrResult['csv_path'] ?? '')),
|
||||||
'run_root' => $this->publicRelativePath($strRunRoot),
|
'run_root' => $this->storageRelativePath($strRunRoot),
|
||||||
'run_csv_path' => $this->publicRelativePath((string)($arrResult['run_csv_path'] ?? '')),
|
'run_csv_path' => $this->storageRelativePath((string)($arrResult['run_csv_path'] ?? '')),
|
||||||
'run_uploaded_report_path' => $this->publicRelativePath($strRunUploadedPath),
|
'run_uploaded_report_path' => $this->storageRelativePath($strRunUploadedPath),
|
||||||
'rows_total' => (int)($arrResult['rows_total'] ?? 0),
|
'rows_total' => (int)($arrResult['rows_total'] ?? 0),
|
||||||
'imported_count' => (int)($arrResult['imported_count'] ?? 0),
|
'imported_count' => (int)($arrResult['imported_count'] ?? 0),
|
||||||
'real_summary_status' => (string)($arrRealSummary['status'] ?? ''),
|
'real_summary_status' => (string)($arrRealSummary['status'] ?? ''),
|
||||||
'real_metrics_available' => !empty($arrRealSummary['real_metrics_available']),
|
'real_metrics_available' => !empty($arrRealSummary['real_metrics_available']),
|
||||||
'real_summary_provider' => (string)($arrRealSummary['provider_key'] ?? ''),
|
'real_summary_provider' => (string)($arrRealSummary['provider_key'] ?? ''),
|
||||||
'metrics' => (array)($arrRealSummary['metrics'] ?? []),
|
'metrics' => (array)($arrRealSummary['metrics'] ?? []),
|
||||||
'summary_json_path' => $this->publicRelativePath((string)($arrRealSummary['summary_json_path'] ?? '')),
|
'summary_json_path' => $this->storageRelativePath((string)($arrRealSummary['summary_json_path'] ?? '')),
|
||||||
'summary_html_path' => $this->publicRelativePath((string)($arrRealSummary['summary_html_path'] ?? '')),
|
'summary_html_path' => $this->storageRelativePath((string)($arrRealSummary['summary_html_path'] ?? '')),
|
||||||
]);
|
]);
|
||||||
} catch (\Throwable $Throwable) {
|
} catch (\Throwable $Throwable) {
|
||||||
return $this->error('9999', '导入站外手工报表失败:' . $Throwable->getMessage());
|
return $this->error('9999', '导入站外手工报表失败:' . $Throwable->getMessage());
|
||||||
@@ -3156,6 +3241,278 @@ class Site extends BaseController
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function buildPostImportWorkflow(array $arrInsertedDomains, array $arrImportProbeSummary = []): array
|
||||||
|
{
|
||||||
|
$arrInsertedDomains = array_values(array_filter(
|
||||||
|
$arrInsertedDomains,
|
||||||
|
static fn ($arrItem): bool => is_array($arrItem) && !empty($arrItem['d_domain'])
|
||||||
|
));
|
||||||
|
if (empty($arrInsertedDomains)) {
|
||||||
|
return [
|
||||||
|
'enabled' => false,
|
||||||
|
'notices' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrHosts = array_values(array_unique(array_filter(array_map(
|
||||||
|
static fn (array $arrItem): string => DomainModel::normalizeHost((string)($arrItem['d_domain'] ?? '')),
|
||||||
|
$arrInsertedDomains
|
||||||
|
))));
|
||||||
|
$arrIds = array_values(array_unique(array_filter(array_map(
|
||||||
|
static fn (array $arrItem): int => (int)($arrItem['d_id'] ?? 0),
|
||||||
|
$arrInsertedDomains
|
||||||
|
))));
|
||||||
|
$arrOpenAiDomains = array_values(array_filter($arrInsertedDomains, static function (array $arrItem): bool {
|
||||||
|
return DomainSeoNamingHelper::shouldOptimizeForOpenAi(
|
||||||
|
(string)($arrItem['tkd_provider'] ?? ''),
|
||||||
|
(string)($arrItem['tkd_mode'] ?? '')
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
|
||||||
|
$boolContainsOpenAiDomains = !empty($arrOpenAiDomains);
|
||||||
|
$strProbeStatus = (string)($arrImportProbeSummary['status'] ?? 'skipped');
|
||||||
|
$strProbeSummaryHtmlPath = $this->publicRelativePath((string)($arrImportProbeSummary['summary_html_path'] ?? ''));
|
||||||
|
$strProbeSummaryJsonPath = $this->publicRelativePath((string)($arrImportProbeSummary['summary_json_path'] ?? ''));
|
||||||
|
$arrArtifactAudit = $this->buildPostImportArtifactAudit($arrInsertedDomains);
|
||||||
|
|
||||||
|
$arrNotices = [
|
||||||
|
'这批域名已经导入成功,但系统不会直接自动开始 AI 生成,请先确认页面探测结果。',
|
||||||
|
'如果由运营继续处理,请先看探测摘要,再手动点击后台 AI 生成功能。',
|
||||||
|
'如果由技术继续处理,请复制 Codex 提示词,把这批域名交给 Codex 接管。',
|
||||||
|
];
|
||||||
|
if ($strProbeStatus !== '') {
|
||||||
|
$arrNotices[] = '本批次导入探测状态:' . $strProbeStatus . '。建议先确认首页、分类页、详情页、播放页链路正常,再进入下一步。';
|
||||||
|
}
|
||||||
|
if ($boolContainsOpenAiDomains) {
|
||||||
|
$arrNotices[] = '这批域名里包含 OpenAI 模式站点,默认建议运营走后台 AI 按钮,技术走 Codex 接管。';
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'enabled' => true,
|
||||||
|
'auto_ai_after_import' => false,
|
||||||
|
'summary' => '导入已完成。请先看探测结果,再明确选择“运营继续”或“技术继续”,系统不会自动偷偷调用 OpenAI。',
|
||||||
|
'inserted_domain_count' => count($arrInsertedDomains),
|
||||||
|
'inserted_domain_hosts' => $arrHosts,
|
||||||
|
'contains_openai_domains' => $boolContainsOpenAiDomains,
|
||||||
|
'artifact_audit' => $arrArtifactAudit,
|
||||||
|
'recommended_for_operator' => 'openai_continue',
|
||||||
|
'recommended_for_technical' => 'codex_continue',
|
||||||
|
'notices' => $arrNotices,
|
||||||
|
'choices' => [
|
||||||
|
[
|
||||||
|
'key' => 'openai_continue',
|
||||||
|
'label' => 'OpenAI继续',
|
||||||
|
'audience' => '运营',
|
||||||
|
'description' => '适合运营同学。先确认探测正常,再把这批新站提交到后台 AI 生成队列。',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'codex_continue',
|
||||||
|
'label' => 'Codex继续',
|
||||||
|
'audience' => '技术',
|
||||||
|
'description' => '适合技术同学。复制提示词给 Codex,由 Codex 接手 SEO 优化、复测、记录和收口。',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'openai' => [
|
||||||
|
'domain_ids' => $arrIds,
|
||||||
|
'domain_id_csv' => implode(',', $arrIds),
|
||||||
|
'hosts' => $arrHosts,
|
||||||
|
'probe_status' => $strProbeStatus,
|
||||||
|
'probe_summary_html_path' => $strProbeSummaryHtmlPath,
|
||||||
|
'probe_summary_json_path' => $strProbeSummaryJsonPath,
|
||||||
|
'operator_steps' => [
|
||||||
|
'先打开本次导入探测摘要,确认首页、分类页、详情页、播放页没有明显报错。',
|
||||||
|
'确认页面链路正常后,回到后台站点列表,筛选或勾选本批域名。',
|
||||||
|
'如果这批站点准备走运营 AI 链路,再点击 AI 生成或 AI 重优化按钮,显式触发 OpenAI 队列。',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'codex' => [
|
||||||
|
'workspace_path' => $this->bootstrapCodeRoot(),
|
||||||
|
'open_in_vscode_hint' => '请先在 VS Code 打开当前项目工作区,再把下面的提示词完整发给 Codex。Codex 接手时不会自动消耗 OpenAI API。',
|
||||||
|
'prompt' => $this->buildPostImportCodexPrompt($arrInsertedDomains, $arrImportProbeSummary),
|
||||||
|
],
|
||||||
|
'ui_suggestion' => [
|
||||||
|
'message' => '建议在导入成功弹窗里直接展示两个按钮:Codex继续、OpenAI继续,并支持一键复制 Codex 提示词。',
|
||||||
|
'button_labels' => ['Codex继续', 'OpenAI继续', '复制Codex提示词'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildPostImportArtifactAudit(array $arrInsertedDomains): array
|
||||||
|
{
|
||||||
|
$arrInsertedDomains = array_values(array_filter(
|
||||||
|
$arrInsertedDomains,
|
||||||
|
static fn ($arrItem): bool => is_array($arrItem) && !empty($arrItem['d_domain'])
|
||||||
|
));
|
||||||
|
if (empty($arrInsertedDomains)) {
|
||||||
|
return [
|
||||||
|
'summary' => [
|
||||||
|
'total' => 0,
|
||||||
|
'state_ready_count' => 0,
|
||||||
|
'manifest_ready_count' => 0,
|
||||||
|
'positioning_ready_count' => 0,
|
||||||
|
'published_copy_ready_count' => 0,
|
||||||
|
'sitemap_generated_count' => 0,
|
||||||
|
'ai_pending_count' => 0,
|
||||||
|
],
|
||||||
|
'items' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrItems = [];
|
||||||
|
$intStateReadyCount = 0;
|
||||||
|
$intManifestReadyCount = 0;
|
||||||
|
$intPositioningReadyCount = 0;
|
||||||
|
$intPublishedCopyReadyCount = 0;
|
||||||
|
$intSitemapGeneratedCount = 0;
|
||||||
|
$intAiPendingCount = 0;
|
||||||
|
|
||||||
|
foreach ($arrInsertedDomains as $arrItem) {
|
||||||
|
$strHost = DomainModel::normalizeHost((string)($arrItem['d_domain'] ?? ''));
|
||||||
|
if ($strHost === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$strHostKey = SeoCopyStore::buildPageKey($strHost, 'host');
|
||||||
|
$strStatePath = SeoCopyGenerationHelper::resolveStatePath($strHost);
|
||||||
|
$strManifestPath = SeoCopyGenerationHelper::resolveManifestPath($strHost);
|
||||||
|
$strPositioningPath = SeoResourcePoolHelper::resourceRoot() . '/site_positioning/by_host/' . SeoResourcePoolHelper::hostFileKey($strHost) . '.json';
|
||||||
|
$strPublishedRoot = rtrim(SeoCopyStore::publishedRoot(), '/') . '/' . $strHostKey;
|
||||||
|
$strSitemapRoot = rtrim((string)root_path('storage/SiteMap'), '/');
|
||||||
|
$strSitemapDir = $strSitemapRoot . '/' . $strHost;
|
||||||
|
|
||||||
|
$boolStateExists = is_file($strStatePath);
|
||||||
|
$boolManifestExists = is_file($strManifestPath);
|
||||||
|
$boolPositioningExists = is_file($strPositioningPath);
|
||||||
|
$boolPublishedCopyExists = !empty(glob($strPublishedRoot . '/*/*.json') ?: []);
|
||||||
|
|
||||||
|
$arrState = $boolStateExists ? SeoCopyGenerationHelper::readState($strHost) : [];
|
||||||
|
$strAiStatus = (string)($arrState['status'] ?? '');
|
||||||
|
$boolAiPending = $strAiStatus === SeoCopyGenerationHelper::STATUS_AI_PENDING;
|
||||||
|
$boolWaitForAi = !empty($arrState['wait_for_ai_before_publish']);
|
||||||
|
|
||||||
|
$arrSitemapCandidates = [
|
||||||
|
$strSitemapDir . '/sitemap_index.xml',
|
||||||
|
$strSitemapDir . '/sitemap-main.xml',
|
||||||
|
$strSitemapDir . '/sitemap-videos-1.xml',
|
||||||
|
];
|
||||||
|
$arrExistingSitemaps = array_values(array_filter($arrSitemapCandidates, static fn (string $strPath): bool => is_file($strPath)));
|
||||||
|
$boolSitemapGenerated = !empty($arrExistingSitemaps);
|
||||||
|
|
||||||
|
if ($boolStateExists) {
|
||||||
|
$intStateReadyCount++;
|
||||||
|
}
|
||||||
|
if ($boolManifestExists) {
|
||||||
|
$intManifestReadyCount++;
|
||||||
|
}
|
||||||
|
if ($boolPositioningExists) {
|
||||||
|
$intPositioningReadyCount++;
|
||||||
|
}
|
||||||
|
if ($boolPublishedCopyExists) {
|
||||||
|
$intPublishedCopyReadyCount++;
|
||||||
|
}
|
||||||
|
if ($boolSitemapGenerated) {
|
||||||
|
$intSitemapGeneratedCount++;
|
||||||
|
}
|
||||||
|
if ($boolAiPending) {
|
||||||
|
$intAiPendingCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrItems[] = [
|
||||||
|
'host' => $strHost,
|
||||||
|
'site_name' => (string)($arrItem['d_name'] ?? ''),
|
||||||
|
'state_exists' => $boolStateExists,
|
||||||
|
'manifest_exists' => $boolManifestExists,
|
||||||
|
'site_positioning_exists' => $boolPositioningExists,
|
||||||
|
'published_copy_exists' => $boolPublishedCopyExists,
|
||||||
|
'ai_status' => $strAiStatus !== '' ? $strAiStatus : 'unknown',
|
||||||
|
'wait_for_ai_before_publish' => $boolWaitForAi,
|
||||||
|
'sitemap_status' => $boolSitemapGenerated ? 'generated' : 'queued_pending',
|
||||||
|
'existing_sitemaps' => array_map(static fn (string $strPath): string => basename($strPath), $arrExistingSitemaps),
|
||||||
|
'notes' => [
|
||||||
|
$boolStateExists ? 'state.json 已生成' : 'state.json 缺失',
|
||||||
|
$boolManifestExists ? 'manifest.json 已生成' : 'manifest.json 缺失',
|
||||||
|
$boolPositioningExists ? 'site_positioning 已生成' : 'site_positioning 缺失',
|
||||||
|
$boolPublishedCopyExists ? 'starter copy 已发布' : 'starter copy 未落盘',
|
||||||
|
$boolSitemapGenerated ? '已检测到 sitemap 文件' : '已请求 sitemap 生成,当前尚未看到落盘文件',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'summary' => [
|
||||||
|
'total' => count($arrItems),
|
||||||
|
'state_ready_count' => $intStateReadyCount,
|
||||||
|
'manifest_ready_count' => $intManifestReadyCount,
|
||||||
|
'positioning_ready_count' => $intPositioningReadyCount,
|
||||||
|
'published_copy_ready_count' => $intPublishedCopyReadyCount,
|
||||||
|
'sitemap_generated_count' => $intSitemapGeneratedCount,
|
||||||
|
'ai_pending_count' => $intAiPendingCount,
|
||||||
|
],
|
||||||
|
'items' => $arrItems,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildPostImportCodexPrompt(array $arrInsertedDomains, array $arrImportProbeSummary = []): string
|
||||||
|
{
|
||||||
|
$arrLines = [
|
||||||
|
'请接手这批刚导入的新站,并继续完成导入后的 SEO 收口工作。',
|
||||||
|
'',
|
||||||
|
'本批域名:',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($arrInsertedDomains as $arrItem) {
|
||||||
|
if (!is_array($arrItem)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$strDomain = DomainModel::normalizeHost((string)($arrItem['d_domain'] ?? ''));
|
||||||
|
if ($strDomain === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrLines[] = sprintf(
|
||||||
|
'- %s | 站点名=%s | 模板ID=%d | TKD来源=%s | TKD模式=%s | 策略=%s',
|
||||||
|
$strDomain,
|
||||||
|
trim((string)($arrItem['d_name'] ?? '')) !== '' ? (string)$arrItem['d_name'] : '未命名',
|
||||||
|
(int)($arrItem['t_id'] ?? 0),
|
||||||
|
(string)($arrItem['tkd_provider'] ?? DomainModel::TKD_PROVIDER_LOCAL),
|
||||||
|
(string)($arrItem['tkd_mode'] ?? DomainModel::TKD_MODE_AUTO_GENERATE),
|
||||||
|
(string)($arrItem['strategy_profile'] ?? DomainModel::SEO_STRATEGY_PROFILE_STANDARD)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$strProbeStatus = (string)($arrImportProbeSummary['status'] ?? 'skipped');
|
||||||
|
$strProbeSummaryHtmlPath = $this->publicRelativePath((string)($arrImportProbeSummary['summary_html_path'] ?? ''));
|
||||||
|
$strProbeSummaryJsonPath = $this->publicRelativePath((string)($arrImportProbeSummary['summary_json_path'] ?? ''));
|
||||||
|
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '已知背景:';
|
||||||
|
$arrLines[] = '- 导入成功后,不要默认静默调用 OpenAI API。';
|
||||||
|
$arrLines[] = '- 如果后台明确点击 AI 优化,再走 OpenAI 队列;否则由 Codex 直接接管人工优化。';
|
||||||
|
$arrLines[] = '- 请优先以 SEO 效果、可收录、可复测、可持续跟踪为目标。';
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '请执行:';
|
||||||
|
$arrLines[] = '1. 复查首页、搜索页、分类页、详情页、播放页是否可访问,确认没有 404、500 或模板错位。';
|
||||||
|
$arrLines[] = '2. 检查导入后的自动产物是否齐全,包括 SEO copy、探测报告、sitemap 队列、站点定位资源。';
|
||||||
|
$arrLines[] = '3. 如果这批站点配置为 OpenAI 模式,但当前是 Codex 接管,请直接做人审优化,不要自动消耗 API。';
|
||||||
|
$arrLines[] = '4. 输出这批站点的 Day0 基线,包含标题、关键词、描述、收录入口、主要风险。';
|
||||||
|
$arrLines[] = '5. 把处理记录写到 GPT 模板专属文档目录,并在公共主线文档里做简短汇总。';
|
||||||
|
|
||||||
|
if ($strProbeStatus !== '' || $strProbeSummaryHtmlPath !== '' || $strProbeSummaryJsonPath !== '') {
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '导入探测信息:';
|
||||||
|
$arrLines[] = '- probe_status: ' . ($strProbeStatus !== '' ? $strProbeStatus : 'skipped');
|
||||||
|
if ($strProbeSummaryHtmlPath !== '') {
|
||||||
|
$arrLines[] = '- probe_summary_html_path: ' . $strProbeSummaryHtmlPath;
|
||||||
|
}
|
||||||
|
if ($strProbeSummaryJsonPath !== '') {
|
||||||
|
$arrLines[] = '- probe_summary_json_path: ' . $strProbeSummaryJsonPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(PHP_EOL, $arrLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加|修改 域名
|
* 添加|修改 域名
|
||||||
@@ -3230,6 +3587,7 @@ class Site extends BaseController
|
|||||||
$DomainModel->save();
|
$DomainModel->save();
|
||||||
DomainModel::flushAllDomianOnCache();
|
DomainModel::flushAllDomianOnCache();
|
||||||
SeoCopyGenerationHelper::initializeForDomain($DomainModel);
|
SeoCopyGenerationHelper::initializeForDomain($DomainModel);
|
||||||
|
DomainSitemapGenerationHelper::queueForDomains([(string)$DomainModel->d_domain]);
|
||||||
|
|
||||||
return $this->success();
|
return $this->success();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
|||||||
namespace app\admin\controller;
|
namespace app\admin\controller;
|
||||||
|
|
||||||
use app\admin\BaseController;
|
use app\admin\BaseController;
|
||||||
|
use app\common\helper\SeoCopyAiRuleConfigHelper;
|
||||||
|
use app\common\helper\SpiderMdConfigHelper;
|
||||||
use app\model\AdminUserModel;
|
use app\model\AdminUserModel;
|
||||||
use app\model\BaoYangRuZhuModel;
|
use app\model\BaoYangRuZhuModel;
|
||||||
use app\model\CaiJiPeiZhiModel;
|
use app\model\CaiJiPeiZhiModel;
|
||||||
@@ -23,6 +25,44 @@ use think\Response;
|
|||||||
|
|
||||||
class SystemConfig extends BaseController
|
class SystemConfig extends BaseController
|
||||||
{
|
{
|
||||||
|
public function previewStorageFile(Request $Request, ?AdminUserModel $AdminUserModel)
|
||||||
|
{
|
||||||
|
$strRelativePath = trim(str_replace('\\', '/', (string)$Request->get('path', '')));
|
||||||
|
if ($strRelativePath === '') {
|
||||||
|
return response('storage path is required', 400, ['Content-Type' => 'text/plain; charset=UTF-8']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$strRelativePath = ltrim($strRelativePath, '/');
|
||||||
|
if (str_starts_with($strRelativePath, 'storage/')) {
|
||||||
|
$strRelativePath = substr($strRelativePath, strlen('storage/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$strStorageRoot = rtrim(str_replace('\\', '/', (string)root_path()), '/') . '/storage';
|
||||||
|
$strStorageRootReal = realpath($strStorageRoot);
|
||||||
|
$strCandidatePath = $strStorageRoot . '/' . $strRelativePath;
|
||||||
|
$strCandidateReal = realpath($strCandidatePath);
|
||||||
|
|
||||||
|
if ($strStorageRootReal === false || $strCandidateReal === false || !is_file($strCandidateReal)) {
|
||||||
|
return response('storage file not found', 404, ['Content-Type' => 'text/plain; charset=UTF-8']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$strStorageRootReal = str_replace('\\', '/', $strStorageRootReal);
|
||||||
|
$strCandidateReal = str_replace('\\', '/', $strCandidateReal);
|
||||||
|
if (!str_starts_with($strCandidateReal, $strStorageRootReal . '/')) {
|
||||||
|
return response('storage file is out of allowed root', 403, ['Content-Type' => 'text/plain; charset=UTF-8']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$strMimeType = (string)(mime_content_type($strCandidateReal) ?: 'application/octet-stream');
|
||||||
|
$strFilename = basename($strCandidateReal);
|
||||||
|
$strContent = (string)file_get_contents($strCandidateReal);
|
||||||
|
|
||||||
|
return response($strContent, 200, [
|
||||||
|
'Content-Type' => $strMimeType,
|
||||||
|
'Content-Disposition' => 'inline; filename="' . addslashes($strFilename) . '"',
|
||||||
|
'X-Storage-Relative-Path' => $strRelativePath,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 采集配置列表
|
* 采集配置列表
|
||||||
@@ -120,6 +160,9 @@ class SystemConfig extends BaseController
|
|||||||
*/
|
*/
|
||||||
public function getSystemConfigList(Request $Request, ?AdminUserModel $AdminUserModel)
|
public function getSystemConfigList(Request $Request, ?AdminUserModel $AdminUserModel)
|
||||||
{
|
{
|
||||||
|
SeoCopyAiRuleConfigHelper::ensureSystemConfigDefaults();
|
||||||
|
SpiderMdConfigHelper::ensureSystemConfigDefaults();
|
||||||
|
|
||||||
$arrData = [
|
$arrData = [
|
||||||
"page" => $Request->get('page', 1),
|
"page" => $Request->get('page', 1),
|
||||||
"limit" => $Request->get('limit', 10),
|
"limit" => $Request->get('limit', 10),
|
||||||
@@ -196,6 +239,25 @@ class SystemConfig extends BaseController
|
|||||||
return $this->success();
|
return $this->success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function resetSeoAiRules(Request $Request, ?AdminUserModel $AdminUserModel)
|
||||||
|
{
|
||||||
|
$arrData = [
|
||||||
|
'sc_code' => trim((string)$Request->post('sc_code', '')),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($arrData['sc_code'] !== '' && !in_array($arrData['sc_code'], SeoCopyAiRuleConfigHelper::editableCodes(), true)) {
|
||||||
|
return $this->error('9993', '无效的 SEO AI 规则编码');
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrResult = SeoCopyAiRuleConfigHelper::resetToDefault($arrData['sc_code'] !== '' ? $arrData['sc_code'] : null);
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'codes' => $arrResult['codes'] ?? [],
|
||||||
|
'count' => (int)($arrResult['count'] ?? 0),
|
||||||
|
'scope' => $arrData['sc_code'] !== '' ? 'single' : 'all',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 资源端点列表
|
* 资源端点列表
|
||||||
|
|||||||
@@ -5,7 +5,13 @@ declare(strict_types=1);
|
|||||||
namespace app\admin\controller;
|
namespace app\admin\controller;
|
||||||
|
|
||||||
use app\admin\BaseController;
|
use app\admin\BaseController;
|
||||||
|
use app\common\helper\VideoMetadataMissingWorkbenchHelper;
|
||||||
|
use app\common\helper\VideoMetadataMissingWorkbenchIndexHelper;
|
||||||
|
use app\common\helper\VideoMetadataMissingTaskPoolHelper;
|
||||||
|
use app\common\helper\VideoMetadataMissingTaskPoolStateHelper;
|
||||||
|
use app\common\helper\VideoMetadataMissingTaskPoolStatusHelper;
|
||||||
use app\model\AdminUserModel;
|
use app\model\AdminUserModel;
|
||||||
|
use app\model\PlanTaskModel;
|
||||||
use app\model\VideoModel;
|
use app\model\VideoModel;
|
||||||
|
|
||||||
use app\model\VideoCategoryModel;
|
use app\model\VideoCategoryModel;
|
||||||
@@ -131,4 +137,363 @@ class Video extends BaseController
|
|||||||
$arrResult = VideoCategoryModel::$arrCategory;
|
$arrResult = VideoCategoryModel::$arrCategory;
|
||||||
return $this->success($arrResult);
|
return $this->success($arrResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getMetadataMissingWorkbenchSummary(Request $Request, ?AdminUserModel $AdminUserModel)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$boolRefresh = (int)$Request->get('refresh', 0) === 1;
|
||||||
|
$intSample = max(1, min(100, (int)$Request->get('sample', 20)));
|
||||||
|
$intQueueLimit = max(1, min(500, (int)$Request->get('queue_limit', 100)));
|
||||||
|
$intPromptLimit = max(1, min(100, (int)$Request->get('prompt_limit', 20)));
|
||||||
|
|
||||||
|
$arrSummary = $boolRefresh ? [] : VideoMetadataMissingWorkbenchHelper::readLatestSummary($this->metadataMissingWorkbenchRoot());
|
||||||
|
if (empty($arrSummary)) {
|
||||||
|
$arrSummary = VideoMetadataMissingWorkbenchHelper::buildSummary($intSample, $intQueueLimit, $intPromptLimit);
|
||||||
|
$arrSummary = VideoMetadataMissingWorkbenchHelper::writeArtifacts($this->metadataMissingWorkbenchRoot(), $arrSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrSummary['summary_json_path'] = $this->storageRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
||||||
|
$arrSummary['summary_html_path'] = $this->storageRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
||||||
|
$arrSummary['prompt_markdown_path'] = $this->storageRelativePath((string)($arrSummary['prompt_markdown_path'] ?? ''));
|
||||||
|
|
||||||
|
return $this->success($arrSummary);
|
||||||
|
} catch (\Throwable $Throwable) {
|
||||||
|
return $this->error('9999', '读取视频缺失字段工作台失败:' . $Throwable->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMetadataMissingCodexPrompt(Request $Request, ?AdminUserModel $AdminUserModel)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$arrSummary = VideoMetadataMissingWorkbenchHelper::readLatestSummary($this->metadataMissingWorkbenchRoot());
|
||||||
|
if (empty($arrSummary)) {
|
||||||
|
$intSample = max(1, min(100, (int)$Request->get('sample', 20)));
|
||||||
|
$intQueueLimit = max(1, min(500, (int)$Request->get('queue_limit', 100)));
|
||||||
|
$intPromptLimit = max(1, min(100, (int)$Request->get('prompt_limit', 20)));
|
||||||
|
$arrSummary = VideoMetadataMissingWorkbenchHelper::buildSummary($intSample, $intQueueLimit, $intPromptLimit);
|
||||||
|
$arrSummary = VideoMetadataMissingWorkbenchHelper::writeArtifacts($this->metadataMissingWorkbenchRoot(), $arrSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'prompt' => (string)($arrSummary['codex_dispatch_prompt'] ?? ''),
|
||||||
|
'prompt_markdown_path' => $this->storageRelativePath((string)($arrSummary['prompt_markdown_path'] ?? '')),
|
||||||
|
'generated_at' => (string)($arrSummary['generated_at'] ?? ''),
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $Throwable) {
|
||||||
|
return $this->error('9999', '读取视频缺失字段提示词失败:' . $Throwable->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMetadataMissingWorkbenchRuns(Request $Request, ?AdminUserModel $AdminUserModel)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$intLimit = max(1, min(50, (int)$Request->get('limit', 20)));
|
||||||
|
$arrSummary = VideoMetadataMissingWorkbenchIndexHelper::buildSummary($this->metadataMissingWorkbenchRunRoot(), $intLimit);
|
||||||
|
return $this->success($arrSummary);
|
||||||
|
} catch (\Throwable $Throwable) {
|
||||||
|
return $this->error('9999', '读取视频缺失字段工作台运行记录失败:' . $Throwable->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMetadataMissingTaskPool(Request $Request, ?AdminUserModel $AdminUserModel)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$intBatchSize = max(5, min(100, (int)$Request->get('batch_size', 20)));
|
||||||
|
$intBatchLimit = max(1, min(50, (int)$Request->get('batch_limit', 10)));
|
||||||
|
$boolRefresh = (int)$Request->get('refresh', 0) === 1;
|
||||||
|
|
||||||
|
$arrSummary = $boolRefresh ? [] : VideoMetadataMissingTaskPoolHelper::readLatestSummary($this->metadataMissingTaskPoolRoot());
|
||||||
|
if (empty($arrSummary)) {
|
||||||
|
$arrWorkbenchSummary = VideoMetadataMissingWorkbenchHelper::readLatestSummary($this->metadataMissingWorkbenchRoot());
|
||||||
|
if (empty($arrWorkbenchSummary)) {
|
||||||
|
$arrWorkbenchSummary = VideoMetadataMissingWorkbenchHelper::buildSummary(20, 100, 20);
|
||||||
|
$arrWorkbenchSummary = VideoMetadataMissingWorkbenchHelper::writeArtifacts($this->metadataMissingWorkbenchRoot(), $arrWorkbenchSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrSummary = VideoMetadataMissingTaskPoolHelper::buildSummary($arrWorkbenchSummary, $intBatchSize, $intBatchLimit);
|
||||||
|
$arrSummary = VideoMetadataMissingTaskPoolHelper::writeArtifacts($this->metadataMissingTaskPoolRoot(), $arrSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrSummary['summary_json_path'] = $this->storageRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
||||||
|
$arrSummary['summary_html_path'] = $this->storageRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
||||||
|
|
||||||
|
return $this->success($arrSummary);
|
||||||
|
} catch (\Throwable $Throwable) {
|
||||||
|
return $this->error('9999', '读取视频缺字段待处理池失败:' . $Throwable->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMetadataMissingTaskPoolStatus(Request $Request, ?AdminUserModel $AdminUserModel)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$arrSummary = VideoMetadataMissingTaskPoolStatusHelper::buildSummary(
|
||||||
|
$this->metadataMissingWorkbenchRoot(),
|
||||||
|
$this->metadataMissingTaskPoolRoot()
|
||||||
|
);
|
||||||
|
|
||||||
|
$arrSummary['workbench']['summary_json_path'] = $this->storageRelativePath((string)($arrSummary['workbench']['summary_json_path'] ?? ''));
|
||||||
|
$arrSummary['workbench']['summary_html_path'] = $this->storageRelativePath((string)($arrSummary['workbench']['summary_html_path'] ?? ''));
|
||||||
|
$arrSummary['task_pool']['summary_json_path'] = $this->storageRelativePath((string)($arrSummary['task_pool']['summary_json_path'] ?? ''));
|
||||||
|
$arrSummary['task_pool']['summary_html_path'] = $this->storageRelativePath((string)($arrSummary['task_pool']['summary_html_path'] ?? ''));
|
||||||
|
|
||||||
|
return $this->success($arrSummary);
|
||||||
|
} catch (\Throwable $Throwable) {
|
||||||
|
return $this->error('9999', '读取视频缺字段任务池状态失败:' . $Throwable->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMetadataMissingTaskPoolOps(Request $Request, ?AdminUserModel $AdminUserModel)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$arrSummary = VideoMetadataMissingTaskPoolStatusHelper::buildSummary(
|
||||||
|
$this->metadataMissingWorkbenchRoot(),
|
||||||
|
$this->metadataMissingTaskPoolRoot()
|
||||||
|
);
|
||||||
|
|
||||||
|
$arrSummary['workbench']['summary_json_path'] = $this->storageRelativePath((string)($arrSummary['workbench']['summary_json_path'] ?? ''));
|
||||||
|
$arrSummary['workbench']['summary_html_path'] = $this->storageRelativePath((string)($arrSummary['workbench']['summary_html_path'] ?? ''));
|
||||||
|
$arrSummary['task_pool']['summary_json_path'] = $this->storageRelativePath((string)($arrSummary['task_pool']['summary_json_path'] ?? ''));
|
||||||
|
$arrSummary['task_pool']['summary_html_path'] = $this->storageRelativePath((string)($arrSummary['task_pool']['summary_html_path'] ?? ''));
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'summary' => $arrSummary,
|
||||||
|
'ops' => VideoMetadataMissingTaskPoolStatusHelper::buildOpsSummary($arrSummary),
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $Throwable) {
|
||||||
|
return $this->error('9999', '读取视频缺字段任务池运维页失败:' . $Throwable->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMetadataMissingTaskPoolPlanTaskStatus(Request $Request, ?AdminUserModel $AdminUserModel)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $this->success(VideoMetadataMissingTaskPoolStatusHelper::buildSummary(
|
||||||
|
$this->metadataMissingWorkbenchRoot(),
|
||||||
|
$this->metadataMissingTaskPoolRoot()
|
||||||
|
));
|
||||||
|
} catch (\Throwable $Throwable) {
|
||||||
|
return $this->error('9999', '读取视频缺字段计划任务状态失败:' . $Throwable->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveMetadataMissingTaskPoolPlanTaskStatus(Request $Request, ?AdminUserModel $AdminUserModel)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$arrData = [
|
||||||
|
'pt_enable' => (int)$Request->post('pt_enable', 0),
|
||||||
|
'pt_limit' => (int)$Request->post('pt_limit', 86400),
|
||||||
|
];
|
||||||
|
$this->validate($arrData, [
|
||||||
|
'pt_enable' => 'require|number|in:0,1',
|
||||||
|
'pt_limit' => 'require|number|egt:0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$PlanTaskModel = PlanTaskModel::where('pt_code', 'REFRESH_VIDEO_METADATA_TASK_POOL')->find();
|
||||||
|
if (!$PlanTaskModel) {
|
||||||
|
return $this->error('9999', '未找到 REFRESH_VIDEO_METADATA_TASK_POOL 任务配置,请先执行 plan:seed:video-metadata-missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
$PlanTaskModel->pt_enable = $arrData['pt_enable'];
|
||||||
|
$PlanTaskModel->pt_limit = $arrData['pt_limit'];
|
||||||
|
$PlanTaskModel->save();
|
||||||
|
|
||||||
|
return $this->success(VideoMetadataMissingTaskPoolStatusHelper::buildSummary(
|
||||||
|
$this->metadataMissingWorkbenchRoot(),
|
||||||
|
$this->metadataMissingTaskPoolRoot()
|
||||||
|
));
|
||||||
|
} catch (\Throwable $Throwable) {
|
||||||
|
return $this->error('9999', '保存视频缺字段计划任务状态失败:' . $Throwable->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMetadataMissingTaskPoolBatchDetail(Request $Request, ?AdminUserModel $AdminUserModel)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$strBatchId = trim((string)$Request->get('batch_id', ''));
|
||||||
|
if ($strBatchId === '') {
|
||||||
|
return $this->error('9999', 'batch_id 不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrSummary = VideoMetadataMissingTaskPoolHelper::readLatestSummary($this->metadataMissingTaskPoolRoot());
|
||||||
|
if (empty($arrSummary)) {
|
||||||
|
return $this->error('9999', '当前还没有可用的缺字段待处理池');
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrBatch = VideoMetadataMissingTaskPoolHelper::findBatchById($arrSummary, $strBatchId);
|
||||||
|
if (empty($arrBatch)) {
|
||||||
|
return $this->error('9999', '没有找到对应批次');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'batch' => $arrBatch,
|
||||||
|
'history' => VideoMetadataMissingTaskPoolStateHelper::readBatchHistory(
|
||||||
|
$this->metadataMissingTaskPoolRoot(),
|
||||||
|
$strBatchId,
|
||||||
|
50
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $Throwable) {
|
||||||
|
return $this->error('9999', '读取视频缺字段批次详情失败:' . $Throwable->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMetadataMissingTaskPoolBatchPrompt(Request $Request, ?AdminUserModel $AdminUserModel)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$strBatchId = trim((string)$Request->get('batch_id', ''));
|
||||||
|
if ($strBatchId === '') {
|
||||||
|
return $this->error('9999', 'batch_id 不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrSummary = VideoMetadataMissingTaskPoolHelper::readLatestSummary($this->metadataMissingTaskPoolRoot());
|
||||||
|
if (empty($arrSummary)) {
|
||||||
|
return $this->error('9999', '当前还没有可用的缺字段待处理池');
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrBatch = VideoMetadataMissingTaskPoolHelper::findBatchById($arrSummary, $strBatchId);
|
||||||
|
if (empty($arrBatch)) {
|
||||||
|
return $this->error('9999', '没有找到对应批次');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'batch_id' => $strBatchId,
|
||||||
|
'batch_label' => (string)($arrBatch['batch_label'] ?? ''),
|
||||||
|
'focus_field' => (string)($arrBatch['focus_field'] ?? ''),
|
||||||
|
'prompt' => (string)($arrBatch['codex_prompt'] ?? ''),
|
||||||
|
'status' => (string)($arrBatch['status'] ?? ''),
|
||||||
|
'status_label' => (string)($arrBatch['status_label'] ?? ''),
|
||||||
|
'owner' => (string)($arrBatch['owner'] ?? ''),
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $Throwable) {
|
||||||
|
return $this->error('9999', '读取视频缺字段批次提示词失败:' . $Throwable->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function runMetadataMissingTaskPool(Request $Request, ?AdminUserModel $AdminUserModel)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$intBatchSize = max(5, min(100, (int)$Request->post('batch_size', 20)));
|
||||||
|
$intBatchLimit = max(1, min(50, (int)$Request->post('batch_limit', 10)));
|
||||||
|
|
||||||
|
$arrWorkbenchSummary = VideoMetadataMissingWorkbenchHelper::readLatestSummary($this->metadataMissingWorkbenchRoot());
|
||||||
|
if (empty($arrWorkbenchSummary)) {
|
||||||
|
$arrWorkbenchSummary = VideoMetadataMissingWorkbenchHelper::buildSummary(20, 100, 20);
|
||||||
|
$arrWorkbenchSummary = VideoMetadataMissingWorkbenchHelper::writeArtifacts($this->metadataMissingWorkbenchRoot(), $arrWorkbenchSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrSummary = VideoMetadataMissingTaskPoolHelper::buildSummary($arrWorkbenchSummary, $intBatchSize, $intBatchLimit);
|
||||||
|
$arrSummary = VideoMetadataMissingTaskPoolHelper::writeArtifacts($this->metadataMissingTaskPoolRoot(), $arrSummary);
|
||||||
|
$arrSummary['summary_json_path'] = $this->storageRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
||||||
|
$arrSummary['summary_html_path'] = $this->storageRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'status' => 'success',
|
||||||
|
'summary' => $arrSummary,
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $Throwable) {
|
||||||
|
return $this->error('9999', '生成视频缺字段待处理池失败:' . $Throwable->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveMetadataMissingTaskPoolBatchState(Request $Request, ?AdminUserModel $AdminUserModel)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$strBatchId = trim((string)$Request->post('batch_id', ''));
|
||||||
|
$strStatus = trim((string)$Request->post('status', VideoMetadataMissingTaskPoolStateHelper::STATUS_PENDING));
|
||||||
|
$strOwner = trim((string)$Request->post('owner', ''));
|
||||||
|
$strNote = trim((string)$Request->post('note', ''));
|
||||||
|
|
||||||
|
if ($strBatchId === '') {
|
||||||
|
return $this->error('9999', 'batch_id 不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrState = VideoMetadataMissingTaskPoolStateHelper::saveBatchState(
|
||||||
|
$this->metadataMissingTaskPoolRoot(),
|
||||||
|
$strBatchId,
|
||||||
|
$strStatus,
|
||||||
|
$strOwner,
|
||||||
|
$strNote
|
||||||
|
);
|
||||||
|
|
||||||
|
$arrSummary = VideoMetadataMissingTaskPoolHelper::readLatestSummary($this->metadataMissingTaskPoolRoot());
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'state' => $arrState,
|
||||||
|
'status_label' => VideoMetadataMissingTaskPoolStateHelper::statusLabel((string)($arrState['status'] ?? 'pending')),
|
||||||
|
'summary' => $arrSummary,
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $Throwable) {
|
||||||
|
return $this->error('9999', '保存视频缺字段批次状态失败:' . $Throwable->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMetadataMissingTaskPoolBatchHistory(Request $Request, ?AdminUserModel $AdminUserModel)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$strBatchId = trim((string)$Request->get('batch_id', ''));
|
||||||
|
$intLimit = max(1, min(200, (int)$Request->get('limit', 50)));
|
||||||
|
if ($strBatchId === '') {
|
||||||
|
return $this->error('9999', 'batch_id 不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'batch_id' => $strBatchId,
|
||||||
|
'items' => VideoMetadataMissingTaskPoolStateHelper::readBatchHistory(
|
||||||
|
$this->metadataMissingTaskPoolRoot(),
|
||||||
|
$strBatchId,
|
||||||
|
$intLimit
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $Throwable) {
|
||||||
|
return $this->error('9999', '读取视频缺字段批次历史失败:' . $Throwable->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function runMetadataMissingWorkbench(Request $Request, ?AdminUserModel $AdminUserModel)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$intSample = max(1, min(100, (int)$Request->post('sample', 20)));
|
||||||
|
$intQueueLimit = max(1, min(500, (int)$Request->post('queue_limit', 100)));
|
||||||
|
$intPromptLimit = max(1, min(100, (int)$Request->post('prompt_limit', 20)));
|
||||||
|
|
||||||
|
$arrSummary = VideoMetadataMissingWorkbenchHelper::buildSummary($intSample, $intQueueLimit, $intPromptLimit);
|
||||||
|
$arrSummary = VideoMetadataMissingWorkbenchHelper::writeArtifacts($this->metadataMissingWorkbenchRoot(), $arrSummary);
|
||||||
|
$arrSummary['summary_json_path'] = $this->storageRelativePath((string)($arrSummary['summary_json_path'] ?? ''));
|
||||||
|
$arrSummary['summary_html_path'] = $this->storageRelativePath((string)($arrSummary['summary_html_path'] ?? ''));
|
||||||
|
$arrSummary['prompt_markdown_path'] = $this->storageRelativePath((string)($arrSummary['prompt_markdown_path'] ?? ''));
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'status' => 'success',
|
||||||
|
'summary' => $arrSummary,
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $Throwable) {
|
||||||
|
return $this->error('9999', '生成视频缺失字段工作台失败:' . $Throwable->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function metadataMissingWorkbenchRoot(): string
|
||||||
|
{
|
||||||
|
return rtrim((string)root_path(), '/') . '/storage/video-metadata-missing/workbench';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function metadataMissingWorkbenchRunRoot(): string
|
||||||
|
{
|
||||||
|
return $this->metadataMissingWorkbenchRoot() . '/runs';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function metadataMissingTaskPoolRoot(): string
|
||||||
|
{
|
||||||
|
return rtrim((string)root_path(), '/') . '/storage/video-metadata-missing/task-pool';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function storageRelativePath(string $strPath): string
|
||||||
|
{
|
||||||
|
$strStorageRoot = rtrim(str_replace('\\', '/', rtrim((string)root_path(), '/') . '/storage/'), '/');
|
||||||
|
$strPath = str_replace('\\', '/', $strPath);
|
||||||
|
if ($strPath !== '' && str_starts_with($strPath, $strStorageRoot . '/')) {
|
||||||
|
return substr($strPath, strlen($strStorageRoot . '/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $strPath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ class AdminCors extends AllowCrossDomain
|
|||||||
protected $header = [
|
protected $header = [
|
||||||
'Access-Control-Allow-Credentials' => 'true',
|
'Access-Control-Allow-Credentials' => 'true',
|
||||||
'Access-Control-Max-Age' => 3600,
|
'Access-Control-Max-Age' => 3600,
|
||||||
'Access-Control-Allow-Methods' => '*',
|
'Access-Control-Allow-Methods' => 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
|
||||||
'Access-Control-Allow-Headers' => '*',
|
'Access-Control-Allow-Headers' => 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With, admin-token',
|
||||||
|
'Vary' => 'Origin',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,17 +35,17 @@ class SeoCollector extends BaseController
|
|||||||
|
|
||||||
protected function summaryRoot(): string
|
protected function summaryRoot(): string
|
||||||
{
|
{
|
||||||
return $this->publicRoot() . '/_admin_templates/domain-seo-external/latest';
|
return $this->storageRoot() . '/external-seo/latest';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function snapshotRunRoot(): string
|
protected function snapshotRunRoot(): string
|
||||||
{
|
{
|
||||||
return $this->publicRoot() . '/_admin_templates/domain-seo-external/snapshot-runs';
|
return $this->storageRoot() . '/external-seo/snapshot-runs';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function snapshotValidateRunRoot(): string
|
protected function snapshotValidateRunRoot(): string
|
||||||
{
|
{
|
||||||
return $this->publicRoot() . '/_admin_templates/domain-seo-external/snapshot-validate-runs';
|
return $this->storageRoot() . '/external-seo/snapshot-validate-runs';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function spiderCrawlLatestRoot(): string
|
protected function spiderCrawlLatestRoot(): string
|
||||||
@@ -138,15 +138,15 @@ class SeoCollector extends BaseController
|
|||||||
$result = DomainExternalSeoSnapshotSchemaHelper::validateSnapshots((array)$snapshots);
|
$result = DomainExternalSeoSnapshotSchemaHelper::validateSnapshots((array)$snapshots);
|
||||||
$runRoot = DomainExternalSeoSnapshotValidateRunHelper::createRunRoot($this->snapshotValidateRunRoot(), 'snapshot_validate');
|
$runRoot = DomainExternalSeoSnapshotValidateRunHelper::createRunRoot($this->snapshotValidateRunRoot(), 'snapshot_validate');
|
||||||
$result = DomainExternalSeoSnapshotValidateRunHelper::writeRunArtifacts(
|
$result = DomainExternalSeoSnapshotValidateRunHelper::writeRunArtifacts(
|
||||||
$this->publicRoot(),
|
$this->storageRoot(),
|
||||||
$runRoot,
|
$runRoot,
|
||||||
$result,
|
$result,
|
||||||
(array)$snapshots
|
(array)$snapshots
|
||||||
);
|
);
|
||||||
$result['run_root'] = $this->publicRelativePath($runRoot);
|
$result['run_root'] = $this->storageRelativePath($runRoot);
|
||||||
$result['summary_json_path'] = $this->publicRelativePath((string)($result['summary_json_path'] ?? ''));
|
$result['summary_json_path'] = $this->storageRelativePath((string)($result['summary_json_path'] ?? ''));
|
||||||
$result['summary_html_path'] = $this->publicRelativePath((string)($result['summary_html_path'] ?? ''));
|
$result['summary_html_path'] = $this->storageRelativePath((string)($result['summary_html_path'] ?? ''));
|
||||||
$result['payload_json_path'] = $this->publicRelativePath((string)($result['payload_json_path'] ?? ''));
|
$result['payload_json_path'] = $this->storageRelativePath((string)($result['payload_json_path'] ?? ''));
|
||||||
|
|
||||||
return $this->success($result);
|
return $this->success($result);
|
||||||
} catch (\Throwable $throwable) {
|
} catch (\Throwable $throwable) {
|
||||||
@@ -166,37 +166,37 @@ class SeoCollector extends BaseController
|
|||||||
$result = DomainExternalSeoSnapshotHelper::ingestSnapshots((array)$snapshots, $source);
|
$result = DomainExternalSeoSnapshotHelper::ingestSnapshots((array)$snapshots, $source);
|
||||||
$runRoot = DomainExternalSeoSnapshotRunHelper::createRunRoot($this->snapshotRunRoot(), 'snapshot_push');
|
$runRoot = DomainExternalSeoSnapshotRunHelper::createRunRoot($this->snapshotRunRoot(), 'snapshot_push');
|
||||||
$result = DomainExternalSeoSnapshotRunHelper::writeRunArtifacts(
|
$result = DomainExternalSeoSnapshotRunHelper::writeRunArtifacts(
|
||||||
$this->publicRoot(),
|
$this->storageRoot(),
|
||||||
$runRoot,
|
$runRoot,
|
||||||
$result,
|
$result,
|
||||||
(array)$snapshots
|
(array)$snapshots
|
||||||
);
|
);
|
||||||
$result['run_root'] = $this->publicRelativePath($runRoot);
|
$result['run_root'] = $this->storageRelativePath($runRoot);
|
||||||
$result['summary_json_path'] = $this->publicRelativePath((string)($result['summary_json_path'] ?? ''));
|
$result['summary_json_path'] = $this->storageRelativePath((string)($result['summary_json_path'] ?? ''));
|
||||||
$result['summary_html_path'] = $this->publicRelativePath((string)($result['summary_html_path'] ?? ''));
|
$result['summary_html_path'] = $this->storageRelativePath((string)($result['summary_html_path'] ?? ''));
|
||||||
$result['payload_json_path'] = $this->publicRelativePath((string)($result['payload_json_path'] ?? ''));
|
$result['payload_json_path'] = $this->storageRelativePath((string)($result['payload_json_path'] ?? ''));
|
||||||
|
|
||||||
$overview = DomainExternalSeoCnOverviewHelper::buildOverview(14, 10);
|
$overview = DomainExternalSeoCnOverviewHelper::buildOverview(14, 10);
|
||||||
$overview = DomainExternalSeoCnOverviewHelper::writeArtifacts($this->summaryRoot(), $overview);
|
$overview = DomainExternalSeoCnOverviewHelper::writeArtifacts($this->summaryRoot(), $overview);
|
||||||
$overview['summary_json_path'] = $this->publicRelativePath((string)($overview['summary_json_path'] ?? ''));
|
$overview['summary_json_path'] = $this->storageRelativePath((string)($overview['summary_json_path'] ?? ''));
|
||||||
$overview['summary_html_path'] = $this->publicRelativePath((string)($overview['summary_html_path'] ?? ''));
|
$overview['summary_html_path'] = $this->storageRelativePath((string)($overview['summary_html_path'] ?? ''));
|
||||||
|
|
||||||
$snapshotTrend = DomainExternalSeoSnapshotTrendHelper::buildSummary($this->snapshotRunRoot(), 10);
|
$snapshotTrend = DomainExternalSeoSnapshotTrendHelper::buildSummary($this->snapshotRunRoot(), 10);
|
||||||
$snapshotTrend = DomainExternalSeoSnapshotTrendHelper::writeArtifacts($this->summaryRoot(), $snapshotTrend);
|
$snapshotTrend = DomainExternalSeoSnapshotTrendHelper::writeArtifacts($this->summaryRoot(), $snapshotTrend);
|
||||||
$snapshotTrend['summary_json_path'] = $this->publicRelativePath((string)($snapshotTrend['summary_json_path'] ?? ''));
|
$snapshotTrend['summary_json_path'] = $this->storageRelativePath((string)($snapshotTrend['summary_json_path'] ?? ''));
|
||||||
$snapshotTrend['summary_html_path'] = $this->publicRelativePath((string)($snapshotTrend['summary_html_path'] ?? ''));
|
$snapshotTrend['summary_html_path'] = $this->storageRelativePath((string)($snapshotTrend['summary_html_path'] ?? ''));
|
||||||
|
|
||||||
$result['post_ingest_overview'] = $overview;
|
$result['post_ingest_overview'] = $overview;
|
||||||
$result['post_ingest_snapshot_trend'] = $snapshotTrend;
|
$result['post_ingest_snapshot_trend'] = $snapshotTrend;
|
||||||
$result = DomainExternalSeoSnapshotRunHelper::writeRunArtifacts(
|
$result = DomainExternalSeoSnapshotRunHelper::writeRunArtifacts(
|
||||||
$this->publicRoot(),
|
$this->storageRoot(),
|
||||||
$runRoot,
|
$runRoot,
|
||||||
$result,
|
$result,
|
||||||
(array)$snapshots
|
(array)$snapshots
|
||||||
);
|
);
|
||||||
$result['summary_json_path'] = $this->publicRelativePath((string)($result['summary_json_path'] ?? ''));
|
$result['summary_json_path'] = $this->storageRelativePath((string)($result['summary_json_path'] ?? ''));
|
||||||
$result['summary_html_path'] = $this->publicRelativePath((string)($result['summary_html_path'] ?? ''));
|
$result['summary_html_path'] = $this->storageRelativePath((string)($result['summary_html_path'] ?? ''));
|
||||||
$result['payload_json_path'] = $this->publicRelativePath((string)($result['payload_json_path'] ?? ''));
|
$result['payload_json_path'] = $this->storageRelativePath((string)($result['payload_json_path'] ?? ''));
|
||||||
|
|
||||||
return $this->success($result);
|
return $this->success($result);
|
||||||
} catch (\Throwable $throwable) {
|
} catch (\Throwable $throwable) {
|
||||||
|
|||||||
50
code/app/command/DomainSpiderMdPlanTaskSeedCommand.php
Normal file
50
code/app/command/DomainSpiderMdPlanTaskSeedCommand.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\command;
|
||||||
|
|
||||||
|
use app\model\PlanTaskModel;
|
||||||
|
use think\console\Command;
|
||||||
|
use think\console\Input;
|
||||||
|
use think\console\Output;
|
||||||
|
|
||||||
|
class DomainSpiderMdPlanTaskSeedCommand extends Command
|
||||||
|
{
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$this->setName('plan:seed:domain-spider-md')
|
||||||
|
->setDescription('补齐蜘蛛池MD自动生成计划任务 GENERATE_DOMAIN_SPIDER_MD');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(Input $input, Output $output)
|
||||||
|
{
|
||||||
|
$strCode = 'GENERATE_DOMAIN_SPIDER_MD';
|
||||||
|
$arrData = [
|
||||||
|
'pt_name' => '蜘蛛池MD自动生成',
|
||||||
|
'pt_code' => $strCode,
|
||||||
|
'pt_enable' => 0,
|
||||||
|
'pt_limit' => 24 * 3600,
|
||||||
|
'pt_last_exec' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
$PlanTaskModel = PlanTaskModel::where('pt_code', $strCode)->find();
|
||||||
|
if ($PlanTaskModel instanceof PlanTaskModel) {
|
||||||
|
$output->writeln('计划任务已存在,无需重复创建:' . $strCode);
|
||||||
|
$output->writeln('pt_id:' . (int)$PlanTaskModel->pt_id);
|
||||||
|
$output->writeln('pt_name:' . (string)$PlanTaskModel->pt_name);
|
||||||
|
$output->writeln('pt_enable:' . (int)$PlanTaskModel->pt_enable);
|
||||||
|
$output->writeln('pt_limit:' . (int)$PlanTaskModel->pt_limit);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$intId = (int)PlanTaskModel::insertGetId($arrData);
|
||||||
|
$output->writeln('计划任务已创建:' . $strCode);
|
||||||
|
$output->writeln('pt_id:' . $intId);
|
||||||
|
$output->writeln('pt_name:' . $arrData['pt_name']);
|
||||||
|
$output->writeln('pt_enable:' . $arrData['pt_enable']);
|
||||||
|
$output->writeln('pt_limit:' . $arrData['pt_limit']);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
134
code/app/command/VideoMetadataAuditCommand.php
Normal file
134
code/app/command/VideoMetadataAuditCommand.php
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\command;
|
||||||
|
|
||||||
|
use app\model\VideoModel;
|
||||||
|
use think\console\Command;
|
||||||
|
use think\console\Input;
|
||||||
|
use think\console\input\Option;
|
||||||
|
use think\console\Output;
|
||||||
|
|
||||||
|
class VideoMetadataAuditCommand extends Command
|
||||||
|
{
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$this->setName('video:metadata:audit')
|
||||||
|
->addOption('sample', null, Option::VALUE_OPTIONAL, '抽样输出条数,默认 20', 20)
|
||||||
|
->setDescription('审计视频库缺失字段情况,并生成历史回填分析文件');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(Input $input, Output $output)
|
||||||
|
{
|
||||||
|
$intSample = max(1, (int)$input->getOption('sample'));
|
||||||
|
$arrSummary = VideoModel::getInstance()->buildMissingMetadataAuditSummary($intSample);
|
||||||
|
|
||||||
|
$strRoot = rtrim((string)root_path(), '/');
|
||||||
|
$strOutputRoot = $strRoot . '/storage/video-metadata-audit';
|
||||||
|
if (!is_dir($strOutputRoot)) {
|
||||||
|
@mkdir($strOutputRoot, 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$strJsonPath = $strOutputRoot . '/latest.json';
|
||||||
|
$strMarkdownPath = $strOutputRoot . '/latest.md';
|
||||||
|
|
||||||
|
file_put_contents($strJsonPath, json_encode($arrSummary, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . PHP_EOL);
|
||||||
|
file_put_contents($strMarkdownPath, $this->buildMarkdown($arrSummary));
|
||||||
|
|
||||||
|
$output->writeln('视频元数据缺失审计已生成');
|
||||||
|
$output->writeln('总视频数:' . (int)($arrSummary['total_videos'] ?? 0));
|
||||||
|
$output->writeln('存在任一缺失字段的视频数:' . (int)($arrSummary['videos_with_any_missing_metadata'] ?? 0));
|
||||||
|
$output->writeln('JSON:' . $strJsonPath);
|
||||||
|
$output->writeln('Markdown:' . $strMarkdownPath);
|
||||||
|
|
||||||
|
foreach ((array)($arrSummary['field_stats'] ?? []) as $arrField) {
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
'%s => 缺失 %d 条,占比 %.2f%%',
|
||||||
|
(string)($arrField['field'] ?? ''),
|
||||||
|
(int)($arrField['missing_count'] ?? 0),
|
||||||
|
((float)($arrField['missing_ratio'] ?? 0)) * 100
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildMarkdown(array $arrSummary): string
|
||||||
|
{
|
||||||
|
$arrLines = [];
|
||||||
|
$arrLines[] = '# 视频元数据缺失审计';
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '- 生成时间:' . (string)($arrSummary['generated_at'] ?? '');
|
||||||
|
$arrLines[] = '- 总视频数:' . (int)($arrSummary['total_videos'] ?? 0);
|
||||||
|
$arrLines[] = '- 任一缺失字段视频数:' . (int)($arrSummary['videos_with_any_missing_metadata'] ?? 0);
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '## 字段缺失统计';
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '| 字段 | 缺失数 | 缺失占比 | 建议动作 |';
|
||||||
|
$arrLines[] = '| --- | ---: | ---: | --- |';
|
||||||
|
|
||||||
|
foreach ((array)($arrSummary['field_stats'] ?? []) as $arrField) {
|
||||||
|
$strField = (string)($arrField['field'] ?? '');
|
||||||
|
$intMissingCount = (int)($arrField['missing_count'] ?? 0);
|
||||||
|
$floatRatio = ((float)($arrField['missing_ratio'] ?? 0)) * 100;
|
||||||
|
$arrLines[] = sprintf(
|
||||||
|
'| %s | %d | %.2f%% | %s |',
|
||||||
|
$strField,
|
||||||
|
$intMissingCount,
|
||||||
|
$floatRatio,
|
||||||
|
$this->buildSuggestedAction($strField)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '## 抽样样本';
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '| v_id | 片名 | 分类 | 年份 | 备注 | 缺失字段 | 更新时间 |';
|
||||||
|
$arrLines[] = '| ---: | --- | --- | --- | --- | --- | --- |';
|
||||||
|
|
||||||
|
foreach ((array)($arrSummary['samples'] ?? []) as $arrSample) {
|
||||||
|
$arrLines[] = sprintf(
|
||||||
|
'| %d | %s | %s | %s | %s | %s | %s |',
|
||||||
|
(int)($arrSample['v_id'] ?? 0),
|
||||||
|
$this->escapeMarkdown((string)($arrSample['v_name'] ?? '')),
|
||||||
|
$this->escapeMarkdown((string)($arrSample['v_category'] ?? '')),
|
||||||
|
$this->escapeMarkdown((string)($arrSample['v_year'] ?? '')),
|
||||||
|
$this->escapeMarkdown((string)($arrSample['v_remarks'] ?? '')),
|
||||||
|
$this->escapeMarkdown(implode(', ', (array)($arrSample['missing_fields'] ?? []))),
|
||||||
|
$this->escapeMarkdown((string)($arrSample['updated_at'] ?? ''))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '## 判读原则';
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '1. 演员、导演、年份、地区、语言这类结构化字段,优先靠重采、补源、人工校正,不能让 AI 猜。';
|
||||||
|
$arrLines[] = '2. 简介、备注这类文案型字段,如果事实基础足够,可以走 AI 生成并写回视频库,但必须只补空字段。';
|
||||||
|
$arrLines[] = '3. 后续采集器已接入“只补空字段、不覆盖已有值”的写库逻辑,新采到的有效字段会逐步沉淀进库。';
|
||||||
|
$arrLines[] = '';
|
||||||
|
|
||||||
|
return implode(PHP_EOL, $arrLines) . PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildSuggestedAction(string $strField): string
|
||||||
|
{
|
||||||
|
return match ($strField) {
|
||||||
|
'v_actor', 'v_director', 'v_year', 'v_lang', 'v_lang_en', 'v_area', 'v_area_en', 'v_publish_date'
|
||||||
|
=> '优先重采或补源,不建议 AI 猜测',
|
||||||
|
'v_description', 'v_remarks'
|
||||||
|
=> '可在事实边界内走 AI 补全并写库',
|
||||||
|
default => '先审计,再决定重采或补写',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function escapeMarkdown(string $strValue): string
|
||||||
|
{
|
||||||
|
$strValue = trim($strValue);
|
||||||
|
if ($strValue === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return str_replace('|', '\\|', $strValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
109
code/app/command/VideoMetadataCopyFillCommand.php
Normal file
109
code/app/command/VideoMetadataCopyFillCommand.php
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\command;
|
||||||
|
|
||||||
|
use app\common\helper\VideoMetadataCopyFillHelper;
|
||||||
|
use app\model\VideoModel;
|
||||||
|
use think\console\Command;
|
||||||
|
use think\console\Input;
|
||||||
|
use think\console\input\Option;
|
||||||
|
use think\console\Output;
|
||||||
|
|
||||||
|
class VideoMetadataCopyFillCommand extends Command
|
||||||
|
{
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$this->setName('video:metadata:fill-copy')
|
||||||
|
->addOption('limit', null, Option::VALUE_OPTIONAL, '本次最多处理多少条,默认 500', 500)
|
||||||
|
->addOption('dry-run', null, Option::VALUE_NONE, '只预览,不写库')
|
||||||
|
->setDescription('批量补写视频库里为空的简介和备注,只补空字段');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(Input $input, Output $output)
|
||||||
|
{
|
||||||
|
$intLimit = max(1, min((int)$input->getOption('limit'), 5000));
|
||||||
|
$boolDryRun = (bool)$input->getOption('dry-run');
|
||||||
|
$VideoModel = VideoModel::getInstance();
|
||||||
|
|
||||||
|
$Cursor = $VideoModel->getCol()->find(
|
||||||
|
[
|
||||||
|
'$or' => [
|
||||||
|
['v_description' => ['$exists' => false]],
|
||||||
|
['v_description' => ''],
|
||||||
|
['v_description' => null],
|
||||||
|
['v_remarks' => ['$exists' => false]],
|
||||||
|
['v_remarks' => ''],
|
||||||
|
['v_remarks' => null],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'limit' => $intLimit,
|
||||||
|
'sort' => ['updated_at' => -1, 'v_id' => -1],
|
||||||
|
'projection' => [
|
||||||
|
'_id' => 0,
|
||||||
|
'v_id' => 1,
|
||||||
|
'v_name' => 1,
|
||||||
|
'v_category' => 1,
|
||||||
|
'v_parent_category' => 1,
|
||||||
|
'v_year' => 1,
|
||||||
|
'v_area' => 1,
|
||||||
|
'v_lang' => 1,
|
||||||
|
'v_director' => 1,
|
||||||
|
'v_actor' => 1,
|
||||||
|
'v_remarks' => 1,
|
||||||
|
'v_description' => 1,
|
||||||
|
'v_isend' => 1,
|
||||||
|
'v_publish_date' => 1,
|
||||||
|
],
|
||||||
|
'typeMap' => VideoModel::$arrOptions['typeMap'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$intScanned = 0;
|
||||||
|
$intUpdated = 0;
|
||||||
|
$arrSamples = [];
|
||||||
|
|
||||||
|
foreach ($Cursor as $arrVideo) {
|
||||||
|
$intScanned++;
|
||||||
|
$arrUpdate = VideoMetadataCopyFillHelper::buildFillPayload((array)$arrVideo, 'local_copy_fill_command');
|
||||||
|
if (empty($arrUpdate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrSamples[] = [
|
||||||
|
'v_id' => (int)($arrVideo['v_id'] ?? 0),
|
||||||
|
'v_name' => (string)($arrVideo['v_name'] ?? ''),
|
||||||
|
'filled_fields' => (array)($arrUpdate['v_metadata_fill']['filled_fields'] ?? []),
|
||||||
|
'generated_description' => (string)($arrUpdate['v_description'] ?? ''),
|
||||||
|
'generated_remarks' => (string)($arrUpdate['v_remarks'] ?? ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!$boolDryRun) {
|
||||||
|
$VideoModel->updateOne(
|
||||||
|
['v_id' => (int)($arrVideo['v_id'] ?? 0)],
|
||||||
|
['$set' => $arrUpdate]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$intUpdated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$output->writeln('视频文案型元数据补写完成');
|
||||||
|
$output->writeln('模式:' . ($boolDryRun ? 'dry-run' : 'write'));
|
||||||
|
$output->writeln('扫描条数:' . $intScanned);
|
||||||
|
$output->writeln('命中可补写条数:' . $intUpdated);
|
||||||
|
|
||||||
|
foreach (array_slice($arrSamples, 0, 5) as $arrSample) {
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
'#%d %s => %s',
|
||||||
|
(int)($arrSample['v_id'] ?? 0),
|
||||||
|
(string)($arrSample['v_name'] ?? ''),
|
||||||
|
implode(', ', (array)($arrSample['filled_fields'] ?? []))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
code/app/command/VideoMetadataMissingPlanTaskSeedCommand.php
Normal file
50
code/app/command/VideoMetadataMissingPlanTaskSeedCommand.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\command;
|
||||||
|
|
||||||
|
use app\model\PlanTaskModel;
|
||||||
|
use think\console\Command;
|
||||||
|
use think\console\Input;
|
||||||
|
use think\console\Output;
|
||||||
|
|
||||||
|
class VideoMetadataMissingPlanTaskSeedCommand extends Command
|
||||||
|
{
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$this->setName('plan:seed:video-metadata-missing')
|
||||||
|
->setDescription('补齐视频缺字段任务池计划任务 REFRESH_VIDEO_METADATA_TASK_POOL');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(Input $input, Output $output)
|
||||||
|
{
|
||||||
|
$strCode = 'REFRESH_VIDEO_METADATA_TASK_POOL';
|
||||||
|
$arrData = [
|
||||||
|
'pt_name' => '视频缺字段任务池刷新',
|
||||||
|
'pt_code' => $strCode,
|
||||||
|
'pt_enable' => 0,
|
||||||
|
'pt_limit' => 24 * 3600,
|
||||||
|
'pt_last_exec' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
$PlanTaskModel = PlanTaskModel::where('pt_code', $strCode)->find();
|
||||||
|
if ($PlanTaskModel instanceof PlanTaskModel) {
|
||||||
|
$output->writeln('计划任务已存在,无需重复创建:' . $strCode);
|
||||||
|
$output->writeln('pt_id:' . (int)$PlanTaskModel->pt_id);
|
||||||
|
$output->writeln('pt_name:' . (string)$PlanTaskModel->pt_name);
|
||||||
|
$output->writeln('pt_enable:' . (int)$PlanTaskModel->pt_enable);
|
||||||
|
$output->writeln('pt_limit:' . (int)$PlanTaskModel->pt_limit);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$intId = (int)PlanTaskModel::insertGetId($arrData);
|
||||||
|
$output->writeln('计划任务已创建:' . $strCode);
|
||||||
|
$output->writeln('pt_id:' . $intId);
|
||||||
|
$output->writeln('pt_name:' . $arrData['pt_name']);
|
||||||
|
$output->writeln('pt_enable:' . $arrData['pt_enable']);
|
||||||
|
$output->writeln('pt_limit:' . $arrData['pt_limit']);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
55
code/app/command/VideoMetadataMissingTaskPoolCommand.php
Normal file
55
code/app/command/VideoMetadataMissingTaskPoolCommand.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\command;
|
||||||
|
|
||||||
|
use app\common\helper\VideoMetadataMissingRefreshHelper;
|
||||||
|
use think\console\Command;
|
||||||
|
use think\console\Input;
|
||||||
|
use think\console\input\Option;
|
||||||
|
use think\console\Output;
|
||||||
|
|
||||||
|
class VideoMetadataMissingTaskPoolCommand extends Command
|
||||||
|
{
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$this->setName('video:metadata:task-pool')
|
||||||
|
->addOption('sample', null, Option::VALUE_OPTIONAL, '缺失字段样本数,默认 20', 20)
|
||||||
|
->addOption('queue-limit', null, Option::VALUE_OPTIONAL, '重采优先队列条数,默认 100', 100)
|
||||||
|
->addOption('prompt-limit', null, Option::VALUE_OPTIONAL, 'Codex 派单样本数,默认 20', 20)
|
||||||
|
->addOption('batch-size', null, Option::VALUE_OPTIONAL, '每批数量,默认 20', 20)
|
||||||
|
->addOption('batch-limit', null, Option::VALUE_OPTIONAL, '最大批次数,默认 10', 10)
|
||||||
|
->setDescription('生成视频缺字段任务池产物,供后台展示、Codex 接手和计划任务挂接');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(Input $input, Output $output)
|
||||||
|
{
|
||||||
|
$intSample = max(1, min(100, (int)$input->getOption('sample')));
|
||||||
|
$intQueueLimit = max(1, min(500, (int)$input->getOption('queue-limit')));
|
||||||
|
$intPromptLimit = max(1, min(100, (int)$input->getOption('prompt-limit')));
|
||||||
|
$intBatchSize = max(5, min(100, (int)$input->getOption('batch-size')));
|
||||||
|
$intBatchLimit = max(1, min(50, (int)$input->getOption('batch-limit')));
|
||||||
|
|
||||||
|
$arrResult = VideoMetadataMissingRefreshHelper::refresh([
|
||||||
|
'sample' => $intSample,
|
||||||
|
'queue_limit' => $intQueueLimit,
|
||||||
|
'prompt_limit' => $intPromptLimit,
|
||||||
|
'batch_size' => $intBatchSize,
|
||||||
|
'batch_limit' => $intBatchLimit,
|
||||||
|
]);
|
||||||
|
$arrWorkbenchSummary = (array)($arrResult['workbench'] ?? []);
|
||||||
|
$arrTaskPoolSummary = (array)($arrResult['task_pool'] ?? []);
|
||||||
|
|
||||||
|
$output->writeln('视频缺字段任务池已生成');
|
||||||
|
$output->writeln('工作台缺字段视频数:' . (int)(($arrWorkbenchSummary['audit'] ?? [])['videos_with_any_missing_metadata'] ?? 0));
|
||||||
|
$output->writeln('工作台重采优先队列:' . (int)(($arrWorkbenchSummary['queue'] ?? [])['queue_size'] ?? 0));
|
||||||
|
$output->writeln('任务池批次数:' . count((array)($arrTaskPoolSummary['batches'] ?? [])));
|
||||||
|
$output->writeln('工作台 JSON:' . (string)($arrWorkbenchSummary['summary_json_path'] ?? ''));
|
||||||
|
$output->writeln('工作台 HTML:' . (string)($arrWorkbenchSummary['summary_html_path'] ?? ''));
|
||||||
|
$output->writeln('任务池 JSON:' . (string)($arrTaskPoolSummary['summary_json_path'] ?? ''));
|
||||||
|
$output->writeln('任务池 HTML:' . (string)($arrTaskPoolSummary['summary_html_path'] ?? ''));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
code/app/command/VideoMetadataMissingWorkbenchCommand.php
Normal file
43
code/app/command/VideoMetadataMissingWorkbenchCommand.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\command;
|
||||||
|
|
||||||
|
use app\common\helper\VideoMetadataMissingWorkbenchHelper;
|
||||||
|
use think\console\Command;
|
||||||
|
use think\console\Input;
|
||||||
|
use think\console\input\Option;
|
||||||
|
use think\console\Output;
|
||||||
|
|
||||||
|
class VideoMetadataMissingWorkbenchCommand extends Command
|
||||||
|
{
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$this->setName('video:metadata:workbench')
|
||||||
|
->addOption('sample', null, Option::VALUE_OPTIONAL, '缺失字段样本数,默认 20', 20)
|
||||||
|
->addOption('queue-limit', null, Option::VALUE_OPTIONAL, '重采优先队列条数,默认 100', 100)
|
||||||
|
->addOption('prompt-limit', null, Option::VALUE_OPTIONAL, 'Codex 派单样本数,默认 20', 20)
|
||||||
|
->setDescription('生成视频缺失字段工作台产物,供后台和 Codex 派单使用');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(Input $input, Output $output)
|
||||||
|
{
|
||||||
|
$intSample = max(1, min(100, (int)$input->getOption('sample')));
|
||||||
|
$intQueueLimit = max(1, min(500, (int)$input->getOption('queue-limit')));
|
||||||
|
$intPromptLimit = max(1, min(100, (int)$input->getOption('prompt-limit')));
|
||||||
|
$strOutputRoot = rtrim((string)root_path(), '/') . '/storage/video-metadata-missing/workbench';
|
||||||
|
|
||||||
|
$arrSummary = VideoMetadataMissingWorkbenchHelper::buildSummary($intSample, $intQueueLimit, $intPromptLimit);
|
||||||
|
$arrSummary = VideoMetadataMissingWorkbenchHelper::writeArtifacts($strOutputRoot, $arrSummary);
|
||||||
|
|
||||||
|
$output->writeln('视频缺失字段工作台已生成');
|
||||||
|
$output->writeln('存在缺字段视频数:' . (int)(($arrSummary['audit'] ?? [])['videos_with_any_missing_metadata'] ?? 0));
|
||||||
|
$output->writeln('重采优先队列:' . (int)(($arrSummary['queue'] ?? [])['queue_size'] ?? 0));
|
||||||
|
$output->writeln('JSON:' . (string)($arrSummary['summary_json_path'] ?? ''));
|
||||||
|
$output->writeln('HTML:' . (string)($arrSummary['summary_html_path'] ?? ''));
|
||||||
|
$output->writeln('Prompt:' . (string)($arrSummary['prompt_markdown_path'] ?? ''));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
106
code/app/command/VideoMetadataRecrawlQueueCommand.php
Normal file
106
code/app/command/VideoMetadataRecrawlQueueCommand.php
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\command;
|
||||||
|
|
||||||
|
use app\model\VideoModel;
|
||||||
|
use think\console\Command;
|
||||||
|
use think\console\Input;
|
||||||
|
use think\console\input\Option;
|
||||||
|
use think\console\Output;
|
||||||
|
|
||||||
|
class VideoMetadataRecrawlQueueCommand extends Command
|
||||||
|
{
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$this->setName('video:metadata:recrawl-queue')
|
||||||
|
->addOption('limit', null, Option::VALUE_OPTIONAL, '输出多少条重采优先项,默认 200', 200)
|
||||||
|
->setDescription('生成演员/导演缺失的视频重采优先队列');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(Input $input, Output $output)
|
||||||
|
{
|
||||||
|
$intLimit = max(1, min((int)$input->getOption('limit'), 5000));
|
||||||
|
$arrSummary = VideoModel::getInstance()->buildRecrawlPriorityQueueSummary($intLimit);
|
||||||
|
|
||||||
|
$strRoot = rtrim((string)root_path(), '/');
|
||||||
|
$strOutputRoot = $strRoot . '/storage/video-metadata-recrawl-queue';
|
||||||
|
if (!is_dir($strOutputRoot)) {
|
||||||
|
@mkdir($strOutputRoot, 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$strJsonPath = $strOutputRoot . '/latest.json';
|
||||||
|
$strMarkdownPath = $strOutputRoot . '/latest.md';
|
||||||
|
|
||||||
|
file_put_contents($strJsonPath, json_encode($arrSummary, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . PHP_EOL);
|
||||||
|
file_put_contents($strMarkdownPath, $this->buildMarkdown($arrSummary));
|
||||||
|
|
||||||
|
$output->writeln('视频元数据重采优先队列已生成');
|
||||||
|
$output->writeln('队列条数:' . (int)($arrSummary['queue_size'] ?? 0));
|
||||||
|
$output->writeln('JSON:' . $strJsonPath);
|
||||||
|
$output->writeln('Markdown:' . $strMarkdownPath);
|
||||||
|
|
||||||
|
foreach (array_slice((array)($arrSummary['items'] ?? []), 0, 10) as $arrItem) {
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
'#%d %s => score:%d | %s',
|
||||||
|
(int)($arrItem['v_id'] ?? 0),
|
||||||
|
(string)($arrItem['v_name'] ?? ''),
|
||||||
|
(int)($arrItem['priority_score'] ?? 0),
|
||||||
|
(string)($arrItem['priority_reason'] ?? '')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildMarkdown(array $arrSummary): string
|
||||||
|
{
|
||||||
|
$arrLines = [];
|
||||||
|
$arrLines[] = '# 视频元数据重采优先队列';
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '- 生成时间:' . (string)($arrSummary['generated_at'] ?? '');
|
||||||
|
$arrLines[] = '- 队列条数:' . (int)($arrSummary['queue_size'] ?? 0);
|
||||||
|
$arrLines[] = '- 当前规则:优先处理演员 / 导演缺失且已有访问信号、仍在更新、多线路可交叉重采的视频';
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '| 优先分 | v_id | 片名 | 分类 | 缺失字段 | 周点击 | 总点击 | 播放源数 | 更新时间 | 优先原因 |';
|
||||||
|
$arrLines[] = '| ---: | ---: | --- | --- | --- | ---: | ---: | ---: | --- | --- |';
|
||||||
|
|
||||||
|
foreach ((array)($arrSummary['items'] ?? []) as $arrItem) {
|
||||||
|
$arrLines[] = sprintf(
|
||||||
|
'| %d | %d | %s | %s | %s | %d | %d | %d | %s | %s |',
|
||||||
|
(int)($arrItem['priority_score'] ?? 0),
|
||||||
|
(int)($arrItem['v_id'] ?? 0),
|
||||||
|
$this->escapeMarkdown((string)($arrItem['v_name'] ?? '')),
|
||||||
|
$this->escapeMarkdown((string)($arrItem['v_category'] ?? '')),
|
||||||
|
$this->escapeMarkdown(implode(', ', (array)($arrItem['missing_fields'] ?? []))),
|
||||||
|
(int)(($arrItem['click_stats'] ?? [])['weekly'] ?? 0),
|
||||||
|
(int)(($arrItem['click_stats'] ?? [])['total'] ?? 0),
|
||||||
|
(int)($arrItem['play_source_count'] ?? 0),
|
||||||
|
$this->escapeMarkdown((string)($arrItem['updated_at'] ?? '')),
|
||||||
|
$this->escapeMarkdown((string)($arrItem['priority_reason'] ?? ''))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '## 执行建议';
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '1. 先处理前 50 条高分项,优先尝试从已有多播放源重新抓取演员/导演。';
|
||||||
|
$arrLines[] = '2. 如果多源都拿不到,再转人工补源或定向接第三方资料源。';
|
||||||
|
$arrLines[] = '3. 不允许用 AI 猜演员/导演;结构化事实字段只能靠真实来源补齐。';
|
||||||
|
$arrLines[] = '4. 每次重采后,复跑 `php think video:metadata:audit` 看缺口是否下降。';
|
||||||
|
$arrLines[] = '';
|
||||||
|
|
||||||
|
return implode(PHP_EOL, $arrLines) . PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function escapeMarkdown(string $strValue): string
|
||||||
|
{
|
||||||
|
$strValue = trim($strValue);
|
||||||
|
if ($strValue === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return str_replace('|', '\\|', $strValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
344
code/app/command/VideoMetadataRecrawlRunCommand.php
Normal file
344
code/app/command/VideoMetadataRecrawlRunCommand.php
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\command;
|
||||||
|
|
||||||
|
use app\model\VideoModel;
|
||||||
|
use app\task\crawler\douban\page\Site as DoubanSite;
|
||||||
|
use app\task\crawler\youzhi\page\Site as YouzhiSite;
|
||||||
|
use think\console\Command;
|
||||||
|
use think\console\Input;
|
||||||
|
use think\console\input\Option;
|
||||||
|
use think\console\Output;
|
||||||
|
|
||||||
|
class VideoMetadataRecrawlRunCommand extends Command
|
||||||
|
{
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$this->setName('video:metadata:recrawl-run')
|
||||||
|
->addOption('limit', null, Option::VALUE_OPTIONAL, '默认按重采队列取多少条,默认 10', 10)
|
||||||
|
->addOption('v-ids', null, Option::VALUE_OPTIONAL, '指定 v_id,多个逗号分隔;指定后优先按 v_id 执行', '')
|
||||||
|
->addOption('sources', null, Option::VALUE_OPTIONAL, '强制指定源站,多个逗号分隔,如 douban,youzhi', '')
|
||||||
|
->addOption('dry-run', null, Option::VALUE_NONE, '只搜索和匹配,不真正回写')
|
||||||
|
->setDescription('按视频元数据重采队列执行演员/导演定向重采,并生成执行结果报告');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(Input $input, Output $output)
|
||||||
|
{
|
||||||
|
$intLimit = max(1, min((int)$input->getOption('limit'), 100));
|
||||||
|
$arrForcedVIds = $this->parseIntList((string)$input->getOption('v-ids'));
|
||||||
|
$arrForcedSources = $this->parseSourceList((string)$input->getOption('sources'));
|
||||||
|
$boolDryRun = (bool)$input->getOption('dry-run');
|
||||||
|
|
||||||
|
$arrCandidates = $this->resolveCandidates($arrForcedVIds, $intLimit);
|
||||||
|
|
||||||
|
if (empty($arrCandidates)) {
|
||||||
|
$output->writeln('没有可执行的候选视频');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrSummary = [
|
||||||
|
'generated_at' => date(DATE_ATOM),
|
||||||
|
'mode' => !empty($arrForcedVIds) ? 'explicit_v_ids' : 'queue',
|
||||||
|
'dry_run' => $boolDryRun,
|
||||||
|
'limit' => $intLimit,
|
||||||
|
'requested_v_ids' => $arrForcedVIds,
|
||||||
|
'forced_sources' => $arrForcedSources,
|
||||||
|
'attempted_videos' => count($arrCandidates),
|
||||||
|
'success_actor_count' => 0,
|
||||||
|
'success_director_count' => 0,
|
||||||
|
'success_both_count' => 0,
|
||||||
|
'failed_both_count' => 0,
|
||||||
|
'items' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($arrCandidates as $arrCandidate) {
|
||||||
|
$arrItemSummary = $this->processCandidate($arrCandidate, $arrForcedSources, $boolDryRun);
|
||||||
|
$arrSummary['items'][] = $arrItemSummary;
|
||||||
|
|
||||||
|
if (!empty($arrItemSummary['actor_filled'])) {
|
||||||
|
$arrSummary['success_actor_count']++;
|
||||||
|
}
|
||||||
|
if (!empty($arrItemSummary['director_filled'])) {
|
||||||
|
$arrSummary['success_director_count']++;
|
||||||
|
}
|
||||||
|
if (!empty($arrItemSummary['actor_filled']) && !empty($arrItemSummary['director_filled'])) {
|
||||||
|
$arrSummary['success_both_count']++;
|
||||||
|
}
|
||||||
|
if (empty($arrItemSummary['actor_filled']) && empty($arrItemSummary['director_filled'])) {
|
||||||
|
$arrSummary['failed_both_count']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
'#%d %s | actor:%s | director:%s | sources:%s',
|
||||||
|
(int)$arrItemSummary['v_id'],
|
||||||
|
(string)$arrItemSummary['v_name'],
|
||||||
|
!empty($arrItemSummary['actor_filled']) ? 'filled' : 'no',
|
||||||
|
!empty($arrItemSummary['director_filled']) ? 'filled' : 'no',
|
||||||
|
implode(',', array_column((array)$arrItemSummary['source_runs'], 'source'))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$strRoot = rtrim((string)root_path(), '/');
|
||||||
|
$strOutputRoot = $strRoot . '/storage/video-metadata-recrawl-run';
|
||||||
|
if (!is_dir($strOutputRoot)) {
|
||||||
|
@mkdir($strOutputRoot, 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$strJsonPath = $strOutputRoot . '/latest.json';
|
||||||
|
$strMarkdownPath = $strOutputRoot . '/latest.md';
|
||||||
|
|
||||||
|
file_put_contents($strJsonPath, json_encode($arrSummary, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . PHP_EOL);
|
||||||
|
file_put_contents($strMarkdownPath, $this->buildMarkdown($arrSummary));
|
||||||
|
|
||||||
|
$output->writeln('视频元数据定向重采报告已生成');
|
||||||
|
$output->writeln('JSON:' . $strJsonPath);
|
||||||
|
$output->writeln('Markdown:' . $strMarkdownPath);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function resolveCandidates(array $arrForcedVIds, int $intLimit): array
|
||||||
|
{
|
||||||
|
if (!empty($arrForcedVIds)) {
|
||||||
|
$arrCandidates = [];
|
||||||
|
foreach ($arrForcedVIds as $intVId) {
|
||||||
|
$arrVideo = VideoModel::getInstance()->getVideoByVId($intVId);
|
||||||
|
if (empty($arrVideo)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrPlaySources = array_values(array_filter(array_map('trim', array_keys((array)($arrVideo['v_play_url'] ?? [])))));
|
||||||
|
$arrCandidates[] = [
|
||||||
|
'v_id' => (int)($arrVideo['v_id'] ?? 0),
|
||||||
|
'v_name' => (string)($arrVideo['v_name'] ?? ''),
|
||||||
|
'v_name_en' => (string)($arrVideo['v_name_en'] ?? ''),
|
||||||
|
'play_sources' => $arrPlaySources,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $arrCandidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (array)(VideoModel::getInstance()->buildRecrawlPriorityQueueSummary($intLimit)['items'] ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function processCandidate(array $arrCandidate, array $arrForcedSources, bool $boolDryRun): array
|
||||||
|
{
|
||||||
|
$intVId = (int)($arrCandidate['v_id'] ?? 0);
|
||||||
|
$strVName = trim((string)($arrCandidate['v_name'] ?? ''));
|
||||||
|
$strVNameEn = trim((string)($arrCandidate['v_name_en'] ?? ''));
|
||||||
|
|
||||||
|
$arrBefore = VideoModel::getInstance()->getVideoByVId($intVId) ?? [];
|
||||||
|
$arrSourceRuns = [];
|
||||||
|
|
||||||
|
$arrSources = !empty($arrForcedSources)
|
||||||
|
? $arrForcedSources
|
||||||
|
: $this->parseSourceList(implode(',', (array)($arrCandidate['play_sources'] ?? [])));
|
||||||
|
|
||||||
|
foreach ($arrSources as $strSource) {
|
||||||
|
$arrSearchResult = $this->searchSourceVideo($strSource, $strVName, $strVNameEn);
|
||||||
|
|
||||||
|
$arrSourceRun = [
|
||||||
|
'source' => $strSource,
|
||||||
|
'matched' => !empty($arrSearchResult),
|
||||||
|
'matched_vod_id' => (int)($arrSearchResult['vod_id'] ?? 0),
|
||||||
|
'matched_vod_name' => (string)($arrSearchResult['vod_name'] ?? ''),
|
||||||
|
'status' => 'no_match',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (empty($arrSearchResult)) {
|
||||||
|
$arrSourceRuns[] = $arrSourceRun;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($boolDryRun) {
|
||||||
|
$arrSourceRun['status'] = 'matched_dry_run';
|
||||||
|
$arrSourceRuns[] = $arrSourceRun;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$boolSaved = $this->fetchAndSaveSourceVideo($strSource, (int)$arrSearchResult['vod_id']);
|
||||||
|
$arrSourceRun['status'] = $boolSaved ? 'saved' : 'fetch_failed';
|
||||||
|
$arrSourceRuns[] = $arrSourceRun;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrAfter = VideoModel::getInstance()->getVideoByVId($intVId) ?? [];
|
||||||
|
|
||||||
|
$VideoModel = VideoModel::getInstance();
|
||||||
|
$boolActorBefore = $VideoModel->isFieldMetadataMissing('v_actor', $arrBefore['v_actor'] ?? null);
|
||||||
|
$boolDirectorBefore = $VideoModel->isFieldMetadataMissing('v_director', $arrBefore['v_director'] ?? null);
|
||||||
|
$boolActorAfter = $VideoModel->isFieldMetadataMissing('v_actor', $arrAfter['v_actor'] ?? null);
|
||||||
|
$boolDirectorAfter = $VideoModel->isFieldMetadataMissing('v_director', $arrAfter['v_director'] ?? null);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'v_id' => $intVId,
|
||||||
|
'v_name' => $strVName,
|
||||||
|
'sources_requested' => $arrSources,
|
||||||
|
'actor_filled' => $boolActorBefore && !$boolActorAfter,
|
||||||
|
'director_filled' => $boolDirectorBefore && !$boolDirectorAfter,
|
||||||
|
'before' => [
|
||||||
|
'actor_empty' => $boolActorBefore,
|
||||||
|
'director_empty' => $boolDirectorBefore,
|
||||||
|
],
|
||||||
|
'after' => [
|
||||||
|
'actor_empty' => $boolActorAfter,
|
||||||
|
'director_empty' => $boolDirectorAfter,
|
||||||
|
],
|
||||||
|
'source_runs' => $arrSourceRuns,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function searchSourceVideo(string $strSource, string $strVName, string $strVNameEn = ''): array
|
||||||
|
{
|
||||||
|
$Site = $this->makeSite($strSource);
|
||||||
|
$strUri = $this->buildSearchUri($strSource, $strVName);
|
||||||
|
|
||||||
|
$Response = $Site->getClient()->get($strUri);
|
||||||
|
$strContent = (string)$Response->getBody()->getContents();
|
||||||
|
$arrContent = json_decode($strContent, true);
|
||||||
|
$arrList = (array)($arrContent['list'] ?? []);
|
||||||
|
|
||||||
|
if (empty($arrList)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$strTargetName = $this->normalizeTitle($strVName);
|
||||||
|
$strTargetNameEn = strtolower(trim($strVNameEn));
|
||||||
|
|
||||||
|
foreach ($arrList as $arrItem) {
|
||||||
|
$strVodName = $this->normalizeTitle((string)($arrItem['vod_name'] ?? ''));
|
||||||
|
$strVodNameEn = strtolower(trim((string)($arrItem['vod_en'] ?? '')));
|
||||||
|
|
||||||
|
if ($strVodName !== '' && $strVodName === $strTargetName) {
|
||||||
|
return (array)$arrItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($strTargetNameEn !== '' && $strVodNameEn !== '' && $strVodNameEn === $strTargetNameEn) {
|
||||||
|
return (array)$arrItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (array)$arrList[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function fetchAndSaveSourceVideo(string $strSource, int $intVodId): bool
|
||||||
|
{
|
||||||
|
if ($intVodId <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$Site = $this->makeSite($strSource);
|
||||||
|
$arrPages = $Site->getVideoInfoPageList([
|
||||||
|
['v_source_id' => $intVodId],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (empty($arrPages)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$boolSaved = false;
|
||||||
|
foreach ($arrPages as $VideoInfoPage) {
|
||||||
|
$boolSaved = $VideoInfoPage->saveVideo() || $boolSaved;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $boolSaved;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function makeSite(string $strSource)
|
||||||
|
{
|
||||||
|
return match ($strSource) {
|
||||||
|
'douban' => new DoubanSite(),
|
||||||
|
'youzhi' => new YouzhiSite(),
|
||||||
|
default => throw new \InvalidArgumentException('不支持的源站:' . $strSource),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildSearchUri(string $strSource, string $strKeyword): string
|
||||||
|
{
|
||||||
|
$strKeyword = urlencode($strKeyword);
|
||||||
|
|
||||||
|
return match ($strSource) {
|
||||||
|
'douban' => '/api.php/provide/vod/at/josn?ac=list&wd=' . $strKeyword,
|
||||||
|
'youzhi' => '/inc/api_mac10.php?ac=list&wd=' . $strKeyword,
|
||||||
|
default => throw new \InvalidArgumentException('不支持的源站:' . $strSource),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function parseIntList(string $strValue): array
|
||||||
|
{
|
||||||
|
return array_values(array_filter(array_map(static function (string $strItem): int {
|
||||||
|
return (int)trim($strItem);
|
||||||
|
}, explode(',', $strValue)), static function (int $intValue): bool {
|
||||||
|
return $intValue > 0;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function parseSourceList(string $strValue): array
|
||||||
|
{
|
||||||
|
$arrAllowed = ['douban', 'youzhi'];
|
||||||
|
|
||||||
|
return array_values(array_filter(array_unique(array_map(static function (string $strItem): string {
|
||||||
|
return trim($strItem);
|
||||||
|
}, explode(',', $strValue))), static function (string $strItem) use ($arrAllowed): bool {
|
||||||
|
return in_array($strItem, $arrAllowed, true);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function normalizeTitle(string $strValue): string
|
||||||
|
{
|
||||||
|
$strValue = trim(mb_strtolower($strValue));
|
||||||
|
if ($strValue === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return preg_replace('/[\s\p{P}\p{S}]+/u', '', $strValue) ?? $strValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildMarkdown(array $arrSummary): string
|
||||||
|
{
|
||||||
|
$arrLines = [];
|
||||||
|
$arrLines[] = '# 视频元数据定向重采执行结果';
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '- 生成时间:' . (string)($arrSummary['generated_at'] ?? '');
|
||||||
|
$arrLines[] = '- 模式:' . (string)($arrSummary['mode'] ?? '');
|
||||||
|
$arrLines[] = '- Dry Run:' . (!empty($arrSummary['dry_run']) ? 'yes' : 'no');
|
||||||
|
$arrLines[] = '- 处理条数:' . (int)($arrSummary['attempted_videos'] ?? 0);
|
||||||
|
$arrLines[] = '- 演员补齐条数:' . (int)($arrSummary['success_actor_count'] ?? 0);
|
||||||
|
$arrLines[] = '- 导演补齐条数:' . (int)($arrSummary['success_director_count'] ?? 0);
|
||||||
|
$arrLines[] = '- 双字段都补齐:' . (int)($arrSummary['success_both_count'] ?? 0);
|
||||||
|
$arrLines[] = '- 双字段都未补齐:' . (int)($arrSummary['failed_both_count'] ?? 0);
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '| v_id | 片名 | actor补齐 | director补齐 | 源站执行摘要 |';
|
||||||
|
$arrLines[] = '| ---: | --- | --- | --- | --- |';
|
||||||
|
|
||||||
|
foreach ((array)($arrSummary['items'] ?? []) as $arrItem) {
|
||||||
|
$arrSourceSummary = array_map(static function (array $arrRun): string {
|
||||||
|
return sprintf(
|
||||||
|
'%s:%s%s',
|
||||||
|
(string)($arrRun['source'] ?? ''),
|
||||||
|
(string)($arrRun['status'] ?? ''),
|
||||||
|
!empty($arrRun['matched_vod_id']) ? '#' . (int)$arrRun['matched_vod_id'] : ''
|
||||||
|
);
|
||||||
|
}, (array)($arrItem['source_runs'] ?? []));
|
||||||
|
|
||||||
|
$arrLines[] = sprintf(
|
||||||
|
'| %d | %s | %s | %s | %s |',
|
||||||
|
(int)($arrItem['v_id'] ?? 0),
|
||||||
|
$this->escapeMarkdown((string)($arrItem['v_name'] ?? '')),
|
||||||
|
!empty($arrItem['actor_filled']) ? 'yes' : 'no',
|
||||||
|
!empty($arrItem['director_filled']) ? 'yes' : 'no',
|
||||||
|
$this->escapeMarkdown(implode(' / ', $arrSourceSummary))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrLines[] = '';
|
||||||
|
return implode(PHP_EOL, $arrLines) . PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function escapeMarkdown(string $strValue): string
|
||||||
|
{
|
||||||
|
return str_replace('|', '\\|', trim($strValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -776,18 +776,24 @@ namespace {
|
|||||||
return $strZh;
|
return $strZh;
|
||||||
}
|
}
|
||||||
|
|
||||||
$strPinYinToolsPath = root_path() . '/extend/tools/pinyin-tool';
|
$strText = trim($strZh);
|
||||||
|
|
||||||
$strPinYin = shell_exec(sprintf("%s %s", $strPinYinToolsPath, escapeshellcmd($strZh)));
|
if (class_exists(\Transliterator::class)) {
|
||||||
|
$Transliterator = \Transliterator::create('Han-Latin; Latin-ASCII; Lower()');
|
||||||
$strPinYin = trim((string) $strPinYin);
|
if ($Transliterator !== null) {
|
||||||
|
$strText = (string) $Transliterator->transliterate($strText);
|
||||||
if (!is_string($strPinYin) || trim($strPinYin) === '') {
|
}
|
||||||
return 'k_' . substr(sha1($strZh), 0, 12);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$strText = strtolower($strText);
|
||||||
|
$strText = preg_replace('/[^a-z0-9]+/i', '-', $strText) ?? '';
|
||||||
|
$strText = trim($strText, '-');
|
||||||
|
|
||||||
return $strPinYin;
|
if ($strText === '') {
|
||||||
|
return 'k-' . substr(sha1($strZh), 0, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $strText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -864,17 +870,41 @@ namespace {
|
|||||||
*/
|
*/
|
||||||
function convertToWebP(string $strInputFile, string $strOutputFile, int $intQuality = 80): bool
|
function convertToWebP(string $strInputFile, string $strOutputFile, int $intQuality = 80): bool
|
||||||
{
|
{
|
||||||
$strCwebpPath = root_path() . '/extend/tools/cwebp';
|
if (!is_file($strInputFile) || !function_exists('imagewebp')) {
|
||||||
|
|
||||||
$strCMD = escapeshellcmd("$strCwebpPath -q $intQuality " . escapeshellarg($strInputFile) . " -o " . escapeshellarg($strOutputFile));
|
|
||||||
|
|
||||||
exec($strCMD . ' 2>&1', $strOutput, $intReturnCode);
|
|
||||||
|
|
||||||
if ($intReturnCode === 0) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$arrImageInfo = @getimagesize($strInputFile);
|
||||||
|
if (!is_array($arrImageInfo)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$Image = match ($arrImageInfo[2] ?? null) {
|
||||||
|
IMAGETYPE_JPEG => @imagecreatefromjpeg($strInputFile),
|
||||||
|
IMAGETYPE_PNG => @imagecreatefrompng($strInputFile),
|
||||||
|
IMAGETYPE_GIF => @imagecreatefromgif($strInputFile),
|
||||||
|
IMAGETYPE_WEBP => function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($strInputFile) : false,
|
||||||
|
default => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($Image === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
imagepalettetotruecolor($Image);
|
||||||
|
imagealphablending($Image, true);
|
||||||
|
imagesavealpha($Image, true);
|
||||||
|
|
||||||
|
$strOutputDir = dirname($strOutputFile);
|
||||||
|
if (!is_dir($strOutputDir) && !@mkdir($strOutputDir, 0777, true) && !is_dir($strOutputDir)) {
|
||||||
|
imagedestroy($Image);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$boolResult = @imagewebp($Image, $strOutputFile, max(0, min(100, $intQuality)));
|
||||||
|
imagedestroy($Image);
|
||||||
|
|
||||||
|
return $boolResult && is_file($strOutputFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -10,6 +10,91 @@ use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
|||||||
|
|
||||||
class DomainBatchImportTemplateHelper
|
class DomainBatchImportTemplateHelper
|
||||||
{
|
{
|
||||||
|
public static function buildNoteDisplayLabel(string $field): string
|
||||||
|
{
|
||||||
|
$label = self::getHeaderLabels()[$field] ?? $field;
|
||||||
|
|
||||||
|
return $label . '(' . $field . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getHeaderLabels(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'd_domain' => '域名',
|
||||||
|
'd_name' => '站点名称',
|
||||||
|
't_id' => '模板ID',
|
||||||
|
'tkd_provider' => 'TKD来源',
|
||||||
|
'tkd_mode' => 'TKD模式',
|
||||||
|
'd_index_title' => '首页标题',
|
||||||
|
'd_index_keywords' => '首页关键词',
|
||||||
|
'd_index_description' => '首页描述',
|
||||||
|
'strategy_profile' => 'SEO策略',
|
||||||
|
'd_keywords' => '站点关键词',
|
||||||
|
'd_description' => '站点描述',
|
||||||
|
'd_baidu_token' => '百度推送Token',
|
||||||
|
'd_match_type' => '匹配类型',
|
||||||
|
'd_parent_domain' => '父域名',
|
||||||
|
'd_seed_scope' => '种子范围',
|
||||||
|
'd_logo_type' => 'LOGO类型',
|
||||||
|
'd_text_logo' => '文字LOGO',
|
||||||
|
'd_img_logo' => '图片LOGO',
|
||||||
|
'd_statis' => '统计代码',
|
||||||
|
'd_content_encode' => '内容编码',
|
||||||
|
'info_id' => '信息ID',
|
||||||
|
'd_seo_cfg' => '高级SEO配置JSON',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getHeaderAliases(): array
|
||||||
|
{
|
||||||
|
$labels = self::getHeaderLabels();
|
||||||
|
$aliases = [];
|
||||||
|
foreach ($labels as $field => $label) {
|
||||||
|
$aliases[$field] = $field;
|
||||||
|
$aliases[mb_strtolower($field)] = $field;
|
||||||
|
$aliases[$label] = $field;
|
||||||
|
$aliases[mb_strtolower($label)] = $field;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
'站点域名' => 'd_domain',
|
||||||
|
'域名host' => 'd_domain',
|
||||||
|
'首页SEO标题' => 'd_index_title',
|
||||||
|
'首页SEO关键词' => 'd_index_keywords',
|
||||||
|
'首页SEO描述' => 'd_index_description',
|
||||||
|
'策略' => 'strategy_profile',
|
||||||
|
'高级配置JSON' => 'd_seo_cfg',
|
||||||
|
'内容编码类型' => 'd_content_encode',
|
||||||
|
] as $alias => $field) {
|
||||||
|
$aliases[$alias] = $field;
|
||||||
|
$aliases[mb_strtolower($alias)] = $field;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $aliases;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function normalizeHeader(string $header): string
|
||||||
|
{
|
||||||
|
$normalized = trim($header);
|
||||||
|
if ($normalized === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$aliases = self::getHeaderAliases();
|
||||||
|
|
||||||
|
return $aliases[$normalized] ?? $aliases[mb_strtolower($normalized)] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDisplayHeaders(): array
|
||||||
|
{
|
||||||
|
$labels = self::getHeaderLabels();
|
||||||
|
|
||||||
|
return array_map(
|
||||||
|
static fn(string $field): string => $labels[$field] ?? $field,
|
||||||
|
self::getHeaders()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public static function writeWorkbook(
|
public static function writeWorkbook(
|
||||||
string $filePath,
|
string $filePath,
|
||||||
string $sheetTitle,
|
string $sheetTitle,
|
||||||
@@ -42,11 +127,13 @@ class DomainBatchImportTemplateHelper
|
|||||||
|
|
||||||
$notesSheet = $spreadsheet->createSheet();
|
$notesSheet = $spreadsheet->createSheet();
|
||||||
$notesSheet->setTitle('notes');
|
$notesSheet->setTitle('notes');
|
||||||
$notesSheet->setCellValue('A1', 'column');
|
$notesSheet->setCellValue('A1', '字段');
|
||||||
$notesSheet->setCellValue('B1', 'note');
|
$notesSheet->setCellValue('B1', '说明');
|
||||||
$notesSheet->getStyle('A1:B1')->getFont()->setBold(true);
|
$notesSheet->getStyle('A1:B1')->getFont()->setBold(true);
|
||||||
$notesSheet->getColumnDimension('A')->setWidth(28);
|
$notesSheet->getColumnDimension('A')->setWidth(42);
|
||||||
$notesSheet->getColumnDimension('B')->setWidth(80);
|
$notesSheet->getColumnDimension('B')->setWidth(120);
|
||||||
|
$notesSheet->getStyle('A:B')->getAlignment()->setWrapText(true);
|
||||||
|
$notesSheet->getStyle('A:B')->getAlignment()->setVertical(\PhpOffice\PhpSpreadsheet\Style\Alignment::VERTICAL_TOP);
|
||||||
|
|
||||||
$rowNo = 2;
|
$rowNo = 2;
|
||||||
foreach ($columnNotes as $column => $note) {
|
foreach ($columnNotes as $column => $note) {
|
||||||
@@ -65,55 +152,55 @@ class DomainBatchImportTemplateHelper
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'd_domain',
|
'd_domain',
|
||||||
't_id',
|
|
||||||
'd_name',
|
'd_name',
|
||||||
'd_keywords',
|
't_id',
|
||||||
|
'tkd_provider',
|
||||||
|
'tkd_mode',
|
||||||
'd_index_title',
|
'd_index_title',
|
||||||
'd_index_keywords',
|
'd_index_keywords',
|
||||||
'd_index_description',
|
'd_index_description',
|
||||||
|
'strategy_profile',
|
||||||
|
'd_keywords',
|
||||||
'd_description',
|
'd_description',
|
||||||
|
'd_baidu_token',
|
||||||
|
'd_match_type',
|
||||||
|
'd_parent_domain',
|
||||||
|
'd_seed_scope',
|
||||||
'd_statis',
|
'd_statis',
|
||||||
'd_logo_type',
|
'd_logo_type',
|
||||||
'd_text_logo',
|
'd_text_logo',
|
||||||
'd_img_logo',
|
'd_img_logo',
|
||||||
'd_content_encode',
|
'd_content_encode',
|
||||||
'info_id',
|
'info_id',
|
||||||
'd_match_type',
|
|
||||||
'd_parent_domain',
|
|
||||||
'd_seed_scope',
|
|
||||||
'd_baidu_token',
|
|
||||||
'd_seo_cfg',
|
'd_seo_cfg',
|
||||||
'strategy_profile',
|
|
||||||
'tkd_mode',
|
|
||||||
'tkd_provider',
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getColumnNotes(): array
|
public static function getColumnNotes(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'd_domain' => '必填,站点域名;wildcard 模式可填 *.example.com 或 example.com,由后端归一化。',
|
'd_domain' => '必填。填写站点正式域名,不要带 https:// 。示例:jingxifa.com。只有做泛域名时,才填 *.example.com。',
|
||||||
't_id' => '必填,模板 ID。',
|
'd_name' => '必填。填写站点前台展示名称,会用于页头、页脚和基础 SEO 文案。示例:鲸溪发影视。',
|
||||||
'd_name' => '必填,站点名称。',
|
't_id' => "通常直接填 1。\n可选值说明:\n- 1:当前默认模板。绝大多数 GPT 模板站都填这个。\n- 其他数字:代表后台另外配置过的模板 ID,只有你明确知道该站要切到别的模板时才改。\n不会判断时就保持 1,不要猜。",
|
||||||
'd_keywords' => '可选,站点关键词。',
|
'd_index_title' => '建议填写。首页给搜索引擎看的标题,尽量自然,不要堆词。建议 28 到 40 字。',
|
||||||
'd_index_title' => '可选,首页标题。',
|
'd_index_keywords' => '建议填写。首页核心关键词,多个词用逗号分隔。示例:短剧推荐,热播短剧,高清短剧。',
|
||||||
'd_index_keywords' => '可选,首页关键词。',
|
'd_index_description' => '建议填写。首页一句话简介,建议 40 到 80 字,写自然一点。',
|
||||||
'd_index_description' => '可选,首页描述。',
|
'strategy_profile' => "SEO 策略档位,默认 standard。\n可选值说明:\n- standard:标准站,最常用默认值。普通内容站、普通影视站、先求稳定收录时选它。\n- traffic:流量型。更强调关键词覆盖、流量页铺量,适合明确要做流量词扩展的站。\n- expand:扩展型。更适合泛域名、子站扩展、批量铺设场景。\n- closed:收缩/关闭型。通常用于不再继续扩张或只保留最小发布动作的站。\n不确定就填 standard。",
|
||||||
'd_description' => '可选,站点描述。',
|
'd_keywords' => '可选。不填也能跑;如要补充站点级关键词,可填在这里。',
|
||||||
'd_statis' => '可选,统计代码。',
|
'd_description' => '可选。不填时系统会按站点名称自动兜底一版简介。',
|
||||||
'd_logo_type' => '必填,0 或 1。',
|
'd_baidu_token' => '可选。只有需要百度主动推送时才填写。',
|
||||||
'd_text_logo' => '可选,文字 LOGO。',
|
'd_match_type' => "域名匹配方式,默认 exact。\n可选值说明:\n- exact:精确匹配。普通单域名站使用这个,例如 jingxifa.com。\n- wildcard:泛匹配。只有泛域名场景才用,例如 *.example.com。\n如果你填的是普通单域名,就保持 exact。",
|
||||||
'd_img_logo' => '可选,图片 LOGO。',
|
'd_parent_domain' => "父域名只在 wildcard 泛域名场景下填写。\n填写规则:\n- 如果 d_domain 是 *.example.com,这里填 example.com。\n- 如果 d_domain 是普通单域名,例如 jingxifa.com,这里留空。\n不要把 https:// 或路径带进来。",
|
||||||
'd_content_encode' => '必填,通常填 0。',
|
'd_seed_scope' => "种子范围,默认 domain。\n可选值说明:\n- domain:按整域名范围处理,普通站点默认用这个。\n- host:按具体 host 处理,常见于泛域名、子站单独铺设的场景。\n不确定就用 domain。",
|
||||||
'info_id' => '可选,通常填 0。',
|
'd_statis' => '可选。统计代码区,运营通常不用填。',
|
||||||
'd_match_type' => '可选,exact 或 wildcard。',
|
'd_logo_type' => "LOGO 类型,通常填 0。\n可选值说明:\n- 0:文字 LOGO 优先。最常用,普通站点直接用这个。\n- 1:图片 LOGO 优先。只有明确要展示图片 LOGO,且 d_img_logo 已准备好时才用。\n不确定就填 0。",
|
||||||
'd_parent_domain' => 'wildcard 模式建议填写根域名。',
|
'd_text_logo' => '可选。填写前台显示的文字 LOGO;不填时可按站点名称兜底。',
|
||||||
'd_seed_scope' => '可选,domain 或 host。',
|
'd_img_logo' => '可选。只有要使用图片 LOGO 时才填写。',
|
||||||
'd_baidu_token' => '可选,百度推送 token。',
|
'd_content_encode' => "内容编码,通常填 0。\n可选值说明:\n- 0:默认编码方式。普通站直接用这个。\n- 其他值:特殊历史兼容场景才会用,运营不要自行改。\n不会判断就填 0。",
|
||||||
'd_seo_cfg' => '可选,JSON;会在 strategy_profile 默认值基础上再做覆盖。',
|
'info_id' => "信息 ID,通常填 0。\n可选值说明:\n- 0:默认即可。\n- 其他数字:通常是历史数据联动或特殊资源绑定才会用。\n普通新站不要改。",
|
||||||
'strategy_profile' => '可选,standard / traffic / expand / closed。',
|
'd_seo_cfg' => "高级 SEO 配置 JSON。普通运营通常留空。\n适用场景:\n- 要覆盖默认 SEO 行为\n- 要补结构化数据\n- 要定制 forge / route_group 等技术项\n注意:必须填写合法 JSON,例如 {\"forge\":{\"route_group\":\"film\"}}。\n如果看不懂,就留空。",
|
||||||
'tkd_mode' => '可选,1/auto_generate=全自动生成;2/ai_optimize=导入 TKD 后本地/AI 优化;3/force_import=强制使用导入值。',
|
'tkd_mode' => "TKD 模式。一般可留空,由系统自动判断。\n常见可选值说明:\n- 留空:推荐。系统会按上下文自动选择。\n - 如果 TKD来源=openai,则留空会自动按 2 / ai_optimize 处理。\n - 如果首页标题、关键词、描述已经手工填写,则更偏向导入现成内容。\n - 如果都没填且来源不是 openai,则会走默认自动生成策略。\n- 1:偏向使用导入内容/现成内容。\n- 2:偏向使用系统生成或 AI 优化策略。\n- 3:强制按导入内容处理。\n不确定时:local 可留空,openai 也可以直接留空。",
|
||||||
'tkd_provider' => '可选,local 或 openai;当前默认 local,本地优化已生效,后续可接 OpenAI API。',
|
'tkd_provider' => "TKD 来源。一般保持 local 或按需填 openai。\n可选值说明:\n- local:本地默认来源,最稳,常规就用它。\n- openai:走 AI 生成或 AI 优化链路。若你填 openai 且 TKD模式留空,系统会自动按 2 / ai_optimize 处理。\n- 其他值:属于技术兼容项,不建议运营手填。\n想走 OpenAI 全自动时,推荐:TKD来源=openai,TKD模式留空或直接填 2。",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,52 +208,52 @@ class DomainBatchImportTemplateHelper
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
[
|
[
|
||||||
'demo-example-com',
|
'd_domain' => 'demo-example-com',
|
||||||
1,
|
'd_name' => 'Demo Example',
|
||||||
'Demo Example',
|
't_id' => 1,
|
||||||
'Demo Example,影视推荐',
|
'd_index_title' => 'Demo Example 影视内容推荐',
|
||||||
'Demo Example 影视内容推荐',
|
'd_index_keywords' => 'Demo Example,影视推荐',
|
||||||
'Demo Example,影视推荐',
|
'd_index_description' => 'Demo Example 内容整理与推荐。',
|
||||||
'Demo Example 内容整理与推荐。',
|
'strategy_profile' => 'standard',
|
||||||
'Demo Example 内容整理与推荐。',
|
'd_keywords' => 'Demo Example,影视推荐',
|
||||||
'',
|
'd_description' => 'Demo Example 内容整理与推荐。',
|
||||||
0,
|
'd_baidu_token' => '',
|
||||||
'Demo Example',
|
'd_match_type' => 'exact',
|
||||||
'',
|
'd_parent_domain' => '',
|
||||||
0,
|
'd_seed_scope' => 'domain',
|
||||||
0,
|
'd_statis' => '',
|
||||||
'exact',
|
'd_logo_type' => 0,
|
||||||
'',
|
'd_text_logo' => 'Demo Example',
|
||||||
'domain',
|
'd_img_logo' => '',
|
||||||
'',
|
'd_content_encode' => 0,
|
||||||
'',
|
'info_id' => 0,
|
||||||
'standard',
|
'd_seo_cfg' => '',
|
||||||
'1',
|
'tkd_mode' => '1',
|
||||||
'local',
|
'tkd_provider' => 'local',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'*.traffic-example.com',
|
'd_domain' => '*.traffic-example.com',
|
||||||
1,
|
'd_name' => 'Traffic Example',
|
||||||
'Traffic Example',
|
't_id' => 1,
|
||||||
'Traffic Example,热门推荐',
|
'd_index_title' => 'Traffic Example 热门内容推荐',
|
||||||
'Traffic Example 热门内容推荐',
|
'd_index_keywords' => 'Traffic Example,热门推荐',
|
||||||
'Traffic Example,热门推荐',
|
'd_index_description' => 'Traffic Example 热门内容整理与推荐。',
|
||||||
'Traffic Example 热门内容整理与推荐。',
|
'strategy_profile' => 'traffic',
|
||||||
'Traffic Example 热门内容整理与推荐。',
|
'd_keywords' => 'Traffic Example,热门推荐',
|
||||||
'',
|
'd_description' => 'Traffic Example 热门内容整理与推荐。',
|
||||||
0,
|
'd_baidu_token' => '',
|
||||||
'Traffic Example',
|
'd_match_type' => 'wildcard',
|
||||||
'',
|
'd_parent_domain' => 'traffic-example.com',
|
||||||
0,
|
'd_seed_scope' => 'host',
|
||||||
0,
|
'd_statis' => '',
|
||||||
'wildcard',
|
'd_logo_type' => 0,
|
||||||
'traffic-example.com',
|
'd_text_logo' => 'Traffic Example',
|
||||||
'host',
|
'd_img_logo' => '',
|
||||||
'',
|
'd_content_encode' => 0,
|
||||||
'{"forge":{"route_group":"film"},"structured_data":{"organization_name":"Traffic Example Studio"}}',
|
'info_id' => 0,
|
||||||
'traffic',
|
'd_seo_cfg' => '{"forge":{"route_group":"film"},"structured_data":{"organization_name":"Traffic Example Studio"}}',
|
||||||
'2',
|
'tkd_mode' => '2',
|
||||||
'local',
|
'tkd_provider' => 'local',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -175,8 +262,45 @@ class DomainBatchImportTemplateHelper
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'headers' => self::getHeaders(),
|
'headers' => self::getHeaders(),
|
||||||
|
'display_headers' => self::getDisplayHeaders(),
|
||||||
|
'header_labels' => self::getHeaderLabels(),
|
||||||
'column_notes' => self::getColumnNotes(),
|
'column_notes' => self::getColumnNotes(),
|
||||||
'example_rows' => self::getExampleRows(),
|
'example_rows' => self::getExampleRows(),
|
||||||
|
'template_type' => 'site_domain_import',
|
||||||
|
'filling_guidance' => [
|
||||||
|
'main_fields' => [
|
||||||
|
'd_domain',
|
||||||
|
'd_name',
|
||||||
|
't_id',
|
||||||
|
'd_index_title',
|
||||||
|
'd_index_keywords',
|
||||||
|
'd_index_description',
|
||||||
|
'strategy_profile',
|
||||||
|
],
|
||||||
|
'optional_fields' => [
|
||||||
|
'd_keywords',
|
||||||
|
'd_description',
|
||||||
|
'd_baidu_token',
|
||||||
|
'd_match_type',
|
||||||
|
'd_parent_domain',
|
||||||
|
'd_seed_scope',
|
||||||
|
'd_statis',
|
||||||
|
'd_logo_type',
|
||||||
|
'd_text_logo',
|
||||||
|
'd_img_logo',
|
||||||
|
'd_content_encode',
|
||||||
|
'info_id',
|
||||||
|
'd_seo_cfg',
|
||||||
|
'tkd_mode',
|
||||||
|
'tkd_provider',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'operator_notice' => [
|
||||||
|
'基础导入模板只负责站点基础信息,不负责 GPT 启动样本字段。',
|
||||||
|
'运营通常只需要先填写域名、站点名称、模板ID、首页标题、首页关键词、首页描述、SEO策略。',
|
||||||
|
'其余字段大多数都有默认值或属于技术配置,可留空。',
|
||||||
|
'如果要做 GPT 供料与启动包,请改用 domain-supply-batch-template.xlsx。',
|
||||||
|
],
|
||||||
'supported_strategy_profiles' => ['standard', 'traffic', 'expand', 'closed'],
|
'supported_strategy_profiles' => ['standard', 'traffic', 'expand', 'closed'],
|
||||||
'supported_match_types' => ['exact', 'wildcard'],
|
'supported_match_types' => ['exact', 'wildcard'],
|
||||||
'supported_seed_scopes' => ['domain', 'host'],
|
'supported_seed_scopes' => ['domain', 'host'],
|
||||||
@@ -189,19 +313,28 @@ class DomainBatchImportTemplateHelper
|
|||||||
$fileName = 'site-domain-import-template.xlsx';
|
$fileName = 'site-domain-import-template.xlsx';
|
||||||
$filePath = $outputRoot . '/' . $fileName;
|
$filePath = $outputRoot . '/' . $fileName;
|
||||||
$summary = self::buildSummary();
|
$summary = self::buildSummary();
|
||||||
|
$headerLabels = self::getHeaderLabels();
|
||||||
$rows = array_map(
|
$rows = array_map(
|
||||||
static function (array $row): array {
|
static function (array $row) use ($headerLabels): array {
|
||||||
return array_combine(self::getHeaders(), $row) ?: [];
|
$displayRow = [];
|
||||||
|
foreach (self::getHeaders() as $field) {
|
||||||
|
$displayRow[$headerLabels[$field] ?? $field] = (string)($row[$field] ?? '');
|
||||||
|
}
|
||||||
|
return $displayRow;
|
||||||
},
|
},
|
||||||
self::getExampleRows()
|
self::getExampleRows()
|
||||||
);
|
);
|
||||||
|
$displayNotes = [];
|
||||||
|
foreach (self::getColumnNotes() as $field => $note) {
|
||||||
|
$displayNotes[self::buildNoteDisplayLabel($field)] = $note;
|
||||||
|
}
|
||||||
|
|
||||||
self::writeWorkbook(
|
self::writeWorkbook(
|
||||||
$filePath,
|
$filePath,
|
||||||
'site-domain-import',
|
'site-domain-import',
|
||||||
self::getHeaders(),
|
self::getDisplayHeaders(),
|
||||||
$rows,
|
$rows,
|
||||||
self::getColumnNotes()
|
$displayNotes
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ class DomainBootstrapApplyHelper
|
|||||||
$DomainModel->d_seo_cfg = $arrNormalizedPayload['d_seo_cfg'] ?? $DomainModel->d_seo_cfg;
|
$DomainModel->d_seo_cfg = $arrNormalizedPayload['d_seo_cfg'] ?? $DomainModel->d_seo_cfg;
|
||||||
$DomainModel->save();
|
$DomainModel->save();
|
||||||
DomainModel::flushAllDomianOnCache();
|
DomainModel::flushAllDomianOnCache();
|
||||||
|
SeoResourcePoolHelper::initSitePositioning((string)$DomainModel->d_domain, 1, true);
|
||||||
|
DomainSitemapGenerationHelper::queueForDomains([(string)$DomainModel->d_domain]);
|
||||||
|
|
||||||
$arrSummary['status'] = 'applied';
|
$arrSummary['status'] = 'applied';
|
||||||
return $arrSummary;
|
return $arrSummary;
|
||||||
|
|||||||
@@ -61,16 +61,34 @@ class DomainBootstrapRegisterHelper
|
|||||||
$arrSeoCfgInput = [];
|
$arrSeoCfgInput = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$strTkdProviderInput = trim((string)($arrPayload['tkd_provider'] ?? $arrPayload['seo_tkd_provider'] ?? ($arrSeoCfgInput['tkd']['provider'] ?? DomainModel::TKD_PROVIDER_LOCAL)));
|
||||||
|
$strTkdProviderNormalized = DomainModel::normalizeTkdProvider($strTkdProviderInput);
|
||||||
$strTkdModeInput = trim((string)($arrPayload['tkd_mode'] ?? $arrPayload['seo_tkd_mode'] ?? ($arrSeoCfgInput['tkd']['mode'] ?? '')));
|
$strTkdModeInput = trim((string)($arrPayload['tkd_mode'] ?? $arrPayload['seo_tkd_mode'] ?? ($arrSeoCfgInput['tkd']['mode'] ?? '')));
|
||||||
if ($strTkdModeInput === '') {
|
if ($strTkdModeInput === '') {
|
||||||
$strTkdModeInput = $boolHasImportedHomeTkd ? DomainModel::TKD_MODE_FORCE_IMPORT : DomainModel::TKD_MODE_AUTO_GENERATE;
|
$strTkdModeInput = $strTkdProviderNormalized === DomainModel::TKD_PROVIDER_OPENAI
|
||||||
|
? DomainModel::TKD_MODE_AI_OPTIMIZE
|
||||||
|
: ($boolHasImportedHomeTkd ? DomainModel::TKD_MODE_FORCE_IMPORT : DomainModel::TKD_MODE_AUTO_GENERATE);
|
||||||
|
}
|
||||||
|
if (DomainSeoNamingHelper::shouldOptimizeForOpenAi($strTkdProviderNormalized, $strTkdModeInput)) {
|
||||||
|
$arrSeoDefaults = DomainSeoNamingHelper::buildSeoDefaults(
|
||||||
|
$strSiteName,
|
||||||
|
$strDomain,
|
||||||
|
$strRawIndexTitle,
|
||||||
|
$strRawIndexKeywords,
|
||||||
|
$strRawIndexDescription
|
||||||
|
);
|
||||||
|
$strSiteName = $arrSeoDefaults['site_name'];
|
||||||
|
$strSiteKeywords = trim((string)($arrPayload['d_keywords'] ?? $strSiteName));
|
||||||
|
$strSiteDescription = trim((string)($arrPayload['d_description'] ?? ($strSiteName . '内容整理与推荐。')));
|
||||||
|
$strIndexTitle = $arrSeoDefaults['index_title'];
|
||||||
|
$strIndexKeywords = $arrSeoDefaults['index_keywords'];
|
||||||
|
$strIndexDescription = $arrSeoDefaults['index_description'];
|
||||||
}
|
}
|
||||||
$strTkdProviderInput = trim((string)($arrPayload['tkd_provider'] ?? $arrPayload['seo_tkd_provider'] ?? ($arrSeoCfgInput['tkd']['provider'] ?? DomainModel::TKD_PROVIDER_LOCAL)));
|
|
||||||
$arrSeoCfgInput['tkd'] = array_merge(
|
$arrSeoCfgInput['tkd'] = array_merge(
|
||||||
(array)($arrSeoCfgInput['tkd'] ?? []),
|
(array)($arrSeoCfgInput['tkd'] ?? []),
|
||||||
[
|
[
|
||||||
'mode' => DomainModel::normalizeTkdMode($strTkdModeInput),
|
'mode' => DomainModel::normalizeTkdMode($strTkdModeInput),
|
||||||
'provider' => DomainModel::normalizeTkdProvider($strTkdProviderInput),
|
'provider' => $strTkdProviderNormalized,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -175,6 +193,7 @@ class DomainBootstrapRegisterHelper
|
|||||||
DomainModel::flushAllDomianOnCache();
|
DomainModel::flushAllDomianOnCache();
|
||||||
SeoCopyGenerationHelper::initializeForDomain($DomainModel);
|
SeoCopyGenerationHelper::initializeForDomain($DomainModel);
|
||||||
SeoResourcePoolHelper::initSitePositioning((string)$DomainModel->d_domain, 1, true);
|
SeoResourcePoolHelper::initSitePositioning((string)$DomainModel->d_domain, 1, true);
|
||||||
|
DomainSitemapGenerationHelper::queueForDomains([(string)$DomainModel->d_domain]);
|
||||||
|
|
||||||
$arrSummary['status'] = 'created';
|
$arrSummary['status'] = 'created';
|
||||||
$arrSummary['found'] = true;
|
$arrSummary['found'] = true;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class DomainExternalSeoManualImportHelper
|
|||||||
{
|
{
|
||||||
public static function defaultRoot(string $strCodeRoot): string
|
public static function defaultRoot(string $strCodeRoot): string
|
||||||
{
|
{
|
||||||
return rtrim($strCodeRoot, '/') . '/public/_admin_templates/domain-seo-external/manual-import';
|
return rtrim($strCodeRoot, '/') . '/storage/external-seo/manual-import';
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getHeaders(): array
|
public static function getHeaders(): array
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ namespace app\common\helper;
|
|||||||
|
|
||||||
class DomainExternalSeoManualImportRunIndexHelper
|
class DomainExternalSeoManualImportRunIndexHelper
|
||||||
{
|
{
|
||||||
protected static function publicRelativePath(string $strPath): string
|
protected static function storageRelativePath(string $strPath): string
|
||||||
{
|
{
|
||||||
$strPublicRoot = str_replace('\\', '/', rtrim(dirname(__DIR__, 3) . '/public', '/'));
|
$strStorageRoot = str_replace('\\', '/', rtrim(dirname(__DIR__, 3) . '/storage', '/'));
|
||||||
$strPath = str_replace('\\', '/', $strPath);
|
$strPath = str_replace('\\', '/', $strPath);
|
||||||
if (str_starts_with($strPath, $strPublicRoot . '/')) {
|
if (str_starts_with($strPath, $strStorageRoot . '/')) {
|
||||||
return substr($strPath, strlen($strPublicRoot . '/'));
|
return substr($strPath, strlen($strStorageRoot . '/'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $strPath;
|
return $strPath;
|
||||||
@@ -47,16 +47,16 @@ class DomainExternalSeoManualImportRunIndexHelper
|
|||||||
|
|
||||||
$arrItems[] = [
|
$arrItems[] = [
|
||||||
'run_id' => basename($strRunRoot),
|
'run_id' => basename($strRunRoot),
|
||||||
'run_root_public' => self::publicRelativePath($strRunRoot),
|
'run_root_public' => self::storageRelativePath($strRunRoot),
|
||||||
'status' => (string)($arrSummary['status'] ?? 'empty'),
|
'status' => (string)($arrSummary['status'] ?? 'empty'),
|
||||||
'provider_key' => (string)($arrSummary['provider_key'] ?? 'manual_csv_import'),
|
'provider_key' => (string)($arrSummary['provider_key'] ?? 'manual_csv_import'),
|
||||||
'rows_total' => self::countCsvRows($strCsvPath),
|
'rows_total' => self::countCsvRows($strCsvPath),
|
||||||
'hosts_count' => self::countCsvHosts($strCsvPath),
|
'hosts_count' => self::countCsvHosts($strCsvPath),
|
||||||
'metrics' => (array)($arrSummary['metrics'] ?? []),
|
'metrics' => (array)($arrSummary['metrics'] ?? []),
|
||||||
'uploaded_report_path' => $strUploadedPath !== '' ? self::publicRelativePath($strUploadedPath) : '',
|
'uploaded_report_path' => $strUploadedPath !== '' ? self::storageRelativePath($strUploadedPath) : '',
|
||||||
'csv_path' => is_file($strCsvPath) ? self::publicRelativePath($strCsvPath) : '',
|
'csv_path' => is_file($strCsvPath) ? self::storageRelativePath($strCsvPath) : '',
|
||||||
'summary_json_path' => self::publicRelativePath($strSummaryPath),
|
'summary_json_path' => self::storageRelativePath($strSummaryPath),
|
||||||
'summary_html_path' => is_file($strRunRoot . '/seo-external-real.summary.html') ? self::publicRelativePath($strRunRoot . '/seo-external-real.summary.html') : '',
|
'summary_html_path' => is_file($strRunRoot . '/seo-external-real.summary.html') ? self::storageRelativePath($strRunRoot . '/seo-external-real.summary.html') : '',
|
||||||
'updated_at' => date(DATE_ATOM, (int)(filemtime($strSummaryPath) ?: time())),
|
'updated_at' => date(DATE_ATOM, (int)(filemtime($strSummaryPath) ?: time())),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -69,16 +69,16 @@ class DomainExternalSeoManualImportRunIndexHelper
|
|||||||
$arrSummary = is_array($arrDecoded) ? $arrDecoded : [];
|
$arrSummary = is_array($arrDecoded) ? $arrDecoded : [];
|
||||||
array_unshift($arrItems, [
|
array_unshift($arrItems, [
|
||||||
'run_id' => 'latest_manual_import',
|
'run_id' => 'latest_manual_import',
|
||||||
'run_root_public' => self::publicRelativePath($strLatestDir),
|
'run_root_public' => self::storageRelativePath($strLatestDir),
|
||||||
'status' => (string)($arrSummary['status'] ?? 'empty'),
|
'status' => (string)($arrSummary['status'] ?? 'empty'),
|
||||||
'provider_key' => (string)($arrSummary['provider_key'] ?? 'manual_csv_import'),
|
'provider_key' => (string)($arrSummary['provider_key'] ?? 'manual_csv_import'),
|
||||||
'rows_total' => self::countCsvRows($strLatestDir . '/manual-seo-import.csv'),
|
'rows_total' => self::countCsvRows($strLatestDir . '/manual-seo-import.csv'),
|
||||||
'hosts_count' => self::countCsvHosts($strLatestDir . '/manual-seo-import.csv'),
|
'hosts_count' => self::countCsvHosts($strLatestDir . '/manual-seo-import.csv'),
|
||||||
'metrics' => (array)($arrSummary['metrics'] ?? []),
|
'metrics' => (array)($arrSummary['metrics'] ?? []),
|
||||||
'uploaded_report_path' => '',
|
'uploaded_report_path' => '',
|
||||||
'csv_path' => self::publicRelativePath($strLatestDir . '/manual-seo-import.csv'),
|
'csv_path' => self::storageRelativePath($strLatestDir . '/manual-seo-import.csv'),
|
||||||
'summary_json_path' => self::publicRelativePath($strLatestSummaryJsonPath),
|
'summary_json_path' => self::storageRelativePath($strLatestSummaryJsonPath),
|
||||||
'summary_html_path' => self::publicRelativePath(dirname($strRoot) . '/latest/seo-external-real.summary.html'),
|
'summary_html_path' => self::storageRelativePath(dirname($strRoot) . '/latest/seo-external-real.summary.html'),
|
||||||
'updated_at' => date(DATE_ATOM, (int)(filemtime($strLatestSummaryJsonPath) ?: time())),
|
'updated_at' => date(DATE_ATOM, (int)(filemtime($strLatestSummaryJsonPath) ?: time())),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class DomainExternalSeoSnapshotAnalysisHelper
|
|||||||
|
|
||||||
$arrProviders = [];
|
$arrProviders = [];
|
||||||
$arrHosts = [];
|
$arrHosts = [];
|
||||||
$arrLatestIndexByHost = [];
|
$arrLatestSignalsByHost = [];
|
||||||
$intKeywordSnapshots = (int)SeoExternalSnapshotModel::where('metric_date', '>=', $strStartDate)
|
$intKeywordSnapshots = (int)SeoExternalSnapshotModel::where('metric_date', '>=', $strStartDate)
|
||||||
->where('scope', 'keyword')
|
->where('scope', 'keyword')
|
||||||
->count();
|
->count();
|
||||||
@@ -28,9 +28,10 @@ class DomainExternalSeoSnapshotAnalysisHelper
|
|||||||
self::eachSnapshotRow(
|
self::eachSnapshotRow(
|
||||||
SeoExternalSnapshotModel::where('metric_date', '>=', $strStartDate)
|
SeoExternalSnapshotModel::where('metric_date', '>=', $strStartDate)
|
||||||
->where('scope', 'host'),
|
->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'],
|
['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, &$arrLatestIndexByHost, &$strLatestMetricDate): void {
|
static function (array $arrItem) use (&$arrProviders, &$arrHosts, &$arrLatestSignalsByHost, &$strLatestMetricDate): void {
|
||||||
$strProvider = trim((string)($arrItem['provider'] ?? ''));
|
$strProvider = trim((string)($arrItem['provider'] ?? ''));
|
||||||
|
$strSnapshotType = trim((string)($arrItem['snapshot_type'] ?? ''));
|
||||||
$strHost = trim((string)($arrItem['host'] ?? ''));
|
$strHost = trim((string)($arrItem['host'] ?? ''));
|
||||||
$strMetricDate = trim((string)($arrItem['metric_date'] ?? ''));
|
$strMetricDate = trim((string)($arrItem['metric_date'] ?? ''));
|
||||||
$strStatus = trim((string)($arrItem['status'] ?? ''));
|
$strStatus = trim((string)($arrItem['status'] ?? ''));
|
||||||
@@ -48,34 +49,33 @@ class DomainExternalSeoSnapshotAnalysisHelper
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($strHost !== '') {
|
if ($strHost !== '') {
|
||||||
$strHostKey = $strHost;
|
$arrCandidate = [
|
||||||
$arrCurrent = $arrLatestIndexByHost[$strHostKey] ?? null;
|
'host' => $strHost,
|
||||||
$boolShouldReplace = false;
|
'provider' => $strProvider,
|
||||||
if (!$arrCurrent) {
|
'snapshot_type' => $strSnapshotType,
|
||||||
$boolShouldReplace = true;
|
'metric_date' => $strMetricDate,
|
||||||
} else {
|
'queried_at' => $intQueriedAt,
|
||||||
$strCurrentDate = (string)($arrCurrent['metric_date'] ?? '');
|
'status' => $strStatus,
|
||||||
$intCurrentQueriedAt = (int)($arrCurrent['queried_at'] ?? 0);
|
'indexed_status' => $strIndexedStatus,
|
||||||
if (strcmp($strCurrentDate, $strMetricDate) < 0) {
|
'result_count_text' => trim((string)($arrItem['result_count_text'] ?? '')),
|
||||||
$boolShouldReplace = true;
|
'baidu_pc_ip_range' => trim((string)($arrItem['baidu_pc_ip_range'] ?? '')),
|
||||||
} elseif ($strCurrentDate === $strMetricDate && $intCurrentQueriedAt < $intQueriedAt) {
|
'baidu_mobile_ip_range' => trim((string)($arrItem['baidu_mobile_ip_range'] ?? '')),
|
||||||
$boolShouldReplace = true;
|
'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) {
|
if ($strSnapshotType === 'site_query'
|
||||||
$arrLatestIndexByHost[$strHostKey] = [
|
&& self::shouldReplaceSnapshot($arrLatestSignalsByHost[$strHost]['index'] ?? null, $strMetricDate, $intQueriedAt)) {
|
||||||
'host' => $strHost,
|
$arrLatestSignalsByHost[$strHost]['index'] = $arrCandidate;
|
||||||
'provider' => $strProvider,
|
}
|
||||||
'metric_date' => $strMetricDate,
|
|
||||||
'queried_at' => $intQueriedAt,
|
if ($strSnapshotType === 'aizhan_summary'
|
||||||
'status' => $strStatus,
|
&& self::shouldReplaceSnapshot($arrLatestSignalsByHost[$strHost]['keyword'] ?? null, $strMetricDate, $intQueriedAt)) {
|
||||||
'indexed_status' => $strIndexedStatus,
|
$arrLatestSignalsByHost[$strHost]['keyword'] = $arrCandidate;
|
||||||
'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),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -85,13 +85,44 @@ class DomainExternalSeoSnapshotAnalysisHelper
|
|||||||
$intIndexedLikeCount = 0;
|
$intIndexedLikeCount = 0;
|
||||||
$intKeywordReadyCount = 0;
|
$intKeywordReadyCount = 0;
|
||||||
$intTrafficReadyCount = 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) {
|
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;
|
$boolKeywordReady = (int)($arrRow['pc_keyword_count'] ?? 0) > 0 || (int)($arrRow['mobile_keyword_count'] ?? 0) > 0;
|
||||||
$boolTrafficReady = !self::isZeroRange((string)($arrRow['baidu_pc_ip_range'] ?? ''))
|
$boolTrafficReady = !self::isZeroRange((string)($arrRow['baidu_pc_ip_range'] ?? ''))
|
||||||
|| !self::isZeroRange((string)($arrRow['baidu_mobile_ip_range'] ?? ''));
|
|| !self::isZeroRange((string)($arrRow['baidu_mobile_ip_range'] ?? ''));
|
||||||
|
$arrRow['indexed_like'] = $boolIndexedLike;
|
||||||
$arrRow['keyword_ready'] = $boolKeywordReady;
|
$arrRow['keyword_ready'] = $boolKeywordReady;
|
||||||
$arrRow['traffic_ready'] = $boolTrafficReady;
|
$arrRow['traffic_ready'] = $boolTrafficReady;
|
||||||
|
|
||||||
@@ -113,6 +144,11 @@ class DomainExternalSeoSnapshotAnalysisHelper
|
|||||||
if ($intLeftKeyword !== $intRightKeyword) {
|
if ($intLeftKeyword !== $intRightKeyword) {
|
||||||
return $intRightKeyword <=> $intLeftKeyword;
|
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'] ?? ''));
|
return strcmp((string)($arrLeft['host'] ?? ''), (string)($arrRight['host'] ?? ''));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -402,6 +438,19 @@ class DomainExternalSeoSnapshotAnalysisHelper
|
|||||||
return $strRange === '' || $strRange === '0 ~ 0' || $strRange === '0~0';
|
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
|
protected static function normalizeIndexState(array $arrRow): string
|
||||||
{
|
{
|
||||||
$strIndexedStatus = trim((string)($arrRow['indexed_status'] ?? ''));
|
$strIndexedStatus = trim((string)($arrRow['indexed_status'] ?? ''));
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ namespace app\common\helper;
|
|||||||
|
|
||||||
class DomainExternalSeoSnapshotRunIndexHelper
|
class DomainExternalSeoSnapshotRunIndexHelper
|
||||||
{
|
{
|
||||||
protected static function publicRelativePath(string $strPath): string
|
protected static function storageRelativePath(string $strPath): string
|
||||||
{
|
{
|
||||||
$strPublicRoot = str_replace('\\', '/', rtrim(dirname(__DIR__, 3) . '/public', '/'));
|
$strStorageRoot = str_replace('\\', '/', rtrim(dirname(__DIR__, 3) . '/storage', '/'));
|
||||||
$strPath = str_replace('\\', '/', $strPath);
|
$strPath = str_replace('\\', '/', $strPath);
|
||||||
if (str_starts_with($strPath, $strPublicRoot . '/')) {
|
if (str_starts_with($strPath, $strStorageRoot . '/')) {
|
||||||
return substr($strPath, strlen($strPublicRoot . '/'));
|
return substr($strPath, strlen($strStorageRoot . '/'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $strPath;
|
return $strPath;
|
||||||
@@ -60,7 +60,7 @@ class DomainExternalSeoSnapshotRunIndexHelper
|
|||||||
|
|
||||||
$arrItems[] = [
|
$arrItems[] = [
|
||||||
'run_id' => basename($strRunRoot),
|
'run_id' => basename($strRunRoot),
|
||||||
'run_root_public' => self::publicRelativePath($strRunRoot),
|
'run_root_public' => self::storageRelativePath($strRunRoot),
|
||||||
'source' => (string)($arrSummary['source'] ?? ''),
|
'source' => (string)($arrSummary['source'] ?? ''),
|
||||||
'received_count' => $intReceivedCount,
|
'received_count' => $intReceivedCount,
|
||||||
'inserted_count' => (int)($arrSummary['inserted_count'] ?? 0),
|
'inserted_count' => (int)($arrSummary['inserted_count'] ?? 0),
|
||||||
@@ -82,9 +82,9 @@ class DomainExternalSeoSnapshotRunIndexHelper
|
|||||||
'post_ingest_snapshot_trend' => (array)($arrSummary['post_ingest_snapshot_trend'] ?? []),
|
'post_ingest_snapshot_trend' => (array)($arrSummary['post_ingest_snapshot_trend'] ?? []),
|
||||||
'post_ingest_health_label' => (string)(($arrSummary['post_ingest_overview']['health_label'] ?? '')),
|
'post_ingest_health_label' => (string)(($arrSummary['post_ingest_overview']['health_label'] ?? '')),
|
||||||
'post_ingest_trend_label' => (string)(($arrSummary['post_ingest_snapshot_trend']['success_trend']['label'] ?? '')),
|
'post_ingest_trend_label' => (string)(($arrSummary['post_ingest_snapshot_trend']['success_trend']['label'] ?? '')),
|
||||||
'summary_json_path' => self::publicRelativePath($strSummaryPath),
|
'summary_json_path' => self::storageRelativePath($strSummaryPath),
|
||||||
'summary_html_path' => self::publicRelativePath($strRunRoot . '/snapshot-ingest.summary.html'),
|
'summary_html_path' => self::storageRelativePath($strRunRoot . '/snapshot-ingest.summary.html'),
|
||||||
'payload_json_path' => self::publicRelativePath($strRunRoot . '/snapshots.payload.json'),
|
'payload_json_path' => self::storageRelativePath($strRunRoot . '/snapshots.payload.json'),
|
||||||
'updated_at' => date(DATE_ATOM, (int)(filemtime($strSummaryPath) ?: time())),
|
'updated_at' => date(DATE_ATOM, (int)(filemtime($strSummaryPath) ?: time())),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ namespace app\common\helper;
|
|||||||
|
|
||||||
class DomainExternalSeoSnapshotValidateRunIndexHelper
|
class DomainExternalSeoSnapshotValidateRunIndexHelper
|
||||||
{
|
{
|
||||||
protected static function publicRelativePath(string $strPath): string
|
protected static function storageRelativePath(string $strPath): string
|
||||||
{
|
{
|
||||||
$strPublicRoot = str_replace('\\', '/', rtrim(dirname(__DIR__, 3) . '/public', '/'));
|
$strStorageRoot = str_replace('\\', '/', rtrim(dirname(__DIR__, 3) . '/storage', '/'));
|
||||||
$strPath = str_replace('\\', '/', $strPath);
|
$strPath = str_replace('\\', '/', $strPath);
|
||||||
if (str_starts_with($strPath, $strPublicRoot . '/')) {
|
if (str_starts_with($strPath, $strStorageRoot . '/')) {
|
||||||
return substr($strPath, strlen($strPublicRoot . '/'));
|
return substr($strPath, strlen($strStorageRoot . '/'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $strPath;
|
return $strPath;
|
||||||
@@ -50,7 +50,7 @@ class DomainExternalSeoSnapshotValidateRunIndexHelper
|
|||||||
|
|
||||||
$arrItems[] = [
|
$arrItems[] = [
|
||||||
'run_id' => basename($strRunRoot),
|
'run_id' => basename($strRunRoot),
|
||||||
'run_root_public' => self::publicRelativePath($strRunRoot),
|
'run_root_public' => self::storageRelativePath($strRunRoot),
|
||||||
'received_count' => $intReceivedCount,
|
'received_count' => $intReceivedCount,
|
||||||
'valid_count' => (int)($arrSummary['valid_count'] ?? 0),
|
'valid_count' => (int)($arrSummary['valid_count'] ?? 0),
|
||||||
'error_count' => $intErrorCount,
|
'error_count' => $intErrorCount,
|
||||||
@@ -63,9 +63,9 @@ class DomainExternalSeoSnapshotValidateRunIndexHelper
|
|||||||
'warnings' => (array)($arrSummary['warnings'] ?? []),
|
'warnings' => (array)($arrSummary['warnings'] ?? []),
|
||||||
'error_buckets' => (array)($arrSummary['error_buckets'] ?? []),
|
'error_buckets' => (array)($arrSummary['error_buckets'] ?? []),
|
||||||
'warning_buckets' => (array)($arrSummary['warning_buckets'] ?? []),
|
'warning_buckets' => (array)($arrSummary['warning_buckets'] ?? []),
|
||||||
'summary_json_path' => self::publicRelativePath($strSummaryPath),
|
'summary_json_path' => self::storageRelativePath($strSummaryPath),
|
||||||
'summary_html_path' => self::publicRelativePath($strRunRoot . '/snapshot-validate.summary.html'),
|
'summary_html_path' => self::storageRelativePath($strRunRoot . '/snapshot-validate.summary.html'),
|
||||||
'payload_json_path' => self::publicRelativePath($strRunRoot . '/snapshots.payload.json'),
|
'payload_json_path' => self::storageRelativePath($strRunRoot . '/snapshots.payload.json'),
|
||||||
'updated_at' => date(DATE_ATOM, (int)(filemtime($strSummaryPath) ?: time())),
|
'updated_at' => date(DATE_ATOM, (int)(filemtime($strSummaryPath) ?: time())),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
87
code/app/common/helper/DomainSeoNamingHelper.php
Normal file
87
code/app/common/helper/DomainSeoNamingHelper.php
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\helper;
|
||||||
|
|
||||||
|
use app\model\DomainModel;
|
||||||
|
|
||||||
|
class DomainSeoNamingHelper
|
||||||
|
{
|
||||||
|
public static function shouldOptimizeForOpenAi(?string $strTkdProvider, ?string $strTkdMode): bool
|
||||||
|
{
|
||||||
|
$strProvider = DomainModel::normalizeTkdProvider((string)$strTkdProvider);
|
||||||
|
$strMode = DomainModel::normalizeTkdMode((string)$strTkdMode);
|
||||||
|
|
||||||
|
return $strProvider === DomainModel::TKD_PROVIDER_OPENAI
|
||||||
|
|| $strMode === DomainModel::TKD_MODE_AI_OPTIMIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function normalizeSiteName(string $strSiteName, string $strDomain = ''): string
|
||||||
|
{
|
||||||
|
$strSiteName = trim($strSiteName);
|
||||||
|
$strSiteName = preg_replace('/\s+/u', '', $strSiteName) ?: $strSiteName;
|
||||||
|
$strSiteName = trim($strSiteName, "-_\t\n\r\0\x0B .|");
|
||||||
|
|
||||||
|
if ($strSiteName === '') {
|
||||||
|
$strSiteName = self::fallbackNameFromDomain($strDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['影视网', '电影网', '剧情网', '短剧网', '影视', '影院', '视频', '短剧', '剧情'] as $suffix) {
|
||||||
|
$quoted = preg_quote($suffix, '/');
|
||||||
|
$strSiteName = preg_replace('/(' . $quoted . '){2,}$/u', $suffix, $strSiteName) ?: $strSiteName;
|
||||||
|
}
|
||||||
|
|
||||||
|
$strSiteName = preg_replace('/^(.{2,12}?)\1$/u', '$1', $strSiteName) ?: $strSiteName;
|
||||||
|
$strSiteName = preg_replace('/官网网$/u', '官网', $strSiteName) ?: $strSiteName;
|
||||||
|
|
||||||
|
return trim($strSiteName) !== '' ? trim($strSiteName) : self::fallbackNameFromDomain($strDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function buildSeoDefaults(
|
||||||
|
string $strSiteName,
|
||||||
|
string $strDomain = '',
|
||||||
|
string $strIndexTitle = '',
|
||||||
|
string $strIndexKeywords = '',
|
||||||
|
string $strIndexDescription = ''
|
||||||
|
): array {
|
||||||
|
$strSiteName = self::normalizeSiteName($strSiteName, $strDomain);
|
||||||
|
|
||||||
|
if (trim($strIndexTitle) === '') {
|
||||||
|
$strIndexTitle = $strSiteName . '-高清影视内容精选-每日更新';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trim($strIndexKeywords) === '') {
|
||||||
|
$strIndexKeywords = implode(',', array_values(array_unique(array_filter([
|
||||||
|
$strSiteName,
|
||||||
|
'高清影视',
|
||||||
|
'热播短剧',
|
||||||
|
'剧情介绍',
|
||||||
|
'热门推荐',
|
||||||
|
]))));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trim($strIndexDescription) === '') {
|
||||||
|
$strIndexDescription = $strSiteName . '专注于高清影视、热播短剧与剧情内容整理,支持分类浏览、搜索查找和热门推荐,内容持续更新。';
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'site_name' => $strSiteName,
|
||||||
|
'index_title' => trim($strIndexTitle),
|
||||||
|
'index_keywords' => trim($strIndexKeywords),
|
||||||
|
'index_description' => trim($strIndexDescription),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function fallbackNameFromDomain(string $strDomain): string
|
||||||
|
{
|
||||||
|
$strDomain = strtolower(trim($strDomain));
|
||||||
|
$strDomain = preg_replace('/^https?:\/\//', '', $strDomain) ?: $strDomain;
|
||||||
|
$strDomain = preg_replace('/^www\./', '', $strDomain) ?: $strDomain;
|
||||||
|
$strDomain = explode('/', $strDomain)[0] ?? $strDomain;
|
||||||
|
$strDomain = explode('.', $strDomain)[0] ?? $strDomain;
|
||||||
|
$strDomain = str_replace(['-', '_'], '', $strDomain);
|
||||||
|
|
||||||
|
return $strDomain !== '' ? $strDomain : '影视站';
|
||||||
|
}
|
||||||
|
}
|
||||||
51
code/app/common/helper/DomainSitemapGenerationHelper.php
Normal file
51
code/app/common/helper/DomainSitemapGenerationHelper.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\helper;
|
||||||
|
|
||||||
|
use app\model\DomainModel;
|
||||||
|
use app\task\logic\VideoSiteMapLogic;
|
||||||
|
use microserver\QueueManage;
|
||||||
|
|
||||||
|
class DomainSitemapGenerationHelper
|
||||||
|
{
|
||||||
|
public static function normalizeDomains(array $arrDomains): array
|
||||||
|
{
|
||||||
|
return array_values(array_unique(array_filter(array_map(
|
||||||
|
static fn($mValue): string => DomainModel::normalizeStoredDomain((string)$mValue),
|
||||||
|
$arrDomains
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function queueForDomains(array $arrDomains): void
|
||||||
|
{
|
||||||
|
$arrDomains = self::normalizeDomains($arrDomains);
|
||||||
|
if (empty($arrDomains)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrTask = [
|
||||||
|
'callback' => [VideoSiteMapLogic::class, 'generateSiteMap'],
|
||||||
|
'data' => [
|
||||||
|
'domains' => $arrDomains,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$QueueManage = new QueueManage(2);
|
||||||
|
$QueueManage->set($arrTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function generateNow(array $arrDomains): void
|
||||||
|
{
|
||||||
|
$arrDomains = self::normalizeDomains($arrDomains);
|
||||||
|
if (empty($arrDomains)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$VideoSiteMapLogic = new VideoSiteMapLogic();
|
||||||
|
$VideoSiteMapLogic->generateSiteMap([
|
||||||
|
'domains' => $arrDomains,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,9 @@ class DomainSpiderCrawlLogHelper
|
|||||||
$arrItem['host'] = strtolower(trim((string)($arrItem['host'] ?? '')));
|
$arrItem['host'] = strtolower(trim((string)($arrItem['host'] ?? '')));
|
||||||
$arrItem['path'] = trim((string)($arrItem['path'] ?? ''));
|
$arrItem['path'] = trim((string)($arrItem['path'] ?? ''));
|
||||||
$arrItem['page_type'] = DomainSpiderCrawlLogSchemaHelper::normalizePageType((string)($arrItem['page_type'] ?? 'other'));
|
$arrItem['page_type'] = DomainSpiderCrawlLogSchemaHelper::normalizePageType((string)($arrItem['page_type'] ?? 'other'));
|
||||||
|
if ($arrItem['page_type'] === 'other') {
|
||||||
|
$arrItem['page_type'] = DomainSpiderCrawlLogSchemaHelper::inferPageTypeFromPath((string)$arrItem['path'], 'other');
|
||||||
|
}
|
||||||
$arrItem['bot_name'] = DomainSpiderCrawlLogSchemaHelper::normalizeBotName((string)($arrItem['bot_name'] ?? 'other_bot'));
|
$arrItem['bot_name'] = DomainSpiderCrawlLogSchemaHelper::normalizeBotName((string)($arrItem['bot_name'] ?? 'other_bot'));
|
||||||
$arrItem['status'] = (int)($arrItem['status'] ?? 0);
|
$arrItem['status'] = (int)($arrItem['status'] ?? 0);
|
||||||
$arrItem['count'] = max(0, (int)($arrItem['count'] ?? 0));
|
$arrItem['count'] = max(0, (int)($arrItem['count'] ?? 0));
|
||||||
@@ -26,6 +29,9 @@ class DomainSpiderCrawlLogHelper
|
|||||||
$arrItem['host'] = strtolower(trim((string)($arrItem['host'] ?? '')));
|
$arrItem['host'] = strtolower(trim((string)($arrItem['host'] ?? '')));
|
||||||
$arrItem['url'] = trim((string)($arrItem['url'] ?? ''));
|
$arrItem['url'] = trim((string)($arrItem['url'] ?? ''));
|
||||||
$arrItem['page_type'] = DomainSpiderCrawlLogSchemaHelper::normalizePageType((string)($arrItem['page_type'] ?? 'other'));
|
$arrItem['page_type'] = DomainSpiderCrawlLogSchemaHelper::normalizePageType((string)($arrItem['page_type'] ?? 'other'));
|
||||||
|
if ($arrItem['page_type'] === 'other') {
|
||||||
|
$arrItem['page_type'] = DomainSpiderCrawlLogSchemaHelper::inferPageTypeFromPath((string)$arrItem['url'], 'other');
|
||||||
|
}
|
||||||
$arrItem['bot_name'] = DomainSpiderCrawlLogSchemaHelper::normalizeBotName((string)($arrItem['bot_name'] ?? 'other_bot'));
|
$arrItem['bot_name'] = DomainSpiderCrawlLogSchemaHelper::normalizeBotName((string)($arrItem['bot_name'] ?? 'other_bot'));
|
||||||
$arrItem['status'] = (int)($arrItem['status'] ?? 0);
|
$arrItem['status'] = (int)($arrItem['status'] ?? 0);
|
||||||
$arrItem['cache_status'] = self::normalizeCacheStatus((string)($arrItem['cache_status'] ?? ''));
|
$arrItem['cache_status'] = self::normalizeCacheStatus((string)($arrItem['cache_status'] ?? ''));
|
||||||
|
|||||||
@@ -202,6 +202,60 @@ class DomainSpiderCrawlLogSchemaHelper
|
|||||||
return in_array($strPageType, self::buildSchema()['page_type_enum'], true) ? $strPageType : 'other';
|
return in_array($strPageType, self::buildSchema()['page_type_enum'], true) ? $strPageType : 'other';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function inferPageTypeFromPath(string $strPath, string $strFallback = 'other'): string
|
||||||
|
{
|
||||||
|
$strPath = strtolower(trim($strPath));
|
||||||
|
if ($strPath === '') {
|
||||||
|
return self::normalizePageType($strFallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
$strPath = parse_url($strPath, PHP_URL_PATH) ?: $strPath;
|
||||||
|
$strPath = '/' . ltrim($strPath, '/');
|
||||||
|
$strNormalized = trim($strPath, '/');
|
||||||
|
|
||||||
|
if ($strNormalized === '' || $strNormalized === 'index.php') {
|
||||||
|
return 'home';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($strNormalized === 'robots.txt') {
|
||||||
|
return 'robots';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($strNormalized, 'sitemap')) {
|
||||||
|
return 'sitemap';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($strNormalized, 'search') || str_starts_with($strNormalized, 'get-index')) {
|
||||||
|
return 'search';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
str_contains($strNormalized, 'videotype')
|
||||||
|
|| str_contains($strNormalized, 'vodtype')
|
||||||
|
|| str_contains($strNormalized, 'fenlei')
|
||||||
|
) {
|
||||||
|
return 'category';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
str_starts_with($strNormalized, 'voddetail/')
|
||||||
|
|| str_starts_with($strNormalized, 'video-info/')
|
||||||
|
|| str_starts_with($strNormalized, 'neirong-')
|
||||||
|
) {
|
||||||
|
return 'detail';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
str_starts_with($strNormalized, 'vodplay/')
|
||||||
|
|| str_starts_with($strNormalized, 'video-play/')
|
||||||
|
|| str_starts_with($strNormalized, 'bf-')
|
||||||
|
) {
|
||||||
|
return 'play';
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::normalizePageType($strFallback);
|
||||||
|
}
|
||||||
|
|
||||||
public static function normalizeBotName(string $strBotName): string
|
public static function normalizeBotName(string $strBotName): string
|
||||||
{
|
{
|
||||||
$strValue = strtolower(trim($strBotName));
|
$strValue = strtolower(trim($strBotName));
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ namespace app\common\helper;
|
|||||||
|
|
||||||
class DomainSpiderCrawlWorkbenchHelper
|
class DomainSpiderCrawlWorkbenchHelper
|
||||||
{
|
{
|
||||||
|
protected const MAX_AGGREGATE_RUNS = 12;
|
||||||
|
|
||||||
protected static function codeRootFromStorageRoot(string $storageRoot): string
|
protected static function codeRootFromStorageRoot(string $storageRoot): string
|
||||||
{
|
{
|
||||||
return rtrim(dirname(rtrim($storageRoot, '/')), '/');
|
return rtrim(dirname(rtrim($storageRoot, '/')), '/');
|
||||||
@@ -308,7 +310,7 @@ class DomainSpiderCrawlWorkbenchHelper
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$items[] = [
|
$items[] = [
|
||||||
'summary' => $summary,
|
'summary_path' => (string)$summaryPath,
|
||||||
'normalized' => self::normalizeRunSummary($publicRoot, $summary, (string)$summaryPath),
|
'normalized' => self::normalizeRunSummary($publicRoot, $summary, (string)$summaryPath),
|
||||||
'generated_at_ts' => $generatedTimestamp,
|
'generated_at_ts' => $generatedTimestamp,
|
||||||
];
|
];
|
||||||
@@ -336,7 +338,14 @@ class DomainSpiderCrawlWorkbenchHelper
|
|||||||
$anomalyRecords = [];
|
$anomalyRecords = [];
|
||||||
$latestGeneratedAt = '';
|
$latestGeneratedAt = '';
|
||||||
foreach ($runEntries as $entry) {
|
foreach ($runEntries as $entry) {
|
||||||
$summary = is_array($entry['summary'] ?? null) ? (array)$entry['summary'] : [];
|
$summaryPath = trim((string)($entry['summary_path'] ?? ''));
|
||||||
|
if ($summaryPath === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$summary = self::readJsonFile($summaryPath);
|
||||||
|
if (empty($summary)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$generatedAt = trim((string)($summary['generated_at'] ?? ''));
|
$generatedAt = trim((string)($summary['generated_at'] ?? ''));
|
||||||
if ($generatedAt !== '' && ($latestGeneratedAt === '' || strcmp($generatedAt, $latestGeneratedAt) > 0)) {
|
if ($generatedAt !== '' && ($latestGeneratedAt === '' || strcmp($generatedAt, $latestGeneratedAt) > 0)) {
|
||||||
$latestGeneratedAt = $generatedAt;
|
$latestGeneratedAt = $generatedAt;
|
||||||
@@ -363,6 +372,15 @@ class DomainSpiderCrawlWorkbenchHelper
|
|||||||
return $aggregated;
|
return $aggregated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static function limitAggregateRunEntries(array $runEntries): array
|
||||||
|
{
|
||||||
|
if (count($runEntries) <= self::MAX_AGGREGATE_RUNS) {
|
||||||
|
return $runEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_slice($runEntries, 0, self::MAX_AGGREGATE_RUNS);
|
||||||
|
}
|
||||||
|
|
||||||
protected static function buildRecentRuns(array $runEntries, int $limit): array
|
protected static function buildRecentRuns(array $runEntries, int $limit): array
|
||||||
{
|
{
|
||||||
$items = [];
|
$items = [];
|
||||||
@@ -993,7 +1011,7 @@ class DomainSpiderCrawlWorkbenchHelper
|
|||||||
$rows = [];
|
$rows = [];
|
||||||
foreach ($windows as $hours) {
|
foreach ($windows as $hours) {
|
||||||
$runEntries = self::collectRunEntries($publicRoot, $runsRoot, $hours);
|
$runEntries = self::collectRunEntries($publicRoot, $runsRoot, $hours);
|
||||||
$summary = self::aggregateRunSummaries($runEntries);
|
$summary = self::aggregateRunSummaries(self::limitAggregateRunEntries($runEntries));
|
||||||
$summary = self::filterSummaryByBotScope($summary, $botScope);
|
$summary = self::filterSummaryByBotScope($summary, $botScope);
|
||||||
$summary = self::filterSummaryBySelectedBots($summary, $selectedBots);
|
$summary = self::filterSummaryBySelectedBots($summary, $selectedBots);
|
||||||
$metrics = self::summarizeWindow($summary, $host);
|
$metrics = self::summarizeWindow($summary, $host);
|
||||||
@@ -1241,7 +1259,7 @@ class DomainSpiderCrawlWorkbenchHelper
|
|||||||
$recentRuns = self::buildRecentRuns($runEntries, $runLimit);
|
$recentRuns = self::buildRecentRuns($runEntries, $runLimit);
|
||||||
|
|
||||||
if ($windowHours > 0 && !empty($runEntries)) {
|
if ($windowHours > 0 && !empty($runEntries)) {
|
||||||
$latestSummary = self::aggregateRunSummaries($runEntries);
|
$latestSummary = self::aggregateRunSummaries(self::limitAggregateRunEntries($runEntries));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($latestSummary) && !empty($recentRuns)) {
|
if (empty($latestSummary) && !empty($recentRuns)) {
|
||||||
|
|||||||
@@ -17,20 +17,33 @@ class DomainSpiderMdRunHelper
|
|||||||
return dirname(__DIR__, 3) . '/storage/spider_md_runs';
|
return dirname(__DIR__, 3) . '/storage/spider_md_runs';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function activeRunRoot(): string
|
||||||
|
{
|
||||||
|
return self::baseRoot() . '/active';
|
||||||
|
}
|
||||||
|
|
||||||
public static function buildSummary(int $limit = 20): array
|
public static function buildSummary(int $limit = 20): array
|
||||||
{
|
{
|
||||||
$limit = max(1, min(100, $limit));
|
$limit = max(1, min(100, $limit));
|
||||||
$runs = [];
|
$runs = [];
|
||||||
SpiderMdConfigHelper::ensureSystemConfigDefaults();
|
SpiderMdConfigHelper::ensureSystemConfigDefaults();
|
||||||
$gitee = self::resolveGiteeConfig();
|
$gitee = self::resolveGiteeConfig();
|
||||||
|
$github = self::resolveGithubConfig();
|
||||||
unset($gitee['token']);
|
unset($gitee['token']);
|
||||||
$matches = glob(self::baseRoot() . '/*/*/summary.json');
|
unset($github['token']);
|
||||||
if (is_array($matches)) {
|
$activeSummaryPath = self::activeRunRoot() . '/summary.json';
|
||||||
rsort($matches);
|
$activePayload = self::readJsonFile($activeSummaryPath);
|
||||||
foreach (array_slice($matches, 0, $limit) as $summaryPath) {
|
if (!empty($activePayload)) {
|
||||||
$payload = self::readJsonFile((string)$summaryPath);
|
$runs[] = $activePayload;
|
||||||
if (!empty($payload)) {
|
} else {
|
||||||
$runs[] = $payload;
|
$matches = glob(self::baseRoot() . '/*/*/summary.json');
|
||||||
|
if (is_array($matches)) {
|
||||||
|
rsort($matches);
|
||||||
|
foreach (array_slice($matches, 0, $limit) as $summaryPath) {
|
||||||
|
$payload = self::readJsonFile((string)$summaryPath);
|
||||||
|
if (!empty($payload)) {
|
||||||
|
$runs[] = $payload;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,6 +52,7 @@ class DomainSpiderMdRunHelper
|
|||||||
'generated_at' => date(DATE_ATOM),
|
'generated_at' => date(DATE_ATOM),
|
||||||
'run_root' => self::baseRoot(),
|
'run_root' => self::baseRoot(),
|
||||||
'gitee' => $gitee,
|
'gitee' => $gitee,
|
||||||
|
'github' => $github,
|
||||||
'runtime_config' => self::resolveRuntimeConfig(),
|
'runtime_config' => self::resolveRuntimeConfig(),
|
||||||
'config_summary' => SpiderMdConfigHelper::buildConfigSummary(),
|
'config_summary' => SpiderMdConfigHelper::buildConfigSummary(),
|
||||||
'latest_run' => $runs[0] ?? null,
|
'latest_run' => $runs[0] ?? null,
|
||||||
@@ -50,51 +64,38 @@ class DomainSpiderMdRunHelper
|
|||||||
{
|
{
|
||||||
$config = self::resolveRuntimeConfig($options);
|
$config = self::resolveRuntimeConfig($options);
|
||||||
$giteeConfig = self::resolveGiteeConfig();
|
$giteeConfig = self::resolveGiteeConfig();
|
||||||
|
$githubConfig = self::resolveGithubConfig();
|
||||||
self::ensureDir(self::baseRoot());
|
self::ensureDir(self::baseRoot());
|
||||||
|
self::ensureDir(self::activeRunRoot());
|
||||||
$forceRegenerate = !empty($options['force_regenerate']);
|
$forceRegenerate = !empty($options['force_regenerate']);
|
||||||
$previous = self::findLatestSuccessfulRunSummary();
|
$previous = self::findLatestSuccessfulRunSummary();
|
||||||
$today = date('Ymd');
|
$remoteTargetChanged = !empty($previous)
|
||||||
$giteeTargetChanged = !empty($previous) ? self::giteeTargetChanged($previous, $giteeConfig) : false;
|
? (self::giteeTargetChanged($previous, $giteeConfig) || self::githubTargetChanged($previous, $githubConfig))
|
||||||
|
: false;
|
||||||
$previousGiteePushStatus = trim((string)($previous['gitee_push_status'] ?? ''));
|
$previousGiteePushStatus = trim((string)($previous['gitee_push_status'] ?? ''));
|
||||||
$allowRetryToday = in_array($previousGiteePushStatus, ['failed', 'partial_failed'], true);
|
$previousGithubPushStatus = trim((string)($previous['github_push_status'] ?? ''));
|
||||||
|
$allowRetryPush = in_array($previousGiteePushStatus, ['failed', 'partial_failed'], true)
|
||||||
|
|| in_array($previousGithubPushStatus, ['failed', 'partial_failed'], true);
|
||||||
|
$forceFullRemoteSync = $forceRegenerate || empty($previous) || $remoteTargetChanged || $allowRetryPush;
|
||||||
|
|
||||||
if (
|
if ($forceRegenerate || empty($previous)) {
|
||||||
!$forceRegenerate
|
|
||||||
&& !empty($previous)
|
|
||||||
&& substr((string)($previous['generated_at'] ?? ''), 0, 10) === date('Y-m-d')
|
|
||||||
&& !$giteeTargetChanged
|
|
||||||
&& !$allowRetryToday
|
|
||||||
) {
|
|
||||||
self::log($logger, '今日已生成过蜘蛛池 MD,跳过本轮;如需全新生成请使用强制重生成');
|
|
||||||
$previous['status'] = 'skipped';
|
|
||||||
$previous['skipped_reason'] = 'already_generated_today';
|
|
||||||
$previous['runtime_config'] = $config;
|
|
||||||
return $previous;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($forceRegenerate || empty($previous) || $giteeTargetChanged) {
|
|
||||||
if ($forceRegenerate) {
|
if ($forceRegenerate) {
|
||||||
self::clearAllRuns();
|
self::clearAllRuns();
|
||||||
} elseif ($giteeTargetChanged) {
|
|
||||||
self::log($logger, '检测到 Gitee 推送目标已变更,本轮按全新生成处理');
|
|
||||||
}
|
}
|
||||||
$dateDir = self::baseRoot() . '/' . $today;
|
|
||||||
self::ensureDir($dateDir);
|
|
||||||
$runId = date('His') . '_spider_md_' . substr(md5(uniqid('', true)), 0, 6);
|
$runId = date('His') . '_spider_md_' . substr(md5(uniqid('', true)), 0, 6);
|
||||||
$runRoot = $dateDir . '/' . $runId;
|
$runRoot = self::activeRunRoot();
|
||||||
self::ensureDir($runRoot);
|
self::ensureDir($runRoot);
|
||||||
self::log($logger, $forceRegenerate ? '开始强制重生成蜘蛛池 MD' : '开始首次生成蜘蛛池 MD');
|
self::log($logger, $forceRegenerate ? '开始强制重生成蜘蛛池 MD' : '开始首次生成蜘蛛池 MD');
|
||||||
} else {
|
} else {
|
||||||
$runId = (string)($previous['run_id'] ?? '');
|
$runId = date('His') . '_spider_md_' . substr(md5(uniqid('', true)), 0, 6);
|
||||||
$runRoot = (string)($previous['run_root'] ?? '');
|
$runRoot = self::activeRunRoot();
|
||||||
if ($runId === '' || $runRoot === '') {
|
|
||||||
throw new \RuntimeException('历史蜘蛛池MD运行目录无效,无法续写');
|
|
||||||
}
|
|
||||||
self::ensureDir($runRoot);
|
self::ensureDir($runRoot);
|
||||||
if ($allowRetryToday) {
|
self::log($logger, '开始增量续写蜘蛛池 MD');
|
||||||
self::log($logger, '检测到上一轮 Gitee 推送失败,本轮继续沿用当前目录并重试推送');
|
if ($remoteTargetChanged) {
|
||||||
} else {
|
self::log($logger, '检测到远端推送目标已变更,本轮保留现有分片,仅执行全量远端同步');
|
||||||
self::log($logger, '开始增量续写蜘蛛池 MD');
|
}
|
||||||
|
if ($allowRetryPush) {
|
||||||
|
self::log($logger, '检测到上一轮远端推送失败,本轮保留现有分片并重试远端同步');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +106,7 @@ class DomainSpiderMdRunHelper
|
|||||||
$existingState = self::loadExistingRunState($runRoot, (int)$config['max_links_per_file']);
|
$existingState = self::loadExistingRunState($runRoot, (int)$config['max_links_per_file']);
|
||||||
$aggregateLinks = [];
|
$aggregateLinks = [];
|
||||||
$writtenFiles = (array)($existingState['files'] ?? []);
|
$writtenFiles = (array)($existingState['files'] ?? []);
|
||||||
|
$changedFiles = [];
|
||||||
$chunkState = [
|
$chunkState = [
|
||||||
'records' => (array)($existingState['pending_records'] ?? []),
|
'records' => (array)($existingState['pending_records'] ?? []),
|
||||||
'max_links_per_file' => (int)$config['max_links_per_file'],
|
'max_links_per_file' => (int)$config['max_links_per_file'],
|
||||||
@@ -112,6 +114,7 @@ class DomainSpiderMdRunHelper
|
|||||||
'generated_at' => date('Y-m-d H:i:s'),
|
'generated_at' => date('Y-m-d H:i:s'),
|
||||||
'run_root' => $runRoot,
|
'run_root' => $runRoot,
|
||||||
'files' => &$writtenFiles,
|
'files' => &$writtenFiles,
|
||||||
|
'changed_files' => &$changedFiles,
|
||||||
'known_urls' => (array)($existingState['known_urls'] ?? []),
|
'known_urls' => (array)($existingState['known_urls'] ?? []),
|
||||||
'reused_last_file' => (string)($existingState['reused_last_file'] ?? ''),
|
'reused_last_file' => (string)($existingState['reused_last_file'] ?? ''),
|
||||||
];
|
];
|
||||||
@@ -147,10 +150,14 @@ class DomainSpiderMdRunHelper
|
|||||||
/** @var DomainModel $domain */
|
/** @var DomainModel $domain */
|
||||||
$domain = $domainRow['domain'];
|
$domain = $domainRow['domain'];
|
||||||
$forgePolicy = $domain->getForgeSeoCfg();
|
$forgePolicy = $domain->getForgeSeoCfg();
|
||||||
|
$existingDetailCount = (int)(($existingState['detail_count_by_host'] ?? [])[$host] ?? 0);
|
||||||
|
$existingForgeCount = (int)(($existingState['forge_count_by_host'] ?? [])[$host] ?? 0);
|
||||||
|
$detailLimit = (int)$config['detail_limit_per_host'];
|
||||||
|
$forgeLimit = (int)$config['forge_limit_per_host'];
|
||||||
$domainProgress[$host] = [
|
$domainProgress[$host] = [
|
||||||
'row' => $domainRow,
|
'row' => $domainRow,
|
||||||
'detail_remaining' => (int)$config['detail_limit_per_host'] > 0 ? (int)$config['detail_limit_per_host'] : null,
|
'detail_remaining' => $detailLimit > 0 ? max(0, $detailLimit - $existingDetailCount) : null,
|
||||||
'forge_remaining' => (int)$config['forge_limit_per_host'] > 0 ? (int)$config['forge_limit_per_host'] : null,
|
'forge_remaining' => $forgeLimit > 0 ? max(0, $forgeLimit - $existingForgeCount) : null,
|
||||||
'forge_per_video' => max(0, (int)($forgePolicy['sitemap_count'] ?? 0)),
|
'forge_per_video' => max(0, (int)($forgePolicy['sitemap_count'] ?? 0)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -208,6 +215,7 @@ class DomainSpiderMdRunHelper
|
|||||||
'max_links_per_file' => (int)$config['max_links_per_file'],
|
'max_links_per_file' => (int)$config['max_links_per_file'],
|
||||||
'config' => $config,
|
'config' => $config,
|
||||||
'mode' => $forceRegenerate || empty($previous) ? 'rebuild' : 'append',
|
'mode' => $forceRegenerate || empty($previous) ? 'rebuild' : 'append',
|
||||||
|
'remote_mode' => 'fixed-root-incremental',
|
||||||
'new_links' => [
|
'new_links' => [
|
||||||
'aggregate_count' => $newAggregateCount,
|
'aggregate_count' => $newAggregateCount,
|
||||||
'detail_count' => $newDetailCount,
|
'detail_count' => $newDetailCount,
|
||||||
@@ -215,19 +223,23 @@ class DomainSpiderMdRunHelper
|
|||||||
'total_links' => $newAggregateCount + $newDetailCount + $newForgeCount,
|
'total_links' => $newAggregateCount + $newDetailCount + $newForgeCount,
|
||||||
],
|
],
|
||||||
'files' => $writtenFiles,
|
'files' => $writtenFiles,
|
||||||
|
'changed_files' => array_values(array_unique($changedFiles)),
|
||||||
|
'full_remote_sync' => $forceFullRemoteSync ? 1 : 0,
|
||||||
'gitee' => array_diff_key($giteeConfig, ['token' => true]),
|
'gitee' => array_diff_key($giteeConfig, ['token' => true]),
|
||||||
|
'github' => array_diff_key($githubConfig, ['token' => true]),
|
||||||
'gitee_files' => [],
|
'gitee_files' => [],
|
||||||
'gitee_pushed' => 0,
|
'gitee_pushed' => 0,
|
||||||
'gitee_push_attempted' => 0,
|
'gitee_push_attempted' => 0,
|
||||||
'gitee_push_status' => 'pending',
|
'gitee_push_status' => 'pending',
|
||||||
'gitee_push_error' => '',
|
'gitee_push_error' => '',
|
||||||
|
'github_files' => [],
|
||||||
|
'github_pushed' => 0,
|
||||||
|
'github_push_attempted' => 0,
|
||||||
|
'github_push_status' => 'pending',
|
||||||
|
'github_push_error' => '',
|
||||||
];
|
];
|
||||||
|
|
||||||
file_put_contents(
|
self::writeSummaryArtifacts($runRoot, $summary);
|
||||||
$runRoot . '/summary.json',
|
|
||||||
json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
|
||||||
);
|
|
||||||
file_put_contents($runRoot . '/summary.html', self::renderSummaryHtml($summary));
|
|
||||||
|
|
||||||
$giteePush = self::pushRunToGitee($summary, $logger);
|
$giteePush = self::pushRunToGitee($summary, $logger);
|
||||||
$summary['gitee_files'] = (array)($giteePush['items'] ?? []);
|
$summary['gitee_files'] = (array)($giteePush['items'] ?? []);
|
||||||
@@ -235,22 +247,20 @@ class DomainSpiderMdRunHelper
|
|||||||
$summary['gitee_push_attempted'] = (int)($giteePush['attempted'] ?? 0);
|
$summary['gitee_push_attempted'] = (int)($giteePush['attempted'] ?? 0);
|
||||||
$summary['gitee_push_status'] = (string)($giteePush['status'] ?? 'skipped');
|
$summary['gitee_push_status'] = (string)($giteePush['status'] ?? 'skipped');
|
||||||
$summary['gitee_push_error'] = (string)($giteePush['error'] ?? '');
|
$summary['gitee_push_error'] = (string)($giteePush['error'] ?? '');
|
||||||
|
$githubPush = self::pushRunToGithub($summary, $logger);
|
||||||
|
$summary['github_files'] = (array)($githubPush['items'] ?? []);
|
||||||
|
$summary['github_pushed'] = !empty($summary['github_files']) ? 1 : 0;
|
||||||
|
$summary['github_push_attempted'] = (int)($githubPush['attempted'] ?? 0);
|
||||||
|
$summary['github_push_status'] = (string)($githubPush['status'] ?? 'skipped');
|
||||||
|
$summary['github_push_error'] = (string)($githubPush['error'] ?? '');
|
||||||
|
|
||||||
file_put_contents(
|
self::writeLinkArtifacts($runRoot, $summary);
|
||||||
$runRoot . '/gitee-links.json',
|
|
||||||
json_encode([
|
|
||||||
'run_id' => $runId,
|
|
||||||
'generated_at' => date(DATE_ATOM),
|
|
||||||
'status' => $summary['gitee_push_status'],
|
|
||||||
'error' => $summary['gitee_push_error'],
|
|
||||||
'items' => $summary['gitee_files'],
|
|
||||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
|
||||||
);
|
|
||||||
file_put_contents(
|
file_put_contents(
|
||||||
$runRoot . '/summary.json',
|
$runRoot . '/summary.json',
|
||||||
json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
||||||
);
|
);
|
||||||
file_put_contents($runRoot . '/summary.html', self::renderSummaryHtml($summary));
|
file_put_contents($runRoot . '/summary.html', self::renderSummaryHtml($summary));
|
||||||
|
self::syncMetadataToRemotes($summary, $logger);
|
||||||
|
|
||||||
self::log($logger, '蜘蛛池 MD 生成完成,共 ' . count($writtenFiles) . ' 个文件');
|
self::log($logger, '蜘蛛池 MD 生成完成,共 ' . count($writtenFiles) . ' 个文件');
|
||||||
|
|
||||||
@@ -579,6 +589,7 @@ class DomainSpiderMdRunHelper
|
|||||||
return (string)($item['section_label'] ?? '');
|
return (string)($item['section_label'] ?? '');
|
||||||
}, $state['records']))),
|
}, $state['records']))),
|
||||||
];
|
];
|
||||||
|
$state['changed_files'][] = $fileName;
|
||||||
|
|
||||||
$state['records'] = [];
|
$state['records'] = [];
|
||||||
$state['reused_last_file'] = '';
|
$state['reused_last_file'] = '';
|
||||||
@@ -586,6 +597,11 @@ class DomainSpiderMdRunHelper
|
|||||||
|
|
||||||
protected static function findLatestSuccessfulRunSummary(): array
|
protected static function findLatestSuccessfulRunSummary(): array
|
||||||
{
|
{
|
||||||
|
$activeSummary = self::readJsonFile(self::activeRunRoot() . '/summary.json');
|
||||||
|
if (!empty($activeSummary) && (string)($activeSummary['status'] ?? '') === 'success') {
|
||||||
|
return $activeSummary;
|
||||||
|
}
|
||||||
|
|
||||||
$matches = glob(self::baseRoot() . '/*/*/summary.json');
|
$matches = glob(self::baseRoot() . '/*/*/summary.json');
|
||||||
if (!is_array($matches) || empty($matches)) {
|
if (!is_array($matches) || empty($matches)) {
|
||||||
return [];
|
return [];
|
||||||
@@ -644,9 +660,45 @@ class DomainSpiderMdRunHelper
|
|||||||
'aggregate_count' => (int)($summary['aggregate_count'] ?? 0),
|
'aggregate_count' => (int)($summary['aggregate_count'] ?? 0),
|
||||||
'detail_count' => (int)($summary['detail_count'] ?? 0),
|
'detail_count' => (int)($summary['detail_count'] ?? 0),
|
||||||
'forge_count' => (int)($summary['forge_count'] ?? 0),
|
'forge_count' => (int)($summary['forge_count'] ?? 0),
|
||||||
|
'detail_count_by_host' => self::countExistingSectionByHost($files, $pendingRecords, 'detail'),
|
||||||
|
'forge_count_by_host' => self::countExistingSectionByHost($files, $pendingRecords, 'forge'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static function countExistingSectionByHost(array $files, array $pendingRecords, string $section): array
|
||||||
|
{
|
||||||
|
$counts = [];
|
||||||
|
foreach ($files as $fileMeta) {
|
||||||
|
$filePath = (string)($fileMeta['file_path'] ?? '');
|
||||||
|
if ($filePath === '' || !is_file($filePath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach (self::parseMarkdownChunkRecords($filePath) as $record) {
|
||||||
|
if ((string)($record['section'] ?? '') !== $section) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$host = trim((string)($record['host'] ?? ''));
|
||||||
|
if ($host === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$counts[$host] = (int)($counts[$host] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($pendingRecords as $record) {
|
||||||
|
if ((string)($record['section'] ?? '') !== $section) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$host = trim((string)($record['host'] ?? ''));
|
||||||
|
if ($host === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$counts[$host] = (int)($counts[$host] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $counts;
|
||||||
|
}
|
||||||
|
|
||||||
protected static function parseMarkdownChunkRecords(string $filePath): array
|
protected static function parseMarkdownChunkRecords(string $filePath): array
|
||||||
{
|
{
|
||||||
$lines = @file($filePath, FILE_IGNORE_NEW_LINES);
|
$lines = @file($filePath, FILE_IGNORE_NEW_LINES);
|
||||||
@@ -792,6 +844,8 @@ class DomainSpiderMdRunHelper
|
|||||||
foreach (glob(self::baseRoot() . '/*') ?: [] as $dateDir) {
|
foreach (glob(self::baseRoot() . '/*') ?: [] as $dateDir) {
|
||||||
self::deletePath((string)$dateDir);
|
self::deletePath((string)$dateDir);
|
||||||
}
|
}
|
||||||
|
self::deletePath(self::activeRunRoot());
|
||||||
|
self::ensureDir(self::activeRunRoot());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static function deletePath(string $path): void
|
protected static function deletePath(string $path): void
|
||||||
@@ -925,23 +979,8 @@ class DomainSpiderMdRunHelper
|
|||||||
}
|
}
|
||||||
|
|
||||||
$items = [];
|
$items = [];
|
||||||
$dateDir = basename(dirname((string)($summary['run_root'] ?? '')));
|
|
||||||
if (!preg_match('/^\d{8}$/', $dateDir)) {
|
|
||||||
$dateDir = date('Ymd', strtotime((string)($summary['generated_at'] ?? 'now')));
|
|
||||||
}
|
|
||||||
$baseRemoteDir = trim((string)$config['root'], '/');
|
$baseRemoteDir = trim((string)$config['root'], '/');
|
||||||
$baseRemoteDir = trim($baseRemoteDir . '/' . $dateDir . '/' . (string)($summary['run_id'] ?? ''), '/');
|
$localFiles = self::resolveIncrementalLocalFiles($summary);
|
||||||
|
|
||||||
$localFiles = array_merge(
|
|
||||||
array_map(static function (array $file): string {
|
|
||||||
return (string)($file['file_path'] ?? '');
|
|
||||||
}, (array)($summary['files'] ?? [])),
|
|
||||||
[
|
|
||||||
(string)($summary['run_root'] ?? '') . '/summary.json',
|
|
||||||
(string)($summary['run_root'] ?? '') . '/summary.html',
|
|
||||||
(string)($summary['run_root'] ?? '') . '/gitee-links.json',
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
foreach ($localFiles as $filePath) {
|
foreach ($localFiles as $filePath) {
|
||||||
@@ -977,7 +1016,7 @@ class DomainSpiderMdRunHelper
|
|||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'attempted' => 1,
|
'attempted' => 1,
|
||||||
'error' => '',
|
'error' => '',
|
||||||
'items' => $items,
|
'items' => self::buildRemoteItems($summary, $config, 'gitee'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1094,6 +1133,378 @@ class DomainSpiderMdRunHelper
|
|||||||
return SpiderMdConfigHelper::resolveGiteeConfig();
|
return SpiderMdConfigHelper::resolveGiteeConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static function resolveGithubConfig(): array
|
||||||
|
{
|
||||||
|
return SpiderMdConfigHelper::resolveGithubConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function githubTargetChanged(array $previous, array $currentConfig): bool
|
||||||
|
{
|
||||||
|
$previousGithub = (array)($previous['github'] ?? []);
|
||||||
|
foreach (['enabled', 'owner', 'repo', 'branch', 'root'] as $key) {
|
||||||
|
if ((string)($previousGithub[$key] ?? '') !== (string)($currentConfig[$key] ?? '')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function pushRunToGithub(array $summary, ?callable $logger = null): array
|
||||||
|
{
|
||||||
|
$config = self::resolveGithubConfig();
|
||||||
|
if (empty($config['enabled']) || empty($config['configured'])) {
|
||||||
|
self::log($logger, 'GitHub 未启用或未配置,本轮跳过 GitHub 推送');
|
||||||
|
return [
|
||||||
|
'status' => 'skipped',
|
||||||
|
'attempted' => 0,
|
||||||
|
'error' => '',
|
||||||
|
'items' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = [];
|
||||||
|
$baseRemoteDir = trim((string)$config['root'], '/');
|
||||||
|
$localFiles = self::resolveIncrementalLocalFiles($summary);
|
||||||
|
|
||||||
|
try {
|
||||||
|
foreach ($localFiles as $filePath) {
|
||||||
|
if (!is_file($filePath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileName = basename($filePath);
|
||||||
|
$remotePath = $baseRemoteDir . '/' . $fileName;
|
||||||
|
$response = self::upsertGithubFile($config, $remotePath, (string)file_get_contents($filePath), 'spider md update ' . $fileName);
|
||||||
|
|
||||||
|
$items[] = [
|
||||||
|
'file_name' => $fileName,
|
||||||
|
'remote_path' => $remotePath,
|
||||||
|
'html_url' => (string)($response['html_url'] ?? self::buildGithubHtmlUrl($config, $remotePath)),
|
||||||
|
'download_url' => (string)($response['download_url'] ?? self::buildGithubRawUrl($config, $remotePath)),
|
||||||
|
'local_path' => $filePath,
|
||||||
|
];
|
||||||
|
self::log($logger, '已推送 GitHub:' . $fileName);
|
||||||
|
}
|
||||||
|
} catch (\Throwable $throwable) {
|
||||||
|
$message = trim($throwable->getMessage());
|
||||||
|
self::log($logger, 'GitHub 推送失败:' . $message);
|
||||||
|
return [
|
||||||
|
'status' => !empty($items) ? 'partial_failed' : 'failed',
|
||||||
|
'attempted' => 1,
|
||||||
|
'error' => $message,
|
||||||
|
'items' => $items,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => 'success',
|
||||||
|
'attempted' => 1,
|
||||||
|
'error' => '',
|
||||||
|
'items' => self::buildRemoteItems($summary, $config, 'github'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function resolveIncrementalLocalFiles(array $summary): array
|
||||||
|
{
|
||||||
|
$changedFileNames = array_values(array_unique(array_filter(array_map(static function ($item): string {
|
||||||
|
return trim((string)$item);
|
||||||
|
}, (array)($summary['changed_files'] ?? [])))));
|
||||||
|
$allFiles = array_values(array_filter(array_map(static function (array $file): string {
|
||||||
|
return (string)($file['file_path'] ?? '');
|
||||||
|
}, (array)($summary['files'] ?? []))));
|
||||||
|
|
||||||
|
if (!empty($summary['full_remote_sync'])) {
|
||||||
|
return $allFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($changedFileNames)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$selected = [];
|
||||||
|
foreach ((array)($summary['files'] ?? []) as $file) {
|
||||||
|
$fileName = trim((string)($file['file_name'] ?? ''));
|
||||||
|
$filePath = (string)($file['file_path'] ?? '');
|
||||||
|
if ($fileName === '' || $filePath === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (in_array($fileName, $changedFileNames, true)) {
|
||||||
|
$selected[] = $filePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($selected));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function buildRemoteItems(array $summary, array $config, string $provider): array
|
||||||
|
{
|
||||||
|
$baseRemoteDir = trim((string)($config['root'] ?? ''), '/');
|
||||||
|
$items = [];
|
||||||
|
$fileNames = [];
|
||||||
|
foreach ((array)($summary['files'] ?? []) as $file) {
|
||||||
|
$fileName = trim((string)($file['file_name'] ?? ''));
|
||||||
|
$filePath = (string)($file['file_path'] ?? '');
|
||||||
|
if ($fileName === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$fileNames[] = [
|
||||||
|
'file_name' => $fileName,
|
||||||
|
'local_path' => $filePath,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$fileNames[] = ['file_name' => 'summary.json', 'local_path' => (string)($summary['run_root'] ?? '') . '/summary.json'];
|
||||||
|
$fileNames[] = ['file_name' => 'summary.html', 'local_path' => (string)($summary['run_root'] ?? '') . '/summary.html'];
|
||||||
|
$fileNames[] = ['file_name' => $provider . '-links.json', 'local_path' => (string)($summary['run_root'] ?? '') . '/' . $provider . '-links.json'];
|
||||||
|
|
||||||
|
foreach ($fileNames as $fileMeta) {
|
||||||
|
$fileName = (string)($fileMeta['file_name'] ?? '');
|
||||||
|
if ($fileName === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$remotePath = trim($baseRemoteDir . '/' . $fileName, '/');
|
||||||
|
$items[] = [
|
||||||
|
'file_name' => $fileName,
|
||||||
|
'remote_path' => $remotePath,
|
||||||
|
'html_url' => $provider === 'github'
|
||||||
|
? self::buildGithubHtmlUrl($config, $remotePath)
|
||||||
|
: self::buildGiteeHtmlUrl($config, $remotePath),
|
||||||
|
'download_url' => $provider === 'github'
|
||||||
|
? self::buildGithubRawUrl($config, $remotePath)
|
||||||
|
: self::buildGiteeRawUrl($config, $remotePath),
|
||||||
|
'local_path' => (string)($fileMeta['local_path'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function writeSummaryArtifacts(string $runRoot, array $summary): void
|
||||||
|
{
|
||||||
|
file_put_contents(
|
||||||
|
$runRoot . '/summary.json',
|
||||||
|
json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
||||||
|
);
|
||||||
|
file_put_contents($runRoot . '/summary.html', self::renderSummaryHtml($summary));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function writeLinkArtifacts(string $runRoot, array $summary): void
|
||||||
|
{
|
||||||
|
file_put_contents(
|
||||||
|
$runRoot . '/gitee-links.json',
|
||||||
|
json_encode([
|
||||||
|
'run_id' => (string)($summary['run_id'] ?? ''),
|
||||||
|
'generated_at' => date(DATE_ATOM),
|
||||||
|
'status' => (string)($summary['gitee_push_status'] ?? 'pending'),
|
||||||
|
'error' => (string)($summary['gitee_push_error'] ?? ''),
|
||||||
|
'items' => (array)($summary['gitee_files'] ?? []),
|
||||||
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
||||||
|
);
|
||||||
|
file_put_contents(
|
||||||
|
$runRoot . '/github-links.json',
|
||||||
|
json_encode([
|
||||||
|
'run_id' => (string)($summary['run_id'] ?? ''),
|
||||||
|
'generated_at' => date(DATE_ATOM),
|
||||||
|
'status' => (string)($summary['github_push_status'] ?? 'pending'),
|
||||||
|
'error' => (string)($summary['github_push_error'] ?? ''),
|
||||||
|
'items' => (array)($summary['github_files'] ?? []),
|
||||||
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function syncMetadataToRemotes(array $summary, ?callable $logger = null): void
|
||||||
|
{
|
||||||
|
self::syncMetadataToGitee($summary, $logger);
|
||||||
|
self::syncMetadataToGithub($summary, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function syncMetadataToGitee(array $summary, ?callable $logger = null): void
|
||||||
|
{
|
||||||
|
$config = self::resolveGiteeConfig();
|
||||||
|
if (empty($config['enabled']) || empty($config['configured'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$baseRemoteDir = trim((string)$config['root'], '/');
|
||||||
|
foreach (['summary.json', 'summary.html', 'gitee-links.json'] as $fileName) {
|
||||||
|
$filePath = (string)($summary['run_root'] ?? '') . '/' . $fileName;
|
||||||
|
if (!is_file($filePath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$remotePath = trim($baseRemoteDir . '/' . $fileName, '/');
|
||||||
|
self::upsertGiteeFile($config, $remotePath, (string)file_get_contents($filePath), 'spider md metadata update ' . $fileName);
|
||||||
|
self::log($logger, '已更新 Gitee 元数据:' . $fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function syncMetadataToGithub(array $summary, ?callable $logger = null): void
|
||||||
|
{
|
||||||
|
$config = self::resolveGithubConfig();
|
||||||
|
if (empty($config['enabled']) || empty($config['configured'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$baseRemoteDir = trim((string)$config['root'], '/');
|
||||||
|
foreach (['summary.json', 'summary.html', 'github-links.json'] as $fileName) {
|
||||||
|
$filePath = (string)($summary['run_root'] ?? '') . '/' . $fileName;
|
||||||
|
if (!is_file($filePath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$remotePath = trim($baseRemoteDir . '/' . $fileName, '/');
|
||||||
|
self::upsertGithubFile($config, $remotePath, (string)file_get_contents($filePath), 'spider md metadata update ' . $fileName);
|
||||||
|
self::log($logger, '已更新 GitHub 元数据:' . $fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function upsertGithubFile(array $config, string $remotePath, string $content, string $message): array
|
||||||
|
{
|
||||||
|
$existing = self::githubRequest(
|
||||||
|
'GET',
|
||||||
|
sprintf(
|
||||||
|
'https://api.github.com/repos/%s/%s/contents/%s?ref=%s',
|
||||||
|
rawurlencode((string)$config['owner']),
|
||||||
|
rawurlencode((string)$config['repo']),
|
||||||
|
str_replace('%2F', '/', rawurlencode($remotePath)),
|
||||||
|
rawurlencode((string)$config['branch'])
|
||||||
|
),
|
||||||
|
$config,
|
||||||
|
[],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'message' => $message,
|
||||||
|
'content' => base64_encode($content),
|
||||||
|
'branch' => (string)$config['branch'],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!empty($existing['sha'])) {
|
||||||
|
$payload['sha'] = (string)$existing['sha'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::githubRequest(
|
||||||
|
'PUT',
|
||||||
|
sprintf(
|
||||||
|
'https://api.github.com/repos/%s/%s/contents/%s',
|
||||||
|
rawurlencode((string)$config['owner']),
|
||||||
|
rawurlencode((string)$config['repo']),
|
||||||
|
str_replace('%2F', '/', rawurlencode($remotePath))
|
||||||
|
),
|
||||||
|
$config,
|
||||||
|
$payload
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function githubRequest(string $method, string $url, array $config, array $payload = [], bool $allowNotFound = false): array
|
||||||
|
{
|
||||||
|
if (!function_exists('curl_init')) {
|
||||||
|
throw new \RuntimeException('当前 PHP 环境缺少 curl 扩展,无法推送 GitHub');
|
||||||
|
}
|
||||||
|
$headers = [
|
||||||
|
'Accept: application/vnd.github+json',
|
||||||
|
'Authorization: Bearer ' . (string)$config['token'],
|
||||||
|
'X-GitHub-Api-Version: 2022-11-28',
|
||||||
|
'User-Agent: SEONexus-SpiderMd',
|
||||||
|
];
|
||||||
|
|
||||||
|
$maxAttempts = 3;
|
||||||
|
$lastError = '';
|
||||||
|
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
|
||||||
|
$curl = curl_init();
|
||||||
|
if ($curl === false) {
|
||||||
|
throw new \RuntimeException('初始化 GitHub 请求失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_setopt_array($curl, [
|
||||||
|
CURLOPT_URL => $url,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 90,
|
||||||
|
CURLOPT_CUSTOMREQUEST => $method,
|
||||||
|
CURLOPT_HTTPHEADER => $headers,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!empty($payload)) {
|
||||||
|
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = curl_exec($curl);
|
||||||
|
$errno = curl_errno($curl);
|
||||||
|
$error = curl_error($curl);
|
||||||
|
$status = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($curl);
|
||||||
|
|
||||||
|
if ($errno !== 0) {
|
||||||
|
$lastError = 'GitHub 请求失败:' . $error;
|
||||||
|
if ($attempt < $maxAttempts) {
|
||||||
|
usleep(300000 * $attempt);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new \RuntimeException($lastError);
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode((string)$response, true);
|
||||||
|
if ($allowNotFound && $status === 404) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status >= 400) {
|
||||||
|
$message = '';
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
$message = trim((string)($decoded['message'] ?? ''));
|
||||||
|
if ($message === '') {
|
||||||
|
$message = json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$message = trim((string)$response);
|
||||||
|
}
|
||||||
|
if ($message === '') {
|
||||||
|
$message = 'HTTP ' . $status . ',响应体为空';
|
||||||
|
} else {
|
||||||
|
$message = 'HTTP ' . $status . ',' . $message;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastError = 'GitHub 请求返回异常:' . $message;
|
||||||
|
if (in_array($status, [429, 500, 502, 503, 504], true) && $attempt < $maxAttempts) {
|
||||||
|
usleep(500000 * $attempt);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new \RuntimeException($lastError);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($decoded) && isset($decoded['content']) && is_array($decoded['content'])) {
|
||||||
|
$decoded['html_url'] = (string)($decoded['content']['html_url'] ?? '');
|
||||||
|
$decoded['download_url'] = (string)($decoded['content']['download_url'] ?? '');
|
||||||
|
$decoded['sha'] = (string)($decoded['content']['sha'] ?? ($decoded['sha'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_array($decoded) ? $decoded : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \RuntimeException($lastError !== '' ? $lastError : 'GitHub 请求失败:未知异常');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function buildGithubHtmlUrl(array $config, string $remotePath): string
|
||||||
|
{
|
||||||
|
return sprintf(
|
||||||
|
'https://github.com/%s/%s/blob/%s/%s',
|
||||||
|
rawurlencode((string)$config['owner']),
|
||||||
|
rawurlencode((string)$config['repo']),
|
||||||
|
rawurlencode((string)$config['branch']),
|
||||||
|
str_replace('%2F', '/', rawurlencode($remotePath))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function buildGithubRawUrl(array $config, string $remotePath): string
|
||||||
|
{
|
||||||
|
return sprintf(
|
||||||
|
'https://raw.githubusercontent.com/%s/%s/%s/%s',
|
||||||
|
rawurlencode((string)$config['owner']),
|
||||||
|
rawurlencode((string)$config['repo']),
|
||||||
|
rawurlencode((string)$config['branch']),
|
||||||
|
str_replace('%2F', '/', rawurlencode($remotePath))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
protected static function readJsonFile(string $path): array
|
protected static function readJsonFile(string $path): array
|
||||||
{
|
{
|
||||||
if (!is_file($path)) {
|
if (!is_file($path)) {
|
||||||
|
|||||||
@@ -8,6 +8,92 @@ use PhpOffice\PhpSpreadsheet\IOFactory;
|
|||||||
|
|
||||||
class DomainSupplyBatchHelper
|
class DomainSupplyBatchHelper
|
||||||
{
|
{
|
||||||
|
protected static function isSampleField(string $field): bool
|
||||||
|
{
|
||||||
|
return in_array($field, [
|
||||||
|
'detail_id',
|
||||||
|
'detail_slug',
|
||||||
|
'search_keyword',
|
||||||
|
'category_parent',
|
||||||
|
'category_child',
|
||||||
|
], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function fillAutoDiscoveredSample(array $row): array
|
||||||
|
{
|
||||||
|
$host = self::hostLabelFromRow($row);
|
||||||
|
if ($host === '') {
|
||||||
|
return [
|
||||||
|
'row' => $row,
|
||||||
|
'status' => 'skipped',
|
||||||
|
'message' => 'host_empty',
|
||||||
|
'filled_fields' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$needsSample = false;
|
||||||
|
foreach ([
|
||||||
|
'detail_id',
|
||||||
|
'detail_slug',
|
||||||
|
'search_keyword',
|
||||||
|
'category_parent',
|
||||||
|
'category_child',
|
||||||
|
] as $field) {
|
||||||
|
if (trim((string)($row[$field] ?? '')) === '') {
|
||||||
|
$needsSample = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$needsSample) {
|
||||||
|
return [
|
||||||
|
'row' => $row,
|
||||||
|
'status' => 'not_needed',
|
||||||
|
'message' => 'sample_fields_already_present',
|
||||||
|
'filled_fields' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$sampleResult = DomainAutoSampleHelper::discover($host, ['limit' => 80]);
|
||||||
|
if (($sampleResult['status'] ?? '') !== 'passed' || empty($sampleResult['sample']) || !is_array($sampleResult['sample'])) {
|
||||||
|
return [
|
||||||
|
'row' => $row,
|
||||||
|
'status' => 'failed',
|
||||||
|
'message' => (string)($sampleResult['message'] ?? 'auto_sample_discovery_failed'),
|
||||||
|
'filled_fields' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$sample = (array)$sampleResult['sample'];
|
||||||
|
$filledFields = [];
|
||||||
|
$mapping = [
|
||||||
|
'detail_id' => (string)($sample['detail']['detail_id'] ?? ''),
|
||||||
|
'detail_slug' => (string)($sample['detail']['detail_slug'] ?? ''),
|
||||||
|
'search_keyword' => (string)($sample['search']['search_keyword'] ?? ''),
|
||||||
|
'category_parent' => (string)($sample['category']['category_parent'] ?? ''),
|
||||||
|
'category_child' => (string)($sample['category']['category_child'] ?? ''),
|
||||||
|
'play_type' => (string)($sample['play']['play_type'] ?? ''),
|
||||||
|
'play_index' => (string)($sample['play']['play_index'] ?? ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($mapping as $field => $value) {
|
||||||
|
if ($value === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (trim((string)($row[$field] ?? '')) !== '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$row[$field] = $value;
|
||||||
|
$filledFields[] = $field;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'row' => $row,
|
||||||
|
'status' => empty($filledFields) ? 'failed' : 'filled',
|
||||||
|
'message' => empty($filledFields) ? 'auto_sample_discovery_returned_empty_fields' : 'auto_sample_discovered',
|
||||||
|
'filled_fields' => $filledFields,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
protected static function publicRelativeToAbsolutePath(string $path): string
|
protected static function publicRelativeToAbsolutePath(string $path): string
|
||||||
{
|
{
|
||||||
$path = str_replace('\\', '/', trim($path));
|
$path = str_replace('\\', '/', trim($path));
|
||||||
@@ -122,12 +208,28 @@ class DomainSupplyBatchHelper
|
|||||||
throw new \RuntimeException('Replay template rows are empty: ' . $summaryPath);
|
throw new \RuntimeException('Replay template rows are empty: ' . $summaryPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$headerLabels = DomainSupplyBatchTemplateHelper::getHeaderLabels();
|
||||||
|
$displayRows = array_map(
|
||||||
|
static function (array $row) use ($headerLabels): array {
|
||||||
|
$displayRow = [];
|
||||||
|
foreach (DomainSupplyBatchTemplateHelper::getHeaders() as $field) {
|
||||||
|
$displayRow[$headerLabels[$field] ?? $field] = (string)($row[$field] ?? '');
|
||||||
|
}
|
||||||
|
return $displayRow;
|
||||||
|
},
|
||||||
|
$rows
|
||||||
|
);
|
||||||
|
$displayNotes = [];
|
||||||
|
foreach (DomainSupplyBatchTemplateHelper::getColumnNotes() as $field => $note) {
|
||||||
|
$displayNotes[$headerLabels[$field] ?? $field] = $note;
|
||||||
|
}
|
||||||
|
|
||||||
DomainBatchImportTemplateHelper::writeWorkbook(
|
DomainBatchImportTemplateHelper::writeWorkbook(
|
||||||
$outputPath,
|
$outputPath,
|
||||||
'domain-supply-batch',
|
'domain-supply-batch',
|
||||||
DomainSupplyBatchTemplateHelper::getHeaders(),
|
DomainSupplyBatchTemplateHelper::getDisplayHeaders(),
|
||||||
$rows,
|
$displayRows,
|
||||||
DomainSupplyBatchTemplateHelper::getColumnNotes()
|
$displayNotes
|
||||||
);
|
);
|
||||||
|
|
||||||
return $outputPath;
|
return $outputPath;
|
||||||
@@ -164,7 +266,9 @@ class DomainSupplyBatchHelper
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$headers = array_map(static fn ($value): string => trim((string)$value), (array)array_shift($rows));
|
$headers = array_map(static function ($value): string {
|
||||||
|
return DomainSupplyBatchTemplateHelper::normalizeHeader(trim((string)$value));
|
||||||
|
}, (array)array_shift($rows));
|
||||||
$normalized = [];
|
$normalized = [];
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$assoc = [];
|
$assoc = [];
|
||||||
@@ -362,9 +466,19 @@ class DomainSupplyBatchHelper
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$autoSample = self::fillAutoDiscoveredSample($row);
|
||||||
|
$row = (array)($autoSample['row'] ?? $row);
|
||||||
$missing = self::validateRow($row);
|
$missing = self::validateRow($row);
|
||||||
$status = empty($missing) ? 'ready' : 'failed';
|
$status = empty($missing) ? 'ready' : 'failed';
|
||||||
$message = empty($missing) ? 'prepared' : ('missing:' . implode(',', $missing));
|
$message = empty($missing) ? 'prepared' : ('missing:' . implode(',', $missing));
|
||||||
|
if (($autoSample['status'] ?? '') === 'filled') {
|
||||||
|
$message = 'auto_sample_filled:' . implode(',', (array)($autoSample['filled_fields'] ?? []));
|
||||||
|
} elseif (($autoSample['status'] ?? '') === 'failed') {
|
||||||
|
$sampleMissing = array_values(array_filter($missing, static fn(string $field): bool => self::isSampleField($field)));
|
||||||
|
if (!empty($sampleMissing)) {
|
||||||
|
$message .= ';auto_sample_failed:' . (string)($autoSample['message'] ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$siteRow = self::buildSiteImportRow($row);
|
$siteRow = self::buildSiteImportRow($row);
|
||||||
$bootstrapRow = self::buildBootstrapBatchRow($row);
|
$bootstrapRow = self::buildBootstrapBatchRow($row);
|
||||||
@@ -385,6 +499,9 @@ class DomainSupplyBatchHelper
|
|||||||
'site_domain' => (string)($siteRow['d_domain'] ?? ''),
|
'site_domain' => (string)($siteRow['d_domain'] ?? ''),
|
||||||
'bundle_host' => (string)($bootstrapRow['host'] ?? ''),
|
'bundle_host' => (string)($bootstrapRow['host'] ?? ''),
|
||||||
'missing_fields' => $missing,
|
'missing_fields' => $missing,
|
||||||
|
'auto_sample_status' => (string)($autoSample['status'] ?? ''),
|
||||||
|
'auto_sample_message' => (string)($autoSample['message'] ?? ''),
|
||||||
|
'auto_sample_filled_fields' => array_values((array)($autoSample['filled_fields'] ?? [])),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,61 +12,145 @@ class DomainSupplyBatchTemplateHelper
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'host',
|
'host',
|
||||||
'template_id',
|
|
||||||
'strategy_profile',
|
|
||||||
'match_type',
|
|
||||||
'parent_domain',
|
|
||||||
'seed_scope',
|
|
||||||
'site_name',
|
'site_name',
|
||||||
'site_keywords',
|
'template_id',
|
||||||
'site_description',
|
|
||||||
'site_index_title',
|
'site_index_title',
|
||||||
'site_index_keywords',
|
'site_index_keywords',
|
||||||
'site_index_description',
|
'site_index_description',
|
||||||
'baidu_token',
|
'strategy_profile',
|
||||||
'seo_cfg_json',
|
|
||||||
'detail_id',
|
'detail_id',
|
||||||
'detail_slug',
|
'detail_slug',
|
||||||
'search_keyword',
|
'search_keyword',
|
||||||
'category_parent',
|
'category_parent',
|
||||||
'category_child',
|
'category_child',
|
||||||
|
'site_keywords',
|
||||||
|
'site_description',
|
||||||
|
'baidu_token',
|
||||||
|
'match_type',
|
||||||
|
'parent_domain',
|
||||||
|
'seed_scope',
|
||||||
'play_type',
|
'play_type',
|
||||||
'play_index',
|
'play_index',
|
||||||
'forge_index',
|
'forge_index',
|
||||||
'rank_slug',
|
'rank_slug',
|
||||||
'base_url',
|
'base_url',
|
||||||
'bundle_name',
|
'bundle_name',
|
||||||
|
'seo_cfg_json',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getHeaderLabels(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'host' => '域名',
|
||||||
|
'site_name' => '站点名称',
|
||||||
|
'template_id' => '模板ID',
|
||||||
|
'site_index_title' => '首页标题',
|
||||||
|
'site_index_keywords' => '首页关键词',
|
||||||
|
'site_index_description' => '首页描述',
|
||||||
|
'strategy_profile' => 'SEO策略',
|
||||||
|
'detail_id' => '详情样本ID',
|
||||||
|
'detail_slug' => '详情样本Slug',
|
||||||
|
'search_keyword' => '搜索样本关键词',
|
||||||
|
'category_parent' => '父分类Slug',
|
||||||
|
'category_child' => '子分类Slug',
|
||||||
|
'site_keywords' => '站点关键词',
|
||||||
|
'site_description' => '站点描述',
|
||||||
|
'baidu_token' => '百度推送Token',
|
||||||
|
'match_type' => '匹配类型',
|
||||||
|
'parent_domain' => '父域名',
|
||||||
|
'seed_scope' => '种子范围',
|
||||||
|
'play_type' => '播放类型',
|
||||||
|
'play_index' => '播放序号',
|
||||||
|
'forge_index' => '锻造序号',
|
||||||
|
'rank_slug' => '榜单Slug',
|
||||||
|
'base_url' => '站点基础地址',
|
||||||
|
'bundle_name' => '自定义Bundle名称',
|
||||||
|
'seo_cfg_json' => '高级SEO配置JSON',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getHeaderAliases(): array
|
||||||
|
{
|
||||||
|
$labels = self::getHeaderLabels();
|
||||||
|
$aliases = [];
|
||||||
|
foreach ($labels as $field => $label) {
|
||||||
|
$aliases[$field] = $field;
|
||||||
|
$aliases[mb_strtolower($field)] = $field;
|
||||||
|
$aliases[$label] = $field;
|
||||||
|
$aliases[mb_strtolower($label)] = $field;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
'站点域名' => 'host',
|
||||||
|
'域名host' => 'host',
|
||||||
|
'策略' => 'strategy_profile',
|
||||||
|
'首页SEO标题' => 'site_index_title',
|
||||||
|
'首页SEO关键词' => 'site_index_keywords',
|
||||||
|
'首页SEO描述' => 'site_index_description',
|
||||||
|
'搜索关键词' => 'search_keyword',
|
||||||
|
'父分类' => 'category_parent',
|
||||||
|
'子分类' => 'category_child',
|
||||||
|
'基础地址' => 'base_url',
|
||||||
|
'高级配置JSON' => 'seo_cfg_json',
|
||||||
|
] as $alias => $field) {
|
||||||
|
$aliases[$alias] = $field;
|
||||||
|
$aliases[mb_strtolower($alias)] = $field;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $aliases;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function normalizeHeader(string $header): string
|
||||||
|
{
|
||||||
|
$normalized = trim($header);
|
||||||
|
if ($normalized === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$aliases = self::getHeaderAliases();
|
||||||
|
|
||||||
|
return $aliases[$normalized] ?? $aliases[mb_strtolower($normalized)] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDisplayHeaders(): array
|
||||||
|
{
|
||||||
|
$labels = self::getHeaderLabels();
|
||||||
|
|
||||||
|
return array_map(
|
||||||
|
static fn(string $field): string => $labels[$field] ?? $field,
|
||||||
|
self::getHeaders()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public static function getColumnNotes(): array
|
public static function getColumnNotes(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'host' => '必填,域名或 wildcard 域名;会同时用于站点导入和 bundle 生成。',
|
'host' => '必填。填写站点正式域名,不要带 https:// 。示例:jingxifa.com。只有做泛域名时,才填 *.example.com。',
|
||||||
'template_id' => '必填,站点模板 ID,默认 1。',
|
'site_name' => '必填。填写站点前台展示名称,会用于页头、页脚和基础 SEO 文案。示例:鲸溪发影视。',
|
||||||
'strategy_profile' => '可选,standard / traffic / expand / closed,默认 standard。',
|
'template_id' => "通常直接填 1。\n可选值说明:\n- 1:当前默认 GPT 模板。绝大多数站都用这个。\n- 其他数字:代表后台另有模板配置,只有技术明确指定时才改。\n不确定就填 1。",
|
||||||
'match_type' => '可选,exact 或 wildcard,默认 exact。',
|
'site_index_title' => '建议填写。首页给搜索引擎看的标题,尽量自然,不要堆词。建议 28 到 40 字。',
|
||||||
'parent_domain' => 'wildcard 模式建议填写根域名。',
|
'site_index_keywords' => '建议填写。首页核心关键词,多个词用逗号分隔。示例:短剧推荐,热播短剧,高清短剧。',
|
||||||
'seed_scope' => '可选,domain 或 host,默认 domain。',
|
'site_index_description' => '建议填写。首页一句话简介,建议 40 到 80 字,写自然一点。',
|
||||||
'site_name' => '可选,站点名称。',
|
'strategy_profile' => "SEO 策略档位,默认 standard。\n可选值说明:\n- standard:标准站,最常用默认值,先求稳定收录和正常运营时选它。\n- traffic:流量型,更偏向做关键词覆盖和流量页扩展。\n- expand:扩展型,更适合泛域名、子站批量扩展。\n- closed:收缩/关闭型,通常用于只保留最小更新动作的站。\n不确定就填 standard。",
|
||||||
'site_keywords' => '可选,站点关键词。',
|
'detail_id' => '通常不用手填。站点部署成功后,系统会优先通过自动样本发现,从当前站点可用内容里自动提取。只有域名还没部署成功、当前只是试跑库、或自动发现失败时,才人工补填真实详情视频 ID。',
|
||||||
'site_description' => '可选,站点描述。',
|
'detail_slug' => '通常不用手填。会和详情样本 ID 一起自动获取。若人工填写,必须和详情样本 ID 对应,不能乱配。',
|
||||||
'site_index_title' => '可选,首页标题;不填时导入链按站点名称推导。',
|
'search_keyword' => '通常不用手填。系统会优先从站点名称、视频 SEO 词、视频标题里自动挑一个可搜索关键词。只有自动发现失败时,再人工填写一个能搜出结果的词。',
|
||||||
'site_index_keywords' => '可选,首页关键词;不填时导入链按站点关键词推导。',
|
'category_parent' => '通常不用手填。系统会从自动样本视频所属分类里提取一级分类 slug。只有自动发现失败时,再人工补填。示例:dian-ying。',
|
||||||
'site_index_description' => '可选,首页描述;不填时导入链按站点描述推导。',
|
'category_child' => '通常不用手填。系统会从自动样本视频所属分类里提取二级分类 slug。只有自动发现失败时,再人工补填。示例:ju-qing-pian。',
|
||||||
'baidu_token' => '可选,百度推送 token。',
|
'site_keywords' => '可选。不填也能跑;如要补充站点级关键词,可填在这里。',
|
||||||
'seo_cfg_json' => '可选,JSON;会在 strategy_profile 默认值基础上继续覆盖。',
|
'site_description' => '可选。不填时系统会按站点名称自动兜底一版简介。',
|
||||||
'detail_id' => '必填,启动包详情样本视频 ID。',
|
'baidu_token' => '可选。只有需要百度主动推送时才填写。',
|
||||||
'detail_slug' => '必填,启动包详情样本 slug。',
|
'match_type' => "域名匹配方式,默认 exact。\n可选值说明:\n- exact:精确匹配。普通单域名站使用这个,例如 jingxifa.com。\n- wildcard:泛匹配。只有泛域名场景才用,例如 *.example.com。\n不确定就保持 exact。",
|
||||||
'search_keyword' => '必填,启动包搜索页样本关键词。',
|
'parent_domain' => "父域名只在 wildcard 泛域名场景下填写。\n填写规则:\n- host 是 *.example.com 时,这里填 example.com。\n- host 是普通单域名时留空。\n不要填协议、路径或 www。",
|
||||||
'category_parent' => '必填,父分类 slug。',
|
'seed_scope' => "种子范围,默认 domain。\n可选值说明:\n- domain:按整域名范围处理,普通站点默认用这个。\n- host:按具体 host 处理,常见于泛域名、子站拆分运营场景。\n不确定就填 domain。",
|
||||||
'category_child' => '必填,子分类 slug。',
|
'play_type' => "播放类型。通常可留空,自动样本成功时系统会自动带出。\n常见可选值说明:\n- douban:常见默认线路,绝大多数站留空后最终也会落到它。\n- youzhi:另一种播放线路标识,只有样本明确属于这条线路时才填。\n- 其他值:必须与站内真实播放线路一致,不能凭感觉乱写。\n不确定就留空,优先让系统自动发现。",
|
||||||
'play_type' => '可选,默认 douban。',
|
'play_index' => "播放序号,默认 1。\n可选值说明:\n- 1:第一条播放线路下的第一集/第一入口,最常用。\n- 2、3、4...:只有你明确知道该样本要绑定到第几集或第几个入口时才改。\n不确定就填 1 或留空。",
|
||||||
'play_index' => '可选,默认 1。',
|
'forge_index' => "锻造序号,默认 1。\n可选值说明:\n- 1:默认主锻造位,绝大多数站都用这个。\n- 其他数字:只有技术明确安排了多锻造位时才改。\n运营通常保持 1 即可。",
|
||||||
'forge_index' => '可选,默认 1。',
|
'rank_slug' => "榜单 slug,默认 daily。\n可选值说明:\n- daily:日榜,最常用默认值。\n- weekly:周榜,只有明确要挂周榜时才用。\n- monthly:月榜,只有明确要挂月榜时才用。\n- 其他 slug:必须后台已有对应榜单配置,否则不要乱填。\n不确定就填 daily。",
|
||||||
'rank_slug' => '可选,默认 daily。',
|
'base_url' => '可选。默认自动按 https://域名 生成。只有特殊环境才手填。',
|
||||||
'base_url' => '可选,默认 https://<host>。',
|
'bundle_name' => '可选。一般留空,系统会按域名自动生成 bundle 目录名。',
|
||||||
'bundle_name' => '可选,自定义 bundle 目录名。',
|
'seo_cfg_json' => '可选。高级配置区,通常给技术同学使用。运营不需要填写;只有明确要覆盖默认 SEO 策略时再填 JSON。',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,8 +218,50 @@ class DomainSupplyBatchTemplateHelper
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'headers' => self::getHeaders(),
|
'headers' => self::getHeaders(),
|
||||||
|
'display_headers' => self::getDisplayHeaders(),
|
||||||
|
'header_labels' => self::getHeaderLabels(),
|
||||||
'column_notes' => self::getColumnNotes(),
|
'column_notes' => self::getColumnNotes(),
|
||||||
'example_rows' => self::getExampleRows(),
|
'example_rows' => self::getExampleRows(),
|
||||||
|
'template_type' => 'gpt_supply_batch',
|
||||||
|
'filling_guidance' => [
|
||||||
|
'main_fields' => [
|
||||||
|
'host',
|
||||||
|
'site_name',
|
||||||
|
'template_id',
|
||||||
|
'site_index_title',
|
||||||
|
'site_index_keywords',
|
||||||
|
'site_index_description',
|
||||||
|
'strategy_profile',
|
||||||
|
],
|
||||||
|
'auto_discovery_fields' => [
|
||||||
|
'detail_id',
|
||||||
|
'detail_slug',
|
||||||
|
'search_keyword',
|
||||||
|
'category_parent',
|
||||||
|
'category_child',
|
||||||
|
],
|
||||||
|
'optional_fields' => [
|
||||||
|
'site_keywords',
|
||||||
|
'site_description',
|
||||||
|
'baidu_token',
|
||||||
|
'match_type',
|
||||||
|
'parent_domain',
|
||||||
|
'seed_scope',
|
||||||
|
'play_type',
|
||||||
|
'play_index',
|
||||||
|
'forge_index',
|
||||||
|
'rank_slug',
|
||||||
|
'base_url',
|
||||||
|
'bundle_name',
|
||||||
|
'seo_cfg_json',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'operator_notice' => [
|
||||||
|
'运营通常只需要先填写主填写区。',
|
||||||
|
'详情样本ID、详情样本Slug、搜索样本关键词、父分类Slug、子分类Slug 通常可以留空。',
|
||||||
|
'这些样本字段会在域名部署成功或试跑库有可用内容时,优先自动发现并补全。',
|
||||||
|
'只有自动发现失败时,才需要人工补填样本字段。',
|
||||||
|
],
|
||||||
'supported_strategy_profiles' => [
|
'supported_strategy_profiles' => [
|
||||||
DomainModel::SEO_STRATEGY_PROFILE_STANDARD,
|
DomainModel::SEO_STRATEGY_PROFILE_STANDARD,
|
||||||
DomainModel::SEO_STRATEGY_PROFILE_TRAFFIC,
|
DomainModel::SEO_STRATEGY_PROFILE_TRAFFIC,
|
||||||
@@ -165,12 +291,29 @@ class DomainSupplyBatchTemplateHelper
|
|||||||
$fileName = 'domain-supply-batch-template.xlsx';
|
$fileName = 'domain-supply-batch-template.xlsx';
|
||||||
$filePath = $outputRoot . '/' . $fileName;
|
$filePath = $outputRoot . '/' . $fileName;
|
||||||
|
|
||||||
|
$headerLabels = self::getHeaderLabels();
|
||||||
|
$displayHeaders = self::getDisplayHeaders();
|
||||||
|
$displayRows = array_map(
|
||||||
|
static function (array $row) use ($headerLabels): array {
|
||||||
|
$displayRow = [];
|
||||||
|
foreach (self::getHeaders() as $field) {
|
||||||
|
$displayRow[$headerLabels[$field] ?? $field] = (string)($row[$field] ?? '');
|
||||||
|
}
|
||||||
|
return $displayRow;
|
||||||
|
},
|
||||||
|
self::getExampleRows()
|
||||||
|
);
|
||||||
|
$displayNotes = [];
|
||||||
|
foreach (self::getColumnNotes() as $field => $note) {
|
||||||
|
$displayNotes[DomainBatchImportTemplateHelper::buildNoteDisplayLabel($field)] = $note;
|
||||||
|
}
|
||||||
|
|
||||||
DomainBatchImportTemplateHelper::writeWorkbook(
|
DomainBatchImportTemplateHelper::writeWorkbook(
|
||||||
$filePath,
|
$filePath,
|
||||||
'domain-supply-batch',
|
'domain-supply-batch',
|
||||||
self::getHeaders(),
|
$displayHeaders,
|
||||||
self::getExampleRows(),
|
$displayRows,
|
||||||
self::getColumnNotes()
|
$displayNotes
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -27,14 +27,6 @@ class JsBuilder
|
|||||||
@mkdir($targetDir, 0755, true);
|
@mkdir($targetDir, 0755, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 输出文件名(域名专属缓存)
|
|
||||||
$targetFile = $targetDir . "{$staticHash}.js";
|
|
||||||
|
|
||||||
// 如果已经生成过,直接返回
|
|
||||||
if (file_exists($targetFile)) {
|
|
||||||
return "/static/js/compiled/{$staticHash}.js";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 你可以像 CSS 一样在这里挂一个“全站基础 JS”
|
// 你可以像 CSS 一样在这里挂一个“全站基础 JS”
|
||||||
// 注意:如果没有这个文件,就别写入,否则会被 skip
|
// 注意:如果没有这个文件,就别写入,否则会被 skip
|
||||||
$listJs = [
|
$listJs = [
|
||||||
@@ -46,9 +38,46 @@ class JsBuilder
|
|||||||
$finalJsFiles = array_merge($listJs, $jsFiles);
|
$finalJsFiles = array_merge($listJs, $jsFiles);
|
||||||
$finalJsFiles = array_values(array_unique(array_filter($finalJsFiles)));
|
$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 文件内容
|
// 合并 JS 文件内容
|
||||||
$allJs = "";
|
$allJs = "";
|
||||||
$allJs .= "/*! compiled: {$staticHash}.js */\n";
|
$allJs .= "/*! compiled: {$targetName} */\n";
|
||||||
$allJs .= "(function(){\n'use strict';\n";
|
$allJs .= "(function(){\n'use strict';\n";
|
||||||
|
|
||||||
foreach ($finalJsFiles as $file) {
|
foreach ($finalJsFiles as $file) {
|
||||||
@@ -82,6 +111,6 @@ class JsBuilder
|
|||||||
file_put_contents($targetFile, $allJs);
|
file_put_contents($targetFile, $allJs);
|
||||||
|
|
||||||
// 返回前端可访问路径
|
// 返回前端可访问路径
|
||||||
return "/static/js/compiled/{$staticHash}.js";
|
return "/static/js/compiled/{$targetName}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ use app\model\DomainModel;
|
|||||||
|
|
||||||
class SeoCopyAiProviderHelper
|
class SeoCopyAiProviderHelper
|
||||||
{
|
{
|
||||||
|
public static function buildSharedAiRuleBlock(): string
|
||||||
|
{
|
||||||
|
return SeoCopyAiRuleConfigHelper::buildSharedRuleBlock();
|
||||||
|
}
|
||||||
|
|
||||||
public static function resolveProviderConfig(?string $strPreferredProvider = null): array
|
public static function resolveProviderConfig(?string $strPreferredProvider = null): array
|
||||||
{
|
{
|
||||||
$strDefaultProvider = self::readEnv('SEO_COPY_AI_PROVIDER', DomainModel::TKD_PROVIDER_LOCAL);
|
$strDefaultProvider = self::readEnv('SEO_COPY_AI_PROVIDER', DomainModel::TKD_PROVIDER_LOCAL);
|
||||||
@@ -699,7 +704,12 @@ class SeoCopyAiProviderHelper
|
|||||||
'content' => [
|
'content' => [
|
||||||
[
|
[
|
||||||
'type' => 'input_text',
|
'type' => 'input_text',
|
||||||
'text' => '你是资深中文 SEO 编辑,目标是在不虚构事实的前提下,输出适合影视站首页、分类页、搜索页、详情页、播放页的高相关、高可读、强点击意图文案。输出必须是合法 JSON。'
|
'text' => implode("\n", [
|
||||||
|
'你是资深中文 SEO 编辑,目标是在不虚构事实的前提下,输出适合影视站首页、分类页、搜索页、详情页、播放页的高相关、高可读、强点击意图文案。',
|
||||||
|
'输出必须是合法 JSON。',
|
||||||
|
'',
|
||||||
|
self::buildSharedAiRuleBlock(),
|
||||||
|
]),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -791,6 +801,10 @@ class SeoCopyAiProviderHelper
|
|||||||
'2. detail 描述不要只改写公共简介,要更像资料页摘要,先告诉用户这页能解决什么,再补事实线索。',
|
'2. detail 描述不要只改写公共简介,要更像资料页摘要,先告诉用户这页能解决什么,再补事实线索。',
|
||||||
'3. play 描述不要只写在线播放,要带回详情路径、线路判断和必要的事实线索。',
|
'3. play 描述不要只写在线播放,要带回详情路径、线路判断和必要的事实线索。',
|
||||||
'4. 不要虚构具体剧情细节;如果缺少事实,就用更保守的资料页表达。',
|
'4. 不要虚构具体剧情细节;如果缺少事实,就用更保守的资料页表达。',
|
||||||
|
'5. detail.description_template、play.description_template 优先按“影视详情页摘要规则”执行。',
|
||||||
|
'6. detail.detail_body_lead/detail_body_tail、play.play_body_lead/play_body_next 优先按“影视详情页正文补全规则”执行。',
|
||||||
|
'',
|
||||||
|
self::buildSharedAiRuleBlock(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1332,24 +1346,12 @@ class SeoCopyAiProviderHelper
|
|||||||
|
|
||||||
protected static function pickThemeKeywords(array $arrResourceContext, string $strFallback, int $intLimit = 4): array
|
protected static function pickThemeKeywords(array $arrResourceContext, string $strFallback, int $intLimit = 4): array
|
||||||
{
|
{
|
||||||
$arrOut = [];
|
return SeoCopyContextHelper::normalizeKeywordCandidates(array_merge(
|
||||||
foreach (array_merge(
|
|
||||||
[$strFallback],
|
[$strFallback],
|
||||||
(array)($arrResourceContext['priority_keywords'] ?? []),
|
(array)($arrResourceContext['priority_keywords'] ?? []),
|
||||||
(array)($arrResourceContext['feedback_keywords'] ?? []),
|
(array)($arrResourceContext['feedback_keywords'] ?? []),
|
||||||
(array)($arrResourceContext['topics'] ?? [])
|
(array)($arrResourceContext['topics'] ?? [])
|
||||||
) as $strItem) {
|
), [], $intLimit);
|
||||||
$strItem = trim((string)$strItem);
|
|
||||||
if ($strItem === '' || in_array($strItem, $arrOut, true)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$arrOut[] = $strItem;
|
|
||||||
if (count($arrOut) >= $intLimit) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $arrOut;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static function pickString(array $arrPool, int $intSeed, string $strSalt): string
|
protected static function pickString(array $arrPool, int $intSeed, string $strSalt): string
|
||||||
|
|||||||
196
code/app/common/helper/SeoCopyAiRuleConfigHelper.php
Normal file
196
code/app/common/helper/SeoCopyAiRuleConfigHelper.php
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\helper;
|
||||||
|
|
||||||
|
use app\model\SystemConfigModel;
|
||||||
|
|
||||||
|
class SeoCopyAiRuleConfigHelper
|
||||||
|
{
|
||||||
|
protected const DEFINITIONS = [
|
||||||
|
'SEO_COPY_AI_RULE_HIGHEST' => [
|
||||||
|
'default' => "最高指令:\n1. 所有 AI 生成文案必须以 SEO 效果、可读性、真实感和可持续收录为共同目标。\n2. 所有生成只能基于已提供字段与事实,不允许虚构剧情、角色关系、上映信息、评价内容、播放权益或不存在的资料。\n3. 如果当前规则开始明显限制 SEO 增长、限制文案质量上限、限制更高质量的智能发挥,不允许静默硬套,必须明确指出冲突点、风险点和可替代优化方案,再继续讨论升级规则。\n4. 不允许因为“遵守规则”而产出明显模板化、机械化、低区分度、低点击意图的文案。\n5. 不允许输出营销承诺词,例如“免费观看”“高清完整版”“极速播放”“本站提供”“全网最全”等。\n6. 不允许堆砌关键词,不允许片名大面积重复,通常不要超过 2 次。\n7. 语言必须自然、克制、像人工编辑,不要满篇泛称、代词和模板痕迹。",
|
||||||
|
'description' => 'SEO文案AI最高指令:控制真实性、SEO效果、去模板化;如果规则本身开始压制SEO增长或高质量发挥,必须先明确提出冲突点再讨论升级,不能静默硬套。',
|
||||||
|
],
|
||||||
|
'SEO_COPY_AI_RULE_DETAIL_SUMMARY' => [
|
||||||
|
'default' => "影视详情页摘要规则:\n1. 适用于详情页 description、详情摘要、播放页短摘要等短文案场景。\n2. 长度控制在 60 到 110 个中文字符之间。\n3. 如果原简介很短,可以结合导演、主演、题材、地区、年份、更新状态补足信息。\n4. 如果信息不足,输出尽量客观、克制的资料型摘要,不要硬编剧情。\n5. 尽量避免“这部作品 / 这部剧 / 这部片子 / 这部故事 / 它 / 这部新作 / 这套剧集 / 这个故事”等泛称或代词。\n6. 电影:更像电影简介,强调人物处境、冲突推进、情绪和命运。\n7. 剧集:更像连续剧情介绍,强调人物关系线和推进节奏。\n8. 综艺:更像节目简介,强调现场互动、气氛和临场变化。\n9. 动漫:更像动画简介,强调世界观、人物立场和设定线索。\n10. 短剧:更像短剧简介,强调人物处境、冲突推进、情绪和命运。",
|
||||||
|
'description' => 'SEO文案AI详情摘要规则:控制 detail/play 短摘要的长度、真实性、场景风格和禁用泛称代词,适合搜索摘要与详情简述。',
|
||||||
|
],
|
||||||
|
'SEO_COPY_AI_RULE_DETAIL_BODY' => [
|
||||||
|
'default' => "影视详情页正文补全规则:\n1. 适用于详情页正文、播放页正文、详情补充介绍等中长文案场景。\n2. 字数控制在 120 到 220 个中文字符之间。\n3. 如果原简介存在且可用,应优先保留原简介核心信息,再补充结构化资料。\n4. 尽量包含作品类型、地区、语言、年份、导演、主演、更新状态等关键信息。\n5. 如果信息不足,写成简洁客观的资料型介绍,不要硬编剧情。\n6. 不要完全照搬字段顺序,要自然组织表达。\n7. 同样避免“这部作品 / 这部剧 / 这部片子 / 它 / 这部新作 / 这套剧集”等泛称或代词大面积出现。\n8. 电影:更像电影简介,强调人物处境、冲突推进、情绪和命运。\n9. 剧集:更像连续剧情介绍,强调人物关系线和推进节奏。\n10. 综艺:更像节目简介,强调现场互动、气氛和临场变化。\n11. 动漫:更像动画简介,强调世界观、人物立场和设定线索。\n12. 短剧:更像短剧简介,强调人物处境、冲突推进、情绪和命运。",
|
||||||
|
'description' => 'SEO文案AI详情正文规则:控制 detail/play 中长正文的真实性、信息密度、字数范围和分类写法,避免模板化和硬编剧情。',
|
||||||
|
],
|
||||||
|
'SEO_COPY_AI_RULE_MISSING_FIELDS' => [
|
||||||
|
'default' => "缺失字段处理规则:\n1. 当演员、导演、年份、地区、语言、简介、备注等字段为空时,只能承认缺失,不能脑补、补写、猜测、拼接不存在的信息。\n2. 缺少剧情简介时,优先退回“资料型写法”:用已知的片名、类型、地区、语言、年份、更新状态、清晰度、集数、备注等字段,组织成客观简介。\n3. 缺少演员或导演时,不要为了句子完整强行写“由某某领衔”之类表达,可以直接改写为题材导向、设定导向或更新状态导向。\n4. 缺少年份时,不要伪造上映时间、播出时间、年代背景;可以直接省略年份,改用类型、地区、语言、更新状态补足。\n5. 缺少地区、语言时,不要写“未知地区”“未知语言”这种伤点击的话术,直接跳过该字段,用其它已有信息维持句子完整。\n6. 缺少简介且字段很少时,允许输出更克制的资料页文案,但仍要尽量保证可读性,不能只剩机械字段堆叠。\n7. 如果只有片名和极少数字段,优先写成“作品资料说明型”短文案,不要硬写剧情,不要假装很懂内容。\n8. 任意缺失字段都不能触发模板化兜底句式,例如“讲述了一段故事”“围绕主人公展开”“精彩剧情值得关注”这类空泛废话。\n9. 当信息不足以支撑长正文时,允许适当缩短,但必须保证自然、克制、像真人编辑整理,而不是错误扩写。\n10. SEO 目标不是把空字段补满,而是在真实边界内,把现有信息组织成最可信、最自然、最不模板化的可收录文案。",
|
||||||
|
'description' => 'SEO文案AI缺失字段处理规则:专门约束演员、导演、年份、简介等字段为空时的写法,要求只在真实信息边界内做资料型组织,禁止脑补和空泛扩写。',
|
||||||
|
],
|
||||||
|
'SEO_COPY_AI_RULE_MOVIE_STYLE' => [
|
||||||
|
'default' => "电影细分规则:\n1. 电影文案更像电影简介,不要写成剧集分集导语。\n2. 优先强调人物处境、核心冲突、情绪变化、命运推进。\n3. 如果事实不足,可优先组织题材、地区、年份、导演、主演、上映阶段或备注状态,不要硬编剧情反转。\n4. 句式可以更集中、更凝练,保留电影应有的整体感和完成度。\n5. 不要泛泛罗列字段,尽量让信息组织像真实电影站资料页摘要。",
|
||||||
|
'description' => 'SEO文案AI电影细分规则:用于电影类详情/播放文案,强调人物处境、冲突推进、情绪与命运,不要写成剧集或综艺口吻。',
|
||||||
|
],
|
||||||
|
'SEO_COPY_AI_RULE_SERIES_STYLE' => [
|
||||||
|
'default' => "剧集细分规则:\n1. 剧集文案更像连续剧情介绍,不要写成单部电影梗概。\n2. 优先强调人物关系线、剧情推进节奏、阶段性发展与追更感。\n3. 可以适度体现“随着剧情推进”“围绕某条关系线展开”等结构,但不要模板化重复。\n4. 如果信息不足,可回到演员、导演、题材、地区、年份、更新状态这些已知事实,保持追剧导向。\n5. 要让用户和蜘蛛感知到这是持续推进型内容,而不是一次性完整闭环。",
|
||||||
|
'description' => 'SEO文案AI剧集细分规则:用于电视剧/连续剧类详情文案,强调人物关系线、推进节奏和持续更新属性。',
|
||||||
|
],
|
||||||
|
'SEO_COPY_AI_RULE_VARIETY_STYLE' => [
|
||||||
|
'default' => "综艺细分规则:\n1. 综艺文案更像节目简介,不要写成剧情片简介。\n2. 优先强调节目形式、现场互动、嘉宾关系、气氛变化和临场感。\n3. 适合突出舞台、话题、竞演、观察、陪伴、互动等节目特征,但不能编造具体桥段。\n4. 如果事实不足,可结合地区、年份、语言、更新状态和已知嘉宾/主持信息,保持节目导向。\n5. 语气可以更灵动,但仍要克制、真实、像人工编辑。",
|
||||||
|
'description' => 'SEO文案AI综艺细分规则:用于综艺/真人秀类文案,强调互动、气氛、节目形式和临场变化,避免剧情片化。',
|
||||||
|
],
|
||||||
|
'SEO_COPY_AI_RULE_ANIME_STYLE' => [
|
||||||
|
'default' => "动漫细分规则:\n1. 动漫文案更像动画简介,不要硬套真人影视口吻。\n2. 优先强调世界观、人物立场、设定线索、阵营关系和冒险方向。\n3. 可以突出成长、战斗、幻想、校园、异世界、科幻等题材感,但不能编造不存在的设定。\n4. 如果信息不足,可先稳住在类型、地区、年份、语言、导演、主演/配音、更新状态等已知资料。\n5. 表达要保留动画内容常见的设定感和想象空间,但不要写成二创文案。",
|
||||||
|
'description' => 'SEO文案AI动漫细分规则:用于动漫/动画类文案,强调世界观、设定线索、人物立场与题材氛围。',
|
||||||
|
],
|
||||||
|
'SEO_COPY_AI_RULE_SHORT_DRAMA_STYLE' => [
|
||||||
|
'default' => "短剧细分规则:\n1. 短剧文案更强调人物处境、冲突推进、情绪起伏和高密度反差感。\n2. 句式可以更紧凑,但不要低俗、不要标题党、不要营销化。\n3. 如果事实有限,可围绕人物关系、身份落差、阶段性冲突、更新状态来组织,不要编造狗血桥段。\n4. 要让文案读起来有短剧节奏感和推动力,但仍保持资料页风格,不要写成宣发广告。\n5. 适合更突出“发展快、情绪强、关系推进明显”的内容质感。",
|
||||||
|
'description' => 'SEO文案AI短剧细分规则:用于短剧类文案,强调人物处境、情绪张力、冲突推进和紧凑节奏。',
|
||||||
|
],
|
||||||
|
'SEO_COPY_AI_RULE_HIGHEST_NOTE' => [
|
||||||
|
'default' => "实验备注:\n- 修改目的:\n- 预期影响:\n- 观察指标:收录 / 蜘蛛抓取 / 页面点击 / 摘要自然度\n- 回看日期:",
|
||||||
|
'description' => 'SEO文案AI规则实验备注:记录最高指令的修改目的、预期影响和回看日期,便于后续SEO复盘。',
|
||||||
|
],
|
||||||
|
'SEO_COPY_AI_RULE_DETAIL_SUMMARY_NOTE' => [
|
||||||
|
'default' => "实验备注:\n- 修改目的:\n- 预期影响:\n- 观察指标:搜索摘要可读性 / 点击意图 / 收录摘要匹配度\n- 回看日期:",
|
||||||
|
'description' => 'SEO文案AI规则实验备注:记录详情摘要规则调整的目的、预期影响和回看日期。',
|
||||||
|
],
|
||||||
|
'SEO_COPY_AI_RULE_DETAIL_BODY_NOTE' => [
|
||||||
|
'default' => "实验备注:\n- 修改目的:\n- 预期影响:\n- 观察指标:正文自然度 / 页面停留 / 蜘蛛深抓 / 详情页收录质量\n- 回看日期:",
|
||||||
|
'description' => 'SEO文案AI规则实验备注:记录详情正文规则调整的目的、预期影响和回看日期。',
|
||||||
|
],
|
||||||
|
'SEO_COPY_AI_RULE_MISSING_FIELDS_NOTE' => [
|
||||||
|
'default' => "实验备注:\n- 修改目的:减少缺字段资源的硬编、废话和模板腔\n- 预期影响:空字段资源也能保持可信、自然、可收录\n- 观察指标:缺字段详情页自然度 / 收录质量 / 低质页占比\n- 回看日期:",
|
||||||
|
'description' => 'SEO文案AI规则实验备注:记录缺失字段处理规则调整的目的、预期影响和回看日期。',
|
||||||
|
],
|
||||||
|
'SEO_COPY_AI_RULE_MOVIE_STYLE_NOTE' => [
|
||||||
|
'default' => "实验备注:\n- 修改目的:\n- 预期影响:电影页更像真人编辑简介\n- 观察指标:电影详情页摘要自然度 / 收录描述质量 / 点击率\n- 回看日期:",
|
||||||
|
'description' => 'SEO文案AI规则实验备注:记录电影细分规则调整的实验目的和复盘节点。',
|
||||||
|
],
|
||||||
|
'SEO_COPY_AI_RULE_SERIES_STYLE_NOTE' => [
|
||||||
|
'default' => "实验备注:\n- 修改目的:\n- 预期影响:剧集页更突出关系线与追更节奏\n- 观察指标:剧集详情页自然度 / 收录稳定性 / 点击率\n- 回看日期:",
|
||||||
|
'description' => 'SEO文案AI规则实验备注:记录剧集细分规则调整的实验目的和复盘节点。',
|
||||||
|
],
|
||||||
|
'SEO_COPY_AI_RULE_VARIETY_STYLE_NOTE' => [
|
||||||
|
'default' => "实验备注:\n- 修改目的:\n- 预期影响:综艺页更有节目感和互动感\n- 观察指标:摘要差异度 / 收录摘要匹配 / 页面点击率\n- 回看日期:",
|
||||||
|
'description' => 'SEO文案AI规则实验备注:记录综艺细分规则调整的实验目的和复盘节点。',
|
||||||
|
],
|
||||||
|
'SEO_COPY_AI_RULE_ANIME_STYLE_NOTE' => [
|
||||||
|
'default' => "实验备注:\n- 修改目的:\n- 预期影响:动漫页更有设定感和世界观线索\n- 观察指标:摘要自然度 / 题材匹配度 / 收录质量\n- 回看日期:",
|
||||||
|
'description' => 'SEO文案AI规则实验备注:记录动漫细分规则调整的实验目的和复盘节点。',
|
||||||
|
],
|
||||||
|
'SEO_COPY_AI_RULE_SHORT_DRAMA_STYLE_NOTE' => [
|
||||||
|
'default' => "实验备注:\n- 修改目的:\n- 预期影响:短剧页更有节奏感和情绪推动力\n- 观察指标:点击意图 / 摘要张力 / 收录质量\n- 回看日期:",
|
||||||
|
'description' => 'SEO文案AI规则实验备注:记录短剧细分规则调整的实验目的和复盘节点。',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function ensureSystemConfigDefaults(): void
|
||||||
|
{
|
||||||
|
$changed = false;
|
||||||
|
|
||||||
|
foreach (self::DEFINITIONS as $code => $definition) {
|
||||||
|
$exists = SystemConfigModel::where('sc_code', $code)->find();
|
||||||
|
if ($exists !== null) {
|
||||||
|
$expectedDescription = (string)($definition['description'] ?? $code);
|
||||||
|
if ((string)($exists->sc_description ?? '') !== $expectedDescription) {
|
||||||
|
$exists->sc_description = $expectedDescription;
|
||||||
|
$exists->save();
|
||||||
|
$changed = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$model = new SystemConfigModel();
|
||||||
|
$model->save([
|
||||||
|
'sc_code' => $code,
|
||||||
|
'sc_val' => (string)($definition['default'] ?? ''),
|
||||||
|
'sc_description' => (string)($definition['description'] ?? $code),
|
||||||
|
'site_id' => 0,
|
||||||
|
]);
|
||||||
|
$changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($changed) {
|
||||||
|
SystemConfigModel::flushCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function buildSharedRuleBlock(): string
|
||||||
|
{
|
||||||
|
self::ensureSystemConfigDefaults();
|
||||||
|
|
||||||
|
$parts = [];
|
||||||
|
foreach (self::displayRuleCodes() as $code) {
|
||||||
|
$value = trim((string)SystemConfigModel::getValByCode($code));
|
||||||
|
if ($value === '') {
|
||||||
|
$value = trim((string)(self::DEFINITIONS[$code]['default'] ?? ''));
|
||||||
|
}
|
||||||
|
if ($value !== '') {
|
||||||
|
$parts[] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode("\n\n", $parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function editableCodes(): array
|
||||||
|
{
|
||||||
|
return array_keys(self::DEFINITIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function noteCodeForRule(string $code): string
|
||||||
|
{
|
||||||
|
return str_ends_with($code, '_NOTE') ? $code : ($code . '_NOTE');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isRuleNoteCode(string $code): bool
|
||||||
|
{
|
||||||
|
return str_ends_with($code, '_NOTE');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function displayRuleCodes(): array
|
||||||
|
{
|
||||||
|
return array_values(array_filter(array_keys(self::DEFINITIONS), static function (string $code): bool {
|
||||||
|
return !self::isRuleNoteCode($code);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function defaultValue(string $code): string
|
||||||
|
{
|
||||||
|
return (string)(self::DEFINITIONS[$code]['default'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function resetToDefault(?string $code = null): array
|
||||||
|
{
|
||||||
|
self::ensureSystemConfigDefaults();
|
||||||
|
|
||||||
|
$codes = $code !== null && $code !== ''
|
||||||
|
? [$code]
|
||||||
|
: self::editableCodes();
|
||||||
|
|
||||||
|
$changed = [];
|
||||||
|
foreach ($codes as $itemCode) {
|
||||||
|
if (!array_key_exists($itemCode, self::DEFINITIONS)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$model = SystemConfigModel::where('sc_code', $itemCode)->find();
|
||||||
|
if ($model === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$model->sc_val = self::defaultValue($itemCode);
|
||||||
|
$model->sc_description = (string)(self::DEFINITIONS[$itemCode]['description'] ?? $itemCode);
|
||||||
|
$model->save();
|
||||||
|
$changed[] = $itemCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($changed)) {
|
||||||
|
SystemConfigModel::flushCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'codes' => array_values($changed),
|
||||||
|
'count' => count($changed),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -101,6 +101,9 @@ class SeoCopyBatchPrepareHelper
|
|||||||
'- 文风要自然、克制、站内导航型,避免“全网最全”“免费观看”等夸张承诺。',
|
'- 文风要自然、克制、站内导航型,避免“全网最全”“免费观看”等夸张承诺。',
|
||||||
'- 输出必须能直接写入目标 JSON 文件。',
|
'- 输出必须能直接写入目标 JSON 文件。',
|
||||||
'',
|
'',
|
||||||
|
'## Highest Rules',
|
||||||
|
SeoCopyAiProviderHelper::buildSharedAiRuleBlock(),
|
||||||
|
'',
|
||||||
'## Scene Rules',
|
'## Scene Rules',
|
||||||
(string)$strRulesJson,
|
(string)$strRulesJson,
|
||||||
'',
|
'',
|
||||||
|
|||||||
@@ -4,6 +4,52 @@ namespace app\common\helper;
|
|||||||
|
|
||||||
class SeoCopyContextHelper
|
class SeoCopyContextHelper
|
||||||
{
|
{
|
||||||
|
public static function normalizeKeywordCandidates(array $arrItems, array $arrExclude = [], int $intLimit = 10): array
|
||||||
|
{
|
||||||
|
$arrExclude = array_values(array_filter(array_map(static function ($item): string {
|
||||||
|
return trim((string)$item);
|
||||||
|
}, $arrExclude), static function (string $item): bool {
|
||||||
|
return $item !== '';
|
||||||
|
}));
|
||||||
|
|
||||||
|
$arrOut = [];
|
||||||
|
foreach ($arrItems as $item) {
|
||||||
|
$strItem = trim((string)$item);
|
||||||
|
if ($strItem === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$strItem = preg_replace('/\s+/u', '', $strItem) ?: $strItem;
|
||||||
|
$strItem = trim($strItem, ",,。;;::|/-_");
|
||||||
|
if ($strItem === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mb_strlen($strItem) < 2 || mb_strlen($strItem) > 14) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($strItem, $arrExclude, true) || in_array($strItem, $arrOut, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/[“”"\'\(\)\[\]{}]/u', $strItem) === 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/(首页|分类页|详情页|搜索页|播放页|首屏|收录|起词|带动|围绕|承接|路径|导购|整理型|流量型|扩展型|继续|建议|适合|当前|优先)/u', $strItem) === 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrOut[] = $strItem;
|
||||||
|
if (count($arrOut) >= $intLimit) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $arrOut;
|
||||||
|
}
|
||||||
|
|
||||||
public static function buildResourceContext(string $strHost): array
|
public static function buildResourceContext(string $strHost): array
|
||||||
{
|
{
|
||||||
$strHost = \app\model\DomainModel::normalizeHost($strHost);
|
$strHost = \app\model\DomainModel::normalizeHost($strHost);
|
||||||
@@ -24,11 +70,7 @@ class SeoCopyContextHelper
|
|||||||
$arrPositioning = SeoResourcePoolHelper::readJsonFile($strRoot . '/site_positioning/by_host/' . $strHostKey . '.json');
|
$arrPositioning = SeoResourcePoolHelper::readJsonFile($strRoot . '/site_positioning/by_host/' . $strHostKey . '.json');
|
||||||
$arrFeedback = SeoResourcePoolHelper::readJsonFile($strRoot . '/keyword_feedback/by_host/' . $strHostKey . '.json');
|
$arrFeedback = SeoResourcePoolHelper::readJsonFile($strRoot . '/keyword_feedback/by_host/' . $strHostKey . '.json');
|
||||||
|
|
||||||
$arrPriorityKeywords = array_values(array_filter(array_map(static function ($value): string {
|
$arrPriorityKeywords = self::normalizeKeywordCandidates((array)($arrPositioning['priority_keywords'] ?? []));
|
||||||
return trim((string)$value);
|
|
||||||
}, (array)($arrPositioning['priority_keywords'] ?? [])), static function (string $value): bool {
|
|
||||||
return $value !== '';
|
|
||||||
}));
|
|
||||||
$arrTopics = array_values(array_filter(array_map(static function ($value): string {
|
$arrTopics = array_values(array_filter(array_map(static function ($value): string {
|
||||||
return trim((string)$value);
|
return trim((string)$value);
|
||||||
}, array_merge((array)($arrPositioning['primary_topics'] ?? []), (array)($arrPositioning['secondary_topics'] ?? []))), static function (string $value): bool {
|
}, array_merge((array)($arrPositioning['primary_topics'] ?? []), (array)($arrPositioning['secondary_topics'] ?? []))), static function (string $value): bool {
|
||||||
@@ -108,11 +150,12 @@ class SeoCopyContextHelper
|
|||||||
$strRawDescription,
|
$strRawDescription,
|
||||||
]
|
]
|
||||||
), 8, $arrExclude);
|
), 8, $arrExclude);
|
||||||
|
$arrThemePhrases = self::filterHomeMetaPhrases($arrPhrases);
|
||||||
|
|
||||||
$strPrimary = $arrPhrases[0] ?? '影视内容推荐';
|
$strPrimary = $arrThemePhrases[0] ?? $arrPhrases[0] ?? '影视内容推荐';
|
||||||
$strSecondary = $arrPhrases[1] ?? '高清内容整理';
|
$strSecondary = $arrThemePhrases[1] ?? $arrPhrases[1] ?? '高清内容整理';
|
||||||
$strTertiary = $arrPhrases[2] ?? '热门片单更新';
|
$strTertiary = $arrThemePhrases[2] ?? $arrPhrases[2] ?? '热门片单更新';
|
||||||
$strQuaternary = $arrPhrases[3] ?? '剧情与演员资料';
|
$strQuaternary = $arrThemePhrases[3] ?? $arrPhrases[3] ?? '剧情与演员资料';
|
||||||
$strSiteSuffix = $strSiteName !== '' ? ('-' . $strSiteName) : '';
|
$strSiteSuffix = $strSiteName !== '' ? ('-' . $strSiteName) : '';
|
||||||
|
|
||||||
if ($strTkdMode === \app\model\DomainModel::TKD_MODE_FORCE_IMPORT) {
|
if ($strTkdMode === \app\model\DomainModel::TKD_MODE_FORCE_IMPORT) {
|
||||||
@@ -135,6 +178,7 @@ class SeoCopyContextHelper
|
|||||||
if (
|
if (
|
||||||
$strTkdMode === \app\model\DomainModel::TKD_MODE_AI_OPTIMIZE
|
$strTkdMode === \app\model\DomainModel::TKD_MODE_AI_OPTIMIZE
|
||||||
&& $strRawTitle !== ''
|
&& $strRawTitle !== ''
|
||||||
|
&& !self::isBoilerplateHomeTitle($strRawTitle)
|
||||||
&& !str_contains($strRawTitle, '{')
|
&& !str_contains($strRawTitle, '{')
|
||||||
&& !str_contains($strRawTitle, '}')
|
&& !str_contains($strRawTitle, '}')
|
||||||
&& mb_strlen($strRawTitle) <= 42
|
&& mb_strlen($strRawTitle) <= 42
|
||||||
@@ -143,6 +187,7 @@ class SeoCopyContextHelper
|
|||||||
$strTitle = $strRawTitle;
|
$strTitle = $strRawTitle;
|
||||||
} elseif (
|
} elseif (
|
||||||
$strRawTitle !== ''
|
$strRawTitle !== ''
|
||||||
|
&& !self::isBoilerplateHomeTitle($strRawTitle)
|
||||||
&& !str_contains($strRawTitle, '{')
|
&& !str_contains($strRawTitle, '{')
|
||||||
&& !str_contains($strRawTitle, '}')
|
&& !str_contains($strRawTitle, '}')
|
||||||
&& mb_strlen($strRawTitle) <= 42
|
&& mb_strlen($strRawTitle) <= 42
|
||||||
@@ -153,24 +198,34 @@ class SeoCopyContextHelper
|
|||||||
$arrTitleParts = array_slice(array_values(array_filter([
|
$arrTitleParts = array_slice(array_values(array_filter([
|
||||||
$strPrimary,
|
$strPrimary,
|
||||||
$strSecondary,
|
$strSecondary,
|
||||||
$strTertiary,
|
])), 0, 2);
|
||||||
])), 0, 3);
|
if ($strSiteName !== '') {
|
||||||
$strTitle = implode('、', $arrTitleParts) . $strSiteSuffix;
|
$strTitle = $strSiteName;
|
||||||
|
if (!empty($arrTitleParts)) {
|
||||||
|
$strTitle .= '-' . implode('·', $arrTitleParts);
|
||||||
|
} else {
|
||||||
|
$strTitle .= '-热门内容推荐';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$strTitle = implode('·', $arrTitleParts) . $strSiteSuffix;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$arrKeywords = [];
|
$arrKeywords = self::buildHomeKeywordBundle(
|
||||||
foreach (array_merge([$strSiteName], (array)($arrResourceContext['priority_keywords'] ?? []), (array)($arrResourceContext['feedback_keywords'] ?? []), array_slice($arrPhrases, 0, 6)) as $strKeyword) {
|
$strSiteName,
|
||||||
$strKeyword = trim((string)$strKeyword);
|
$strHost,
|
||||||
if ($strKeyword === '' || in_array($strKeyword, $arrKeywords, true)) {
|
$arrResourceContext,
|
||||||
continue;
|
$arrThemePhrases
|
||||||
}
|
);
|
||||||
$arrKeywords[] = $strKeyword;
|
|
||||||
}
|
|
||||||
if (empty($arrKeywords)) {
|
if (empty($arrKeywords)) {
|
||||||
$arrKeywords = array_values(array_filter([$strSiteName, '影视内容推荐', '剧情介绍', '演员资料']));
|
$arrKeywords = array_values(array_filter([$strSiteName, '影视内容推荐', '剧情介绍', '演员资料']));
|
||||||
}
|
}
|
||||||
|
|
||||||
$strPhraseSummary = implode('、', array_slice($arrPhrases, 0, 4));
|
$arrDescriptionKeywords = array_values(array_filter(array_slice($arrKeywords, 1, 4), static function ($item): bool {
|
||||||
|
return trim((string)$item) !== '';
|
||||||
|
}));
|
||||||
|
$strPhraseSummary = implode('、', $arrDescriptionKeywords);
|
||||||
if ($strPhraseSummary === '') {
|
if ($strPhraseSummary === '') {
|
||||||
$strPhraseSummary = '影视内容推荐、剧情介绍、演员资料、播放线索';
|
$strPhraseSummary = '影视内容推荐、剧情介绍、演员资料、播放线索';
|
||||||
}
|
}
|
||||||
@@ -195,13 +250,13 @@ class SeoCopyContextHelper
|
|||||||
|
|
||||||
$strDescription = $strSiteName !== ''
|
$strDescription = $strSiteName !== ''
|
||||||
? sprintf(
|
? sprintf(
|
||||||
'%s围绕%s等主题整理首页内容,持续补充热门片单、剧情介绍、演员资料与播放线索。%s',
|
'%s首页重点承接%s等内容入口,适合先看首屏推荐、题材分区和站内搜索,再决定继续进入详情页或播放页。%s',
|
||||||
$strSiteName,
|
$strSiteName,
|
||||||
$strPhraseSummary,
|
$strPhraseSummary,
|
||||||
$strDescriptionTail
|
$strDescriptionTail
|
||||||
)
|
)
|
||||||
: sprintf(
|
: sprintf(
|
||||||
'当前首页围绕%s等主题整理内容,持续补充热门片单、剧情介绍、演员资料与播放线索。%s',
|
'当前首页重点承接%s等内容入口,适合先看首屏推荐、题材分区和站内搜索,再决定继续进入详情页或播放页。%s',
|
||||||
$strPhraseSummary,
|
$strPhraseSummary,
|
||||||
$strDescriptionTail
|
$strDescriptionTail
|
||||||
);
|
);
|
||||||
@@ -220,6 +275,68 @@ class SeoCopyContextHelper
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static function buildHomeKeywordBundle(
|
||||||
|
string $strSiteName,
|
||||||
|
string $strHost,
|
||||||
|
array $arrResourceContext,
|
||||||
|
array $arrThemePhrases
|
||||||
|
): array {
|
||||||
|
$arrBasePool = array_merge(
|
||||||
|
[$strSiteName],
|
||||||
|
(array)($arrResourceContext['priority_keywords'] ?? []),
|
||||||
|
(array)($arrResourceContext['topics'] ?? []),
|
||||||
|
array_slice($arrThemePhrases, 0, 6),
|
||||||
|
['电视剧', '电影', '短剧', '剧情介绍']
|
||||||
|
);
|
||||||
|
|
||||||
|
$arrKeywords = self::normalizeKeywordCandidates(
|
||||||
|
$arrBasePool,
|
||||||
|
[$strHost, preg_replace('/^www\./i', '', $strHost)],
|
||||||
|
8
|
||||||
|
);
|
||||||
|
|
||||||
|
$arrKeywords = array_values(array_filter($arrKeywords, static function (string $item): bool {
|
||||||
|
if ($item === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/(高清影视内容精选|每日更新|高清影视|内容推荐|内容整理|热门片单更新|播放线索|演员资料)/u', $item) === 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}));
|
||||||
|
|
||||||
|
return array_values(array_slice(array_unique($arrKeywords), 0, 6));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function isBoilerplateHomeTitle(string $strTitle): bool
|
||||||
|
{
|
||||||
|
$strTitle = trim($strTitle);
|
||||||
|
if ($strTitle === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return preg_match('/(高清影视内容精选|每日更新|影视内容推荐)/u', $strTitle) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function filterHomeMetaPhrases(array $arrPhrases): array
|
||||||
|
{
|
||||||
|
return array_values(array_filter(array_map(static function ($item): string {
|
||||||
|
return trim((string)$item);
|
||||||
|
}, $arrPhrases), static function (string $item): bool {
|
||||||
|
if ($item === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/(高清影视内容精选|每日更新|高清影视|影视内容推荐|内容整理与推荐|热门片单更新|剧情与演员资料)/u', $item) === 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
public static function extractPhrases(array $arrTexts, int $intLimit = 6, array $arrExclude = []): array
|
public static function extractPhrases(array $arrTexts, int $intLimit = 6, array $arrExclude = []): array
|
||||||
{
|
{
|
||||||
$arrStopWords = [
|
$arrStopWords = [
|
||||||
@@ -256,6 +373,9 @@ class SeoCopyContextHelper
|
|||||||
if (preg_match('/(是一个|为您提供|欢迎|尽在|在线观看|免费看|免费高清|好网站|视觉体验|拥有全网|影视剧资源|各种|平台|专区|网站)/u', $strPart) === 1) {
|
if (preg_match('/(是一个|为您提供|欢迎|尽在|在线观看|免费看|免费高清|好网站|视觉体验|拥有全网|影视剧资源|各种|平台|专区|网站)/u', $strPart) === 1) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (preg_match('/(收录|起词|带动|导购|整理型|流量型|扩展型|首屏|详情页|分类页|搜索页|播放页|继续浏览|浏览路线|站外反馈|活跃词)/u', $strPart) === 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (preg_match('/(搜索结果|分类频道|内容列表|搜索落地页)$/u', $strPart) === 1) {
|
if (preg_match('/(搜索结果|分类频道|内容列表|搜索落地页)$/u', $strPart) === 1) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ class SeoCopyFallbackBuilder
|
|||||||
{
|
{
|
||||||
$strSiteName = self::siteName($arrFacts);
|
$strSiteName = self::siteName($arrFacts);
|
||||||
$strScopes = self::joinItems((array)($arrFacts['content_scopes'] ?? []), 3, '影视内容');
|
$strScopes = self::joinItems((array)($arrFacts['content_scopes'] ?? []), 3, '影视内容');
|
||||||
|
$strLanding = $strSiteName . '首页当前覆盖' . $strScopes . '等常见内容入口,适合先看首屏推荐,再按分类、榜单或搜索继续缩小范围。';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
'search_landing' => $strLanding,
|
||||||
'intro_text' => $strSiteName . '首页当前覆盖' . $strScopes . '等常见内容入口,建议先看首屏内容卡片,再继续按分类、榜单或搜索缩小范围。',
|
'intro_text' => $strSiteName . '首页当前覆盖' . $strScopes . '等常见内容入口,建议先看首屏内容卡片,再继续按分类、榜单或搜索缩小范围。',
|
||||||
'intro_meta' => '首页更适合先发现值得点开的内容,说明性提示只做辅助,不建议压住首屏主体内容。',
|
'intro_meta' => '首页更适合先发现值得点开的内容,说明性提示只做辅助,不建议压住首屏主体内容。',
|
||||||
'guide_cards' => [
|
'guide_cards' => [
|
||||||
@@ -94,8 +96,10 @@ class SeoCopyFallbackBuilder
|
|||||||
private static function buildRankIndex(array $arrFacts): array
|
private static function buildRankIndex(array $arrFacts): array
|
||||||
{
|
{
|
||||||
$strPeriods = self::joinItems((array)($arrFacts['rank_periods'] ?? []), 3, '常见榜单周期');
|
$strPeriods = self::joinItems((array)($arrFacts['rank_periods'] ?? []), 3, '常见榜单周期');
|
||||||
|
$strLanding = '当前榜单首页汇总了' . $strPeriods . '等常见周期入口,适合先确定周期,再继续进入具体榜单列表页。';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
'search_landing' => $strLanding,
|
||||||
'intro_text' => '当前榜单首页汇总了' . $strPeriods . '等常见周期入口,适合先确定要看的榜单范围再继续下钻。',
|
'intro_text' => '当前榜单首页汇总了' . $strPeriods . '等常见周期入口,适合先确定要看的榜单范围再继续下钻。',
|
||||||
'intro_meta' => '如果你更关心具体周期,可以直接进入对应榜单列表页,再继续看详情和播放路径。',
|
'intro_meta' => '如果你更关心具体周期,可以直接进入对应榜单列表页,再继续看详情和播放路径。',
|
||||||
'guide_cards' => [
|
'guide_cards' => [
|
||||||
@@ -111,8 +115,10 @@ class SeoCopyFallbackBuilder
|
|||||||
$strPeriod = self::fallback((string)($arrFacts['rank_period_name'] ?? ''), '当前周期');
|
$strPeriod = self::fallback((string)($arrFacts['rank_period_name'] ?? ''), '当前周期');
|
||||||
$strScope = self::fallback((string)($arrFacts['rank_scope_name'] ?? ''), '当前范围');
|
$strScope = self::fallback((string)($arrFacts['rank_scope_name'] ?? ''), '当前范围');
|
||||||
$intVisible = (int)($arrFacts['page_stats']['visible'] ?? 0);
|
$intVisible = (int)($arrFacts['page_stats']['visible'] ?? 0);
|
||||||
|
$strLanding = '当前榜单列表页展示的是' . $strScope . '下的' . $strPeriod . '结果,当前页可继续浏览的条目约为' . max(1, $intVisible) . '条。';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
'search_landing' => $strLanding,
|
||||||
'intro_text' => '当前榜单列表页展示的是' . $strScope . '下的' . $strPeriod . '结果,当前页可继续浏览的条目约为' . max(1, $intVisible) . '条。',
|
'intro_text' => '当前榜单列表页展示的是' . $strScope . '下的' . $strPeriod . '结果,当前页可继续浏览的条目约为' . max(1, $intVisible) . '条。',
|
||||||
'intro_meta' => '如果你想横向对比结果,可以切到其他周期榜单,或者直接进入详情页继续确认内容。',
|
'intro_meta' => '如果你想横向对比结果,可以切到其他周期榜单,或者直接进入详情页继续确认内容。',
|
||||||
'guide_cards' => [
|
'guide_cards' => [
|
||||||
|
|||||||
@@ -87,11 +87,14 @@ class SeoCopyGenerationHelper
|
|||||||
json_encode($manifest, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
|
json_encode($manifest, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$seededPublishedCopy = self::ensurePublishedStarterCopy($DomainModel, $overrides);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'state' => $state,
|
'state' => $state,
|
||||||
'manifest' => $manifest,
|
'manifest' => $manifest,
|
||||||
'state_path' => self::resolveStatePath($host),
|
'state_path' => self::resolveStatePath($host),
|
||||||
'manifest_path' => self::resolveManifestPath($host),
|
'manifest_path' => self::resolveManifestPath($host),
|
||||||
|
'seeded_published_copy' => $seededPublishedCopy,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,13 +117,17 @@ class SeoCopyGenerationHelper
|
|||||||
$hasLocalCopy = self::hasLocalCopyPayload($host);
|
$hasLocalCopy = self::hasLocalCopyPayload($host);
|
||||||
$tkdMode = DomainModel::normalizeTkdMode((string)($tkdCfg['mode'] ?? DomainModel::TKD_MODE_AUTO_GENERATE));
|
$tkdMode = DomainModel::normalizeTkdMode((string)($tkdCfg['mode'] ?? DomainModel::TKD_MODE_AUTO_GENERATE));
|
||||||
$tkdProvider = DomainModel::normalizeTkdProvider((string)($tkdCfg['provider'] ?? DomainModel::TKD_PROVIDER_LOCAL));
|
$tkdProvider = DomainModel::normalizeTkdProvider((string)($tkdCfg['provider'] ?? DomainModel::TKD_PROVIDER_LOCAL));
|
||||||
|
$boolOpenAiCopyPreferred = $tkdProvider === DomainModel::TKD_PROVIDER_OPENAI
|
||||||
|
|| $tkdMode === DomainModel::TKD_MODE_AI_OPTIMIZE;
|
||||||
$waitForAi = !empty($copyCfg['wait_for_ai_before_publish']) || ($tkdProvider === DomainModel::TKD_PROVIDER_OPENAI && $tkdMode !== DomainModel::TKD_MODE_FORCE_IMPORT);
|
$waitForAi = !empty($copyCfg['wait_for_ai_before_publish']) || ($tkdProvider === DomainModel::TKD_PROVIDER_OPENAI && $tkdMode !== DomainModel::TKD_MODE_FORCE_IMPORT);
|
||||||
|
|
||||||
$status = self::STATUS_LOCAL_READY;
|
$status = self::STATUS_LOCAL_READY;
|
||||||
if ($waitForAi) {
|
if ($waitForAi) {
|
||||||
$status = $hasLocalCopy ? self::STATUS_LOCAL_READY : self::STATUS_AI_PENDING;
|
$status = $hasLocalCopy ? self::STATUS_LOCAL_READY : self::STATUS_AI_PENDING;
|
||||||
}
|
}
|
||||||
if (!empty($rawCopyCfg['status']) && in_array((string)$rawCopyCfg['status'], [self::STATUS_DRAFT, self::STATUS_LOCAL_READY, self::STATUS_AI_PENDING, self::STATUS_AI_READY], true)) {
|
$strRawStatus = (string)($rawCopyCfg['status'] ?? '');
|
||||||
|
$boolLegacyDraftForOpenAi = $boolOpenAiCopyPreferred && $strRawStatus === self::STATUS_DRAFT;
|
||||||
|
if (!$boolLegacyDraftForOpenAi && $strRawStatus !== '' && in_array($strRawStatus, [self::STATUS_DRAFT, self::STATUS_LOCAL_READY, self::STATUS_AI_PENDING, self::STATUS_AI_READY], true)) {
|
||||||
$status = (string)$rawCopyCfg['status'];
|
$status = (string)$rawCopyCfg['status'];
|
||||||
}
|
}
|
||||||
if (!empty($overrides['status'])) {
|
if (!empty($overrides['status'])) {
|
||||||
@@ -193,12 +200,83 @@ class SeoCopyGenerationHelper
|
|||||||
return is_dir($root);
|
return is_dir($root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static function hasPublishedCopyPayload(string $host): bool
|
||||||
|
{
|
||||||
|
$host = self::normalizeHost($host);
|
||||||
|
if ($host === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$root = SeoCopyStore::publishedRoot() . '/' . self::buildHostKey($host);
|
||||||
|
if (!is_dir($root)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrFiles = glob($root . '/*/*.json') ?: [];
|
||||||
|
return !empty($arrFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function ensurePublishedStarterCopy(DomainModel $DomainModel, array $overrides = []): array
|
||||||
|
{
|
||||||
|
$host = self::normalizeHost((string)($DomainModel->d_domain ?? ''));
|
||||||
|
if ($host === '') {
|
||||||
|
return [
|
||||||
|
'attempted' => false,
|
||||||
|
'reason' => 'empty_host',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('seed_published_copy', $overrides) && empty($overrides['seed_published_copy'])) {
|
||||||
|
return [
|
||||||
|
'attempted' => false,
|
||||||
|
'reason' => 'disabled_by_override',
|
||||||
|
'host' => $host,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::hasPublishedCopyPayload($host)) {
|
||||||
|
return [
|
||||||
|
'attempted' => false,
|
||||||
|
'reason' => 'already_exists',
|
||||||
|
'host' => $host,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$arrResult = SeoCopyAiProviderHelper::generatePublishedCopy($DomainModel, [
|
||||||
|
// 首次初始化始终只落一本地增强版 starter,避免误调远程 AI。
|
||||||
|
'provider' => DomainModel::TKD_PROVIDER_LOCAL,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'attempted' => true,
|
||||||
|
'reason' => 'seeded_local_starter',
|
||||||
|
'host' => $host,
|
||||||
|
'provider_effective' => (string)($arrResult['provider_effective'] ?? DomainModel::TKD_PROVIDER_LOCAL),
|
||||||
|
'written_pages' => (array)($arrResult['written_pages'] ?? []),
|
||||||
|
'message' => (string)($arrResult['message'] ?? ''),
|
||||||
|
];
|
||||||
|
} catch (\Throwable $throwable) {
|
||||||
|
self::appendHistory($host, [
|
||||||
|
'type' => 'published_copy_seed_failed',
|
||||||
|
'message' => $throwable->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'attempted' => true,
|
||||||
|
'reason' => 'seed_failed',
|
||||||
|
'host' => $host,
|
||||||
|
'error' => $throwable->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected static function buildStateMessage(string $status, bool $waitForAi, bool $hasLocalCopy): string
|
protected static function buildStateMessage(string $status, bool $waitForAi, bool $hasLocalCopy): string
|
||||||
{
|
{
|
||||||
return match ($status) {
|
return match ($status) {
|
||||||
self::STATUS_AI_PENDING => $waitForAi
|
self::STATUS_AI_PENDING => $waitForAi
|
||||||
? '当前站点已导入,等待 AI 生成首页/分类/搜索等核心文案后再进入发布阶段。'
|
? '当前站点已进入 AI 待处理状态。默认由 Codex/人工优先接管优化;只有后台显式点击 AI 优化时,才会调用远程 AI 生成。'
|
||||||
: '当前站点已进入 AI 生成队列。',
|
: '当前站点已进入 AI 待处理状态,可由 Codex/人工继续优化,或在后台手动触发 AI 任务。',
|
||||||
self::STATUS_AI_READY => 'AI 文案已就绪,可继续发布或切换到 AI 版本。',
|
self::STATUS_AI_READY => 'AI 文案已就绪,可继续发布或切换到 AI 版本。',
|
||||||
self::STATUS_LOCAL_READY => $hasLocalCopy
|
self::STATUS_LOCAL_READY => $hasLocalCopy
|
||||||
? '本地规则文案与本地文案稿均可用,可直接上线或继续等待 AI 增强。'
|
? '本地规则文案与本地文案稿均可用,可直接上线或继续等待 AI 增强。'
|
||||||
|
|||||||
@@ -24,7 +24,93 @@ class SeoCopyStore
|
|||||||
return trim($strTemplate);
|
return trim($strTemplate);
|
||||||
}
|
}
|
||||||
|
|
||||||
return trim(strtr($strTemplate, $arrReplace));
|
foreach ($arrTokens as $strKey => $mValue) {
|
||||||
|
$strKey = trim((string)$strKey);
|
||||||
|
if ($strKey === '' || trim((string)$mValue) !== '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$strTemplate = self::stripEmptyTokenArtifacts($strTemplate, $strKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::normalizeInterpolatedText(strtr($strTemplate, $arrReplace));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function stripEmptyTokenArtifacts(string $strTemplate, string $strKey): string
|
||||||
|
{
|
||||||
|
$strToken = preg_quote('{' . $strKey . '}', '/');
|
||||||
|
$arrLabelMap = [
|
||||||
|
'video_alias' => ['别名'],
|
||||||
|
'year' => ['年份'],
|
||||||
|
'area_name' => ['地区'],
|
||||||
|
'lang_name' => ['语言'],
|
||||||
|
'actor_names' => ['演员'],
|
||||||
|
'director_names' => ['导演'],
|
||||||
|
'remarks' => ['备注'],
|
||||||
|
'play_line' => ['播放线路', '线路'],
|
||||||
|
'episode_name' => ['集数', '期数', '剧集'],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ((array)($arrLabelMap[$strKey] ?? []) as $strLabel) {
|
||||||
|
$strLabel = preg_quote($strLabel, '/');
|
||||||
|
$arrLabelPatterns = [
|
||||||
|
'/' . $strLabel . '\s*' . $strToken . '\s*[、,,]/u',
|
||||||
|
'/[、,,]\s*' . $strLabel . '\s*' . $strToken . '/u',
|
||||||
|
'/' . $strLabel . '\s*' . $strToken . '/u',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($arrLabelPatterns as $strPattern) {
|
||||||
|
$strTemplate = preg_replace($strPattern, '', $strTemplate) ?? $strTemplate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrPatterns = [
|
||||||
|
// 删掉前置连词 + 空 token,例如 “和{remarks}”
|
||||||
|
'/\s*(?:和|及|与|并|或)\s*' . $strToken . '/u',
|
||||||
|
// 删掉空 token + 后置连词,例如 “{remarks}和”
|
||||||
|
'/' . $strToken . '\s*(?:和|及|与|并|或)\s*/u',
|
||||||
|
// 删掉与空 token 紧邻的顿号/逗号
|
||||||
|
'/[、,,]\s*' . $strToken . '/u',
|
||||||
|
'/' . $strToken . '\s*[、,,]/u',
|
||||||
|
// 默认兜底:直接删掉空 token
|
||||||
|
'/' . $strToken . '/u',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($arrPatterns as $strPattern) {
|
||||||
|
$strTemplate = preg_replace($strPattern, '', $strTemplate) ?? $strTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $strTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function normalizeInterpolatedText(string $strValue): string
|
||||||
|
{
|
||||||
|
$strValue = preg_replace('/\{[a-z0-9_]+\}/iu', '', $strValue) ?? $strValue;
|
||||||
|
$strValue = str_replace(['高清高清播放', '高清高清'], ['高清播放', '高清'], $strValue);
|
||||||
|
$strValue = preg_replace('/\s{2,}/u', ' ', $strValue) ?? $strValue;
|
||||||
|
$strValue = preg_replace('/\s*[_||]+\s*/u', ' - ', $strValue) ?? $strValue;
|
||||||
|
$strValue = preg_replace('/\s*-\s*-\s*/u', ' - ', $strValue) ?? $strValue;
|
||||||
|
$strValue = preg_replace('/(会把|把)([^,。;!?、\\s]{1,24})和([^,。;!?、\\s]{1,24})(等资料|这类线索)/u', '$1$2、$3$4', $strValue) ?? $strValue;
|
||||||
|
$strValue = preg_replace('/(年份|地区|语言|演员|导演|备注|别名|线路|集数)(?=[A-Za-z0-9])/u', '$1 ', $strValue) ?? $strValue;
|
||||||
|
$strValue = preg_replace('/(地区|语言|演员|导演|备注|别名)([\p{Han}A-Za-z])/u', '$1 $2', $strValue) ?? $strValue;
|
||||||
|
$strValue = preg_replace('/地区\s*([^,。;!?、]+?)和语言\s*([^,。;!?、]+)/u', '地区 $1,语言 $2', $strValue) ?? $strValue;
|
||||||
|
$strValue = preg_replace('/(会把)([^,。;!?、\\s]{1,24})、([^,。;!?、\\s]{1,24})(这类线索)/u', '$1$2和$3$4', $strValue) ?? $strValue;
|
||||||
|
$strValue = preg_replace('/(会把)([^,。;!?、\\s]{1,24})、([^,。;!?、\\s]{1,24})(等资料)/u', '$1$2、$3$4', $strValue) ?? $strValue;
|
||||||
|
$strValue = preg_replace('/补看(\d{4}[^、,。;!?]*?)、([\p{Han}A-Za-z]{1,20})、([\p{Han}A-Za-z]{1,20})与([\p{Han}A-Za-z·、,,]{1,40})/u', '补看 $1、$2、$3,以及$4', $strValue) ?? $strValue;
|
||||||
|
$strValue = preg_replace('/(别名|年份|地区|语言|演员|导演|备注)\s+与/u', '$1与', $strValue) ?? $strValue;
|
||||||
|
$strValue = preg_replace('/本页,建议/u', '本页建议', $strValue) ?? $strValue;
|
||||||
|
$strValue = preg_replace('/本页,适合/u', '本页适合', $strValue) ?? $strValue;
|
||||||
|
$strValue = preg_replace('/本页,方便/u', '本页方便', $strValue) ?? $strValue;
|
||||||
|
$strValue = preg_replace('/([、,,])(?:\s*\\1)+/u', '$1', $strValue) ?? $strValue;
|
||||||
|
$strValue = preg_replace('/([((【\[])\s*[、,,\-]+\s*/u', '$1', $strValue) ?? $strValue;
|
||||||
|
$strValue = preg_replace('/\s*[、,,\-]+\s*([))】\]])/u', '$1', $strValue) ?? $strValue;
|
||||||
|
$strValue = preg_replace('/(会把|把|将|用|看)、/u', '$1', $strValue) ?? $strValue;
|
||||||
|
$strValue = preg_replace('/(\d{4})-\d{2}-\d{2}(?:\s+\d{2}:\d{2}:\d{2})?/u', '$1', $strValue) ?? $strValue;
|
||||||
|
$strValue = preg_replace('/\s*([,。;!?、])/u', '$1', $strValue) ?? $strValue;
|
||||||
|
$strValue = preg_replace('/([((【\[])\s+/u', '$1', $strValue) ?? $strValue;
|
||||||
|
$strValue = preg_replace('/\s+([))】\]])/u', '$1', $strValue) ?? $strValue;
|
||||||
|
|
||||||
|
return trim($strValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function buildPageKey(array|string $mValue, string $strFallback = 'index'): string
|
public static function buildPageKey(array|string $mValue, string $strFallback = 'index'): string
|
||||||
|
|||||||
@@ -4,6 +4,31 @@ namespace app\common\helper;
|
|||||||
|
|
||||||
class SeoWordsHelper
|
class SeoWordsHelper
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Resolve a stable forge keyword from video seo words.
|
||||||
|
*/
|
||||||
|
public static function resolveForgeWord(array $video = [], int $intForgeId = 1): string
|
||||||
|
{
|
||||||
|
$intForgeId = max(1, $intForgeId);
|
||||||
|
|
||||||
|
$arrWords = array_values(array_filter(array_map(
|
||||||
|
static fn($mVal): string => trim((string)$mVal),
|
||||||
|
(array)($video['v_seo_words'] ?? [])
|
||||||
|
), static fn(string $strVal): bool => $strVal !== ''));
|
||||||
|
|
||||||
|
if (!empty($arrWords)) {
|
||||||
|
$intIndex = ($intForgeId - 1) % count($arrWords);
|
||||||
|
return $arrWords[$intIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
$strName = trim((string)($video['v_name'] ?? ''));
|
||||||
|
if ($strName !== '') {
|
||||||
|
return $strName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim((string)($video['v_name_en'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据 $intRewrite 处理 SeoWords
|
* 根据 $intRewrite 处理 SeoWords
|
||||||
*/
|
*/
|
||||||
@@ -22,32 +47,39 @@ class SeoWordsHelper
|
|||||||
// 0:原序全部
|
// 0:原序全部
|
||||||
case 0:
|
case 0:
|
||||||
$arrSeoWords = $words;
|
$arrSeoWords = $words;
|
||||||
|
break;
|
||||||
|
|
||||||
// 1:前 6
|
// 1:前 6
|
||||||
case 1:
|
case 1:
|
||||||
$arrSeoWords = array_slice($words, 0, 6);
|
$arrSeoWords = array_slice($words, 0, 6);
|
||||||
|
break;
|
||||||
|
|
||||||
// 2:随机 8
|
// 2:随机 8
|
||||||
case 2:
|
case 2:
|
||||||
shuffle($words);
|
shuffle($words);
|
||||||
$arrSeoWords = array_slice($words, 0, 8);
|
$arrSeoWords = array_slice($words, 0, 8);
|
||||||
|
break;
|
||||||
|
|
||||||
// 3:插年份
|
// 3:插年份
|
||||||
case 3:
|
case 3:
|
||||||
$arrSeoWords = self::appendYear($words, $video);
|
$arrSeoWords = self::appendYear($words, $video);
|
||||||
|
break;
|
||||||
|
|
||||||
// 4:插“在线观看 / 免费 / 高清”
|
// 4:插“在线观看 / 免费 / 高清”
|
||||||
case 4:
|
case 4:
|
||||||
$arrSeoWords = self::appendSuffix($words);
|
$arrSeoWords = self::appendSuffix($words);
|
||||||
|
break;
|
||||||
|
|
||||||
// 5:重排 + 去重
|
// 5:重排 + 去重
|
||||||
case 5:
|
case 5:
|
||||||
shuffle($words);
|
shuffle($words);
|
||||||
$arrSeoWords = array_values(array_unique($words));
|
$arrSeoWords = array_values(array_unique($words));
|
||||||
|
break;
|
||||||
|
|
||||||
// 兜底
|
// 兜底
|
||||||
default:
|
default:
|
||||||
$arrSeoWords = array_slice($words, 0, 6);
|
$arrSeoWords = array_slice($words, 0, 6);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$arrNewsSeoWords = [];
|
$arrNewsSeoWords = [];
|
||||||
|
|||||||
@@ -1710,7 +1710,13 @@ class SiteStyle
|
|||||||
{
|
{
|
||||||
if (!$domainRow) return null;
|
if (!$domainRow) return null;
|
||||||
$raw = is_array($domainRow) ? ($domainRow['t_cfg'] ?? null) : ($domainRow->t_cfg ?? null);
|
$raw = is_array($domainRow) ? ($domainRow['t_cfg'] ?? null) : ($domainRow->t_cfg ?? null);
|
||||||
return $raw ? json_decode($raw, true) : null;
|
if (is_array($raw)) {
|
||||||
|
return $raw;
|
||||||
|
}
|
||||||
|
if (!is_string($raw) || trim($raw) === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return json_decode($raw, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function writeDbConfig($domainRow, array $cfg): void
|
private static function writeDbConfig($domainRow, array $cfg): void
|
||||||
|
|||||||
@@ -36,32 +36,62 @@ class SpiderMdConfigHelper
|
|||||||
],
|
],
|
||||||
'SPIDER_MD_GITEE_ENABLED' => [
|
'SPIDER_MD_GITEE_ENABLED' => [
|
||||||
'default' => '0',
|
'default' => '0',
|
||||||
'description' => '蜘蛛池MD是否启用 Gitee 推送',
|
'description' => '蜘蛛池MD是否启用 Gitee 推送(0=关闭,1=开启;开启后会把生成的 markdown 与 links.json 同步到 Gitee)',
|
||||||
'secret' => false,
|
'secret' => false,
|
||||||
],
|
],
|
||||||
'SPIDER_MD_GITEE_OWNER' => [
|
'SPIDER_MD_GITEE_OWNER' => [
|
||||||
'default' => '',
|
'default' => '',
|
||||||
'description' => '蜘蛛池MD Gitee 仓库 owner',
|
'description' => '蜘蛛池MD Gitee 仓库 owner(仓库所属用户名或组织名,例如 yourname)',
|
||||||
'secret' => false,
|
'secret' => false,
|
||||||
],
|
],
|
||||||
'SPIDER_MD_GITEE_REPO' => [
|
'SPIDER_MD_GITEE_REPO' => [
|
||||||
'default' => '',
|
'default' => '',
|
||||||
'description' => '蜘蛛池MD Gitee 仓库 repo',
|
'description' => '蜘蛛池MD Gitee 仓库 repo(仓库名,不带 owner,例如 seo-spider-md)',
|
||||||
'secret' => false,
|
'secret' => false,
|
||||||
],
|
],
|
||||||
'SPIDER_MD_GITEE_BRANCH' => [
|
'SPIDER_MD_GITEE_BRANCH' => [
|
||||||
'default' => 'master',
|
'default' => 'master',
|
||||||
'description' => '蜘蛛池MD Gitee 分支',
|
'description' => '蜘蛛池MD Gitee 分支(默认 master;推送文件会写入这个分支)',
|
||||||
'secret' => false,
|
'secret' => false,
|
||||||
],
|
],
|
||||||
'SPIDER_MD_GITEE_ROOT' => [
|
'SPIDER_MD_GITEE_ROOT' => [
|
||||||
'default' => 'seo-spider-md',
|
'default' => 'seo-spider-md',
|
||||||
'description' => '蜘蛛池MD Gitee 远程根目录',
|
'description' => '蜘蛛池MD Gitee 远程根目录(仓库内子目录,例如 seo-spider-md;留空表示直接写仓库根目录)',
|
||||||
'secret' => false,
|
'secret' => false,
|
||||||
],
|
],
|
||||||
'SPIDER_MD_GITEE_TOKEN' => [
|
'SPIDER_MD_GITEE_TOKEN' => [
|
||||||
'default' => '',
|
'default' => '',
|
||||||
'description' => '蜘蛛池MD Gitee Token',
|
'description' => '蜘蛛池MD Gitee Token(需要仓库 contents / push 权限;后台保存后会参与远程推送)',
|
||||||
|
'secret' => true,
|
||||||
|
],
|
||||||
|
'SPIDER_MD_GITHUB_ENABLED' => [
|
||||||
|
'default' => '0',
|
||||||
|
'description' => '蜘蛛池MD是否启用 GitHub 推送(0=关闭,1=开启;开启后会把生成的 markdown 与 links.json 同步到 GitHub)',
|
||||||
|
'secret' => false,
|
||||||
|
],
|
||||||
|
'SPIDER_MD_GITHUB_OWNER' => [
|
||||||
|
'default' => '',
|
||||||
|
'description' => '蜘蛛池MD GitHub 仓库 owner(仓库所属用户名或组织名,例如 yourname)',
|
||||||
|
'secret' => false,
|
||||||
|
],
|
||||||
|
'SPIDER_MD_GITHUB_REPO' => [
|
||||||
|
'default' => '',
|
||||||
|
'description' => '蜘蛛池MD GitHub 仓库 repo(仓库名,不带 owner,例如 seo-spider-md)',
|
||||||
|
'secret' => false,
|
||||||
|
],
|
||||||
|
'SPIDER_MD_GITHUB_BRANCH' => [
|
||||||
|
'default' => 'main',
|
||||||
|
'description' => '蜘蛛池MD GitHub 分支(默认 main;推送文件会写入这个分支)',
|
||||||
|
'secret' => false,
|
||||||
|
],
|
||||||
|
'SPIDER_MD_GITHUB_ROOT' => [
|
||||||
|
'default' => 'seo-spider-md',
|
||||||
|
'description' => '蜘蛛池MD GitHub 远程根目录(仓库内子目录,例如 seo-spider-md;留空表示直接写仓库根目录)',
|
||||||
|
'secret' => false,
|
||||||
|
],
|
||||||
|
'SPIDER_MD_GITHUB_TOKEN' => [
|
||||||
|
'default' => '',
|
||||||
|
'description' => '蜘蛛池MD GitHub Token(建议使用细粒度 Token,并授予当前仓库 Contents: Read and write 权限)',
|
||||||
'secret' => true,
|
'secret' => true,
|
||||||
],
|
],
|
||||||
'SPIDER_MD_RETENTION_DAYS' => [
|
'SPIDER_MD_RETENTION_DAYS' => [
|
||||||
@@ -197,6 +227,43 @@ class SpiderMdConfigHelper
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function resolveGithubConfig(): array
|
||||||
|
{
|
||||||
|
self::ensureSystemConfigDefaults();
|
||||||
|
|
||||||
|
$enabledMeta = self::readValueMeta('SPIDER_MD_GITHUB_ENABLED');
|
||||||
|
$ownerMeta = self::readValueMeta('SPIDER_MD_GITHUB_OWNER');
|
||||||
|
$repoMeta = self::readValueMeta('SPIDER_MD_GITHUB_REPO');
|
||||||
|
$branchMeta = self::readValueMeta('SPIDER_MD_GITHUB_BRANCH');
|
||||||
|
$rootMeta = self::readValueMeta('SPIDER_MD_GITHUB_ROOT');
|
||||||
|
$tokenMeta = self::readValueMeta('SPIDER_MD_GITHUB_TOKEN');
|
||||||
|
|
||||||
|
$enabled = (int)($enabledMeta['value'] ?? '0') === 1;
|
||||||
|
$owner = trim((string)($ownerMeta['value'] ?? ''));
|
||||||
|
$repo = trim((string)($repoMeta['value'] ?? ''));
|
||||||
|
$branch = trim((string)($branchMeta['value'] ?? 'main'));
|
||||||
|
$root = trim((string)($rootMeta['value'] ?? 'seo-spider-md'));
|
||||||
|
$token = trim((string)($tokenMeta['value'] ?? ''));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'enabled' => $enabled ? 1 : 0,
|
||||||
|
'configured' => ($enabled && $owner !== '' && $repo !== '' && $token !== '') ? 1 : 0,
|
||||||
|
'owner' => $owner,
|
||||||
|
'repo' => $repo,
|
||||||
|
'branch' => $branch !== '' ? $branch : 'main',
|
||||||
|
'root' => $root,
|
||||||
|
'token' => $token,
|
||||||
|
'sources' => [
|
||||||
|
'enabled' => (string)($enabledMeta['source'] ?? 'env'),
|
||||||
|
'owner' => (string)($ownerMeta['source'] ?? 'env'),
|
||||||
|
'repo' => (string)($repoMeta['source'] ?? 'env'),
|
||||||
|
'branch' => (string)($branchMeta['source'] ?? 'env'),
|
||||||
|
'root' => (string)($rootMeta['source'] ?? 'env'),
|
||||||
|
'token' => (string)($tokenMeta['source'] ?? 'env'),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
protected static function readOperationalValue(string $code): string
|
protected static function readOperationalValue(string $code): string
|
||||||
{
|
{
|
||||||
return (string)(self::readValueMeta($code)['value'] ?? '');
|
return (string)(self::readValueMeta($code)['value'] ?? '');
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class UrlBuilder
|
|||||||
return $this->replacePattern($pattern, [
|
return $this->replacePattern($pattern, [
|
||||||
'strParentCategory' => $strParentCategory,
|
'strParentCategory' => $strParentCategory,
|
||||||
'strCategory' => $strCategory,
|
'strCategory' => $strCategory,
|
||||||
'intPage' => max(1, (int)$intPage),
|
'intPage' => $this->normalizePageValue($intPage),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,4 +226,13 @@ class UrlBuilder
|
|||||||
}
|
}
|
||||||
return $this->trimSlash($pattern);
|
return $this->trimSlash($pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function normalizePageValue(int|string $page): string
|
||||||
|
{
|
||||||
|
if (is_string($page) && strpos($page, '{page}') !== false) {
|
||||||
|
return '{page}';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string)max(1, (int)$page);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
162
code/app/common/helper/VideoMetadataCopyFillHelper.php
Normal file
162
code/app/common/helper/VideoMetadataCopyFillHelper.php
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\helper;
|
||||||
|
|
||||||
|
class VideoMetadataCopyFillHelper
|
||||||
|
{
|
||||||
|
public static function buildFillPayload(array $arrVideo, string $strSource = 'local_copy_fill'): array
|
||||||
|
{
|
||||||
|
$strDescription = trim((string)($arrVideo['v_description'] ?? ''));
|
||||||
|
$strRemarks = trim((string)($arrVideo['v_remarks'] ?? ''));
|
||||||
|
|
||||||
|
$arrUpdate = [];
|
||||||
|
$arrFilledFields = [];
|
||||||
|
|
||||||
|
if ($strDescription === '') {
|
||||||
|
$strGeneratedDescription = self::buildDescription($arrVideo);
|
||||||
|
if ($strGeneratedDescription !== '') {
|
||||||
|
$arrUpdate['v_description'] = $strGeneratedDescription;
|
||||||
|
$arrFilledFields[] = 'v_description';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($strRemarks === '') {
|
||||||
|
$strGeneratedRemarks = self::buildRemarks($arrVideo);
|
||||||
|
if ($strGeneratedRemarks !== '') {
|
||||||
|
$arrUpdate['v_remarks'] = $strGeneratedRemarks;
|
||||||
|
$arrFilledFields[] = 'v_remarks';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($arrFilledFields)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrUpdate['updated_at'] = new \MongoDB\BSON\UTCDateTime();
|
||||||
|
$arrUpdate['v_metadata_fill'] = [
|
||||||
|
'last_source' => $strSource,
|
||||||
|
'updated_at' => new \MongoDB\BSON\UTCDateTime(),
|
||||||
|
'filled_fields' => $arrFilledFields,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $arrUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function buildDescription(array $arrVideo): string
|
||||||
|
{
|
||||||
|
$strVideoName = trim((string)($arrVideo['v_name'] ?? ''));
|
||||||
|
if ($strVideoName === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$strCategory = trim((string)($arrVideo['v_category'] ?? ''));
|
||||||
|
$strParentCategory = trim((string)($arrVideo['v_parent_category'] ?? ''));
|
||||||
|
$strCategoryLabel = $strCategory !== '' ? $strCategory : $strParentCategory;
|
||||||
|
$strYear = trim((string)($arrVideo['v_year'] ?? ''));
|
||||||
|
$strArea = self::joinList($arrVideo['v_area'] ?? []);
|
||||||
|
$strLang = self::joinList($arrVideo['v_lang'] ?? []);
|
||||||
|
$strDirector = self::joinList($arrVideo['v_director'] ?? []);
|
||||||
|
$strActor = self::joinList($arrVideo['v_actor'] ?? [], 4);
|
||||||
|
$strRemarks = trim((string)($arrVideo['v_remarks'] ?? ''));
|
||||||
|
$boolEnded = (int)($arrVideo['v_isend'] ?? 0) === 1;
|
||||||
|
|
||||||
|
$arrSentences = [];
|
||||||
|
|
||||||
|
$strHead = $strVideoName;
|
||||||
|
if ($strCategoryLabel !== '') {
|
||||||
|
$strHead .= '属于' . $strCategoryLabel;
|
||||||
|
} else {
|
||||||
|
$strHead .= '为影视内容资料页';
|
||||||
|
}
|
||||||
|
if ($strYear !== '') {
|
||||||
|
$strHead .= ',记录年份为' . $strYear;
|
||||||
|
}
|
||||||
|
if ($strArea !== '') {
|
||||||
|
$strHead .= ',地区信息为' . $strArea;
|
||||||
|
}
|
||||||
|
if ($strLang !== '') {
|
||||||
|
$strHead .= ',语言信息为' . $strLang;
|
||||||
|
}
|
||||||
|
$arrSentences[] = $strHead . '。';
|
||||||
|
|
||||||
|
$arrPeople = [];
|
||||||
|
if ($strDirector !== '') {
|
||||||
|
$arrPeople[] = '导演信息为' . $strDirector;
|
||||||
|
}
|
||||||
|
if ($strActor !== '') {
|
||||||
|
$arrPeople[] = '演员阵容包括' . $strActor;
|
||||||
|
}
|
||||||
|
if (!empty($arrPeople)) {
|
||||||
|
$arrSentences[] = implode(',', $arrPeople) . '。';
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrStatus = [];
|
||||||
|
if ($strRemarks !== '') {
|
||||||
|
$arrStatus[] = '当前更新备注为' . $strRemarks;
|
||||||
|
}
|
||||||
|
$arrStatus[] = $boolEnded ? '内容状态显示已完结' : '内容状态以当前更新进度为准';
|
||||||
|
$arrSentences[] = implode(',', $arrStatus) . '。';
|
||||||
|
|
||||||
|
$strDescription = preg_replace('/\s+/u', '', implode('', $arrSentences));
|
||||||
|
$strDescription = trim((string)$strDescription);
|
||||||
|
|
||||||
|
if ($strDescription === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mb_strlen($strDescription) > 180) {
|
||||||
|
$strDescription = mb_substr($strDescription, 0, 178) . '。';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $strDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function buildRemarks(array $arrVideo): string
|
||||||
|
{
|
||||||
|
$strRemarks = trim((string)($arrVideo['v_remarks'] ?? ''));
|
||||||
|
if ($strRemarks !== '') {
|
||||||
|
return $strRemarks;
|
||||||
|
}
|
||||||
|
|
||||||
|
$boolEnded = (int)($arrVideo['v_isend'] ?? 0) === 1;
|
||||||
|
$strPublishDate = trim((string)($arrVideo['v_publish_date'] ?? ''));
|
||||||
|
$strYear = trim((string)($arrVideo['v_year'] ?? ''));
|
||||||
|
|
||||||
|
if ($boolEnded) {
|
||||||
|
return '已完结';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($strPublishDate !== '') {
|
||||||
|
return '更新至' . mb_substr($strPublishDate, 0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($strYear !== '') {
|
||||||
|
return $strYear . '年内容';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function joinList($mixedValue, int $intLimit = 3): string
|
||||||
|
{
|
||||||
|
$arrValues = is_array($mixedValue) ? $mixedValue : [$mixedValue];
|
||||||
|
$arrValues = array_values(array_filter(array_map(static function ($mixedItem): string {
|
||||||
|
return trim((string)$mixedItem);
|
||||||
|
}, $arrValues), static function (string $strItem): bool {
|
||||||
|
if ($strItem === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrInvalid = ['未知', '内详', '暂无', '不详', '待补充', '未知导演', '未知演员'];
|
||||||
|
return !in_array($strItem, $arrInvalid, true);
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (empty($arrValues)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode('、', array_slice($arrValues, 0, $intLimit));
|
||||||
|
}
|
||||||
|
}
|
||||||
62
code/app/common/helper/VideoMetadataMissingRefreshHelper.php
Normal file
62
code/app/common/helper/VideoMetadataMissingRefreshHelper.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\helper;
|
||||||
|
|
||||||
|
class VideoMetadataMissingRefreshHelper
|
||||||
|
{
|
||||||
|
public static function refresh(array $arrOptions = []): array
|
||||||
|
{
|
||||||
|
$intSample = max(1, min(100, (int)($arrOptions['sample'] ?? 20)));
|
||||||
|
$intQueueLimit = max(1, min(500, (int)($arrOptions['queue_limit'] ?? 100)));
|
||||||
|
$intPromptLimit = max(1, min(100, (int)($arrOptions['prompt_limit'] ?? 20)));
|
||||||
|
$intBatchSize = max(5, min(100, (int)($arrOptions['batch_size'] ?? 20)));
|
||||||
|
$intBatchLimit = max(1, min(50, (int)($arrOptions['batch_limit'] ?? 10)));
|
||||||
|
|
||||||
|
$strStorageRoot = rtrim((string)root_path(), '/') . '/storage/video-metadata-missing';
|
||||||
|
$strWorkbenchRoot = $strStorageRoot . '/workbench';
|
||||||
|
$strTaskPoolRoot = $strStorageRoot . '/task-pool';
|
||||||
|
|
||||||
|
$arrWorkbenchSummary = VideoMetadataMissingWorkbenchHelper::buildSummary(
|
||||||
|
$intSample,
|
||||||
|
$intQueueLimit,
|
||||||
|
$intPromptLimit
|
||||||
|
);
|
||||||
|
$arrWorkbenchSummary = VideoMetadataMissingWorkbenchHelper::writeArtifacts(
|
||||||
|
$strWorkbenchRoot,
|
||||||
|
$arrWorkbenchSummary
|
||||||
|
);
|
||||||
|
|
||||||
|
$arrTaskPoolSummary = VideoMetadataMissingTaskPoolHelper::buildSummary(
|
||||||
|
$arrWorkbenchSummary,
|
||||||
|
$intBatchSize,
|
||||||
|
$intBatchLimit
|
||||||
|
);
|
||||||
|
$arrTaskPoolSummary = VideoMetadataMissingTaskPoolHelper::writeArtifacts(
|
||||||
|
$strTaskPoolRoot,
|
||||||
|
$arrTaskPoolSummary
|
||||||
|
);
|
||||||
|
$arrTaskPoolStatusSummary = VideoMetadataMissingTaskPoolStatusHelper::buildSummary(
|
||||||
|
$strWorkbenchRoot,
|
||||||
|
$strTaskPoolRoot
|
||||||
|
);
|
||||||
|
$arrTaskPoolStatusSummary = VideoMetadataMissingTaskPoolStatusHelper::writeArtifacts(
|
||||||
|
$strTaskPoolRoot,
|
||||||
|
$arrTaskPoolStatusSummary
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'options' => [
|
||||||
|
'sample' => $intSample,
|
||||||
|
'queue_limit' => $intQueueLimit,
|
||||||
|
'prompt_limit' => $intPromptLimit,
|
||||||
|
'batch_size' => $intBatchSize,
|
||||||
|
'batch_limit' => $intBatchLimit,
|
||||||
|
],
|
||||||
|
'workbench' => $arrWorkbenchSummary,
|
||||||
|
'task_pool' => $arrTaskPoolSummary,
|
||||||
|
'task_pool_status' => $arrTaskPoolStatusSummary,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
450
code/app/common/helper/VideoMetadataMissingTaskPoolHelper.php
Normal file
450
code/app/common/helper/VideoMetadataMissingTaskPoolHelper.php
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\helper;
|
||||||
|
|
||||||
|
class VideoMetadataMissingTaskPoolHelper
|
||||||
|
{
|
||||||
|
public static function buildSummary(array $arrWorkbenchSummary, int $intBatchSize = 20, int $intBatchLimit = 10): array
|
||||||
|
{
|
||||||
|
$intBatchSize = max(5, min($intBatchSize, 100));
|
||||||
|
$intBatchLimit = max(1, min($intBatchLimit, 50));
|
||||||
|
|
||||||
|
$arrAudit = (array)($arrWorkbenchSummary['audit'] ?? []);
|
||||||
|
$arrQueue = (array)($arrWorkbenchSummary['queue'] ?? []);
|
||||||
|
$arrItems = (array)($arrQueue['items'] ?? []);
|
||||||
|
$arrFieldStats = (array)($arrAudit['field_stats'] ?? []);
|
||||||
|
usort($arrFieldStats, static function (array $arrA, array $arrB): int {
|
||||||
|
return (int)($arrB['missing_count'] ?? 0) <=> (int)($arrA['missing_count'] ?? 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
$arrBatches = self::buildBatches($arrItems, $arrFieldStats, $intBatchSize, $intBatchLimit);
|
||||||
|
|
||||||
|
$arrSummary = [
|
||||||
|
'generated_at' => date('c'),
|
||||||
|
'source_generated_at' => (string)($arrWorkbenchSummary['generated_at'] ?? ''),
|
||||||
|
'batch_size' => $intBatchSize,
|
||||||
|
'batch_limit' => $intBatchLimit,
|
||||||
|
'total_videos' => (int)($arrAudit['total_videos'] ?? 0),
|
||||||
|
'videos_with_any_missing_metadata' => (int)($arrAudit['videos_with_any_missing_metadata'] ?? 0),
|
||||||
|
'queue_size' => (int)($arrQueue['queue_size'] ?? 0),
|
||||||
|
'top_missing_fields' => array_values(array_slice($arrFieldStats, 0, 5)),
|
||||||
|
'batches' => $arrBatches,
|
||||||
|
'todo_summary' => self::buildTodoSummary($arrBatches),
|
||||||
|
'operator_hint' => '这是一套给技术 / Codex 使用的缺字段待处理池。当前先用产物型批次池,不直接入库,不自动补事实字段。',
|
||||||
|
];
|
||||||
|
|
||||||
|
return VideoMetadataMissingTaskPoolStateHelper::applyStateMap($arrSummary, [], '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function writeArtifacts(string $strOutputRoot, array $arrSummary): array
|
||||||
|
{
|
||||||
|
$strOutputRoot = rtrim(str_replace('\\', '/', $strOutputRoot), '/');
|
||||||
|
$arrSummary = VideoMetadataMissingTaskPoolStateHelper::applyStateMap(
|
||||||
|
$arrSummary,
|
||||||
|
VideoMetadataMissingTaskPoolStateHelper::readStateMap($strOutputRoot),
|
||||||
|
$strOutputRoot
|
||||||
|
);
|
||||||
|
$arrSummary['run_id'] = self::buildRunId();
|
||||||
|
$arrSummary['summary_json_path'] = $strOutputRoot . '/index.json';
|
||||||
|
$arrSummary['summary_html_path'] = $strOutputRoot . '/index.html';
|
||||||
|
$arrSummary = self::writeLatestBatchArtifacts($strOutputRoot, $arrSummary);
|
||||||
|
|
||||||
|
self::writeFile(
|
||||||
|
$arrSummary['summary_json_path'],
|
||||||
|
json_encode($arrSummary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
||||||
|
);
|
||||||
|
self::writeFile($arrSummary['summary_html_path'], self::renderHtml($arrSummary));
|
||||||
|
self::writeRunArtifacts($strOutputRoot, $arrSummary);
|
||||||
|
|
||||||
|
return $arrSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function readLatestSummary(string $strOutputRoot): array
|
||||||
|
{
|
||||||
|
$strPath = rtrim(str_replace('\\', '/', $strOutputRoot), '/') . '/index.json';
|
||||||
|
if (!is_file($strPath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrSummary = json_decode((string)file_get_contents($strPath), true);
|
||||||
|
if (!is_array($arrSummary)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return VideoMetadataMissingTaskPoolStateHelper::applyStateMap(
|
||||||
|
$arrSummary,
|
||||||
|
VideoMetadataMissingTaskPoolStateHelper::readStateMap($strOutputRoot),
|
||||||
|
$strOutputRoot
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function findBatchById(array $arrSummary, string $strBatchId): array
|
||||||
|
{
|
||||||
|
$strBatchId = trim($strBatchId);
|
||||||
|
if ($strBatchId === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ((array)($arrSummary['batches'] ?? []) as $arrBatch) {
|
||||||
|
if (!is_array($arrBatch)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((string)($arrBatch['batch_id'] ?? '') === $strBatchId) {
|
||||||
|
return $arrBatch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function buildBatches(array $arrItems, array $arrFieldStats, int $intBatchSize, int $intBatchLimit): array
|
||||||
|
{
|
||||||
|
$arrFieldOrder = [];
|
||||||
|
foreach ($arrFieldStats as $arrField) {
|
||||||
|
$strField = trim((string)($arrField['field'] ?? ''));
|
||||||
|
if ($strField !== '') {
|
||||||
|
$arrFieldOrder[] = $strField;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (empty($arrFieldOrder)) {
|
||||||
|
$arrFieldOrder = ['v_director', 'v_actor'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrBuckets = [];
|
||||||
|
foreach ($arrItems as $arrItem) {
|
||||||
|
if (!is_array($arrItem)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrMissingFields = array_values(array_filter((array)($arrItem['missing_fields'] ?? []), static fn ($strField): bool => trim((string)$strField) !== ''));
|
||||||
|
$strPrimaryField = '';
|
||||||
|
foreach ($arrFieldOrder as $strField) {
|
||||||
|
if (in_array($strField, $arrMissingFields, true)) {
|
||||||
|
$strPrimaryField = $strField;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($strPrimaryField === '') {
|
||||||
|
$strPrimaryField = (string)($arrMissingFields[0] ?? 'unknown');
|
||||||
|
}
|
||||||
|
$arrBuckets[$strPrimaryField][] = $arrItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrBatches = [];
|
||||||
|
$intBatchNo = 1;
|
||||||
|
foreach ($arrFieldOrder as $strField) {
|
||||||
|
$arrBucket = (array)($arrBuckets[$strField] ?? []);
|
||||||
|
if (empty($arrBucket)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrChunks = array_chunk($arrBucket, $intBatchSize);
|
||||||
|
foreach ($arrChunks as $intChunkIndex => $arrChunk) {
|
||||||
|
if (count($arrBatches) >= $intBatchLimit) {
|
||||||
|
break 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
$strBatchId = sprintf('video-metadata-batch-%02d', $intBatchNo);
|
||||||
|
$arrBatches[] = [
|
||||||
|
'batch_id' => $strBatchId,
|
||||||
|
'batch_no' => $intBatchNo,
|
||||||
|
'focus_field' => $strField,
|
||||||
|
'batch_label' => self::buildBatchLabel($strField, $intChunkIndex + 1),
|
||||||
|
'items_count' => count($arrChunk),
|
||||||
|
'priority_level' => self::resolvePriorityLevel($strField),
|
||||||
|
'items' => array_values($arrChunk),
|
||||||
|
'codex_prompt' => self::buildBatchPrompt($strField, $strBatchId, $arrChunk),
|
||||||
|
];
|
||||||
|
$intBatchNo++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $arrBatches;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function buildTodoSummary(array $arrBatches): array
|
||||||
|
{
|
||||||
|
$arrTodo = [];
|
||||||
|
foreach ($arrBatches as $arrBatch) {
|
||||||
|
$arrTodo[] = [
|
||||||
|
'batch_id' => (string)($arrBatch['batch_id'] ?? ''),
|
||||||
|
'label' => (string)($arrBatch['batch_label'] ?? ''),
|
||||||
|
'focus_field' => (string)($arrBatch['focus_field'] ?? ''),
|
||||||
|
'items_count' => (int)($arrBatch['items_count'] ?? 0),
|
||||||
|
'priority_level' => (string)($arrBatch['priority_level'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $arrTodo;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function buildBatchLabel(string $strField, int $intChunkNo): string
|
||||||
|
{
|
||||||
|
$arrMap = [
|
||||||
|
'v_director' => '导演缺失批次',
|
||||||
|
'v_actor' => '演员缺失批次',
|
||||||
|
'v_lang' => '语言缺失批次',
|
||||||
|
'v_lang_en' => '英文语言缺失批次',
|
||||||
|
'v_area' => '地区缺失批次',
|
||||||
|
'v_area_en' => '英文地区缺失批次',
|
||||||
|
'v_year' => '年份缺失批次',
|
||||||
|
'v_description' => '简介缺失批次',
|
||||||
|
'v_remarks' => '备注缺失批次',
|
||||||
|
'v_publish_date' => '上映时间缺失批次',
|
||||||
|
];
|
||||||
|
|
||||||
|
return ($arrMap[$strField] ?? ($strField . ' 缺失批次')) . ' #' . $intChunkNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function resolvePriorityLevel(string $strField): string
|
||||||
|
{
|
||||||
|
return match ($strField) {
|
||||||
|
'v_director', 'v_actor' => 'P1',
|
||||||
|
'v_lang', 'v_lang_en', 'v_area', 'v_area_en' => 'P2',
|
||||||
|
default => 'P3',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function buildBatchPrompt(string $strField, string $strBatchId, array $arrItems): string
|
||||||
|
{
|
||||||
|
$arrLines = [];
|
||||||
|
$arrLines[] = '# 视频缺字段批次接手';
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '- 批次ID:' . $strBatchId;
|
||||||
|
$arrLines[] = '- 重点字段:' . $strField;
|
||||||
|
$arrLines[] = '- 批次数量:' . count($arrItems);
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '要求:';
|
||||||
|
$arrLines[] = '1. 结构化字段不能编造,优先建议重采、交叉源核验、人工确认。';
|
||||||
|
$arrLines[] = '2. 先按优先级给出处理顺序,再给出每条或每组的建议动作。';
|
||||||
|
$arrLines[] = '3. 如果某些条目只适合补文案,不适合补事实字段,要明确标记。';
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '待处理视频:';
|
||||||
|
|
||||||
|
foreach ($arrItems as $arrItem) {
|
||||||
|
$arrLines[] = sprintf(
|
||||||
|
'- v_id=%d | %s | 分类=%s | 缺失=%s | 原因=%s',
|
||||||
|
(int)($arrItem['v_id'] ?? 0),
|
||||||
|
trim((string)($arrItem['v_name'] ?? '')),
|
||||||
|
trim((string)($arrItem['v_category'] ?? '')),
|
||||||
|
implode(',', (array)($arrItem['missing_fields'] ?? [])),
|
||||||
|
trim((string)($arrItem['priority_reason'] ?? ''))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '请输出:';
|
||||||
|
$arrLines[] = '1. 本批次的处理优先顺序';
|
||||||
|
$arrLines[] = '2. 每条适合走重采 / 人工核验 / 仅补文案 哪一种';
|
||||||
|
$arrLines[] = '3. 如果要继续写脚本或命令,请直接给执行建议';
|
||||||
|
$arrLines[] = '';
|
||||||
|
|
||||||
|
return implode(PHP_EOL, $arrLines) . PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function renderHtml(array $arrSummary): string
|
||||||
|
{
|
||||||
|
$arrBatches = (array)($arrSummary['batches'] ?? []);
|
||||||
|
$strRows = '';
|
||||||
|
foreach ($arrBatches as $arrBatch) {
|
||||||
|
$strPromptPath = htmlspecialchars((string)($arrBatch['prompt_markdown_path'] ?? ''), ENT_QUOTES, 'UTF-8');
|
||||||
|
$strDetailPath = htmlspecialchars((string)($arrBatch['detail_json_path'] ?? ''), ENT_QUOTES, 'UTF-8');
|
||||||
|
$strHistoryPath = htmlspecialchars((string)($arrBatch['history_json_path'] ?? ''), ENT_QUOTES, 'UTF-8');
|
||||||
|
$strRows .= '<tr>'
|
||||||
|
. '<td>' . htmlspecialchars((string)($arrBatch['batch_id'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>'
|
||||||
|
. '<td>' . htmlspecialchars((string)($arrBatch['batch_label'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>'
|
||||||
|
. '<td>' . htmlspecialchars((string)($arrBatch['focus_field'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>'
|
||||||
|
. '<td>' . htmlspecialchars((string)($arrBatch['priority_level'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>'
|
||||||
|
. '<td>' . htmlspecialchars((string)($arrBatch['status_label'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>'
|
||||||
|
. '<td>' . htmlspecialchars((string)($arrBatch['owner'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>'
|
||||||
|
. '<td>' . (int)($arrBatch['history_count'] ?? 0) . '</td>'
|
||||||
|
. '<td>' . (int)($arrBatch['items_count'] ?? 0) . '</td>'
|
||||||
|
. '<td>'
|
||||||
|
. '<button type="button" class="btn" data-copy-prompt="' . htmlspecialchars((string)($arrBatch['codex_prompt'] ?? ''), ENT_QUOTES, 'UTF-8') . '">复制提示词</button> '
|
||||||
|
. ($strDetailPath !== '' ? '<a class="btn link" href="' . $strDetailPath . '" target="_blank">详情JSON</a> ' : '')
|
||||||
|
. ($strPromptPath !== '' ? '<a class="btn link" href="' . $strPromptPath . '" target="_blank">提示词MD</a> ' : '')
|
||||||
|
. ($strHistoryPath !== '' ? '<a class="btn link" href="' . $strHistoryPath . '" target="_blank">历史JSON</a>' : '')
|
||||||
|
. '</td>'
|
||||||
|
. '</tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$strCards = '';
|
||||||
|
foreach ($arrBatches as $arrBatch) {
|
||||||
|
$strPromptPath = htmlspecialchars((string)($arrBatch['prompt_markdown_path'] ?? ''), ENT_QUOTES, 'UTF-8');
|
||||||
|
$strDetailPath = htmlspecialchars((string)($arrBatch['detail_json_path'] ?? ''), ENT_QUOTES, 'UTF-8');
|
||||||
|
$strHistoryPath = htmlspecialchars((string)($arrBatch['history_json_path'] ?? ''), ENT_QUOTES, 'UTF-8');
|
||||||
|
$strCards .= '<div class="batch-card">'
|
||||||
|
. '<h3>' . htmlspecialchars((string)($arrBatch['batch_label'] ?? ''), ENT_QUOTES, 'UTF-8') . '</h3>'
|
||||||
|
. '<p><strong>批次ID:</strong>' . htmlspecialchars((string)($arrBatch['batch_id'] ?? ''), ENT_QUOTES, 'UTF-8') . '</p>'
|
||||||
|
. '<p><strong>重点字段:</strong>' . htmlspecialchars((string)($arrBatch['focus_field'] ?? ''), ENT_QUOTES, 'UTF-8') . '</p>'
|
||||||
|
. '<p><strong>优先级:</strong>' . htmlspecialchars((string)($arrBatch['priority_level'] ?? ''), ENT_QUOTES, 'UTF-8') . '</p>'
|
||||||
|
. '<p><strong>状态:</strong>' . htmlspecialchars((string)($arrBatch['status_label'] ?? ''), ENT_QUOTES, 'UTF-8') . '</p>'
|
||||||
|
. '<p><strong>负责人:</strong>' . htmlspecialchars((string)($arrBatch['owner'] ?? ''), ENT_QUOTES, 'UTF-8') . '</p>'
|
||||||
|
. '<p><strong>历史次数:</strong>' . (int)($arrBatch['history_count'] ?? 0) . '</p>'
|
||||||
|
. '<p><strong>数量:</strong>' . (int)($arrBatch['items_count'] ?? 0) . '</p>'
|
||||||
|
. '<p class="actions">'
|
||||||
|
. '<button type="button" class="btn" data-copy-prompt="' . htmlspecialchars((string)($arrBatch['codex_prompt'] ?? ''), ENT_QUOTES, 'UTF-8') . '">复制提示词</button> '
|
||||||
|
. ($strDetailPath !== '' ? '<a class="btn link" href="' . $strDetailPath . '" target="_blank">详情JSON</a> ' : '')
|
||||||
|
. ($strPromptPath !== '' ? '<a class="btn link" href="' . $strPromptPath . '" target="_blank">提示词MD</a> ' : '')
|
||||||
|
. ($strHistoryPath !== '' ? '<a class="btn link" href="' . $strHistoryPath . '" target="_blank">历史JSON</a>' : '')
|
||||||
|
. '</p>'
|
||||||
|
. '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>视频缺字段待处理池</title>
|
||||||
|
<style>
|
||||||
|
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#f6f8fb;color:#1f2937;margin:0;padding:24px;}
|
||||||
|
.wrap{max-width:1200px;margin:0 auto;}
|
||||||
|
.card{background:#fff;border-radius:16px;box-shadow:0 8px 24px rgba(15,23,42,.06);padding:20px;margin-bottom:20px;}
|
||||||
|
.cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;}
|
||||||
|
.batch-card{border:1px solid #e5e7eb;border-radius:12px;padding:16px;background:#f8fafc;}
|
||||||
|
.batch-card h3{margin:0 0 10px;}
|
||||||
|
.batch-card p{margin:6px 0;font-size:14px;}
|
||||||
|
.actions{margin-top:12px;}
|
||||||
|
.btn{display:inline-block;border:1px solid #cbd5e1;background:#fff;color:#0f172a;border-radius:8px;padding:6px 10px;font-size:13px;text-decoration:none;cursor:pointer;}
|
||||||
|
.btn.link{cursor:pointer;}
|
||||||
|
.toolbar{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:12px;}
|
||||||
|
.toolbar .msg{font-size:13px;color:#475569;}
|
||||||
|
table{width:100%;border-collapse:collapse;}
|
||||||
|
th,td{border-bottom:1px solid #e5e7eb;padding:10px;text-align:left;vertical-align:top;font-size:14px;}
|
||||||
|
th{background:#f8fafc;}
|
||||||
|
@media (max-width: 900px){.cards{grid-template-columns:1fr;}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="card">
|
||||||
|
<h1>视频缺字段待处理池</h1>
|
||||||
|
<p>生成时间:' . htmlspecialchars((string)($arrSummary['generated_at'] ?? ''), ENT_QUOTES, 'UTF-8') . '</p>
|
||||||
|
<p>说明:当前先做产物型任务池,不自动补事实字段,不直接入库。</p>
|
||||||
|
<div class="toolbar"><span class="msg" id="copy-status">点击“复制提示词”后,可直接粘贴到 Codex 窗口。</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>批次卡片</h2>
|
||||||
|
<div class="cards">' . $strCards . '</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>批次ID</th><th>批次名称</th><th>重点字段</th><th>优先级</th><th>状态</th><th>负责人</th><th>历史</th><th>数量</th><th>动作</th></tr></thead>
|
||||||
|
<tbody>' . $strRows . '</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
document.querySelectorAll("[data-copy-prompt]").forEach(function (btn) {
|
||||||
|
btn.addEventListener("click", async function () {
|
||||||
|
var text = btn.getAttribute("data-copy-prompt") || "";
|
||||||
|
var status = document.getElementById("copy-status");
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
if (status) status.textContent = "提示词已复制,可以直接粘贴给 Codex。";
|
||||||
|
} catch (err) {
|
||||||
|
if (status) status.textContent = "当前浏览器未允许剪贴板复制,请改为打开提示词 MD 手动复制。";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function writeRunArtifacts(string $strOutputRoot, array $arrSummary): void
|
||||||
|
{
|
||||||
|
$strRunId = trim((string)($arrSummary['run_id'] ?? ''));
|
||||||
|
if ($strRunId === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$strDay = substr((string)($arrSummary['generated_at'] ?? date('c')), 0, 10);
|
||||||
|
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $strDay)) {
|
||||||
|
$strDay = date('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
$strRunRoot = $strOutputRoot . '/runs/' . $strDay . '/' . $strRunId;
|
||||||
|
self::writeFile(
|
||||||
|
$strRunRoot . '/video-metadata-missing-task-pool.summary.json',
|
||||||
|
json_encode($arrSummary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
||||||
|
);
|
||||||
|
self::writeFile(
|
||||||
|
$strRunRoot . '/video-metadata-missing-task-pool.summary.html',
|
||||||
|
self::renderHtml($arrSummary)
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ((array)($arrSummary['batches'] ?? []) as $arrBatch) {
|
||||||
|
$strBatchId = trim((string)($arrBatch['batch_id'] ?? ''));
|
||||||
|
if ($strBatchId === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self::writeFile($strRunRoot . '/batches/' . $strBatchId . '.md', (string)($arrBatch['codex_prompt'] ?? ''));
|
||||||
|
self::writeFile(
|
||||||
|
$strRunRoot . '/batches/' . $strBatchId . '.json',
|
||||||
|
json_encode($arrBatch, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function writeLatestBatchArtifacts(string $strOutputRoot, array $arrSummary): array
|
||||||
|
{
|
||||||
|
foreach ((array)($arrSummary['batches'] ?? []) as $intIndex => $arrBatch) {
|
||||||
|
if (!is_array($arrBatch)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$strBatchId = trim((string)($arrBatch['batch_id'] ?? ''));
|
||||||
|
if ($strBatchId === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$strDetailPath = $strOutputRoot . '/batches/' . $strBatchId . '.json';
|
||||||
|
$strPromptPath = $strOutputRoot . '/batches/' . $strBatchId . '.md';
|
||||||
|
$strHistoryPath = $strOutputRoot . '/state/history/' . $strBatchId . '.json';
|
||||||
|
|
||||||
|
self::writeFile(
|
||||||
|
$strDetailPath,
|
||||||
|
json_encode($arrBatch, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
||||||
|
);
|
||||||
|
self::writeFile($strPromptPath, (string)($arrBatch['codex_prompt'] ?? ''));
|
||||||
|
|
||||||
|
$arrSummary['batches'][$intIndex]['detail_json_path'] = self::storageRelativePath($strDetailPath);
|
||||||
|
$arrSummary['batches'][$intIndex]['prompt_markdown_path'] = self::storageRelativePath($strPromptPath);
|
||||||
|
$arrSummary['batches'][$intIndex]['history_json_path'] = is_file($strHistoryPath)
|
||||||
|
? self::storageRelativePath($strHistoryPath)
|
||||||
|
: self::storageRelativePath($strHistoryPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $arrSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function writeFile(string $strPath, string $strContent): void
|
||||||
|
{
|
||||||
|
$strDir = dirname($strPath);
|
||||||
|
if (!is_dir($strDir)) {
|
||||||
|
@mkdir($strDir, 0777, true);
|
||||||
|
}
|
||||||
|
file_put_contents($strPath, $strContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function buildRunId(): string
|
||||||
|
{
|
||||||
|
return 'video-metadata-missing-task-pool-' . date('Ymd-His');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function storageRelativePath(string $strPath): string
|
||||||
|
{
|
||||||
|
$strStorageRoot = str_replace('\\', '/', rtrim(dirname(__DIR__, 3) . '/storage', '/'));
|
||||||
|
$strPath = str_replace('\\', '/', $strPath);
|
||||||
|
if (str_starts_with($strPath, $strStorageRoot . '/')) {
|
||||||
|
return substr($strPath, strlen($strStorageRoot . '/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $strPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\helper;
|
||||||
|
|
||||||
|
class VideoMetadataMissingTaskPoolStateHelper
|
||||||
|
{
|
||||||
|
public const STATUS_PENDING = 'pending';
|
||||||
|
public const STATUS_DISPATCHED = 'dispatched';
|
||||||
|
public const STATUS_PROCESSING = 'processing';
|
||||||
|
public const STATUS_DONE = 'done';
|
||||||
|
public const STATUS_SKIPPED = 'skipped';
|
||||||
|
|
||||||
|
public static function readStateMap(string $strOutputRoot): array
|
||||||
|
{
|
||||||
|
$strPath = self::stateFilePath($strOutputRoot);
|
||||||
|
if (!is_file($strPath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrData = json_decode((string)file_get_contents($strPath), true);
|
||||||
|
if (!is_array($arrData)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (array)($arrData['batches'] ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function saveBatchState(
|
||||||
|
string $strOutputRoot,
|
||||||
|
string $strBatchId,
|
||||||
|
string $strStatus,
|
||||||
|
string $strOwner = '',
|
||||||
|
string $strNote = ''
|
||||||
|
): array {
|
||||||
|
$strBatchId = trim($strBatchId);
|
||||||
|
if ($strBatchId === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$strStatus = self::normalizeStatus($strStatus);
|
||||||
|
$arrMap = self::readStateMap($strOutputRoot);
|
||||||
|
$arrMap[$strBatchId] = [
|
||||||
|
'batch_id' => $strBatchId,
|
||||||
|
'status' => $strStatus,
|
||||||
|
'owner' => trim($strOwner),
|
||||||
|
'note' => trim($strNote),
|
||||||
|
'updated_at' => self::nowIso8601WithMicroseconds(),
|
||||||
|
];
|
||||||
|
|
||||||
|
self::writeStateMap($strOutputRoot, $arrMap);
|
||||||
|
self::appendBatchHistory($strOutputRoot, $strBatchId, $arrMap[$strBatchId]);
|
||||||
|
return $arrMap[$strBatchId];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function readBatchHistory(string $strOutputRoot, string $strBatchId, int $intLimit = 50): array
|
||||||
|
{
|
||||||
|
$strBatchId = trim($strBatchId);
|
||||||
|
if ($strBatchId === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$strPath = self::historyFilePath($strOutputRoot, $strBatchId);
|
||||||
|
if (!is_file($strPath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrData = json_decode((string)file_get_contents($strPath), true);
|
||||||
|
if (!is_array($arrData)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrItems = array_values((array)($arrData['items'] ?? []));
|
||||||
|
usort($arrItems, static function (array $arrA, array $arrB): int {
|
||||||
|
return strcmp((string)($arrB['updated_at'] ?? ''), (string)($arrA['updated_at'] ?? ''));
|
||||||
|
});
|
||||||
|
|
||||||
|
return array_slice($arrItems, 0, max(1, min($intLimit, 200)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function applyStateMap(array $arrSummary, array $arrStateMap, string $strOutputRoot = ''): array
|
||||||
|
{
|
||||||
|
$arrBatches = (array)($arrSummary['batches'] ?? []);
|
||||||
|
$arrStatusBuckets = [];
|
||||||
|
$strOutputRoot = rtrim(str_replace('\\', '/', $strOutputRoot), '/');
|
||||||
|
|
||||||
|
foreach ($arrBatches as &$arrBatch) {
|
||||||
|
$strBatchId = trim((string)($arrBatch['batch_id'] ?? ''));
|
||||||
|
$arrState = (array)($arrStateMap[$strBatchId] ?? []);
|
||||||
|
$strStatus = self::normalizeStatus((string)($arrState['status'] ?? self::STATUS_PENDING));
|
||||||
|
$arrBatch['status'] = $strStatus;
|
||||||
|
$arrBatch['status_label'] = self::statusLabel($strStatus);
|
||||||
|
$arrBatch['owner'] = trim((string)($arrState['owner'] ?? ''));
|
||||||
|
$arrBatch['note'] = trim((string)($arrState['note'] ?? ''));
|
||||||
|
$arrBatch['state_updated_at'] = trim((string)($arrState['updated_at'] ?? ''));
|
||||||
|
$arrBatch['history_count'] = $strOutputRoot !== ''
|
||||||
|
? count(self::readBatchHistory($strOutputRoot, $strBatchId, 200))
|
||||||
|
: 0;
|
||||||
|
$arrStatusBuckets[$strStatus] = (int)($arrStatusBuckets[$strStatus] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
unset($arrBatch);
|
||||||
|
|
||||||
|
$arrSummary['batches'] = $arrBatches;
|
||||||
|
$arrSummary['status_buckets'] = $arrStatusBuckets;
|
||||||
|
|
||||||
|
$arrTodoSummary = [];
|
||||||
|
foreach ($arrBatches as $arrBatch) {
|
||||||
|
$arrTodoSummary[] = [
|
||||||
|
'batch_id' => (string)($arrBatch['batch_id'] ?? ''),
|
||||||
|
'label' => (string)($arrBatch['batch_label'] ?? ''),
|
||||||
|
'focus_field' => (string)($arrBatch['focus_field'] ?? ''),
|
||||||
|
'items_count' => (int)($arrBatch['items_count'] ?? 0),
|
||||||
|
'priority_level' => (string)($arrBatch['priority_level'] ?? ''),
|
||||||
|
'status' => (string)($arrBatch['status'] ?? self::STATUS_PENDING),
|
||||||
|
'status_label' => (string)($arrBatch['status_label'] ?? self::statusLabel(self::STATUS_PENDING)),
|
||||||
|
'owner' => (string)($arrBatch['owner'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$arrSummary['todo_summary'] = $arrTodoSummary;
|
||||||
|
|
||||||
|
return $arrSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function statusLabel(string $strStatus): string
|
||||||
|
{
|
||||||
|
return match (self::normalizeStatus($strStatus)) {
|
||||||
|
self::STATUS_PENDING => '待处理',
|
||||||
|
self::STATUS_DISPATCHED => '已派单',
|
||||||
|
self::STATUS_PROCESSING => '处理中',
|
||||||
|
self::STATUS_DONE => '已完成',
|
||||||
|
self::STATUS_SKIPPED => '已跳过',
|
||||||
|
default => '待处理',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function normalizeStatus(string $strStatus): string
|
||||||
|
{
|
||||||
|
$strStatus = strtolower(trim($strStatus));
|
||||||
|
$arrAllowed = [
|
||||||
|
self::STATUS_PENDING,
|
||||||
|
self::STATUS_DISPATCHED,
|
||||||
|
self::STATUS_PROCESSING,
|
||||||
|
self::STATUS_DONE,
|
||||||
|
self::STATUS_SKIPPED,
|
||||||
|
];
|
||||||
|
|
||||||
|
return in_array($strStatus, $arrAllowed, true) ? $strStatus : self::STATUS_PENDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function stateFilePath(string $strOutputRoot): string
|
||||||
|
{
|
||||||
|
return rtrim(str_replace('\\', '/', $strOutputRoot), '/') . '/state/batch-status.json';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function writeStateMap(string $strOutputRoot, array $arrMap): void
|
||||||
|
{
|
||||||
|
$strPath = self::stateFilePath($strOutputRoot);
|
||||||
|
$strDir = dirname($strPath);
|
||||||
|
if (!is_dir($strDir)) {
|
||||||
|
@mkdir($strDir, 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($strPath, json_encode([
|
||||||
|
'updated_at' => self::nowIso8601WithMicroseconds(),
|
||||||
|
'batches' => $arrMap,
|
||||||
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function appendBatchHistory(string $strOutputRoot, string $strBatchId, array $arrState): void
|
||||||
|
{
|
||||||
|
$strPath = self::historyFilePath($strOutputRoot, $strBatchId);
|
||||||
|
$strDir = dirname($strPath);
|
||||||
|
if (!is_dir($strDir)) {
|
||||||
|
@mkdir($strDir, 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrData = [];
|
||||||
|
if (is_file($strPath)) {
|
||||||
|
$arrDecoded = json_decode((string)file_get_contents($strPath), true);
|
||||||
|
if (is_array($arrDecoded)) {
|
||||||
|
$arrData = $arrDecoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrItems = array_values((array)($arrData['items'] ?? []));
|
||||||
|
$arrItems[] = [
|
||||||
|
'batch_id' => $strBatchId,
|
||||||
|
'status' => (string)($arrState['status'] ?? self::STATUS_PENDING),
|
||||||
|
'status_label' => self::statusLabel((string)($arrState['status'] ?? self::STATUS_PENDING)),
|
||||||
|
'owner' => (string)($arrState['owner'] ?? ''),
|
||||||
|
'note' => (string)($arrState['note'] ?? ''),
|
||||||
|
'updated_at' => (string)($arrState['updated_at'] ?? self::nowIso8601WithMicroseconds()),
|
||||||
|
];
|
||||||
|
|
||||||
|
file_put_contents($strPath, json_encode([
|
||||||
|
'updated_at' => self::nowIso8601WithMicroseconds(),
|
||||||
|
'batch_id' => $strBatchId,
|
||||||
|
'items' => $arrItems,
|
||||||
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function historyFilePath(string $strOutputRoot, string $strBatchId): string
|
||||||
|
{
|
||||||
|
return rtrim(str_replace('\\', '/', $strOutputRoot), '/') . '/state/history/' . $strBatchId . '.json';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function nowIso8601WithMicroseconds(): string
|
||||||
|
{
|
||||||
|
$floatNow = microtime(true);
|
||||||
|
$intNow = (int)$floatNow;
|
||||||
|
$intMicro = (int)(($floatNow - $intNow) * 1000000);
|
||||||
|
|
||||||
|
return sprintf('%s.%06d%s', date('Y-m-d\\TH:i:s', $intNow), $intMicro, date('P', $intNow));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,398 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\helper;
|
||||||
|
|
||||||
|
use app\model\PlanTaskModel;
|
||||||
|
|
||||||
|
class VideoMetadataMissingTaskPoolStatusHelper
|
||||||
|
{
|
||||||
|
public static function buildSummary(string $strWorkbenchRoot, string $strTaskPoolRoot): array
|
||||||
|
{
|
||||||
|
$arrWorkbench = VideoMetadataMissingWorkbenchHelper::readLatestSummary($strWorkbenchRoot);
|
||||||
|
$arrTaskPool = VideoMetadataMissingTaskPoolHelper::readLatestSummary($strTaskPoolRoot);
|
||||||
|
$arrBatchSummary = self::buildBatchSummary((array)($arrTaskPool['batches'] ?? []));
|
||||||
|
$arrPlanTask = self::buildPlanTaskStatus();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'generated_at' => date(DATE_ATOM),
|
||||||
|
'workbench' => [
|
||||||
|
'exists' => !empty($arrWorkbench),
|
||||||
|
'generated_at' => (string)($arrWorkbench['generated_at'] ?? ''),
|
||||||
|
'videos_with_any_missing_metadata' => (int)(($arrWorkbench['audit'] ?? [])['videos_with_any_missing_metadata'] ?? 0),
|
||||||
|
'queue_size' => (int)(($arrWorkbench['queue'] ?? [])['queue_size'] ?? 0),
|
||||||
|
'summary_html_path' => (string)($arrWorkbench['summary_html_path'] ?? ''),
|
||||||
|
'summary_json_path' => (string)($arrWorkbench['summary_json_path'] ?? ''),
|
||||||
|
],
|
||||||
|
'task_pool' => [
|
||||||
|
'exists' => !empty($arrTaskPool),
|
||||||
|
'generated_at' => (string)($arrTaskPool['generated_at'] ?? ''),
|
||||||
|
'batch_count' => count((array)($arrTaskPool['batches'] ?? [])),
|
||||||
|
'todo_summary_count' => count((array)($arrTaskPool['todo_summary'] ?? [])),
|
||||||
|
'summary_html_path' => (string)($arrTaskPool['summary_html_path'] ?? ''),
|
||||||
|
'summary_json_path' => (string)($arrTaskPool['summary_json_path'] ?? ''),
|
||||||
|
'batch_status_summary' => $arrBatchSummary,
|
||||||
|
],
|
||||||
|
'plan_task' => $arrPlanTask,
|
||||||
|
'operator_hint' => self::buildOperatorHint($arrWorkbench, $arrTaskPool, $arrPlanTask),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function writeArtifacts(string $strTaskPoolRoot, array $arrSummary): array
|
||||||
|
{
|
||||||
|
$strTaskPoolRoot = rtrim(str_replace('\\', '/', $strTaskPoolRoot), '/');
|
||||||
|
$strStatusRoot = $strTaskPoolRoot . '/status';
|
||||||
|
$strOpsRoot = $strTaskPoolRoot . '/ops';
|
||||||
|
$arrSummary['summary_json_path'] = $strStatusRoot . '/index.json';
|
||||||
|
$arrSummary['summary_html_path'] = $strStatusRoot . '/index.html';
|
||||||
|
$arrSummary['ops_json_path'] = $strOpsRoot . '/index.json';
|
||||||
|
$arrSummary['ops_html_path'] = $strOpsRoot . '/index.html';
|
||||||
|
|
||||||
|
self::writeFile(
|
||||||
|
$arrSummary['summary_json_path'],
|
||||||
|
json_encode($arrSummary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
||||||
|
);
|
||||||
|
self::writeFile($arrSummary['summary_html_path'], self::renderHtml($arrSummary));
|
||||||
|
self::writeFile(
|
||||||
|
$arrSummary['ops_json_path'],
|
||||||
|
json_encode(self::buildOpsSummary($arrSummary), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
||||||
|
);
|
||||||
|
self::writeFile($arrSummary['ops_html_path'], self::renderOpsHtml($arrSummary));
|
||||||
|
|
||||||
|
return $arrSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function buildBatchSummary(array $arrBatches): array
|
||||||
|
{
|
||||||
|
$arrCounts = [
|
||||||
|
'pending' => 0,
|
||||||
|
'dispatched' => 0,
|
||||||
|
'processing' => 0,
|
||||||
|
'done' => 0,
|
||||||
|
'skipped' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($arrBatches as $arrBatch) {
|
||||||
|
$strStatus = (string)($arrBatch['status'] ?? VideoMetadataMissingTaskPoolStateHelper::STATUS_PENDING);
|
||||||
|
if (!array_key_exists($strStatus, $arrCounts)) {
|
||||||
|
$arrCounts[$strStatus] = 0;
|
||||||
|
}
|
||||||
|
$arrCounts[$strStatus]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $arrCounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function buildPlanTaskStatus(): array
|
||||||
|
{
|
||||||
|
$strTaskCode = 'REFRESH_VIDEO_METADATA_TASK_POOL';
|
||||||
|
$strDbError = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$PlanTaskModel = PlanTaskModel::where('pt_code', $strTaskCode)->find();
|
||||||
|
} catch (\Throwable $Throwable) {
|
||||||
|
$PlanTaskModel = null;
|
||||||
|
$strDbError = $Throwable->getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'task_code' => $strTaskCode,
|
||||||
|
'db_ready' => $strDbError === '',
|
||||||
|
'db_error' => $strDbError,
|
||||||
|
'task_exists' => $PlanTaskModel instanceof PlanTaskModel,
|
||||||
|
'pt_id' => (int)($PlanTaskModel->pt_id ?? 0),
|
||||||
|
'pt_name' => (string)($PlanTaskModel->pt_name ?? '视频缺字段任务池刷新'),
|
||||||
|
'pt_enable' => (int)($PlanTaskModel->pt_enable ?? 0),
|
||||||
|
'pt_limit' => (int)($PlanTaskModel->pt_limit ?? 0),
|
||||||
|
'pt_last_exec' => !empty($PlanTaskModel->pt_last_exec) ? date(DATE_ATOM, (int)$PlanTaskModel->pt_last_exec) : '',
|
||||||
|
'status_label' => $strDbError !== ''
|
||||||
|
? 'db_blocked'
|
||||||
|
: (($PlanTaskModel instanceof PlanTaskModel)
|
||||||
|
? ((int)($PlanTaskModel->pt_enable ?? 0) === 1 ? 'enabled' : 'disabled')
|
||||||
|
: 'missing'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function buildOperatorHint(array $arrWorkbench, array $arrTaskPool, array $arrPlanTask): array
|
||||||
|
{
|
||||||
|
$arrHint = [];
|
||||||
|
|
||||||
|
if (empty($arrWorkbench)) {
|
||||||
|
$arrHint[] = '当前还没有工作台产物,建议先手动执行 video:metadata:task-pool 生成一轮。';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($arrWorkbench) && empty($arrTaskPool)) {
|
||||||
|
$arrHint[] = '当前已有工作台,但还没有任务池产物,建议再执行一次 task-pool 刷新命令。';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($arrPlanTask['status_label'] ?? '') === 'missing') {
|
||||||
|
$arrHint[] = '如果要接入数据库计划任务,请新增 pt_code=REFRESH_VIDEO_METADATA_TASK_POOL。';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($arrPlanTask['status_label'] ?? '') === 'disabled') {
|
||||||
|
$arrHint[] = '当前数据库计划任务已存在但未启用,启用前建议先保持较大执行间隔。';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($arrPlanTask['status_label'] ?? '') === 'enabled') {
|
||||||
|
$arrHint[] = '当前数据库计划任务已启用;这条任务只刷新产物,不自动补事实字段。';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($arrPlanTask['status_label'] ?? '') === 'db_blocked') {
|
||||||
|
$arrHint[] = '当前脚本环境拿不到数据库任务表配置,因此这里只能返回任务池现状;如果在正式后台环境调用,一般会恢复数据库计划任务状态识别。';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($arrHint)) {
|
||||||
|
$arrHint[] = '当前工作台、任务池、计划任务状态都已具备,下一步可以做后台入口卡片或操作面板。';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $arrHint;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function renderHtml(array $arrSummary): string
|
||||||
|
{
|
||||||
|
$arrWorkbench = (array)($arrSummary['workbench'] ?? []);
|
||||||
|
$arrTaskPool = (array)($arrSummary['task_pool'] ?? []);
|
||||||
|
$arrBatchStatus = (array)($arrTaskPool['batch_status_summary'] ?? []);
|
||||||
|
$arrPlanTask = (array)($arrSummary['plan_task'] ?? []);
|
||||||
|
$arrHints = (array)($arrSummary['operator_hint'] ?? []);
|
||||||
|
|
||||||
|
$strHintRows = '';
|
||||||
|
foreach ($arrHints as $strHint) {
|
||||||
|
$strHintRows .= '<li>' . htmlspecialchars((string)$strHint, ENT_QUOTES, 'UTF-8') . '</li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$strBatchRows = '';
|
||||||
|
foreach ($arrBatchStatus as $strStatus => $intCount) {
|
||||||
|
$strBatchRows .= '<tr>'
|
||||||
|
. '<td>' . htmlspecialchars((string)$strStatus, ENT_QUOTES, 'UTF-8') . '</td>'
|
||||||
|
. '<td>' . (int)$intCount . '</td>'
|
||||||
|
. '</tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>视频缺字段任务池状态总览</title>
|
||||||
|
<style>
|
||||||
|
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#f6f8fb;color:#1f2937;margin:0;padding:24px;}
|
||||||
|
.wrap{max-width:1180px;margin:0 auto;}
|
||||||
|
.card{background:#fff;border-radius:16px;box-shadow:0 8px 24px rgba(15,23,42,.06);padding:20px;margin-bottom:20px;}
|
||||||
|
.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px;}
|
||||||
|
.stat{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:16px;}
|
||||||
|
.stat .num{font-size:28px;font-weight:700;margin-top:6px;}
|
||||||
|
table{width:100%;border-collapse:collapse;}
|
||||||
|
th,td{border-bottom:1px solid #e5e7eb;padding:10px;text-align:left;vertical-align:top;font-size:14px;}
|
||||||
|
th{background:#f8fafc;}
|
||||||
|
ul{margin:0;padding-left:20px;}
|
||||||
|
.path{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:13px;color:#475569;word-break:break-all;}
|
||||||
|
@media (max-width: 960px){.grid{grid-template-columns:1fr 1fr;}}
|
||||||
|
@media (max-width: 640px){.grid{grid-template-columns:1fr;} body{padding:14px;}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="card">
|
||||||
|
<h1>视频缺字段任务池状态总览</h1>
|
||||||
|
<p>生成时间:' . htmlspecialchars((string)($arrSummary['generated_at'] ?? ''), ENT_QUOTES, 'UTF-8') . '</p>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="stat"><div>缺字段视频数</div><div class="num">' . (int)($arrWorkbench['videos_with_any_missing_metadata'] ?? 0) . '</div></div>
|
||||||
|
<div class="stat"><div>重采优先队列</div><div class="num">' . (int)($arrWorkbench['queue_size'] ?? 0) . '</div></div>
|
||||||
|
<div class="stat"><div>任务池批次数</div><div class="num">' . (int)($arrTaskPool['batch_count'] ?? 0) . '</div></div>
|
||||||
|
<div class="stat"><div>计划任务状态</div><div class="num">' . htmlspecialchars((string)($arrPlanTask['status_label'] ?? ''), ENT_QUOTES, 'UTF-8') . '</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>工作台 / 任务池产物</h2>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>模块</th><th>生成时间</th><th>HTML</th><th>JSON</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>工作台</td>
|
||||||
|
<td>' . htmlspecialchars((string)($arrWorkbench['generated_at'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>
|
||||||
|
<td class="path">' . htmlspecialchars((string)($arrWorkbench['summary_html_path'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>
|
||||||
|
<td class="path">' . htmlspecialchars((string)($arrWorkbench['summary_json_path'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>任务池</td>
|
||||||
|
<td>' . htmlspecialchars((string)($arrTaskPool['generated_at'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>
|
||||||
|
<td class="path">' . htmlspecialchars((string)($arrTaskPool['summary_html_path'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>
|
||||||
|
<td class="path">' . htmlspecialchars((string)($arrTaskPool['summary_json_path'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>批次状态统计</h2>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>状态</th><th>数量</th></tr></thead>
|
||||||
|
<tbody>' . $strBatchRows . '</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>计划任务状态</h2>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>字段</th><th>值</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>task_code</td><td>' . htmlspecialchars((string)($arrPlanTask['task_code'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td></tr>
|
||||||
|
<tr><td>status_label</td><td>' . htmlspecialchars((string)($arrPlanTask['status_label'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td></tr>
|
||||||
|
<tr><td>pt_enable</td><td>' . (int)($arrPlanTask['pt_enable'] ?? 0) . '</td></tr>
|
||||||
|
<tr><td>pt_limit</td><td>' . (int)($arrPlanTask['pt_limit'] ?? 0) . '</td></tr>
|
||||||
|
<tr><td>pt_last_exec</td><td>' . htmlspecialchars((string)($arrPlanTask['pt_last_exec'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td></tr>
|
||||||
|
<tr><td>db_error</td><td>' . htmlspecialchars((string)($arrPlanTask['db_error'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>操作提示</h2>
|
||||||
|
<ul>' . $strHintRows . '</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function buildOpsSummary(array $arrSummary): array
|
||||||
|
{
|
||||||
|
$arrPlanTask = (array)($arrSummary['plan_task'] ?? []);
|
||||||
|
$arrWorkbench = (array)($arrSummary['workbench'] ?? []);
|
||||||
|
$arrTaskPool = (array)($arrSummary['task_pool'] ?? []);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'generated_at' => (string)($arrSummary['generated_at'] ?? ''),
|
||||||
|
'commands' => [
|
||||||
|
[
|
||||||
|
'label' => '刷新工作台 + 任务池',
|
||||||
|
'command' => 'php SEONexus/code/think video:metadata:task-pool --sample=8 --queue-limit=12 --prompt-limit=6 --batch-size=10 --batch-limit=4',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => '补齐计划任务种子',
|
||||||
|
'command' => 'php SEONexus/code/think plan:seed:video-metadata-missing',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => '查看任务池状态接口',
|
||||||
|
'command' => 'GET /admin/video/metadata/missing/task-pool/status',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => '查看任务池运维接口',
|
||||||
|
'command' => 'GET /admin/video/metadata/missing/task-pool/ops',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => '保存任务池计划任务状态',
|
||||||
|
'command' => 'POST /admin/video/metadata/missing/task-pool/plan-task/status/save',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'plan_task' => $arrPlanTask,
|
||||||
|
'workbench' => $arrWorkbench,
|
||||||
|
'task_pool' => $arrTaskPool,
|
||||||
|
'recommended_action' => (($arrPlanTask['status_label'] ?? '') === 'disabled')
|
||||||
|
? '先在后台把 REFRESH_VIDEO_METADATA_TASK_POOL 打开,建议 pt_limit 保持 86400。'
|
||||||
|
: '当前计划任务已具备基础状态,继续保持刷新即可。',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function renderOpsHtml(array $arrSummary): string
|
||||||
|
{
|
||||||
|
$arrOps = self::buildOpsSummary($arrSummary);
|
||||||
|
$arrCommands = (array)($arrOps['commands'] ?? []);
|
||||||
|
$arrPlanTask = (array)($arrOps['plan_task'] ?? []);
|
||||||
|
$arrWorkbench = (array)($arrOps['workbench'] ?? []);
|
||||||
|
$arrTaskPool = (array)($arrOps['task_pool'] ?? []);
|
||||||
|
|
||||||
|
$strCommandRows = '';
|
||||||
|
foreach ($arrCommands as $arrCommand) {
|
||||||
|
$strCommandRows .= '<tr><td>'
|
||||||
|
. htmlspecialchars((string)($arrCommand['label'] ?? ''), ENT_QUOTES, 'UTF-8')
|
||||||
|
. '</td><td><code>'
|
||||||
|
. htmlspecialchars((string)($arrCommand['command'] ?? ''), ENT_QUOTES, 'UTF-8')
|
||||||
|
. '</code></td></tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>视频缺字段任务池运维页</title>
|
||||||
|
<style>
|
||||||
|
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#f6f8fb;color:#1f2937;margin:0;padding:24px;}
|
||||||
|
.wrap{max-width:1180px;margin:0 auto;}
|
||||||
|
.card{background:#fff;border-radius:16px;box-shadow:0 8px 24px rgba(15,23,42,.06);padding:20px;margin-bottom:20px;}
|
||||||
|
.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px;}
|
||||||
|
.stat{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:16px;}
|
||||||
|
.stat .num{font-size:28px;font-weight:700;margin-top:6px;}
|
||||||
|
table{width:100%;border-collapse:collapse;}
|
||||||
|
th,td{border-bottom:1px solid #e5e7eb;padding:10px;text-align:left;vertical-align:top;font-size:14px;}
|
||||||
|
th{background:#f8fafc;}
|
||||||
|
code{white-space:pre-wrap;word-break:break-all;}
|
||||||
|
.hint{background:#f8fafc;border-left:4px solid #2563eb;padding:12px 14px;border-radius:8px;}
|
||||||
|
.path{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:13px;color:#475569;word-break:break-all;}
|
||||||
|
@media (max-width: 960px){.grid{grid-template-columns:1fr 1fr;}}
|
||||||
|
@media (max-width: 640px){.grid{grid-template-columns:1fr;} body{padding:14px;}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="card">
|
||||||
|
<h1>视频缺字段任务池运维页</h1>
|
||||||
|
<p>生成时间:' . htmlspecialchars((string)($arrOps['generated_at'] ?? ''), ENT_QUOTES, 'UTF-8') . '</p>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="stat"><div>缺字段视频数</div><div class="num">' . (int)($arrWorkbench['videos_with_any_missing_metadata'] ?? 0) . '</div></div>
|
||||||
|
<div class="stat"><div>任务池批次数</div><div class="num">' . (int)($arrTaskPool['batch_count'] ?? 0) . '</div></div>
|
||||||
|
<div class="stat"><div>计划任务状态</div><div class="num">' . htmlspecialchars((string)($arrPlanTask['status_label'] ?? ''), ENT_QUOTES, 'UTF-8') . '</div></div>
|
||||||
|
<div class="stat"><div>建议间隔</div><div class="num">' . (int)($arrPlanTask['pt_limit'] ?? 0) . '</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>推荐动作</h2>
|
||||||
|
<div class="hint">' . htmlspecialchars((string)($arrOps['recommended_action'] ?? ''), ENT_QUOTES, 'UTF-8') . '</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>常用命令</h2>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>用途</th><th>命令</th></tr></thead>
|
||||||
|
<tbody>' . $strCommandRows . '</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>当前产物</h2>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>模块</th><th>HTML</th><th>JSON</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>工作台</td>
|
||||||
|
<td class="path">' . htmlspecialchars((string)($arrWorkbench['summary_html_path'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>
|
||||||
|
<td class="path">' . htmlspecialchars((string)($arrWorkbench['summary_json_path'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>任务池</td>
|
||||||
|
<td class="path">' . htmlspecialchars((string)($arrTaskPool['summary_html_path'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>
|
||||||
|
<td class="path">' . htmlspecialchars((string)($arrTaskPool['summary_json_path'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function writeFile(string $strPath, string $strContent): void
|
||||||
|
{
|
||||||
|
$strDir = dirname($strPath);
|
||||||
|
if (!is_dir($strDir)) {
|
||||||
|
mkdir($strDir, 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($strPath, $strContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
329
code/app/common/helper/VideoMetadataMissingWorkbenchHelper.php
Normal file
329
code/app/common/helper/VideoMetadataMissingWorkbenchHelper.php
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\helper;
|
||||||
|
|
||||||
|
use app\model\VideoModel;
|
||||||
|
|
||||||
|
class VideoMetadataMissingWorkbenchHelper
|
||||||
|
{
|
||||||
|
public static function buildSummary(
|
||||||
|
int $intSampleLimit = 20,
|
||||||
|
int $intQueueLimit = 100,
|
||||||
|
int $intPromptLimit = 20
|
||||||
|
): array {
|
||||||
|
$VideoModel = VideoModel::getInstance();
|
||||||
|
$arrAudit = $VideoModel->buildMissingMetadataAuditSummary($intSampleLimit);
|
||||||
|
$intQueueScanLimit = max(500, min(5000, $intQueueLimit * 20));
|
||||||
|
$arrQueue = $VideoModel->buildRecrawlPriorityQueueSummary($intQueueLimit, $intQueueScanLimit);
|
||||||
|
|
||||||
|
$arrFieldStats = (array)($arrAudit['field_stats'] ?? []);
|
||||||
|
usort($arrFieldStats, static function (array $arrA, array $arrB): int {
|
||||||
|
return (int)($arrB['missing_count'] ?? 0) <=> (int)($arrA['missing_count'] ?? 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
$arrTopFields = array_values(array_slice($arrFieldStats, 0, 5));
|
||||||
|
$arrPromptItems = array_values(array_slice((array)($arrQueue['items'] ?? []), 0, max(1, min($intPromptLimit, 100))));
|
||||||
|
|
||||||
|
$arrSummary = [
|
||||||
|
'generated_at' => date('c'),
|
||||||
|
'sample_limit' => $intSampleLimit,
|
||||||
|
'queue_limit' => $intQueueLimit,
|
||||||
|
'queue_scan_limit' => $intQueueScanLimit,
|
||||||
|
'prompt_limit' => $intPromptLimit,
|
||||||
|
'audit' => $arrAudit,
|
||||||
|
'queue' => $arrQueue,
|
||||||
|
'top_missing_fields' => $arrTopFields,
|
||||||
|
'next_actions' => self::buildNextActions($arrAudit, $arrQueue, $arrTopFields),
|
||||||
|
'codex_dispatch_prompt' => self::buildCodexDispatchPrompt($arrPromptItems, $arrTopFields),
|
||||||
|
'operator_notes' => self::buildOperatorNotes(),
|
||||||
|
];
|
||||||
|
|
||||||
|
return $arrSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function writeArtifacts(string $strOutputRoot, array $arrSummary): array
|
||||||
|
{
|
||||||
|
$strOutputRoot = rtrim(str_replace('\\', '/', $strOutputRoot), '/');
|
||||||
|
$arrSummary['summary_json_path'] = $strOutputRoot . '/index.json';
|
||||||
|
$arrSummary['summary_html_path'] = $strOutputRoot . '/index.html';
|
||||||
|
$arrSummary['prompt_markdown_path'] = $strOutputRoot . '/prompts/latest.md';
|
||||||
|
$arrSummary['run_id'] = self::buildRunId();
|
||||||
|
|
||||||
|
self::writeFile(
|
||||||
|
$strOutputRoot . '/index.json',
|
||||||
|
json_encode($arrSummary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
||||||
|
);
|
||||||
|
self::writeFile($strOutputRoot . '/index.html', self::renderHtml($arrSummary));
|
||||||
|
self::writeFile($strOutputRoot . '/prompts/latest.md', (string)($arrSummary['codex_dispatch_prompt'] ?? ''));
|
||||||
|
self::writeRunArtifacts($strOutputRoot, $arrSummary);
|
||||||
|
|
||||||
|
return $arrSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function readLatestSummary(string $strOutputRoot): array
|
||||||
|
{
|
||||||
|
$strPath = rtrim(str_replace('\\', '/', $strOutputRoot), '/') . '/index.json';
|
||||||
|
if (!is_file($strPath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrSummary = json_decode((string)file_get_contents($strPath), true);
|
||||||
|
return is_array($arrSummary) ? $arrSummary : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function buildNextActions(array $arrAudit, array $arrQueue, array $arrTopFields): array
|
||||||
|
{
|
||||||
|
$intAnyMissing = (int)($arrAudit['videos_with_any_missing_metadata'] ?? 0);
|
||||||
|
$intQueueSize = (int)($arrQueue['queue_size'] ?? 0);
|
||||||
|
$strTopField = (string)(($arrTopFields[0] ?? [])['field'] ?? '');
|
||||||
|
|
||||||
|
$arrActions = [];
|
||||||
|
$arrActions[] = [
|
||||||
|
'key' => 'workbench_refresh',
|
||||||
|
'label' => '先刷新缺失字段工作台',
|
||||||
|
'summary' => '把当前视频库缺失字段、优先重采队列、Codex 派单提示词一起固化成后台可读产物。',
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($intQueueSize > 0) {
|
||||||
|
$arrActions[] = [
|
||||||
|
'key' => 'recrawl_priority',
|
||||||
|
'label' => '优先处理演员 / 导演缺失高优先视频',
|
||||||
|
'summary' => '当前重采优先队列 ' . $intQueueSize . ' 条,优先解决结构化字段空值,避免详情页长期资料残缺。',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($intAnyMissing > 0) {
|
||||||
|
$arrActions[] = [
|
||||||
|
'key' => 'codex_dispatch',
|
||||||
|
'label' => '把缺失池派给 Codex 做人工接手优化',
|
||||||
|
'summary' => '当前仍有 ' . $intAnyMissing . ' 条视频存在缺字段,建议技术从后台复制提示词,分批派给 Codex 跟进。',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($strTopField !== '') {
|
||||||
|
$arrActions[] = [
|
||||||
|
'key' => 'focus_field',
|
||||||
|
'label' => '本轮重点字段:' . $strTopField,
|
||||||
|
'summary' => '当前缺失量最大的字段先处理,收敛速度最快,也最容易改善详情页完整度。',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $arrActions;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function buildOperatorNotes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'title' => '这套工作台是干什么的',
|
||||||
|
'content' => '它负责把“视频库哪些字段是空的、哪些视频优先补、给 Codex 的派单提示词”集中整理出来,方便技术和运营协同处理。',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => '当前不会自动做什么',
|
||||||
|
'content' => '不会因为检测到空字段,就自动调用 AI API 去猜演员、导演、年份、地区等结构化资料;这类字段仍然优先重采或人工确认。',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => '适合 Codex 接手的场景',
|
||||||
|
'content' => '当后台已经把缺失视频列表和重点字段列出来后,技术可以复制提示词给 Codex,让 Codex按批次分析、整理、制定补料策略,避免运营直接面对复杂技术字段。',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => '适合后台 AI 的场景',
|
||||||
|
'content' => '仅适合在事实基础足够时补简介、备注、摘要类文案字段,不适合直接猜演员、导演、上映时间、地区、语言等结构化事实字段。',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function buildCodexDispatchPrompt(array $arrPromptItems, array $arrTopFields): string
|
||||||
|
{
|
||||||
|
$arrLines = [];
|
||||||
|
$arrLines[] = '# 视频缺失字段 Codex 接手提示词';
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '你现在接手的是“视频元数据缺失字段处理”任务。';
|
||||||
|
$arrLines[] = '目标:优先提升详情页资料完整度,减少演员、导演、年份、地区、语言、发布时间、简介、备注为空的情况。';
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '执行原则:';
|
||||||
|
$arrLines[] = '1. 结构化字段如演员、导演、年份、地区、语言、发布时间,不允许编造。';
|
||||||
|
$arrLines[] = '2. 简介、备注等文案字段,只能在现有事实边界内补全,不允许虚构剧情和角色关系。';
|
||||||
|
$arrLines[] = '3. 优先处理高优先级、点击高、播放源多、近期更新的视频。';
|
||||||
|
$arrLines[] = '4. 输出时请先给出分批处理建议,再给出每批次的风险说明。';
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '当前缺失最严重字段:';
|
||||||
|
foreach ($arrTopFields as $arrField) {
|
||||||
|
$arrLines[] = sprintf(
|
||||||
|
'- %s:缺失 %d 条,占比 %.2f%%',
|
||||||
|
(string)($arrField['field'] ?? ''),
|
||||||
|
(int)($arrField['missing_count'] ?? 0),
|
||||||
|
((float)($arrField['missing_ratio'] ?? 0)) * 100
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '本轮优先样本:';
|
||||||
|
foreach ($arrPromptItems as $arrItem) {
|
||||||
|
$arrLines[] = sprintf(
|
||||||
|
'- v_id=%d | %s | 分类=%s | 缺失=%s | 原因=%s',
|
||||||
|
(int)($arrItem['v_id'] ?? 0),
|
||||||
|
trim((string)($arrItem['v_name'] ?? '')),
|
||||||
|
trim((string)($arrItem['v_category'] ?? '')),
|
||||||
|
implode(',', (array)($arrItem['missing_fields'] ?? [])),
|
||||||
|
trim((string)($arrItem['priority_reason'] ?? ''))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$arrLines[] = '';
|
||||||
|
$arrLines[] = '请输出:';
|
||||||
|
$arrLines[] = '1. 本轮处理优先级和分批方案';
|
||||||
|
$arrLines[] = '2. 哪些字段适合重采,哪些字段适合人工补充,哪些字段适合仅补文案';
|
||||||
|
$arrLines[] = '3. 如果需要后续命令或脚本,请直接给出可执行建议';
|
||||||
|
$arrLines[] = '';
|
||||||
|
|
||||||
|
return implode(PHP_EOL, $arrLines) . PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function renderHtml(array $arrSummary): string
|
||||||
|
{
|
||||||
|
$arrAudit = (array)($arrSummary['audit'] ?? []);
|
||||||
|
$arrQueue = (array)($arrSummary['queue'] ?? []);
|
||||||
|
$arrTopFields = (array)($arrSummary['top_missing_fields'] ?? []);
|
||||||
|
$arrActions = (array)($arrSummary['next_actions'] ?? []);
|
||||||
|
$arrItems = array_values(array_slice((array)($arrQueue['items'] ?? []), 0, 20));
|
||||||
|
|
||||||
|
$strPrompt = htmlspecialchars((string)($arrSummary['codex_dispatch_prompt'] ?? ''), ENT_QUOTES, 'UTF-8');
|
||||||
|
|
||||||
|
$strTopRows = '';
|
||||||
|
foreach ($arrTopFields as $arrField) {
|
||||||
|
$strTopRows .= '<tr>'
|
||||||
|
. '<td>' . htmlspecialchars((string)($arrField['field'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>'
|
||||||
|
. '<td>' . (int)($arrField['missing_count'] ?? 0) . '</td>'
|
||||||
|
. '<td>' . number_format(((float)($arrField['missing_ratio'] ?? 0)) * 100, 2) . '%</td>'
|
||||||
|
. '</tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$strActionRows = '';
|
||||||
|
foreach ($arrActions as $arrAction) {
|
||||||
|
$strActionRows .= '<li><strong>'
|
||||||
|
. htmlspecialchars((string)($arrAction['label'] ?? ''), ENT_QUOTES, 'UTF-8')
|
||||||
|
. '</strong>:'
|
||||||
|
. htmlspecialchars((string)($arrAction['summary'] ?? ''), ENT_QUOTES, 'UTF-8')
|
||||||
|
. '</li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$strItemRows = '';
|
||||||
|
foreach ($arrItems as $arrItem) {
|
||||||
|
$strItemRows .= '<tr>'
|
||||||
|
. '<td>' . (int)($arrItem['v_id'] ?? 0) . '</td>'
|
||||||
|
. '<td>' . htmlspecialchars((string)($arrItem['v_name'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>'
|
||||||
|
. '<td>' . htmlspecialchars((string)($arrItem['v_category'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>'
|
||||||
|
. '<td>' . htmlspecialchars(implode(', ', (array)($arrItem['missing_fields'] ?? [])), ENT_QUOTES, 'UTF-8') . '</td>'
|
||||||
|
. '<td>' . (int)($arrItem['priority_score'] ?? 0) . '</td>'
|
||||||
|
. '<td>' . htmlspecialchars((string)($arrItem['priority_reason'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>'
|
||||||
|
. '</tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>视频缺失字段工作台</title>
|
||||||
|
<style>
|
||||||
|
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#f6f8fb;color:#1f2937;margin:0;padding:24px;}
|
||||||
|
.wrap{max-width:1280px;margin:0 auto;}
|
||||||
|
.card{background:#fff;border-radius:16px;box-shadow:0 8px 24px rgba(15,23,42,.06);padding:20px;margin-bottom:20px;}
|
||||||
|
h1,h2{margin:0 0 12px;}
|
||||||
|
.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px;}
|
||||||
|
.stat{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:16px;}
|
||||||
|
.stat .num{font-size:28px;font-weight:700;margin-top:6px;}
|
||||||
|
table{width:100%;border-collapse:collapse;}
|
||||||
|
th,td{border-bottom:1px solid #e5e7eb;padding:10px;text-align:left;vertical-align:top;font-size:14px;}
|
||||||
|
th{background:#f8fafc;}
|
||||||
|
pre{white-space:pre-wrap;word-break:break-word;background:#0f172a;color:#e2e8f0;padding:16px;border-radius:12px;overflow:auto;}
|
||||||
|
ul{margin:0;padding-left:20px;}
|
||||||
|
@media (max-width: 960px){.grid{grid-template-columns:1fr 1fr;}}
|
||||||
|
@media (max-width: 640px){.grid{grid-template-columns:1fr;} body{padding:14px;}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="card">
|
||||||
|
<h1>视频缺失字段工作台</h1>
|
||||||
|
<p>生成时间:' . htmlspecialchars((string)($arrSummary['generated_at'] ?? ''), ENT_QUOTES, 'UTF-8') . '</p>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="stat"><div>总视频数</div><div class="num">' . (int)($arrAudit['total_videos'] ?? 0) . '</div></div>
|
||||||
|
<div class="stat"><div>存在缺字段视频</div><div class="num">' . (int)($arrAudit['videos_with_any_missing_metadata'] ?? 0) . '</div></div>
|
||||||
|
<div class="stat"><div>重采优先队列</div><div class="num">' . (int)($arrQueue['queue_size'] ?? 0) . '</div></div>
|
||||||
|
<div class="stat"><div>Codex 派单样本</div><div class="num">' . (int)($arrSummary['prompt_limit'] ?? 0) . '</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>下一步动作</h2>
|
||||||
|
<ul>' . $strActionRows . '</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>缺失最多字段 Top</h2>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>字段</th><th>缺失数</th><th>缺失占比</th></tr></thead>
|
||||||
|
<tbody>' . $strTopRows . '</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>高优先级视频样本</h2>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>v_id</th><th>片名</th><th>分类</th><th>缺失字段</th><th>优先分</th><th>优先原因</th></tr></thead>
|
||||||
|
<tbody>' . $strItemRows . '</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Codex 派单提示词</h2>
|
||||||
|
<pre>' . $strPrompt . '</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function writeFile(string $strPath, string $strContent): void
|
||||||
|
{
|
||||||
|
$strDir = dirname($strPath);
|
||||||
|
if (!is_dir($strDir)) {
|
||||||
|
@mkdir($strDir, 0777, true);
|
||||||
|
}
|
||||||
|
file_put_contents($strPath, $strContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function writeRunArtifacts(string $strOutputRoot, array $arrSummary): void
|
||||||
|
{
|
||||||
|
$strRunId = trim((string)($arrSummary['run_id'] ?? ''));
|
||||||
|
if ($strRunId === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$strDay = substr((string)($arrSummary['generated_at'] ?? date('c')), 0, 10);
|
||||||
|
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $strDay)) {
|
||||||
|
$strDay = date('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
$strRunRoot = $strOutputRoot . '/runs/' . $strDay . '/' . $strRunId;
|
||||||
|
self::writeFile(
|
||||||
|
$strRunRoot . '/video-metadata-missing-workbench.summary.json',
|
||||||
|
json_encode($arrSummary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
||||||
|
);
|
||||||
|
self::writeFile(
|
||||||
|
$strRunRoot . '/video-metadata-missing-workbench.summary.html',
|
||||||
|
self::renderHtml($arrSummary)
|
||||||
|
);
|
||||||
|
self::writeFile(
|
||||||
|
$strRunRoot . '/prompt.md',
|
||||||
|
(string)($arrSummary['codex_dispatch_prompt'] ?? '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function buildRunId(): string
|
||||||
|
{
|
||||||
|
return 'video-metadata-missing-workbench-' . date('Ymd-His');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\helper;
|
||||||
|
|
||||||
|
class VideoMetadataMissingWorkbenchIndexHelper
|
||||||
|
{
|
||||||
|
public static function buildSummary(string $strRunRoot, int $intLimit = 20): array
|
||||||
|
{
|
||||||
|
$strRunRoot = rtrim(str_replace('\\', '/', $strRunRoot), '/');
|
||||||
|
if ($strRunRoot === '' || !is_dir($strRunRoot)) {
|
||||||
|
return [
|
||||||
|
'items' => [],
|
||||||
|
'total' => 0,
|
||||||
|
'latest_run' => [],
|
||||||
|
'health_buckets' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrFiles = array_merge(
|
||||||
|
(array)glob($strRunRoot . '/*/*/video-metadata-missing-workbench.summary.json')
|
||||||
|
);
|
||||||
|
$arrFiles = array_values(array_filter(array_unique($arrFiles), 'is_file'));
|
||||||
|
usort($arrFiles, static function (string $strLeft, string $strRight): int {
|
||||||
|
return ((int)(filemtime($strRight) ?: 0)) <=> ((int)(filemtime($strLeft) ?: 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
$arrItems = [];
|
||||||
|
foreach (array_slice($arrFiles, 0, max(1, $intLimit)) as $strSummaryPath) {
|
||||||
|
$arrData = json_decode((string)file_get_contents($strSummaryPath), true);
|
||||||
|
if (!is_array($arrData)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrAudit = (array)($arrData['audit'] ?? []);
|
||||||
|
$arrQueue = (array)($arrData['queue'] ?? []);
|
||||||
|
$arrTopFields = (array)($arrData['top_missing_fields'] ?? []);
|
||||||
|
$intAnyMissing = (int)($arrAudit['videos_with_any_missing_metadata'] ?? 0);
|
||||||
|
$intTotal = (int)($arrAudit['total_videos'] ?? 0);
|
||||||
|
$floatRatio = $intTotal > 0 ? round($intAnyMissing / $intTotal, 4) : 0.0;
|
||||||
|
[$strHealthLabel, $strHealthReason] = self::resolveHealth($floatRatio, $arrTopFields);
|
||||||
|
|
||||||
|
$strRunDir = dirname($strSummaryPath);
|
||||||
|
$arrItems[] = [
|
||||||
|
'run_id' => basename($strRunDir),
|
||||||
|
'generated_at' => (string)($arrData['generated_at'] ?? date(DATE_ATOM, (int)(filemtime($strSummaryPath) ?: time()))),
|
||||||
|
'videos_with_any_missing_metadata' => $intAnyMissing,
|
||||||
|
'total_videos' => $intTotal,
|
||||||
|
'missing_ratio' => $floatRatio,
|
||||||
|
'queue_size' => (int)($arrQueue['queue_size'] ?? 0),
|
||||||
|
'top_field' => (string)(($arrTopFields[0] ?? [])['field'] ?? ''),
|
||||||
|
'top_field_missing_count' => (int)(($arrTopFields[0] ?? [])['missing_count'] ?? 0),
|
||||||
|
'health_label' => $strHealthLabel,
|
||||||
|
'health_reason' => $strHealthReason,
|
||||||
|
'summary_json_path' => self::storageRelativePath($strSummaryPath),
|
||||||
|
'summary_html_path' => self::storageRelativePath($strRunDir . '/video-metadata-missing-workbench.summary.html'),
|
||||||
|
'prompt_markdown_path' => self::storageRelativePath($strRunDir . '/prompt.md'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrHealthBuckets = [];
|
||||||
|
foreach ($arrItems as $arrItem) {
|
||||||
|
$strLabel = (string)($arrItem['health_label'] ?? 'unknown');
|
||||||
|
$arrHealthBuckets[$strLabel] = (int)($arrHealthBuckets[$strLabel] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'items' => $arrItems,
|
||||||
|
'total' => count($arrItems),
|
||||||
|
'latest_run' => $arrItems[0] ?? [],
|
||||||
|
'health_buckets' => $arrHealthBuckets,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function resolveHealth(float $floatRatio, array $arrTopFields): array
|
||||||
|
{
|
||||||
|
$strTopField = (string)(($arrTopFields[0] ?? [])['field'] ?? '');
|
||||||
|
if ($floatRatio >= 0.30) {
|
||||||
|
return ['high_attention', '当前整体缺字段占比仍高,且结构化字段缺失量较大。重点先收敛演员、导演。'];
|
||||||
|
}
|
||||||
|
if ($strTopField === 'v_actor' || $strTopField === 'v_director') {
|
||||||
|
return ['structure_focus', '当前主要仍是演员 / 导演缺失,适合优先走重采和人工核验链。'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['steady', '当前缺字段问题相对可控,适合按优先队列持续收敛。'];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function storageRelativePath(string $strPath): string
|
||||||
|
{
|
||||||
|
$strStorageRoot = str_replace('\\', '/', rtrim(dirname(__DIR__, 3) . '/storage', '/'));
|
||||||
|
$strPath = str_replace('\\', '/', $strPath);
|
||||||
|
if (str_starts_with($strPath, $strStorageRoot . '/')) {
|
||||||
|
return substr($strPath, strlen($strStorageRoot . '/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $strPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace app\home;
|
namespace app\home;
|
||||||
|
|
||||||
|
use app\common\helper\DomainSeoNamingHelper;
|
||||||
use app\model\ConverterMovel;
|
use app\model\ConverterMovel;
|
||||||
use app\model\GanRaoMaModel;
|
use app\model\GanRaoMaModel;
|
||||||
use app\model\CategoryModel;
|
use app\model\CategoryModel;
|
||||||
@@ -75,8 +76,16 @@ abstract class BaseController
|
|||||||
*/
|
*/
|
||||||
private function initSiteTKD()
|
private function initSiteTKD()
|
||||||
{
|
{
|
||||||
|
$strSiteName = DomainSeoNamingHelper::normalizeSiteName(
|
||||||
|
(string)($this->request->DomainModel->d_name ?? ''),
|
||||||
|
(string)($this->request->DomainModel->d_domain ?? '')
|
||||||
|
);
|
||||||
|
if ($strSiteName !== '' && isset($this->request->DomainModel)) {
|
||||||
|
$this->request->DomainModel->d_name = $strSiteName;
|
||||||
|
}
|
||||||
|
|
||||||
ConverterMovel::setVal([
|
ConverterMovel::setVal([
|
||||||
'strSiteName' => $this->request->DomainModel->d_name,
|
'strSiteName' => $strSiteName,
|
||||||
'strSiteDomain' => $this->request->DomainModel->d_domain,
|
'strSiteDomain' => $this->request->DomainModel->d_domain,
|
||||||
'strSiteKeywords' => $this->request->DomainModel->d_keywords,
|
'strSiteKeywords' => $this->request->DomainModel->d_keywords,
|
||||||
'strSiteDescription' => $this->request->DomainModel->d_description,
|
'strSiteDescription' => $this->request->DomainModel->d_description,
|
||||||
|
|||||||
@@ -22,35 +22,60 @@ $strTmpCode = $siteContext->getTemplate();
|
|||||||
if ($strTmpCode == 'videoGpt1') {
|
if ($strTmpCode == 'videoGpt1') {
|
||||||
// if (env('IS_GPT_TMP', false)) {
|
// if (env('IS_GPT_TMP', false)) {
|
||||||
|
|
||||||
|
$routeGetHead = static function (string $route, $handler) {
|
||||||
|
return Route::rule($route, $handler, 'GET|HEAD');
|
||||||
|
};
|
||||||
|
|
||||||
// ---------- Common public routes (keep compatible) ----------
|
// ---------- Common public routes (keep compatible) ----------
|
||||||
Route::get('/robots', function (\think\Request $Request, SiteContext $SiteContext) {
|
$routeGetHead('/robots', function (\think\Request $Request, SiteContext $SiteContext) {
|
||||||
return view('sitemap/robots.txt');
|
return view('sitemap/robots.txt');
|
||||||
})->ext('txt');
|
})->ext('txt');
|
||||||
|
|
||||||
Route::get('/sitemap', function (\think\Request $Request, SiteContext $SiteContext) {
|
$routeGetHead('/sitemap', function (\think\Request $Request, SiteContext $SiteContext) {
|
||||||
return view('video/getMap.html');
|
return view('video/getMap.html');
|
||||||
})->ext('html');
|
})->ext('html');
|
||||||
|
|
||||||
Route::get('rss/baidu', function () {
|
$routeGetHead('/sitemap.xml', function (\think\Request $Request, SiteContext $SiteContext) {
|
||||||
|
return $SiteContext->getSiteMapByCode('INDEX');
|
||||||
|
});
|
||||||
|
|
||||||
|
$routeGetHead('rss/baidu', function () {
|
||||||
return view('rss/baidu.xml')->contentType('text/xml');
|
return view('rss/baidu.xml')->contentType('text/xml');
|
||||||
})->ext('xml');
|
})->ext('xml');
|
||||||
|
|
||||||
Route::get('rss/so', function () {
|
$routeGetHead('rss/so', function (\think\Request $Request, SiteContext $SiteContext) {
|
||||||
return view('rss/so.xml')->contentType('text/xml');
|
return $SiteContext->getSoSitemapResponse();
|
||||||
})->ext('xml');
|
})->ext('xml');
|
||||||
|
|
||||||
Route::get('/sitemap_index', function (\think\Request $Request, SiteContext $SiteContext) {
|
$routeGetHead('/sitemap_index', function (\think\Request $Request, SiteContext $SiteContext) {
|
||||||
return $SiteContext->getSiteMapByCode('INDEX');
|
return $SiteContext->getSiteMapByCode('INDEX');
|
||||||
})->ext('xml');
|
})->ext('xml');
|
||||||
|
|
||||||
Route::get('/sitemap-main', function (\think\Request $Request, SiteContext $SiteContext) {
|
$routeGetHead('/sitemap_index.xml', function (\think\Request $Request, SiteContext $SiteContext) {
|
||||||
|
return $SiteContext->getSiteMapByCode('INDEX');
|
||||||
|
});
|
||||||
|
|
||||||
|
$routeGetHead('/sitemap-main', function (\think\Request $Request, SiteContext $SiteContext) {
|
||||||
return $SiteContext->getSiteMapByCode('MAIN');
|
return $SiteContext->getSiteMapByCode('MAIN');
|
||||||
})->ext('xml');
|
})->ext('xml');
|
||||||
|
|
||||||
Route::get('/sitemap-videos-:page', function (\think\Request $Request, SiteContext $SiteContext) {
|
$routeGetHead('/sitemap-main.xml', function (\think\Request $Request, SiteContext $SiteContext) {
|
||||||
|
return $SiteContext->getSiteMapByCode('MAIN');
|
||||||
|
});
|
||||||
|
|
||||||
|
$routeGetHead('/sitemap-videos-:page', function (\think\Request $Request, SiteContext $SiteContext) {
|
||||||
return $SiteContext->getSiteMapByCode('VIDEO');
|
return $SiteContext->getSiteMapByCode('VIDEO');
|
||||||
})->ext('xml');
|
})->ext('xml');
|
||||||
|
|
||||||
|
$routeGetHead('/sitemap-videos-:page.xml', function (\think\Request $Request, SiteContext $SiteContext) {
|
||||||
|
return $SiteContext->getSiteMapByCode('VIDEO');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 榜单首页历史深链优先硬绑定,避免被通用 rank_list 模式误吞到列表页模板。
|
||||||
|
$routeGetHead('/phb-index', function () {
|
||||||
|
return view('video/getRankIndex.html');
|
||||||
|
});
|
||||||
|
|
||||||
// ========== 1️⃣ 取当前域名冻结的 family ==========
|
// ========== 1️⃣ 取当前域名冻结的 family ==========
|
||||||
$tpStyle = SiteStyle::getConfig();
|
$tpStyle = SiteStyle::getConfig();
|
||||||
$family = $tpStyle['template_cfg']['url_family'];
|
$family = $tpStyle['template_cfg']['url_family'];
|
||||||
@@ -62,19 +87,34 @@ if ($strTmpCode == 'videoGpt1') {
|
|||||||
string $route,
|
string $route,
|
||||||
string $view,
|
string $view,
|
||||||
array $pattern = []
|
array $pattern = []
|
||||||
) use (&$registered) {
|
) use (&$registered, $routeGetHead) {
|
||||||
|
|
||||||
if (isset($registered[$route])) {
|
if (isset($registered[$route])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$r = Route::get($route, fn() => view($view));
|
$r = $routeGetHead($route, fn() => view($view));
|
||||||
|
|
||||||
if ($pattern) {
|
if ($pattern) {
|
||||||
$r->pattern($pattern);
|
$r->pattern($pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
$registered[$route] = true;
|
$registered[$route] = true;
|
||||||
|
|
||||||
|
// 兼容带 .html 的冻结路径。
|
||||||
|
// ThinkPHP 在旧路由里普遍使用 "/path" + ->ext('html'),
|
||||||
|
// 而当前 family 里有一部分站点会直接冻结成 "/get.html"、"/record.html"。
|
||||||
|
// 这里同时补一个无后缀别名,避免这类路径在 GPT 新路由分支下落不到视图。
|
||||||
|
if (str_ends_with($route, '.html')) {
|
||||||
|
$routeWithoutExt = substr($route, 0, -5);
|
||||||
|
if ($routeWithoutExt !== '' && !isset($registered[$routeWithoutExt])) {
|
||||||
|
$alias = $routeGetHead($routeWithoutExt, fn() => view($view))->ext('html');
|
||||||
|
if ($pattern) {
|
||||||
|
$alias->pattern($pattern);
|
||||||
|
}
|
||||||
|
$registered[$routeWithoutExt] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== 2.5️⃣ 冻结前按 sort 排序 ==========
|
// ========== 2.5️⃣ 冻结前按 sort 排序 ==========
|
||||||
@@ -126,7 +166,7 @@ if ($strTmpCode == 'videoGpt1') {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'category_home':
|
case 'category_home':
|
||||||
$register($route, 'video/getCategory.html');
|
$register($route, 'video/getMap.html');
|
||||||
break;
|
break;
|
||||||
case 'category_parent':
|
case 'category_parent':
|
||||||
$register($route, 'video/getCategoryType.html',[
|
$register($route, 'video/getCategoryType.html',[
|
||||||
@@ -143,7 +183,7 @@ if ($strTmpCode == 'videoGpt1') {
|
|||||||
|
|
||||||
case 'detail':
|
case 'detail':
|
||||||
$register($route, 'video/getVideoInfo.html', [
|
$register($route, 'video/getVideoInfo.html', [
|
||||||
'intVId' => '\d+',
|
'intVId' => '\d*',
|
||||||
'strPinyin' => '[\w-]+'
|
'strPinyin' => '[\w-]+'
|
||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
@@ -167,7 +207,7 @@ if ($strTmpCode == 'videoGpt1') {
|
|||||||
$register($route, 'video/getRankIndex.html');
|
$register($route, 'video/getRankIndex.html');
|
||||||
break;
|
break;
|
||||||
case 'rank_list':
|
case 'rank_list':
|
||||||
$register($route, 'video/getRankIndex.html',[
|
$register($route, 'video/getRankList.html',[
|
||||||
'strParentCategory' => '[a-z\-]*', // 允许空值
|
'strParentCategory' => '[a-z\-]*', // 允许空值
|
||||||
'strCategory' => '[a-z\-]*', // 允许空值
|
'strCategory' => '[a-z\-]*', // 允许空值
|
||||||
'strSortType' => '[a-z\-]*', // 允许空值
|
'strSortType' => '[a-z\-]*', // 允许空值
|
||||||
@@ -191,6 +231,19 @@ if ($strTmpCode == 'videoGpt1') {
|
|||||||
// 这里只补“旧 detail / play 深链入口”,避免历史外链、旧 sitemap、
|
// 这里只补“旧 detail / play 深链入口”,避免历史外链、旧 sitemap、
|
||||||
// 百度已抓取的旧 family URL 在路由层直接 404。
|
// 百度已抓取的旧 family URL 在路由层直接 404。
|
||||||
$legacyCompatRoutes = [
|
$legacyCompatRoutes = [
|
||||||
|
'rank_index' => [
|
||||||
|
'/paihang/index',
|
||||||
|
'/rank/index',
|
||||||
|
'/top/index',
|
||||||
|
'/phb/index',
|
||||||
|
'/phb-index',
|
||||||
|
|
||||||
|
// 历史榜单聚合入口兼容。
|
||||||
|
// 旧站点上曾出现过这类“label”别名入口,当前 GPT 新路由
|
||||||
|
// 没有注册,访问会直接落到 404。这里统一回榜单首页模板。
|
||||||
|
'/label/new.html',
|
||||||
|
'/label/new',
|
||||||
|
],
|
||||||
'detail_forge' => [
|
'detail_forge' => [
|
||||||
'/vodinfo/:strPinyin-:intVId-:intVForgeId',
|
'/vodinfo/:strPinyin-:intVId-:intVForgeId',
|
||||||
'/vod/:strPinyin-:intVId-:intVForgeId',
|
'/vod/:strPinyin-:intVId-:intVForgeId',
|
||||||
@@ -222,6 +275,9 @@ if ($strTmpCode == 'videoGpt1') {
|
|||||||
'/shipin-neiron/:intVId-:intVForgeId',
|
'/shipin-neiron/:intVId-:intVForgeId',
|
||||||
],
|
],
|
||||||
'detail' => [
|
'detail' => [
|
||||||
|
'/detail/:strPinyin',
|
||||||
|
'/detail/pinyin-:strPinyin',
|
||||||
|
'/detail/:strPinyin/:intVForgeId',
|
||||||
'/voddetail/:strPinyin-:intVId',
|
'/voddetail/:strPinyin-:intVId',
|
||||||
'/vodinfo/:strPinyin-:intVId',
|
'/vodinfo/:strPinyin-:intVId',
|
||||||
'/vod/:strPinyin-:intVId',
|
'/vod/:strPinyin-:intVId',
|
||||||
@@ -288,10 +344,15 @@ if ($strTmpCode == 'videoGpt1') {
|
|||||||
foreach ($legacyCompatRoutes as $page => $routes) {
|
foreach ($legacyCompatRoutes as $page => $routes) {
|
||||||
foreach ($routes as $route) {
|
foreach ($routes as $route) {
|
||||||
switch ($page) {
|
switch ($page) {
|
||||||
|
case 'rank_index':
|
||||||
|
$register($route, 'video/getRankIndex.html');
|
||||||
|
break;
|
||||||
|
|
||||||
case 'detail':
|
case 'detail':
|
||||||
$register($route, 'video/getVideoInfo.html', [
|
$register($route, 'video/getVideoInfo.html', [
|
||||||
'intVId' => '\d+',
|
'intVId' => '\d*',
|
||||||
'strPinyin' => '[\w-]+',
|
'strPinyin' => '[\w-]+',
|
||||||
|
'intVForgeId' => '\d*',
|
||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -337,6 +398,10 @@ if ($strTmpCode == 'videoGpt1') {
|
|||||||
return view('video/getMap.html');
|
return view('video/getMap.html');
|
||||||
})->ext('html');
|
})->ext('html');
|
||||||
|
|
||||||
|
Route::get('/sitemap.xml', function (\think\Request $Request, SiteContext $SiteContext) {
|
||||||
|
return $SiteContext->getSiteMapByCode('INDEX');
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* /rss/baidu
|
* /rss/baidu
|
||||||
*/
|
*/
|
||||||
@@ -348,9 +413,8 @@ if ($strTmpCode == 'videoGpt1') {
|
|||||||
/**
|
/**
|
||||||
* /rss/so
|
* /rss/so
|
||||||
*/
|
*/
|
||||||
Route::get('rss/so', function () {
|
Route::get('rss/so', function (\think\Request $Request, SiteContext $SiteContext) {
|
||||||
return view('rss/so.xml')
|
return $SiteContext->getSoSitemapResponse();
|
||||||
->contentType('text/xml');
|
|
||||||
})->ext('xml');
|
})->ext('xml');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -361,6 +425,10 @@ if ($strTmpCode == 'videoGpt1') {
|
|||||||
// return view('sitemap/sitemap_index.xml');
|
// return view('sitemap/sitemap_index.xml');
|
||||||
})->ext('xml');
|
})->ext('xml');
|
||||||
|
|
||||||
|
Route::get('/sitemap_index.xml', function (\think\Request $Request, SiteContext $SiteContext) {
|
||||||
|
return $SiteContext->getSiteMapByCode('INDEX');
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 首页、分类列表、排行榜、男生/女生频道。
|
* 首页、分类列表、排行榜、男生/女生频道。
|
||||||
*/
|
*/
|
||||||
@@ -369,6 +437,10 @@ if ($strTmpCode == 'videoGpt1') {
|
|||||||
// return view('sitemap/sitemap-main.xml');
|
// return view('sitemap/sitemap-main.xml');
|
||||||
})->ext('xml');
|
})->ext('xml');
|
||||||
|
|
||||||
|
Route::get('/sitemap-main.xml', function (\think\Request $Request, SiteContext $SiteContext) {
|
||||||
|
return $SiteContext->getSiteMapByCode('MAIN');
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 小说目录页面。
|
* 小说目录页面。
|
||||||
*/
|
*/
|
||||||
@@ -408,6 +480,10 @@ if ($strTmpCode == 'videoGpt1') {
|
|||||||
// return view('sitemap/sitemap-books.xml');
|
// return view('sitemap/sitemap-books.xml');
|
||||||
})->ext('xml');
|
})->ext('xml');
|
||||||
|
|
||||||
|
Route::get('/sitemap-videos-:page.xml', function (\think\Request $Request, SiteContext $SiteContext) {
|
||||||
|
return $SiteContext->getSiteMapByCode('VIDEO');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 搜索首页
|
* 搜索首页
|
||||||
@@ -851,7 +927,7 @@ if ($strTmpCode == 'videoGpt1') {
|
|||||||
],
|
],
|
||||||
|
|
||||||
//排行榜
|
//排行榜
|
||||||
'video_rank_index' => ['/paihang/index', '/rank/index', '/paihang-quan', '/top/index', '/paihang-bang', '/ranks/index', '/sort/index', '/order/index', '/phb/index'],
|
'video_rank_index' => ['/paihang/index', '/rank/index', '/paihang-quan', '/top/index', '/paihang-bang', '/ranks/index', '/sort/index', '/order/index', '/phb/index', '/phb-index'],
|
||||||
|
|
||||||
//排行榜
|
//排行榜
|
||||||
'video_rank_list' => [
|
'video_rank_list' => [
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export_name="arrVideoPiaofang" /}
|
|||||||
{block name="description"}{site:replace code="VIDEO@INDEX@INDEX@DESCRIPTION"}{/block}
|
{block name="description"}{site:replace code="VIDEO@INDEX@INDEX@DESCRIPTION"}{/block}
|
||||||
|
|
||||||
{block name="head"}
|
{block name="head"}
|
||||||
|
<link rel="canonical" href="https://{$DomainModel->d_domain}" />
|
||||||
<!-- 社交媒体标签 -->
|
<!-- 社交媒体标签 -->
|
||||||
<meta property="og:title" content='{site:replace code="VIDEO@INDEX@INDEX@TITLE"}' />
|
<meta property="og:title" content='{site:replace code="VIDEO@INDEX@INDEX@TITLE"}' />
|
||||||
<meta property="og:description" content='{site:replace code="VIDEO@INDEX@INDEX@DESCRIPTION"}' />
|
<meta property="og:description" content='{site:replace code="VIDEO@INDEX@INDEX@DESCRIPTION"}' />
|
||||||
@@ -26,6 +27,7 @@ export_name="arrVideoPiaofang" /}
|
|||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
"@type": "WebSite",
|
"@type": "WebSite",
|
||||||
"name": "{$DomainModel->d_name}",
|
"name": "{$DomainModel->d_name}",
|
||||||
"url": "https://{$DomainModel->d_domain}",
|
"url": "https://{$DomainModel->d_domain}",
|
||||||
@@ -95,10 +97,10 @@ export_name="arrVideoPiaofang" /}
|
|||||||
{volist name="arrVideoPiaofang" id="Video" key="key"}
|
{volist name="arrVideoPiaofang" id="Video" key="key"}
|
||||||
{if $key < 5 }
|
{if $key < 5 }
|
||||||
{
|
{
|
||||||
"@type": "ListItem{$Video.v_id}",
|
"@type": "ListItem",
|
||||||
"position": {$key},
|
"position": {$key},
|
||||||
"item": {
|
"item": {
|
||||||
"@type": "Movies",
|
"@type": "Movie",
|
||||||
"name": "{$Video.v_name}",
|
"name": "{$Video.v_name}",
|
||||||
"url": 'https://{$DomainModel->d_domain}{site:vurl v_id="$Video.v_id" v_py="$Video.v_name_en"/}',
|
"url": 'https://{$DomainModel->d_domain}{site:vurl v_id="$Video.v_id" v_py="$Video.v_name_en"/}',
|
||||||
"description": "{$Video.v_description}",
|
"description": "{$Video.v_description}",
|
||||||
|
|||||||
@@ -1,35 +1,16 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<url>
|
<url>
|
||||||
<loc>https://www.{$DomainModel->d_domain}/</loc>
|
<loc>https://{$DomainModel->d_domain}/</loc>
|
||||||
<lastmod>{:date('Y-m-d')}</lastmod>
|
<lastmod>{:date('Y-m-d')}</lastmod>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://www.{$DomainModel->d_domain}{$strRankUrlTemp}</loc>
|
<loc>https://{$DomainModel->d_domain}{$strRankUrlTemp}</loc>
|
||||||
<lastmod>{:date('Y-m-d')}</lastmod>
|
<lastmod>{:date('Y-m-d')}</lastmod>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<!-- 分类 -->
|
|
||||||
{novel:category d_key="strCategoryPinyin" d_val="strCategoryName"}
|
|
||||||
<!-- 排序 -->
|
|
||||||
{novel:sort d_key="sort_key" d_val="strSortName"}
|
|
||||||
<!-- 状态 -->
|
|
||||||
{novel:status d_key="status_key" d_val="strStatusName"}
|
|
||||||
<url>
|
|
||||||
<loc>https://www.{$DomainModel->d_domain}{site:nflurl category="$strCategoryPinyin" order="$sort_key" status="$status_key" /}</loc>
|
|
||||||
<lastmod>{:date('Y-m-d')}</lastmod>
|
|
||||||
<changefreq>weekly</changefreq>
|
|
||||||
<priority>0.8</priority>
|
|
||||||
</url>
|
|
||||||
{/novel:status}
|
|
||||||
{/novel:sort}
|
|
||||||
{/novel:category}
|
|
||||||
|
|
||||||
|
|
||||||
<!-- 注意 所有链接不要分页,有分页的取第一页即可 -->
|
|
||||||
</urlset>
|
</urlset>
|
||||||
@@ -1,39 +1,7 @@
|
|||||||
{novel:pagerexp page="1" limit="40000"
|
|
||||||
sort_type="news"
|
|
||||||
d_key="d_key"
|
|
||||||
d_val="Novel"
|
|
||||||
cache_life="3600" func="generateCategoryPager" export_name="resData" /}
|
|
||||||
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<sitemap>
|
<sitemap>
|
||||||
<loc>https://www.{$DomainModel->d_domain}/sitemap-main.xml</loc>
|
<loc>https://{$DomainModel->d_domain}/sitemap-main.xml</loc>
|
||||||
<lastmod>{:date('Y-m-d')}</lastmod>
|
<lastmod>{:date('Y-m-d')}</lastmod>
|
||||||
</sitemap>
|
</sitemap>
|
||||||
|
|
||||||
<!--小说总数量/50000 得出 sitemap-books 一共有多少个 -->
|
|
||||||
{for start="0" end="$resData.p_data.pages"}
|
|
||||||
<sitemap>
|
|
||||||
<loc>https://www.{$DomainModel->d_domain}/sitemap-books-{$i+1}.xml</loc>
|
|
||||||
<lastmod>{:date('Y-m-d')}</lastmod>
|
|
||||||
</sitemap>
|
|
||||||
{/for}
|
|
||||||
|
|
||||||
<!-- 小说总数量/50000 得出 sitemap-books-catalog一共有多少个,每个小说只取一个目录链接,分页不需要加入去 -->
|
|
||||||
{for start="0" end="$resData.p_data.pages"}
|
|
||||||
<sitemap>
|
|
||||||
<loc>https://www.{$DomainModel->d_domain}/sitemap-books-catalog-{$i+1}.xml</loc>
|
|
||||||
<lastmod>{:date('Y-m-d')}</lastmod>
|
|
||||||
</sitemap>
|
|
||||||
{/for}
|
|
||||||
|
|
||||||
<!-- 小说章节取最新章节 -->
|
|
||||||
{for start="0" end="$resData.p_data.pages"}
|
|
||||||
<sitemap>
|
|
||||||
<loc>https://www.{$DomainModel->d_domain}/sitemap-chapters-{$i+1}.xml</loc>
|
|
||||||
<lastmod>{:date('Y-m-d')}</lastmod>
|
|
||||||
</sitemap>
|
|
||||||
{/for}
|
|
||||||
|
|
||||||
|
|
||||||
</sitemapindex>
|
</sitemapindex>
|
||||||
@@ -13,10 +13,12 @@
|
|||||||
{block name="description"}{site:replace code="VIDEO@GETCATEGORYINDEX@DESCRIPTION"}{/block}
|
{block name="description"}{site:replace code="VIDEO@GETCATEGORYINDEX@DESCRIPTION"}{/block}
|
||||||
|
|
||||||
{block name="head"}
|
{block name="head"}
|
||||||
|
<meta name="robots" content="index,follow">
|
||||||
|
<link rel="canonical" href='https://{$DomainModel->d_domain}{site:vciurl parent_category="$Request.route.strParentCategory" /}'>
|
||||||
<!-- 社交媒体标签 -->
|
<!-- 社交媒体标签 -->
|
||||||
<meta property="og:title" content='{site:replace code="VIDEO@GETCATEGORYINDEX@TITLE"}' />
|
<meta property="og:title" content='{site:replace code="VIDEO@GETCATEGORYINDEX@TITLE"}' />
|
||||||
<meta property="og:description" content='{site:replace code="VIDEO@GETCATEGORYINDEX@DESCRIPTION"}' />
|
<meta property="og:description" content='{site:replace code="VIDEO@GETCATEGORYINDEX@DESCRIPTION"}' />
|
||||||
<meta property="og:url" content="https://{$DomainModel->d_domain}" />
|
<meta property="og:url" content='https://{$DomainModel->d_domain}{site:vciurl parent_category="$Request.route.strParentCategory" /}' />
|
||||||
{switch $Request.route.strParentCategory }
|
{switch $Request.route.strParentCategory }
|
||||||
{case 'dian-ying' }
|
{case 'dian-ying' }
|
||||||
<meta property="og:type" content="video.movie" />
|
<meta property="og:type" content="video.movie" />
|
||||||
@@ -45,32 +47,13 @@
|
|||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "CollectionPage",
|
"@type": "CollectionPage",
|
||||||
"name": "{$DomainModel->d_name} - 最新{site:getval code="strVideoParentCategoryName" /}推荐",
|
"name": "{$DomainModel->d_name} - 最新{site:getval code="strVideoParentCategoryName" /}推荐",
|
||||||
"url": "https://{$DomainModel->d_domain}",
|
"url": "https://{$DomainModel->d_domain}{site:vciurl parent_category="$Request.route.strParentCategory" /}",
|
||||||
"description": "{site:replace code="VIDEO@GETCATEGORYINDEX@DESCRIPTION"}",
|
"description": "{site:replace code="VIDEO@GETCATEGORYINDEX@DESCRIPTION"}",
|
||||||
"potentialAction": {
|
"isPartOf": {
|
||||||
"@type": "SearchAction",
|
"@type": "WebSite",
|
||||||
"target": 'https://{$DomainModel->d_domain}{site:vsurl key="search_term_string" p="1"/}",
|
"name": "{$DomainModel->d_name}",
|
||||||
"query-input": "required name=search_term_string"
|
"url": "https://{$DomainModel->d_domain}"
|
||||||
}
|
}
|
||||||
"hasPart": [
|
|
||||||
{video:ranklist count="5"
|
|
||||||
v_parent_category_en="$Request.route.strParentCategory"
|
|
||||||
sort_type="weekly"
|
|
||||||
d_key="key" d_val="Video" cache_life="3600"}
|
|
||||||
|
|
||||||
{if $key < 5 }
|
|
||||||
{
|
|
||||||
"@type": "VideoObject",
|
|
||||||
"name": "{$Video.v_name}",
|
|
||||||
"description": "{$Video.v_description}",
|
|
||||||
"thumbnailUrl": '{$Video.v_pic}',
|
|
||||||
"uploadDate": "{:date('Y-m-d')}",
|
|
||||||
"url": 'https://{$DomainModel->d_domain}{site:vurl v_id="$Video.v_id" v_py="$Video.v_name_en"/}',
|
|
||||||
},
|
|
||||||
{/if}
|
|
||||||
{/video:ranklist}
|
|
||||||
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
@@ -89,7 +72,7 @@
|
|||||||
"item": "https://{$DomainModel->d_domain}{site:vciurl parent_category="$Request.route.strParentCategory" /}"
|
"item": "https://{$DomainModel->d_domain}{site:vciurl parent_category="$Request.route.strParentCategory" /}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ href='https://{$DomainModel->d_domain}{site:vurl v_id="$arrVideo.v_id" v_py="$ar
|
|||||||
</div>
|
</div>
|
||||||
<p class="data">
|
<p class="data">
|
||||||
<span class="text-muted">分类:</span>
|
<span class="text-muted">分类:</span>
|
||||||
<a href='{site:vsurl key="$arrVideo.v_parent_category" p="1"/}'>{$arrVideo.v_parent_category}</a>
|
<a href='{site:vciurl parent_category="$arrVideo.v_parent_category_en" /}'>{$arrVideo.v_parent_category}</a>
|
||||||
<span class="split-line"></span>
|
<span class="split-line"></span>
|
||||||
<span class="text-muted hidden-xs">地区:</span>
|
<span class="text-muted hidden-xs">地区:</span>
|
||||||
{volist name='$arrVideo.v_area' id='vo' key='index'}
|
{volist name='$arrVideo.v_area' id='vo' key='index'}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
<meta property="og:type" content="video.movie" />
|
<meta property="og:type" content="video.movie" />
|
||||||
{/switch}
|
{/switch}
|
||||||
<meta property="og:url"
|
<meta property="og:url"
|
||||||
content='https://{$DomainModel->d_domain}{site:vurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en"/}'>
|
content='https://{$DomainModel->d_domain}{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>
|
||||||
<meta property="og:image" content="{$arrVideo.v_pic}">
|
<meta property="og:image" content="{$arrVideo.v_pic}">
|
||||||
<meta property="og:site_name" content="{$DomainModel->d_name}">
|
<meta property="og:site_name" content="{$DomainModel->d_name}">
|
||||||
<meta property="og:video:type" content="video/m3u8">
|
<meta property="og:video:type" content="video/m3u8">
|
||||||
@@ -69,7 +69,7 @@ href='https://{$DomainModel->d_domain}{site:vpurl v_id="$arrVideo.v_id" v_py="$a
|
|||||||
"description": "{$arrVideo.v_description}",
|
"description": "{$arrVideo.v_description}",
|
||||||
"thumbnailUrl": "{$arrVideo.v_pic}",
|
"thumbnailUrl": "{$arrVideo.v_pic}",
|
||||||
"uploadDate": "{:date('Y-m-d')}",
|
"uploadDate": "{:date('Y-m-d')}",
|
||||||
"url": 'https://{$DomainModel->d_domain}{site:vurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en"/}',
|
"url": 'https://{$DomainModel->d_domain}{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}',
|
||||||
|
|
||||||
"inLanguage": [
|
"inLanguage": [
|
||||||
{volist name='$arrVideo.v_lang' id='strVideoLang' key='index'}
|
{volist name='$arrVideo.v_lang' id='strVideoLang' key='index'}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
{block name="keywords"}{site:replace code="VIDEO@INDEX@INDEX@KEYWORDS"}{/block}
|
{block name="keywords"}{site:replace code="VIDEO@INDEX@INDEX@KEYWORDS"}{/block}
|
||||||
{block name="description"}{site:replace code="VIDEO@INDEX@INDEX@DESCRIPTION"}{/block}
|
{block name="description"}{site:replace code="VIDEO@INDEX@INDEX@DESCRIPTION"}{/block}
|
||||||
{block name="head"}
|
{block name="head"}
|
||||||
|
<link rel="canonical" href="https://{$DomainModel->d_domain}" />
|
||||||
{// 社交媒体标签 }
|
{// 社交媒体标签 }
|
||||||
<meta property="og:title" content='{site:replace code="VIDEO@INDEX@INDEX@TITLE"}' />
|
<meta property="og:title" content='{site:replace code="VIDEO@INDEX@INDEX@TITLE"}' />
|
||||||
<meta property="og:description" content='{site:replace code="VIDEO@INDEX@INDEX@DESCRIPTION"}' />
|
<meta property="og:description" content='{site:replace code="VIDEO@INDEX@INDEX@DESCRIPTION"}' />
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
"@type": "WebSite",
|
"@type": "WebSite",
|
||||||
"name": "{$DomainModel->d_name}",
|
"name": "{$DomainModel->d_name}",
|
||||||
"url": "https://{$DomainModel->d_domain}",
|
"url": "https://{$DomainModel->d_domain}",
|
||||||
@@ -84,10 +86,10 @@
|
|||||||
{volist name="arrVideoPiaofang" id="Video" key="key"}
|
{volist name="arrVideoPiaofang" id="Video" key="key"}
|
||||||
{if $key < 5 }
|
{if $key < 5 }
|
||||||
{
|
{
|
||||||
"@type": "ListItem{$Video.v_id}",
|
"@type": "ListItem",
|
||||||
"position": {$key},
|
"position": {$key},
|
||||||
"item": {
|
"item": {
|
||||||
"@type": "Movies",
|
"@type": "Movie",
|
||||||
"name": "{$Video.v_name}",
|
"name": "{$Video.v_name}",
|
||||||
"url": 'https://{$DomainModel->d_domain}{site:vurl v_id="$Video.v_id" v_py="$Video.v_name_en"/}',
|
"url": 'https://{$DomainModel->d_domain}{site:vurl v_id="$Video.v_id" v_py="$Video.v_name_en"/}',
|
||||||
"description": "{$Video.v_description}",
|
"description": "{$Video.v_description}",
|
||||||
|
|||||||
@@ -1,35 +1,16 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<url>
|
<url>
|
||||||
<loc>https://www.{$DomainModel->d_domain}/</loc>
|
<loc>https://{$DomainModel->d_domain}/</loc>
|
||||||
<lastmod>{:date('Y-m-d')}</lastmod>
|
<lastmod>{:date('Y-m-d')}</lastmod>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://www.{$DomainModel->d_domain}{$strRankUrlTemp}</loc>
|
<loc>https://{$DomainModel->d_domain}{$strRankUrlTemp}</loc>
|
||||||
<lastmod>{:date('Y-m-d')}</lastmod>
|
<lastmod>{:date('Y-m-d')}</lastmod>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<!-- 分类 -->
|
|
||||||
{novel:category d_key="strCategoryPinyin" d_val="strCategoryName"}
|
|
||||||
<!-- 排序 -->
|
|
||||||
{novel:sort d_key="sort_key" d_val="strSortName"}
|
|
||||||
<!-- 状态 -->
|
|
||||||
{novel:status d_key="status_key" d_val="strStatusName"}
|
|
||||||
<url>
|
|
||||||
<loc>https://www.{$DomainModel->d_domain}{site:nflurl category="$strCategoryPinyin" order="$sort_key" status="$status_key" /}</loc>
|
|
||||||
<lastmod>{:date('Y-m-d')}</lastmod>
|
|
||||||
<changefreq>weekly</changefreq>
|
|
||||||
<priority>0.8</priority>
|
|
||||||
</url>
|
|
||||||
{/novel:status}
|
|
||||||
{/novel:sort}
|
|
||||||
{/novel:category}
|
|
||||||
|
|
||||||
|
|
||||||
<!-- 注意 所有链接不要分页,有分页的取第一页即可 -->
|
|
||||||
</urlset>
|
</urlset>
|
||||||
@@ -1,39 +1,7 @@
|
|||||||
{novel:pagerexp page="1" limit="40000"
|
|
||||||
sort_type="news"
|
|
||||||
d_key="d_key"
|
|
||||||
d_val="Novel"
|
|
||||||
cache_life="3600" func="generateCategoryPager" export_name="resData" /}
|
|
||||||
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<sitemap>
|
<sitemap>
|
||||||
<loc>https://www.{$DomainModel->d_domain}/sitemap-main.xml</loc>
|
<loc>https://{$DomainModel->d_domain}/sitemap-main.xml</loc>
|
||||||
<lastmod>{:date('Y-m-d')}</lastmod>
|
<lastmod>{:date('Y-m-d')}</lastmod>
|
||||||
</sitemap>
|
</sitemap>
|
||||||
|
|
||||||
<!--小说总数量/50000 得出 sitemap-books 一共有多少个 -->
|
|
||||||
{for start="0" end="$resData.p_data.pages"}
|
|
||||||
<sitemap>
|
|
||||||
<loc>https://www.{$DomainModel->d_domain}/sitemap-books-{$i+1}.xml</loc>
|
|
||||||
<lastmod>{:date('Y-m-d')}</lastmod>
|
|
||||||
</sitemap>
|
|
||||||
{/for}
|
|
||||||
|
|
||||||
<!-- 小说总数量/50000 得出 sitemap-books-catalog一共有多少个,每个小说只取一个目录链接,分页不需要加入去 -->
|
|
||||||
{for start="0" end="$resData.p_data.pages"}
|
|
||||||
<sitemap>
|
|
||||||
<loc>https://www.{$DomainModel->d_domain}/sitemap-books-catalog-{$i+1}.xml</loc>
|
|
||||||
<lastmod>{:date('Y-m-d')}</lastmod>
|
|
||||||
</sitemap>
|
|
||||||
{/for}
|
|
||||||
|
|
||||||
<!-- 小说章节取最新章节 -->
|
|
||||||
{for start="0" end="$resData.p_data.pages"}
|
|
||||||
<sitemap>
|
|
||||||
<loc>https://www.{$DomainModel->d_domain}/sitemap-chapters-{$i+1}.xml</loc>
|
|
||||||
<lastmod>{:date('Y-m-d')}</lastmod>
|
|
||||||
</sitemap>
|
|
||||||
{/for}
|
|
||||||
|
|
||||||
|
|
||||||
</sitemapindex>
|
</sitemapindex>
|
||||||
@@ -13,10 +13,12 @@
|
|||||||
{block name="description"}{site:replace code="VIDEO@GETCATEGORYINDEX@DESCRIPTION"}{/block}
|
{block name="description"}{site:replace code="VIDEO@GETCATEGORYINDEX@DESCRIPTION"}{/block}
|
||||||
|
|
||||||
{block name="head"}
|
{block name="head"}
|
||||||
|
<meta name="robots" content="index,follow">
|
||||||
|
<link rel="canonical" href='https://{$DomainModel->d_domain}{site:vciurl parent_category="$Request.route.strParentCategory" /}'>
|
||||||
<!-- 社交媒体标签 -->
|
<!-- 社交媒体标签 -->
|
||||||
<meta property="og:title" content='{site:replace code="VIDEO@GETCATEGORYINDEX@TITLE"}' />
|
<meta property="og:title" content='{site:replace code="VIDEO@GETCATEGORYINDEX@TITLE"}' />
|
||||||
<meta property="og:description" content='{site:replace code="VIDEO@GETCATEGORYINDEX@DESCRIPTION"}' />
|
<meta property="og:description" content='{site:replace code="VIDEO@GETCATEGORYINDEX@DESCRIPTION"}' />
|
||||||
<meta property="og:url" content="https://{$DomainModel->d_domain}" />
|
<meta property="og:url" content='https://{$DomainModel->d_domain}{site:vciurl parent_category="$Request.route.strParentCategory" /}' />
|
||||||
{switch $Request.route.strParentCategory }
|
{switch $Request.route.strParentCategory }
|
||||||
{case 'dian-ying' }
|
{case 'dian-ying' }
|
||||||
<meta property="og:type" content="video.movie" />
|
<meta property="og:type" content="video.movie" />
|
||||||
@@ -45,32 +47,13 @@
|
|||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "CollectionPage",
|
"@type": "CollectionPage",
|
||||||
"name": "{$DomainModel->d_name} - 最新{site:getval code="strVideoParentCategoryName" /}推荐",
|
"name": "{$DomainModel->d_name} - 最新{site:getval code="strVideoParentCategoryName" /}推荐",
|
||||||
"url": "https://{$DomainModel->d_domain}",
|
"url": "https://{$DomainModel->d_domain}{site:vciurl parent_category="$Request.route.strParentCategory" /}",
|
||||||
"description": "{site:replace code="VIDEO@GETCATEGORYINDEX@DESCRIPTION"}",
|
"description": "{site:replace code="VIDEO@GETCATEGORYINDEX@DESCRIPTION"}",
|
||||||
"potentialAction": {
|
"isPartOf": {
|
||||||
"@type": "SearchAction",
|
"@type": "WebSite",
|
||||||
"target": 'https://{$DomainModel->d_domain}{site:vsurl key="search_term_string" p="1"/}",
|
"name": "{$DomainModel->d_name}",
|
||||||
"query-input": "required name=search_term_string"
|
"url": "https://{$DomainModel->d_domain}"
|
||||||
}
|
}
|
||||||
"hasPart": [
|
|
||||||
{video:ranklist count="5"
|
|
||||||
v_parent_category_en="$Request.route.strParentCategory"
|
|
||||||
sort_type="weekly"
|
|
||||||
d_key="key" d_val="Video" cache_life="3600"}
|
|
||||||
|
|
||||||
{if $key < 5 }
|
|
||||||
{
|
|
||||||
"@type": "VideoObject",
|
|
||||||
"name": "{$Video.v_name}",
|
|
||||||
"description": "{$Video.v_description}",
|
|
||||||
"thumbnailUrl": '{$Video.v_pic}',
|
|
||||||
"uploadDate": "{:date('Y-m-d')}",
|
|
||||||
"url": 'https://{$DomainModel->d_domain}{site:vurl v_id="$Video.v_id" v_py="$Video.v_name_en"/}',
|
|
||||||
},
|
|
||||||
{/if}
|
|
||||||
{/video:ranklist}
|
|
||||||
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
@@ -89,7 +72,7 @@
|
|||||||
"item": "https://{$DomainModel->d_domain}{site:vciurl parent_category="$Request.route.strParentCategory" /}"
|
"item": "https://{$DomainModel->d_domain}{site:vciurl parent_category="$Request.route.strParentCategory" /}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ href='https://{$DomainModel->d_domain}{site:vurl v_id="$arrVideo.v_id" v_py="$ar
|
|||||||
</h2>
|
</h2>
|
||||||
<p class="data">
|
<p class="data">
|
||||||
<span class="text-muted">类型:</span>
|
<span class="text-muted">类型:</span>
|
||||||
<a href='{site:vsurl key="$arrVideo.v_category" p="1"/}'>{$arrVideo.v_category}</a>
|
<a href='{site:vclurl parent_category="$arrVideo.v_parent_category_en" category="$arrVideo.v_category_en" area="all" lang="all" year="all" order="all" page="1"/}'>{$arrVideo.v_category}</a>
|
||||||
|
|
||||||
<span class="split-line"></span>
|
<span class="split-line"></span>
|
||||||
<span class="text-muted hidden-xs">地区:</span>
|
<span class="text-muted hidden-xs">地区:</span>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
{default /}
|
{default /}
|
||||||
<meta property="og:type" content="video.movie" />
|
<meta property="og:type" content="video.movie" />
|
||||||
{/switch}
|
{/switch}
|
||||||
<meta property="og:url" content='https://{$DomainModel->d_domain}{site:vurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en"/}'>
|
<meta property="og:url" content='https://{$DomainModel->d_domain}{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>
|
||||||
<meta property="og:image" content="{$arrVideo.v_pic}">
|
<meta property="og:image" content="{$arrVideo.v_pic}">
|
||||||
<meta property="og:site_name" content="{$DomainModel->d_name}">
|
<meta property="og:site_name" content="{$DomainModel->d_name}">
|
||||||
<meta property="og:video:type" content="video/m3u8">
|
<meta property="og:video:type" content="video/m3u8">
|
||||||
@@ -68,7 +68,7 @@ href='https://{$DomainModel->d_domain}{site:vpurl v_id="$arrVideo.v_id" v_py="$a
|
|||||||
"description": "{$arrVideo.v_description}",
|
"description": "{$arrVideo.v_description}",
|
||||||
"thumbnailUrl": "{$arrVideo.v_pic}",
|
"thumbnailUrl": "{$arrVideo.v_pic}",
|
||||||
"uploadDate": "{:date('Y-m-d')}",
|
"uploadDate": "{:date('Y-m-d')}",
|
||||||
"url": 'https://{$DomainModel->d_domain}{site:vurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en"/}',
|
"url": 'https://{$DomainModel->d_domain}{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}',
|
||||||
|
|
||||||
"inLanguage": [
|
"inLanguage": [
|
||||||
{volist name='$arrVideo.v_lang' id='strVideoLang' key='index'}
|
{volist name='$arrVideo.v_lang' id='strVideoLang' key='index'}
|
||||||
@@ -205,7 +205,7 @@ href='https://{$DomainModel->d_domain}{site:vpurl v_id="$arrVideo.v_id" v_py="$a
|
|||||||
</h1>
|
</h1>
|
||||||
<p class="data margin-0">
|
<p class="data margin-0">
|
||||||
<span class="text-muted">类型:</span>
|
<span class="text-muted">类型:</span>
|
||||||
<a href='{site:vsurl key="$arrVideo.v_category" p="1"/}'>{$arrVideo.v_category}</a>
|
<a href='{site:vclurl parent_category="$arrVideo.v_parent_category_en" category="$arrVideo.v_category_en" area="all" lang="all" year="all" order="all" page="1"/}'>{$arrVideo.v_category}</a>
|
||||||
|
|
||||||
<span class="split-line"></span>
|
<span class="split-line"></span>
|
||||||
<span class="text-muted">地区:</span>
|
<span class="text-muted">地区:</span>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export_name="arrVideoPiaofang" /}
|
|||||||
{block name="keywords"}{site:replace code="VIDEO@INDEX@INDEX@KEYWORDS"}{/block}
|
{block name="keywords"}{site:replace code="VIDEO@INDEX@INDEX@KEYWORDS"}{/block}
|
||||||
{block name="description"}{site:replace code="VIDEO@INDEX@INDEX@DESCRIPTION"}{/block}
|
{block name="description"}{site:replace code="VIDEO@INDEX@INDEX@DESCRIPTION"}{/block}
|
||||||
{block name="head"}
|
{block name="head"}
|
||||||
|
<link rel="canonical" href="https://{$DomainModel->d_domain}" />
|
||||||
<!-- 社交媒体标签 -->
|
<!-- 社交媒体标签 -->
|
||||||
<meta property="og:title" content='{site:replace code="VIDEO@INDEX@INDEX@TITLE"}' />
|
<meta property="og:title" content='{site:replace code="VIDEO@INDEX@INDEX@TITLE"}' />
|
||||||
<meta property="og:description" content='{site:replace code="VIDEO@INDEX@INDEX@DESCRIPTION"}' />
|
<meta property="og:description" content='{site:replace code="VIDEO@INDEX@INDEX@DESCRIPTION"}' />
|
||||||
@@ -19,6 +20,7 @@ export_name="arrVideoPiaofang" /}
|
|||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
"@type": "WebSite",
|
"@type": "WebSite",
|
||||||
"name": "{$DomainModel->d_name}",
|
"name": "{$DomainModel->d_name}",
|
||||||
"url": "https://{$DomainModel->d_domain}",
|
"url": "https://{$DomainModel->d_domain}",
|
||||||
@@ -85,10 +87,10 @@ export_name="arrVideoPiaofang" /}
|
|||||||
{volist name="arrVideoPiaofang" id="Video" key="key"}
|
{volist name="arrVideoPiaofang" id="Video" key="key"}
|
||||||
{if $key < 5 }
|
{if $key < 5 }
|
||||||
{
|
{
|
||||||
"@type": "ListItem{$Video.v_id}",
|
"@type": "ListItem",
|
||||||
"position": {$key},
|
"position": {$key},
|
||||||
"item": {
|
"item": {
|
||||||
"@type": "Movies",
|
"@type": "Movie",
|
||||||
"name": "{$Video.v_name}",
|
"name": "{$Video.v_name}",
|
||||||
"url": 'https://{$DomainModel->d_domain}{site:vurl v_id="$Video.v_id" v_py="$Video.v_name_en"/}',
|
"url": 'https://{$DomainModel->d_domain}{site:vurl v_id="$Video.v_id" v_py="$Video.v_name_en"/}',
|
||||||
"description": "{$Video.v_description}",
|
"description": "{$Video.v_description}",
|
||||||
|
|||||||
@@ -1,35 +1,16 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<url>
|
<url>
|
||||||
<loc>https://www.{$DomainModel->d_domain}/</loc>
|
<loc>https://{$DomainModel->d_domain}/</loc>
|
||||||
<lastmod>{:date('Y-m-d')}</lastmod>
|
<lastmod>{:date('Y-m-d')}</lastmod>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://www.{$DomainModel->d_domain}{$strRankUrlTemp}</loc>
|
<loc>https://{$DomainModel->d_domain}{$strRankUrlTemp}</loc>
|
||||||
<lastmod>{:date('Y-m-d')}</lastmod>
|
<lastmod>{:date('Y-m-d')}</lastmod>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|
||||||
<!-- 分类 -->
|
|
||||||
{novel:category d_key="strCategoryPinyin" d_val="strCategoryName"}
|
|
||||||
<!-- 排序 -->
|
|
||||||
{novel:sort d_key="sort_key" d_val="strSortName"}
|
|
||||||
<!-- 状态 -->
|
|
||||||
{novel:status d_key="status_key" d_val="strStatusName"}
|
|
||||||
<url>
|
|
||||||
<loc>https://www.{$DomainModel->d_domain}{site:nflurl category="$strCategoryPinyin" order="$sort_key" status="$status_key" /}</loc>
|
|
||||||
<lastmod>{:date('Y-m-d')}</lastmod>
|
|
||||||
<changefreq>weekly</changefreq>
|
|
||||||
<priority>0.8</priority>
|
|
||||||
</url>
|
|
||||||
{/novel:status}
|
|
||||||
{/novel:sort}
|
|
||||||
{/novel:category}
|
|
||||||
|
|
||||||
|
|
||||||
<!-- 注意 所有链接不要分页,有分页的取第一页即可 -->
|
|
||||||
</urlset>
|
</urlset>
|
||||||
@@ -1,39 +1,7 @@
|
|||||||
{novel:pagerexp page="1" limit="40000"
|
|
||||||
sort_type="news"
|
|
||||||
d_key="d_key"
|
|
||||||
d_val="Novel"
|
|
||||||
cache_life="3600" func="generateCategoryPager" export_name="resData" /}
|
|
||||||
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<sitemap>
|
<sitemap>
|
||||||
<loc>https://www.{$DomainModel->d_domain}/sitemap-main.xml</loc>
|
<loc>https://{$DomainModel->d_domain}/sitemap-main.xml</loc>
|
||||||
<lastmod>{:date('Y-m-d')}</lastmod>
|
<lastmod>{:date('Y-m-d')}</lastmod>
|
||||||
</sitemap>
|
</sitemap>
|
||||||
|
|
||||||
<!--小说总数量/50000 得出 sitemap-books 一共有多少个 -->
|
|
||||||
{for start="0" end="$resData.p_data.pages"}
|
|
||||||
<sitemap>
|
|
||||||
<loc>https://www.{$DomainModel->d_domain}/sitemap-books-{$i+1}.xml</loc>
|
|
||||||
<lastmod>{:date('Y-m-d')}</lastmod>
|
|
||||||
</sitemap>
|
|
||||||
{/for}
|
|
||||||
|
|
||||||
<!-- 小说总数量/50000 得出 sitemap-books-catalog一共有多少个,每个小说只取一个目录链接,分页不需要加入去 -->
|
|
||||||
{for start="0" end="$resData.p_data.pages"}
|
|
||||||
<sitemap>
|
|
||||||
<loc>https://www.{$DomainModel->d_domain}/sitemap-books-catalog-{$i+1}.xml</loc>
|
|
||||||
<lastmod>{:date('Y-m-d')}</lastmod>
|
|
||||||
</sitemap>
|
|
||||||
{/for}
|
|
||||||
|
|
||||||
<!-- 小说章节取最新章节 -->
|
|
||||||
{for start="0" end="$resData.p_data.pages"}
|
|
||||||
<sitemap>
|
|
||||||
<loc>https://www.{$DomainModel->d_domain}/sitemap-chapters-{$i+1}.xml</loc>
|
|
||||||
<lastmod>{:date('Y-m-d')}</lastmod>
|
|
||||||
</sitemap>
|
|
||||||
{/for}
|
|
||||||
|
|
||||||
|
|
||||||
</sitemapindex>
|
</sitemapindex>
|
||||||
@@ -13,10 +13,12 @@
|
|||||||
{block name="description"}{site:replace code="VIDEO@GETCATEGORYINDEX@DESCRIPTION"}{/block}
|
{block name="description"}{site:replace code="VIDEO@GETCATEGORYINDEX@DESCRIPTION"}{/block}
|
||||||
|
|
||||||
{block name="head"}
|
{block name="head"}
|
||||||
|
<meta name="robots" content="index,follow">
|
||||||
|
<link rel="canonical" href='https://{$DomainModel->d_domain}{site:vciurl parent_category="$Request.route.strParentCategory" /}'>
|
||||||
<!-- 社交媒体标签 -->
|
<!-- 社交媒体标签 -->
|
||||||
<meta property="og:title" content='{site:replace code="VIDEO@GETCATEGORYINDEX@TITLE"}' />
|
<meta property="og:title" content='{site:replace code="VIDEO@GETCATEGORYINDEX@TITLE"}' />
|
||||||
<meta property="og:description" content='{site:replace code="VIDEO@GETCATEGORYINDEX@DESCRIPTION"}' />
|
<meta property="og:description" content='{site:replace code="VIDEO@GETCATEGORYINDEX@DESCRIPTION"}' />
|
||||||
<meta property="og:url" content="https://{$DomainModel->d_domain}" />
|
<meta property="og:url" content='https://{$DomainModel->d_domain}{site:vciurl parent_category="$Request.route.strParentCategory" /}' />
|
||||||
{switch $Request.route.strParentCategory }
|
{switch $Request.route.strParentCategory }
|
||||||
{case 'dian-ying' }
|
{case 'dian-ying' }
|
||||||
<meta property="og:type" content="video.movie" />
|
<meta property="og:type" content="video.movie" />
|
||||||
@@ -45,32 +47,13 @@
|
|||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "CollectionPage",
|
"@type": "CollectionPage",
|
||||||
"name": "{$DomainModel->d_name} - 最新{site:getval code="strVideoParentCategoryName" /}推荐",
|
"name": "{$DomainModel->d_name} - 最新{site:getval code="strVideoParentCategoryName" /}推荐",
|
||||||
"url": "https://{$DomainModel->d_domain}",
|
"url": "https://{$DomainModel->d_domain}{site:vciurl parent_category="$Request.route.strParentCategory" /}",
|
||||||
"description": "{site:replace code="VIDEO@GETCATEGORYINDEX@DESCRIPTION"}",
|
"description": "{site:replace code="VIDEO@GETCATEGORYINDEX@DESCRIPTION"}",
|
||||||
"potentialAction": {
|
"isPartOf": {
|
||||||
"@type": "SearchAction",
|
"@type": "WebSite",
|
||||||
"target": 'https://{$DomainModel->d_domain}{site:vsurl key="search_term_string" p="1"/}",
|
"name": "{$DomainModel->d_name}",
|
||||||
"query-input": "required name=search_term_string"
|
"url": "https://{$DomainModel->d_domain}"
|
||||||
}
|
}
|
||||||
"hasPart": [
|
|
||||||
{video:ranklist count="5"
|
|
||||||
v_parent_category_en="$Request.route.strParentCategory"
|
|
||||||
sort_type="weekly"
|
|
||||||
d_key="key" d_val="Video" cache_life="3600"}
|
|
||||||
|
|
||||||
{if $key < 5 }
|
|
||||||
{
|
|
||||||
"@type": "VideoObject",
|
|
||||||
"name": "{$Video.v_name}",
|
|
||||||
"description": "{$Video.v_description}",
|
|
||||||
"thumbnailUrl": '{$Video.v_pic}',
|
|
||||||
"uploadDate": "{:date('Y-m-d')}",
|
|
||||||
"url": 'https://{$DomainModel->d_domain}{site:vurl v_id="$Video.v_id" v_py="$Video.v_name_en"/}',
|
|
||||||
},
|
|
||||||
{/if}
|
|
||||||
{/video:ranklist}
|
|
||||||
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
@@ -89,7 +72,7 @@
|
|||||||
"item": "https://{$DomainModel->d_domain}{site:vciurl parent_category="$Request.route.strParentCategory" /}"
|
"item": "https://{$DomainModel->d_domain}{site:vciurl parent_category="$Request.route.strParentCategory" /}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -216,11 +216,11 @@ href='https://{$DomainModel->d_domain}{site:vurl v_id="$arrVideo.v_id" v_py="$ar
|
|||||||
<p class="bb52e4 data">
|
<p class="bb52e4 data">
|
||||||
<span class="ed25a2 text-muted">分类:</span>
|
<span class="ed25a2 text-muted">分类:</span>
|
||||||
<a
|
<a
|
||||||
href='{site:vsurl key="$arrVideo.v_parent_category" p="1"/}'>{$arrVideo.v_parent_category}</a>
|
href='{site:vciurl parent_category="$arrVideo.v_parent_category_en" /}'>{$arrVideo.v_parent_category}</a>
|
||||||
</p>
|
</p>
|
||||||
<p class="bb52e4 data">
|
<p class="bb52e4 data">
|
||||||
<span class="ed25a2 text-muted">类型:</span>
|
<span class="ed25a2 text-muted">类型:</span>
|
||||||
<a href='{site:vsurl key="$arrVideo.v_category" p="1"/}'>{$arrVideo.v_category}</a>
|
<a href='{site:vclurl parent_category="$arrVideo.v_parent_category_en" category="$arrVideo.v_category_en" area="all" lang="all" year="all" order="all" page="1"/}'>{$arrVideo.v_category}</a>
|
||||||
</p>
|
</p>
|
||||||
<p class="d994f6 data">
|
<p class="d994f6 data">
|
||||||
<span class="d8c8c2 text-muted">地区:</span>
|
<span class="d8c8c2 text-muted">地区:</span>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
<meta property="og:type" content="video.movie" />
|
<meta property="og:type" content="video.movie" />
|
||||||
{/switch}
|
{/switch}
|
||||||
<meta property="og:url"
|
<meta property="og:url"
|
||||||
content='https://{$DomainModel->d_domain}{site:vurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en"/}'>
|
content='https://{$DomainModel->d_domain}{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>
|
||||||
<meta property="og:image" content="{$arrVideo.v_pic}">
|
<meta property="og:image" content="{$arrVideo.v_pic}">
|
||||||
<meta property="og:site_name" content="{$DomainModel->d_name}">
|
<meta property="og:site_name" content="{$DomainModel->d_name}">
|
||||||
<meta property="og:video:type" content="video/m3u8">
|
<meta property="og:video:type" content="video/m3u8">
|
||||||
@@ -69,7 +69,7 @@ href='https://{$DomainModel->d_domain}{site:vpurl v_id="$arrVideo.v_id" v_py="$a
|
|||||||
"description": "{$arrVideo.v_description}",
|
"description": "{$arrVideo.v_description}",
|
||||||
"thumbnailUrl": "{$arrVideo.v_pic}",
|
"thumbnailUrl": "{$arrVideo.v_pic}",
|
||||||
"uploadDate": "{:date('Y-m-d')}",
|
"uploadDate": "{:date('Y-m-d')}",
|
||||||
"url": 'https://{$DomainModel->d_domain}{site:vurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en"/}',
|
"url": 'https://{$DomainModel->d_domain}{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}',
|
||||||
|
|
||||||
"inLanguage": [
|
"inLanguage": [
|
||||||
{volist name='$arrVideo.v_lang' id='strVideoLang' key='index'}
|
{volist name='$arrVideo.v_lang' id='strVideoLang' key='index'}
|
||||||
|
|||||||
@@ -1,12 +1,98 @@
|
|||||||
{extend name="base" /}
|
{extend name="base" /}
|
||||||
|
|
||||||
{block name="get-data"}
|
{block name="get-data"}
|
||||||
|
{video:seocopy scene="home" export_name="seoCopy" /}
|
||||||
|
|
||||||
|
{php}
|
||||||
|
$strHomeIntroTitle = trim((string)($DomainModel->d_name ?? ''));
|
||||||
|
if ($strHomeIntroTitle === '') {
|
||||||
|
$strHomeIntroTitle = '影视内容推荐';
|
||||||
|
}
|
||||||
|
|
||||||
|
$strHomeIntroText = trim((string)($seoCopy['intro_text'] ?? $seoCopy['category_intro'] ?? ''));
|
||||||
|
if ($strHomeIntroText === '') {
|
||||||
|
$strHomeIntroText = trim((string)($seoCopy['description'] ?? ''));
|
||||||
|
}
|
||||||
|
if ($strHomeIntroText === '') {
|
||||||
|
$strHomeIntroText = $strHomeIntroTitle . '首页整理了电影、剧集、综艺、动漫等常见入口,适合先看首屏推荐,再继续按分类、榜单和搜索缩小范围。';
|
||||||
|
}
|
||||||
|
|
||||||
|
$strHomeIntroMeta = trim((string)($seoCopy['intro_meta'] ?? $seoCopy['faq_content'] ?? ''));
|
||||||
|
if ($strHomeIntroMeta === '') {
|
||||||
|
$strHomeIntroMeta = '建议先看首屏推荐,再根据题材、热度和搜索词继续深入浏览。';
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrHomeCategoryNames = array_values(array_filter(array_map(static function ($arrCategory): string {
|
||||||
|
return trim((string)($arrCategory['name'] ?? ''));
|
||||||
|
}, (array)($TpStyle['template_cfg']['pages']['home']['categories'] ?? [])), static function (string $strName): bool {
|
||||||
|
return $strName !== '';
|
||||||
|
}));
|
||||||
|
$strHomeCategorySummary = implode('、', array_slice($arrHomeCategoryNames, 0, 3));
|
||||||
|
if ($strHomeCategorySummary === '') {
|
||||||
|
$strHomeCategorySummary = '电影、剧集、综艺';
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrHomeSlotKeys = array_values(array_filter(array_map(static function ($arrSlot): string {
|
||||||
|
return trim((string)($arrSlot['layout_key'] ?? ''));
|
||||||
|
}, (array)($TpStyle['template_cfg']['pages']['home']['slots'] ?? [])), static function (string $strKey): bool {
|
||||||
|
return $strKey !== '';
|
||||||
|
}));
|
||||||
|
$strHomePrimarySlot = $arrHomeSlotKeys[0] ?? 'news';
|
||||||
|
$arrHomePrimarySlotLabelMap = [
|
||||||
|
'news' => '更新区',
|
||||||
|
'rank' => '榜单区',
|
||||||
|
'tuijian' => '推荐区',
|
||||||
|
'update' => '更新区',
|
||||||
|
'piaofang' => '热度区',
|
||||||
|
'lunli' => '内容区',
|
||||||
|
];
|
||||||
|
$strHomePrimarySlotLabel = $arrHomePrimarySlotLabelMap[$strHomePrimarySlot] ?? '推荐区';
|
||||||
|
|
||||||
|
$intHomeTopSlotCount = count((array)($TpStyle['template_cfg']['pages']['home']['slots'] ?? []));
|
||||||
|
$intHomeCategoryCount = count((array)($TpStyle['template_cfg']['pages']['home']['categories'] ?? []));
|
||||||
|
|
||||||
|
$arrHomeQuickSignals = [
|
||||||
|
['label' => '首屏主区', 'text' => '先看' . $strHomePrimarySlotLabel],
|
||||||
|
['label' => '频道方向', 'text' => $strHomeCategorySummary],
|
||||||
|
['label' => '内容覆盖', 'text' => '当前约有' . (1 + $intHomeTopSlotCount + $intHomeCategoryCount) . '组入口区块'],
|
||||||
|
['label' => '浏览节奏', 'text' => '首屏吸引 -> 详情判断 -> 播放选择'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$arrHomeGuideCards = array_values(array_filter((array)($seoCopy['guide_cards'] ?? []), static function ($arrCard): bool {
|
||||||
|
return is_array($arrCard)
|
||||||
|
&& (
|
||||||
|
trim((string)($arrCard['title'] ?? '')) !== ''
|
||||||
|
|| trim((string)($arrCard['text'] ?? '')) !== ''
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (empty($arrHomeGuideCards)) {
|
||||||
|
$arrHomeGuideCards = [
|
||||||
|
[
|
||||||
|
'title' => '首屏入口',
|
||||||
|
'text' => '首页第一屏更适合先看' . $strHomePrimarySlotLabel . '和刚刚更新,快速判断当前最值得点开的内容。',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => '内容范围',
|
||||||
|
'text' => '当前首页已经把' . $strHomeCategorySummary . '等入口串起来,适合横向切换不同内容类型。',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => '搜索建议',
|
||||||
|
'text' => '如果你已经知道片名、演员或题材词,直接用搜索通常会比翻列表更快。',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => '后续路径',
|
||||||
|
'text' => '找到感兴趣的内容后,建议先看详情页确认题材和线路,再决定是否直接进入播放页。',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
{/php}
|
||||||
|
|
||||||
{/block}
|
{/block}
|
||||||
|
|
||||||
{block name="title"}{site:replace code="VIDEO@INDEX@INDEX@TITLE"}{/block}
|
{block name="title"}{site:seotkd code="title" page="home" /}{/block}
|
||||||
{block name="keywords"}{site:replace code="VIDEO@INDEX@INDEX@KEYWORDS"}{/block}
|
{block name="keywords"}{site:seotkd code="keywords" page="home" /}{/block}
|
||||||
{block name="description"}{site:replace code="VIDEO@INDEX@INDEX@DESCRIPTION"}{/block}
|
{block name="description"}{site:seotkd code="description" page="home" /}{/block}
|
||||||
|
|
||||||
{block name="head"}
|
{block name="head"}
|
||||||
<meta name="robots" content="index,follow">
|
<meta name="robots" content="index,follow">
|
||||||
@@ -14,8 +100,8 @@
|
|||||||
|
|
||||||
|
|
||||||
{// 社交媒体标签 }
|
{// 社交媒体标签 }
|
||||||
<meta property="og:title" content='{site:replace code="VIDEO@INDEX@INDEX@TITLE"}' />
|
<meta property="og:title" content='{site:seotkd code="title" page="home" /}' />
|
||||||
<meta property="og:description" content='{site:replace code="VIDEO@INDEX@INDEX@DESCRIPTION"}' />
|
<meta property="og:description" content='{site:seotkd code="description" page="home" /}' />
|
||||||
<meta property="og:url" content="https://{$DomainModel->d_domain}" />
|
<meta property="og:url" content="https://{$DomainModel->d_domain}" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
{// 结构化数据 }
|
{// 结构化数据 }
|
||||||
@@ -26,7 +112,7 @@
|
|||||||
"name": "{$DomainModel->d_name}",
|
"name": "{$DomainModel->d_name}",
|
||||||
"url": "https://{$DomainModel->d_domain}",
|
"url": "https://{$DomainModel->d_domain}",
|
||||||
"alternateName": "{$DomainModel->d_domain}",
|
"alternateName": "{$DomainModel->d_domain}",
|
||||||
"description": "{site:replace code="VIDEO@INDEX@INDEX@DESCRIPTION"}",
|
"description": "{site:seotkd code='description' page='home' /}",
|
||||||
"potentialAction": {
|
"potentialAction": {
|
||||||
"@type": "SearchAction",
|
"@type": "SearchAction",
|
||||||
"target": 'https://{$DomainModel->d_domain}{site:vsurl key="search_term_string" p="1"/}',
|
"target": 'https://{$DomainModel->d_domain}{site:vsurl key="search_term_string" p="1"/}',
|
||||||
@@ -42,6 +128,25 @@
|
|||||||
{block name="main"}
|
{block name="main"}
|
||||||
<main class="page-home" style="max-width:{$TpStyle.template_cfg.global.page_max_width_pc}px;margin:0 auto;">
|
<main class="page-home" style="max-width:{$TpStyle.template_cfg.global.page_max_width_pc}px;margin:0 auto;">
|
||||||
|
|
||||||
|
<section class="{$TpStyle.dom_prefix}-home-intro" style="margin:0 0 18px;padding:20px 22px;border:1px solid var(--border-color);border-radius:24px;background:linear-gradient(135deg,rgba(255,255,255,.98),var(--bg-soft-color));box-shadow:0 12px 28px rgba(0,0,0,.04);">
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:10px;">
|
||||||
|
<span style="display:inline-flex;width:28px;height:3px;border-radius:999px;background:linear-gradient(90deg,var(--color-primary),var(--color-secondary));"></span>
|
||||||
|
<span style="display:inline-flex;align-items:center;padding:4px 10px;border-radius:999px;background:rgba(0,0,0,.04);color:var(--text-muted-color);font-size:12px;line-height:1.5;">首页导览</span>
|
||||||
|
</div>
|
||||||
|
<h1 style="margin:0 0 10px;color:var(--text-color);font-size:24px;line-height:1.35;">{$strHomeIntroTitle}</h1>
|
||||||
|
<p style="margin:0 0 10px;color:var(--text-color);font-size:15px;line-height:1.9;">{$strHomeIntroText}</p>
|
||||||
|
<p style="margin:0;color:var(--text-muted-color);font-size:13px;line-height:1.8;">{$strHomeIntroMeta}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="{$TpStyle.dom_prefix}-home-quick-signals" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;margin:0 0 18px;">
|
||||||
|
{foreach $arrHomeQuickSignals as $arrSignal}
|
||||||
|
<article style="padding:12px 14px;border-radius:16px;background:rgba(255,255,255,.92);border:1px solid rgba(0,0,0,.06);box-shadow:0 8px 18px rgba(0,0,0,.03);">
|
||||||
|
<strong style="display:block;margin:0 0 6px;color:var(--text-color);font-size:12px;line-height:1.5;">{$arrSignal.label}</strong>
|
||||||
|
<span style="display:block;color:var(--text-muted-color);font-size:13px;line-height:1.7;">{$arrSignal.text}</span>
|
||||||
|
</article>
|
||||||
|
{/foreach}
|
||||||
|
</section>
|
||||||
|
|
||||||
{// lunli }
|
{// lunli }
|
||||||
{assign name="Slot" value="$TpStyle.template_cfg.pages.home.lunliSlots[0]"}
|
{assign name="Slot" value="$TpStyle.template_cfg.pages.home.lunliSlots[0]"}
|
||||||
{assign name="title" value="$Slot.title_text"}
|
{assign name="title" value="$Slot.title_text"}
|
||||||
@@ -74,6 +179,8 @@
|
|||||||
|
|
||||||
{/foreach}
|
{/foreach}
|
||||||
|
|
||||||
|
{include file="module/seo_copy/collection" /}
|
||||||
|
|
||||||
{// 分类}
|
{// 分类}
|
||||||
{foreach $TpStyle.template_cfg.pages.home.categories as $HomeCategory}
|
{foreach $TpStyle.template_cfg.pages.home.categories as $HomeCategory}
|
||||||
|
|
||||||
@@ -105,4 +212,3 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/block}
|
{/block}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
<article class="{$TpStyle.dom_prefix}-d1-wrap">
|
<article class="{$TpStyle.dom_prefix}-d1-wrap">
|
||||||
|
{php}
|
||||||
|
$strDetailPlaySlug = trim((string)($arrVideo['v_name_en'] ?? ''));
|
||||||
|
if ($strDetailPlaySlug === '') {
|
||||||
|
$strDetailPlaySlug = (string)($arrVideo['v_id'] ?? '');
|
||||||
|
}
|
||||||
|
{/php}
|
||||||
|
|
||||||
{// 顶部基本信息 }
|
{// 顶部基本信息 }
|
||||||
<header class="{$TpStyle.dom_prefix}-d1-hd">
|
<header class="{$TpStyle.dom_prefix}-d1-hd">
|
||||||
@@ -42,14 +48,17 @@
|
|||||||
<?php $keyLine = 0; ?>
|
<?php $keyLine = 0; ?>
|
||||||
{volist name='$arrPlayUrlType' id='arrPlayUrl'}
|
{volist name='$arrPlayUrlType' id='arrPlayUrl'}
|
||||||
<?php $keyLine++ ;?>
|
<?php $keyLine++ ;?>
|
||||||
|
{php}
|
||||||
|
$strEpisodePlayUrl = app(\app\services\VideoService::class)->getVideoPlayUrl(
|
||||||
|
(int)($arrVideo['v_id'] ?? 0),
|
||||||
|
$strDetailPlaySlug,
|
||||||
|
(string)$strPlayGroupName,
|
||||||
|
(int)$keyLine
|
||||||
|
);
|
||||||
|
{/php}
|
||||||
<li>
|
<li>
|
||||||
<a class="{if $keyLine == $Request.route.intPlayIndex}active{/if}"
|
<a class="{if $keyLine == $Request.route.intPlayIndex}active{/if}"
|
||||||
href='{site:vpurl
|
href="{$strEpisodePlayUrl}"
|
||||||
v_id="$arrVideo.v_id"
|
|
||||||
v_py="$arrVideo.v_name_en"
|
|
||||||
play_type="$strPlayGroupName"
|
|
||||||
play_index="$keyLine"
|
|
||||||
/}'
|
|
||||||
m3u8="{$arrPlayUrl.url}">
|
m3u8="{$arrPlayUrl.url}">
|
||||||
{$arrPlayUrl.name}
|
{$arrPlayUrl.name}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
<main class="{$TpStyle.dom_prefix}-d2-page">
|
<main class="{$TpStyle.dom_prefix}-d2-page">
|
||||||
|
{php}
|
||||||
|
$strDetailPlaySlug = trim((string)($arrVideo['v_name_en'] ?? ''));
|
||||||
|
if ($strDetailPlaySlug === '') {
|
||||||
|
$strDetailPlaySlug = (string)($arrVideo['v_id'] ?? '');
|
||||||
|
}
|
||||||
|
{/php}
|
||||||
|
|
||||||
{// 顶部大横图 }
|
{// 顶部大横图 }
|
||||||
<section class="{$TpStyle.dom_prefix}-d2-hero">
|
<section class="{$TpStyle.dom_prefix}-d2-hero">
|
||||||
@@ -45,8 +51,16 @@
|
|||||||
<?php $keyLine = 0; ?>
|
<?php $keyLine = 0; ?>
|
||||||
{volist name='$arrPlayUrlType' id='arrPlayUrl'}
|
{volist name='$arrPlayUrlType' id='arrPlayUrl'}
|
||||||
<?php $keyLine++ ;?>
|
<?php $keyLine++ ;?>
|
||||||
|
{php}
|
||||||
|
$strEpisodePlayUrl = app(\app\services\VideoService::class)->getVideoPlayUrl(
|
||||||
|
(int)($arrVideo['v_id'] ?? 0),
|
||||||
|
$strDetailPlaySlug,
|
||||||
|
(string)$strPlayGroupName,
|
||||||
|
(int)$keyLine
|
||||||
|
);
|
||||||
|
{/php}
|
||||||
<li>
|
<li>
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="$strPlayGroupName" play_index="$keyLine" /}'
|
<a href="{$strEpisodePlayUrl}"
|
||||||
class="{if $keyLine == $Request.route.intPlayIndex}active{/if}"
|
class="{if $keyLine == $Request.route.intPlayIndex}active{/if}"
|
||||||
m3u8="{$arrPlayUrl.url}">
|
m3u8="{$arrPlayUrl.url}">
|
||||||
{$arrPlayUrl.name}
|
{$arrPlayUrl.name}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
<article class="{$TpStyle.dom_prefix}-d3-page">
|
<article class="{$TpStyle.dom_prefix}-d3-page">
|
||||||
|
{php}
|
||||||
|
$strDetailPlaySlug = trim((string)($arrVideo['v_name_en'] ?? ''));
|
||||||
|
if ($strDetailPlaySlug === '') {
|
||||||
|
$strDetailPlaySlug = (string)($arrVideo['v_id'] ?? '');
|
||||||
|
}
|
||||||
|
{/php}
|
||||||
|
|
||||||
<section class="{$TpStyle.dom_prefix}-d3-top">
|
<section class="{$TpStyle.dom_prefix}-d3-top">
|
||||||
<div class="{$TpStyle.dom_prefix}-d3-picbox">
|
<div class="{$TpStyle.dom_prefix}-d3-picbox">
|
||||||
@@ -32,12 +38,20 @@
|
|||||||
<?php $keyLineGroup = 0; ?>
|
<?php $keyLineGroup = 0; ?>
|
||||||
{foreach $arrVideo.v_play_url as $strPlayGroupName=>$arrPlayUrlType }
|
{foreach $arrVideo.v_play_url as $strPlayGroupName=>$arrPlayUrlType }
|
||||||
<?php $keyLineGroup++ ;?>
|
<?php $keyLineGroup++ ;?>
|
||||||
<div class="{$TpStyle.dom_prefix}-d3-epbox {eq name='$keyLineGroup' value='1'}active{/eq}"
|
<div class="{$TpStyle.dom_prefix}-d3-epbox {eq name='$keyLineGroup' value='1'}active{/eq}"
|
||||||
id="d3_box_{$strPlayGroupName}">
|
id="d3_box_{$strPlayGroupName}">
|
||||||
<?php $keyLine = 0; ?>
|
<?php $keyLine = 0; ?>
|
||||||
{volist name='$arrPlayUrlType' id='arrPlayUrl'}
|
{volist name='$arrPlayUrlType' id='arrPlayUrl'}
|
||||||
<?php $keyLine++ ;?>
|
<?php $keyLine++ ;?>
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="$strPlayGroupName" play_index="$keyLine" /}'
|
{php}
|
||||||
|
$strEpisodePlayUrl = app(\app\services\VideoService::class)->getVideoPlayUrl(
|
||||||
|
(int)($arrVideo['v_id'] ?? 0),
|
||||||
|
$strDetailPlaySlug,
|
||||||
|
(string)$strPlayGroupName,
|
||||||
|
(int)$keyLine
|
||||||
|
);
|
||||||
|
{/php}
|
||||||
|
<a href="{$strEpisodePlayUrl}"
|
||||||
class="{$TpStyle.dom_prefix}-d3-epitem {if $keyLine == $Request.route.intPlayIndex}active{/if}"
|
class="{$TpStyle.dom_prefix}-d3-epitem {if $keyLine == $Request.route.intPlayIndex}active{/if}"
|
||||||
m3u8="{$arrPlayUrl.url}">
|
m3u8="{$arrPlayUrl.url}">
|
||||||
{$arrPlayUrl.name}
|
{$arrPlayUrl.name}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
<section class="{$TpStyle.dom_prefix}-d4-page">
|
<section class="{$TpStyle.dom_prefix}-d4-page">
|
||||||
|
{php}
|
||||||
|
$strDetailPlaySlug = trim((string)($arrVideo['v_name_en'] ?? ''));
|
||||||
|
if ($strDetailPlaySlug === '') {
|
||||||
|
$strDetailPlaySlug = (string)($arrVideo['v_id'] ?? '');
|
||||||
|
}
|
||||||
|
{/php}
|
||||||
|
|
||||||
<header class="{$TpStyle.dom_prefix}-d4-head">
|
<header class="{$TpStyle.dom_prefix}-d4-head">
|
||||||
<h1>{$arrVideo.v_name}</h1>
|
<h1>{$arrVideo.v_name}</h1>
|
||||||
@@ -42,8 +48,16 @@
|
|||||||
<?php $keyLine = 0; ?>
|
<?php $keyLine = 0; ?>
|
||||||
{volist name='$arrPlayUrlType' id='arrPlayUrl'}
|
{volist name='$arrPlayUrlType' id='arrPlayUrl'}
|
||||||
<?php $keyLine++ ;?>
|
<?php $keyLine++ ;?>
|
||||||
|
{php}
|
||||||
|
$strEpisodePlayUrl = app(\app\services\VideoService::class)->getVideoPlayUrl(
|
||||||
|
(int)($arrVideo['v_id'] ?? 0),
|
||||||
|
$strDetailPlaySlug,
|
||||||
|
(string)$strPlayGroupName,
|
||||||
|
(int)$keyLine
|
||||||
|
);
|
||||||
|
{/php}
|
||||||
<li>
|
<li>
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="$strPlayGroupName" play_index="$keyLine" /}'
|
<a href="{$strEpisodePlayUrl}"
|
||||||
class="{if $keyLine == $Request.route.intPlayIndex}active{/if}"
|
class="{if $keyLine == $Request.route.intPlayIndex}active{/if}"
|
||||||
m3u8="{$arrPlayUrl.url}">
|
m3u8="{$arrPlayUrl.url}">
|
||||||
{$arrPlayUrl.name}
|
{$arrPlayUrl.name}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
<article class="{$TpStyle.dom_prefix}-d5-page">
|
<article class="{$TpStyle.dom_prefix}-d5-page">
|
||||||
|
{php}
|
||||||
|
$strDetailPlaySlug = trim((string)($arrVideo['v_name_en'] ?? ''));
|
||||||
|
if ($strDetailPlaySlug === '') {
|
||||||
|
$strDetailPlaySlug = (string)($arrVideo['v_id'] ?? '');
|
||||||
|
}
|
||||||
|
{/php}
|
||||||
|
|
||||||
<header class="{$TpStyle.dom_prefix}-d5-head">
|
<header class="{$TpStyle.dom_prefix}-d5-head">
|
||||||
<h1>{$arrVideo.v_name}</h1>
|
<h1>{$arrVideo.v_name}</h1>
|
||||||
@@ -35,7 +41,15 @@
|
|||||||
<?php $keyLine = 0; ?>
|
<?php $keyLine = 0; ?>
|
||||||
{volist name='$arrPlayUrlType' id='arrPlayUrl'}
|
{volist name='$arrPlayUrlType' id='arrPlayUrl'}
|
||||||
<?php $keyLine++ ;?>
|
<?php $keyLine++ ;?>
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="$strPlayGroupName" play_index="$keyLine" /}'
|
{php}
|
||||||
|
$strEpisodePlayUrl = app(\app\services\VideoService::class)->getVideoPlayUrl(
|
||||||
|
(int)($arrVideo['v_id'] ?? 0),
|
||||||
|
$strDetailPlaySlug,
|
||||||
|
(string)$strPlayGroupName,
|
||||||
|
(int)$keyLine
|
||||||
|
);
|
||||||
|
{/php}
|
||||||
|
<a href="{$strEpisodePlayUrl}"
|
||||||
class="{$TpStyle.dom_prefix}-d5-epbtn {if $keyLine == $Request.route.intPlayIndex}active{/if}"
|
class="{$TpStyle.dom_prefix}-d5-epbtn {if $keyLine == $Request.route.intPlayIndex}active{/if}"
|
||||||
m3u8="{$arrPlayUrl.url}">
|
m3u8="{$arrPlayUrl.url}">
|
||||||
{$arrPlayUrl.name}
|
{$arrPlayUrl.name}
|
||||||
|
|||||||
@@ -1,111 +1,123 @@
|
|||||||
{// ===================== Action Variants ===================== }
|
{// ===================== Action Variants ===================== }
|
||||||
|
{php}
|
||||||
|
$strDetailActionSlug = trim((string)($arrVideo['v_name_en'] ?? request()->route('strPinyin') ?? ''));
|
||||||
|
if ($strDetailActionSlug === '') {
|
||||||
|
$strDetailActionSlug = (string)($arrVideo['v_id'] ?? '');
|
||||||
|
}
|
||||||
|
$strDetailDefaultPlayUrl = app(\app\services\VideoService::class)->getVideoPlayUrl(
|
||||||
|
(int)($arrVideo['v_id'] ?? 0),
|
||||||
|
$strDetailActionSlug,
|
||||||
|
'default',
|
||||||
|
1
|
||||||
|
);
|
||||||
|
{/php}
|
||||||
|
|
||||||
{if $variant == 0}
|
{if $variant == 0}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-action v0">
|
<div class="{$TpStyle.dom_prefix}-dm-action v0">
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>立即播放</a>
|
<a href="{$strDetailDefaultPlayUrl}">立即播放</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 1}
|
{elseif $variant == 1}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-action v1">
|
<div class="{$TpStyle.dom_prefix}-dm-action v1">
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>在线播放</a>
|
<a href="{$strDetailDefaultPlayUrl}">在线播放</a>
|
||||||
<span>无需安装</span>
|
<span>无需安装</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 2}
|
{elseif $variant == 2}
|
||||||
<nav class="{$TpStyle.dom_prefix}-dm-action v2">
|
<nav class="{$TpStyle.dom_prefix}-dm-action v2">
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>▶ 播放</a>
|
<a href="{$strDetailDefaultPlayUrl}">▶ 播放</a>
|
||||||
<a href="#desc">剧情</a>
|
<a href="#desc">剧情</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{elseif $variant == 3}
|
{elseif $variant == 3}
|
||||||
<ul class="{$TpStyle.dom_prefix}-dm-action v3">
|
<ul class="{$TpStyle.dom_prefix}-dm-action v3">
|
||||||
<li><a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>高清</a></li>
|
<li><a href="{$strDetailDefaultPlayUrl}">高清</a></li>
|
||||||
<li><a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>免费</a></li>
|
<li><a href="{$strDetailDefaultPlayUrl}">免费</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{elseif $variant == 4}
|
{elseif $variant == 4}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-action v4">
|
<div class="{$TpStyle.dom_prefix}-dm-action v4">
|
||||||
<button onclick="location.href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'">立即观看</button>
|
<button onclick="location.href='{$strDetailDefaultPlayUrl}'">立即观看</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 5}
|
{elseif $variant == 5}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-action v5">
|
<div class="{$TpStyle.dom_prefix}-dm-action v5">
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>{$arrVideo.v_name} 在线观看</a>
|
<a href="{$strDetailDefaultPlayUrl}">{$arrVideo.v_name} 在线观看</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 6}
|
{elseif $variant == 6}
|
||||||
<footer class="{$TpStyle.dom_prefix}-dm-action v6">
|
<footer class="{$TpStyle.dom_prefix}-dm-action v6">
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>开始播放</a>
|
<a href="{$strDetailDefaultPlayUrl}">开始播放</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
{elseif $variant == 7}
|
{elseif $variant == 7}
|
||||||
<section class="{$TpStyle.dom_prefix}-dm-action v7">
|
<section class="{$TpStyle.dom_prefix}-dm-action v7">
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>免费观看</a>
|
<a href="{$strDetailDefaultPlayUrl}">免费观看</a>
|
||||||
<small>高清资源</small>
|
<small>高清资源</small>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{elseif $variant == 8}
|
{elseif $variant == 8}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-action v8">
|
<div class="{$TpStyle.dom_prefix}-dm-action v8">
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>播放正片</a>
|
<a href="{$strDetailDefaultPlayUrl}">播放正片</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 9}
|
{elseif $variant == 9}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-action v9">
|
<div class="{$TpStyle.dom_prefix}-dm-action v9">
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>立即观看 {$arrVideo.v_name}</a>
|
<a href="{$strDetailDefaultPlayUrl}">立即观看 {$arrVideo.v_name}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 10}
|
{elseif $variant == 10}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-action v10">
|
<div class="{$TpStyle.dom_prefix}-dm-action v10">
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>在线播放 · 高清</a>
|
<a href="{$strDetailDefaultPlayUrl}">在线播放 · 高清</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 11}
|
{elseif $variant == 11}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-action v11">
|
<div class="{$TpStyle.dom_prefix}-dm-action v11">
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>点击播放</a>
|
<a href="{$strDetailDefaultPlayUrl}">点击播放</a>
|
||||||
<span>支持多线路</span>
|
<span>支持多线路</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 12}
|
{elseif $variant == 12}
|
||||||
<nav class="{$TpStyle.dom_prefix}-dm-action v12">
|
<nav class="{$TpStyle.dom_prefix}-dm-action v12">
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>播放</a>
|
<a href="{$strDetailDefaultPlayUrl}">播放</a>
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>下载</a>
|
<a href="{$strDetailDefaultPlayUrl}">下载</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{elseif $variant == 13}
|
{elseif $variant == 13}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-action v13">
|
<div class="{$TpStyle.dom_prefix}-dm-action v13">
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>▶</a>
|
<a href="{$strDetailDefaultPlayUrl}">▶</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 14}
|
{elseif $variant == 14}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-action v14">
|
<div class="{$TpStyle.dom_prefix}-dm-action v14">
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>{$arrVideo.v_name} 免费看</a>
|
<a href="{$strDetailDefaultPlayUrl}">{$arrVideo.v_name} 免费看</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 15}
|
{elseif $variant == 15}
|
||||||
<aside class="{$TpStyle.dom_prefix}-dm-action v15">
|
<aside class="{$TpStyle.dom_prefix}-dm-action v15">
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>立即播放</a>
|
<a href="{$strDetailDefaultPlayUrl}">立即播放</a>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{elseif $variant == 16}
|
{elseif $variant == 16}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-action v16"
|
<div class="{$TpStyle.dom_prefix}-dm-action v16"
|
||||||
data-action="play">
|
data-action="play">
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>播放影片</a>
|
<a href="{$strDetailDefaultPlayUrl}">播放影片</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 17}
|
{elseif $variant == 17}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-action v17">
|
<div class="{$TpStyle.dom_prefix}-dm-action v17">
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>高清播放</a>
|
<a href="{$strDetailDefaultPlayUrl}">高清播放</a>
|
||||||
<em>无需登录</em>
|
<em>无需登录</em>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 18}
|
{elseif $variant == 18}
|
||||||
<section class="{$TpStyle.dom_prefix}-dm-action v18">
|
<section class="{$TpStyle.dom_prefix}-dm-action v18">
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>在线观看</a>
|
<a href="{$strDetailDefaultPlayUrl}">在线观看</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{elseif $variant == 19}
|
{elseif $variant == 19}
|
||||||
<footer class="{$TpStyle.dom_prefix}-dm-action v19">
|
<footer class="{$TpStyle.dom_prefix}-dm-action v19">
|
||||||
<a href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>马上播放</a>
|
<a href="{$strDetailDefaultPlayUrl}">马上播放</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
{// ===================== Cover Variants ===================== }
|
{// ===================== Cover Variants ===================== }
|
||||||
{video:imgalt video="$arrVideo" slot="detail_cover" item_type="poster" export_name="strAlt" /}
|
{video:imgalt video="$arrVideo" slot="detail_cover" item_type="poster" export_name="strAlt" /}
|
||||||
|
{php}
|
||||||
|
$strDetailCoverSlug = trim((string)($arrVideo['v_name_en'] ?? request()->route('strPinyin') ?? ''));
|
||||||
|
if ($strDetailCoverSlug === '') {
|
||||||
|
$strDetailCoverSlug = (string)($arrVideo['v_id'] ?? '');
|
||||||
|
}
|
||||||
|
$strDetailCoverPlayUrl = app(\app\services\VideoService::class)->getVideoPlayUrl(
|
||||||
|
(int)($arrVideo['v_id'] ?? 0),
|
||||||
|
$strDetailCoverSlug,
|
||||||
|
'default',
|
||||||
|
1
|
||||||
|
);
|
||||||
|
{/php}
|
||||||
|
|
||||||
{if $variant == 0}
|
{if $variant == 0}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-cover v0">
|
<div class="{$TpStyle.dom_prefix}-dm-cover v0">
|
||||||
@@ -20,7 +32,7 @@
|
|||||||
|
|
||||||
{elseif $variant == 2}
|
{elseif $variant == 2}
|
||||||
<a class="{$TpStyle.dom_prefix}-dm-cover v2"
|
<a class="{$TpStyle.dom_prefix}-dm-cover v2"
|
||||||
href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>
|
href="{$strDetailCoverPlayUrl}">
|
||||||
<img class="{$TpStyle.dom_prefix}-dm-cover-img"
|
<img class="{$TpStyle.dom_prefix}-dm-cover-img"
|
||||||
src="{site:cfg code='PUBLIC_COVER_DOMAIN' encode='false'/}{$arrVideo.v_pic}"
|
src="{site:cfg code='PUBLIC_COVER_DOMAIN' encode='false'/}{$arrVideo.v_pic}"
|
||||||
alt="{$strAlt}">
|
alt="{$strAlt}">
|
||||||
@@ -89,7 +101,7 @@
|
|||||||
|
|
||||||
{elseif $variant == 11}
|
{elseif $variant == 11}
|
||||||
<a class="{$TpStyle.dom_prefix}-dm-cover v11"
|
<a class="{$TpStyle.dom_prefix}-dm-cover v11"
|
||||||
href='{site:vpurl v_id="$arrVideo.v_id" v_py="$arrVideo.v_name_en" play_type="default" play_index="1" /}'>
|
href="{$strDetailCoverPlayUrl}">
|
||||||
<img class="{$TpStyle.dom_prefix}-dm-cover-bg" alt="{$strAlt}"
|
<img class="{$TpStyle.dom_prefix}-dm-cover-bg" alt="{$strAlt}"
|
||||||
src="{site:cfg code='PUBLIC_COVER_DOMAIN' encode='false'/}{$arrVideo.v_pic}">
|
src="{site:cfg code='PUBLIC_COVER_DOMAIN' encode='false'/}{$arrVideo.v_pic}">
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,80 +1,207 @@
|
|||||||
{// ===================== Desc Variants ===================== }
|
{// ===================== Desc Variants ===================== }
|
||||||
|
|
||||||
|
{php}
|
||||||
|
$intCurrentForgeId = (int)(request()->route('intVForgeId') ?? 0);
|
||||||
|
$strSeoCopyScene = $intCurrentForgeId > 0 ? 'forge' : 'detail';
|
||||||
|
{/php}
|
||||||
|
|
||||||
|
{video:seocopy scene="$strSeoCopyScene" video="$arrVideo" export_name="seoCopy" /}
|
||||||
|
|
||||||
|
{php}
|
||||||
|
$strDetailMainDescription = trim(preg_replace('/\s+/u', ' ', strip_tags((string)($arrVideo['v_description'] ?? ''))));
|
||||||
|
if ($strDetailMainDescription === '') {
|
||||||
|
$strDetailMainDescription = '当前内容暂无完整剧情简介,建议结合详情信息与播放入口继续判断。';
|
||||||
|
}
|
||||||
|
|
||||||
|
$arrPlayGroups = is_array($arrVideo['v_play_url'] ?? null) ? $arrVideo['v_play_url'] : [];
|
||||||
|
$strPrimaryPlayType = '';
|
||||||
|
$intPrimaryPlayIndex = 1;
|
||||||
|
if (!empty($arrPlayGroups)) {
|
||||||
|
$strPrimaryPlayType = (string)array_key_first($arrPlayGroups);
|
||||||
|
}
|
||||||
|
|
||||||
|
$strDetailSlug = trim((string)($arrVideo['v_name_en'] ?? request()->route('strPinyin') ?? ''));
|
||||||
|
if ($strDetailSlug === '') {
|
||||||
|
$strDetailSlug = (string)($arrVideo['v_id'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
$strPreferredPlayUrl = '';
|
||||||
|
if ((int)($arrVideo['v_id'] ?? 0) > 0 && $strPrimaryPlayType !== '') {
|
||||||
|
$strPreferredPlayUrl = app(\app\services\VideoService::class)->getVideoPlayUrl(
|
||||||
|
(int)$arrVideo['v_id'],
|
||||||
|
$strDetailSlug,
|
||||||
|
$strPrimaryPlayType,
|
||||||
|
$intPrimaryPlayIndex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$boolIsForgeDetail = $intCurrentForgeId > 0;
|
||||||
|
$strOriginVideoName = trim((string)($arrVideo['v_name'] ?? ''));
|
||||||
|
$strMainVideoInfoUrl = '';
|
||||||
|
if ((int)($arrVideo['v_id'] ?? 0) > 0) {
|
||||||
|
$strMainVideoInfoUrl = app(\app\services\VideoService::class)->getVideoInfoUrl(
|
||||||
|
(int)$arrVideo['v_id'],
|
||||||
|
$strDetailSlug
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$strDetailBody = trim((string)($seoCopy['detail_faq'] ?? ''));
|
||||||
|
$strDetailBodyLead = trim((string)($seoCopy['detail_body_lead'] ?? ''));
|
||||||
|
$strDetailPlayLinkLead = trim((string)($seoCopy['detail_play_link_lead'] ?? ''));
|
||||||
|
$strDetailBodyTail = trim((string)($seoCopy['detail_body_tail'] ?? ''));
|
||||||
|
$strForgeBodyLead = trim((string)($seoCopy['forge_body_lead'] ?? ''));
|
||||||
|
$strForgeReturnLead = trim((string)($seoCopy['forge_return_lead'] ?? ''));
|
||||||
|
if ($strForgeReturnLead === '') {
|
||||||
|
$strForgeReturnLead = '如果想回到标准详情,可返回';
|
||||||
|
}
|
||||||
|
$strForgePlayLead = trim((string)($seoCopy['forge_play_lead'] ?? ''));
|
||||||
|
$strForgeDetailNote = trim((string)($seoCopy['forge_detail_note'] ?? ''));
|
||||||
|
|
||||||
|
$arrGuideCards = array_values(array_filter((array)($seoCopy['guide_cards'] ?? []), static function ($arrCard): bool {
|
||||||
|
return is_array($arrCard)
|
||||||
|
&& (
|
||||||
|
trim((string)($arrCard['title'] ?? '')) !== ''
|
||||||
|
|| trim((string)($arrCard['text'] ?? '')) !== ''
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
|
||||||
|
$arrForgeEntryLinks = [];
|
||||||
|
foreach ($arrGuideCards as $arrGuideCard) {
|
||||||
|
$strGuideTitle = trim((string)($arrGuideCard['title'] ?? ''));
|
||||||
|
$strGuideText = trim((string)($arrGuideCard['text'] ?? ''));
|
||||||
|
$strGuideLine = trim($strGuideTitle . ($strGuideText !== '' ? ':' . $strGuideText : ''));
|
||||||
|
if ($strGuideLine !== '') {
|
||||||
|
$arrForgeEntryLinks[] = [
|
||||||
|
'url' => $strMainVideoInfoUrl !== '' ? $strMainVideoInfoUrl : '/',
|
||||||
|
'text' => $strGuideLine,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{/php}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.{$TpStyle.dom_prefix}-dm-desc,
|
||||||
|
.{$TpStyle.dom_prefix}-dm-desc-body,
|
||||||
|
.{$TpStyle.dom_prefix}-dm-desc-guide,
|
||||||
|
.{$TpStyle.dom_prefix}-dm-forge-note {
|
||||||
|
min-width: 0;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.{$TpStyle.dom_prefix}-dm-desc p,
|
||||||
|
.{$TpStyle.dom_prefix}-dm-desc li,
|
||||||
|
.{$TpStyle.dom_prefix}-dm-desc blockquote,
|
||||||
|
.{$TpStyle.dom_prefix}-dm-desc summary,
|
||||||
|
.{$TpStyle.dom_prefix}-dm-desc-body p,
|
||||||
|
.{$TpStyle.dom_prefix}-dm-desc-guide p,
|
||||||
|
.{$TpStyle.dom_prefix}-dm-forge-note p {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.78;
|
||||||
|
}
|
||||||
|
.{$TpStyle.dom_prefix}-dm-desc header,
|
||||||
|
.{$TpStyle.dom_prefix}-dm-desc summary {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.{$TpStyle.dom_prefix}-dm-desc p,
|
||||||
|
.{$TpStyle.dom_prefix}-dm-desc li,
|
||||||
|
.{$TpStyle.dom_prefix}-dm-desc blockquote,
|
||||||
|
.{$TpStyle.dom_prefix}-dm-desc summary,
|
||||||
|
.{$TpStyle.dom_prefix}-dm-desc-body p,
|
||||||
|
.{$TpStyle.dom_prefix}-dm-desc-guide p,
|
||||||
|
.{$TpStyle.dom_prefix}-dm-forge-note p {
|
||||||
|
font-size: 13px !important;
|
||||||
|
line-height: 1.74 !important;
|
||||||
|
}
|
||||||
|
.{$TpStyle.dom_prefix}-dm-desc header,
|
||||||
|
.{$TpStyle.dom_prefix}-dm-desc summary {
|
||||||
|
font-size: 15px !important;
|
||||||
|
}
|
||||||
|
.{$TpStyle.dom_prefix}-dm-desc-body,
|
||||||
|
.{$TpStyle.dom_prefix}-dm-desc-guide,
|
||||||
|
.{$TpStyle.dom_prefix}-dm-forge-note {
|
||||||
|
margin-top: 8px !important;
|
||||||
|
padding: 10px 10px !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
{if $variant == 0}
|
{if $variant == 0}
|
||||||
<p class="{$TpStyle.dom_prefix}-dm-desc v0">
|
<p class="{$TpStyle.dom_prefix}-dm-desc v0">
|
||||||
{$arrVideo.v_description}
|
{$strDetailMainDescription}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{elseif $variant == 1}
|
{elseif $variant == 1}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-desc v1">
|
<div class="{$TpStyle.dom_prefix}-dm-desc v1">
|
||||||
<p>{$arrVideo.v_description|mb_substr=0,120}</p>
|
<p>{$strDetailMainDescription|mb_substr=0,120}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 2}
|
{elseif $variant == 2}
|
||||||
<section class="{$TpStyle.dom_prefix}-dm-desc v2">
|
<section class="{$TpStyle.dom_prefix}-dm-desc v2">
|
||||||
<p>{$arrVideo.v_description|mb_substr=0,200}</p>
|
<p>{$strDetailMainDescription|mb_substr=0,200}</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{elseif $variant == 3}
|
{elseif $variant == 3}
|
||||||
<article class="{$TpStyle.dom_prefix}-dm-desc v3">
|
<article class="{$TpStyle.dom_prefix}-dm-desc v3">
|
||||||
<p>{$arrVideo.v_description}</p>
|
<p>{$strDetailMainDescription}</p>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
{elseif $variant == 4}
|
{elseif $variant == 4}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-desc v4">
|
<div class="{$TpStyle.dom_prefix}-dm-desc v4">
|
||||||
<p>
|
<p>
|
||||||
剧情简介:{$arrVideo.v_description|mb_substr=0,150}
|
剧情简介:{$strDetailMainDescription|mb_substr=0,150}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 5}
|
{elseif $variant == 5}
|
||||||
<details class="{$TpStyle.dom_prefix}-dm-desc v5">
|
<details class="{$TpStyle.dom_prefix}-dm-desc v5">
|
||||||
<summary>剧情介绍</summary>
|
<summary>剧情介绍</summary>
|
||||||
<p>{$arrVideo.v_description}</p>
|
<p>{$strDetailMainDescription}</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
{elseif $variant == 6}
|
{elseif $variant == 6}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-desc v6">
|
<div class="{$TpStyle.dom_prefix}-dm-desc v6">
|
||||||
<p>{$arrVideo.v_description|mb_substr=0,100}...</p>
|
<p>{$strDetailMainDescription|mb_substr=0,100}...</p>
|
||||||
<a href="#desc">查看详情</a>
|
<a href="#desc">查看详情</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 7}
|
{elseif $variant == 7}
|
||||||
<section class="{$TpStyle.dom_prefix}-dm-desc v7">
|
<section class="{$TpStyle.dom_prefix}-dm-desc v7">
|
||||||
<header>影片简介</header>
|
<header>影片简介</header>
|
||||||
<p>{$arrVideo.v_description|mb_substr=0,180}</p>
|
<p>{$strDetailMainDescription|mb_substr=0,180}</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{elseif $variant == 8}
|
{elseif $variant == 8}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-desc v8">
|
<div class="{$TpStyle.dom_prefix}-dm-desc v8">
|
||||||
<blockquote>
|
<blockquote>
|
||||||
{$arrVideo.v_description|mb_substr=0,160}
|
{$strDetailMainDescription|mb_substr=0,160}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 9}
|
{elseif $variant == 9}
|
||||||
<p class="{$TpStyle.dom_prefix}-dm-desc v9">
|
<p class="{$TpStyle.dom_prefix}-dm-desc v9">
|
||||||
{$arrVideo.v_name}({$arrVideo.v_year})剧情:
|
{$arrVideo.v_name}({$arrVideo.v_year})剧情:
|
||||||
{$arrVideo.v_description|mb_substr=0,140}
|
{$strDetailMainDescription|mb_substr=0,140}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{elseif $variant == 10}
|
{elseif $variant == 10}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-desc v10">
|
<div class="{$TpStyle.dom_prefix}-dm-desc v10">
|
||||||
<p itemprop="description">
|
<p itemprop="description">
|
||||||
{$arrVideo.v_description|mb_substr=0,160}
|
{$strDetailMainDescription|mb_substr=0,160}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 11}
|
{elseif $variant == 11}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-desc v11"
|
<div class="{$TpStyle.dom_prefix}-dm-desc v11"
|
||||||
data-desc="{$arrVideo.v_description|mb_substr=0,200}">
|
data-desc="{$strDetailMainDescription|mb_substr=0,200}">
|
||||||
<p>{$arrVideo.v_description|mb_substr=0,120}</p>
|
<p>{$strDetailMainDescription|mb_substr=0,120}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 12}
|
{elseif $variant == 12}
|
||||||
<section class="{$TpStyle.dom_prefix}-dm-desc v12">
|
<section class="{$TpStyle.dom_prefix}-dm-desc v12">
|
||||||
<p>
|
<p>
|
||||||
{$arrVideo.v_description|mb_substr=0,100}
|
{$strDetailMainDescription|mb_substr=0,100}
|
||||||
<span class="{$TpStyle.dom_prefix}-dm-desc-more">
|
<span class="{$TpStyle.dom_prefix}-dm-desc-more">
|
||||||
{$arrVideo.v_name} 在线观看
|
{$arrVideo.v_name} 在线观看
|
||||||
</span>
|
</span>
|
||||||
@@ -84,7 +211,7 @@
|
|||||||
{elseif $variant == 13}
|
{elseif $variant == 13}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-desc v13">
|
<div class="{$TpStyle.dom_prefix}-dm-desc v13">
|
||||||
<p>
|
<p>
|
||||||
{$arrVideo.v_description|mb_substr=0,130}
|
{$strDetailMainDescription|mb_substr=0,130}
|
||||||
高清完整版内容介绍。
|
高清完整版内容介绍。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,7 +219,7 @@
|
|||||||
{elseif $variant == 14}
|
{elseif $variant == 14}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-desc v14">
|
<div class="{$TpStyle.dom_prefix}-dm-desc v14">
|
||||||
<p>
|
<p>
|
||||||
{$arrVideo.v_description|mb_substr=0,150}
|
{$strDetailMainDescription|mb_substr=0,150}
|
||||||
</p>
|
</p>
|
||||||
<p class="{$TpStyle.dom_prefix}-dm-desc-note">
|
<p class="{$TpStyle.dom_prefix}-dm-desc-note">
|
||||||
本片支持在线播放
|
本片支持在线播放
|
||||||
@@ -101,7 +228,7 @@
|
|||||||
|
|
||||||
{elseif $variant == 15}
|
{elseif $variant == 15}
|
||||||
<p class="{$TpStyle.dom_prefix}-dm-desc v15">
|
<p class="{$TpStyle.dom_prefix}-dm-desc v15">
|
||||||
{$arrVideo.v_description|mb_substr=0,120}
|
{$strDetailMainDescription|mb_substr=0,120}
|
||||||
免费观看{$arrVideo.v_name}
|
免费观看{$arrVideo.v_name}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -109,30 +236,59 @@
|
|||||||
<div class="{$TpStyle.dom_prefix}-dm-desc v16">
|
<div class="{$TpStyle.dom_prefix}-dm-desc v16">
|
||||||
<p>
|
<p>
|
||||||
<strong>{$arrVideo.v_name}</strong>
|
<strong>{$arrVideo.v_name}</strong>
|
||||||
{$arrVideo.v_description|mb_substr=0,140}
|
{$strDetailMainDescription|mb_substr=0,140}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 17}
|
{elseif $variant == 17}
|
||||||
<section class="{$TpStyle.dom_prefix}-dm-desc v17">
|
<section class="{$TpStyle.dom_prefix}-dm-desc v17">
|
||||||
<p>{$arrVideo.v_description|mb_substr=0,180}</p>
|
<p>{$strDetailMainDescription|mb_substr=0,180}</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{elseif $variant == 18}
|
{elseif $variant == 18}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-desc v18">
|
<div class="{$TpStyle.dom_prefix}-dm-desc v18">
|
||||||
<p class="{$TpStyle.dom_prefix}-dm-desc-hidden">
|
<p class="{$TpStyle.dom_prefix}-dm-desc-hidden">
|
||||||
{$arrVideo.v_description}
|
{$strDetailMainDescription}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 19}
|
{elseif $variant == 19}
|
||||||
<article class="{$TpStyle.dom_prefix}-dm-desc v19">
|
<article class="{$TpStyle.dom_prefix}-dm-desc v19">
|
||||||
<header>剧情简介</header>
|
<header>剧情简介</header>
|
||||||
<p>{$arrVideo.v_description|mb_substr=0,160}</p>
|
<p>{$strDetailMainDescription|mb_substr=0,160}</p>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{notempty name="$strDetailBody"}
|
||||||
|
<div class="{$TpStyle.dom_prefix}-dm-desc-body" style="margin-top:10px;padding:12px 12px;border:1px solid var(--border-color);border-radius:14px;background:var(--bg-soft-color);">
|
||||||
|
<p style="margin:0;color:var(--text-color);line-height:1.8;">{$strDetailBody}</p>
|
||||||
|
</div>
|
||||||
|
{/notempty}
|
||||||
|
|
||||||
|
{if empty($boolIsForgeDetail)}
|
||||||
|
{include file="module/seo_copy/detail" /}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{if !empty($boolIsForgeDetail)}
|
||||||
|
<div class="{$TpStyle.dom_prefix}-dm-desc-guide" style="margin-top:10px;padding:12px 12px;border:1px solid var(--border-color);border-radius:14px;background:var(--bg-soft-color);">
|
||||||
|
<p style="margin:0;color:var(--text-color);line-height:1.8;">{$strForgeBodyLead|default='当前页围绕延伸词补充主片相关信息,适合先确认内容归属。'}</p>
|
||||||
|
<p style="margin:8px 0 0;color:var(--text-muted-color);line-height:1.7;">
|
||||||
|
{notempty name="$strMainVideoInfoUrl"}
|
||||||
|
{$strForgeReturnLead|default='如果想回到标准详情,可返回'} <a href="{$strMainVideoInfoUrl}">《{$strOriginVideoName}》详情页</a>;
|
||||||
|
{/notempty}
|
||||||
|
{notempty name="$strPreferredPlayUrl"}
|
||||||
|
{$strForgePlayLead|default='如果准备继续观看,也可以直接进入'} <a href="{$strPreferredPlayUrl}">播放页</a>。
|
||||||
|
{/notempty}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="{$TpStyle.dom_prefix}-dm-forge-note" style="margin-top:10px;padding:12px 12px;border:1px solid var(--border-color);border-radius:14px;background:var(--bg-soft-color);">
|
||||||
|
<p style="margin:0;color:var(--text-color);line-height:1.8;">{$strForgeDetailNote|default='当前页提供的是延伸词视角,适合先确认内容归属,再决定是否回到标准详情或播放页。'}</p>
|
||||||
|
{notempty name="$strMainVideoInfoUrl"}
|
||||||
|
<p style="margin:8px 0 0;color:var(--text-muted-color);line-height:1.7;">如果你想继续查看主内容页,可以回到 <a href="{$strMainVideoInfoUrl}">《{$strOriginVideoName}》详情页</a>。</p>
|
||||||
|
{/notempty}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{video:seoaddon video="$arrVideo" export_name="arrAddon" /}
|
{video:seoaddon video="$arrVideo" export_name="arrAddon" /}
|
||||||
{include file="module/detail/_seo_addon" /}
|
{include file="module/detail/_seo_addon" /}
|
||||||
|
|
||||||
|
|||||||
@@ -1,117 +1,133 @@
|
|||||||
{// ===================== Title Variants ===================== }
|
{// ===================== Title Variants ===================== }
|
||||||
|
|
||||||
|
{php}
|
||||||
|
$strDetailTitleName = trim((string)($arrVideo['v_name'] ?? ''));
|
||||||
|
if ($strDetailTitleName === '') {
|
||||||
|
$strDetailTitleName = '视频详情';
|
||||||
|
}
|
||||||
|
|
||||||
|
$strDetailTitleYear = trim((string)($arrVideo['v_year'] ?? ''));
|
||||||
|
$boolShowDetailYear = $strDetailTitleYear !== ''
|
||||||
|
&& preg_match('/^(19|20)\d{2}$/', $strDetailTitleYear)
|
||||||
|
&& $strDetailTitleYear !== '0';
|
||||||
|
|
||||||
|
$strDetailTitleScore = trim((string)($arrVideo['v_score'] ?? ''));
|
||||||
|
$boolShowDetailScore = $strDetailTitleScore !== ''
|
||||||
|
&& is_numeric($strDetailTitleScore)
|
||||||
|
&& (float)$strDetailTitleScore > 0;
|
||||||
|
{/php}
|
||||||
|
|
||||||
{if $variant == 0}
|
{if $variant == 0}
|
||||||
<h1 class="{$TpStyle.dom_prefix}-dm-title v0">
|
<h1 class="{$TpStyle.dom_prefix}-dm-title v0">
|
||||||
{$arrVideo.v_name}
|
{$strDetailTitleName}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{elseif $variant == 1}
|
{elseif $variant == 1}
|
||||||
<h1 class="{$TpStyle.dom_prefix}-dm-title v1">
|
<h1 class="{$TpStyle.dom_prefix}-dm-title v1">
|
||||||
{$arrVideo.v_name}
|
{$strDetailTitleName}
|
||||||
<small>{$arrVideo.v_year}</small>
|
{if $boolShowDetailYear}<small>{$strDetailTitleYear}</small>{/if}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{elseif $variant == 2}
|
{elseif $variant == 2}
|
||||||
<h2 class="{$TpStyle.dom_prefix}-dm-title v2">
|
<h1 class="{$TpStyle.dom_prefix}-dm-title v2">
|
||||||
《{$arrVideo.v_name}》
|
《{$strDetailTitleName}》
|
||||||
</h2>
|
</h1>
|
||||||
|
|
||||||
{elseif $variant == 3}
|
{elseif $variant == 3}
|
||||||
<header class="{$TpStyle.dom_prefix}-dm-title-wrap v3">
|
<header class="{$TpStyle.dom_prefix}-dm-title-wrap v3">
|
||||||
<h1 class="{$TpStyle.dom_prefix}-dm-title">{$arrVideo.v_name}</h1>
|
<h1 class="{$TpStyle.dom_prefix}-dm-title">{$strDetailTitleName}</h1>
|
||||||
<span class="{$TpStyle.dom_prefix}-dm-sub">{$arrVideo.v_year}</span>
|
{if $boolShowDetailYear}<span class="{$TpStyle.dom_prefix}-dm-sub">{$strDetailTitleYear}</span>{/if}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{elseif $variant == 4}
|
{elseif $variant == 4}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-title-box v4">
|
<div class="{$TpStyle.dom_prefix}-dm-title-box v4">
|
||||||
<strong class="{$TpStyle.dom_prefix}-dm-title">{$arrVideo.v_name}</strong>
|
<h1 class="{$TpStyle.dom_prefix}-dm-title">{$strDetailTitleName}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 5}
|
{elseif $variant == 5}
|
||||||
<h1 class="{$TpStyle.dom_prefix}-dm-title v5">
|
<h1 class="{$TpStyle.dom_prefix}-dm-title v5">
|
||||||
{$arrVideo.v_name}
|
{$strDetailTitleName}
|
||||||
<sup>HD</sup>
|
<sup>HD</sup>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{elseif $variant == 6}
|
{elseif $variant == 6}
|
||||||
<section class="{$TpStyle.dom_prefix}-dm-title-wrap v6">
|
<section class="{$TpStyle.dom_prefix}-dm-title-wrap v6">
|
||||||
<h1 class="{$TpStyle.dom_prefix}-dm-title">{$arrVideo.v_name}</h1>
|
<h1 class="{$TpStyle.dom_prefix}-dm-title">{$strDetailTitleName}</h1>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{elseif $variant == 7}
|
{elseif $variant == 7}
|
||||||
<article class="{$TpStyle.dom_prefix}-dm-title-wrap v7">
|
<article class="{$TpStyle.dom_prefix}-dm-title-wrap v7">
|
||||||
<h2 class="{$TpStyle.dom_prefix}-dm-title">{$arrVideo.v_name}</h2>
|
<h1 class="{$TpStyle.dom_prefix}-dm-title">{$strDetailTitleName}</h1>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
{elseif $variant == 8}
|
{elseif $variant == 8}
|
||||||
<figure class="{$TpStyle.dom_prefix}-dm-title-wrap v8">
|
<figure class="{$TpStyle.dom_prefix}-dm-title-wrap v8">
|
||||||
<figcaption class="{$TpStyle.dom_prefix}-dm-title">
|
<h1 class="{$TpStyle.dom_prefix}-dm-title">
|
||||||
{$arrVideo.v_name}
|
{$strDetailTitleName}
|
||||||
</figcaption>
|
</h1>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
{elseif $variant == 9}
|
{elseif $variant == 9}
|
||||||
<h1 class="{$TpStyle.dom_prefix}-dm-title v9">
|
<h1 class="{$TpStyle.dom_prefix}-dm-title v9">
|
||||||
{$arrVideo.v_name}
|
{$strDetailTitleName}
|
||||||
<span class="{$TpStyle.dom_prefix}-dm-tag">在线观看</span>
|
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{elseif $variant == 10}
|
{elseif $variant == 10}
|
||||||
<h1 class="{$TpStyle.dom_prefix}-dm-title v10" data-title="{$arrVideo.v_name}">
|
<h1 class="{$TpStyle.dom_prefix}-dm-title v10" data-title="{$strDetailTitleName}">
|
||||||
{$arrVideo.v_name}
|
{$strDetailTitleName}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{elseif $variant == 11}
|
{elseif $variant == 11}
|
||||||
<h2 class="{$TpStyle.dom_prefix}-dm-title v11">
|
<h1 class="{$TpStyle.dom_prefix}-dm-title v11">
|
||||||
<strong>{$arrVideo.v_name}</strong>
|
<strong>{$strDetailTitleName}</strong>
|
||||||
</h2>
|
</h1>
|
||||||
|
|
||||||
{elseif $variant == 12}
|
{elseif $variant == 12}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-title-box v12">
|
<div class="{$TpStyle.dom_prefix}-dm-title-box v12">
|
||||||
<h1 class="{$TpStyle.dom_prefix}-dm-title">
|
<h1 class="{$TpStyle.dom_prefix}-dm-title">
|
||||||
{$arrVideo.v_name}
|
{$strDetailTitleName}
|
||||||
<em>{$arrVideo.v_year}</em>
|
{if $boolShowDetailYear}<em>{$strDetailTitleYear}</em>{/if}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 13}
|
{elseif $variant == 13}
|
||||||
<h1 class="{$TpStyle.dom_prefix}-dm-title v13">
|
<h1 class="{$TpStyle.dom_prefix}-dm-title v13">
|
||||||
{$arrVideo.v_name}
|
{$strDetailTitleName}
|
||||||
<span class="{$TpStyle.dom_prefix}-dm-score">{$arrVideo.v_score}</span>
|
{if $boolShowDetailScore}<span class="{$TpStyle.dom_prefix}-dm-score">{$strDetailTitleScore}</span>{/if}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{elseif $variant == 14}
|
{elseif $variant == 14}
|
||||||
<header class="{$TpStyle.dom_prefix}-dm-title-wrap v14">
|
<header class="{$TpStyle.dom_prefix}-dm-title-wrap v14">
|
||||||
<h1 class="{$TpStyle.dom_prefix}-dm-title">{$arrVideo.v_name}</h1>
|
<h1 class="{$TpStyle.dom_prefix}-dm-title">{$strDetailTitleName}</h1>
|
||||||
<i class="{$TpStyle.dom_prefix}-dm-icon"></i>
|
<i class="{$TpStyle.dom_prefix}-dm-icon"></i>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{elseif $variant == 15}
|
{elseif $variant == 15}
|
||||||
<h1 class="{$TpStyle.dom_prefix}-dm-title v15">
|
<h1 class="{$TpStyle.dom_prefix}-dm-title v15">
|
||||||
{$arrVideo.v_name} 免费高清完整版
|
{$strDetailTitleName} 免费高清完整版
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{elseif $variant == 16}
|
{elseif $variant == 16}
|
||||||
<h1 class="{$TpStyle.dom_prefix}-dm-title v16" itemprop="name">
|
<h1 class="{$TpStyle.dom_prefix}-dm-title v16" itemprop="name">
|
||||||
{$arrVideo.v_name}
|
{$strDetailTitleName}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{elseif $variant == 17}
|
{elseif $variant == 17}
|
||||||
<div class="{$TpStyle.dom_prefix}-dm-title-box v17">
|
<div class="{$TpStyle.dom_prefix}-dm-title-box v17">
|
||||||
<h2 class="{$TpStyle.dom_prefix}-dm-title">{$arrVideo.v_name}</h2>
|
<h1 class="{$TpStyle.dom_prefix}-dm-title">{$strDetailTitleName}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{elseif $variant == 18}
|
{elseif $variant == 18}
|
||||||
<h1 class="{$TpStyle.dom_prefix}-dm-title v18">
|
<h1 class="{$TpStyle.dom_prefix}-dm-title v18">
|
||||||
{$arrVideo.v_name}
|
{$strDetailTitleName}
|
||||||
<span class="{$TpStyle.dom_prefix}-dm-hidden">
|
<span class="{$TpStyle.dom_prefix}-dm-hidden">
|
||||||
{$arrVideo.v_year} 在线观看
|
{if $boolShowDetailYear}{$strDetailTitleYear}{/if}
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{elseif $variant == 19}
|
{elseif $variant == 19}
|
||||||
<section class="{$TpStyle.dom_prefix}-dm-title-wrap v19">
|
<section class="{$TpStyle.dom_prefix}-dm-title-wrap v19">
|
||||||
<h1 class="{$TpStyle.dom_prefix}-dm-title">{$arrVideo.v_name}</h1>
|
<h1 class="{$TpStyle.dom_prefix}-dm-title">{$strDetailTitleName}</h1>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -18,6 +18,4 @@ $title = $Slot['title_text'] ?? null;
|
|||||||
{// ===== Shell + Item ===== }
|
{// ===== Shell + Item ===== }
|
||||||
{include file="module/list/shell/_shell_router" /}
|
{include file="module/list/shell/_shell_router" /}
|
||||||
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
<label>跳到</label>
|
<label>跳到</label>
|
||||||
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
||||||
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
||||||
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);p=Math.max(1,Math.min(max,p));var u='{:$first_url ?? '#'}';if(!u){return;}u=u.replace(/page\d+$/i,'page'+p);window.location.href=u;})(this)">确定</button>
|
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);var u='{:$first_url ?? '#'}';p=Math.max(1,Math.min(max,p));if(!u||u==='#'){return;}if(/[?&]page=\\d+/i.test(u)){u=u.replace(/([?&]page=)\\d+/i,'$1'+p);}else if(/[?&]page=\\{page\\}/i.test(u)){u=u.replace(/([?&]page=)\\{page\\}/i,'$1'+p);}else if(/-\\{page\\}(?=$|[?#])/i.test(u)){u=u.replace(/-\\{page\\}(?=$|[?#])/i,'-'+p);}else{u=u.replace(/-(\\d+)(?=$|[?#])/,'-'+p);}window.location.href=u;})(this)">确定</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
{/case}
|
{/case}
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
<label>跳到</label>
|
<label>跳到</label>
|
||||||
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
||||||
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
||||||
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);p=Math.max(1,Math.min(max,p));var u='{:$first_url ?? '#'}';if(!u){return;}u=u.replace(/page\d+$/i,'page'+p);window.location.href=u;})(this)">确定</button>
|
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);var u='{:$first_url ?? '#'}';p=Math.max(1,Math.min(max,p));if(!u||u==='#'){return;}if(/[?&]page=\\d+/i.test(u)){u=u.replace(/([?&]page=)\\d+/i,'$1'+p);}else if(/[?&]page=\\{page\\}/i.test(u)){u=u.replace(/([?&]page=)\\{page\\}/i,'$1'+p);}else if(/-\\{page\\}(?=$|[?#])/i.test(u)){u=u.replace(/-\\{page\\}(?=$|[?#])/i,'-'+p);}else{u=u.replace(/-(\\d+)(?=$|[?#])/,'-'+p);}window.location.href=u;})(this)">确定</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/case}
|
{/case}
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
<label>跳到</label>
|
<label>跳到</label>
|
||||||
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
||||||
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
||||||
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);p=Math.max(1,Math.min(max,p));var u='{:$first_url ?? '#'}';if(!u){return;}u=u.replace(/page\d+$/i,'page'+p);window.location.href=u;})(this)">确定</button>
|
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);var u='{:$first_url ?? '#'}';p=Math.max(1,Math.min(max,p));if(!u||u==='#'){return;}if(/[?&]page=\\d+/i.test(u)){u=u.replace(/([?&]page=)\\d+/i,'$1'+p);}else if(/[?&]page=\\{page\\}/i.test(u)){u=u.replace(/([?&]page=)\\{page\\}/i,'$1'+p);}else if(/-\\{page\\}(?=$|[?#])/i.test(u)){u=u.replace(/-\\{page\\}(?=$|[?#])/i,'-'+p);}else{u=u.replace(/-(\\d+)(?=$|[?#])/,'-'+p);}window.location.href=u;})(this)">确定</button>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
{/case}
|
{/case}
|
||||||
@@ -278,7 +278,7 @@
|
|||||||
<label>跳到</label>
|
<label>跳到</label>
|
||||||
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
||||||
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
||||||
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);p=Math.max(1,Math.min(max,p));var u='{:$first_url ?? '#'}';if(!u){return;}u=u.replace(/page\d+$/i,'page'+p);window.location.href=u;})(this)">确定</button>
|
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);var u='{:$first_url ?? '#'}';p=Math.max(1,Math.min(max,p));if(!u||u==='#'){return;}if(/[?&]page=\\d+/i.test(u)){u=u.replace(/([?&]page=)\\d+/i,'$1'+p);}else if(/[?&]page=\\{page\\}/i.test(u)){u=u.replace(/([?&]page=)\\{page\\}/i,'$1'+p);}else if(/-\\{page\\}(?=$|[?#])/i.test(u)){u=u.replace(/-\\{page\\}(?=$|[?#])/i,'-'+p);}else{u=u.replace(/-(\\d+)(?=$|[?#])/,'-'+p);}window.location.href=u;})(this)">确定</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{/case}
|
{/case}
|
||||||
@@ -358,7 +358,7 @@
|
|||||||
<label>跳到</label>
|
<label>跳到</label>
|
||||||
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
||||||
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
||||||
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);p=Math.max(1,Math.min(max,p));var u='{:$first_url ?? '#'}';if(!u){return;}u=u.replace(/page\d+$/i,'page'+p);window.location.href=u;})(this)">确定</button>
|
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);var u='{:$first_url ?? '#'}';p=Math.max(1,Math.min(max,p));if(!u||u==='#'){return;}if(/[?&]page=\\d+/i.test(u)){u=u.replace(/([?&]page=)\\d+/i,'$1'+p);}else if(/[?&]page=\\{page\\}/i.test(u)){u=u.replace(/([?&]page=)\\{page\\}/i,'$1'+p);}else if(/-\\{page\\}(?=$|[?#])/i.test(u)){u=u.replace(/-\\{page\\}(?=$|[?#])/i,'-'+p);}else{u=u.replace(/-(\\d+)(?=$|[?#])/,'-'+p);}window.location.href=u;})(this)">确定</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
{/case}
|
{/case}
|
||||||
@@ -438,7 +438,7 @@
|
|||||||
<label>跳到</label>
|
<label>跳到</label>
|
||||||
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
||||||
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
||||||
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);p=Math.max(1,Math.min(max,p));var u='{:$first_url ?? '#'}';if(!u){return;}u=u.replace(/page\d+$/i,'page'+p);window.location.href=u;})(this)">确定</button>
|
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);var u='{:$first_url ?? '#'}';p=Math.max(1,Math.min(max,p));if(!u||u==='#'){return;}if(/[?&]page=\\d+/i.test(u)){u=u.replace(/([?&]page=)\\d+/i,'$1'+p);}else if(/[?&]page=\\{page\\}/i.test(u)){u=u.replace(/([?&]page=)\\{page\\}/i,'$1'+p);}else if(/-\\{page\\}(?=$|[?#])/i.test(u)){u=u.replace(/-\\{page\\}(?=$|[?#])/i,'-'+p);}else{u=u.replace(/-(\\d+)(?=$|[?#])/,'-'+p);}window.location.href=u;})(this)">确定</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
{/case}
|
{/case}
|
||||||
@@ -518,7 +518,7 @@
|
|||||||
<label>跳到</label>
|
<label>跳到</label>
|
||||||
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
||||||
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
||||||
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);p=Math.max(1,Math.min(max,p));var u='{:$first_url ?? '#'}';if(!u){return;}u=u.replace(/page\d+$/i,'page'+p);window.location.href=u;})(this)">确定</button>
|
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);var u='{:$first_url ?? '#'}';p=Math.max(1,Math.min(max,p));if(!u||u==='#'){return;}if(/[?&]page=\\d+/i.test(u)){u=u.replace(/([?&]page=)\\d+/i,'$1'+p);}else if(/[?&]page=\\{page\\}/i.test(u)){u=u.replace(/([?&]page=)\\{page\\}/i,'$1'+p);}else if(/-\\{page\\}(?=$|[?#])/i.test(u)){u=u.replace(/-\\{page\\}(?=$|[?#])/i,'-'+p);}else{u=u.replace(/-(\\d+)(?=$|[?#])/,'-'+p);}window.location.href=u;})(this)">确定</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/case}
|
{/case}
|
||||||
@@ -598,7 +598,7 @@
|
|||||||
<label>跳到</label>
|
<label>跳到</label>
|
||||||
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
||||||
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
||||||
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);p=Math.max(1,Math.min(max,p));var u='{:$first_url ?? '#'}';if(!u){return;}u=u.replace(/page\d+$/i,'page'+p);window.location.href=u;})(this)">确定</button>
|
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);var u='{:$first_url ?? '#'}';p=Math.max(1,Math.min(max,p));if(!u||u==='#'){return;}if(/[?&]page=\\d+/i.test(u)){u=u.replace(/([?&]page=)\\d+/i,'$1'+p);}else if(/[?&]page=\\{page\\}/i.test(u)){u=u.replace(/([?&]page=)\\{page\\}/i,'$1'+p);}else if(/-\\{page\\}(?=$|[?#])/i.test(u)){u=u.replace(/-\\{page\\}(?=$|[?#])/i,'-'+p);}else{u=u.replace(/-(\\d+)(?=$|[?#])/,'-'+p);}window.location.href=u;})(this)">确定</button>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
{/case}
|
{/case}
|
||||||
@@ -678,7 +678,7 @@
|
|||||||
<label>跳到</label>
|
<label>跳到</label>
|
||||||
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
||||||
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
||||||
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);p=Math.max(1,Math.min(max,p));var u='{:$first_url ?? '#'}';if(!u){return;}u=u.replace(/page\d+$/i,'page'+p);window.location.href=u;})(this)">确定</button>
|
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);var u='{:$first_url ?? '#'}';p=Math.max(1,Math.min(max,p));if(!u||u==='#'){return;}if(/[?&]page=\\d+/i.test(u)){u=u.replace(/([?&]page=)\\d+/i,'$1'+p);}else if(/[?&]page=\\{page\\}/i.test(u)){u=u.replace(/([?&]page=)\\{page\\}/i,'$1'+p);}else if(/-\\{page\\}(?=$|[?#])/i.test(u)){u=u.replace(/-\\{page\\}(?=$|[?#])/i,'-'+p);}else{u=u.replace(/-(\\d+)(?=$|[?#])/,'-'+p);}window.location.href=u;})(this)">确定</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{/case}
|
{/case}
|
||||||
@@ -758,7 +758,7 @@
|
|||||||
<label>跳到</label>
|
<label>跳到</label>
|
||||||
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
||||||
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
||||||
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);p=Math.max(1,Math.min(max,p));var u='{:$first_url ?? '#'}';if(!u){return;}u=u.replace(/page\d+$/i,'page'+p);window.location.href=u;})(this)">确定</button>
|
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);var u='{:$first_url ?? '#'}';p=Math.max(1,Math.min(max,p));if(!u||u==='#'){return;}if(/[?&]page=\\d+/i.test(u)){u=u.replace(/([?&]page=)\\d+/i,'$1'+p);}else if(/[?&]page=\\{page\\}/i.test(u)){u=u.replace(/([?&]page=)\\{page\\}/i,'$1'+p);}else if(/-\\{page\\}(?=$|[?#])/i.test(u)){u=u.replace(/-\\{page\\}(?=$|[?#])/i,'-'+p);}else{u=u.replace(/-(\\d+)(?=$|[?#])/,'-'+p);}window.location.href=u;})(this)">确定</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
{/case}
|
{/case}
|
||||||
|
|||||||
@@ -327,7 +327,7 @@
|
|||||||
<label>跳到</label>
|
<label>跳到</label>
|
||||||
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
||||||
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
||||||
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);p=Math.max(1,Math.min(max,p));var u='{:$first_urll ?? '#'}';if(!u){return;}u=u.replace(/page\d+$/i,'page'+p);window.location.href=u;})(this)">确定</button>
|
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);var u='{:$first_urll ?? '#'}';p=Math.max(1,Math.min(max,p));if(!u||u==='#'){return;}if(/[?&]page=\\d+/i.test(u)){u=u.replace(/([?&]page=)\\d+/i,'$1'+p);}else if(/[?&]page=\\{page\\}/i.test(u)){u=u.replace(/([?&]page=)\\{page\\}/i,'$1'+p);}else if(/-\\{page\\}(?=$|[?#])/i.test(u)){u=u.replace(/-\\{page\\}(?=$|[?#])/i,'-'+p);}else{u=u.replace(/-(\\d+)(?=$|[?#])/,'-'+p);}window.location.href=u;})(this)">确定</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -745,7 +745,7 @@
|
|||||||
<label>跳到</label>
|
<label>跳到</label>
|
||||||
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
<input class="{$TpStyle.dom_prefix}-pg-inp" type="number" min="1" max="{$pages}" value="{$page}">
|
||||||
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
<button class="{$TpStyle.dom_prefix}-pg-btn" type="button"
|
||||||
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);p=Math.max(1,Math.min(max,p));var u='{:$first_urll ?? '#'}';if(!u){return;}u=u.replace(/page\d+$/i,'page'+p);window.location.href=u;})(this)">确定</button>
|
onclick="(function(btn){var wrap=btn.parentNode;var inp=wrap.querySelector('input');var max=parseInt(wrap.getAttribute('data-pages')||1);var p=parseInt(inp.value||1);var u='{:$first_urll ?? '#'}';p=Math.max(1,Math.min(max,p));if(!u||u==='#'){return;}if(/[?&]page=\\d+/i.test(u)){u=u.replace(/([?&]page=)\\d+/i,'$1'+p);}else if(/[?&]page=\\{page\\}/i.test(u)){u=u.replace(/([?&]page=)\\{page\\}/i,'$1'+p);}else if(/-\\{page\\}(?=$|[?#])/i.test(u)){u=u.replace(/-\\{page\\}(?=$|[?#])/i,'-'+p);}else{u=u.replace(/-(\\d+)(?=$|[?#])/,'-'+p);}window.location.href=u;})(this)">确定</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
{php}
|
||||||
|
$strPlayModuleSlug = trim((string)($arrVideo['v_name_en'] ?? request()->route('strPinyin') ?? ''));
|
||||||
|
if ($strPlayModuleSlug === '') {
|
||||||
|
$strPlayModuleSlug = (string)($arrVideo['v_id'] ?? '');
|
||||||
|
}
|
||||||
|
{/php}
|
||||||
<article class="{$TpStyle.dom_prefix}-p1-wrap">
|
<article class="{$TpStyle.dom_prefix}-p1-wrap">
|
||||||
|
|
||||||
{// 顶部区域:封面 + 简介 }
|
{// 顶部区域:封面 + 简介 }
|
||||||
@@ -8,12 +14,13 @@
|
|||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<section class="{$TpStyle.dom_prefix}-p1-info">
|
<section class="{$TpStyle.dom_prefix}-p1-info">
|
||||||
<h1>{$arrVideo.v_name}</h1>
|
<h1>{$strPlayMainHeading}</h1>
|
||||||
|
|
||||||
<p class="{$TpStyle.dom_prefix}-p1-meta">
|
<p class="{$TpStyle.dom_prefix}-p1-meta">
|
||||||
<span>{$arrVideo.v_category}</span>
|
<span>{$arrVideo.v_category}</span>
|
||||||
<span>{$arrVideo.v_area}</span>
|
<span>{$arrVideo.v_area}</span>
|
||||||
<span>{$arrVideo.v_year}</span>
|
<span>{$arrVideo.v_year}</span>
|
||||||
|
<span>{$strPlayHeadingMeta}</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="{$TpStyle.dom_prefix}-p1-des">
|
<p class="{$TpStyle.dom_prefix}-p1-des">
|
||||||
@@ -56,12 +63,7 @@
|
|||||||
{volist name='$arrPlayUrlType' id='arrPlayUrl'}
|
{volist name='$arrPlayUrlType' id='arrPlayUrl'}
|
||||||
<?php $epIndex++; ?>
|
<?php $epIndex++; ?>
|
||||||
<li>
|
<li>
|
||||||
<a href='{site:vpurl
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlayModuleSlug, (string)$strPlayGroupName, (int)$epIndex)}"
|
||||||
v_id="$arrVideo.v_id"
|
|
||||||
v_py="$arrVideo.v_name_en"
|
|
||||||
play_type="$strPlayGroupName"
|
|
||||||
play_index="$epIndex"
|
|
||||||
/}'
|
|
||||||
class="
|
class="
|
||||||
{if $epIndex == $Request.route.intPlayIndex}active{/if}
|
{if $epIndex == $Request.route.intPlayIndex}active{/if}
|
||||||
{$TpStyle.dom_prefix}-p1-ep
|
{$TpStyle.dom_prefix}-p1-ep
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
{php}
|
||||||
|
$strPlayModuleSlug = trim((string)($arrVideo['v_name_en'] ?? request()->route('strPinyin') ?? ''));
|
||||||
|
if ($strPlayModuleSlug === '') {
|
||||||
|
$strPlayModuleSlug = (string)($arrVideo['v_id'] ?? '');
|
||||||
|
}
|
||||||
|
{/php}
|
||||||
<article class="{$TpStyle.dom_prefix}-p2-shell">
|
<article class="{$TpStyle.dom_prefix}-p2-shell">
|
||||||
|
|
||||||
{// 全宽播放器 }
|
{// 全宽播放器 }
|
||||||
@@ -7,11 +13,12 @@
|
|||||||
|
|
||||||
{// 基本信息 }
|
{// 基本信息 }
|
||||||
<section class="{$TpStyle.dom_prefix}-p2-info">
|
<section class="{$TpStyle.dom_prefix}-p2-info">
|
||||||
<h1>{$arrVideo.v_name}</h1>
|
<h1>{$strPlayMainHeading}</h1>
|
||||||
<p class="{$TpStyle.dom_prefix}-p2-meta">
|
<p class="{$TpStyle.dom_prefix}-p2-meta">
|
||||||
<span>{$arrVideo.v_category}</span>
|
<span>{$arrVideo.v_category}</span>
|
||||||
<span>{$arrVideo.v_area}</span>
|
<span>{$arrVideo.v_area}</span>
|
||||||
<span>{$arrVideo.v_year}</span>
|
<span>{$arrVideo.v_year}</span>
|
||||||
|
<span>{$strPlayHeadingMeta}</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="{$TpStyle.dom_prefix}-p2-actors">
|
<p class="{$TpStyle.dom_prefix}-p2-actors">
|
||||||
主演:{$arrVideo.v_actor}
|
主演:{$arrVideo.v_actor}
|
||||||
@@ -42,12 +49,7 @@
|
|||||||
<?php $epIndex=0; ?>
|
<?php $epIndex=0; ?>
|
||||||
{volist name='$arrPlayUrlType' id='arrPlayUrl'}
|
{volist name='$arrPlayUrlType' id='arrPlayUrl'}
|
||||||
<?php $epIndex++; ?>
|
<?php $epIndex++; ?>
|
||||||
<a href='{site:vpurl
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlayModuleSlug, (string)$strPlayGroupName, (int)$epIndex)}"
|
||||||
v_id="$arrVideo.v_id"
|
|
||||||
v_py="$arrVideo.v_name_en"
|
|
||||||
play_type="$strPlayGroupName"
|
|
||||||
play_index="$epIndex"
|
|
||||||
/}'
|
|
||||||
class="{$TpStyle.dom_prefix}-p2-epbtn {if $epIndex == $Request.route.intPlayIndex}active{/if}"
|
class="{$TpStyle.dom_prefix}-p2-epbtn {if $epIndex == $Request.route.intPlayIndex}active{/if}"
|
||||||
m3u8="{$arrPlayUrl.url}">
|
m3u8="{$arrPlayUrl.url}">
|
||||||
{$arrPlayUrl.name}
|
{$arrPlayUrl.name}
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
{php}
|
||||||
|
$strPlayModuleSlug = trim((string)($arrVideo['v_name_en'] ?? request()->route('strPinyin') ?? ''));
|
||||||
|
if ($strPlayModuleSlug === '') {
|
||||||
|
$strPlayModuleSlug = (string)($arrVideo['v_id'] ?? '');
|
||||||
|
}
|
||||||
|
{/php}
|
||||||
<main class="{$TpStyle.dom_prefix}-p3-layout">
|
<main class="{$TpStyle.dom_prefix}-p3-layout">
|
||||||
|
|
||||||
<section class="{$TpStyle.dom_prefix}-p3-main">
|
<section class="{$TpStyle.dom_prefix}-p3-main">
|
||||||
@@ -7,9 +13,9 @@
|
|||||||
<div id="dplayer" class="{$TpStyle.dom_prefix}-p3-dp"></div>
|
<div id="dplayer" class="{$TpStyle.dom_prefix}-p3-dp"></div>
|
||||||
|
|
||||||
<div class="{$TpStyle.dom_prefix}-p3-info">
|
<div class="{$TpStyle.dom_prefix}-p3-info">
|
||||||
<h1>{$arrVideo.v_name}</h1>
|
<h1>{$strPlayMainHeading}</h1>
|
||||||
<p class="{$TpStyle.dom_prefix}-p3-meta">
|
<p class="{$TpStyle.dom_prefix}-p3-meta">
|
||||||
{$arrVideo.v_category} · {$arrVideo.v_area} · {$arrVideo.v_year}
|
{$arrVideo.v_category} · {$arrVideo.v_area} · {$arrVideo.v_year} · {$strPlayHeadingMeta}
|
||||||
</p>
|
</p>
|
||||||
<p class="{$TpStyle.dom_prefix}-p3-actor">
|
<p class="{$TpStyle.dom_prefix}-p3-actor">
|
||||||
主演:{$arrVideo.v_actor}
|
主演:{$arrVideo.v_actor}
|
||||||
@@ -41,12 +47,7 @@
|
|||||||
<?php $epIndex=0; ?>
|
<?php $epIndex=0; ?>
|
||||||
{volist name='$arrPlayUrlType' id='arrPlayUrl'}
|
{volist name='$arrPlayUrlType' id='arrPlayUrl'}
|
||||||
<?php $epIndex++; ?>
|
<?php $epIndex++; ?>
|
||||||
<a href='{site:vpurl
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlayModuleSlug, (string)$strPlayGroupName, (int)$epIndex)}"
|
||||||
v_id="$arrVideo.v_id"
|
|
||||||
v_py="$arrVideo.v_name_en"
|
|
||||||
play_type="$strPlayGroupName"
|
|
||||||
play_index="$epIndex"
|
|
||||||
/}'
|
|
||||||
class="{$TpStyle.dom_prefix}-p3-ep {if $epIndex == $Request.route.intPlayIndex}active{/if}"
|
class="{$TpStyle.dom_prefix}-p3-ep {if $epIndex == $Request.route.intPlayIndex}active{/if}"
|
||||||
m3u8="{$arrPlayUrl.url}">
|
m3u8="{$arrPlayUrl.url}">
|
||||||
{$arrPlayUrl.name}
|
{$arrPlayUrl.name}
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
{php}
|
||||||
|
$strPlayModuleSlug = trim((string)($arrVideo['v_name_en'] ?? request()->route('strPinyin') ?? ''));
|
||||||
|
if ($strPlayModuleSlug === '') {
|
||||||
|
$strPlayModuleSlug = (string)($arrVideo['v_id'] ?? '');
|
||||||
|
}
|
||||||
|
{/php}
|
||||||
<section class="{$TpStyle.dom_prefix}-p4-page">
|
<section class="{$TpStyle.dom_prefix}-p4-page">
|
||||||
|
|
||||||
<div class="{$TpStyle.dom_prefix}-p4-top">
|
<div class="{$TpStyle.dom_prefix}-p4-top">
|
||||||
@@ -5,8 +11,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<header class="{$TpStyle.dom_prefix}-p4-info">
|
<header class="{$TpStyle.dom_prefix}-p4-info">
|
||||||
<h1>{$arrVideo.v_name}</h1>
|
<h1>{$strPlayMainHeading}</h1>
|
||||||
<p>{$arrVideo.v_year} · {$arrVideo.v_category} · {$arrVideo.v_area}</p>
|
<p>{$arrVideo.v_year} · {$arrVideo.v_category} · {$arrVideo.v_area} · {$strPlayHeadingMeta}</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{// 下拉线路 }
|
{// 下拉线路 }
|
||||||
@@ -34,12 +40,7 @@
|
|||||||
{volist name='$arrPlayUrlType' id='arrPlayUrl'}
|
{volist name='$arrPlayUrlType' id='arrPlayUrl'}
|
||||||
<?php $epIndex++; ?>
|
<?php $epIndex++; ?>
|
||||||
<li>
|
<li>
|
||||||
<a href='{site:vpurl
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlayModuleSlug, (string)$strPlayGroupName, (int)$epIndex)}"
|
||||||
v_id="$arrVideo.v_id"
|
|
||||||
v_py="$arrVideo.v_name_en"
|
|
||||||
play_type="$strPlayGroupName"
|
|
||||||
play_index="$epIndex"
|
|
||||||
/}'
|
|
||||||
class="{if $epIndex == $Request.route.intPlayIndex}active{/if}"
|
class="{if $epIndex == $Request.route.intPlayIndex}active{/if}"
|
||||||
m3u8="{$arrPlayUrl.url}">
|
m3u8="{$arrPlayUrl.url}">
|
||||||
{$arrPlayUrl.name}
|
{$arrPlayUrl.name}
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
{php}
|
||||||
|
$strPlayModuleSlug = trim((string)($arrVideo['v_name_en'] ?? request()->route('strPinyin') ?? ''));
|
||||||
|
if ($strPlayModuleSlug === '') {
|
||||||
|
$strPlayModuleSlug = (string)($arrVideo['v_id'] ?? '');
|
||||||
|
}
|
||||||
|
{/php}
|
||||||
<article class="{$TpStyle.dom_prefix}-p5-wrap">
|
<article class="{$TpStyle.dom_prefix}-p5-wrap">
|
||||||
|
|
||||||
{// 封面 + 播放器组合卡片 }
|
{// 封面 + 播放器组合卡片 }
|
||||||
@@ -5,13 +11,13 @@
|
|||||||
<div class="{$TpStyle.dom_prefix}-p5-left">
|
<div class="{$TpStyle.dom_prefix}-p5-left">
|
||||||
<img loading="lazy"
|
<img loading="lazy"
|
||||||
src="{site:cfg code='PUBLIC_COVER_DOMAIN' encode='false'/}{$arrVideo.v_pic}"
|
src="{site:cfg code='PUBLIC_COVER_DOMAIN' encode='false'/}{$arrVideo.v_pic}"
|
||||||
alt="{$arrVideo.v_name}"
|
alt="{$strPlayMainHeading}"
|
||||||
class="{$TpStyle.dom_prefix}-p5-pic">
|
class="{$TpStyle.dom_prefix}-p5-pic">
|
||||||
</div>
|
</div>
|
||||||
<div class="{$TpStyle.dom_prefix}-p5-right">
|
<div class="{$TpStyle.dom_prefix}-p5-right">
|
||||||
<h1>{$arrVideo.v_name}</h1>
|
<h1>{$strPlayMainHeading}</h1>
|
||||||
<p class="{$TpStyle.dom_prefix}-p5-meta">
|
<p class="{$TpStyle.dom_prefix}-p5-meta">
|
||||||
{$arrVideo.v_year} · {$arrVideo.v_category} · {$arrVideo.v_area}
|
{$arrVideo.v_year} · {$arrVideo.v_category} · {$arrVideo.v_area} · {$strPlayHeadingMeta}
|
||||||
</p>
|
</p>
|
||||||
<div id="dplayer" class="{$TpStyle.dom_prefix}-p5-dp"></div>
|
<div id="dplayer" class="{$TpStyle.dom_prefix}-p5-dp"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,12 +46,7 @@
|
|||||||
<?php $epIndex=0; ?>
|
<?php $epIndex=0; ?>
|
||||||
{volist name='$arrPlayUrlType' id='arrPlayUrl'}
|
{volist name='$arrPlayUrlType' id='arrPlayUrl'}
|
||||||
<?php $epIndex++; ?>
|
<?php $epIndex++; ?>
|
||||||
<a href='{site:vpurl
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlayModuleSlug, (string)$strPlayGroupName, (int)$epIndex)}"
|
||||||
v_id="$arrVideo.v_id"
|
|
||||||
v_py="$arrVideo.v_name_en"
|
|
||||||
play_type="$strPlayGroupName"
|
|
||||||
play_index="$epIndex"
|
|
||||||
/}'
|
|
||||||
class="{$TpStyle.dom_prefix}-p5-epbtn {if $epIndex == $Request.route.intPlayIndex}active{/if}"
|
class="{$TpStyle.dom_prefix}-p5-epbtn {if $epIndex == $Request.route.intPlayIndex}active{/if}"
|
||||||
m3u8="{$arrPlayUrl.url}">
|
m3u8="{$arrPlayUrl.url}">
|
||||||
{$arrPlayUrl.name}
|
{$arrPlayUrl.name}
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
data-preload="{$pageCfg.player.opt.preload|default='metadata'}"
|
data-preload="{$pageCfg.player.opt.preload|default='metadata'}"
|
||||||
data-ratio="{$pageCfg.player.opt.ratio|default='16x9'}">
|
data-ratio="{$pageCfg.player.opt.ratio|default='16x9'}">
|
||||||
|
|
||||||
|
<h1 style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">
|
||||||
|
{$strPlayMainHeading|default=$arrVideo.v_name|default='正在播放'}
|
||||||
|
</h1>
|
||||||
|
|
||||||
{if $variant == 0}
|
{if $variant == 0}
|
||||||
<div class="{$TpStyle.dom_prefix}-player-core dplayer"></div>
|
<div class="{$TpStyle.dom_prefix}-player-core dplayer"></div>
|
||||||
|
|
||||||
@@ -17,7 +21,7 @@
|
|||||||
|
|
||||||
{elseif $variant == 2}
|
{elseif $variant == 2}
|
||||||
<header class="{$TpStyle.dom_prefix}-player-head">
|
<header class="{$TpStyle.dom_prefix}-player-head">
|
||||||
<h2 class="{$TpStyle.dom_prefix}-player-title">{$arrVideo.v_name|default='正在播放'}</h2>
|
<h2 class="{$TpStyle.dom_prefix}-player-title">{$strPlayMainHeading|default=$arrVideo.v_name|default='正在播放'}</h2>
|
||||||
</header>
|
</header>
|
||||||
<div class="{$TpStyle.dom_prefix}-player-core dplayer"></div>
|
<div class="{$TpStyle.dom_prefix}-player-core dplayer"></div>
|
||||||
|
|
||||||
@@ -30,7 +34,7 @@
|
|||||||
{elseif $variant == 4}
|
{elseif $variant == 4}
|
||||||
<article class="{$TpStyle.dom_prefix}-player-article">
|
<article class="{$TpStyle.dom_prefix}-player-article">
|
||||||
<header class="{$TpStyle.dom_prefix}-player-head">
|
<header class="{$TpStyle.dom_prefix}-player-head">
|
||||||
<h1 class="{$TpStyle.dom_prefix}-player-title">{$arrVideo.v_name|default='正在播放'}</h1>
|
<h2 class="{$TpStyle.dom_prefix}-player-title">{$strPlayMainHeading|default=$arrVideo.v_name|default='正在播放'}</h2>
|
||||||
</header>
|
</header>
|
||||||
<div class="{$TpStyle.dom_prefix}-player-core dplayer"></div>
|
<div class="{$TpStyle.dom_prefix}-player-core dplayer"></div>
|
||||||
</article>
|
</article>
|
||||||
@@ -38,7 +42,7 @@
|
|||||||
{elseif $variant == 5}
|
{elseif $variant == 5}
|
||||||
<figure class="{$TpStyle.dom_prefix}-player-figure">
|
<figure class="{$TpStyle.dom_prefix}-player-figure">
|
||||||
<div class="{$TpStyle.dom_prefix}-player-core dplayer"></div>
|
<div class="{$TpStyle.dom_prefix}-player-core dplayer"></div>
|
||||||
<figcaption class="{$TpStyle.dom_prefix}-player-caption">{$arrVideo.v_name|default='正在播放'}</figcaption>
|
<figcaption class="{$TpStyle.dom_prefix}-player-caption">{$strPlayMainHeading|default=$arrVideo.v_name|default='正在播放'}</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
{elseif $variant == 6}
|
{elseif $variant == 6}
|
||||||
@@ -111,7 +115,7 @@
|
|||||||
|
|
||||||
{elseif $variant == 18}
|
{elseif $variant == 18}
|
||||||
<header class="{$TpStyle.dom_prefix}-player-head">
|
<header class="{$TpStyle.dom_prefix}-player-head">
|
||||||
<h3 class="{$TpStyle.dom_prefix}-player-title">{$arrVideo.v_name|default='正在播放'}</h3>
|
<h2 class="{$TpStyle.dom_prefix}-player-title">{$strPlayMainHeading|default=$arrVideo.v_name|default='正在播放'}</h2>
|
||||||
</header>
|
</header>
|
||||||
<main class="{$TpStyle.dom_prefix}-player-body">
|
<main class="{$TpStyle.dom_prefix}-player-body">
|
||||||
<div class="{$TpStyle.dom_prefix}-player-core dplayer"></div>
|
<div class="{$TpStyle.dom_prefix}-player-core dplayer"></div>
|
||||||
@@ -123,7 +127,7 @@
|
|||||||
{else}
|
{else}
|
||||||
<article class="{$TpStyle.dom_prefix}-player-article">
|
<article class="{$TpStyle.dom_prefix}-player-article">
|
||||||
<header class="{$TpStyle.dom_prefix}-player-head">
|
<header class="{$TpStyle.dom_prefix}-player-head">
|
||||||
<h2 class="{$TpStyle.dom_prefix}-player-title">{$arrVideo.v_name|default='正在播放'}</h2>
|
<h2 class="{$TpStyle.dom_prefix}-player-title">{$strPlayMainHeading|default=$arrVideo.v_name|default='正在播放'}</h2>
|
||||||
</header>
|
</header>
|
||||||
<div class="{$TpStyle.dom_prefix}-player-wrap">
|
<div class="{$TpStyle.dom_prefix}-player-wrap">
|
||||||
<div class="{$TpStyle.dom_prefix}-player-core dplayer"></div>
|
<div class="{$TpStyle.dom_prefix}-player-core dplayer"></div>
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
{php}
|
||||||
|
$strPlaylineSlug = trim((string)($arrVideo['v_name_en'] ?? request()->route('strPinyin') ?? ''));
|
||||||
|
if ($strPlaylineSlug === '') {
|
||||||
|
$strPlaylineSlug = (string)($arrVideo['v_id'] ?? '');
|
||||||
|
}
|
||||||
|
{/php}
|
||||||
<section class="{$TpStyle.dom_prefix}-pl {$TpStyle.dom_prefix}-pl-a">
|
<section class="{$TpStyle.dom_prefix}-pl {$TpStyle.dom_prefix}-pl-a">
|
||||||
|
|
||||||
{assign name="variant" value="$pageCfg.playline.variant"}
|
{assign name="variant" value="$pageCfg.playline.variant"}
|
||||||
@@ -12,7 +18,7 @@
|
|||||||
<ul class="{$TpStyle.dom_prefix}-pl-grid">
|
<ul class="{$TpStyle.dom_prefix}-pl-grid">
|
||||||
{foreach $arrVideo.v_play_url as $line=>$list}
|
{foreach $arrVideo.v_play_url as $line=>$list}
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<li><a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">{$ep.name}</a></li>
|
<li><a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">{$ep.name}</a></li>
|
||||||
{/volist}
|
{/volist}
|
||||||
{/foreach}
|
{/foreach}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -24,7 +30,7 @@
|
|||||||
<h4>{video:getlinename code="$line" encode="false"/}</h4>
|
<h4>{video:getlinename code="$line" encode="false"/}</h4>
|
||||||
<ol>
|
<ol>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<li><a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">{$ep.name}</a></li>
|
<li><a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">{$ep.name}</a></li>
|
||||||
{/volist}
|
{/volist}
|
||||||
</ol>
|
</ol>
|
||||||
</section>
|
</section>
|
||||||
@@ -37,7 +43,7 @@
|
|||||||
<dt>{video:getlinename code="$line" encode="false"/}</dt>
|
<dt>{video:getlinename code="$line" encode="false"/}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">{$ep.name}</a>
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">{$ep.name}</a>
|
||||||
{/volist}
|
{/volist}
|
||||||
</dd>
|
</dd>
|
||||||
{/foreach}
|
{/foreach}
|
||||||
@@ -49,7 +55,7 @@
|
|||||||
<strong>{video:getlinename code="$line" encode="false"/}</strong>
|
<strong>{video:getlinename code="$line" encode="false"/}</strong>
|
||||||
<ul>
|
<ul>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<li><a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">{$ep.name}</a></li>
|
<li><a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">{$ep.name}</a></li>
|
||||||
{/volist}
|
{/volist}
|
||||||
</ul>
|
</ul>
|
||||||
{/foreach}
|
{/foreach}
|
||||||
@@ -61,7 +67,7 @@
|
|||||||
<header>{video:getlinename code="$line" encode="false"/}</header>
|
<header>{video:getlinename code="$line" encode="false"/}</header>
|
||||||
<div>
|
<div>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">{$ep.name}</a>
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">{$ep.name}</a>
|
||||||
{/volist}
|
{/volist}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
@@ -75,7 +81,7 @@
|
|||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{$i}</td>
|
<td>{$i}</td>
|
||||||
<td><a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">{$ep.name}</a></td>
|
<td><a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">{$ep.name}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{/volist}
|
{/volist}
|
||||||
{/foreach}
|
{/foreach}
|
||||||
@@ -87,7 +93,7 @@
|
|||||||
<nav>
|
<nav>
|
||||||
<b>{video:getlinename code="$line" encode="false"/}</b>
|
<b>{video:getlinename code="$line" encode="false"/}</b>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">{$ep.name}</a>
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">{$ep.name}</a>
|
||||||
{/volist}
|
{/volist}
|
||||||
</nav>
|
</nav>
|
||||||
{/foreach}
|
{/foreach}
|
||||||
@@ -98,7 +104,7 @@
|
|||||||
<p>{video:getlinename code="$line" encode="false"/}:</p>
|
<p>{video:getlinename code="$line" encode="false"/}:</p>
|
||||||
<div>
|
<div>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<span><a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">{$ep.name}</a></span>
|
<span><a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">{$ep.name}</a></span>
|
||||||
{/volist}
|
{/volist}
|
||||||
</div>
|
</div>
|
||||||
{/foreach}
|
{/foreach}
|
||||||
@@ -111,7 +117,7 @@
|
|||||||
{video:getlinename code="$line" encode="false"/}
|
{video:getlinename code="$line" encode="false"/}
|
||||||
<ul>
|
<ul>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<li><a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">{$ep.name}</a></li>
|
<li><a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">{$ep.name}</a></li>
|
||||||
{/volist}
|
{/volist}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
@@ -123,7 +129,7 @@
|
|||||||
{foreach $arrVideo.v_play_url as $line=>$list}
|
{foreach $arrVideo.v_play_url as $line=>$list}
|
||||||
<h5>{video:getlinename code="$line" encode="false"/}</h5>
|
<h5>{video:getlinename code="$line" encode="false"/}</h5>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">{$ep.name}</a>
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">{$ep.name}</a>
|
||||||
{/volist}
|
{/volist}
|
||||||
{/foreach}
|
{/foreach}
|
||||||
|
|
||||||
@@ -133,7 +139,7 @@
|
|||||||
<section>
|
<section>
|
||||||
<h4>{video:getlinename code="$line" encode="false"/}</h4>
|
<h4>{video:getlinename code="$line" encode="false"/}</h4>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">{$ep.name}</a>
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">{$ep.name}</a>
|
||||||
{/volist}
|
{/volist}
|
||||||
</section>
|
</section>
|
||||||
{/foreach}
|
{/foreach}
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
{php}
|
||||||
|
$strPlaylineSlug = trim((string)($arrVideo['v_name_en'] ?? request()->route('strPinyin') ?? ''));
|
||||||
|
if ($strPlaylineSlug === '') {
|
||||||
|
$strPlaylineSlug = (string)($arrVideo['v_id'] ?? '');
|
||||||
|
}
|
||||||
|
{/php}
|
||||||
<section class="{$TpStyle.dom_prefix}-pl {$TpStyle.dom_prefix}-pl-b">
|
<section class="{$TpStyle.dom_prefix}-pl {$TpStyle.dom_prefix}-pl-b">
|
||||||
|
|
||||||
{assign name="variant" value="$pageCfg.playline.variant"}
|
{assign name="variant" value="$pageCfg.playline.variant"}
|
||||||
@@ -14,7 +20,7 @@
|
|||||||
{foreach $arrVideo.v_play_url as $line=>$list}
|
{foreach $arrVideo.v_play_url as $line=>$list}
|
||||||
<ul class="{$TpStyle.dom_prefix}-pl-list">
|
<ul class="{$TpStyle.dom_prefix}-pl-list">
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<li><a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">{$ep.name}</a></li>
|
<li><a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">{$ep.name}</a></li>
|
||||||
{/volist}
|
{/volist}
|
||||||
</ul>
|
</ul>
|
||||||
{/foreach}
|
{/foreach}
|
||||||
@@ -32,7 +38,7 @@
|
|||||||
<ul class="{$TpStyle.dom_prefix}-pl-episodes">
|
<ul class="{$TpStyle.dom_prefix}-pl-episodes">
|
||||||
{foreach $arrVideo.v_play_url as $line=>$list}
|
{foreach $arrVideo.v_play_url as $line=>$list}
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<li><a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">{$ep.name}</a></li>
|
<li><a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">{$ep.name}</a></li>
|
||||||
{/volist}
|
{/volist}
|
||||||
{/foreach}
|
{/foreach}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -45,7 +51,7 @@
|
|||||||
<dt>{video:getlinename code="$line" encode="false"/}</dt>
|
<dt>{video:getlinename code="$line" encode="false"/}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">{$ep.name}</a>
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">{$ep.name}</a>
|
||||||
{/volist}
|
{/volist}
|
||||||
</dd>
|
</dd>
|
||||||
{/foreach}
|
{/foreach}
|
||||||
@@ -58,7 +64,7 @@
|
|||||||
<header>{video:getlinename code="$line" encode="false"/}</header>
|
<header>{video:getlinename code="$line" encode="false"/}</header>
|
||||||
<div>
|
<div>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<span><a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">{$ep.name}</a></span>
|
<span><a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">{$ep.name}</a></span>
|
||||||
{/volist}
|
{/volist}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -70,7 +76,7 @@
|
|||||||
{foreach $arrVideo.v_play_url as $line=>$list}
|
{foreach $arrVideo.v_play_url as $line=>$list}
|
||||||
<tr><th>{video:getlinename code="$line" encode="false"/}</th><td>
|
<tr><th>{video:getlinename code="$line" encode="false"/}</th><td>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">{$ep.name}</a>
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">{$ep.name}</a>
|
||||||
{/volist}
|
{/volist}
|
||||||
</td></tr>
|
</td></tr>
|
||||||
{/foreach}
|
{/foreach}
|
||||||
@@ -87,7 +93,7 @@
|
|||||||
<section>
|
<section>
|
||||||
{foreach $arrVideo.v_play_url as $line=>$list}
|
{foreach $arrVideo.v_play_url as $line=>$list}
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">{$ep.name}</a>
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">{$ep.name}</a>
|
||||||
{/volist}
|
{/volist}
|
||||||
{/foreach}
|
{/foreach}
|
||||||
</section>
|
</section>
|
||||||
@@ -103,7 +109,7 @@
|
|||||||
<ol class="{$TpStyle.dom_prefix}-pl-episodes">
|
<ol class="{$TpStyle.dom_prefix}-pl-episodes">
|
||||||
{foreach $arrVideo.v_play_url as $line=>$list}
|
{foreach $arrVideo.v_play_url as $line=>$list}
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<li><a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">{$ep.name}</a></li>
|
<li><a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">{$ep.name}</a></li>
|
||||||
{/volist}
|
{/volist}
|
||||||
{/foreach}
|
{/foreach}
|
||||||
</ol>
|
</ol>
|
||||||
@@ -114,7 +120,7 @@
|
|||||||
<p><b>{video:getlinename code="$line" encode="false"/}</b></p>
|
<p><b>{video:getlinename code="$line" encode="false"/}</b></p>
|
||||||
<p>
|
<p>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">{$ep.name}</a>
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">{$ep.name}</a>
|
||||||
{/volist}
|
{/volist}
|
||||||
</p>
|
</p>
|
||||||
{/foreach}
|
{/foreach}
|
||||||
@@ -126,7 +132,7 @@
|
|||||||
<tr><td>{video:getlinename code="$line" encode="false"/}</td><td>
|
<tr><td>{video:getlinename code="$line" encode="false"/}</td><td>
|
||||||
<table>
|
<table>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<tr><td><a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">{$ep.name}</a></td></tr>
|
<tr><td><a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">{$ep.name}</a></td></tr>
|
||||||
{/volist}
|
{/volist}
|
||||||
</table>
|
</table>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
@@ -140,7 +146,7 @@
|
|||||||
<h3>{video:getlinename code="$line" encode="false"/}</h3>
|
<h3>{video:getlinename code="$line" encode="false"/}</h3>
|
||||||
<p>
|
<p>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">{$ep.name}</a>
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">{$ep.name}</a>
|
||||||
{/volist}
|
{/volist}
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
@@ -152,7 +158,7 @@
|
|||||||
<div class="{if $intLineIndex==0}active{/if}">
|
<div class="{if $intLineIndex==0}active{/if}">
|
||||||
<strong>{video:getlinename code="$line" encode="false"/}</strong>
|
<strong>{video:getlinename code="$line" encode="false"/}</strong>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">{$ep.name}</a>
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">{$ep.name}</a>
|
||||||
{/volist}
|
{/volist}
|
||||||
</div>
|
</div>
|
||||||
{php}$intLineIndex++;{/php}
|
{php}$intLineIndex++;{/php}
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
{php}
|
||||||
|
$strPlaylineSlug = trim((string)($arrVideo['v_name_en'] ?? request()->route('strPinyin') ?? ''));
|
||||||
|
if ($strPlaylineSlug === '') {
|
||||||
|
$strPlaylineSlug = (string)($arrVideo['v_id'] ?? '');
|
||||||
|
}
|
||||||
|
{/php}
|
||||||
<section class="{$TpStyle.dom_prefix}-pl {$TpStyle.dom_prefix}-pl-c">
|
<section class="{$TpStyle.dom_prefix}-pl {$TpStyle.dom_prefix}-pl-c">
|
||||||
|
|
||||||
{assign name="variant" value="$pageCfg.playline.variant"}
|
{assign name="variant" value="$pageCfg.playline.variant"}
|
||||||
@@ -10,7 +16,7 @@
|
|||||||
<ol>
|
<ol>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<li>
|
<li>
|
||||||
<a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">
|
||||||
{$ep.name}
|
{$ep.name}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -27,7 +33,7 @@
|
|||||||
<div>
|
<div>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<p>
|
<p>
|
||||||
<a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">
|
||||||
{$ep.name}
|
{$ep.name}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -43,7 +49,7 @@
|
|||||||
<dt>{video:getlinename code="$line" encode="false"/}</dt>
|
<dt>{video:getlinename code="$line" encode="false"/}</dt>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<dd>
|
<dd>
|
||||||
<a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">
|
||||||
{$ep.name}
|
{$ep.name}
|
||||||
</a>
|
</a>
|
||||||
</dd>
|
</dd>
|
||||||
@@ -58,7 +64,7 @@
|
|||||||
<p>
|
<p>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<span>
|
<span>
|
||||||
<a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">
|
||||||
{$ep.name}
|
{$ep.name}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
@@ -73,7 +79,7 @@
|
|||||||
<strong>{video:getlinename code="$line" encode="false"/}</strong>
|
<strong>{video:getlinename code="$line" encode="false"/}</strong>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<div>
|
<div>
|
||||||
<a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">
|
||||||
{$ep.name}
|
{$ep.name}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,7 +95,7 @@
|
|||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
<a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">
|
||||||
{$ep.name}
|
{$ep.name}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -106,7 +112,7 @@
|
|||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<div>
|
<div>
|
||||||
<time>{$i}</time>
|
<time>{$i}</time>
|
||||||
<a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">
|
||||||
{$ep.name}
|
{$ep.name}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,7 +128,7 @@
|
|||||||
<p>
|
<p>
|
||||||
本线路提供以下资源:
|
本线路提供以下资源:
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">
|
||||||
{$ep.name}
|
{$ep.name}
|
||||||
</a>
|
</a>
|
||||||
{/volist}
|
{/volist}
|
||||||
@@ -137,7 +143,7 @@
|
|||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<p>
|
<p>
|
||||||
第 {$i} 集:
|
第 {$i} 集:
|
||||||
<a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">
|
||||||
{$ep.name}
|
{$ep.name}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -149,7 +155,7 @@
|
|||||||
{foreach $arrVideo.v_play_url as $line=>$list}
|
{foreach $arrVideo.v_play_url as $line=>$list}
|
||||||
<h5>{video:getlinename code="$line" encode="false"/}</h5>
|
<h5>{video:getlinename code="$line" encode="false"/}</h5>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">
|
||||||
{$ep.name}
|
{$ep.name}
|
||||||
</a>
|
</a>
|
||||||
{/volist}
|
{/volist}
|
||||||
@@ -161,7 +167,7 @@
|
|||||||
<section>
|
<section>
|
||||||
<strong>{video:getlinename code="$line" encode="false"/} : </strong>
|
<strong>{video:getlinename code="$line" encode="false"/} : </strong>
|
||||||
{volist name="list" id="ep" key="i"}
|
{volist name="list" id="ep" key="i"}
|
||||||
<a href="{site:vpurl v_id='$arrVideo.v_id' play_type='$line' play_index='$i'/}">
|
<a href="{:app('app\\services\\VideoService')->getVideoPlayUrl((int)$arrVideo['v_id'], $strPlaylineSlug, (string)$line, (int)$i)}">
|
||||||
{$ep.name}
|
{$ep.name}
|
||||||
</a>
|
</a>
|
||||||
{/volist}
|
{/volist}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{case value="0"}
|
{case value="0"}
|
||||||
<header class='__PFX__-sh-c0'>
|
<header class='__PFX__-sh-c0'>
|
||||||
<div class='wrap'>
|
<div class='wrap'>
|
||||||
<h1><span class='__PFX__-k is-pill'>{$Request.get.keyword}</span></h1>
|
<h1><span class='__PFX__-k is-pill'>{$Request.get.keyword}</span> 搜索结果</h1>
|
||||||
<p>搜索结果</p>
|
<p>搜索结果</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -11,21 +11,21 @@
|
|||||||
<section class='__PFX__-sh-c1'>
|
<section class='__PFX__-sh-c1'>
|
||||||
<div class='center'>
|
<div class='center'>
|
||||||
<h2>Search</h2>
|
<h2>Search</h2>
|
||||||
<h1><mark class='__PFX__-k is-pill'>{$Request.get.keyword}</mark></h1>
|
<h1><mark class='__PFX__-k is-pill'>{$Request.get.keyword}</mark> 搜索结果</h1>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{/case}
|
{/case}
|
||||||
{case value="2"}
|
{case value="2"}
|
||||||
<div class='__PFX__-sh-c2'>
|
<div class='__PFX__-sh-c2'>
|
||||||
<div class='center'>
|
<div class='center'>
|
||||||
<h1>“<strong class='__PFX__-k is-pill'>{$Request.get.keyword}</strong>”</h1><small>Results</small>
|
<h1>“<strong class='__PFX__-k is-pill'>{$Request.get.keyword}</strong>” 搜索结果</h1><small>Results</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/case}
|
{/case}
|
||||||
{case value="3"}
|
{case value="3"}
|
||||||
<header class='__PFX__-sh-c3'>
|
<header class='__PFX__-sh-c3'>
|
||||||
<div class='center'>
|
<div class='center'>
|
||||||
<h1><em class='__PFX__-k is-pill'>{$Request.get.keyword}</em></h1>
|
<h1><em class='__PFX__-k is-pill'>{$Request.get.keyword}</em> 搜索结果</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class='underline' aria-hidden='true'></div>
|
<div class='underline' aria-hidden='true'></div>
|
||||||
</header>
|
</header>
|
||||||
@@ -34,14 +34,14 @@
|
|||||||
<section class='__PFX__-sh-c4'>
|
<section class='__PFX__-sh-c4'>
|
||||||
<div class='center'>
|
<div class='center'>
|
||||||
<p class='kicker'>关键词</p>
|
<p class='kicker'>关键词</p>
|
||||||
<h1><span class='__PFX__-k is-pill' aria-label='keyword'>{$Request.get.keyword}</span></h1>
|
<h1><span class='__PFX__-k is-pill' aria-label='keyword'>{$Request.get.keyword}</span> 搜索结果</h1>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{/case}
|
{/case}
|
||||||
{case value="5"}
|
{case value="5"}
|
||||||
<header class='__PFX__-sh-c5'>
|
<header class='__PFX__-sh-c5'>
|
||||||
<div class='center'>
|
<div class='center'>
|
||||||
<h1>搜索:<span class='__PFX__-k is-pill'>{$Request.get.keyword}</span></h1>
|
<h1><span class='__PFX__-k is-pill'>{$Request.get.keyword}</span> 搜索结果</h1>
|
||||||
<p class='sub'>为你呈现匹配内容</p>
|
<p class='sub'>为你呈现匹配内容</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
{case value="6"}
|
{case value="6"}
|
||||||
<section class='__PFX__-sh-c6'>
|
<section class='__PFX__-sh-c6'>
|
||||||
<div class='center'>
|
<div class='center'>
|
||||||
<h1><mark class='__PFX__-k is-pill'>{$Request.get.keyword}</mark></h1>
|
<h1><mark class='__PFX__-k is-pill'>{$Request.get.keyword}</mark> 搜索结果</h1>
|
||||||
<div class='dots' aria-hidden='true'><span></span><span></span><span></span></div>
|
<div class='dots' aria-hidden='true'><span></span><span></span><span></span></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -58,14 +58,14 @@
|
|||||||
<header class='__PFX__-sh-c7'>
|
<header class='__PFX__-sh-c7'>
|
||||||
<div class='center'>
|
<div class='center'>
|
||||||
<nav aria-label='breadcrumb'><span>搜索</span></nav>
|
<nav aria-label='breadcrumb'><span>搜索</span></nav>
|
||||||
<h1><strong class='__PFX__-k is-pill'>{$Request.get.keyword}</strong></h1>
|
<h1><strong class='__PFX__-k is-pill'>{$Request.get.keyword}</strong> 搜索结果</h1>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{/case}
|
{/case}
|
||||||
{case value="8"}
|
{case value="8"}
|
||||||
<section class='__PFX__-sh-c8'>
|
<section class='__PFX__-sh-c8'>
|
||||||
<div class='center card'>
|
<div class='center card'>
|
||||||
<h1><em class='__PFX__-k is-pill'>{$Request.get.keyword}</em></h1>
|
<h1><em class='__PFX__-k is-pill'>{$Request.get.keyword}</em> 搜索结果</h1>
|
||||||
<p>搜索结果页</p>
|
<p>搜索结果页</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
{case value="9"}
|
{case value="9"}
|
||||||
<div class='__PFX__-sh-c9'>
|
<div class='__PFX__-sh-c9'>
|
||||||
<div class='center'>
|
<div class='center'>
|
||||||
<h1><span class='__PFX__-k is-pill' aria-label='keyword'>{$Request.get.keyword}</span></h1>
|
<h1><span class='__PFX__-k is-pill' aria-label='keyword'>{$Request.get.keyword}</span> 搜索结果</h1>
|
||||||
<p class='sub'>精选匹配条目</p><a class='btn' href='javascript:void(0)' rel='nofollow'>换词</a>
|
<p class='sub'>精选匹配条目</p><a class='btn' href='javascript:void(0)' rel='nofollow'>换词</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,14 +82,14 @@
|
|||||||
<header class='__PFX__-sh-c10'>
|
<header class='__PFX__-sh-c10'>
|
||||||
<div class='center'>
|
<div class='center'>
|
||||||
<h2>站内检索</h2>
|
<h2>站内检索</h2>
|
||||||
<h1><span class='__PFX__-k is-pill'>{$Request.get.keyword}</span></h1>
|
<h1><span class='__PFX__-k is-pill'>{$Request.get.keyword}</span> 搜索结果</h1>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{/case}
|
{/case}
|
||||||
{case value="11"}
|
{case value="11"}
|
||||||
<section class='__PFX__-sh-c11'>
|
<section class='__PFX__-sh-c11'>
|
||||||
<div class='center'>
|
<div class='center'>
|
||||||
<h1><mark class='__PFX__-k is-pill'>{$Request.get.keyword}</mark></h1>
|
<h1><mark class='__PFX__-k is-pill'>{$Request.get.keyword}</mark> 搜索结果</h1>
|
||||||
<p class='sub'>相关搜索结果</p>
|
<p class='sub'>相关搜索结果</p>
|
||||||
</div><svg class='sig' aria-hidden='true'></svg>
|
</div><svg class='sig' aria-hidden='true'></svg>
|
||||||
</section>
|
</section>
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
{case value="12"}
|
{case value="12"}
|
||||||
<header class='__PFX__-sh-c12'>
|
<header class='__PFX__-sh-c12'>
|
||||||
<div class='center'>
|
<div class='center'>
|
||||||
<h1><strong class='__PFX__-k is-pill'>{$Request.get.keyword}</strong></h1>
|
<h1><strong class='__PFX__-k is-pill'>{$Request.get.keyword}</strong> 搜索结果</h1>
|
||||||
<p class='sub'>结果列表</p>
|
<p class='sub'>结果列表</p>
|
||||||
</div>
|
</div>
|
||||||
<div class='frame' aria-hidden='true'></div>
|
<div class='frame' aria-hidden='true'></div>
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
{case value="13"}
|
{case value="13"}
|
||||||
<section class='__PFX__-sh-c13'>
|
<section class='__PFX__-sh-c13'>
|
||||||
<div class='center'>
|
<div class='center'>
|
||||||
<h1><em class='__PFX__-k is-pill'>{$Request.get.keyword}</em></h1>
|
<h1><em class='__PFX__-k is-pill'>{$Request.get.keyword}</em> 搜索结果</h1>
|
||||||
<p class='meta'>Search Result Page</p>
|
<p class='meta'>Search Result Page</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
{case value="14"}
|
{case value="14"}
|
||||||
<header class='__PFX__-sh-c14'>
|
<header class='__PFX__-sh-c14'>
|
||||||
<div class='center'>
|
<div class='center'>
|
||||||
<h1><span class='__PFX__-k is-pill' aria-label='keyword'>{$Request.get.keyword}</span></h1>
|
<h1><span class='__PFX__-k is-pill' aria-label='keyword'>{$Request.get.keyword}</span> 搜索结果</h1>
|
||||||
<ul class='tabs'>
|
<ul class='tabs'>
|
||||||
<li>全部</li>
|
<li>全部</li>
|
||||||
<li>相关</li>
|
<li>相关</li>
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
{case value="15"}
|
{case value="15"}
|
||||||
<section class='__PFX__-sh-c15'>
|
<section class='__PFX__-sh-c15'>
|
||||||
<div class='center'>
|
<div class='center'>
|
||||||
<h1><span class='__PFX__-k is-pill'>{$Request.get.keyword}</span></h1>
|
<h1><span class='__PFX__-k is-pill'>{$Request.get.keyword}</span> 搜索结果</h1>
|
||||||
<details open>
|
<details open>
|
||||||
<summary>展开提示</summary>
|
<summary>展开提示</summary>
|
||||||
<p>结果展示</p>
|
<p>结果展示</p>
|
||||||
@@ -136,7 +136,7 @@
|
|||||||
{case value="16"}
|
{case value="16"}
|
||||||
<header class='__PFX__-sh-c16'>
|
<header class='__PFX__-sh-c16'>
|
||||||
<div class='center'>
|
<div class='center'>
|
||||||
<h1><mark class='__PFX__-k is-pill'>{$Request.get.keyword}</mark></h1>
|
<h1><mark class='__PFX__-k is-pill'>{$Request.get.keyword}</mark> 搜索结果</h1>
|
||||||
<p class='sub'>按相关度排序</p>
|
<p class='sub'>按相关度排序</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
{case value="17"}
|
{case value="17"}
|
||||||
<div class='__PFX__-sh-c17' role='banner'>
|
<div class='__PFX__-sh-c17' role='banner'>
|
||||||
<div class='center'>
|
<div class='center'>
|
||||||
<h1><strong class='__PFX__-k is-pill'>{$Request.get.keyword}</strong></h1>
|
<h1><strong class='__PFX__-k is-pill'>{$Request.get.keyword}</strong> 搜索结果</h1>
|
||||||
<p class='sub'>即时匹配</p>
|
<p class='sub'>即时匹配</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -152,7 +152,7 @@
|
|||||||
{case value="18"}
|
{case value="18"}
|
||||||
<section class='__PFX__-sh-c18'>
|
<section class='__PFX__-sh-c18'>
|
||||||
<div class='center'>
|
<div class='center'>
|
||||||
<h1><em class='__PFX__-k is-pill'>{$Request.get.keyword}</em></h1>
|
<h1><em class='__PFX__-k is-pill'>{$Request.get.keyword}</em> 搜索结果</h1>
|
||||||
<p class='sub'>搜索结果</p>
|
<p class='sub'>搜索结果</p>
|
||||||
<div class='pillrow'><span>搜索</span><span>结果</span></div>
|
<div class='pillrow'><span>搜索</span><span>结果</span></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -161,7 +161,7 @@
|
|||||||
{case value="19"}
|
{case value="19"}
|
||||||
<header class='__PFX__-sh-c19'>
|
<header class='__PFX__-sh-c19'>
|
||||||
<div class='center'>
|
<div class='center'>
|
||||||
<h1><span class='__PFX__-k is-pill' aria-label='keyword'>{$Request.get.keyword}</span></h1>
|
<h1><span class='__PFX__-k is-pill' aria-label='keyword'>{$Request.get.keyword}</span> 搜索结果</h1>
|
||||||
</div>
|
</div>
|
||||||
<p class='sub center'>结果页</p>
|
<p class='sub center'>结果页</p>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
{switch $variant}
|
{switch $variant}
|
||||||
{case value="0"}
|
{case value="0"}
|
||||||
<section class='__PFX__-sh-m0'>
|
<section class='__PFX__-sh-m0'>
|
||||||
|
<h1 style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">
|
||||||
|
{$Request.get.keyword} 搜索结果
|
||||||
|
</h1>
|
||||||
<figure>
|
<figure>
|
||||||
<figcaption>🔍 <span class='__PFX__-k is-accent'>{$Request.get.keyword}</span></figcaption>
|
<figcaption>🔍 <span class='__PFX__-k is-accent'>{$Request.get.keyword}</span></figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
@@ -8,6 +11,9 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="1"}
|
{case value="1"}
|
||||||
<section class='__PFX__-sh-m1'>
|
<section class='__PFX__-sh-m1'>
|
||||||
|
<h1 style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">
|
||||||
|
{$Request.get.keyword} 搜索结果
|
||||||
|
</h1>
|
||||||
<figure>
|
<figure>
|
||||||
<div class='media'></div>
|
<div class='media'></div>
|
||||||
<figcaption><mark class='__PFX__-k is-accent'>{$Request.get.keyword}</mark> · 搜索结果</figcaption>
|
<figcaption><mark class='__PFX__-k is-accent'>{$Request.get.keyword}</mark> · 搜索结果</figcaption>
|
||||||
@@ -16,6 +22,9 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="2"}
|
{case value="2"}
|
||||||
<div class='__PFX__-sh-m2'>
|
<div class='__PFX__-sh-m2'>
|
||||||
|
<h1 style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">
|
||||||
|
{$Request.get.keyword} 搜索结果
|
||||||
|
</h1>
|
||||||
<figure>
|
<figure>
|
||||||
<figcaption><span class='ico' aria-hidden='true'>⌕</span><strong
|
<figcaption><span class='ico' aria-hidden='true'>⌕</span><strong
|
||||||
class='__PFX__-k is-accent'>{$Request.get.keyword}</strong></figcaption>
|
class='__PFX__-k is-accent'>{$Request.get.keyword}</strong></figcaption>
|
||||||
@@ -25,6 +34,9 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="3"}
|
{case value="3"}
|
||||||
<section class='__PFX__-sh-m3'>
|
<section class='__PFX__-sh-m3'>
|
||||||
|
<h1 style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">
|
||||||
|
{$Request.get.keyword} 搜索结果
|
||||||
|
</h1>
|
||||||
<figure>
|
<figure>
|
||||||
<figcaption>Search: <em class='__PFX__-k is-accent'>{$Request.get.keyword}</em></figcaption><small>Results</small>
|
<figcaption>Search: <em class='__PFX__-k is-accent'>{$Request.get.keyword}</em></figcaption><small>Results</small>
|
||||||
</figure>
|
</figure>
|
||||||
@@ -32,6 +44,9 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="4"}
|
{case value="4"}
|
||||||
<div class='__PFX__-sh-m4'>
|
<div class='__PFX__-sh-m4'>
|
||||||
|
<h1 style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">
|
||||||
|
{$Request.get.keyword} 搜索结果
|
||||||
|
</h1>
|
||||||
<figure>
|
<figure>
|
||||||
<figcaption><span class='__PFX__-k is-accent'>{$Request.get.keyword}</span></figcaption>
|
<figcaption><span class='__PFX__-k is-accent'>{$Request.get.keyword}</span></figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
@@ -40,6 +55,9 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="5"}
|
{case value="5"}
|
||||||
<section class='__PFX__-sh-m5'>
|
<section class='__PFX__-sh-m5'>
|
||||||
|
<h1 style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">
|
||||||
|
{$Request.get.keyword} 搜索结果
|
||||||
|
</h1>
|
||||||
<figure>
|
<figure>
|
||||||
<figcaption>“<span class='__PFX__-k is-accent'>{$Request.get.keyword}</span>”</figcaption>
|
<figcaption>“<span class='__PFX__-k is-accent'>{$Request.get.keyword}</span>”</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
@@ -48,6 +66,9 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="6"}
|
{case value="6"}
|
||||||
<div class='__PFX__-sh-m6'>
|
<div class='__PFX__-sh-m6'>
|
||||||
|
<h1 style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">
|
||||||
|
{$Request.get.keyword} 搜索结果
|
||||||
|
</h1>
|
||||||
<figure>
|
<figure>
|
||||||
<figcaption><mark class='__PFX__-k is-accent'>{$Request.get.keyword}</mark></figcaption>
|
<figcaption><mark class='__PFX__-k is-accent'>{$Request.get.keyword}</mark></figcaption>
|
||||||
<p class='sub'>搜索结果页</p>
|
<p class='sub'>搜索结果页</p>
|
||||||
@@ -56,6 +77,9 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="7"}
|
{case value="7"}
|
||||||
<section class='__PFX__-sh-m7'>
|
<section class='__PFX__-sh-m7'>
|
||||||
|
<h1 style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">
|
||||||
|
{$Request.get.keyword} 搜索结果
|
||||||
|
</h1>
|
||||||
<figure>
|
<figure>
|
||||||
<figcaption><strong class='__PFX__-k is-accent'>{$Request.get.keyword}</strong></figcaption>
|
<figcaption><strong class='__PFX__-k is-accent'>{$Request.get.keyword}</strong></figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
@@ -64,6 +88,9 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="8"}
|
{case value="8"}
|
||||||
<div class='__PFX__-sh-m8'>
|
<div class='__PFX__-sh-m8'>
|
||||||
|
<h1 style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">
|
||||||
|
{$Request.get.keyword} 搜索结果
|
||||||
|
</h1>
|
||||||
<figure>
|
<figure>
|
||||||
<figcaption><span class='tag'>TAG</span><em class='__PFX__-k is-accent'>{$Request.get.keyword}</em></figcaption>
|
<figcaption><span class='tag'>TAG</span><em class='__PFX__-k is-accent'>{$Request.get.keyword}</em></figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
@@ -71,6 +98,9 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="9"}
|
{case value="9"}
|
||||||
<section class='__PFX__-sh-m9'>
|
<section class='__PFX__-sh-m9'>
|
||||||
|
<h1 style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">
|
||||||
|
{$Request.get.keyword} 搜索结果
|
||||||
|
</h1>
|
||||||
<figure>
|
<figure>
|
||||||
<figcaption><span class='__PFX__-k is-accent'>{$Request.get.keyword}</span></figcaption>
|
<figcaption><span class='__PFX__-k is-accent'>{$Request.get.keyword}</span></figcaption>
|
||||||
<div class='stroke' aria-hidden='true'></div>
|
<div class='stroke' aria-hidden='true'></div>
|
||||||
@@ -79,6 +109,9 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="10"}
|
{case value="10"}
|
||||||
<div class='__PFX__-sh-m10'>
|
<div class='__PFX__-sh-m10'>
|
||||||
|
<h1 style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">
|
||||||
|
{$Request.get.keyword} 搜索结果
|
||||||
|
</h1>
|
||||||
<figure>
|
<figure>
|
||||||
<figcaption><span class='__PFX__-k is-accent'>{$Request.get.keyword}</span></figcaption>
|
<figcaption><span class='__PFX__-k is-accent'>{$Request.get.keyword}</span></figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
@@ -90,6 +123,9 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="11"}
|
{case value="11"}
|
||||||
<section class='__PFX__-sh-m11'>
|
<section class='__PFX__-sh-m11'>
|
||||||
|
<h1 style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">
|
||||||
|
{$Request.get.keyword} 搜索结果
|
||||||
|
</h1>
|
||||||
<figure>
|
<figure>
|
||||||
<figcaption><mark class='__PFX__-k is-accent'>{$Request.get.keyword}</mark></figcaption>
|
<figcaption><mark class='__PFX__-k is-accent'>{$Request.get.keyword}</mark></figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
@@ -101,6 +137,9 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="12"}
|
{case value="12"}
|
||||||
<div class='__PFX__-sh-m12'>
|
<div class='__PFX__-sh-m12'>
|
||||||
|
<h1 style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">
|
||||||
|
{$Request.get.keyword} 搜索结果
|
||||||
|
</h1>
|
||||||
<figure>
|
<figure>
|
||||||
<figcaption><strong class='__PFX__-k is-accent'>{$Request.get.keyword}</strong></figcaption>
|
<figcaption><strong class='__PFX__-k is-accent'>{$Request.get.keyword}</strong></figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
@@ -109,6 +148,9 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="13"}
|
{case value="13"}
|
||||||
<section class='__PFX__-sh-m13'>
|
<section class='__PFX__-sh-m13'>
|
||||||
|
<h1 style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">
|
||||||
|
{$Request.get.keyword} 搜索结果
|
||||||
|
</h1>
|
||||||
<figure>
|
<figure>
|
||||||
<figcaption><em class='__PFX__-k is-accent'>{$Request.get.keyword}</em></figcaption>
|
<figcaption><em class='__PFX__-k is-accent'>{$Request.get.keyword}</em></figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
@@ -117,6 +159,9 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="14"}
|
{case value="14"}
|
||||||
<div class='__PFX__-sh-m14'>
|
<div class='__PFX__-sh-m14'>
|
||||||
|
<h1 style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">
|
||||||
|
{$Request.get.keyword} 搜索结果
|
||||||
|
</h1>
|
||||||
<figure>
|
<figure>
|
||||||
<figcaption>🔎 <span class='__PFX__-k is-accent'>{$Request.get.keyword}</span></figcaption>
|
<figcaption>🔎 <span class='__PFX__-k is-accent'>{$Request.get.keyword}</span></figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
@@ -125,6 +170,9 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="15"}
|
{case value="15"}
|
||||||
<section class='__PFX__-sh-m15'>
|
<section class='__PFX__-sh-m15'>
|
||||||
|
<h1 style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">
|
||||||
|
{$Request.get.keyword} 搜索结果
|
||||||
|
</h1>
|
||||||
<figure>
|
<figure>
|
||||||
<figcaption><span class='__PFX__-k is-accent'>{$Request.get.keyword}</span></figcaption>
|
<figcaption><span class='__PFX__-k is-accent'>{$Request.get.keyword}</span></figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
@@ -133,6 +181,9 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="16"}
|
{case value="16"}
|
||||||
<div class='__PFX__-sh-m16'>
|
<div class='__PFX__-sh-m16'>
|
||||||
|
<h1 style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">
|
||||||
|
{$Request.get.keyword} 搜索结果
|
||||||
|
</h1>
|
||||||
<figure>
|
<figure>
|
||||||
<figcaption><mark class='__PFX__-k is-accent'>{$Request.get.keyword}</mark></figcaption>
|
<figcaption><mark class='__PFX__-k is-accent'>{$Request.get.keyword}</mark></figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
@@ -142,6 +193,9 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="17"}
|
{case value="17"}
|
||||||
<section class='__PFX__-sh-m17'>
|
<section class='__PFX__-sh-m17'>
|
||||||
|
<h1 style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">
|
||||||
|
{$Request.get.keyword} 搜索结果
|
||||||
|
</h1>
|
||||||
<figure>
|
<figure>
|
||||||
<figcaption><strong class='__PFX__-k is-accent'>{$Request.get.keyword}</strong></figcaption>
|
<figcaption><strong class='__PFX__-k is-accent'>{$Request.get.keyword}</strong></figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
@@ -150,6 +204,9 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="18"}
|
{case value="18"}
|
||||||
<div class='__PFX__-sh-m18'>
|
<div class='__PFX__-sh-m18'>
|
||||||
|
<h1 style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">
|
||||||
|
{$Request.get.keyword} 搜索结果
|
||||||
|
</h1>
|
||||||
<figure>
|
<figure>
|
||||||
<figcaption><em class='__PFX__-k is-accent'>{$Request.get.keyword}</em></figcaption>
|
<figcaption><em class='__PFX__-k is-accent'>{$Request.get.keyword}</em></figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
@@ -158,6 +215,9 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="19"}
|
{case value="19"}
|
||||||
<section class='__PFX__-sh-m19'>
|
<section class='__PFX__-sh-m19'>
|
||||||
|
<h1 style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">
|
||||||
|
{$Request.get.keyword} 搜索结果
|
||||||
|
</h1>
|
||||||
<figure>
|
<figure>
|
||||||
<figcaption><span class='__PFX__-k is-accent'>{$Request.get.keyword}</span></figcaption>
|
<figcaption><span class='__PFX__-k is-accent'>{$Request.get.keyword}</span></figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{case value="0"}
|
{case value="0"}
|
||||||
<article class='__PFX__-sh-r0'>
|
<article class='__PFX__-sh-r0'>
|
||||||
<header>
|
<header>
|
||||||
<h1><mark class='__PFX__-k is-tag'>{$Request.get.keyword}</mark></h1>
|
<h1><mark class='__PFX__-k is-tag'>{$Request.get.keyword}</mark> 搜索结果</h1>
|
||||||
<p>与你查询相关的内容合集</p>
|
<p>与你查询相关的内容合集</p>
|
||||||
</header>
|
</header>
|
||||||
<aside>关键词:<mark class='__PFX__-k is-tag'>{$Request.get.keyword}</mark></aside>
|
<aside>关键词:<mark class='__PFX__-k is-tag'>{$Request.get.keyword}</mark></aside>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<article class='__PFX__-sh-r1'>
|
<article class='__PFX__-sh-r1'>
|
||||||
<header>
|
<header>
|
||||||
<h2>搜索结果</h2>
|
<h2>搜索结果</h2>
|
||||||
<h1><span class='__PFX__-k is-tag'>{$Request.get.keyword}</span></h1>
|
<h1><span class='__PFX__-k is-tag'>{$Request.get.keyword}</span> 搜索结果</h1>
|
||||||
</header>
|
</header>
|
||||||
<section class='meta'><span>匹配</span><span>整理</span></section>
|
<section class='meta'><span>匹配</span><span>整理</span></section>
|
||||||
</article>
|
</article>
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
{case value="2"}
|
{case value="2"}
|
||||||
<main class='__PFX__-sh-r2'>
|
<main class='__PFX__-sh-r2'>
|
||||||
<section class='hero'>
|
<section class='hero'>
|
||||||
<h1>“<strong class='__PFX__-k is-tag'>{$Request.get.keyword}</strong>”</h1>
|
<h1>“<strong class='__PFX__-k is-tag'>{$Request.get.keyword}</strong>” 搜索结果</h1>
|
||||||
<p>为你推荐相关内容</p>
|
<p>为你推荐相关内容</p>
|
||||||
</section>
|
</section>
|
||||||
<section class='info'>
|
<section class='info'>
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
{case value="3"}
|
{case value="3"}
|
||||||
<article class='__PFX__-sh-r3'>
|
<article class='__PFX__-sh-r3'>
|
||||||
<header>
|
<header>
|
||||||
<h1>搜索:<em class='__PFX__-k is-tag'>{$Request.get.keyword}</em></h1>
|
<h1><em class='__PFX__-k is-tag'>{$Request.get.keyword}</em> 搜索结果</h1>
|
||||||
</header>
|
</header>
|
||||||
<section class='cards'>
|
<section class='cards'>
|
||||||
<div class='card'>相关</div>
|
<div class='card'>相关</div>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
{case value="4"}
|
{case value="4"}
|
||||||
<main class='__PFX__-sh-r4'>
|
<main class='__PFX__-sh-r4'>
|
||||||
<header>
|
<header>
|
||||||
<h1><code class='__PFX__-k is-tag'>{$Request.get.keyword}</code></h1>
|
<h1><code class='__PFX__-k is-tag'>{$Request.get.keyword}</code> 搜索结果</h1>
|
||||||
<p>结果列表</p>
|
<p>结果列表</p>
|
||||||
</header>
|
</header>
|
||||||
<aside class='note'><strong>提示</strong>:尝试更具体的词</aside>
|
<aside class='note'><strong>提示</strong>:尝试更具体的词</aside>
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
{case value="5"}
|
{case value="5"}
|
||||||
<article class='__PFX__-sh-r5'>
|
<article class='__PFX__-sh-r5'>
|
||||||
<header>
|
<header>
|
||||||
<h1><mark class='__PFX__-k is-tag'>{$Request.get.keyword}</mark></h1>
|
<h1><mark class='__PFX__-k is-tag'>{$Request.get.keyword}</mark> 搜索结果</h1>
|
||||||
<p>按热度聚合的搜索内容</p>
|
<p>按热度聚合的搜索内容</p>
|
||||||
</header>
|
</header>
|
||||||
<footer>来源:站内检索</footer>
|
<footer>来源:站内检索</footer>
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
<main class='__PFX__-sh-r6'>
|
<main class='__PFX__-sh-r6'>
|
||||||
<header>
|
<header>
|
||||||
<h2>关键词</h2>
|
<h2>关键词</h2>
|
||||||
<h1><span class='__PFX__-k is-tag'>{$Request.get.keyword}</span></h1>
|
<h1><span class='__PFX__-k is-tag'>{$Request.get.keyword}</span> 搜索结果</h1>
|
||||||
</header>
|
</header>
|
||||||
<section class='kv'><span>类型</span><span>内容</span></section>
|
<section class='kv'><span>类型</span><span>内容</span></section>
|
||||||
</main>
|
</main>
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
{case value="7"}
|
{case value="7"}
|
||||||
<article class='__PFX__-sh-r7'>
|
<article class='__PFX__-sh-r7'>
|
||||||
<header>
|
<header>
|
||||||
<h1><strong class='__PFX__-k is-tag'>{$Request.get.keyword}</strong></h1>
|
<h1><strong class='__PFX__-k is-tag'>{$Request.get.keyword}</strong> 搜索结果</h1>
|
||||||
</header>
|
</header>
|
||||||
<section class='grid'>
|
<section class='grid'>
|
||||||
<div>摘要</div>
|
<div>摘要</div>
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
{case value="9"}
|
{case value="9"}
|
||||||
<article class='__PFX__-sh-r9'>
|
<article class='__PFX__-sh-r9'>
|
||||||
<header>
|
<header>
|
||||||
<h1><code class='__PFX__-k is-tag'>{$Request.get.keyword}</code></h1>
|
<h1><code class='__PFX__-k is-tag'>{$Request.get.keyword}</code> 搜索结果</h1>
|
||||||
<p>相关内容已按主题整理</p>
|
<p>相关内容已按主题整理</p>
|
||||||
</header>
|
</header>
|
||||||
<section class='list'>
|
<section class='list'>
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
{case value="10"}
|
{case value="10"}
|
||||||
<main class='__PFX__-sh-r10'>
|
<main class='__PFX__-sh-r10'>
|
||||||
<header>
|
<header>
|
||||||
<h1><mark class='__PFX__-k is-tag'>{$Request.get.keyword}</mark></h1>
|
<h1><mark class='__PFX__-k is-tag'>{$Request.get.keyword}</mark> 搜索结果</h1>
|
||||||
</header>
|
</header>
|
||||||
<section class='stat'><span class='label'>匹配</span><span class='value'>N</span></section>
|
<section class='stat'><span class='label'>匹配</span><span class='value'>N</span></section>
|
||||||
</main>
|
</main>
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
{case value="11"}
|
{case value="11"}
|
||||||
<article class='__PFX__-sh-r11'>
|
<article class='__PFX__-sh-r11'>
|
||||||
<header>
|
<header>
|
||||||
<h1>搜索结果</h1>
|
<h1><span class='__PFX__-k is-tag'>{$Request.get.keyword}</span> 搜索结果</h1>
|
||||||
</header>
|
</header>
|
||||||
<section>
|
<section>
|
||||||
<p>关键词:<span class='__PFX__-k is-tag'>{$Request.get.keyword}</span></p>
|
<p>关键词:<span class='__PFX__-k is-tag'>{$Request.get.keyword}</span></p>
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
{case value="12"}
|
{case value="12"}
|
||||||
<main class='__PFX__-sh-r12'>
|
<main class='__PFX__-sh-r12'>
|
||||||
<section class='banner'>
|
<section class='banner'>
|
||||||
<h1><strong class='__PFX__-k is-tag'>{$Request.get.keyword}</strong></h1>
|
<h1><strong class='__PFX__-k is-tag'>{$Request.get.keyword}</strong> 搜索结果</h1>
|
||||||
<p>搜索结果展示页</p>
|
<p>搜索结果展示页</p>
|
||||||
</section>
|
</section>
|
||||||
<section class='meta'><span>更新</span><span>相关</span></section>
|
<section class='meta'><span>更新</span><span>相关</span></section>
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
{case value="13"}
|
{case value="13"}
|
||||||
<article class='__PFX__-sh-r13'>
|
<article class='__PFX__-sh-r13'>
|
||||||
<header>
|
<header>
|
||||||
<h1><em class='__PFX__-k is-tag'>{$Request.get.keyword}</em></h1>
|
<h1><em class='__PFX__-k is-tag'>{$Request.get.keyword}</em> 搜索结果</h1>
|
||||||
<p>你可能还想找:</p>
|
<p>你可能还想找:</p>
|
||||||
</header>
|
</header>
|
||||||
<nav class='sugs' aria-label='suggest'><a href='javascript:void(0)' rel='nofollow'>相近词</a></nav>
|
<nav class='sugs' aria-label='suggest'><a href='javascript:void(0)' rel='nofollow'>相近词</a></nav>
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
{case value="14"}
|
{case value="14"}
|
||||||
<main class='__PFX__-sh-r14'>
|
<main class='__PFX__-sh-r14'>
|
||||||
<header>
|
<header>
|
||||||
<h1>“<code class='__PFX__-k is-tag'>{$Request.get.keyword}</code>”</h1>
|
<h1>“<code class='__PFX__-k is-tag'>{$Request.get.keyword}</code>” 搜索结果</h1>
|
||||||
</header>
|
</header>
|
||||||
<section class='panel'>
|
<section class='panel'>
|
||||||
<div class='hd'>概览</div>
|
<div class='hd'>概览</div>
|
||||||
@@ -157,7 +157,7 @@
|
|||||||
{case value="15"}
|
{case value="15"}
|
||||||
<article class='__PFX__-sh-r15'>
|
<article class='__PFX__-sh-r15'>
|
||||||
<header>
|
<header>
|
||||||
<h1><mark class='__PFX__-k is-tag'>{$Request.get.keyword}</mark></h1>
|
<h1><mark class='__PFX__-k is-tag'>{$Request.get.keyword}</mark> 搜索结果</h1>
|
||||||
<p>结果已按相关度排序</p>
|
<p>结果已按相关度排序</p>
|
||||||
</header>
|
</header>
|
||||||
<section class='filters'><span>全部</span><span>最新</span><span>推荐</span></section>
|
<section class='filters'><span>全部</span><span>最新</span><span>推荐</span></section>
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
{case value="16"}
|
{case value="16"}
|
||||||
<main class='__PFX__-sh-r16'>
|
<main class='__PFX__-sh-r16'>
|
||||||
<header>
|
<header>
|
||||||
<h1><span class='__PFX__-k is-tag'>{$Request.get.keyword}</span></h1>
|
<h1><span class='__PFX__-k is-tag'>{$Request.get.keyword}</span> 搜索结果</h1>
|
||||||
<p>站内搜索</p>
|
<p>站内搜索</p>
|
||||||
</header>
|
</header>
|
||||||
<section class='steps'><span>1 查询</span><span>2 匹配</span><span>3 展示</span></section>
|
<section class='steps'><span>1 查询</span><span>2 匹配</span><span>3 展示</span></section>
|
||||||
@@ -175,7 +175,7 @@
|
|||||||
{case value="17"}
|
{case value="17"}
|
||||||
<article class='__PFX__-sh-r17'>
|
<article class='__PFX__-sh-r17'>
|
||||||
<header>
|
<header>
|
||||||
<h1><strong class='__PFX__-k is-tag'>{$Request.get.keyword}</strong></h1>
|
<h1><strong class='__PFX__-k is-tag'>{$Request.get.keyword}</strong> 搜索结果</h1>
|
||||||
</header>
|
</header>
|
||||||
<section class='qa'>
|
<section class='qa'>
|
||||||
<details open>
|
<details open>
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
{case value="18"}
|
{case value="18"}
|
||||||
<main class='__PFX__-sh-r18'>
|
<main class='__PFX__-sh-r18'>
|
||||||
<header>
|
<header>
|
||||||
<h1><em class='__PFX__-k is-tag'>{$Request.get.keyword}</em></h1>
|
<h1><em class='__PFX__-k is-tag'>{$Request.get.keyword}</em> 搜索结果</h1>
|
||||||
<p>聚合页</p>
|
<p>聚合页</p>
|
||||||
</header>
|
</header>
|
||||||
<section class='table'>
|
<section class='table'>
|
||||||
@@ -204,7 +204,7 @@
|
|||||||
{case value="19"}
|
{case value="19"}
|
||||||
<article class='__PFX__-sh-r19'>
|
<article class='__PFX__-sh-r19'>
|
||||||
<header>
|
<header>
|
||||||
<h1><code class='__PFX__-k is-tag'>{$Request.get.keyword}</code></h1>
|
<h1><code class='__PFX__-k is-tag'>{$Request.get.keyword}</code> 搜索结果</h1>
|
||||||
<p>内容合集</p>
|
<p>内容合集</p>
|
||||||
</header>
|
</header>
|
||||||
<footer><small>Search Hub</small></footer>
|
<footer><small>Search Hub</small></footer>
|
||||||
|
|||||||
@@ -7,51 +7,50 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="1"}
|
{case value="1"}
|
||||||
<section class='__PFX__-sh-e1'>
|
<section class='__PFX__-sh-e1'>
|
||||||
<h1>搜索:<span class='__PFX__-k'>{$Request.get.keyword}</span></h1>
|
<h1><span class='__PFX__-k'>{$Request.get.keyword}</span> 搜索结果</h1>
|
||||||
<p>结果列表</p>
|
<p>结果列表</p>
|
||||||
<link rel='canonical' href='/' />
|
|
||||||
</section>
|
</section>
|
||||||
{/case}
|
{/case}
|
||||||
{case value="2"}
|
{case value="2"}
|
||||||
<div class='__PFX__-sh-e2' role='region' aria-label='search-head'>
|
<div class='__PFX__-sh-e2' role='region' aria-label='search-head'>
|
||||||
<h1><em class='__PFX__-k'>{$Request.get.keyword}</em></h1>
|
<h1><em class='__PFX__-k'>{$Request.get.keyword}</em> 搜索结果</h1>
|
||||||
<p>搜索结果页</p>
|
<p>搜索结果页</p>
|
||||||
</div>
|
</div>
|
||||||
{/case}
|
{/case}
|
||||||
{case value="3"}
|
{case value="3"}
|
||||||
<header class='__PFX__-sh-e3'>
|
<header class='__PFX__-sh-e3'>
|
||||||
<h1><i class='__PFX__-k'>{$Request.get.keyword}</i></h1>
|
<h1><i class='__PFX__-k'>{$Request.get.keyword}</i> 搜索结果</h1>
|
||||||
<p>站内搜索结果</p>
|
<p>站内搜索结果</p>
|
||||||
</header>
|
</header>
|
||||||
{/case}
|
{/case}
|
||||||
{case value="4"}
|
{case value="4"}
|
||||||
<section class='__PFX__-sh-e4'>
|
<section class='__PFX__-sh-e4'>
|
||||||
<h1><mark class='__PFX__-k'>{$Request.get.keyword}</mark> 相关内容</h1>
|
<h1><mark class='__PFX__-k'>{$Request.get.keyword}</mark> 搜索结果</h1>
|
||||||
<p>搜索结果</p>
|
<p>搜索结果</p>
|
||||||
</section>
|
</section>
|
||||||
{/case}
|
{/case}
|
||||||
{case value="5"}
|
{case value="5"}
|
||||||
<div class='__PFX__-sh-e5'>
|
<div class='__PFX__-sh-e5'>
|
||||||
<h1>“<b class='__PFX__-k'>{$Request.get.keyword}</b>”</h1>
|
<h1>“<b class='__PFX__-k'>{$Request.get.keyword}</b>” 搜索结果</h1>
|
||||||
<p>搜索结果列表页</p>
|
<p>搜索结果列表页</p>
|
||||||
</div>
|
</div>
|
||||||
{/case}
|
{/case}
|
||||||
{case value="6"}
|
{case value="6"}
|
||||||
<section class='__PFX__-sh-e6'>
|
<section class='__PFX__-sh-e6'>
|
||||||
<h1><span class='__PFX__-k'>{$Request.get.keyword}</span></h1>
|
<h1><span class='__PFX__-k'>{$Request.get.keyword}</span> 搜索结果</h1>
|
||||||
<p>结果页(SEO)</p>
|
<p>结果页(SEO)</p>
|
||||||
<hr />
|
<hr />
|
||||||
</section>
|
</section>
|
||||||
{/case}
|
{/case}
|
||||||
{case value="7"}
|
{case value="7"}
|
||||||
<div class='__PFX__-sh-e7'>
|
<div class='__PFX__-sh-e7'>
|
||||||
<h1>关键词:<em class='__PFX__-k'>{$Request.get.keyword}</em></h1>
|
<h1><em class='__PFX__-k'>{$Request.get.keyword}</em> 搜索结果</h1>
|
||||||
<p>搜索结果</p>
|
<p>搜索结果</p>
|
||||||
</div>
|
</div>
|
||||||
{/case}
|
{/case}
|
||||||
{case value="8"}
|
{case value="8"}
|
||||||
<section class='__PFX__-sh-e8'>
|
<section class='__PFX__-sh-e8'>
|
||||||
<h1><i class='__PFX__-k'>{$Request.get.keyword}</i></h1>
|
<h1><i class='__PFX__-k'>{$Request.get.keyword}</i> 搜索结果</h1>
|
||||||
<p>索引列表页</p>
|
<p>索引列表页</p>
|
||||||
<nav aria-label='meta'><span>Search</span></nav>
|
<nav aria-label='meta'><span>Search</span></nav>
|
||||||
</section>
|
</section>
|
||||||
@@ -64,27 +63,25 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="10"}
|
{case value="10"}
|
||||||
<header class='__PFX__-sh-e10'>
|
<header class='__PFX__-sh-e10'>
|
||||||
<h1>搜索结果</h1>
|
<h1><b class='__PFX__-k'>{$Request.get.keyword}</b> 搜索结果</h1>
|
||||||
<p><b class='__PFX__-k'>{$Request.get.keyword}</b></p>
|
|
||||||
</header>
|
</header>
|
||||||
{/case}
|
{/case}
|
||||||
{case value="11"}
|
{case value="11"}
|
||||||
<section class='__PFX__-sh-e11'>
|
<section class='__PFX__-sh-e11'>
|
||||||
<h1>查询词</h1>
|
<h1><span class='__PFX__-k'>{$Request.get.keyword}</span> 搜索结果</h1>
|
||||||
<p><span class='__PFX__-k'>{$Request.get.keyword}</span></p>
|
|
||||||
<p>结果页</p>
|
<p>结果页</p>
|
||||||
</section>
|
</section>
|
||||||
{/case}
|
{/case}
|
||||||
{case value="12"}
|
{case value="12"}
|
||||||
<div class='__PFX__-sh-e12'>
|
<div class='__PFX__-sh-e12'>
|
||||||
<h1><em class='__PFX__-k'>{$Request.get.keyword}</em></h1>
|
<h1><em class='__PFX__-k'>{$Request.get.keyword}</em> 搜索结果</h1>
|
||||||
<p>聚合结果页</p>
|
<p>聚合结果页</p>
|
||||||
<p class='micro'>可抓取</p>
|
<p class='micro'>可抓取</p>
|
||||||
</div>
|
</div>
|
||||||
{/case}
|
{/case}
|
||||||
{case value="13"}
|
{case value="13"}
|
||||||
<section class='__PFX__-sh-e13'>
|
<section class='__PFX__-sh-e13'>
|
||||||
<h1><i class='__PFX__-k'>{$Request.get.keyword}</i></h1>
|
<h1><i class='__PFX__-k'>{$Request.get.keyword}</i> 搜索结果</h1>
|
||||||
<p>搜索结果目录</p>
|
<p>搜索结果目录</p>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>关键词</dt>
|
<dt>关键词</dt>
|
||||||
@@ -94,7 +91,7 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="14"}
|
{case value="14"}
|
||||||
<div class='__PFX__-sh-e14'>
|
<div class='__PFX__-sh-e14'>
|
||||||
<h1><mark class='__PFX__-k'>{$Request.get.keyword}</mark></h1>
|
<h1><mark class='__PFX__-k'>{$Request.get.keyword}</mark> 搜索结果</h1>
|
||||||
<p>结果页</p>
|
<p>结果页</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>相关</li>
|
<li>相关</li>
|
||||||
@@ -104,20 +101,20 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="15"}
|
{case value="15"}
|
||||||
<header class='__PFX__-sh-e15'>
|
<header class='__PFX__-sh-e15'>
|
||||||
<h1><b class='__PFX__-k'>{$Request.get.keyword}</b></h1>
|
<h1><b class='__PFX__-k'>{$Request.get.keyword}</b> 搜索结果</h1>
|
||||||
<p>搜索结果索引</p>
|
<p>搜索结果索引</p>
|
||||||
<address>站内</address>
|
<address>站内</address>
|
||||||
</header>
|
</header>
|
||||||
{/case}
|
{/case}
|
||||||
{case value="16"}
|
{case value="16"}
|
||||||
<section class='__PFX__-sh-e16'>
|
<section class='__PFX__-sh-e16'>
|
||||||
<h1><span class='__PFX__-k'>{$Request.get.keyword}</span></h1>
|
<h1><span class='__PFX__-k'>{$Request.get.keyword}</span> 搜索结果</h1>
|
||||||
<p>Search result page</p>
|
<p>Search result page</p>
|
||||||
</section>
|
</section>
|
||||||
{/case}
|
{/case}
|
||||||
{case value="17"}
|
{case value="17"}
|
||||||
<div class='__PFX__-sh-e17'>
|
<div class='__PFX__-sh-e17'>
|
||||||
<h1><em class='__PFX__-k'>{$Request.get.keyword}</em></h1>
|
<h1><em class='__PFX__-k'>{$Request.get.keyword}</em> 搜索结果</h1>
|
||||||
<p>结果列表页</p>
|
<p>结果列表页</p>
|
||||||
<details open>
|
<details open>
|
||||||
<summary>说明</summary>
|
<summary>说明</summary>
|
||||||
@@ -127,14 +124,14 @@
|
|||||||
{/case}
|
{/case}
|
||||||
{case value="18"}
|
{case value="18"}
|
||||||
<section class='__PFX__-sh-e18'>
|
<section class='__PFX__-sh-e18'>
|
||||||
<h1><i class='__PFX__-k'>{$Request.get.keyword}</i></h1>
|
<h1><i class='__PFX__-k'>{$Request.get.keyword}</i> 搜索结果</h1>
|
||||||
<p>站内搜索</p>
|
<p>站内搜索</p>
|
||||||
<p class='micro'>list</p>
|
<p class='micro'>list</p>
|
||||||
</section>
|
</section>
|
||||||
{/case}
|
{/case}
|
||||||
{case value="19"}
|
{case value="19"}
|
||||||
<div class='__PFX__-sh-e19'>
|
<div class='__PFX__-sh-e19'>
|
||||||
<h1><mark class='__PFX__-k'>{$Request.get.keyword}</mark></h1>
|
<h1><mark class='__PFX__-k'>{$Request.get.keyword}</mark> 搜索结果</h1>
|
||||||
<p>搜索结果</p>
|
<p>搜索结果</p>
|
||||||
<hr />
|
<hr />
|
||||||
<p class='micro'>indexable</p>
|
<p class='micro'>indexable</p>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user