From 3bb4fb15c664d29d179083698fdad35a661b1d7f Mon Sep 17 00:00:00 2001
From: riku <risaku@163.com>
Date: 星期四, 28 八月 2025 14:57:40 +0800
Subject: [PATCH] 2025.8.28 1. 添加走航季度报告相关统计功能(待完成)

---
 src/main/kotlin/com/flightfeather/uav/biz/mission/MissionUtil.kt                        |   49 +++-
 src/main/kotlin/com/flightfeather/uav/biz/report/MissionClue.kt                         |    2 
 src/main/resources/templates/underway_season_report.docx                                |    0 
 src/main/kotlin/com/flightfeather/uav/biz/report/MissionInventory.kt                    |    4 
 src/test/kotlin/com/flightfeather/uav/lightshare/service/impl/MissionServiceImplTest.kt |   81 +++++++
 pom.xml                                                                                 |   24 ++
 src/main/kotlin/com/flightfeather/uav/common/utils/FileUtil.kt                          |    4 
 src/main/kotlin/com/flightfeather/uav/domain/repository/MissionRep.kt                   |   28 ++
 src/main/kotlin/com/flightfeather/uav/biz/report/UnderwaySeasonReport.kt                |   72 +++++++
 src/main/kotlin/com/flightfeather/uav/common/utils/CommonUtil.kt                        |    2 
 src/main/kotlin/com/flightfeather/uav/lightshare/service/impl/MissionServiceImpl.kt     |    2 
 src/main/kotlin/com/flightfeather/uav/common/file/Docx4jGenerator.kt                    |  114 +++++++++++
 src/main/kotlin/com/flightfeather/uav/biz/report/MissionSummary.kt                      |  115 +++++++++++
 src/main/kotlin/com/flightfeather/uav/common/utils/ObjToMapUtil.kt                      |   62 ++++++
 14 files changed, 533 insertions(+), 26 deletions(-)

diff --git a/pom.xml b/pom.xml
index 5655f62..6b67ad9 100644
--- a/pom.xml
+++ b/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>
diff --git a/src/main/kotlin/com/flightfeather/uav/biz/mission/MissionUtil.kt b/src/main/kotlin/com/flightfeather/uav/biz/mission/MissionUtil.kt
index b5017c7..1edaf99 100644
--- a/src/main/kotlin/com/flightfeather/uav/biz/mission/MissionUtil.kt
+++ b/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
 
-            distance += MapUtil.getDistance(
-                a.longitude!!.toDouble(),
-                a.latitude!!.toDouble(),
-                b.longitude!!.toDouble(),
-                b.latitude!!.toDouble()
-            )
+        for (point in data) {
+            // 璺宠繃鏃犳晥鐐�
+            if (point.longitude == null || point.latitude == null) continue
+
+            // 濡傛灉瀛樺湪涓婁竴涓湁鏁堢偣锛屽垯璁$畻璺濈
+            lastValidPoint?.let { prevPoint ->
+                distance += MapUtil.getDistance(
+                    prevPoint.longitude!!.toDouble(),
+                    prevPoint.latitude!!.toDouble(),
+                    point.longitude!!.toDouble(),
+                    point.latitude!!.toDouble()
+                )
+            }
+
+            // 鏇存柊涓婁竴涓湁鏁堢偣
+            lastValidPoint = point
         }
         return distance
     }
