feiyu02
15 小时以前 e58a05b78d09bcd4c1a12e8610c5adfc316494e8
2025.12.18
已修改13个文件
已添加1个文件
380 ■■■■ 文件已修改
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/BaseExceptionContinuous.kt 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/report/MissionGridFusion.kt 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/config/RTExcWindLevelConfig.kt 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedClue.kt 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedData.kt 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedSource.kt 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/domain/entity/BaseRealTimeData.kt 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/lightshare/service/impl/MissionServiceImpl.kt 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/model/epw/MutationDataPreprocess.kt 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/socket/eunm/FactorType.kt 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-test.yml 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/kotlin/com/flightfeather/uav/biz/mission/MissionUtilTest.kt 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/kotlin/com/flightfeather/uav/biz/sourcetrace/SourceTraceControllerTest.kt 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/kotlin/com/flightfeather/uav/lightshare/service/impl/MissionServiceImplTest.kt 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/BaseExceptionContinuous.kt
@@ -75,7 +75,7 @@
    }
    /**
     * åˆ¤æ–­æ•°æ®é‡çº§åœ¨å¼‚常判断的范围内
     * åˆ¤æ–­æ•°æ®é‡çº§æ˜¯å¦åœ¨å¼‚常判断的范围内
     * é»˜è®¤æ‰€æœ‰é‡çº§éƒ½åœ¨å¼‚常判断的范围内
     */
    open fun judgeDataScale(p: BaseRealTimeData?, n: BaseRealTimeData): MutableMap<FactorType, Boolean> {
@@ -126,9 +126,15 @@
        }
    }
    /**
     * å¼‚常数据处理核心判断逻辑
     */
    override fun onNextData(data: BaseRealTimeData) {
        // 1. åˆ¤æ–­æ–°æ•°æ®çš„æ—¶é—´è¿žç»­æ€§
        val isContinue = isContinuous(lastData, data)
        // 2. è®¡ç®—各个监测因子是否发生异常
        val hasException = judge(lastData, data)
        // 3. éåŽ†é…ç½®ä¸­é€‰ä¸­çš„ç›‘æµ‹å› å­ï¼Œåˆ¤æ–­æ˜¯å¦å‘ç”Ÿå¼‚å¸¸
        config.factorFilter.selectedList.forEach { s ->
            val f = s.main
            // æŽ’除此异常类型不适用的监测因子
@@ -151,12 +157,18 @@
                it.addHistoryData(data)
            }
        }
        // 4. æ›´æ–°æœ€æ–°æ•°æ®
        lastData = data
        // 5. ä¿å­˜å¹¶ç§»é™¤å•因子异常
        removeSingleFactor(data)
        // 6. æ£€æŸ¥å»¶è¿Ÿå¼‚常
        val fittedComb = checkDelayedExceptions(data)
        // 7. åˆå¹¶å¼‚常结果
        mergeExceptionResult(data, fittedComb)
        // 8. è§¦å‘新异常结果事件
        onNewResult(result)
        // 9. æ¸…除异常记录
        clearExceptions(data)
    }
src/main/kotlin/com/flightfeather/uav/biz/report/MissionGridFusion.kt
@@ -149,16 +149,32 @@
                                    town = if (address.address.contains(address.streetNumber)) {
                                        address.address
                                    } else {
                                        address.address + "(" + address.street + address.streetNumber + ")"
//                                        address.address + "(" + address.street + address.streetNumber + ")"
                                        address.address
                                    }
                                }
                                val polygon = listOf(
                                var polygon = listOf(
                                    gf.cell.point1Lon.toDouble() to gf.cell.point1Lat.toDouble(),
                                    gf.cell.point2Lon.toDouble() to gf.cell.point2Lat.toDouble(),
                                    gf.cell.point3Lon.toDouble() to gf.cell.point3Lat.toDouble(),
                                    gf.cell.point4Lon.toDouble() to gf.cell.point4Lat.toDouble(),
                                )
                                bounds = MapUtil.calFourBoundaries(polygon)
                                // å°†ç½‘格搜索范围扩大一圈网格(针对北纬东经的情况下)
                                bounds?.let { bs->
                                    val offsetLon = bs[1] - bs[0]
                                    val offsetLat = bs[3] - bs[2]
                                    polygon = listOf(
                                        // ç½‘格西北角
                                        bs[0] - offsetLon to bs[3] + offsetLat,
                                        // ç½‘格东北角
                                        bs[1] + offsetLon to bs[3] + offsetLat,
                                        // ç½‘格东南角
                                        bs[1] + offsetLon to bs[2] - offsetLat,
                                        // ç½‘格西南角
                                        bs[0] - offsetLon to bs[2] - offsetLat,
                                    )
                                }
                                highRiskScenes =
                                    sceneInfoRep.findByPolygon(polygon, listOf(SceneType.TYPE19, SceneType.TYPE20, SceneType.TYPE21))
                            }
