restore bootstrap and seo helper chain after rebase drop

This commit is contained in:
root
2026-04-15 14:00:19 +08:00
parent 887e95286f
commit 0794c903d7
147 changed files with 43977 additions and 646 deletions

View File

@@ -3,39 +3,53 @@
declare(strict_types=1);
use app\api\controller\Novel;
use app\api\controller\SeoCollector;
use app\api\controller\Statis;
use app\api\middleware\SeoCollectorTokenAuth;
use think\facade\Route;
Route::post("/publish", [Statis::class, 'publish'])
// ->middleware(\think\middleware\Throttle::class, [
// 'visit_rate' => '1/m',
// 'key' => '__CONTROLLER__/__ACTION__/__IP__',
// ])
->name('Statis@publish');
// Route::get("/search", [Novel::class, 'search'])
// ->middleware(\think\middleware\Throttle::class, [
// 'visit_rate' => '1/m',
// 'key' => '__CONTROLLER__/__ACTION__/__IP__',
// ])
// ->name('Novel@search');
Route::post("/publishv", [Statis::class, 'publishV'])
// ->middleware(\think\middleware\Throttle::class, [
// 'visit_rate' => '1/m',
// 'key' => '__CONTROLLER__/__ACTION__/__IP__',
// ])
->name('Statis@publishV');
Route::post("/publish", [Statis::class, 'publish'])
// ->middleware(\think\middleware\Throttle::class, [
// 'visit_rate' => '1/m',
// 'key' => '__CONTROLLER__/__ACTION__/__IP__',
// ])
->name('Statis@publish');
// Route::get("/search", [Novel::class, 'search'])
// ->middleware(\think\middleware\Throttle::class, [
// 'visit_rate' => '1/m',
// 'key' => '__CONTROLLER__/__ACTION__/__IP__',
// ])
// ->name('Novel@search');
Route::post("/publishv", [Statis::class, 'publishV'])
// ->middleware(\think\middleware\Throttle::class, [
// 'visit_rate' => '1/m',
// 'key' => '__CONTROLLER__/__ACTION__/__IP__',
// ])
->name('Statis@publishV');
Route::get("/test0909", [Statis::class, 'test0909'])
// ->middleware(\think\middleware\Throttle::class, [
// 'visit_rate' => '1/m',
// 'key' => '__CONTROLLER__/__ACTION__/__IP__',
// ])
->name('Statis@test0909');
Route::group('/seo-collector', function () {
Route::get('/collect/hosts', [SeoCollector::class, 'collectHosts'])->name('SeoCollector@collectHosts');
Route::get('/snapshots/schema', [SeoCollector::class, 'snapshotSchema'])->name('SeoCollector@snapshotSchema');
Route::get('/snapshots/checklist', [SeoCollector::class, 'snapshotChecklist'])->name('SeoCollector@snapshotChecklist');
Route::post('/snapshots/validate', [SeoCollector::class, 'validateSnapshots'])->name('SeoCollector@validateSnapshots');
Route::post('/snapshots/save', [SeoCollector::class, 'saveSnapshots'])->name('SeoCollector@saveSnapshots');
Route::get('/crawl-logs/schema', [SeoCollector::class, 'crawlLogSchema'])->name('SeoCollector@crawlLogSchema');
Route::get('/crawl-logs/checklist', [SeoCollector::class, 'crawlLogChecklist'])->name('SeoCollector@crawlLogChecklist');
Route::post('/crawl-logs/validate', [SeoCollector::class, 'validateCrawlLogs'])->name('SeoCollector@validateCrawlLogs');
Route::post('/crawl-logs/save', [SeoCollector::class, 'saveCrawlLogs'])->name('SeoCollector@saveCrawlLogs');
})->middleware(SeoCollectorTokenAuth::class);

View File