-}
\ No newline at end of file
+
+    /**
+     * 鏍规嵁杞ㄨ抗鐐硅绠楁墍灞炲尯鍩燂紙涔¢晣+琛楅亾锛�
+     * @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
+        // 灏哤GS84鍧愭爣杞崲涓篏CJ02鍧愭爣鍚庤繘琛岄�嗗湴鐞嗙紪鐮佽幏鍙栧湴鍧�淇℃伅
+        val address = AMapService.reGeo(MapUtil.wgs84ToGcj02(pair as Pair<Double, Double>))
+        // 杩斿洖涔¢晣鍜岃閬撳悕绉扮粍鍚�
+        return address.township + address.street
+    }
+}
diff --git a/src/main/kotlin/com/flightfeather/uav/biz/report/MissionClue.kt b/src/main/kotlin/com/flightfeather/uav/biz/report/MissionClue.kt
index 73855b5..f61020e 100644
--- a/src/main/kotlin/com/flightfeather/uav/biz/report/MissionClue.kt
+++ b/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
     }
diff --git a/src/main/kotlin/com/flightfeather/uav/biz/report/MissionInventory.kt b/src/main/kotlin/com/flightfeather/uav/biz/report/MissionInventory.kt
index 701526e..fe910c4 100644
--- a/src/main/kotlin/com/flightfeather/uav/biz/report/MissionInventory.kt
+++ b/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
 
diff --git a/src/main/kotlin/com/flightfeather/uav/biz/report/MissionSummary.kt b/src/main/kotlin/com/flightfeather/uav/biz/report/MissionSummary.kt
index beb76d5..3cc0cbb 100644
--- a/src/main/kotlin/com/flightfeather/uav/biz/report/MissionSummary.kt
+++ b/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)
     }
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/flightfeather/uav/biz/report/UnderwaySeasonReport.kt b/src/main/kotlin/com/flightfeather/uav/biz/report/UnderwaySeasonReport.kt
new file mode 100644
index 0000000..b6366d2
--- /dev/null
+++ b/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.*
+//
+///**
+// * 璧拌埅瀛e害鎶ュ憡
+// * @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", "鍖栧伐鍖築", "灞呮皯鍖篊"),
+//            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
+//        )
+//
+//    }
+//}
\ No newline at end of file
diff --git a/src/main/kotlin/com/flightfeather/uav/common/file/Docx4jGenerator.kt b/src/main/kotlin/com/flightfeather/uav/common/file/Docx4jGenerator.kt
new file mode 100644
index 0000000..d33e105
--- /dev/null
+++ b/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
+    }
+
+    /**
+     * 娣诲姞鍥剧墖鍒癢ord鏂囨。
+     * @param imagePath 鍥剧墖璺緞
+     * @param width 瀹藉害(鍍忕礌)
+     * @param height 楂樺害(鍍忕礌)
+     * @param paragraphId 娈佃惤ID锛屾寚瀹氬浘鐗囨彃鍏ヤ綅缃�
+     */
+    fun addImage(imagePath: String, width: Int, height: Int, paragraphId: String): Docx4jGenerator {
+        // 瀹炵幇鍥剧墖娣诲姞閫昏緫
+        return this
+    }
+
+    /**
+     * 娣诲姞琛ㄦ牸鍒癢ord鏂囨。
+     * @param data 琛ㄦ牸鏁版嵁
+     * @param paragraphId 娈佃惤ID锛屾寚瀹氳〃鏍兼彃鍏ヤ綅缃�
+     */
+    fun addTable(data: List<List<String>>, paragraphId: String): Docx4jGenerator {
+        // 瀹炵幇琛ㄦ牸娣诲姞閫昏緫
+        return this
+    }
+
+    /**
+     * 淇濆瓨鐢熸垚鐨刉ord鏂囦欢
+     * @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)
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/flightfeather/uav/common/utils/CommonUtil.kt b/src/main/kotlin/com/flightfeather/uav/common/utils/CommonUtil.kt
index e8ff31b..c958a6b 100644
--- a/src/main/kotlin/com/flightfeather/uav/common/utils/CommonUtil.kt
+++ b/src/main/kotlin/com/flightfeather/uav/common/utils/CommonUtil.kt
@@ -17,4 +17,4 @@
 
         return null
     }
-}
\ No newline at end of file
+}
diff --git a/src/main/kotlin/com/flightfeather/uav/common/utils/FileUtil.kt b/src/main/kotlin/com/flightfeather/uav/common/utils/FileUtil.kt
index 851cedf..a474c2e 100644
--- a/src/main/kotlin/com/flightfeather/uav/common/utils/FileUtil.kt
+++ b/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 {
diff --git a/src/main/kotlin/com/flightfeather/uav/common/utils/ObjToMapUtil.kt b/src/main/kotlin/com/flightfeather/uav/common/utils/ObjToMapUtil.kt
new file mode 100644
index 0000000..4560884
--- /dev/null
+++ b/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
+
+/**
+ * 瀵硅薄杞崲涓洪敭瀵瑰�糾ap缁撴瀯
+ * @date 2025/8/28 10:19
+ * @author feiyu
+ */
+object ObjToMapUtil {
+
+    /**
+     * 灏嗗璞¤浆鎹负Map缁撴瀯锛堟敮鎸佸祵濂楀璞¢�掑綊杞崲锛�
+     * @param obj 寰呰浆鎹㈢殑瀵硅薄锛堥潪绌猴級
+     * @return 鍖呭惈瀵硅薄灞炴�у悕鍜屽�肩殑Map锛屽祵濂楀璞′細杞崲涓篗ap锛岄泦鍚�/鏁扮粍浼氶�掑綊澶勭悊鍏冪礌
+     */
+    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 -> ""
+            // 澶勭悊闆嗗悎绫诲瀷锛圠ist/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)
+            // 鍩虹绫诲瀷锛圛nt/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
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/flightfeather/uav/domain/repository/MissionRep.kt b/src/main/kotlin/com/flightfeather/uav/domain/repository/MissionRep.kt
index 2540179..ce7cdec 100644
--- a/src/main/kotlin/com/flightfeather/uav/domain/repository/MissionRep.kt
+++ b/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鐨凟xample鏋勫缓鏌ヨ鏉′欢
+        return missionMapper.selectByExample(Example(Mission::class.java).apply {
+            // 鍒涘缓鏌ヨ鏉′欢锛歴tartTime瀛楁鍦ㄦ寚瀹氭椂闂磋寖鍥村唴
+            createCriteria().andBetween("startTime", startTime, endTime)
+                // 杩囨护鎺夋病鏈夌粨鏉熸椂闂寸殑浠诲姟锛堢‘淇濅换鍔″凡瀹屾垚锛�
+                .andIsNotNull("endTime")
+        })
+    }
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/flightfeather/uav/lightshare/service/impl/MissionServiceImpl.kt b/src/main/kotlin/com/flightfeather/uav/lightshare/service/impl/MissionServiceImpl.kt
index d00408f..c8ace2f 100644
--- a/src/main/kotlin/com/flightfeather/uav/lightshare/service/impl/MissionServiceImpl.kt
+++ b/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)
     }
 }
\ No newline at end of file
diff --git a/src/main/resources/templates/underway_season_report.docx b/src/main/resources/templates/underway_season_report.docx
new file mode 100644
index 0000000..de22f86
--- /dev/null
+++ b/src/main/resources/templates/underway_season_report.docx
Binary files differ
diff --git a/src/test/kotlin/com/flightfeather/uav/lightshare/service/impl/MissionServiceImplTest.kt b/src/test/kotlin/com/flightfeather/uav/lightshare/service/impl/MissionServiceImplTest.kt
index f7f4603..f21cc8c 100644
--- a/src/test/kotlin/com/flightfeather/uav/lightshare/service/impl/MissionServiceImplTest.kt
+++ b/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)
+    }
 }
\ No newline at end of file

--
Gitblit v1.9.3