|
|
<?php
|
|
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
|
|
use App\Http\Controllers\Controller;
|
|
|
use App\Http\Controllers\Concerns\AuthorizesActivitySubmitter;
|
|
|
use App\Models\Activity;
|
|
|
use App\Support\VerifyPortalCode;
|
|
|
use Carbon\Carbon;
|
|
|
use Illuminate\Http\JsonResponse;
|
|
|
use Illuminate\Http\Request;
|
|
|
use Illuminate\Support\Str;
|
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
|
|
class ActivityController extends Controller
|
|
|
{
|
|
|
use AuthorizesActivitySubmitter;
|
|
|
public function index(Request $request): JsonResponse
|
|
|
{
|
|
|
$query = Activity::with(['venue:id,name', 'activityDays'])->orderByHotThenScheduleStatusPriority();
|
|
|
$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'));
|
|
|
}
|
|
|
if ($request->filled('schedule_status')) {
|
|
|
$query->whereComputedScheduleStatus($request->string('schedule_status'));
|
|
|
}
|
|
|
if ($request->filled('audit_status')) {
|
|
|
$query->where('audit_status', $request->string('audit_status'));
|
|
|
}
|
|
|
if ($request->filled('reservation_type')) {
|
|
|
$query->where('reservation_type', (string) $request->input('reservation_type'));
|
|
|
}
|
|
|
|
|
|
$pageSize = max(1, min(100, (int) $request->input('page_size', 10)));
|
|
|
$page = $query->paginate($pageSize);
|
|
|
$page->getCollection()->transform(function (Activity $a) use ($request) {
|
|
|
$a->setAttribute(
|
|
|
'schedule_status',
|
|
|
Activity::computeScheduleStatusForDisplay($a)
|
|
|
);
|
|
|
if (! $request->user()?->isSuperAdmin()) {
|
|
|
$a->makeHidden(['is_hot']);
|
|
|
}
|
|
|
|
|
|
return $a;
|
|
|
});
|
|
|
|
|
|
return response()->json($page);
|
|
|
}
|
|
|
|
|
|
public function store(Request $request): JsonResponse
|
|
|
{
|
|
|
$data = $request->validate([
|
|
|
'venue_id' => ['required', 'integer', 'exists:venues,id'],
|
|
|
'reservation_type' => ['nullable', 'string', 'max:32'],
|
|
|
'location' => ['required', 'string', 'max:500'],
|
|
|
'check_in_meeting_point' => ['nullable', 'string', 'max:500'],
|
|
|
'specific_time' => ['nullable', 'string', 'max:2000'],
|
|
|
'offline_reservation_method' => ['nullable', 'string', 'max:500'],
|
|
|
'ticket_fee_note' => ['nullable', 'string', 'max:1000'],
|
|
|
'external_url' => ['nullable', 'string', 'max:2000'],
|
|
|
'title' => ['required', 'string', 'max:150'],
|
|
|
'contact_name' => ['required', 'string', 'max:100'],
|
|
|
'summary' => ['nullable', 'string', 'max:255'],
|
|
|
'category' => ['nullable', 'string', 'max:50'],
|
|
|
'quota' => ['nullable', 'integer', 'min:0'],
|
|
|
'start_at' => ['required', 'date'],
|
|
|
'end_at' => ['required', 'date'],
|
|
|
'address' => ['nullable', 'string', 'max:255'],
|
|
|
'contact_phone' => ['required', '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'],
|
|
|
'is_hot' => ['sometimes', 'boolean'],
|
|
|
]);
|
|
|
if (! $request->user()?->isSuperAdmin()) {
|
|
|
unset($data['is_hot']);
|
|
|
}
|
|
|
$data = $this->applyReservationTypeDefaults($data);
|
|
|
$this->assertTicketNoteWhenNeeded($data, null);
|
|
|
$this->assertOfflineMethodWhenNeeded($data, null);
|
|
|
$this->assertExternalUrlWhenOther($data, null);
|
|
|
|
|
|
$this->ensureVenuePermission($request, (int) $data['venue_id']);
|
|
|
|
|
|
if (! $request->user()?->isSuperAdmin()) {
|
|
|
unset($data['sort']);
|
|
|
}
|
|
|
|
|
|
$this->normalizeActivityDates($data);
|
|
|
|
|
|
$auditStatus = $request->user()->isSuperAdmin() ? Activity::AUDIT_APPROVED : Activity::AUDIT_PENDING;
|
|
|
|
|
|
$activity = Activity::create($data + [
|
|
|
'quota' => $data['quota'] ?? 0,
|
|
|
'tags' => array_values($data['tags'] ?? []),
|
|
|
'sort' => $data['sort'] ?? 0,
|
|
|
'is_active' => $data['is_active'] ?? true,
|
|
|
'audit_status' => $auditStatus,
|
|
|
'audit_remark' => null,
|
|
|
'last_approved_snapshot' => null,
|
|
|
'submitted_by' => $request->user()->isSuperAdmin() ? null : $request->user()->id,
|
|
|
]);
|
|
|
if (! $activity->verify_portal_token) {
|
|
|
$activity->forceFill(['verify_portal_token' => (string) Str::uuid()])->save();
|
|
|
}
|
|
|
VerifyPortalCode::ensureForActivity($activity);
|
|
|
|
|
|
$activity->load('activityDays');
|
|
|
$activity->forceFill(['schedule_status' => Activity::computeScheduleStatusForDisplay($activity)])->saveQuietly();
|
|
|
|
|
|
$act = $activity->load('venue:id,name');
|
|
|
if (! $request->user()?->isSuperAdmin()) {
|
|
|
$act->makeHidden(['is_hot']);
|
|
|
}
|
|
|
|
|
|
return response()->json($act, 201);
|
|
|
}
|
|
|
|
|
|
public function update(Request $request, Activity $activity): JsonResponse
|
|
|
{
|
|
|
$this->ensureVenuePermission($request, $activity->venue_id);
|
|
|
$this->authorizeActivityEditForNonSuperAdmin($request, $activity);
|
|
|
|
|
|
$data = $request->validate([
|
|
|
'venue_id' => ['sometimes', 'integer', 'exists:venues,id'],
|
|
|
'reservation_type' => ['nullable', 'string', 'max:32'],
|
|
|
'location' => ['required', 'string', 'max:500'],
|
|
|
'check_in_meeting_point' => ['nullable', 'string', 'max:500'],
|
|
|
'specific_time' => ['nullable', 'string', 'max:2000'],
|
|
|
'offline_reservation_method' => ['nullable', 'string', 'max:500'],
|
|
|
'ticket_fee_note' => ['nullable', 'string', 'max:1000'],
|
|
|
'external_url' => ['nullable', 'string', 'max:2000'],
|
|
|
'title' => ['sometimes', 'string', 'max:150'],
|
|
|
'contact_name' => ['sometimes', 'nullable', 'string', 'max:100'],
|
|
|
'summary' => ['sometimes', 'nullable', 'string', 'max:255'],
|
|
|
'category' => ['nullable', 'string', 'max:50'],
|
|
|
'quota' => ['sometimes', 'integer', 'min:0'],
|
|
|
'start_at' => ['required', 'date'],
|
|
|
'end_at' => ['required', 'date'],
|
|
|
'address' => ['nullable', 'string', 'max:255'],
|
|
|
'contact_phone' => ['required', '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'],
|
|
|
'is_hot' => ['sometimes', 'boolean'],
|
|
|
]);
|
|
|
$data = $this->applyReservationTypeDefaults($data, $activity);
|
|
|
$this->assertTicketNoteWhenNeeded($data, $activity);
|
|
|
$this->assertOfflineMethodWhenNeeded($data, $activity);
|
|
|
$this->assertExternalUrlWhenOther($data, $activity);
|
|
|
|
|
|
if (array_key_exists('venue_id', $data)) {
|
|
|
$this->ensureVenuePermission($request, (int) $data['venue_id']);
|
|
|
}
|
|
|
$user = $request->user();
|
|
|
if (! $user?->isSuperAdmin()) {
|
|
|
unset($data['sort'], $data['is_hot']);
|
|
|
/** 场馆编辑(含已结束、已退回)后均进入待审核,由超级管理员再次审核 */
|
|
|
$data['audit_status'] = Activity::AUDIT_PENDING;
|
|
|
$data['audit_remark'] = null;
|
|
|
} else {
|
|
|
$data['audit_status'] = Activity::AUDIT_APPROVED;
|
|
|
$data['audit_remark'] = null;
|
|
|
$data['last_approved_snapshot'] = null;
|
|
|
}
|
|
|
|
|
|
$this->normalizeActivityDates($data);
|
|
|
|
|
|
$activity->fill($data)->save();
|
|
|
if (array_key_exists('tags', $data)) {
|
|
|
$activity->tags = array_values($data['tags'] ?? []);
|
|
|
$activity->save();
|
|
|
}
|
|
|
|
|
|
$activity->load('activityDays');
|
|
|
$activity->forceFill(['schedule_status' => Activity::computeScheduleStatusForDisplay($activity)])->saveQuietly();
|
|
|
|
|
|
$fresh = $activity->fresh()->load(['venue:id,name', 'activityDays']);
|
|
|
if (! $request->user()?->isSuperAdmin()) {
|
|
|
$fresh->makeHidden(['is_hot']);
|
|
|
}
|
|
|
|
|
|
return response()->json($fresh);
|
|
|
}
|
|
|
|
|
|
public function approve(Request $request, Activity $activity): JsonResponse
|
|
|
{
|
|
|
abort_unless($request->user()?->isSuperAdmin(), 403, '仅超级管理员可审核');
|
|
|
$this->ensureVenuePermission($request, $activity->venue_id);
|
|
|
|
|
|
$data = $request->validate([
|
|
|
'mark_hot' => ['required', 'boolean'],
|
|
|
]);
|
|
|
|
|
|
$activity->audit_status = Activity::AUDIT_APPROVED;
|
|
|
$activity->audit_remark = null;
|
|
|
$activity->last_approved_snapshot = null;
|
|
|
$activity->is_hot = (bool) $data['mark_hot'];
|
|
|
$activity->save();
|
|
|
|
|
|
return response()->json($activity->fresh()->load('venue:id,name'));
|
|
|
}
|
|
|
|
|
|
public function updateBehindScenes(Request $request, Activity $activity): JsonResponse
|
|
|
{
|
|
|
$this->ensureVenuePermission($request, $activity->venue_id);
|
|
|
$this->authorizeActivityEditForNonSuperAdmin($request, $activity);
|
|
|
$status = Activity::computeScheduleStatusForDisplay($activity->load('activityDays'));
|
|
|
abort_unless($status === 'ended', 422, '仅已结束活动可上传花絮');
|
|
|
|
|
|
$data = $request->validate([
|
|
|
'behind_scenes_media' => ['present', 'array'],
|
|
|
'behind_scenes_media.*.type' => ['required', 'in:image'],
|
|
|
'behind_scenes_media.*.url' => ['required', 'string', 'max:255'],
|
|
|
]);
|
|
|
|
|
|
$activity->behind_scenes_media = array_values($data['behind_scenes_media'] ?? []);
|
|
|
$activity->save();
|
|
|
|
|
|
$fresh = $activity->fresh()->load('venue:id,name');
|
|
|
if (! $request->user()?->isSuperAdmin()) {
|
|
|
$fresh->makeHidden(['is_hot']);
|
|
|
}
|
|
|
|
|
|
return response()->json($fresh);
|
|
|
}
|
|
|
|
|
|
public function setHotFlag(Request $request, Activity $activity): JsonResponse
|
|
|
{
|
|
|
abort_unless($request->user()?->isSuperAdmin(), 403, '仅超级管理员可设置热门活动');
|
|
|
$this->ensureVenuePermission($request, $activity->venue_id);
|
|
|
|
|
|
$data = $request->validate([
|
|
|
'is_hot' => ['required', 'boolean'],
|
|
|
]);
|
|
|
$activity->is_hot = (bool) $data['is_hot'];
|
|
|
$activity->save();
|
|
|
|
|
|
return response()->json($activity->fresh()->load('venue:id,name'));
|
|
|
}
|
|
|
|
|
|
public function reject(Request $request, Activity $activity): JsonResponse
|
|
|
{
|
|
|
abort_unless($request->user()?->isSuperAdmin(), 403, '仅超级管理员可审核');
|
|
|
$this->ensureVenuePermission($request, $activity->venue_id);
|
|
|
|
|
|
$data = $request->validate([
|
|
|
'remark' => ['nullable', 'string', 'max:2000'],
|
|
|
]);
|
|
|
|
|
|
$activity->audit_status = Activity::AUDIT_REJECTED;
|
|
|
$activity->audit_remark = $data['remark'] ?? null;
|
|
|
$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);
|
|
|
$this->authorizeActivityEditForNonSuperAdmin($request, $activity);
|
|
|
$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);
|
|
|
$this->authorizeActivityEditForNonSuperAdmin($request, $activity);
|
|
|
$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);
|
|
|
$this->authorizeActivityEditForNonSuperAdmin($request, $activity);
|
|
|
$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<string, mixed> $data
|
|
|
*/
|
|
|
private function normalizeActivityDates(array &$data): void
|
|
|
{
|
|
|
$tz = config('app.timezone');
|
|
|
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'], $tz)->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'], $tz)->endOfDay();
|
|
|
}
|
|
|
}
|
|
|
if (! empty($data['start_at']) && ! empty($data['end_at']) && $data['end_at']->lt($data['start_at'])) {
|
|
|
throw ValidationException::withMessages(['end_at' => ['结束日期不能早于开始日期']]);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @param array<string, mixed> $data
|
|
|
* @return array<string, mixed>
|
|
|
*/
|
|
|
private function applyReservationTypeDefaults(array $data, ?Activity $existing = null): array
|
|
|
{
|
|
|
$newTypes = [Activity::RESERVATION_TYPE_PHONE, Activity::RESERVATION_TYPE_WECHAT_MP, Activity::RESERVATION_TYPE_OFFLINE_VISIT];
|
|
|
|
|
|
if (array_key_exists('reservation_type', $data)) {
|
|
|
$t = (string) ($data['reservation_type'] ?? '');
|
|
|
$data['reservation_type'] = $t === '' ? Activity::RESERVATION_TYPE_ONLINE : $t;
|
|
|
} elseif ($existing !== null) {
|
|
|
$data['reservation_type'] = (string) ($existing->reservation_type ?? Activity::RESERVATION_TYPE_ONLINE);
|
|
|
} else {
|
|
|
$data['reservation_type'] = Activity::RESERVATION_TYPE_ONLINE;
|
|
|
}
|
|
|
|
|
|
$t = $data['reservation_type'];
|
|
|
|
|
|
if (in_array($t, $newTypes, true)) {
|
|
|
$data['external_url'] = null;
|
|
|
if (! array_key_exists('offline_reservation_method', $data) && $existing !== null) {
|
|
|
$data['offline_reservation_method'] = $existing->offline_reservation_method;
|
|
|
}
|
|
|
} elseif ($t === Activity::RESERVATION_TYPE_OFFLINE) {
|
|
|
$data['external_url'] = null;
|
|
|
if (! array_key_exists('offline_reservation_method', $data) && $existing !== null) {
|
|
|
$data['offline_reservation_method'] = $existing->offline_reservation_method;
|
|
|
}
|
|
|
} elseif ($t === Activity::RESERVATION_TYPE_OTHER) {
|
|
|
if (! array_key_exists('external_url', $data) && $existing !== null) {
|
|
|
$data['external_url'] = $existing->external_url;
|
|
|
}
|
|
|
$data['offline_reservation_method'] = null;
|
|
|
} elseif ($t === Activity::RESERVATION_TYPE_ONLINE) {
|
|
|
$data['external_url'] = null;
|
|
|
} elseif ($t === Activity::RESERVATION_TYPE_NONE) {
|
|
|
$data['external_url'] = null;
|
|
|
if (! array_key_exists('offline_reservation_method', $data) && $existing !== null) {
|
|
|
$data['offline_reservation_method'] = $existing->offline_reservation_method;
|
|
|
}
|
|
|
} else {
|
|
|
// 自定义文案或历史值:仅展示,不参与线上报名/外链;去掉外链以免脏数据
|
|
|
$data['external_url'] = null;
|
|
|
}
|
|
|
|
|
|
$ticket = trim((string) ($data['offline_reservation_method'] ?? ''));
|
|
|
if ($ticket !== Activity::TICKET_PAID) {
|
|
|
$data['ticket_fee_note'] = null;
|
|
|
} elseif (array_key_exists('ticket_fee_note', $data)) {
|
|
|
$fn = trim((string) $data['ticket_fee_note']);
|
|
|
$data['ticket_fee_note'] = $fn !== '' ? $fn : null;
|
|
|
}
|
|
|
|
|
|
return $data;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @param array<string, mixed> $data
|
|
|
*/
|
|
|
private function assertTicketNoteWhenNeeded(array $data, ?Activity $existing = null): void
|
|
|
{
|
|
|
$newTypes = [
|
|
|
Activity::RESERVATION_TYPE_PHONE,
|
|
|
Activity::RESERVATION_TYPE_WECHAT_MP,
|
|
|
Activity::RESERVATION_TYPE_OFFLINE_VISIT,
|
|
|
Activity::RESERVATION_TYPE_NONE,
|
|
|
Activity::RESERVATION_TYPE_ONLINE,
|
|
|
];
|
|
|
if (! in_array(($data['reservation_type'] ?? ''), $newTypes, true)) {
|
|
|
return;
|
|
|
}
|
|
|
$m = trim((string) ($data['offline_reservation_method'] ?? $existing?->offline_reservation_method ?? ''));
|
|
|
if (! in_array($m, [Activity::TICKET_FREE, Activity::TICKET_PAID], true)) {
|
|
|
throw ValidationException::withMessages(['offline_reservation_method' => ['请选择门票说明(免费/收费)']]);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @param array<string, mixed> $data
|
|
|
*/
|
|
|
private function assertOfflineMethodWhenNeeded(array $data, ?Activity $existing = null): void
|
|
|
{
|
|
|
if (($data['reservation_type'] ?? '') !== Activity::RESERVATION_TYPE_OFFLINE) {
|
|
|
return;
|
|
|
}
|
|
|
$m = trim((string) ($data['offline_reservation_method'] ?? $existing?->offline_reservation_method ?? ''));
|
|
|
if ($m === '') {
|
|
|
throw ValidationException::withMessages(['offline_reservation_method' => ['选择「线下预约」时请填写预约方式']]);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @param array<string, mixed> $data
|
|
|
*/
|
|
|
private function assertExternalUrlWhenOther(array $data, ?Activity $existing = null): void
|
|
|
{
|
|
|
if (($data['reservation_type'] ?? '') !== Activity::RESERVATION_TYPE_OTHER) {
|
|
|
return;
|
|
|
}
|
|
|
$url = trim((string) ($data['external_url'] ?? $existing?->external_url ?? ''));
|
|
|
if ($url === '') {
|
|
|
throw ValidationException::withMessages(['external_url' => ['选择「外链跳转预约」时请填写外链地址']]);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
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, '仅可操作已绑定场馆');
|
|
|
}
|
|
|
}
|