feiyu02
2025-08-14 b10c22af595bd995e56946bff63b8f2f984b13e8
2025.8.14
1. 动态溯源模块添加滑动平均异常计算(调试中)
已修改15个文件
已添加2个文件
573 ■■■■ 文件已修改
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/BaseExceptionContinuous.kt 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/BaseExceptionResult.kt 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/model/ExceptionTag.kt 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/SourceTraceController.kt 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/config/RTExcWindLevelConfig.kt 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/exceptiontype/RTExcSlideAverage.kt 194 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedSource.kt 85 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedSummary.kt 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/domain/entity/SourceTraceMsg.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/domain/mapper/SourceTraceMsgMapper.kt 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/domain/repository/SourceTraceRep.kt 48 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/lightshare/bean/SourceTraceMsgVo.kt 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/generator/generatorConfig.xml 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/SourceTraceMsgMapper.xml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/kotlin/com/flightfeather/uav/biz/dataanalysis/BaseExceptionContinuousTest.kt 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/kotlin/com/flightfeather/uav/biz/sourcetrace/SourceTraceControllerTest.kt 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/kotlin/com/flightfeather/uav/lightshare/service/impl/DataAnalysisServiceImplTest.kt 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/BaseExceptionContinuous.kt
@@ -154,8 +154,8 @@
        lastData = data
        removeSingleFactor(data)
        checkDelayedExceptions(data)
        mergeExceptionResult(data)
        val fittedComb = checkDelayedExceptions(data)
        mergeExceptionResult(data, fittedComb)
        onNewResult(result)
        clearExceptions(data)
    }