@@ -0,0 +1,304 @@
<?php
declare(strict_types=1);
namespace app\api\controller;
use app\api\BaseController;
use app\common\helper\DomainExternalSeoSnapshotHelper;
use app\common\helper\DomainExternalSeoSnapshotSchemaHelper;
use app\common\helper\DomainExternalSeoSnapshotRunHelper;
use app\common\helper\DomainExternalSeoSnapshotTrendHelper;
use app\common\helper\DomainExternalSeoCnOverviewHelper;
use app\common\helper\DomainExternalSeoSnapshotValidateRunHelper;
use app\common\helper\DomainSpiderCrawlLogHelper;
use app\common\helper\DomainSpiderCrawlLogRunHelper;
use app\common\helper\DomainSpiderCrawlLogSchemaHelper;
use think\Request;
class SeoCollector extends BaseController
{
protected function codeRoot(): string
{
return dirname(__DIR__, 3);
}
protected function publicRoot(): string
{
return $this->codeRoot() . '/public';
}
protected function storageRoot(): string
{
return $this->codeRoot() . '/storage';
}
protected function summaryRoot(): string
{
return $this->publicRoot() . '/_admin_templates/domain-seo-external/latest';
}
protected function snapshotRunRoot(): string
{
return $this->publicRoot() . '/_admin_templates/domain-seo-external/snapshot-runs';
}
protected function snapshotValidateRunRoot(): string
{
return $this->publicRoot() . '/_admin_templates/domain-seo-external/snapshot-validate-runs';
}
protected function spiderCrawlLatestRoot(): string
{
return $this->storageRoot() . '/domain-spider-crawl/latest';
}
protected function spiderCrawlRunRoot(): string
{
return $this->storageRoot() . '/domain-spider-crawl/runs';
}
protected function publicRelativePath(string $path): string
{
$publicRoot = str_replace('\\', '/', rtrim($this->publicRoot(), '/'));
$path = str_replace('\\', '/', $path);
if (str_starts_with($path, $publicRoot . '/')) {
return substr($path, strlen($publicRoot . '/'));
}
return $path;
}
protected function storageRelativePath(string $path): string
{
$storageRoot = str_replace('\\', '/', rtrim($this->storageRoot(), '/'));
$path = str_replace('\\', '/', $path);
if (str_starts_with($path, $storageRoot . '/')) {
return substr($path, strlen($storageRoot . '/'));
}
return $path;
}
protected function success(array $data = [])
{
return json([
'code' => '1000',
'msg' => '成功',
'data' => $data,
], 200, [], [JSON_UNESCAPED_UNICODE]);
}
protected function error(string $code, string $msg)
{
return json([
'code' => $code,
'msg' => $msg,
'data' => [],
], 200, [], [JSON_UNESCAPED_UNICODE]);
}
public function collectHosts(Request $request)
{
try {
$limit = max(1, min(5000, (int)$request->get('limit', 1000)));
$includeWildcard = (int)$request->get('include_wildcard', 0) === 1;
return $this->success(DomainExternalSeoSnapshotHelper::buildCollectHosts($limit, $includeWildcard));
} catch (\Throwable $throwable) {
return $this->error('9999', '读取站外采集域名列表失败:' . $throwable->getMessage());
}
}
public function snapshotSchema()
{
try {
return $this->success(DomainExternalSeoSnapshotSchemaHelper::buildSchema());
} catch (\Throwable $throwable) {
return $this->error('9999', '读取站外 SEO 快照字段规则失败:' . $throwable->getMessage());
}
}
public function snapshotChecklist()
{
try {
return $this->success(DomainExternalSeoSnapshotSchemaHelper::buildChecklist());
} catch (\Throwable $throwable) {
return $this->error('9999', '读取站外 SEO 快照字段校验清单失败:' . $throwable->getMessage());
}
}
public function validateSnapshots(Request $request)
{
try {
$snapshots = $this->resolveSnapshots($request);
if (empty($snapshots)) {
return $this->error('9999', '请提交 snapshots');
}
$result = DomainExternalSeoSnapshotSchemaHelper::validateSnapshots((array)$snapshots);
$runRoot = DomainExternalSeoSnapshotValidateRunHelper::createRunRoot($this->snapshotValidateRunRoot(), 'snapshot_validate');
$result = DomainExternalSeoSnapshotValidateRunHelper::writeRunArtifacts(
$this->publicRoot(),
$runRoot,
$result,
(array)$snapshots
);
$result['run_root'] = $this->publicRelativePath($runRoot);
$result['summary_json_path'] = $this->publicRelativePath((string)($result['summary_json_path'] ?? ''));
$result['summary_html_path'] = $this->publicRelativePath((string)($result['summary_html_path'] ?? ''));
$result['payload_json_path'] = $this->publicRelativePath((string)($result['payload_json_path'] ?? ''));
return $this->success($result);
} catch (\Throwable $throwable) {
return $this->error('9999', '校验站外 SEO 快照失败:' . $throwable->getMessage());
}
}
public function saveSnapshots(Request $request)
{
try {
$snapshots = $this->resolveSnapshots($request);
if (empty($snapshots)) {
return $this->error('9999', '请提交 snapshots');
}
$source = trim((string)$request->post('source', 'push_api'));
$result = DomainExternalSeoSnapshotHelper::ingestSnapshots((array)$snapshots, $source);
$runRoot = DomainExternalSeoSnapshotRunHelper::createRunRoot($this->snapshotRunRoot(), 'snapshot_push');
$result = DomainExternalSeoSnapshotRunHelper::writeRunArtifacts(
$this->publicRoot(),
$runRoot,
$result,
(array)$snapshots
);
$result['run_root'] = $this->publicRelativePath($runRoot);
$result['summary_json_path'] = $this->publicRelativePath((string)($result['summary_json_path'] ?? ''));
$result['summary_html_path'] = $this->publicRelativePath((string)($result['summary_html_path'] ?? ''));
$result['payload_json_path'] = $this->publicRelativePath((string)($result['payload_json_path'] ?? ''));
$overview = DomainExternalSeoCnOverviewHelper::buildOverview(14, 10);
$overview = DomainExternalSeoCnOverviewHelper::writeArtifacts($this->summaryRoot(), $overview);
$overview['summary_json_path'] = $this->publicRelativePath((string)($overview['summary_json_path'] ?? ''));
$overview['summary_html_path'] = $this->publicRelativePath((string)($overview['summary_html_path'] ?? ''));
$snapshotTrend = DomainExternalSeoSnapshotTrendHelper::buildSummary($this->snapshotRunRoot(), 10);
$snapshotTrend = DomainExternalSeoSnapshotTrendHelper::writeArtifacts($this->summaryRoot(), $snapshotTrend);
$snapshotTrend['summary_json_path'] = $this->publicRelativePath((string)($snapshotTrend['summary_json_path'] ?? ''));
$snapshotTrend['summary_html_path'] = $this->publicRelativePath((string)($snapshotTrend['summary_html_path'] ?? ''));
$result['post_ingest_overview'] = $overview;
$result['post_ingest_snapshot_trend'] = $snapshotTrend;
$result = DomainExternalSeoSnapshotRunHelper::writeRunArtifacts(
$this->publicRoot(),
$runRoot,
$result,
(array)$snapshots
);
$result['summary_json_path'] = $this->publicRelativePath((string)($result['summary_json_path'] ?? ''));
$result['summary_html_path'] = $this->publicRelativePath((string)($result['summary_html_path'] ?? ''));
$result['payload_json_path'] = $this->publicRelativePath((string)($result['payload_json_path'] ?? ''));
return $this->success($result);
} catch (\Throwable $throwable) {
return $this->error('9999', '保存站外 SEO 快照失败:' . $throwable->getMessage());
}
}
public function crawlLogSchema()
{
try {
return $this->success(DomainSpiderCrawlLogSchemaHelper::buildSchema());
} catch (\Throwable $throwable) {
return $this->error('9999', '读取蜘蛛抓取日志字段规则失败:' . $throwable->getMessage());
}
}
public function crawlLogChecklist()
{
try {
return $this->success(DomainSpiderCrawlLogSchemaHelper::buildChecklist());
} catch (\Throwable $throwable) {
return $this->error('9999', '读取蜘蛛抓取日志校验清单失败:' . $throwable->getMessage());
}
}
public function validateCrawlLogs(Request $request)
{
try {
$payload = $this->resolveCrawlLogPayload($request);
$result = DomainSpiderCrawlLogSchemaHelper::validatePayload($payload);
$runRoot = DomainSpiderCrawlLogRunHelper::createRunRoot($this->spiderCrawlRunRoot(), 'crawl_log_validate');
$result = DomainSpiderCrawlLogRunHelper::writeRunArtifacts($this->storageRoot(), $runRoot, $result, $payload);
$result['run_root'] = $this->storageRelativePath($runRoot);
$result['summary_json_path'] = $this->storageRelativePath((string)($result['summary_json_path'] ?? ''));
$result['summary_html_path'] = $this->storageRelativePath((string)($result['summary_html_path'] ?? ''));
$result['payload_json_path'] = $this->storageRelativePath((string)($result['payload_json_path'] ?? ''));
return $this->success($result);
} catch (\Throwable $throwable) {
return $this->error('9999', '校验蜘蛛抓取日志失败:' . $throwable->getMessage());
}
}
public function saveCrawlLogs(Request $request)
{
try {
$payload = $this->resolveCrawlLogPayload($request);
$source = trim((string)($payload['source'] ?? $request->post('source', 'frontend_nginx_push')));
$result = DomainSpiderCrawlLogHelper::ingestPayload($payload, $source);
$runRoot = DomainSpiderCrawlLogRunHelper::createRunRoot($this->spiderCrawlRunRoot(), 'crawl_log_push');
$result = DomainSpiderCrawlLogRunHelper::writeRunArtifacts($this->storageRoot(), $runRoot, $result, $payload);
$latest = DomainSpiderCrawlLogHelper::writeLatestArtifacts($this->spiderCrawlLatestRoot(), $result);
$result['run_root'] = $this->storageRelativePath($runRoot);
$result['summary_json_path'] = $this->storageRelativePath((string)($result['summary_json_path'] ?? ''));
$result['summary_html_path'] = $this->storageRelativePath((string)($result['summary_html_path'] ?? ''));
$result['payload_json_path'] = $this->storageRelativePath((string)($result['payload_json_path'] ?? ''));
$result['latest_summary_json_path'] = $this->storageRelativePath((string)($latest['summary_json_path'] ?? ''));
$result['latest_summary_html_path'] = $this->storageRelativePath((string)($latest['summary_html_path'] ?? ''));
return $this->success($result);
} catch (\Throwable $throwable) {
return $this->error('9999', '保存蜘蛛抓取日志失败:' . $throwable->getMessage());
}
}
protected function resolveSnapshots(Request $request): array
{
$snapshots = $request->post('snapshots/a', []);
if (empty($snapshots)) {
$snapshotsJson = trim((string)$request->post('snapshots_json', ''));
if ($snapshotsJson !== '') {
$decoded = json_decode($snapshotsJson, true);
$snapshots = is_array($decoded) ? $decoded : [];
}
}
if (empty($snapshots) && $request->getContent()) {
$decoded = json_decode((string)$request->getContent(), true);
if (is_array($decoded)) {
$snapshots = (array)($decoded['snapshots'] ?? $decoded);
}
}
return (array)$snapshots;
}
protected function resolveCrawlLogPayload(Request $request): array
{
$arrPayload = [
'source' => trim((string)$request->post('source', 'frontend_nginx_push')),
'summary_records' => $request->post('summary_records/a', []),
'anomaly_records' => $request->post('anomaly_records/a', []),
];
if ((!empty($arrPayload['summary_records']) || !empty($arrPayload['anomaly_records'])) === false && $request->getContent()) {
$arrDecoded = json_decode((string)$request->getContent(), true);
if (is_array($arrDecoded)) {
$arrPayload['source'] = trim((string)($arrDecoded['source'] ?? $arrPayload['source']));
$arrPayload['summary_records'] = array_values((array)($arrDecoded['summary_records'] ?? []));
$arrPayload['anomaly_records'] = array_values((array)($arrDecoded['anomaly_records'] ?? []));
}
}
return $arrPayload;
}
}