riku
2025-08-28 3bb4fb15c664d29d179083698fdad35a661b1d7f
2025.8.28
1. 添加走航季度报告相关统计功能(待完成)
已修改10个文件
已添加4个文件
551 ■■■■■ 文件已修改
pom.xml 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/mission/MissionUtil.kt 43 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/report/MissionClue.kt 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/report/MissionInventory.kt 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/report/MissionSummary.kt 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/biz/report/UnderwaySeasonReport.kt 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/common/file/Docx4jGenerator.kt 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/common/utils/CommonUtil.kt 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/common/utils/FileUtil.kt 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/common/utils/ObjToMapUtil.kt 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/domain/repository/MissionRep.kt 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/kotlin/com/flightfeather/uav/lightshare/service/impl/MissionServiceImpl.kt 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/templates/underway_season_report.docx 补丁 | 查看 | 原始文档 | blame | 历史
src/test/kotlin/com/flightfeather/uav/lightshare/service/impl/MissionServiceImplTest.kt 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pom.xml
@@ -267,6 +267,30 @@
            <artifactId>commons-lang3</artifactId>
            <version>3.17.0</version>
        </dependency>
        <dependency>
            <groupId>org.docx4j</groupId>
            <artifactId>docx4j-core</artifactId>
            <version>11.4.9</version> <!-- ä½¿ç”¨æœ€æ–°ç¨³å®šç‰ˆ -->
        </dependency>
        <!-- å¦‚需处理图片/表格等高级功能,可添加 -->
        <dependency>
            <groupId>org.docx4j</groupId>
            <artifactId>docx4j-ImportXHTML</artifactId>
            <version>11.4.8</version>
        </dependency>
        <!-- MockK å•元测试库(用于 Kotlin) -->
        <dependency>
            <groupId>io.mockk</groupId>
            <artifactId>mockk</artifactId>
            <version>1.14.5</version> <!-- ä½¿ç”¨æœ€æ–°ç¨³å®šç‰ˆ -->
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>RELEASE</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
src/main/kotlin/com/flightfeather/uav/biz/mission/MissionUtil.kt
@@ -1,7 +1,9 @@
package com.flightfeather.uav.biz.mission
import com.flightfeather.uav.common.net.AMapService
import com.flightfeather.uav.common.utils.MapUtil
import com.flightfeather.uav.domain.entity.BaseRealTimeData
import com.flightfeather.uav.domain.entity.avg
/**
 * èµ°èˆªä»»åŠ¡è®¡ç®—å·¥å…·
@@ -15,18 +17,43 @@
     */
    fun calKilometres(data: List<BaseRealTimeData>): Double {
        var distance = .0
        for (i in 1 until data.size) {
            val a = data[i - 1]
            val b = data[i]
            if (a.longitude == null || a.latitude == null || b.longitude == null || b.latitude == null) continue
        var lastValidPoint: BaseRealTimeData? = null
        for (point in data) {
            // è·³è¿‡æ— æ•ˆç‚¹
            if (point.longitude == null || point.latitude == null) continue
            // å¦‚果存在上一个有效点,则计算距离
            lastValidPoint?.let { prevPoint ->
            distance += MapUtil.getDistance(
                a.longitude!!.toDouble(),
                a.latitude!!.toDouble(),
                b.longitude!!.toDouble(),
                b.latitude!!.toDouble()
                    prevPoint.longitude!!.toDouble(),
                    prevPoint.latitude!!.toDouble(),
                    point.longitude!!.toDouble(),
                    point.latitude!!.toDouble()
            )
        }
            // æ›´æ–°ä¸Šä¸€ä¸ªæœ‰æ•ˆç‚¹
            lastValidPoint = point
        }
        return distance
    }
    /**
     * æ ¹æ®è½¨è¿¹ç‚¹è®¡ç®—所属区域(乡镇+街道)
     * @param data èµ°èˆªè½¨è¿¹ç‚¹åˆ—表
     * @return åŒºåŸŸåç§°ï¼ˆä¹¡é•‡+街道),若无法计算则返回null
     */
    @Suppress("UNCHECKED_CAST")
    fun calRegion(data: List<BaseRealTimeData>): String? {
        // è®¡ç®—所有轨迹点的平均坐标(中心点)
        val avgData = data.avg()
        val pair = avgData.longitude?.toDouble() to avgData.latitude?.toDouble()
        // è‹¥å¹³å‡åæ ‡æ— æ•ˆåˆ™è¿”回null
        if (pair.first == null || pair.second == null) return null
        // å°†WGS84坐标转换为GCJ02坐标后进行逆地理编码获取地址信息
        val address = AMapService.reGeo(MapUtil.wgs84ToGcj02(pair as Pair<Double, Double>))
        // è¿”回乡镇和街道名称组合
        return address.township + address.street
    }
}
src/main/kotlin/com/flightfeather/uav/biz/report/MissionClue.kt
@@ -8,7 +8,7 @@
 */