@@ -189,16 +205,32 @@
                                    town = if (address.address.contains(address.streetNumber)) {
                                        address.address
                                    } else {
                                        address.address + address.street + address.streetNumber
//                                        address.address + "(" + address.street + address.streetNumber + ")"
                                        address.address
                                    }
                                }
                                val polygon = listOf(
                                var polygon = listOf(
                                    gf.cell.point1Lon.toDouble() to gf.cell.point1Lat.toDouble(),
                                    gf.cell.point2Lon.toDouble() to gf.cell.point2Lat.toDouble(),
                                    gf.cell.point3Lon.toDouble() to gf.cell.point3Lat.toDouble(),
                                    gf.cell.point4Lon.toDouble() to gf.cell.point4Lat.toDouble(),
                                )
                                bounds = MapUtil.calFourBoundaries(polygon)
                                // å°†ç½‘格搜索范围扩大一圈网格(针对北纬东经的情况下)
                                bounds?.let { bs->
                                    val offsetLon = bs[1] - bs[0]
                                    val offsetLat = bs[3] - bs[2]
                                    polygon = listOf(
                                        // ç½‘格西北角
                                        bs[0] - offsetLon to bs[3] + offsetLat,
                                        // ç½‘格东北角
                                        bs[1] + offsetLon to bs[3] + offsetLat,
                                        // ç½‘格东南角
                                        bs[1] + offsetLon to bs[2] - offsetLat,
                                        // ç½‘格西南角
                                        bs[0] - offsetLon to bs[2] - offsetLat,
                                    )
                                }
                                highRiskScenes =
                                    sceneInfoRep.findByPolygon(polygon, listOf(SceneType.TYPE19, SceneType.TYPE20, SceneType.TYPE21))
                            })
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/config/RTExcWindLevelConfig.kt
@@ -49,7 +49,7 @@
        .0 to 1.5,
        0.2 to 0.5,
        DistanceType.TYPE2,
        1
        2
    )
    // 2 - 4级风
