Jerry Liang 1 год назад
Родитель
Сommit
736e1680e4

+ 9 - 1
service/domain/sitesAnalysisDomain.go

@@ -24,7 +24,7 @@ type SiteAnalysisInfo struct {
 
 type SiteAnalysisOptions struct {
 	EnableTimeline bool `bson:"enableTimeline" json:"enableTimeline"`
-	// EnableRuntime  bool `bson:"enableRuntime" json:"enableRuntime"`
+	EnableRuntime  bool `bson:"enableRuntime" json:"enableRuntime"`
 }
 
 type SiteAnalysisConfigWrite struct {
@@ -55,6 +55,7 @@ type TaskSiteAnalysis struct {
 
 type SiteAnalysisData struct {
 	Timeline []TimelineItem `bson:"timeline" json:"timeline"`
+	Runtime  []RuntimeItem  `bson:"runtime" json:"runtime"`
 }
 
 /************** Timeline **************/
@@ -75,3 +76,10 @@ type TimelineItem struct {
 	Info *string      `bson:"info" json:"info"` // 时间点的说明信息
 	Data *string      `bson:"data" json:"data"` // 时间点的详细数据 JSON string
 }
+
+/************** Runtime **************/
+
+type RuntimeItem struct {
+	Time     string `bson:"time" json:"time"`         // YYYY-MM-DD HH:mm:SS
+	Data_201 string `bson:"data_201" json:"data_201"` // JSON数据
+}

+ 29 - 0
service/jobs/analysisSiteData.go

@@ -16,11 +16,13 @@ import (
 type SiteDataSet struct {
 	Log_0    []domain.MqttLogItem
 	Log_101  []domain.MqttLogItem
+	Log_201  []domain.MqttLogItem
 	Log_357  []domain.MqttLogItem
 	EventLog []domain.EventAlarmItem
 	AlarmLog []domain.EventAlarmItem
 }
 
+// 分析站点时间线数据
 func AnalysisSiteTimeline(id primitive.ObjectID, data SiteDataSet) error {
 	var timelineItemList []domain.TimelineItem
 
@@ -179,3 +181,30 @@ func timelineSlice(list *[]domain.TimelineItem) {
 		return ti.Before(tj)
 	})
 }
+
+// 分析站点运行实时数据
+func AnalysisSiteRuntime(id primitive.ObjectID, data SiteDataSet) error {
+	var runtimeItemList []domain.RuntimeItem
+
+	// 分析offline
+	for _, log := range data.Log_201 {
+		_, err := time.Parse(time.DateTime, log.LogTime)
+		if err != nil {
+			zap.L().Error("201日志时间格式错误", zap.String("time", log.LogTime))
+			return err
+		}
+		runtimeItem := domain.RuntimeItem{
+			Time:     log.LogTime,
+			Data_201: log.LogData,
+		}
+		runtimeItemList = append(runtimeItemList, runtimeItem)
+	}
+
+	// 将运行时数据存入数据库
+	if err := repositories.InsertSiteAnalysisRuntime(id, runtimeItemList); err != nil {
+		zap.L().Error("插入站点分析runtime数据失败", zap.String("id", id.String()))
+		return err
+	}
+
+	return nil
+}

+ 33 - 3
service/repositories/mongo/sitesAnalysisRepo.go

@@ -104,6 +104,9 @@ func InsertSiteAnalysisConfig(writeConfig domain.SiteAnalysisConfigWrite) (primi
 	if writeConfig.Options.EnableTimeline {
 		config.Item = append(config.Item, "时间线")
 	}
+	if writeConfig.Options.EnableRuntime {
+		config.Item = append(config.Item, "运行时")
+	}
 
 	collection := database.GetCollection("SiteAnalysisConfig")
 
@@ -133,6 +136,9 @@ func UpdateSiteAnalysisConfig(id primitive.ObjectID, writeConfig domain.SiteAnal
 	if writeConfig.Options.EnableTimeline {
 		config.Item = append(config.Item, "时间线")
 	}
+	if writeConfig.Options.EnableRuntime {
+		config.Item = append(config.Item, "运行时")
+	}
 
 	collection := database.GetCollection("SiteAnalysisConfig")
 
@@ -258,9 +264,33 @@ func InsertSiteAnalysisTimeline(id primitive.ObjectID, timeline []domain.Timelin
 	update := bson.M{
 		// 如果存在就更新非progress字段
 		"$set": bson.M{
-			"data": bson.M{
-				"timeline": timeline,
-			},
+			"data.timeline": timeline,
+		},
+	}
+
+	opts := options.Update().SetUpsert(true)
+
+	_, err := collection.UpdateOne(ctx, filter, update, opts)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// 插入分析结果runtime数据
+func InsertSiteAnalysisRuntime(id primitive.ObjectID, runtime []domain.RuntimeItem) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection("SiteAnalysisConfig")
+
+	// _, err := collection.InsertOne(ctx, config)
+	filter := bson.M{"_id": id} // 按ID匹配
+	update := bson.M{
+		// 如果存在就更新非progress字段
+		"$set": bson.M{
+			"data.runtime": runtime,
 		},
 	}
 

+ 1 - 0
service/repositories/repositories.go

@@ -53,6 +53,7 @@ var UpdateSiteAnalysisResult = mongo.UpdateSiteAnalysisResult
 var DeleteSiteAnalysisConfig = mongo.DeleteSiteAnalysisConfig
 var InsertSiteAnalysisData = mongo.InsertSiteAnalysisData
 var InsertSiteAnalysisTimeline = mongo.InsertSiteAnalysisTimeline
+var InsertSiteAnalysisRuntime = mongo.InsertSiteAnalysisRuntime
 
 /*************** PGSQL ***************/
 

+ 2 - 0
service/services/sitesAnalysisServices.go

@@ -32,6 +32,7 @@ func InsertSiteAnalysisConfig(writeConfig domain.SiteAnalysisConfigWrite) error
 		TimePeriod: writeConfig.TimePeriod,
 		Options: domain.SiteAnalysisOptions{
 			EnableTimeline: writeConfig.Options.EnableTimeline,
+			EnableRuntime:  writeConfig.Options.EnableRuntime,
 		},
 	})
 
@@ -51,6 +52,7 @@ func UpdateSiteAnalysisConfig(id primitive.ObjectID, writeConfig domain.SiteAnal
 		TimePeriod: writeConfig.TimePeriod,
 		Options: domain.SiteAnalysisOptions{
 			EnableTimeline: writeConfig.Options.EnableTimeline,
+			EnableRuntime:  writeConfig.Options.EnableRuntime,
 		},
 	})
 

+ 23 - 3
service/taskQueue/taskSiteAnalysis.go

@@ -62,6 +62,8 @@ var executeTaskSiteAnalysis = func(ctx context.Context, payload domain.TaskSiteA
 	// 将 endTime 调整到当天的 23:59:59
 	endTime = endTime.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
 
+	fmt.Println(payload.Options)
+
 	if payload.Options.EnableTimeline {
 		// 时间线展示功能
 		// Site 时间线生成逻辑:
@@ -125,9 +127,27 @@ var executeTaskSiteAnalysis = func(ctx context.Context, payload domain.TaskSiteA
 		}
 	}
 
-	// if payload.Options.EnableRuntime {
-	// 	// 实时数据展示
-	// }
+	if payload.Options.EnableRuntime {
+		// 实时数据展示
+
+		// 查询MQTT日志201
+		fmt.Println("查询MQTT日志201")
+		logList_201, err := repositories.QueryMqttLogListOnTimeRange(payload.Sn, domain.MQTT_LOG_201, startTime, endTime)
+		if err != nil {
+			zap.L().Error("查询MQTT日志201失败")
+			return err
+		}
+
+		// 分析数据,并将结果写入数据库
+		fmt.Println("分析数据,并将结果写入数据库")
+		err = jobs.AnalysisSiteRuntime(payload.Id, jobs.SiteDataSet{
+			Log_201: logList_201,
+		})
+		if err != nil {
+			zap.L().Error("站点运行时数据分析失败", zap.String("sn", payload.Sn))
+			return err
+		}
+	}
 
 	return nil
 }

+ 6 - 0
src/service/sitesAnalysis/interface.ts

@@ -7,6 +7,7 @@ export interface AnalysisListFilter {
 
 export interface AnalysisConfigOptions {
   enableTimeline: boolean
+  enableRuntime: boolean
 }
 
 export interface AnalysisConfig {
@@ -30,6 +31,7 @@ export interface AnalysisItemInfoPage extends ResPage<AnalysisItemInfo[]> {}
 
 export interface AnalysisData {
   timeline: TimelineItem[]
+  runtime: RuntimeItem[]
 }
 
 export interface AnalysisResultData {
@@ -44,3 +46,7 @@ export interface TimelineItem {
   info?: string
   data?: string
 }
+export interface RuntimeItem {
+  time: string
+  data_201: string
+}

+ 8 - 2
src/views/sites-analysis/ModalData.vue

@@ -13,11 +13,16 @@
       </NFormItem>
 
       <NCollapse :trigger-areas="['arrow']" class="p-4">
-        <NCollapseItem :disable="!form.options.enableTimeline" title="时间线">
+        <NCollapseItem :disable="!form.options.enableTimeline" title="时间线数据">
           <template #header-extra>
             <NSwitch v-model:value="form.options.enableTimeline" size="small" />
           </template>
         </NCollapseItem>
+        <NCollapseItem :disable="!form.options.enableRuntime" title="运行时数据">
+          <template #header-extra>
+            <NSwitch v-model:value="form.options.enableRuntime" size="small" />
+          </template>
+        </NCollapseItem>
       </NCollapse>
     </NForm>
     <div class="flex justify-end gap-x-4">
@@ -64,7 +69,8 @@ const [form, setUserForm, reset, updateInitialState] = useResettableReactive<Ana
   timePeriod: null,
 
   options: {
-    enableTimeline: true
+    enableTimeline: true,
+    enableRuntime: true
   }
 })
 

+ 6 - 5
src/views/sites-analysis/component/siteRuntimeChart.vue

@@ -40,10 +40,10 @@ interface ChartDataType {
 // }
 
 // 计算时间相关信息
-const startTime = props.data[0].time
-const startTimeDayjs = dayjs(props.data[0].time)
-const endTime = props.data[props.data.length - 1].time
-const endTimeDayjs = dayjs(props.data[props.data.length - 1].time)
+const startTime = (props.data.length > 0) ? props.data[0].time : "2025-01-01"
+const startTimeDayjs = dayjs(startTime)
+const endTime = (props.data.length > 0) ? props.data[props.data.length - 1].time : "2025-01-01"
+const endTimeDayjs = dayjs(endTime)
 const diffSeconds = endTimeDayjs.diff(startTimeDayjs, 'second');
 
 // 计算某个点的时间 YYYY-MM-DD HH:mm:ss
@@ -232,7 +232,8 @@ const handleDragEnd = () => {
 onMounted(() => {
   runtimeChart = new Chart({
     container: chartRuntime.value!,
-    autoFit: true
+    autoFit: true,
+    height: 100 * Object.keys(runtimeChartOpt.childrenName).length,
   });
 
   runtimeChart.options(getChartOpt(timeRange.value))

+ 40 - 44
src/views/sites-analysis/component/siteTimeline.vue

@@ -12,10 +12,11 @@
 import { ref } from 'vue';
 import { NCard, NTimeline, NTimelineItem } from 'naive-ui'
 import type { TimelineItemProps, TimelineItemSlots } from 'naive-ui'
-import { getAnalysisData } from '@/service/sitesAnalysis';
+import type { TimelineItem } from '@/service/sitesAnalysis/interface';
 
 const props = defineProps<{
   id: string
+  data: TimelineItem[]
 }>()
 
 type TimelineItemType = 'info' | 'event' | 'alarm'
@@ -70,52 +71,47 @@ const timelineComponents = computed<TimelineComponent[]>(() => {
 });
 
 onMounted(async () => {
-  // 获取Timeline数据
-  const analysisData = await getAnalysisData(props.id)
-  console.log({ analysisData })
-  if (analysisData.data !== undefined) {
-    timeLine.value = analysisData.data.timeline.map<TimelineItemConfig>(item => {
-      let type: TimelineItemType = "info"
-      let timelineType: NTimelineItemType = undefined
-      let title = ""
-      switch (item.type) {
-        case 'offline':
-          title = "离线"
-          break
-        case 'online':
-          title = "上线"
-          timelineType = "info"
-          break
-        case 'mode_change':
-          title = item.info ?? "未知"
-          break
-        case 'event':
-          type = 'event'
-          title = item.info ?? "未知"
-          timelineType = "warning"
-          break
-        case 'alarm':
-          type = 'alarm'
-          title = item.info ?? "未知"
-          timelineType = "error"
-          break
-        default:
-          title = "未知"
-          break
-      }
-      return {
-        type,
+  timeLine.value = props.data.map<TimelineItemConfig>(item => {
+    let type: TimelineItemType = "info"
+    let timelineType: NTimelineItemType = undefined
+    let title = ""
+    switch (item.type) {
+      case 'offline':
+        title = "离线"
+        break
+      case 'online':
+        title = "上线"
+        timelineType = "info"
+        break
+      case 'mode_change':
+        title = item.info ?? "未知"
+        break
+      case 'event':
+        type = 'event'
+        title = item.info ?? "未知"
+        timelineType = "warning"
+        break
+      case 'alarm':
+        type = 'alarm'
+        title = item.info ?? "未知"
+        timelineType = "error"
+        break
+      default:
+        title = "未知"
+        break
+    }
+    return {
+      type,
+      time: item.time.toString(),
+      title,
+      content: "",
+      props: {
         time: item.time.toString(),
         title,
-        content: "",
-        props: {
-          time: item.time.toString(),
-          title,
-          type: timelineType,
-        }
+        type: timelineType,
       }
-    })
-  }
+    }
+  })
 })
 
 </script>

+ 49 - 3
src/views/sites-analysis/index.vue

@@ -2,10 +2,10 @@
   <div class="flex flex-col gap-y-2 p-4">
     <NTabs type="line" animated>
       <NTabPane name="timeline" tab="时间线">
-        <SiteTimeline :id="analysisID" />
+        <SiteTimeline v-if="showData" :id="analysisID" :data="timelineData ?? []" />
       </NTabPane>
       <NTabPane name="runtime" tab="运行时">
-        <SiteRuntimeChart />
+        <SiteRuntimeChart v-if="showData" :data="transformRuntimeData(runtimeData)" />
       </NTabPane>
     </NTabs>
   </div>
@@ -15,7 +15,10 @@
 import { useRoute } from 'vue-router'
 import { NTabs, NTabPane } from 'naive-ui'
 import SiteTimeline from './component/siteTimeline.vue'
-import SiteRuntimeChart from './component/siteRuntimeChart.vue'
+import SiteRuntimeChart, { type RuntimeDataType } from './component/siteRuntimeChart.vue'
+import type { TimelineItem, RuntimeItem } from '@/service/sitesAnalysis/interface';
+import { getAnalysisData } from '@/service/sitesAnalysis';
+import { time } from 'console';
 
 defineOptions({
   name: 'SiteAnalysis',
@@ -25,4 +28,47 @@ const route = useRoute()
 
 // 获取 name
 const analysisID = route.params.name as string
+
+const message = useMessage()
+
+const showData = ref(false)
+
+let timelineData: TimelineItem[] | undefined = undefined
+let runtimeData: RuntimeItem[] | undefined = undefined
+
+// 转换并解析运行时json数据
+const transformRuntimeData = (data?: RuntimeItem[]): RuntimeDataType[] => {
+  if (data === undefined) return []
+  return data.map(d => {
+    const obj = JSON.parse(d.data_201);
+    const dataArea = obj['dataArea']
+    if (dataArea === null) {
+      return {
+        time: d.time
+      }
+    }
+    return {
+      time: d.time,
+      ...(dataArea['mode'] != null && { RunMode: dataArea['mode'] }),
+      ...(dataArea['run_status'] != null && { RunStatus: dataArea['run_status'] }),
+      ...(dataArea['elecnet_state'] != null && { GridStatus: dataArea['elecnet_state'] })
+    }
+  })
+}
+
+onMounted(async () => {
+  // 获取Timeline数据
+  const analysisData = await getAnalysisData(analysisID)
+  console.log({ analysisData })
+  if (analysisData.data !== undefined) {
+    timelineData = analysisData.data.timeline
+    runtimeData = analysisData.data.runtime
+  }
+
+  if (timelineData !== undefined && runtimeData !== undefined) {
+    showData.value = true
+  } else {
+    message.error("分析数据获取失败!")
+  }
+})
 </script>