with(['categoryItem']); if ($kw = $request->query('keyword')) { $query->where(function ($q) use ($kw) { $q->where('title', 'like', "%{$kw}%") ->orWhere('source', 'like', "%{$kw}%"); }); } if ($request->filled('category_dict_item_id')) { $query->where('category_dict_item_id', (int) $request->query('category_dict_item_id')); } if ($request->filled('status')) { $query->where('status', (int) $request->query('status')); } if ($request->filled('crawl_job_id')) { $query->where('crawl_job_id', (int) $request->query('crawl_job_id')); } if ($request->filled('has_cover')) { $has = (int) $request->query('has_cover'); if ($has === 1) { $query->whereNotNull('cover_url')->where('cover_url', '!=', ''); } else { $query->where(function ($q) { $q->whereNull('cover_url')->orWhere('cover_url', ''); }); } } $paginator = $query ->orderByDesc('published_at') ->orderByDesc('id') ->paginate((int) $request->query('page_size', 20)) ->withQueryString(); $paginator->getCollection()->transform(fn (News $n) => $this->serializeList($n)); return $this->paginated($paginator); } public function store(Request $request): JsonResponse { if (! DictType::query()->where('code', 'news_category')->where('status', 1)->exists()) { return $this->fail('字典「资讯分类」未配置,请执行 NewsDictionarySeeder 或在字典中维护 news_category', 422); } $data = $this->validatedNews($request); $row = News::query()->create($data); return $this->ok(['id' => $row->id], '已创建'); } public function show(int $news): JsonResponse { $model = News::query()->with(['categoryItem'])->findOrFail($news); return $this->ok($this->serializeDetail($model)); } public function update(Request $request, int $news): JsonResponse { $model = News::query()->findOrFail($news); $data = $this->validatedNews($request, partial: true); $model->fill($data); $model->save(); return $this->ok(null, '已保存'); } public function destroy(int $news): JsonResponse { News::query()->findOrFail($news)->delete(); return $this->ok(null, '已删除'); } /** 发布:status=1 并写入发布时间 */ public function publish(Request $request, int $news): JsonResponse { $data = $request->validate([ 'status' => ['required', 'integer', 'in:0,1'], 'published_at' => ['nullable', 'date'], ]); $model = News::query()->findOrFail($news); $model->status = (int) $data['status']; if ($model->status === 1) { $model->published_at = isset($data['published_at']) ? \Carbon\Carbon::parse($data['published_at']) : ($model->published_at ?? now()); } else { $model->published_at = null; } $model->save(); return $this->ok(null, '已更新发布状态'); } /** * @return array */ protected function validatedNews(Request $request, bool $partial = false): array { $categoryTypeId = DictType::query()->where('code', 'news_category')->where('status', 1)->value('id'); $rules = [ 'title' => [$partial ? 'sometimes' : 'required', 'string', 'max:255'], 'category_dict_item_id' => $this->dictItemRules($categoryTypeId, $partial ? 'sometimes' : 'required'), 'source' => ['nullable', 'string', 'max:128'], 'cover_url' => ['nullable', 'string', 'max:512'], 'content_html' => [$partial ? 'sometimes' : 'required', 'string'], 'published_at' => [$partial ? 'sometimes' : 'required', 'date'], 'status' => ['nullable', 'integer', 'in:0,1'], ]; return $request->validate($rules); } /** * @return array */ protected function dictItemRules(?int $dictTypeId, string $presence): array { if (! $dictTypeId) { return match ($presence) { 'required' => ['required', 'integer'], 'sometimes' => ['sometimes', 'nullable', 'integer'], default => ['nullable', 'integer'], }; } $exists = Rule::exists('dict_items', 'id')->where( fn ($q) => $q->where('dict_type_id', $dictTypeId)->where('status', 1) ); return match ($presence) { 'required' => ['required', 'integer', $exists], 'sometimes' => ['sometimes', 'nullable', 'integer', $exists], default => ['nullable', 'integer', $exists], }; } /** * @return array */ protected function serializeList(News $n): array { return [ 'id' => $n->id, 'title' => $n->title, 'category_dict_item_id' => $n->category_dict_item_id, 'category_item' => $this->serializeDictItem($n->categoryItem), 'source' => $n->source, 'source_url' => $n->source_url, 'cover_url' => $n->cover_url, 'has_cover' => $n->cover_url ? 1 : 0, 'status' => (int) $n->status, 'published_at' => $n->published_at?->toIso8601String(), 'created_at' => $n->created_at?->toIso8601String(), ]; } /** * @return array */ protected function serializeDetail(News $n): array { $row = $this->serializeList($n); $row['content_html'] = $n->content_html; return $row; } /** * @return array{id:int,label:string,value:string}|null */ protected function serializeDictItem(?DictItem $item): ?array { if (! $item) { return null; } return [ 'id' => $item->id, 'label' => $item->label, 'value' => $item->value, ]; } }