Browse Source

430提交

Lan 2 months ago
parent
commit
d17ec90d7c
81 changed files with 1424 additions and 247 deletions
  1. BIN
      backend/api/__pycache__/__init__.cpython-310.pyc
  2. BIN
      backend/api/__pycache__/admin.cpython-310.pyc
  3. BIN
      backend/api/__pycache__/api_alert.cpython-310.pyc
  4. BIN
      backend/api/__pycache__/api_calculate.cpython-310.pyc
  5. BIN
      backend/api/__pycache__/api_graph.cpython-310.pyc
  6. BIN
      backend/api/__pycache__/api_prepare.cpython-310.pyc
  7. BIN
      backend/api/__pycache__/api_rawDataTrans.cpython-310.pyc
  8. BIN
      backend/api/__pycache__/api_results.cpython-310.pyc
  9. BIN
      backend/api/__pycache__/api_system.cpython-310.pyc
  10. BIN
      backend/api/__pycache__/api_user.cpython-310.pyc
  11. BIN
      backend/api/__pycache__/apps.cpython-310.pyc
  12. BIN
      backend/api/__pycache__/scheduler.cpython-310.pyc
  13. BIN
      backend/api/__pycache__/serializers.cpython-310.pyc
  14. BIN
      backend/api/__pycache__/tokenAuthentication.cpython-310.pyc
  15. BIN
      backend/api/__pycache__/urls.cpython-310.pyc
  16. BIN
      backend/api/__pycache__/utils.cpython-310.pyc
  17. 90 0
      backend/api/api_alert.py
  18. 167 55
      backend/api/api_calculate.py
  19. 89 54
      backend/api/api_rawDataTrans.py
  20. 33 0
      backend/api/api_system.py
  21. 6 0
      backend/api/apps.py
  22. 86 0
      backend/api/migrations/0020_alert_systemperformance.py
  23. 18 0
      backend/api/migrations/0021_rename_indicator_alert_metric.py
  24. BIN
      backend/api/migrations/__pycache__/0001_initial.cpython-310.pyc
  25. BIN
      backend/api/migrations/__pycache__/0002_alter_user_options_user_last_login.cpython-310.pyc
  26. BIN
      backend/api/migrations/__pycache__/0003_view_file.cpython-310.pyc
  27. BIN
      backend/api/migrations/__pycache__/0004_rename_display_name_user_displayname_file_usage.cpython-310.pyc
  28. BIN
      backend/api/migrations/__pycache__/0005_file_associate_file_content.cpython-310.pyc
  29. BIN
      backend/api/migrations/__pycache__/0006_alter_file_associate.cpython-310.pyc
  30. BIN
      backend/api/migrations/__pycache__/0007_fileinfo.cpython-310.pyc
  31. BIN
      backend/api/migrations/__pycache__/0008_mission_result.cpython-310.pyc
  32. BIN
      backend/api/migrations/__pycache__/0009_alter_fileinfo_file_alter_mission_name.cpython-310.pyc
  33. BIN
      backend/api/migrations/__pycache__/0010_algorithm_plan.cpython-310.pyc
  34. BIN
      backend/api/migrations/__pycache__/0011_result_plan_result_state.cpython-310.pyc
  35. BIN
      backend/api/migrations/__pycache__/0012_result_edgefile_result_nodefile_alter_result_plan.cpython-310.pyc
  36. BIN
      backend/api/migrations/__pycache__/0013_remove_result_state_alter_file_usage.cpython-310.pyc
  37. BIN
      backend/api/migrations/__pycache__/0014_result_progress.cpython-310.pyc
  38. BIN
      backend/api/migrations/__pycache__/0015_mission_state.cpython-310.pyc
  39. BIN
      backend/api/migrations/__pycache__/0016_alter_result_edgefile_alter_result_nodefile.cpython-310.pyc
  40. BIN
      backend/api/migrations/__pycache__/0017_graph_graphtoken.cpython-310.pyc
  41. BIN
      backend/api/migrations/__pycache__/0018_rename_edgemap_graph_edges_and_more.cpython-310.pyc
  42. BIN
      backend/api/migrations/__pycache__/0019_alter_graph_user.cpython-310.pyc
  43. BIN
      backend/api/migrations/__pycache__/0020_alert_systemperformance.cpython-310.pyc
  44. BIN
      backend/api/migrations/__pycache__/0021_rename_indicator_alert_metric.cpython-310.pyc
  45. BIN
      backend/api/migrations/__pycache__/__init__.cpython-310.pyc
  46. 3 1
      backend/api/models/__init__.py
  47. BIN
      backend/api/models/__pycache__/__init__.cpython-310.pyc
  48. BIN
      backend/api/models/__pycache__/alert.cpython-310.pyc
  49. BIN
      backend/api/models/__pycache__/algorithm.cpython-310.pyc
  50. BIN
      backend/api/models/__pycache__/file.cpython-310.pyc
  51. BIN
      backend/api/models/__pycache__/graph.cpython-310.pyc
  52. BIN
      backend/api/models/__pycache__/mission.cpython-310.pyc
  53. BIN
      backend/api/models/__pycache__/plan.cpython-310.pyc
  54. BIN
      backend/api/models/__pycache__/result.cpython-310.pyc
  55. BIN
      backend/api/models/__pycache__/system.cpython-310.pyc
  56. BIN
      backend/api/models/__pycache__/user.cpython-310.pyc
  57. BIN
      backend/api/models/__pycache__/view.cpython-310.pyc
  58. 72 0
      backend/api/models/alert.py
  59. 1 1
      backend/api/models/file.py
  60. 20 0
      backend/api/models/system.py
  61. 68 0
      backend/api/scheduler.py
  62. 5 0
      backend/api/urls.py
  63. 27 1
      backend/api/utils.py
  64. BIN
      backend/backend/__pycache__/__init__.cpython-310.pyc
  65. BIN
      backend/backend/__pycache__/settings.cpython-310.pyc
  66. BIN
      backend/backend/__pycache__/urls.cpython-310.pyc
  67. BIN
      backend/backend/__pycache__/wsgi.cpython-310.pyc
  68. 60 0
      backend/backend/settings.py
  69. BIN
      backend/db.sqlite3
  70. BIN
      scheduler/__pycache__/processManager.cpython-310.pyc
  71. BIN
      scheduler/__pycache__/utils.cpython-310.pyc
  72. 4 4
      scheduler/algo1Folder/controller.py
  73. 116 21
      scheduler/processManager.py
  74. 9 3
      scheduler/scheduler.py
  75. 19 6
      scheduler/utils.py
  76. 3 2
      viewer/src/api/axios.js
  77. 4 0
      viewer/src/store/userInfo.js
  78. 2 3
      viewer/src/views/dashoard/analyze.vue
  79. 20 13
      viewer/src/views/dashoard/calculate.vue
  80. 9 1
      viewer/src/views/dashoard/dashboard.vue
  81. 493 82
      viewer/src/views/dashoard/monitor.vue

BIN
backend/api/__pycache__/__init__.cpython-310.pyc


BIN
backend/api/__pycache__/admin.cpython-310.pyc


BIN
backend/api/__pycache__/api_alert.cpython-310.pyc


BIN
backend/api/__pycache__/api_calculate.cpython-310.pyc


BIN
backend/api/__pycache__/api_graph.cpython-310.pyc


BIN
backend/api/__pycache__/api_prepare.cpython-310.pyc


BIN
backend/api/__pycache__/api_rawDataTrans.cpython-310.pyc


BIN
backend/api/__pycache__/api_results.cpython-310.pyc


BIN
backend/api/__pycache__/api_system.cpython-310.pyc


BIN
backend/api/__pycache__/api_user.cpython-310.pyc


BIN
backend/api/__pycache__/apps.cpython-310.pyc


BIN
backend/api/__pycache__/scheduler.cpython-310.pyc


BIN
backend/api/__pycache__/serializers.cpython-310.pyc


BIN
backend/api/__pycache__/tokenAuthentication.cpython-310.pyc


BIN
backend/api/__pycache__/urls.cpython-310.pyc


BIN
backend/api/__pycache__/utils.cpython-310.pyc


+ 90 - 0
backend/api/api_alert.py

@@ -0,0 +1,90 @@
+from django.contrib import auth
+from rest_framework.views import APIView
+from django.core.exceptions import ValidationError
+
+from api.utils import *
+from api.models import Alert
+import requests
+
+import json
+
+class AlertAPI(APIView):
+    # 获取告警列表
+    def get(self, request):
+        if not request.user.identity == 'admin':
+            return failed(message="仅允许管理员访问")
+        alerts = []
+        for alert in Alert.objects.all():
+            alerts.append({
+                'name': alert.name,
+                'level': alert.level,
+                'metric': alert.metric,
+                'threshold': alert.threshold,
+                'handle': alert.handle,
+            })
+        return success(data=alerts)
+    
+    def post(self, request):
+        if not request.user.identity == 'admin':
+            return failed(message="仅允许管理员访问")
+        operation = request.data.get('operation')
+        # 新建告警
+        if operation == 'create':
+            data = request.data
+            try:
+                alert = Alert.objects.create(
+                    name=data.get('name'),
+                    level=data.get('level'),
+                    metric=data.get('metric'),
+                    threshold=float(data.get('threshold')),
+                    handle=data.get('handle'),
+                    state='enable'  # 目前创建的告警都将持续生效
+                )
+                # 创建成功返回新建告警的ID
+                return success(message="告警规则创建成功", data={'id': alert.id})
+            except ValidationError as error:
+                return failed(message=f"参数验证失败: {error}")
+                
+            except Exception as error:
+                # 处理其他异常(如 threshold 转换失败)
+                return failed(message=f"创建失败: {str(error)}")
+        
+        # 删除告警
+        if operation == 'delete':
+            name = request.data.get('name')
+            if not name:
+                return failed(message="删除告警时未传递ID信息")
+            try:
+                alert = Alert.objects.get(name=str(name))
+            except Alert.DoesNotExist:
+                return failed(message=f"未找到名称为{name}的告警")
+            alert.delete()
+            return success(message="告警规则删除成功", data={'id': alert.id})
+        
+        # 开关告警
+        if operation == 'switch':
+            id = request.data.get('id')
+            state = request.data.get('state')
+            if not state in ['enable', 'disable']:
+                return failed(message=f"传递状态值:{state}不被允许")
+            if not id:
+                return failed(message="开关告警时未传递ID信息")
+            try:
+                alert = Alert.objects.get(id=int(id))
+            except Alert.DoesNotExist:
+                return failed(message=f"未找到ID为{id}的告警")
+            alert.state = state
+            alert.save()
+            return success(message="告警规则切换开关成功", data={'id': alert.id})
+
+
+class AlertCheck(APIView):
+    # 检查是否有触发的告警
+    def get(self, request):
+        if not request.user.identity == 'admin':
+            return failed(message="仅允许管理员访问")
+        triggeredAlerts = [alert.id for alert in Alert.objects.checkAlert()]
+        # 返回的仅为触发告警的ID
+        return success(data=triggeredAlerts)
+    
+            

+ 167 - 55
backend/api/api_calculate.py

@@ -1,30 +1,32 @@
 from django.contrib import auth
 from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.authtoken.models import Token
-from rest_framework.authentication import BasicAuthentication, TokenAuthentication
-from .serializers import UserRegisterSerializer
-
-from django.middleware.csrf import get_token
-from django.contrib.auth import login
 
 from api.utils import *
 from api.models import File, Mission
 import requests
-
+import logging
 import json
 
