feat(error_log): 添加清空所有错误日志的功能

主要变更:
- 在数据库服务层 ([`app/database/services.py:364`](app/database/services.py:364)) 添加了 `delete_all_error_logs` 函数。
- 在错误日志路由 ([`app/router/error_log_routes.py:186`](app/router/error_log_routes.py:186)) 中添加了新的 `DELETE /api/logs/errors/all` API 端点。
- 在前端 ([`app/static/js/error_logs.js`](app/static/js/error_logs.js)) 添加了“清空全部”按钮和相应的处理逻辑,并重构了删除确认模态框以支持此新功能。
- 将 [`app/core/application.py:42`](app/core/application.py:42) 中的 `initialize_database()` 调用从异步更改为同步。
This commit is contained in:
snaily
2025-05-15 00:23:53 +08:00
parent 4becc8d4d4
commit e260ad02bf
4 changed files with 109 additions and 43 deletions

View File

@@ -39,7 +39,7 @@ def update_template_globals(app: FastAPI, update_info: dict):
# --- Helper functions for lifespan ---
async def _setup_database_and_config(app_settings):
"""Initializes database, syncs settings, and initializes KeyManager."""
await initialize_database()
initialize_database()
logger.info("Database initialized successfully")
await connect_to_db()
await sync_initial_settings()

View File

