You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
421 lines
17 KiB
421 lines
17 KiB
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\TicketGrabEvent;
|
|
use App\Models\TicketGrabEventVenue;
|
|
use App\Models\TicketGrabVenueReleaseDay;
|
|
use App\Models\Reservation;
|
|
use App\Services\TicketGrabReleaseDayService;
|
|
use App\Support\CalendarDateFormat;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class TicketGrabEventController extends Controller
|
|
{
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$query = TicketGrabEvent::query()
|
|
->with('venues:id,name')
|
|
->orderByDesc('id');
|
|
|
|
$this->restrictByEventVenues($request, $query);
|
|
|
|
if ($request->filled('keyword')) {
|
|
$keyword = trim((string) $request->input('keyword'));
|
|
$query->where('title', 'like', "%{$keyword}%");
|
|
}
|
|
if ($request->filled('is_active')) {
|
|
$query->where('is_active', (bool) $request->boolean('is_active'));
|
|
}
|
|
if ($request->filled('schedule_status')) {
|
|
$st = (string) $request->string('schedule_status');
|
|
if (in_array($st, ['not_started', 'ongoing', 'ended'], true)) {
|
|
$query->whereComputedScheduleStatus($st);
|
|
}
|
|
}
|
|
|
|
$pageSize = max(1, min(100, (int) $request->input('page_size', 10)));
|
|
$page = $query->paginate($pageSize);
|
|
$page->getCollection()->transform(function (TicketGrabEvent $e) {
|
|
$e->setAttribute(
|
|
'schedule_status',
|
|
TicketGrabEvent::computeScheduleStatusFromBounds($e->start_at, $e->end_at)
|
|
);
|
|
|
|
return $e;
|
|
});
|
|
|
|
return response()->json($page);
|
|
}
|
|
|
|
public function store(Request $request): JsonResponse
|
|
{
|
|
$data = $this->validated($request, true);
|
|
$this->assertVenueIdsAllowed($request, $data['venue_ids'] ?? []);
|
|
|
|
$audit = $request->user()?->isSuperAdmin() ? TicketGrabEvent::AUDIT_APPROVED : TicketGrabEvent::AUDIT_PENDING;
|
|
|
|
$ctrl = $this;
|
|
$e = DB::transaction(function () use ($data, $audit, $ctrl) {
|
|
$e = new TicketGrabEvent;
|
|
$e->fill($ctrl->mapMainFields($data, true, true));
|
|
$e->audit_status = $audit;
|
|
$e->total_quota = 0;
|
|
$e->registered_count = 0;
|
|
$e->schedule_status = TicketGrabEvent::computeScheduleStatusFromBounds($e->start_at, $e->end_at);
|
|
$e->save();
|
|
$ctrl->syncPivots($e->id, $data['venues'] ?? [], true);
|
|
|
|
return $e;
|
|
});
|
|
|
|
if ($e->booking_start_at && $e->booking_end_at && (count($data['venues'] ?? [])) > 0) {
|
|
TicketGrabReleaseDayService::rebuildAllReleaseDaysForEvent($e->id);
|
|
}
|
|
|
|
return response()->json($e->fresh()->load('venues:id,name,address', 'eventVenuePivots'), 201);
|
|
}
|
|
|
|
public function show(Request $request, TicketGrabEvent $ticketGrabEvent): JsonResponse
|
|
{
|
|
$this->assertEventAccessible($request, $ticketGrabEvent);
|
|
$ticketGrabEvent->load('venues', 'eventVenuePivots.venue', 'releaseDays');
|
|
|
|
return response()->json($ticketGrabEvent);
|
|
}
|
|
|
|
public function update(Request $request, TicketGrabEvent $ticketGrabEvent): JsonResponse
|
|
{
|
|
$this->assertEventAccessible($request, $ticketGrabEvent);
|
|
$data = $this->validated($request, false);
|
|
if (isset($data['venue_ids'])) {
|
|
$this->assertVenueIdsAllowed($request, $data['venue_ids'] ?? []);
|
|
}
|
|
|
|
$needRebuild = false;
|
|
if (array_key_exists('booking_start_at', $data) || array_key_exists('booking_end_at', $data)
|
|
|| array_key_exists('venues', $data)) {
|
|
$needRebuild = true;
|
|
}
|
|
|
|
DB::transaction(function () use ($data, $ticketGrabEvent, $request) {
|
|
$m = $this->mapMainFields($data, true, true);
|
|
if ($m !== []) {
|
|
$ticketGrabEvent->fill($m);
|
|
}
|
|
if (! $request->user()?->isSuperAdmin()) {
|
|
$ticketGrabEvent->audit_status = TicketGrabEvent::AUDIT_PENDING;
|
|
} else {
|
|
$ticketGrabEvent->audit_status = TicketGrabEvent::AUDIT_APPROVED;
|
|
}
|
|
$ticketGrabEvent->schedule_status = TicketGrabEvent::computeScheduleStatusFromBounds(
|
|
$ticketGrabEvent->start_at,
|
|
$ticketGrabEvent->end_at
|
|
);
|
|
$ticketGrabEvent->save();
|
|
if (array_key_exists('venues', $data) && is_array($data['venues'])) {
|
|
$this->syncPivots($ticketGrabEvent->id, $data['venues'], true);
|
|
}
|
|
});
|
|
|
|
if ($needRebuild) {
|
|
$hasPiv = TicketGrabEventVenue::query()->where('ticket_grab_event_id', $ticketGrabEvent->id)->exists();
|
|
if ($ticketGrabEvent->fresh()->booking_start_at && $ticketGrabEvent->booking_end_at && $hasPiv) {
|
|
$hasR = Reservation::query()
|
|
->where('ticket_grab_event_id', $ticketGrabEvent->id)
|
|
->where('reservation_kind', 'ticket_grab')
|
|
->where('status', '!=', 'cancelled')
|
|
->exists();
|
|
if (! $hasR) {
|
|
TicketGrabReleaseDayService::rebuildAllReleaseDaysForEvent($ticketGrabEvent->id);
|
|
}
|
|
}
|
|
}
|
|
|
|
return response()->json($ticketGrabEvent->fresh()->load('venues', 'eventVenuePivots'));
|
|
}
|
|
|
|
public function toggle(Request $request, TicketGrabEvent $ticketGrabEvent): JsonResponse
|
|
{
|
|
$this->assertEventAccessible($request, $ticketGrabEvent);
|
|
$ticketGrabEvent->is_active = ! $ticketGrabEvent->is_active;
|
|
$ticketGrabEvent->save();
|
|
|
|
return response()->json($ticketGrabEvent);
|
|
}
|
|
|
|
public function releaseConfig(Request $request, TicketGrabEvent $ticketGrabEvent): JsonResponse
|
|
{
|
|
$this->assertEventAccessible($request, $ticketGrabEvent);
|
|
$ticketGrabEvent->load('venues', 'eventVenuePivots');
|
|
// 打开弹窗前按「今天」重算滚入并写回库,否则界面仍显示历史累积的 carry_in
|
|
TicketGrabReleaseDayService::syncCarryInChain($ticketGrabEvent->id);
|
|
|
|
$byVenue = [];
|
|
$days = TicketGrabVenueReleaseDay::query()
|
|
->where('ticket_grab_event_id', $ticketGrabEvent->id)
|
|
->orderBy('venue_id')
|
|
->orderBy('release_date')
|
|
->get();
|
|
foreach ($days as $d) {
|
|
$byVenue[$d->venue_id][] = [
|
|
'release_date' => $d->release_date->toDateString(),
|
|
'day_quota' => (int) $d->day_quota,
|
|
'booked_count' => (int) $d->booked_count,
|
|
'carry_in' => (int) $d->carry_in,
|
|
'total_day_pool' => $d->totalDayPool(),
|
|
'current_remaining' => $d->remainingForDisplay(),
|
|
];
|
|
}
|
|
$entryStats = $this->entryDateStats($ticketGrabEvent->id, $ticketGrabEvent->start_at, $ticketGrabEvent->end_at);
|
|
|
|
return response()->json([
|
|
'event' => [
|
|
'id' => $ticketGrabEvent->id,
|
|
'title' => $ticketGrabEvent->title,
|
|
'booking_start_at' => CalendarDateFormat::ymdFromDateValue($ticketGrabEvent->booking_start_at),
|
|
'booking_end_at' => CalendarDateFormat::ymdFromDateValue($ticketGrabEvent->booking_end_at),
|
|
'start_at' => CalendarDateFormat::ymdFromDatetime($ticketGrabEvent->start_at),
|
|
'end_at' => CalendarDateFormat::ymdFromDatetime($ticketGrabEvent->end_at),
|
|
'daily_release_start_time' => $ticketGrabEvent->daily_release_start_time,
|
|
'daily_release_end_time' => $ticketGrabEvent->daily_release_end_time,
|
|
],
|
|
'venues' => $ticketGrabEvent->eventVenuePivots->map(function ($p) use ($byVenue) {
|
|
return [
|
|
'id' => $p->id,
|
|
'venue_id' => $p->venue_id,
|
|
'venue_total_quota' => (int) $p->venue_total_quota,
|
|
'release_days' => $byVenue[$p->venue_id] ?? [],
|
|
];
|
|
}),
|
|
'entry_date_stats' => $entryStats,
|
|
]);
|
|
}
|
|
|
|
public function updateReleaseConfig(Request $request, TicketGrabEvent $ticketGrabEvent): JsonResponse
|
|
{
|
|
$this->assertEventAccessible($request, $ticketGrabEvent);
|
|
$data = $request->validate([
|
|
'venue_day_quotas' => ['required', 'array', 'min:1'],
|
|
'venue_day_quotas.*.venue_id' => ['required', 'integer', 'exists:venues,id'],
|
|
'venue_day_quotas.*.days' => ['required', 'array'],
|
|
'venue_day_quotas.*.days.*.date' => ['required', 'date'],
|
|
'venue_day_quotas.*.days.*.day_quota' => ['required', 'integer', 'min:0'],
|
|
]);
|
|
$this->assertVenueIdsAllowed(
|
|
$request,
|
|
array_map('intval', array_column($data['venue_day_quotas'], 'venue_id'))
|
|
);
|
|
|
|
DB::transaction(function () use ($data, $ticketGrabEvent) {
|
|
foreach ($data['venue_day_quotas'] as $block) {
|
|
$vid = (int) $block['venue_id'];
|
|
$map = [];
|
|
foreach ($block['days'] as $r) {
|
|
$map[Carbon::parse($r['date'])->toDateString()] = (int) $r['day_quota'];
|
|
}
|
|
TicketGrabReleaseDayService::updateDayQuotasFromAdmin($ticketGrabEvent->id, $vid, $map);
|
|
}
|
|
});
|
|
|
|
return $this->releaseConfig($request, $ticketGrabEvent);
|
|
}
|
|
|
|
private function entryDateStats(int $eventId, $start, $end): array
|
|
{
|
|
if (! $start || ! $end) {
|
|
return [];
|
|
}
|
|
$s = $start->copy()->startOfDay();
|
|
$e = $end->copy()->startOfDay();
|
|
if ($e->lt($s)) {
|
|
return [];
|
|
}
|
|
$counts = Reservation::query()
|
|
->where('ticket_grab_event_id', $eventId)
|
|
->where('reservation_kind', 'ticket_grab')
|
|
->where('status', '!=', 'cancelled')
|
|
->selectRaw('entry_date, sum(ticket_count) as t')
|
|
->whereNotNull('entry_date')
|
|
->groupBy('entry_date')
|
|
->pluck('t', 'entry_date');
|
|
|
|
$out = [];
|
|
for ($d = $s->copy(); $d->lte($e); $d->addDay()) {
|
|
$k = $d->toDateString();
|
|
$out[] = [
|
|
'entry_date' => $k,
|
|
'reservation_count' => (int) ($counts->get($k) ?? 0),
|
|
];
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
private function assertEventAccessible(Request $request, TicketGrabEvent $e): void
|
|
{
|
|
$pivots = TicketGrabEventVenue::query()
|
|
->where('ticket_grab_event_id', $e->id)
|
|
->pluck('venue_id')
|
|
->all();
|
|
if ($pivots === []) {
|
|
$this->assertVenueIdsAllowed($request, []);
|
|
|
|
return;
|
|
}
|
|
$this->assertVenueIdsAllowed($request, $pivots);
|
|
}
|
|
|
|
private function assertVenueIdsAllowed(Request $request, array $venueIds): void
|
|
{
|
|
$user = $request->user();
|
|
if (! $user || $user->isSuperAdmin()) {
|
|
return;
|
|
}
|
|
$allow = $user->venues()->pluck('venues.id');
|
|
foreach (array_unique(array_map('intval', $venueIds)) as $id) {
|
|
if ($id > 0 && ! $allow->contains($id)) {
|
|
abort(403, '仅可操作已绑定场馆');
|
|
}
|
|
}
|
|
}
|
|
|
|
private function restrictByEventVenues(Request $request, $query): void
|
|
{
|
|
$user = $request->user();
|
|
if (! $user?->isSuperAdmin()) {
|
|
$ids = $user->venues()->pluck('venues.id');
|
|
if ($ids->isEmpty()) {
|
|
$query->whereRaw('1=0');
|
|
} else {
|
|
$query->where(function ($q) use ($ids) {
|
|
foreach ($ids as $i => $venueId) {
|
|
$q->{$i === 0 ? 'whereExists' : 'orWhereExists'}(function ($s) use ($venueId) {
|
|
$s->selectRaw('1')
|
|
->from('ticket_grab_event_venue as tgev')
|
|
->whereColumn('tgev.ticket_grab_event_id', 'ticket_grab_events.id')
|
|
->where('tgev.venue_id', $venueId);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param bool $stripNonModel 去掉 venues 等非模型字段
|
|
*/
|
|
private function mapMainFields(array $data, bool $isCreate, bool $stripNonModel): array
|
|
{
|
|
$keys = [
|
|
'title', 'summary', 'start_at', 'end_at', 'booking_start_at', 'booking_end_at',
|
|
'daily_release_start_time', 'daily_release_end_time',
|
|
'booking_audience', 'age_limit_start', 'age_limit_end',
|
|
'address', 'detail_html', 'reservation_notice', 'cover_image', 'gallery_media', 'tags',
|
|
'sort', 'is_active', 'audit_remark', 'audit_status', 'last_approved_snapshot',
|
|
];
|
|
$row = array_intersect_key($data, array_flip($keys));
|
|
if (isset($row['tags']) && is_array($row['tags'])) {
|
|
$row['tags'] = array_values($row['tags']);
|
|
}
|
|
$tz = (string) config('app.timezone');
|
|
if (array_key_exists('start_at', $row) && $row['start_at'] !== null && $row['start_at'] !== '') {
|
|
$row['start_at'] = Carbon::parse($row['start_at'], $tz)->startOfDay();
|
|
}
|
|
if (array_key_exists('end_at', $row) && $row['end_at'] !== null && $row['end_at'] !== '') {
|
|
$row['end_at'] = Carbon::parse($row['end_at'], $tz)->endOfDay();
|
|
}
|
|
if (array_key_exists('booking_start_at', $row) && $row['booking_start_at']) {
|
|
$row['booking_start_at'] = Carbon::parse($row['booking_start_at'], $tz)->toDateString();
|
|
}
|
|
if (array_key_exists('booking_end_at', $row) && $row['booking_end_at']) {
|
|
$row['booking_end_at'] = Carbon::parse($row['booking_end_at'], $tz)->toDateString();
|
|
}
|
|
if (array_key_exists('age_limit_start', $row) && $row['age_limit_start']) {
|
|
$row['age_limit_start'] = Carbon::parse($row['age_limit_start'], $tz)->toDateString();
|
|
}
|
|
if (array_key_exists('age_limit_end', $row) && $row['age_limit_end']) {
|
|
$row['age_limit_end'] = Carbon::parse($row['age_limit_end'], $tz)->toDateString();
|
|
}
|
|
if (isset($row['start_at'], $row['end_at']) && $row['end_at'] && $row['start_at']
|
|
&& $row['end_at']->lt($row['start_at'])) {
|
|
throw ValidationException::withMessages(['end_at' => ['结束不能早于开始']]);
|
|
}
|
|
|
|
if ($stripNonModel) {
|
|
unset($row['venues']);
|
|
}
|
|
|
|
return $row;
|
|
}
|
|
|
|
private function validated(Request $request, bool $isCreate = true): array
|
|
{
|
|
$rules = [
|
|
'title' => [$isCreate ? 'required' : 'sometimes', 'string', 'max:150'],
|
|
'summary' => ['nullable', 'string', 'max:255'],
|
|
'start_at' => ['nullable', 'date'],
|
|
'end_at' => ['nullable', 'date'],
|
|
'booking_start_at' => ['nullable', 'date'],
|
|
'booking_end_at' => ['nullable', 'date'],
|
|
'daily_release_start_time' => ['nullable', 'regex:/^\d{1,2}:\d{2}$/'],
|
|
'daily_release_end_time' => ['nullable', 'regex:/^\d{1,2}:\d{2}$/'],
|
|
'booking_audience' => ['nullable', 'in:all,school_age'],
|
|
'age_limit_start' => ['nullable', 'date'],
|
|
'age_limit_end' => ['nullable', 'date'],
|
|
'address' => ['nullable', 'string', 'max:255'],
|
|
'detail_html' => ['nullable', 'string'],
|
|
'reservation_notice' => ['nullable', 'string'],
|
|
'cover_image' => ['nullable', 'string', 'max:255'],
|
|
'gallery_media' => ['nullable', 'array'],
|
|
'tags' => ['nullable', 'array'],
|
|
'sort' => ['nullable', 'integer', 'min:0'],
|
|
'is_active' => ['boolean'],
|
|
'venues' => [$isCreate ? 'required' : 'sometimes', 'array', 'min:1'],
|
|
'venues.*.venue_id' => ['required', 'integer', 'exists:venues,id'],
|
|
'venues.*.venue_total_quota' => ['required', 'integer', 'min:0'],
|
|
];
|
|
if (! $isCreate) {
|
|
$rules['venues'] = ['sometimes', 'array', 'min:1'];
|
|
}
|
|
if (! $request->user()?->isSuperAdmin()) {
|
|
$rules['sort'] = ['nullable', 'prohibited'];
|
|
}
|
|
|
|
return $request->validate($rules) + [
|
|
'venue_ids' => $request->input('venues') ? array_map('intval', array_column($request->input('venues', []), 'venue_id')) : [],
|
|
];
|
|
}
|
|
|
|
private function syncPivots(int $eventId, array $venues, bool $allowReplace): void
|
|
{
|
|
if (! $allowReplace) {
|
|
return;
|
|
}
|
|
$seen = [];
|
|
foreach ($venues as $v) {
|
|
$vid = (int) $v['venue_id'];
|
|
$seen[] = $vid;
|
|
TicketGrabEventVenue::query()->updateOrCreate(
|
|
['ticket_grab_event_id' => $eventId, 'venue_id' => $vid],
|
|
['venue_total_quota' => (int) $v['venue_total_quota']],
|
|
);
|
|
}
|
|
if ($seen) {
|
|
TicketGrabEventVenue::query()
|
|
->where('ticket_grab_event_id', $eventId)
|
|
->whereNotIn('venue_id', $seen)
|
|
->delete();
|
|
}
|
|
$sum = (int) TicketGrabEventVenue::query()
|
|
->where('ticket_grab_event_id', $eventId)
|
|
->sum('venue_total_quota');
|
|
TicketGrabEvent::query()->where('id', $eventId)->update(['total_quota' => $sum]);
|
|
}
|
|
}
|