@@ -95,6 +95,24 @@
            DistanceType.TYPE1,
            1
        ),
        FactorType.NO to WindLevelCondition(
            .0 to 1.5,
            8.0 to Double.MAX_VALUE,
            DistanceType.TYPE1,
            4
        ),
        FactorType.NO2 to WindLevelCondition(
            .0 to 1.5,
            4.0 to Double.MAX_VALUE,
            DistanceType.TYPE1,
            4
        ),
        FactorType.CO to WindLevelCondition(
            .0 to 1.5,
            10.0 to Double.MAX_VALUE,
            DistanceType.TYPE1,
            5
        ),
    )
    // åœ¨é£Žé€Ÿå¤„于1.6 - 7.9 m/s ä¹‹é—´æ—¶
    var changeRateUp2 = mutableMapOf(
@@ -119,6 +137,24 @@
            DistanceType.TYPE3,
            1
        ),
        FactorType.NO to WindLevelCondition(
            1.6 to 7.9,
            8.0 to Double.MAX_VALUE,
            DistanceType.TYPE3,
            4
        ),
        FactorType.NO2 to WindLevelCondition(
            1.6 to 7.9,
            4.0 to Double.MAX_VALUE,
            DistanceType.TYPE3,
            4
        ),
        FactorType.CO to WindLevelCondition(
            1.6 to 7.9,
            10.0 to Double.MAX_VALUE,
            DistanceType.TYPE3,
            5
        ),
    )
    // åœ¨é£Žé€Ÿå¤„于8.0 - 13.8 m/s ä¹‹é—´æ—¶
    var changeRateUp3 = mutableMapOf(
@@ -142,6 +178,24 @@
            6.0 to Double.MAX_VALUE,
            DistanceType.TYPE4,
            1
        ),
        FactorType.NO to WindLevelCondition(
            8.0 to 13.8,
            8.0 to Double.MAX_VALUE,
            DistanceType.TYPE4,
            4
        ),
        FactorType.NO2 to WindLevelCondition(
            8.0 to 13.8,
            4.0 to Double.MAX_VALUE,
            DistanceType.TYPE4,
            4
        ),
        FactorType.CO to WindLevelCondition(
            8.0 to 13.8,
            10.0 to Double.MAX_VALUE,
            DistanceType.TYPE4,
            5
        ),
    )
@@ -168,6 +222,24 @@
            DistanceType.TYPE1,
            3
        ),
        FactorType.NO to WindLevelCondition(
            .0 to Double.MAX_VALUE,
            -Double.MAX_VALUE to -6.0,
            DistanceType.TYPE1,
            4
        ),
        FactorType.NO2 to WindLevelCondition(
            .0 to Double.MAX_VALUE,
            -Double.MAX_VALUE to -2.0,
            DistanceType.TYPE1,
            4
        ),
        FactorType.CO to WindLevelCondition(
            .0 to Double.MAX_VALUE,
            -Double.MAX_VALUE to -10.0,
            DistanceType.TYPE1,
            5
        ),
    )
    /****数据有上升趋势提醒*****************************************************************************/
@@ -182,16 +254,22 @@
        // PM10在一个监测周期(4秒)上升量级在2 - 4μg/m³之间,连续发生3次
        FactorType.PM10 to WindLevelCondition(
            .0 to Double.MAX_VALUE,
            2.0 to 4.0,
            4.0 to 8.0,
            DistanceType.TYPE1,
            2
            4
        ),
        // VOC在一个监测周期(4秒)上升量级在3 - 6μg/m³之间,连续发生2次
        FactorType.VOC to WindLevelCondition(
            .0 to Double.MAX_VALUE,
            3.0 to 6.0,
            2.0 to 4.0,
            DistanceType.TYPE1,
            2
            4
        ),
        FactorType.CO to WindLevelCondition(
            .0 to Double.MAX_VALUE,
            5.0 to 10.0,
            DistanceType.TYPE1,
            5
        ),
    )
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedClue.kt
@@ -20,18 +20,6 @@
 */
