middleware('auth:api'); $this->middleware('permission:tickets.use|platform.tickets.view|platform.tickets.manage,api')->only(['index']); $this->middleware('permission:platform.tickets.manage,api')->only(['store', 'update', 'destroy']); } #[Apidoc\Title('工单分类列表'), Apidoc\Method('GET'), Apidoc\Url('/ticket-categories')] public function index(Request $request): JsonResponse { $validated = $request->validate([ 'active_only' => ['nullable', 'boolean'], ]); $query = TicketCategory::query() ->with('parent:id,name,parent_id') ->orderBy('parent_id') ->orderBy('id'); if ((bool) ($validated['active_only'] ?? false)) { $query->where('is_active', true); } $categories = $query->get(); return response()->json([ 'code' => 0, 'message' => 'ok', 'data' => $categories, 'tree' => $this->buildCategoryTree($categories), ]); } #[Apidoc\Title('创建工单分类'), Apidoc\Method('POST'), Apidoc\Url('/ticket-categories')] public function store(Request $request): JsonResponse { $validated = $request->validate([ 'parent_id' => ['nullable', 'integer', 'exists:ticket_categories,id'], 'name' => ['required', 'string', 'max:100'], 'description' => ['nullable', 'string', 'max:255'], 'is_active' => ['sometimes', 'boolean'], ]); $this->ensureUniqueNameInParent($validated['name'], $validated['parent_id'] ?? null); $category = TicketCategory::query()->create([ 'parent_id' => $validated['parent_id'] ?? null, 'name' => $validated['name'], 'description' => $validated['description'] ?? null, 'is_active' => (bool) ($validated['is_active'] ?? true), ]); $this->auditLog($request, 'ticket_category_create', ['metadata' => ['ticket_category_id' => $category->id]]); return response()->json(['code' => 0, 'message' => 'ok', 'data' => $category], 201); } #[Apidoc\Title('更新工单分类'), Apidoc\Method('PUT'), Apidoc\Url('/ticket-categories/{id}')] public function update(Request $request, int $id): JsonResponse { $category = TicketCategory::query()->findOrFail($id); $validated = $request->validate([ 'parent_id' => ['nullable', 'integer', Rule::exists('ticket_categories', 'id')->where(fn ($query) => $query->where('id', '!=', $category->id))], 'name' => ['required', 'string', 'max:100'], 'description' => ['nullable', 'string', 'max:255'], 'is_active' => ['sometimes', 'boolean'], ]); $parentId = $validated['parent_id'] ?? null; if ($parentId !== null && $this->isDescendant((int) $parentId, $category)) { throw ValidationException::withMessages([ 'parent_id' => ['不能选择当前分类的下级分类作为父级。'], ]); } $this->ensureUniqueNameInParent($validated['name'], $parentId, $category->id); $category->update([ 'parent_id' => $parentId, 'name' => $validated['name'], 'description' => $validated['description'] ?? null, 'is_active' => array_key_exists('is_active', $validated) ? (bool) $validated['is_active'] : $category->is_active, ]); $this->auditLog($request, 'ticket_category_update', ['metadata' => ['ticket_category_id' => $category->id]]); return response()->json(['code' => 0, 'message' => 'ok', 'data' => $category->fresh()]); } #[Apidoc\Title('删除工单分类'), Apidoc\Method('DELETE'), Apidoc\Url('/ticket-categories/{id}')] public function destroy(Request $request, int $id): JsonResponse { $category = TicketCategory::query()->findOrFail($id); $this->auditLog($request, 'ticket_category_delete', ['metadata' => ['ticket_category_id' => $category->id]]); $category->delete(); return response()->json(['code' => 0, 'message' => 'ok', 'data' => null]); } private function buildCategoryTree($categories): array { $items = $categories ->map(function (TicketCategory $category): array { return [ 'id' => $category->id, 'parent_id' => $category->parent_id, 'name' => $category->name, 'description' => $category->description, 'is_active' => $category->is_active, 'children' => [], ]; }) ->keyBy('id') ->all(); $tree = []; foreach ($items as $id => &$item) { $parentId = $item['parent_id']; if ($parentId !== null && isset($items[$parentId])) { $items[$parentId]['children'][] = &$item; continue; } $tree[] = &$item; } unset($item); return $tree; } private function isDescendant(int $parentId, TicketCategory $category): bool { $childrenByParent = TicketCategory::query() ->whereNotNull('parent_id') ->get(['id', 'parent_id']) ->groupBy('parent_id'); $pending = collect($childrenByParent->get($category->id, []))->pluck('id')->all(); while (! empty($pending)) { $nextId = (int) array_shift($pending); if ($nextId === $parentId) { return true; } $pending = [ ...$pending, ...collect($childrenByParent->get($nextId, []))->pluck('id')->all(), ]; } return false; } private function ensureUniqueNameInParent(string $name, ?int $parentId, ?int $exceptId = null): void { $exists = TicketCategory::query() ->where('name', $name) ->when($parentId === null, fn ($query) => $query->whereNull('parent_id'), fn ($query) => $query->where('parent_id', $parentId)) ->when($exceptId !== null, fn ($query) => $query->where('id', '!=', $exceptId)) ->exists(); if ($exists) { throw ValidationException::withMessages([ 'name' => ['同级分类下已存在相同名称。'], ]); } } }