Files
DevandCursor 60daff2365 搭建轻舟一期门户、后台和统一登录。
让分类、会员和示例工具共用同一账号,本地即可跑通登录与授权。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-09 16:46:35 +08:00

256 lines
9.8 KiB
PHP

<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Models\Agreement;
use App\Models\Article;
use App\Models\Banner;
use App\Models\Category;
use App\Models\HomeSection;
use App\Models\Navigation;
use App\Models\Plan;
use App\Models\Tool;
use App\Models\UserToolOpen;
use App\Support\Presenter;
use App\Support\Settings;
use Illuminate\Http\Request;
class PortalController extends Controller
{
public function meta()
{
$categories = Category::query()
->with(['children' => fn ($q) => $q->where('status', 'published')])
->whereNull('parent_id')
->where('status', 'published')
->where('show_in_nav', true)
->orderByDesc('sort')
->orderBy('id')
->get();
return $this->ok([
'site_name' => Settings::get('site_name', '轻舟'),
'description' => Settings::get('site_description', ''),
'icp' => Settings::get('icp', ''),
'mail_enabled' => Settings::bool('mail_enabled'),
'nav' => $this->nav('header'),
'footer' => $this->nav('footer'),
'categories' => $categories->map(fn (Category $category) => $this->categoryNode($category)),
]);
}
public function home()
{
$sections = HomeSection::query()
->with(['tools' => fn ($q) => $q->published()->with('tags')])
->where('status', 'published')
->orderByDesc('sort')
->orderBy('id')
->get();
$banners = Banner::query()->where('status', 'published')
->where(function ($q) {
$q->whereNull('starts_at')->orWhere('starts_at', '<=', now());
})
->where(function ($q) {
$q->whereNull('ends_at')->orWhere('ends_at', '>=', now());
})
->orderByDesc('sort')->get();
$announcement = Article::query()->published()->where('type', 'announcement')->latest('published_at')->first();
return $this->ok([
'sections' => $sections->map(fn (HomeSection $section) => [
'id' => $section->id,
'title' => $section->title,
'slug' => $section->slug,
'tools' => $section->tools->take($section->display_limit)->map(fn (Tool $tool) => Presenter::toolCard($tool))->values(),
]),
'banners' => $banners->map(fn (Banner $banner) => [
'id' => $banner->id,
'title' => $banner->title,
'image' => $banner->image,
'link' => $banner->link,
]),
'announcement' => $announcement ? [
'title' => $announcement->title,
'slug' => $announcement->slug,
'summary' => $announcement->summary,
] : null,
]);
}
public function category(Request $request, string $slug)
{
$category = Category::query()->with('children')->where('slug', $slug)->where('status', 'published')->firstOrFail();
$ids = $category->children->pluck('id')->push($category->id);
$query = Tool::query()->published()->with('tags')->whereHas('categories', fn ($q) => $q->whereIn('categories.id', $ids));
$this->applySort($query, (string) $request->query('sort', 'recommend'));
$page = $query->paginate(24);
return $this->ok([
'category' => $this->categoryNode($category),
'items' => collect($page->items())->map(fn (Tool $tool) => Presenter::toolCard($tool))->values(),
'total' => $page->total(),
'page' => $page->currentPage(),
'per_page' => $page->perPage(),
]);
}
public function tool(string $slug)
{
$tool = Tool::query()->published()->with('tags', 'categories')->where('slug', $slug)->firstOrFail();
$related = Tool::query()->published()->with('tags')
->whereKeyNot($tool->id)
->whereHas('categories', fn ($q) => $q->whereIn('categories.id', $tool->categories->pluck('id')))
->limit(4)
->get();
return $this->ok(Presenter::toolDetail($tool) + [
'related' => $related->map(fn (Tool $item) => Presenter::toolCard($item))->values(),
]);
}
public function go(string $slug)
{
$tool = Tool::query()->published()->where('slug', $slug)->where('type', 'link')->firstOrFail();
$tool->increment('clicks');
return redirect()->away($tool->open_url);
}
public function search(Request $request)
{
$keyword = trim((string) $request->query('q', ''));
$scope = (string) $request->query('scope', 'tools');
$like = '%'.addcslashes($keyword, '%_\\').'%';
if ($scope === 'help') {
$query = Article::query()->published()->where('type', 'help');
if ($keyword !== '') {
$query->where(fn ($q) => $q->where('title', 'like', $like)->orWhere('summary', 'like', $like));
}
$page = $query->latest('published_at')->paginate(24);
return $this->ok([
'items' => collect($page->items())->map(fn (Article $article) => [
'title' => $article->title,
'slug' => $article->slug,
'summary' => $article->summary,
])->values(),
'total' => $page->total(),
'page' => $page->currentPage(),
'per_page' => $page->perPage(),
]);
}
if ($scope === 'recent') {
$user = $request->user();
if (! $user) {
return response()->json(['message' => '请先登录', 'code' => 'unauthenticated'], 401);
}
$opens = UserToolOpen::query()->with('tool.tags')->where('user_id', $user->id)->orderByDesc('last_opened_at')->get();
$items = $opens->filter(function (UserToolOpen $open) use ($keyword) {
return $open->tool && $open->tool->status === 'published'
&& ($keyword === '' || str_contains($open->tool->name, $keyword) || str_contains($open->tool->summary, $keyword));
})->map(fn (UserToolOpen $open) => Presenter::toolCard($open->tool))->values();
return $this->ok(['items' => $items, 'total' => $items->count(), 'page' => 1, 'per_page' => $items->count()]);
}
$query = Tool::query()->published()->with('tags');
if ($keyword !== '') {
$query->where(function ($q) use ($like) {
$q->where('name', 'like', $like)
->orWhere('summary', 'like', $like)
->orWhereHas('tags', fn ($tag) => $tag->where('name', 'like', $like));
});
}
$page = $query->orderByDesc('is_recommended')->orderByDesc('sort')->paginate(24);
$hot = $page->total() === 0
? Tool::query()->published()->with('tags')->orderByDesc('is_recommended')->limit(8)->get()
: collect();
return $this->ok([
'items' => collect($page->items())->map(fn (Tool $tool) => Presenter::toolCard($tool))->values(),
'hot' => $hot->map(fn (Tool $tool) => Presenter::toolCard($tool))->values(),
'total' => $page->total(),
'page' => $page->currentPage(),
'per_page' => $page->perPage(),
]);
}
public function plans()
{
$plans = Plan::query()->with('entitlements')->where('status', 'published')->orderByDesc('sort')->orderBy('id')->get();
return $this->ok($plans->map(fn (Plan $plan) => Presenter::plan($plan))->values());
}
public function articles(Request $request)
{
$type = (string) $request->query('type', 'help');
$items = Article::query()->published()->where('type', $type)->latest('published_at')->get();
return $this->ok($items->map(fn (Article $article) => [
'title' => $article->title,
'slug' => $article->slug,
'summary' => $article->summary,
'published_at' => $article->published_at?->toIso8601String(),
])->values());
}
public function article(string $slug)
{
$article = Article::query()->published()->where('slug', $slug)->firstOrFail();
return $this->ok([
'title' => $article->title,
'slug' => $article->slug,
'summary' => $article->summary,
'body' => $article->body,
'type' => $article->type,
'published_at' => $article->published_at?->toIso8601String(),
]);
}
public function agreement(string $type)
{
$agreement = Agreement::query()->where('type', $type)->firstOrFail();
return $this->ok([
'type' => $agreement->type,
'title' => $agreement->title,
'body' => $agreement->body,
]);
}
private function nav(string $group): array
{
return Navigation::query()->where('group_name', $group)->where('status', 'published')->orderByDesc('sort')->get()
->map(fn (Navigation $item) => ['title' => $item->title, 'link' => $item->link])->all();
}
private function categoryNode(Category $category): array
{
return [
'id' => $category->id,
'name' => $category->name,
'slug' => $category->slug,
'icon' => $category->icon,
'children' => $category->children->map(fn (Category $child) => [
'id' => $child->id,
'name' => $child->name,
'slug' => $child->slug,
'icon' => $child->icon,
])->values(),
];
}
private function applySort($query, string $sort): void
{
match ($sort) {
'latest' => $query->orderByDesc('published_at'),
'name' => $query->orderBy('name'),
default => $query->orderByDesc('is_recommended')->orderByDesc('sort'),
};
}
}