+logger = logging.getLogger('calculate')
+
 class CalculateAPI(APIView):
   def post(self, request):
     user = request.user
     try:
-      mission = Mission.objects.get(id=request.data.get('mission'))
+      if request.data.get('mission'):
+        missionId = request.data.get('mission')
+      elif request.data.get('missionId'):
+        missionId = request.data.get('missionId')
+      mission = Mission.objects.get(id=int(missionId))
     except Mission.DoesNotExist:
       return failed(message="处理任务控制失败,未找到处理任务")
     except Exception as error:
       print("处理任务控制失败", error)
       return failed(message="处理任务控制失败,未找到处理任务")
+    # 检测用户权限
+    if not mission.user == user and not user.identity == 'admin':
+      logger.error(f"未授权用户{user.username}尝试执行任务{mission.name}:{mission.id}的计算操作")
+      return failed(message="处理任务控制失败,用户没有操作权限")
     
     command = request.data.get('command')
     try:
@@ -33,6 +35,7 @@ class CalculateAPI(APIView):
       print("处理任务控制代码错误")
       return failed(message="处理任务控制失败,控制代码错误")
 
+    # 进行状态检查
     if command == 'start':
       # 如任务已经启动,则不操作
       if not mission.state in ['init', 'pause']:
@@ -46,50 +49,159 @@ class CalculateAPI(APIView):
     # 向调度程序提交计算任务
     # mission = request.json['mission']
     # plans = request.json['plans']
-    calculateData = {
-      'mission': {
-        'id': mission.id,
-      },
-      'plans': []
-    }
-    rootPlan = mission.own_plans.get(parent=None)
-    calculateData['plans'].append({
-      'id': rootPlan.id,
-      'nodes': mission.nodeFile.toJson(),
-      'edges': mission.edgeFile.toJson(),
-      'children': list(mission.own_plans.filter(parent=rootPlan).values_list('id', flat=True)),
-    })
+    
+    # 根据控制指令不同,执行不同操作
+    # 启动计算任务,构造通用数据结构
+    if command == 'start':
+      calculateData = {
+        'mission': {
+          'id': mission.id,
+        },
+        'plans': []
+      }
+      rootPlan = mission.own_plans.get(parent=None)
 
-    rootPlans = [ child for child in mission.own_plans.filter(parent=rootPlan)]
-    while rootPlans:
-      tempPlans = rootPlans.copy()
-      rootPlans = []
-      for p in tempPlans:
-        children = [ child for child in mission.own_plans.filter(parent=p)]
-        # 判断是否父节点存在计算结果,有则作为子节点输入
-        if hasattr(p.parent, 'own_result'):
-          calculateData['plans'].append({
-            'id': p.id,
-            'algorithm': p.algorithm.name,
-            'nodes': p.parent.own_result.nodeFile.toJson(),
-            'edges': p.parent.own_result.edgeFile.toJson(),
-            'children': [child.id for child in children],
-          })
+      # 如果是恢复历史任务,则需要进行特殊处理未下一步运行提供所有plan的原料数据,因此此时可能scheduler已经终止过,过程数据已丢失
+      # 如果是从初始状态或停止状态启动
+      if mission.state == 'pause':
+        # 如果是暂停中恢复,需要找出当前停在哪里,将父节点的结果作为原料输入
+        # 找出最新plan
+        lastPlans = [child for child in mission.own_plans.filter(parent=rootPlan).all()]
+        latestPlans = []
+        while lastPlans:
+          currentPlan = lastPlans.pop()
+          # 检查当前currentPlan是否已经计算完毕,如果已经计算完毕则继续找它的子节点,直到找到未计算完的
+          # 注意可能当前plan的result已经被删除
+          if hasattr(currentPlan, 'own_result') and currentPlan.own_result.progress == 100:
+            # 计算完毕,将子节点加入寻找列表
+            lastPlans.extend([child for child in mission.own_plans.filter(parent=currentPlan).all()])
+          else:
+            # 没有计算完毕,从这里恢复,需要用父节点结果作为自己的输入
+            parentPlan = currentPlan.parent
+            # 判断是否父节点是根节点,是则用mission的数据作为输入
+            if parentPlan.parent == None:
+              nodesJson = mission.nodeFile.toJson()
+              edgesJson = mission.edgeFile.toJson()
+            else:
+              nodesJson = parentPlan.own_result.nodeFile.toJson()
+              edgesJson = parentPlan.own_result.edgeFile.toJson()
+            latestPlans.append(currentPlan)
+            calculateData['plans'].append({
+              'id': currentPlan.id,
+              'algorithm': currentPlan.algorithm.name,
+              'nodes': nodesJson,
+              'edges': edgesJson,
+              'children': list(mission.own_plans.filter(parent=currentPlan).values_list('id', flat=True)),
+              # 新式控制方法,检测到带有root字段,则为初始节点
+              'root': True,
+            })
+        # 开始弹栈,将所有后续子节点加入
+        while latestPlans:
+          currentPlan = latestPlans.pop()
+          for child in mission.own_plans.filter(parent=currentPlan).all():
+            latestPlans.append(child)
+            # 子节点既没有初始数据也没有root标记
+            calculateData['plans'].append({
+              'id': child.id,
+              'algorithm': child.algorithm.name,
+              'nodes': None,
+              'edges': None,
+              'children': list(mission.own_plans.filter(parent=child).values_list('id', flat=True)),
+            })
+          
+        response = requests.post(SCHEDULER_BASE_URL + '/resumeMission', json=calculateData)
+        print(response.json())
+        if response.json()['code'] == 'OK':
+          # 更新mission的运行状态
+          mission.state = 'calculating'
+          mission.save()
+          return success(message="恢复计算任务成功")
         else:
-          calculateData['plans'].append({
-            'id': p.id,
-            'algorithm': p.algorithm.name,
-            'nodes': None,
-            'edges': None,
-            'children': [child.id for child in children],
-          })
-        rootPlans.extend(children)
-    response = requests.post(SCHEDULER_BASE_URL + '/addMission', json=calculateData)
-    print(response.json())
-    if response.json()['code'] == 'OK':
-      # 更新mission的运行状态
-      mission.state = 'calculating'
-      mission.save()
-      return success(message="成功启动计算任务")
-    else:
-      return failed(message="启动计算任务失败")
+          return failed(message="恢复计算任务失败") 
+      # 如果不是恢复计算任务,则正常计算
+      calculateData['plans'].append({
+        'id': rootPlan.id,
+        'nodes': mission.nodeFile.toJson(),
+        'edges': mission.edgeFile.toJson(),
+        'children': list(mission.own_plans.filter(parent=rootPlan).values_list('id', flat=True)),
+      })
+
+      rootPlans = [ child for child in mission.own_plans.filter(parent=rootPlan)]
+      while rootPlans:
+        tempPlans = rootPlans.copy()
+        rootPlans = []
+        for p in tempPlans:
+          children = [ child for child in mission.own_plans.filter(parent=p)]
+          # 判断是否父节点存在计算结果,有则作为子节点输入
+          if hasattr(p.parent, 'own_result') and p.parent.own_result.nodeFile and p.parent.own_result.edgeFile:
+            calculateData['plans'].append({
+              'id': p.id,
+              'algorithm': p.algorithm.name,
+              'nodes': p.parent.own_result.nodeFile.toJson(),
+              'edges': p.parent.own_result.edgeFile.toJson(),
+              'children': [child.id for child in children],
+            })
+          else:
+            calculateData['plans'].append({
+              'id': p.id,
+              'algorithm': p.algorithm.name,
+              'nodes': None,
+              'edges': None,
+              'children': [child.id for child in children],
+            })
+          rootPlans.extend(children)
+      response = requests.post(SCHEDULER_BASE_URL + '/addMission', json=calculateData)
+      print(response.json())
+      if response.json()['code'] == 'OK':
+        # 更新mission的运行状态
+        mission.state = 'calculating'
+        mission.save()
+        return success(message="启动计算任务成功")
+      else:
+        return failed(message="启动计算任务失败")
+      
+    # 暂停计算
+    if command == 'pause':
+      # 暂停任务时仅需要传递mission的id
+      calculateData = {
+        'mission': {
+          'id': mission.id,
+        }
+      }
+      response = requests.post(SCHEDULER_BASE_URL + '/pauseMission', json=calculateData)
+      if response.json()['code'] == 'OK':
+        mission.state = 'pause'
+        mission.save()
+        # 暂停后,所有当前未完成任务全部变为初始状态,删除其result
+        for plan in mission.own_plans.all():
+          if hasattr(plan, 'own_result') and plan.own_result.progress != 100:
+            result = plan.own_result
+            result.delete()
+        return success(message="暂停计算任务成功")
+      else:
+        print(response)
+        return failed(message="暂停计算任务失败", data=response)
+      
+    # 停止计算
+    if command == 'stop':
+      # 停止任务时仅需要传递mission的id
+      calculateData = {
+        'mission': {
+          'id': mission.id,
+        }
+      }
+      response = requests.post(SCHEDULER_BASE_URL + '/stopMission', json=calculateData)
+      if response.json()['code'] == 'OK':
+        # 停止后所有任务相关数据删除,恢复初始状态
+        mission.state = 'init'
+        mission.save()
+        # 停止后,所有plan的进度需要全部归零,即删除所有result,或将result的进度改为-2
+        # 尝试删除所有result方案,下次运行时会重新创建result
+        for plan in mission.own_plans.all():
+          if hasattr(plan, 'own_result'):
+            result = plan.own_result
+            result.delete()
+        return success(message="暂停计算任务成功")
+      else:
+        print(response)
+        return failed(message="暂停计算任务失败", data=response)

+ 89 - 54
backend/api/api_rawDataTrans.py

@@ -5,7 +5,7 @@ from rest_framework import status
 from rest_framework.authtoken.models import Token
 from rest_framework.authentication import BasicAuthentication, TokenAuthentication
 from .serializers import UserRegisterSerializer
-
+import logging
 from django.middleware.csrf import get_token
 from django.contrib.auth import login
 
@@ -17,6 +17,7 @@ import json, csv
 
 PLAN_FAILED = -1
 MISSION_FAILED = -2
+logger = logging.getLogger("process.manager.reporter")
 
 class RawDataTrans(APIView):
     authentication_classes = []
@@ -34,62 +35,96 @@ class RawDataTrans(APIView):
         
     def post(self, request):
         # 处理进程反馈计算结果
-        mission = Mission.objects.get(id=int(request.data.get('missionId')))
-        plan = Plan.objects.get(id=int(request.data.get('planId')))
-        nodes = request.data.get('nodes')
-        edges = request.data.get('edges')
-        progress = request.data.get('progress')
-        print(request.data)
-        print(mission, plan, progress)
-        for param in [mission, plan, progress]:
-            if param is None:
-                print("结果传递参数不足")
-                return failed(message="缺少结果参数")
-            
-        if int(progress) == 100:
-            if not nodes or not edges:
-                print("进度完成却没有返回结果数据")
-                return failed(message="缺少结果参数")
-        try:
-            result = plan.own_result
+        progress_result = request.data.get('result')
+        performance = request.data.get('performance')
+
+        for progress_data in progress_result:
+            mission = Mission.objects.get(id=int(progress_data['missionId']))
+            plan = Plan.objects.get(id=int(progress_data['planId']))
+            if 'nodes' in progress_data:
+                nodes = progress_data['nodes']
+            else:
+                nodes = None
+            if 'edges' in progress_data:
+                edges = progress_data['edges']
+            else:
+                edges = None
+            progress = progress_data['progress']
+            print(request.data)
+            print(mission, plan, progress)
+            for param in [mission, plan, progress]:
+                if param is None:
+                    print("结果传递参数不足")
+                    return failed(message="缺少结果参数")
+                
             if int(progress) == 100:
-                # 任务完成后需要保存结果文件
-                # 读取nodes和edges,生成结果文件
-                nodeFile = File(type='csv', usage='result', content='node', user=plan.user)
-                nodeFile.save()
-                if not nodeFile.generate(nodes) == OK:
-                    print("保存节点结果文件失败")
-                    return failed(message="保存节点结果文件失败")
-                edgeFile = File(type='csv', usage='result', content='edge', user=plan.user)
-                edgeFile.save()
-                if not edgeFile.generate(edges) == OK:
-                    print("保存边结果文件失败")
-                    return failed(message="保存边结果文件失败")
-                nodeFile.associate = edgeFile
-                edgeFile.associate = nodeFile
-                nodeFile.save()
-                edgeFile.save()
+                if not nodes or not edges:
+                    print("进度完成却没有返回结果数据")
+                    return failed(message="缺少结果参数")
+            try:
+                result = plan.own_result
+                if int(progress) == 100:
+                    # 任务完成后需要保存结果文件
+                    # 读取nodes和edges,生成结果文件
+                    nodeFile = File(type='csv', usage='result', content='node', user=plan.user)
+                    nodeFile.save()
+                    if not nodeFile.generate(nodes) == OK:
+                        print("保存节点结果文件失败")
+                        return failed(message="保存节点结果文件失败")
+                    edgeFile = File(type='csv', usage='result', content='edge', user=plan.user)
+                    edgeFile.save()
+                    if not edgeFile.generate(edges) == OK:
+                        print("保存边结果文件失败")
+                        return failed(message="保存边结果文件失败")
+                    nodeFile.associate = edgeFile
+                    edgeFile.associate = nodeFile
+                    nodeFile.save()
+                    edgeFile.save()
 
-                # 将文件与结果绑定
-                result.nodeFile = nodeFile
-                result.edgeFile = edgeFile
-                result.progress = 100
-                result.save()
+                    # 将文件与结果绑定
+                    result.nodeFile = nodeFile
+                    result.edgeFile = edgeFile
+                    result.progress = 100
+                    result.save()
 
-            else:
-                # 进度不到百分百,正在执行中,仅更新进度数值
-                # 注意使用负数进度值表示单个处理失败或整个任务失败
+                else:
+                    # 进度不到百分百,正在执行中,仅更新进度数值
+                    # 注意使用负数进度值表示单个处理失败或整个任务失败
+                    result.progress = int(progress)
+                    result.save()
+            except Result.DoesNotExist:
+                # 不存在结果文件,需要新建
+                result = Result()
+                result.plan = plan
+                result.mission = mission
+                result.user = plan.user
                 result.progress = int(progress)
                 result.save()
-            return success(message="保存结果文件成功")
-        except Result.DoesNotExist:
-            # 不存在结果文件,需要新建
-            result = Result()
-            result.plan = plan
-            result.mission = mission
-            result.user = plan.user
-            result.progress = int(progress)
-            result.save()
+        
+        # 计算系统性能占用信息
+        system_performance = performance['system']
+        process_performance = performance['process']
+        try:
+            SYSTEMPERFORMANCE['cpu'] = system_performance['cpu']
+            SYSTEMPERFORMANCE['mem_total'] = system_performance['mem_total']
+            SYSTEMPERFORMANCE['mem_used'] = system_performance['mem_used']
+        except Exception as error:
+            logger.error("进程管理器传递了错误的系统性能信息")
+            return failed(message="系统性能汇报信息错误")
+    
+        # 处理进程占用性能
+        PROCESSPERFORMANCE.clear()
+        for process in process_performance:
+            PROCESSPERFORMANCE.append({
+                'pid': process['pid'],
+                'missionId': process['missionId'],
+                'planId': process['planId'],
+                'cpu': process['cpu'],
+                'mem_used': process['mem_used'],
+                'startTime': process['startTime'],
+                'algorithm': process['algorithm'],
+                'creator': Mission.objects.get(id=int(process['missionId'])).user.username,
+            })
+        logger.info(PROCESSPERFORMANCE)
 
-            return success(message="保存结果文件成功")
-            
+        return success(message="汇报结果文件成功")

+ 33 - 0
backend/api/api_system.py

@@ -0,0 +1,33 @@
+from django.contrib import auth
+from rest_framework.views import APIView
+from django.core.exceptions import ValidationError
+
+from api.utils import TRIGGEREDALERTS, PROCESSPERFORMANCE, SYSTEMPERFORMANCE, success, failed
+from api.models import Alert, Mission
+import requests
+
+import json
+
+class SystemPerformanceAPI(APIView):
+    # 获取系统性能信息
+    def get(self, request):
+        if not request.user.identity == 'admin':
+            return failed(message="仅允许管理员访问")
+        processes = []
+        for process in PROCESSPERFORMANCE:
+            processes.append({
+                **process,
+                'user': Mission.objects.get(id=process['missionId']).user.username,
+            })
+        # 注意将触发的告警一并传递
+        triggeredAlerts = []
+        for alert in TRIGGEREDALERTS:
+            triggeredAlerts.append({
+                'name': alert.name,
+                'level': alert.level,
+                'metric': alert.metric,
+                'threshold': alert.threshold,
+                'handle': alert.handle,
+            })
+            
+        return success(data={**SYSTEMPERFORMANCE, 'processes': processes, 'triggeredAlerts': triggeredAlerts})

+ 6 - 0
backend/api/apps.py

@@ -4,3 +4,9 @@ from django.apps import AppConfig
 class ApiConfig(AppConfig):
     default_auto_field = 'django.db.models.BigAutoField'
     name = 'api'
+
+    def ready(self):
+        import os
+        if os.environ.get('RUN_MAIN') or os.environ.get('SERVER_GATEWAY'):  # 检测开发环境,避免重复启用定时任务
+            from .scheduler import start_scheduler
+            start_scheduler()

+ 86 - 0
backend/api/migrations/0020_alert_systemperformance.py

@@ -0,0 +1,86 @@
+# Generated by Django 4.2 on 2025-04-28 20:22
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("api", "0019_alter_graph_user"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="Alert",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("create_time", models.DateTimeField(auto_now_add=True)),
+                ("update_time", models.DateTimeField(auto_now=True)),
+                ("name", models.CharField(default="未命名告警规则", max_length=64)),
+                (
+                    "level",
+                    models.CharField(
+                        choices=[("system", "系统级"), ("process", "进程级")],
+                        default="system",
+                        max_length=16,
+                    ),
+                ),
+                (
+                    "indicator",
+                    models.CharField(
+                        choices=[("cpu", "cpu使用率"), ("mem", "内存占用"), ("disk", "硬盘占用")],
+                        default="cpu",
+                        max_length=16,
+                    ),
+                ),
+                ("threshold", models.FloatField()),
+                (
+                    "handle",
+                    models.CharField(
+                        choices=[
+                            ("noAction", "不做处理"),
+                            ("closeLatest", "关闭最新进程"),
+                            ("closeHighestCpu", "关闭CPU占用最高进程"),
+                            ("closeHighestMem", "关闭MEM占用最高进程"),
+                        ],
+                        default="noAction",
+                        max_length=16,
+                    ),
+                ),
+                (
+                    "state",
+                    models.CharField(
+                        choices=[("enable", "启用"), ("disable", "未启用")],
+                        default="disable",
+                        max_length=16,
+                    ),
+                ),
+            ],
+        ),
+        migrations.CreateModel(
+            name="SystemPerformance",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("cpu", models.FloatField()),
+                ("mem_total", models.FloatField()),
+                ("mem_used", models.FloatField()),
+                ("create_time", models.DateTimeField(auto_now_add=True)),
+            ],
+        ),
+    ]

+ 18 - 0
backend/api/migrations/0021_rename_indicator_alert_metric.py

@@ -0,0 +1,18 @@
+# Generated by Django 4.2 on 2025-04-28 22:34
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("api", "0020_alert_systemperformance"),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name="alert",
+            old_name="indicator",
+            new_name="metric",
+        ),
+    ]

BIN
backend/api/migrations/__pycache__/0001_initial.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/0002_alter_user_options_user_last_login.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/0003_view_file.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/0004_rename_display_name_user_displayname_file_usage.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/0005_file_associate_file_content.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/0006_alter_file_associate.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/0007_fileinfo.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/0008_mission_result.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/0009_alter_fileinfo_file_alter_mission_name.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/0010_algorithm_plan.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/0011_result_plan_result_state.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/0012_result_edgefile_result_nodefile_alter_result_plan.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/0013_remove_result_state_alter_file_usage.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/0014_result_progress.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/0015_mission_state.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/0016_alter_result_edgefile_alter_result_nodefile.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/0017_graph_graphtoken.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/0018_rename_edgemap_graph_edges_and_more.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/0019_alter_graph_user.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/0020_alert_systemperformance.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/0021_rename_indicator_alert_metric.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/__init__.cpython-310.pyc


+ 3 - 1
backend/api/models/__init__.py

@@ -5,4 +5,6 @@ from .mission import Mission
 from .result import Result
 from .plan import Plan
 from .algorithm import Algorithm
-from .graph import Graph, GraphToken
+from .graph import Graph, GraphToken
+from .system import SystemPerformance
+from .alert import Alert

BIN
backend/api/models/__pycache__/__init__.cpython-310.pyc


BIN
backend/api/models/__pycache__/alert.cpython-310.pyc


BIN
backend/api/models/__pycache__/algorithm.cpython-310.pyc


BIN
backend/api/models/__pycache__/file.cpython-310.pyc


BIN
backend/api/models/__pycache__/graph.cpython-310.pyc


BIN
backend/api/models/__pycache__/mission.cpython-310.pyc


BIN
backend/api/models/__pycache__/plan.cpython-310.pyc


BIN
backend/api/models/__pycache__/result.cpython-310.pyc


BIN
backend/api/models/__pycache__/system.cpython-310.pyc


BIN
backend/api/models/__pycache__/user.cpython-310.pyc


BIN
backend/api/models/__pycache__/view.cpython-310.pyc


+ 72 - 0
backend/api/models/alert.py