class PollutedClue() : BaseExceptionResult() {
//    constructor(
//        tag: ExceptionTag, factor: FactorFilter.SelectedFactor, eType: ExceptionType, config: RTExcWindLevelConfig,
//        windLevelCondition: RTExcWindLevelConfig.WindLevelCondition?,
//    ) : this() {
//        if (tag.exceptionData.isEmpty()) return
//        deviceCode = tag.startData?.deviceCode
//        pollutedData = PollutedData(
//            tag.startData!!, tag.endData, factor, tag.exceptionData, tag.historyData, eType, windLevelCondition
//        )
//        pollutedArea = PollutedArea(tag.historyData, tag.exceptionData, config, windLevelCondition)
//    }
    constructor(
        exceptions: List<Pair<FactorFilter.SelectedFactor, ExceptionTag>>,
        eType: ExceptionType,
@@ -97,11 +85,6 @@
        pollutedArea = PollutedArea(historyData, exceptionData, config, windLevelCondition)
    }
    /**
     * 6. å±•示数据变化情况,上升速率等等
     */
    /**
     * @see [MsgType]
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedData.kt
@@ -34,15 +34,6 @@
        var max: Double? = null
    }
    /**
     * 9. å…³è”因子
     *     a) pm2.5、pm10特别高,两者在各情况下同步展示,pm2.5占pm10的比重变化,比重越高,越有可能是餐饮
     *     b) pm10特别高、pm2.5较高,大颗粒扬尘污染,只展示pm10,pm2.5占pm10的比重变化,工地为主
     *     c) VOC较高,同比计算pm2.5的量级,可能存在同步偏高(汽修、加油站), åŒæ­¥è®¡ç®—O3是否有高值
     *     d) VOC较高,处于加油站(车辆拥堵情况),CO一般较高, åŒæ­¥è®¡ç®—O3是否有高值
     *     e) æ°®æ°§åŒ–合物,一般由于机动车尾气,同步计算CO
     */
    constructor(
        start: BaseRealTimeData,
        end: BaseRealTimeData?,
@@ -59,8 +50,8 @@
        endTime = end?.dataTime
//        startData = start.getByFactorType(factor.main)
//        endData = end?.getByFactorType(factor.main) ?: startData
        startData = start
        endData = end
//        startData = start
//        endData = end
        windSpeed = exceptionData.first().windSpeed?.toDouble()
        times = windLevelCondition?.countLimit
@@ -100,8 +91,8 @@
    var startTime: Date? = null
    var endTime: Date? = null
    var startData: BaseRealTimeData? = null
    var endData: BaseRealTimeData? = null
//    var startData: BaseRealTimeData? = null
//    var endData: BaseRealTimeData? = null
    // é£Žé€Ÿ
    var windSpeed: Double? = null
@@ -110,7 +101,7 @@
    var times: Int? = null
    var historyDataList = mutableListOf<DataVo>()
    // å¼‚常监测数据,包含单此异常中所有发生了异常的数据值(可能不是时间连续的数据)
    // å¼‚常监测数据,包含单次异常中所有发生了异常的数据值(可能不是时间连续的数据)
    var dataList: MutableList<BaseRealTimeData> = mutableListOf()
    var dataVoList: MutableList<DataVo> = mutableListOf()
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedSource.kt
@@ -46,12 +46,12 @@
        // Fixme 2025.5.14: æ±¡æŸ“源的坐标是高德地图坐标系(火星坐标系),而走航数据是WGS84坐标系
        // æŒ‰ç…§åŒºåŸŸæ£€ç´¢å†…部污染源信息
        var result = mutableListOf<SceneInfo>()
        // 1. é¦–先按照四至范围从数据库初步筛选污染源,此处的区域坐标已转换为火星坐标系
        val polygonTmp = pollutedArea.polygon
        this.sceneList = emptyList()
        if (polygonTmp != null) {
            val fb = MapUtil.calFourBoundaries(polygonTmp)
            // 1. é¦–先按照四至范围从数据库初步筛选污染源,此处的区域坐标已转换为火星坐标系
            val sceneList = sceneInfoRep.findByCoordinateRange(fb)
            // 2. å†ç²¾ç¡®åˆ¤æ–­æ˜¯å¦åœ¨åå‘溯源区域多边形内部
            sceneList.forEach {
@@ -61,6 +61,7 @@
                }
            }
            // 3. å†ç»Ÿä¸€æ£€ç´¢è¿‘距离污染圆形区域内部的污染源
            val closePolygonTmp = pollutedArea.closePolygon!!
            val closeFb = MapUtil.calFourBoundaries(closePolygonTmp)
            val closeSceneList = sceneInfoRep.findByCoordinateRange(closeFb)
@@ -70,13 +71,12 @@
                    result.add(it)
                }
            }
            // åŽ»é‡
            // 4. åŽ»é‡
            result = result.distinctBy { it.guid }.toMutableList()
            // æ ¹æ®æ±¡æŸ“因子的量级,计算主要的污染场景类型,筛选结果
            // 5. æ ¹æ®æ±¡æŸ“因子的量级,计算主要的污染场景类型,筛选结果
            val mainSceneType = calSceneType(pollutedData)
            if (mainSceneType != null) {
//            this.conclusion = mainSceneType.first
                result = result.filter {
                    val r = mainSceneType.second.find { s ->
                        s.value == it.typeId.toInt()
@@ -107,7 +107,7 @@
//                    val coAvg = round(pollutedData.dataList.map { it.co!! }.average()) / 1000
                    val coAvg = round(pollutedData.statisticMap[FactorType.CO]?.avg ?: .0) / 1000
                    "氮氧化合物偏高,CO的量级为${coAvg}mg/m³,一般由于机动车尾气造成,污染源以汽修、加油站为主" to
                            listOf(SceneType.TYPE6, SceneType.TYPE10, SceneType.TYPE17)
                            listOf(SceneType.TYPE1, SceneType.TYPE6, SceneType.TYPE10, SceneType.TYPE17)
                }
                FactorType.CO -> "" to listOf(SceneType.TYPE6, SceneType.TYPE10, SceneType.TYPE17)
@@ -229,9 +229,6 @@
     * @return æº¯æºæè¿°
     */
    private fun summaryTxt(pollutedData: PollutedData, sceneList: List<SceneInfoVo>): String {
//        pollutedData.exception
//        pollutedData.selectedFactor?.main
        val st = DateUtil.instance.getTime(pollutedData.startTime)
        val et = DateUtil.instance.getTime(pollutedData.endTime)
@@ -277,7 +274,7 @@
                        val curValue = pollutedData.dataList.last().getByFactorType(s.key)
                        if (preValue == null || curValue == null) return@forEach
                        val r = round((curValue - preValue) / preValue * 100)
                        txt += ",从${preValue}μg/m³快速上升至${curValue}μg/m³,变化率为${r}%"
                        txt += ",${s.key.getTxt()}从${preValue}μg/m³快速上升至${curValue}μg/m³,变化率为${r}%"
                    }
                }
            }
