BastionSSO/app/Http/Controllers/Api/TicketCategoryController.php

181 lines
6.7 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\TicketCategory;
use hg\apidoc\annotation as Apidoc;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
#[Apidoc\Title('工单分类管理')]
class TicketCategoryController extends Controller
{
public function __construct()
{
$this->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' => ['同级分类下已存在相同名称。'],
]);
}
}
}