class MissionClue {
    inner class Clue{
    class Clue{
        var factor:FactorType?=null
        var riskRegion:String?=null
    }
src/main/kotlin/com/flightfeather/uav/biz/report/MissionInventory.kt
@@ -14,7 +14,7 @@
class MissionInventory {
    // èµ°èˆªæ¸…单信息
    inner class MissionInfo : Mission() {
    class MissionInfo : Mission() {
        // é¦–要污染物
        var mainFactor: String? = null
@@ -26,7 +26,7 @@
    }
    // èµ°èˆªè¯¦æƒ…信息
    inner class MissionDetail : Mission() {
    class MissionDetail : Mission() {
        var keyScene: List<SceneInfo>? = null
        var dataStatistics: List<FactorStatistics>? = null
src/main/kotlin/com/flightfeather/uav/biz/report/MissionSummary.kt
@@ -1,16 +1,29 @@
package com.flightfeather.uav.biz.report
import com.flightfeather.uav.biz.sourcetrace.model.PollutedClue
import com.flightfeather.uav.domain.entity.Mission
import com.flightfeather.uav.domain.repository.MissionRep
import com.flightfeather.uav.lightshare.bean.AreaVo
import com.flightfeather.uav.socket.eunm.FactorType
import com.flightfeather.uav.socket.sender.MsgType
import org.springframework.stereotype.Component
import java.util.*
import kotlin.math.round
/**
 * èµ°èˆªä»»åŠ¡æ±‡æ€»
 * @date 2025/8/22
 * @author feiyu02
 */
@Component
class MissionSummary {
class MissionSummary() {
    inner class Summary(
    data class Summary(
        // æ±‡æ€»å‘¨æœŸå¼€å§‹æ—¶é—´
        val startTime: Date,
        // æ±‡æ€»å‘¨æœŸç»“束时间
        val endTime: Date,
        // èµ°èˆªåŒºåŸŸä¿¡æ¯
        val area: AreaVo,
        // èµ°èˆªæ¬¡æ•°
        val count: Int,
        // æ€»é‡Œç¨‹æ•°ï¼ˆå…¬é‡Œï¼‰
@@ -27,7 +40,101 @@
        val probByFactor:List<Triple<String, Int, Double>>
    )
    fun execute() {
    /**
     * æ ¹æ®æ—¶é—´èŒƒå›´æŸ¥è¯¢èµ°èˆªä»»åŠ¡å¹¶ç”Ÿæˆç»Ÿè®¡ç»“æžœ
     * @param startTime ç»Ÿè®¡å¼€å§‹æ—¶é—´
     * @param endTime ç»Ÿè®¡ç»“束时间
     * @param missions èµ°èˆªä»»åŠ¡åˆ—è¡¨ï¼ˆå¤–éƒ¨ä¼ å…¥ï¼Œé¿å…é‡å¤æŸ¥è¯¢ï¼‰
     * @param clues æ±¡æŸ“线索列表(用于问题统计)
     * @return èµ°èˆªä»»åŠ¡ç»Ÿè®¡ç»“æžœSummary
     */
    fun execute(startTime: Date, endTime: Date, missions: List<Mission?>, clues: List<PollutedClue?>): Summary {
        // 1. æŸ¥è¯¢æŒ‡å®šæ—¶é—´èŒƒå›´å†…的走航任务
        if (missions.isEmpty()) {
            return Summary(
                startTime = startTime,
                endTime = endTime,
                area = AreaVo(), // ç©ºä»»åŠ¡æ—¶è¿”å›žé»˜è®¤åŒºåŸŸä¿¡æ¯
                count = 0,
                kilometres = 0.0,
                regionList = emptyList(),
                countByDegree = emptyList(),
                probCount = 0,
                highRiskSceneCount = 0,
                probByFactor = emptyList()
            )
        }
        // 2. åŸºç¡€ç»Ÿè®¡ï¼šæ€»ä»»åŠ¡æ•°ã€æ€»é‡Œç¨‹
        val totalCount = missions.size
        val totalKilometres = missions.sumOf { it?.kilometres?.toDouble() ?: 0.0 }
        // 3. åŒºåŸŸä¿¡æ¯ï¼šå–首个任务的行政区划信息(如存在多区域可扩展为合并逻辑)
        val firstMission = missions.first()
        val area = AreaVo().apply {
            provinceCode = firstMission?.provinceCode
            provinceName = firstMission?.provinceName
            cityCode = firstMission?.cityCode
            cityName = firstMission?.cityName
            districtCode = firstMission?.districtCode
            districtName = firstMission?.districtName
            townCode = firstMission?.townCode
            townName = firstMission?.townName
        }
        // 4. æ¶‰åŠåŒºåŸŸåˆ—表:去重收集所有任务的region字段
        val regionList = missions.mapNotNull { it?.region }.distinct()
        // 5. ç©ºæ°”质量等级分布:按pollutionDegree分组统计次数及占比
        val degreeGroups = missions
            .filter { it?.pollutionDegree != null } // è¿‡æ»¤æ— æ•ˆç­‰çº§
            .groupBy { it?.pollutionDegree!! }
            .mapValues { it.value.size }
        val countByDegree = degreeGroups.map { (degree, count) ->
            Triple(degree, count, count.toDouble() / totalCount)
        }
        // 6. é—®é¢˜ç›¸å…³ç»Ÿè®¡ï¼ˆç¤ºä¾‹ï¼šæ­¤å¤„假设需关联其他表,暂返回0,实际需根据业务补充)
        val clueRes = calClue(clues)
        val probCount = clueRes.first // éœ€å…³è”问题表统计
        val highRiskSceneCount = clueRes.second // éœ€å…³è”场景表统计
        val probByFactor = clueRes.third
        // 7. æž„建并返回统计结果
        return Summary(
            startTime = startTime,
            endTime = endTime,
            area = area,
            count = totalCount,
            kilometres = totalKilometres,
            regionList = regionList,
            countByDegree = countByDegree,
            probCount = probCount,
            highRiskSceneCount = highRiskSceneCount,
            probByFactor = probByFactor
        )
    }
    private fun calClue(clues: List<PollutedClue?>): Triple<Int, Int, List<Triple<String, Int, Double>>> {
        var probCount = 0 // éœ€å…³è”问题表统计
        var highRiskSceneCount = 0 // éœ€å…³è”场景表统计
        val probByFactorMap = mutableMapOf<FactorType, Int>() // éœ€å…³è”因子表统计
        clues.forEach { c ->
            if (c?.msgType == MsgType.PolClue.value) {
                c.pollutedSource?.sceneList?.size?.let { s -> highRiskSceneCount += s }
                c.pollutedData?.statisticMap?.keys?.forEach { k ->
                    probCount++
                    if (!probByFactorMap.containsKey(k)) {
                        probByFactorMap[k] = 0
                    }
                    probByFactorMap[k] = probByFactorMap[k]!! + 1
                }
            }
        }
        val probByFactor = probByFactorMap.entries.map {
            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)
    }
}
src/main/kotlin/com/flightfeather/uav/biz/report/UnderwaySeasonReport.kt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
//package com.flightfeather.uav.biz.report
//
//import com.flightfeather.uav.common.file.Docx4jGenerator
//import com.flightfeather.uav.common.utils.DateUtil
//import com.flightfeather.uav.common.utils.ObjToMapUtil
//import com.flightfeather.uav.lightshare.bean.AreaVo
//import org.springframework.stereotype.Component
//import java.time.LocalDateTime
//import java.time.format.DateTimeFormatter
//import java.util.*
//
///**
// * èµ°èˆªå­£åº¦æŠ¥å‘Š
// * @date 2025/8/28 10:02
// * @author feiyu
// */
//@Component
//class UnderwaySeasonReport {
//
//    fun generate() {
//        // å‡†å¤‡æ•°æ®æ¨¡åž‹
//        val summary = MissionSummary.Summary(
//            time = "2025年第三季度(7-9月)",
//            area = AreaVo().apply {
//                // å›ºå®šè¡Œæ”¿åŒºåˆ’信息
//                provinceCode = "31"
//                provinceName = "上海市"
//                cityCode = "310100"
//                cityName = "上海市"
//                districtCode = "310101"
//                districtName = "黄浦区"
//                townCode = "310101001"
//                townName = "外滩街道"
//                // å›ºå®šåŒºåŸŸç¼–码和名称
//                areaCode = "SH-HP-001"
//                area = "黄浦区走航监测区域"
//                // å›ºå®šç®¡ç†å…¬å¸ä¿¡æ¯
//                managementCompanyId = "MC001"
//                managementCompany = "上海环境监测有限公司"
//                // å›ºå®šåœºæ™¯ç±»åž‹ID
//                sceneTypeId = "ST001"
//            },
//            count = 34,
//            kilometres = 256.8,
//            regionList = listOf("工业园区A", "化工区B", "居民区C"),
//            countByDegree = listOf(
//                Triple("优", 15, 0.45),    // ä¼˜ï¼š15次,占比45%
//                Triple("良", 12, 0.36),    // è‰¯ï¼š12次,占比36%
//                Triple("轻度污染", 5, 0.15),// è½»åº¦æ±¡æŸ“:5次,占比15%
//                Triple("中度污染", 2, 0.04) // ä¸­åº¦æ±¡æŸ“:2次,占比4%
//            ),
//            probCount = 50,
//            highRiskSceneCount = 8,
//            probByFactor = listOf(
//                Triple("VOCs", 28, 0.56),  // VOCs:28次,占比56%
//                Triple("PM2.5", 12, 0.24), // PM2.5:12次,占比24%
//                Triple("NO2", 7, 0.14),    // NO2:7次,占比14%
//                Triple("SO2", 3, 0.06)     // SO2:3次,占比6%
//            )
//        )
//        val dataModel = ObjToMapUtil.objectToMap(summary)
//
//        // ç”ŸæˆWord文件
//        val timeStr = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now())
//        Docx4jGenerator.generate(
//            templatePath = "underway_season_report.docx",
//            outputPath = "E:/idea/underway-spring/src/main/resources/templates/mission_summary_${timeStr}.docx",
//            dataModel = dataModel
//        )
//
//    }
//}
src/main/kotlin/com/flightfeather/uav/common/file/Docx4jGenerator.kt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,114 @@
package com.flightfeather.uav.common.file
import freemarker.template.Configuration
import freemarker.template.Template
import org.docx4j.openpackaging.packages.WordprocessingMLPackage
import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.OutputStreamWriter
import java.nio.charset.StandardCharsets
/**
 * Word文件生成器(基于Docx4j + FreeMarker)
 * @date 2025/8/28 09:29
 * @author feiyu
 */
class Docx4jGenerator(
    private val templatePath: String,
    private val freemarkerConfig: Configuration = defaultFreemarkerConfig()
) {
    private var wordMLPackage: WordprocessingMLPackage? = null
    private var mainDocumentPart: MainDocumentPart? = null
    /**
     * åˆ›å»ºWord文档包
     */
    fun loadTemplate(): Docx4jGenerator {
        wordMLPackage = WordprocessingMLPackage.createPackage()
        mainDocumentPart = wordMLPackage?.mainDocumentPart
        return this
    }
    /**
     * ä½¿ç”¨FreeMarker填充模板数据
     */
    fun fillData(dataModel: Map<String, Any>): Docx4jGenerator {
        val template = freemarkerConfig.getTemplate(templatePath.substringAfterLast("/"))
        val xmlContent = renderTemplate(template, dataModel)
        mainDocumentPart?.unmarshal(ByteArrayInputStream(xmlContent.toByteArray(StandardCharsets.UTF_8)))
        return this
    }
    /**
     * æ·»åŠ å›¾ç‰‡åˆ°Word文档
     * @param imagePath å›¾ç‰‡è·¯å¾„
     * @param width å®½åº¦(像素)
     * @param height é«˜åº¦(像素)
     * @param paragraphId æ®µè½ID,指定图片插入位置
     */
    fun addImage(imagePath: String, width: Int, height: Int, paragraphId: String): Docx4jGenerator {
        // å®žçŽ°å›¾ç‰‡æ·»åŠ é€»è¾‘
        return this
    }
    /**
     * æ·»åŠ è¡¨æ ¼åˆ°Word文档
     * @param data è¡¨æ ¼æ•°æ®
     * @param paragraphId æ®µè½ID,指定表格插入位置
     */
    fun addTable(data: List<List<String>>, paragraphId: String): Docx4jGenerator {
        // å®žçŽ°è¡¨æ ¼æ·»åŠ é€»è¾‘
        return this
    }
    /**
     * ä¿å­˜ç”Ÿæˆçš„Word文件
     * @param outputPath è¾“出文件路径
     */
    fun save(outputPath: String) {
        wordMLPackage?.save(File(outputPath))
    }
    /**
     * ä½¿ç”¨FreeMarker渲染模板
     */
    private fun renderTemplate(template: Template, dataModel: Map<String, Any>): String {
        val outputStream = ByteArrayOutputStream()
        val writer = OutputStreamWriter(outputStream, StandardCharsets.UTF_8)
        template.process(dataModel, writer)
        writer.flush()
        return outputStream.toString(StandardCharsets.UTF_8.name())
    }
    companion object {
        /**
         * é»˜è®¤FreeMarker配置
         */
        fun defaultFreemarkerConfig(): Configuration {
            val config = Configuration(Configuration.VERSION_2_3_31)
            config.defaultEncoding = "UTF-8"
            config.setClassForTemplateLoading(Docx4jGenerator::class.java, "/templates")
            return config
        }
        /**
         * ç®€åŒ–调用的静态方法
         */
        fun generate(
            templatePath: String,
            outputPath: String,
            dataModel: Map<String, Any>,
            config: Configuration = defaultFreemarkerConfig()
        ) {
            Docx4jGenerator(templatePath, config)
                .loadTemplate()
                .fillData(dataModel)
                .save(outputPath)
        }
    }
}
src/main/kotlin/com/flightfeather/uav/common/utils/CommonUtil.kt
src/main/kotlin/com/flightfeather/uav/common/utils/FileUtil.kt
@@ -65,12 +65,12 @@
//            }
            //新建输出流
            fw = FileWriter(file, true)
            bw = BufferedWriter(fw)
            bw = fw?.let { BufferedWriter(it) }
        }
        //第一次写文档时初始化输出流
        if (bw == null || fw == null) {
            fw = FileWriter(file, true)
            bw = BufferedWriter(fw)
            bw = fw?.let { BufferedWriter(it) }
        }
        bw?.run {
src/main/kotlin/com/flightfeather/uav/common/utils/ObjToMapUtil.kt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
package com.flightfeather.uav.common.utils
import kotlin.reflect.KProperty1
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.jvm.isAccessible
/**
 * å¯¹è±¡è½¬æ¢ä¸ºé”®å¯¹å€¼map结构
 * @date 2025/8/28 10:19
 * @author feiyu
 */
object ObjToMapUtil {
    /**
     * å°†å¯¹è±¡è½¬æ¢ä¸ºMap结构(支持嵌套对象递归转换)
     * @param obj å¾…转换的对象(非空)
     * @return åŒ…含对象属性名和值的Map,嵌套对象会转换为Map,集合/数组会递归处理元素
     */
    fun <T : Any> objectToMap(obj: T): Map<String, Any> {
        return obj::class.declaredMemberProperties.associate { property ->
            property.isAccessible = true
            val value = try {
                (property as KProperty1<T, *>).get(obj) } catch (e: Exception) { null }
            property.name to processValue(value)
        }
    }
    /**
     * é€’归处理属性值,将嵌套对象转换为Map,集合/数组递归处理元素
     */
    private fun processValue(value: Any?): Any {
        return when {
            value == null -> ""
            // å¤„理集合类型(List/Set)
            value is Collection<*> -> value.map { processValue(it) }
            // å¤„理Map类型(转换值)
            value is Map<*, *> -> value.mapValues { processValue(it.value) }
            // å¤„理数组类型
            value.javaClass.isArray -> (value as Array<*>).map { processValue(it) }
            // å¤„理自定义对象(递归转换)
            isCustomObject(value) -> objectToMap(value)
            // åŸºç¡€ç±»åž‹ï¼ˆInt/String/Boolean等)直接返回
            else -> value
        }
    }
    /**
     * åˆ¤æ–­æ˜¯å¦ä¸ºéœ€è¦é€’归转换的自定义对象
     */
    private fun isCustomObject(value: Any): Boolean {
        val clazz = value::class.java
        return !clazz.isPrimitive &&
                !clazz.isEnum &&
                value !is String &&
                value !is Number &&
                value !is Boolean &&
                value !is Char &&
                value !is Collection<*> &&
                value !is Map<*, *> &&
                !clazz.isArray
    }
}
src/main/kotlin/com/flightfeather/uav/domain/repository/MissionRep.kt
@@ -3,17 +3,45 @@
import com.flightfeather.uav.domain.entity.Mission
import com.flightfeather.uav.domain.mapper.MissionMapper
import org.springframework.stereotype.Repository
import tk.mybatis.mapper.entity.Example
import java.util.*
@Repository
class MissionRep(
    private val missionMapper: MissionMapper,
) {
    /**
     * æ ¹æ®ä»»åŠ¡ç¼–ç æŸ¥è¯¢å•ä¸ªèµ°èˆªä»»åŠ¡
     * @param missionCode ä»»åŠ¡ç¼–ç ï¼ˆä¸»é”®ï¼‰
     * @return æŸ¥è¯¢åˆ°çš„任务对象,若不存在则返回null
     */
    fun findOne(missionCode:String?): Mission? {
        return missionMapper.selectByPrimaryKey(missionCode)
    }
    /**
     * æ ¹æ®ä»»åŠ¡å¯¹è±¡å±žæ€§æ¡ä»¶æŸ¥è¯¢ä»»åŠ¡åˆ—è¡¨
     * @param mission åŒ…含查询条件的任务对象(如设备类型、区域等)
     * @return ç¬¦åˆæ¡ä»¶çš„任务列表
     */
    fun findList(mission: Mission): List<Mission?> {
        return missionMapper.select(mission)
    }
    /**
     * æ ¹æ®æ—¶é—´èŒƒå›´æŸ¥è¯¢èµ°èˆªä»»åŠ¡åˆ—è¡¨
     * @param startTime æŸ¥è¯¢èµ·å§‹æ—¶é—´ï¼ˆåŒ…含)
     * @param endTime æŸ¥è¯¢ç»“束时间(包含)
     * @return ç¬¦åˆæ—¶é—´æ¡ä»¶çš„走航任务列表
     */
    fun findByTimeRange(startTime: Date, endTime: Date): List<Mission?> {
        // ä½¿ç”¨tk.mybatis的Example构建查询条件
        return missionMapper.selectByExample(Example(Mission::class.java).apply {
            // åˆ›å»ºæŸ¥è¯¢æ¡ä»¶ï¼šstartTime字段在指定时间范围内
            createCriteria().andBetween("startTime", startTime, endTime)
                // è¿‡æ»¤æŽ‰æ²¡æœ‰ç»“束时间的任务(确保任务已完成)
                .andIsNotNull("endTime")
        })
    }
}
src/main/kotlin/com/flightfeather/uav/lightshare/service/impl/MissionServiceImpl.kt
@@ -93,6 +93,8 @@
        val mission = missionRep.findOne(missionCode) ?: throw BizException("走航任务不存在")
        val data = realTimeDataRep.fetchData(mission)
        mission.kilometres = MissionUtil.calKilometres(data).toFloat()
        // todo: è®¡ç®—走航任务所在中心区域
        mission.region = MissionUtil.calRegion(data)
        return updateMission(mission)
    }
}
src/main/resources/templates/underway_season_report.docx
Binary files differ
src/test/kotlin/com/flightfeather/uav/lightshare/service/impl/MissionServiceImplTest.kt
@@ -2,22 +2,30 @@
import com.flightfeather.uav.biz.FactorFilter
import com.flightfeather.uav.biz.report.MissionReport
import com.flightfeather.uav.common.exception.BizException
import com.flightfeather.uav.domain.repository.MissionRep
import com.flightfeather.uav.lightshare.service.MissionService
import org.junit.Test
import org.junit.Assert.*
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.junit4.SpringRunner
import javax.servlet.http.HttpServletResponse
@RunWith(SpringRunner::class)
@SpringBootTest
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.runner.RunWith
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
class MissionServiceImplTest {
    @Autowired
    lateinit var missionService: MissionService
    private var missionRep: MissionRep = mockk()
    @Autowired
    lateinit var missionReport: MissionReport
@@ -27,4 +35,67 @@
        missionReport.execute("SH-CN-20240723-01", FactorFilter.default())
        missionReport.execute("SH-CN-20240723-02", FactorFilter.default())
    }
    @Test
    fun `calMissionInfo should throw BizException when mission not found`() {
        // Arrange
        val missionCode = "M001"
        every { missionRep.findOne(missionCode) } returns null
        // Act & Assert
        val exception = assertThrows<BizException> {
            missionService.calMissionInfo(missionCode)
        }
        assertEquals("走航任务不存在", exception.message)
    }
    @Test
    fun `calMissionInfo should calculate and update mission info successfully`() {
        // Arrange
        val missionCode = "M001"
        val mission = Mission(missionCode)
        val data = listOf<RealTimeData>()
        every { missionRep.findOne(missionCode) } returns mission
        every { realTimeDataRep.fetchData(mission) } returns data
        every { missionUtil.calKilometres(data) } returns 100.0
        every { missionUtil.calRegion(data) } returns "Center"
        every { missionRep.updateMission(mission) } returns true
        // Act
        val result = missionService.calMissionInfo(missionCode)
        // Assert
        assertTrue(result)
        assertEquals(100.0f, mission.kilometres)
        assertEquals("Center", mission.region)
        verify {
            missionRep.findOne(missionCode)
            realTimeDataRep.fetchData(mission)
            missionUtil.calKilometres(data)
            missionUtil.calRegion(data)
            missionRep.updateMission(mission)
        }
    }
    @Test
    fun `calMissionInfo should return false when update fails`() {
        // Arrange
        val missionCode = "M001"
        val mission = Mission(missionCode)
        val data = listOf<RealTimeData>()
        every { missionRep.findOne(missionCode) } returns mission
        every { realTimeDataRep.fetchData(mission) } returns data
        every { missionUtil.calKilometres(data) } returns 100.0
        every { missionUtil.calRegion(data) } returns "Center"
        every { missionRep.updateMission(mission) } returns false
        // Act
        val result = missionService.calMissionInfo(missionCode)
        // Assert
        assertFalse(result)
    }
}