src/main/kotlin/com/flightfeather/uav/domain/entity/BaseRealTimeData.kt
@@ -122,7 +122,7 @@
    }
    fun getByFactorType(type: FactorType?): Float? {
        return when (type) {
        val res =  when (type) {
            FactorType.NO2 -> no2
            FactorType.CO -> co
            FactorType.H2S -> h2s
@@ -144,6 +144,7 @@
            FactorType.NO -> no
            else -> null
        }
        return if (res != null) round(res * 100) / 100 else null
    }
}
src/main/kotlin/com/flightfeather/uav/lightshare/service/impl/MissionServiceImpl.kt
@@ -94,7 +94,7 @@
        val data = realTimeDataRep.fetchData(mission)
        mission.kilometres = MissionUtil.calKilometres(data).toFloat()
        // todo: è®¡ç®—走航任务所在中心区域
        mission.region = MissionUtil.calRegion(data)
//        mission.region = MissionUtil.calRegion(data)
        return updateMission(mission)
    }
}
src/main/kotlin/com/flightfeather/uav/model/epw/MutationDataPreprocess.kt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,143 @@
package com.flightfeather.uav.model.epw
import com.flightfeather.uav.common.utils.DateUtil
import com.flightfeather.uav.lightshare.bean.DataVo
import com.flightfeather.uav.socket.bean.AirData
import com.flightfeather.uav.socket.eunm.FactorType
/**
 * çªå˜æ•°æ®é¢„处理
 * 1. é’ˆå¯¹å•个数据突高或突低,前后两个数据的量级接近的情况,进行处理
 * 2. é’ˆå¯¹æœ‰å…³è”关系的监测因子,数据量级出现明显错误的情况,进行处理
 * @date 2025/11/20
 * @author feiyu02
 */