@@ -0,0 +1,72 @@
+from django.db import models
+import os, errno
+from api.utils import SYSTEMPERFORMANCE, PROCESSPERFORMANCE
+
+from api.utils import *
+alertLever = [
+    ('system', '系统级'),
+    ('process', '进程级'),
+]
+alertMetric = [
+    ('cpu', 'cpu使用率'),
+    ('mem', '内存占用'),
+    ('disk', '硬盘占用')
+]
+alertHandle = [
+    ('noAction', '不做处理'),
+    ('closeLatest', '关闭最新进程'),
+    ('closeHighestCpu', '关闭CPU占用最高进程'),
+    ('closeHighestMem', '关闭MEM占用最高进程'),
+]
+alertState = [
+    ('enable', '启用'),
+    ('disable', '未启用'),
+]
+
+class AlertManager(models.Manager):
+    def checkAlert(self):
+        triggeredAlert = []
+        for alert in self.get_queryset().filter(state='enable'):
+            print(SYSTEMPERFORMANCE)
+            # 系统级检测
+            if alert.level == 'system':
+                if alert.metric == 'cpu' and alert.threshold <= float(SYSTEMPERFORMANCE['cpu']):
+                    triggeredAlert.append(alert)
+                if alert.metric == 'mem' and alert.threshold <= float(SYSTEMPERFORMANCE['mem_used']):
+                    triggeredAlert.append(alert)
+                if alert.metric == 'disk' and alert.threshold <= float(SYSTEMPERFORMANCE['disk_used']):
+                    triggeredAlert.append(alert)
+            # 进程级检测,遍历所有活跃进程,检查性能占用
+            if alert.level == 'process':
+                # PROCESSPERFORMANCE结构应为:
+                # cpu、mem_used
+                for pid in PROCESSPERFORMANCE:
+                    if alert.metric == 'cpu' and alert.threshold <= float(PROCESSPERFORMANCE[pid]['cpu']):
+                        triggeredAlert.append(alert)
+                    if alert.metric == 'mem' and alert.threshold <= float(PROCESSPERFORMANCE[pid]['mem']):
+                        triggeredAlert.append(alert)
+        return triggeredAlert
+
+    
+
+class Alert(models.Model):
+    create_time = models.DateTimeField(auto_now_add=True)
+    update_time = models.DateTimeField(auto_now=True)
+
+    name = models.CharField(default="未命名告警规则", max_length=64, unique=True)
+    level = models.CharField(choices=alertLever, default='system', max_length=16)
+    # 监控指标
+    metric = models.CharField(choices=alertMetric, default='cpu', max_length=16)
+    # 触发阈值
+    threshold = models.FloatField()
+
+    handle = models.CharField(choices=alertHandle, default='noAction', max_length=16)
+
+    state = models.CharField(choices=alertState, default='disable', max_length=16)
+
+
+
+    objects = AlertManager()
+
+    class Meta:
+        app_label = 'api'

+ 1 - 1
backend/api/models/file.py

@@ -21,7 +21,7 @@ contents = [
     ('edge', 'edge'),
 ]
 
-BASE_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'uploads')
+
 
 
 class FileManager(models.Manager):

+ 20 - 0
backend/api/models/system.py

@@ -0,0 +1,20 @@
+from django.db import models
+import os, errno
+
+from api.utils import *
+
+class SystemPerformanceManager(models.Manager):
+    def statistic(self, start, end):
+      # 系统性能可能后续要加入历史统计功能,预留相关函数
+      pass
+
+class SystemPerformance(models.Model):
+    cpu = models.FloatField()
+    mem_total = models.FloatField()
+    mem_used = models.FloatField()
+    create_time = models.DateTimeField(auto_now_add=True)
+
+    objects = SystemPerformanceManager()
+
+    class Meta:
+        app_label = 'api'

+ 68 - 0
backend/api/scheduler.py

@@ -0,0 +1,68 @@
+# your_app/scheduler.py
+import logging
+from apscheduler.schedulers.background import BackgroundScheduler
+from django_apscheduler.jobstores import DjangoJobStore
+from .models import Alert
+from .utils import SYSTEMPERFORMANCE, TRIGGEREDALERTS, BASE_FILE_PATH
+from django.db import DatabaseError
+from pathlib import Path
+
+logger = logging.getLogger("scheduler")
+
+def getUploadFolderSize():
+    total_size = 0
+    for path in Path(BASE_FILE_PATH).rglob('*'):
+        if path.is_file():
+            try:
+                total_size += path.stat().st_size
+            except (FileNotFoundError, PermissionError):
+                pass
+    # 返回值单位是B
+    return total_size
+
+
+
+def checkAlert():
+    # 计算用户上传文件总大小,存入系统性能信息
+    SYSTEMPERFORMANCE['disk_used'] = getUploadFolderSize()
+    # 获取所有开启的告警,存入触发告警列表
+    TRIGGEREDALERTS.clear()
+    TRIGGEREDALERTS.extend(Alert.objects.checkAlert())
+    # 显示当前触发的告警
+    if TRIGGEREDALERTS:
+        try:
+            # 获取所有告警对象
+            if TRIGGEREDALERTS:
+                # 生成制表符分隔的表格化输出
+                alert_table = "\n".join([
+                    f"{alert.name}\t{alert.level}\t{alert.metric}\t{alert.threshold}"
+                    for alert in TRIGGEREDALERTS
+                ])
+                logger.warning(f"当前触发告警 (共{len(TRIGGEREDALERTS)}条):\n名称\t级别\t指标\t阈值\n{alert_table}")
+                
+        except DatabaseError as e:
+            logger.error(f"获取告警数据失败: {str(e)}")
+
+# 初始化调度器
+scheduler = BackgroundScheduler()
+
+def start_scheduler():
+    try:
+        # 使用Django的JobStore
+        scheduler.add_jobstore(DjangoJobStore(), "default")
+        # 添加任务(避免重复添加)
+        if not scheduler.get_job("5_seconds_job"):
+            scheduler.add_job(
+                checkAlert,
+                'interval',
+                seconds=5,
+                id="5_seconds_job",
+                replace_existing=True,
+            )
+            logger.info("定时任务已启动")
+        
+        # 启动调度器
+        if not scheduler.running:
+            scheduler.start()
+    except Exception as e:
+        logger.error(f"调度器启动失败: {str(e)}")

+ 5 - 0
backend/api/urls.py

@@ -6,6 +6,8 @@ from .api_calculate import CalculateAPI
 from .api_rawDataTrans import RawDataTrans
 from .api_results import Results
 from .api_graph import GenerateGraph, ViewGraphByToken
+from .api_alert import AlertAPI, AlertCheck
+from .api_system import SystemPerformanceAPI
 
 urlpatterns = [
     path('register/', UserRegisterAPI.as_view(), name='user_register_api'),
@@ -18,4 +20,7 @@ urlpatterns = [
     path('results/', Results.as_view(), name='results_api'),
     path('generateGraph/', GenerateGraph.as_view(), name="generate_graph_api"),
     path('viewGraphByToken/', ViewGraphByToken.as_view(), name="view_graph_by_token_api"),
+    path('alert/', AlertAPI.as_view(), name="alert_api"),
+    path('alertCheck/', AlertCheck.as_view(), name="alert_check_api"),
+    path('systemPerformance/', SystemPerformanceAPI.as_view(), name="system_performance_api"),
 ]

+ 27 - 1
backend/api/utils.py

@@ -1,8 +1,14 @@
 from rest_framework.response import Response
 from rest_framework import status
+from django.apps import apps
+import os
 
 SCHEDULER_BASE_URL = "http://localhost:5000"
 
+# 动态生成上传文件夹 
+BASE_FILE_PATH = os.path.join(apps.get_app_config('api').path, 'uploads')
+
+
 OK = 0
 FAILED = 1
 FILE_ALREADY_EXIST = 101
@@ -38,4 +44,24 @@ def success(message="访问成功", data=None, code=200):
         'status': 'success',
         'message': message,
         'data': data,
-    }, status=mStatus)
+    }, status=mStatus)
+
+# 用来控制允许保存多少用户上传的文件和结果文件,默认5GB
+MAX_STORAGE = 5
+
+# 两个变量用来存储系统性能参数和进程性能参数
+#: 存储系统性能信息
+#: 可用参数包括: cpu, mem_total, mem_used, disk_used(单位是B)
+SYSTEMPERFORMANCE = {
+    'cpu': 0,
+    'mem_total': 0,
+    'mem_used': 0,
+    'disk_used': 0,
+    'disk_total': MAX_STORAGE * 1024**3,
+}
+
+# 存放正在运行的进程及参数信息
+PROCESSPERFORMANCE = []
+
+# 存放触发的告警信息
+TRIGGEREDALERTS = []

BIN
backend/backend/__pycache__/__init__.cpython-310.pyc


BIN
backend/backend/__pycache__/settings.cpython-310.pyc


BIN
backend/backend/__pycache__/urls.cpython-310.pyc


BIN
backend/backend/__pycache__/wsgi.cpython-310.pyc


+ 60 - 0
backend/backend/settings.py

@@ -38,6 +38,7 @@ INSTALLED_APPS = [
     'django.contrib.sessions',
     'django.contrib.messages',
     'django.contrib.staticfiles',
+    'django_apscheduler',
     'rest_framework',
     'rest_framework.authtoken',
     'api',
@@ -140,3 +141,62 @@ STATIC_URL = 'static/'
 # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
 
 DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+
+
+# settings.py
+
+# 日志输出,带颜色
+import colorama
+import logging
+colorama.init()
+
+
+class ColoredFormatter(logging.Formatter):
+    # ANSI 颜色代码
+    COLOR_CODES = {
+        logging.DEBUG: '\033[37m',    # 白色
+        logging.INFO: '\033[92m',     # 绿色
+        logging.WARNING: '\033[93m',  # 黄色
+        logging.ERROR: '\033[91m',    # 红色
+        logging.CRITICAL: '\033[41m'  # 红底白字
+    }
+    RESET_CODE = '\033[0m'
+    def format(self, record):
+        # 添加颜色代码
+        color = self.COLOR_CODES.get(record.levelno, '')
+        message = super().format(record)
+        return f"{color}{message}{self.RESET_CODE}"
+
+
+LOGGING = {
+    'version': 1,
+    'disable_existing_loggers': False,
+    'formatters': {
+        'verbose': {
+            # 包含 logger 名称、时间、模块、进程、消息等级 和 消息内容
+            'format': '{levelname} {asctime} {module} {process:d} {thread:d} [{name}] {message}',
+            'style': '{',  # 使用花括号风格(Python 3.2+)
+        },
+        'simple': {
+            # 仅包含 logger 名称、等级 和 消息内容(开发环境推荐)
+            '()': ColoredFormatter,
+            'format': '[{name}] {levelname} {message}',
+            'style': '{',
+        },
+    },
+    'root': {
+        'handlers': ['console'],
+        'level': 'INFO',
+    },
+    'handlers': {
+        'console': {
+            'class': 'logging.StreamHandler',
+            'formatter': 'simple',  # 使用简洁格式
+        },
+    },
+    'loggers': {
+        'apscheduler': {
+            'level': 'WARNING',
+        },
+    }
+}

BIN
backend/db.sqlite3


BIN
scheduler/__pycache__/processManager.cpython-310.pyc


BIN
scheduler/__pycache__/utils.cpython-310.pyc


+ 4 - 4
scheduler/algo1Folder/controller.py

@@ -40,7 +40,7 @@ start_time = time.perf_counter()
 count = 0
 while True:
     count += 1
-    if time.perf_counter() - start_time >= 2.0:
+    if time.perf_counter() - start_time >= 10.0:
         break   
 progressData['progress'] = 20
 print(json.dumps({'msg': 'progress', 'data': progressData}), flush=True)
@@ -48,7 +48,7 @@ start_time = time.perf_counter()
 count = 0
 while True:
     count += 1
-    if time.perf_counter() - start_time >= 2.0:
+    if time.perf_counter() - start_time >= 10.0:
         break   
 
 progressData['progress'] = 40
@@ -57,7 +57,7 @@ start_time = time.perf_counter()
 count = 0
 while True:
     count += 1
-    if time.perf_counter() - start_time >= 2.0:
+    if time.perf_counter() - start_time >= 10.0:
         break   
 progressData['progress'] = 60
 print(json.dumps({'msg': 'progress', 'data': progressData}), flush=True)
@@ -65,7 +65,7 @@ start_time = time.perf_counter()
 count = 0
 while True:
     count += 1
-    if time.perf_counter() - start_time >= 2.0:
+    if time.perf_counter() - start_time >= 10.0:
         break   
 
 

+ 116 - 21
scheduler/processManager.py

@@ -11,7 +11,6 @@ from typing import Dict, Optional, List, Union
 import requests
 from utils import store
 
-
 from utils import SCHEDULER_BASE_URL, BACKEND_BASE_URL
 
 
@@ -23,10 +22,17 @@ class ProcessManager:
         self.timeout = timeout                # 进程超时时间(秒)
         self._monitor_thread: Optional[threading.Thread] = None
         self._running = False
+        self.logger = logging.getLogger("ProcessManager")
 
-        # 子进程的通信队列相关参数
+        # 子进程的通信队列相关参数
         self._reader_threads: Dict[int, threading.Thread] = {} # pid:
 
+        # 汇报信息队列相关参数
+        self.report_queue = Queue()
+        self._report_thread = threading.Thread(target=self._report_loop, daemon=True)
+        self.max_batch = 20
+        self.report_interval = 5 # 5s发送一次数据
+
         # 配置日志
         self.log_dir: str = "process_logs"
         # 确保日志目录存在
@@ -35,8 +41,7 @@ class ProcessManager:
             level=logging.INFO,
             format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
         )
