orderByDesc('id'); $this->restrictByVenue($request, $query); if ($request->filled('keyword')) { $keyword = trim((string) $request->input('keyword')); $query->where('title', 'like', "%{$keyword}%"); } if ($request->filled('venue_id')) { $query->where('venue_id', (int) $request->input('venue_id')); } if ($request->filled('is_active')) { $query->where('is_active', (bool) $request->boolean('is_active')); } $pageSize = max(1, min(100, (int) $request->input('page_size', 10))); return response()->json($query->paginate($pageSize)); } public function store(Request $request): JsonResponse { $data = $request->validate([ 'venue_id' => ['required', 'integer', 'exists:venues,id'], 'title' => ['required', 'string', 'max:150'], 'summary' => ['nullable', 'string', 'max:255'], 'category' => ['nullable', 'string', 'max:50'], 'quota' => ['nullable', 'integer', 'min:0'], 'start_at' => ['nullable', 'date'], 'end_at' => ['nullable', 'date'], 'address' => ['nullable', 'string', 'max:255'], 'lat' => ['nullable', 'numeric'], 'lng' => ['nullable', 'numeric'], 'detail_html' => ['nullable', 'string'], 'cover_image' => ['nullable', 'string', 'max:255'], 'gallery_media' => ['nullable', 'array'], 'gallery_media.*.type' => ['required_with:gallery_media', 'in:image,video'], 'gallery_media.*.url' => ['required_with:gallery_media', 'string', 'max:255'], 'tags' => ['nullable', 'array'], 'tags.*' => ['string', 'max:50'], 'reservation_notice' => ['nullable', 'string'], 'open_time' => ['nullable', 'string'], 'sort' => ['nullable', 'integer', 'min:0'], 'is_active' => ['boolean'], ]); $this->ensureVenuePermission($request, (int) $data['venue_id']); if (!$request->user()?->isSuperAdmin()) { unset($data['sort']); } $this->normalizeActivityDates($data); $activity = Activity::create($data + [ 'quota' => $data['quota'] ?? 0, 'tags' => array_values($data['tags'] ?? []), 'sort' => $data['sort'] ?? 0, 'is_active' => $data['is_active'] ?? true, ]); return response()->json($activity->load('venue:id,name'), 201); } public function update(Request $request, Activity $activity): JsonResponse { $this->ensureVenuePermission($request, $activity->venue_id); $data = $request->validate([ 'venue_id' => ['sometimes', 'integer', 'exists:venues,id'], 'title' => ['sometimes', 'string', 'max:150'], 'summary' => ['sometimes', 'nullable', 'string', 'max:255'], 'category' => ['nullable', 'string', 'max:50'], 'quota' => ['sometimes', 'integer', 'min:0'], 'start_at' => ['nullable', 'date'], 'end_at' => ['nullable', 'date'], 'address' => ['nullable', 'string', 'max:255'], 'lat' => ['nullable', 'numeric'], 'lng' => ['nullable', 'numeric'], 'detail_html' => ['nullable', 'string'], 'cover_image' => ['nullable', 'string', 'max:255'], 'gallery_media' => ['nullable', 'array'], 'gallery_media.*.type' => ['required_with:gallery_media', 'in:image,video'], 'gallery_media.*.url' => ['required_with:gallery_media', 'string', 'max:255'], 'tags' => ['sometimes', 'nullable', 'array'], 'tags.*' => ['string', 'max:50'], 'reservation_notice' => ['nullable', 'string'], 'open_time' => ['nullable', 'string'], 'sort' => ['nullable', 'integer', 'min:0'], 'is_active' => ['boolean'], ]); if (array_key_exists('venue_id', $data)) { $this->ensureVenuePermission($request, (int) $data['venue_id']); } if (!$request->user()?->isSuperAdmin()) { unset($data['sort']); } $this->normalizeActivityDates($data); $activity->fill($data)->save(); if (array_key_exists('tags', $data)) { $activity->tags = array_values($data['tags'] ?? []); $activity->save(); } return response()->json($activity->fresh()->load('venue:id,name')); } public function toggle(Request $request, Activity $activity): JsonResponse { $this->ensureVenuePermission($request, $activity->venue_id); $activity->is_active = !$activity->is_active; $activity->save(); return response()->json($activity->fresh()->load('venue:id,name')); } public function destroy(Request $request, Activity $activity): JsonResponse { $this->ensureVenuePermission($request, $activity->venue_id); $count = $activity->reservations()->count(); if ($count > 0) { return response()->json([ 'message' => '该活动已有报名记录,不能删除', 'reservation_count' => $count, ], 422); } $activity->delete(); return response()->json(['message' => '删除成功']); } public function restore(Request $request, int $activityId): JsonResponse { $activity = Activity::withTrashed()->findOrFail($activityId); $this->ensureVenuePermission($request, $activity->venue_id); $disableAfterRestore = $request->boolean('disable_after_restore'); $activity->restore(); if ($disableAfterRestore) { $activity->is_active = false; $activity->save(); } return response()->json($activity->fresh()->load('venue:id,name')); } /** * 开始/结束仅传年月日时:开始为当日 00:00:00,结束为当日 23:59:59,允许同一天。 * * @param array $data */ private function normalizeActivityDates(array &$data): void { if (array_key_exists('start_at', $data)) { if ($data['start_at'] === null || $data['start_at'] === '') { $data['start_at'] = null; } else { $data['start_at'] = Carbon::parse($data['start_at'])->startOfDay(); } } if (array_key_exists('end_at', $data)) { if ($data['end_at'] === null || $data['end_at'] === '') { $data['end_at'] = null; } else { $data['end_at'] = Carbon::parse($data['end_at'])->endOfDay(); } } if (!empty($data['start_at']) && !empty($data['end_at']) && $data['end_at']->lt($data['start_at'])) { throw ValidationException::withMessages(['end_at' => ['结束日期不能早于开始日期']]); } } private function restrictByVenue(Request $request, $query): void { $user = $request->user(); if ($user->isSuperAdmin()) { return; } $query->whereIn('venue_id', $user->venues()->pluck('venues.id')); } private function ensureVenuePermission(Request $request, int $venueId): void { $user = $request->user(); if ($user->isSuperAdmin()) { return; } $allowed = $user->venues()->where('venues.id', $venueId)->exists(); abort_unless($allowed, 403, '仅可操作已绑定场馆'); } }