@@ -286,6 +286,8 @@
     * å°†ä¸åœ¨å…³è”关系中的监测因子异常存储,并剔除
     */
    fun removeSingleFactor(data: BaseRealTimeData) {
        if (latestExceptions.isEmpty()) return
        // æŸ¥æ‰¾ä¸åœ¨å› å­å…³è”组合中的异常因子
        val sfList = latestExceptions.filter {
            config.factorFilter.combination.find { c -> c.find { f -> f == it.first.main } != null } == null
@@ -302,11 +304,13 @@
    /**
     * æ£€æŸ¥å»¶è¿Ÿçš„待合并异常与当前异常是否能匹配
     * 1. å°†é—留的超过等待数据周期的异常存储
     * 2. å°†åŒ¹é…æˆåŠŸçš„åˆå¹¶å¼‚å¸¸å­˜å‚¨
     * 2. å°†åŒ¹é…æˆåŠŸçš„åˆå¹¶å¼‚å¸¸å­˜å‚¨ï¼ŒåŒæ—¶å°†å…³è”å…³ç³»æ ‡è®°ä¸ºå·²åŒ¹é…
     * 3. ä¿ç•™ä¾æ—§æœªåˆå¹¶æˆåŠŸå¹¶ä¸”å¯ç»§ç»­ç­‰å¾…çš„å¼‚å¸¸
     * @return è¢«åŒ¹é…æˆåŠŸçš„å…³è”å…³ç³»
     */
    fun checkDelayedExceptions(data: BaseRealTimeData): List<List<FactorType>> {
        if (latestExceptions.isEmpty()) return emptyList()
        // è¢«åŒ¹é…æˆåŠŸçš„ç›‘æµ‹å› å­å…³è”å…³ç³»
        val fittedComb = mutableListOf<List<FactorType>>()
        // é—留的进入下一个数据周期做判断的待合并异常集合
@@ -316,14 +320,13 @@
        // æœ¬æ¬¡æ•°æ®å‘¨æœŸä¸­ï¼Œè¢«åŒ¹é…æˆåŠŸçš„å¼‚å¸¸é›†åˆ
        val exceps = mutableListOf<Pair<FactorFilter.SelectedFactor, T>>()
        remainingExceptions.forEach {
            // æ£€æŸ¥å½“前新异常中,是否包含因子关联关系中的异常
            // æ£€æŸ¥æœ¬æ¬¡æ•°æ®å‘¨æœŸçš„异常中,是否包含因子关联关系中的异常
            val combRes = matchCombFactor(it.combination, latestExceptions)
            val res = combRes.second
            // åˆ¤æ–­æœ¬æ¬¡æ•°æ®å‘¨æœŸä¸­æ‰¾åˆ°çš„因子和已有的因子是否满足关联关系
            val findFactors = mutableListOf<FactorType>()
            res.forEach {r -> findFactors.add(r.first.main) }
            it.exceptions.forEach {r -> findFactors.add(r.first.main) }
            // åˆ¤æ–­æ˜¯å¦è¿˜æœ‰ç¼ºå¤±å¼‚常
            val isFitAll = findFactors.distinct() == it.combination
            // å¦‚果已经没有缺失的异常因子,则可合并为组合异常
            if (isFitAll) {
@@ -377,11 +380,20 @@
    /**
     * åˆå¹¶å¼‚常
     * @param data å½“前监测数据
     * @param fittedComb åœ¨é—留的异常[remainingExceptions]判断中,已经进行匹配判断的关联关系,将不再进行匹配
     */
    open fun mergeExceptionResult(data: BaseRealTimeData) {
    open fun mergeExceptionResult(data: BaseRealTimeData, fittedComb: List<List<FactorType>>) {
        if (latestExceptions.isEmpty()) return
        val combinedExc = mutableListOf<List<Pair<FactorFilter.SelectedFactor, T>>>()
        // éåŽ†æ‰€æœ‰çš„å› å­ç»„åˆ
        config.factorFilter.combination.forEach { c ->
            /**
             * è·³è¿‡å·²ç»åœ¨[checkDelayedExceptions]中判断过的关联关系
             */
            if (fittedComb.indexOf(c) >= 0) return@forEach
            val combRes = matchCombFactor(c, latestExceptions)
            val res = combRes.second
            val exist = combRes.first
@@ -392,7 +404,7 @@
            }
            // å¦åˆ™å°†å¼‚常的深拷贝版本存入待合并异常集合
            // TODO 2025.8.4: åŽç»­æ·»åŠ å½“å…³è”çš„ç›‘æµ‹å› å­ç´¯è®¡å¼‚å¸¸è®¡æ•°æŽ¥è¿‘é˜ˆå€¼æ—¶ï¼Œæ‰å­˜å…¥é›†åˆçš„é€»è¾‘
            else {
            else if (res.isNotEmpty()) {
                remainingExceptions.add(RemainException(res, c))
            }
        }
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/BaseExceptionResult.kt
@@ -1,6 +1,7 @@
package com.flightfeather.uav.biz.dataanalysis
import com.flightfeather.uav.lightshare.eunm.ExceptionStatusType
import java.io.Serializable
import java.util.*
/**
@@ -8,7 +9,7 @@
 * @date 2025/5/13
 * @author feiyu02
 */
abstract class BaseExceptionResult {
abstract class BaseExceptionResult : Serializable {
    // å¼‚常编号
    var guid: String? = null
src/main/kotlin/com/flightfeather/uav/biz/dataanalysis/model/ExceptionTag.kt
@@ -48,11 +48,11 @@
            // ä¿è¯åŽ†å²æ•°æ®åŒ…å«æ‰€æœ‰å¼‚å¸¸æ•°æ®ï¼ˆå¼‚å¸¸æ•°æ®å¯èƒ½ä¸è¿žç»­ï¼‰ï¼Œå¹¶ä¸”åœ¨é¦–ä¸ªå¼‚å¸¸æ•°æ®ä¹‹å‰æœ€å¤šå†ä¿å­˜10个数据
            val i = historyData.indexOf(exceptionData.first())
            if (i > MAX_HISTORY) {
                historyData = historyData.subList(i - MAX_HISTORY, historyData.size)
                historyData = historyData.subList(i - MAX_HISTORY, historyData.size).toMutableList()
            }
        } else {
            if (historyData.size > MAX_HISTORY) {
                historyData = historyData.subList(historyData.size - MAX_HISTORY, historyData.size)
                historyData = historyData.subList(historyData.size - MAX_HISTORY, historyData.size).toMutableList()
            }
        }
    }
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/SourceTraceController.kt
@@ -84,6 +84,7 @@
    private fun newTask() {
        taskList.apply {
//            add(RTExcSlideAverage(config) { dataChangeCallback(it) }.also { it.init() })
            add(RTExcWindLevel1(config) { exceptionCallback(it) }.also { it.init() })
            add(RTExcWindLevel1_1(config) { exceptionCallback(it) }.also { it.init() })
            add(RTExcWindLevel4(config) { exceptionCallback(it) }.also { it.init() })
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/config/RTExcWindLevelConfig.kt
@@ -57,7 +57,7 @@
        1.6 to 7.9,
        0.2 to Double.MAX_VALUE,
        DistanceType.TYPE3,
        3
        2
//        1
    )
@@ -66,7 +66,7 @@
        8.0 to 13.8,
        0.1 to Double.MAX_VALUE,
        DistanceType.TYPE4,
        3
        2
    )
    /****数据快速上升*****************************************************************************/
@@ -103,14 +103,14 @@
            1.6 to 7.9,
            4.0 to Double.MAX_VALUE,
            DistanceType.TYPE3,
            3
            2
        ),
        // PM10在一个监测周期(4秒)上升量级大于等于4μg/m³,连续发生3次
        FactorType.PM10 to WindLevelCondition(
            1.6 to 7.9,
            4.0 to Double.MAX_VALUE,
            DistanceType.TYPE3,
            3
            2
        ),
        // VOC在一个监测周期(4秒)上升量级大于等于6μg/m³,连续发生1次
        FactorType.VOC to WindLevelCondition(
@@ -127,14 +127,14 @@
            8.0 to 13.8,
            4.0 to Double.MAX_VALUE,
            DistanceType.TYPE4,
            3
            2
        ),
        // PM10在一个监测周期(4秒)上升量级大于等于4μg/m³,连续发生3次
        FactorType.PM10 to WindLevelCondition(
            8.0 to 13.8,
            4.0 to Double.MAX_VALUE,
            DistanceType.TYPE4,
            3
            2
        ),
        // VOC在一个监测周期(4秒)上升量级大于等于6μg/m³,连续发生1次
        FactorType.VOC to WindLevelCondition(
@@ -177,14 +177,14 @@
            .0 to Double.MAX_VALUE,
            2.0 to 4.0,
            DistanceType.TYPE1,
            3
            2
        ),
        // PM10在一个监测周期(4秒)上升量级在2 - 4μg/m³之间,连续发生3次
        FactorType.PM10 to WindLevelCondition(
            .0 to Double.MAX_VALUE,
            2.0 to 4.0,
            DistanceType.TYPE1,
            3
            2
        ),
        // VOC在一个监测周期(4秒)上升量级在3 - 6μg/m³之间,连续发生2次
        FactorType.VOC to WindLevelCondition(
@@ -194,4 +194,13 @@
            2
        ),
    )
    /****滑动平均值异常*****************************************************************************/
    // æ±‚滑动平均值的数据组个数
    var changeTrendGroup = 12
    // æ»‘动平均值连续
    var changeTrendInterval = 12
    var changeTrendRate = .2
    // æ»‘动平均值变化率异常连续次数
    var changeTrendTimes = 3
}
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/exceptiontype/RTExcSlideAverage.kt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,194 @@
package com.flightfeather.uav.biz.sourcetrace.exceptiontype
import com.flightfeather.uav.biz.FactorFilter
import com.flightfeather.uav.biz.dataanalysis.BaseExceptionAnalysis
import com.flightfeather.uav.biz.dataanalysis.exceptiontype.ExceptionSlideAverage.Tag
import com.flightfeather.uav.biz.dataanalysis.model.ExceptionResult
import com.flightfeather.uav.biz.dataanalysis.model.ExceptionSlideAverageTag
import com.flightfeather.uav.biz.dataanalysis.model.ExceptionTag
import com.flightfeather.uav.biz.dataanalysis.model.ExceptionType
import com.flightfeather.uav.biz.sourcetrace.RealTimeAnalysisConfig
import com.flightfeather.uav.biz.sourcetrace.config.RTExcWindLevelConfig
import com.flightfeather.uav.biz.sourcetrace.model.PollutedClue
import com.flightfeather.uav.biz.sourcetrace.model.RealTimeExceptionResult
import com.flightfeather.uav.domain.entity.BaseRealTimeData
import com.flightfeather.uav.lightshare.eunm.ExceptionStatusType
import com.flightfeather.uav.socket.eunm.FactorType
import kotlin.math.abs
/**
 * æ»‘动平均值突变异常
 * @date 2025/5/13
 * @author feiyu02
 */
class RTExcSlideAverage : BaseExceptionAnalysis<RTExcWindLevelConfig, PollutedClue> {
    constructor(config: RTExcWindLevelConfig) : super(config)
    constructor(config: RTExcWindLevelConfig, callback: NewPolluteClueCallback) : super(config){
        this.callback = callback
    }
    private var callback: NewPolluteClueCallback? = null
    private val historyDataList = mutableListOf<BaseRealTimeData>()
    private val tempDataList = mutableListOf<BaseRealTimeData>()
    private var lastData: BaseRealTimeData? = null
    protected val tagMap = mutableMapOf<FactorType, ExceptionSlideAverageTag>()
    override fun init() {
        super.init()
        historyDataList.clear()
        tempDataList.clear()
        lastData = null
        tagMap.clear()
        config.factorFilter.mainList().forEach { f ->
            tagMap[f] = ExceptionSlideAverageTag()
        }
    }
    override fun getExceptionType(): ExceptionType {
        return ExceptionType.TYPE7
    }
    override fun onNextData(data: BaseRealTimeData) {
        historyDataList.add(data)
        // æ•°æ®åŠ å…¥ä¸´æ—¶æ•°ç»„
        tempDataList.add(data)
        // æ•°æ®é‡è¶…出设置数量时,去除当前数据组首个数据
        if (tempDataList.size > config.changeTrendGroup) {
            tempDataList.removeAt(0)
        }
        config.factorFilter.selectedList.forEach { s ->
            val f = s.main
            tagMap[f]?.let {
                it.eIndex++
                it.endData = lastData
                if (it.startData == null) {
                    it.startData = data
                }
                // æ•°æ®é‡ç­‰äºŽè®¾ç½®æ•°é‡æ—¶ï¼Œè®¡ç®—当前数据组均值
                if (tempDataList.size == config.changeTrendGroup) {
                    calAvg(f, tempDataList)
                    if (checkSlideAvg(f)) {
                        it.addExceptionData(data)
                        checkResult(s)
                    } else {
                        recordException(s, it, data)
                    }
                }
            }
        }
        lastData = data
    }
    override fun onDone() {
        checkResult(exceptionStatus = ExceptionStatusType.Ended)
    }
    /**
     * å¼‚常结束,记录异常
     */
    fun recordException(factor: FactorFilter.SelectedFactor, tag: ExceptionSlideAverageTag, data: BaseRealTimeData) {
        checkResult(factor, ExceptionStatusType.Ended)
        tag.refreshAfterCheckResult(historyDataList, config.changeTrendGroup)
    }
    /**
     * å½“前数据未出现异常时,或数据循环结束时,判断后续步骤
     */
    private fun checkResult(
        factor: FactorFilter.SelectedFactor? = null,
        exceptionStatus: ExceptionStatusType = ExceptionStatusType.InProgress
    ) {
        val tag = tagMap[factor?.main]
        if (factor != null && tag != null) {
            if (tag.exceptionExisted) {
                onNewException(tag, factor, exceptionStatus)
            }
        } else {
            config.factorFilter.selectedList.forEach { f ->
                val tag1 = tagMap[f.main] ?: return@forEach
                if (tag1.exceptionExisted) {
                    onNewException(tag1, f, exceptionStatus)
                }
            }
        }
    }
    /**
     * æ–°å¢žæˆ–更新一条异常
     */
    open fun onNewException(tag: ExceptionSlideAverageTag, factor: FactorFilter.SelectedFactor, exceptionStatus: ExceptionStatusType) {
        if (tag.startData == null) return
        val ex = newResult(listOf(factor to tag))
        callback?.invoke(ex)
    }
    fun newResult(exceptions: List<Pair<FactorFilter.SelectedFactor, ExceptionTag>>): PollutedClue {
        return PollutedClue(exceptions, getExceptionType(), config, null)
    }
    /**
     * è®¡ç®—一组数据的均值
     */
    private fun calAvg(type: FactorType, list: List<BaseRealTimeData>) {
        var total = .0
        var valid = true
        val count = list.size
        if (count == 0) return
        list.forEach {
            val v = it.getByFactorType(type)
            if (v == null) {
                valid = false
            } else {
                total += v
            }
        }
        val avg = total / count
        tagMap[type]?.avgListReverse?.add(0, Pair(avg, valid))
    }
    /**
     * è®¡ç®—数据组之间的均值差异是否连续超过限定比率
     */
    private fun checkSlideAvg(type: FactorType): Boolean {
        val tag = tagMap[type] ?: return false
        // è®¡ç®—滑动均值最低要求个数
        val minSize = config.changeTrendTimes + config.changeTrendInterval
        if (tag.avgListReverse.size < minSize) {
            return false
        } else {
            // æ»‘动均值满足数量时,计算均值之间是否连续超过限定比率
            val rateList = mutableListOf<Pair<Double, Boolean>>()
            for (i in tag.avgListReverse.indices) {
                if (i >= config.changeTrendTimes) break
                val r = calAvgChangeRate(tag.avgListReverse[i], tag.avgListReverse[i + config.changeTrendInterval])
                rateList.add(r)
            }
            for (y in rateList) {
                if (!y.second || y.first < config.changeTrendRate) {
                    return false
                }
            }
            return true
        }
    }
    /**
     * è®¡ç®—滑动均值变化率
     * æ±‚a1相对于a2的变化率
     */
    private fun calAvgChangeRate(a1: Pair<Double, Boolean>, a2: Pair<Double, Boolean>): Pair<Double, Boolean> {
        val valid = a1.second && a2.second
        return if (a2.first == .0) {
            Pair(1.0, valid)
        } else {
            Pair(abs(a1.first - a2.first) / a2.first, valid)
        }
    }
}
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedSource.kt
@@ -45,41 +45,44 @@
        // æŒ‰ç…§åŒºåŸŸæ£€ç´¢å†…部污染源信息
        var result = mutableListOf<SceneInfo>()
        // 1. é¦–先按照四至范围从数据库初步筛选污染源,此处的区域坐标已转换为火星坐标系
        val polygonTmp = pollutedArea.polygon!!
        val fb = MapUtil.calFourBoundaries(polygonTmp)
        val sceneList = sceneInfoRep.findByCoordinateRange(fb)
        // 2. å†ç²¾ç¡®åˆ¤æ–­æ˜¯å¦åœ¨åå‘溯源区域多边形内部
        sceneList.forEach {
            val point = it!!.longitude.toDouble() to it.latitude.toDouble()
            if (MapUtil.isPointInPolygon(point, polygonTmp)) {
                result.add(it)
            }
        }
        val polygonTmp = pollutedArea.polygon
        this.sceneList = emptyList()
        val closePolygonTmp = pollutedArea.closePolygon!!
        val closeFb = MapUtil.calFourBoundaries(closePolygonTmp)
        val closeSceneList = sceneInfoRep.findByCoordinateRange(closeFb)
        // 2. å†ç²¾ç¡®åˆ¤æ–­æ˜¯å¦åœ¨åå‘溯源区域多边形内部
        closeSceneList.forEach {
            val point = it!!.longitude.toDouble() to it.latitude.toDouble()
            if (MapUtil.isPointInPolygon(point, closePolygonTmp)) {
                result.add(it)
            }
        }
        // æ ¹æ®æ±¡æŸ“因子的量级,计算主要的污染场景类型,筛选结果
        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()
        if (polygonTmp != null) {
            val fb = MapUtil.calFourBoundaries(polygonTmp)
            val sceneList = sceneInfoRep.findByCoordinateRange(fb)
            // 2. å†ç²¾ç¡®åˆ¤æ–­æ˜¯å¦åœ¨åå‘溯源区域多边形内部
            sceneList.forEach {
                val point = it!!.longitude.toDouble() to it.latitude.toDouble()
                if (MapUtil.isPointInPolygon(point, polygonTmp)) {
                    result.add(it)
                }
                r != null
            }.toMutableList()
        }
            }
        this.sceneList = findClosestStation(sceneInfoRep, result)
            val closePolygonTmp = pollutedArea.closePolygon!!
            val closeFb = MapUtil.calFourBoundaries(closePolygonTmp)
            val closeSceneList = sceneInfoRep.findByCoordinateRange(closeFb)
            // 2. å†ç²¾ç¡®åˆ¤æ–­æ˜¯å¦åœ¨åå‘溯源区域多边形内部
            closeSceneList.forEach {
                val point = it!!.longitude.toDouble() to it.latitude.toDouble()
                if (MapUtil.isPointInPolygon(point, closePolygonTmp)) {
                    result.add(it)
                }
            }
            // æ ¹æ®æ±¡æŸ“因子的量级,计算主要的污染场景类型,筛选结果
            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()
                    }
                    r != null
                }.toMutableList()
            }
            this.sceneList = findClosestStation(sceneInfoRep, result)
        }
        val txt = summaryTxt(pollutedData, this.sceneList!!)
        this.conclusion = txt
