181 lines
6.7 KiB
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' => ['同级分类下已存在相同名称。'],
|
|
]);
|
|
}
|
|
}
|
|
}
|