-        self.logger = logging.getLogger("ProcessManager")
-
+        
         # 准备日志文件
         timestamp = time.strftime("%Y%m%d-%H%M%S")
         log_prefix = os.path.join(
@@ -48,6 +53,73 @@ class ProcessManager:
         # with open(f"{log_prefix}.stdout", "w") as stdout_f, \
         #         open(f"{log_prefix}.stderr", "w") as stderr_f:
 
+    def _report_loop(self):
+        # 只有启动监视之后才会运行
+        self.logger.info("开始监控汇报信息")
+        while self._running:
+            try:
+                batch = []
+                start_time = time.time()
+                while len(batch) < self.max_batch:
+                    try:
+                        item = self.report_queue.get_nowait()
+                        self.logger.info(f"从队列中读取到消息{item}")
+                        batch.append(item)
+                    except Empty:
+                        # 此处放置超时检测,当到达指定时间周期后,跳出循环发送数据
+                        # 注意例外情况,当batch列表被填满时,将直接发送
+                        if time.time() - start_time >= self.report_interval:
+                            break
+                        time.sleep(0.1)
+                        continue
+                # 超时后发送数据
+                self._report_batch(batch)
+            except Exception as error:
+                self.logger.error(f"批量汇报线程出现异常:{error}", exc_info=True)
+    
+    # 批量汇报函数
+    def _report_batch(self, batch: list):
+        # 汇报所有进度信息,-1表示任务终止
+        report_progress = []
+        for item in batch:
+            # 检测item中存放的是什么数据
+            report_progress.append(item)
+        # 汇报系统性能信息
+        report_performance = {}
+        cpu_system = psutil.cpu_percent(interval=1)
+        mem_system = psutil.virtual_memory()
+        mem_total = float(mem_system.total) / (1024**3)
+        mem_used = float(mem_system.used) / (1024**3)
+        # 放入系统性能占用信息
+        report_performance['system'] = {
+            'cpu': round(cpu_system, 1),
+            'mem_total': mem_total,
+            'mem_used': mem_used,
+        }
+        
+        # 放入进程性能占用信息
+        report_performance['process'] = []
+        for pid in self.processes:
+            ps_proc = psutil.Process(pid)
+            cpu = round(ps_proc.cpu_percent(interval=1) / psutil.cpu_count(), 1)
+            mem = ps_proc.memory_info().rss / (1024**2)  # 转为MB
+            self.logger.info(f"获取到进程{pid}信息: CPU:{cpu} MEM:{mem}")
+            report_performance['process'].append({
+                'planId': self.processes[pid]['meta']['plan']['id'],
+                'missionId': self.processes[pid]['meta']['mission']['id'],
+                'pid': pid,
+                'cpu': cpu,
+                'mem_used': mem,
+                'startTime': time.strftime("%Y-%m-%d %H:%M:%S", self.processes[pid]['meta']['startTime']),
+                'algorithm': self.processes[pid]['meta']['algorithm'],
+            })
+        # 发送汇报请求
+        response = requests.post(BACKEND_BASE_URL + "/rawDataTrans/", json={
+            'result': report_progress,
+            'performance': report_performance,
+        })
+        # self.logger.info(f"批量信息已向后端服务器汇报 response:{response}")
+
     def spawn(
         self,
         command: Union[str, List[str]],
@@ -111,13 +183,17 @@ class ProcessManager:
     def stop(self, missionId: int):
         plansStopped = []
         pids = [] # to delete
-        for pid in self.processes:
-            if int(self.processes[pid]['meta']['mission']['id']) == missionId:
-                pids.append(pid)
-                plansStopped.append({'planId': int(self.processes[pid]['meta']['plan']['id'])})
-        for pid in pids:
-            self.remove_process(pid)
-
+        try:
+            for pid in self.processes:
+                if int(self.processes[pid]['meta']['mission']['id']) == missionId:
+                    pids.append(pid)
+                    plansStopped.append({'planId': int(self.processes[pid]['meta']['plan']['id'])})
+            for pid in pids:
+                self.remove_process(pid)
+            return True
+        except Exception as error:
+            self.logger.error(f"停止pid={pid}的Plan时发生错误,Error:{error}")
+            return False
 
     def _start_reader(self, pid:int, stdout, stderr):
         """为每个子进程启动独立的非阻塞读取线程"""
@@ -183,6 +259,9 @@ class ProcessManager:
             daemon=True  # 随主进程退出
         )
         self._monitor_thread.start()
+        self.logger.info("启动汇报子线程")
+        self._report_thread.start()
+        self.logger.info("启动汇报子线程")
         self.logger.info("进程监控线程已启动")
 
     def stop_monitoring(self):
@@ -204,8 +283,9 @@ class ProcessManager:
                 except Exception as error:
                     self.logger.error(f"移除处理进程监视失败 MissionId: {missionId} PlanId: {planId} Error: {error}")
                     return False
-                # 清理进程列表
-                del self.processes[pid]
+                # 不应该在这里移除,保持整个process遍历时字典内容不改变
+                # # 移除监视进程列表
+                # del self.processes[pid]
                 # 清理读取线程
                 if pid in self._reader_threads:
                     for t in self._reader_threads[pid]:
@@ -232,8 +312,9 @@ class ProcessManager:
         except Exception as error:
             self.logger.error(f"移除处理进程监视-with pid失败 pid:{pid_to_del}")
             return False
-        # 移除监视进程列表
-        del self.processes[pid]
+        # 不应该在这里移除,保持整个process遍历时字典内容不改变
+        # # 移除监视进程列表
+        # del self.processes[pid]
         # 清理读取线程
         if pid_to_del in self._reader_threads:
             for t in self._reader_threads[pid_to_del]:
@@ -310,8 +391,11 @@ class ProcessManager:
                 
                 # 正常读取子进程输出
                 try:
+                    count  = 0
                     while True:
                         pipe_type, message = info["msg_queue"].get_nowait()
+                        count += 1
+                        self.logger.info(f"Count is:{count}")
                         response = self._handle_process_message(pid, pipe_type, message)
                         if 'finished' in response and response['finished']:
                             # 正常退出
@@ -346,16 +430,21 @@ class ProcessManager:
             results=report['results']
             for nextTask in store.solveMission(missionId=missionId, planId=planId, results=results):
                 task = store.prepareTask(missionId=missionId, planId=nextTask['id'])
-                if self.spawn(command=task['command'], cwd=task['cwd'], plan=task['plan'], mission=task['mission']):
+                if self.spawn(command=task['command'], cwd=task['cwd'], plan=task['plan'], mission=task['mission'], startTime=time.localtime(), algorithm=task['algorithm']):
                     self.logger.info(f"创建后续计算任务成功 MissionId:{missionId} PlanId:{task['plan']['id']}")
                 else:
                     self.logger.error(f"创建后续计算任务失败 MissionId:{missionId} PlanId:{task['plan']['id']}")
                     # 任务无法继续,向django汇报下一个任务失败
-                    response = requests.post(BACKEND_BASE_URL + "/rawDataTrans/", json={
+                    self.report_queue.put({
                         'missionId': task['mission']['id'],
                         'planId': task['plan']['id'],
                         'progress': -1, # -1 表示单个任务失败 -2 表示全体mission失败
                     })
+                    # response = requests.post(BACKEND_BASE_URL + "/rawDataTrans/", json={
+                    #     'missionId': task['mission']['id'],
+                    #     'planId': task['plan']['id'],
+                    #     'progress': -1, # -1 表示单个任务失败 -2 表示全体mission失败
+                    # })
                     
                     
     def _handle_process_message(self, pid:int, pipe_type:str, message:str):
@@ -372,13 +461,14 @@ class ProcessManager:
             msg = data.get("msg")
             if msg == "progress":
                 # 获得进度汇报,向django汇报
-                response = requests.post(BACKEND_BASE_URL + "/rawDataTrans/", json=data.get("data"))
+                self.report_queue.put(data.get("data"))
+                # response = requests.post(BACKEND_BASE_URL + "/rawDataTrans/", json=data.get("data"))
                 return {'finished': False}
             
             if msg == "result":
                 # 获得返回结果,向django汇报
-                response = requests.post(BACKEND_BASE_URL + "/rawDataTrans/", json=data.get("data"))
-                self.logger.info(f"进程结果已向后端服务器反馈 pid:{pid} response:{response}")
+                self.report_queue.put(data.get("data"))
+                # response = requests.post(BACKEND_BASE_URL + "/rawDataTrans/", json=data.get("data"))
                 self.logger.info(f"进程 {pid} 报告已完成")
                 # 标记该进程正常退出
                 return {'finished': True, 'results': data.get('data')}
@@ -402,7 +492,12 @@ class ProcessManager:
             f"命令: {info['command']}\n"
         )
         # 发现进程崩溃,向django汇报任务失败
-        response = requests.post(BACKEND_BASE_URL + "/rawDataTrans/", json={'missionId': info['meta']['mission']['id'], 'planId': info['meta']['plan']['id'], 'progress': -1}) # -1表示单个任务失败
+        self.report_queue.put({
+            'missionId': info['meta']['mission']['id'],
+            'planId': info['meta']['plan']['id'],
+            'progress': -1, # -1 表示单个任务失败 -2 表示全体mission失败
+        })
+        # response = requests.post(BACKEND_BASE_URL + "/rawDataTrans/", json={'missionId': info['meta']['mission']['id'], 'planId': info['meta']['plan']['id'], 'progress': -1}) # -1表示单个任务失败
         # 调用store终止该mission
         store.removeMission(missionId=info['meta']['mission']['id'])
         self.logger.error(f"任务进程发生崩溃,Mission {info['meta']['mission']['id']}已终止")

+ 9 - 3
scheduler/scheduler.py

@@ -7,6 +7,7 @@ from pathlib import Path
 import requests
 from utils import store, TaskState
 from processManager import ProcessManager
+import time
 
 BASE_DIR = Path(__file__).resolve().parent
 DB_DIR = BASE_DIR.parent / 'backend' / 'db.sqlite3'
@@ -56,7 +57,7 @@ def add_mission():
     if store.addMission(mission, plans):
         # 读取task列表,启动任务
         for task in store.initMissionTasks(missionId=mission['id']):
-            if not manager.spawn(command=task['command'], cwd=task['cwd'], plan=task['plan'], mission=task['mission']):
+            if not manager.spawn(command=task['command'], cwd=task['cwd'], plan=task['plan'], mission=task['mission'], startTime = time.localtime(), algorithm=task['algorithm']):
                 logger.error(f"创建计算任务失败 :MissionId:{mission['id']} PlanId:{task['plan']['id']}")
                 return jsonify({"code": "ERROR", "status": ""})
         return jsonify({"code": "OK", "status": "started"})
@@ -77,11 +78,13 @@ def pause_mission():
 @app.route('/resumeMission', methods=['POST'])
 def resume_mission():
     mission = request.json['mission']
+    plans = request.json['plans']
     # store中的恢复mission即读取该mission现有的tasks列表,将其中的任务重新加入进程管理器中运行
-    resumedTasks = store.resumeMission(missionId=int(mission['id']))
+    # 需要传递plans,预防已经失去保存的mission信息时用于恢复
+    resumedTasks = store.resumeMission(missionId=int(mission['id']), plans=plans)
     for nextTask in resumedTasks:
         task = store.prepareTask(missionId=int(mission['id']), planId=nextTask['id'])
-        if not manager.spawn(command=task['command'], cwd=task['cwd'], plan=task['plan'], mission=task['mission']):
+        if not manager.spawn(command=task['command'], cwd=task['cwd'], plan=task['plan'], mission=task['mission'], startTime=time.localtime(), algorithm=task['algorithm']):
             logger.info(f"任务 MissionId:{mission['id']} 恢复运行出错,尝试将其移除")
             # 任务恢复运行出错时,将其彻底停止以避免bug
             if not manager.stop(missionId=int(mission['id'])):
@@ -91,6 +94,7 @@ def resume_mission():
             else:
                 logger.info(f"移除 MissionId:{mission['id']} 任务数据出错")
     logger.info(f"任务 MissionId:{mission['id']} 已恢复")
+    return jsonify({"code": "OK", "status": "任务已恢复"})
     
     
 @app.route('/stopMission', methods=['POST'])
@@ -103,8 +107,10 @@ def stop_mission():
     # 其次在数据管理器中删除该Mission的所有数据
     if store.stopMission(missionId=int(mission['id'])):
         logger.info(f"停止 MissionId:{mission['id']} 成功")
+        return jsonify({"code": "OK", "status": "任务已停止"})
     else:
         logger.info(f"停止 MissionId:{mission['id']} 失败")
+        return jsonify({"code": "ERROR", "status": "任务停止失败"})
 
 
 @app.route('/get_status/<task_id>')

+ 19 - 6
scheduler/utils.py

@@ -72,7 +72,7 @@ class Store:
     
     # 当出现进程错误或需要移除mission时使用
     def removeMission(self, missionId: int):
-        mission = [m for m in self.missions if m['id'] == missionId]
+        mission = [m for m in self.missions if int(m['id']) == missionId]
         if not mission:
             logger.error("移除Mission时出错,未找到该Mission")
             return
@@ -122,22 +122,33 @@ class Store:
         # 返回下一轮需要调用的任务,如果存在并行,则列表长度大于1
         return [plan for plan in mission['plans'] if plan['id'] in children]
     
-    def resumeMission(self, missionId: int):
-        mission = [m for m in self.missions if m['id'] == missionId]
+    def resumeMission(self, missionId: int, plans: list):
+        mission = [m for m in self.missions if int(m['id']) == missionId]
         if not mission:
-            logger.error("未找到要恢复的mission")
+            logger.error("未找到要恢复的mission,依据传递plans重新创建")
+            # 如果没有找到要恢复的Mission,就重新构造mission
+            # 此时传递的plans中全部都是第一级任务,应直接装入Task列表            
+            tasks = [plan['id'] for plan in plans if 'root' in plan]
+            mission = {
+                'id': missionId,
+                'plans': plans,
+                'tasks': tasks,
+            }
+            # 构造mission之后还需要加入missions以便后续继续追踪
+            self.missions.append(mission)
         else:
+            logger.info("已找到要恢复的mission,正在恢复任务")
             mission = mission[0]
         # 返回所有的tasks,由于暂停时仅在maanger中停止了进程,而没有在store中处理,因此tasks中保存的就是此前的运行任务
         return [plan for plan in mission['plans'] if plan['id'] in mission['tasks']]
             
     def stopMission(self, missionId: int):
-        mission = [m for m in self.missions if m['id'] == missionId]
+        mission = [m for m in self.missions if int(m['id']) == missionId]
         if not mission:
             logger.error("未找到要停止的mission")
             return False
         # 停止后不需恢复,所以直接摘除该mission即可
-        self.missions = [m for m in self.missions if m['id'] != missionId]
+        self.missions = [m for m in self.missions if int(m['id']) != missionId]
         return True
 
     def initMissionTasks(self, missionId: int):
@@ -164,6 +175,7 @@ class Store:
                 },
                 'cwd': str(algorithm['path']),
                 'command': algorithm['command'],
+                'algorithm': plan['algorithm'],
             })
             logger.info(f"初始化计算任务:{preparedTasks}")
         return preparedTasks
