2025.10.14
1. 新增数据统计颗粒度选项,可选秒级数据、分钟数据进行数据统计
2. 典型隐患区域统计新增按照污染溯源区域进行分类统计的功能
已修改8个文件
167 ■■■■ 文件已修改
src/main/kotlin/com/flightfeather/uav/biz/report/MissionInventory.kt 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/report/MissionRiskArea.kt 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/report/MissionSummary.kt 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/domain/entity/BaseRealTimeData.kt 29 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/lightshare/service/DataAnalysisService.kt 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/lightshare/service/impl/DataAnalysisServiceImpl.kt 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/lightshare/web/DataAnalysisController.kt 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/kotlin/com/flightfeather/uav/lightshare/service/impl/DataAnalysisServiceImplTest.kt 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/report/MissionInventory.kt
@@ -182,6 +182,7 @@
        mission: Mission,
        pollutedClues: List<PollutedClue?>,
        data: List<BaseRealTimeData>,
        granularity: String,
        minDis: Double = 100.0,
    ): MissionDetail {
        // 创建任务详情对象并复制基本信息
@@ -222,7 +223,7 @@
//        missionDetail.keyScene = relatedScenes
        // 计算环境因子统计数据(平均值、最小值、最大值)
        missionDetail.dataStatistics = data.calDataStatistics()
        missionDetail.dataStatistics = data.calDataStatistics(granularity)
        // 异常数据点数量统计
//        val clues = pollutedClues.filter { it?.msgType == MsgType.PolClue.value }
src/main/kotlin/com/flightfeather/uav/biz/report/MissionRiskArea.kt
@@ -15,6 +15,7 @@
    class ClueByArea {
        var sceneInfo: SceneInfo? = null
        var address: String? = null
        var clueByFactorList: MutableList<ClueByFactor>? = null
    }
@@ -23,7 +24,13 @@
        var clues: MutableList<PollutedClue>? = null
    }
    fun generateClueByRiskArea(keyScenes: List<SceneInfo?>, pollutedClues: List<PollutedClue?>): List<ClueByArea> {
    /**
     * 生成走航典型隐患区域,根据关键场景列表进行分组
     * @param keyScenes 关键场景列表
     * @param pollutedClues 污染线索列表
     * @return 按区域和因子分组的污染线索
     */
    fun generateClueByKeyRiskScene(keyScenes: List<SceneInfo?>, pollutedClues: List<PollutedClue?>): List<ClueByArea> {
        val result = mutableListOf<ClueByArea>()
        
        pollutedClues.forEach { pollutedClue ->
@@ -83,4 +90,47 @@
        
        return result
    }
    /**
     * 生成走航典型隐患区域,根据污染线索溯源地址进行分组
     * @param pollutedClues 污染线索列表
     * @return 按区域和因子分组的污染线索
     */
    fun generateClueByRiskArea(pollutedClues: List<PollutedClue?>): List<ClueByArea> {
        val result = mutableListOf<ClueByArea>()
        pollutedClues.forEach { pollutedClue ->
            if (pollutedClue == null) return@forEach
            val dataList = pollutedClue.pollutedData?.dataList ?: emptyList()
            if (dataList.isEmpty()) return@forEach
            // 按污染溯源地址和因子分组线索
            pollutedClue.pollutedArea?.address?.let { address ->
                var clueByArea = result.find { it.address == address }
                if (clueByArea == null) {
                    clueByArea = ClueByArea().apply {
                        this.address = address
                        this.clueByFactorList = mutableListOf()
                    }
                    result.add(clueByArea)
                }
                val firstFactorType = pollutedClue.pollutedData?.statisticMap?.keys?.first()
                val afType =  AggregatedFactorType.getAFType(firstFactorType)
                val factorName = afType?.des ?: firstFactorType?.des
                var clueByFactor = clueByArea.clueByFactorList?.find { it.factor == factorName }
                if (clueByFactor == null) {
                    clueByFactor = ClueByFactor().apply {
                        this.factor = factorName
                        this.clues = mutableListOf()
                    }
                    clueByArea.clueByFactorList?.add(clueByFactor)
                }
                clueByFactor.clues?.add(pollutedClue)
            }
        }
        return result
    }
}
src/main/kotlin/com/flightfeather/uav/biz/report/MissionSummary.kt
@@ -106,7 +106,7 @@
        val probByFactor = clueRes.third
        // 7. 从异常所在地区和溯源的场景中统计聚焦区域
        val focusRegion = calFocusRegion(clues)
        val focusRegion = calFocusRegion(clues, area)
        // 8. 构建并返回统计结果
        return Summary(
@@ -141,17 +141,17 @@
            }
        }
        val probByFactor = probByFactorMap.entries.map {
            val per = if(probCount == 0) .0 else round(it.value.toDouble() / probCount * 100) / 100
            val per = if (probCount == 0) .0 else round(it.value.toDouble() / probCount * 100) / 100
            Triple(it.key.des, it.value, per)
        }
        return Triple(probCount, highRiskSceneCount, probByFactor)
    }
    private fun calFocusRegion(clues: List<PollutedClue?>): List<String> {
    private fun calFocusRegion(clues: List<PollutedClue?>, area: AreaVo): List<String> {
        // 统计每个区域或场景出现的次数
        val focusArea = mutableMapOf<String, Int>()
        val focusScene = mutableMapOf<String, Int>()
        clues.forEach { c->
        clues.forEach { c ->
            if (c?.msgType == MsgType.PolClue.value) {
                if (!c.pollutedArea?.address.isNullOrBlank()) {
                    if (focusArea.containsKey(c.pollutedArea?.address)) {
@@ -160,7 +160,7 @@
                        focusArea[c.pollutedArea?.address!!] = 1
                    }
                }
                c.pollutedSource?.sceneList?.forEach { s->
                c.pollutedSource?.sceneList?.forEach { s ->
                    if (s.name != null) {
                        if (focusScene.containsKey(s.name!!)) {
                            focusScene[s.name!!] = focusScene[s.name!!]!! + 1
@@ -171,7 +171,18 @@
                }
            }
        }
        return focusArea.entries.sortedByDescending { it.value }.map { it.key }.take(FOCUS_AREA_COUNT) +
                focusScene.entries.sortedByDescending { it.value }.map { it.key }.take(FOCUS_AREA_COUNT)
        return focusArea.entries.sortedByDescending { it.value }
            .filter {
                // 仅统计包含区县名称的区域
                area.districtName ?: return@filter true
                return@filter it.key.contains(area.districtName!!)
            }
            .map { it.key }.take(FOCUS_AREA_COUNT) +
                focusScene.entries.sortedByDescending { it.value }
                    .filter {
                        // 仅统计包含区县名称的区域
                        area.districtName ?: return@filter true
                        return@filter it.key.contains(area.districtName!!)
                    }.map { it.key }.take(FOCUS_AREA_COUNT)
    }
}
src/main/kotlin/com/flightfeather/uav/domain/entity/BaseRealTimeData.kt
@@ -10,6 +10,7 @@
import java.math.BigDecimal
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.temporal.ChronoUnit
import java.util.*
import javax.persistence.Column
import javax.persistence.GeneratedValue
@@ -332,11 +333,33 @@
/**
 * 计算实时监测数据列表的统计信息
 * 为每种环境因子计算最小值、最大值和平均值
 *
 * @param granularity 数据颗粒度,可选值为SECOND, MINUTE, HOUR, 默认MINUTE
 * @return 包含各环境因子统计信息的FactorStatistics列表
 *         每个FactorStatistics对象包含因子类型、最小值、最大值和平均值
 */
fun List<BaseRealTimeData>.calDataStatistics(): List<FactorStatistics> {
fun List<BaseRealTimeData>.calDataStatistics(granularity: String): List<FactorStatistics> {
    // 检查颗粒度是否有效
    if (granularity !in listOf("SECOND", "MINUTE", "HOUR")) {
        throw IllegalArgumentException("无效的颗粒度参数,可选值为SECOND, MINUTE, HOUR")
    }
    val groupedData = when (granularity) {
        "SECOND" -> this
        "MINUTE" -> groupBy { it.dataTime?.toInstant()?.truncatedTo(ChronoUnit.MINUTES) }.mapValues {
            it.value.avg().apply {
                dataTime = Date.from(it.key)
                createTime = dataTime
            }
        }.values.toList()
        "HOUR" -> groupBy { it.dataTime?.toInstant()?.truncatedTo(ChronoUnit.HOURS) }.mapValues {
            it.value.avg().apply {
                dataTime = Date.from(it.key)
                createTime = dataTime
            }
        }.values.toList()
        else -> throw IllegalArgumentException("无效的颗粒度参数,可选值为SECOND, MINUTE, HOUR")
    }
    // 初始化各环境因子的统计对象列表
    val statistics = mutableListOf<FactorStatistics>()
@@ -357,7 +380,7 @@
    ).forEach { statistics.add(FactorStatistics(it)) }
    // 计算平均值并同时更新各因子的最小值和最大值
    val avgData = avg { item ->
    val avgData = groupedData.avg { item ->
        // 更新每个因子的最小和最大值
        statistics[0].updateMinAndMaxValue(item.no2)
        statistics[1].updateMinAndMaxValue(item.co)
src/main/kotlin/com/flightfeather/uav/lightshare/service/DataAnalysisService.kt
@@ -85,24 +85,30 @@
     * @param areaVo 区域参数
     * @return 任务详情列表,每个元素包含任务完整信息、场景数据和统计结果
     */
    fun generateMissionDetail(startTime: Date, endTime: Date, areaVo: AreaVo): List<MissionDetail>
    fun generateMissionDetail(startTime: Date, endTime: Date, areaVo: AreaVo,granularity: String?): List<MissionDetail>
    /**
     * 生成走航任务详情(按任务编号筛选)
     * 根据任务编号查询并生成详细的任务报告,包含任务完整信息、场景数据和统计结果
     * @param missionCode 任务编号,用于唯一标识特定的走航任务
     * @param granularity 数据颗粒度,可选值为SECOND, MINUTE, HOUR, 默认MINUTE
     * @return 任务详情对象,包含任务完整信息、场景数据和统计结果
     */
    fun generateMissionDetail(missionCode: String): MissionDetail
    fun generateMissionDetail(missionCode: String, granularity: String?): MissionDetail
    /**
     * 获取走航任务详情(直接处理任务数据)
     * 处理已有的任务、污染线索和实时数据,生成详细任务报告
     * @param keyScenes 关键场景列表,用于分析走航是否经过该区域
     * @param missionCluesData 包含任务、污染线索和实时数据的Triple列表
     * @param granularity 数据颗粒度,可选值为SECOND, MINUTE, HOUR, 默认MINUTE
     * @return 任务详情列表
     */
    fun generateMissionDetail(keyScenes: List<SceneInfo?>, missionCluesData: List<Triple<Mission, List<PollutedClue?>, List<BaseRealTimeData>>>): List<MissionDetail>
    fun generateMissionDetail(
        keyScenes: List<SceneInfo?>,
        missionCluesData: List<Triple<Mission, List<PollutedClue?>, List<BaseRealTimeData>>>,
        granularity: String,
    ): List<MissionDetail>
    fun generateClueByRiskArea(startTime: Date, endTime: Date, areaVo: AreaVo): List<MissionRiskArea.ClueByArea>
src/main/kotlin/com/flightfeather/uav/lightshare/service/impl/DataAnalysisServiceImpl.kt
@@ -23,7 +23,6 @@
import com.flightfeather.uav.socket.eunm.FactorType
import com.flightfeather.uav.socket.sender.MsgType
import org.springframework.stereotype.Service
import tk.mybatis.mapper.entity.Example
import java.util.*
/**
@@ -173,11 +172,17 @@
     * @param startTime 查询起始时间(包含)
     * @param endTime 查询结束时间(包含)
     * @param areaVo 区域参数,包含省、市、区编码
     * @param granularity 数据颗粒度,可选值为SECOND, MINUTE, HOUR, 默认MINUTE
     * @return 任务详情列表,每个元素包含任务完整信息、污染线索及实时监测数据
     * @see MissionRep.findByAreaAndTime 区域时间筛选数据源
     * @see realTimeDataRep.fetchData 实时数据获取接口
     */
    override fun generateMissionDetail(startTime: Date, endTime: Date, areaVo: AreaVo): List<MissionInventory.MissionDetail> {
    override fun generateMissionDetail(
        startTime: Date,
        endTime: Date,
        areaVo: AreaVo,
        granularity: String?,
    ): List<MissionInventory.MissionDetail> {
        val missionCluesData = missionRep.findByAreaAndTime(areaVo, startTime, endTime).filterNotNull().map {
            Triple(
                it,
@@ -192,10 +197,10 @@
                SceneType.TYPE21.value
            )
        )
        return generateMissionDetail(keyScenes, missionCluesData)
        return generateMissionDetail(keyScenes, missionCluesData, granularity ?: "MINUTE")
    }
    override fun generateMissionDetail(missionCode: String): MissionInventory.MissionDetail {
    override fun generateMissionDetail(missionCode: String, granularity: String?): MissionInventory.MissionDetail {
        val mission = missionRep.findOne(missionCode) ?: throw BizException("任务不存在")
        val missionClues = sourceTraceRep.fetchList(mission.deviceCode, mission.startTime, mission.endTime, MsgType.PolClue) as List<PollutedClue?>
        val realTimeData = realTimeDataRep.fetchData(mission)
@@ -206,7 +211,7 @@
                SceneType.TYPE21.value
            )
        )
        return MissionInventory().generateMissionDetail(keyScenes, mission, missionClues, realTimeData)
        return MissionInventory().generateMissionDetail(keyScenes, mission, missionClues, realTimeData, granularity ?: "MINUTE")
    }
    /**
@@ -220,9 +225,10 @@
    override fun generateMissionDetail(
        keyScenes: List<SceneInfo?>,
        missionCluesData: List<Triple<Mission, List<PollutedClue?>, List<BaseRealTimeData>>>,
        granularity: String,
    ): List<MissionInventory.MissionDetail> {
        return missionCluesData.map {
            MissionInventory().generateMissionDetail(keyScenes, it.first, it.second, it.third)
            MissionInventory().generateMissionDetail(keyScenes, it.first, it.second, it.third, granularity)
        }
    }
@@ -237,14 +243,14 @@
            val clue = sourceTraceRep.fetchList(it.deviceCode, it.startTime, it.endTime, MsgType.PolClue) as List<PollutedClue?>
            clues.addAll(clue)
        }
        val keyScenes = sceneInfoRep.findBySceneTypes(
            listOf(
                SceneType.TYPE19.value,
                SceneType.TYPE20.value,
                SceneType.TYPE21.value
            )
        )
        return generateClueByRiskArea(keyScenes, clues)
//        val keyScenes = sceneInfoRep.findBySceneTypes(
//            listOf(
//                SceneType.TYPE19.value,
//                SceneType.TYPE20.value,
//                SceneType.TYPE21.value
//            )
//        )
        return MissionRiskArea().generateClueByRiskArea(clues)
    }
    override fun generateClueByRiskArea(missionCode: String): List<MissionRiskArea.ClueByArea> {
@@ -264,7 +270,7 @@
        keyScenes: List<SceneInfo?>,
        pollutedClues: List<PollutedClue?>,
    ): List<MissionRiskArea.ClueByArea> {
        return MissionRiskArea().generateClueByRiskArea(keyScenes, pollutedClues)
        return MissionRiskArea().generateClueByKeyRiskScene(keyScenes, pollutedClues)
    }
    override fun generateGridFusion(
src/main/kotlin/com/flightfeather/uav/lightshare/web/DataAnalysisController.kt
@@ -94,12 +94,14 @@
        @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,
    ) = resPack {
        dataAnalysisService.generateMissionDetail(
            Date.from(startTime.atZone(ZoneId.systemDefault()).toInstant()),
            Date.from(endTime.atZone(ZoneId.systemDefault()).toInstant()),
            areaVo
            areaVo,granularity
        )
    }
@@ -107,7 +109,9 @@
    @GetMapping("/report/missionDetail/one")
    fun generateOneMissionDetail(
        @ApiParam("任务编号") @RequestParam missionCode: String,
    ) = resPack { dataAnalysisService.generateMissionDetail(missionCode) }
        @ApiParam("数据颗粒度", allowableValues = "SECOND, MINUTE, HOUR") @RequestParam(required = false)
        granularity: String?,
    ) = resPack { dataAnalysisService.generateMissionDetail(missionCode, granularity) }
    @ApiOperation(value = "走航典型隐患区域统计")
    @PostMapping("/report/clueByRiskArea")
src/test/kotlin/com/flightfeather/uav/lightshare/service/impl/DataAnalysisServiceImplTest.kt
@@ -45,7 +45,7 @@
    @Test
    fun generateMissionDetail() {
        val res = dataAnalysisService.generateMissionDetail(startTime, endTime, areaVo)
        val res = dataAnalysisService.generateMissionDetail(startTime, endTime, areaVo, "MINUTE")
        println(res)
    }