class MutationDataPreprocess {
    // ä¿å­˜æ•°æ®æœ€å¤§è®°å½•æ•°
    private val MAX_COUNT = 15
    private val lastData = mutableListOf<DataVo>()
    // é‡çº§å˜åŒ–阈值倍数(用于判断是否为突变)
    private val MUTATION_THRESHOLD = 5.0
    // å‰åŽæ•°æ®æŽ¥è¿‘阈值倍数(用于判断前后数据是否量级接近)
    private val SIMILARITY_THRESHOLD = 1.5
    /**
     * æ•°æ®å¹³æ»‘
     * è§£å†³å•个数据相比前后两个数据的量级过大或过小的问题
     * @param data åŽŸå§‹æ•°æ®
     * @return é¢„处理后的数据
     */
    fun preprocess(data: List<DataVo>): List<DataVo> {
        // å½“新数据与旧数据采样时间差超过1分钟时,认为两组数据已无关联性,清空旧数据
        if (lastData.isNotEmpty() && data.isNotEmpty()) {
            val lastTime = DateUtil.instance.StringToDate(lastData.last().time)
            val thisTime = DateUtil.instance.StringToDate(data.first().time)
            if ((thisTime?.time?.minus(lastTime?.time ?: 0) ?: 0) >= (60 * 1000)) {
                lastData.clear()
            }
        }
        lastData.addAll(data)
        detectAndReplaceMutation(lastData)
        saveHistory(lastData)
        return lastData
    }
    /**
     * æ£€æµ‹å¹¶æ›¿æ¢å¼‚常突变数据
     * @param data åŽŸå§‹æ•°æ®åˆ—è¡¨
     * @return å¤„理后的数据列表
     */
    fun detectAndReplaceMutation(data: MutableList<DataVo>){
        if (data.size < 3) return // æ•°æ®é‡ä¸è¶³ï¼Œæ— æ³•进行前后对比
        // éåŽ†æ•°æ®ï¼Œä»Žç¬¬äºŒä¸ªå¼€å§‹åˆ°å€’æ•°ç¬¬äºŒä¸ªç»“æŸ
        for (i in 1 until data.size - 1) {
            val currentData = data[i]
            val prevData = data[i - 1]
            val nextData = data[i + 1]
            // æ£€æŸ¥æ¯ä¸ªç›‘测因子
            if (currentData.values != null && prevData.values != null && nextData.values != null) {
                processEachFactor(currentData, prevData, nextData)
            }
        }
    }
    /**
     * å¤„理每个监测因子的数据突变
     */
    private fun processEachFactor(currentData: DataVo, prevData: DataVo, nextData: DataVo) {
        val currentFactors = mutableMapOf<String, AirData>()
        val prevFactors = mutableMapOf<String, AirData>()
        val nextFactors = mutableMapOf<String, AirData>()
        // æž„建因子名称到AirData的映射
        currentData.values?.forEach { currentFactors[it.factorName ?: ""] = it }
        prevData.values?.forEach { prevFactors[it.factorName ?: ""] = it }
        nextData.values?.forEach { nextFactors[it.factorName ?: ""] = it }
        // éåŽ†å½“å‰æ•°æ®ä¸­çš„æ‰€æœ‰å› å­
        currentFactors.forEach { (factorName, currentFactor) ->
            val prevFactor = prevFactors[factorName]
            val nextFactor = nextFactors[factorName]
            // ç¡®ä¿ä¸‰ä¸ªæ•°æ®ç‚¹éƒ½æœ‰è¯¥å› å­çš„æ•°æ®
            if (prevFactor != null && nextFactor != null &&
                currentFactor.factorData != null && prevFactor.factorData != null && nextFactor.factorData != null) {
                val currentValue = currentFactor.factorData!!
                val prevValue = prevFactor.factorData!!
                val nextValue = nextFactor.factorData!!
                // è·³è¿‡0值或负值,避免除零错误
                if (prevValue <= 0 || nextValue <= 0) return@forEach
                // æ£€æŸ¥æ˜¯å¦ä¸ºå¼‚常突变数据
                if (isMutationData(currentValue, prevValue, nextValue)) {
                    // æ›¿æ¢ä¸ºå‰ä¸€ä¸ªæ•°æ®çš„值
                    currentFactor.factorData = prevValue
                }
            }
        }
    }
    /**
     * åˆ¤æ–­å½“前数据是否为突变数据
     * æ¡ä»¶ï¼š1. å½“前数据与前一个数据的量级变化超过阈值
     *       2. å‰ä¸€ä¸ªæ•°æ®ä¸ŽåŽä¸€ä¸ªæ•°æ®çš„量级接近
     */
    private fun isMutationData(currentValue: Double, prevValue: Double, nextValue: Double): Boolean {
        // è®¡ç®—变化率
        val currentToPrevRatio = Math.max(currentValue, prevValue) / Math.min(currentValue, prevValue)
        val prevToNextRatio = Math.max(prevValue, nextValue) / Math.min(prevValue, nextValue)
        // åˆ¤æ–­æ˜¯å¦æ»¡è¶³çªå˜æ¡ä»¶
        return currentToPrevRatio > MUTATION_THRESHOLD && prevToNextRatio <= SIMILARITY_THRESHOLD
    }
    fun saveHistory(data: MutableList<DataVo>) {
//        // å°†æ–°æ•°æ®çš„至多最后15个保存下来(已经过预处理),用于下一次的判断
//        val newList = mutableListOf<DataVo>()
//        val s = if ((data.lastIndex - MAX_COUNT + 1) < 0) 0 else data.lastIndex - MAX_COUNT + 1
//        data.subList(s, data.lastIndex + 1).forEach {
//            newList.add(it.copy())
//        }
//        // å½“新数据与旧数据采样时间差超过1分钟时,认为两组数据已无关联性,清空旧数据
//        if (lastData.isNotEmpty() && newList.isNotEmpty()) {
//            val lastTime = DateUtil.instance.StringToDate(lastData.last().time)
//            val thisTime = DateUtil.instance.StringToDate(newList.first().time)
//            if ((thisTime?.time?.minus(lastTime?.time ?: 0) ?: 0) >= (60 * 1000)) {
//                lastData.clear()
//            }
//        }
//        lastData.addAll(newList)
        // ç¡®ä¿ä¿å­˜çš„æ•°æ®æœ€å¤šåªæœ‰æœ€æ–°çš„15个
        while (data.size > MAX_COUNT) {
            data.removeAt(0)
        }
    }
}
src/main/kotlin/com/flightfeather/uav/socket/eunm/FactorType.kt
@@ -157,7 +157,7 @@
        }
        /**
         * ä¸å¤„理低于此值的值
         * ä¸å¤„理低于此值的数据
         */
        fun getVMin(type: FactorType): Double = when (type) {
            NO -> 1.0
src/main/resources/application-test.yml
@@ -7,13 +7,13 @@
    #    password: cn.FLIGHTFEATHER
    #   è¿œç¨‹æœåС噍
#    url: jdbc:mysql://47.100.191.150:3306/dronemonitor?serverTimezone=Asia/Shanghai&prepStmtCacheSize=517&cachePrepStmts=true&autoReconnect=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
#    username: remoteU1
#    password: eSoF8DnzfGTlhAjE
    url: jdbc:mysql://114.215.109.124:3306/dronemonitor?serverTimezone=Asia/Shanghai&prepStmtCacheSize=517&cachePrepStmts=true&autoReconnect=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
    url: jdbc:mysql://47.100.191.150:3306/dronemonitor?serverTimezone=Asia/Shanghai&prepStmtCacheSize=517&cachePrepStmts=true&autoReconnect=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
    username: remoteU1
    password: feiyu2024
    password: eSoF8DnzfGTlhAjE
#    url: jdbc:mysql://114.215.109.124:3306/dronemonitor?serverTimezone=Asia/Shanghai&prepStmtCacheSize=517&cachePrepStmts=true&autoReconnect=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
#    username: remoteU1
#    password: feiyu2024
springfox:
  documentation:
src/test/kotlin/com/flightfeather/uav/biz/mission/MissionUtilTest.kt
@@ -22,9 +22,12 @@
    @Test
    fun calKilometres() {
        val m = missionRep.findOne("20250819") ?: return
        val data = realTimeDataRep.fetchData(m)
        MissionUtil.calKilometres(data)
        val mcodeList = listOf("20250427", "20250526", "20250530")
        mcodeList.forEach { mcode ->
            val m = missionRep.findOne(mcode) ?: return@forEach
            val data = realTimeDataRep.fetchData(m)
            MissionUtil.calKilometres(data)
        }
//        val d = MapUtil.getDistance(121.425187, 31.225907, 121.425196, 31.225892)
//        println(d)
//        val d1 = MapUtil.getDistance(121.425196, 31.225892, 121.425187, 31.225907)
src/test/kotlin/com/flightfeather/uav/biz/sourcetrace/SourceTraceControllerTest.kt
@@ -45,10 +45,10 @@
//            "SH-CN-20240723(02)",
////            "SH-CN-20250723(01)"
//        )
//        val startTime = LocalDateTime.of(2024, 12, 31, 0, 0, 0).atZone(ZoneId.systemDefault()).toInstant()
        val startTime = LocalDateTime.of(2025, 7, 1, 0, 0, 0).atZone(ZoneId.systemDefault()).toInstant()
        val endTime = LocalDateTime.of(2025, 9, 30, 23, 59, 59).atZone(ZoneId.systemDefault()).toInstant()
//        val endTime = LocalDateTime.of(2025, 7, 31, 23, 59, 59).atZone(ZoneId.systemDefault()).toInstant()
//        val startTime = LocalDateTime.of(2025, 7, 1, 0, 0, 0).atZone(ZoneId.systemDefault()).toInstant()
//        val endTime = LocalDateTime.of(2025, 9, 30, 23, 59, 59).atZone(ZoneId.systemDefault()).toInstant()
        val startTime = LocalDateTime.of(2025, 11, 2, 0, 0, 0).atZone(ZoneId.systemDefault()).toInstant()
        val endTime = LocalDateTime.of(2025, 11, 2, 23, 59, 59).atZone(ZoneId.systemDefault()).toInstant()
        val missions = missionMapper.selectByExample(Example(Mission::class.java).apply {
            createCriteria().andBetween("startTime", startTime, endTime)
        })
@@ -116,8 +116,10 @@
//            "SH-CN-20240723(02)",
////            "SH-CN-20250723(01)"
//        )
        val startTime = LocalDateTime.of(2025, 7, 1, 0, 0, 0).atZone(ZoneId.systemDefault()).toInstant()
        val endTime = LocalDateTime.of(2025, 9, 30, 23, 59, 59).atZone(ZoneId.systemDefault()).toInstant()
//        val startTime = LocalDateTime.of(2025, 7, 1, 0, 0, 0).atZone(ZoneId.systemDefault()).toInstant()
//        val endTime = LocalDateTime.of(2025, 9, 30, 23, 59, 59).atZone(ZoneId.systemDefault()).toInstant()
        val startTime = LocalDateTime.of(2025, 11, 2, 0, 0, 0).atZone(ZoneId.systemDefault()).toInstant()
        val endTime = LocalDateTime.of(2025, 11, 2, 23, 59, 59).atZone(ZoneId.systemDefault()).toInstant()
        val missions = missionMapper.selectByExample(Example(Mission::class.java).apply {
            createCriteria().andBetween("startTime", startTime, endTime)
        })
src/test/kotlin/com/flightfeather/uav/lightshare/service/impl/MissionServiceImplTest.kt
@@ -34,7 +34,7 @@
    @Test
    fun calMissionInfo() {
        missionMapper.selectByExample(Example(Mission::class.java).apply {
            createCriteria().andGreaterThanOrEqualTo("startTime", "2025-07-08 08:30:00")
            createCriteria().andBetween("startTime", "2025-12-05 00:00:00", "2025-12-31 23:59:59")
        }).forEach {mission ->
            mission?.let { missionService.calMissionInfo(it.missionCode) }
            Thread.sleep(1000)