@@ -192,6 +204,7 @@ class Store:
                 'mission': {
                     'id': mission['id'],
                 },
+                'algorithm': plan['algorithm'],
             }
             return preparedTask
         print("未找到plan,是否错误传入更新请求")

+ 3 - 2
viewer/src/api/axios.js

@@ -16,8 +16,8 @@ api.interceptors.request.use(async config => {
     const { method } = config;
     if (method === 'post' || method === 'put' || method === 'delete' || method === 'options'
     ) {
-        csrfToken = await getCSRFToken(config.url);
-        config.headers['X-CSRFToken'] = csrfToken;
+        // csrfToken = await getCSRFToken(config.url);
+        // config.headers['X-CSRFToken'] = csrfToken;
         if(userToken !== ""){
             config.headers.Authorization = 'Token ' + userToken;
         }else{
@@ -37,6 +37,7 @@ api.interceptors.request.use(async config => {
 });
  
 async function getCSRFToken(url) {
+    console.log(csrfToken, baseURL + url)
     if(csrfToken === ""){
         const response = await axios.get(baseURL + url);
         csrfToken = response.data.csrftoken; 

+ 4 - 0
viewer/src/store/userInfo.js

@@ -32,6 +32,8 @@ export function useUserInfo() {
                 registerResult.data = userInfo;
                 // 保存用户token
                 setUserToken(response.data.token);
+                // 保存用户信息
+                sessionStorage.setItem('user-info', JSON.stringify(userInfo.value))
             }
         } catch (error) {
             console.log(error)
@@ -80,6 +82,8 @@ export function useUserInfo() {
 
                 // 保存用户token
                 setUserToken(response.data.token);
+                // 保存用户信息
+                sessionStorage.setItem('user-info', JSON.stringify(userInfo.value))
             }
         } catch (error) {
             console.log(error)

+ 2 - 3
viewer/src/views/dashoard/analyze.vue

@@ -254,8 +254,7 @@ const handleDeleteFile = (file) => {
       type: 'warning'
     }
   ).then(() => {
-    console.log(file)
-    deleteData('uploadfile/', { id: file.id }).then(response => {
+    deleteData('/uploadfile/', { id: file.id }).then(response => {
       if (response.status == 'success') {
         ElMessage.success('文件已删除')
         updateUploadHistory()
@@ -271,7 +270,7 @@ const handleDeleteFile = (file) => {
 }
 
 const updateUploadHistory = () => {
-  getData('uploadfile/')
+  getData('/uploadfile/')
     .then(response => {
       fileHistory.value = []
       response.data.reverse().forEach(item => {

+ 20 - 13
viewer/src/views/dashoard/calculate.vue

@@ -63,13 +63,9 @@
       <!-- 动态内容区域 -->
       <div class="content-area">
         <template v-if="currentView">
-          <component :is="currentViewComponent" :result="currentResult"/>
+          <component :is="currentViewComponent" :result="currentResult" />
         </template>
-        <el-empty
-          v-else
-          description="请选择视图展示方式"
-          class="empty-tip"
-        >
+        <el-empty v-else description="请选择视图展示方式" class="empty-tip">
           <template #image>
             <el-icon :size="80" color="var(--el-color-info)">
               <Select />
@@ -171,7 +167,7 @@ const viewResult = (index) => {
     currentResult.value = null
     currentResult.value = index
     showModal.value = true
-    
+
   } else {
     ElMessage.warning("请先开始计算处理,或等待该任务完成")
   }
@@ -290,7 +286,7 @@ const startCalculate = async () => {
           if (result.progress !== 100) {
             missionComplete = false
           }
-          if (result.progress == -1 || result.progress == -2){
+          if (result.progress == -1 || result.progress == -2) {
             // -1表示单个任务中止
 
             // -2表示整个任务中止
@@ -313,11 +309,17 @@ const startCalculate = async () => {
   }, 2000) // 2s更新一次
 }
 
-const pauseCalculate = () => {
+const pauseCalculate = async () => {
   console.log('pause')
-  //暂停、停止时都清除定时器
+
   try {
+    //暂停、停止时都清除定时器
     clearInterval(progressInterval.value)
+    // 发送暂停请求
+    const response = await postData('/calculate/', {
+      mission: mission.id,
+      command: 'pause',
+    })
   } catch (error) {
     console.log(error)
   }
@@ -325,11 +327,16 @@ const pauseCalculate = () => {
   // todos
 }
 
-const stopCalculate = () => {
+const stopCalculate = async () => {
   console.log('stop')
   //暂停、停止时都清除定时器
   try {
     clearInterval(progressInterval.value)
+    // 发送暂停请求
+    const response = await postData('/calculate/', {
+      mission: mission.id,
+      command: 'stop',
+    })
   } catch (error) {
     console.log(error)
   }
@@ -528,9 +535,9 @@ onUnmounted(() => {
   margin-top: 10px;
   font-size: 14px;
 }
+
 /* 按钮图标样式 */
-.el-button [class*=el-icon] + span {
+.el-button [class*=el-icon]+span {
   margin-left: 6px;
 }
-
 </style>

+ 9 - 1
viewer/src/views/dashoard/dashboard.vue

@@ -37,7 +37,7 @@
 </template>
 
 <script setup>
-import { ref, computed, inject } from 'vue'
+import { ref, computed, inject, onMounted } from 'vue'
 import { useRouter } from 'vue-router'
 import { ArrowDown } from '@element-plus/icons-vue'
 
@@ -77,6 +77,14 @@ const logout = () => {
 const navigateTo = (type) => {
   router.push(`/dashboard/${type}`)
 }
+
+onMounted( ()=> {
+  // 如果页面刷新,则从缓存中读取登陆时保存的用户信息数据
+  if(useUserInfo.userInfo.value.username == ''){
+    useUserInfo.userInfo.value = JSON.parse(sessionStorage.getItem('user-info'))
+    console.log(useUserInfo.userInfo.value)
+  }
+})
 </script>
 
 <style lang="scss" scoped>

+ 493 - 82
viewer/src/views/dashoard/monitor.vue

@@ -1,6 +1,34 @@
 <template>
     <!-- monitor.vue -->
     <div class="monitor-container">
+        <!-- 告警信息显示 -->
+        <div class="alert-notifications">
+            <transition-group name="el-fade-in">
+                <el-alert v-for="alert in triggeredAlerts" :key="alert.name" type="error" effect="dark" :closable="true"
+                    @close="handleCloseAlert(alert)" class="alert-item">
+                    <div class="alert-content">
+                        <div class="alert-metric">
+                            <el-icon style="height: 100%;">
+                                <warning />
+                            </el-icon>
+                            触发告警:{{ alert.name }}
+                        </div>
+                        <div class="alert-metric">
+                            {{ metricMap[alert.metric] }}超出阈值
+                        </div>
+                        <div class="alert-details">
+                            <span class="alert-threshold">
+                                阈值:{{ alert.threshold }}{{ alert.metric === 'cpu' ? '%' : 'MB' }}
+                            </span>
+                            <span class="alert-handle">
+                                方案:{{ handleMap[alert.handle] }}
+                            </span>
+                        </div>
+                    </div>
+                </el-alert>
+            </transition-group>
+        </div>
+
         <!-- 系统状态概览 -->
         <div class="system-monitor">
             <el-row :gutter="16" class="chart-grid">
@@ -81,36 +109,79 @@
                                 </div>
                             </template>
 
-                            <el-table :data="processList" v-loading="loading">
-                                <el-table-column prop="name" label="进程名称" min-width="150" />
-                                <el-table-column label="CPU占用">
+                            <el-table ref="treeTableRef" :data="groupedProcessList" row-key="missionId"
+                                :tree-props="{ children: 'processes' }" @row-click="expandMissionRow">
+                                <el-table-column label="" min-width="30">
                                     <template #default="{ row }">
-                                        {{ row.cpu.toFixed(1) }}%
+                                        <span v-if="row.isGroup" class="mission-info">
+                                            任务进程:
+                                        </span>
                                     </template>
                                 </el-table-column>
-                                <el-table-column label="内存占用">
+                                <!-- 创建者信息 -->
+                                <el-table-column label="创建者" width="160">
                                     <template #default="{ row }">
-                                        {{ (row.memory / 1024).toFixed(1) }} MB
+                                        <span v-if="row.isGroup" class="mission-info">
+                                            {{ row.creator }}
+                                        </span>
                                     </template>
                                 </el-table-column>
-                                <el-table-column prop="runtime" label="运行时间">
+                                <!-- 任务信息 -->
+                                <el-table-column label="任务ID" width="160">
                                     <template #default="{ row }">
-                                        {{ formatRuntime(row.startTime) }}
+                                        <span v-if="row.isGroup" class="mission-info">
+                                            {{ row.missionId }}
+                                        </span>
+                                        <span v-else class="mission-info">
+                                            下属子进程:
+                                        </span>
+                                    </template>
+                                </el-table-column>
+                                <!-- 规划信息 -->
+                                <el-table-column label="规划ID" width="160">
+                                    <template #default="{ row }">
+                                        <span v-if="row.isGroup" class="total-cpu" />
+                                        <span v-else>{{ row.planId }}</span>
+                                    </template>
+                                </el-table-column>
+                                <!-- CPU占用 -->
+                                <el-table-column label="CPU占用" width="160">
+                                    <template #default="{ row }">
+                                        <span v-if="row.isGroup" class="total-cpu">
+                                            {{ row.totalCPU.toFixed(1) }}%
+                                        </span>
+                                        <span v-else>{{ row.cpu.toFixed(1) }}%</span>
                                     </template>
                                 </el-table-column>
-                                <el-table-column label="磁盘IO">
+
+                                <!-- 内存占用 -->
+                                <el-table-column label="内存占用" width="160">
                                     <template #default="{ row }">
-                                        ↑{{ row.io.read }} ↓{{ row.io.write }} KB/s
+                                        <span v-if="row.isGroup" class="total-mem">
+                                            {{ (row.totalMemory / 1024).toFixed(1) }} GB
+                                        </span>
+                                        <span v-else>{{ (row.memory / 1024).toFixed(1) }} GB</span>
+                                    </template>
+                                </el-table-column>
+                                <!-- 运行时间 -->
+                                <el-table-column label="运行时间" width="160">
+                                    <template #default="{ row }">
+                                        {{ formatRuntime(row.startTime) }}
                                     </template>
                                 </el-table-column>
-                                <el-table-column label="操作" width="150">
+                                <!-- 操作 -->
+                                <el-table-column label="操作" width="300">
                                     <template #default="{ row }">
-                                        <el-button size="small" @click="pauseProcess(row.pid)">
-                                            暂停
-                                        </el-button>
-                                        <el-button size="small" type="danger" @click="killProcess(row.pid)">
-                                            终止
-                                        </el-button>
+                                        <div v-if="row.processes" class="group-actions">
+                                            <el-button size="small" @click.stop="pauseMission(row.missionId)"
+                                                :disabled="row.status === 'paused'">
+                                                {{ row.status === 'paused' ? '已暂停' : '暂停任务' }}
+                                            </el-button>
+                                            <el-button size="small" type="danger"
+                                                @click.stop="stopMission(row.missionId)">
+                                                终止任务
+                                            </el-button>
+                                        </div>
                                     </template>
                                 </el-table-column>
                             </el-table>
@@ -136,7 +207,8 @@
                             </el-table-column>
                             <el-table-column label="操作" width="80">
                                 <template #default="{ row }">
-                                    <el-button type="danger" size="small" @click="deleteAlert(row.id)">删除</el-button>
+                                    <el-button type="danger" size="small"
+                                        @click.stop="deleteAlert(row.name)">删除</el-button>
                                 </template>
                             </el-table-column>
                         </el-table>
@@ -146,7 +218,7 @@
         </div>
         <!-- 告警创建模态框 -->
         <el-dialog v-model="alertDialogVisible" title="创建告警规则">
-            <el-form :model="newAlert" :rules="alertRules" ref="alertForm">
+            <el-form :model="newAlert" :rules="alertRules" ref="createAlertForm">
                 <el-form-item label="规则名称" prop="name">
                     <el-input v-model="newAlert.name" />
                 </el-form-item>
@@ -170,16 +242,17 @@
                     <el-input v-model.number="newAlert.threshold">
                         <template #append>
                             <span v-if="newAlert.metric === 'cpu'">%</span>
-                            <span v-else>GB</span>
+                            <span v-else>MB</span>
                         </template>
                     </el-input>
                 </el-form-item>
 
-                <el-form-item label="处理方案" prop="action">
-                    <el-select v-model="newAlert.action">
-                        <el-option label="终止高占用进程" value="1" />
-                        <el-option label="终止最新进程" value="2" />
-                        <el-option label="不做处理" value="3" />
+                <el-form-item label="处理方案" prop="handle">
+                    <el-select v-model="newAlert.handle">
+                        <el-option label="不做处理" value="noAction" />
+                        <el-option label="关闭最新进程" value="closeLatest" />
+                        <el-option label="关闭CPU占用最高进程" value="closeHighestCpu" />
+                        <el-option label="关闭MEM占用最高进程" value="closeHighestMem" />
                     </el-select>
                 </el-form-item>
             </el-form>
@@ -200,10 +273,10 @@
                     {{ metricMap[selectedAlert?.metric] }}
                 </el-descriptions-item>
                 <el-descriptions-item label="阈值">
-                    {{ selectedAlert?.threshold }}{{ selectedAlert?.metric === 'cpu' ? '%' : 'GB' }}
+                    {{ selectedAlert?.threshold }}{{ selectedAlert?.metric === 'cpu' ? '%' : 'MB' }}
                 </el-descriptions-item>
                 <el-descriptions-item label="处理方案">
-                    {{ actionMap[selectedAlert?.action] }}
+                    {{ handleMap[selectedAlert?.handle] }}
                 </el-descriptions-item>
                 <!-- <el-descriptions-item label="创建时间">
                     {{ new Date(selectedAlert?.createTime).toLocaleString() }}
@@ -215,9 +288,11 @@
 
 <script setup>
 import { ref, computed, onMounted, onBeforeUnmount, reactive } from 'vue'
+import { CaretRight, CaretBottom, Warning } from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
 import LineChart from '@/views/components/monitor/LineChart.vue'
 import PieChart from '@/views/components/monitor/PieChart.vue'
+import { postData, getData } from '@/api/axios'
 
 // 模拟系统状态数据
 const systemStatus = ref({
@@ -232,7 +307,79 @@ const cpuHistory = ref([])
 const memoryHistory = ref([])
 const processHistory = ref([])
 const processList = ref([])
-const loading = ref(false)
+// 任务、进程表格的引用
+const treeTableRef = ref(null)
+
+// 为方便以任务分组显示,将process分组组织
+const groups = new Map()
+const groupedProcessList = computed(() => {
+    // 初始化每个mission的CPU和MEM总和
+    groups.forEach(group => {
+        group.totalCPU = 0
+        group.totalMemory = 0
+        group.processes = []
+    })
+    const currentMissionIds = new Set()
+    // 处理原始数据分组
+    processList.value.forEach(process => {
+        currentMissionIds.add(process.missionId)
+        if (!groups.has(process.missionId)) {
+            groups.set(process.missionId, {
+                missionId: process.missionId,
+                creator: process.user,
+                totalCPU: 0,
+                totalMemory: 0,
+                processes: [],
+                isGroup: true,
+                status: 'running',
+                startTime: process.startTime,
+                expanded: false,
+                remain: 5,
+            })
+        }
+        const group = groups.get(process.missionId)
+        group.totalCPU += process.cpu
+        group.totalMemory += process.memory
+        group.processes.push({
+            ...process,
+            isGroup: false
+        })
+
+        // 保留最早启动时间
+        if (new Date(process.startTime) < new Date(group.startTime)) {
+            group.startTime = process.startTime
+        }
+    })
+    Object.keys(groups).forEach(missionId => {
+        if (!currentMissionIds.has(missionId)) {
+            groups[missionId].remain -= 1
+            if (groups[missionId].remain <= 0) {
+                delete groups[missionId]
+            }
+        }
+    })
+    console.log(Array.from(groups.values()))
+    return Array.from(groups.values())
+})
+
+const toggleRowExpansion = (row) => {
+    if (!treeTableRef.value) return
+
+    // 通过 Element Plus 的实例方法操作
+    treeTableRef.value.toggleRowExpansion(row)
+}
+
+const expandMissionRow = (row, column, event) => {
+    if (row.isGroup) {
+        // 阻止事件冒泡到父元素
+        event.stopPropagation()
+        row.expanded = !row.expanded
+        toggleRowExpansion(row)
+    }
+}
+
+
+const loading = ref(true)
 const activeProcessCount = computed(() => systemStatus.value.processes)
 const systemStats = computed(() => [
     { title: 'CPU', value: `${systemStatus.value.cpu}%` },
@@ -244,13 +391,23 @@ const systemStats = computed(() => [
 // 告警相关数据
 const alerts = ref([])
 const alertDialogVisible = ref(false)
-const newAlert = reactive({
+const initAlert = {
     name: '',
     level: 'system',
     metric: 'cpu',
     threshold: null,
-    action: '3'
-})
+    handle: 'noAction'
+}
+// 当前触发的告警列表
+const triggeredAlerts = ref([])
+// 关闭告警提示框
+const handleCloseAlert = (alert) => {
+    triggeredAlerts.value = triggeredAlerts.value.filter(a => a.name !== alert.name)
+}
+const newAlert = ref({ ...initAlert })
+//创建告警的模态框
+const createAlertForm = ref(null)
+
 const detailDialogVisible = ref(false)
 const selectedAlert = ref(null)
 const metricMap = {
@@ -259,10 +416,11 @@ const metricMap = {
     disk: '磁盘空间'
 }
 
-const actionMap = {
-    '1': '终止高占用进程',
-    '2': '终止最新进程',
-    '3': '不做任何处理'
+const handleMap = {
+    'noAction': '不做处理',
+    'closeLatest': '关闭最新进程',
+    'closeHighestCpu': '关闭CPU占用最高进程',
+    'closeHighestMem': '关闭MEM占用最高进程',
 }
 
 
@@ -275,7 +433,8 @@ const alertRules = {
         { required: true, message: '请输入阈值', trigger: 'blur' },
         {
             validator: (_, value, callback) => {
-                if (newAlert.metric === 'cpu') {
+                if (newAlert.value.metric === 'cpu') {
+                    console.log(value)
                     return value >= 0 && value <= 100 ? callback() : callback('CPU阈值需在0-100之间')
                 }
                 return value > 0 ? callback() : callback('阈值必须大于0')
@@ -283,15 +442,15 @@ const alertRules = {
             trigger: 'blur'
         }
     ],
-    action: [{ required: true, message: '请选择处理方案', trigger: 'change' }]
+    handle: [{ required: true, message: '请选择处理方案', trigger: 'change' }]
 }
 
 // 显示创建告警规则modal
 const showCreateAlert = () => {
+    newAlert.value = { ...initAlert }
     alertDialogVisible.value = true
 }
 
-
 // 显示告警规则详情
 const showAlertDetail = (row) => {
     selectedAlert.value = row
@@ -299,82 +458,128 @@ const showAlertDetail = (row) => {
 }
 
 // 创建告警
-const createAlert = () => {
-    //post告警规则到后端
+const createAlert = async () => {
+    try {
+        await createAlertForm.value.validate()
+    } catch (error) {
+        ElMessage.error("新建告警表单验证失败,请核对输入")
+        return
+    }
+    for (let i = 0; i < alerts.value.length; i++) {
+        if (alerts.value[i].name == newAlert.value.name) {
+            ElMessage.error("新建告警表单验证失败,禁止创建重名告警")
+            return
+        }
+    }
+    // post告警规则到后端
+    const response = postData('/alert/', {
+        ...newAlert.value,
+        operation: 'create',
+    })
+    console.log((response))
+    //保存告警规则到前端
+    console.log(newAlert)
     alerts.value.push({
-        ...newAlert,
+        ...newAlert.value,
         id: Date.now()
     })
     alertDialogVisible.value = false
 }
 
 // 删除告警
-const deleteAlert = (id) => {
-    alerts.value = alerts.value.filter(a => a.id !== id)
+const deleteAlert = async (name) => {
+    const response = await postData("/alert/", { operation: 'delete', name: name })
+    // 成功删除
+    if (response.status == 'success') {
+        alerts.value = alerts.value.filter(a => a.name !== name)
+    } else {
+        ElMessage.error("删除告警失败: " + name)
+    }
+
+
+    // event.stopPropagation()
 }
 
 const formatRuntime = (timestamp) => {
-    const seconds = Math.floor((Date.now() - timestamp) / 1000)
+    // const isoFormat = timestamp.replace(' ', 'T') + 'Z'
+    const targetTime = new Date(timestamp)
+    const seconds = Math.floor((Date.now() - targetTime.getTime()) / 1000)
     const h = Math.floor(seconds / 3600)
     const m = Math.floor((seconds % 3600) / 60)
-    return `${h}h ${m}m`
+    const s = Math.floor((seconds % 60))
+    return `${h}h ${m}m ${s}s`
 }
 
 const fetchSystemStatus = async () => {
     try {
         // 实际应调用API接口
-        const mockData = {
-            cpu: +(Math.random() * 100).toFixed(2),
-            memory: { used: +(8 + Math.random() * 4).toFixed(2), total: 32 },
-            disk: { used: +(256 + Math.random() * 10).toFixed(2),  total: 512 },
-            processes: processList.value.length
+        // 获取system的详细性能信息
+        const response = await getData("/systemPerformance")
+        loading.value = false
+        console.log(response)
+        systemStatus.value = {
+            cpu: response.data.cpu,
+            memory: { used: response.data.mem_used, total: response.data.mem_total },
+            disk: {
+                // 换算成GB
+                used: Math.round(response.data.disk_used / 10737418.24) / 100,
+                total: Math.round(response.data.disk_total / 10737418.24) / 100,
+            },
+            processes: response.data.processes.length,
         }
-        systemStatus.value = mockData
+        // 获取当前触发的告警
+        triggeredAlerts.value = response.data.triggeredAlerts
+
         cpuHistory.value = [...cpuHistory.value.slice(-29), systemStatus.value.cpu]
         memoryHistory.value = [...memoryHistory.value.slice(-29), systemStatus.value.memory.used]
         processHistory.value = [...processHistory.value.slice(-29), systemStatus.value.processes]
+
+        // 将processes放入进程列表
+        processList.value = []
+        response.data.processes.forEach(p => {
+            processList.value.push({
+                pid: p.pid,
+                missionId: p.missionId,
+                planId: p.planId,
+                algorithm: p.algorithm,
+                cpu: p.cpu,
+                user: p.user,
+                memory: p.mem_used,
+                startTime: p.startTime,
+            })
+        })
+
     } catch (error) {
+        console.log(error)
         ElMessage.error('获取系统状态失败')
     }
 }
 
-const fetchProcessList = async () => {
-    loading.value = true
+const fetchAlerts = async () => {
     try {
-        // 模拟进程数据
-        processList.value = Array.from({ length: 15 }, (_, i) => ({
-            pid: 1000 + i,
-            name: `Process ${i + 1}`,
-            cpu: Math.random() * 100,
-            memory: Math.random() * 1024 * 1024,
-            startTime: Date.now() - Math.random() * 3600000,
-            io: {
-                read: Math.random() * 100,
-                write: Math.random() * 50
-            }
-        }))
-    } finally {
-        loading.value = false
+        const response = await getData('/alert/')
+        response.data.forEach(alert => {
+            alerts.value.push(alert)
+        })
+    } catch (error) {
+        console.log(error)
+        ElMessage.error('获取告警列表失败')
     }
 }
 
-const processAction = async (pid, action) => {
-    try {
-        const res = await fetch(`/api/process/${pid}`, {
-            method: 'POST',
-            body: JSON.stringify({ action })
-        })
-
-        if (!res.ok) throw new Error()
-        ElMessage.success(`${action === 'pause' ? '暂停' : '终止'}成功`)
-        await fetchProcessList()
-    } catch (error) {
-        ElMessage.error('操作失败,请检查权限或进程状态')
+const processAction = async (missionId, action) => {
+    const response = await postData('/calculate/', {
+        command: action,
+        missionId: missionId,
+    })
+    if (response.status !== 'success') {
+        ElMessage.error(`对任务:${missionId} 执行${action === 'pause' ? '暂停' : '停止'}操作失败`)
     }
+    ElMessage.success(`${action === 'pause' ? '暂停' : '终止'}成功`)
 }
 
-const pauseProcess = (pid) => processAction(pid, 'pause')
-const killProcess = (pid) => processAction(pid, 'kill')
+const pauseMission = (missionId) => processAction(missionId, 'pause')
+const stopMission = (missionId) => processAction(missionId, 'stop')
 
 const refreshProcess = () => {
     fetchProcessList()
@@ -382,8 +587,9 @@ const refreshProcess = () => {
 
 onMounted(() => {
     fetchSystemStatus()
-    fetchProcessList()
     updateInterval = setInterval(fetchSystemStatus, 1000)
+    // 仅获取一次alert,当多个管理员登陆时可能出现问题
+    fetchAlerts()
 })
 
 onBeforeUnmount(() => {
@@ -589,5 +795,210 @@ onBeforeUnmount(() => {
         color: #303133;
         padding-left: 20px !important;
     }
+
+    .sub-processes {
+        padding: 8px 0;
+
+        .sub-process {
+            display: flex;
+            align-items: center;
+            padding: 4px 0;
+
+            >span {
+                margin-right: 20px;
+
+                &.pid {
+                    width: 80px;
+                    color: #666;
+                }
+
+                &.cpu {
+                    width: 80px;
+                    color: #67c23a;
+                }
+
+                &.memory {
+                    width: 100px;
+                    color: #409eff;
+                }
+
+                &.time {
+                    color: #909399;
+                }
+            }
+        }
+    }
+}
+
+.mission-header {
+    padding: 8px 0;
+}
+
+.process-detail {
+    display: flex;
+    align-items: center;
+
+    .pid {
+        width: 80px;
+        color: #666;
+    }
+
+    .algorithm {
+        flex: 1;
+        color: #409eff;
+    }
+}
+
+.mission-info {
+    font-weight: 600;
+    color: #303133;
+    margin-bottom: 4px;
+}
+
+.total-cpu {
+    font-weight: 600;
+    color: #67c23a;
+}
+
+.total-mem {
+    font-weight: 600;
+    color: #409eff;
+}
+
+.mission-actions {
+    display: flex;
+    gap: 8px;
+
+    .el-button {
+        flex: 1;
+        padding: 8px 12px;
+    }
+}
+
+:deep(.el-table__row--level-0) {
+    background-color: #f8faff;
+
+    td {
+        background-color: inherit !important;
+        border-bottom: 2px solid #ebeef5;
+    }
+}
+
+// 自定义展开图标布局
+.mission-header-wrapper {
+    display: flex;
+    align-items: center;
+    width: 100%;
+
+    .expand-icon {
+        margin-right: 8px;
+        transition: transform 0.2s;
+        cursor: pointer;
+        color: #909399;
+        font-size: 14px;
+
+        &.is-expanded {
+            transform: rotate(90deg);
+        }
+    }
+
+    .mission-header {
+        flex: 1;
+        min-width: 0; // 防止内容溢出
+    }
+}
+
+// 覆盖默认表格样式
+:deep(.el-table) {
+    .el-table__expand-icon {
+        // 隐藏默认展开箭头
+        display: none;
+    }
+
+    // 调整行高适应新布局
+    .el-table__row--level-0 {
+        td {
+            padding: 12px 0;
+        }
+    }
+}
+
+.alert-notifications {
+    position: fixed;
+    padding: 0px 4px;
+    top: 20px;
+    right: 20px;
+    z-index: 9999;
+    max-height: calc(100vh - 40px);
+    overflow-y: auto;
+
+    .alert-item {
+        width: 320px;
+        margin-bottom: 12px;
+        border: 1px solid #f3d19e; // 调整为浅橙色边框
+        border-radius: 8px;
+        background-color: #fffbe6; // 浅黄色背景
+        box-shadow: 0 2px 12px rgba(230, 162, 60, 0.2); // 橙色系阴影
+        transition: all 0.3s;
+
+        &:hover {
+            transform: translateX(-4px);
+            background-color: #fff8dc; // 悬停时稍深的背景
+        }
+
+        .alert-content {
+            .alert-metric {
+                color: #e6a23c; // 主警告色
+                font-weight: 600;
+                display: flex;
+                align-items: center; // 确保flex容器垂直居中
+                height: 32px; // 添加固定高度
+                line-height: 32px; // 保持行高与高度一致
+
+                .el-icon {
+                    color: #e6a23c; // 图标颜色同步
+                    margin-right: 8px;
+                    font-size: 18px; // 调大图标尺寸
+                    vertical-align: middle; // 添加垂直对齐
+                    position: relative;
+                    top: -0.5px; // 微调图标位置
+                }
+
+                &>span {
+                    display: inline-flex;
+                    align-items: center;
+                    height: 100%;
+                    line-height: normal; // 重置行高
+                }
+            }
+
+            .alert-details {
+                span {
+                    &.alert-threshold {
+                        color: #d48836; // 深橙色
+                    }
+
+                    &.alert-handle {
+                        color: #b88230; // 棕橙色
+                    }
+                }
+            }
+        }
+
+        // 覆盖element默认样式
+        :deep(.el-alert__title) {
+            color: #d48836; // 标题使用深橙色
+            font-size: 14px;
+        }
+
+        // 关闭按钮颜色调整
+        :deep(.el-alert__closebtn) {
+            color: #d48836;
+
+            &:hover {
+                color: #e6a23c;
+            }
+        }
+    }
 }
 </style>