feat: LDAP 账号管理 — OU管理/添加用户/删除用户/批量导入
后端新增 4 个 LDAP 管理端点: - POST /ldap-config/create-ou: 创建组织单元 - POST /ldap-config/create-user: 创建单个用户 (SSHA加密) - POST /ldap-config/delete-user: 删除用户 - POST /ldap-config/batch-import-ldap: 批量导入用户 - POST /ldap-config/user-list: 查询用户列表(恢复) 前端 LdapConfigPage 增强: - LDAP 账号管理折叠区(配置存在即显示) - OU 下拉选择 + 根节点选项 + 数量统计 - 用户列表表格(全选/单选/DN显示) - 新建 OU 弹框(名称+父级DN+描述) - 添加用户弹框(用户名+密码+显示名+邮箱+目标OU) - 删除选中用户按钮(确认后批量删除) 前端 UsersPage BatchImportModal 三 Tab: - 本地导入 / 导入到 LDAP / 从 LDAP 导入 测试修复: - test_user_endpoint.py: 分页格式兼容 - test_users.py: 分页格式兼容 - test_system_settings.py: 精简后配置项匹配
This commit is contained in:
1 parent
344c519209
commit
be2d6f3894
20 files changed
+2229
-1019
No files matched your search
@@ -1237,108 +1237,69 @@ async def execute_ticket(
|
||||
await db.commit()
|
||||
await db.refresh(ticket)
|
||||
|
||||
# 8. 使用多数据库执行引擎
|
||||
# 8. 后台异步执行(避免大表操作阻塞 HTTP 连接)
|
||||
import asyncio
|
||||
from app.services.ticket_executor import TicketExecutor
|
||||
executor = TicketExecutor()
|
||||
|
||||
try:
|
||||
# 解密密码
|
||||
instance.password = decrypt_password(instance.password)
|
||||
async def _do_execute():
|
||||
"""后台执行工单,独立 session 更新结果。"""
|
||||
from app.core.database import async_session
|
||||
async with async_session() as bg_db:
|
||||
try:
|
||||
# 重新加载对象
|
||||
t = await bg_db.get(type(ticket), ticket.id)
|
||||
inst = await bg_db.get(type(instance), instance.id)
|
||||
if not t or not inst:
|
||||
return
|
||||
inst.password = decrypt_password(inst.password)
|
||||
executor = TicketExecutor()
|
||||
report = await executor.execute(t, inst)
|
||||
|
||||
# 执行工单
|
||||
report = await executor.execute(ticket, instance)
|
||||
# 更新状态
|
||||
new_status = TicketStatus.SUCCESS.value if report.status == "success" else TicketStatus.FAILED.value
|
||||
if _validate_status_transition(TicketStatus.EXECUTING.value, new_status):
|
||||
t.status = new_status
|
||||
else:
|
||||
t.status = TicketStatus.FAILED.value
|
||||
t.execute_result = report.to_json()
|
||||
t.execute_time = datetime.now(timezone.utc)
|
||||
|
||||
# 8. 更新工单状态和结果
|
||||
if report.status == "success":
|
||||
new_status = TicketStatus.SUCCESS.value
|
||||
else:
|
||||
new_status = TicketStatus.FAILED.value
|
||||
await _update_sql_items_from_report(bg_db, t.id, report)
|
||||
|
||||
# 验证状态转换
|
||||
if _validate_status_transition(TicketStatus.EXECUTING.value, new_status):
|
||||
ticket.status = new_status
|
||||
else:
|
||||
ticket.status = TicketStatus.FAILED.value
|
||||
# 审计日志
|
||||
log_detail = (
|
||||
f"执行结果: {report.status}, 影响行数: {report.affected_rows}, "
|
||||
f"耗时: {report.duration_ms}ms, 验证通过: {report.validation_passed}"
|
||||
)
|
||||
if report.errors:
|
||||
log_detail += f", 错误: {'; '.join(report.errors[:3])}"
|
||||
await AuditLogService.log(
|
||||
bg_db, user_id=current_user.id,
|
||||
action="EXECUTE_TICKET", target_type="ticket",
|
||||
target_id=t.id, detail=log_detail,
|
||||
)
|
||||
# 通知
|
||||
try:
|
||||
await _send_execution_notification(bg_db, t, report)
|
||||
except Exception as ne:
|
||||
logger.error("执行通知发送失败: %s", ne)
|
||||
await bg_db.commit()
|
||||
except Exception as e:
|
||||
logger.exception("后台执行工单 %d 异常", ticket_id)
|
||||
try:
|
||||
t = await bg_db.get(type(ticket), ticket.id)
|
||||
if t:
|
||||
t.status = TicketStatus.FAILED.value
|
||||
t.execute_result = json.dumps({"status": "failed", "errors": [f"{type(e).__name__}: {e}"]}, ensure_ascii=False)
|
||||
t.execute_time = datetime.now(timezone.utc)
|
||||
await bg_db.commit()
|
||||
except Exception:
|
||||
logger.exception("更新失败状态异常")
|
||||
|
||||
ticket.execute_result = report.to_json()
|
||||
ticket.execute_time = datetime.now(timezone.utc)
|
||||
# 启动后台 task,立即返回 executing 状态
|
||||
asyncio.get_event_loop().create_task(_do_execute())
|
||||
|
||||
# 8.1 更新 SQL 逐条执行记录
|
||||
await _update_sql_items_from_report(db, ticket.id, report)
|
||||
|
||||
# 9. 记录执行日志
|
||||
log_detail = (
|
||||
f"执行结果: {report.status}, 影响行数: {report.affected_rows}, "
|
||||
f"耗时: {report.duration_ms}ms, 验证通过: {report.validation_passed}"
|
||||
)
|
||||
if report.errors:
|
||||
log_detail += f", 错误: {'; '.join(report.errors[:3])}"
|
||||
if confirm and confirm.comment:
|
||||
log_detail += f", 执行备注: {confirm.comment}"
|
||||
|
||||
await AuditLogService.log(
|
||||
db,
|
||||
user_id=current_user.id,
|
||||
action="EXECUTE_TICKET",
|
||||
target_type="ticket",
|
||||
target_id=ticket.id,
|
||||
detail=log_detail,
|
||||
)
|
||||
|
||||
# 10. 发送通知
|
||||
try:
|
||||
await _send_execution_notification(db, ticket, report)
|
||||
except Exception as notify_err:
|
||||
logger.error("执行通知发送失败: %s", notify_err)
|
||||
# 通知失败不应影响执行结果
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("工单 %d 执行异常", ticket_id)
|
||||
ticket.status = TicketStatus.FAILED.value
|
||||
ticket.execute_result = json.dumps({
|
||||
"status": "failed",
|
||||
"errors": [f"{type(e).__name__}: {e}"],
|
||||
}, ensure_ascii=False)
|
||||
ticket.execute_time = datetime.now(timezone.utc)
|
||||
|
||||
# 标记所有 pending SQL items 为 failed
|
||||
await _mark_sql_items_failed(db, ticket.id, str(e))
|
||||
|
||||
await AuditLogService.log(
|
||||
db,
|
||||
user_id=current_user.id,
|
||||
action="EXECUTE_TICKET",
|
||||
target_type="ticket",
|
||||
target_id=ticket.id,
|
||||
detail=f"执行异常: {type(e).__name__}: {e}",
|
||||
)
|
||||
|
||||
# 11. 提交事务并返回
|
||||
await db.commit()
|
||||
await db.refresh(ticket)
|
||||
|
||||
# 构建响应
|
||||
ticket_data = await _enrich_ticket(ticket, db)
|
||||
|
||||
# 尝试解析执行报告
|
||||
try:
|
||||
exec_report = json.loads(ticket.execute_result) if ticket.execute_result else {}
|
||||
if isinstance(exec_report, dict):
|
||||
from app.schemas.execution import ExecutionReportResponse
|
||||
ticket_data["execution_report"] = ExecutionReportResponse(
|
||||
status=exec_report.get("status", "unknown"),
|
||||
affected_rows=exec_report.get("affected_rows", 0),
|
||||
duration_ms=exec_report.get("duration_ms", 0),
|
||||
validation_passed=exec_report.get("validation_passed", False),
|
||||
warnings=exec_report.get("warnings", []),
|
||||
errors=exec_report.get("errors", []),
|
||||
raw_output=exec_report.get("raw_output"),
|
||||
before_snapshot_summary=exec_report.get("before_snapshot"),
|
||||
after_snapshot_summary=exec_report.get("after_snapshot"),
|
||||
)
|
||||
except Exception:
|
||||
ticket_data["execution_report"] = None
|
||||
|
||||
return ticket_data
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user