@@ -360,7 +360,38 @@ async def delete_error_log_by_id(log_id: int) -> bool:
except Exception as e:
logger.error(f"Error deleting error log with ID {log_id}: {e}", exc_info=True)
raise
async def delete_all_error_logs() -> int:
"""
删除所有错误日志条目。
Returns:
int: 被删除的错误日志数量。
"""
try:
# 1. 获取删除前的总数
count_query = select(func.count()).select_from(ErrorLog)
# fetch_val() is suitable here as we expect a single scalar value
total_to_delete = await database.fetch_val(count_query)
if total_to_delete == 0:
logger.info("No error logs found to delete.")
return 0
# 2. 执行删除操作
# This creates a query like "DELETE FROM error_log"
delete_query = delete(ErrorLog)
await database.execute(delete_query)
logger.info(f"Successfully deleted all {total_to_delete} error logs.")
return total_to_delete
except Exception as e:
logger.error(f"Failed to delete all error logs: {str(e)}", exc_info=True)
# Re-raise the exception so it can be caught by the service layer or route handler
raise
# 新增函数:添加请求日志
async def add_request_log(
model_name: Optional[str],

View File

@@ -183,6 +183,28 @@ async def delete_error_logs_bulk_api(
)
@router.delete("/errors/all", status_code=status.HTTP_204_NO_CONTENT)
async def delete_all_error_logs_api(request: Request):
"""
删除所有错误日志 (异步)
"""
auth_token = request.cookies.get("auth_token")
if not auth_token or not verify_auth_token(auth_token):
logger.warning("Unauthorized access attempt to delete all error logs")
raise HTTPException(status_code=401, detail="Not authenticated")
try:
deleted_count = await error_log_service.process_delete_all_error_logs()
logger.info(f"Successfully deleted all {deleted_count} error logs.")
# No body needed for 204 response
return Response(status_code=status.HTTP_204_NO_CONTENT)
except Exception as e:
logger.exception(f"Error deleting all error logs: {str(e)}")
raise HTTPException(
status_code=500, detail="Internal server error during deletion of all logs"
)
@router.delete("/errors/{log_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_error_log_api(request: Request, log_id: int = Path(..., ge=1)):
"""
@@ -192,7 +214,7 @@ async def delete_error_log_api(request: Request, log_id: int = Path(..., ge=1)):
if not auth_token or not verify_auth_token(auth_token):
logger.warning(f"Unauthorized access attempt to delete error log ID: {log_id}")
raise HTTPException(status_code=401, detail="Not authenticated")
try:
success = await error_log_service.process_delete_error_log_by_id(log_id)
if not success:
@@ -209,25 +231,3 @@ async def delete_error_log_api(request: Request, log_id: int = Path(..., ge=1)):
raise HTTPException(
status_code=500, detail="Internal server error during deletion"
)
@router.delete("/errors/all", status_code=status.HTTP_204_NO_CONTENT)
async def delete_all_error_logs_api(request: Request):
"""
删除所有错误日志 (异步)
"""
auth_token = request.cookies.get("auth_token")
if not auth_token or not verify_auth_token(auth_token):
logger.warning("Unauthorized access attempt to delete all error logs")
raise HTTPException(status_code=401, detail="Not authenticated")
try:
deleted_count = await error_log_service.process_delete_all_error_logs()
logger.info(f"Successfully deleted all {deleted_count} error logs.")
# No body needed for 204 response
return Response(status_code=status.HTTP_204_NO_CONTENT)
except Exception as e:
logger.exception(f"Error deleting all error logs: {str(e)}")
raise HTTPException(
status_code=500, detail="Internal server error during deletion of all logs"
)

View File

@@ -111,6 +111,8 @@ let cancelDeleteBtn; // 新增:取消删除按钮
let confirmDeleteBtn; // 新增:确认删除按钮
let deleteConfirmMessage; // 新增:删除确认消息元素
let idsToDeleteGlobally = []; // 新增存储待删除的ID
let currentConfirmCallback = null; // 新增:存储当前的确认回调
let deleteAllLogsBtn; // 新增:清空全部按钮
// Helper functions for initialization
function cacheDOMElements() {
@@ -147,9 +149,10 @@ function cacheDOMElements() {
cancelDeleteBtn = document.getElementById("cancelDeleteBtn");
confirmDeleteBtn = document.getElementById("confirmDeleteBtn");
deleteConfirmMessage = document.getElementById("deleteConfirmMessage");
}
function initializePageSizeControls() {
deleteAllLogsBtn = document.getElementById("deleteAllLogsBtn"); // 缓存清空全部按钮
}
function initializePageSizeControls() {
if (pageSizeSelector) {
pageSizeSelector.value = errorLogState.pageSize;
pageSizeSelector.addEventListener("change", function () {
@@ -247,9 +250,35 @@ function initializeActionControls() {
}
// Bulk selection listeners are closely related to actions
setupBulkSelectionListeners();
}
// 页面加载完成后执行
// 为 "清空全部" 按钮添加事件监听器
if (deleteAllLogsBtn) {
deleteAllLogsBtn.addEventListener("click", function() {
const message = "您确定要清空所有错误日志吗?此操作不可恢复!";
showDeleteConfirmModal(message, handleDeleteAllLogs); // 传入回调
});
}
}
// 新增:处理 "清空全部" 逻辑的函数
async function handleDeleteAllLogs() {
const url = "/api/logs/errors/all";
const options = {
method: "DELETE",
};
try {
await fetchAPI(url, options);
showNotification("所有错误日志已成功清空", "success");
if (selectAllCheckbox) selectAllCheckbox.checked = false; // 取消全选
loadErrorLogs(); // 重新加载日志
} catch (error) {
console.error("清空所有错误日志失败:", error);
showNotification(`清空失败: ${error.message}`, "error", 5000);
}
}
// 页面加载完成后执行
document.addEventListener("DOMContentLoaded", function () {
cacheDOMElements();
initializePageSizeControls();
@@ -267,31 +296,33 @@ document.addEventListener("DOMContentLoaded", function () {
});
// 新增:显示删除确认模态框
function showDeleteConfirmModal(message) {
function showDeleteConfirmModal(message, confirmCallback) {
if (deleteConfirmModal && deleteConfirmMessage) {
deleteConfirmMessage.textContent = message;
currentConfirmCallback = confirmCallback; // 存储回调
deleteConfirmModal.classList.add("show");
document.body.style.overflow = "hidden"; // Prevent body scrolling
}
}
// 新增:隐藏删除确认模态框
function hideDeleteConfirmModal() {
if (deleteConfirmModal) {
deleteConfirmModal.classList.remove("show");
document.body.style.overflow = ""; // Restore body scrolling
idsToDeleteGlobally = []; // 清空待删除ID
currentConfirmCallback = null; // 清除回调
}
}
// 新增:处理确认删除按钮点击
function handleConfirmDelete() {
if (idsToDeleteGlobally.length > 0) {
performActualDelete(idsToDeleteGlobally);
if (typeof currentConfirmCallback === 'function') {
currentConfirmCallback(); // 调用存储的回调
}
hideDeleteConfirmModal(); // 关闭模态框
}
// Fallback copy function using document.execCommand
function fallbackCopyTextToClipboard(text) {
const textArea = document.createElement("textarea");
@@ -553,11 +584,13 @@ function handleDeleteSelected() {
}
// 存储待删除ID并显示模态框
idsToDeleteGlobally = logIdsToDelete;
idsToDeleteGlobally = logIdsToDelete; // 仍然需要设置,因为 performActualDelete 会用到
const message = `确定要删除选中的 ${logIdsToDelete.length} 条日志吗?此操作不可恢复!`;
showDeleteConfirmModal(message);
showDeleteConfirmModal(message, function() { // 传入匿名回调
performActualDelete(idsToDeleteGlobally);
});
}
// 新增:执行实际的删除操作(提取自原 handleDeleteSelected 和 handleDeleteLogRow
async function performActualDelete(logIds) {
if (!logIds || logIds.length === 0) return;
@@ -599,12 +632,14 @@ function handleDeleteLogRow(logId) {
if (!logId) return;
// 存储待删除ID并显示模态框
idsToDeleteGlobally = [parseInt(logId)]; // 存储为数组
idsToDeleteGlobally = [parseInt(logId)]; // 存储为数组 // 仍然需要设置,因为 performActualDelete 会用到
// 使用通用确认消息不显示具体ID
const message = `确定要删除这条日志吗?此操作不可恢复!`;
showDeleteConfirmModal(message);
showDeleteConfirmModal(message, function() { // 传入匿名回调
performActualDelete([parseInt(logId)]); // 确保传递的是数组
});
}
// 新增:处理 ID 排序点击的函数
function handleSortById() {
if (errorLogState.sort.field === "id") {