Files
ai-web/app/Http/Controllers/Site/ToolController.php

296 lines
9.7 KiB
PHP
Raw Normal View History

2026-02-11 17:28:36 +08:00
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Site;
use App\Http\Controllers\Controller;
use App\Models\AiModel;
use App\Models\Article;
use App\Models\Category;
use App\Models\Tool;
use App\Support\MarkdownRenderer;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class ToolController extends Controller
{
public function __construct(
private readonly MarkdownRenderer $markdownRenderer,
) {
}
public function index(Request $request): View
{
$querySignature = sha1((string) $request->getQueryString());
$activeTab = $this->resolveTab($request);
2026-02-12 10:57:53 +08:00
$payload = Cache::remember("tools_portal_v2_{$querySignature}", now()->addMinutes(10), function () use ($request, $activeTab): array {
2026-02-11 17:28:36 +08:00
$builder = Tool::query()
->published()
2026-02-12 10:57:53 +08:00
->with('category');
2026-02-11 17:28:36 +08:00
2026-02-12 10:57:53 +08:00
$this->applyFilters($builder, $request, false);
2026-02-11 17:28:36 +08:00
$this->applyTabSorting($builder, $activeTab);
2026-02-12 10:57:53 +08:00
$categories = Category::query()
2026-02-11 17:28:36 +08:00
->where('type', 'tool')
->where('is_active', true)
->withCount([
'tools as published_tools_count' => fn (Builder $query): Builder => $query->published(),
])
->orderByDesc('published_tools_count')
->orderBy('name')
->get();
2026-02-12 10:57:53 +08:00
$portalTools = $builder->limit(360)->get();
$toolsByCategory = $portalTools->groupBy(fn (Tool $tool): string => $tool->category?->slug ?? 'uncategorized');
$categorySections = $categories->map(function (Category $category) use ($toolsByCategory): array {
$sectionTools = $toolsByCategory->get($category->slug, collect())->take(18);
return [
'slug' => $category->slug,
'name' => $category->name,
'count' => (int) ($category->published_tools_count ?? 0),
'tools' => $sectionTools,
];
})->values();
2026-02-11 17:28:36 +08:00
return [
2026-02-12 10:57:53 +08:00
'categories' => $categories,
'categorySections' => $categorySections,
'hotTools' => $portalTools->take(18),
'latestTools' => Tool::query()->published()->with('category')->latest('published_at')->limit(18)->get(),
'filters' => $request->only(['q', 'pricing', 'api', 'tab']),
2026-02-11 17:28:36 +08:00
'activeTab' => $activeTab,
'tabOptions' => $this->tabOptions(),
2026-02-12 10:57:53 +08:00
'toolStats' => $this->buildToolStats(),
2026-02-11 17:28:36 +08:00
];
});
return view('public.tools.index', $payload);
}
2026-02-12 10:57:53 +08:00
public function list(Request $request): View
{
$activeTab = $this->resolveTab($request);
$builder = Tool::query()
->published()
->with('category');
$this->applyFilters($builder, $request, true);
$this->applyTabSorting($builder, $activeTab);
$categories = Category::query()
->where('type', 'tool')
->where('is_active', true)
->withCount([
'tools as published_tools_count' => fn (Builder $query): Builder => $query->published(),
])
->orderByDesc('published_tools_count')
->orderBy('name')
->get();
return view('public.tools.list', [
'items' => $builder->paginate(36)->withQueryString(),
'categories' => $categories,
'filters' => $request->only(['q', 'category', 'pricing', 'api', 'tab']),
'activeTab' => $activeTab,
'tabOptions' => $this->tabOptions(),
'toolStats' => $this->buildToolStats(),
'sidebarModels' => AiModel::query()->published()->orderByDesc('total_score')->limit(8)->get(),
'sidebarNews' => Article::query()->published()->latest('published_at')->limit(8)->get(),
]);
}
2026-02-11 17:28:36 +08:00
public function byCategory(string $slug, Request $request): View
{
$request->merge(['category' => $slug]);
2026-02-12 10:57:53 +08:00
return $this->list($request);
2026-02-11 17:28:36 +08:00
}
public function show(string $slug): View
{
/** @var Tool $tool */
$tool = Tool::query()
->published()
->with(['category', 'source', 'alternative'])
->where('slug', $slug)
->firstOrFail();
$relatedTools = Tool::query()
->published()
->whereKeyNot($tool->id)
->when($tool->category_id !== null, fn (Builder $query): Builder => $query->where('category_id', $tool->category_id))
->orderByDesc('published_at')
->limit(8)
->get();
$latestTools = Tool::query()
->published()
->whereKeyNot($tool->id)
->with('category')
->latest('published_at')
->limit(10)
->get();
return view('public.tools.show', [
'item' => $tool,
'relatedTools' => $relatedTools,
'latestTools' => $latestTools,
'capabilityTags' => $this->extractCapabilityTags($tool),
'descriptionHtml' => $this->markdownRenderer->render($tool->description),
'showRiskNotice' => $this->containsRiskKeyword($tool->summary.' '.$tool->description),
]);
}
private function resolveTab(Request $request): string
{
$tab = (string) $request->string('tab');
$allowedTabs = array_column($this->tabOptions(), 'key');
return in_array($tab, $allowedTabs, true) ? $tab : 'recommended';
}
/**
* @return array<int, array{key: string, label: string}>
*/
private function tabOptions(): array
{
return [
['key' => 'recommended', 'label' => '综合推荐'],
['key' => 'latest', 'label' => '最新收录'],
2026-02-12 10:57:53 +08:00
['key' => 'api', 'label' => 'API优先'],
2026-02-11 17:28:36 +08:00
['key' => 'free', 'label' => '免费优先'],
];
}
private function applyTabSorting(Builder $builder, string $activeTab): void
{
if ($activeTab === 'api') {
$builder->where('has_api', true);
}
if ($activeTab === 'free') {
$builder->whereIn('pricing_type', ['free', 'freemium']);
}
if ($activeTab === 'latest') {
$builder->orderByDesc('published_at');
return;
}
$builder
->orderBy('is_stale')
->orderByDesc('has_api')
->orderByDesc('published_at');
}
2026-02-12 10:57:53 +08:00
private function applyFilters(Builder $builder, Request $request, bool $withCategory): void
2026-02-11 17:28:36 +08:00
{
if ($request->filled('q')) {
$keyword = trim((string) $request->string('q'));
$builder->whereFullText(['name', 'summary', 'description'], $keyword);
}
2026-02-12 10:57:53 +08:00
if ($withCategory && $request->filled('category')) {
2026-02-11 17:28:36 +08:00
$builder->whereHas('category', function (Builder $categoryQuery) use ($request): void {
$categoryQuery->where('slug', (string) $request->string('category'));
});
}
if ($request->filled('pricing')) {
$builder->where('pricing_type', (string) $request->string('pricing'));
}
if ($request->filled('api')) {
$builder->where('has_api', $request->boolean('api'));
}
}
2026-02-12 10:57:53 +08:00
/**
* @return array{total:int, api:int, free:int, updated_7d:int}
*/
private function buildToolStats(): array
{
return [
'total' => Tool::query()->published()->count(),
'api' => Tool::query()->published()->where('has_api', true)->count(),
'free' => Tool::query()->published()->whereIn('pricing_type', ['free', 'freemium'])->count(),
'updated_7d' => Tool::query()->published()->where('updated_at', '>=', now()->subDays(7))->count(),
];
}
2026-02-11 17:28:36 +08:00
private function containsRiskKeyword(string $text): bool
{
return str_contains($text, '医疗')
|| str_contains($text, '法律')
|| str_contains($text, '投资')
|| str_contains($text, 'medical')
|| str_contains($text, 'legal')
|| str_contains($text, 'investment');
}
/**
* @return array<int, string>
*/
private function extractCapabilityTags(Tool $tool): array
{
$text = mb_strtolower($tool->name.' '.$tool->summary.' '.$tool->description);
$tagMap = [
'写作' => '内容写作',
'文案' => '文案生成',
'翻译' => '多语翻译',
'图像' => '图像生成',
'图片' => '图像处理',
'视频' => '视频生成',
'语音' => '语音处理',
'音频' => '音频处理',
'客服' => '智能客服',
'代码' => '代码助手',
'编程' => '开发辅助',
'搜索' => '知识检索',
'自动化' => '流程自动化',
'agent' => 'Agent 工作流',
'api' => '开放 API',
];
$tags = [];
foreach ($tagMap as $keyword => $label) {
if (str_contains($text, $keyword)) {
$tags[] = $label;
}
}
if ($tool->has_api) {
$tags[] = 'API 集成';
}
if ($tool->pricing_type === 'free') {
$tags[] = '完全免费';
}
if ($tool->pricing_type === 'freemium') {
$tags[] = '免费增值';
}
if ($tool->pricing_type === 'paid') {
$tags[] = '商业付费';
}
if ($tool->category?->name !== null) {
$tags[] = $tool->category->name;
}
return array_values(array_unique(array_slice($tags, 0, 8)));
}
}
2026-02-12 10:57:53 +08:00