@@ -97,17 +100,17 @@
                // æ°®æ°§åŒ–合物,一般由于机动车尾气,同步计算CO
                FactorType.NO2 -> {
                    val coAvg = round(pollutedData.dataList.map { it.co!! }.average()) / 1000
                     "氮氧化合物偏高,CO的量级为${coAvg}mg/m³,一般由于机动车尾气造成,污染源以汽修、加油站为主" to
                    "氮氧化合物偏高,CO的量级为${coAvg}mg/m³,一般由于机动车尾气造成,污染源以汽修、加油站为主" to
                            listOf(SceneType.TYPE6, SceneType.TYPE10, SceneType.TYPE17)
                }
                FactorType.CO ->  null
                FactorType.CO -> null
                FactorType.H2S ->  null
                FactorType.H2S -> null
                FactorType.SO2 ->  null
                FactorType.SO2 -> null
                FactorType.O3 ->  null
                FactorType.O3 -> null
                // a) pm2.5、pm10特别高,两者在各情况下同步展示,pm2.5占pm10的比重变化,比重越高,越有可能是餐饮
                // b) pm10特别高、pm2.5较高,大颗粒扬尘污染,只展示pm10,pm2.5占pm10的比重变化,工地为主
                FactorType.PM25,
@@ -121,7 +124,7 @@
                    }.average()
                    val str =
                        "PM2.5量级为${pm25Avg}μg/m³,PM10量级为${pm10Avg}μg/m³,PM2.5占PM10的比重为${round(percentageAvg * 100)}%"
                     if (percentageAvg > 0.666) {
                    if (percentageAvg > 0.666) {
                        "${str},比重较大,污染源以餐饮为主,工地次之" to
                                listOf(
                                    SceneType.TYPE1,
@@ -156,11 +159,11 @@
                    val pm25Avg = round(pollutedData.dataList.map { it.pm25!! }.average() * 10) / 10
                    val coAvg = round(pollutedData.dataList.map { it.co!! }.average()) / 1000
                    val o3Avg = round(pollutedData.dataList.map { it.o3!! }.average() * 10) / 10
                     "VOC偏高,同时PM2.5量级为${pm25Avg}μg/m³,CO量级为${coAvg}mg/m³,O3量级为${o3Avg}μg/m³,污染源以汽修、加油站为主" to
                    "VOC偏高,同时PM2.5量级为${pm25Avg}μg/m³,CO量级为${coAvg}mg/m³,O3量级为${o3Avg}μg/m³,污染源以汽修、加油站为主" to
                            listOf(SceneType.TYPE6, SceneType.TYPE17, SceneType.TYPE12)
                }
                else ->  null
                else -> null
            }
            des = res?.first
            res?.second?.let { sceneTypes.addAll(it) }
@@ -212,7 +215,7 @@
        val et = DateUtil.instance.getTime(pollutedData.endTime)
        var txt =
            "在${st}至${et}之间,出现${pollutedData.exception}"
        pollutedData.statisticMap.entries.forEach {s ->
        pollutedData.statisticMap.entries.forEach { s ->
            txt += ",${s.key.des}最低值为${s.value.min}μg/m³,最高值为${s.value.max}μg/m³,均值为${s.value.avg}μg/m³"
        }
        if (sceneList.isEmpty()) {
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedSummary.kt
@@ -160,7 +160,8 @@
        // å½“前的走航数据的定位和污染源距离是否是逐渐接近,若走航远离了主要污染源,提示用户调整走航路线
        if (!result.sortedSceneList.isNullOrEmpty()) {
            val sT = DateUtil.instance.dateToString(clueList.first().pollutedData?.startTime, DateUtil.DateStyle.HH_MM_SS)
            val sT =
                DateUtil.instance.dateToString(clueList.first().pollutedData?.startTime, DateUtil.DateStyle.HH_MM_SS)
            val eT = DateUtil.instance.dateToString(clueList.last().pollutedData?.endTime, DateUtil.DateStyle.HH_MM_SS)
            val closetScene = result.sortedSceneList?.first()
            // èµ°èˆªè·¯çº¿è°ƒæ•´å»ºè®®
@@ -180,9 +181,10 @@
                val origin = MapUtil.wgs84ToGcj02(lastP.longitude!!.toDouble() to lastP.latitude!!.toDouble())
                val destination = closetScene.first!!.longitude.toDouble() to closetScene.first!!.latitude.toDouble()
                // å»ºè®®çš„走航路线
                result.direction = AMapService.directionDriving(origin, destination)
//                Thread.sleep(200)
                if (config.isSearchAddress) {
                    // å»ºè®®çš„走航路线
                    result.direction = AMapService.directionDriving(origin, destination)
                }
            }
            // çº¿ç´¢åˆ†æžå®ŒæˆåŽï¼Œç§»åŠ¨è‡³åŽ†å²çº¿ç´¢åˆ—è¡¨
            historyClueList.addAll(clueList)
src/main/kotlin/com/flightfeather/uav/domain/entity/SourceTraceMsg.java
@@ -6,6 +6,7 @@
@Table(name = "source_trace_msg")
public class SourceTraceMsg {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    @Column(name = "device_code")
@@ -28,8 +29,6 @@
    @Column(name = "create_time")
    private Date createTime;
    private String content;
    /**
     * @return id
@@ -141,19 +140,5 @@
     */
    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }
    /**
     * @return content
     */
    public String getContent() {
        return content;
    }
    /**
     * @param content
     */
    public void setContent(String content) {
        this.content = content == null ? null : content.trim();
    }
}
src/main/kotlin/com/flightfeather/uav/domain/mapper/SourceTraceMsgMapper.kt
@@ -3,6 +3,14 @@
import com.flightfeather.uav.domain.MyMapper
import com.flightfeather.uav.domain.entity.SourceTraceMsg
import org.apache.ibatis.annotations.Mapper
import org.apache.ibatis.annotations.Select
@Mapper
interface SourceTraceMsgMapper : MyMapper<SourceTraceMsg?>
interface SourceTraceMsgMapper : MyMapper<SourceTraceMsg?> {
    /**
     * é‡ç½®è‡ªå¢žid
     */
    @Select("alter table source_trace_msg auto_increment = #{param1}")
    fun resetAutoIncrement(id: Int)
}
src/main/kotlin/com/flightfeather/uav/domain/repository/SourceTraceRep.kt
@@ -8,9 +8,12 @@
import com.flightfeather.uav.common.utils.GsonUtils
import com.flightfeather.uav.domain.entity.Mission
import com.flightfeather.uav.domain.entity.SourceTraceMsg
import com.flightfeather.uav.domain.entity.SourceTraceMsgBlob
import com.flightfeather.uav.domain.mapper.SourceTraceMsgBlobMapper
import com.flightfeather.uav.domain.mapper.SourceTraceMsgMapper
import com.flightfeather.uav.socket.sender.MsgType
import org.springframework.stereotype.Repository
import org.springframework.transaction.annotation.Transactional
import tk.mybatis.mapper.entity.Example
import java.util.*
@@ -20,11 +23,22 @@
 * @author feiyu02
 */
@Repository
class SourceTraceRep(private val sourceTraceMsgMapper: SourceTraceMsgMapper) {
class SourceTraceRep(
    private val sourceTraceMsgMapper: SourceTraceMsgMapper,
    private val sourceTraceMsgBlobMapper: SourceTraceMsgBlobMapper,
) {
    private fun insertBlob(stm: SourceTraceMsg, json: String) {
        sourceTraceMsgBlobMapper.insert(SourceTraceMsgBlob().apply {
            msgId = stm.id
            content = json
        })
    }
    /**
     * æ’入溯源信息和提醒信息
     */
    @Transactional
    fun insert(msgType: MsgType, obj: PollutedClue): Int {
        val stm = SourceTraceMsg().apply {
            deviceCode = obj.deviceCode
@@ -33,11 +47,12 @@
            startTime = obj.pollutedData?.startTime
            endTime = obj.pollutedData?.endTime
            this.msgType = msgType.value
            content = GsonUtils.gson.toJson(obj)
            createTime = Date()
        }
        return if (fetchOneExist(stm) == null) {
            sourceTraceMsgMapper.insert(stm)
            val c = sourceTraceMsgMapper.insert(stm)
            insertBlob(stm, GsonUtils.gson.toJson(obj))
            c
        } else {
            0
        }
@@ -54,17 +69,19 @@
    /**
     * æ’入线索信息
     */
    @Transactional
    fun insert(res: AnalysisResult): Int {
        val stm = SourceTraceMsg().apply {
            deviceCode = res.deviceCode
            startTime = res.time
            endTime = res.time
            this.msgType = MsgType.AnaResult.value
            content = GsonUtils.gson.toJson(res)
            createTime = Date()
        }
        return if (fetchOneExist(stm) == null) {
            sourceTraceMsgMapper.insert(stm)
            val c = sourceTraceMsgMapper.insert(stm)
            insertBlob(stm, GsonUtils.gson.toJson(res))
            c
        } else {
            0
        }
@@ -83,21 +100,17 @@
    }
    fun fetchList(deviceCode: String, startTime: Date, endTime: Date): List<BaseExceptionResult?> {
        return sourceTraceMsgMapper.selectByExample(Example(SourceTraceMsg::class.java).apply {
            createCriteria().andEqualTo("deviceCode", deviceCode)
                .andGreaterThanOrEqualTo("startTime", startTime)
                .andLessThanOrEqualTo("endTime", endTime)
            orderBy("id").desc()
        }).map { stm ->
        return sourceTraceMsgBlobMapper.selectWithBlob(deviceCode, startTime, endTime)
            .map { stm ->
            when (stm?.msgType) {
                MsgType.PolClue.value,
                MsgType.DataChange.value,
                    -> {
                    GsonUtils.gson.fromJson(stm.content, PollutedClue::class.java)
                    GsonUtils.gson.fromJson(stm.blobContent, PollutedClue::class.java)
                }
                MsgType.AnaResult.value -> {
                    GsonUtils.gson.fromJson(stm.content, AnalysisResult::class.java)
                    GsonUtils.gson.fromJson(stm.blobContent, AnalysisResult::class.java)
                }
                else -> null
@@ -105,11 +118,18 @@
        }
    }
    @Transactional
    fun delete(mission: Mission): Int {
        return sourceTraceMsgMapper.deleteByExample(Example(SourceTraceMsg::class.java).apply {
        val idList = sourceTraceMsgMapper.selectByExample(Example(SourceTraceMsg::class.java).apply {
            createCriteria().andEqualTo("deviceCode", mission.deviceCode)
                .andGreaterThanOrEqualTo("startTime", mission.startTime)
                .andLessThanOrEqualTo("endTime", mission.endTime)
        }).map { it?.id }
        sourceTraceMsgMapper.deleteByExample(Example(SourceTraceMsg::class.java).apply {
            createCriteria().andIn("id", idList)
        })
        return sourceTraceMsgBlobMapper.deleteByExample(Example(SourceTraceMsgBlob::class.java).apply {
            createCriteria().andIn("msgId", idList)
        })
    }
}
src/main/kotlin/com/flightfeather/uav/lightshare/bean/SourceTraceMsgVo.kt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package com.flightfeather.uav.lightshare.bean
import com.flightfeather.uav.domain.entity.SourceTraceMsg
/**
 *
 * @date 2025/8/6
 * @author feiyu02
 */
class SourceTraceMsgVo : SourceTraceMsg() {
    var blobId: Int? = null
    var blobMsgId: Int? = null
    var blobContent: String? = null
}
src/main/resources/generator/generatorConfig.xml
@@ -25,15 +25,15 @@
            <property name="suppressAllComments" value="true"/>
        </commentGenerator>
        <!--数据库链接URL,用户名、密码 -->
<!--        <jdbcConnection driverClass="com.mysql.jdbc.Driver" connectionURL="jdbc:mysql://47.100.191.150:3306/dronemonitor?serverTimezone=Asia/Shanghai"-->
<!--                        userId="remoteU1"-->
<!--                        password="eSoF8DnzfGTlhAjE">-->
<!--        </jdbcConnection>-->
        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/dronemonitor?serverTimezone=Asia/Shanghai"
                        userId="root"
                        password="123456">
        <jdbcConnection driverClass="com.mysql.jdbc.Driver" connectionURL="jdbc:mysql://47.100.191.150:3306/dronemonitor?serverTimezone=Asia/Shanghai"
                        userId="remoteU1"
                        password="eSoF8DnzfGTlhAjE">
        </jdbcConnection>
<!--        <jdbcConnection driverClass="com.mysql.jdbc.Driver"-->
<!--                        connectionURL="jdbc:mysql://localhost:3306/dronemonitor?serverTimezone=Asia/Shanghai"-->
<!--                        userId="root"-->
<!--                        password="123456">-->
<!--        </jdbcConnection>-->
        <javaTypeResolver>
            <property name="forceBigDecimals" value="false"/>
        </javaTypeResolver>
@@ -71,5 +71,6 @@
<!--        <table tableName="grid_data" domainObjectName="GridData" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"/>-->
<!--        <table tableName="grid_data_detail" domainObjectName="GridDataDetail" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"/>-->
        <table tableName="source_trace_msg" domainObjectName="SourceTraceMsg" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"/>
        <table tableName="source_trace_msg_blob" domainObjectName="SourceTraceMsgBlob" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"/>
    </context>
</generatorConfiguration>
src/main/resources/mapper/SourceTraceMsgMapper.xml
@@ -14,22 +14,10 @@
    <result column="msg_type" jdbcType="INTEGER" property="msgType" />
    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
  </resultMap>
  <resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="com.flightfeather.uav.domain.entity.SourceTraceMsg">
    <!--
      WARNING - @mbg.generated
    -->
    <result column="content" jdbcType="LONGVARCHAR" property="content" />
  </resultMap>
  <sql id="Base_Column_List">
    <!--
      WARNING - @mbg.generated
    -->
    id, device_code, factor_name, exception_type, start_time, end_time, msg_type, create_time
  </sql>
  <sql id="Blob_Column_List">
    <!--
      WARNING - @mbg.generated
    -->
    content
  </sql>
</mapper>
src/test/kotlin/com/flightfeather/uav/biz/dataanalysis/BaseExceptionContinuousTest.kt
@@ -26,7 +26,7 @@
            BaseRealTimeData().apply { id = 3 },
            endData!!
        )
        historyData = mutableListOf()
//        historyData = mutableListOf()
        exceptionExisted = true
        exceptionCreated = false
//        exceptionResult =
@@ -103,33 +103,38 @@
    @Test
    fun checkDelayedExceptions() {
        taskList().forEach { exc ->
            val e = exceptions()
            exc.remainingExceptions.add(RemainException(listOf(e[3], e[5]), listOf(FactorType.VOC, FactorType.PM25, FactorType.CO)))
            exc.remainingExceptions.add(RemainException(listOf(e[1]), listOf(FactorType.VOC, FactorType.CO)))
            exc.remainingExceptions.add(RemainException(listOf(e[3]), listOf(FactorType.PM10, FactorType.PM25)))
        val res = listOf(FactorFilter.SelectedFactor(FactorType.PM25) to exceptionTag)
        val c = listOf(FactorType.PM25, FactorType.PM10, FactorType.VOC)
        val r = RemainException(res, c)
        println(r)
//        taskList().forEach { exc ->
//            val e = exceptions()
//            exc.remainingExceptions.add(RemainException(listOf(e[3], e[5]), listOf(FactorType.VOC, FactorType.PM25, FactorType.CO)))
//            exc.remainingExceptions.add(RemainException(listOf(e[1]), listOf(FactorType.VOC, FactorType.CO)))
//            exc.remainingExceptions.add(RemainException(listOf(e[3]), listOf(FactorType.PM10, FactorType.PM25)))
//
//            exc.latestExceptions.clear()
//            exc.latestExceptions.addAll(exceptions())
//
//            exc.removeSingleFactor(BaseRealTimeData())
//
//            val resList = exc.result.map {
//                it.pollutedData?.statisticMap?.entries?.map { e -> e.key }
//            }
//            assertContentEquals(
//                listOf(listOf(FactorType.NO2), listOf(FactorType.O3)),
//                resList,
//                "异常结果应该都是不在组合中的异常"
//            )
//
//            val resList2 = exc.latestExceptions.map { it.first.main }
//            assertContentEquals(
//                listOf(FactorType.CO, FactorType.PM25, FactorType.PM10, FactorType.VOC),
//                resList2,
//                "剩余的应该是不在组合中的异常"
//            )
//        }
            exc.latestExceptions.clear()
            exc.latestExceptions.addAll(exceptions())
            exc.removeSingleFactor(BaseRealTimeData())
            val resList = exc.result.map {
                it.pollutedData?.statisticMap?.entries?.map { e -> e.key }
            }
            assertContentEquals(
                listOf(listOf(FactorType.NO2), listOf(FactorType.O3)),
                resList,
                "异常结果应该都是不在组合中的异常"
            )
            val resList2 = exc.latestExceptions.map { it.first.main }
            assertContentEquals(
                listOf(FactorType.CO, FactorType.PM25, FactorType.PM10, FactorType.VOC),
                resList2,
                "剩余的应该是不在组合中的异常"
            )
        }
    }
    @Test
@@ -140,11 +145,33 @@
            FactorFilter.SelectedFactor(FactorType.CO),
        )
        val factorList2 = listOf(
            FactorFilter.SelectedFactor(FactorType.CO),
            FactorFilter.SelectedFactor(FactorType.PM25),
            FactorFilter.SelectedFactor(FactorType.PM10),
        )
        println(factorList == factorList2)
        val factorList3 = listOf(
            FactorFilter.SelectedFactor(FactorType.PM25),
            FactorFilter.SelectedFactor(FactorType.VOC),
        )
        val comb = listOf(
            listOf(
                FactorFilter.SelectedFactor(FactorType.PM10),
                FactorFilter.SelectedFactor(FactorType.PM25),
                FactorFilter.SelectedFactor(FactorType.CO),
            ),
            listOf(
                FactorFilter.SelectedFactor(FactorType.PM25),
                FactorFilter.SelectedFactor(FactorType.PM10),
            ),
            listOf(
                FactorFilter.SelectedFactor(FactorType.VOC),
                FactorFilter.SelectedFactor(FactorType.PM25),
            )
        )
        val i = comb.indexOf(factorList)
        println(i)
    }
    @Test
src/test/kotlin/com/flightfeather/uav/biz/sourcetrace/SourceTraceControllerTest.kt
@@ -37,8 +37,12 @@
    @Test
    fun autoSourceTrace() {
        val sourceTraceController = SourceTraceController(sceneInfoRep, sourceTraceRep, false)
        val mCode = listOf("SH-CN-20241227", "SH-CN-20241127", "SH-CN-20240906", "SH-CN-20240830(05)",
            "SH-CN-20240830(04)", "SH-CN-20240823", "SH-CN-20240723(02)", "SH-CN-20250723(01)")
        val mCode = listOf(
            "SH-CN-20241227", "SH-CN-20241127", "SH-CN-20240906", "SH-CN-20240830(05)",
            "SH-CN-20240830(04)", "SH-CN-20240823",
            "SH-CN-20240723(02)",
//            "SH-CN-20250723(01)"
        )
        mCode.forEach { c->
            missionRep.findOne(c)?.let {m ->
                val rtData = realTimeDataService.getSecondData(
@@ -99,8 +103,12 @@
    @Test
    fun deleteSourceTrace() {
        val mCode = listOf("SH-CN-20241227", "SH-CN-20241127", "SH-CN-20240906", "SH-CN-20240830(05)",
            "SH-CN-20240830(04)", "SH-CN-20240823", "SH-CN-20240723(02)", "SH-CN-20250723(01)")
        val mCode = listOf(
            "SH-CN-20241227", "SH-CN-20241127", "SH-CN-20240906", "SH-CN-20240830(05)",
            "SH-CN-20240830(04)", "SH-CN-20240823",
            "SH-CN-20240723(02)",
//            "SH-CN-20250723(01)"
        )
        mCode.forEach {c ->
            missionRep.findOne(c)?.let {
                sourceTraceRep.delete(it)
src/test/kotlin/com/flightfeather/uav/lightshare/service/impl/DataAnalysisServiceImplTest.kt
@@ -19,4 +19,9 @@
    fun testPollutionTrace() {
        dataAnalysisService.pollutionTrace("SH-CN-20250116")
    }
    @Test
    fun fetchHistory() {
        dataAnalysisService.fetchHistory("SH-CN-20250723(01)")
    }
}