restore admin helper chain and stabilize seo routes

This commit is contained in:
root
2026-04-15 18:51:27 +08:00
parent bd073c6795
commit 7fff1a9944
24 changed files with 2293 additions and 41 deletions

View File

@@ -82,17 +82,15 @@ class VideoService
$strTmpCode = $this->SiteContext->TemplatesModel['t_code'];
if ($strTmpCode == 'videoGpt1') {
// videoGpt1 runtime resolves detail data by v_id.
// Use id-first urls to avoid stale slug drift creating dead deep links.
return $this->SiteContext->UrlBuilder->detail('', $intVId);
} else {
$strKey = 'VIDEO_INFO_URL';
$strUrl = $this->SiteContext->DomainModel->getFomartUrl($strKey, [
'intVId' => $intVId,
'strPinYin' => $strPinYin
]);
return (string)$strUrl;
return $this->SiteContext->UrlBuilder->detail($strPinYin, $intVId);
}
$strKey = 'VIDEO_INFO_URL';
$strUrl = $this->SiteContext->DomainModel->getFomartUrl($strKey, [
'intVId' => $intVId,
'strPinYin' => $strPinYin
]);
return (string)$strUrl;
}
/**
@@ -108,16 +106,16 @@ class VideoService
$strTmpCode = $this->SiteContext->TemplatesModel['t_code'];
if ($strTmpCode == 'videoGpt1') {
return $this->SiteContext->UrlBuilder->detailForge('', $intVId, $intVForgeId);
} else {
$strKey = 'FORGE_VIDEO_INFO_URL';
$strUrl = $this->SiteContext->DomainModel->getFomartUrl($strKey, [
'intVId' => $intVId,
'strPinYin' => $strPinYin,
'intVForgeId' => $intVForgeId,
]);
return (string)$strUrl;
return $this->SiteContext->UrlBuilder->detailForge($strPinYin, $intVId, $intVForgeId);
}
$strKey = 'FORGE_VIDEO_INFO_URL';
$strUrl = $this->SiteContext->DomainModel->getFomartUrl($strKey, [
'intVId' => $intVId,
'strPinYin' => $strPinYin,
'intVForgeId' => $intVForgeId,
]);
return (string)$strUrl;
}
/**
@@ -134,17 +132,17 @@ class VideoService
$strTmpCode = $this->SiteContext->TemplatesModel['t_code'];
if ($strTmpCode == 'videoGpt1') {
return $this->SiteContext->UrlBuilder->play('', $intVId, $strPlayType, $intPlayIndex);
} else {
$strKey = 'VIDEO_PLAY_URL';
$strUrl = $this->SiteContext->DomainModel->getFomartUrl($strKey, [
'intVId' => $intVId,
'strPinYin' => $strPinYin,
'strPlayType' => $strPlayType,
'intPlayIndex' => $intPlayIndex,
]);
return (string)$strUrl;
return $this->SiteContext->UrlBuilder->play($strPinYin, $intVId, $strPlayType, $intPlayIndex);
}
$strKey = 'VIDEO_PLAY_URL';
$strUrl = $this->SiteContext->DomainModel->getFomartUrl($strKey, [
'intVId' => $intVId,
'strPinYin' => $strPinYin,
'strPlayType' => $strPlayType,
'intPlayIndex' => $intPlayIndex,
]);
return (string)$strUrl;
}
@@ -762,16 +760,42 @@ class VideoService
*/
public function getVideoByVId(int $intVId, int|NULL $intVForgeId)
{
$boolRequirePlayable = (string)request()->route('strPlayType') !== '' || (int)request()->route('intPlayIndex') > 0;
$this->debugFallbackProbe('enter_getVideoByVId', [
'requested_v_id' => $intVId,
'forge_id' => $intVForgeId,
'require_playable' => $boolRequirePlayable ? 1 : 0,
'host' => (string)(request()->host(true) ?: request()->host()),
'path' => (string)request()->pathinfo(),
]);
// 视频详情
$arrVideo = $this->VideoModel->getVideoByVId($intVId);
$this->debugFallbackProbe('after_direct_lookup', [
'requested_v_id' => $intVId,
'found' => empty($arrVideo) ? 0 : 1,
'resolved_v_id' => (int)($arrVideo['v_id'] ?? 0),
]);
if (empty($arrVideo)) {
$arrVideo = $this->resolveFallbackVideoByRequest($intVId, $boolRequirePlayable);
}
if (empty($arrVideo)) {
$this->debugFallbackProbe('final_404', [
'requested_v_id' => $intVId,
'path' => (string)request()->pathinfo(),
]);
throw new HttpException(404, '视频不存在');
}
if ($intVForgeId >= 0 && !empty($arrVideo['v_seo_words'])) {
$arrVideo['v_name'] = $arrVideo['v_seo_words'][$intVForgeId - 1];
$intForgeOffset = max(0, $intVForgeId - 1);
if (!empty($arrVideo['v_seo_words'][$intForgeOffset])) {
$arrVideo['v_name'] = $arrVideo['v_seo_words'][$intForgeOffset];
}
}
// 取前5个元素
@@ -785,7 +809,7 @@ class VideoService
$strTopLang = '2025';
// 点击数
$arrVideoClicks = $this->VideoClicksModel->getStats($intVId);
$arrVideoClicks = $this->VideoClicksModel->getStats((int)($arrVideo['v_id'] ?? $intVId));
// 更新值
ConverterMovel::setVal([
@@ -806,6 +830,185 @@ class VideoService
return $arrVideo;
}
protected function resolveFallbackVideoByRequest(int $requestedVId, bool $requirePlayable = false): ?array
{
$strHost = (string)($this->SiteContext->DomainModel->d_domain ?? 'default');
$arrBindings = $this->readFallbackBindings($strHost);
$strBindingKey = (string)$requestedVId;
$this->debugFallbackProbe('enter_resolve_fallback', [
'requested_v_id' => $requestedVId,
'require_playable' => $requirePlayable ? 1 : 0,
'host' => $strHost,
'existing_bindings' => count($arrBindings),
]);
$boundVId = (int)($arrBindings[$strBindingKey] ?? 0);
if ($boundVId > 0 && $boundVId !== $requestedVId) {
$arrVideo = $this->VideoModel->getVideoByVId($boundVId);
if (!empty($arrVideo) && (!$requirePlayable || $this->hasPlayableSource($arrVideo))) {
$this->debugFallbackProbe('reuse_bound_video', [
'requested_v_id' => $requestedVId,
'bound_v_id' => $boundVId,
]);
$arrVideo['_fallback_reason'] = 'bound_random_video';
$arrVideo['_requested_v_id'] = $requestedVId;
return $arrVideo;
}
}
$arrFilter = [];
if ($requirePlayable) {
$arrFilter['v_play_url'] = ['$exists' => true, '$ne' => []];
}
for ($attempt = 0; $attempt < 6; $attempt++) {
$cursor = $this->VideoModel->getCol()->aggregate([
['$match' => $arrFilter],
['$sample' => ['size' => 8]],
]);
$arrRows = iterator_to_array($cursor);
$this->debugFallbackProbe('sample_attempt', [
'requested_v_id' => $requestedVId,
'attempt' => $attempt + 1,
'sample_count' => count($arrRows),
]);
foreach ($arrRows as $row) {
$candidateId = (int)($row['v_id'] ?? 0);
if ($candidateId <= 0 || $candidateId === $requestedVId) {
continue;
}
$arrVideo = $this->VideoModel->getVideoByVId($candidateId);
if (empty($arrVideo)) {
continue;
}
if ($requirePlayable && !$this->hasPlayableSource($arrVideo)) {
continue;
}
$arrBindings[$strBindingKey] = $candidateId;
$this->writeFallbackBindings($strHost, $arrBindings);
$this->debugFallbackProbe('sample_success', [
'requested_v_id' => $requestedVId,
'candidate_v_id' => $candidateId,
'attempt' => $attempt + 1,
]);
$arrVideo['_fallback_reason'] = 'random_video';
$arrVideo['_requested_v_id'] = $requestedVId;
return $arrVideo;
}
}
$arrVideo = $this->VideoModel->findOne($arrFilter, ['sort' => ['v_level' => -1]]);
if (!empty($arrVideo)) {
$candidateId = (int)($arrVideo['v_id'] ?? 0);
if ($candidateId > 0 && $candidateId !== $requestedVId) {
$arrBindings[$strBindingKey] = $candidateId;
$this->writeFallbackBindings($strHost, $arrBindings);
$this->debugFallbackProbe('ordered_fallback_success', [
'requested_v_id' => $requestedVId,
'candidate_v_id' => $candidateId,
]);
$arrVideo['_fallback_reason'] = 'ordered_fallback_video';
$arrVideo['_requested_v_id'] = $requestedVId;
return $arrVideo;
}
}
$this->debugFallbackProbe('fallback_exhausted', [
'requested_v_id' => $requestedVId,
'require_playable' => $requirePlayable ? 1 : 0,
]);
return null;
}
protected function hasPlayableSource(array $arrVideo): bool
{
$arrPlayUrl = (array)($arrVideo['v_play_url'] ?? []);
if (empty($arrPlayUrl)) {
return false;
}
foreach ($arrPlayUrl as $episodes) {
if (is_array($episodes) && !empty($episodes)) {
return true;
}
}
return false;
}
protected function readFallbackBindings(string $host): array
{
$path = $this->fallbackBindingsPath($host);
if (!is_file($path)) {
return [];
}
$content = @file_get_contents($path);
if (!is_string($content) || trim($content) === '') {
return [];
}
$payload = json_decode($content, true);
return is_array($payload) ? $payload : [];
}
protected function writeFallbackBindings(string $host, array $bindings): void
{
$path = $this->fallbackBindingsPath($host);
$dir = dirname($path);
if (!is_dir($dir)) {
@mkdir($dir, 0777, true);
}
@file_put_contents(
$path,
json_encode($bindings, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL,
LOCK_EX
);
}
protected function fallbackBindingsPath(string $host): string
{
$host = trim(strtolower($host));
if ($host === '') {
$host = 'default';
}
$safeHost = preg_replace('/[^a-z0-9._-]+/i', '-', $host);
return rtrim((string)root_path(), DIRECTORY_SEPARATOR)
. DIRECTORY_SEPARATOR . 'storage'
. DIRECTORY_SEPARATOR . 'video_fallback_bindings'
. DIRECTORY_SEPARATOR . $safeHost . '.json';
}
protected function debugFallbackProbe(string $stage, array $payload = []): void
{
$path = rtrim((string)root_path(), DIRECTORY_SEPARATOR)
. DIRECTORY_SEPARATOR . 'storage'
. DIRECTORY_SEPARATOR . 'video_fallback_bindings'
. DIRECTORY_SEPARATOR . '_probe.log';
$dir = dirname($path);
if (!is_dir($dir)) {
@mkdir($dir, 0777, true);
}
$line = [
'ts' => date('c'),
'stage' => $stage,
'payload' => $payload,
];
@file_put_contents(
$path,
json_encode($line, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL,
FILE_APPEND | LOCK_EX
);
}
/**
* 获取 播放线路-中文
*