package controllers import ( "ColdVerify_local/lib" "ColdVerify_local/logs" "ColdVerify_local/models/Device" "ColdVerify_local/models/Task" "ColdVerify_local/models/VerifyTemplate" "errors" "fmt" "math" "math/rand" "net/http" "sort" "strconv" "strings" "time" "github.com/beego/beego/v2/client/orm" beego "github.com/beego/beego/v2/server/web" ) type TaskDataHandleController struct { beego.Controller } /* 同区域数据缺失 */ // 测点自检 自动添加缺失终端,取关联绑定终端平均复制 func (c *TaskDataHandleController) SSE_Automatically_add_missing_terminal() { T_task_id := c.GetString("T_task_id") // v26nplogbwt1 T_start := c.GetString("T_start") // 开始测点编号 T_end := c.GetString("T_end") // 结束测点编号 println("T_task_id:", T_task_id, "T_start:", T_start, "T_end:", T_end) c.Ctx.ResponseWriter.Header().Set("Content-Type", "text/event-stream") c.Ctx.ResponseWriter.Header().Set("Cache-Control", "no-cache") c.Ctx.ResponseWriter.Header().Set("Connection", "keep-alive") lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "任务开始......"}) Task_r, err := Task.Read_Task(T_task_id) if err != nil { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "获取任务信息失败!"}) return } // 解析开始/结束编号 var startId, endId int hasRange := false if T_start != "" { startId, _ = strconv.Atoi(T_start) hasRange = true } if T_end != "" { endId, _ = strconv.Atoi(T_end) hasRange = true } if !hasRange || startId <= 0 || endId <= 0 || startId > endId { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "请指定有效的开始和结束测点编号!"}) return } // 获取所有设备,构建T_id -> Device映射 DeviceClassList_r := Device.Read_DeviceClassList_List_id_By_Terminal(Task_r.T_class, false) deviceMap := make(map[int]Device.DeviceClassList) for _, d := range DeviceClassList_r { tId, err := strconv.Atoi(d.T_id) if err == nil { deviceMap[tId] = d } } lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("终端总共:%d 个,检查范围: %03d ~ %03d,正在检查自检探头...", len(DeviceClassList_r), startId, endId)}) // 获取第一个设备用于获取T_remark参考 var firstRemark string if len(DeviceClassList_r) > 0 { firstRemark = DeviceClassList_r[len(DeviceClassList_r)-1].T_remark } // 遍历完整范围,不限于设备列表 for id := startId; id <= endId; id++ { tidStr := fmt.Sprintf("%03d", id) var class_ Device.DeviceClassList device, exists := deviceMap[id] if exists { class_ = device } else { // 虚拟设备:T_sn和T_id保持一致 class_ = Device.DeviceClassList{ T_class: Task_r.T_class, T_id: tidStr, T_sn: tidStr, T_remark: firstRemark, } } // 检查该测点是否已有数据 // 真实设备用实际T_sn查询,虚拟设备用T_id查询 querySn := tidStr if exists { querySn = class_.T_sn } _, cnt := Task.Read_TaskData_ById_List(Task_r.T_task_id, querySn, class_.T_id, "", "", 0, 1) if cnt == 0 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "找到" + class_.T_id + " " + class_.T_remark + " 自检探头"}) // 使用T_id作为T_sn,确保补充的数据sn=编号 device := Device.DeviceClassList{ T_class: class_.T_class, T_id: class_.T_id, T_sn: tidStr, T_remark: class_.T_remark, } c.SetAdjacentDeviceAVGTaskData(T_task_id, device, "", "") // -----------开始平均复制到结束 } } lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 1, Msg: "完成!"}) // Close the connection c.Ctx.ResponseWriter.WriteHeader(http.StatusOK) } // selectReferenceSensor 选择最稳定的传感器作为参考(标准差最小) func selectReferenceSensor(allData map[string]map[string]float32, devices []Device.DeviceClassList) string { var bestId string bestStddev := float32(math.MaxFloat32) for _, dev := range devices { tm := allData[dev.T_id] if tm == nil || len(tm) < 10 { continue } var sum, sumSq float32 var count int for _, v := range tm { sum += v sumSq += v * v count++ } if count < 10 { continue } mean := sum / float32(count) variance := sumSq/float32(count) - mean*mean stddev := float32(math.Sqrt(float64(variance))) if stddev < bestStddev { bestStddev = stddev bestId = dev.T_id } } return bestId } // calcBaselineDiff 计算传感器与参考传感器的基准差值(取前N分钟数据的均值) func calcBaselineDiff(sensorData, refData map[string]float32, allTimes []string, minutes int) float32 { if len(allTimes) == 0 { return 0 } startTime := allTimes[0] st, _ := time.Parse("2006-01-02 15:04", startTime) endTime := st.Add(time.Duration(minutes) * time.Minute) var sum float32 var count int for _, t := range allTimes { ct, _ := time.Parse("2006-01-02 15:04", t) if ct.After(endTime) { break } sp, ok1 := sensorData[t] rp, ok2 := refData[t] if ok1 && ok2 { sum += sp - rp count++ } } if count == 0 { return 0 } return sum / float32(count) } // getDeviceById 根据T_id查找设备 func getDeviceById(devices []Device.DeviceClassList, id string) (Device.DeviceClassList, bool) { for _, d := range devices { if d.T_id == id { return d, true } } return Device.DeviceClassList{}, false } // findNearestTime 在时间列表中找目标时间前后最近的两个时间点 func findNearestTime(allTimes []string, target string) (string, string) { var prev, next string for _, t := range allTimes { if t < target { prev = t } else if t > target && next == "" { next = t } } return prev, next } // 测点数据自检 自动添加缺失数据,取关联绑定终端平均复制 func (c *TaskDataHandleController) SSE_Automatically_add_missing_data() { T_task_id := c.GetString("T_task_id") // v26nplogbwt1 c.Ctx.ResponseWriter.Header().Set("Content-Type", "text/event-stream") c.Ctx.ResponseWriter.Header().Set("Cache-Control", "no-cache") c.Ctx.ResponseWriter.Header().Set("Connection", "keep-alive") lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "任务开始......"}) Task_r, err := Task.Read_Task(T_task_id) if err != nil { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "获取任务信息失败!"}) return } // 时间间隔 s T_saveT := 60 DeviceClassList_list := Device.Read_DeviceClassList_List_id_By_Terminal(Task_r.T_class, false) // 分组统计每个sn的数据数量 snList := Device.JoinDeviceClassListSnToString(DeviceClassList_list) TaskData_Total_GroupBySnId := Task.Read_TaskData_Total_GroupBySnId(Task_r.T_task_id, snList, "", "") if len(TaskData_Total_GroupBySnId) == 0 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 1, Msg: "暂无测点需要自检!"}) } Devicedata_list_MAX := TaskData_Total_GroupBySnId[0].Total TaskData_Total_GroupBySnId_Map := make(map[string]int64) for _, v := range TaskData_Total_GroupBySnId { TaskData_Total_GroupBySnId_Map[v.T_sn] = v.Total } startTime, endTime := Task.Read_TaskData_T_time_T_Min_Max(Task_r.T_task_id, TaskData_Total_GroupBySnId[0].T_sn, TaskData_Total_GroupBySnId[0].T_id, "", "") logs.Println("数据标准数量:", Devicedata_list_MAX) // 选择 数据缺失的终端 for _, DeviceClassList_r := range DeviceClassList_list { total, ok := TaskData_Total_GroupBySnId_Map[DeviceClassList_r.T_sn] if !ok { // 测点自检事再处理 lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: DeviceClassList_r.T_id + " 终端自检数据 , 测点缺失,跳过"}) continue } if Devicedata_list_MAX == total { continue } list, cnt := Task.Read_TaskData_ById_List_AES(Task_r.T_task_id, DeviceClassList_r.T_sn, DeviceClassList_r.T_id, "", "", 0, 9999) lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: DeviceClassList_r.T_id + " 终端自检数据 ,数据差值:" + lib.To_string(Devicedata_list_MAX-cnt) + " "}) // 开始结束时间不同,直接执行平均复制到 if startTime != list[0].T_time || endTime != list[len(list)-1].T_time { if Devicedata_list_MAX-cnt != 1 { // -----------开始平均复制到 device := Device.DeviceClassList{ T_class: DeviceClassList_r.T_class, T_id: DeviceClassList_r.T_id, T_sn: DeviceClassList_r.T_sn, T_remark: DeviceClassList_r.T_remark, } c.SetAdjacentDeviceAVGTaskData(T_task_id, device, "", "") // -----------开始平均复制到结束 continue } else { if startTime != list[0].T_time { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: DeviceClassList_r.T_id + " 找到到自检时间点 " + startTime + " ~ " + list[0].T_time + " 开始自检"}) Task.InsertTaskData(Task_r.T_task_id, Task.TaskData_{ T_sn: list[0].T_sn, T_id: list[0].T_id, T_t: list[0].T_t, T_rh: list[0].T_rh, T_time: startTime, }) continue } if endTime != list[len(list)-1].T_time { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: DeviceClassList_r.T_id + " 找到自检时间点 " + list[len(list)-1].T_time + " ~ " + endTime + " 开始自检"}) Task.InsertTaskData(Task_r.T_task_id, Task.TaskData_{ T_sn: list[len(list)-1].T_sn, T_id: list[len(list)-1].T_id, T_t: list[len(list)-1].T_t, T_rh: list[len(list)-1].T_rh, T_time: endTime, }) continue } } } for i := 0; i < len(list)-1; i++ { current := list[i].T_time next := list[i+1].T_time ct, _ := time.Parse("2006-01-02 15:04:05", current) nt, _ := time.Parse("2006-01-02 15:04:05", next) interval := nt.Unix() - ct.Unix() //logs.Debug("时间间隔:", interval, "保存时间:", saveTime) //fmt.Println("当前:", current, "下一个:", next) // 缺一个时间点 补漏 if int(interval) == 2*T_saveT { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: list[i].T_id + " 找到自检时间点 " + current + " ~ " + next + " 开始自检"}) t := ct.Add(time.Second * time.Duration(T_saveT)).Format("2006-01-02 15:04") //时间临时变量 ttt := (list[i].T_t + list[i+1].T_t) / 2 trht := (list[i].T_rh + list[i+1].T_rh) / 2 Task.InsertTaskData(Task_r.T_task_id, Task.TaskData_{ T_sn: list[i].T_sn, T_id: list[i].T_id, T_t: ttt, T_rh: trht, T_time: t, }) continue } // 缺的数据大于一个时间点,执行平均复制到 if int(interval) > T_saveT { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: list[i].T_id + " 找到自检时间点 " + current + " ~ " + next + " 开始自检"}) // -----------开始平均复制到 device := Device.DeviceClassList{ T_class: DeviceClassList_r.T_class, T_id: DeviceClassList_r.T_id, T_sn: DeviceClassList_r.T_sn, T_remark: DeviceClassList_r.T_remark, } c.SetAdjacentDeviceAVGTaskData(T_task_id, device, "", "") // -----------开始平均复制到结束 // 平均复制结束,跳出循环 break } } } lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 1, Msg: "完成!"}) // Close the connection c.Ctx.ResponseWriter.WriteHeader(http.StatusOK) } // 数据持续时间 x 分钟 没有变化 func (c *TaskDataHandleController) SSE_Continuously_unchanged_data() { T_task_id := c.GetString("T_task_id") // v26nplogbwt1 T_timeout, _ := c.GetInt("T_timeout", 30) // 持续时间 c.Ctx.ResponseWriter.Header().Set("Content-Type", "text/event-stream") c.Ctx.ResponseWriter.Header().Set("Cache-Control", "no-cache") c.Ctx.ResponseWriter.Header().Set("Connection", "keep-alive") lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "任务开始......"}) Task_r, err := Task.Read_Task(T_task_id) if err != nil { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "获取任务信息失败!"}) return } // 获取 备注 下面关联设备,数量 DeviceClassList_list := Device.Read_DeviceClassList_List_id_By_Terminal(Task_r.T_class, false) // 选择 数据缺失的终端 var DeleteDeviceClassList []Device.DeviceClassList for _, DeviceClassList_r := range DeviceClassList_list { TaskData_list, _ := Task.Read_TaskData_ById_List_AES(Task_r.T_task_id, DeviceClassList_r.T_sn, DeviceClassList_r.T_id, "", "", 0, 9999) if len(TaskData_list) == 0 { continue } maxCount, start, end := Task.FindUnchangedInterval(TaskData_list) if strings.Contains(DeviceClassList_r.T_remark, "保温箱外环境测点") || strings.Contains(DeviceClassList_r.T_remark, "冷藏库作业口外部环境测点") || strings.Contains(DeviceClassList_r.T_remark, "冷藏库外部环境测点") || strings.Contains(DeviceClassList_r.T_remark, "冷藏柜外部环境测点") || strings.Contains(DeviceClassList_r.T_remark, "冷藏车外部环境测点") || strings.Contains(DeviceClassList_r.T_remark, "仓库室外测点") { continue } if maxCount > T_timeout { Task.DeleteTaskDataByTimeRange(T_task_id, DeviceClassList_r.T_sn, DeviceClassList_r.T_id, "", "") DeleteDeviceClassList = append(DeleteDeviceClassList, DeviceClassList_r) lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "找到" + DeviceClassList_r.T_id + " 探头, " + "最多连续 " + lib.To_string(maxCount) + " 分钟没有变化," + "开始时间:" + start + " 结束时间:" + end + "," + "开始自检"}) } } for _, class_ := range DeleteDeviceClassList { // -----------开始平均复制到 device := Device.DeviceClassList{ T_class: class_.T_class, T_id: class_.T_id, T_sn: class_.T_sn, T_remark: class_.T_remark, } c.SetAdjacentDeviceAVGTaskData(T_task_id, device, "", "") // -----------开始平均复制到结束 } lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 1, Msg: "完成!"}) // Close the connection c.Ctx.ResponseWriter.WriteHeader(http.StatusOK) } // 区间数据校正 (布点区域数据自检) 均匀性布点,产品存放区域测点 区间数据超标校正 超标数据偏移到区间内 func (c *TaskDataHandleController) SSE_Interval_data_correction() { T_task_id := c.GetString("T_task_id") // v26nplogbwt1 c.Ctx.ResponseWriter.Header().Set("Content-Type", "text/event-stream") c.Ctx.ResponseWriter.Header().Set("Cache-Control", "no-cache") c.Ctx.ResponseWriter.Header().Set("Connection", "keep-alive") lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "任务开始......"}) Task_r, err := Task.Read_Task(T_task_id) if err != nil { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "获取任务信息失败!"}) return } 温度控制范围最小值 := float64(lib.To_float32(VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "温度控制范围最小值"))) 温度控制范围最高值 := float64(lib.To_float32(VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "温度控制范围最高值"))) if 温度控制范围最小值 == 0 || 温度控制范围最高值 == 0 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "温度控制范围 标签值不正确!"}) return } // 均匀性布点 产品存放区域测点 部点终端_list := Device.Read_DeviceClassList_List_id_T_remark(Task_r.T_class, "均匀性布点|产品存放区域测点|作业出入口总测点") if len(部点终端_list) <= 2 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "均匀性布点|产品存放区域测点|作业出入口总测点 太少了,至少两条以上!"}) return } 部点终端_sn_list := Device.JoinDeviceClassListSnToString(部点终端_list) var 开始时间, 结束时间, 趋势时间 string 开始时间, 结束时间, 趋势时间 = c.GetStartTimeAndEndTime(Task_r, 部点终端_sn_list, 温度控制范围最高值) if 开始时间 == "" || 结束时间 == "" { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "未找到 开始时间 或 结束时间!"}) return } type AVGClassList struct { T_sn string T_id string T_max float64 T_min float64 T_diff float64 // 最大最小值差异 } fmt.Println("数据准备:", 开始时间, 结束时间, 温度控制范围最小值, 温度控制范围最高值) // -------------------- 温湿度绑定点vga_H -------------------- lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "--- 进行处理 数据!---"}) lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("开始时间:%s 结束时间:%s", 开始时间, 结束时间)}) // 先整体向上偏移 for _, i2 := range 部点终端_list { T_min := Task.Read_TaskData_min(T_task_id, i2.T_sn, i2.T_id, "", "") if T_min < RoundToDecimal(温度控制范围最小值+0.1, 1) { Task.UpdateTaskDataTemperatureAndHumidity(Task_r.T_task_id, i2.T_sn, i2.T_id, "", "", RoundToDecimal(温度控制范围最小值+0.1-T_min, 1), 0) } } // ----------获取保留数据------------ //var 开空开, 保空开 string //var 开空开驼峰, 保空开驼峰 Task.CalculateHumps_R //var valueStrings1, valueStrings2 []string var BWXValueStrings []string if Task_r.T_device_type != "X" { //valueStrings1, valueStrings2, 开空开, 保空开, 开空开驼峰, 保空开驼峰 = c.GetRetainData(Task_r, 部点终端_list, false) } else { BWXValueStrings = c.GetBWXRetainData(Task_r, 部点终端_sn_list, 结束时间, 趋势时间) } // ----------获取保留数据结束------------ var AVGClassList_r []AVGClassList for _, i2 := range 部点终端_list { T_max := Task.Read_TaskData_max(T_task_id, i2.T_sn, i2.T_id, 开始时间, 结束时间) T_min := Task.Read_TaskData_min(T_task_id, i2.T_sn, i2.T_id, "", "") AVGClassList_r = append(AVGClassList_r, AVGClassList{T_sn: i2.T_sn, T_id: i2.T_id, T_max: T_max, T_min: T_min, T_diff: T_max - T_min}) } for _, AVGClassList_i := range AVGClassList_r { if AVGClassList_i.T_max < 温度控制范围最高值 && AVGClassList_i.T_min > 温度控制范围最小值 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "测点 " + lib.To_string(AVGClassList_i.T_id) + " 最大值:" + lib.To_string(AVGClassList_i.T_max) + "℃ " + " 最小值:" + lib.To_string(AVGClassList_i.T_min) + "℃ " + " 符合要求!"}) continue } var vgaca float64 if RoundToDecimal(AVGClassList_i.T_max-AVGClassList_i.T_min, 1) > RoundToDecimal(温度控制范围最高值-温度控制范围最小值-0.4, 1) { // 压缩 diff := RoundToDecimal(AVGClassList_i.T_max-AVGClassList_i.T_min, 1) // 获取压缩度 compress := RoundToDecimal((温度控制范围最高值-温度控制范围最小值-0.6)/diff, 2) // 压缩 Task.UpdateTaskDataTemperatureAndHumidityByGeometric(Task_r.T_task_id, AVGClassList_i.T_sn, AVGClassList_i.T_id, "", "", compress, 1) T_max_compress := RoundToDecimal(AVGClassList_i.T_max*compress, 1) T_min_compress := RoundToDecimal(AVGClassList_i.T_min*compress, 1) // 判断压缩后是否在 温度控制范围最小值-温度控制范围最高值范围内 不做处理 if T_max_compress <= RoundToDecimal(温度控制范围最高值-0.1, 1) && T_min_compress >= RoundToDecimal(温度控制范围最小值+0.1, 1) { 参考值 := RoundToDecimal((温度控制范围最高值+温度控制范围最小值)/2, 1) 中间值 := RoundToDecimal((AVGClassList_i.T_max+AVGClassList_i.T_min)/2, 1) if 参考值 > 中间值 { // 向上偏移 vgaca = 参考值 - 中间值 Task.UpdateTaskDataTemperatureAndHumidity(Task_r.T_task_id, AVGClassList_i.T_sn, AVGClassList_i.T_id, "", "", vgaca, 0) lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "进行处理 数据!" + lib.To_string(AVGClassList_i.T_id) + " 最大值:" + lib.To_string(AVGClassList_i.T_max) + "℃ " + " 最小值:" + lib.To_string(AVGClassList_i.T_min) + "℃ " + " 压缩:" + lib.To_string(compress) + " 压缩后最大值:" + lib.To_string(T_max_compress) + "℃ " + " 压缩后最小值:" + lib.To_string(T_min_compress) + "℃ " + " 向上偏移:" + lib.To_string(vgaca) + "℃ "}) continue } if 参考值 < 中间值 { // 向下偏移 vgaca = 中间值 - 参考值 Task.UpdateTaskDataTemperatureAndHumidity(Task_r.T_task_id, AVGClassList_i.T_sn, AVGClassList_i.T_id, "", "", -vgaca, 0) lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "进行处理 数据!" + lib.To_string(AVGClassList_i.T_id) + " 最大值:" + lib.To_string(AVGClassList_i.T_max) + "℃ " + " 最小值:" + lib.To_string(AVGClassList_i.T_min) + "℃ " + " 压缩:" + lib.To_string(compress) + " 压缩后最大值:" + lib.To_string(T_max_compress) + "℃ " + " 压缩后最小值:" + lib.To_string(T_min_compress) + "℃ " + " 向下偏移:" + lib.To_string(vgaca) + "℃ "}) continue } lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "进行处理 数据!" + lib.To_string(AVGClassList_i.T_id) + " 最大值:" + lib.To_string(AVGClassList_i.T_max) + "℃ " + " 最小值:" + lib.To_string(AVGClassList_i.T_min) + "℃ " + " 压缩:" + lib.To_string(compress) + " 压缩后最大值:" + lib.To_string(T_max_compress) + "℃ " + " 压缩后最小值:" + lib.To_string(T_min_compress) + "℃ "}) continue } // 压缩后仍高于 温度控制范围最高值,向下偏移 if T_max_compress >= 温度控制范围最高值 { vgaca = RoundToDecimal((T_max_compress+T_min_compress)/2-(温度控制范围最高值+温度控制范围最小值)/2, 1) //vgaca = RoundToDecimal(T_max_compress-温度控制范围最高值+0.1, 1) lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "进行处理 数据!" + lib.To_string(AVGClassList_i.T_id) + " 最大值:" + lib.To_string(AVGClassList_i.T_max) + "℃ " + " 最小值:" + lib.To_string(AVGClassList_i.T_min) + "℃ " + " 压缩:" + lib.To_string(compress) + " 压缩后最大值:" + lib.To_string(RoundToDecimal(AVGClassList_i.T_max*compress, 1)) + "℃ " + " 压缩后最小值:" + lib.To_string(RoundToDecimal(AVGClassList_i.T_min*compress, 1)) + "℃ " + " 数据偏差:" + lib.To_string(vgaca) + "℃ " + " 向下偏移:" + lib.To_string(vgaca) + "℃ "}) Task.UpdateTaskDataTemperatureAndHumidity(Task_r.T_task_id, AVGClassList_i.T_sn, AVGClassList_i.T_id, "", "", -vgaca, 0) continue } // 压缩后仍低于 温度控制范围最小值,向上偏移 if T_min_compress <= 温度控制范围最小值 { // 向上偏移 vgaca = RoundToDecimal((温度控制范围最高值+温度控制范围最小值)/2-(T_max_compress+T_min_compress)/2, 1) //vgaca = RoundToDecimal(温度控制范围最小值-AVGClassList_i.T_min*compress+0.1, 1) lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "进行处理 数据!" + lib.To_string(AVGClassList_i.T_id) + " 最大值:" + lib.To_string(AVGClassList_i.T_max) + "℃ " + " 最小值:" + lib.To_string(AVGClassList_i.T_min) + "℃ " + " 压缩:" + lib.To_string(compress) + " 压缩后最大值:" + lib.To_string(RoundToDecimal(AVGClassList_i.T_max*compress, 1)) + "℃ " + " 压缩后最小值:" + lib.To_string(RoundToDecimal(AVGClassList_i.T_min*compress, 1)) + "℃ " + " 数据偏差:" + lib.To_string(vgaca) + "℃ " + " 向上偏移:" + lib.To_string(vgaca) + "℃ "}) Task.UpdateTaskDataTemperatureAndHumidity(Task_r.T_task_id, AVGClassList_i.T_sn, AVGClassList_i.T_id, "", "", vgaca, 0) } continue } else { // 向下偏移 if AVGClassList_i.T_max >= 温度控制范围最高值 { vgaca = RoundToDecimal(AVGClassList_i.T_max-温度控制范围最高值+0.2, 1) lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "进行处理 数据!" + lib.To_string(AVGClassList_i.T_id) + " 最大值:" + lib.To_string(AVGClassList_i.T_max) + "℃ " + " 最小值:" + lib.To_string(AVGClassList_i.T_min) + "℃ " + " 数据偏差:" + lib.To_string(vgaca) + "℃ " + " 向下偏移:" + lib.To_string(vgaca) + "℃ "}) // 偏移 Task.UpdateTaskDataTemperatureAndHumidity(Task_r.T_task_id, AVGClassList_i.T_sn, AVGClassList_i.T_id, "", "", -vgaca, 0) continue } if AVGClassList_i.T_min <= 温度控制范围最小值 { vgaca = RoundToDecimal(温度控制范围最小值-AVGClassList_i.T_min+0.2, 1) lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "进行处理 数据!" + lib.To_string(AVGClassList_i.T_id) + " 最大值:" + lib.To_string(AVGClassList_i.T_max) + "℃ " + " 最小值:" + lib.To_string(AVGClassList_i.T_min) + "℃ " + " 数据偏差: " + lib.To_string(vgaca) + "℃ " + " 向上偏移:" + lib.To_string(vgaca) + "℃ "}) Task.UpdateTaskDataTemperatureAndHumidity(Task_r.T_task_id, AVGClassList_i.T_sn, AVGClassList_i.T_id, "", "", vgaca, 0) } } } // ----------恢复保留数据------------ if Task_r.T_device_type != "X" { //c.SaveRetainData(Task_r, 部点终端_list, valueStrings1, valueStrings2, 开空开, 保空开, 开空开驼峰, 保空开驼峰, 温度控制范围最高值, 60) } else { c.SaveBWXRetainData(Task_r, 部点终端_list, BWXValueStrings, 结束时间, 趋势时间, 60) } // ----------恢复保留数据结束------------ lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 1, Msg: "完成!"}) // Close the connection c.Ctx.ResponseWriter.WriteHeader(http.StatusOK) } // 绑定点与终端比对 (绑定点数据自检) 终端数据为参照物,绑定点数据在终端偏差±1℃,保温箱为±0.5℃ func (c *TaskDataHandleController) SSE_Comparison_between_binding_points_and_terminals() { T_task_id := c.GetString("T_task_id") //T_deviation, _ := c.GetFloat("T_deviation", 1.0) // 默认偏差1.0 T_deviation := 0.5 c.Ctx.ResponseWriter.Header().Set("Content-Type", "text/event-stream") c.Ctx.ResponseWriter.Header().Set("Cache-Control", "no-cache") c.Ctx.ResponseWriter.Header().Set("Connection", "keep-alive") lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "任务开始......"}) Task_r, err := Task.Read_Task(T_task_id) if err != nil { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "获取任务信息失败!"}) return } // 保温箱偏差强制为0.5 if Task_r.T_device_type == "X" { T_deviation = 0.5 } // 温度控制范围 温度控制范围最高值 := float64(lib.To_float32(VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "温度控制范围最高值"))) if 温度控制范围最高值 == 0 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "温度控制范围最高值 标签值不正确!"}) return } 温度控制范围最小值 := float64(lib.To_float32(VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "温度控制范围最小值"))) if 温度控制范围最小值 == 0 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "温度控制范围最小值 标签值不正确!"}) return } // 均匀性布点 (用于获取保留数据) 部点终端_list := Device.Read_DeviceClassList_List_id_T_remark(Task_r.T_class, "均匀性布点|产品存放区域测点") 部点终端_sn_list := Device.JoinDeviceClassListSnToString(部点终端_list) var 开始时间, 结束时间, 趋势时间 string 开始时间, 结束时间, 趋势时间 = c.GetStartTimeAndEndTime(Task_r, 部点终端_sn_list, 温度控制范围最高值) if 开始时间 == "" || 结束时间 == "" { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "未找到 开始时间 或 结束时间!"}) return } // ----------获取保留数据------------ var 开空开, 保空开 string var 开空开驼峰, 保空开驼峰 Task.CalculateHumps_R var BWXValueStrings []string var valueStrings1, valueStrings2 []string if Task_r.T_device_type != "X" { valueStrings1, valueStrings2, 开空开, 保空开, 开空开驼峰, 保空开驼峰 = c.GetRetainData(Task_r, 部点终端_list, true) } else { BWXValueStrings = c.GetBWXRetainData(Task_r, 部点终端_sn_list, 结束时间, 趋势时间) } // ----------获取保留数据结束------------ 监测终端_list := Device.Read_DeviceClassList_List_id_By_Terminal(Task_r.T_class, true) if len(监测终端_list) < 1 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "未找到 监测终端!"}) return } 温湿度绑定点_list := Device.Read_DeviceClassList_List_id_T_remark(Task_r.T_class, "温湿度绑定点") if len(温湿度绑定点_list) < 1 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "未找到 温湿度绑定点!"}) return } // ================= 计算监测终端的整体平均值作为唯一基准 ================= var 监测终端总温度 float64 var 监测终端有效数量 int for _, term := range 监测终端_list { avg := Task.Read_TaskData_AVG(T_task_id, term.T_sn, term.T_id, 开始时间, 结束时间) if avg > 0 { 监测终端总温度 += avg 监测终端有效数量++ } } if 监测终端有效数量 == 0 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "监测终端无有效数据!"}) return } 监测终端整体Avg := RoundToDecimal(监测终端总温度/float64(监测终端有效数量), 1) lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("基准确定:监测终端整体平均值 = %.1f℃, 允许偏差 = ±%.1f℃", 监测终端整体Avg, T_deviation)}) // ================= 逐个遍历温湿度绑定点,超标则随机偏移到 0.5 或 0.4 ================= rand.Seed(time.Now().UnixNano()) // 确保每次随机不同 for _, bindPoint := range 温湿度绑定点_list { 绑定点_id := bindPoint.T_id 绑定点_sn := bindPoint.T_sn 绑定点_remark := bindPoint.T_remark // 1. 计算该绑定点的平均值 绑定点Avg_raw := Task.Read_TaskData_AVG(T_task_id, 绑定点_sn, 绑定点_id, 开始时间, 结束时间) 绑定点Avg := RoundToDecimal(绑定点Avg_raw, 1) // 2. 计算与监测终端基准的偏差 偏差 := RoundToDecimal(math.Abs(绑定点Avg-监测终端整体Avg), 1) lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("测点[%s] 当前平均值:%.1f, 与监测终端偏差:%.1f", 绑定点_remark, 绑定点Avg, 偏差)}) // 3. 判断是否在合规范围内 (偏差 > 0.5 才处理) if 偏差 <= T_deviation { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("测点[%s] 偏差 %.1f <= %.1f,已在合规范围内,跳过不处理", 绑定点_remark, 偏差, T_deviation)}) continue } // 4. 不在合规范围内,需要单独处理该测点 T_max := Task.Read_TaskData_max(T_task_id, 绑定点_sn, 绑定点_id, 开始时间, 结束时间) T_min := Task.Read_TaskData_min(T_task_id, 绑定点_sn, 绑定点_id, 开始时间, 结束时间) var targetAvg float64 var 偏移量 float64 var 动作 string // 随机选择处理后的“目标偏差值”为 0.5 或 0.4 目标偏差 := 0.5 if rand.Intn(2) == 1 { 目标偏差 = 0.4 } if 绑定点Avg > 监测终端整体Avg { // 偏高,需要向下压。目标值 = 基准 + 目标偏差 (例如 3.9 + 0.5 = 4.4 或 3.9 + 0.4 = 4.3) targetAvg = RoundToDecimal(监测终端整体Avg+目标偏差, 1) 偏移量 = RoundToDecimal(绑定点Avg-targetAvg, 1) 动作 = "下移" // 注意:T_deviation 传 0.0,防止底层函数重复叠加偏差 c.SetAverageShiftedDownward(T_task_id, 绑定点_id, 0.0, targetAvg, 绑定点Avg, T_min, T_max, 温度控制范围最小值, 温度控制范围最高值, "", "") } else { // 偏低,需要向上拉。目标值 = 基准 - 目标偏差 (例如 3.9 - 0.5 = 3.4 或 3.9 - 0.4 = 3.5) targetAvg = RoundToDecimal(监测终端整体Avg-目标偏差, 1) 偏移量 = RoundToDecimal(targetAvg-绑定点Avg, 1) 动作 = "上移" // 注意:T_deviation 传 0.0 c.SetAverageShiftedUpward(T_task_id, 绑定点_id, 0.0, targetAvg, 绑定点Avg, T_min, T_max, 温度控制范围最小值, 温度控制范围最高值, "", "") } lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf(" 测点[%s] 偏差:%.1f > %.1f → %s%.1f → 动态目标Avg:%.1f (目标偏差:%.1f)", 绑定点_remark, 偏差, T_deviation, 动作, 偏移量, targetAvg, 目标偏差)}) } // ----------恢复保留数据------------ if Task_r.T_device_type != "X" { c.SaveRetainData(Task_r, 部点终端_list, valueStrings1, valueStrings2, 开空开, 保空开, 开空开驼峰, 保空开驼峰, 温度控制范围最高值, 60) } else { c.SaveBWXRetainData(Task_r, 部点终端_list, BWXValueStrings, 结束时间, 趋势时间, 60) } // ----------恢复保留数据结束------------ lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 1, Msg: "完成!"}) c.Ctx.ResponseWriter.WriteHeader(http.StatusOK) } /* 绑定点与冷热点比对 (冷热点数据自检) 策略:以“温湿度绑定点”数据为参照物基准,部点终端数据在基准偏差±T_deviation℃以内。 若超出,则固定偏移一固定值,使差值降至合规范围内(目标差值随机取 0.7, 0.8, 0.9, 1.0)。 */ func (c *TaskDataHandleController) SSE_Compare_binding_points_with_cold_and_hot_spots() { T_task_id := c.GetString("T_task_id") T_deviation, _ := c.GetFloat("T_deviation", 1.0) c.Ctx.ResponseWriter.Header().Set("Content-Type", "text/event-stream") c.Ctx.ResponseWriter.Header().Set("Cache-Control", "no-cache") c.Ctx.ResponseWriter.Header().Set("Connection", "keep-alive") lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "任务开始......"}) Task_r, err := Task.Read_Task(T_task_id) if err != nil { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "获取任务信息失败!"}) return } if Task_r.T_device_type == "X" { T_deviation = 0.5 } 温度控制范围最高值 := float64(lib.To_float32(VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "温度控制范围最高值"))) if 温度控制范围最高值 == 0 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "温度控制范围最高值 标签值不正确!"}) return } 温度控制范围最小值 := float64(lib.To_float32(VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "温度控制范围最小值"))) if 温度控制范围最小值 == 0 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "温度控制范围最小值 标签值不正确!"}) return } // ========== 【核心修改】:读取任务终端(温湿度绑定点)作为基准 ========== 任务终端_list := make([]Device.DeviceClassList, 0) // 兼容 "温湿度绑定点01" 和 "温湿度绑定点1" 绑定点01_list := Device.Read_DeviceClassList_List_id_T_remark(Task_r.T_class, "温湿度绑定点01|温湿度绑定点1") if len(绑定点01_list) == 0 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "没有找到 温湿度绑定点01 或 温湿度绑定点1 数据!"}) return } 任务终端_list = append(任务终端_list, 绑定点01_list...) lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("基准点1 (%s): %s (%s)", 绑定点01_list[0].T_remark, 绑定点01_list[0].T_id, 绑定点01_list[0].T_sn)}) // 兼容 "温湿度绑定点02" 和 "温湿度绑定点2" 绑定点02_list := Device.Read_DeviceClassList_List_id_T_remark(Task_r.T_class, "温湿度绑定点02|温湿度绑定点2") if len(绑定点02_list) > 0 { 任务终端_list = append(任务终端_list, 绑定点02_list...) lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("基准点2 (%s): %s (%s)", 绑定点02_list[0].T_remark, 绑定点02_list[0].T_id, 绑定点02_list[0].T_sn)}) } else { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "未找到 温湿度绑定点02/2,仅使用 温湿度绑定点01/1 作为基准"}) } // 均匀性布点 产品存放区域测点 (需要被修正的测点) 部点终端_list := Device.Read_DeviceClassList_List_id_T_remark(Task_r.T_class, "均匀性布点|产品存放区域测点") if len(部点终端_list) == 0 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "没有找到 均匀性布点/产品存放区域测点 数据!"}) return } 部点终端_sn_list := Device.JoinDeviceClassListSnToString(部点终端_list) var 开始时间, 结束时间, 趋势时间 string 开始时间, 结束时间, 趋势时间 = c.GetStartTimeAndEndTime(Task_r, 部点终端_sn_list, 温度控制范围最高值) if 开始时间 == "" || 结束时间 == "" { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "未找到 开始时间 或 结束时间!"}) return } // ----------获取保留数据------------ var 开空开, 保空开 string var 开空开驼峰, 保空开驼峰 Task.CalculateHumps_R var BWXValueStrings []string var valueStrings1, valueStrings2 []string if Task_r.T_device_type != "X" { valueStrings1, valueStrings2, 开空开, 保空开, 开空开驼峰, 保空开驼峰 = c.GetRetainData(Task_r, 部点终端_list, true) } else { BWXValueStrings = c.GetBWXRetainData(Task_r, 部点终端_sn_list, 结束时间, 趋势时间) } // ----------获取保留数据结束------------ lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "--- 开始时间: " + 开始时间 + " ---"}) lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "--- 结束时间: " + 结束时间 + " ---"}) // ========== 第一步:计算基准点平均值 ========== lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "------ 第一步:计算温湿度绑定点基准 ------"}) var 任务终端总Avg float64 var 任务终端数量 int for _, dev := range 任务终端_list { avg := Task.Read_TaskData_AVG(T_task_id, dev.T_sn, dev.T_id, 开始时间, 结束时间) if avg != 0 { 任务终端总Avg += avg 任务终端数量++ lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("基准点 %s Avg: %.1f", dev.T_id, RoundToDecimal(avg, 1))}) } } if 任务终端数量 == 0 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "温湿度绑定点无数据!"}) return } 基准点 := RoundToDecimal(任务终端总Avg/float64(任务终端数量), 1) lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("最终基准点(绑定点Avg): %.1f, 允许偏差阈值: ±%.1f", 基准点, T_deviation)}) // ========== 第二步:逐终端对比并修正 ========== lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "------ 第二步:逐终端对比与修正 ------"}) // 随机选择的目标差值(确保修正后严格在合规范围内,且不贴死边界) 可选差值 := []float64{0.7, 0.8, 0.9, 1.0} // 如果是保温箱 (T_deviation = 0.5),目标差值应调整为 0.3, 0.4, 0.5 if Task_r.T_device_type == "X" { 可选差值 = []float64{0.3, 0.4, 0.5} } 修正数量 := 0 跳过数量 := 0 for _, dev := range 部点终端_list { 终端_id := dev.T_id 终端_Avg := Task.Read_TaskData_AVG(T_task_id, dev.T_sn, 终端_id, 开始时间, 结束时间) 终端_Avg = RoundToDecimal(终端_Avg, 1) 差值 := RoundToDecimal(math.Abs(终端_Avg-基准点), 1) if 差值 <= T_deviation { // ✓ 在范围内,不动 跳过数量++ lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("测点 %s Avg:%.1f 基准:%.1f 差值:%.1f ≤ %.1f 合规,不处理", 终端_id, 终端_Avg, 基准点, 差值, T_deviation)}) continue } // 超出范围,随机取目标差值 目标差值 := RoundToDecimal(可选差值[rand.Intn(len(可选差值))], 1) 偏移量 := RoundToDecimal(差值-目标差值, 1) if 终端_Avg > 基准点 { // 偏高,统一下移 Task.UpdateTaskDataTemperatureAndHumidityByGeometricAVG( T_task_id, 终端_id, "", "", -偏移量) newAvg := RoundToDecimal(终端_Avg-偏移量, 1) newDiff := RoundToDecimal(math.Abs(newAvg-基准点), 1) 修正数量++ lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("测点 %s Avg:%.1f→%.1f 下移:%.1f 差值:%.1f→%.1f(目标%.1f) ✓", 终端_id, 终端_Avg, newAvg, 偏移量, 差值, newDiff, 目标差值)}) } else { // 偏低,统一上移 Task.UpdateTaskDataTemperatureAndHumidityByGeometricAVG( T_task_id, 终端_id, "", "", 偏移量) newAvg := RoundToDecimal(终端_Avg+偏移量, 1) newDiff := RoundToDecimal(math.Abs(newAvg-基准点), 1) 修正数量++ lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("测点 %s Avg:%.1f→%.1f 上移:%.1f 差值:%.1f→%.1f(目标%.1f) ✓", 终端_id, 终端_Avg, newAvg, 偏移量, 差值, newDiff, 目标差值)}) } } lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("处理完成: %d 个终端在范围内未处理, %d 个终端已修正", 跳过数量, 修正数量)}) // ========== 校验 ========== lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "------ 校验修正结果 ------"}) for _, dev := range 部点终端_list { 终端_Avg := Task.Read_TaskData_AVG(T_task_id, dev.T_sn, dev.T_id, 开始时间, 结束时间) 终端_Avg = RoundToDecimal(终端_Avg, 1) 差值 := RoundToDecimal(math.Abs(终端_Avg-基准点), 1) status := "✓ 合规" if 差值 > T_deviation { status = "✗ 仍超出" } lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("校验: 测点 %s Avg:%.1f 差值:%.1f %s", dev.T_id, 终端_Avg, 差值, status)}) } // ----------恢复保留数据------------ if Task_r.T_device_type != "X" { c.SaveRetainData(Task_r, 部点终端_list, valueStrings1, valueStrings2, 开空开, 保空开, 开空开驼峰, 保空开驼峰, 温度控制范围最高值, 60) } else { c.SaveBWXRetainData(Task_r, 部点终端_list, BWXValueStrings, 结束时间, 趋势时间, 60) } // ----------恢复保留数据结束------------ lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 1, Msg: "完成!"}) c.Ctx.ResponseWriter.WriteHeader(http.StatusOK) } // 获取相邻2个终端值平均复制到 func (c *TaskDataHandleController) SetAdjacentDeviceAVGTaskData(T_task_id string, device Device.DeviceClassList, StartTime, EndTime string) { DeviceClassListT_remark_r := Device.Read_DeviceClassList_List_id_T_remark(device.T_class, device.T_remark) // 分组统计每个sn的数据数量 snList := Device.JoinDeviceClassListSnToString(DeviceClassListT_remark_r) TaskData_Total_GroupBySnId := Task.Read_TaskData_Total_GroupBySnId(T_task_id, snList, StartTime, EndTime) // 如果同组设备不足,扩大到整个终端类型 if len(TaskData_Total_GroupBySnId) < 2 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "[" + device.T_remark + "]同组数据不足,扩大到全部终端搜索..."}) DeviceClassListT_remark_r = Device.Read_DeviceClassList_List_id_By_Terminal(device.T_class, false) snList = Device.JoinDeviceClassListSnToString(DeviceClassListT_remark_r) TaskData_Total_GroupBySnId = Task.Read_TaskData_Total_GroupBySnId(T_task_id, snList, StartTime, EndTime) } if len(TaskData_Total_GroupBySnId) < 2 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "[" + device.T_remark + "]中没有找到 至少2条 可用数据"}) return } // 获取 备注 下面关联设备,数量 DeviceClassListT_remark_r_list_MAX := TaskData_Total_GroupBySnId[0].Total TaskData_Total_GroupBySnId_Map := make(map[string]int64) for _, v := range TaskData_Total_GroupBySnId { TaskData_Total_GroupBySnId_Map[v.T_sn] = v.Total } var completeDataDeviceClassList []Device.DeviceClassList for _, v := range DeviceClassListT_remark_r { // 排除目标设备本身,只使用其他有完整数据的设备作为平均源 if TaskData_Total_GroupBySnId_Map[v.T_sn] == DeviceClassListT_remark_r_list_MAX && v.T_sn != device.T_sn { completeDataDeviceClassList = append(completeDataDeviceClassList, v) } } if len(completeDataDeviceClassList) < 2 { // 如果只有1个设备有数据,直接用该设备的数据复制(不平均) if len(completeDataDeviceClassList) == 1 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "[" + device.T_remark + "]仅1个完整数据源,直接复制数据"}) sn1, id_str1 := completeDataDeviceClassList[0].T_sn, completeDataDeviceClassList[0].T_id lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: device.T_sn + "," + device.T_id + "开始复制" + fmt.Sprintf("%s,%s", sn1, id_str1)}) List1, _ := Task.Read_TaskData_ById_List_AES(T_task_id, sn1, id_str1, StartTime, EndTime, 0, 9999) // 如果源设备数据被更新为T_id作为T_sn,用T_id回退查询 if len(List1) == 0 { List1, _ = Task.Read_TaskData_ById_List_AES(T_task_id, id_str1, id_str1, StartTime, EndTime, 0, 9999) } if len(List1) == 0 { return } T_saveT := 60 ct, _ := lib.TimeStrToTime(List1[0].T_time) var valueStrings []string for i := 0; i < len(List1); i++ { valueStrings = append(valueStrings, fmt.Sprintf("('%s','%s',%v,%v,'%s')", device.T_sn, device.T_id, List1[i].T_t, List1[i].T_rh, ct.Format("2006-01-02 15:04:05"))) ct = ct.Add(time.Second * time.Duration(T_saveT)) } Task.DeleteTaskDataByTimeRange(T_task_id, device.T_sn, device.T_id, StartTime, EndTime) err := Task.Batch_Adds_TaskData(T_task_id, valueStrings) if err == nil { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("%d/%d", len(valueStrings), len(valueStrings))}) } return } lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "[" + device.T_remark + "]中没有找到 至少2条 完整可用数据"}) return } //twoDevice := getBeforeAndAfter(device.T_id, completeDataDeviceClassList) //sn1, id_str1 := twoDevice[0].T_sn, twoDevice[0].T_id //sn2, id_str2 := twoDevice[1].T_sn, twoDevice[1].T_id // 根据目标T_id选最近的2个设备做平均,避免所有设备数据相同 sn1, id_str1, sn2, id_str2 := findClosestTwo(device.T_id, completeDataDeviceClassList) lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: device.T_sn + "," + device.T_id + "开始平均复制到" + fmt.Sprintf("%s,%s|%s,%s", sn1, id_str1, sn2, id_str2)}) List1, _ := Task.Read_TaskData_ById_List_AES(T_task_id, sn1, id_str1, StartTime, EndTime, 0, 9999) List2, _ := Task.Read_TaskData_ById_List_AES(T_task_id, sn2, id_str2, StartTime, EndTime, 0, 9999) // 如果源设备数据被更新为T_id作为T_sn,用T_id回退查询 if len(List1) == 0 { List1, _ = Task.Read_TaskData_ById_List_AES(T_task_id, id_str1, id_str1, StartTime, EndTime, 0, 9999) } if len(List2) == 0 { List2, _ = Task.Read_TaskData_ById_List_AES(T_task_id, id_str2, id_str2, StartTime, EndTime, 0, 9999) } num := len(List1) if len(List2) < len(List1) { num = len(List2) } if num == 0 { return } T_saveT := 60 //var list []Task.TaskData_ ct, _ := lib.TimeStrToTime(List1[0].T_time) var valueStrings []string for i := 0; i < num; i++ { if List1[i].T_time != List2[i].T_time { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: fmt.Sprintf("%s【%s】、%s【%s】时间不一致", List1[i].T_id, List1[i].T_time, List2[i].T_id, List2[i].T_time)}) return } T_t := (List1[i].T_t + List2[i].T_t) / 2 T_rh := (List1[i].T_rh + List2[i].T_rh) / 2 valueStrings = append(valueStrings, fmt.Sprintf("('%s','%s',%v,%v,'%s')", device.T_sn, device.T_id, T_t, T_rh, ct.Format("2006-01-02 15:04:05"))) ct = ct.Add(time.Second * time.Duration(T_saveT)) } Task.DeleteTaskDataByTimeRange(T_task_id, device.T_sn, device.T_id, StartTime, EndTime) err := Task.Batch_Adds_TaskData(T_task_id, valueStrings) if err == nil { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("%d/%d", len(valueStrings), len(valueStrings))}) } } func getBeforeAndAfter(T_id string, data []Device.DeviceClassList) []Device.DeviceClassList { var result []Device.DeviceClassList var index int for i, d := range data { if d.T_id == T_id { index = i break } } if index == 0 { result = append(result, data[len(data)-1], data[1]) } else if index == len(data)-1 { result = append(result, data[len(data)-2], data[0]) } else { result = append(result, data[index-1], data[index+1]) } return result } // findClosestTwo 根据目标T_id找最近的2个设备,用于平均源选择 func findClosestTwo(targetId string, data []Device.DeviceClassList) (string, string, string, string) { target, _ := strconv.Atoi(targetId) // 按T_id排序 sorted := make([]Device.DeviceClassList, len(data)) copy(sorted, data) for i := 0; i < len(sorted); i++ { for j := i + 1; j < len(sorted); j++ { idI, _ := strconv.Atoi(sorted[i].T_id) idJ, _ := strconv.Atoi(sorted[j].T_id) if idI > idJ { sorted[i], sorted[j] = sorted[j], sorted[i] } } } // 找最近的两个设备 var closest1, closest2 Device.DeviceClassList minDiff1, minDiff2 := 99999, 99999 for _, d := range sorted { id, err := strconv.Atoi(d.T_id) if err != nil { id = 0 } diff := id - target if diff < 0 { diff = -diff } if diff < minDiff1 { minDiff2 = minDiff1 closest2 = closest1 minDiff1 = diff closest1 = d } else if diff < minDiff2 { minDiff2 = diff closest2 = d } } return closest1.T_sn, closest1.T_id, closest2.T_sn, closest2.T_id } func RoundToDecimal(num float64, decimal int) float64 { shift := math.Pow(10, float64(decimal)) return math.Round(num*shift) / shift } /* 数据自检名称:平均值数据自检 测点:柜内所有测点|箱内所有测点|均匀性布点和产品存放区域测点、温湿度绑定点01、温湿度绑定点02 时间:采用“绑定点数据自检”的时间 策略:先执行完成“绑定点数据自检”后,温湿度绑定点01和温湿度绑定点02的数据就作为基准数据不变,温湿度绑定点01平均值与柜内所有测点平均值之间的差异在±0.5℃范围以内,温湿度绑定点02平均值与柜内所有测点平均值之间的差异在±0.5℃范围以内,若柜内所有测点的平均值较大或较小,则对柜内所有测点中平均值较大或较小的测点曲线进行偏移调整,使柜内所有测点的平均值靠近温湿度绑定点的平均值,从而达到绑定点与柜内所有测点的平均值差异在±0.5℃。以上“柜内所有测点”要分别换成“箱内所有测点”、“均匀性布点和产品存放区域测点”;“平均值数据自检”和“冷热点数据自检”可以同时勾选(冷库和冷车会用到)进行自检也可以单独执行(冷柜和保温箱只执行“平均值数据自检”) */ func RoundToDecimalHalfUp(val float64, n int) float64 { pow := math.Pow(10, float64(n)) return math.Round(val*pow) / pow } /* 数据自检名称:平均值数据自检 策略:逐个测点计算平均值,与绑定点平均值对比。 若偏差 > 0.5,则单独对该测点进行偏移(目标设为 基准±0.3,留出0.2安全余量避免贴边界)。 若偏差 ≤ 0.5,则合规,不处理。 */ func (c *TaskDataHandleController) SSE_Compare_binding_points_with_Average() { T_task_id := c.GetString("T_task_id") 偏移阈值 := 0.5 c.Ctx.ResponseWriter.Header().Set("Content-Type", "text/event-stream") c.Ctx.ResponseWriter.Header().Set("Cache-Control", "no-cache") c.Ctx.ResponseWriter.Header().Set("Connection", "keep-alive") lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "任务开始......"}) Task_r, err := Task.Read_Task(T_task_id) if err != nil { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "获取任务信息失败!"}) return } // 温度控制范围 温度控制范围最高值 := float64(lib.To_float32(VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "温度控制范围最高值"))) if 温度控制范围最高值 == 0 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "温度控制范围最高值 标签值不正确!"}) return } 温度控制范围最小值 := float64(lib.To_float32(VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "温度控制范围最小值"))) if 温度控制范围最小值 == 0 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "温度控制范围最小值 标签值不正确!"}) return } // 布点终端 部点终端_list := Device.Read_DeviceClassList_List_id_T_remark(Task_r.T_class, "均匀性布点|柜内所有测点|箱内所有测点|产品存放区域测点") if len(部点终端_list) == 0 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "没有找到 柜内所有测点|箱内所有测点|均匀性布点|产品存放区域测点 测点数据!"}) return } 部点终端_sn_list := Device.JoinDeviceClassListSnToString(部点终端_list) // 动态兼容“满载”和“空载”的时间范围 开始时间 := VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "温度分布特性的测试与分析(满载)开始时间") 结束时间 := VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "温度分布特性的测试与分析(满载)结束时间") if 开始时间 == "" || 结束时间 == "" { 开始时间 = VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "温度分布特性的测试与分析(空载)开始时间") 结束时间 = VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "温度分布特性的测试与分析(空载)结束时间") } if 开始时间 == "" || 结束时间 == "" { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "未找到 温度分布特性的测试与分析(满载/空载)的开始时间 或 结束时间!"}) return } lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "--- 实际使用测试时间范围: " + 开始时间 + " 至 " + 结束时间 + " ---"}) // ----------获取保留数据------------ var valueStrings1, valueStrings2 []string var 开空开, 保空开 string var 开空开驼峰, 保空开驼峰 Task.CalculateHumps_R var BWXValueStrings []string if Task_r.T_device_type != "X" { valueStrings1, valueStrings2, 开空开, 保空开, 开空开驼峰, 保空开驼峰 = c.GetRetainData(Task_r, 部点终端_list, true) } else { BWXValueStrings = c.GetBWXRetainData(Task_r, 部点终端_sn_list, 结束时间, 结束时间) } // ----------获取保留数据结束------------ // 温湿度绑定点 温湿度绑定点_list := Device.Read_DeviceClassList_List_id_T_remark(Task_r.T_class, "温湿度绑定点") if len(温湿度绑定点_list) < 1 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "未找到 温湿度绑定点!"}) return } var 温湿度绑定点1, _ Device.DeviceClassList for _, list := range 温湿度绑定点_list { if strings.Contains(list.T_remark, "温湿度绑定点1") { 温湿度绑定点1 = list } if strings.Contains(list.T_remark, "温湿度绑定点2") { _ = list } } // 1. 绑定点1的平均值作为基准 绑定点1_Avg_raw := Task.Read_TaskData_AVG(T_task_id, 温湿度绑定点1.T_sn, 温湿度绑定点1.T_id, 开始时间, 结束时间) 绑定点1_Avg := RoundToDecimalHalfUp(绑定点1_Avg_raw, 1) if 绑定点1_Avg == 0 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "温湿度绑定点1 无平均值数据!"}) return } 压缩上限 := RoundToDecimalHalfUp(绑定点1_Avg+偏移阈值, 1) // 基准 + 0.5 压缩下限 := RoundToDecimalHalfUp(绑定点1_Avg-偏移阈值, 1) // 基准 - 0.5 lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("定点1 = %.1f, 合规范围 [%.1f, %.1f]", 绑定点1_Avg, 压缩下限, 压缩上限)}) lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "逐终端检查:"}) // 初始化随机数种子 rand.Seed(time.Now().UnixNano()) // 定义安全边界:0.4 或 0.5 二选一 随机偏移量 := 0.4 if rand.Intn(2) == 1 { 随机偏移量 = 0.5 } // 提前解析时间,用于后续的防断崖校验 endTimeObj, err := time.Parse("2006-01-02 15:04", 结束时间) if err != nil { endTimeObj, err = time.Parse("2006-01-02 15:04:05", 结束时间+":00") } compareTimeStr := endTimeObj.Format("2006-01-02 15:04") targetTimeObj := endTimeObj.Add(1 * time.Minute) targetTimeStr := targetTimeObj.Format("2006-01-02 15:04") lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("------ 开始校验 %s 之后的数据防断崖处理 ------", 结束时间)}) // 获取 ORM 实例用于执行原生 SQL 更新 //o := orm.NewOrm() for _, dev := range 部点终端_list { 终端_id := dev.T_id 终端_sn := dev.T_sn // ================= 步骤 1:先执行平均值偏移 ================= 终端_Avg_raw := Task.Read_TaskData_AVG(T_task_id, 终端_sn, 终端_id, 开始时间, 结束时间) 终端_Avg := RoundToDecimalHalfUp(终端_Avg_raw, 1) 偏差 := RoundToDecimalHalfUp(math.Abs(终端_Avg-绑定点1_Avg), 1) lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("测点[%s] 当前平均值:%.1f, 偏差:%.1f", 终端_id, 终端_Avg, 偏差)}) if 偏差 > 偏移阈值 { 终端_Min := Task.Read_TaskData_min(T_task_id, 终端_sn, 终端_id, 开始时间, 结束时间) 终端_Max := Task.Read_TaskData_max(T_task_id, 终端_sn, 终端_id, 开始时间, 结束时间) var targetAvg, 偏移量 float64 var 动作 string if 终端_Avg > 绑定点1_Avg { // 降:目标值 = 基准 + 0.4 或 基准 + 0.5 targetAvg = RoundToDecimalHalfUp(绑定点1_Avg+随机偏移量, 1) 偏移量 = RoundToDecimalHalfUp(终端_Avg-targetAvg, 1) 动作 = "下移" c.SetAverageShiftedDownward(T_task_id, 终端_id, 0.0, targetAvg, 终端_Avg, 终端_Min, 终端_Max, 温度控制范围最小值, 温度控制范围最高值, "", "") } else { // 升:目标值 = 基准 - 0.4 或 基准 - 0.5 targetAvg = RoundToDecimalHalfUp(绑定点1_Avg-随机偏移量, 1) 偏移量 = RoundToDecimalHalfUp(targetAvg-终端_Avg, 1) 动作 = "上移" c.SetAverageShiftedUpward(T_task_id, 终端_id, 0.0, targetAvg, 终端_Avg, 终端_Min, 终端_Max, 温度控制范围最小值, 温度控制范围最高值, "", "") } lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf(" 测点[%s] 偏差:%.1f > 0.5 → %s%.1f → 动态目标Avg:%.1f (随机安全值)", 终端_id, 偏差, 动作, 偏移量, targetAvg)}) } else { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("测点[%s] 偏差 %.1f <= 0.5,已在合规范围内,跳过不处理", 终端_id, 偏差)}) } } // 重新获取处理后的整体平均值用于后续对比 处理后整体Avg := Task.Read_TaskData_Average(Task_r.T_task_id, 部点终端_sn_list, 开始时间, 结束时间) 处理后整体Avg = RoundToDecimalHalfUp(处理后整体Avg, 1) T_deviation := 1.0 温控传感器绑定点_list := Device.Read_DeviceClassList_List_id_T_remark(Task_r.T_class, "温控传感器绑定点1|温控传感器绑定点2") if len(温控传感器绑定点_list) == 0 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "没有找到温控传感器绑定点!"}) } for i, classList := range 温控传感器绑定点_list { 温控传感器绑定点OriginalAvg := Task.Read_TaskData_Average(Task_r.T_task_id, classList.T_sn, 开始时间, 结束时间) 温控传感器绑定点OriginalAvg = RoundToDecimalHalfUp(温控传感器绑定点OriginalAvg, 1) lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "------ 进行 温控传感器绑定点" + strconv.Itoa(i+1) + " 平均值处理 ------"}) if (温控传感器绑定点OriginalAvg >= 处理后整体Avg && 温控传感器绑定点OriginalAvg <= 处理后整体Avg+T_deviation) || (温控传感器绑定点OriginalAvg <= 处理后整体Avg && 温控传感器绑定点OriginalAvg >= 处理后整体Avg-T_deviation) { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: " 温控传感器绑定点" + lib.To_string(i+1) + " 平均值:" + lib.To_string(温控传感器绑定点OriginalAvg) + "℃ " + " 测点平均值" + lib.To_string(处理后整体Avg) + "℃ " + " 数据偏差: " + lib.To_string(RoundToDecimalHalfUp(math.Abs(温控传感器绑定点OriginalAvg-处理后整体Avg), 1)) + "℃ " + " 设置:" + lib.To_string(T_deviation) + "℃" + " 符合要求!"}) } else { if 温控传感器绑定点OriginalAvg < 处理后整体Avg-T_deviation { T_max := Task.Read_TaskData_max(T_task_id, classList.T_sn, classList.T_id, 开始时间, 结束时间) T_min := Task.Read_TaskData_min(T_task_id, classList.T_sn, classList.T_id, 开始时间, 结束时间) c.SetAverageShiftedUpward(T_task_id, classList.T_id, T_deviation, RoundToDecimalHalfUp(处理后整体Avg-T_deviation, 1), 温控传感器绑定点OriginalAvg, T_min, T_max, 温度控制范围最小值, 温度控制范围最高值, "", "") lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "温控传感器绑定点" + lib.To_string(i+1) + " 平均值向上偏移"}) } if 温控传感器绑定点OriginalAvg > 处理后整体Avg+T_deviation { T_max := Task.Read_TaskData_max(T_task_id, classList.T_sn, classList.T_id, 开始时间, 结束时间) T_min := Task.Read_TaskData_min(T_task_id, classList.T_sn, classList.T_id, 开始时间, 结束时间) c.SetAverageShiftedDownward(T_task_id, classList.T_id, T_deviation, RoundToDecimalHalfUp(处理后整体Avg+T_deviation, 1), 温控传感器绑定点OriginalAvg, T_min, T_max, 温度控制范围最小值, 温度控制范围最高值, "", "") lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "温控传感器绑定点" + lib.To_string(i+1) + " 平均值向下偏移"}) } } } // ----------恢复保留数据------------ if Task_r.T_device_type != "X" { c.SaveRetainData(Task_r, 部点终端_list, valueStrings1, valueStrings2, 开空开, 保空开, 开空开驼峰, 保空开驼峰, 温度控制范围最高值, 60) } else { c.SaveBWXRetainData(Task_r, 部点终端_list, BWXValueStrings, 结束时间, 结束时间, 60) } // ----------恢复保留数据结束------------ lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("------ 开始校验 %s 之后的数据防断崖处理 ------", 结束时间)}) o := orm.NewOrm() for _, dev := range 部点终端_list { 终端_id := dev.T_id 终端_sn := dev.T_sn var temp46, temp47 float64 // 查询该测点在 结束时间(如 16:46) 的最后一条温度值 sql46 := fmt.Sprintf("SELECT t_t FROM z_task_data_%s WHERE t_sn = ? AND DATE_FORMAT(t_time, '%%Y-%%m-%%d %%H:%%i') = ? ORDER BY t_time DESC LIMIT 1", Task_r.T_task_id) err46 := o.Raw(sql46, 终端_sn, compareTimeStr).QueryRow(&temp46) if err46 == nil { // 查询该测点在 结束时间+1分钟(如 16:47) 的第一条温度值 sql47 := fmt.Sprintf("SELECT t_t FROM z_task_data_%s WHERE t_sn = ? AND DATE_FORMAT(t_time, '%%Y-%%m-%%d %%H:%%i') = ? ORDER BY t_time ASC LIMIT 1", Task_r.T_task_id) err47 := o.Raw(sql47, 终端_sn, targetTimeStr).QueryRow(&temp47) // 对比并修正 if err47 == nil && temp47 < temp46 { updateSql := fmt.Sprintf("UPDATE z_task_data_%s SET t_t = ? WHERE t_sn = ? AND DATE_FORMAT(t_time, '%%Y-%%m-%%d %%H:%%i') = ?", Task_r.T_task_id) _, updateErr := o.Raw(updateSql, temp46, 终端_sn, targetTimeStr).Exec() if updateErr == nil { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf(" 测点[%s] 防断崖兜底: %s 温度 %.1f℃ < %s 温度 %.1f℃,已强制拉平至 %.1f℃", 终端_id, targetTimeStr, temp47, compareTimeStr, temp46, temp46)}) } } } } lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 1, Msg: "完成!"}) c.Ctx.ResponseWriter.WriteHeader(http.StatusOK) } /* 平均值下移 targetAvg 参考平均值 originalAvg 原始平均值 T_min 整段数据最小值 T_max 整段数据最大值 minLimit 温度控制范围最小值 maxLimit 温度控制范围最大值 */ func (c *TaskDataHandleController) SetAverageShiftedDownward(T_task_id, T_id string, T_deviation, targetAvg, originalAvg, T_min, T_max, minLimit, maxLimit float64, StartTime, EndTime string) { 目标avg := RoundToDecimal(targetAvg+T_deviation, 1) vgaca := RoundToDecimal(originalAvg-目标avg, 1) //向下偏移后整段数据最小值大于温度控制范围最小值 if RoundToDecimal(T_min-vgaca, 1) > RoundToDecimal(minLimit, 1) && RoundToDecimal(T_max-vgaca, 1) < RoundToDecimal(maxLimit, 1) { Task.UpdateTaskDataTemperatureAndHumidityByGeometricAVG(T_task_id, T_id, "", "", -vgaca) lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "进行处理 数据!" + " 测点 " + lib.To_string(T_id) + " 最大值:" + lib.To_string(T_max) + "℃ " + " 最小值:" + lib.To_string(T_min) + "℃ " + " 参考平均值:" + lib.To_string(RoundToDecimal(targetAvg, 1)) + "℃ " + " 原始平均值:" + lib.To_string(RoundToDecimal(originalAvg, 1)) + "℃ " + " 数据偏差:" + lib.To_string(vgaca) + "℃ " + " 向下偏移:" + lib.To_string(vgaca) + "℃ "}) } else { var err error var compress float64 compress, vgaca, err = GetLinearTransformationValue(目标avg, originalAvg, T_min, T_max, minLimit, maxLimit) if err != nil { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "进行处理 数据!" + " 测点" + lib.To_string(T_id) + " 最大值:" + lib.To_string(T_max) + "℃ " + " 最小值:" + lib.To_string(T_min) + "℃ " + " 参考平均值:" + lib.To_string(RoundToDecimal(targetAvg, 1)) + "℃ " + " 原始平均值:" + lib.To_string(RoundToDecimal(originalAvg, 1)) + "℃ " + " 无合法解: 无法满足所有约束条件!"}) return } lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "进行处理 数据!" + " 测点" + lib.To_string(T_id) + " 最大值:" + lib.To_string(T_max) + "℃ " + " 最小值:" + lib.To_string(T_min) + "℃ " + " 参考平均值:" + lib.To_string(RoundToDecimal(targetAvg, 1)) + "℃ " + " 原始平均值:" + lib.To_string(RoundToDecimal(originalAvg, 1)) + "℃ " + " 缩放:" + lib.To_string(compress) + " 缩放后最大值:" + lib.To_string(RoundToDecimal(T_max*compress, 1)) + "℃ " + " 缩放后最小值:" + lib.To_string(RoundToDecimal(T_min*compress, 1)) + "℃ " + " 数据偏差:" + lib.To_string(vgaca) + "℃ " + " 偏移:" + lib.To_string(vgaca) + "℃ "}) // 压缩 Task.UpdateTaskDataTemperatureAndHumidityByGeometric_id(T_task_id, T_id, "", "", compress) // 偏移 if vgaca != 0 { Task.UpdateTaskDataTemperatureAndHumidityByGeometricAVG(T_task_id, T_id, "", "", vgaca) } } } func GetLinearTransformationValue(targetAvg, originalAvg, T_min, T_max, minLimit, maxLimit float64) (float64, float64, error) { //var vgaca float64 //compress1 := RoundToDecimal((minLimit+0.3-targetAvg)/(T_min-originalAvg), 1) //compress2 := RoundToDecimal((maxLimit-0.3-targetAvg)/(T_max-originalAvg), 1) //compress := math.Min(compress1, compress2) //if RoundToDecimal(T_max*compress, 1) < RoundToDecimal(maxLimit, 1) && RoundToDecimal(T_min*compress, 1) > RoundToDecimal(minLimit, 1) { // vgaca = 0 //} else { // vgaca = RoundToDecimal(targetAvg-compress*originalAvg, 1) //} //for _, x := range []float64{T_min, T_max} { // newVal := RoundToDecimal(compress*x+vgaca, 1) // if newVal < minLimit || newVal > maxLimit { // return 0, 0, errors.New("无合法解: 无法满足所有约束条件") // } //} // //return compress, vgaca, nil minLimit = RoundToDecimal(minLimit+0.2, 1) maxLimit = RoundToDecimal(maxLimit-0.2, 1) // 策略1: 使用最小边界值 (使变换后最小值 = minBound) k1 := (targetAvg - minLimit) / (originalAvg - T_min) b1 := minLimit - k1*T_min valid1 := true for _, x := range []float64{T_min, T_max} { newVal := RoundToDecimal(k1*x+b1, 1) if newVal < minLimit || newVal > maxLimit { valid1 = false break } } // 策略2: 使用最大边界值 (使变换后最大值 = maxBound),策略1失败时使用 k2 := float64(0) b2 := float64(0) valid2 := false if !valid1 { k2 = (maxLimit - targetAvg) / (T_max - originalAvg) b2 = targetAvg - k2*originalAvg valid2 = true for _, x := range []float64{T_min, T_max} { newVal := RoundToDecimal(k2*x+b2, 1) if newVal < minLimit || newVal > maxLimit { valid2 = false break } } } // 根据结果选择有效策略 switch { case valid1: return k1, b1, nil case valid2: return k2, b2, nil default: return 0, 0, errors.New("无合法解: 无法满足所有约束条件") } } /* 平均值上移 targetAvg 参考平均值 originalAvg 原始平均值 T_min 整段数据最小值 T_max 整段数据最大值 minLimit 温度控制范围最小值 maxLimit 温度控制范围最大值 */ func (c *TaskDataHandleController) SetAverageShiftedUpward(T_task_id, T_id string, T_deviation, targetAvg, originalAvg, T_min, T_max, minLimit, maxLimit float64, StartTime, EndTime string) { 目标avg := RoundToDecimal(targetAvg-T_deviation, 1) vgaca := RoundToDecimal(目标avg-originalAvg, 1) if RoundToDecimal(T_max+vgaca, 1) < RoundToDecimal(maxLimit, 1) && RoundToDecimal(T_min+vgaca, 1) > RoundToDecimal(minLimit, 1) { Task.UpdateTaskDataTemperatureAndHumidityByGeometricAVG(T_task_id, T_id, "", "", vgaca) lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "进行处理 数据!" + " 测点 " + lib.To_string(T_id) + " 最大值:" + lib.To_string(T_max) + "℃ " + " 最小值:" + lib.To_string(T_min) + "℃ " + " 参考平均值:" + lib.To_string(RoundToDecimal(targetAvg, 1)) + "℃ " + " 原始平均值:" + lib.To_string(RoundToDecimal(originalAvg, 1)) + "℃ " + " 数据偏差:" + lib.To_string(vgaca) + "℃ " + " 向上偏移:" + lib.To_string(vgaca) + "℃ "}) } else { var err error var compress float64 compress, vgaca, err = GetLinearTransformationValue(目标avg, originalAvg, T_min, T_max, minLimit, maxLimit) if err != nil { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "进行处理 数据!" + " 测点" + lib.To_string(T_id) + " 最大值:" + lib.To_string(T_max) + "℃ " + " 最小值:" + lib.To_string(T_min) + "℃ " + " 参考平均值:" + lib.To_string(RoundToDecimal(targetAvg, 1)) + "℃ " + " 原始平均值:" + lib.To_string(RoundToDecimal(originalAvg, 1)) + "℃ " + " 无合法解: 无法满足所有约束条件!"}) return } lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "进行处理 数据!" + " 测点" + lib.To_string(T_id) + " 最大值:" + lib.To_string(T_max) + "℃ " + " 最小值:" + lib.To_string(T_min) + "℃ " + " 参考平均值:" + lib.To_string(RoundToDecimal(targetAvg, 1)) + "℃ " + " 原始平均值:" + lib.To_string(RoundToDecimal(originalAvg, 1)) + "℃ " + " 缩放:" + lib.To_string(compress) + " 缩放后最大值:" + lib.To_string(RoundToDecimal(T_max*compress, 1)) + "℃ " + " 缩放后最小值:" + lib.To_string(RoundToDecimal(T_min*compress, 1)) + "℃ " + " 数据偏差:" + lib.To_string(vgaca) + "℃ " + " 偏移:" + lib.To_string(vgaca) + "℃ "}) // 压缩 Task.UpdateTaskDataTemperatureAndHumidityByGeometric_id(T_task_id, T_id, "", "", compress) // 偏移 if vgaca != 0 { Task.UpdateTaskDataTemperatureAndHumidityByGeometricAVG(T_task_id, T_id, "", "", vgaca) } } } func (c *TaskDataHandleController) GetCalculateHumps(T_task_id string, SN_list []Device.DeviceClassList, startTime, types string) (calculateHumps Task.CalculateHumps_R) { SN := Device.JoinDeviceClassListSnToString(SN_list) var is bool calculateHumps, is = Task.Redis_CalculateHumps_Get(T_task_id + types) if !is { list := Task.Read_TaskData_ById_AVG(T_task_id, SN, startTime, "") // 获取第一个驼峰结束时间点 CalculateHumps_list := Task.CalculateHumpsByThreeDots(list) if len(CalculateHumps_list) < 1 { return } calculateHumps = CalculateHumps_list[0] Task.Redis_CalculateHumps_Set(T_task_id+types, calculateHumps) } return calculateHumps } func (c *TaskDataHandleController) GetMetadata(T_task_id string, SN_list []Device.DeviceClassList, startTime, types string) (valueStrings []string, calculateHumps Task.CalculateHumps_R) { SN := Device.JoinDeviceClassListSnToString(SN_list) var is bool calculateHumps, is = Task.Redis_CalculateHumps_Get(T_task_id + types) if !is { return } endTime := calculateHumps.End.T_time data1, _ := Task.Read_TaskData_ById_List_AES(T_task_id, SN, "", startTime, endTime, 0, 9999) for _, v := range data1 { valueStrings = append(valueStrings, fmt.Sprintf("('%s','%s',%v,%v,'%s')", v.T_sn, v.T_id, v.T_t, v.T_rh, v.T_time)) } return valueStrings, calculateHumps } func (c *TaskDataHandleController) GetCalculateHumpsMaps(T_task_id string, SN_list []Device.DeviceClassList, startTime string) map[string]Task.CalculateHumps_R { var CalculateHumpsMaps = make(map[string]Task.CalculateHumps_R) SN := Device.JoinDeviceClassListSnToString(SN_list) allList := Task.Read_TaskData_ById_AVG(T_task_id, SN, startTime, "") // 获取第一个驼峰结束时间点 CalculateHumps_list := Task.CalculateHumpsByThreeDots(allList) if len(CalculateHumps_list) < 1 { return CalculateHumpsMaps } for _, device := range SN_list { list := Task.Read_TaskData_ById_AVG(T_task_id, device.T_sn, startTime, "") //获取第一个驼峰结束时间点 CalculateHumps := Task.CalculateHumpsByThreeDots(list) if len(CalculateHumps) < 1 { CalculateHumpsMaps[device.T_sn] = CalculateHumps_list[0] continue } if CalculateHumps[0].Peak.T_time != CalculateHumps_list[0].Peak.T_time { CalculateHumps[0].Peak.T_time = CalculateHumps_list[0].Peak.T_time } et1, _ := lib.TimeStrToTime(CalculateHumps_list[0].End.T_time) et2, _ := lib.TimeStrToTime(CalculateHumps[0].End.T_time) if et1.Before(et2) { CalculateHumps[0].End.T_time = CalculateHumps_list[0].End.T_time } CalculateHumpsMaps[device.T_sn] = CalculateHumps[0] } return CalculateHumpsMaps } // 获取保留数据 func (c *TaskDataHandleController) GetRetainData(Task_r Task.Task, SN_list []Device.DeviceClassList, useCache bool) (valueStrings1, valueStrings2 []string, kkkStartTime, bkkStartTime string, kkkCalculateHumps, bkkCalculateHumps Task.CalculateHumps_R) { var 开空开, 保空开 string var 开空开驼峰, 保空开驼峰 Task.CalculateHumps_R if Task_r.T_device_type != "X" { 开空开 = VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "开空开") if len(开空开) == 0 { 开空开 = VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "开满开") } if len(开空开) == 0 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "未找到 开空开/开满开 时间 标签!"}) return } //保空开 = VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "保空开") //if len(保空开) == 0 { // 保空开 = VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "保满开") //} //if len(保空开) == 0 { // lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "未找到 保空开/保满开 时间 标签!"}) // return //} //if !useCache { // 开空开驼峰 = c.GetCalculateHumps(Task_r.T_task_id, SN_list, 开空开, "kkk") // 保空开驼峰 = c.GetCalculateHumps(Task_r.T_task_id, SN_list, 保空开, "bkk") //} // //valueStrings1, 开空开驼峰 = c.GetMetadata(Task_r.T_task_id, SN_list, 开空开, "kkk") //valueStrings2, 保空开驼峰 = c.GetMetadata(Task_r.T_task_id, SN_list, 保空开, "bkk") SN := Device.JoinDeviceClassListSnToString(SN_list) data1, _ := Task.Read_TaskData_ById_List_AES(Task_r.T_task_id, SN, "", 开空开, "", 0, 9999) for _, v := range data1 { valueStrings1 = append(valueStrings1, fmt.Sprintf("('%s','%s',%v,%v,'%s')", v.T_sn, v.T_id, v.T_t, v.T_rh, v.T_time)) } } return valueStrings1, valueStrings2, 开空开, 保空开, 开空开驼峰, 保空开驼峰 } // 获取保留数据 func (c *TaskDataHandleController) SaveRetainData(Task_r Task.Task, SN_list []Device.DeviceClassList, valueStrings1, valueStrings2 []string, kkkStartTime, bkkStartTime string, kkkCalculateHumps, bkkCalculateHumps Task.CalculateHumps_R, maxLimit float64, saveTime int) { var err error if len(valueStrings1) > 0 { //将开空开/开满开时间开始第一个驼峰 写入原始数据 //将 开空开/开满开时间 后时间写入原始数据 SN := Device.JoinDeviceClassListSnToString(SN_list) Task.DeleteTaskAllDataByTimeRange(Task_r.T_task_id, SN, kkkStartTime, kkkCalculateHumps.End.T_time) err = Task.Batch_Adds_TaskData(Task_r.T_task_id, valueStrings1) if err == nil { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("恢复 开空开/开满开 时间点后驼峰 %s ~ %s 数据 %d/%d", kkkStartTime, kkkCalculateHumps.End.T_time, len(valueStrings1), len(valueStrings1))}) } } if len(valueStrings2) > 0 { //将开空开/开满开时间开始第一个驼峰 写入原始数据 SN := Device.JoinDeviceClassListSnToString(SN_list) Task.DeleteTaskAllDataByTimeRange(Task_r.T_task_id, SN, bkkStartTime, bkkCalculateHumps.End.T_time) err = Task.Batch_Adds_TaskData(Task_r.T_task_id, valueStrings2) if err == nil { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("恢复 保空开/保满开 时间点后驼峰 %s ~ %s 数据 %d/%d", kkkStartTime, kkkCalculateHumps.End.T_time, len(valueStrings1), len(valueStrings1))}) } } //c.SaveRetainDataAndTrend(Task_r, SN_list, valueStrings1, kkkStartTime, kkkCalculateHumps, maxLimit, saveTime) //c.SaveRetainDataAndTrend(Task_r, SN_list, valueStrings2, bkkStartTime, bkkCalculateHumps, maxLimit, saveTime) } // 获取保留数据 func (c *TaskDataHandleController) SaveRetainDataTrend(Task_r Task.Task, SN_list []Device.DeviceClassList, valueStrings1, valueStrings2 []string, kkkStartTime, bkkStartTime string, kkkCalculateHumps, bkkCalculateHumps Task.CalculateHumps_R, maxLimit float64, saveTime int) { c.SaveRetainDataAndTrend(Task_r, SN_list, valueStrings1, kkkStartTime, kkkCalculateHumps, maxLimit, saveTime) c.SaveRetainDataAndTrend(Task_r, SN_list, valueStrings2, bkkStartTime, bkkCalculateHumps, maxLimit, saveTime) } func (c *TaskDataHandleController) SaveRetainDataAndTrend(Task_r Task.Task, SN_list []Device.DeviceClassList, valueStrings1 []string, kkkStartTime string, kkkCalculateHumps Task.CalculateHumps_R, maxLimit float64, saveTime int) { var err error if len(valueStrings1) > 0 { //将开空开/开满开时间开始第一个驼峰 写入原始数据 SN := Device.JoinDeviceClassListSnToString(SN_list) Task.DeleteTaskAllDataByTimeRange(Task_r.T_task_id, SN, kkkStartTime, kkkCalculateHumps.End.T_time) err = Task.Batch_Adds_TaskData(Task_r.T_task_id, valueStrings1) if err == nil { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("恢复 开空开/开满开 保空开/保满开 时间点后驼峰 %s ~ %s 数据 %d/%d", kkkStartTime, kkkCalculateHumps.End.T_time, len(valueStrings1), len(valueStrings1))}) } startTimeT, _ := lib.TimeStrToTime(kkkStartTime) startTime := startTimeT.Add(-time.Minute).Format("2006-01-02 15:04") endTimeT, _ := lib.TimeStrToTime(kkkCalculateHumps.End.T_time) endTime := endTimeT.Add(time.Minute).Format("2006-01-02 15:04") // 执行数据平滑 for _, v := range SN_list { sn := v.T_sn id_str := v.T_id //AllList, _ := Task.Read_TaskData_ById_List_AES(Task_r.T_task_id, sn, id_str, startTime, kkkCalculateHumps.End.T_time, 0, 9999) var trendTime, declineTrendTime string trendTime = kkkCalculateHumps.Peak.T_time declineTrendTime = kkkCalculateHumps.Peak.T_time lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "执行数据上升趋势"}) if len(trendTime) > 0 { list, _ := Task.Read_TaskData_ById_List_AES(Task_r.T_task_id, sn, id_str, startTime, trendTime, 0, 9999) if len(list) <= 2 { continue } first := list[0] var last Task.TaskData_ if len(list) > 10 { last = list[len(list)-2] } else { last = list[len(list)-1] } current, _ := time.Parse("2006-01-02 15:04", first.T_time) next, _ := time.Parse("2006-01-02 15:04", last.T_time) interval := next.Sub(current).Seconds() / float64(saveTime) //ttInterval := (last.T_t - first.T_t) / float32(interval) trhInterval := (last.T_rh - first.T_rh) / float32(interval) ttList := generateRisingCurve(float64(first.T_t), float64(last.T_t), int(interval+1)) //tt := first.T_t ttrh := first.T_rh var valueStrings []string for i := 0; i <= int(interval); i++ { //tt += ttInterval ttrh += trhInterval ttime := current.Format("2006-01-02 15:04") valueStrings = append(valueStrings, fmt.Sprintf("('%s','%s',%v,%v,'%s')", first.T_sn, id_str, ttList[i], ttrh, ttime)) current = current.Add(time.Second * time.Duration(saveTime)) } //for current.Unix() <= next.Unix() { // // tt += ttInterval // ttrh += trhInterval // ttime := current.Format("2006-01-02 15:04") // valueStrings = append(valueStrings, fmt.Sprintf("('%s','%s',%v,%v,'%s')", first.T_sn, id_str, tt, ttrh, ttime)) // current = current.Add(time.Second * time.Duration(saveTime)) //} Task.DeleteTaskDataByTimeRange(Task_r.T_task_id, sn, id_str, first.T_time, last.T_time) Task.Batch_Adds_TaskData(Task_r.T_task_id, valueStrings) } lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "执行数据下降趋势"}) if len(declineTrendTime) > 0 { list, _ := Task.Read_TaskData_ById_List_AES(Task_r.T_task_id, sn, id_str, declineTrendTime, endTime, 0, 9999) if len(list) <= 2 { continue } if list[len(list)-1].T_t > list[len(list)-2].T_t { continue } var first Task.TaskData_ if len(list) > 10 { first = list[1] } else { first = list[0] } last := list[len(list)-1] current, _ := time.Parse("2006-01-02 15:04", first.T_time) next, _ := time.Parse("2006-01-02 15:04", last.T_time) interval := next.Sub(current).Seconds() / float64(saveTime) //ttInterval := (last.T_t - first.T_t) / float32(interval) trhInterval := (last.T_rh - first.T_rh) / float32(interval) ttList := generateTemperatureCurve(float64(first.T_t), float64(last.T_t), int(interval+1)) //tt := first.T_t ttrh := first.T_rh var valueStrings []string for i := 0; i <= int(interval); i++ { //tt += ttInterval ttrh += trhInterval ttime := current.Format("2006-01-02 15:04") valueStrings = append(valueStrings, fmt.Sprintf("('%s','%s',%v,%v,'%s')", first.T_sn, id_str, ttList[i], ttrh, ttime)) current = current.Add(time.Second * time.Duration(saveTime)) } Task.DeleteTaskDataByTimeRange(Task_r.T_task_id, sn, id_str, first.T_time, last.T_time) Task.Batch_Adds_TaskData(Task_r.T_task_id, valueStrings) } } } } // 获取保留数据 func (c *TaskDataHandleController) GetBWXRetainData(Task_r Task.Task, SN string, endTime, trendTime string) (BWXValueStrings []string) { if endTime != trendTime { data1, _ := Task.Read_TaskData_ById_List_AES(Task_r.T_task_id, SN, "", trendTime, "", 0, 9999) for _, v := range data1 { BWXValueStrings = append(BWXValueStrings, fmt.Sprintf("('%s','%s',%v,%v,'%s')", v.T_sn, v.T_id, v.T_t, v.T_rh, v.T_time)) } } return } func (c *TaskDataHandleController) SaveBWXRetainData(Task_r Task.Task, SN_list []Device.DeviceClassList, BWXValueStrings []string, endTime, trendTime string, saveTime int) { var err error if endTime != trendTime && len(BWXValueStrings) > 0 { //将开空开/开满开时间开始第一个驼峰 写入原始数据 for _, device := range SN_list { Task.DeleteTaskDataByTimeRange(Task_r.T_task_id, device.T_sn, device.T_id, trendTime, "") } err = Task.Batch_Adds_TaskData(Task_r.T_task_id, BWXValueStrings) if err == nil { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("恢复 保温箱 原始数据 %d/%d", len(BWXValueStrings), len(BWXValueStrings))}) } var startTime string startTime = VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "静态开箱作业开箱结束时间") if len(startTime) == 0 { startTime = VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "动态开箱作业开箱结束时间") } if len(startTime) == 0 { startTime = VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "开箱作业开箱结束时间") } if len(startTime) == 0 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "获取 静态开箱作业开箱结束时间|动态开箱作业开箱结束时间|开箱作业开箱结束时间 失败"}) return } startTimeT, _ := lib.TimeStrToTime(startTime) startTime = startTimeT.Add(30 * time.Minute).Format("2006-01-02 15:04") // 执行数据平滑 for _, v := range SN_list { sn := v.T_sn id_str := v.T_id list, _ := Task.Read_TaskData_ById_List_AES(Task_r.T_task_id, sn, id_str, startTime, trendTime, 0, 9999) first := list[0] last := list[len(list)-1] current, _ := time.Parse("2006-01-02 15:04", first.T_time) next, _ := time.Parse("2006-01-02 15:04", last.T_time) interval := next.Sub(current).Seconds() / float64(saveTime) ttInterval := (last.T_t - first.T_t) / float32(interval) trhInterval := (last.T_rh - first.T_rh) / float32(interval) tt := first.T_t ttrh := first.T_rh var valueStrings []string for current.Unix() <= next.Unix() { tt += ttInterval ttrh += trhInterval ttime := current.Format("2006-01-02 15:04") valueStrings = append(valueStrings, fmt.Sprintf("('%s','%s',%v,%v,'%s')", first.T_sn, id_str, tt, ttrh, ttime)) current = current.Add(time.Second * time.Duration(saveTime)) } Task.DeleteTaskDataByTimeRange(Task_r.T_task_id, sn, id_str, startTime, trendTime) err = Task.Batch_Adds_TaskData(Task_r.T_task_id, valueStrings) if err == nil { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("保温箱数据趋势 %d/%d", len(valueStrings), len(valueStrings))}) } } } } // 获取开始结束时间 func (c *TaskDataHandleController) GetStartTimeAndEndTime(Task_r Task.Task, SN string, maxLimit float64) (startTime, endTime, trendTime string) { // 开始时间 获取温度下降到第二个低点时间 // 1. 获取温度平均值 list := Task.Read_TaskData_ById_AVG(Task_r.T_task_id, SN, "", "") if len(list) < 2 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "均匀性布点+产品存放区域测点+作业出入口总测点 数据平均值 少于2条!"}) return } // 找平均值低于温度控制范围最高值的第二个最低点 lowPoint := 0 for i := 1; i <= len(list)-2; i++ { if list[i].T_t < list[i-1].T_t && list[i].T_t < list[i+1].T_t && list[i].T_t < float32(maxLimit) { lowPoint += 1 } if lowPoint == 2 { startTime = list[i].T_time break } } // 保温箱没有开空开,开满开 // 结束时间温度超限取没有超限时间点,结束时间温度未超限取最后一个时间点 if Task_r.T_device_type == "X" { list2 := Task.Read_TaskData_ById_AVG_DESC(Task_r.T_task_id, SN, "", "") if len(list) == 0 { return } if float64(list2[0].T_t) < maxLimit { endTime = list2[0].T_time trendTime = list2[0].T_time return } for i, avg := range list2 { if RoundToDecimal(float64(avg.T_t), 1) < maxLimit { endTime = avg.T_time trendTime = list2[i-1].T_time break } } } else { endTime = VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "开空开") if len(endTime) == 0 { endTime = VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "开满开") } if len(endTime) == 0 { endTime = VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "现场测试结束时间") } if len(endTime) == 0 { return } } return } /* 计算绑定点的值 A 监测终端01平均值 B 监测终端02平均值 C 温湿度绑定点01平均值 D 温湿度绑定点02平均值 */ func findBindingPointsOptimalAdjustment(A, B, C, D, T_deviation float64) (float64, float64, bool) { type DataPair struct { A1 float64 B1 float64 } T_deviation_half := 0.1 var dataPairs []DataPair var A1, B1 float64 = -100, -100 // 生成A1和B1的可能值 for a1 := A - T_deviation; a1 <= A+T_deviation; a1 += T_deviation_half { for b1 := B - T_deviation; b1 <= B+T_deviation; b1 += T_deviation_half { // 检查是否存在满足条件的C值 minC := math.Max(a1-T_deviation_half, b1-T_deviation_half) maxC := math.Min(a1+T_deviation_half, b1+T_deviation_half) if minC <= maxC { dataPairs = append(dataPairs, DataPair{A1: a1, B1: b1}) } } } if len(dataPairs) == 1 { A1 = dataPairs[0].A1 B1 = dataPairs[0].B1 } if len(dataPairs) > 1 { // 寻找最优解 minAdjustment := math.MaxFloat64 var optimalPair DataPair for _, pair := range dataPairs { adjustment := math.Abs(pair.A1-C) + math.Abs(pair.B1-D) if adjustment < minAdjustment { minAdjustment = adjustment optimalPair = pair } } A1 = optimalPair.A1 B1 = optimalPair.B1 } if A1 == -100 || B1 == -100 { return A1, B1, false } return A1, B1, true } /* 计算绑定点的值 A 温湿度绑定点1平均值 B 温湿度绑定点2平均值 C 测点平均值 */ func findAverageOptimalAdjustment(A, B, C float64) float64 { // 计算有效区间 minA := A - 0.5 maxA := A + 0.5 minB := B - 0.5 maxB := B + 0.5 // 求交集范围 lowerBound := math.Max(minA, minB) upperBound := math.Min(maxA, maxB) // 确定最优调整值 var optimalC float64 if C < lowerBound { optimalC = lowerBound // 取区间下限 } else if C > upperBound { optimalC = upperBound // 取区间上限 } else { optimalC = C // 已在区间内无需调整 } return optimalC } func generateTemperatureCurve(startTemp, minTemp float64, steps int) []float64 { if steps <= 0 { return []float64{} } // 计算动态参数确保严格递减且高于最低温度 base := minTemp + 0.1 // 确保所有值高于minTemp decayFactor := math.Log((startTemp-base)/(minTemp+0.5-base)) / float64(steps-1) temperatures := make([]float64, steps) prevTemp := startTemp for i := 0; i < steps; i++ { // 使用指数衰减模型确保严格单调递减 temp := base + (startTemp-base)*math.Exp(-decayFactor*float64(i)) // 确保严格递减且高于最低温度 if temp >= prevTemp { temp = prevTemp - 0.1 } if temp <= minTemp { temp = minTemp + 0.01 + 0.05*float64(steps-i)/float64(steps) } // 保持精度并确保唯一性 temp = math.Round(temp*100) / 100 temperatures[i] = temp prevTemp = temp } return temperatures } func generateRisingCurve(minTemp, maxTemp float64, steps int) []float64 { if steps <= 0 { return []float64{} } // 计算动态参数确保严格递增且低于最高温度 growthFactor := math.Log((maxTemp-minTemp-0.1)/0.1) / float64(steps-1) temperatures := make([]float64, steps) prevTemp := minTemp for i := 0; i < steps; i++ { // 使用指数增长模型确保严格单调递增 temp := minTemp + (maxTemp-minTemp)*(1-math.Exp(-growthFactor*float64(i))) // 确保严格递增且低于最高温度 if temp <= prevTemp { temp = prevTemp + 0.1 } if temp >= maxTemp { temp = maxTemp - 0.01 - 0.05*float64(steps-i-1)/float64(steps) } // 保持精度并确保唯一性 temp = math.Round(temp*100) / 100 temperatures[i] = temp prevTemp = temp } return temperatures } // 鼓包/缺数据及超上下限处理 // 处理顺序:1. 超高温修复 -> 2. 超低温修复 -> 3. 鼓包修复 -> 4. 兜底裁剪 func (c *TaskDataHandleController) SSE_Process_hump_data() { T_task_id := c.GetString("T_task_id") c.Ctx.ResponseWriter.Header().Set("Content-Type", "text/event-stream") c.Ctx.ResponseWriter.Header().Set("Cache-Control", "no-cache") c.Ctx.ResponseWriter.Header().Set("Connection", "keep-alive") lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "综合异常检测与修复任务开始......"}) Task_r, err := Task.Read_Task(T_task_id) if err != nil { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "获取任务信息失败!"}) return } // 从表单中获取时间范围 开始时间 := VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "温度分布特性的测试与分析(满载)开始时间") 结束时间 := VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "温度分布特性的测试与分析(满载)结束时间") if len(开始时间) == 0 || len(结束时间) == 0 || 开始时间 == "null" || 结束时间 == "null" { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "获取表单中'温度分布特性的测试与分析(满载)开始/结束时间'失败,请先填写表单时间字段!"}) return } lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "时间范围: " + 开始时间 + " ~ " + 结束时间}) DeviceClassList_list := Device.Read_DeviceClassList_List_id_By_Terminal(Task_r.T_class, false) lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "终端总共:" + lib.To_string(len(DeviceClassList_list)) + " 个,开始检测..."}) if len(DeviceClassList_list) == 0 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "没有找到终端设备!"}) return } // ========== 步骤1:加载所有传感器数据 ========== allData := make(map[string]map[string]float32) for _, dev := range DeviceClassList_list { dataList, cnt := Task.Read_TaskData_ById_List_AES(Task_r.T_task_id, dev.T_sn, dev.T_id, 开始时间, 结束时间, 0, 9999) if cnt < 10 { continue } timeMap := make(map[string]float32) for _, d := range dataList { timeMap[d.T_time] = d.T_t } allData[dev.T_id] = timeMap } // ========== 步骤2:收集所有时间点并排序 ========== timeSet := make(map[string]bool) for _, tm := range allData { for t := range tm { timeSet[t] = true } } allTimes := make([]string, 0, len(timeSet)) for t := range timeSet { allTimes = append(allTimes, t) } sort.Strings(allTimes) if len(allTimes) < 5 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "时间点不足,无法检测"}) } else { // ========== 步骤2.5:从数据库表单读取温度控制范围 ========== // 1. 兼容读取最高值/最大值 最高值Str := VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "温度控制范围最高值") if 最高值Str == "" { 最高值Str = VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "温度控制范围最大值") } 温度控制范围最高值 := lib.To_float32(最高值Str) // 2. 兼容读取最低值/最小值 最低值Str := VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "温度控制范围最低值") if 最低值Str == "" { 最低值Str = VerifyTemplate.Read_VerifyTemplateMapData_T_name(Task_r.T_task_id, Task_r.T_VerifyTemplate_id, "温度控制范围最小值") } 温度控制范围最低值 := lib.To_float32(最低值Str) // 3. 校验读取结果 if 温度控制范围最高值 <= 0 || 温度控制范围最低值 >= 温度控制范围最高值 { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: "获取表单中'温度控制范围最高值/最低值'失败或逻辑无效,请先填写!"}) return } // 4. 留出 0.1 的安全余量 (防止浮点数精度导致刚好等于上限被误判) upperLimit := 温度控制范围最高值 - 0.1 lowerLimit := 温度控制范围最低值 + 0.1 lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("温度控制范围: %.2f°C ~ %.2f°C (已含0.1安全余量)", lowerLimit, upperLimit)}) // ========== 检测参数 ========== const DEVIATION_THRESHOLD float32 = 0.8 const MIN_DURATION = 5 const MERGE_GAP = 5 const MIN_RISE_FOR_BULGE float32 = 0.5 const MAX_MONOTONIC_EXPAND = 120 const TREND_WINDOW = 20 const TREND_SLOPE_THRESHOLD float32 = 0.01 const TREND_MIN_DURATION = 30 const MONOTONIC_MIN_DURATION = 20 const MONOTONIC_TOLERANCE = 2 type BulgeSegment struct { Device string StartTime string EndTime string } var allBulges []BulgeSegment // ========== 步骤3:鼓包检测 (基于原始数据) ========== for _, dev := range DeviceClassList_list { tm := allData[dev.T_id] if tm == nil || len(tm) < 10 { continue } type DataPoint struct { Time string Temp float32 Deviation float32 IsBulge bool } var points []DataPoint var temps []float32 for _, t := range allTimes { if v, ok := tm[t]; ok { refAll := medianOfAllDevices(allData, t) deviation := v - refAll points = append(points, DataPoint{t, v, deviation, deviation > DEVIATION_THRESHOLD}) temps = append(temps, v) } } n := len(points) if n < MIN_DURATION { continue } // 1. 偏差鼓包检测 i := 0 for i < n { if points[i].IsBulge { i++ continue } j := i for j+1 < n && !points[j+1].IsBulge { j++ } leftTrue := i-1 >= 0 && points[i-1].IsBulge rightTrue := j+1 < n && points[j+1].IsBulge gapLen := j - i + 1 if leftTrue && rightTrue && gapLen <= MERGE_GAP { for k := i; k <= j; k++ { points[k].IsBulge = true } } i = j + 1 } type Seg struct{ Start, End int } var bulgeSegs []Seg start := -1 for i, p := range points { if p.IsBulge && start == -1 { start = i } else if !p.IsBulge && start != -1 { bulgeSegs = append(bulgeSegs, Seg{start, i - 1}) start = -1 } } if start != -1 { bulgeSegs = append(bulgeSegs, Seg{start, n - 1}) } // 2. 斜率趋势检测 trendMask := detectTrends(temps, TREND_WINDOW, TREND_SLOPE_THRESHOLD, TREND_MIN_DURATION) var trendSegs []Seg start = -1 for i, v := range trendMask { if v && start == -1 { start = i } else if !v && start != -1 { trendSegs = append(trendSegs, Seg{start, i - 1}) start = -1 } } if start != -1 { trendSegs = append(trendSegs, Seg{start, n - 1}) } // 3. 单调趋势检测 monoMask := detectMonotonicTrends(temps, MONOTONIC_MIN_DURATION, MONOTONIC_TOLERANCE) var monoSegs []Seg start = -1 for i, v := range monoMask { if v && start == -1 { start = i } else if !v && start != -1 { monoSegs = append(monoSegs, Seg{start, i - 1}) start = -1 } } if start != -1 { monoSegs = append(monoSegs, Seg{start, n - 1}) } // 合并所有段 allSegs := append(bulgeSegs, trendSegs...) allSegs = append(allSegs, monoSegs...) if len(allSegs) == 0 { continue } sort.Slice(allSegs, func(i, j int) bool { return allSegs[i].Start < allSegs[j].Start }) var merged []Seg for _, seg := range allSegs { if len(merged) == 0 { merged = append(merged, seg) } else { last := &merged[len(merged)-1] if seg.Start <= last.End+MERGE_GAP { if seg.End > last.End { last.End = seg.End } } else { merged = append(merged, seg) } } } for _, seg := range merged { if seg.End-seg.Start+1 < MIN_DURATION { continue } peakIdx := seg.Start peakVal := points[seg.Start].Temp for k := seg.Start + 1; k <= seg.End; k++ { if points[k].Temp > peakVal { peakVal = points[k].Temp peakIdx = k } } startIdx := seg.Start steps := 0 for startIdx > 0 && steps < MAX_MONOTONIC_EXPAND && points[startIdx-1].Temp < points[startIdx].Temp { startIdx-- steps++ } endIdx := peakIdx steps = 0 for endIdx < len(points)-1 && steps < MAX_MONOTONIC_EXPAND && points[endIdx+1].Temp <= points[endIdx].Temp { endIdx++ steps++ } if endIdx-startIdx+1 < MIN_DURATION { continue } maxDev := float32(0) for k := startIdx; k <= endIdx; k++ { if float32(math.Abs(float64(points[k].Deviation))) > maxDev { maxDev = float32(math.Abs(float64(points[k].Deviation))) } } if maxDev < 0.3 { continue } rise := points[peakIdx].Temp - points[startIdx].Temp minTemp := points[startIdx].Temp for k := startIdx; k <= endIdx; k++ { if points[k].Temp < minTemp { minTemp = points[k].Temp } } drop := points[startIdx].Temp - minTemp if rise >= MIN_RISE_FOR_BULGE || drop >= MIN_RISE_FOR_BULGE { allBulges = append(allBulges, BulgeSegment{ Device: dev.T_id, StartTime: points[startIdx].Time, EndTime: points[endIdx].Time, }) } } } // 输出鼓包检测结果 if len(allBulges) > 0 { deviceBulges := make(map[string][]BulgeSegment) for _, b := range allBulges { deviceBulges[b.Device] = append(deviceBulges[b.Device], b) } for deviceId, bulges := range deviceBulges { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf("%s 检测到 %d 个鼓包段", deviceId, len(bulges))}) } } // ========== 步骤4:按顺序修复 (先上下限,后鼓包) ========== const BLEND = 10 const TARGET_SMOOTH = 7 const EPS float32 = 0.03 const STABLE_N = 4 const MAX_EXPAND = 30 for _, dev := range DeviceClassList_list { tm := allData[dev.T_id] if tm == nil || len(tm) < 10 { continue } var fixTimes []string var fixTemps []float32 var fixRefExcl []float32 for _, t := range allTimes { if v, ok := tm[t]; ok { refExcl := leaveOneOutMedian(allData, dev.T_id, t) fixTimes = append(fixTimes, t) fixTemps = append(fixTemps, v) fixRefExcl = append(fixRefExcl, refExcl) } } n := len(fixTimes) if n < 10 { continue } offsetWindow := 241 if n/2 < offsetWindow { offsetWindow = n / 2 } if offsetWindow < 11 { offsetWindow = 11 } diff := make([]float32, n) for i := 0; i < n; i++ { diff[i] = fixTemps[i] - fixRefExcl[i] } offset := medianRolling(diff, offsetWindow) target := make([]float32, n) for i := 0; i < n; i++ { target[i] = fixRefExcl[i] + offset[i] } targetS := meanRolling(target, TARGET_SMOOTH) yFixed := make([]float32, n) copy(yFixed, fixTemps) // 【第1步】修复超高温段 highSegs := findAndMergeLimitSegments(fixTemps, upperLimit, true, MERGE_GAP) for _, seg := range highSegs { s2, e2 := expandByLimitCondition(yFixed, seg.Start, seg.End, upperLimit, MAX_EXPAND, STABLE_N, true) yFixed = compressSegment(yFixed, targetS, s2, e2, n, upperLimit, BLEND) } // 【第2步】修复超低温段 lowSegs := findAndMergeLimitSegments(fixTemps, lowerLimit, false, MERGE_GAP) for _, seg := range lowSegs { s2, e2 := expandByLimitCondition(yFixed, seg.Start, seg.End, lowerLimit, MAX_EXPAND, STABLE_N, false) // 低温修复技巧:取负值,复用 compressSegment 将其压到 -lowerLimit 之下,然后再取负还原 yNeg := make([]float32, n) targetNeg := make([]float32, n) for i := 0; i < n; i++ { yNeg[i] = -yFixed[i] targetNeg[i] = -targetS[i] } yNegFixed := compressSegment(yNeg, targetNeg, s2, e2, n, -lowerLimit, BLEND) for i := 0; i < n; i++ { yFixed[i] = -yNegFixed[i] } } // 【第3步】修复鼓包段 devBulges := false for _, bulge := range allBulges { if bulge.Device != dev.T_id { continue } devBulges = true s, e := -1, -1 for i, ft := range fixTimes { if ft == bulge.StartTime { s = i } if ft == bulge.EndTime { e = i } } if s < 0 || e < 0 || e <= s { continue } residual := make([]float32, n) for i := 0; i < n; i++ { residual[i] = yFixed[i] - targetS[i] } s2, e2 := expandByResidual(residual, s, e, EPS, STABLE_N, MAX_EXPAND) yFixed = compressSegment(yFixed, targetS, s2, e2, n, upperLimit, BLEND) } // 【第4步】兜底裁剪:确保绝对不超限 for i := 0; i < n; i++ { if yFixed[i] > upperLimit { yFixed[i] = upperLimit } else if yFixed[i] < lowerLimit { yFixed[i] = lowerLimit } } // 写入数据库 if devBulges || len(highSegs) > 0 || len(lowSegs) > 0 { Task.DeleteTaskDataByTimeRange(Task_r.T_task_id, dev.T_sn, dev.T_id, fixTimes[0], fixTimes[n-1]) var valueStrings []string for i := 0; i < n; i++ { valueStrings = append(valueStrings, fmt.Sprintf("('%s','%s',%.2f,0,'%s')", dev.T_sn, dev.T_id, yFixed[i], fixTimes[i])) } err = Task.Batch_Adds_TaskData(T_task_id, valueStrings) if err != nil { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 2, Msg: dev.T_id + " 数据修复失败: " + err.Error()}) } else { lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: fmt.Sprintf(" %s 修复完成 (共%d个数据点)", dev.T_id, n)}) } } } } // ========== 步骤5:缺数据检测 (保持不变) ========== lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 0, Msg: "开始缺数据检测..."}) totalMissing := 0 for _, dev := range DeviceClassList_list { dataList, cnt := Task.Read_TaskData_ById_List_AES(Task_r.T_task_id, dev.T_sn, dev.T_id, 开始时间, 结束时间, 0, 9999) if cnt < 2 { continue } missingCount := 0 for i := 0; i < len(dataList)-1; i++ { ct, _ := time.Parse("2006-01-02 15:04", dataList[i].T_time) nt, _ := time.Parse("2006-01-02 15:04", dataList[i+1].T_time) interval := int(nt.Unix() - ct.Unix()) if interval == 120 { t := ct.Add(60 * time.Second).Format("2006-01-02 15:04") ttt := (dataList[i].T_t + dataList[i+1].T_t) / 2 trht := (dataList[i].T_rh + dataList[i+1].T_rh) / 2 Task.InsertTaskData(Task_r.T_task_id, Task.TaskData_{ T_sn: dataList[i].T_sn, T_id: dataList[i].T_id, T_t: ttt, T_rh: trht, T_time: t, }) missingCount++ } else if interval > 120 { device := Device.DeviceClassList{ T_class: dev.T_class, T_id: dev.T_id, T_sn: dev.T_sn, T_remark: dev.T_remark, } c.SetAdjacentDeviceAVGTaskData(T_task_id, device, 开始时间, 结束时间) missingCount++ break } } if missingCount > 0 { totalMissing += missingCount } } lib.SseWriteJSON(c.Ctx.ResponseWriter, lib.JSONSSE{State: 1, Msg: fmt.Sprintf("完成!缺数据补全: %d处", totalMissing)}) c.Ctx.ResponseWriter.WriteHeader(http.StatusOK) } // ==================== 新增辅助函数:上下限修复专用 ==================== // findAndMergeLimitSegments 查找并合并超出限值的连续段 func findAndMergeLimitSegments(temps []float32, limit float32, isHigh bool, mergeGap int) []struct{ Start, End int } { n := len(temps) mask := make([]bool, n) for i := 0; i < n; i++ { if isHigh && temps[i] > limit { mask[i] = true } else if !isHigh && temps[i] < limit { mask[i] = true } } var segs []struct{ Start, End int } start := -1 for i := 0; i < n; i++ { if mask[i] && start == -1 { start = i } else if !mask[i] && start != -1 { segs = append(segs, struct{ Start, End int }{start, i - 1}) start = -1 } } if start != -1 { segs = append(segs, struct{ Start, End int }{start, n - 1}) } if len(segs) == 0 { return segs } // 合并间隔 <= mergeGap 的段 var merged []struct{ Start, End int } merged = append(merged, segs[0]) for i := 1; i < len(segs); i++ { last := &merged[len(merged)-1] if segs[i].Start <= last.End+mergeGap { if segs[i].End > last.End { last.End = segs[i].End } } else { merged = append(merged, segs[i]) } } return merged } // expandByLimitCondition 基于限值条件向外扩展区间 func expandByLimitCondition(yFixed []float32, s, e int, limit float32, maxExpand, stableN int, isHigh bool) (int, int) { n := len(yFixed) s2, e2 := s, e // 向左扩 steps := 0 for s2 > 0 && steps < maxExpand { l := s2 - stableN if l < 0 { l = 0 } window := yFixed[l:s2] allOk := len(window) == stableN if allOk { for _, v := range window { if isHigh && v > limit+0.01 { allOk = false break } else if !isHigh && v < limit-0.01 { allOk = false break } } } if allOk { break } s2-- steps++ } // 向右扩 steps = 0 for e2 < n-1 && steps < maxExpand { r := e2 + 1 + stableN if r > n { r = n } window := yFixed[e2+1 : r] allOk := len(window) == stableN if allOk { for _, v := range window { if isHigh && v > limit+0.01 { allOk = false break } else if !isHigh && v < limit-0.01 { allOk = false break } } } if allOk { break } e2++ steps++ } return s2, e2 } // ==================== 以下保留您原有的辅助函数 ==================== // detectTrends 斜率趋势检测(线性回归斜率绝对值 > 阈值,持续时间 >= minDuration) func detectTrends(series []float32, window int, slopeThresh float32, minDuration int) []bool { n := len(series) mask := make([]bool, n) if n < window { return mask } slopes := make([]float32, n) for i := 0; i <= n-window; i++ { // 计算线性回归斜率 x := make([]float64, window) y := make([]float64, window) for k := 0; k < window; k++ { x[k] = float64(k) y[k] = float64(series[i+k]) } // 最小二乘法 sumX, sumY, sumXY, sumX2 := 0.0, 0.0, 0.0, 0.0 for k := 0; k < window; k++ { sumX += x[k] sumY += y[k] sumXY += x[k] * y[k] sumX2 += x[k] * x[k] } denom := float64(window)*sumX2 - sumX*sumX if denom == 0 { slopes[i+window/2] = 0 } else { slope := float32((float64(window)*sumXY - sumX*sumY) / denom) slopes[i+window/2] = slope } } // 强斜率标记 strong := make([]bool, n) for i := 0; i < n; i++ { if math.Abs(float64(slopes[i])) > float64(slopeThresh) { strong[i] = true } } // 找连续强斜率段(长度 >= minDuration) count := 0 for i := 0; i < n; i++ { if strong[i] { count++ } else { if count >= minDuration { for j := i - count; j < i; j++ { mask[j] = true } } count = 0 } } if count >= minDuration { for j := n - count; j < n; j++ { mask[j] = true } } return mask } // detectMonotonicTrends 单调趋势检测(同向比例 >= 0.8,持续 >= minDuration) func detectMonotonicTrends(series []float32, minDuration int, tolerance int) []bool { n := len(series) mask := make([]bool, n) if n < minDuration { return mask } // 计算差分符号 signs := make([]int, n-1) for i := 0; i < n-1; i++ { if series[i+1] > series[i] { signs[i] = 1 } else if series[i+1] < series[i] { signs[i] = -1 } else { signs[i] = 0 } } window := minDuration half := window / 2 for i := 0; i < n; i++ { l := i - half if l < 0 { l = 0 } r := i + half + 1 if r > n-1 { r = n - 1 } if l > r { continue } seg := signs[l:r] pos, neg := 0, 0 for _, s := range seg { if s > 0 { pos++ } else if s < 0 { neg++ } } total := pos + neg if total < window/2 { continue } ratio := float32(max(pos, neg)) / float32(total) if ratio >= 0.8 { mask[i] = true } } // 合并连续段,去除长度小于 minDuration 的 // 简单做法:先找到段,再过滤 var segs []struct{ s, e int } start := -1 for i, v := range mask { if v && start == -1 { start = i } else if !v && start != -1 { segs = append(segs, struct{ s, e int }{start, i - 1}) start = -1 } } if start != -1 { segs = append(segs, struct{ s, e int }{start, n - 1}) } finalMask := make([]bool, n) for _, seg := range segs { if seg.e-seg.s+1 >= minDuration { for i := seg.s; i <= seg.e; i++ { finalMask[i] = true } } } return finalMask } // 辅助函数:max func max(a, b int) int { if a > b { return a } return b } // medianOfSlice 计算float32切片的中位数 func medianOfSlice(vals []float32) float32 { if len(vals) == 0 { return 0 } sorted := make([]float32, len(vals)) copy(sorted, vals) sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] }) n := len(sorted) if n%2 == 0 { return (sorted[n/2-1] + sorted[n/2]) / 2 } return sorted[n/2] } // medianOfAllDevices 计算所有设备在某个时间点的中位数 func medianOfAllDevices(allData map[string]map[string]float32, t string) float32 { var vals []float32 for _, tm := range allData { if v, ok := tm[t]; ok { vals = append(vals, v) } } return medianOfSlice(vals) } // leaveOneOutMedian 计算排除当前设备后其他设备的中位数 // 与c.txt一致:在所有值中找到与当前值最接近的值,排除它,取剩余的中位数 func leaveOneOutMedian(allData map[string]map[string]float32, deviceId string, t string) float32 { var vals []float32 for _, tm := range allData { if v, ok := tm[t]; ok { vals = append(vals, v) } } if len(vals) <= 1 { return medianOfAllDevices(allData, t) } // 获取当前设备的值 currentVal := float32(0) if tm, ok := allData[deviceId]; ok { if v, ok2 := tm[t]; ok2 { currentVal = v } } // 找到与当前值最接近的值的索引,排除它 closestIdx := 0 closestDiff := float32(math.MaxFloat32) for i, v := range vals { diff := float32(math.Abs(float64(v - currentVal))) if diff < closestDiff { closestDiff = diff closestIdx = i } } // 排除最接近的值 vv := append(vals[:closestIdx], vals[closestIdx+1:]...) if len(vv) == 0 { return medianOfAllDevices(allData, t) } return medianOfSlice(vv) } // medianRolling 滚动中位数 func medianRolling(arr []float32, window int) []float32 { n := len(arr) result := make([]float32, n) half := window / 2 minPeriods := window / 5 if minPeriods < 5 { minPeriods = 5 } for i := 0; i < n; i++ { start := i - half if start < 0 { start = 0 } end := i + half + 1 if end > n { end = n } if end-start < minPeriods { if n >= minPeriods { // 边界用可用数据 start = 0 end = n } else { result[i] = arr[i] continue } } vals := arr[start:end] result[i] = medianOfSlice(vals) } return result } // meanRolling 滚动均值 func meanRolling(arr []float32, window int) []float32 { n := len(arr) result := make([]float32, n) half := window / 2 minPeriods := window / 3 if minPeriods < 3 { minPeriods = 3 } for i := 0; i < n; i++ { start := i - half if start < 0 { start = 0 } end := i + half + 1 if end > n { end = n } if end-start < minPeriods { start = 0 end = n } sum := float32(0) count := 0 for j := start; j < end; j++ { sum += arr[j] count++ } if count > 0 { result[i] = sum / float32(count) } } return result } // smoothW 生成平滑混合权重 // w[s:e] = 1.0, 前后blend点线性过渡到0 func smoothW(n, s, e, blend int) []float32 { w := make([]float32, n) if e <= s { return w } // 核心区间 = 1.0 for i := s; i <= e; i++ { w[i] = 1.0 } // 左侧过渡 ls := s - blend if ls < 0 { ls = 0 } if ls < s { length := s - ls + 1 // length >= 2 for i := 0; i < length; i++ { w[ls+i] = float32(i) / float32(length-1) } } // 右侧过渡 re := e + blend if re >= n { re = n - 1 } if e < re { length := re - e + 1 // length >= 2 for i := 0; i < length; i++ { w[e+i] = float32(length-1-i) / float32(length-1) } } return w } // expandByResidual 以residual为依据扩展区间 // 向两侧扩展直到 residual<=eps 连续stable_n个点 func expandByResidual(residual []float32, s, e int, eps float32, stableN, maxExpand int) (int, int) { n := len(residual) s2, e2 := s, e // 向左扩 steps := 0 for s2 > 0 && steps < maxExpand { l := s2 - stableN if l < 0 { l = 0 } window := residual[l:s2] allOk := len(window) == stableN if allOk { for _, v := range window { if v > eps { allOk = false break } } } if allOk { break } s2-- steps++ } // 向右扩 steps = 0 for e2 < n-1 && steps < maxExpand { r := e2 + 1 + stableN if r > n { r = n } window := residual[e2+1 : r] allOk := len(window) == stableN if allOk { for _, v := range window { if v > eps { allOk = false break } } } if allOk { break } e2++ steps++ } return s2, e2 } // compressSegment 保形压缩(c.txt算法) // 把[s,e](含超限外扩)整体压到 upperLimit 之下 // 1) 基准峰(target_s)若超限,先按形状等比压矮(保留尖峰,不削平) // 2) 鼓包偏差(dev)再按剩余余量等比压缩 // 数学保证:区间内 inner <= upperLimit;blend 为凸组合,不超限 func compressSegment(yFixed, targetS []float32, s, e, n int, upperLimit float32, blend int) []float32 { // 1. 超限外扩:肩膀上所有 >上限 的点都包进区间 guard := 0 for s > 0 && guard < 500 && yFixed[s-1] > upperLimit { s-- guard++ } guard = 0 for e < n-1 && guard < 500 && yFixed[e+1] > upperLimit { e++ guard++ } w := smoothW(n, s, e, blend) // 2. 基准峰保形压缩 baseMin := float32(math.MaxFloat32) for i := s; i <= e; i++ { if targetS[i] < baseMin { baseMin = targetS[i] } } // 【关键修复】baseDev 必须是全局的,模仿 Python 的 target_s - base_min baseDev := make([]float32, n) maxBaseDev := float32(0) for i := 0; i < n; i++ { baseDev[i] = targetS[i] - baseMin // 仅统计区间内的最大偏差用于计算缩放比例 if i >= s && i <= e { if baseDev[i] > maxBaseDev { maxBaseDev = baseDev[i] } } } allowBase := upperLimit - baseMin if allowBase < 0 { allowBase = 0 } scaleB := float32(1.0) if maxBaseDev > 0 { scaleB = allowBase / maxBaseDev if scaleB > 1 { scaleB = 1 } } targetC := make([]float32, n) for i := 0; i < n; i++ { targetC[i] = baseMin + baseDev[i]*scaleB } // 3. 鼓包偏差保形压缩 // 【关键修复】dev 必须是全局的,模仿 Python 的 y_fixed - target_s dev := make([]float32, n) maxDev := float32(0) for i := 0; i < n; i++ { dev[i] = yFixed[i] - targetS[i] // 仅统计区间内的最大偏差 if i >= s && i <= e { if dev[i] > maxDev { maxDev = dev[i] } } } maxTargetC := float32(0) for i := s; i <= e; i++ { if targetC[i] > maxTargetC { maxTargetC = targetC[i] } } headroom := upperLimit - maxTargetC if headroom < 0 { headroom = 0 } scale := float32(1.0) if maxDev > 0 { scale = headroom / maxDev if scale > 1 { scale = 1 } } inner := make([]float32, n) for i := 0; i < n; i++ { inner[i] = targetC[i] + dev[i]*scale } // 4. 混合 result := make([]float32, n) for i := 0; i < n; i++ { result[i] = (1-w[i])*yFixed[i] + w[i]*inner[i] } return result }