feiyu02
23 小时以前 8eb584869b4fd4de0f51c93f2616f12e51df9193
2025.12.18
1. 动态溯源相关分析逻辑调整;
2. 走航报告接口参数调整;
已修改10个文件
623 ■■■■■ 文件已修改
src/main/kotlin/com/flightfeather/uav/biz/report/MissionRiskArea.kt 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedArea.kt 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedClue.kt 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedData.kt 257 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedSource.kt 124 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/lightshare/bean/AnalysisOption.kt 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/lightshare/service/DataAnalysisService.kt 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/lightshare/service/impl/DataAnalysisServiceImpl.kt 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/lightshare/web/DataAnalysisController.kt 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/kotlin/com/flightfeather/uav/biz/sourcetrace/SourceTraceControllerTest.kt 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/report/MissionRiskArea.kt
@@ -56,16 +56,11 @@
        
        pollutedClues.forEach { pollutedClue ->
            if (pollutedClue == null) return@forEach
            val dataList = pollutedClue.pollutedData?.dataList ?: emptyList()
            if (dataList.isEmpty()) return@forEach
            // 计算单个PollutedClue的均值经纬度
            val avgData = dataList.avg()
            val wgs84Lng = avgData.longitude?.toDouble() ?: return@forEach
            val wgs84Lat = avgData.latitude?.toDouble() ?: return@forEach
            val wgs84Center = pollutedClue.pollutedData?.getExceptionCenter() ?: return@forEach
            
            // 坐标转换
            val gcj02Point = MapUtil.wgs84ToGcj02(wgs84Lng to wgs84Lat)
            val gcj02Point = MapUtil.wgs84ToGcj02(wgs84Center)
            
            // 查找最近场景
            var minDistance = Double.MAX_VALUE
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedArea.kt
@@ -1,5 +1,7 @@
package com.flightfeather.uav.biz.sourcetrace.model
import com.flightfeather.uav.biz.FactorFilter
import com.flightfeather.uav.biz.dataanalysis.model.ExceptionTag
import com.flightfeather.uav.biz.sourcetrace.config.RTExcWindLevelConfig
import com.flightfeather.uav.common.net.AMapService
import com.flightfeather.uav.common.utils.MapUtil
@@ -20,15 +22,14 @@
     */
    constructor(
        historyData: List<BaseRealTimeData>,
        exceptionData: List<BaseRealTimeData>,
        pollutedData: PollutedData,
        config: RTExcWindLevelConfig,
        windLevelCondition: RTExcWindLevelConfig.WindLevelCondition?,
    ) : this() {
        distanceType = windLevelCondition?.distanceType
        distanceRange = distanceType?.disRange
        distanceDes = distanceType?.des
        windLevelCondition?.let { sourceTrace(historyData, exceptionData, config, it) }
        windLevelCondition?.let { sourceTrace(pollutedData, config, it) }
    }
    // 所属街镇
@@ -64,19 +65,13 @@
     * 反向溯源
     */
    private fun sourceTrace(
        historyData: List<BaseRealTimeData>,
        exceptionData: List<BaseRealTimeData>,
        pollutedData: PollutedData,
        config: RTExcWindLevelConfig,
        windLevelCondition: RTExcWindLevelConfig.WindLevelCondition,
    ) {
        val avgData = if (exceptionData.size == 1) {
            exceptionData.first()
        } else {
            exceptionData.avg()
        }
        val avgData = pollutedData.getExceptionAvgData()
        val pair = avgData.longitude!!.toDouble() to avgData.latitude!!.toDouble()
        polygon = calSector(
            avgData.windDirection?.toDouble() ?: .0,
            pair,
@@ -87,7 +82,7 @@
            MapUtil.wgs84ToGcj02(it)
        }
        closePolygon = closeSourceTrace(historyData, pair).map {
        closePolygon = closeSourceTrace(pair).map {
            // 将坐标转换为gcj02(火星坐标系),因为污染源场景信息都为此坐标系
            MapUtil.wgs84ToGcj02(it)
        }
@@ -161,10 +156,7 @@
        return result
    }
    private fun closeSourceTrace(
        historyData: List<BaseRealTimeData>,
        center: Pair<Double, Double>,
    ): List<Pair<Double, Double>> {
    private fun closeSourceTrace(center: Pair<Double, Double>): List<Pair<Double, Double>> {
        val result = mutableListOf<Pair<Double, Double>>()
        var startDeg = 0
        while (startDeg <= 360) {
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedClue.kt
@@ -28,61 +28,12 @@
    ) : this() {
        if (exceptions.isEmpty() || exceptions[0].second.exceptionData.isEmpty()) return
        deviceCode = exceptions[0].second.startData?.deviceCode
        var startData: BaseRealTimeData? = null
        var endData: BaseRealTimeData? = null
        var exceptionData = mutableListOf<BaseRealTimeData>()
        var historyData = mutableListOf<BaseRealTimeData>()
        exceptions.forEach { e ->
            // 将采样时间最早的作为开始数据
            if (startData == null) {
                startData = e.second.startData
            } else {
                if (e.second.startData?.dataTime!! < startData!!.dataTime) {
                    startData = e.second.startData
                }
            }
            // 将采样时间最晚的作为结束数据
            if (endData == null) {
                endData = e.second.endData
            } else {
                if (e.second.endData?.dataTime!! > endData!!.dataTime) {
                    endData = e.second.endData
                }
            }
            // 将所有异常数据去重合并
            if (exceptionData.isEmpty()) {
                exceptionData = e.second.exceptionData
            } else {
                e.second.exceptionData.forEach {
                    if (exceptionData.find { d -> d.dataTime == it.dataTime } == null) {
                        exceptionData.add(it)
                    }
                }
            }
            // 将所有历史数据去重合并
            if (historyData.isEmpty()) {
                historyData = e.second.historyData
            } else {
                e.second.historyData.forEach {
                    if (historyData.find { d -> d.dataTime == it.dataTime } == null) {
                        historyData.add(it)
                    }
                }
            }
        }
        // 按照采样时间升序排列
        exceptionData.sortBy { it.dataTime }
        historyData.sortBy { it.dataTime }
        // 获取去重后的监测因子类型
        val factorList = exceptions.map { it.first }.distinct()
        pollutedData = PollutedData(
            startData!!, endData, factorList, exceptionData, historyData, eType, windLevelCondition
        )
        pollutedArea = PollutedArea(historyData, exceptionData, config, windLevelCondition)
        pollutedData = PollutedData(exceptions, eType)
        pollutedArea = PollutedArea(pollutedData!!, config, windLevelCondition)
    }
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedData.kt
@@ -1,10 +1,12 @@
package com.flightfeather.uav.biz.sourcetrace.model
import com.flightfeather.uav.biz.FactorFilter
import com.flightfeather.uav.biz.dataanalysis.model.ExceptionTag
import com.flightfeather.uav.biz.dataanalysis.model.ExceptionType
import com.flightfeather.uav.biz.sourcetrace.config.RTExcWindLevelConfig
import com.flightfeather.uav.common.utils.DateUtil
import com.flightfeather.uav.domain.entity.BaseRealTimeData
import com.flightfeather.uav.domain.entity.avg
import com.flightfeather.uav.lightshare.bean.DataVo
import com.flightfeather.uav.socket.eunm.FactorType
import java.util.Date
@@ -17,12 +19,77 @@
 */
class PollutedData() {
    companion object {
        // 默认数据采样时间间隔,单位:秒
        const val DEFAULT_PERIOD = 4
    }
    /**
     * 异常数据分组情况统计
     */
    inner class ExcGroup{
        constructor(dataIndexList: List<Int>, factorType: FactorType){
            this.dataIndexList = dataIndexList
            this.factorType = factorType
            val first = getFirstDataValue()?.toDouble()
            val last = getLastDataValue()?.toDouble()
            if (first != null && last != null) {
                per = round((last - first) / first * 100) / 100
                rate = round((last - first) / DEFAULT_PERIOD * 100) / 100
            }
        }
        var factorType: FactorType? = null
        /**
         * 异常数据对应历史数据[historyDataList]中的索引值
         */
        var dataIndexList: List<Int>? = null
        // 变化幅度
        var per: Double? = null
        // 变化速率
        var rate: Double? = null
        /**
         * 获取异常数据的第一个数据
         * !!!!第一个数据其实是首个异常数据的前一个数据值!!!!
         */
        fun getFirstData(): BaseRealTimeData? {
            return dataIndexList?.firstOrNull()?.let {
                val i = if (it > 0) it - 1 else it
                historyDataList[i].toBaseRealTimeData(BaseRealTimeData::class.java)
            }
        }
        fun getFirstDataValue(): Float? {
            return getFirstData()?.getByFactorType(factorType)
        }
        /**
         * 获取异常数据的最后一个数据
         */
        fun getLastData(): BaseRealTimeData? {
            return dataIndexList?.lastOrNull()?.let {
                historyDataList[it].toBaseRealTimeData(BaseRealTimeData::class.java)
            }
        }
        fun getLastDataValue(): Float? {
            return getLastData()?.getByFactorType(factorType)
        }
    }
    /**
     * 各监测因子异常统计信息
     */
    inner class Statistic(){
        var factorId: Int? = null
        var factorName: String? = null
        var subFactorId: List<Int>? = null
        var subFactorName: List<String>? = null
        var selectedFactor: FactorFilter.SelectedFactor? = null
        /**
         * 异常数据对应历史数据[historyDataList]中的索引值
         */
        var dataIndexList: List<Int>? = null
        // 因子量级平均变化幅度
        var avgPer: Double? = null
@@ -32,110 +99,176 @@
        var avg: Double? = null
        var min: Double? = null
        var max: Double? = null
        var excGroup: List<ExcGroup>? = null
        /**
         * 获取异常数据
         */
        fun getExceptionData(): List<BaseRealTimeData>? {
            return dataIndexList?.map { historyDataList[it].toBaseRealTimeData(BaseRealTimeData::class.java) }
    }
    constructor(
        start: BaseRealTimeData,
        end: BaseRealTimeData?,
        factorList: List<FactorFilter.SelectedFactor>,
        exceptionData: List<BaseRealTimeData>,
        historyData: List<BaseRealTimeData>,
        eType: ExceptionType,
        windLevelCondition: RTExcWindLevelConfig.WindLevelCondition?,
    ) : this() {
        /**
         * 获取异常数据分段情况
         * 将连续的异常数据分为一组
         */
        fun getExceptionDataGroup(): List<List<Int>> {
            val res = mutableListOf<MutableList<Int>>()
            var curGroup = mutableListOf<Int>()
            var lastIndex = -2
            dataIndexList?.forEach {
                if (curGroup.isEmpty()) {
                    curGroup.add(it)
                } else if (it - lastIndex == 1) {
                    curGroup.add(it)
                } else {
                    res.add(curGroup)
                    curGroup = mutableListOf(it)
                }
                lastIndex = it
            }
            if (curGroup.isNotEmpty()) {
                res.add(curGroup)
            }
            return res
        }
    }
    constructor(exceptions: List<Pair<FactorFilter.SelectedFactor, ExceptionTag>>, eType: ExceptionType,) : this() {
        // 遍历所有的因子的异常,整合统一的异常结果,具体如下
        var startData: BaseRealTimeData? = null
        var endData: BaseRealTimeData? = null
        var historyData = mutableListOf<BaseRealTimeData>()
        var _times = 0
        exceptions.forEach { e ->
            // 将采样时间最早的数据作为开始数据
            if (startData == null) {
                startData = e.second.startData
            } else {
                if (e.second.startData?.dataTime!! < startData!!.dataTime) {
                    startData = e.second.startData
                }
            }
            // 将采样时间最晚的作为结束数据
            if (endData == null) {
                endData = e.second.endData
            } else {
                if (e.second.endData?.dataTime!! > endData!!.dataTime) {
                    endData = e.second.endData
                }
            }
            // 将所有历史数据去重合并
            if (historyData.isEmpty()) {
                historyData = e.second.historyData
            } else {
                e.second.historyData.forEach {
                    if (historyData.find { d -> d.dataTime == it.dataTime } == null) {
                        historyData.add(it)
                    }
                }
            }
            _times += e.second.exceptionData.size
        }
        // 按照采样时间升序排列
        historyData.sortBy { it.dataTime }
        exception = eType.des
        exceptionType = eType.value
        startTime = start.dataTime
        endTime = end?.dataTime
//        startData = start.getByFactorType(factor.main)
//        endData = end?.getByFactorType(factor.main) ?: startData
//        startData = start
//        endData = end
        windSpeed = exceptionData.first().windSpeed?.toDouble()
        times = windLevelCondition?.countLimit
        dataList.add(start)
        exceptionData.forEach {
            dataList.add(it)
        }
        dataVoList.addAll(dataList.map { it.toDataVo() })
        startTime = startData?.dataTime
        endTime = endData?.dataTime
        windSpeed = historyData.avg().windSpeed?.toDouble()
        times = _times
        historyDataList.addAll(historyData.map { it.toDataVo() })
        // 再次整合异常数据,分别计算各因子的异常结果统计
        exceptions.forEach {e ->
            statisticMap[e.first.main] = Statistic().apply {
                factorId = e.first.main.value
                factorName = e.first.main.des
                subFactorId = e.first.subs.map { it.value }
                subFactorName = e.first.subs.map { it.des }
                selectedFactor = e.first
                dataIndexList = e.second.exceptionData.map {
                    historyDataList.indexOfFirst { d ->
                        d.time == DateUtil.instance.dateToString(it.dataTime, DateUtil.DateStyle.YYYY_MM_DD_HH_MM_SS)
                    }
                }
        factorList.forEach { f->
            statisticMap[f.main] = Statistic().apply {
                factorId = f.main.value
                factorName = f.main.des
                subFactorId = f.subs.map { it.value }
                subFactorName = f.subs.map { it.des }
                selectedFactor = f
                avgPer = calPer(f.main)
                avgRate = calRate(f.main)
                val s = dataSummary(dataList, f.main)
                val s = dataSummary(e.second.exceptionData, e.first.main)
                avg = s.first
                min = s.second
                max = s.third
                excGroup = getExceptionDataGroup().map { ExcGroup(it, e.first.main) }
                avgPer = excGroup?.mapNotNull { it.per }?.average()
                avgRate = excGroup?.mapNotNull { it.rate }?.average()
            }
        }
    }
    var deviceCode: String? = null
    var exception: String? = null
    var exceptionType: Int? = null
    var startTime: Date? = null
    var endTime: Date? = null
//    var startData: BaseRealTimeData? = null
//    var endData: BaseRealTimeData? = null
    // 风速
    var windSpeed: Double? = null
    // 发生次数
    var times: Int? = null
    var historyDataList = mutableListOf<DataVo>()
    // 异常监测数据,包含单次异常中所有发生了异常的数据值(可能不是时间连续的数据)
    var dataList: MutableList<BaseRealTimeData> = mutableListOf()
    var dataVoList: MutableList<DataVo> = mutableListOf()
//    var dataList: MutableList<BaseRealTimeData> = mutableListOf()
//    var dataVoList: MutableList<DataVo> = mutableListOf()
    var statisticMap = mutableMapOf<FactorType, Statistic>()
    /**
     * 获取所有异常因子名称
     */
    fun toFactorNames(): String {
        val factors = statisticMap.entries.map { it.key }.sortedBy { it.value }.joinToString(";") { it.des }
        return factors
    }
    private fun calPer(factorType: FactorType): Double? {
        val list = dataList
        if (list.size < 2) return null
    fun getExceptionAvgData(): BaseRealTimeData {
        val exceptionDataList = statisticMap.flatMap { it.value.getExceptionData() ?: emptyList() }
        val avgData = exceptionDataList.avg()
        return avgData
    }
    /**
     * 获取异常数据中心坐标(异常数据中经度纬度的平均值)
     */
    fun getExceptionCenter(): Pair<Double, Double>? {
        val avgData = getExceptionAvgData()
        val wgs84Lng = avgData.longitude?.toDouble()
        val wgs84Lat = avgData.latitude?.toDouble()
        return if (wgs84Lng == null || wgs84Lat == null) null else Pair(wgs84Lng, wgs84Lat)
    }
    private fun calPer(exceptionData: List<BaseRealTimeData?>, factorType: FactorType): Double? {
        if (exceptionData.size < 2) return null
        var total = .0
        for (i in 0 until list.size - 1) {
            val p = list[i].getByFactorType(factorType) ?: .0f
            val n = list[i + 1].getByFactorType(factorType) ?: .0f
        for (i in 0 until exceptionData.size - 1) {
            val p = exceptionData[i]?.getByFactorType(factorType) ?: .0f
            val n = exceptionData[i + 1]?.getByFactorType(factorType) ?: .0f
            total += (n - p) / p
        }
        return total / (list.size - 1)
        return total / (exceptionData.size - 1)
    }
    private fun calRate(factorType: FactorType): Double? {
        val list = dataList
        if (list.size < 2) return null
    private fun calRate(exceptionData: List<BaseRealTimeData?>, factorType: FactorType): Double? {
        if (exceptionData.size < 2) return null
        var total = .0
        for (i in 0 until list.size - 1) {
            val p = list[i].getByFactorType(factorType) ?: .0f
            val n = list[i + 1].getByFactorType(factorType) ?: .0f
        for (i in 0 until exceptionData.size - 1) {
            val p = exceptionData[i]?.getByFactorType(factorType) ?: .0f
            val n = exceptionData[i + 1]?.getByFactorType(factorType) ?: .0f
            total += (n - p) / 4
        }
        return total / (list.size - 1)
        return total / (exceptionData.size - 1)
    }
    private fun dataSummary(exceptionData: List<BaseRealTimeData?>, factorType: FactorType): Triple<Double, Double,
src/main/kotlin/com/flightfeather/uav/biz/sourcetrace/model/PollutedSource.kt
@@ -12,7 +12,6 @@
import com.flightfeather.uav.socket.eunm.FactorType
import org.springframework.beans.BeanUtils
import org.springframework.web.context.ContextLoader
import kotlin.math.round
/**
 * 污染来源
@@ -76,14 +75,12 @@
            // 5. 根据污染因子的量级,计算主要的污染场景类型,筛选结果
            val mainSceneType = calSceneType(pollutedData)
            if (mainSceneType != null) {
                result = result.filter {
                    val r = mainSceneType.second.find { s ->
                val r = mainSceneType.find { s ->
                        s.value == it.typeId.toInt()
                    }
                    r != null
                }.toMutableList()
            }
            this.sceneList = findClosestStation(sceneInfoRep, result)
        }
@@ -95,8 +92,7 @@
     * 计算可能的相关污染场景类型以及推理结论
     */
    @Throws(Exception::class)
    private fun calSceneType(pollutedData: PollutedData): Pair<String, List<SceneType>>? {
        var des: String? = null
    private fun calSceneType(pollutedData: PollutedData): List<SceneType> {
        val sceneTypes = mutableListOf<SceneType>()
        pollutedData.statisticMap.entries.forEach { s ->
            val res = when (s.key) {
@@ -104,56 +100,16 @@
                FactorType.NO,
                FactorType.NO2,
                    -> {
//                    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.TYPE1, SceneType.TYPE6, SceneType.TYPE10, SceneType.TYPE17)
                }
                FactorType.CO -> "" to listOf(SceneType.TYPE6, SceneType.TYPE10, SceneType.TYPE17)
                FactorType.CO -> listOf(SceneType.TYPE6, SceneType.TYPE10, SceneType.TYPE17)
                FactorType.H2S -> null
                FactorType.SO2 -> null
                FactorType.O3 -> null
                // a) pm2.5、pm10特别高,两者在各情况下同步展示,pm2.5占pm10的比重变化,比重越高,越有可能是餐饮
                // b) pm10特别高、pm2.5较高,大颗粒扬尘污染,只展示pm10,pm2.5占pm10的比重变化,工地为主
                FactorType.PM25,
                FactorType.PM10,
                    -> {
//                    val pm25Avg = round(pollutedData.dataList.map { it.pm25!! }.average() * 10) / 10
//                    val pm10Avg = round(pollutedData.dataList.map { it.pm10!! }.average() * 10) / 10
                    val pm25Avg = round((pollutedData.statisticMap[FactorType.PM25]?.avg ?: .0) * 10) / 10
                    val pm10Avg = round((pollutedData.statisticMap[FactorType.PM10]?.avg ?: .0) * 10) / 10
                    // 计算异常数据的pm2.5占pm10比重的均值
                    val percentageAvg = pollutedData.dataList.map {
                        it.pm25!! / it.pm10!!
                    }.average()
                    val str =
                        "PM2.5量级为${pm25Avg}μg/m³,PM10量级为${pm10Avg}μg/m³,PM2.5占PM10的比重为${round(percentageAvg * 100)}%"
                    if (percentageAvg > 0.666) {
                        "${str},比重较大,污染源以餐饮为主,工地次之" to
                                listOf(
                                    SceneType.TYPE1,
                                    SceneType.TYPE2,
                                    SceneType.TYPE3,
                                    SceneType.TYPE14,
                                    SceneType.TYPE5,
                                    SceneType.TYPE18
                                )
                    } else if (percentageAvg < 0.333) {
                        "${str},比重较小,属于大颗粒扬尘污染,污染源以工地为主" to
                                listOf(
                                    SceneType.TYPE1,
                                    SceneType.TYPE2,
                                    SceneType.TYPE3,
                                    SceneType.TYPE14,
                                    SceneType.TYPE5,
                                    SceneType.TYPE18
                                )
                    } else {
                        "${str},污染源以餐饮、工地为主" to
                                listOf(
                                    SceneType.TYPE1,
                                    SceneType.TYPE2,
@@ -163,26 +119,16 @@
                                    SceneType.TYPE18
                                )
                    }
                }
                // c) VOC较高,同比计算pm2.5的量级,可能存在同步偏高(汽修、加油站), 同步计算O3是否有高值
                // d) VOC较高,处于加油站(车辆拥堵情况),CO一般较高, 同步计算O3是否有高值
                FactorType.VOC -> {
//                    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
                    val pm25Avg = round((pollutedData.statisticMap[FactorType.PM25]?.avg ?: .0)) / 10
                    val coAvg = round((pollutedData.statisticMap[FactorType.CO]?.avg ?: .0)) / 1000
                    val o3Avg = round((pollutedData.statisticMap[FactorType.O3]?.avg ?: .0)) / 10
                    "VOC偏高,同时PM2.5量级为${pm25Avg}μg/m³,CO量级为${coAvg}mg/m³,O3量级为${o3Avg}μg/m³,污染源以汽修、加油站为主" to
                            listOf(SceneType.TYPE5, SceneType.TYPE6, SceneType.TYPE17, SceneType.TYPE12, SceneType.TYPE18)
                }
                else -> null
            }
            des = res?.first
            res?.second?.let { sceneTypes.addAll(it) }
            res?.let { sceneTypes.addAll(it) }
        }
        return (des ?: "") to sceneTypes
        return sceneTypes.distinct()
    }
    /**
@@ -233,56 +179,32 @@
        val et = DateUtil.instance.getTime(pollutedData.endTime)
        // 1. 描述异常发生的时间和异常类型
        var txt = "在${st}至${et}之间,出现${pollutedData.exception}"
        var txt = "在${st}至${et}之间,出现${pollutedData.exception}${pollutedData.times}次"
        // 2. 描述异常数据的变化情况
        // 异常数据长度应该大于1,首个值是异常开始数据的前一个正常值,后续为异常数据值(但不一定时间连续)
        if (pollutedData.dataList.size > 1) {
            val historyDataList = pollutedData.historyDataList.map { it.toBaseRealTimeData(BaseRealTimeData::class.java) }
            when (pollutedData.exceptionType) {
                // 量级突变
                ExceptionType.TYPE4.value -> {
                    val exceptionPair = mutableListOf<Pair<BaseRealTimeData, BaseRealTimeData>>()
                    pollutedData.dataList.forEachIndexed { index, baseRealTimeData ->
                        if (index == 0) return@forEachIndexed
                        val preIndex = historyDataList.indexOfFirst {
                            it.dataTime == baseRealTimeData.dataTime
                        }
                        exceptionPair.add(
                            (if (preIndex - 1 < 0) historyDataList[0] else historyDataList[preIndex - 1])
                                    to baseRealTimeData
                        )
                    }
                    val statArr = mutableListOf<String>()
                    pollutedData.statisticMap.entries.forEach { s ->
                        val txtArr = mutableListOf<String>()
                        exceptionPair.forEach exception@{ p ->
                            val preValue = p.first.getByFactorType(s.key)
                            val curValue = p.second.getByFactorType(s.key)
                            if (preValue == null || curValue == null) return@exception
                            val r = round((curValue - preValue) / preValue * 100)
                            txtArr.add("从${preValue}μg/m³突变至${curValue}μg/m³,变化率为${r}%")
            s.value.excGroup?.forEach exception@{ p ->
                val preValue = p.getFirstDataValue()
                val curValue = p.getLastDataValue()
                val per = p.per?.times(100)
                val rate = p.rate
                if (preValue == null || curValue == null || per == null) return@exception
                when (pollutedData.exceptionType) {
                    // 量级突变
                    ExceptionType.TYPE4.value -> {
                        txtArr.add("从${preValue}μg/m³突变至${curValue}μg/m³,变化率为${per}%")
                    }
                    // 快速上升
                    ExceptionType.TYPE9.value -> {
                        txtArr.add("从${preValue}μg/m³快速上升至${curValue}μg/m³,变化速率为${rate}μg/m³/秒,变化率为${per}%")
                    }
                }
                        }
                        statArr.add("${s.key.getTxt()}量级${txtArr.joinToString(",")}")
                    }
                    txt += ",${statArr.joinToString(";")}"
                }
                // 快速上升
                ExceptionType.TYPE9.value -> {
                    pollutedData.statisticMap.entries.forEach { s ->
                        val preValue = pollutedData.dataList.first().getByFactorType(s.key)
                        val curValue = pollutedData.dataList.last().getByFactorType(s.key)
                        if (preValue == null || curValue == null) return@forEach
                        val r = round((curValue - preValue) / preValue * 100)
                        txt += ",${s.key.getTxt()}从${preValue}μg/m³快速上升至${curValue}μg/m³,变化率为${r}%"
                    }
                }
            }
        } else {
            pollutedData.statisticMap.entries.forEach { s ->
                txt += ",${s.key.des}最低值为${s.value.min}μg/m³,最高值为${s.value.max}μg/m³,均值为${s.value.avg}μg/m³"
            }
        }
        // 3. 描述发现的风险源情况
        if (sceneList.isEmpty()) {
src/main/kotlin/com/flightfeather/uav/lightshare/bean/AnalysisOption.kt
@@ -3,7 +3,9 @@
import com.fasterxml.jackson.annotation.JsonFormat
import io.swagger.annotations.ApiModel
import io.swagger.annotations.ApiModelProperty
import io.swagger.annotations.ApiParam
import org.springframework.format.annotation.DateTimeFormat
import org.springframework.web.bind.annotation.RequestParam
import java.time.LocalDateTime
/**
@@ -36,4 +38,10 @@
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @ApiModelProperty(value = "结束时间")
    var endTime: LocalDateTime? = null
    @ApiModelProperty(value = "数据颗粒度", allowableValues = "SECOND, MINUTE, HOUR")
    var granularity: String? = null
    @ApiParam("需要统计的监测因子", example = "NO2, CO")
    var factorTypes: List<String>? = null
}
src/main/kotlin/com/flightfeather/uav/lightshare/service/DataAnalysisService.kt
@@ -32,6 +32,7 @@
     * @param missionCode 走航任务编号,用于唯一标识特定的走航任务
     * @return 异常结果列表,每个元素包含异常类型、位置和详细信息
     */
    @Deprecated("该接口已废弃,使用/report/missionSummary/one接口")
    fun pollutionTrace(missionCode: String): List<ExceptionResult>
    /**
src/main/kotlin/com/flightfeather/uav/lightshare/service/impl/DataAnalysisServiceImpl.kt
@@ -265,13 +265,6 @@
        if (removeNoPollutedSource) {
            clues.removeIf { it?.pollutedSource?.sceneList.isNullOrEmpty() }
        }
//        val keyScenes = sceneInfoRep.findBySceneTypes(
//            listOf(
//                SceneType.TYPE19.value,
//                SceneType.TYPE20.value,
//                SceneType.TYPE21.value
//            )
//        )
        return MissionRiskArea().generateClueByRiskArea(clues)
    }
src/main/kotlin/com/flightfeather/uav/lightshare/web/DataAnalysisController.kt
@@ -25,12 +25,11 @@
@RequestMapping("air/analysis")
class DataAnalysisController(private val dataAnalysisService: DataAnalysisService) {
    @Deprecated("该接口已废弃,使用/report/missionSummary/one接口")
    @ApiOperation(value = "污染溯源分析")
    @GetMapping("/pollution/trace")
    fun pollutionTrace(
        @ApiParam("走航任务编号") @RequestParam missionCode: String,
//        @RequestParam("page", required = false) page: Int?,
//        @RequestParam("per_page", required = false) perPage: Int?,
    ) = resPack { dataAnalysisService.pollutionTrace(missionCode) }
    @ApiOperation(value = "获取历史污染溯源结果")
@@ -42,16 +41,14 @@
    @ApiOperation(value = "生成走航任务汇总统计")
    @PostMapping("/report/missionSummary")
    fun generateMissionSummary(
        @ApiParam("开始时间") @RequestParam
        @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
        startTime: LocalDateTime,
        @ApiParam("结束时间") @RequestParam
        @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
        endTime: LocalDateTime,
        @ApiParam("区域") @RequestBody areaVo: AreaVo,
        @ApiParam("分析选项") @RequestBody analysisOption: AnalysisOption,
    ) = resPack {
        if (analysisOption.startTime == null || analysisOption.endTime == null || analysisOption.area == null)
            throw BizException("参数错误, startTime, endTime, area不能为空")
        val startTime = analysisOption.startTime!!.atZone(ZoneId.systemDefault()).toInstant()
        val endTime = analysisOption.endTime!!.atZone(ZoneId.systemDefault()).toInstant()
        val areaVo = analysisOption.area!!
        dataAnalysisService.generateMissionSummary(
            Date.from(startTime.atZone(ZoneId.systemDefault()).toInstant()),
            Date.from(endTime.atZone(ZoneId.systemDefault()).toInstant()),
@@ -68,16 +65,14 @@
    @ApiOperation(value = "生成走航任务清单")
    @PostMapping("/report/missionList")
    fun generateMissionList(
        @ApiParam("开始时间") @RequestParam
        @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
        startTime: LocalDateTime,
        @ApiParam("结束时间") @RequestParam
        @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
        endTime: LocalDateTime,
        @ApiParam("区域") @RequestBody areaVo: AreaVo,
        @ApiParam("分析选项") @RequestBody analysisOption: AnalysisOption,
    ) = resPack {
        if (analysisOption.startTime == null || analysisOption.endTime == null || analysisOption.area == null)
            throw BizException("参数错误, startTime, endTime, area不能为空")
        val startTime = analysisOption.startTime!!.atZone(ZoneId.systemDefault()).toInstant()
        val endTime = analysisOption.endTime!!.atZone(ZoneId.systemDefault()).toInstant()
        val areaVo = analysisOption.area!!
        dataAnalysisService.generateMissionList(
            Date.from(startTime.atZone(ZoneId.systemDefault()).toInstant()),
            Date.from(endTime.atZone(ZoneId.systemDefault()).toInstant()),
@@ -88,18 +83,15 @@
    @ApiOperation(value = "生成走航任务详情")
    @PostMapping("/report/missionDetail")
    fun generateMissionDetail(
        @ApiParam("开始时间") @RequestParam
        @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
        startTime: LocalDateTime,
        @ApiParam("结束时间") @RequestParam
        @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
        endTime: LocalDateTime,
        @ApiParam("数据颗粒度", allowableValues = "SECOND, MINUTE, HOUR") @RequestParam(required = false)
        granularity: String?,
        @ApiParam("区域") @RequestBody areaVo: AreaVo,
        @ApiParam("分析选项") @RequestBody analysisOption: AnalysisOption,
    ) = resPack {
        if (analysisOption.startTime == null || analysisOption.endTime == null || analysisOption.area == null)
            throw BizException("参数错误, startTime, endTime, area不能为空")
        val startTime = analysisOption.startTime!!.atZone(ZoneId.systemDefault()).toInstant()
        val endTime = analysisOption.endTime!!.atZone(ZoneId.systemDefault()).toInstant()
        val areaVo = analysisOption.area!!
        val granularity = analysisOption.granularity
        dataAnalysisService.generateMissionDetail(
            Date.from(startTime.atZone(ZoneId.systemDefault()).toInstant()),
            Date.from(endTime.atZone(ZoneId.systemDefault()).toInstant()),
@@ -118,10 +110,10 @@
    @ApiOperation(value = "走航典型隐患区域统计")
    @PostMapping("/report/clueByRiskArea")
    fun generateClueByRiskArea(
        @ApiParam("区域") @RequestBody analysisOption: AnalysisOption,
        @ApiParam("分析选项") @RequestBody analysisOption: AnalysisOption,
    ) = resPack {
        if (analysisOption.startTime == null || analysisOption.endTime == null || analysisOption.area == null)
            throw BizException("参数错误, startTime, endTime, areaVo不能为空")
            throw BizException("参数错误, startTime, endTime, area不能为空")
        val startTime = analysisOption.startTime!!.atZone(ZoneId.systemDefault()).toInstant()
        val endTime = analysisOption.endTime!!.atZone(ZoneId.systemDefault()).toInstant()
        val areaVo = analysisOption.area!!
@@ -143,20 +135,17 @@
    @ApiOperation(value = "叠加融合分析")
    @PostMapping("/report/gridFusion")
    fun generateGridFusion(
        @ApiParam("开始时间") @RequestParam
        @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
        startTime: LocalDateTime,
        @ApiParam("结束时间") @RequestParam
        @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
        endTime: LocalDateTime,
        @ApiParam("需要统计的监测因子", example = "NO2, CO") @RequestParam
        factorTypes: String,
        @ApiParam("区域") @RequestBody areaVo: AreaVo,
        @ApiParam("分析选项") @RequestBody analysisOption: AnalysisOption,
    ) = resPack {
        if (analysisOption.startTime == null || analysisOption.endTime == null || analysisOption.area == null || analysisOption.factorTypes == null)
            throw BizException("参数错误, startTime, endTime, area, factorTypes不能为空")
        val startTime = analysisOption.startTime!!.atZone(ZoneId.systemDefault()).toInstant()
        val endTime = analysisOption.endTime!!.atZone(ZoneId.systemDefault()).toInstant()
        val areaVo = analysisOption.area!!
        val factorTypes = analysisOption.factorTypes!!.map { FactorType.valueOf(it) }
        dataAnalysisService.generateGridFusion(
            factorTypes.split(",").map { FactorType.valueOf(it) },
            factorTypes,
            Date.from(startTime.atZone(ZoneId.systemDefault()).toInstant()),
            Date.from(endTime.atZone(ZoneId.systemDefault()).toInstant()),
            areaVo
src/test/kotlin/com/flightfeather/uav/biz/sourcetrace/SourceTraceControllerTest.kt
@@ -39,16 +39,8 @@
    @Test
    fun autoSourceTrace() {
        val sourceTraceController = SourceTraceController(sceneInfoRep, sourceTraceRep, true)
//        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 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 startTime = LocalDateTime.of(2025, 12, 11, 0, 0, 0).atZone(ZoneId.systemDefault()).toInstant()
        val endTime = LocalDateTime.of(2025, 12, 11, 23, 59, 59).atZone(ZoneId.systemDefault()).toInstant()
        val missions = missionMapper.selectByExample(Example(Mission::class.java).apply {
            createCriteria().andBetween("startTime", startTime, endTime)
        })
@@ -69,57 +61,12 @@
            }
            sourceTraceController.initTask()
        }
//        val missions = missionMapper.selectByExample(Example(Mission::class.java).apply {
//            createCriteria().andEqualTo("deviceType", "0a")
//                .andLessThanOrEqualTo("startTime", "2024-11-07 15:00:00")
//            orderBy("startTime").desc()
//        })
//        missions.forEach { m ->
//            val rtData = realTimeDataService.getSecondData(
//                m?.deviceType,
//                m?.deviceCode,
//                DateUtil.instance.dateToString(m?.startTime, DateUtil.DateStyle.YYYY_MM_DD_HH_MM_SS),
//                DateUtil.instance.dateToString(m?.endTime, DateUtil.DateStyle.YYYY_MM_DD_HH_MM_SS),
//                null,
//                1,
//                10000
//            )
//            rtData.data?.forEach { d ->
//                val rtdVehicle = d.toBaseRealTimeData(RealTimeDataVehicle::class.java)
////            Thread.sleep(500)
//                sourceTraceController.addOneData(rtdVehicle)
//            }
//            sourceTraceController.initTask()
//        }
//        val rtData = realTimeDataService.getSecondData(
//            "0a",
//            "0a0000000001",
//            "2024-08-23 12:30:23",
//            "2024-08-23 15:12:56",
//            null,
//            1,
//            10000
//        )
//        rtData.data?.forEach { d ->
//            val rtdVehicle = d.toBaseRealTimeData(RealTimeDataVehicle::class.java)
////            Thread.sleep(500)
//            sourceTraceController.addOneData(rtdVehicle)
//        }
    }
    @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 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 startTime = LocalDateTime.of(2025, 12, 11, 0, 0, 0).atZone(ZoneId.systemDefault()).toInstant()
        val endTime = LocalDateTime.of(2025, 12, 11, 23, 59, 59).atZone(ZoneId.systemDefault()).toInstant()
        val missions = missionMapper.selectByExample(Example(Mission::class.java).apply {
            createCriteria().andBetween("startTime", startTime, endTime)
        })