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.

270 lines
10 KiB

7 days ago
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Activity;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class ActivityController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = Activity::with('venue:id,name')->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'));
}
5 days ago
if ($request->filled('schedule_status')) {
$query->whereComputedScheduleStatus($request->string('schedule_status'));
}
6 days ago
if ($request->filled('audit_status')) {
$query->where('audit_status', $request->string('audit_status'));
}
7 days ago
$pageSize = max(1, min(100, (int) $request->input('page_size', 10)));
5 days ago
$page = $query->paginate($pageSize);
$page->getCollection()->transform(function (Activity $a) {
$a->setAttribute(
'schedule_status',
Activity::computeScheduleStatusFromBounds($a->start_at, $a->end_at)
);
return $a;
});
return response()->json($page);
7 days ago
}
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'],
6 days ago
'contact_phone' => ['nullable', 'string', 'max:20'],
7 days ago
'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']);
6 days ago
if (! $request->user()?->isSuperAdmin()) {
7 days ago
unset($data['sort']);
}
$this->normalizeActivityDates($data);
5 days ago
$data['schedule_status'] = Activity::computeScheduleStatusFromBounds(
$data['start_at'] ?? null,
$data['end_at'] ?? null
);
7 days ago
6 days ago
$auditStatus = $request->user()->isSuperAdmin() ? Activity::AUDIT_APPROVED : Activity::AUDIT_PENDING;
7 days ago
$activity = Activity::create($data + [
'quota' => $data['quota'] ?? 0,
'tags' => array_values($data['tags'] ?? []),
'sort' => $data['sort'] ?? 0,
'is_active' => $data['is_active'] ?? true,
6 days ago
'audit_status' => $auditStatus,
'audit_remark' => null,
'last_approved_snapshot' => null,
7 days ago
]);
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'],
6 days ago
'contact_phone' => ['nullable', 'string', 'max:20'],
7 days ago
'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']);
}
6 days ago
$user = $request->user();
if (! $user?->isSuperAdmin()) {
7 days ago
unset($data['sort']);
6 days ago
$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;
7 days ago
}
$this->normalizeActivityDates($data);
5 days ago
$start = array_key_exists('start_at', $data) ? $data['start_at'] : $activity->start_at;
$end = array_key_exists('end_at', $data) ? $data['end_at'] : $activity->end_at;
$data['schedule_status'] = Activity::computeScheduleStatusFromBounds($start, $end);
7 days ago
$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'));
}
6 days ago
public function approve(Request $request, Activity $activity): JsonResponse
{
abort_unless($request->user()?->isSuperAdmin(), 403, '仅超级管理员可审核');
$this->ensureVenuePermission($request, $activity->venue_id);
$activity->audit_status = Activity::AUDIT_APPROVED;
$activity->audit_remark = null;
$activity->last_approved_snapshot = null;
$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'));
}
7 days ago
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<string, mixed> $data
*/
private function normalizeActivityDates(array &$data): void
{
6 days ago
$tz = config('app.timezone');
7 days ago
if (array_key_exists('start_at', $data)) {
if ($data['start_at'] === null || $data['start_at'] === '') {
$data['start_at'] = null;
} else {
6 days ago
$data['start_at'] = Carbon::parse($data['start_at'], $tz)->startOfDay();
7 days ago
}
}
if (array_key_exists('end_at', $data)) {
if ($data['end_at'] === null || $data['end_at'] === '') {
$data['end_at'] = null;
} else {
6 days ago
$data['end_at'] = Carbon::parse($data['end_at'], $tz)->endOfDay();
7 days ago
}
}
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, '仅可操作已绑定场馆');
}
}