user()?->isSuperAdmin(), 403, '仅超级管理员可操作'); $data = $request->validate([ 'name' => ['required', 'string', 'max:120'], 'venue_type' => ['nullable', 'string', 'max:80'], 'unit_name' => ['nullable', 'string', 'max:120'], 'district' => ['nullable', 'string', 'max:80'], 'ticket_type' => ['nullable', 'string', 'max:80'], 'open_time' => ['nullable', 'string', 'max:120'], 'reservation_notice' => ['nullable', 'string'], 'study_courses' => ['nullable', 'string'], 'address' => ['nullable', 'string', 'max:255'], 'lat' => ['nullable', 'numeric'], 'lng' => ['nullable', 'numeric'], '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'], 'detail_html' => ['nullable', 'string'], 'live_people_count' => ['nullable', 'integer', 'min:0'], 'sort' => ['nullable', 'integer', 'min:0'], 'is_active' => ['boolean'], ]); $venue = Venue::create($data + ['is_active' => $data['is_active'] ?? true]); return response()->json($venue, 201); } public function index(Request $request): JsonResponse { $user = $request->user(); $keyword = trim((string) $request->string('keyword')); $district = trim((string) $request->string('district')); $venueType = trim((string) $request->string('venue_type')); $ticketType = trim((string) $request->string('ticket_type')); $isActive = $request->input('is_active'); // 必须用关联本身的查询,勿用 getQuery()->get(),否则会绕过 BelongsToMany::get() // 的 select `venues.*`,在 join user_venue 时 `id` 可能被 pivot 表的 id 覆盖(列表 id 比库大 1)。 if ($user->isSuperAdmin()) { $query = Venue::query(); } else { $query = $user->venues(); } if ($keyword !== '') { $query->where(function ($q) use ($keyword) { $q->where('name', 'like', '%' . $keyword . '%') ->orWhere('address', 'like', '%' . $keyword . '%') ->orWhere('unit_name', 'like', '%' . $keyword . '%') ->orWhere('open_time', 'like', '%' . $keyword . '%') ->orWhere('reservation_notice', 'like', '%' . $keyword . '%') ->orWhere('study_courses', 'like', '%' . $keyword . '%'); }); } if ($district !== '') { $query->where('district', $district); } if ($venueType !== '') { $query->where('venue_type', $venueType); } if ($ticketType !== '') { $query->where('ticket_type', $ticketType); } if ($isActive !== null && $isActive !== '') { $query->where('is_active', (int) $isActive === 1); } $venues = $query->orderBy('venues.sort')->orderByDesc('venues.id')->get(); return response()->json($venues); } public function update(Request $request, int $id): JsonResponse { $venue = Venue::query()->findOrFail($id); $this->ensureVenuePermission($request, $venue->id); $data = $request->validate([ 'name' => ['sometimes', 'string', 'max:120'], 'venue_type' => ['nullable', 'string', 'max:80'], 'unit_name' => ['nullable', 'string', 'max:120'], 'district' => ['nullable', 'string', 'max:80'], 'ticket_type' => ['nullable', 'string', 'max:80'], 'open_time' => ['nullable', 'string', 'max:120'], 'reservation_notice' => ['nullable', 'string'], 'study_courses' => ['nullable', 'string'], 'address' => ['nullable', 'string', 'max:255'], 'lat' => ['nullable', 'numeric'], 'lng' => ['nullable', 'numeric'], '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'], 'detail_html' => ['nullable', 'string'], 'live_people_count' => ['nullable', 'integer', 'min:0'], 'sort' => ['nullable', 'integer', 'min:0'], 'is_active' => ['boolean'], ]); if (!$request->user()?->isSuperAdmin()) { unset($data['sort']); } $venue->fill($data)->save(); return response()->json($venue); } 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, '仅可操作已绑定场馆'); } }