33 KiB
Executable file
سناریو: حذف تیکتهای پشتیبانی توسط مدیر سیستم
📋 خلاصه اجرایی
این سناریو قابلیت حذف تیکتهای پشتیبانی توسط مدیر سیستم (superadmin) را پیادهسازی میکند. در این سناریو:
- فقط مدیر سیستم میتواند تیکتها را حذف کند
- اپراتورها و کاربران عادی امکان حذف تیکت ندارند
- دکمه حذف فقط در صفحه لیست تیکتهای اپراتور برای مدیر سیستم نمایش داده میشود
- حذف تیکت به صورت Hard Delete انجام میشود (تیکت و پیامهای مرتبط کاملاً حذف میشوند)
🎯 اهداف
- امنیت: فقط مدیر سیستم (superadmin) بتواند تیکتها را حذف کند
- UI/UX: دکمه حذف فقط برای superadmin در لیست تیکتهای اپراتور نمایش داده شود
- تأیید دو مرحلهای: قبل از حذف، از کاربر تأیید گرفته شود
- حذف کامل: تیکت و تمام پیامهای مرتبط با آن حذف شوند
🏗️ معماری
┌─────────────────────────────────────────────────────────┐
│ Frontend (Flutter) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ OperatorTicketsPage │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ if (isSuperAdmin) │ │ │
│ │ │ - نمایش دکمه حذف در Bulk Actions │ │ │
│ │ │ - نمایش آیکون حذف در هر ردیف │ │ │
│ │ │ else │ │ │
│ │ │ - عدم نمایش دکمه حذف │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ SupportService.deleteTicket(ticketId) │ │
│ │ ↓ │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Backend (FastAPI) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ DELETE /api/v1/support/operator/tickets/{id} │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ @require_superadmin │ │ │
│ │ │ 1. بررسی وجود تیکت │ │ │
│ │ │ 2. حذف تیکت از دیتابیس │ │ │
│ │ │ 3. بازگشت پاسخ موفقیت │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ TicketRepository.delete_ticket(ticket_id) │ │
│ │ ↓ │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Database │
│ ┌────────────────────────────────────────────────────┐ │
│ │ support_tickets │ │
│ │ - حذف رکورد تیکت (CASCADE) │ │
│ │ support_messages │ │
│ │ - حذف تمام پیامهای مرتبط (CASCADE) │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
📝 پیادهسازی
1. Backend - Repository Layer
فایل: hesabixAPI/adapters/db/repositories/support/ticket_repository.py
در این فایل، متد جدید delete_ticket را اضافه میکنیم:
def delete_ticket(self, ticket_id: int) -> bool:
"""
حذف تیکت و تمام پیامهای مرتبط
Args:
ticket_id: شناسه تیکت
Returns:
True اگر تیکت حذف شد، False اگر تیکت یافت نشد
"""
ticket = self.get_by_id(ticket_id)
if not ticket:
return False
# حذف تیکت (پیامها به صورت CASCADE حذف میشوند)
self.db.delete(ticket)
self.db.commit()
return True
توضیحات:
- متد
get_by_idازBaseRepositoryارثبری شده و تیکت را از دیتابیس میگیرد - با حذف تیکت، به دلیل
ondelete="CASCADE"در model، تمام پیامهای مرتبط نیز حذف میشوند - اگر تیکت یافت نشد،
Falseبرمیگرداند
2. Backend - API Layer
فایل: hesabixAPI/adapters/api/v1/support/operator.py
در این فایل، endpoint جدید برای حذف تیکت اضافه میکنیم:
@router.delete("/tickets/{ticket_id}", response_model=SuccessResponse)
@require_app_permission("superadmin")
async def delete_ticket(
request: Request,
ticket_id: int,
current_user: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""حذف تیکت (فقط برای مدیر سیستم)"""
ticket_repo = TicketRepository(db)
# حذف تیکت
deleted = ticket_repo.delete_ticket(ticket_id)
if not deleted:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="تیکت یافت نشد یا قبلاً حذف شده است"
)
return success_response(
{"message": "تیکت با موفقیت حذف شد", "ticket_id": ticket_id},
request
)
توضیحات:
- دکوراتور
@require_app_permission("superadmin")تضمین میکند فقط superadmin بتواند این endpoint را صدا بزند - متد HTTP:
DELETE - اگر تیکت یافت نشد، خطای
404برمیگرداند - در صورت موفقیت، پیام موفقیت و ID تیکت حذف شده را برمیگرداند
3. Backend - Permission Decorator (اختیاری)
اگر دکوراتور @require_app_permission("superadmin") وجود ندارد، میتوانیم آن را به فایل permissions اضافه کنیم:
فایل: hesabixAPI/app/core/permissions.py
from functools import wraps
from fastapi import HTTPException, status
from app.core.auth_dependency import AuthContext
def require_superadmin(func):
"""
دکوراتور برای محدود کردن دسترسی به superadmin
"""
@wraps(func)
async def wrapper(*args, current_user: AuthContext, **kwargs):
if not current_user.is_superadmin():
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="فقط مدیر سیستم مجاز به انجام این عملیات است"
)
return await func(*args, current_user=current_user, **kwargs)
return wrapper
توضیحات:
- این دکوراتور میتواند به جای
@require_app_permission("superadmin")استفاده شود - با استفاده از
current_user.is_superadmin()چک میکند که آیا کاربر superadmin است یا نه
4. Frontend - Service Layer
فایل: hesabixUI/hesabix_ui/lib/services/support_service.dart
متد جدید برای حذف تیکت اضافه میکنیم:
/// حذف تیکت (فقط برای مدیر سیستم)
Future<void> deleteTicket(int ticketId) async {
try {
await _apiClient.delete(
'/api/v1/support/operator/tickets/$ticketId',
);
} on DioException catch (e) {
throw _handleError(e);
}
}
/// حذف چندین تیکت به صورت گروهی (فقط برای مدیر سیستم)
Future<Map<String, dynamic>> deleteTickets(List<int> ticketIds) async {
try {
final results = <int, dynamic>{};
for (final ticketId in ticketIds) {
try {
await deleteTicket(ticketId);
results[ticketId] = {'success': true};
} catch (e) {
results[ticketId] = {'success': false, 'error': e.toString()};
}
}
final successCount = results.values.where((r) => r['success'] == true).length;
final failCount = results.values.where((r) => r['success'] == false).length;
return {
'total': ticketIds.length,
'success': successCount,
'failed': failCount,
'results': results,
};
} on DioException catch (e) {
throw _handleError(e);
}
}
توضیحات:
- متد
deleteTicket: حذف یک تیکت - متد
deleteTickets: حذف چندین تیکت به صورت گروهی (برای Bulk Delete) - در صورت خطا، پیام خطا را به صورت Exception برمیگرداند
5. Frontend - UI Layer
فایل: hesabixUI/hesabix_ui/lib/pages/profile/operator/operator_tickets_page.dart
تغییرات مورد نیاز:
5.1. اضافه کردن State برای چک کردن superadmin
class _OperatorTicketsPageState extends State<OperatorTicketsPage> {
Set<int> _selectedRows = <int>{};
// Support data for filters
final SupportService _supportService = SupportService(ApiClient());
List<SupportStatus> _statuses = [];
List<SupportPriority> _priorities = [];
// Refresh counter to force data table refresh
int _refreshCounter = 0;
// Check if current user is superadmin
bool _isSuperAdmin = false;
@override
void initState() {
super.initState();
_loadMetadata();
_checkUserPermissions();
}
Future<void> _checkUserPermissions() async {
try {
final apiClient = ApiClient();
final response = await apiClient.get<Map<String, dynamic>>('/api/v1/auth/me');
final permissions = response.data?['data']?['permissions'] as Map<String, dynamic>?;
final isSuperAdmin = permissions?['is_superadmin'] as bool? ?? false;
setState(() {
_isSuperAdmin = isSuperAdmin;
});
} catch (e) {
// Handle error silently
}
}
// ... بقیه کد
}
5.2. اضافه کردن متدهای حذف
Future<void> _deleteTicket(int ticketId) async {
// نمایش دیالوگ تأیید
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(t.confirmDelete),
content: Text(t.deleteTicketConfirmMessage),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(t.cancel),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: Text(t.delete),
),
],
),
);
if (confirmed != true) return;
try {
await _supportService.deleteTicket(ticketId);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(t.ticketDeletedSuccessfully)),
);
// Refresh the data table
setState(() {
_refreshCounter++;
});
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(t.errorDeletingTicket(e.toString())),
backgroundColor: Colors.red,
),
);
}
}
}
Future<void> _deleteSelectedTickets() async {
if (_selectedRows.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(t.noTicketsSelected)),
);
return;
}
// نمایش دیالوگ تأیید
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(t.confirmBulkDelete),
content: Text(t.deleteBulkTicketsConfirmMessage(_selectedRows.length)),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(t.cancel),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: Text(t.delete),
),
],
),
);
if (confirmed != true) return;
try {
// نمایش loading
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const Center(
child: CircularProgressIndicator(),
),
);
final result = await _supportService.deleteTickets(_selectedRows.toList());
// بستن loading
if (mounted) Navigator.of(context).pop();
if (mounted) {
final successCount = result['success'] as int;
final failCount = result['failed'] as int;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
t.bulkDeleteResult(successCount, failCount),
),
backgroundColor: failCount > 0 ? Colors.orange : Colors.green,
),
);
// پاک کردن انتخابها و refresh
setState(() {
_selectedRows.clear();
_refreshCounter++;
});
}
} catch (e) {
// بستن loading
if (mounted) Navigator.of(context).pop();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(t.errorDeletingTickets(e.toString())),
backgroundColor: Colors.red,
),
);
}
}
}
5.3. اضافه کردن دکمه حذف گروهی و آیکون حذف به DataTable
@override
Widget build(BuildContext context) {
final t = AppLocalizations.of(context);
final theme = Theme.of(context);
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
t.operatorPanel,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const Spacer(),
// دکمه حذف گروهی (فقط برای superadmin)
if (_isSuperAdmin && _selectedRows.isNotEmpty) ...[
ElevatedButton.icon(
onPressed: _deleteSelectedTickets,
icon: const Icon(Icons.delete_outline),
label: Text(t.deleteSelected(_selectedRows.length)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade50,
foregroundColor: Colors.red.shade700,
),
),
const SizedBox(width: 8),
],
],
),
const SizedBox(height: 16),
Expanded(
child: DataTableWidget<Map<String, dynamic>>(
key: ValueKey('data_table_$_refreshCounter'),
config: DataTableConfig<Map<String, dynamic>>(
title: 'لیست تیکتهای پشتیبانی - پنل اپراتور',
endpoint: '/api/v1/support/operator/tickets/search',
columns: [
// اضافه کردن ستون Actions در ابتدا (فقط برای superadmin)
if (_isSuperAdmin)
ActionsColumn<Map<String, dynamic>>(
'actions',
t.actions,
width: ColumnWidth.small,
actions: [
DataTableAction(
icon: Icons.delete_outline,
label: t.delete,
color: Colors.red,
onPressed: (ticketData) {
final ticketId = ticketData['id'] as int;
_deleteTicket(ticketId);
},
),
],
),
TextColumn(
'title',
'عنوان',
sortable: true,
searchable: true,
width: ColumnWidth.large,
),
// ... بقیه ستونها
],
searchFields: ['title', 'description', 'user.first_name', 'user.last_name', 'user.email'],
// ... بقیه تنظیمات
),
fromJson: (json) => json,
calendarController: widget.calendarController,
),
),
],
),
),
);
}
6. Frontend - Localization (ترجمهها)
فایل: hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart
کلیدهای ترجمه جدید که باید اضافه شوند:
// در کلاس AppLocalizations
String get confirmDelete => 'تأیید حذف';
String get deleteTicketConfirmMessage => 'آیا از حذف این تیکت اطمینان دارید؟ این عملیات قابل برگشت نیست.';
String get ticketDeletedSuccessfully => 'تیکت با موفقیت حذف شد';
String errorDeletingTicket(String error) => 'خطا در حذف تیکت: $error';
String get confirmBulkDelete => 'تأیید حذف گروهی';
String deleteBulkTicketsConfirmMessage(int count) => 'آیا از حذف $count تیکت انتخاب شده اطمینان دارید؟ این عملیات قابل برگشت نیست.';
String get noTicketsSelected => 'هیچ تیکتی انتخاب نشده است';
String deleteSelected(int count) => 'حذف $count مورد انتخابی';
String bulkDeleteResult(int success, int failed) =>
failed > 0
? 'حذف انجام شد: $success موفق، $failed ناموفق'
: '$success تیکت با موفقیت حذف شد';
String errorDeletingTickets(String error) => 'خطا در حذف تیکتها: $error';
String get actions => 'عملیات';
String get delete => 'حذف';
String get cancel => 'لغو';
🔒 نکات امنیتی
1. بررسی دسترسی در Backend
- استفاده از دکوراتور
@require_app_permission("superadmin")تضمین میکند که فقط superadmin بتواند endpoint را صدا بزند - حتی اگر کاربر از Postman یا ابزار دیگری استفاده کند، بدون permission مناسب نمیتواند تیکت را حذف کند
2. عدم نمایش UI برای کاربران غیرمجاز
- با چک کردن
_isSuperAdminدر frontend، دکمه حذف فقط برای superadmin نمایش داده میشود - این امر از سردرگمی کاربران جلوگیری میکند
3. تأیید قبل از حذف
- قبل از حذف، از کاربر تأیید گرفته میشود تا از حذف تصادفی جلوگیری شود
- پیام تأیید شامل هشدار "عملیات قابل برگشت نیست" است
4. Hard Delete vs Soft Delete
- در این پیادهسازی، حذف به صورت Hard Delete است (تیکت کاملاً از دیتابیس حذف میشود)
- اگر نیاز به Soft Delete دارید (نگهداری تاریخچه)، میتوانید فیلد
deleted_atبه model اضافه کنید
🧪 تست سناریو
تست Backend (Postman/cURL)
1. تست حذف تیکت با SuperAdmin
# درخواست با API Key مدیر سیستم
curl -X DELETE \
'http://localhost:8000/api/v1/support/operator/tickets/123' \
-H 'Authorization: ApiKey YOUR_SUPERADMIN_API_KEY' \
-H 'Content-Type: application/json'
# پاسخ موفق:
{
"data": {
"message": "تیکت با موفقیت حذف شد",
"ticket_id": 123
},
"success": true
}
2. تست حذف تیکت با اپراتور عادی (باید خطا بدهد)
# درخواست با API Key اپراتور عادی
curl -X DELETE \
'http://localhost:8000/api/v1/support/operator/tickets/123' \
-H 'Authorization: ApiKey OPERATOR_API_KEY' \
-H 'Content-Type: application/json'
# پاسخ خطا:
{
"detail": "فقط مدیر سیستم مجاز به انجام این عملیات است",
"success": false
}
3. تست حذف تیکت غیرموجود
curl -X DELETE \
'http://localhost:8000/api/v1/support/operator/tickets/99999' \
-H 'Authorization: ApiKey YOUR_SUPERADMIN_API_KEY' \
-H 'Content-Type: application/json'
# پاسخ خطا:
{
"detail": "تیکت یافت نشد یا قبلاً حذف شده است",
"success": false
}
تست Frontend
سناریوی تست 1: مدیر سیستم
- با حساب superadmin وارد شوید
- به صفحه "پنل اپراتور" بروید
- انتظار: دکمه حذف و آیکون حذف در هر ردیف نمایش داده شود
- یک تیکت را انتخاب کنید
- روی آیکون حذف کلیک کنید
- انتظار: دیالوگ تأیید نمایش داده شود
- روی "حذف" کلیک کنید
- انتظار: تیکت حذف شود و پیام موفقیت نمایش داده شود
- انتظار: لیست تیکتها refresh شود
سناریوی تست 2: اپراتور عادی
- با حساب اپراتور عادی وارد شوید
- به صفحه "پنل اپراتور" بروید
- انتظار: دکمه حذف و آیکون حذف نمایش داده نشود
- انتظار: فقط عملیات مجاز (مشاهده، ویرایش وضعیت، پاسخ) نمایش داده شود
سناریوی تست 3: حذف گروهی
- با حساب superadmin وارد شوید
- چند تیکت را انتخاب کنید
- انتظار: دکمه "حذف X مورد انتخابی" نمایش داده شود
- روی دکمه کلیک کنید
- انتظار: دیالوگ تأیید با تعداد تیکتهای انتخابی نمایش داده شود
- روی "حذف" کلیک کنید
- انتظار: loading نمایش داده شود
- انتظار: تمام تیکتهای انتخابی حذف شوند
- انتظار: پیام موفقیت با آمار (تعداد موفق/ناموفق) نمایش داده شود
📊 Database Schema
تغییری در schema نیاز نیست، اما CASCADE relationship را بررسی کنید:
فایل: hesabixAPI/adapters/db/models/support/message.py
class Message(Base):
"""پیامهای تیکت"""
__tablename__ = "support_messages"
# ...
# Foreign Key با CASCADE
ticket_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("support_tickets.id", ondelete="CASCADE"), # ✅ این باید CASCADE باشد
nullable=False,
index=True
)
اگر ondelete="CASCADE" تنظیم نشده، باید آن را اضافه کنید و migration بزنید.
🚀 Migration (در صورت نیاز)
اگر ondelete="CASCADE" در model موجود نیست:
# ایجاد migration جدید
cd hesabixAPI
alembic revision --autogenerate -m "Add CASCADE delete for ticket messages"
# اعمال migration
alembic upgrade head
📝 Checklist پیادهسازی
Backend
- اضافه کردن متد
delete_ticketبهTicketRepository - اضافه کردن endpoint
DELETE /tickets/{ticket_id}درoperator.py - تست endpoint با superadmin API key
- تست endpoint با operator API key (باید خطا بدهد)
- تست حذف تیکت غیرموجود
- بررسی CASCADE delete در database schema
- اجرای migration (در صورت نیاز)
Frontend
- اضافه کردن متد
deleteTicketبهSupportService - اضافه کردن متد
deleteTickets(bulk delete) بهSupportService - اضافه کردن چک
_isSuperAdminدرOperatorTicketsPage - اضافه کردن دکمه حذف گروهی (conditional rendering)
- اضافه کردن ستون Actions با آیکون حذف
- پیادهسازی متد
_deleteTicketبا دیالوگ تأیید - پیادهسازی متد
_deleteSelectedTicketsبا دیالوگ تأیید - اضافه کردن کلیدهای ترجمه به
app_localizations_fa.dart - تست UI با حساب superadmin
- تست UI با حساب operator
- تست حذف تک تیکت
- تست حذف گروهی
Documentation
- بهروزرسانی API documentation
- بهروزرسانی User Guide (در صورت نیاز)
🎨 UI/UX Mockup
نمایش برای SuperAdmin
┌─────────────────────────────────────────────────────────────┐
│ پنل اپراتور [حذف 3 مورد انتخابی] │
├─────────────────────────────────────────────────────────────┤
│ [✓] [🗑️] | عنوان تیکت 1 | کاربر 1 | باز | عادی | ... │
│ [✓] [🗑️] | عنوان تیکت 2 | کاربر 2 | بسته | فوری | ... │
│ [✓] [🗑️] | عنوان تیکت 3 | کاربر 3 | باز | عادی | ... │
│ [ ] [🗑️] | عنوان تیکت 4 | کاربر 4 | باز | کم | ... │
└─────────────────────────────────────────────────────────────┘
نمایش برای Operator (بدون دکمه حذف)
┌─────────────────────────────────────────────────────────────┐
│ پنل اپراتور │
├─────────────────────────────────────────────────────────────┤
│ [✓] | عنوان تیکت 1 | کاربر 1 | باز | عادی | ... │
│ [✓] | عنوان تیکت 2 | کاربر 2 | بسته | فوری | ... │
│ [✓] | عنوان تیکت 3 | کاربر 3 | باز | عادی | ... │
│ [ ] | عنوان تیکت 4 | کاربر 4 | باز | کم | ... │
└─────────────────────────────────────────────────────────────┘
دیالوگ تأیید حذف
┌──────────────────────────────────┐
│ تأیید حذف │
├──────────────────────────────────┤
│ آیا از حذف این تیکت اطمینان │
│ دارید؟ این عملیات قابل برگشت │
│ نیست. │
│ │
│ [لغو] [حذف 🗑️] │
└──────────────────────────────────┘
🔄 جریان کامل (Flow Diagram)
کاربر SuperAdmin
↓
ورود به صفحه لیست تیکتهای اپراتور
↓
چک permission → is_superadmin == true
↓
نمایش دکمه حذف در UI
↓
کلیک روی آیکون حذف یک تیکت
↓
نمایش دیالوگ تأیید
↓
کاربر تأیید میکند
↓
Frontend → API Call: DELETE /api/v1/support/operator/tickets/{id}
↓
Backend → بررسی permission با @require_app_permission("superadmin")
↓
Backend → حذف تیکت از database (CASCADE delete پیامها)
↓
Backend → پاسخ موفقیت
↓
Frontend → نمایش پیام موفقیت
↓
Frontend → Refresh لیست تیکتها
↓
پایان
💡 پیشنهادات بهبود (اختیاری)
1. Soft Delete به جای Hard Delete
اگر میخواهید تاریخچه را نگه دارید:
# در model Ticket
deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
deleted_by: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id"), nullable=True)
# در repository
def soft_delete_ticket(self, ticket_id: int, user_id: int) -> bool:
ticket = self.get_by_id(ticket_id)
if not ticket:
return False
ticket.deleted_at = datetime.utcnow()
ticket.deleted_by = user_id
self.db.commit()
return True
2. Activity Log
ثبت لاگ حذف تیکت برای audit:
# بعد از حذف تیکت
from app.services.activity_log_service import log_activity
log_activity(
db=db,
user_id=current_user.get_user_id(),
entity_type="ticket",
entity_id=ticket_id,
action="delete",
details={"ticket_title": ticket.title}
)
3. Undo قابلیت بازگردانی
با استفاده از Soft Delete + Undo API:
@router.post("/tickets/{ticket_id}/restore", response_model=SuccessResponse)
@require_app_permission("superadmin")
async def restore_ticket(ticket_id: int, ...):
"""بازگردانی تیکت حذف شده"""
# ...
4. Bulk Delete API Endpoint
بهینهسازی حذف گروهی با یک API call:
@router.delete("/tickets/bulk", response_model=SuccessResponse)
@require_app_permission("superadmin")
async def bulk_delete_tickets(
ticket_ids: List[int],
...
):
"""حذف گروهی تیکتها"""
# ...
📞 پشتیبانی و سوالات
اگر در پیادهسازی با مشکل مواجه شدید:
- لاگهای backend را بررسی کنید
- Network tab مرورگر را برای خطاهای API چک کنید
- مطمئن شوید permission ها صحیح تنظیم شدهاند
- مطمئن شوید CASCADE delete در database تنظیم شده است
📚 منابع مرتبط
تاریخ ایجاد: 2025-12-05
نسخه: 1.0
نویسنده: AI Assistant (Cursor)