orderBy('sort')->orderByDesc('id'); StudyTourPayload::applyListFilters($q, $request); return response()->json($q->get()); } public function store(Request $request): JsonResponse { $this->ensureSuperAdmin($request); $data = $request->validate(StudyTourPayload::validationRules(true)); $data = StudyTourPayload::normalizeIncoming($data); $row = StudyTour::create($data + [ 'tags' => $data['tags'] ?? [], 'venue_ids' => $data['venue_ids'] ?? [], 'venue_items' => $data['venue_items'] ?? [], 'seasons' => $data['seasons'] ?? [], 'suitable_audience' => $data['suitable_audience'] ?? null, 'route_plans' => $data['route_plans'] ?? [], 'courses' => $data['courses'] ?? [], 'gallery_media' => $data['gallery_media'] ?? [], 'sort' => $data['sort'] ?? 0, 'is_on_shelf' => $data['is_on_shelf'] ?? true, ]); return response()->json($row, 201); } public function update(Request $request, StudyTour $studyTour): JsonResponse { $this->ensureSuperAdmin($request); $data = $request->validate(StudyTourPayload::validationRules(false)); $data = StudyTourPayload::normalizeIncoming($data); $studyTour->fill($data)->save(); return response()->json($studyTour->fresh()); } public function destroy(Request $request, StudyTour $studyTour): JsonResponse { $this->ensureSuperAdmin($request); $studyTour->delete(); return response()->json(['message' => '删除成功']); } public function export(Request $request): BinaryFileResponse|JsonResponse { $this->ensureSuperAdmin($request); $data = $request->validate([ 'ids' => ['required', 'array', 'min:1', 'max:'.StudyTourDeclarationExporter::MAX_BATCH], 'ids.*' => ['integer', 'min:1'], ]); $ids = array_values(array_unique(array_map('intval', $data['ids']))); $order = array_flip($ids); $tours = StudyTour::query() ->whereIn('id', $ids) ->get() ->sortBy(fn (StudyTour $row) => $order[$row->id] ?? 9999) ->values(); if ($tours->isEmpty()) { return response()->json(['message' => '未找到可导出的线路'], 422); } try { $download = StudyTourDeclarationExporter::writeDownload($tours); } catch (\Throwable $e) { return response()->json(['message' => '导出失败:'.$e->getMessage()], 422); } $ascii = str_ends_with(strtolower($download['filename']), '.zip') ? 'study-tours.zip' : 'study-tour.docx'; $response = response()->download( $download['path'], $ascii, ['Content-Type' => $download['mime']] )->deleteFileAfterSend(true); $response->headers->set( 'Content-Disposition', HeaderUtils::makeDisposition( HeaderUtils::DISPOSITION_ATTACHMENT, $download['filename'], $ascii ) ); return $response; } public function parseDoc(Request $request): JsonResponse { $request->validate([ 'file' => ['required', 'file', 'max:10240'], ]); $uploaded = $request->file('file'); if ($uploaded === null) { return response()->json(['message' => '请上传申报表文件'], 422); } $path = null; try { $path = SecurePrivateImportUpload::storeTemporary( $uploaded, 'tmp/declaration-import', ['doc', 'docx'], 10240 ); $extension = strtolower((string) pathinfo($path, PATHINFO_EXTENSION)); $result = StudyTourDeclarationParser::parseFile($path, $extension); } catch (\Illuminate\Validation\ValidationException $e) { throw $e; } catch (\Throwable $e) { return response()->json(['message' => '申报表解析失败:'.$e->getMessage()], 422); } finally { if (is_string($path) && is_file($path)) { @unlink($path); } } return response()->json($result); } private function ensureSuperAdmin(Request $request): void { abort_unless($request->user()?->isSuperAdmin(), 403, '仅超级管理员可操作'); } }