feat: 添加登录日志和操作日志功能
1. 新增日志相关数据库模型、API路由、前端页面与权限配置 2. 为现有业务接口添加操作日志记录功能 3. 优化导入顺序与权限初始化数据
This commit is contained in:
parent
acb86e4fe1
commit
19d6db6ecc
0
backend/auth/apps/logs/__init__.py
Normal file
0
backend/auth/apps/logs/__init__.py
Normal file
116
backend/auth/apps/logs/urls.py
Normal file
116
backend/auth/apps/logs/urls.py
Normal file
@ -0,0 +1,116 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, and_, delete
|
||||
from database import get_db
|
||||
from models import LoginLog, OperationLog, User
|
||||
from schemas import (
|
||||
LoginLogResponse,
|
||||
LoginLogSearch,
|
||||
LoginLogCleanRequest,
|
||||
OperationLogResponse,
|
||||
OperationLogSearch,
|
||||
)
|
||||
from middleware import get_current_user, has_permission
|
||||
from utils.like_filter import ilike_contains
|
||||
|
||||
router = APIRouter(tags=["logs"])
|
||||
|
||||
|
||||
# ========================================
|
||||
# 登录日志
|
||||
# ========================================
|
||||
|
||||
@router.post("/login-logs/search", response_model=list[LoginLogResponse])
|
||||
async def search_login_logs(
|
||||
query: LoginLogSearch,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(has_permission("log:login")),
|
||||
):
|
||||
"""搜索登录日志"""
|
||||
stmt = select(LoginLog)
|
||||
filters = []
|
||||
|
||||
cond = ilike_contains(LoginLog.username, query.username)
|
||||
if cond is not None:
|
||||
filters.append(cond)
|
||||
if query.status:
|
||||
filters.append(LoginLog.status == query.status)
|
||||
if query.start_time:
|
||||
filters.append(LoginLog.login_time >= query.start_time)
|
||||
if query.end_time:
|
||||
filters.append(LoginLog.login_time <= query.end_time)
|
||||
|
||||
if filters:
|
||||
stmt = stmt.where(and_(*filters))
|
||||
stmt = stmt.order_by(LoginLog.login_time.desc())
|
||||
|
||||
result = await db.execute(stmt)
|
||||
rows = result.scalars().all()
|
||||
return [LoginLogResponse.model_validate(r) for r in rows]
|
||||
|
||||
|
||||
@router.delete("/login-logs/clean")
|
||||
async def clean_login_logs(
|
||||
clean_data: LoginLogCleanRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(has_permission("log:clean")),
|
||||
):
|
||||
"""清理指定日期之前的登录日志"""
|
||||
stmt = delete(LoginLog).where(LoginLog.login_time < clean_data.before_date)
|
||||
result = await db.execute(stmt)
|
||||
await db.commit()
|
||||
return {
|
||||
"message": f"已清理 {result.rowcount} 条登录日志",
|
||||
"deleted_count": result.rowcount,
|
||||
}
|
||||
|
||||
|
||||
# ========================================
|
||||
# 操作日志
|
||||
# ========================================
|
||||
|
||||
@router.post("/operation-logs/search", response_model=list[OperationLogResponse])
|
||||
async def search_operation_logs(
|
||||
query: OperationLogSearch,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(has_permission("log:operation")),
|
||||
):
|
||||
"""搜索操作日志"""
|
||||
stmt = select(OperationLog)
|
||||
filters = []
|
||||
|
||||
cond = ilike_contains(OperationLog.username, query.username)
|
||||
if cond is not None:
|
||||
filters.append(cond)
|
||||
if query.action_type:
|
||||
filters.append(OperationLog.action_type == query.action_type)
|
||||
if query.target_type:
|
||||
filters.append(OperationLog.target_type == query.target_type)
|
||||
if query.start_time:
|
||||
filters.append(OperationLog.createtime >= query.start_time)
|
||||
if query.end_time:
|
||||
filters.append(OperationLog.createtime <= query.end_time)
|
||||
|
||||
if filters:
|
||||
stmt = stmt.where(and_(*filters))
|
||||
stmt = stmt.order_by(OperationLog.createtime.desc())
|
||||
|
||||
result = await db.execute(stmt)
|
||||
rows = result.scalars().all()
|
||||
return [OperationLogResponse.model_validate(r) for r in rows]
|
||||
|
||||
|
||||
@router.delete("/operation-logs/clean")
|
||||
async def clean_operation_logs(
|
||||
clean_data: LoginLogCleanRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(has_permission("log:clean")),
|
||||
):
|
||||
"""清理指定日期之前的操作日志"""
|
||||
stmt = delete(OperationLog).where(OperationLog.createtime < clean_data.before_date)
|
||||
result = await db.execute(stmt)
|
||||
await db.commit()
|
||||
return {
|
||||
"message": f"已清理 {result.rowcount} 条操作日志",
|
||||
"deleted_count": result.rowcount,
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, and_
|
||||
from sqlalchemy.orm import selectinload
|
||||
@ -17,6 +17,7 @@ from schemas import (
|
||||
from middleware import get_current_user
|
||||
from models import User
|
||||
from utils.like_filter import ilike_contains
|
||||
from utils.log_utils import get_client_ip, get_user_agent, create_operation_log
|
||||
|
||||
router = APIRouter(prefix="/permissions", tags=["permissions"])
|
||||
|
||||
@ -27,6 +28,7 @@ permission_crud = FastCRUD(Permission, PermissionResponse)
|
||||
@router.post("/", response_model=PermissionResponse)
|
||||
async def create_permission(
|
||||
permission_data: PermissionCreate,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -46,15 +48,24 @@ async def create_permission(
|
||||
)
|
||||
|
||||
db.add(db_permission)
|
||||
await db.commit()
|
||||
await db.flush()
|
||||
await db.refresh(db_permission)
|
||||
|
||||
# 记录操作日志
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "create", "permission",
|
||||
db_permission.id, db_permission.name,
|
||||
{"name": permission_data.name, "code": permission_data.code},
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
return db_permission
|
||||
|
||||
|
||||
@router.post("/search", response_model=list[PermissionResponse])
|
||||
async def search_permissions(
|
||||
query: dict = {},
|
||||
request: Request = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -70,11 +81,21 @@ async def search_permissions(
|
||||
stmt = stmt.where(and_(*filters))
|
||||
result = await db.execute(stmt)
|
||||
permissions = result.scalars().all()
|
||||
|
||||
# 记录查询日志
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "search", "permission",
|
||||
None, None, {"filters": {k: v for k, v in query.items() if v}},
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return [PermissionResponse.model_validate(p) for p in permissions]
|
||||
|
||||
|
||||
@router.get("/tree", response_model=list[PermissionTreeResponse])
|
||||
async def get_permission_tree(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -89,6 +110,7 @@ async def get_permission_tree(
|
||||
"name": perm.name,
|
||||
"code": perm.code,
|
||||
"parentid": perm.parentid,
|
||||
"route": perm.route,
|
||||
"description": perm.description,
|
||||
"createtime": perm.createtime,
|
||||
"updatetime": perm.updatetime,
|
||||
@ -106,11 +128,20 @@ async def get_permission_tree(
|
||||
# 子节点,添加到父节点的 children
|
||||
perm_dict[parentid]["children"].append(perm_data)
|
||||
|
||||
# 记录查询日志
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "search", "permission",
|
||||
None, None, None,
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return tree
|
||||
|
||||
|
||||
@router.get("/menu", response_model=list[PermissionTreeResponse])
|
||||
async def get_menu(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -122,20 +153,20 @@ async def get_menu(
|
||||
.filter(Role.id == current_user.role_id)
|
||||
)
|
||||
role = result.scalar_one_or_none()
|
||||
|
||||
|
||||
if not role:
|
||||
raise HTTPException(status_code=404, detail="角色不存在")
|
||||
|
||||
# 获取用户有权限的权限编码集合
|
||||
user_permission_codes = {perm.code for perm in role.permissions}
|
||||
|
||||
|
||||
# 获取所有顶级模块和二级模块权限(有route字段的)
|
||||
result = await db.execute(
|
||||
select(Permission)
|
||||
.filter(Permission.route.isnot(None))
|
||||
)
|
||||
all_menu_permissions = result.scalars().all()
|
||||
|
||||
|
||||
# 过滤用户有权限的菜单
|
||||
user_menu_permissions = []
|
||||
for perm in all_menu_permissions:
|
||||
@ -147,7 +178,7 @@ async def get_menu(
|
||||
break
|
||||
if has_access:
|
||||
user_menu_permissions.append(perm)
|
||||
|
||||
|
||||
# 转换为字典,便于查找
|
||||
perm_dict = {perm.id: {
|
||||
"id": perm.id,
|
||||
@ -160,7 +191,7 @@ async def get_menu(
|
||||
"updatetime": perm.updatetime,
|
||||
"children": []
|
||||
} for perm in user_menu_permissions}
|
||||
|
||||
|
||||
# 构建树形结构
|
||||
tree = []
|
||||
for perm_id, perm_data in perm_dict.items():
|
||||
@ -171,13 +202,22 @@ async def get_menu(
|
||||
elif parentid in perm_dict:
|
||||
# 子节点,添加到父节点的 children
|
||||
perm_dict[parentid]["children"].append(perm_data)
|
||||
|
||||
|
||||
# 记录查询日志
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "search", "permission",
|
||||
None, None, {"type": "menu_tree"},
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return tree
|
||||
|
||||
|
||||
@router.get("/{permission_id}", response_model=PermissionResponse)
|
||||
async def get_permission(
|
||||
permission_id: int,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -185,6 +225,15 @@ async def get_permission(
|
||||
permission = await permission_crud.get(db, id=permission_id)
|
||||
if not permission:
|
||||
raise HTTPException(status_code=404, detail="权限不存在")
|
||||
|
||||
# 记录查询日志
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "search", "permission",
|
||||
permission_id, permission.name, None,
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return permission
|
||||
|
||||
|
||||
@ -192,10 +241,17 @@ async def get_permission(
|
||||
async def update_permission(
|
||||
permission_id: int,
|
||||
permission_data: PermissionCreate,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""更新权限"""
|
||||
# 获取原权限数据
|
||||
result = await db.execute(select(Permission).filter(Permission.id == permission_id))
|
||||
old_permission = result.scalar_one_or_none()
|
||||
if not old_permission:
|
||||
raise HTTPException(status_code=404, detail="权限不存在")
|
||||
|
||||
# 检查权限编码是否被其他权限使用
|
||||
result = await db.execute(
|
||||
select(Permission).filter(Permission.code == permission_data.code, Permission.id != permission_id)
|
||||
@ -213,12 +269,27 @@ async def update_permission(
|
||||
)
|
||||
if not updated_permission:
|
||||
raise HTTPException(status_code=404, detail="权限不存在")
|
||||
# 记录操作日志
|
||||
changed = {}
|
||||
for field in ["name", "code", "parentid", "description"]:
|
||||
old_val = getattr(old_permission, field, None)
|
||||
new_val = update_data.get(field)
|
||||
if old_val != new_val:
|
||||
changed[field] = {"old": old_val, "new": new_val}
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "update", "permission",
|
||||
permission_id, old_permission.name, changed if changed else None,
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return updated_permission
|
||||
|
||||
|
||||
@router.delete("/{permission_id}")
|
||||
async def delete_permission(
|
||||
permission_id: int,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -228,6 +299,13 @@ async def delete_permission(
|
||||
existing_permission = result.scalar_one_or_none()
|
||||
if not existing_permission:
|
||||
raise HTTPException(status_code=404, detail="权限不存在")
|
||||
# 记录操作日志(删除前)
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "delete", "permission",
|
||||
permission_id, existing_permission.name,
|
||||
{"name": existing_permission.name, "code": existing_permission.code},
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
|
||||
# 1. 删除角色-权限关联表中的记录
|
||||
from sqlalchemy import delete as sql_delete
|
||||
@ -256,6 +334,7 @@ async def delete_permission(
|
||||
@router.post("/assign", response_model=RolePermissionResponse)
|
||||
async def assign_permissions_to_role(
|
||||
data: RolePermissionAssign,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -276,6 +355,14 @@ async def assign_permissions_to_role(
|
||||
|
||||
# 更新角色的权限
|
||||
role.permissions = permissions
|
||||
|
||||
# 记录操作日志
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "update", "role",
|
||||
role.id, role.name,
|
||||
{"permission_ids": data.permission_ids},
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
await db.refresh(role)
|
||||
|
||||
@ -290,6 +377,7 @@ async def assign_permissions_to_role(
|
||||
@router.get("/role/{role_id}", response_model=RolePermissionResponse)
|
||||
async def get_role_permissions(
|
||||
role_id: int,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -300,6 +388,14 @@ async def get_role_permissions(
|
||||
if not role:
|
||||
raise HTTPException(status_code=404, detail="角色不存在")
|
||||
|
||||
# 记录查询日志
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "search", "permission",
|
||||
role_id, role.name, {"type": "role_permissions"},
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
# 返回角色权限信息
|
||||
return RolePermissionResponse(
|
||||
role_id=role.id,
|
||||
@ -310,6 +406,7 @@ async def get_role_permissions(
|
||||
|
||||
@router.get("/user/me", response_model=RolePermissionResponse)
|
||||
async def get_current_user_permissions(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -321,20 +418,20 @@ async def get_current_user_permissions(
|
||||
.filter(Role.id == current_user.role_id)
|
||||
)
|
||||
role = result.scalar_one_or_none()
|
||||
|
||||
|
||||
if not role:
|
||||
raise HTTPException(status_code=404, detail="角色不存在")
|
||||
|
||||
# 获取用户有权限的权限编码集合
|
||||
user_permission_codes = {perm.code for perm in role.permissions}
|
||||
|
||||
|
||||
# 获取所有顶级模块和二级模块权限(有route字段的)
|
||||
result = await db.execute(
|
||||
select(Permission)
|
||||
.filter(Permission.route.isnot(None))
|
||||
)
|
||||
all_menu_permissions = result.scalars().all()
|
||||
|
||||
|
||||
# 过滤用户有权限的菜单
|
||||
user_menu_permissions = []
|
||||
for perm in all_menu_permissions:
|
||||
@ -346,7 +443,7 @@ async def get_current_user_permissions(
|
||||
break
|
||||
if has_access:
|
||||
user_menu_permissions.append(perm)
|
||||
|
||||
|
||||
# 转换为字典,便于查找
|
||||
perm_dict = {perm.id: {
|
||||
"id": perm.id,
|
||||
@ -359,7 +456,7 @@ async def get_current_user_permissions(
|
||||
"updatetime": perm.updatetime,
|
||||
"children": []
|
||||
} for perm in user_menu_permissions}
|
||||
|
||||
|
||||
# 构建树形结构
|
||||
menu_tree = []
|
||||
for perm_id, perm_data in perm_dict.items():
|
||||
@ -371,6 +468,14 @@ async def get_current_user_permissions(
|
||||
# 子节点,添加到父节点的 children
|
||||
perm_dict[parentid]["children"].append(perm_data)
|
||||
|
||||
# 记录查询日志
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "search", "permission",
|
||||
None, None, {"type": "user_permissions_menu"},
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
# 返回用户权限信息和菜单
|
||||
return RolePermissionResponse(
|
||||
role_id=role.id,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from fastcrud import FastCRUD
|
||||
@ -8,6 +8,7 @@ from models import Role, User
|
||||
from schemas import RoleCreate, RoleResponse
|
||||
from middleware import get_current_user
|
||||
from utils.like_filter import ilike_contains
|
||||
from utils.log_utils import get_client_ip, get_user_agent, create_operation_log
|
||||
|
||||
router = APIRouter(prefix="/roles", tags=["roles"])
|
||||
|
||||
@ -18,13 +19,12 @@ role_crud = FastCRUD(Role, RoleResponse, RoleCreate)
|
||||
@router.post("/", response_model=RoleResponse)
|
||||
async def create_role(
|
||||
role_data: RoleCreate,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""创建角色"""
|
||||
# 检查角色名是否已存在
|
||||
from sqlalchemy import select
|
||||
|
||||
result = await db.execute(select(Role).filter(Role.name == role_data.name))
|
||||
existing_role = result.scalar_one_or_none()
|
||||
if existing_role:
|
||||
@ -38,18 +38,26 @@ async def create_role(
|
||||
name=role_data.name,
|
||||
creator=role_data.creator
|
||||
)
|
||||
|
||||
|
||||
db.add(new_role)
|
||||
await db.flush() # 获取生成的ID
|
||||
await db.refresh(new_role)
|
||||
|
||||
# 记录操作日志
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "create", "role",
|
||||
new_role.id, new_role.name, {"name": role_data.name},
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
return RoleResponse.model_validate(new_role)
|
||||
|
||||
|
||||
@router.post("/search", response_model=list[RoleResponse])
|
||||
async def search_roles(
|
||||
query: dict = {},
|
||||
request: Request = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -61,12 +69,22 @@ async def search_roles(
|
||||
stmt = stmt.where(cond)
|
||||
result = await db.execute(stmt)
|
||||
rows = result.scalars().all()
|
||||
|
||||
# 记录查询日志
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "search", "role",
|
||||
None, None, {"filters": {k: v for k, v in query.items() if v}},
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return [RoleResponse.model_validate(r) for r in rows]
|
||||
|
||||
|
||||
@router.get("/{role_id}", response_model=RoleResponse)
|
||||
async def get_role(
|
||||
role_id: int,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -75,6 +93,15 @@ async def get_role(
|
||||
role = result.scalar_one_or_none()
|
||||
if not role:
|
||||
raise HTTPException(status_code=404, detail="角色不存在")
|
||||
|
||||
# 记录查询日志
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "search", "role",
|
||||
role_id, role.name, None,
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return RoleResponse.model_validate(role)
|
||||
|
||||
|
||||
@ -82,13 +109,18 @@ async def get_role(
|
||||
async def update_role(
|
||||
role_id: int,
|
||||
role_data: RoleCreate,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""更新角色"""
|
||||
# 检查角色名是否被其他角色使用
|
||||
from sqlalchemy import select
|
||||
# 获取原角色数据
|
||||
result = await db.execute(select(Role).filter(Role.id == role_id))
|
||||
old_role = result.scalar_one_or_none()
|
||||
if not old_role:
|
||||
raise HTTPException(status_code=404, detail="角色不存在")
|
||||
|
||||
# 检查角色名是否被其他角色使用
|
||||
result = await db.execute(
|
||||
select(Role).filter(Role.name == role_data.name, Role.id != role_id)
|
||||
)
|
||||
@ -98,29 +130,39 @@ async def update_role(
|
||||
|
||||
# 使用原生 SQLAlchemy 更新角色(FastCRUD 有问题)
|
||||
from sqlalchemy import update
|
||||
|
||||
|
||||
stmt = (
|
||||
update(Role)
|
||||
.where(Role.id == role_id)
|
||||
.values(name=role_data.name)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
|
||||
|
||||
if result.rowcount == 0:
|
||||
raise HTTPException(status_code=404, detail="角色不存在")
|
||||
|
||||
|
||||
# 记录操作日志
|
||||
changed = {}
|
||||
if old_role.name != role_data.name:
|
||||
changed["name"] = {"old": old_role.name, "new": role_data.name}
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "update", "role",
|
||||
role_id, old_role.name, changed if changed else None,
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
# 查询更新后的数据
|
||||
result = await db.execute(select(Role).filter(Role.id == role_id))
|
||||
updated_role = result.scalar_one_or_none()
|
||||
|
||||
|
||||
return updated_role
|
||||
|
||||
|
||||
@router.delete("/{role_id}")
|
||||
async def delete_role(
|
||||
role_id: int,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -139,6 +181,13 @@ async def delete_role(
|
||||
status_code=400, detail=f"该角色正在被 {len(users)} 个用户使用,无法删除"
|
||||
)
|
||||
|
||||
# 记录操作日志(删除前)
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "delete", "role",
|
||||
role_id, existing_role.name, {"name": existing_role.name},
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
|
||||
# 使用FastCRUD删除角色
|
||||
await role_crud.delete(db, id=role_id)
|
||||
return {"message": "角色删除成功"}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, File, UploadFile
|
||||
from fastapi import APIRouter, Depends, HTTPException, File, UploadFile, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, and_
|
||||
from fastcrud import FastCRUD
|
||||
@ -16,6 +16,7 @@ from schemas import (
|
||||
)
|
||||
from middleware import get_current_user
|
||||
from utils.like_filter import ilike_contains
|
||||
from utils.log_utils import get_client_ip, get_user_agent, create_operation_log
|
||||
|
||||
router = APIRouter(prefix="/settings", tags=["settings"])
|
||||
|
||||
@ -30,6 +31,7 @@ UPLOAD_DIR.mkdir(exist_ok=True)
|
||||
@router.post("/", response_model=SystemSettingResponse)
|
||||
async def create_setting(
|
||||
setting_data: SystemSettingCreate,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -50,20 +52,39 @@ async def create_setting(
|
||||
)
|
||||
|
||||
db.add(db_setting)
|
||||
await db.commit()
|
||||
await db.flush()
|
||||
await db.refresh(db_setting)
|
||||
|
||||
# 记录操作日志
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "create", "setting",
|
||||
db_setting.id, db_setting.key,
|
||||
{"key": setting_data.key, "type": setting_data.type},
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return db_setting
|
||||
|
||||
|
||||
@router.get("/", response_model=list[SystemSettingResponse])
|
||||
async def get_settings(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取所有设置项"""
|
||||
result = await db.execute(select(SystemSetting))
|
||||
settings = result.scalars().all()
|
||||
|
||||
# 记录查询日志
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "search", "setting",
|
||||
None, None, None,
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return [SystemSettingResponse.model_validate(s) for s in settings]
|
||||
|
||||
|
||||
@ -97,6 +118,7 @@ async def get_public_setting(
|
||||
@router.get("/{setting_id}", response_model=SystemSettingResponse)
|
||||
async def get_setting(
|
||||
setting_id: int,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -104,12 +126,22 @@ async def get_setting(
|
||||
setting = await setting_crud.get(db, id=setting_id)
|
||||
if not setting:
|
||||
raise HTTPException(status_code=404, detail="设置项不存在")
|
||||
|
||||
# 记录查询日志
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "search", "setting",
|
||||
setting_id, setting.key, None,
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return setting
|
||||
|
||||
|
||||
@router.get("/key/{key}", response_model=SystemSettingResponse)
|
||||
async def get_setting_by_key(
|
||||
key: str,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -118,6 +150,15 @@ async def get_setting_by_key(
|
||||
setting = result.scalar_one_or_none()
|
||||
if not setting:
|
||||
raise HTTPException(status_code=404, detail="设置项不存在")
|
||||
|
||||
# 记录查询日志
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "search", "setting",
|
||||
setting.id, setting.key, None,
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return setting
|
||||
|
||||
|
||||
@ -125,10 +166,17 @@ async def get_setting_by_key(
|
||||
async def update_setting(
|
||||
setting_id: int,
|
||||
setting_data: SystemSettingUpdate,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""更新设置项"""
|
||||
# 获取原设置数据
|
||||
result = await db.execute(select(SystemSetting).filter(SystemSetting.id == setting_id))
|
||||
old_setting = result.scalar_one_or_none()
|
||||
if not old_setting:
|
||||
raise HTTPException(status_code=404, detail="设置项不存在")
|
||||
|
||||
# 检查设置键名是否被其他设置使用
|
||||
if setting_data.key:
|
||||
result = await db.execute(
|
||||
@ -147,12 +195,28 @@ async def update_setting(
|
||||
)
|
||||
if not updated_setting:
|
||||
raise HTTPException(status_code=404, detail="设置项不存在")
|
||||
|
||||
# 记录操作日志
|
||||
changed = {}
|
||||
for field in ["key", "value", "type", "description"]:
|
||||
old_val = getattr(old_setting, field, None)
|
||||
new_val = update_data.get(field) if field in update_data else old_val
|
||||
if old_val != new_val:
|
||||
changed[field] = {"old": old_val, "new": new_val}
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "update", "setting",
|
||||
setting_id, old_setting.key, changed if changed else None,
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return updated_setting
|
||||
|
||||
|
||||
@router.delete("/{setting_id}")
|
||||
async def delete_setting(
|
||||
setting_id: int,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -163,6 +227,14 @@ async def delete_setting(
|
||||
if not existing_setting:
|
||||
raise HTTPException(status_code=404, detail="设置项不存在")
|
||||
|
||||
# 记录操作日志(删除前)
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "delete", "setting",
|
||||
setting_id, existing_setting.key,
|
||||
{"key": existing_setting.key, "value": existing_setting.value},
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
|
||||
# 使用FastCRUD删除设置
|
||||
deleted = await setting_crud.delete(db, id=setting_id)
|
||||
return {"message": "设置项删除成功"}
|
||||
@ -186,7 +258,7 @@ async def upload_image(
|
||||
if file_size > 2 * 1024 * 1024:
|
||||
raise HTTPException(status_code=400, detail="文件大小不能超过2MB")
|
||||
chunk = await file.read(1024)
|
||||
|
||||
|
||||
# 重置文件指针
|
||||
await file.seek(0)
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ from .users.urls import router as users_router
|
||||
from .roles.urls import router as roles_router
|
||||
from .permissions.urls import router as permissions_router
|
||||
from .settings.urls import router as settings_router
|
||||
from .logs.urls import router as logs_router
|
||||
|
||||
# 创建主路由
|
||||
api_router = APIRouter()
|
||||
@ -11,4 +12,5 @@ api_router = APIRouter()
|
||||
api_router.include_router(users_router)
|
||||
api_router.include_router(roles_router)
|
||||
api_router.include_router(permissions_router)
|
||||
api_router.include_router(settings_router)
|
||||
api_router.include_router(settings_router)
|
||||
api_router.include_router(logs_router)
|
||||
@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status, Request
|
||||
from fastapi.responses import Response
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, and_
|
||||
@ -19,10 +19,11 @@ from schemas import (
|
||||
from utils.password import verify_password, get_password_hash
|
||||
from utils.jwt import create_access_token
|
||||
from config import app_settings
|
||||
from utils.redis_client import get_redis, set_token_in_redis
|
||||
from utils.redis_client import get_redis, set_token_in_redis, delete_token_from_redis
|
||||
from utils.captcha import create_captcha, validate_captcha
|
||||
from middleware import get_current_user
|
||||
from utils.like_filter import ilike_contains
|
||||
from utils.log_utils import get_client_ip, get_user_agent, save_login_log, create_operation_log
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["users"])
|
||||
|
||||
@ -35,26 +36,31 @@ async def get_captcha(redis_conn: redis.Redis = Depends(get_redis)):
|
||||
"""获取验证码图片"""
|
||||
# 创建验证码
|
||||
captcha_id, image_bytes = await create_captcha(redis_conn)
|
||||
|
||||
|
||||
# 返回验证码图片,同时在响应头中返回验证码ID
|
||||
response = Response(content=image_bytes, media_type="image/png")
|
||||
response.headers["X-Captcha-ID"] = captcha_id
|
||||
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(
|
||||
user_data: UserLogin,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
redis_conn: redis.Redis = Depends(get_redis),
|
||||
):
|
||||
"""用户登录"""
|
||||
ip = get_client_ip(request)
|
||||
ua = get_user_agent(request)
|
||||
|
||||
# 验证验证码
|
||||
is_captcha_valid = await validate_captcha(
|
||||
redis_conn, user_data.captcha_id, user_data.captcha_code
|
||||
)
|
||||
if not is_captcha_valid:
|
||||
await save_login_log(db, user_data.username, ip, ua, "failure", "验证码错误或已过期")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="验证码错误或已过期",
|
||||
@ -67,6 +73,7 @@ async def login(
|
||||
|
||||
# 验证用户是否存在且密码正确
|
||||
if not user or not verify_password(user_data.password, user.password_hash):
|
||||
await save_login_log(db, user_data.username, ip, ua, "failure", "用户名或密码错误")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户名或密码错误",
|
||||
@ -88,12 +95,16 @@ async def login(
|
||||
user.lastlogintime = datetime.utcnow()
|
||||
await db.commit()
|
||||
|
||||
# 记录登录成功日志
|
||||
await save_login_log(db, user.username, ip, ua, "success", None)
|
||||
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
@router.post("/", response_model=UserResponse)
|
||||
async def create_user(
|
||||
user_data: UserCreateRequest,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -118,12 +129,23 @@ async def create_user(
|
||||
created_user = await user_crud.create(
|
||||
db, user_data_create, schema_to_select=UserResponse, return_as_model=True
|
||||
)
|
||||
|
||||
# 记录操作日志
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "create", "user",
|
||||
created_user.id, created_user.username,
|
||||
{"username": user_data.username, "nickname": user_data.nickname},
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return created_user
|
||||
|
||||
|
||||
@router.post("/search", response_model=list[UserResponse])
|
||||
async def search_users(
|
||||
query: dict = {},
|
||||
request: Request = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -144,18 +166,39 @@ async def search_users(
|
||||
stmt = stmt.where(and_(*filters))
|
||||
result = await db.execute(stmt)
|
||||
rows = result.scalars().all()
|
||||
|
||||
# 记录查询日志
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "search", "user",
|
||||
None, None, {"filters": {k: v for k, v in query.items() if v}},
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return [UserResponse.model_validate(u) for u in rows]
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
async def get_current_user_info(current_user: User = Depends(get_current_user)):
|
||||
async def get_current_user_info(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取当前用户信息(须声明在 /{user_id} 之前,否则会被当成 id)"""
|
||||
# 记录查询日志
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "search", "user",
|
||||
current_user.id, current_user.username, None,
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
return current_user
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserResponse)
|
||||
async def get_user(
|
||||
user_id: int,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -163,6 +206,15 @@ async def get_user(
|
||||
user = await user_crud.get(db, id=user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
# 记录查询日志
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "search", "user",
|
||||
user_id, user.username, None,
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return user
|
||||
|
||||
|
||||
@ -170,10 +222,17 @@ async def get_user(
|
||||
async def update_user(
|
||||
user_id: int,
|
||||
user_data: UserUpdate,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""更新用户"""
|
||||
# 获取原用户数据用于记录变更
|
||||
result = await db.execute(select(User).filter(User.id == user_id))
|
||||
old_user = result.scalar_one_or_none()
|
||||
if not old_user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
# 检查用户名是否被其他用户使用
|
||||
result = await db.execute(
|
||||
select(User).filter(User.username == user_data.username, User.id != user_id)
|
||||
@ -199,12 +258,28 @@ async def update_user(
|
||||
)
|
||||
if not updated_user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
# 记录操作日志(计算变更字段)
|
||||
changed = {}
|
||||
for field in ["username", "nickname", "email", "phone", "role_id"]:
|
||||
old_val = getattr(old_user, field, None)
|
||||
new_val = update_data.get(field)
|
||||
if old_val != new_val:
|
||||
changed[field] = {"old": old_val, "new": new_val}
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "update", "user",
|
||||
user_id, old_user.username, changed if changed else None,
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return updated_user
|
||||
|
||||
|
||||
@router.delete("/{user_id}")
|
||||
async def delete_user(
|
||||
user_id: int,
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@ -219,6 +294,13 @@ async def delete_user(
|
||||
elif existing_user.role_id == 1:
|
||||
raise HTTPException(status_code=400, detail="不能删除管理员用户")
|
||||
else:
|
||||
# 记录操作日志(删除前记录被删用户信息)
|
||||
await create_operation_log(
|
||||
db, current_user.id, current_user.username, "delete", "user",
|
||||
user_id, existing_user.username,
|
||||
{"username": existing_user.username, "nickname": existing_user.nickname},
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
)
|
||||
# 使用FastCRUD删除用户
|
||||
deleted = await user_crud.delete(db, id=user_id)
|
||||
return {"message": "用户删除成功"}
|
||||
@ -226,11 +308,18 @@ async def delete_user(
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
redis_conn: redis.Redis = Depends(get_redis),
|
||||
):
|
||||
"""用户登出"""
|
||||
from utils.redis_client import delete_token_from_redis
|
||||
|
||||
await delete_token_from_redis(redis_conn, current_user.id)
|
||||
|
||||
# 记录登出日志
|
||||
await save_login_log(
|
||||
db, current_user.username,
|
||||
get_client_ip(request), get_user_agent(request),
|
||||
"success", None,
|
||||
)
|
||||
return {"message": "登出成功"}
|
||||
|
||||
@ -14,7 +14,8 @@ async def init_permissions(db: AsyncSession):
|
||||
# ========================================
|
||||
{"name": "权限管理", "code": "perm", "parentid": 0, "route": "/admin/permissions", "description": "权限管理模块"},
|
||||
{"name": "系统设置", "code": "system", "parentid": 0, "route": "/admin/settings", "description": "系统设置模块"},
|
||||
|
||||
{"name": "日志管理", "code": "log", "parentid": 0, "route": "/admin/logs", "description": "日志管理模块"},
|
||||
|
||||
# ========================================
|
||||
# 权限管理 -> 二级子模块
|
||||
# ========================================
|
||||
@ -23,51 +24,62 @@ async def init_permissions(db: AsyncSession):
|
||||
{"name": "菜单管理", "code": "perm:menu", "parentid": 1, "route": "/admin/menu", "description": "菜单管理子模块"},
|
||||
|
||||
# ========================================
|
||||
# 用户管理操作(parentid=3)
|
||||
# 用户管理操作(parentid=4)
|
||||
# ========================================
|
||||
{"name": "查看列表", "code": "perm:user:list", "parentid": 3, "description": "查看用户列表"},
|
||||
{"name": "查看详情", "code": "perm:user:detail", "parentid": 3, "description": "查看用户详情"},
|
||||
{"name": "搜索", "code": "perm:user:search", "parentid": 3, "description": "搜索用户"},
|
||||
{"name": "创建", "code": "perm:user:create", "parentid": 3, "description": "创建新用户"},
|
||||
{"name": "编辑", "code": "perm:user:edit", "parentid": 3, "description": "编辑用户信息"},
|
||||
{"name": "删除", "code": "perm:user:delete", "parentid": 3, "description": "删除用户"},
|
||||
{"name": "导出", "code": "perm:user:export", "parentid": 3, "description": "导出用户数据"},
|
||||
{"name": "批量操作", "code": "perm:user:batch", "parentid": 3, "description": "批量操作用户"},
|
||||
{"name": "查看列表", "code": "perm:user:list", "parentid": 4, "description": "查看用户列表"},
|
||||
{"name": "查看详情", "code": "perm:user:detail", "parentid": 4, "description": "查看用户详情"},
|
||||
{"name": "搜索", "code": "perm:user:search", "parentid": 4, "description": "搜索用户"},
|
||||
{"name": "创建", "code": "perm:user:create", "parentid": 4, "description": "创建新用户"},
|
||||
{"name": "编辑", "code": "perm:user:edit", "parentid": 4, "description": "编辑用户信息"},
|
||||
{"name": "删除", "code": "perm:user:delete", "parentid": 4, "description": "删除用户"},
|
||||
{"name": "导出", "code": "perm:user:export", "parentid": 4, "description": "导出用户数据"},
|
||||
{"name": "批量操作", "code": "perm:user:batch", "parentid": 4, "description": "批量操作用户"},
|
||||
|
||||
# ========================================
|
||||
# 角色管理操作(parentid=4)
|
||||
# 角色管理操作(parentid=5)
|
||||
# ========================================
|
||||
{"name": "查看列表", "code": "perm:role:list", "parentid": 4, "description": "查看角色列表"},
|
||||
{"name": "查看详情", "code": "perm:role:detail", "parentid": 4, "description": "查看角色详情"},
|
||||
{"name": "搜索", "code": "perm:role:search", "parentid": 4, "description": "搜索角色"},
|
||||
{"name": "创建", "code": "perm:role:create", "parentid": 4, "description": "创建新角色"},
|
||||
{"name": "编辑", "code": "perm:role:edit", "parentid": 4, "description": "编辑角色信息"},
|
||||
{"name": "删除", "code": "perm:role:delete", "parentid": 4, "description": "删除角色"},
|
||||
{"name": "导出", "code": "perm:role:export", "parentid": 4, "description": "导出角色数据"},
|
||||
{"name": "分配权限", "code": "perm:role:assign", "parentid": 4, "description": "为角色分配权限"},
|
||||
{"name": "查看列表", "code": "perm:role:list", "parentid": 5, "description": "查看角色列表"},
|
||||
{"name": "查看详情", "code": "perm:role:detail", "parentid": 5, "description": "查看角色详情"},
|
||||
{"name": "搜索", "code": "perm:role:search", "parentid": 5, "description": "搜索角色"},
|
||||
{"name": "创建", "code": "perm:role:create", "parentid": 5, "description": "创建新角色"},
|
||||
{"name": "编辑", "code": "perm:role:edit", "parentid": 5, "description": "编辑角色信息"},
|
||||
{"name": "删除", "code": "perm:role:delete", "parentid": 5, "description": "删除角色"},
|
||||
{"name": "导出", "code": "perm:role:export", "parentid": 5, "description": "导出角色数据"},
|
||||
{"name": "分配权限", "code": "perm:role:assign", "parentid": 5, "description": "为角色分配权限"},
|
||||
|
||||
# ========================================
|
||||
# 权限配置操作(parentid=5)
|
||||
# 菜单管理操作(parentid=6)
|
||||
# ========================================
|
||||
{"name": "查看列表", "code": "perm:menu:list", "parentid": 5, "description": "查看权限列表"},
|
||||
{"name": "查看详情", "code": "perm:menu:detail", "parentid": 5, "description": "查看权限详情"},
|
||||
{"name": "搜索", "code": "perm:menu:search", "parentid": 5, "description": "搜索权限"},
|
||||
{"name": "创建", "code": "perm:menu:create", "parentid": 5, "description": "创建新权限"},
|
||||
{"name": "编辑", "code": "perm:menu:edit", "parentid": 5, "description": "编辑权限信息"},
|
||||
{"name": "删除", "code": "perm:menu:delete", "parentid": 5, "description": "删除权限"},
|
||||
{"name": "导出", "code": "perm:menu:export", "parentid": 5, "description": "导岀权限数据"},
|
||||
|
||||
{"name": "查看列表", "code": "perm:menu:list", "parentid": 6, "description": "查看菜单列表"},
|
||||
{"name": "查看详情", "code": "perm:menu:detail", "parentid": 6, "description": "查看菜单详情"},
|
||||
{"name": "搜索", "code": "perm:menu:search", "parentid": 6, "description": "搜索菜单"},
|
||||
{"name": "创建", "code": "perm:menu:create", "parentid": 6, "description": "创建新菜单"},
|
||||
{"name": "编辑", "code": "perm:menu:edit", "parentid": 6, "description": "编辑菜单信息"},
|
||||
{"name": "删除", "code": "perm:menu:delete", "parentid": 6, "description": "删除菜单"},
|
||||
{"name": "导出", "code": "perm:menu:export", "parentid": 6, "description": "导出菜单数据"},
|
||||
|
||||
# ========================================
|
||||
# 系统设置 -> 基础配置操作(parentid=6)
|
||||
# 日志管理 -> 子模块(parentid=3)
|
||||
# ========================================
|
||||
{"name": "查看配置", "code": "system:basic:view", "parentid": 6, "description": "查看基础配置"},
|
||||
{"name": "修改配置", "code": "system:basic:edit", "parentid": 6, "description": "修改基础配置"},
|
||||
|
||||
{"name": "登录日志", "code": "log:login", "parentid": 3, "route": "/admin/login-logs", "description": "登录日志查看"},
|
||||
{"name": "操作日志", "code": "log:operation", "parentid": 3, "route": "/admin/operation-logs", "description": "操作日志查看"},
|
||||
|
||||
# ========================================
|
||||
# 系统设置 -> 高级配置操作(parentid=7)
|
||||
# 登录日志操作(parentid=30)
|
||||
# ========================================
|
||||
{"name": "查看配置", "code": "system:advanced:view", "parentid": 7, "description": "查看高级配置"},
|
||||
{"name": "修改配置", "code": "system:advanced:edit", "parentid": 7, "description": "修改高级配置"},
|
||||
{"name": "查看列表", "code": "log:login:list", "parentid": 30, "description": "查看登录日志列表"},
|
||||
{"name": "搜索", "code": "log:login:search", "parentid": 30, "description": "搜索登录日志"},
|
||||
|
||||
# ========================================
|
||||
# 操作日志操作(parentid=31)
|
||||
# ========================================
|
||||
{"name": "查看列表", "code": "log:operation:list", "parentid": 31, "description": "查看操作日志列表"},
|
||||
{"name": "搜索", "code": "log:operation:search", "parentid": 31, "description": "搜索操作日志"},
|
||||
|
||||
# ========================================
|
||||
# 日志清理(parentid=3)
|
||||
# ========================================
|
||||
{"name": "清理日志", "code": "log:clean", "parentid": 3, "description": "清理旧日志"},
|
||||
]
|
||||
|
||||
# 检查现有权限
|
||||
@ -351,7 +363,7 @@ async def init_system_settings(db: AsyncSession):
|
||||
},
|
||||
{
|
||||
"key": "页脚信息",
|
||||
"value": "© 2026 图书管理系统 版权所有 | 技术支持:强蕊科技",
|
||||
"value": "Copyright © 2026 图书管理系统 版权所有 | 技术支持:强蕊科技",
|
||||
"type": "string",
|
||||
"description": "页脚信息"
|
||||
},
|
||||
|
||||
@ -8,6 +8,7 @@ from typing import Callable
|
||||
from database import get_db
|
||||
from utils.jwt import decode_token
|
||||
from utils.redis_client import get_redis, check_token_in_redis
|
||||
from models import User
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
@ -41,9 +42,6 @@ async def get_current_user(
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# 查找用户
|
||||
from models import User
|
||||
|
||||
result = await db.execute(select(User).filter(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user:
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, func, Table
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, func, Table, Index, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from datetime import datetime
|
||||
@ -97,3 +97,44 @@ class SystemSetting(Base):
|
||||
updatetime = Column(
|
||||
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间"
|
||||
)
|
||||
|
||||
|
||||
class LoginLog(Base):
|
||||
"""登录日志"""
|
||||
|
||||
__tablename__ = "login_log"
|
||||
id = Column(Integer, primary_key=True, index=True, comment="日志ID")
|
||||
username = Column(String(50), nullable=False, comment="用户名")
|
||||
login_time = Column(DateTime, default=datetime.utcnow, comment="登录时间")
|
||||
ip_address = Column(String(50), nullable=True, comment="IP地址")
|
||||
user_agent = Column(String(500), nullable=True, comment="User-Agent")
|
||||
status = Column(String(20), nullable=False, comment="状态: success/failure")
|
||||
failure_reason = Column(String(255), nullable=True, comment="失败原因")
|
||||
|
||||
|
||||
Index("ix_login_log_login_time", LoginLog.login_time)
|
||||
Index("ix_login_log_username", LoginLog.username)
|
||||
Index("ix_login_log_status", LoginLog.status)
|
||||
|
||||
|
||||
class OperationLog(Base):
|
||||
"""操作日志"""
|
||||
|
||||
__tablename__ = "operation_log"
|
||||
id = Column(Integer, primary_key=True, index=True, comment="日志ID")
|
||||
user_id = Column(Integer, nullable=False, comment="用户ID")
|
||||
username = Column(String(50), nullable=False, comment="用户名")
|
||||
action_type = Column(String(20), nullable=False, comment="操作类型: create/update/delete")
|
||||
target_type = Column(String(50), nullable=False, comment="目标类型: user/role/permission/setting")
|
||||
target_id = Column(Integer, nullable=True, comment="目标ID")
|
||||
target_name = Column(String(100), nullable=True, comment="目标名称")
|
||||
detail = Column(Text, nullable=True, comment="变更详情(JSON)")
|
||||
ip_address = Column(String(50), nullable=True, comment="IP地址")
|
||||
user_agent = Column(String(500), nullable=True, comment="User-Agent")
|
||||
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
|
||||
|
||||
|
||||
Index("ix_operation_log_createtime", OperationLog.createtime)
|
||||
Index("ix_operation_log_username", OperationLog.username)
|
||||
Index("ix_operation_log_action_type", OperationLog.action_type)
|
||||
Index("ix_operation_log_target_type", OperationLog.target_type)
|
||||
@ -227,3 +227,62 @@ class SystemSettingResponse(BaseModel):
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class LoginLogResponse(BaseModel):
|
||||
"""登录日志响应模型"""
|
||||
|
||||
id: int = Field(..., description="日志ID")
|
||||
username: str = Field(..., description="用户名")
|
||||
login_time: datetime = Field(..., description="登录时间")
|
||||
ip_address: Optional[str] = Field(None, description="IP地址")
|
||||
user_agent: Optional[str] = Field(None, description="User-Agent")
|
||||
status: str = Field(..., description="状态: success/failure")
|
||||
failure_reason: Optional[str] = Field(None, description="失败原因")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class LoginLogSearch(BaseModel):
|
||||
"""登录日志搜索模型"""
|
||||
|
||||
username: Optional[str] = Field(None, description="用户名(模糊搜索)")
|
||||
status: Optional[str] = Field(None, description="状态")
|
||||
start_time: Optional[datetime] = Field(None, description="开始时间")
|
||||
end_time: Optional[datetime] = Field(None, description="结束时间")
|
||||
|
||||
|
||||
class LoginLogCleanRequest(BaseModel):
|
||||
"""登录日志清理请求模型"""
|
||||
|
||||
before_date: datetime = Field(..., description="清理此日期之前的日志")
|
||||
|
||||
|
||||
class OperationLogResponse(BaseModel):
|
||||
"""操作日志响应模型"""
|
||||
|
||||
id: int = Field(..., description="日志ID")
|
||||
user_id: int = Field(..., description="用户ID")
|
||||
username: str = Field(..., description="用户名")
|
||||
action_type: str = Field(..., description="操作类型: create/update/delete")
|
||||
target_type: str = Field(..., description="目标类型: user/role/permission/setting")
|
||||
target_id: Optional[int] = Field(None, description="目标ID")
|
||||
target_name: Optional[str] = Field(None, description="目标名称")
|
||||
detail: Optional[str] = Field(None, description="变更详情(JSON)")
|
||||
ip_address: Optional[str] = Field(None, description="IP地址")
|
||||
user_agent: Optional[str] = Field(None, description="User-Agent")
|
||||
createtime: datetime = Field(..., description="创建时间")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class OperationLogSearch(BaseModel):
|
||||
"""操作日志搜索模型"""
|
||||
|
||||
username: Optional[str] = Field(None, description="用户名(模糊搜索)")
|
||||
action_type: Optional[str] = Field(None, description="操作类型")
|
||||
target_type: Optional[str] = Field(None, description="目标类型")
|
||||
start_time: Optional[datetime] = Field(None, description="开始时间")
|
||||
end_time: Optional[datetime] = Field(None, description="结束时间")
|
||||
70
backend/auth/utils/log_utils.py
Normal file
70
backend/auth/utils/log_utils.py
Normal file
@ -0,0 +1,70 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from fastapi import Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from models import LoginLog, OperationLog
|
||||
|
||||
|
||||
def get_client_ip(request: Request) -> str:
|
||||
"""从请求中提取客户端IP地址,支持代理转发"""
|
||||
xff = request.headers.get("X-Forwarded-For")
|
||||
if xff:
|
||||
return xff.split(",")[0].strip()
|
||||
xri = request.headers.get("X-Real-IP")
|
||||
if xri:
|
||||
return xri.strip()
|
||||
return request.client.host if request.client else "unknown"
|
||||
|
||||
|
||||
def get_user_agent(request: Request) -> str:
|
||||
"""从请求中提取 User-Agent"""
|
||||
return request.headers.get("User-Agent", "")
|
||||
|
||||
|
||||
async def save_login_log(
|
||||
db: AsyncSession,
|
||||
username: str,
|
||||
ip_address: str,
|
||||
user_agent: str,
|
||||
status: str,
|
||||
failure_reason: str | None = None,
|
||||
):
|
||||
"""保存登录日志(独立提交,确保失败记录不丢失)"""
|
||||
log = LoginLog(
|
||||
username=username,
|
||||
login_time=datetime.utcnow(),
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
status=status,
|
||||
failure_reason=failure_reason,
|
||||
)
|
||||
db.add(log)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def create_operation_log(
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
username: str,
|
||||
action_type: str,
|
||||
target_type: str,
|
||||
target_id: int | None = None,
|
||||
target_name: str | None = None,
|
||||
detail: dict | None = None,
|
||||
ip_address: str = "",
|
||||
user_agent: str = "",
|
||||
):
|
||||
"""创建操作日志(不提交,由调用方事务统一提交)"""
|
||||
log = OperationLog(
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
action_type=action_type,
|
||||
target_type=target_type,
|
||||
target_id=target_id,
|
||||
target_name=target_name,
|
||||
detail=json.dumps(detail, ensure_ascii=False) if detail else None,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
createtime=datetime.utcnow(),
|
||||
)
|
||||
db.add(log)
|
||||
@ -68,7 +68,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, markRaw } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { Key, Setting, Folder } from "@element-plus/icons-vue";
|
||||
import { Key, Setting, Folder, Document } from "@element-plus/icons-vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { useAuthStore } from "@/modules/admin/stores/auth";
|
||||
import axios from "axios";
|
||||
@ -87,6 +87,7 @@ const pageTitle = computed(() => (route.meta.title as string) || "");
|
||||
const iconMap: Record<string, any> = {
|
||||
"perm": markRaw(Key),
|
||||
"system": markRaw(Setting),
|
||||
"log": markRaw(Document),
|
||||
};
|
||||
|
||||
function getIcon(code: string) {
|
||||
|
||||
83
frontend/src/modules/admin/views/logs/LoginLogManage.vue
Normal file
83
frontend/src/modules/admin/views/logs/LoginLogManage.vue
Normal file
@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<fs-page>
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding" />
|
||||
</fs-page>
|
||||
|
||||
<!-- 清理旧日志对话框 -->
|
||||
<el-dialog v-model="cleanDlgVisible" title="清理旧日志" width="420px">
|
||||
<el-form>
|
||||
<el-form-item label="清理此日期之前的日志">
|
||||
<el-date-picker
|
||||
v-model="cleanBeforeDate"
|
||||
type="datetime"
|
||||
placeholder="选择日期时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="cleanDlgVisible = false">取消</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
:loading="cleanLoading"
|
||||
@click="onClean"
|
||||
>确认清理</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useFs } from "@fast-crud/fast-crud";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import dayjs from "dayjs";
|
||||
import { http } from "@/modules/admin/api/http";
|
||||
import { buildLoginLogCrudOptions } from "./login-log-crud";
|
||||
|
||||
const { crudBinding, crudRef, crudExpose } = useFs({
|
||||
createCrudOptions: buildLoginLogCrudOptions({
|
||||
onClean: () => {
|
||||
cleanDlgVisible.value = true;
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const cleanDlgVisible = ref(false);
|
||||
const cleanBeforeDate = ref<Date>();
|
||||
const cleanLoading = ref(false);
|
||||
|
||||
async function onClean() {
|
||||
if (!cleanBeforeDate.value) {
|
||||
ElMessage.warning("请选择要清理的日期");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要清理 ${dayjs(cleanBeforeDate.value).format("YYYY-MM-DD HH:mm:ss")} 之前的所有登录日志吗?此操作不可撤销。`,
|
||||
"确认清理",
|
||||
{ confirmButtonText: "确认清理", cancelButtonText: "取消", type: "warning" }
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
cleanLoading.value = true;
|
||||
try {
|
||||
const { data } = await http.delete("/login-logs/clean", {
|
||||
data: { before_date: cleanBeforeDate.value.toISOString() },
|
||||
});
|
||||
ElMessage.success(data.message || "清理成功");
|
||||
cleanDlgVisible.value = false;
|
||||
crudExpose.doRefresh();
|
||||
} catch (error) {
|
||||
ElMessage.error("清理失败");
|
||||
} finally {
|
||||
cleanLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
crudExpose.doRefresh();
|
||||
});
|
||||
</script>
|
||||
83
frontend/src/modules/admin/views/logs/OperationLogManage.vue
Normal file
83
frontend/src/modules/admin/views/logs/OperationLogManage.vue
Normal file
@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<fs-page>
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding" />
|
||||
</fs-page>
|
||||
|
||||
<!-- 清理旧日志对话框 -->
|
||||
<el-dialog v-model="cleanDlgVisible" title="清理旧日志" width="420px">
|
||||
<el-form>
|
||||
<el-form-item label="清理此日期之前的日志">
|
||||
<el-date-picker
|
||||
v-model="cleanBeforeDate"
|
||||
type="datetime"
|
||||
placeholder="选择日期时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="cleanDlgVisible = false">取消</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
:loading="cleanLoading"
|
||||
@click="onClean"
|
||||
>确认清理</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useFs } from "@fast-crud/fast-crud";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import dayjs from "dayjs";
|
||||
import { http } from "@/modules/admin/api/http";
|
||||
import { buildOperationLogCrudOptions } from "./operation-log-crud";
|
||||
|
||||
const { crudBinding, crudRef, crudExpose } = useFs({
|
||||
createCrudOptions: buildOperationLogCrudOptions({
|
||||
onClean: () => {
|
||||
cleanDlgVisible.value = true;
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const cleanDlgVisible = ref(false);
|
||||
const cleanBeforeDate = ref<Date>();
|
||||
const cleanLoading = ref(false);
|
||||
|
||||
async function onClean() {
|
||||
if (!cleanBeforeDate.value) {
|
||||
ElMessage.warning("请选择要清理的日期");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要清理 ${dayjs(cleanBeforeDate.value).format("YYYY-MM-DD HH:mm:ss")} 之前的所有操作日志吗?此操作不可撤销。`,
|
||||
"确认清理",
|
||||
{ confirmButtonText: "确认清理", cancelButtonText: "取消", type: "warning" }
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
cleanLoading.value = true;
|
||||
try {
|
||||
const { data } = await http.delete("/operation-logs/clean", {
|
||||
data: { before_date: cleanBeforeDate.value.toISOString() },
|
||||
});
|
||||
ElMessage.success(data.message || "清理成功");
|
||||
cleanDlgVisible.value = false;
|
||||
crudExpose.doRefresh();
|
||||
} catch (error) {
|
||||
ElMessage.error("清理失败");
|
||||
} finally {
|
||||
cleanLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
crudExpose.doRefresh();
|
||||
});
|
||||
</script>
|
||||
124
frontend/src/modules/admin/views/logs/login-log-crud.ts
Normal file
124
frontend/src/modules/admin/views/logs/login-log-crud.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import type { CreateCrudOptions } from "@fast-crud/fast-crud";
|
||||
import { dict } from "@fast-crud/fast-crud";
|
||||
import dayjs from "dayjs";
|
||||
import { http } from "@/modules/admin/api/http";
|
||||
|
||||
export type LoginLogRow = {
|
||||
id: number;
|
||||
username: string;
|
||||
login_time: string;
|
||||
ip_address: string;
|
||||
user_agent: string;
|
||||
status: string;
|
||||
failure_reason: string | null;
|
||||
};
|
||||
|
||||
export function buildLoginLogCrudOptions(opts: {
|
||||
onClean: () => void;
|
||||
}): CreateCrudOptions<LoginLogRow> {
|
||||
return () => ({
|
||||
crudOptions: {
|
||||
pagination: { show: false },
|
||||
search: {
|
||||
show: true,
|
||||
options: { layout: "inline" },
|
||||
expand: false,
|
||||
},
|
||||
request: {
|
||||
pageRequest: async (query) => {
|
||||
const { data } = await http.post<LoginLogRow[]>(
|
||||
"/login-logs/search",
|
||||
query
|
||||
);
|
||||
return data;
|
||||
},
|
||||
transformQuery: ({ form }) => {
|
||||
const params: Record<string, unknown> = {};
|
||||
if (form?.username) params.username = form.username;
|
||||
if (form?.status) params.status = form.status;
|
||||
return params;
|
||||
},
|
||||
transformRes: ({ res }) => {
|
||||
const records = Array.isArray(res) ? res : [];
|
||||
return {
|
||||
currentPage: 1,
|
||||
pageSize: Math.max(records.length, 1),
|
||||
total: records.length,
|
||||
records,
|
||||
};
|
||||
},
|
||||
},
|
||||
rowHandle: { show: false },
|
||||
toolbar: {
|
||||
buttons: {
|
||||
clean: {
|
||||
text: "清理旧日志",
|
||||
type: "warning",
|
||||
click: () => opts.onClean(),
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
id: {
|
||||
title: "ID",
|
||||
key: "id",
|
||||
type: "number",
|
||||
column: { width: 70 },
|
||||
form: { show: false },
|
||||
},
|
||||
username: {
|
||||
title: "用户名",
|
||||
key: "username",
|
||||
type: "text",
|
||||
search: { show: true },
|
||||
},
|
||||
status: {
|
||||
title: "状态",
|
||||
key: "status",
|
||||
type: "dict-select",
|
||||
search: { show: true },
|
||||
dict: dict({
|
||||
data: [
|
||||
{ value: "success", label: "成功" },
|
||||
{ value: "failure", label: "失败" },
|
||||
],
|
||||
value: "value",
|
||||
label: "label",
|
||||
}),
|
||||
column: { width: 80 },
|
||||
},
|
||||
failure_reason: {
|
||||
title: "失败原因",
|
||||
key: "failure_reason",
|
||||
type: "text",
|
||||
column: { width: 160 },
|
||||
},
|
||||
ip_address: {
|
||||
title: "IP地址",
|
||||
key: "ip_address",
|
||||
type: "text",
|
||||
column: { width: 150 },
|
||||
},
|
||||
user_agent: {
|
||||
title: "User-Agent",
|
||||
key: "user_agent",
|
||||
type: "text",
|
||||
column: { width: 300 },
|
||||
},
|
||||
login_time: {
|
||||
title: "登录时间",
|
||||
key: "login_time",
|
||||
type: "text",
|
||||
column: { width: 170 },
|
||||
form: { show: false },
|
||||
valueBuilder({ value, row, key }) {
|
||||
if (!row || value == null) return;
|
||||
(row as Record<string, unknown>)[key as string] = dayjs(
|
||||
value
|
||||
).format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
168
frontend/src/modules/admin/views/logs/operation-log-crud.ts
Normal file
168
frontend/src/modules/admin/views/logs/operation-log-crud.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import type { CreateCrudOptions } from "@fast-crud/fast-crud";
|
||||
import { dict } from "@fast-crud/fast-crud";
|
||||
import dayjs from "dayjs";
|
||||
import { http } from "@/modules/admin/api/http";
|
||||
|
||||
export type OperationLogRow = {
|
||||
id: number;
|
||||
user_id: number;
|
||||
username: string;
|
||||
action_type: string;
|
||||
target_type: string;
|
||||
target_id: number | null;
|
||||
target_name: string | null;
|
||||
detail: string | null;
|
||||
ip_address: string;
|
||||
user_agent: string;
|
||||
createtime: string;
|
||||
};
|
||||
|
||||
export function buildOperationLogCrudOptions(opts: {
|
||||
onClean: () => void;
|
||||
}): CreateCrudOptions<OperationLogRow> {
|
||||
return () => ({
|
||||
crudOptions: {
|
||||
pagination: { show: false },
|
||||
search: {
|
||||
show: true,
|
||||
options: { layout: "inline" },
|
||||
expand: false,
|
||||
},
|
||||
request: {
|
||||
pageRequest: async (query) => {
|
||||
const { data } = await http.post<OperationLogRow[]>(
|
||||
"/operation-logs/search",
|
||||
query
|
||||
);
|
||||
return data;
|
||||
},
|
||||
transformQuery: ({ form }) => {
|
||||
const params: Record<string, unknown> = {};
|
||||
if (form?.username) params.username = form.username;
|
||||
if (form?.action_type) params.action_type = form.action_type;
|
||||
if (form?.target_type) params.target_type = form.target_type;
|
||||
return params;
|
||||
},
|
||||
transformRes: ({ res }) => {
|
||||
const records = Array.isArray(res) ? res : [];
|
||||
return {
|
||||
currentPage: 1,
|
||||
pageSize: Math.max(records.length, 1),
|
||||
total: records.length,
|
||||
records,
|
||||
};
|
||||
},
|
||||
},
|
||||
rowHandle: { show: false },
|
||||
toolbar: {
|
||||
buttons: {
|
||||
clean: {
|
||||
text: "清理旧日志",
|
||||
type: "warning",
|
||||
click: () => opts.onClean(),
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
id: {
|
||||
title: "ID",
|
||||
key: "id",
|
||||
type: "number",
|
||||
column: { width: 70 },
|
||||
form: { show: false },
|
||||
},
|
||||
user_id: {
|
||||
title: "用户ID",
|
||||
key: "user_id",
|
||||
type: "number",
|
||||
column: { width: 80 },
|
||||
form: { show: false },
|
||||
},
|
||||
username: {
|
||||
title: "操作人",
|
||||
key: "username",
|
||||
type: "text",
|
||||
search: { show: true },
|
||||
},
|
||||
action_type: {
|
||||
title: "操作类型",
|
||||
key: "action_type",
|
||||
type: "dict-select",
|
||||
search: { show: true },
|
||||
dict: dict({
|
||||
data: [
|
||||
{ value: "create", label: "创建" },
|
||||
{ value: "update", label: "更新" },
|
||||
{ value: "delete", label: "删除" },
|
||||
],
|
||||
value: "value",
|
||||
label: "label",
|
||||
}),
|
||||
column: { width: 90 },
|
||||
},
|
||||
target_type: {
|
||||
title: "目标类型",
|
||||
key: "target_type",
|
||||
type: "dict-select",
|
||||
search: { show: true },
|
||||
dict: dict({
|
||||
data: [
|
||||
{ value: "user", label: "用户" },
|
||||
{ value: "role", label: "角色" },
|
||||
{ value: "permission", label: "权限" },
|
||||
{ value: "setting", label: "系统设置" },
|
||||
],
|
||||
value: "value",
|
||||
label: "label",
|
||||
}),
|
||||
column: { width: 100 },
|
||||
},
|
||||
target_name: {
|
||||
title: "目标名称",
|
||||
key: "target_name",
|
||||
type: "text",
|
||||
column: { width: 140 },
|
||||
},
|
||||
target_id: {
|
||||
title: "目标ID",
|
||||
key: "target_id",
|
||||
type: "number",
|
||||
column: { width: 80 },
|
||||
form: { show: false },
|
||||
},
|
||||
detail: {
|
||||
title: "变更详情",
|
||||
key: "detail",
|
||||
type: "text",
|
||||
column: { width: 200 },
|
||||
form: { show: false },
|
||||
},
|
||||
ip_address: {
|
||||
title: "IP地址",
|
||||
key: "ip_address",
|
||||
type: "text",
|
||||
column: { width: 150 },
|
||||
},
|
||||
user_agent: {
|
||||
title: "User-Agent",
|
||||
key: "user_agent",
|
||||
type: "text",
|
||||
column: { width: 300 },
|
||||
},
|
||||
createtime: {
|
||||
title: "操作时间",
|
||||
key: "createtime",
|
||||
type: "text",
|
||||
column: { width: 170 },
|
||||
form: { show: false },
|
||||
valueBuilder({ value, row, key }) {
|
||||
if (!row || value == null) return;
|
||||
(row as Record<string, unknown>)[key as string] = dayjs(
|
||||
value
|
||||
).format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -9,6 +9,7 @@ export type PermissionRow = {
|
||||
name: string;
|
||||
code: string;
|
||||
parentid: number;
|
||||
route?: string | null;
|
||||
description?: string | null;
|
||||
createtime: string;
|
||||
updatetime: string;
|
||||
@ -134,6 +135,15 @@ export const createPermissionCrudOptions: CreateCrudOptions<PermissionRow> = ()
|
||||
type: "textarea",
|
||||
column: { show: false },
|
||||
},
|
||||
route: {
|
||||
title: "路由",
|
||||
key: "route",
|
||||
type: "text",
|
||||
column: { width: 200 },
|
||||
form: {
|
||||
placeholder: "如:/admin/users",
|
||||
},
|
||||
},
|
||||
createtime: {
|
||||
title: "创建时间",
|
||||
key: "createtime",
|
||||
|
||||
@ -52,6 +52,20 @@ const router = createRouter({
|
||||
component: () =>
|
||||
import("@/modules/admin/views/settings/SettingManage.vue"),
|
||||
},
|
||||
{
|
||||
path: "login-logs",
|
||||
name: "admin-login-logs",
|
||||
meta: { title: "登录日志", permission: "log:" },
|
||||
component: () =>
|
||||
import("@/modules/admin/views/logs/LoginLogManage.vue"),
|
||||
},
|
||||
{
|
||||
path: "operation-logs",
|
||||
name: "admin-operation-logs",
|
||||
meta: { title: "操作日志", permission: "log:" },
|
||||
component: () =>
|
||||
import("@/modules/admin/views/logs/OperationLogManage.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user