This commit is contained in:
cjd
2026-02-05 22:22:10 +08:00
parent fef9fe0c31
commit bf3a2e6971
273 changed files with 30605 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
<?php
namespace App\Http\Controllers;
use App\Models\Article;
use App\Models\ContentViewLog;
use App\Models\SiteSetting;
use Illuminate\Http\Request;
class ArticleController extends Controller
{
public function index(Request $request)
{
$perPage = (int) SiteSetting::value('article_page_size', 10);
$tagSlug = $request->query('tag');
$query = Article::published()->orderByDesc('published_at');
if ($tagSlug) {
$query->whereHas('tags', function ($tagQuery) use ($tagSlug) {
$tagQuery->where('slug', $tagSlug);
});
}
$articles = $query->paginate($perPage);
return view('article.index', [
'articles' => $articles,
'tagSlug' => $tagSlug,
]);
}
public function show(string $slug, Request $request)
{
$article = Article::with('tags')->where('slug', $slug)->firstOrFail();
$this->recordView($article, $request);
$comments = $article->comments()
->where('status', 'approved')
->orderByDesc('created_at')
->get();
$relatedArticles = Article::published()
->where('id', '!=', $article->id)
->whereHas('tags', function ($query) use ($article) {
$query->whereIn('tags.id', $article->tags->pluck('id'));
})
->orderByDesc('published_at')
->limit(6)
->get();
return view('article.show', [
'article' => $article,
'comments' => $comments,
'relatedArticles' => $relatedArticles,
]);
}
private function recordView(Article $article, Request $request): void
{
$ip = $request->ip() ?? '0.0.0.0';
$userAgent = $request->userAgent() ?? 'unknown';
$userAgentHash = sha1($userAgent);
$log = ContentViewLog::firstOrCreate([
'content_type' => 'article',
'content_id' => $article->id,
'ip' => $ip,
'user_agent_hash' => $userAgentHash,
'viewed_on' => now()->toDateString(),
]);
if ($log->wasRecentlyCreated) {
$article->increment('view_count');
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Http\Controllers;
use App\Models\Category;
use App\Models\Tag;
use App\Models\SiteSetting;
class CategoryController extends Controller
{
public function index()
{
$categories = Category::withCount('products')
->whereNull('parent_id')
->orderBy('sort')
->get();
return view('category.index', [
'categories' => $categories,
]);
}
public function show(string $slug)
{
$category = Category::where('slug', $slug)->firstOrFail();
$perPage = (int) SiteSetting::value('list_page_size', 20);
$moreThreshold = (int) SiteSetting::value('list_more_threshold', 20);
$tagSlug = request()->query('tag');
$pricing = request()->query('pricing');
$tags = Tag::orderBy('name')->get();
$products = $category->products()
->published()
->when($tagSlug, function ($builder) use ($tagSlug) {
$builder->whereHas('tags', function ($query) use ($tagSlug) {
$query->where('slug', $tagSlug);
});
})
->when($pricing, function ($builder) use ($pricing) {
$builder->where('pricing_type', $pricing);
})
->orderBy('sort')
->orderByDesc('hot_score')
->paginate($perPage);
return view('category.show', [
'category' => $category,
'products' => $products,
'moreThreshold' => $moreThreshold,
'tags' => $tags,
'tagSlug' => $tagSlug,
'pricing' => $pricing,
]);
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace App\Http\Controllers;
use App\Models\Article;
use App\Models\Comment;
use App\Models\Product;
use App\Models\SensitiveWord;
use App\Models\SiteSetting;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class CommentController extends Controller
{
public function captcha()
{
$code = (string) random_int(1000, 9999);
session(['captcha_code' => $code]);
$svg = <<<SVG
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="44">
<rect width="100%" height="100%" fill="#f3f4f6"/>
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle"
font-family="Arial, sans-serif" font-size="20" fill="#111827">{$code}</text>
</svg>
SVG;
return response($svg, 200)->header('Content-Type', 'image/svg+xml');
}
public function store(Request $request)
{
if (!SiteSetting::value('comments_enabled', '1')) {
return back()->withErrors(['comments' => '评论功能已关闭。']);
}
$data = $request->validate([
'target_type' => ['required', 'in:product,article'],
'target_id' => ['required', 'integer'],
'nickname' => ['required', 'string', 'max:50'],
'email' => ['nullable', 'email', 'max:255'],
'content' => ['required', 'string', 'max:1000'],
'captcha' => ['required', 'string', 'max:10'],
]);
$captcha = (string) session('captcha_code');
session()->forget('captcha_code');
if ($captcha === '' || $data['captcha'] !== $captcha) {
return back()->withErrors(['captcha' => '验证码错误,请重试。']);
}
$ip = $request->ip() ?? '0.0.0.0';
$rateKey = "comment_rate_{$ip}";
if (Cache::has($rateKey)) {
return back()->withErrors(['comments' => '提交太频繁,请稍后再试。']);
}
Cache::put($rateKey, true, now()->addMinutes(10));
$targetExists = $data['target_type'] === 'product'
? Product::where('id', $data['target_id'])->exists()
: Article::where('id', $data['target_id'])->exists();
if (!$targetExists) {
return back()->withErrors(['comments' => '评论目标不存在。']);
}
$content = $this->filterSensitiveWords($data['content']);
Comment::create([
'target_type' => $data['target_type'],
'target_id' => $data['target_id'],
'nickname' => $data['nickname'],
'email' => $data['email'] ?? null,
'content' => $content,
'status' => 'pending',
'ip' => $ip,
'user_agent' => $request->userAgent(),
]);
return back()->with('success', '评论已提交,审核通过后展示。');
}
public function like(Comment $comment)
{
$comment->increment('like_count');
return back();
}
private function filterSensitiveWords(string $content): string
{
$words = SensitiveWord::pluck('word')->filter()->all();
foreach ($words as $word) {
$content = str_replace($word, str_repeat('*', mb_strlen($word)), $content);
}
return $content;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers;
use App\Models\ContactMessage;
use Illuminate\Http\Request;
class ContactController extends Controller
{
public function show()
{
return view('page.contact');
}
public function store(Request $request)
{
$data = $request->validate([
'name' => ['required', 'string', 'max:100'],
'email' => ['required', 'email', 'max:255'],
'content' => ['required', 'string', 'max:2000'],
]);
ContactMessage::create($data);
return back()->with('success', '提交成功,我们会尽快联系你。');
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests, ValidatesRequests;
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers;
use App\Models\Category;
use App\Models\Product;
use App\Models\SiteSetting;
class HomeController extends Controller
{
public function index()
{
$featuredLimit = (int) SiteSetting::value('home_featured_limit', 8);
$newLimit = (int) SiteSetting::value('home_new_limit', 8);
$categoryLimit = (int) SiteSetting::value('home_category_limit', 20);
$featuredProducts = Product::published()
->featured()
->orderBy('sort')
->orderByDesc('hot_score')
->limit($featuredLimit)
->get();
$newProducts = Product::published()
->orderByDesc('sort')
->orderByDesc('created_at')
->limit($newLimit)
->get();
$categories = Category::with([
'children',
'products' => function ($query) use ($categoryLimit) {
$query->published()
->orderBy('sort')
->orderByDesc('hot_score')
->limit($categoryLimit);
},
])
->whereNull('parent_id')
->orderBy('sort')
->get();
return view('home', [
'featuredProducts' => $featuredProducts,
'newProducts' => $newProducts,
'categories' => $categories,
]);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Controllers;
use App\Models\Product;
class OutboundController extends Controller
{
public function redirect(string $slug)
{
$product = Product::where('slug', $slug)->firstOrFail();
$product->increment('click_count');
$product->refreshHotScore();
return redirect()->away($product->website_url);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Controllers;
use App\Models\SiteSetting;
class PageController extends Controller
{
public function about()
{
$content = SiteSetting::value('about_content', "我们专注于收录优质 AI 工具与产品,帮助用户快速找到合适的解决方案。");
return view('page.about', [
'content' => $content,
]);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Http\Controllers;
use App\Models\ContentViewLog;
use App\Models\Product;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function show(string $slug, Request $request)
{
$product = Product::with('tags', 'category')->where('slug', $slug)->firstOrFail();
$this->recordView($product, $request);
$comments = $product->comments()
->where('status', 'approved')
->orderByDesc('created_at')
->get();
$relatedProducts = Product::published()
->where('id', '!=', $product->id)
->whereHas('tags', function ($query) use ($product) {
$query->whereIn('tags.id', $product->tags->pluck('id'));
})
->orderByDesc('hot_score')
->limit(8)
->get();
return view('product.show', [
'product' => $product,
'comments' => $comments,
'relatedProducts' => $relatedProducts,
]);
}
private function recordView(Product $product, Request $request): void
{
$ip = $request->ip() ?? '0.0.0.0';
$userAgent = $request->userAgent() ?? 'unknown';
$userAgentHash = sha1($userAgent);
$log = ContentViewLog::firstOrCreate([
'content_type' => 'product',
'content_id' => $product->id,
'ip' => $ip,
'user_agent_hash' => $userAgentHash,
'viewed_on' => now()->toDateString(),
]);
if ($log->wasRecentlyCreated) {
$product->increment('view_count');
$product->refreshHotScore();
}
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use App\Models\SiteSetting;
use Illuminate\Http\Request;
class SearchController extends Controller
{
public function index(Request $request)
{
$query = trim((string) $request->query('q', ''));
$perPage = (int) SiteSetting::value('list_page_size', 20);
$products = Product::published()
->when($query !== '', function ($builder) use ($query) {
$builder->where('name', 'like', "%{$query}%");
})
->orderByDesc('hot_score')
->paginate($perPage)
->appends(['q' => $query]);
return view('search.index', [
'query' => $query,
'products' => $products,
]);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Http\Controllers;
use App\Models\Article;
use App\Models\Category;
use App\Models\Product;
use App\Models\Tag;
class SitemapController extends Controller
{
public function index()
{
$baseUrl = rtrim(config('app.url'), '/');
$urls = [
'/',
'/categories',
'/tags',
'/articles',
'/about',
'/contact',
];
$categories = Category::all()->map(fn ($c) => "/category/{$c->slug}")->all();
$tags = Tag::all()->map(fn ($t) => "/tag/{$t->slug}")->all();
$products = Product::published()->get()->map(fn ($p) => "/product/{$p->slug}")->all();
$articles = Article::published()->get()->map(fn ($a) => "/article/{$a->slug}")->all();
$allUrls = array_merge($urls, $categories, $tags, $products, $articles);
$xml = new \XMLWriter();
$xml->openMemory();
$xml->startDocument('1.0', 'UTF-8');
$xml->startElement('urlset');
$xml->writeAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
foreach ($allUrls as $path) {
$xml->startElement('url');
$xml->writeElement('loc', $baseUrl . $path);
$xml->endElement();
}
$xml->endElement();
$xml->endDocument();
return response($xml->outputMemory(), 200)
->header('Content-Type', 'application/xml');
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers;
use App\Models\SiteSetting;
use App\Models\Tag;
class TagController extends Controller
{
public function index()
{
$query = request()->query('q');
$tags = Tag::when($query, function ($builder) use ($query) {
$builder->where('name', 'like', "%{$query}%");
})
->orderByDesc('hot_score')
->orderBy('name')
->get();
return view('tag.index', [
'tags' => $tags,
'query' => $query,
]);
}
public function show(string $slug)
{
$tag = Tag::where('slug', $slug)->firstOrFail();
$perPage = (int) SiteSetting::value('list_page_size', 20);
$moreThreshold = (int) SiteSetting::value('list_more_threshold', 20);
$pricing = request()->query('pricing');
$products = $tag->products()
->published()
->when($pricing, function ($builder) use ($pricing) {
$builder->where('pricing_type', $pricing);
})
->orderBy('product_tag.sort')
->orderByDesc('hot_score')
->paginate($perPage);
return view('tag.show', [
'tag' => $tag,
'products' => $products,
'moreThreshold' => $moreThreshold,
'pricing' => $pricing,
]);
}
}