浏览代码

Merge branch 'develop' into TPE/Daniel

jeff 3 月之前
父节点
当前提交
bae10c4cd6
共有 100 个文件被更改,包括 26830 次插入1555 次删除
  1. 1 0
      .gitignore
  2. 7 0
      TEAMModelBI/ClientApp/src/language/lang/zh-cn.js
  3. 7 0
      TEAMModelBI/ClientApp/src/language/lang/zh-tw.js
  4. 100 24
      TEAMModelBI/ClientApp/src/view/product/details.vue
  5. 48 16
      TEAMModelBI/ClientApp/src/view/product/index.vue
  6. 1 0
      TEAMModelBI/Controllers/BICommon/BINoticeController.cs
  7. 182 14
      TEAMModelBI/Controllers/BIProductAnalysis/ProductAnalysisController.cs
  8. 36 1
      TEAMModelBI/Controllers/BITest/TestController.cs
  9. 18 2
      TEAMModelBI/Controllers/RepairApi/SchoolRepController.cs
  10. 二进制
      TEAMModelBI/Services/ipip.ipdb
  11. 2 0
      TEAMModelBI/Startup.cs
  12. 15 3
      TEAMModelBI/TEAMModelBI.csproj
  13. 0 6
      TEAMModelOS.Extension/Contest.Server/Contest.Server.csproj
  14. 0 1
      TEAMModelOS.Extension/HTEX.Test/HTEX.Test.csproj
  15. 6 1
      TEAMModelOS.Extension/IES.Exam/IES.ExamClient/IES.ExamClient.njsproj
  16. 246 32
      TEAMModelOS.Extension/IES.Exam/IES.ExamClient/app.js
  17. 31 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamClient/constants.js
  18. 69 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamClient/main.js
  19. 137 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamClient/menuManager.js
  20. 2 2
      TEAMModelOS.Extension/IES.Exam/IES.ExamClient/package.json
  21. 89 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamClient/serverManager.js
  22. 194 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamClient/updateManager.js
  23. 27 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamClient/utils.js
  24. 544 544
      TEAMModelOS.Extension/IES.Exam/IES.ExamClient/yarn.lock
  25. 90 28
      TEAMModelOS.Extension/IES.Exam/IES.ExamLibrary/Models/EvaluationCommon.cs
  26. 11 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamLibrary/Models/ExamConstant.cs
  27. 3 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/versions.json
  28. 22 1
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/BaseController.cs
  29. 63 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/FrameworkController.cs
  30. 235 2
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/IndexController.cs
  31. 812 477
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/ManageController.cs
  32. 352 2
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/StudentController.cs
  33. 0 33
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/WeatherForecastController.cs
  34. 24 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/CenterServiceConnectionService.cs
  35. 2 1
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/LiteDBFactory.cs
  36. 1 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/ServiceInitializer.cs
  37. 178 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/SubjectPushService.cs
  38. 76 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/AESHelper.cs
  39. 6 1
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/Constant.cs
  40. 31 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/FileHelper.cs
  41. 1 1
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/JwtAuthExtension.cs
  42. 170 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/ZipHelper.cs
  43. 4 1
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/IES.ExamServer.csproj
  44. 0 9
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Models/ErrorViewModel.cs
  45. 65 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Models/EvaluationRound.cs
  46. 314 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Models/EvaluationStudent.cs
  47. 1 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Models/Teacher.cs
  48. 59 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Models/MusicAiRecord.cs
  49. 16 1
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Program.cs
  50. 39 12
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Services/IndexService.cs
  51. 448 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Services/ManageService.cs
  52. 0 13
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/WeatherForecast.cs
  53. 5 1
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/appsettings.json
  54. 4 1
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/package.json
  55. 24 17
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/api/http.js
  56. 78 2
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/api/index.js
  57. 1 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/icon/no_data.svg
  58. 237 7
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/demo_index.html
  59. 44 4
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.css
  60. 1 1
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.js
  61. 71 1
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.json
  62. 二进制
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.ttf
  63. 二进制
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.woff
  64. 二进制
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.woff2
  65. 11 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/main.js
  66. 847 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/utils/evTools.js
  67. 4 20
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/utils/public.js
  68. 138 2
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/admin/ActivityManage.less
  69. 1065 247
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/admin/ActivityManage.vue
  70. 264 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/admin/TestPaper.less
  71. 315 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/admin/TestPaper.vue
  72. 35 11
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/login/Admin.vue
  73. 225 4
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/login/Student.vue
  74. 二进制
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/login/background.jpg
  75. 9 1
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/vue.config.js
  76. 64 1
      TEAMModelOS.Function/CosmosDBTriggers/TriggerExam.cs
  77. 20 2
      TEAMModelOS.Function/IESTimerTrigger.cs
  78. 13614 0
      TEAMModelOS.Function/JsonFile/Region/region_cn.json
  79. 1880 0
      TEAMModelOS.Function/JsonFile/Region/region_en.json
  80. 1896 0
      TEAMModelOS.Function/JsonFile/Region/region_gl.json
  81. 2 1
      TEAMModelOS.Function/Program.cs
  82. 二进制
      TEAMModelOS.Function/Services/ipip.ipdb
  83. 11 3
      TEAMModelOS.Function/TEAMModelOS.Function.csproj
  84. 68 0
      TEAMModelOS.SDK/DI/IPIP/BaseStation.cs
  85. 72 0
      TEAMModelOS.SDK/DI/IPIP/BaseStationInfo.cs
  86. 66 0
      TEAMModelOS.SDK/DI/IPIP/City.cs
  87. 199 0
      TEAMModelOS.SDK/DI/IPIP/CityInfo.cs
  88. 69 0
      TEAMModelOS.SDK/DI/IPIP/District.cs
  89. 88 0
      TEAMModelOS.SDK/DI/IPIP/DistrictInfo.cs
  90. 68 0
      TEAMModelOS.SDK/DI/IPIP/IDC.cs
  91. 71 0
      TEAMModelOS.SDK/DI/IPIP/IDCInfo.cs
  92. 13 0
      TEAMModelOS.SDK/DI/IPIP/IPFormatException.cs
  93. 11 0
      TEAMModelOS.SDK/DI/IPIP/InvalidDatabaseException.cs
  94. 18 0
      TEAMModelOS.SDK/DI/IPIP/MetaData.cs
  95. 12 0
      TEAMModelOS.SDK/DI/IPIP/NotFoundException.cs
  96. 241 0
      TEAMModelOS.SDK/DI/IPIP/Reader.cs
  97. 150 0
      TEAMModelOS.SDK/Extension/GeoRegion.cs
  98. 0 1
      TEAMModelOS.SDK/Extension/HttpContextExtensions.cs
  99. 34 0
      TEAMModelOS.SDK/Models/Cosmos/Common/GeoAnalysis.cs
  100. 0 0
      TEAMModelOS.SDK/Models/Cosmos/Common/IotTeachingData.cs

+ 1 - 0
.gitignore

@@ -277,3 +277,4 @@ TEAMModelBI/Properties/ServiceDependencies/teammodelbi-RC - Web Deploy/profile.a
 /TEAMModelOS.Extension/IES.Exam/IES.ExamClient/server/wwwroot
 /TEAMModelOS.Extension/IES.Exam/IES.ExamClient/server
 /TEAMModelOS.Extension/IES.Exam/wwwroot
+/TEAMModelOS.Extension/IES.Exam/IES.ExamClient/IES.ExamServer.zip

+ 7 - 0
TEAMModelBI/ClientApp/src/language/lang/zh-cn.js

@@ -690,6 +690,7 @@ const zh_cn = {
         parameterError: '参数错误',
     },
     product: {
+        geo: '地理位置',
         dataTarget: '取得对象',
         dataFilter: '数据筛选',
         filterType: '筛选类型',
@@ -821,6 +822,12 @@ const zh_cn = {
         view: '查看',
         message1_1: '当前查询人数仅支持小于或等于',
         message1_2: '名用户',
+        teacherData: '教师数据',
+        tmid: '醍摩豆ID',
+        schoolName: '学校',
+        stuLessonLengMin: '学生参与总时数',
+        item: '题目数',
+        interact: '互动次数',
     },
     userInquire: {
         index: {

+ 7 - 0
TEAMModelBI/ClientApp/src/language/lang/zh-tw.js

@@ -683,6 +683,7 @@ const zh_tw = {
         parameterError: '參數錯誤',
     },
     product: {
+        geo: '地理位置',
         dataTarget: '取得對象',
         dataFilter: '數據篩選',
         filterType: '篩選類型',
@@ -814,6 +815,12 @@ const zh_tw = {
         view: '查看',
         message1_1: '當前查詢人數僅支持小於或等於',
         message1_2: '名用戶',
+        teacherData: '教師數據',
+        tmid: '醍摩豆ID',
+        schoolName: '學校',
+        stuLessonLengMin: '學生參與總時數',
+        item: '題目數',
+        interact: '互動次數',        
     },
     userInquire: {
         index: {

+ 100 - 24
TEAMModelBI/ClientApp/src/view/product/details.vue

@@ -153,10 +153,12 @@
             </div>
           </div>
         </div>
-        <!--学区or城市样式内容-->
-        <div class="data-tables" style="height: 700px;width: 100%;" v-loading="searchLoading"  :element-loading-text="$t(`product.prepareData`)+'...'">
-          <el-auto-resizer>
-            <template #default="{ height, width }">
+        
+        <!--老師個人資料列表-->        
+        <div class="data-tables-teacher" style="height: 700px;width: 100%;" v-loading="searchLoading"  :element-loading-text="$t(`product.prepareData`)+'...'">          
+          <el-button type="primary" @click="exportExcel()" style="display: block;margin-top: 10px;">匯出</el-button>
+          <el-auto-resizer>            
+            <template #default="{ height, width }">              
               <el-table-v2 v-if="filterdata.length > 0" v-model:sort-state="sortState" :columns="columns" :data="filterdata" :width="width" :height="height" @column-sort="onSort" fixed  :row-class="rowClassName" />
             </template>
           </el-auto-resizer>
@@ -333,6 +335,8 @@ import { ref, reactive, getCurrentInstance, defineEmits, computed, onMounted } f
 import { ElMessage, TableV2SortOrder, ElLoading, ElCheckbox,HeaderCellSlotProps,ElPopover,Column, RowClassNameGetter} from 'element-plus'
 import Xlines from '@/components/echarts/Xline.vue'
 import * as echarts from 'echarts'
+import excel from "../../until/excel"
+
 let props = defineProps({
   detailsData: Object,
   pattern:Object,
@@ -347,17 +351,38 @@ let { proxy } = getCurrentInstance()
 const activeName = ref('areas')
 let appearState = ref('area')
 const sortState = ref({
+  'schoolName':TableV2SortOrder.ASC,
+  'schoolId':TableV2SortOrder.ASC,
+  'name':TableV2SortOrder.ASC,
+  'tmid':TableV2SortOrder.ASC,
   'deviceCnt':TableV2SortOrder.ASC,
   'tmidCnt': TableV2SortOrder.ASC,
   'stuShow': TableV2SortOrder.ASC,
   'lessonRecord':TableV2SortOrder.ASC,
   'lessonLengMin':TableV2SortOrder.ASC,
   'tGreen':TableV2SortOrder.ASC,
-  'date':TableV2SortOrder.ASC,
+  'tLesson':TableV2SortOrder.ASC,
+  'stuLessonLengMin':TableV2SortOrder.ASC,
+  'item':TableV2SortOrder.ASC,
+  'interact':TableV2SortOrder.ASC,
 })
 let searchLoading=ref(false)
 let sortValue = reactive({})
-let columns = ref([
+let columns = ref([  
+  {
+    key: "schoolName",
+    dataKey: "schoolName",//需要渲染当前列的数据字段,如{id:9527,name:'Mike'},则填id
+    title: proxy.$t(`product.schoolName`),//显示在单元格表头的文本
+    width: 220,//当前列的宽度,必须设置
+    headerClass: 'general',
+  },  
+  {
+    key: "schoolId",
+    dataKey: "schoolId",//需要渲染当前列的数据字段,如{id:9527,name:'Mike'},则填id
+    title: proxy.$t(`product.simpleCode`),//显示在单元格表头的文本
+    width: 100,//当前列的宽度,必须设置
+    headerClass: 'general',
+  },  
   {
     key: "name",
     dataKey: "name",//需要渲染当前列的数据字段,如{id:9527,name:'Mike'},则填id
@@ -367,6 +392,15 @@ let columns = ref([
     // sortable: true,
     headerClass: 'general',
   },
+  {
+    key: "tmid",
+    dataKey: "tmid",//需要渲染当前列的数据字段,如{id:9527,name:'Mike'},则填id
+    title: proxy.$t(`product.tmid`),//显示在单元格表头的文本
+    width: 150,//当前列的宽度,必须设置
+    fixed: false,//是否固定列
+    // sortable: true,
+    headerClass: 'general',
+  },
   {
     key: "date",
     dataKey: "date",//需要渲染当前列的数据字段,如{id:9527,name:'Mike'},则填id
@@ -376,13 +410,7 @@ let columns = ref([
      sortable: true,
     headerClass: 'general',
   },
-  {
-    key: "schoolId",
-    dataKey: "schoolId",//需要渲染当前列的数据字段,如{id:9527,name:'Mike'},则填id
-    title: proxy.$t(`product.simpleCode`),//显示在单元格表头的文本
-    width: 100,//当前列的宽度,必须设置
-    headerClass: 'general',
-  },
+  
   {
     key: "deviceCnt",
     dataKey: "deviceCnt",//需要渲染当前列的数据字段,如{id:9527,name:'Mike'},则填id
@@ -402,7 +430,7 @@ let columns = ref([
   {
     key: "stuShow",
     dataKey: "stuShow",//需要渲染当前列的数据字段,如{id:9527,name:'Mike'},则填id
-    title: proxy.$t(`product.studentNumber`),//显示在单元格表头的文本
+    title: proxy.$t(`product.studentNumberUnique`),//显示在单元格表头的文本
     width: 100,//当前列的宽度,必须设置
     headerClass: 'general',
     sortable:true
@@ -439,6 +467,30 @@ let columns = ref([
     headerClass: 'general',
     sortable:true
   },
+  {
+    key: "stuLessonLengMin",
+    dataKey: "stuLessonLengMin",
+    title: proxy.$t(`product.stuLessonLengMin`),//显示在单元格表头的文本
+    width: 150,//当前列的宽度,必须设置
+    headerClass: 'general',
+    sortable:true
+  }, 
+  {
+    key: "item",
+    dataKey: "item",
+    title: proxy.$t(`product.item`),//显示在单元格表头的文本
+    width: 100,//当前列的宽度,必须设置
+    headerClass: 'general',
+    sortable:true
+  },
+  {
+    key: "interact",
+    dataKey: "interact",
+    title: proxy.$t(`product.interact`),//显示在单元格表头的文本
+    width: 100,//当前列的宽度,必须设置
+    headerClass: 'general',
+    sortable:true
+  },
   // {
   //   key: "date",
   //   dataKey: "date",
@@ -1314,8 +1366,7 @@ function init (againvalue) {
   }else{
     isShowAuth = false;
   }
- 
- if(props.postData.target === "school"){
+ if (props.postData.target === "school" || props.postData.target === "geo"){
   serachToresult()
  } 
  
@@ -1345,7 +1396,9 @@ function serachToresult() {
     if (res.state === 200) {
         res.data.forEach((item) => { 
           item.name = item.tmidInfo.name ? item.tmidInfo.name : proxy.$t(`product.noneYet`) 
-          item.schoolId = item.tmid
+          // 把學校名稱
+          item.schoolName = item.school.name
+          //item.schoolId = item.tmid
           });
         res.geo.forEach((item) => {
           if (item.school.name || (item.school.name === "")) {
@@ -1363,6 +1416,29 @@ function serachToresult() {
     ElMessage.error(proxy.$t(`product.apiErrpr`) + ',' + proxy.$t(`product.getDataError`))
   })
 }
+//匯出各縣市的統購資料
+function exportExcel() {    
+  let title = [proxy.$t(`product.schoolName`), proxy.$t(`product.simpleCode`), proxy.$t(`product.name`), proxy.$t(`product.tmid`), proxy.$t(`product.time`), proxy.$t(`product.classroomNumber`), proxy.$t(`product.teacherNumber`)
+  , proxy.$t(`product.studentNumber`), proxy.$t(`product.lessonNumber`), proxy.$t(`product.lessonHours`), proxy.$t(`product.tGreen`), proxy.$t(`product.tLesson`)
+  , proxy.$t(`product.stuLessonLengMin`), proxy.$t(`product.item`), proxy.$t(`product.interact`)];
+  let key = ['schoolName', 'schoolId', 'name', 'tmid', 'date', 'deviceCnt', 'tmidCnt', 'stuShow', 'lessonRecord', 'lessonLengMin', 'tGreen', 'tLesson',
+    'stuLessonLengMin', 'item','interact'];
+
+  let arr = excel.export_json_to_array(key, filterdata.value);
+  arr.unshift(title);
+  let ws = XLSX.utils.aoa_to_sheet(arr);
+  ws = excel.export_auto_width(ws, arr);
+  let wb = XLSX.utils.book_new();
+  let sheetName ='Data';
+  XLSX.utils.book_append_sheet(wb, ws, sheetName);
+  //檔案做成
+  let filename = schoolData.value.name + '_' + proxy.$t(`product.teacherData`);
+  XLSX.writeFile(wb, filename + '.xlsx', {
+      bookType: 'xlsx',
+      bookSST: true,
+      type: 'array'
+  });           
+  }
 init()
 </script>
 <style  scoped>
@@ -1922,16 +1998,17 @@ init()
 .contentbox .el-divider {
   margin: 10px 0;
 }
-.data-tables .header-class,
-  .data-tables 
+.data-tables-teacher .header-class,
+  .data-tables-teacher
   .el-table-v2__row-cell 
   {
-    /* width: v-bind(cellWidth + "%") !important; */
+    max-width: 15% !important;   
     justify-content: center;
     text-align: center;
+    white-space:nowrap;
   }
-  .data-tables .general {
-    /* width: v-bind(cellWidth + "%") !important; */
+  .data-tables-teacher .general {
+    max-width: 15% !important;   
     justify-content: center;
     text-align: center;
   }
@@ -1939,8 +2016,7 @@ init()
     justify-content: left;
     text-align: left;
   }
-  .data-tables .btn-class {
-    /* width: v-bind(cellWidth + "%") !important; */
+  .data-tables .btn-class {    
     justify-content: center;
     text-align: center;
   }

+ 48 - 16
TEAMModelBI/ClientApp/src/view/product/index.vue

@@ -9,8 +9,9 @@
             <div class="filtratebox-phase">
                 <span class="filtratebox-phase-title">{{$t(`product.dataTarget`)}}:</span>
                 <div class="filtratebox-phase-content">
-                    <div class="phase-item" :class="[clickNum.target==='school' ? 'filter-click':'' ]" @click="clickNum.target='school'">school</div>
-                    <div class="phase-item" :class="[clickNum.target==='tmid' ? 'filter-click':'' ]" @click="clickNum.target='tmid'">tmid</div>
+                    <div class="phase-item" :class="[clickNum.target==='school' ? 'filter-click':'' ]" @click="clickNum.target='school'">{{$t(`product.school`)}}</div>
+                    <div class="phase-item" :class="[clickNum.target==='tmid' ? 'filter-click':'' ]" @click="clickNum.target='tmid'">{{$t(`product.tmid`)}}</div>
+                    <div class="phase-item" :class="[clickNum.target==='geo' ? 'filter-click':'' ]" @click="clickNum.target='geo'">{{$t(`product.geo`)}}</div>
                 </div>
             </div>
             <!--产品类型-->
@@ -42,13 +43,10 @@
             <div class="select-result">
               <!--目标范围-->
               <div v-show="clickNum.filter === 0">
-                <div class="filtratebox-phase">
+                <div class="filtratebox-phase" v-show="clickNum.target==='school'">
                   <span class="filtratebox-phase-title subclass">{{productData.sourceName}}:</span>
                   <div class="filtratebox-phase-content">
                     <div class="phase-item" v-for="(items,index) in productData.source" :key="items.value" :class="[index ===clickNum.subject ? 'filter-click':'' ]" @click="(clickNum.subject=index,optionsValue='')">{{items.name}}</div>
-                    <!-- <div class="phase-item filter-click">456456</div>
-                <div class="phase-item">456456</div>
-                <div class="phase-item">456456</div> -->
                   </div>
                 </div>
                 <div class="filtratebox-phase">
@@ -60,7 +58,7 @@
                 <div class="filtratebox-phase">
                   <span class="filtratebox-phase-title subclass">{{$t(`product.accurateSelect`)}}:</span>
                   <div class="filtratebox-phase-content precise">
-                    <div v-if="(clickNum.subject === 0 || clickNum.subject === 2) && clickNum.filter===0" class="schoolclass">
+                    <div v-if="clickNum.target==='school' && (clickNum.subject === 0 || clickNum.subject === 2) && clickNum.filter===0" class="schoolclass">
                       <el-cascader v-model="optionsValue" :options="options" :props="props2" :collapse-tags=true :collapse-tags-tooltip=true filterable :filter-method="keywords" :placeholder="$t(`product.pleaseSelect`)">
                       </el-cascader>
                       <div class="addschoolbtn" @click="(adddialog=true,addvalue='',searchInit())">
@@ -875,15 +873,15 @@ function dataInit () {
     ElMessage.error(proxy.$t(`product.apiErrpr`) + ',' + proxy.$t(`product.basicDataError`))
   })
 }
-function serachToresult(startTime, endTime, product, schools, unit, target) {
+function serachToresult(startTime, endTime, product, schools, unit, target, geo=null) {
   // let data = { "dateFrom": "2023-04-12", "dateTo": "2023-04-19", "prod": "HiTeach", "schoolIds": ["tbslgb", "habook"], "dateUnit": "Day" }
   if (!startTime || !endTime) {
     ElMessage.info(proxy.$t(`product.timeRangeSelectError`))
     return
   }
   searchLoading.value = true;
-  let data = { "dateFrom": startTime, "dateTo": endTime, "prod": product, "schoolIds": schools, "dateUnit": unit, "target": target }
-  console.log(data, '内容')
+  let data = { "dateFrom": startTime, "dateTo": endTime, "prod": product, "schoolIds": schools, "dateUnit": unit, "target": target, "geo": geo }
+  console.log(data, 'serachToresult data内容')
   console.log(clickNum.value.time, '数字')
   postData.value = data
   proxy.$api.getUseproduct(data).then(async (res) => {
@@ -1065,11 +1063,10 @@ function timeChange (value) {
 }
 //整理数据内容
 async function searchData () {
-  console.log(optionsValue.value)
+  console.log(optionsValue.value) //國省市
   console.log(productData.value.timevalue)
   console.log(clickNum.value.filter, 'NUM')
   console.log(clickNum.value.target, 'target')
-
   // if (!productData.value.timevalue) { return }
   let searchValue = optionsValue.value
   let schoolArr = []
@@ -1077,6 +1074,7 @@ async function searchData () {
   let times = { start: productData.value.timevalue[0], end: productData.value.timevalue[1] }
   let yearValues=''
   let target = clickNum.value.target
+  let geo = null
   clickNum.value.time === 2 ? (yearValues=productData.value.timevalue.slice(0,4),times.start=productData.value.timevalue,times.end=yearValues+'-12-31'):'' 
   if (clickNum.value.filter === 0 && searchValue) { // 篩選類型 => 來源類型
     if (clickNum.value.subject === 0) {// 目标范围 => 學校     
@@ -1106,7 +1104,35 @@ async function searchData () {
       console.log(optionsValue.value, '城市关键值')
       console.log(typeof optionsValue.value, 'type')
       let state = ''
+      geo = { "countryId": null, "provinceId": null, "cityId": null }
       typeof optionsValue.value == 'string' ? state = 'province' : state = 'city'
+        if (state === 'province') {
+            if (siteValue === 'cn') {
+                geo.countryId = 'CN'
+                geo.provinceId = optionsData.find((obj) => obj.name === optionsValue.value).code;
+            }
+            else {
+                geo.countryId = optionsData.find((obj) => obj.name === optionsValue.value).code;
+            }
+        }
+        else if (state === 'city') {
+            if (siteValue === 'cn') {
+                geo.countryId = 'CN'
+                let provinceId = optionsData.find((obj) => obj.name === (optionsValue?.value?.[0] || ''))?.code;
+                geo.provinceId = provinceId
+                let cityDic = optionsData.find((obj) => obj.code === provinceId).children;
+                let cityId = cityDic.find((obj) => obj.name === (optionsValue?.value?.[1] || ''))?.code;
+                geo.cityId = cityId
+            }
+            else {
+                let countryId = optionsData.find((obj) => obj.name === optionsValue.value[0]).code;
+                geo.countryId = countryId;
+                geo.provinceId = null;
+                let cityDic = optionsData.find((obj) => obj.code === countryId).children;
+                let cityId = cityDic.find((obj) => obj.name === (optionsValue?.value?.[1] || ''))?.code;
+                geo.cityId = cityId
+            }
+        }
       let resultData = await filterDistrict(state, optionsValue.value)
       schoolArr = resultData
       console.log(resultData, state, '省级查询及状态')
@@ -1123,17 +1149,23 @@ async function searchData () {
   console.log(schoolArr, dateUnits, times, '结果')
   !searchValue ? schoolArr = [] : ''
   schoolArr = [...new Set(schoolArr)]
-  serachToresult(times.start, times.end, "HiTeach", schoolArr, dateUnits, target)
+  serachToresult(times.start, times.end, "HiTeach", schoolArr, dateUnits, target, geo)
 }
 function serarchInit (value) {
   let filterKey = value
-  console.log(filterKey)
-  if (filterKey === 0) {
+  if (clickNum.value.target === 'geo') { //取得對象:地理位置
+      filterKey = 1;
+      clickNum.value.filter = 1;
+      if (clickNum.value.district === 2) { //地區選擇若為"學區" => 改選"城市"
+          clickNum.value.district = 1
+      }
+  }
+  if (filterKey === 0) { //篩選類型:來源類型
     // dataSource.value.composite=
     console.log(clickNum.value.subject, '0学区值0')
     options.value = dataSource.value.composite
     clickNum.value.subject === 2 ? options.value = optionsData : ''
-  } else if (filterKey === 1) {
+  } else if (filterKey === 1) { //篩選類型:地區城市
     options.value = optionsData
     console.log(clickNum.value.district, '1学区值1')
     clickNum.value.district === 2 ? options.value = dataSource.value.composite : ''

+ 1 - 0
TEAMModelBI/Controllers/BICommon/BINoticeController.cs

@@ -569,6 +569,7 @@ namespace TEAMModelBI.Controllers.BICommon
             var cosmosClientCsv2 = _azureCosmos.GetCosmosClient(name: "CoreServiceV2");
             var cosmosClientCsv2Cn = _azureCosmos.GetCosmosClient(name: "CoreServiceV2CnRead");
             bool isChina = (_option.Location.Contains("China")) ? true : false;
+            List<string> comeRemoveStr = GeoRegion.comeRemoveStr;
             if (isChina) countryId = "CN";
             List<TmidInfo> tmidExInfos = new List<TmidInfo>(); //TMID資訊
             List<AreaInfo> geoInfos = new(); //輸出:地理位置為單位

文件差异内容过多而无法显示
+ 182 - 14
TEAMModelBI/Controllers/BIProductAnalysis/ProductAnalysisController.cs


+ 36 - 1
TEAMModelBI/Controllers/BITest/TestController.cs

@@ -68,6 +68,9 @@ using Azure.Messaging.ServiceBus.Administration;
 using static TEAMModelOS.SDK.CoreAPIHttpService;
 using System.Xml;
 using System.Drawing.Printing;
+using TEAMModelOS.SDK.DI.IPIP;
+using Microsoft.International.Converters.TraditionalChineseToSimplifiedConverter;
+using static TEAMModelOS.SDK.Extension.GeoRegion;
 
 namespace TEAMModelBI.Controllers.BITest
 {
@@ -87,7 +90,8 @@ namespace TEAMModelBI.Controllers.BITest
         private readonly CoreAPIHttpService _coreAPIHttpService;
         private readonly IHttpClientFactory _httpClient;
         private IPSearcher _ipSearcher;
-        public TestController(IPSearcher ipSearcher, AzureCosmosFactory azureCosmos, AzureStorageFactory azureStorage, AzureRedisFactory azureRedis, DingDing dingDing, IOptionsSnapshot<Option> option, IWebHostEnvironment hostingEnvironment, IConfiguration configuration, CoreAPIHttpService coreAPIHttpService, IHttpClientFactory httpClient)
+        private readonly City _city;
+        public TestController(IPSearcher ipSearcher, AzureCosmosFactory azureCosmos, AzureStorageFactory azureStorage, AzureRedisFactory azureRedis, DingDing dingDing, IOptionsSnapshot<Option> option, IWebHostEnvironment hostingEnvironment, IConfiguration configuration, CoreAPIHttpService coreAPIHttpService, IHttpClientFactory httpClient, City city)
         {
             _azureCosmos = azureCosmos;
             _azureStorage = azureStorage;
@@ -98,6 +102,7 @@ namespace TEAMModelBI.Controllers.BITest
             _configuration = configuration;
             _coreAPIHttpService = coreAPIHttpService;
             _httpClient = httpClient; _ipSearcher = ipSearcher;
+            _city = city;
         }
 
         /// <summary>
@@ -2045,6 +2050,36 @@ namespace TEAMModelBI.Controllers.BITest
             return Ok(new { state = 200, result });
         }
 
+        /// <summary>
+        /// IP轉換地理位置
+        /// </summary>
+        /// <param name="jsonElement"></param>
+        /// <returns></returns>
+        [ProducesDefaultResponseType]
+        [HttpPost("getGeoFromIpTest")]
+        public async Task<IActionResult> getGeoFromIpTest(JsonElement jsonElement)
+        {
+            string ip = (jsonElement.TryGetProperty("ip", out JsonElement _ip)) ? _ip.ToString() : string.Empty;
+            string lang = (jsonElement.TryGetProperty("lang", out JsonElement _lang)) ? _lang.ToString() : "zh-tw";
+
+            List<regionrow> region_gl = new List<regionrow>();
+            using (StreamReader r = new StreamReader("JsonFile/Region/region_gl.json"))
+            {
+                string json_g = r.ReadToEnd();
+                region_gl = JsonSerializer.Deserialize<List<regionrow>>(json_g);
+            }
+            List<regionrow> region_cn = new List<regionrow>();
+            using (StreamReader r = new StreamReader("JsonFile/Region/region_cn.json"))
+            {
+                string json_c = r.ReadToEnd();
+                region_cn = JsonSerializer.Deserialize<List<regionrow>>(json_c);
+            }
+            List<string> ipToGeoList = new List<string>();
+            IotTeachingData.Geo geo = new IotTeachingData.Geo();
+            (geo, ipToGeoList) = BIProdAnalysis.getGeoFromIp(_city, ip, region_gl, region_cn);
+            return Ok(new { state = 200, geo, geoFromIp = ipToGeoList });
+        }
+
         public class linqTest
         {
             public string id { get; set; }

+ 18 - 2
TEAMModelBI/Controllers/RepairApi/SchoolRepController.cs

@@ -27,6 +27,8 @@ using TEAMModelOS.SDK.Models.Service.BI;
 using TEAMModelOS.SDK.Models.Service.BIStatsWay;
 using StackExchange.Redis;
 using System.Text.RegularExpressions;
+using TEAMModelOS.SDK.DI.IPIP;
+using static TEAMModelOS.SDK.Extension.GeoRegion;
 
 namespace TEAMModelBI.Controllers.RepairApi
 {
@@ -44,8 +46,9 @@ namespace TEAMModelBI.Controllers.RepairApi
         private readonly CoreAPIHttpService _coreAPIHttpService;
         private readonly IWebHostEnvironment _environment; //读取文件
         private readonly IHttpClientFactory _httpClient;
+        private readonly City _city;
 
-        public SchoolRepController(AzureCosmosFactory azureCosmos, DingDing dingDing, AzureStorageFactory azureStorage, IOptionsSnapshot<Option> option, AzureRedisFactory azureRedis, IConfiguration configuration, CoreAPIHttpService coreAPIHttpService, IHttpClientFactory httpClient, IWebHostEnvironment hostingEnvironment)
+        public SchoolRepController(AzureCosmosFactory azureCosmos, DingDing dingDing, AzureStorageFactory azureStorage, IOptionsSnapshot<Option> option, AzureRedisFactory azureRedis, IConfiguration configuration, CoreAPIHttpService coreAPIHttpService, IHttpClientFactory httpClient, IWebHostEnvironment hostingEnvironment, City city)
         {
             _azureCosmos = azureCosmos;
             _dingDing = dingDing;
@@ -56,6 +59,7 @@ namespace TEAMModelBI.Controllers.RepairApi
             _coreAPIHttpService = coreAPIHttpService;
             _httpClient = httpClient;
             _environment = hostingEnvironment;
+            _city = city;
         }
 
         /// <summary>
@@ -586,6 +590,18 @@ namespace TEAMModelBI.Controllers.RepairApi
             string month = (jsonElement.TryGetProperty("month", out JsonElement _month)) ? $"{_month}" : string.Empty;
             string day = (jsonElement.TryGetProperty("day", out JsonElement _day)) ? $"{_day}" : string.Empty;
             var redisClinet2 = _azureRedis.GetRedisClient(2);
+            List<regionrow> _region_gl = new List<regionrow>();
+            using (StreamReader r = new StreamReader("JsonFile/Region/region_gl.json"))
+            {
+                string json_g = r.ReadToEnd();
+                _region_gl = JsonSerializer.Deserialize<List<regionrow>>(json_g);
+            }
+            List<regionrow> _region_cn = new List<regionrow>();
+            using (StreamReader r = new StreamReader("JsonFile/Region/region_cn.json"))
+            {
+                string json_c = r.ReadToEnd();
+                _region_cn = JsonSerializer.Deserialize<List<regionrow>>(json_c);
+            }
             var keys = new HashSet<RedisKey>();
             int nextCursor = 0;
             do
@@ -621,7 +637,7 @@ namespace TEAMModelBI.Controllers.RepairApi
                     {
                         string m = date.Substring(0, 2);
                         string d = date.Substring(2, 2);
-                        await BIProdAnalysis.BICreatDailyAnalData(_azureRedis, _azureCosmosClient, _azureCosmosClientCsv2, _azureCosmosClientCsv2Read, _dingDing, y, m, d);
+                        await BIProdAnalysis.BICreatDailyAnalData(_azureRedis, _azureCosmosClient, _azureCosmosClientCsv2, _azureCosmosClientCsv2Read, _dingDing, _city, _option.Location, y, m, d, _region_gl, _region_cn);
                         resultKeys.Add($"{key}");
                     }
                 }

二进制
TEAMModelBI/Services/ipip.ipdb


+ 2 - 0
TEAMModelBI/Startup.cs

@@ -26,6 +26,7 @@ using TEAMModelOS.SDK.Models;
 using VueCliMiddleware;
 using System.Net.Http;
 using TEAMModelOS.Filter;
+using TEAMModelOS.SDK.DI.IPIP;
 
 namespace TEAMModelBI
 {
@@ -131,6 +132,7 @@ namespace TEAMModelBI
             services.AddSnowflakeId(Convert.ToInt64(Configuration.GetValue<string>("Option:LocationNum")), 1);
             services.AddHttpClient();
             services.AddHttpClient<DingDing>();
+            services.AddSingleton(new City(@"Services/ipip.ipdb"));
             //services.AddCoreAPIHttpService(Configuration);
             services.AddHttpClient<CoreAPIHttpService>().ConfigureHttpMessageHandlerBuilder(builder =>
             {

+ 15 - 3
TEAMModelBI/TEAMModelBI.csproj

@@ -55,6 +55,18 @@
 	    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
 	  </Content>
 	</ItemGroup>
+	<ItemGroup>
+		<None Update="Services\ipip.ipdb">
+			<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+			<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
+		</None>
+	</ItemGroup>
+	<ItemGroup>
+		<None Update="JsonFile/Region/*.json">
+			<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+			<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
+		</None>
+	</ItemGroup>
 	<PropertyGroup>
 		<SpaRoot>ClientApp\</SpaRoot>
 		<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
@@ -65,9 +77,9 @@
 		<SpaRoot>ClientApp\</SpaRoot>
 		<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
 		<UserSecretsId>078b5d89-7d90-4f6a-88fc-7d96025990a8</UserSecretsId>
-		<Version>5.2502.5</Version>
-		<AssemblyVersion>5.2502.5.1</AssemblyVersion>
-		<FileVersion>5.2502.5.1</FileVersion>
+		<Version>5.2502.19</Version>
+		<AssemblyVersion>5.2502.19.1</AssemblyVersion>
+		<FileVersion>5.2502.19.1</FileVersion>
 		<Description>TEAMModelBI(BI)</Description>
 		<PackageReleaseNotes>BI版本说明版本切换标记2022000908</PackageReleaseNotes>
 		<PackageId>TEAMModelBI</PackageId>

+ 0 - 6
TEAMModelOS.Extension/Contest.Server/Contest.Server.csproj

@@ -9,12 +9,6 @@
     <SpaProxyServerUrl>https://localhost:5173</SpaProxyServerUrl>
   </PropertyGroup>
 
-  <ItemGroup>
-    <ProjectReference Include="..\Contest.Client\Contest.Client.esproj">
-      <ReferenceOutputAssembly>false</ReferenceOutputAssembly>
-    </ProjectReference>
-  </ItemGroup>
-
   <ItemGroup>
     <PackageReference Include="Microsoft.AspNetCore.SpaProxy">
       <Version>6.*-*</Version>

+ 0 - 1
TEAMModelOS.Extension/HTEX.Test/HTEX.Test.csproj

@@ -16,7 +16,6 @@
 
   <ItemGroup>
     <ProjectReference Include="..\..\TEAMModelOS.SDK\TEAMModelOS.SDK.csproj" />
-    <ProjectReference Include="..\HTEX.Lib\HTEX.Lib.csproj" />
   </ItemGroup>
 
 </Project>

+ 6 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/IES.ExamClient.njsproj

@@ -28,13 +28,18 @@
     <DebugSymbols>true</DebugSymbols>
   </PropertyGroup>
   <ItemGroup>
-    <Content Include="app.原始备份.js" />
     <Content Include="app.js" />
+    <Content Include="constants.js" />
     <Content Include="header.bmp" />
     <Content Include="logo.ico" />
+    <Content Include="main.js" />
+    <Content Include="menuManager.js" />
+    <Content Include="serverManager.js" />
     <Content Include="sidebar.bmp" />
     <Content Include="package.json" />
     <Content Include="README.md" />
+    <Content Include="updateManager.js" />
+    <Content Include="utils.js" />
   </ItemGroup>
   <Import Project="$(VSToolsPath)\Node.js Tools\Microsoft.NodejsToolsV2.targets" />
 </Project>

+ 246 - 32
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/app.js

@@ -1,43 +1,216 @@
-const { app, BrowserWindow } = require('electron');
+const { app, BrowserWindow, Menu, dialog } = require('electron');
 const { exec } = require('child_process');
 const path = require('path');
 const axios = require('axios');
 const https = require('https');
 const fs = require('fs');
-const AdmZip = require('adm-zip'); // 用于解压 zip 文件
-// 忽略证书的检测
-//app.commandLine.appendSwitch('ignore-certificate-errors'); 原本用于解决SignalR 自签名证书不能使用的问题,将 "@microsoft/signalr": "^8.0.7", 改为  "@microsoft/signalr": "^7.0.14",
+const AdmZip = require('adm-zip'); // 鐢ㄤ簬瑙e帇 zip 鏂囦欢
+// 忽略证书的检测
+//app.commandLine.appendSwitch('ignore-certificate-errors'); 鍘熸湰鐢ㄤ簬瑙e喅SignalR 鑷��鍚嶈瘉涔︿笉鑳戒娇鐢ㄧ殑闂��锛屽皢 "@microsoft/signalr": "^8.0.7", 鏀逛负  "@microsoft/signalr": "^7.0.14",
 const cert = fs.readFileSync('server\\Configs\\cer\\cert.pem');
 const agent = new https.Agent({
     ca: cert,
-    rejectUnauthorized: true, // 启用证书验证
+    rejectUnauthorized: true, // 鍚�敤璇佷功楠岃瘉
 });
-// 定义 Web API 的启动路径和健康检测 URL
+let win = null;
+// 定义 Web API 的启动路径和健康检测 URL
 let serverPath;
 if (app.isPackaged) {
-    // 打包后的路径
+    // 鎵撳寘鍚庣殑璺�緞
     serverPath = path.dirname(app.getPath('exe')) ;
 } else {
-    // 开发环境的路径
+    // 寮€鍙戠幆澧冪殑璺�緞
     serverPath = __dirname;
 }
-console.log('Server path:', serverPath);// 打印路径进行检查
+console.log('Server path:', serverPath);// 打印路径进行检查
 const baseUrl = 'https://exam.habook.local:8888';
+const remoteVersionsUrl = 'https://teammodelos.blob.core.chinacloudapi.cn/0-public/exam-server/versions.json'; // 浜戠�鐗堟湰淇℃伅 URL
+const remoteZipBaseUrl = 'https://teammodelos.blob.core.chinacloudapi.cn/0-public/exam-server'; // 浜戠� zip 鏂囦欢鐨勫熀纭€ URL
 let serverProcess;
-// 读取本地 appsettings.json 文件
+// 璇诲彇鏈�湴 appsettings.json 鏂囦欢
 const getLocalVersion = () => {
     const appSettingsPath = path.join(serverPath, 'server', 'appsettings.json');
+    console.log(appSettingsPath)
     try {
-        const appSettings = JSON.parse(fs.readFileSync(appSettingsPath, 'utf-8'));
-        return appSettings.Version; // 假设 version 字段存储版本号
+        if (fs.existsSync(appSettingsPath)) {
+            const appSettings = JSON.parse(fs.readFileSync(appSettingsPath, 'utf-8'));
+            console.log(appSettings)
+            return appSettings.Version || '1.250101.01'; // 默认版本号
+        }
     } catch (error) {
         console.error('Error reading appsettings.json:', error);
+        return '1.250101.01';
+    }
+};
+// 从云端获取历史版本信息
+const getRemoteVersions = async () => {
+    try {
+        const response = await axios.get(remoteVersionsUrl, {
+            validateStatus: (status) => status === 200 || status === 404, // 鍏佽� 404 鐘舵€佺爜
+            timeout: 5000 // 设置超时时间为 5 秒
+        });
+        if (response.status === 404) {
+            console.error('Remote versions file not found.');
+            return null;
+        }
+        return response.data.versions; // 返回版本号数组
+    } catch (error) {
+        if (error.code === 'ECONNABORTED') {
+            console.error('Request to fetch remote versions timed out.');
+        } else {
+            console.error('Error fetching remote versions:', error);
+        }
         return null;
     }
 };
-// 启动 Web API 的函数
 
-// 检查服务器健康状态的函数
+// 版本号格式化(补全当天版本号为两位数)
+const formatVersion = (version) => {
+    const parts = version.split('.');
+    if (parts.length === 3) {
+        const dayVersion = parts[2].padStart(2, '0'); // 补全当天版本号为两位数
+        return `${parts[0]}.${parts[1]}.${dayVersion}`;
+    }
+    return version;
+};
+// 版本号比较(去掉 . 并转换为数字)
+const compareVersions = (localVersion, remoteVersion) => {
+    const localNumber = parseInt(localVersion.replace(/\./g, ''), 10);
+    const remoteNumber = parseInt(remoteVersion.replace(/\./g, ''), 10);
+    return remoteNumber > localNumber;
+};
+// 涓嬭浇鏂囦欢
+const downloadFile = async (url, outputPath) => {
+    try {
+        const writer = fs.createWriteStream(outputPath);
+
+        console.log(`Downloading file from ${url}...`);
+        const response = await axios({
+            url,
+            method: 'GET',
+            responseType: 'stream',
+            timeout: 10000 // 设置超时时间为 10 秒
+        });
+
+        response.data.pipe(writer);
+
+        await new Promise((resolve, reject) => {
+            writer.on('finish', resolve);
+            writer.on('error', reject);
+        });
+
+        console.log('Download completed.');
+        return true;
+    } catch (error) {
+        if (error.code === 'ECONNABORTED') {
+            console.error('Download timed out.');
+        } else {
+            console.error('Error downloading file:', error);
+        }
+        return false;
+    }
+};
+
+// 检查是否需要更新
+const checkForUpdates = async () => {
+    const localVersion = formatVersion(getLocalVersion());
+    console.log('Local version:', localVersion);
+
+    const remoteVersions = await getRemoteVersions();
+    if (!remoteVersions || remoteVersions.length === 0) {
+        console.log('No remote versions found.');
+        return;
+    }
+
+    // 鑾峰彇鏈€鏂扮増鏈�彿
+    const latestRemoteVersion = formatVersion(remoteVersions[remoteVersions.length - 1]);
+    console.log('Latest remote version:', latestRemoteVersion);
+
+    if (compareVersions(localVersion, latestRemoteVersion)) {
+        const { response } = await dialog.showMessageBox({
+            type: 'info',
+            title: '鐗堟湰鏇存柊',
+            message: `是否更新到(${latestRemoteVersion}) 版本?`,
+            buttons: ['是', '否']
+        });
+
+        if (response === 0) { // 鐢ㄦ埛閫夋嫨 Yes
+            await updateServer(latestRemoteVersion);
+        }
+    } else {
+        console.log('未检测到新版本。');
+        const { response } = await dialog.showMessageBox({
+            type: 'info',
+            title: '鐗堟湰鏇存柊',
+            message: `鏈��娴嬪埌鏂扮増鏈�€俙,
+            buttons: ['鍏抽棴']
+        });
+        createMenu();
+    }
+};
+// 下载并更新 IES.ExamServer.exe
+const updateServer = async (latestVersion) => {
+    try {
+        const zipUrl = `${remoteZipBaseUrl}/server-${latestVersion}.zip`; // 构造下载 URL
+        const zipPath = path.join(serverPath, 'IES.ExamServer.zip');
+
+        // 1. 涓嬭浇鏈€鏂扮殑 IES.ExamServer.zip
+        const downloadSuccess = await downloadFile(zipUrl, zipPath);
+        if (!downloadSuccess) {
+            throw new Error('Failed to download IES.ExamServer.zip.');
+        }
+
+        try {
+            // 2. 鍏抽棴 IES.ExamServer.exe
+            console.log('Shutting down IES.ExamServer...');
+            const response =  await axios.get(`${baseUrl}/index/shutdown`, {
+                httpsAgent: agent,
+                validateStatus: (status) => status === 200 || status === 404, // 鍏佽� 404 鐘舵€佺爜
+                timeout: 5000 // 设置超时时间为 5 秒
+            });
+            if (response.status === 404) {
+                console.error('API: index/shutdown not found.');
+               
+            }
+            if (response.status === 200) {
+                console.log('IES.ExamServer shutdown completed.');
+            }
+        } catch (error) {
+            console.error('Error Shutting down IES.ExamServer...', error);
+        }
+        await delay(1000);
+        // 3. 瑙e帇 IES.ExamServer.zip
+        console.log('Extracting IES.ExamServer.zip...');
+        const zip = new AdmZip(zipPath);
+        zip.extractAllTo(path.join(serverPath, 'server'), true); // 瑕嗙洊瑙e帇
+        console.log('Extraction completed.');
+        // 4. 鍚�姩鏂扮殑 IES.ExamServer.exe
+        console.log('Starting IES.ExamServer...');
+        // 5. 更新菜单栏的当前版本号
+        createMenu(); // 閲嶆柊鍒涘缓鑿滃崟
+        await startServer();
+        console.log('IES.ExamServer started successfully.');
+        const { response } = await dialog.showMessageBox({
+            type: 'info',
+            title: '鐗堟湰鏇存柊',
+            message: `鏇存柊鎴愬姛銆俙,
+            buttons: ['鍏抽棴']
+        });
+        win.loadURL(baseUrl, {
+            agent: agent
+        });
+    } catch (error) {
+        console.error('Error updating server:', error);
+    }
+};
+// 启动 Web API 的函数
+function delay(ms) {
+    return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+ 
+
+// 妫€鏌ユ湇鍔″櫒鍋ュ悍鐘舵€佺殑鍑芥暟
 const checkServerHealth = async () => {
     try {
         const response = await axios.get(`${baseUrl}/index/health`, {
@@ -55,25 +228,25 @@ const checkServerHealth = async () => {
 const startServer = () => {
     return new Promise((resolve, reject) => {
         serverProcess = exec(path.join(serverPath, 'server', 'IES.ExamServer.exe'), {
-            cwd: `${serverPath}/server` // 设置工作目录为 server 目录
+            cwd: `${serverPath}/server` // 设置工作目录为 server 目录
             , stdio: 'inherit'
         });
-        // 监听标准输出
+        // 鐩戝惉鏍囧噯杈撳嚭
         serverProcess.stdout.on('data', (data) => {
             console.log(`Server stdout: ${data}`);
         });
 
-        // 监听标准错误输出
+        // 鐩戝惉鏍囧噯閿欒�杈撳嚭
         serverProcess.stderr.on('data', (data) => {
             console.log(`Server stderr: ${data}`);
         });
 
-        // 监听进程退出事件
+        // 监听进程退出事件
         serverProcess.on('close', (data) => {
             console.log(`Server process exited with code ${data}`);
             reject(new Error(`Server process exited with code ${data}`));
         });
-        // 等待 Web API 启动成功
+        // 绛夊緟 Web API 鍚�姩鎴愬姛
         const checkHealth = async () => {
             try {
                 const response = await axios.get(`${baseUrl}/index/health`, {
@@ -85,21 +258,62 @@ const startServer = () => {
                 }
             } catch (error) {
                 console.log('Waiting for server to start...');
-                setTimeout(checkHealth, 1000); // 每隔 1 秒检查一次
+                setTimeout(checkHealth, 1000); // 每隔 1 秒检查一次
             }
         };
         checkHealth();
     });
 };
 
-// 创建 Electron 窗口的函数
+// 鍒涘缓鑿滃崟
+const createMenu = () => {
+    const localVersion = formatVersion(getLocalVersion());
+    console.log('Local version:', localVersion);
+    // 获取 Electron、Node.js 和 Chrome 的版本
+    const electronVersion = process.versions.electron;
+    const nodeVersion = process.versions.node;
+    const chromeVersion = process.versions.chrome;
+    const appVersion = app.getVersion(); // 鑾峰彇搴旂敤绋嬪簭鐗堟湰
+    const template = [
+        {
+            label: '甯�姪',
+            submenu: [
+                {
+                    label: '妫€鏌ユ洿鏂版湇鍔$�',
+                    click: () => {
+                        checkForUpdates();
+                    }
+                },
+                {
+                    label: `服务端版本: ${localVersion}`
+                   
+                }
+                ,{
+                    label: `搴旂敤绋嬪簭鐗堟湰: ${appVersion}` // 鏄剧ず搴旂敤绋嬪簭鐗堟湰
+                },
+                //{
+                //    label: `Electron 鐗堟湰: ${electronVersion}` // 鏄剧ず Electron 鐗堟湰
+                //},
+                //{
+                //    label: `Node.js 鐗堟湰: ${nodeVersion}` // 鏄剧ず Node.js 鐗堟湰
+                //},
+                {
+                    label: `浏览器内核版本: ${chromeVersion}` // 显示 Chrome 版本
+                }
+            ]
+        }
+    ];
+    const menu = Menu.buildFromTemplate(template);
+    Menu.setApplicationMenu(menu);
+};
+// 创建 Electron 窗口的函数
 const createWindow = async () => {
     try {
         const isServerRunning = await checkServerHealth();
         if (!isServerRunning) {
-            await startServer(); // 启动 Web API
+            await startServer(); // 鍚�姩 Web API
         }
-        const win = new BrowserWindow({
+        win = new BrowserWindow({
             width: 800,
             height: 600,
             webPreferences: {
@@ -117,25 +331,25 @@ const createWindow = async () => {
     }
 };
 
-// 当 Electron 应用准备好时创建窗口
+// 当 Electron 应用准备好时创建窗口
 app.whenReady().then(() => {
     createWindow();
-
+    createMenu();
     app.on('activate', () => {
         if (BrowserWindow.getAllWindows().length === 0) {
             createWindow();
         }
     });
-    // 监听 before-quit 事件,关闭 IES.ExamServer.exe 进程
+    // 监听 before-quit 事件,关闭 IES.ExamServer.exe 进程
     app.on('before-quit', async (event) => {
-        event.preventDefault(); // 阻止默认的退出行为
+        event.preventDefault(); // 阻止默认的退出行为
         if (serverProcess) {
             console.log('Killing server process...');
             serverProcess.kill();
         }
         try {
             console.log('index/shutdown api ...');
-            // 发起 HTTP 请求来关闭.NET Core Web API
+            // 发起 HTTP 请求来关闭.NET Core Web API
             const response = await axios.get(`${baseUrl}/index/shutdown`, {
                 httpsAgent: agent
             });
@@ -143,15 +357,15 @@ app.whenReady().then(() => {
                 console.log('Server is shutdown!');
                 //resolve();
             }
-           // app.quit(); // 关闭 Electron 应用程序
+           // app.quit(); // 鍏抽棴 Electron 搴旂敤绋嬪簭
         } catch (error) {
-            console.error('关闭.NET Core Web API 时出错:', error);
+            console.error('关闭.NET Core Web API 时出错:', error);
         }
     });
 });
 
-// 当所有窗口关闭时退出应用(macOS 除外)
+// 当所有窗口关闭时退出应用(macOS 除外)
 app.on('window-all-closed', function () {
-    // 无论什么平台,都退出程序
+    // 无论什么平台,都退出程序
     app.quit();
 });

+ 31 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/constants.js

@@ -0,0 +1,31 @@
+const fs = require('fs');
+const path = require('path');
+const https = require('https');
+const { app } = require('electron');
+
+const cert = fs.readFileSync('server\\Configs\\cer\\cert.pem');
+const agent = new https.Agent({
+    ca: cert,
+    rejectUnauthorized: true,
+   // secureProtocol: 'TLSv1_2_method', // 强制使用 TLS 1.2
+});
+
+let serverPath;
+if (app.isPackaged) {
+    serverPath = path.dirname(app.getPath('exe'));
+} else {
+    serverPath = __dirname;
+}
+
+const baseUrl = 'https://exam.habook.local:8888';
+//const baseUrl = 'http://exam.habook.local:9999';
+const remoteVersionsUrl = 'https://teammodelos.blob.core.chinacloudapi.cn/0-public/exam-server/versions.json';
+const remoteZipBaseUrl = 'https://teammodelos.blob.core.chinacloudapi.cn/0-public/exam-server';
+
+module.exports = {
+    serverPath,
+    baseUrl,
+    remoteVersionsUrl,
+    remoteZipBaseUrl,
+    agent
+};

+ 69 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/main.js

@@ -0,0 +1,69 @@
+const { app, BrowserWindow } = require('electron');
+const serverManager = require('./serverManager');
+const menuManager = require('./menuManager');
+const updateManager = require('./updateManager');
+const constants = require('./constants');
+//app.disableHardwareAcceleration(); //使用windows7 ,或者虚拟机的时候 需要验证 禁用 GPU 加速
+//app.commandLine.appendSwitch('ignore-certificate-errors')
+let win = null;
+
+// 创建 Electron 窗口的函数
+const createWindow = async () => {
+    try {
+       
+        const isServerRunning = await serverManager.checkServerHealth();
+        if (!isServerRunning) {
+            await serverManager.startServer(); // 启动 Web API
+        }
+        win = new BrowserWindow({
+            width: 800,
+            height: 600,
+            webPreferences: {
+                nodeIntegration: true,
+                contextIsolation: false,
+            },
+        });
+        //win.webContents.session.setCertificateVerifyProc((request, callback) => {
+        //    // 始终返回 0 表示验证通过
+        //    callback(0);
+        //});
+        win.maximize();
+        win.loadURL(constants.baseUrl, {
+            agent: constants.agent
+        });
+
+    } catch (error) {
+        console.error('Error starting server or loading window:', error);
+    }
+};
+// 定义回调函数
+const checkForUpdatesHandler = () => {
+    updateManager.checkForUpdates(win, () => {
+        menuManager.createMenu(checkForUpdatesHandler); // 重新创建菜单并传递回调函数
+    });
+};
+// 当 Electron 应用准备好时创建窗口
+app.whenReady().then(() => {
+    process.env.NODE_OPTIONS = '--tls-min-v1.2';
+    createWindow();
+    // 创建菜单并传递回调函数
+    menuManager.createMenu(checkForUpdatesHandler);
+
+    app.on('activate', () => {
+        if (BrowserWindow.getAllWindows().length === 0) {
+            createWindow();
+        }
+    });
+
+    // 监听 before-quit 事件,关闭 IES.ExamServer.exe 进程
+    app.on('before-quit', async (event) => {
+        event.preventDefault(); // 阻止默认的退出行为
+        await serverManager.shutdownServer();
+        //app.quit(); // 关闭 Electron 应用程序
+    });
+});
+
+// 当所有窗口关闭时退出应用(macOS 除外)
+app.on('window-all-closed', function () {
+    app.quit();
+});

+ 137 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/menuManager.js

@@ -0,0 +1,137 @@
+const { Menu, dialog, BrowserWindow, session } = require('electron');
+//const updateManager = require('./updateManager');
+const constants = require('./constants');
+const utils = require('./utils');
+const path = require('path');
+const fs = require('fs');
+// 创建菜单
+const createMenu = (checkForUpdatesCallback) => {
+    const localVersion = utils.formatVersion(getLocalVersion());
+    console.log('Local version:', localVersion);
+
+    const electronVersion = process.versions.electron;
+    const nodeVersion = process.versions.node;
+    const chromeVersion = process.versions.chrome;
+    const appVersion = require('electron').app.getVersion();
+
+    const template = [
+        {
+            label: '主页',
+            click: () => {
+                const win = BrowserWindow.getFocusedWindow(); // 获取当前获得焦点的窗口
+                console.log('win', win);
+                if (win) {
+                    win.loadURL(constants.baseUrl, {
+                        agent: constants.agent
+                    });
+                } else {
+                    console.warn('没有获得焦点的窗口');
+                }
+            }
+        },
+        {
+            label: '设置',
+            submenu: [
+                {
+                    label: '清理缓存',
+                    click: () => {
+                        clearCache();
+                    }
+                },
+                {
+                    label: '调试模式',
+                    click: () => {
+                        openDevTools();
+                    }
+                }
+            ]
+        },
+        { label: '帮助' },
+        {
+            label: '版本',
+            submenu: [
+                {
+                    label: '检查更新服务端',
+                    click: () => {
+                        if (typeof checkForUpdatesCallback === 'function') {
+                            checkForUpdatesCallback(); // 确保回调函数是函数
+                        } else {
+                            console.error('checkForUpdatesCallback is not a function');
+                        }
+                    }
+                },
+                {
+                    label: `服务端版本: ${localVersion}`
+                },
+                {
+                    label: `应用程序版本: ${appVersion}`
+                },
+                {
+                    label: `浏览器内核版本: ${chromeVersion}`
+                }
+            ]
+        }
+    ];
+
+    const menu = Menu.buildFromTemplate(template);
+    Menu.setApplicationMenu(menu);
+};
+// 开启调试模式
+const openDevTools = () => {
+    const win = BrowserWindow.getFocusedWindow(); // 获取当前获得焦点的窗口
+    if (win) {
+        win.webContents.openDevTools(); // 打开开发者工具
+    } else {
+        console.warn('没有获得焦点的窗口');
+    }
+};
+// 清理缓存
+const clearCache = () => {
+    const win = BrowserWindow.getFocusedWindow(); // 获取当前获得焦点的窗口
+   // console.log('win', win);
+    if (win) {
+        session.defaultSession.clearStorageData({
+            storages: ['cookies', 'localstorage', 'shadercache', 'serviceworkers', 'cachestorage'],
+            quotas: ['temporary', 'persistent', 'syncable'],
+        }, () => {
+            console.log('缓存和其他存储数据已清理');
+            console.log('缓存已清理');
+            dialog.showMessageBox(win, {
+                type: 'info',
+                title: '清理缓存',
+                message: '缓存已清理',
+                buttons: ['确定']
+            });
+        });
+        // 清理缓存
+        //win.webContents.on('did-finish-load', () => {
+        //    win.webContents.clearCache();
+        //    console.log('缓存已清理');
+        //});
+        // 清理默认会话的缓存
+        session.defaultSession.clearCache(() => {
+            console.log('缓存已清理');
+        });
+        win.loadURL(constants.baseUrl, {
+            agent: constants.agent
+        });
+    } else {
+        console.warn('没有获得焦点的窗口');
+    }
+};
+// 获取本地版本号
+const getLocalVersion = () => {
+    const appSettingsPath = path.join(constants.serverPath, 'server', 'appsettings.json');
+    try {
+        if (fs.existsSync(appSettingsPath)) {
+            const appSettings = JSON.parse(fs.readFileSync(appSettingsPath, 'utf-8'));
+            return appSettings.Version || '1.250101.01'; // 默认版本号
+        }
+    } catch (error) {
+        console.error('Error reading appsettings.json:', error);
+        return '1.250101.01';
+    }
+};
+module.exports = {
+    createMenu
+};

+ 2 - 2
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/package.json

@@ -2,7 +2,7 @@
   "name": "examclient",
   "version": "1.0.0",
   "description": "IES.ExamClient",
-  "main": "app.js",
+  "main": "main.js",
   "scripts": {
     "start": "electron .",
     "test": "echo \"Error: no test specified\" && exit 1",
@@ -13,7 +13,7 @@
   "author": "",
   "license": "MIT",
   "devDependencies": {
-    "electron": "20.3.12"
+    "electron": "21.4.4"
   },
   "build": {
     "appId": "exam.habook.local",

+ 89 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/serverManager.js

@@ -0,0 +1,89 @@
+const { exec } = require('child_process');
+const axios = require('axios');
+const fs = require('fs');
+const path = require('path');
+const constants = require('./constants');
+const utils = require('./utils');
+
+let serverProcess;
+
+// 启动 Web API 的函数
+const startServer = () => {
+    return new Promise((resolve, reject) => {
+        serverProcess = exec(path.join(constants.serverPath, 'server', 'IES.ExamServer.exe'), {
+            cwd: `${constants.serverPath}/server`, // 设置工作目录为 server 目录
+            stdio: 'inherit'
+        });
+
+        serverProcess.stdout.on('data', (data) => {
+            console.log(`Server stdout: ${data}`);
+        });
+
+        serverProcess.stderr.on('data', (data) => {
+            console.log(`Server stderr: ${data}`);
+        });
+
+        serverProcess.on('close', (data) => {
+            console.log(`Server process exited with code ${data}`);
+            reject(new Error(`Server process exited with code ${data}`));
+        });
+
+        // 等待 Web API 启动成功
+        const checkHealth = async () => {
+            try {
+                const response = await axios.get(`${constants.baseUrl}/index/health`, {
+                httpsAgent: constants.agent
+                });
+                if (response.status === 200) {
+                    console.log('Server is up and running!');
+                    resolve();
+                }
+            } catch (error) {
+                console.log('Waiting for server to start...', error);
+                setTimeout(checkHealth, 1000); // 每隔 1 秒检查一次
+            }
+        };
+        checkHealth();
+    });
+};
+
+// 检查服务器健康状态的函数
+const checkServerHealth = async () => {
+    try {
+        const response = await axios.get(`${constants.baseUrl}/index/health`, {
+            httpsAgent: constants.agent
+        });
+        if (response.status === 200) {
+            console.log('Server is up and running!');
+            return true;
+        }
+    } catch (error) {
+        console.log('Server is not running yet.', error);
+        return false;
+    }
+};
+
+// 关闭服务器的函数
+const shutdownServer = async () => {
+    if (serverProcess) {
+        console.log('Killing server process...');
+        serverProcess.kill();
+    }
+    try {
+        console.log('index/shutdown api ...');
+        const response = await axios.get(`${constants.baseUrl}/index/shutdown`, {
+            httpsAgent: constants.agent
+        });
+        if (response.status === 200) {
+            console.log('Server is shutdown!');
+        }
+    } catch (error) {
+        console.error('关闭.NET Core Web API 时出错:', error);
+    }
+};
+
+module.exports = {
+    startServer,
+    checkServerHealth,
+    shutdownServer
+};

+ 194 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/updateManager.js

@@ -0,0 +1,194 @@
+const axios = require('axios');
+const fs = require('fs');
+const path = require('path');
+const AdmZip = require('adm-zip');
+const { dialog } = require('electron');
+const constants = require('./constants');
+const utils = require('./utils');
+const serverManager = require('./serverManager');
+//const menuManager = require('./menuManager'); // 引入 menuManager
+// 获取本地版本号
+const getLocalVersion = () => {
+    const appSettingsPath = path.join(constants.serverPath, 'server', 'appsettings.json');
+    try {
+        if (fs.existsSync(appSettingsPath)) {
+            const appSettings = JSON.parse(fs.readFileSync(appSettingsPath, 'utf-8'));
+            return appSettings.Version || '1.250101.01'; // 默认版本号
+        }
+    } catch (error) {
+        console.error('Error reading appsettings.json:', error);
+        return '1.250101.01';
+    }
+};
+
+// 从云端获取历史版本信息
+const getRemoteVersions = async () => {
+    try {
+        const response = await axios.get(constants.remoteVersionsUrl, {
+            validateStatus: (status) => status === 200 || status === 404, // 允许 404 状态码
+            timeout: 5000 // 设置超时时间为 5 秒
+        });
+        if (response.status === 404) {
+            console.error('Remote versions file not found.');
+            return null;
+        }
+        return response.data.versions; // 返回版本号数组
+    } catch (error) {
+        if (error.code === 'ECONNABORTED') {
+            console.error('Request to fetch remote versions timed out.');
+        } else {
+            console.error('Error fetching remote versions:', error);
+        }
+        return null;
+    }
+};
+
+// 下载文件
+const downloadFile = async (url, outputPath) => {
+    try {
+        const writer = fs.createWriteStream(outputPath);
+
+        console.log(`Downloading file from ${url}...`);
+        const response = await axios({
+            url,
+            method: 'GET',
+            responseType: 'stream',
+            timeout: 10000 // 设置超时时间为 10 秒
+        });
+
+        response.data.pipe(writer);
+
+        await new Promise((resolve, reject) => {
+            writer.on('finish', resolve);
+            writer.on('error', reject);
+        });
+
+        console.log('Download completed.');
+        return true;
+    } catch (error) {
+        if (error.code === 'ECONNABORTED') {
+            console.error('Download timed out.');
+        } else {
+            console.error('Error downloading file:', error);
+        }
+        return false;
+    }
+};
+
+// 检查是否需要更新
+const checkForUpdates = async (win, createMenuCallback) => {
+    const localVersion = utils.formatVersion(getLocalVersion());
+    console.log('Local version:', localVersion);
+
+    const remoteVersions = await getRemoteVersions();
+    if (!remoteVersions || remoteVersions.length === 0) {
+        console.log('No remote versions found.');
+        //menuManager.createMenu(win); // 未检测到新版本时更新菜单
+        if (typeof createMenuCallback === 'function') {
+            createMenuCallback(win); // 确保回调函数是函数
+        } else {
+            console.error('createMenuCallback is not a function');
+        }
+        return;
+    }
+
+    // 获取最新版本号
+    const latestRemoteVersion = utils.formatVersion(remoteVersions[remoteVersions.length - 1]);
+    console.log('Latest remote version:', latestRemoteVersion);
+
+    if (utils.compareVersions(localVersion, latestRemoteVersion)) {
+        const { response } = await dialog.showMessageBox({
+            type: 'info',
+            title: '版本更新',
+            message: `是否更新到(${latestRemoteVersion}) 版本?`,
+            buttons: ['是', '否']
+        });
+
+        if (response === 0) { // 用户选择 Yes
+            await updateServer(latestRemoteVersion, win, createMenuCallback);
+        }
+    } else {
+        console.log('未检测到新版本。');
+        const { response } = await dialog.showMessageBox({
+            type: 'info',
+            title: '版本更新',
+            message: `未检测到新版本。`,
+            buttons: ['关闭']
+        });
+        //menuManager.createMenu(win); // 未检测到新版本时更新菜单
+        if (typeof createMenuCallback === 'function') {
+            createMenuCallback(win); // 确保回调函数是函数
+        } else {
+            console.error('createMenuCallback is not a function');
+        }
+    }
+};
+
+// 下载并更新 IES.ExamServer.exe
+const updateServer = async (latestVersion, win, createMenuCallback) => {
+    try {
+        const zipUrl = `${constants.remoteZipBaseUrl}/server-${latestVersion}.zip`; // 构造下载 URL
+        const zipPath = path.join(constants.serverPath, 'IES.ExamServer.zip');
+
+        // 1. 下载最新的 IES.ExamServer.zip
+        const downloadSuccess = await downloadFile(zipUrl, zipPath);
+        if (!downloadSuccess) {
+            throw new Error('Failed to download IES.ExamServer.zip.');
+        }
+
+        try {
+            // 2. 关闭 IES.ExamServer.exe
+            console.log('Shutting down IES.ExamServer...');
+            const response = await axios.get(`${constants.baseUrl}/index/shutdown`, {
+                httpsAgent: constants.agent,
+                validateStatus: (status) => status === 200 || status === 404, // 允许 404 状态码
+                timeout: 5000 // 设置超时时间为 5 秒
+            });
+            if (response.status === 404) {
+                console.error('API: index/shutdown not found.');
+            }
+            if (response.status === 200) {
+                console.log('IES.ExamServer shutdown completed.');
+            }
+        } catch (error) {
+            console.error('Error Shutting down IES.ExamServer...', error);
+        }
+        await utils.delay(1000);
+
+        // 3. 解压 IES.ExamServer.zip
+        console.log('Extracting IES.ExamServer.zip...');
+        const zip = new AdmZip(zipPath);
+        zip.extractAllTo(path.join(constants.serverPath, 'server'), true); // 覆盖解压
+        console.log('Extraction completed.');
+
+        // 4. 启动新的 IES.ExamServer.exe
+        console.log('Starting IES.ExamServer...');
+        await serverManager.startServer();
+        console.log('IES.ExamServer started successfully.');
+        // 5. 重新加载页面
+        if (win) {
+            win.loadURL(constants.baseUrl, {
+                agent: constants.agent
+            });
+        }
+        // 6. 更新菜单栏
+        //menuManager.createMenu(win); // 更新成功后更新菜单
+        if (typeof createMenuCallback === 'function') {
+            createMenuCallback(win); // 确保回调函数是函数
+        } else {
+            console.error('createMenuCallback is not a function');
+        }
+        const { response } = await dialog.showMessageBox({
+            type: 'info',
+            title: '版本更新',
+            message: `更新成功。`,
+            buttons: ['关闭']
+        });
+    } catch (error) {
+        console.error('Error updating server:', error);
+    }
+};
+
+module.exports = {
+    checkForUpdates
+};

+ 27 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/utils.js

@@ -0,0 +1,27 @@
+// 延迟函数
+const delay = (ms) => {
+    return new Promise(resolve => setTimeout(resolve, ms));
+};
+
+// 版本号格式化(补全当天版本号为两位数)
+const formatVersion = (version) => {
+    const parts = version.split('.');
+    if (parts.length === 3) {
+        const dayVersion = parts[2].padStart(2, '0'); // 补全当天版本号为两位数
+        return `${parts[0]}.${parts[1]}.${dayVersion}`;
+    }
+    return version;
+};
+
+// 版本号比较(去掉 . 并转换为数字)
+const compareVersions = (localVersion, remoteVersion) => {
+    const localNumber = parseInt(localVersion.replace(/\./g, ''), 10);
+    const remoteNumber = parseInt(remoteVersion.replace(/\./g, ''), 10);
+    return remoteNumber > localNumber;
+};
+
+module.exports = {
+    delay,
+    formatVersion,
+    compareVersions
+};

文件差异内容过多而无法显示
+ 544 - 544
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/yarn.lock


+ 90 - 28
TEAMModelOS.Extension/IES.Exam/IES.ExamLibrary/Models/EvaluationCommon.cs

@@ -57,11 +57,14 @@ namespace IES.ExamServer.Models
         /// <summary>
         /// AI音乐评测
         /// </summary>
-        public AIMusic? music { get; set; }
+        public MusicAI? music { get; set; }
         /// <summary>
         /// 活动数据包生成最新时间戳
         /// </summary>
         public long dataTime { get; set; }
+        /// <summary>
+        /// 活动数据包大小
+        /// </summary>
         public long dataSize { get; set; }
         /// <summary>
         /// 活动文件包生成最新时间戳
@@ -87,16 +90,19 @@ namespace IES.ExamServer.Models
         /// <summary>
         /// 活动页面代码文件生成最新时间戳
         /// </summary>
-        public long webviewTime { get; set; }
+       // public long webviewTime { get; set; }
         /// <summary>
         /// 活动页面代码文件数量
         /// </summary>
-        public long webviewCount { get; set; }
+       // public long webviewCount { get; set; }
         /// <summary>
         /// 活动页面代码文件大小
         /// </summary>
-        public long webviewSize { get; set; }
-        public string? webviewPath { get; set; }
+      //  public long webviewSize { get; set; }
+        /// <summary>
+        /// 活动页面代码文件路径
+        /// </summary>
+       // public string? webviewPath { get; set; }
         /// <summary>
         /// 学生数量
         /// </summary>
@@ -108,7 +114,7 @@ namespace IES.ExamServer.Models
         /// <summary>
         /// 名单集合
         /// </summary>
-        public List<string> grouplist { get; set; } = new List<string>();
+        public List<EvaluationGroupListDto> grouplist { get; set; } = new List<EvaluationGroupListDto>();
         /// <summary>
         /// 名单哈希值
         /// </summary>
@@ -118,12 +124,20 @@ namespace IES.ExamServer.Models
         /// </summary>
         public string? dataHash { get; set; }
         /// <summary>
-        /// 开卷
+        ///提取
         /// </summary>
         public string? shortCode { get; set; }
-      
+        /// <summary>
+        /// 开卷码
+        /// </summary>
+        public string? openCode { get; set; }
+    }
+
+    public class EvaluationGroupListDto
+    {
+        public string? id { get; set; }
+        public string? name { get; set; }
     }
-  
     public class EvaluationClient : EvaluationMain
     {
         /// <summary>
@@ -137,35 +151,63 @@ namespace IES.ExamServer.Models
         /// <summary>
         /// 临时密码
         /// </summary>
-        public string? password { get; set; }
+        // public string? password { get; set; }
         /// <summary>
         /// 记录地址
         /// </summary>
-        public string? recordUrl { get; set; }
+        //public string? recordUrl { get; set; }
+
+
+
+        //取消以下字段,以防止在更新时丢失数据,将单独的设置信息放在  EvaluationRoundSetting中、
         /// <summary>
         /// 激活状态0未激活,1 激活
         /// </summary>
-        public int activate { get; set; }
-       
-        /// <summary>
-        /// 倒计时类型 0 未设置,1统一以服务器时间为基准介绍,2,以开始作答为基准,开始作答向局域网端发送请求,返回开始作答时间。
-        /// </summary>
-        public int countdownType {  get; set; }
-        /// <summary>
-        /// 倒计时,时长,按毫秒为单位
-        /// </summary>
-        public long countdown { get; set; }
-        /// <summary>
-        /// 截至时间,countdownType=1 时有值
-        /// </summary>
-        public long deadline {  get; set; }
+        //public int activate { get; set; }
+
+        ///// <summary>
+        ///// 倒计时类型 0 未设置,1统一以服务器时间为基准介绍,2,以开始作答为基准,开始作答向局域网端发送请求,返回开始作答时间。
+        ///// </summary>
+        //public int countdownType {  get; set; }
+        ///// <summary>
+        ///// 倒计时,时长,按毫秒为单位
+        ///// </summary>
+        //public long countdown { get; set; }
+        ///// <summary>
+        ///// 截至时间,countdownType=1 时有值
+        ///// </summary>
+        //public long deadline {  get; set; }
+        ///// <summary>
+        ///// 开考时间
+        ///// </summary>
+        //public long startline { get; set; }
+        ///// <summary>
+        ///// 轮次id
+        ///// </summary>
+        //public string? roundId { get; set; }
 
     }
     public class SubjectExam
     {
+        /// <summary>
+        /// 评测id
+        /// </summary>
         public string? examId { get; set; }
+        /// <summary>
+        /// 评测名称
+        /// </summary>
         public string? examName { get; set; }
+        /// <summary>
+        /// 评测科目id
+        /// </summary>
         public string? subjectId { get; set; }
+        /// <summary>
+        /// 乱序作答0 顺序作答,1乱序作答
+        /// </summary>
+        public int disorder { get; set; }
+        /// <summary>
+        /// 评测科目名称
+        /// </summary>
         public string? subjectName { get; set; }
         public List<SubjectExamPaper> papers { get; set; } = new List<SubjectExamPaper>();
     }
@@ -185,8 +227,12 @@ namespace IES.ExamServer.Models
     /// <summary>
     /// AI音乐评测
     /// </summary>
-    public class AIMusic
+    public class MusicAI
     {
+        /// <summary>
+        /// quota_22 的acId  
+        /// </summary>
+        public string? taskId { get; set; }
         /// <summary>
         /// 关联的评测
         /// </summary>
@@ -254,7 +300,7 @@ namespace IES.ExamServer.Models
         /// <summary>
         /// 评测的班级列表
         /// </summary>
-        public List<string> classes { get; set; } = new List<string>();
+        public List<EvaluationGroupListDto> classes { get; set; } = new List<EvaluationGroupListDto>();
         public string? owner { get; set; }
         
         public string? scope {  get; set; }
@@ -264,6 +310,10 @@ namespace IES.ExamServer.Models
         /// 评测类型Exam,投票评选Vote,问卷调查Survey
         /// </summary>
         public string? type { get; set; }
+        /// <summary>
+        /// 乱序作答0 顺序作答,1乱序作答
+        /// </summary>
+        public int disorder { get; set;}
     }
     public class SubjectExamPaper
     {
@@ -283,6 +333,14 @@ namespace IES.ExamServer.Models
         /// 试卷哈希值
         /// </summary>
         public string? paperHash { get; set; }
+        /// <summary>
+        /// 本地路径
+        /// </summary>
+        public string? local { get; set; }
+        /// <summary>
+        /// 题目数量
+        /// </summary>
+        public int questionCount { get; set; }
     }
     public class EvaluationPaper: SubjectExamPaper
     {
@@ -308,10 +366,14 @@ namespace IES.ExamServer.Models
     public class BlobHashInfo 
     {
         /// <summary>
-        /// 文件路径
+        /// 云端blob文件路径
         /// </summary>
         public string? path { get; set; }
         /// <summary>
+        /// 本地存储文件路径
+        /// </summary>
+        public string? local { get; set; }
+        /// <summary>
         /// 文件大小
         /// </summary>
         public long size {  get; set; }

+ 11 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamLibrary/Models/ExamConstant.cs

@@ -10,5 +10,16 @@ namespace IES.ExamLibrary.Models
         public static readonly string ScopeStudent = "student";
         public static readonly string ScopeVisitor = "visitor";
         public static readonly string JwtSecretKey = "fXO6ko/qyXeYrkecPeKdgXnuLXf9vMEtnBC9OB3s+aA=";
+        public static readonly string[] AZ_19NI1O0 = {
+            "A", "B", "C", "D", "E", "F", "G", "H",
+            "J", "K", "L", "M", "N", "P", "Q", "R", 
+            "S", "T", "U", "V", "W", "X", "Y", "Z", 
+            "2", "3", "4", "5", "6", "7", "8", "9" 
+        };
+        public static readonly string[] AZ09 = {
+            "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
+            "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
+            "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
+        };
     }
 }

+ 3 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/versions.json

@@ -0,0 +1,3 @@
+{
+  "versions": [ "1.250205.01", "1.250205.02" ]
+}

+ 22 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/BaseController.cs

@@ -1,4 +1,7 @@
-using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Primitives;
 
 namespace IES.ExamServer.Controllers
 {
@@ -78,6 +81,24 @@ namespace IES.ExamServer.Controllers
             return HttpContext.Request.Headers["Authorization"].ToString();
         }
 
+        /// <summary>
+        /// 取得驗證金鑰,Authorization
+        /// </summary>        
+        public string? GetXAuthToken()
+        {
+            //return HttpContext.Request.Headers["X-Auth-AuthToken"].ToString();
+            try
+            {
+                if (HttpContext.Request.Headers.TryGetValue("X-Auth-AuthToken", out StringValues value))
+                    return value.ToString();
+                else
+                    return null;
+            }
+            catch
+            {
+                return null;
+            }
+        }
         /// <summary>
         /// 取得JWT驗證金鑰,Authorization Bearer
         /// </summary>

+ 63 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/FrameworkController.cs

@@ -0,0 +1,63 @@
+using Microsoft.AspNetCore.Mvc;
+using System.Text.Json.Nodes;
+
+namespace IES.ExamServer.Controllers
+{
+    [ApiController]
+    [Route("controller/framework")]
+    public class FrameworkController : BaseController
+    {
+        public FrameworkController() 
+        {
+        
+        }
+        [HttpGet("doHttpRequest")]
+        public IActionResult DoHttpRequest(JsonNode json )
+        {
+            string ip = $"{json["id"]}";//请求:{"id":"ip"}返回:192.168.56.1
+            return Ok("ok");
+        }
+        /// <summary>
+        /// 本地服务状态检查接⼝(⽹⻚调本地服务接⼝)  完成
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("check")]
+        public IActionResult check()
+        {
+            return Ok(1==1);
+        }
+        [HttpGet("getCacheFile")]
+        public IActionResult GetCacheFile([FromQuery] string name )
+        {
+            return Ok("ok");
+        }
+        [HttpPost("uploadFile")]
+        public IActionResult UploadFile(JsonNode json)
+        {
+            /*
+              {
+                 "fileName": "f824810b-0d3a-4772-85dc-443eb8895955.wav",
+                 "musicQuestionId": "1835",
+                 "thirdAnswerId": "62abefa1-dfd4-419f-8f4d-05b9e9ea3057::82be3962-674c-1172-0caf-b2dcdc2bd935::202106001",
+                 "thirdStudentName": "曾义程",
+                 "thirdSchoolId": "hbcn",
+                 "thirdStudentId": "202106001",
+                 "schoolUniCode": "cb138fa4-31ba-423c-a0b7-c954c9d3c445",
+                 "questionItemId": "249",
+                 "id": "b6141be4c697bd9aa008013a3857e971",
+                 "status": 0,
+                 "name": "f824810b-0d3a-4772-85dc-443eb8895955.wav",
+                 "createTime": 1733389000260
+             }
+            返回正常
+            返回
+             {newurl: fileName}
+            异常
+ 
+            返回
+             false
+             */
+            return Ok("ok");
+        }
+    }
+}

文件差异内容过多而无法显示
+ 235 - 2
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/IndexController.cs


文件差异内容过多而无法显示
+ 812 - 477
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/ManageController.cs


+ 352 - 2
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/StudentController.cs

@@ -1,6 +1,356 @@
-namespace IES.ExamServer.Controllers
+using IES.ExamLibrary.Models;
+using IES.ExamServer.DI;
+using IES.ExamServer.Filters;
+using IES.ExamServer.Helper;
+using IES.ExamServer.Helpers;
+using IES.ExamServer.Models;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Hosting;
+using Microsoft.VisualBasic;
+using System;
+using System.Text.Json.Nodes;
+
+namespace IES.ExamServer.Controllers
 {
-    public class StudentController
+    [ApiController]
+    [Route("student")]
+    public class StudentController : BaseController
     {
+        private readonly IConfiguration _configuration;
+        private readonly IHttpClientFactory _httpClientFactory;
+        private readonly IMemoryCache _memoryCache;
+        private readonly ILogger<IndexController> _logger;
+        private readonly CenterServiceConnectionService _connectionService;
+        private readonly LiteDBFactory _liteDBFactory;
+        public StudentController(ILogger<IndexController> logger, IConfiguration configuration, IHttpClientFactory httpClientFactory,
+         IMemoryCache memoryCache, CenterServiceConnectionService connectionService, LiteDBFactory liteDBFactory)
+        {
+            _logger = logger;
+            _configuration=configuration;
+            _httpClientFactory=httpClientFactory;
+            _memoryCache=memoryCache;
+            _connectionService=connectionService;
+            _liteDBFactory=liteDBFactory;
+        }
+
+        /// <summary>
+        ///  学生提交科目作答
+        /// </summary>
+        /// <param name="json"></param>
+        /// <returns></returns>
+        [HttpPost("answer-subject-result")]
+        [AuthToken("student")]
+        public IActionResult AnswerSubjectResult(JsonNode json)
+        {
+            List<List<string>>? answers = json["answer"]?.ToObject<List<List<string>>>();
+            string evaluationId = $"{json["evaluationId"]}";
+            string examId = $"{json["examId"]}";
+            string subjectId = $"{json["subjectId"]}";
+            string paperId = $"{json["paperId"]}";
+            int costTime = int.Parse($"{json["costTime"]}");
+            string settingId = $"{json["settingId"]}";
+            var token = GetAuthTokenInfo();
+            EvaluationRoundSetting? setting = _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationRoundSetting>().FindOne(x => x.id!.Equals(settingId) && evaluationId.Equals(x.evaluationId) && x.activate==1);
+            EvaluationClient? evaluationClient = _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationClient>().FindOne(x=>evaluationId.Equals(x.id));
+            if (evaluationClient!=null && setting!=null && setting.activate==1)
+            {
+                string resultId = ShaHashHelper.GetSHA1(evaluationId+_connectionService?.serverDevice?.school?.id+token.id);
+                EvaluationStudentResult studentResult = _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationStudentResult>()
+                .FindOne(x => resultId.Equals(x.id) && token.id.Equals(x.studentId) && evaluationId.Equals(x.evaluationId));
+                if (studentResult!=null)
+                {
+                    long now = DateTimeOffset.Now.ToUnixTimeMilliseconds();
+                    //判断开始时间
+                    if ((setting.startline>0 && setting.startline>now)|| evaluationClient.stime>now)
+                    {
+                        //未到开始时间
+                        return Ok(new { msg = "未到开始时间。", code = 1 });
+                    }
+
+                    //判断截止时间
+                    long deadline;
+                    if (setting.countdownType==2)
+                    {
+
+                        deadline= studentResult.startTime+setting.countdown;
+                    }
+                    else
+                    {
+                        deadline= setting.startline+setting.countdown;
+
+                    }
+                    deadline+= 10*60*1000;//漂移10分钟,允许学生延迟提交,但是前端页面显示的截止时间是准的,时间一到,可自动提交。
+                    if ((deadline>0&&deadline<now)|| evaluationClient.etime<now)
+                    {
+                        //已过截止时间
+                        return Ok(new { msg = "已过截止时间。", code = 2 });
+                    }
+                    if (!string.IsNullOrWhiteSpace(subjectId)  && !string.IsNullOrWhiteSpace(examId) && !string.IsNullOrWhiteSpace(paperId)&& costTime>0)
+                    {
+                       
+                        if (answers!.IsNotEmpty())
+                        {
+                            var subjectExams= evaluationClient.subjects.FindAll(x => examId.Equals(x.examId)&& subjectId.Equals(x.subjectId));
+                            var papers= subjectExams.FirstOrDefault()?.papers.FindAll(x => paperId.Equals(x.paperId));
+                            if (papers.IsNotEmpty())
+                            {
+                                if (answers!.Count()!=papers!.First()?.questionCount) 
+                                {
+                                    return Ok(new { msg = "提交的答案数量与试卷题目数量不匹配。", code = 5 });
+                                }
+                            }
+                            string subjectResultId = ShaHashHelper.GetSHA1(evaluationClient.id+examId+subjectId+token.id);
+                            var subjectResult = studentResult.subjectResults.Where(x => subjectResultId.Equals(x.id) &&  subjectId.Equals(x.subjectId)
+                                    && examId.Equals(x.examId) && paperId.Equals(x.paperId)).FirstOrDefault();
+                            if (subjectResult!=null)
+                            {
+                                subjectResult.answers=answers;
+                                subjectResult.finished=1;
+                                subjectResult.costTime=costTime;
+                                subjectResult.submitTime=now;
+                                subjectResult.pushed=0;//强制重新推送
+                            }
+                            EvaluationSubjectResult result = new EvaluationSubjectResult()
+                            {
+                                id = subjectResultId,
+                                evaluationId = evaluationId,
+                                examId = examId,
+                                examName = subjectResult?.examName,
+                                subjectId = subjectId,
+                                subjectName = subjectResult?.subjectName,
+                                paperId =paperId,
+                                paperName = subjectResult?.paperName,
+                                createTime=now,
+                                finished=1,
+                                costTime=costTime,
+                                submitTime=now,
+                                answers=answers,
+                                questionCount=papers.IsNotEmpty()? papers!.First().questionCount : 0,
+                                pushed=0//强制重新推送
+                            };
+                            _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationSubjectResult>().Upsert(result);
+
+                            if (_connectionService!.centerIsConnected)
+                            {
+
+                            }
+                        }
+                       
+                    }
+                    return Ok(new { code = 200, studentResult = studentResult, msg = "提交成功!" });
+                }
+                else { 
+                    return Ok(new { msg = "未找到该学生的作答信息。", code = 4});
+                }
+
+            }
+            else 
+            {
+                return Ok(new { msg = "未匹配到正则开考的评测。", code = 3 });
+            }
+           
+
+           
+        }
+
+
+
+        /// <summary>
+        /// 获取学生当前考试的作答信息。
+        /// </summary>
+        /// <param name="json"></param>
+        /// <returns></returns>
+        [HttpPost("load-evaluation-result")]
+        [AuthToken("student")]
+        public IActionResult LoadEvaluationResult(JsonNode json)
+        {
+            string evaluationId = $"{json["evaluationId"]}";
+            var token = GetAuthTokenInfo();//6af32bbd-144e-4366-8bc0-61ba4c85677c
+            string resultId = ShaHashHelper.GetSHA1(evaluationId+_connectionService?.serverDevice?.school?.id+token.id);
+            string? scoolId = _connectionService?.serverDevice?.school?.id;
+            EvaluationStudentResult studentResult = _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationStudentResult>()
+                .FindOne(x=> resultId.Equals(x.id) && !string.IsNullOrWhiteSpace(x.schoolId) &&  x.schoolId.Equals(scoolId)&& token.id.Equals(x.studentId) && evaluationId.Equals(x.evaluationId));
+            if (studentResult!=null)
+            {
+                //标记开始作答
+                if (studentResult.finished<1)
+                {
+                    studentResult.finished=1;
+                }
+                if (studentResult.startTime<=0) 
+                {
+                    studentResult.startTime=DateTimeOffset.Now.ToUnixTimeMilliseconds();
+                }
+                _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationStudentResult>().Update(studentResult);
+                return Ok(new { code = 200, studentResult = studentResult });
+            }
+            else { 
+                return Ok(new { msg = "未找到该学生的作答信息。", code = 404});
+            }
+        }
+
+        /// <summary>
+        /// 登录
+        /// </summary>
+        /// <param name="json"></param>
+        /// <returns></returns>
+        [HttpPost("login")]
+        public IActionResult Login(JsonNode json) 
+        {
+            string studentId = $"{json["studentId"]}";
+            string studentName = $"{json["studentName"]}";
+            string evaluationId = $"{json["evaluationId"]}";
+            string settingId = $"{json["settingId"]}";
+            EvaluationRoundSetting? setting = _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationRoundSetting>().FindOne(x => x.id!.Equals(settingId) && evaluationId.Equals(x.evaluationId) && x.activate==1);
+            EvaluationClient? evaluationClient = null;
+            if (setting!=null)
+            {
+                evaluationClient = _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationClient>().FindOne(x => x.id!.Equals(setting.evaluationId));
+                if (evaluationClient!=null)
+                {
+                    //检查是否在作答时间内
+                    (code, msg)=  CheckActivate(evaluationClient, setting);
+                    if (code==200)
+                    {
+                        long time = DateTimeOffset.Now.ToUnixTimeMilliseconds();
+                        _memoryCache.TryGetValue(Constant._KeyServerDevice, out ServerDevice? server);
+                        School? school = null;
+                        ///获取名单文件,解析信息,应该在设置开考轮次的时候进行解析,并放在数据库中,并且在此时看是否是需要 分配试卷,还是在登录的时候获取试卷。
+                        if (server!=null)
+                        {
+                            school = server.school;
+                        }
+                        EvaluationMember? member = _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationMember>().FindOne(x => studentId.Equals(x.id)&& studentName.Equals(x.name));
+                        if (member!=null)
+                        {
+                            if (evaluationId.Equals(member.evaluationId))
+                            {
+                                string x_auth_token = JwtAuthExtension.CreateAuthToken("www.teammodel.cn", studentId, studentName, picture: string.Empty, ExamConstant.JwtSecretKey, ExamConstant.ScopeStudent, 8, schoolID: school?.id, new string[] { "student" }, expire: 1);
+                                return Ok(new { code = 200, x_auth_token = x_auth_token });
+                            }
+                            else
+                            {
+
+                                return Ok(new { msg = "当前考试未添加该学生!", code = 7 });
+                            }
+
+                        }
+                        else
+                        {
+                           return Ok(new { msg = "学者账号和姓名不匹配!", code = 1 });
+                        }
+
+                    }
+                    else { 
+                        return Ok(new { msg = msg, code = code });
+                    }
+                }
+                else { 
+                    return Ok(new { msg = "未找到考试设置。", code = 6 });
+                }
+            }
+            else { 
+                return Ok(new { msg = "未找到考试设置。", code = 2 });
+            }
+        }
+        /// <summary>
+        /// 学生端获取激活的考试
+        /// </summary>
+        /// <param name="json"></param>
+        /// <returns></returns>
+        [HttpPost("get-activate-evaluation")]
+        public   IActionResult GetActivateEvaluation(JsonNode json) 
+        {
+            //  _connectionService.serverDevice.school.id?
+
+            try
+            {
+                IEnumerable<EvaluationRoundSetting>? settings = _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationRoundSetting>().Find(x =>x.activate==1 );
+                if (settings != null && settings.Count() > 0) 
+                {
+                    if (settings.Count()>1)
+                    {
+                        msg="有多个正在开考的评测,请监考教师重新设置开考评测。";
+                        code=2;
+                    }
+                    else 
+                    {
+                        EvaluationRoundSetting? setting = settings.First(); ;
+
+                        EvaluationClient evaluationClient = _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationClient>().FindOne(x => x.id ==setting.evaluationId);
+                        if (evaluationClient!=null)
+                        {
+                            (code ,msg)= CheckActivate(evaluationClient, setting);
+                            var anonymousObject = new Dictionary<string, object?>();
+                            var properties = evaluationClient.GetType().GetProperties();
+                            foreach (var property in properties)
+                            {
+                                if (!property.Name.Equals("shortCode") && !property.Name.Equals("openCode"))
+                                {
+                                    anonymousObject[property.Name] = property.GetValue(evaluationClient);
+                                }
+                            }
+                            return Ok(new { evaluationClient = anonymousObject, code = code, msg = msg, setting });
+                        }
+                        else 
+                        {
+                            msg="未找到评测信息。";
+                            code=6;
+                        }
+                        //foreach (var subject in evaluationClient.subjects)
+                        //{
+                        //    foreach (var paper in subject.papers)
+                        //    {
+                        //        paper.blob=$"package/{evaluationClient.id}/papers/{paper.paperId}";
+                        //    }
+                        //}
+                      
+                    }
+                }
+                else
+                {
+                    msg="暂无正在开考的评测";
+                    code=1;
+                }
+            }
+            catch (Exception ex) { }
+            return Ok(new { msg ,code});
+        }
+
+        private (int code, string msg) CheckActivate(EvaluationClient evaluationClient, EvaluationRoundSetting setting)
+        {
+            code = 200;
+            if (evaluationClient.scope!.Equals("school"))
+            {
+                if (!evaluationClient!.ownerId!.Equals($"{_connectionService.serverDevice?.school?.id}"))
+                {
+                    msg="授权学校与评测归属学校不一致。";
+                    code=3;
+                }
+            }
+            //当前时间
+            long now = DateTimeOffset.Now.ToUnixTimeMilliseconds();
+            //if (setting.countdownType>0)
+            //{
+               
+            //}
+            //else
+            //{
+               
+            //}
+            if ((setting.startline>0 &&setting.startline>now) || evaluationClient.stime>now)
+            {
+                msg="评测暂未开始。";
+                code=4;
+            }
+            if ((setting.deadline>0 && setting.deadline<now)|| evaluationClient.etime<now)
+            {
+                msg="评测已经结束。";
+                code=5;
+            }
+            return (code, msg);
+        }
+       
     }
 }

+ 0 - 33
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/WeatherForecastController.cs

@@ -1,33 +0,0 @@
-using Microsoft.AspNetCore.Mvc;
-
-namespace IES.ExamServer.Server.Controllers
-{
-    [ApiController]
-    [Route("[controller]")]
-    public class WeatherForecastController : ControllerBase
-    {
-        private static readonly string[] Summaries = new[]
-        {
-            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
-        };
-
-        private readonly ILogger<WeatherForecastController> _logger;
-
-        public WeatherForecastController(ILogger<WeatherForecastController> logger)
-        {
-            _logger = logger;
-        }
-
-        [HttpGet]
-        public IEnumerable<WeatherForecast> Get()
-        {
-            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
-            {
-                Date = DateTime.Now.AddDays(index),
-                TemperatureC = Random.Shared.Next(-20, 55),
-                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
-            })
-            .ToArray();
-        }
-    }
-}

+ 24 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/CenterServiceConnectionService.cs

@@ -6,9 +6,33 @@ namespace IES.ExamServer.DI
     {
         private bool _centerIsConnected;
         private bool _notifyIsConnected;
+       // private bool _musicIsConnected;
         public string? centerUrl { get; set; }
         public string? notifyUrl { get; set; }
+        /// <summary>
+        /// 音乐播放服务
+        /// "MusicAIServer": {
+        ///    "MusicUrl": "https://musicapi.winteach.cn/api/v1",
+        ///    "AppId": "8a68f563f3384662acbc268336b98ae2",
+        ///    "KeyAES": "GcRHG7pGgepXXOOU",
+        ///    "IvAES": "W5yt6WthEs2mQlSn"
+        ///  }
+        /// "MusicAIServer": {
+        ///    "MusicUrl": "https://tmdapi.yosocloud.com/api/v1",
+        ///    "AppId": "8a68f563f3384662acbc268336b98ae2",
+        ///    "KeyAES": "GcRHG7pGgepXXOOU",
+        ///    "IvAES": "W5yt6WthEs2mQlSn"
+        ///  }
+        /// </summary>
+        public string? musicUrl { get; set; }
+        public string? loginToken { get; set; }
+
         public ServerDevice? serverDevice {  get; set; }
+        public bool musicIsConnected
+        { get;set;
+            //get { return string.IsNullOrWhiteSpace(musicUrl) ? false : _musicIsConnected; }
+            //set { _musicIsConnected = value; }
+        }
         public bool notifyIsConnected
         {
             get { return string.IsNullOrWhiteSpace(notifyUrl) ? false : _notifyIsConnected; }

+ 2 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/LiteDBFactory.cs

@@ -30,6 +30,7 @@ namespace IES.ExamServer.DI
 
         public LiteDatabase GetLiteDatabase(string name = "Master")
         {
+            //return new LiteDatabase(_optionsMonitor.Get(name).Connectionstring);
             return LiteDatabases.GetOrAdd(name, x => new LiteDatabase(_optionsMonitor.Get(name).Connectionstring));
         }
     }
@@ -46,7 +47,7 @@ namespace IES.ExamServer.DI
             if (connectionstrings == null) throw new ArgumentNullException(nameof(connectionstrings));
 
 
-            services.TryAddSingleton<LiteDBFactory>();
+            services.TryAddTransient<LiteDBFactory>();
             //多个连接字符串注入
             connectionstrings.ForEach(connection =>
             {

+ 1 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/ServiceInitializer.cs

@@ -112,6 +112,7 @@ namespace IES.ExamServer.DI
                     throw new Exception($"Failed to download data. Status code: {message.StatusCode}");
                 }
             }
+            _connectionService.musicUrl = _configuration.GetValue<string>("ExamServer:MusicUrl");
             _connectionService.notifyUrl = notify == 1 ? notifyUrl : null;
             _connectionService.notifyIsConnected = notify == 1;
             // 单例模式存储云端数据中心连接状态

+ 178 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/SubjectPushService.cs

@@ -0,0 +1,178 @@
+using IES.ExamServer.Helpers;
+using IES.ExamServer.Models;
+using LiteDB;
+using System;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Channels;
+
+namespace IES.ExamServer.DI
+{
+    public class SubjectPushService: BackgroundService
+    {
+        private readonly DataQueue _dataQueue;
+        private readonly IHttpClientFactory _httpClientFactory;
+        private readonly LiteDBFactory _liteDBFactory;
+        private readonly CenterServiceConnectionService _connectionService;
+        private readonly IConfiguration _configuration;
+        public SubjectPushService(DataQueue dataQueue, IHttpClientFactory httpClientFactory, LiteDBFactory liteDBFactory, CenterServiceConnectionService connectionService, IConfiguration configuration)
+        {
+            
+            _dataQueue = dataQueue;
+            _httpClientFactory = httpClientFactory;
+            _liteDBFactory = liteDBFactory;
+            _connectionService = connectionService;
+            _configuration= configuration;
+
+        }
+        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+        {
+            ///数据中心链接了,开始推送数据
+            if (_connectionService.centerIsConnected) 
+            {
+                // 启动时加载未推送的数据
+                //await LoadUnpushedDataAsync(stoppingToken);
+            }
+            while (!stoppingToken.IsCancellationRequested)
+            {
+                // 从 Channel 中读取数据
+                if (await _dataQueue.Reader.WaitToReadAsync(stoppingToken))
+                {
+                    while (_dataQueue.Reader.TryRead(out var data))
+                    {
+
+                        await PushSubjectResultDataToCloudAsync(data, stoppingToken);
+                       
+                    }
+                }
+            }
+        }
+        
+        private async Task PushSubjectResultDataToCloudAsync((EvaluationStudentResult studentResult, EvaluationSubjectResult subjectResult, string resultId)data, CancellationToken cancellationToken)
+        {
+            if (_connectionService.centerIsConnected) 
+            {
+                try
+                {
+                    //  var json = JsonSerializer.Serialize(data);
+                    //var content = new StringContent(json, Encoding.UTF8, "application/json");
+                    var httpClient = _httpClientFactory.CreateClient();
+
+                    string url = $"{_connectionService.centerUrl}/common/exam/upsert-new-record";
+                    if (httpClient.DefaultRequestHeaders.Contains("X-Auth-AuthToken")) {
+                        httpClient.DefaultRequestHeaders.Remove("X-Auth-AuthToken");
+                    }
+                    httpClient.DefaultRequestHeaders.Add("X-Auth-AuthToken", _connectionService.loginToken);
+                    var response = await httpClient.PostAsJsonAsync(url, new { 
+                        id =data.subjectResult.examId,
+                        answer=data.subjectResult.answers,
+                        subjectId=data.subjectResult.subjectId,
+                        classId=data.studentResult.classId,
+                        ownerId=data.studentResult.ownerId,
+                        paperId=data.subjectResult.paperId,
+                        studentId=data.studentResult.studentId,
+                        studentName=data.studentResult.studentName,
+                    }, cancellationToken);
+
+                    if (response.IsSuccessStatusCode)
+                    {
+                        MarkDataAsPushed(data);
+                        Console.WriteLine($"Data {data.resultId} pushed successfully.");
+                    }
+                    else
+                    {
+                        Console.WriteLine($"Failed to push data {data.resultId}. Retrying...");
+                        // 推送失败,重新加入队列
+                        await _dataQueue.Writer.WriteAsync(data, cancellationToken);
+                    }
+                }
+                catch (Exception ex)
+                {
+                    Console.WriteLine($"Error pushing data {data.resultId}: {ex.Message}");
+                    // 推送失败,重新加入队列
+                    await _dataQueue.Writer.WriteAsync(data, cancellationToken);
+                }
+            }
+        }
+        private void MarkDataAsPushed((EvaluationStudentResult studentResult, EvaluationSubjectResult subjectResult, string resultId) data)
+        {
+            var collection = _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationStudentResult>();
+            EvaluationSubjectResult? subjectResult = data.studentResult.subjectResults.Where(x => x.id!.Equals(data.resultId)).FirstOrDefault();
+            if (subjectResult!=null) 
+            {
+                subjectResult.pushed=1;
+                collection.Upsert(data.studentResult);
+                var collectionSub = _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationSubjectResult>();
+                var unpushedData = collectionSub.FindOne(x => data.resultId.Equals(x.id));
+                if (unpushedData!=null) 
+                {
+                    unpushedData.pushed=1;
+                    _dataQueue.MarkAsProcessed((unpushedData.id!, unpushedData.examId!, unpushedData.evaluationId!, unpushedData.subjectId!,unpushedData, data.studentResult));
+                }
+            }
+            
+        }
+        private async Task LoadUnpushedDataAsync(CancellationToken cancellationToken)
+        {
+            var collection = _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationSubjectResult>();
+            var unpushedData = collection.Find(x => x.pushed!=1);
+            foreach (var data in unpushedData)
+            {
+                var studentResults  = _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationStudentResult>().Find(x => x.subjectResults.Where(x => x.id!.Equals(data.id)).IsNotEmpty()).Distinct();
+                // 将未推送的数据写入 Channel
+                foreach (var studentResult in studentResults)
+                {
+                    await _dataQueue.WriteAsync((studentResult!,data, data.id!), cancellationToken); 
+                }
+            }
+        }
+    }
+
+    public class DataQueue
+    {
+        private readonly Channel<(EvaluationStudentResult studentResult, EvaluationSubjectResult subjectResult, string resultId)> _channel;
+        private readonly HashSet<(string resultId,string examId,string evaluationId,string subjectId, EvaluationSubjectResult subjectResult, EvaluationStudentResult studentResult)> _uniqueIds; // 用于维护唯一性
+
+        public async Task<bool> TryAddAsync((string resultId, string examId, string evaluationId, string subjectId, EvaluationSubjectResult subjectResult, EvaluationStudentResult studentResult) data, CancellationToken cancellationToken = default)
+        {
+            var key = (data.resultId, data.examId,data.evaluationId,data.subjectId,data.subjectResult,data.studentResult); // 复合键
+            lock (_uniqueIds)
+            {
+                if (_uniqueIds.Contains(key))
+                {
+                    return false;
+                }
+                _uniqueIds.Add(key);
+            }
+
+            await _channel.Writer.WriteAsync((data.studentResult,data.subjectResult, data.resultId), cancellationToken);
+            return true;
+        }
+        public DataQueue() {
+            // 创建一个无界 Channel
+            _channel = Channel.CreateUnbounded<(EvaluationStudentResult studentResult, EvaluationSubjectResult subjectResult, string resultId)>();
+            _uniqueIds = new HashSet<(string resultId, string examId, string evaluationId, string subjectId, EvaluationSubjectResult subjectResult, EvaluationStudentResult studentResult)>();
+        }
+        public ChannelWriter<(EvaluationStudentResult studentResult, EvaluationSubjectResult subjectResult, string resultId)> Writer => _channel.Writer;
+        public ChannelReader<(EvaluationStudentResult studentResult, EvaluationSubjectResult subjectResult, string resultId)> Reader => _channel.Reader;
+
+        // 批量写入数据
+        public async Task WriteAsync(( EvaluationStudentResult studentResult, EvaluationSubjectResult subjectResult, string resultId) data, CancellationToken cancellationToken = default)
+        {
+            await _channel.Writer.WriteAsync((data.studentResult,data.subjectResult, data.resultId), cancellationToken);
+        }
+        // 获取当前队列中的未上传数据数量
+        public int GetPendingCount()
+        {
+            return _channel.Reader.Count;
+        }
+        // 从队列中移除数据时,同时从 HashSet 中移除 ID
+        public void MarkAsProcessed((string resultId, string examId, string evaluationId, string subjectId, EvaluationSubjectResult subjectResult, EvaluationStudentResult studentResult) key)
+        {
+            lock (_uniqueIds) // 加锁确保线程安全
+            {
+                _uniqueIds.Remove(key);
+            }
+        }
+    }
+}

+ 76 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/AESHelper.cs

@@ -0,0 +1,76 @@
+using IES.ExamServer.Helper;
+using System;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.Json;
+
+namespace IES.ExamServer.Helpers
+{
+    public class AESHelper
+    {
+        private static readonly byte[] Key = Encoding.UTF8.GetBytes(Constant._MusicAIServerAESKey);
+        private static readonly byte[] IV = Encoding.UTF8.GetBytes(Constant._MusicAIServerAESIv);
+
+        // 解密方法
+        public static string Decrypt(string encryptedText)
+        {
+            Console.WriteLine("解密前的内容:", encryptedText);
+            if (string.IsNullOrEmpty(encryptedText))
+            {
+                return encryptedText;
+            }
+
+            using (Aes aesAlg = Aes.Create())
+            {
+                aesAlg.Key = Key;
+                aesAlg.IV = IV;
+                aesAlg.Mode = CipherMode.CBC;
+                aesAlg.Padding = PaddingMode.PKCS7;
+
+                ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
+
+                byte[] encryptedBytes = Convert.FromBase64String(encryptedText);
+                byte[] decryptedBytes = decryptor.TransformFinalBlock(encryptedBytes, 0, encryptedBytes.Length);
+
+                string decryptedText = Encoding.UTF8.GetString(decryptedBytes);
+                Console.WriteLine("解密后的内容:", decryptedText);
+
+                if (decryptedText.StartsWith("{"))
+                {
+                    return JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(decryptedText));
+                }
+                else
+                {
+                    return decryptedText;
+                }
+            }
+        }
+
+        // 加密方法
+        public static string Encrypt(string plainText)
+        {
+            Console.WriteLine("加密前的内容:", plainText);
+            if (string.IsNullOrEmpty(plainText))
+            {
+                return plainText;
+            }
+
+            using (Aes aesAlg = Aes.Create())
+            {
+                aesAlg.Key = Key;
+                aesAlg.IV = IV;
+                aesAlg.Mode = CipherMode.CBC;
+                aesAlg.Padding = PaddingMode.PKCS7;
+
+                ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
+
+                byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);
+                byte[] encryptedBytes = encryptor.TransformFinalBlock(plainTextBytes, 0, plainTextBytes.Length);
+
+                string encryptedText = Convert.ToBase64String(encryptedBytes);
+                Console.WriteLine("加密后的内容:", encryptedText);
+                return encryptedText;
+            }
+        }
+    }
+}

+ 6 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/Constant.cs

@@ -12,7 +12,7 @@ namespace IES.ExamServer.Helper
         public static readonly string _KeyServerDevice = "Server:Device:Info";
         public static readonly string _KeySignalRClientClients = "SignalRClient:Clients";
         public static readonly string _KeySignalRClientConnects = "SignalRClient:Connects";
-       
+        public static readonly string _KeyEvaluationZipExtract = "Evaluation:Zip:Extract";
         public static readonly string _X_Auth_AuthToken = "X-Auth-AuthToken";
         public static readonly string _Message_grant_type_check_file = "check_file";
         public static readonly string _Message_grant_type_ies_qrcode_login = "ies_qrcode_login";
@@ -27,5 +27,10 @@ namespace IES.ExamServer.Helper
         public static readonly int _Message_status_info = 0;
         public static readonly int _Message_status_success = 1;
         public static readonly int _Message_status_warning = 2;
+        #region 智音AI音乐相关常量
+        public static readonly string _MusicAIServerAppId = "8a68f563f3384662acbc268336b98ae2";
+        public static readonly string _MusicAIServerAESKey = "GcRHG7pGgepXXOOU";
+        public static readonly string _MusicAIServerAESIv = "W5yt6WthEs2mQlSn";
+        #endregion
     }
 }

+ 31 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/FileHelper.cs

@@ -2,6 +2,37 @@
 {
     public class FileHelper
     {
+        /// <summary>
+        /// 删除整个文件夹及其内容
+        /// </summary>
+        /// <param name="folderPath"></param>
+        /// <returns></returns>
+        public static bool DeleteFolder(string folderPath) 
+        {
+            bool result = false;
+            try
+            {
+                if (Directory.Exists(folderPath))
+                {
+                    // 删除文件夹及其所有内容
+                    Directory.Delete(folderPath, true);
+                    result = true;
+                    //Console.WriteLine($"Folder '{folderPath}' deleted successfully.");
+                }
+                else
+                {
+                   //Console.WriteLine($"Folder '{folderPath}' does not exist.");
+                    result = true;
+                }
+            }
+            catch (Exception ex)
+            {
+                result = false;
+               //Console.WriteLine($"Error: {ex.Message}");
+            }
+            return result;
+
+        }
         /// <summary>
         /// 列出文件夹下的所有子文件及子文件夹的子文件
         /// </summary>

+ 1 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/JwtAuthExtension.cs

@@ -46,7 +46,7 @@ namespace IES.ExamServer
                 { "roles",roles}, // 登入者的角色,角色類型 (Admin、Teacher、Student) 
                 { "scope",scope},  //登入者的入口类型。 (teacher 教师端登录的醍摩豆ID、tmduser学生端登录的醍摩豆ID、student学生端登录校内账号的学生ID)
                 { "timezone",timezone},
-               
+                { JwtRegisteredClaimNames.Jti,Guid.NewGuid().ToString()}
             };
             // 建立一組對稱式加密的金鑰,主要用於 JWT 簽章之用
             var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(salt));

+ 170 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/ZipHelper.cs

@@ -0,0 +1,170 @@
+
+using System;
+using System.IO;
+using ICSharpCode.SharpZipLib.Zip;
+using ICSharpCode.SharpZipLib.Core;
+namespace IES.ExamServer.Helpers
+{
+    public static class ZipHelper
+    {
+        /*
+        static void Main(string[] args)
+        {
+            string sourceDirectory = "path/to/your/source/directory";
+            string zipFilePath = "protected.zip";
+            string extractPath = "path/to/extract/folder";
+            string password = "yourpassword";
+
+            // 创建带密码的 ZIP 文件
+            bool zipResult = ZipHelper.CreatePasswordProtectedZip(sourceDirectory, zipFilePath, password);
+            if (zipResult)
+            {
+                Console.WriteLine("压缩成功!");
+            }
+            else
+            {
+                Console.WriteLine("压缩失败!");
+            }
+
+            // 解压带密码的 ZIP 文件
+            bool extractResult = ZipHelper.ExtractPasswordProtectedZip(zipFilePath, extractPath, password);
+            if (extractResult)
+            {
+                Console.WriteLine("解压成功!");
+            }
+            else
+            {
+                Console.WriteLine("解压失败!");
+            }
+        }
+        */
+
+        /// <summary>
+        /// 创建带密码的 ZIP 文件
+        /// </summary>
+        /// <param name="sourceDirectory">要压缩的目录路径</param>
+        /// <param name="zipFilePath">生成的 ZIP 文件路径</param>
+        /// <param name="password">ZIP 文件密码</param>
+        /// <returns>是否成功</returns>
+        public static (bool res, string msg) CreatePasswordProtectedZip(string sourceDirectory, string zipFilePath, string password)
+        {
+            if (!Directory.Exists(sourceDirectory))
+            {
+               // Console.WriteLine("源目录不存在。");
+                return (false, "源目录不存在。");
+            }
+
+            try
+            {
+                using (FileStream fsOut = File.Create(zipFilePath))
+                using (ZipOutputStream zipStream = new ZipOutputStream(fsOut))
+                {
+                    zipStream.SetLevel(0); // 设置压缩级别 (0-9)
+                    zipStream.Password = password; // 设置密码
+
+                    CompressFolder(sourceDirectory, zipStream, "");
+                }
+
+                //Console.WriteLine($"ZIP 文件已成功创建:{zipFilePath}");
+                return (true, $"ZIP 文件已成功创建")  ;
+            }
+            catch (Exception ex)
+            {
+              // Console.WriteLine($"创建 ZIP 文件时发生错误:{ex.Message}");
+                return (false, $"创建 ZIP 文件时发生错误!");
+            }
+        }
+
+        /// <summary>
+        /// 解压带密码的 ZIP 文件
+        /// </summary>
+        /// <param name="zipFilePath">ZIP 文件路径</param>
+        /// <param name="extractPath">解压目标目录</param>
+        /// <param name="password">ZIP 文件密码</param>
+        /// <returns>是否成功</returns>
+        public static (bool res,string msg) ExtractPasswordProtectedZip(string zipFilePath, string extractPath, string password)
+        {
+            if (!File.Exists(zipFilePath))
+            {
+                return (false, "ZIP 文件不存在。");
+            }
+            try
+            {
+                using (ZipInputStream zipInputStream = new ZipInputStream(File.OpenRead(zipFilePath)))
+                {
+                    zipInputStream.Password = password; // 设置解压密码
+
+                    ZipEntry entry;
+                    while ((entry = zipInputStream.GetNextEntry()) != null)
+                    {
+                        string entryFileName = entry.Name;
+                        string fullPath = Path.Combine(extractPath, entryFileName);
+
+                        // 创建目录(如果条目是目录)
+                        string? directoryName = Path.GetDirectoryName(fullPath);
+                        if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName))
+                        {
+                            Directory.CreateDirectory(directoryName);
+                        }
+
+                        // 如果是文件,则解压文件
+                        if (!entry.IsDirectory)
+                        {
+                            using (FileStream streamWriter = File.Create(fullPath))
+                            {
+                                byte[] buffer = new byte[4096];
+                                int bytesRead;
+                                while ((bytesRead = zipInputStream.Read(buffer, 0, buffer.Length)) > 0)
+                                {
+                                    streamWriter.Write(buffer, 0, bytesRead);
+                                }
+                            }
+                        }
+                    }
+                }
+
+                return  (true, $"ZIP 文件已成功解压到:{extractPath}");
+            }
+            catch (Exception ex)
+            {
+                return (false, $"解压 ZIP 文件时发生错误:{ex.Message}");
+            }
+        }
+
+        /// <summary>
+        /// 递归压缩文件夹
+        /// </summary>
+        private static void CompressFolder(string path, ZipOutputStream zipStream, string folderName)
+        {
+            foreach (string file in Directory.GetFiles(path))
+            {
+                string fileName = Path.GetFileName(file);
+                string relativePath = Path.Combine(folderName, fileName);
+
+                ZipEntry entry = new ZipEntry(relativePath);
+                entry.DateTime = DateTime.Now;
+                zipStream.PutNextEntry(entry);
+
+                using (FileStream fs = File.OpenRead(file))
+                {
+                    byte[] buffer = new byte[4096];
+                    int bytesRead;
+                    while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
+                    {
+                        zipStream.Write(buffer, 0, bytesRead);
+                    }
+                }
+
+                zipStream.CloseEntry();
+            }
+
+            // 递归处理子目录
+            foreach (string folder in Directory.GetDirectories(path))
+            {
+                string folderNameOnly = Path.GetFileName(folder);
+                string newFolderName = Path.Combine(folderName, folderNameOnly);
+                CompressFolder(folder, zipStream, newFolderName);
+            }
+        }
+    }
+}

+ 4 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/IES.ExamServer.csproj

@@ -17,18 +17,21 @@
   </ItemGroup>
 
   <ItemGroup>
+	<PackageReference Include="Hardware.Info" Version="101.0.1" />
 	<PackageReference Include="LiteDB" Version="5.0.21" />
 	<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.36" />
 	<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.36" />
 	<PackageReference Include="NLog" Version="5.3.4" />
 	<PackageReference Include="NLog.Extensions.Logging" Version="5.3.15" />
+	<PackageReference Include="SharpZipLib" Version="1.4.2" />
 	<PackageReference Include="SkiaSharp.QrCode" Version="0.7.0" />
 	<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
-	<PackageReference Include="System.Management" Version="6.0.2" />
     <PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="6.0.36" />
+    <PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" />
   </ItemGroup>
   <ItemGroup>
 	<Folder Include="Logs\DataLogs\" />
+	
   </ItemGroup>
   <ItemGroup>
     <Content Update="appsettings.Development.json">

+ 0 - 9
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Models/ErrorViewModel.cs

@@ -1,9 +0,0 @@
-namespace IES.ExamServer.Models
-{
-    public class ErrorViewModel
-    {
-        public string? RequestId { get; set; }
-
-        public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
-    }
-}

+ 65 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Models/EvaluationRound.cs

@@ -0,0 +1,65 @@
+namespace IES.ExamServer.Models
+{
+    /// <summary>
+    /// 评测开考轮次
+    /// </summary>
+    public class EvaluationRound
+    {
+
+    }
+    public class EvaluationRoundSetting
+    {
+
+        /// <summary>
+        /// 开考轮次信息id= sha1(评测id_班级id集合)
+        /// </summary>
+        public string? id { get; set; }
+        /// <summary>
+        /// 评测id
+        /// </summary>
+        public string? evaluationId { get; set; }
+        public List<EvaluationGroupListDto> groupList { get; set; } = new List<EvaluationGroupListDto>();
+        public int activate { get; set; }
+        /// <summary>
+        /// 倒计时类型 0 未设置,1统一以服务器时间为基准介绍,2,以开始作答为基准,开始作答向局域网端发送请求,返回开始作答时间。
+        /// </summary>
+        public int countdownType { get; set; }
+        /// <summary>
+        /// 倒计时,时长,按毫秒为单位
+        /// </summary>
+        public long countdown { get; set; }
+        /// <summary>
+        /// 截至时间,countdownType=1 时有值
+        /// </summary>
+        public long deadline { get; set; }
+        /// <summary>
+        /// 开考时间
+        /// </summary>
+        public long startline { get; set; }
+        /// <summary>
+        /// 创建时间
+        /// </summary>
+        public long createTime { get; set; }
+        /// <summary>
+        /// 学生数量
+        /// </summary>
+        public int studentCount { get; set; }
+        /// <summary>
+        /// 班级数量
+        /// </summary>
+        public int classCount { get; set; }
+        /// <summary>
+        /// 已完成人数
+        /// </summary>
+        public int finisheCount { get; set; }
+        /// <summary>
+        /// 进行中  剩下的是未开始
+        /// </summary>
+        public int doingCount { get; set; }
+        /// <summary>
+        /// 乱序作答0 顺序作答,1乱序作答,此字段仅供监考教师设置,如果评测要求是乱序,监考教师则不能修改为顺序作答,只能是从顺序作答改为乱序作答。
+        /// </summary>
+        public int disorder { get; set; }
+
+    }
+}

+ 314 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Models/EvaluationStudent.cs

@@ -0,0 +1,314 @@
+namespace IES.ExamServer.Models
+{
+    public class EvaluationMember
+    {
+        /// <summary>
+        /// 账号id
+        /// </summary>
+        public string? id { get; set; }
+        /// <summary>
+        /// 学校
+        /// </summary>
+        public string? schoolId { get; set; }
+        /// <summary>
+        /// 名称
+        /// </summary>
+        public string? name { get; set; }
+        /// <summary>
+        ///类型 1 tmdid,2 student 3 simple(簡易課程名單)
+        /// </summary>
+        public int type { get; set; }
+        /// <summary>
+        /// 性别 M( male,男) F (female 女)  N(secret 保密) 
+        /// </summary>
+        public string? gender { get; set; }
+        /// <summary>
+        /// 行政班
+        /// </summary>
+        public string? classId { get; set; }
+        /// <summary>
+        /// 行政班名称
+        /// </summary>
+        public string? className { get; set; }
+        /// <summary>
+        /// 学段
+        /// </summary>
+        public string? periodId { get; set; }
+        /// <summary>
+        /// 评测id
+        /// </summary>
+        public string? evaluationId { get; set; }
+        /// <summary>
+        /// 开考轮次的id
+        /// </summary>
+        public string? roundId { get; set; }
+    }
+    public class EvaluationGroupList
+    {
+        /// <summary>
+        /// 名单id
+        /// </summary>
+        public string? id { get; set; }
+        /// <summary>
+        /// 名称
+        /// </summary>
+        public string? name { get; set; }
+        /// <summary>
+        /// 学段
+        /// </summary>
+        public string? periodId { get; set; }
+        /// <summary>
+        /// 名单所属范围 school,private
+        /// </summary>
+        public string? scope { get; set; }
+        /// <summary>
+        /// 学校
+        /// </summary>
+        public string? school { get; set; }
+        /// <summary>
+        ///教学班teach ,行政班(学生搜寻classId动态返回)class 
+        /// </summary>
+        public string type { get; set; } = "class";
+        /// <summary>
+        /// 成员
+        /// </summary>
+        public List<EvaluationMember> members { get; set; } = new List<EvaluationMember>();
+    }
+    public abstract class EvaluationStudent
+    {
+        /// <summary>
+        /// sha1(evaluationId-schoolId-studentId)
+        /// </summary>
+        public string? id { get; set; }
+        public string? schoolId { get; set; }
+
+        /// 本次考试 的id
+        /// </summary>
+        public string? evaluationId { get; set; }
+        /// <summary>
+        /// 关联的区级评测id
+        /// </summary>
+        public string? pid { get; set; }
+        /// <summary>
+        /// 开考轮次的id
+        /// </summary>
+        //public string? roundId { get; set; }
+        /// <summary>
+        /// 学生id
+        /// </summary>
+        public string? studentId { get; set; }
+        /// <summary>
+        /// 学生姓名
+        /// </summary>
+        public string? studentName { get; set; }
+        /// <summary>
+        /// 学生头像
+        /// </summary>
+        //public string? studentPicture { get; set; }
+        /// <summary>
+        /// 班级id
+        /// </summary>
+        public string? classId { get; set; }
+        /// <summary>
+        /// 班级名称
+        /// </summary>
+        public string? className { get; set; }
+       
+        /// <summary>
+        /// 提交时间,单位毫秒
+        /// </summary>
+        //public long submitTime { get; set; }
+       
+        
+        public string? ownerId { get; set; }
+        /// <summary>
+        /// 数据范围
+        /// </summary>
+        public string? scope { get; set; }
+        /// <summary>
+        /// 类型: Exam 普通评测, Art艺术评测
+        /// </summary>
+        public string? type { get; set; }
+    }
+
+    public class EvaluationStudentResult : EvaluationStudent
+    {
+        /// <summary>
+        /// 是否作答完成0 未开始(未登录,最终状态变为缺考),1未完成(已经登录,倒计时结束,需要自动提交。未完成的,需要教师手动推送(联网的情况)。),2已完成(完成作答,直接推送),3缺考(教师手动设置缺考的学生,或者全部推送时,批量设置。)
+        /// </summary>
+        public int finished { get; set; }
+        /// <summary>
+        /// 开考时间,单位毫秒
+        /// </summary>
+        public long startTime { get; set; }
+        /// <summary>
+        /// 是否推送0 未推送,1推送
+        /// </summary>
+        public int pushed { get; set; }
+        //public HashSet<string> subjectResultIds= new HashSet<string>();
+        public HashSet<EvaluationSubjectResult> subjectResults { get; set; } = new HashSet<EvaluationSubjectResult>();
+        public EvaluationMusicAIResult? musicAIResult { get; set; }
+        public EvaluationVoteResult? voteResult { get; set; }
+        public EvaluationSurveyResult? surveyResult { get; set; }
+    }
+
+    public class EvaluationStudentPaper 
+    {
+        /// <summary>
+        /// 作答结果id=sha1(学生id+evaluationId+examId+subjectId)
+        /// </summary>
+        public string? id { get; set; }
+        /// <summary>
+        /// 学生id
+        /// </summary>
+        public string? studentId { get; set; }
+        /// <summary>
+        /// 学生姓名
+        /// </summary>
+        public string? studentName { get; set; }
+        public string? classId { get; set; }
+        public string? className { get; set; }
+        /// <summary>
+        /// 评测id
+        /// </summary>
+        public string? evaluationId { get; set; }
+        /// <summary>
+        /// 科目对应的评测id
+        /// </summary>
+        public string? examId { get; set; }
+        /// <summary>
+        /// 科目对应的评测名称
+        /// </summary>
+        public string? examName { get; set; }
+        /// <summary>
+        /// 科目id
+        /// </summary>
+        public string? subjectId { get; set; }
+        /// <summary>
+        /// 科目名称
+        /// </summary>
+        public string? subjectName { get; set; }
+        /// <summary>
+        /// 试卷id
+        /// </summary>
+        public string? paperId { get; set; }
+        /// <summary>
+        /// 试卷名称
+        /// </summary>
+        public string? paperName { get; set; }
+        public int questionCount { get; set;}
+    }
+
+    public abstract class EvaluationResult 
+    {
+
+        /// <summary>
+        /// 作答耗时,单位毫秒
+        /// </summary>
+        public long costTime { get; set; }
+        /// <summary>
+        /// 0 未作答,1 已经作答。2缺考
+        /// </summary>
+        public int finished { get; set; }
+       
+        /// <summary>
+        /// 评测id
+        /// </summary>
+        public string? evaluationId { get; set; }
+        /// <summary>
+        /// 提交时间,单位毫秒
+        /// </summary>
+        public long submitTime { get; set; }
+
+        public long createTime { get; set; }
+        /// <summary>
+        /// 是否推送 0 未推送 1 已推送
+        /// </summary>
+        public int pushed { get; set; }
+    }
+
+    /// <summary>
+    /// AI音乐作答结果
+    /// </summary>
+    public class EvaluationMusicAIResult :  EvaluationResult
+    {
+        /// <summary>
+        /// quota_22 的acId  
+        /// </summary>
+        public string? taskId { get; set; }
+        
+        /// <summary>
+        /// AI 音乐评测题目id
+        /// </summary>
+        public string? questionId { get; set; }
+        /// <summary>
+        /// AI 音乐评测题目名称
+        /// </summary>
+        public string? questionName { get; set; }
+    }
+   
+
+    /// <summary>
+    /// 评测科目作答结果
+    /// </summary>
+    public class EvaluationSubjectResult :EvaluationResult
+    {
+        /// <summary>
+        /// 作答结果id=sha1(学生id+evaluationId+examId+subjectId)
+        /// </summary>
+        public string? id { get; set; }
+        /// <summary>
+        /// 科目对应的评测id
+        /// </summary>
+        public string? examId { get; set; }
+        /// <summary>
+        /// 评测名称
+        /// </summary>
+        public string ? examName { get; set; }
+        /// <summary>
+        /// 科目id
+        /// </summary>
+        public string? subjectId { get; set; }
+        /// <summary>
+        /// 科目名称
+        /// </summary>
+        public string? subjectName { get; set; }
+        /// <summary>
+        /// 试卷id
+        /// </summary>
+        public string? paperId { get; set; }
+        /// <summary>
+        /// 试卷名称
+        /// </summary>
+        public string? paperName { get; set; }
+        /// <summary>
+        /// 学生答案
+        /// </summary>
+        public List<List<string>>? answers { get; set; } = new List<List<string>>();
+        /// <summary>
+        /// 题目数量
+        /// </summary>
+        public int questionCount { get; set; }
+    }
+    /// <summary>
+    /// 投票作答结果
+    /// </summary>
+    public class EvaluationVoteResult : EvaluationResult
+    {
+        /// <summary>
+        /// 作答结果id= +sha1(evaluationId_Vote_学生id)
+        /// </summary>
+        //待完善
+    }
+    /// <summary>
+    /// 评测问卷作答结果
+    /// </summary>
+    public class EvaluationSurveyResult : EvaluationResult
+    {
+        /// <summary>
+        /// 作答结果id= +sha1(evaluationId_Survey_学生id)
+        /// </summary>
+        //待完善
+    }
+
+}

+ 1 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Models/Teacher.cs

@@ -6,6 +6,7 @@ using System.Threading.Tasks;
 
 namespace IES.ExamServer.Models
 {
+    public class EvaluationTeacher { }
     public class Teacher
     {
         public string? id {  get; set; }

+ 59 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Models/MusicAiRecord.cs

@@ -0,0 +1,59 @@
+namespace IES.ExamServer.Models
+{
+    public class MusicAiRecord
+    {
+        /// <summary>
+        ///  文件名
+        /// </summary>
+        public string? fileName { get; set; }
+        /// <summary>
+        /// 问题id
+        /// </summary>
+        public string? musicQuestionId { get; set; }
+        /// <summary>
+        /// 醍摩豆传输的答案id
+        /// </summary>
+        public string? thirdAnswerId { get; set; }
+        /// <summary>
+        /// 学生名字
+        /// </summary>
+        public string? thirdStudentName { get; set; }
+        /// <summary>
+        /// 醍摩豆学校编码
+        /// </summary>
+        public string? thirdSchoolId { get; set; }
+        /// <summary>
+        /// 醍摩豆学生id
+        /// </summary>
+        public string? thirdStudentId { get; set; }
+        /// <summary>
+        /// 智音学校编码
+        /// </summary>
+        public string? schoolUniCode { get; set; }
+        /// <summary>
+        /// 音乐id
+        /// </summary>
+        public string? questionItemId { get; set; }
+        /// <summary>
+        /// 数据id
+        /// </summary>
+        public string? id { get; set; }
+        /// <summary>
+        /// 状态
+        /// </summary>
+        public int status { get; set; }
+        /// <summary>
+        /// 文件名
+        /// </summary>
+        public string? name { get; set; }
+        /// <summary>
+        /// 创建时间
+        /// </summary>
+        public long createTime { get; set; }
+        /// <summary>
+        /// 阿里云OSS文件地址
+        /// </summary>
+        public string? recordUrl { get; set;}
+    }
+    
+}

+ 16 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Program.cs

@@ -10,6 +10,7 @@ using System.Text.Encodings.Web;
 using System.Text.Unicode;
 using static System.Net.Mime.MediaTypeNames;
 using System.Diagnostics;
+using System.Security.Authentication;
 namespace IES.ExamServer.Server
 {
     public class Program
@@ -20,7 +21,17 @@ namespace IES.ExamServer.Server
             var builder = WebApplication.CreateBuilder(args);
 
             // Add services to the container.
-
+            builder.WebHost.ConfigureKestrel(serverOptions =>
+            {
+                // 配置支持的 SSL/TLS 协议版本
+                serverOptions.ConfigureHttpsDefaults(httpsOptions =>
+                {
+                    httpsOptions.SslProtocols = SslProtocols.Tls |
+                                              SslProtocols.Tls11 |
+                                              SslProtocols.Tls12 |
+                                              SslProtocols.Tls13;
+                });
+            });
             string path = $"{builder.Environment.ContentRootPath}/Configs";
             // Add services to the container.
             builder.Configuration.SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
@@ -56,6 +67,10 @@ namespace IES.ExamServer.Server
             // 注册 ConnectionService 为单例
             builder.Services.AddSingleton<CenterServiceConnectionService>();
             builder.Services.AddSingleton<ServiceInitializer>();
+            // 注册 DataQueue 服务
+            builder.Services.AddSingleton<DataQueue>();
+            // 注册后台服务
+            builder.Services.AddHostedService<SubjectPushService>();
             builder.Services.AddCors(options =>
             {
                 //options.AddDefaultPolicy(

+ 39 - 12
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Services/IndexService.cs

@@ -4,18 +4,51 @@ using System.Diagnostics;
 using System.Net.NetworkInformation;
 using System.Net;
 using System.Runtime.InteropServices;
-using System.Text.Json;
 using System.Text.Json.Nodes;
 using System.Text.RegularExpressions;
-using System.Management;
 using IES.ExamServer.Models;
-using System.CodeDom.Compiler;
 using System.Text;
+using IES.ExamServer.DI;
+using IES.ExamServer.Helper;
 
 namespace IES.ExamServer.Services
 {
     public static class IndexService
     {
+
+        public static async Task<(string content_response, HttpStatusCode code)> GetMusicServer_thirdLocalCacheConfig(IHttpClientFactory httpClientFactory, CenterServiceConnectionService _connectionService , string requestBody)
+        {
+            string content_response = string.Empty;
+            HttpStatusCode code = HttpStatusCode.OK ;
+            try {
+                string url = _connectionService!.musicUrl!;
+                var client = httpClientFactory.CreateClient();
+                //client.DefaultRequestHeaders.Add("Accept", "application/json, text/plain, */*");
+                client.DefaultRequestHeaders.Add("appId", Constant._MusicAIServerAppId);
+                client.DefaultRequestHeaders.Add("encrypt", "1");
+                client.DefaultRequestHeaders.Add("schoolCode", _connectionService.serverDevice?.school?.id);
+                var content = new StringContent(requestBody, Encoding.UTF8, "application/json");
+                // 发送 PUT 请求
+                HttpResponseMessage response = await client.PutAsync($"{url}/musicLocalCache/thirdLocalCacheConfig", content);
+                // 处理响应
+
+                if (response.IsSuccessStatusCode)
+                {
+                    content_response = await response.Content.ReadAsStringAsync();
+                    //Console.WriteLine("Response: " + responseData);
+                }
+                else
+                {
+                    //   Console.WriteLine("Error: " + response.StatusCode);
+                    code=response.StatusCode;
+                    content_response = await response.Content.ReadAsStringAsync();
+                }
+            } catch (Exception ex) {
+                code= HttpStatusCode.InternalServerError;
+                content_response = ex.Message;
+            }
+            return (content_response, code);
+        }
         public static ServerDevice GetServerDevice( string remote,string region)
         {
             string hostName = $"{Environment.UserName}-{Dns.GetHostName()}";
@@ -297,25 +330,19 @@ namespace IES.ExamServer.Services
             //{
             //    throw new Exception("未获取到端口信息!");
             //}
-            var networks = device.networks;
+            var networks = device.networks.ToList();
             if (device.networks.IsNotEmpty()) 
             {
                var order=  device.networks.OrderByDescending(x => x.physical).ToList();
                 for (int i=0; i<order.Count();i++) 
                 {
-                    if (i==0)
-                    {
-                        order[i].domain="exam.habook.local";
-                    }
-                    else {
-                        order[i].domain=$"exam{i}.habook.local";
-                    }
+                    order[i].domain="exam.habook.local";
                 }
                 //优先以物理网卡来生成hash,如果没有则以所有网卡生成hash
                 var physical = order.FindAll(x => x.physical==1);
                 if (physical.IsNotEmpty())
                 {
-                    networks.AddRange(physical);
+                    networks=physical;
                 }
             }
             StringBuilder sb= new StringBuilder();

+ 448 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Services/ManageService.cs

@@ -0,0 +1,448 @@
+using IES.ExamServer.DI;
+using IES.ExamServer.DI.SignalRHost;
+using IES.ExamServer.Helper;
+using IES.ExamServer.Helpers;
+using IES.ExamServer.Models;
+using Microsoft.AspNetCore.SignalR;
+using Microsoft.Extensions.Caching.Memory;
+using System.IO;
+using System.Net.Http;
+using System.Text.Json.Nodes;
+using System.Text.RegularExpressions;
+
+namespace IES.ExamServer.Services
+{
+    public class ManageService
+    {
+
+        public async static Task<(EvaluationClient? evaluationCloud, string centerCode, string centerMsg)> GetEvaluationFromCenter(string? x_auth_token, IConfiguration _configuration,IHttpClientFactory _httpClientFactory, string shortCode, string evaluationId)
+        {
+            EvaluationClient? evaluationCloud = null;
+            string centerCode = string.Empty, centerMsg = string.Empty;
+            string? CenterUrl = _configuration.GetValue<string>("ExamServer:CenterUrl");
+            var client = _httpClientFactory.CreateClient();
+            if (client.DefaultRequestHeaders.Contains(Constant._X_Auth_AuthToken))
+            {
+                client.DefaultRequestHeaders.Remove(Constant._X_Auth_AuthToken);
+            }
+            client.DefaultRequestHeaders.Add(Constant._X_Auth_AuthToken, x_auth_token);
+            try
+            {
+                HttpResponseMessage message = await client.PostAsJsonAsync($"{CenterUrl}/evaluation-sync/find-sync-info", new { shortCode, evaluationId });
+                if (message.IsSuccessStatusCode)
+                {
+                    string content = await message.Content.ReadAsStringAsync();
+                    JsonNode? jsonNode = content.ToObject<JsonNode>();
+                    if (jsonNode != null)
+                    {
+                        centerCode = $"{jsonNode["code"]}";
+                        centerMsg = $"{jsonNode["msg"]}";
+                        if ($"{jsonNode["code"]}".Equals("200"))
+                        {
+                            evaluationCloud = jsonNode["evaluation"]?.ToObject<EvaluationClient>();
+                        }
+                    }
+                    else
+                    {
+                        centerCode = "500";
+                        centerMsg = "数据转换异常";
+                    }
+                }
+                else
+                {
+                    centerCode = $"{message.StatusCode}";
+                    centerMsg = "数据中心访问异常";
+                }
+            }
+            catch (Exception ex)
+            {
+                centerCode = $"500";
+                centerMsg = $"数据中心访问异常:{ex.Message}";
+            }
+            return (evaluationCloud, centerCode, centerMsg);
+        }
+
+        /// <summary>
+        /// 检查本地评测文件,文件包,名单信息
+        /// </summary>
+        /// <param name="evaluationLocal"></param>
+        /// <param name="evaluationCloud"></param>
+        /// <param name="msgs"></param>
+        /// <param name="_liteDBFactory"></param>
+        /// <returns></returns>
+        public async static Task<(List<string> successMsgs, List<string> errorMsgs)> CheckFile(EvaluationClient evaluationLocal, List<string> successMsgs,List<string> errorMsgs, IHubContext<SignalRExamServerHub> _signalRExamServerHub, 
+            IMemoryCache _memoryCache, ILogger _logger, string deviceId,string evaluationPath)
+        {
+           
+        
+            int msg_status = Constant._Message_status_info;
+            string content = msg_status.Equals(Constant._Message_status_success) ? "成功" : "失败";
+           
+            if (!Directory.Exists(evaluationPath)) 
+            {
+                
+                errorMsgs.Add($"评测目录不存在:{evaluationPath}");
+            }
+            else
+            {
+                string evaluationDataPath = Path.Combine(evaluationPath, "data");
+                string path_groupList = Path.Combine(evaluationDataPath, "groupList.json");
+                msg_status =Constant._Message_status_info;
+                if (!System.IO.File.Exists(path_groupList))
+                {
+                    msg_status=Constant._Message_status_error;
+                }
+                else
+                {
+                    msg_status=Constant._Message_status_success;
+                   
+                    
+                }
+                await _signalRExamServerHub.SendMessage(_memoryCache, _logger, deviceId, Constant._Message_grant_type_check_file,
+                    new MessageContent { dataId=evaluationLocal.id, dataName=evaluationLocal.name, messageType= Constant._Message_type_check, status=msg_status, content="评测名单文件(groupList.json)" });
+                content = msg_status.Equals(Constant._Message_status_success) ? "成功" : "失败";
+                if (msg_status.Equals(Constant._Message_status_success)|| msg_status.Equals(Constant._Message_status_info))
+                {
+                    successMsgs.Add($"评测名单文件(groupList.json),检测结果:{content}");
+                }
+                else 
+                {
+                    errorMsgs.Add($"评测名单文件(groupList.json),检测结果:{content}");
+                }
+                string path_source = Path.Combine(evaluationDataPath, "source.json");
+                msg_status = Constant._Message_status_info;
+                if (!System.IO.File.Exists(path_source))
+                {
+                    msg_status=Constant._Message_status_error;
+                   
+                   
+                }
+                else
+                {
+                    msg_status=Constant._Message_status_success;
+                   
+                    
+                }
+                await _signalRExamServerHub.SendMessage(_memoryCache, _logger, deviceId, Constant._Message_grant_type_check_file,
+                    new MessageContent { dataId=evaluationLocal.id, dataName=evaluationLocal.name, messageType= Constant._Message_type_check, status=msg_status, content="评测原始数据(source.json)" });
+                content = msg_status.Equals(Constant._Message_status_success) ? "成功" : "失败";
+               
+                if (msg_status.Equals(Constant._Message_status_success)|| msg_status.Equals(Constant._Message_status_info))
+                {
+                    successMsgs.Add($"评测原始数据(source.json),检测结果:{content}");
+                }
+                else
+                {
+                    errorMsgs.Add($"评测原始数据(source.json),检测结果:{content}");
+                }
+                msg_status =Constant._Message_status_info;
+                try
+                {
+                    //TODO 重整本地文件路径。 文件可能不存在D:\VisualStudioProjects\TEAMModelOS\TEAMModelOS.Extension\IES.Exam\IES.ExamServer\wwwroot\package\exam\6af32bbd-144e-4366-8bc0-61ba4c85677c\evaluation.json
+                    string path_evaluation = Path.Combine(evaluationDataPath, "evaluation.json");
+                    if (!System.IO.File.Exists(path_evaluation))
+                    {
+                        msg_status=Constant._Message_status_error;
+                       
+                       
+                    }
+                    else
+                    {
+                        msg_status=Constant._Message_status_success;
+                       
+                        
+                    }
+                    //数据格式:  [消息][信息/错误/警告][15:43]=>[开始检查评测信息文件...]
+                    //数据格式:  [检查][成功/失败][15:43]=>[评测数据文件:/wwwroot/package/623a9fe6-5445-0938-ff77-aeb80066ef27/evaluation.json]
+                    //数据格式:  [下载][成功/失败][15:43]=>[评测数据文件:/wwwroot/package/623a9fe6-5445-0938-ff77-aeb80066ef27/evaluation.json][1024kb][15ms]
+                    await _signalRExamServerHub.SendMessage(_memoryCache, _logger, deviceId, Constant._Message_grant_type_check_file,
+                        new MessageContent { dataId=evaluationLocal.id, dataName=evaluationLocal.name, messageType= Constant._Message_type_check, status=msg_status, content="评测数据文件(evaluation.json)" });
+                    content = msg_status.Equals(Constant._Message_status_success) ? "成功" : "失败";
+                    if (msg_status.Equals(Constant._Message_status_success)|| msg_status.Equals(Constant._Message_status_info))
+                    {
+                        successMsgs.Add($"评测数据文件(evaluation.json),检测结果:{content}");
+                    }
+                    else
+                    {
+                        errorMsgs.Add($"评测数据文件(evaluation.json),检测结果:{content}");
+                    }
+                    if (System.IO.File.Exists(path_evaluation))
+                    {
+                        string evaluation_str = await System.IO.File.ReadAllTextAsync(path_evaluation);
+                        JsonNode? evaluation_data = evaluation_str.ToObject<JsonNode>();
+                        if (evaluation_data==null)
+                        {
+                           
+                           
+                            msg_status=Constant._Message_status_error;
+                            await _signalRExamServerHub.SendMessage(_memoryCache, _logger, deviceId, Constant._Message_grant_type_check_file,
+                                new MessageContent { dataId=evaluationLocal.id, dataName=evaluationLocal.name, messageType= Constant._Message_type_check, status=msg_status, content="评测数据文件(evaluation.json),文件读取失败!" });
+                            errorMsgs.Add("评测数据文件(evaluation.json),文件读取失败!");
+                        }
+                        else
+                        {
+                            EvaluationClient? evaluationClient = evaluation_data["evaluationClient"]?.ToObject<EvaluationClient>();
+                            if (evaluationClient!=null)
+                            {
+                               
+                                
+                                await _signalRExamServerHub.SendMessage(_memoryCache, _logger, deviceId, Constant._Message_grant_type_check_file,
+                                    new MessageContent { dataId=evaluationLocal.id, dataName=evaluationLocal.name, messageType=Constant._Message_type_message, status=msg_status, content="评测数据文件(evaluation.json),读取评测基本信息..." });
+                                successMsgs.Add($"评测数据文件(evaluation.json),读取评测基本信息...");
+                                if (!string.IsNullOrWhiteSpace(evaluationLocal.blobHash) && evaluationLocal.blobHash.Equals(evaluationClient.blobHash)
+                                    &&(evaluationLocal.blobTime==evaluationClient.blobTime)
+                                    &&(evaluationLocal.blobCount== evaluationClient.blobCount)
+                                    &&(evaluationLocal.blobSize== evaluationClient.blobSize)
+                                    && (evaluationLocal.dataTime==evaluationClient.dataTime)
+                                    &&(evaluationLocal.dataSize==evaluationClient.dataSize)
+                                    )
+                                {
+                                    msg_status=Constant._Message_status_info;
+                                }
+                                else
+                                {
+                                    msg_status=Constant._Message_status_error;
+                                }
+
+                                await _signalRExamServerHub.SendMessage(_memoryCache, _logger, deviceId, Constant._Message_grant_type_check_file,
+                                    new MessageContent { dataId=evaluationLocal.id, dataName=evaluationLocal.name, messageType=Constant._Message_type_message, status=msg_status, content="评测数据文件(evaluation.json),校验评测基本信息..." });
+                                content = msg_status.Equals(Constant._Message_status_success)||msg_status.Equals(Constant._Message_status_info) ? "成功" : "失败";
+                                if (msg_status.Equals(Constant._Message_status_success)|| msg_status.Equals(Constant._Message_status_info))
+                                {
+                                    successMsgs.Add($"评测数据文件(evaluation.json),校验评测基本信息,检测结果:{content}");
+                                }
+                                else
+                                {
+                                    errorMsgs.Add($"评测数据文件(evaluation.json),校验评测基本信息,检测结果:{content}"); ;
+                                }
+                            }
+                            else
+                            {
+                               
+                               
+                                msg_status=Constant._Message_status_error;
+                                await _signalRExamServerHub.SendMessage(_memoryCache, _logger, deviceId, Constant._Message_grant_type_check_file,
+                                new MessageContent { dataId=evaluationLocal.id, dataName=evaluationLocal.name, messageType= Constant._Message_type_check, status=msg_status, content="评测数据文件(evaluation.json),读取评测基本信息失败!" });
+                                errorMsgs.Add("评测数据文件(evaluation.json),读取评测基本信息失败!");
+                            }
+
+                            List<EvaluationExam>? evaluationExams = evaluation_data["evaluationExams"]?.ToObject<List<EvaluationExam>>();
+                            if (evaluationExams.IsEmpty())
+                            {
+                               
+                               
+                                msg_status=Constant._Message_status_error;
+                                content = msg_status.Equals(Constant._Message_status_success)||msg_status.Equals(Constant._Message_status_info) ? "成功" : "失败";
+                                errorMsgs.Add($"评测数据文件(evaluation.json),读取评测试卷信息失败");
+                            }
+                            else
+                            {
+                                msg_status=Constant._Message_status_info;
+                                await _signalRExamServerHub.SendMessage(_memoryCache, _logger, deviceId, Constant._Message_grant_type_check_file,
+                                    new MessageContent { dataId=evaluationLocal.id, dataName=evaluationLocal.name, messageType=Constant._Message_type_message, status=msg_status, content="评测数据文件(evaluation.json),读取评测试卷信息..." });
+                                successMsgs.Add($"评测数据文件(evaluation.json),读取评测试卷信息...");
+                                string pattern = @"paper/[^/]+/([^/]+/[^/]+\.[^/]+)";
+                                foreach (var evaluationExam in evaluationExams!)
+                                {
+                                    msg_status=Constant._Message_status_info;
+                                    await _signalRExamServerHub.SendMessage(_memoryCache, _logger, deviceId, Constant._Message_grant_type_check_file,
+                                        new MessageContent { dataId=evaluationLocal.id, dataName=evaluationLocal.name, messageType=Constant._Message_type_message, status=msg_status, content=$"校验评测科目试卷:{evaluationExam.subjectName}-{evaluationExam.examName}" });
+                                    successMsgs.Add($"校验评测科目试卷:{evaluationExam.subjectName}-{evaluationExam.examName}");
+                                    string path_papers = Path.Combine(evaluationPath, "papers");
+                                    var papers_files = FileHelper.ListAllFiles(path_papers);
+                                    int paperIndex = 0;
+                                    foreach (var paper in evaluationExam.papers)
+                                    {
+                                        paperIndex++;
+                                        List<MessageContent> contents = new List<MessageContent>();
+                                        int blob_error_count = 0;
+                                        foreach (var blobInfo in paper.blobs)
+                                        {
+                                            msg_status=Constant._Message_status_info;
+                                            if (!string.IsNullOrWhiteSpace(blobInfo.path))
+                                            {
+
+                                                Match match = Regex.Match(blobInfo.path, pattern);
+                                                if (match.Success)
+                                                {
+                                                    string result = match.Groups[1].Value.Replace("/", "\\");
+                                                    
+                                                    string extension = Path.GetExtension(result);
+                                                    if (extension.Equals(extension.ToUpper()))
+                                                    {
+                                                        string fileName = Path.GetFileNameWithoutExtension(result);
+                                                        result= $"{fileName!}_1{extension}";
+                                                    }
+                                                    
+                                                    var file = papers_files.Find(x => x.Contains(result));
+                                                    if (file!=null)
+                                                    {
+                                                        msg_status=1;
+                                                        msg_status=Constant._Message_status_success;
+                                                    }
+                                                    else
+                                                    {
+                                                        msg_status=Constant._Message_status_error;
+                                                        blob_error_count++;
+                                                        errorMsgs.Add($"试卷名称:[{paperIndex}]{evaluationExam.examName}-{evaluationExam.subjectName}-{paper.paperName},未匹配到本地文件:{blobInfo.path}");
+                                                    }
+                                                }
+                                                else
+                                                {
+                                                    msg_status=Constant._Message_status_error;
+                                                    blob_error_count++;
+                                                    errorMsgs.Add($"试卷名称:[{paperIndex}]{evaluationExam.examName}-{evaluationExam.subjectName}-{paper.paperName},未提取到正则匹配的文件:{blobInfo.path}");
+                                                }
+                                            }
+                                            else
+                                            {
+                                                msg_status=Constant._Message_status_error; ;
+                                                blob_error_count++;
+                                                errorMsgs.Add($"试卷名称:[{paperIndex}]{evaluationExam.examName}-{evaluationExam.subjectName}-{paper.paperName},文件路径为空:{blobInfo.ToJsonString()}");
+                                            }
+                                           
+                                            contents.Add(new MessageContent
+                                            {
+                                                dataId=evaluationLocal.id,
+                                                dataName=evaluationLocal.name,
+                                                messageType=Constant._Message_type_check,
+                                                status=msg_status,
+                                                content=$"试卷文件信息:{paper.paperName}"
+                                            });
+                                        }
+                                        int paper_msg_status = Constant._Message_status_info;
+                                        if (blob_error_count>0)
+                                        {
+                                            paper_msg_status=Constant._Message_status_error;
+                                            errorMsgs.Add($"试卷名称:[{paperIndex}]{evaluationExam.examName}-{evaluationExam.subjectName}-{paper.paperName},文件数量:{paper.blobs.Count()},检测成功数量:{contents.Count(x => x.status==Constant._Message_status_success)},检测异常数量{contents.Count(x => x.status==Constant._Message_status_error)}");
+                                        }
+                                        else
+                                        {
+                                            paper_msg_status=Constant._Message_status_success;
+                                            successMsgs.Add($"试卷名称:[{paperIndex}]{evaluationExam.examName}-{evaluationExam.subjectName}-{paper.paperName},文件数量:{paper.blobs.Count()},检测成功数量:{contents.Count(x => x.status==Constant._Message_status_success)},检测异常数量{contents.Count(x => x.status==Constant._Message_status_error)}");
+                                        }
+                                        await _signalRExamServerHub.SendMessage(_memoryCache, _logger, deviceId, Constant._Message_grant_type_check_file,
+                                            new MessageContent
+                                            {
+                                                dataId=evaluationLocal.id,
+                                                dataName=evaluationLocal.name,
+                                                messageType=Constant._Message_type_message,
+                                                status=paper_msg_status,
+                                                content=$"试卷名称:[{paperIndex}]{evaluationExam.examName}-{evaluationExam.subjectName}-{paper.paperName},文件数量:{paper.blobs.Count()},检测成功数量:{contents.Count(x => x.status==Constant._Message_status_success)},检测异常数量{contents.Count(x => x.status==Constant._Message_status_error)}",
+                                                contents=contents
+                                            });
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+                catch (Exception e)
+                {
+                    errorMsgs.Add($"校验评测试卷文件信息异常:{e.Message}");
+                    _logger.LogData<object>(new { code = 500, msg = e.Message, data = new { content = e.StackTrace } }, evaluationLocal.id!);
+                }
+            }
+            return (successMsgs , errorMsgs);
+        }
+
+        public static (EvaluationCheckDataResult result, EvaluationClient? evaluationLocal)  CheckData( EvaluationClient? evaluationLocal, EvaluationClient? evaluationCloud, List<string> successMsgs,List<string> errorMsgs, LiteDBFactory _liteDBFactory )
+        {
+            //数据,文件,页面 0 没有更新,1 有更新
+            int data=0, blob=0, groupList=0, status=0,zip=0;
+            long dataSize=0, blobSize=0, studentCount=0;
+            if (evaluationLocal== null && evaluationCloud==null)
+            {
+                //线上线下没有数据
+                status=1;
+                errorMsgs.Add($"本地和数据中心均没查询到评测!");
+            }
+            else if (evaluationLocal!=null && evaluationCloud!=null)
+            {
+                successMsgs.Add($"检测到本地和数据中心均有数据!");
+                //线上线下有数据
+                status = 2;
+                if ((!string.IsNullOrWhiteSpace(evaluationLocal.blobHash) &&  !evaluationLocal.blobHash.Equals(evaluationCloud.blobHash))
+                    ||(evaluationLocal.blobTime<evaluationCloud.blobTime)
+                    ||(evaluationLocal.blobCount!= evaluationCloud.blobCount)
+                    ||(evaluationLocal.blobSize!= evaluationCloud.blobSize))
+                {
+                    blob=1;
+                    blobSize=evaluationCloud.blobSize;
+                    errorMsgs.Add($"文件包校验失败,需要更新文件包!");
+                }
+                if ((evaluationLocal.dataTime<evaluationCloud.dataTime)
+                    ||(evaluationLocal.dataSize!=evaluationCloud.dataSize)
+                    ||(evaluationLocal.paperCount!= evaluationCloud.paperCount)
+                    )
+                {
+                    data=1;
+                    errorMsgs.Add($"数据包校验失败,需要更新数据包!");
+                    dataSize=evaluationCloud.dataSize;
+                }
+
+                if ((evaluationLocal.studentCount!= evaluationCloud.studentCount)||(!$"{evaluationLocal.grouplistHash}".Equals(evaluationCloud.grouplistHash)))
+                {
+                    groupList=1;
+                    studentCount=evaluationCloud.studentCount;
+                    errorMsgs.Add($"名单信息校验失败,需要更新名单信息!");
+                }
+               // evaluationLocal=evaluationCloud;
+               // _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationClient>().Upsert(evaluationLocal);
+            }
+            else if (evaluationLocal!=null && evaluationCloud==null)
+            {
+                //线下有数据,线上没有数据,可能没联网。
+                status = 3;
+                successMsgs.Add($"数据中心未连接,使用本地数据进行作答!");
+            }
+            else if (evaluationLocal==null && evaluationCloud!=null)
+            {
+                //线下没有数据,线上有数据
+                evaluationLocal= evaluationCloud;
+                blob=1;
+                data=1;
+                groupList=1;
+                blobSize=evaluationCloud.blobSize;
+                dataSize=evaluationCloud.dataSize;
+                studentCount=evaluationCloud.studentCount;
+                status = 4;
+                //foreach (var subject in evaluationLocal.subjects)
+                //{
+                //    foreach (var paper in subject.papers)
+                //    {
+                //        paper.blob=$"package/{evaluationLocal.id}/papers/{paper.paperId}";
+                //    }
+                //}
+                _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationClient>().Upsert(evaluationLocal);
+
+                errorMsgs.Add($"获取到最新的评测数据,需要下载数据包,文件包,名单信息!");
+            }
+            EvaluationCheckDataResult checkDataResult= new EvaluationCheckDataResult { data = data, blob = blob, groupList = groupList, status = status, dataSize = dataSize, blobSize = blobSize, studentCount = studentCount,zip=zip ,successMsgs=successMsgs,errorMsgs=errorMsgs};
+            return (checkDataResult, evaluationLocal);
+        }
+       
+        public class EvaluationCheckDataResult
+        {
+            public int zip { get; set; }
+            public int data { get; set; }
+            public int blob { get; set; }
+            public int groupList { get; set; }
+            public int status { get; set; }
+            public long dataSize { get; set; }
+            public long blobSize { get; set; }
+            public long studentCount { get; set; }
+            public List<string> successMsgs { get; set; } = new List<string>();
+            public List<string> errorMsgs { get; set; } = new List<string>();
+        }
+        public class EvaluationCheckFileResult
+        {
+            public int checkTotal { get; set; }
+            public int checkSuccess { get; set; }
+
+            public int checkError { get; set; }
+            public List<string> successMsgs { get; set; } = new List<string>();
+            public List<string> errorMsgs { get; set; } = new List<string>();
+        }
+    }
+}

+ 0 - 13
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/WeatherForecast.cs

@@ -1,13 +0,0 @@
-namespace IES.ExamServer.Server
-{
-    public class WeatherForecast
-    {
-        public DateTime Date { get; set; }
-
-        public int TemperatureC { get; set; }
-
-        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
-
-        public string? Summary { get; set; }
-    }
-}

+ 5 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/appsettings.json

@@ -22,6 +22,9 @@
           "Path": "Configs/cer/cert.pem",
           "KeyPath": "Configs/cer/key.pem"
         }
+      },
+      "Http": {
+        "Url": "http://*:9999"
       }
     }
   },
@@ -29,7 +32,8 @@
     "Timeout": 30000,
     "Delay": 500,
     "CenterUrl": "https://www.teammodel.cn",
-    "NotifyUrl": "https://www.winteach.cn"
+    "NotifyUrl": "https://www.winteach.cn",
+    "MusicUrl": "https://musicapi.winteach.cn/api/v1"
   },
   "ExamClient": {
     "Domain": "habook.local"

+ 4 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/package.json

@@ -17,9 +17,12 @@
     "core-js": "^3.8.3",
     "element-ui": "^2.15.14",
     "js-audio-recorder": "^1.0.7",
+    "jwt-decode": "^4.0.0",
+    "lodash": "^4.17.21",
     "qrcodejs2": "^0.0.2",
     "vue": "^2.6.14",
-    "vue-router": "^3.6.5"
+    "vue-router": "^3.6.5",
+    "vuescroll": "^4.18.1"
   },
   "devDependencies": {
     "@babel/core": "^7.12.16",

+ 24 - 17
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/api/http.js

@@ -9,6 +9,7 @@ const NO_ACCESS_API = [
     '/index/login-check',
     '/index/list-schools',
     '/index/bind-school',
+    '/manage/get-activate-evaluation',
 ]
 
 // 需要携带access_token 不需要auth-token
@@ -50,11 +51,6 @@ let loading = undefined
 
 // http request 拦截器
 axios.interceptors.request.use(config => {
-    loading = Loading.service({
-        lock: true,
-        text: '加载中',
-        background: 'rgba(0, 0, 0, 0.7)'
-    })
 
     let isNeedAccess = true
 
@@ -80,12 +76,12 @@ axios.interceptors.request.use(config => {
     }
 
     // 检查是否有access_token
-    let access_token = localStorage.getItem('access_token')
+    /* let access_token = localStorage.getItem('access_token')
     if (!access_token) {
         loginOut()
         sessionStorage.setItem('loginOut', 'token无效')
         return
-    }
+    } */
 
     // 检查是否快到期
     let isExpired = checkToken()
@@ -136,9 +132,6 @@ axios.interceptors.response.use(response => {
     // 四小时没操作过则需重新登录
     let endTime = (new Date().getTime() + (4 * 60 * 60 * 1000))
     localStorage.setItem('webEndTime', endTime)
-    setTimeout(() => {
-        loading.close()
-    }, 1500)
     return response
 }, error => {
     console.log('vbfbbtfnt', error);
@@ -180,12 +173,11 @@ axios.interceptors.response.use(response => {
             message: 'http.error400'
         })
     }
-    loading.close()
     return Promise.reject(error)
 })
 
 function handleHeader(config) {
-    config.headers['Authorization'] = 'Bearer ' + localStorage.getItem('access_token')
+    config.headers['Authorization'] = 'Bearer ' + localStorage.getItem('auth_token')
     config.headers['Content-Type'] = 'application/json'
     config.headers['lang'] = localStorage.getItem('local') || navigator.language.toLowerCase()
 
@@ -244,7 +236,7 @@ function refreshToken() {
     }).then(res => {
         if (res.data.code === 200) {
             localStorage.setItem("auth_token", res.data.token)
-            localStorage.setItem("access_token", res.data.auth_token.access_token)
+            // localStorage.setItem("access_token", res.data.auth_token.access_token)
             localStorage.setItem("expires_in", res.data.auth_token.expires_in)
             // token刷新完成,触发挂载的API
             trigger()
@@ -266,9 +258,10 @@ function refreshToken() {
 // 超时退出重新登录
 function loginOut() {
     localStorage.clear()
+    sessionStorage.clear()
     console.log('超时退出');
     // router.push({path: '/home/homePage'})
-    window.location.href = window.location.origin + '/login/adminPage'
+    window.location.href = window.location.origin + '/login/admin'
 }
 
 /**
@@ -287,10 +280,10 @@ export function fetch(url, params) {
     return new Promise((resolve, reject) => {
         axios.get(apiUrl, data).then(response => {
             resolve(response.data)
-            app.$message({
+            /* app.$message({
                 type: 'success',
                 message: '数据访问成功!'
-            })
+            }) */
         }).catch(err => {
             reject(err)
         })
@@ -304,19 +297,33 @@ export function fetch(url, params) {
  * @returns {Promise}
  */
 
-export function post(url, params) {
+export function post(url, params, isLoad, loadInfo, timeout) {
     let data = {}
     let apiUrl = `${apiPrefix}${url}`
     data.method = apiUrl
     data.params = params
     data.lang = localStorage.getItem('local')
     return new Promise((resolve, reject) => {
+        if(isLoad) {
+            loading = Loading.service({
+                lock: true,
+                text: loadInfo || '加载中',
+                background: 'rgba(0, 0, 0, 0.7)'
+            })
+        }
+        if(timeout) {
+            axios.defaults.timeout = timeout // 设置超时时长
+        }
         axios.post(apiUrl, params).then(response => {
             if (response) {
                 resolve(response.data)
             }
         }, err => {
             reject(err)
+        }).finally(() => {
+            if(isLoad) setTimeout(() => {
+                    loading.close()
+                }, 1500)
         })
     })
 }

+ 78 - 2
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/api/index.js

@@ -8,7 +8,12 @@ export default {
     getDevice: function (data) {
         return post('/index/device', data)
     },
-
+    /**
+     * 获取服务器当前时间
+     */
+    getNowTime: function (data) {
+        return fetch('/index/nowtime', data)
+    },
     // 登录页面
     /**
      * 获取二维码 / 短信验证码
@@ -37,7 +42,78 @@ export default {
     },
 
     // 管理页面
-
+    /**
+     * 获取本地评测列表
+     */
+    getActivityList: function (data) {
+        return post('/manage/list-local-evaluation', data)
+    },
+    /**
+     * 获取评测信息
+     * @param {String} deviceId - 前端浏览器设备id
+     * @param {String} shortCode - 提取码
+     * @param {String} evaluationId - 评测id
+     * @param {Number} checkCenter - 是否连接数据中心搜索  1连接 其他:不连接
+     */
+    getExamInfo: function (data) {
+        return post('/manage/check-evaluation', data)
+    },
+    /**
+     * 根据开卷码检测文件
+     * @param {String} deviceId - 前端浏览器设备id
+     * @param {String} evaluationId - 评测id
+     * @param {String} openCode - 开卷码
+     * @param {String} shortCode - 提取码
+     */
+    openEvaluation: function (data) {
+        return post('/manage/open-evaluation', data, true, '正在检测评测文件...', 5 * 60 * 1000)
+    },
+    /**
+     * 下载数据包、文件包
+     * @param {String} deviceId - 前端浏览器设备id
+     * @param {String} evaluationId - 评测id
+     * @param {String} shortCode - 提取码
+     */
+    updatePackage: function (data) {
+        return post('/manage/download-package', data, true, '正在下载...', 5 * 60 * 1000)
+    },
+    /**
+     * 获取开考信息
+     * @param {String} evaluationId - 评测id
+     * @param {String} openCode - 开卷码
+     * @param {String} shortCode - 提取码
+     * @param {String} settingId - 轮次设置id
+     */
+    getExamRoundInfo: function (data) {
+        return post('/manage/load-evaluation-round', data)
+    },
+    /**
+     * 设置开考信息
+     * @param {String} evaluationId - 评测id
+     * @param {String} groupList - 名单 e.g. [{id: '', name: ''}]
+     * @param {Number} activate - 0停止 1开始
+     * @param {Number} countdownType - 倒计时类型  0未设置  1统一以服务器时间为基准介绍  2以开始作答为基准,开始作答向局域网端发送请求,返回开始作答时间
+     * @param {Number} countdown - 倒计时(毫秒)
+     * @param {Number} deadline - 截至时间  countdownType=1 时有值
+     * @param {Number} startline - 开考时间
+     * @param {String} openCode - 开卷码
+     * @param {String} shortCode - 提取码
+     */
+    setExamRoundInfo: function (data) {
+        return post('/manage/setting-evaluation-round', data)
+    },
+    /**
+     * 当前评测的开考设置列表信息
+     * @param {String} evaluationId - 评测id
+     * @param {String} openCode - 开卷码
+     * @param {String} shortCode - 提取码
+     */
+    getRoundList: function(data) {
+        return post('/manage/list-evaluation-round', data)
+    },
 
     // 学生页面
+    stuGetEvaluation: function(data) {
+        return post('/manage/get-activate-evaluation', data)
+    },
 }

文件差异内容过多而无法显示
+ 1 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/icon/no_data.svg


+ 237 - 7
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/demo_index.html

@@ -54,6 +54,66 @@
       <div class="content unicode" style="display: block;">
           <ul class="icon_lists dib-box">
           
+            <li class="dib">
+              <span class="icon element-icons">&#xe615;</span>
+                <div class="name">07_箭头_向下</div>
+                <div class="code-name">&amp;#xe615;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xee1a;</span>
+                <div class="name">07_箭头_向上</div>
+                <div class="code-name">&amp;#xee1a;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe614;</span>
+                <div class="name">管理中心</div>
+                <div class="code-name">&amp;#xe614;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe78c;</span>
+                <div class="name">工作台电脑</div>
+                <div class="code-name">&amp;#xe78c;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe9d9;</span>
+                <div class="name">mti-电脑</div>
+                <div class="code-name">&amp;#xe9d9;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe7e9;</span>
+                <div class="name">云端-关闭-线性</div>
+                <div class="code-name">&amp;#xe7e9;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe60d;</span>
+                <div class="name">云端资源</div>
+                <div class="code-name">&amp;#xe60d;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe618;</span>
+                <div class="name">时间</div>
+                <div class="code-name">&amp;#xe618;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe79b;</span>
+                <div class="name">时间</div>
+                <div class="code-name">&amp;#xe79b;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe642;</span>
+                <div class="name">电脑</div>
+                <div class="code-name">&amp;#xe642;</div>
+              </li>
+          
             <li class="dib">
               <span class="icon element-icons">&#xe647;</span>
                 <div class="name">切换</div>
@@ -174,9 +234,9 @@
 <pre><code class="language-css"
 >@font-face {
   font-family: 'element-icons';
-  src: url('iconfont.woff2?t=1737019993465') format('woff2'),
-       url('iconfont.woff?t=1737019993465') format('woff'),
-       url('iconfont.ttf?t=1737019993465') format('truetype');
+  src: url('iconfont.woff2?t=1740223260776') format('woff2'),
+       url('iconfont.woff?t=1740223260776') format('woff'),
+       url('iconfont.ttf?t=1740223260776') format('truetype');
 }
 </code></pre>
           <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -202,6 +262,96 @@
       <div class="content font-class">
         <ul class="icon_lists dib-box">
           
+          <li class="dib">
+            <span class="icon element-icons el-icon-jiantouxiangxia"></span>
+            <div class="name">
+              07_箭头_向下
+            </div>
+            <div class="code-name">.el-icon-jiantouxiangxia
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-jiantouxiangshang"></span>
+            <div class="name">
+              07_箭头_向上
+            </div>
+            <div class="code-name">.el-icon-jiantouxiangshang
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-guanlizhongxin"></span>
+            <div class="name">
+              管理中心
+            </div>
+            <div class="code-name">.el-icon-guanlizhongxin
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-a-ziyuan491"></span>
+            <div class="name">
+              工作台电脑
+            </div>
+            <div class="code-name">.el-icon-a-ziyuan491
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-mti-diannao"></span>
+            <div class="name">
+              mti-电脑
+            </div>
+            <div class="code-name">.el-icon-mti-diannao
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-yunduan-guanbi-xianxing"></span>
+            <div class="name">
+              云端-关闭-线性
+            </div>
+            <div class="code-name">.el-icon-yunduan-guanbi-xianxing
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-yunduanziyuan"></span>
+            <div class="name">
+              云端资源
+            </div>
+            <div class="code-name">.el-icon-yunduanziyuan
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-shijian"></span>
+            <div class="name">
+              时间
+            </div>
+            <div class="code-name">.el-icon-shijian
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-shijian1"></span>
+            <div class="name">
+              时间
+            </div>
+            <div class="code-name">.el-icon-shijian1
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-diannao"></span>
+            <div class="name">
+              电脑
+            </div>
+            <div class="code-name">.el-icon-diannao
+            </div>
+          </li>
+          
           <li class="dib">
             <span class="icon element-icons el-icon-qiehuan"></span>
             <div class="name">
@@ -230,11 +380,11 @@
           </li>
           
           <li class="dib">
-            <span class="icon element-icons el-icon-my-note"></span>
+            <span class="icon element-icons el-icon-mynote1"></span>
             <div class="name">
               物品-书笔
             </div>
-            <div class="code-name">.el-icon-my-note
+            <div class="code-name">.el-icon-mynote1
             </div>
           </li>
           
@@ -382,6 +532,86 @@
       <div class="content symbol">
           <ul class="icon_lists dib-box">
           
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-jiantouxiangxia"></use>
+                </svg>
+                <div class="name">07_箭头_向下</div>
+                <div class="code-name">#el-icon-jiantouxiangxia</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-jiantouxiangshang"></use>
+                </svg>
+                <div class="name">07_箭头_向上</div>
+                <div class="code-name">#el-icon-jiantouxiangshang</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-guanlizhongxin"></use>
+                </svg>
+                <div class="name">管理中心</div>
+                <div class="code-name">#el-icon-guanlizhongxin</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-a-ziyuan491"></use>
+                </svg>
+                <div class="name">工作台电脑</div>
+                <div class="code-name">#el-icon-a-ziyuan491</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-mti-diannao"></use>
+                </svg>
+                <div class="name">mti-电脑</div>
+                <div class="code-name">#el-icon-mti-diannao</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-yunduan-guanbi-xianxing"></use>
+                </svg>
+                <div class="name">云端-关闭-线性</div>
+                <div class="code-name">#el-icon-yunduan-guanbi-xianxing</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-yunduanziyuan"></use>
+                </svg>
+                <div class="name">云端资源</div>
+                <div class="code-name">#el-icon-yunduanziyuan</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-shijian"></use>
+                </svg>
+                <div class="name">时间</div>
+                <div class="code-name">#el-icon-shijian</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-shijian1"></use>
+                </svg>
+                <div class="name">时间</div>
+                <div class="code-name">#el-icon-shijian1</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-diannao"></use>
+                </svg>
+                <div class="name">电脑</div>
+                <div class="code-name">#el-icon-diannao</div>
+            </li>
+          
             <li class="dib">
                 <svg class="icon svg-icon" aria-hidden="true">
                   <use xlink:href="#el-icon-qiehuan"></use>
@@ -408,10 +638,10 @@
           
             <li class="dib">
                 <svg class="icon svg-icon" aria-hidden="true">
-                  <use xlink:href="#el-icon-my-note"></use>
+                  <use xlink:href="#el-icon-mynote1"></use>
                 </svg>
                 <div class="name">物品-书笔</div>
-                <div class="code-name">#el-icon-my-note</div>
+                <div class="code-name">#el-icon-mynote1</div>
             </li>
           
             <li class="dib">

+ 44 - 4
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.css

@@ -1,8 +1,8 @@
 @font-face {
   font-family: "element-icons"; /* Project id 4795944 */
-  src: url('iconfont.woff2?t=1737019993465') format('woff2'),
-       url('iconfont.woff?t=1737019993465') format('woff'),
-       url('iconfont.ttf?t=1737019993465') format('truetype');
+  src: url('iconfont.woff2?t=1740223260776') format('woff2'),
+       url('iconfont.woff?t=1740223260776') format('woff'),
+       url('iconfont.ttf?t=1740223260776') format('truetype');
 }
 
 .element-icons {
@@ -13,6 +13,46 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.el-icon-jiantouxiangxia:before {
+  content: "\e615";
+}
+
+.el-icon-jiantouxiangshang:before {
+  content: "\ee1a";
+}
+
+.el-icon-guanlizhongxin:before {
+  content: "\e614";
+}
+
+.el-icon-a-ziyuan491:before {
+  content: "\e78c";
+}
+
+.el-icon-mti-diannao:before {
+  content: "\e9d9";
+}
+
+.el-icon-yunduan-guanbi-xianxing:before {
+  content: "\e7e9";
+}
+
+.el-icon-yunduanziyuan:before {
+  content: "\e60d";
+}
+
+.el-icon-shijian:before {
+  content: "\e618";
+}
+
+.el-icon-shijian1:before {
+  content: "\e79b";
+}
+
+.el-icon-diannao:before {
+  content: "\e642";
+}
+
 .el-icon-qiehuan:before {
   content: "\e647";
 }
@@ -25,7 +65,7 @@
   content: "\e809";
 }
 
-.el-icon-my-note:before {
+.el-icon-mynote1:before {
   content: "\e71f";
 }
 

文件差异内容过多而无法显示
+ 1 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.js


+ 71 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.json

@@ -5,6 +5,76 @@
   "css_prefix_text": "el-icon-",
   "description": "",
   "glyphs": [
+    {
+      "icon_id": "609292",
+      "name": "07_箭头_向下",
+      "font_class": "jiantouxiangxia",
+      "unicode": "e615",
+      "unicode_decimal": 58901
+    },
+    {
+      "icon_id": "43421025",
+      "name": "07_箭头_向上",
+      "font_class": "jiantouxiangshang",
+      "unicode": "ee1a",
+      "unicode_decimal": 60954
+    },
+    {
+      "icon_id": "26126920",
+      "name": "管理中心",
+      "font_class": "guanlizhongxin",
+      "unicode": "e614",
+      "unicode_decimal": 58900
+    },
+    {
+      "icon_id": "27285274",
+      "name": "工作台电脑",
+      "font_class": "a-ziyuan491",
+      "unicode": "e78c",
+      "unicode_decimal": 59276
+    },
+    {
+      "icon_id": "42902080",
+      "name": "mti-电脑",
+      "font_class": "mti-diannao",
+      "unicode": "e9d9",
+      "unicode_decimal": 59865
+    },
+    {
+      "icon_id": "12690973",
+      "name": "云端-关闭-线性",
+      "font_class": "yunduan-guanbi-xianxing",
+      "unicode": "e7e9",
+      "unicode_decimal": 59369
+    },
+    {
+      "icon_id": "20904889",
+      "name": "云端资源",
+      "font_class": "yunduanziyuan",
+      "unicode": "e60d",
+      "unicode_decimal": 58893
+    },
+    {
+      "icon_id": "10352325",
+      "name": "时间",
+      "font_class": "shijian",
+      "unicode": "e618",
+      "unicode_decimal": 58904
+    },
+    {
+      "icon_id": "25931963",
+      "name": "时间",
+      "font_class": "shijian1",
+      "unicode": "e79b",
+      "unicode_decimal": 59291
+    },
+    {
+      "icon_id": "6527108",
+      "name": "电脑",
+      "font_class": "diannao",
+      "unicode": "e642",
+      "unicode_decimal": 58946
+    },
     {
       "icon_id": "9568050",
       "name": "切换",
@@ -29,7 +99,7 @@
     {
       "icon_id": "6536038",
       "name": "物品-书笔",
-      "font_class": "my-note",
+      "font_class": "mynote1",
       "unicode": "e71f",
       "unicode_decimal": 59167
     },

二进制
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.ttf


二进制
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.woff


二进制
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.woff2


+ 11 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/main.js

@@ -4,12 +4,17 @@ import router from './router/router'
 import apiTools from '@/api'
 import { fetch, post } from '@/api/http'
 import tools from '@/utils/public.js'
+import evTools from '@/utils/evTools.js'
 
 import ElementUI from 'element-ui'
 import 'element-ui/lib/theme-chalk/index.css'
 import axios from 'axios'
 import '@/assets/reset.css'
 import "@/assets/iconfont/iconfont.css"
+import _ from 'lodash'
+
+import vuescroll from 'vuescroll/dist/vuescroll-native'
+
 
 Vue.use(ElementUI)
 Vue.config.productionTip = false
@@ -17,8 +22,14 @@ Vue.config.productionTip = false
 Vue.prototype.$api = apiTools
 Vue.prototype.$axios = axios
 Vue.prototype.$tools = tools
+Vue.prototype.$evTools = evTools
 Vue.prototype.$post = post
 Vue.prototype.$get = fetch
+Vue.prototype._ = _
+
+// 定义vuescroll全局滚动条组件
+Vue.component('vuescroll', vuescroll)
+Vue.prototype.$vuescrollConfig = tools.vueScrollOpt
 
 const app = new Vue({
     el: '#app',

+ 847 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/utils/evTools.js

@@ -0,0 +1,847 @@
+import $tools from './public.js'
+import { app } from '@/main.js'
+
+
+export default {
+	/* 根据登录后的用户信息获取blobHOST域名 */
+	getBlobHost(url) {
+		let s = url || store.state.user.userProfile.blob_uri || store.state.user.studentProfile.blob_uri
+		let pattern = /[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?/
+		return s.split('//')[0] + '//' + s.match(pattern)[0]
+	},
+	getAbilityDetailById(abilityId) {
+		return new Promise((r, j) => {
+			$api.ability.FindAbilityById({
+				"scope": "school",
+				"schoolCode": store.state.userInfo.schoolCode,
+				"abilityId": abilityId,
+				"standard": sessionStorage.getItem('standard')
+			}).then(res => {
+				if (!res.error) {
+					r(res.ability)
+				} else {
+					j(res.error)
+				}
+			}).catch(e => {
+				j(e)
+			})
+		})
+	},
+	/* 获取试题保存在Blob的JSON文件 */
+	/* 创建Blob试题格式 */
+	createBlobItem(item) {
+		return new Promise((r, j) => {
+			let itemJson = {
+				id: item.id,
+				pid: item.pid || null,
+				exercise: {
+					answer: item.answer,
+					explain: item.explain,
+					type: item.type,
+					answerType: item.answerType || 'text',
+					useAutoScore: item.useAutoScore || false,
+					answerLang: item.answerLang || 'en-US',
+					objective: this.getItemType(item.type),
+					opts: item.option ? item.option.length : 0,
+					knowledge: item.knowledge,
+					field: item.field,
+					level: item.level,
+					periodId: item.periodId,
+					gradeIds: item.gradeIds,
+					subjectId: item.subjectId,
+					children: item.children || [],
+					// scope:item.scope,
+					score: item.score || 0,
+					source: item.source || 0,
+					blankCount: item.blankCount || 1,
+					repair: item.repair,
+					createTime: new Date().getTime(),
+					creator: store.state.userInfo.TEAMModelId || 'null'
+				},
+				item: [{
+					type: 'Html',
+					uid: item.id,
+					question: item.question,
+					option: item.option
+				}]
+			}
+			r(itemJson)
+		})
+	},
+	/* 获取保存在COSMOS里面的试题格式 */
+	createCosmosItem(item, scope, code) {
+		return new Promise((r, j) => {
+			let cosmosItem = {
+				id: item.id,
+				pid: item.pid || null,
+				code: code || item.code,
+				scope: scope || item.scope,
+				score: item.score || 0,
+				source: item.source || 0,
+				type: item.type,
+				answer: item.answer || [],
+				answerType: item.answerType || 'text',
+				useAutoScore: item.useAutoScore || false,
+				answerLang: item.answerLang || 'en-US',
+				objective: this.getItemType(item.type),
+				question: this.getSimpleText(item.question),
+				knowledge: item.knowledge,
+				field: item.field,
+				level: item.level,
+				periodId: item.periodId,
+				gradeIds: item.gradeIds,
+				subjectId: item.subjectId,
+				repair: item.repair,
+				blankCount: item.blankCount || 1,
+				blob: item.blob,
+				createTime: new Date().getTime(),
+				creator: store.state.userInfo.TEAMModelId || 'null',
+				tags: item.tags || []
+			}
+			r(cosmosItem)
+		})
+	},
+	/* 生成试卷的index.json文件格式 */
+	createBlobPaper(paper, slides) {
+		return new Promise((r, j) => {
+			let paperItem = {
+				id: paper.id,
+				name: paper.name,
+				// code:paper.code,
+				// scope:paper.scope,
+				blob: paper.blob || '',
+				itemSort: paper.itemSort || 0,
+				isNumOption: paper.isNumOption || 0,
+				qamode: paper.qamode || 0,
+				multipleRule: paper.multipleRule,
+				attachments: paper.attachments || [],
+				tags: paper.tags || [],
+				slides: slides,
+				points: paper.points,
+				periodId: paper.periodId,
+				gradeIds: paper.gradeIds,
+				subjectId: paper.subjectId,
+				subjectName: paper.subjectName,
+				score: paper.score,
+				sheet: paper.sheet || null,
+				typeSummaryInfo: paper.typeSummaryInfo || null,
+				orderTemp: paper.orderTemp || 0,
+				secret: paper.secret || 0,
+				markModel: paper.markModel || 0,
+				creatorId: paper.creatorId || store.state.userInfo.TEAMModelId
+			}
+			r(paperItem)
+		})
+	},
+	/* 生成试卷保存在cosmos的数据结构 */
+	createCosmosPaper(paper) {
+		return new Promise((r, j) => {
+			let paperItem = {
+				id: paper.id,
+				name: paper.name,
+				code: paper.code,
+				blob: paper.blob,
+				tags: paper.tags || [],
+				qamode: paper.qamode || 0,
+				attachments: paper.attachments || [],
+				sheet: paper.sheet || null,
+				itemSort: paper.itemSort || 0,
+				isNumOption: paper.isNumOption || 0,
+				scope: paper.scope,
+				scoring: paper.scoring,
+				points: paper.points,
+				periodId: paper.periodId,
+				gradeIds: paper.gradeIds,
+				subjectId: paper.subjectId,
+				subjectName: paper.subjectName,
+				score: paper.score,
+				multipleRule: paper.multipleRule,
+				secret: paper.secret || 0,
+				markModel: paper.markModel || 0,
+				creatorId: paper.creatorId || store.state.userInfo.TEAMModelId
+			}
+			r(paperItem)
+		})
+	},
+	/* 根据醍摩豆ID获取对应用户Blob内部的完整试题 */
+	getFullItemByTmdId(tmdId, blob, inSyllabus) {
+		return new Promise(async (r, j) => {
+			let privateSas = await this.getBlobPrivateSas(tmdId)
+			let fullPath = this.getBlobHost() + '/' + tmdId + blob + privateSas
+			let jsonData = JSON.parse(await $tools.getFile(fullPath))
+			// 如果是综合题 那就拿到children里面的小题id集合 去换取小题的blobJSON文件 然后替换children的内容
+			if (jsonData.exercise.children.length && jsonData.exercise.type === 'compose') {
+				let childrenUrls;
+				if (inSyllabus) {
+					let syllabusPrefix = '/syllabus/' + blob.split('/')[2] + '/' + jsonData.id
+					childrenUrls = jsonData.exercise.children.map(i => this.getBlobHost() + '/' + tmdId + syllabusPrefix + '/' + i.id + '.json' + privateSas)
+				} else {
+					childrenUrls = jsonData.exercise.children.map(i => this.getBlobHost() + '/' + tmdId + '/item/' + i + '/' + i + '.json' + privateSas)
+				}
+				console.log(childrenUrls);
+				jsonData.exercise.children = await this.getFullChildren(childrenUrls, tmdId, inSyllabus)
+			}
+
+			// 调整渲染试题数据结构
+			jsonData.exercise.question = jsonData.item[0].question
+			jsonData.exercise.blob = fullPath
+			jsonData.exercise.code = tmdId
+			jsonData.exercise.option = jsonData.item[0].option
+			jsonData.exercise.id = jsonData.id
+			jsonData.exercise.scope = 'private'
+			jsonData.exercise.pid = jsonData.pid
+			jsonData.exercise = await this.doAddHost(jsonData.exercise, null, tmdId, inSyllabus)
+			r(jsonData.exercise)
+		})
+	},
+	/* 根据醍摩豆ID获取对应BLOB个人容器授权信息 */
+	getBlobPrivateSas(tmdId) {
+		return new Promise((r, j) => {
+			$api.blob.blobSasR({
+				name: tmdId,
+				role: 'teacher'
+			}).then(res => {
+				if (!res.error) {
+					r('?' + res.sas)
+				}
+			})
+		})
+	},
+
+	/* 根据醍摩豆ID获取对应BLOB个人容器授权信息 */
+	getBlobPrivateSasObj(tmdId) {
+		return new Promise((r, j) => {
+			$api.blob.blobSasR({
+				name: tmdId,
+				role: 'teacher'
+			}).then(res => {
+				if (!res.error) {
+					res.sas = '?' + res.sas
+					r(res)
+				}
+			})
+		})
+	},
+
+	/* 根据醍摩豆ID获取对应BLOB个人容器授权信息 */
+	getBlobSchoolSas(schoolCode) {
+		return new Promise((r, j) => {
+			$api.blob.blobSasR({
+				name: schoolCode,
+				role: 'school'
+			}).then(res => {
+				if (!res.error) {
+					r('?' + res.sas)
+				}
+			})
+		})
+	},
+	/* 获取完整的试题数据 */
+	getFullItem(list, examScope, inSyllabus) {
+		console.log('接受到的examScope', examScope)
+		return new Promise(async (resolve, reject) => {
+			if (list.length === 0) return
+			let promiseArr = []
+			console.log('getFullITEM接收到的list')
+			console.log(list)
+			for (let i = 0; i < list.length; i++) {
+				promiseArr.push(new Promise(async (r, j) => {
+					if (list[i].blob) {
+						let curScope = list[i].scope
+						const blobHost = curScope === 'school' ? JSON.parse(decodeURIComponent(localStorage.school_profile, "utf-8")).blob_uri : JSON.parse(decodeURIComponent(localStorage.user_profile, "utf-8")).blob_uri
+						// 根据试题的Blob地址 去读取JSON文件
+						let sasString = curScope === 'school' ? await $tools.getSchoolSas() : await $tools.getPrivateSas()
+						try {
+							let jsonInfo = list[i].blob.includes('https://') ? await $tools.getFile(list[i].blob + sasString.sas) : await $tools.getFile(blobHost + list[i].blob + sasString.sas)
+							let jsonData = JSON.parse(jsonInfo)
+							// 如果是综合题 那就拿到children里面的小题id集合 去换取小题的blobJSON文件 然后替换children的内容
+							if (jsonData.exercise.children.length && jsonData.exercise.type === 'compose') {
+								let childrenUrls;
+								if (inSyllabus) {
+									let syllabusPrefix = '/syllabus/' + list[i].blob.split('/')[2] + '/' + list[i].id
+									childrenUrls = jsonData.exercise.children.map(i => blobHost + syllabusPrefix + '/' + i.id + '.json' + sasString.sas)
+								} else {
+									childrenUrls = jsonData.exercise.children.map(i => blobHost + '/item/' + i + '/' + i + '.json' + sasString.sas)
+								}
+								jsonData.exercise.children = await this.getFullChildren(childrenUrls, list[i].code, list[i].scope, inSyllabus)
+							}
+							// 调整渲染试题数据结构
+							jsonData.id = list[i].id
+							jsonData.exercise.question = jsonData.item[0].question
+							jsonData.exercise.createTime = list[i].createTime || 0
+							jsonData.exercise.blob = list[i].blob
+							jsonData.exercise.code = list[i].code
+							jsonData.exercise.scope = list[i].scope
+							jsonData.exercise.option = jsonData.item[0].option
+							jsonData.exercise.id = list[i].id
+							jsonData.exercise.pid = jsonData.pid
+							jsonData.exercise.tags = list[i]?.tags || []
+							if(inSyllabus && list[i].nodeId) jsonData.exercise.nodeId = list[i].nodeId
+							jsonData.exercise = await this.doAddHost(jsonData.exercise, null, null, inSyllabus)
+							r(jsonData.exercise)
+						} catch (e) {
+							console.log(e)
+							j(e)
+							// this.$Message.error(e)
+						}
+
+					} else {
+						r(null)
+					}
+				}))
+			}
+			Promise.allSettled(promiseArr).then(result => {
+				console.log('从Blob获取来的试题', result.filter(i => i.status === 'fulfilled').map(j => j.value))
+				resolve(result.filter(i => i.status === 'fulfilled').map(j => j.value))
+			}).catch(err => {
+				Message.error(app.$t('utils.fileReadFail'))
+				reject(err)
+			})
+		})
+	},
+	/* 保存综合题小题 */
+	saveChildren(children, containerClient) {
+		return new Promise((resolve, reject) => {
+			let promiseArr = []
+			let itemJsonFiles = []
+			children.forEach(exerciseItem => {
+				promiseArr.push(new Promise(async (r, j) => {
+					// 将当前的试题数据转化为BLOB内部的试题JSON格式
+					const itemJsonFile = await this.createBlobItem(exerciseItem)
+					const cosmosItem = await this.createCosmosItem(exerciseItem)
+					// 首先保存新题目的JSON文件到Blob 然后返回URL链接
+					let file = new File([JSON.stringify(itemJsonFile)], exerciseItem.id + ".json");
+					try {
+						// 等待上传blob的返回结果
+						let blobFile = await containerClient.upload(file, { path: 'item/' + exerciseItem.id })
+						if (blobFile.blob) {
+							// 保存试题JSON文件到试卷文件夹需要
+							itemJsonFiles.push(file)
+							// 保存到COSMOS是不含base64图片编码的数据 避免数据量过大
+							cosmosItem.blob = blobFile.blob
+							// 保存当前试题到数据库
+							that.saveExercise(cosmosItem).then(res => {
+								r(res.itemInfo)
+							})
+						} else {
+							j(500)
+						}
+					} catch (e) {
+						j(500)
+					}
+				}))
+			})
+			Promise.all(promiseArr).then(result => {
+				if (result.length) {
+					resolve(itemJsonFiles)
+				} else {
+					resolve([])
+				}
+			})
+
+		})
+
+	},
+	/* 获取综合题子题的Blob数据 */
+	getFullChildren(urls, code, scope, inSyllabus) {
+		return new Promise((resolve, reject) => {
+			let promiseArr = []
+			urls.forEach(url => {
+				promiseArr.push(new Promise(async (r, j) => {
+					try {
+						let jsonData = JSON.parse(await $tools.getFile(url))
+						// 调整渲染试题数据结构
+						jsonData.exercise.question = jsonData.item[0].question
+						jsonData.exercise.blob = url
+						jsonData.exercise.code = code
+						jsonData.exercise.option = jsonData.item[0].option
+						jsonData.exercise.id = jsonData.id
+						jsonData.exercise.pid = jsonData.pid
+						jsonData.exercise.scope = scope || 'private'
+						jsonData.exercise = await this.doAddHost(jsonData.exercise, null, null, inSyllabus)
+						r(jsonData.exercise)
+					} catch (e) {
+						j(e)
+					}
+				}))
+			})
+
+			Promise.allSettled(promiseArr).then(result => {
+				if (result.length) {
+					// resolve(result)
+					resolve(result.filter(i => i.status === 'fulfilled').map(j => j.value))
+				} else {
+					resolve([])
+				}
+			})
+		})
+	},
+	/* 根据醍摩豆ID获取对应用户Blob内部的完整试卷 */
+	getFullPaperByTmdId(tmdId, blob, nodeId) {
+		return new Promise(async (r, j) => {
+			console.log('根据ID获取试题')
+			let privateSas = await this.getBlobPrivateSas(tmdId)
+			let fullPath = this.getBlobHost() + '/' + tmdId + blob + '/index.json' + privateSas
+			try {
+				let jsonInfo = await $tools.getFile(fullPath)
+				let jsonData = JSON.parse(jsonInfo)
+				jsonData.scope = 'private'
+				jsonData.code = tmdId
+				// 获取试卷包含的试题数据并包装好
+				if (jsonData.slides && jsonData.slides.length) {
+					let promiseArr = []
+					let allItems = []
+					jsonData.item = []
+					const path = this.getBlobHost() + '/' + tmdId + blob
+					jsonData.slides.forEach(async (item, index) => {
+						promiseArr.push(new Promise(async (resolve, reject) => {
+							try {
+								// 获取题目JSON并且包装成完整试题对象
+								let itemJson = JSON.parse(await $tools.getFile(path + '/' + item.url + privateSas))
+								itemJson.exercise.question = itemJson.item[0].question
+								itemJson.exercise.option = itemJson.item[0].option
+								itemJson.exercise.id = itemJson.id
+								itemJson.exercise.pid = itemJson.pid
+								itemJson.exercise.blob = path + '/' + item.url // 添加blob是方便在保存试卷是 refresh 与导入的试题区分开
+								itemJson.exercise.scope = 'private'
+								itemJson.exercise.score = item.scoring ? item.scoring.score : 0
+								try {
+									itemJson.exercise = await this.doAddHost(itemJson.exercise, { name: jsonData.name }, nodeId ? 'syllabus' : tmdId, nodeId) // 检测试题中的富文本 为有src为相对路径的音视频文件添加blob的HOST地址
+									resolve(itemJson.exercise)
+								} catch (e) {
+									reject(e)
+								}
+							} catch (e) {
+								reject(e)
+							}
+						}))
+					})
+
+					Promise.all(promiseArr).then(res => {
+						res.forEach((resItem, resIndex) => {
+							resItem.children = []
+							if (resItem.pid) {
+								let pItem = res.filter(i => i.id === resItem.pid)[0]
+								pItem.children.push(resItem)
+								pItem.score = pItem.score + resItem.score
+							}
+						})
+						jsonData.item = res.filter(i => !i.pid)
+						r(jsonData)
+					}).catch(e => {
+						// Message.error('试卷文件读取失败')
+						j(e)
+					})
+				}
+			} catch (e) {
+				console.log(e)
+				j(e)
+			}
+		})
+	},
+	/* 获取完整的试卷数据 */
+	getFullPaper(paper, examScope, nodeId) {
+		console.log(paper)
+		console.log(examScope)
+		console.log(nodeId)
+		let curScope = examScope || paper.examScope || paper.scope
+		return new Promise(async (r, j) => {
+			let blobHost = curScope === 'school' ? JSON.parse(decodeURIComponent(localStorage.school_profile, "utf-8")).blob_uri : 
+			JSON.parse(decodeURIComponent(localStorage.user_profile, "utf-8")).blob_uri
+			let sasString = curScope === 'school' ? await $tools.getSchoolSas() : await $tools.getPrivateSas()
+			let privateSas = sasString.sas
+			// 如果是活動版sas拿法不同
+			if(paper.blob.indexOf("jointexam") != -1){	
+				if(paper.creatorId){
+					privateSas = await this.getBlobPrivateSas(paper.creatorId)
+				}else{
+					privateSas = await this.getBlobPrivateSas(paper.examId)
+				}		
+				
+				blobHost ="https://teammodel.blob.core.windows.net/"+paper.creatorId
+			}
+			
+			// 根据试卷的Blob地址 去读取JSON文件			
+			try {
+				let paperBlob = paper.blob 				
+				let jsonInfo = await $tools.getFile(blobHost + paperBlob + '/index.json' + privateSas)
+				let jsonData = JSON.parse(jsonInfo)
+				jsonData.scope = paper.scope
+				jsonData.code = paper.code
+				jsonData.sheet = paper.sheet || null
+				// 获取试卷包含的试题数据并包装好
+				if (jsonData.slides && jsonData.slides.length) {
+					let promiseArr = []
+					let allItems = []
+					jsonData.item = []
+					const path = blobHost + paper.blob
+					jsonData.slides.forEach(async (item, index) => {
+						promiseArr.push(new Promise(async (resolve, reject) => {
+							try {
+								// 获取题目JSON并且包装成完整试题对象
+								let itemJson = JSON.parse(await $tools.getFile(path + '/' + item.url + privateSas))
+								itemJson.exercise.question = itemJson.item[0].question
+								itemJson.exercise.option = itemJson.item[0].option
+								itemJson.exercise.id = itemJson.id
+								itemJson.exercise.pid = itemJson.pid
+								itemJson.exercise.scope = paper.scope
+								itemJson.exercise.blob = path + '/' + item.url // 添加blob是方便在保存试卷是 refresh 与导入的试题区分开
+								itemJson.exercise.score = item.scoring ? item.scoring.score : 0
+								try {
+									let p = nodeId ? { name: paper.name } : paper
+									itemJson.exercise = await this.doAddHost(itemJson.exercise, p, nodeId ? 'syllabus' : null, nodeId) // 检测试题中的富文本 为有src为相对路径的音视频文件添加blob的HOST地址
+									resolve(itemJson.exercise)
+								} catch (e) {
+									reject(e)
+								}
+							} catch (e) {
+								reject(e)
+							}
+						}))
+					})
+
+					Promise.allSettled(promiseArr).then(res => {
+						res = res.filter(i => i.status === 'fulfilled').map(j => j.value)
+						res.forEach((resItem, resIndex) => {
+							resItem.children = []
+							if (resItem.pid) {
+								let pItem = res.filter(i => i.id === resItem.pid)[0]
+								pItem.children.push(resItem)
+								pItem.score = pItem.score + resItem.score
+							}
+						})
+						jsonData.item = res.filter(i => !i.pid)
+						r(jsonData)
+					}).catch(e => {
+						// Message.error('试卷文件读取失败')
+						j(e)
+					})
+				}
+			} catch (e) {
+				j(e)
+			}
+		})
+	},
+	/* 获取完整的试卷数据 */
+	getStuPaper(paper, examScope) {
+		let curScope = examScope || paper.scope
+		console.log(...arguments);
+		return new Promise(async (r, j) => {
+			// let blobHost = this.getBlobHost()
+			// 根据试卷的Blob地址 去读取JSON文件
+			let cntr = paper.code
+			let sas = await $tools.getBlobSas(cntr)
+			let sasString = "?" + sas.sas
+			let blobHost = sas.url
+			let paperBlobPath = blobHost + '/' + cntr + paper.blob
+			let fullPath = paperBlobPath + '/index.json' + sasString
+			console.log(fullPath);
+			try {
+				let jsonInfo = await $tools.getFile(fullPath)
+				let jsonData = JSON.parse(jsonInfo)
+				jsonData.scope = curScope
+				jsonData.code = paper.code
+				jsonData.sheet = paper.sheet || null
+				paper.tags = paper.tags || jsonData.tags
+				// 获取试卷包含的试题数据并包装好
+				if (jsonData.slides && jsonData.slides.length) {
+					jsonData.item = []
+					let promiseArr = []
+					jsonData.slides.forEach((item, index) => {
+						promiseArr.push(new Promise(async (resolve, reject) => {
+							// 获取题目JSON并且包装成完整试题对象
+							let itemFullPath = paperBlobPath + '/' + item.url + sasString
+							let itemJson = JSON.parse(await $tools.getFile(itemFullPath))
+							itemJson.exercise.question = itemJson.item[0].question
+							itemJson.exercise.option = itemJson.item[0].option
+							itemJson.exercise.id = itemJson.id
+							itemJson.exercise.pid = itemJson.pid
+							itemJson.exercise.scope = curScope
+							itemJson.exercise.score = item.scoring ? item.scoring.score : 0
+							// jsonData.item.push(itemJson.exercise)
+							try {
+								itemJson.exercise = await this.doAddHost(itemJson.exercise, paper, paper.code, null, sasString)
+								resolve(itemJson.exercise)
+							} catch (e) {
+								reject(e)
+							}
+						}))
+					})
+					Promise.all(promiseArr).then(res => {
+						res.forEach((resItem, resIndex) => {
+							resItem.children = []
+							if (resItem.pid) {
+								let pItem = res.filter(i => i.id === resItem.pid)[0]
+								pItem.children.push(resItem)
+								pItem.score = pItem.score + resItem.score
+							}
+						})
+						jsonData.item = res.filter(i => !i.pid)
+						r(jsonData)
+					}).catch(e => {
+						j(e)
+					})
+				}
+			} catch (e) {
+				j(e)
+			}
+		})
+	},
+	// 艺术测评专用获取试卷方案
+	getStuPaperForArt(paper, examScope, sas) {
+		let curScope = examScope || paper.scope
+		console.log(...arguments);
+		return new Promise(async (r, j) => {
+			// let blobHost = this.getBlobHost()
+			// 根据试卷的Blob地址 去读取JSON文件
+			let cntr = paper.code
+			let sasString = "?" + sas.sas
+			let blobHost = sas.url
+			let paperBlobPath = blobHost + '/' + cntr + paper.blob
+			let fullPath = paperBlobPath + '/index.json' + sasString
+			console.log(fullPath);
+			try {
+				let jsonInfo = await $tools.getFile(fullPath)
+				let jsonData = JSON.parse(jsonInfo)
+				jsonData.scope = curScope
+				jsonData.code = paper.code
+				jsonData.sheet = paper.sheet || null
+				paper.tags = paper.tags || jsonData.tags
+				r(jsonData)
+				// 获取试卷包含的试题数据并包装好
+				// if (jsonData.slides && jsonData.slides.length) {
+				// 	jsonData.item = []
+				// 	let promiseArr = []
+				// 	jsonData.slides.forEach((item, index) => {
+				// 		promiseArr.push(new Promise(async (resolve, reject) => {
+				// 			// 获取题目JSON并且包装成完整试题对象
+				// 			let itemFullPath = paperBlobPath + '/' + item.url + sasString
+				// 			let itemJson = JSON.parse(await $tools.getFile(itemFullPath))
+				// 			itemJson.exercise.question = itemJson.item[0].question
+				// 			itemJson.exercise.option = itemJson.item[0].option
+				// 			itemJson.exercise.id = itemJson.id
+				// 			itemJson.exercise.pid = itemJson.pid
+				// 			itemJson.exercise.scope = curScope
+				// 			itemJson.exercise.score = item.scoring ? item.scoring.score : 0
+				// 			// jsonData.item.push(itemJson.exercise)
+				// 			try {
+				// 				itemJson.exercise = await this.doAddHost(itemJson.exercise, paper, paper.code, null, sasString)
+				// 				resolve(itemJson.exercise)
+				// 			} catch (e) {
+				// 				reject(e)
+				// 			}
+				// 		}))
+				// 	})
+				// 	Promise.all(promiseArr).then(res => {
+				// 		res.forEach((resItem, resIndex) => {
+				// 			resItem.children = []
+				// 			if (resItem.pid) {
+				// 				let pItem = res.filter(i => i.id === resItem.pid)[0]
+				// 				pItem.children.push(resItem)
+				// 				pItem.score = pItem.score + resItem.score
+				// 			}
+				// 		})
+				// 		jsonData.item = res.filter(i => !i.pid)
+				// 		r(jsonData)
+				// 	}).catch(e => {
+				// 		j(e)
+				// 	})
+				// }
+			} catch (e) {
+				j(e)
+			}
+		})
+	},
+
+	/* 获取完整的试卷数据 */
+	getComposeItem(paper) {
+		return new Promise(async (r, j) => {
+			console.log(paper);
+			// 根据试卷的Blob地址 去读取JSON文件
+			let cntr = paper.code
+			let sas = await $tools.getBlobSas(cntr)
+			let sasString = sas.sas
+			let fullPath = paper.blob + "?" + sasString
+			try {
+				let jsonInfo = await $tools.getFile(fullPath)
+				let jsonData = JSON.parse(jsonInfo)
+				// 获取试卷包含的试题数据并包装好
+				if (jsonData.length) {
+					r(jsonData)
+				}
+			} catch (e) {
+				j(e)
+			}
+		})
+	},
+	/* 提取富文本内容中的文本 */
+	getSimpleText(html) {
+		var r = /<\/?(img)[^>]*>/gi;
+		return html.replace(r, "");
+	},
+	/* 判断是否为客观题 */
+	getItemType(type) {
+		const objective = ['single', 'multiple', 'judge']
+		return objective.includes(type)
+	},
+	/* 获取img标签内的src */
+	getImgSrc(richtext) {
+		let imgList = [];
+		richtext.replace(/<video [^>]*src=['"]([^'"]+)[^>]*>/g, (match, capture) => {
+			imgList.push(capture);
+		});
+		return imgList;
+	},
+
+
+
+
+
+
+	getPaperInfo(examId, paperId) {
+		return new Promise(async (resolve, reject) => {
+			let url = `/package/${examId}/papers/${paperId}`
+            let indexUrl = url + '/index.json'
+            try {
+                let jsonInfo = await $tools.getFile(indexUrl)
+                let jsonData = JSON.parse(jsonInfo)
+                // 获取试卷包含的试题数据并包装好
+                if (jsonData.slides && jsonData.slides.length) {
+                    jsonData.item = []
+                    let promiseArr = []
+                    jsonData.slides.forEach((item, index) => {
+                        promiseArr.push(new Promise(async (resolve, reject) => {
+                            // 获取题目JSON并且包装成完整试题对象
+                            let itemFullPath = url + '/' + item.url
+                            let itemJson = JSON.parse(await $tools.getFile(itemFullPath))
+                            itemJson.exercise.question = itemJson.item[0].question
+                            itemJson.exercise.option = itemJson.item[0].option
+                            itemJson.exercise.id = itemJson.id
+                            itemJson.exercise.pid = itemJson.pid
+                            // itemJson.exercise.scope = curScope
+                            itemJson.exercise.score = item.scoring ? item.scoring.score : 0
+                            try {
+                                console.log('多媒体链接', await this.doAddHost(itemJson.exercise, url));
+                                // this.processNum++
+                                resolve(itemJson.exercise)
+                            } catch (e) {
+                                reject(e)
+                            }
+                        }))
+                    })
+                    Promise.all(promiseArr).then(res => {
+                        res.forEach((resItem, resIndex) => {
+                            resItem.children = []
+                            if (resItem.pid) {
+                                let pItem = res.filter(i => i.id === resItem.pid)[0]
+                                pItem.children.push(resItem)
+                                pItem.score = pItem.score + resItem.score
+                            }
+                        })
+                        jsonData.item = res.filter(i => !i.pid)
+                        console.log('题目详细内容', jsonData);
+                        /* this.paperList.push(jsonData)
+                        this.paperInfo = jsonData
+                        this.isShowPaper = true
+                        this.isLoading.close() */
+						resolve(jsonData)
+                    }).catch(e => {
+                        console.error('22222222222', e);
+						reject(undefined)
+                    })
+                }
+            } catch (error) {
+                console.error('33333333333333', error);
+				reject(undefined)
+                /* this.$message({
+                    message: '打开试卷失败',
+                    type: 'error'
+                }); */
+            }
+		})
+	},
+	/* 给富文本添加 cntr是防止读取的是其他用户的BLOB */
+	async doAddHost(exerciseItem, url) {
+		// console.log(exerciseItem, paperItem, cntr, nodeId, sasString)
+		if (exerciseItem.source && exerciseItem.source === 3) {
+			return exerciseItem
+		}
+		/* 如果操作的是试卷内的试题 则需要拿试卷的code来作为containerName */
+		let isSubjective = exerciseItem.type === 'complete' || exerciseItem.type === 'subjective' || exerciseItem.type === 'compose'
+		let richTextObj = {
+			question: exerciseItem.question,
+			answer: Array.isArray(exerciseItem.answer) && exerciseItem.answer.length ? exerciseItem.answer[0] : exerciseItem.answer,
+			explain: exerciseItem.explain,
+		}
+		isSubjective && delete richTextObj.answer
+		return new Promise((resolve, reject) => {
+			let promiseArr = []
+			// 遍历题目的所有富文本内容
+			for (let key in richTextObj) {
+				promiseArr.push(new Promise(async (r, j) => {
+					let videoSrcList = this.getRichTextSrc(richTextObj[key], 'video')
+					let audioSrcList = this.getRichTextSrc(richTextObj[key], 'audio')
+					let srcList = videoSrcList.concat(audioSrcList)
+					if (srcList.length) {
+						console.log('要添加list', srcList)
+						for (let i = 0; i < srcList.length; i++) {
+							let src = decodeURI(srcList[i])
+							let showSrc = src
+							let spStr = src.split('.')
+							if(spStr[spStr.length - 1] === 'MP4' || spStr[spStr.length - 1] === 'MP3') {
+								showSrc = src.split('.').slice(0, -1).join('.')
+								showSrc = showSrc + '_1.' + spStr[spStr.length - 1]
+							}
+							let blobUrl = url + '/' + showSrc
+							try {
+								richTextObj[key] = richTextObj[key].replaceAll(`src="${src}"`, `src="${blobUrl}"`);
+							} catch (e) {
+								j(500)
+							}
+						}
+						if (key === 'answer' && Array.isArray(exerciseItem.answer) && exerciseItem.answer.length) {
+							exerciseItem.answer[0] = richTextObj[key]
+						} else {
+							exerciseItem[key] = richTextObj[key]
+						}
+						r(200)
+					} else {
+						r(200)
+					}
+				}))
+			}
+			Promise.all(promiseArr).then(result => {
+				//console.log('添加HOST之后的',exerciseItem)
+				resolve(exerciseItem)
+			}).catch(e => {
+				reject(e)
+			})
+		})
+	},
+	/* 获取富文本的资源src数据 */
+	getRichTextSrc(richText, type) {
+		if (!richText) {
+			return []
+		}
+		var videoReg = /<video.*?(?:>|\/>)/gi;
+		var imgReg = /<img.*?(?:>|\/>)/gi;
+		var audioReg = /<audio.*?(?:>|\/>)/gi;
+		//匹配src属性
+		var srcReg = /src=[\'\"]?([^\'\"]*)[\'\"]?/i;
+		var arr = String(richText).match(type === 'img' ? imgReg : type === 'video' ? videoReg : audioReg);
+		var result = []
+		if (!arr || !arr.length) {
+			return []
+		} else {
+			for (var i = 0; i < arr.length; i++) {
+				var src = arr[i].match(srcReg);
+				//获取图片地址
+				if (src[1]) {
+					result.push(src[1])
+				}
+			}
+			return result
+		}
+
+	}
+}

+ 4 - 20
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/utils/public.js

@@ -710,23 +710,7 @@ export default {
 	/* 数字与中文转换 */
 	getChineseByNum(num) {
 		num = Number(num)
-		var upperCaseNumber = [
-			app.$t('learnActivity.score.zero'),
-			app.$t('learnActivity.score.one'),
-			app.$t('learnActivity.score.two'),
-			app.$t('learnActivity.score.three'),
-			app.$t('learnActivity.score.four'),
-			app.$t('learnActivity.score.five'),
-			app.$t('learnActivity.score.six'),
-			app.$t('learnActivity.score.seven'),
-			app.$t('learnActivity.score.eight'),
-			app.$t('learnActivity.score.nine'),
-			app.$t('learnActivity.score.ten'),
-			app.$t('learnActivity.score.hundred'),
-			app.$t('learnActivity.score.thousand'),
-			app.$t('learnActivity.score.tenThd'),
-			app.$t('learnActivity.score.hMillion')
-		]
+		var upperCaseNumber = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '百', '千', '万', '亿']
 		var length = String(num).length
 		if (length === 1) {
 			return upperCaseNumber[num]
@@ -734,10 +718,10 @@ export default {
 			if (num === 10) {
 				return upperCaseNumber[num]
 			} else if (num > 10 && num < 20) {
-				return app.$t('learnActivity.score.ten') + upperCaseNumber[String(num).charAt(1)]
+				return '十' + upperCaseNumber[String(num).charAt(1)]
 			} else {
-				return upperCaseNumber[String(num).charAt(0)] + app.$t('learnActivity.score.ten') + upperCaseNumber[
-					String(num).charAt(1)].replace(app.$t('learnActivity.score.zero'), '')
+				return upperCaseNumber[String(num).charAt(0)] + '十' + upperCaseNumber[
+					String(num).charAt(1)].replace('零', '')
 			}
 		}
 	},

+ 138 - 2
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/admin/ActivityManage.less

@@ -1,6 +1,25 @@
 .el-container {
     height: 100%;
 
+    .base-user-center {
+        position: absolute;
+        top: 10px;
+        right: 20px;
+        display: flex;
+        height: 40px;
+        align-items: center;
+        cursor: pointer;
+
+        .el-avatar {
+            width: 35px;
+            height: 35px;
+        }
+    }
+
+    .content-box {
+        height: calc(100% - 60px);
+    }
+
     .el-header,
     .el-footer {
         background-color: #b3c0d1;
@@ -11,8 +30,66 @@
     .el-aside {
         // background-color: #d3dce6;
         color: #333;
-        line-height: 200px;
+        // line-height: 200px;
         border-right: 1px solid #ccc;
+        width: 350px !important;
+
+
+        .exam-list {
+            padding: 15px 10px;
+            border-bottom: 1px dashed #ccc;
+            cursor: pointer;
+
+            &>p:not(:last-child) {
+                margin-bottom: 7px;
+
+                &>.el-tag {
+                    margin-right: 10px;
+                }
+            }
+
+            .owner-tag {
+                &-area {
+                    color: #2d8cf0;
+                }
+
+                &-school {
+                    color: #7651dc;
+                    background-color: #efefff;
+                    border-color: #e9e9fc;
+                }
+
+                &-teacher {
+                    color: #414141;
+                    background-color: #e9e9e9;
+                    border-color: #e4e4e4;
+                }
+            }
+
+            .type-teg {
+                &-Art {
+                    color: #9fa91d;
+                    background-color: #feffef;
+                    border-color: #e4e7bf;
+                }
+
+                &-Exam {
+                    color: #724747;
+                    background-color: #f9f9f9;
+                    border-color: #f1eded;
+                }
+            }
+        }
+
+        .exam-list-active {
+            background: #E1EFF6;
+
+            &::before {
+                transform-origin: center left;
+                transform: scaleX(1);
+                transition: transform 0.3s ease-in-out;
+            }
+        }
     }
 
     .el-main {
@@ -77,7 +154,7 @@
             font-size: 16px;
             height: 100%;
 
-            &>div {
+            .info-tag {
                 margin-top: 15px;
             }
 
@@ -128,6 +205,65 @@
                 }
             }
         }
+
+        .answer-content {
+            .student-info {
+                margin: 10px 20px 30px 20px;
+                border-radius: 5px;
+                width: 320px;
+                border: 1px #909399 solid;
+
+
+                .name-info {
+                    background-color: #909399;
+                    color: #fff;
+                    padding: 5px 10px;
+                    border-top-left-radius: 5px;
+                    border-top-right-radius: 5px;
+                    font-weight: bold;
+                }
+            }
+
+            .short-code {
+                float: right;
+            }
+        }
+
+        .el-tab-pane {
+            margin: 0 15px 15px;
+            height: calc(100% - 30px);
+        }
+    }
+
+    .open-evaluation {
+        position: absolute;
+        top: 70px;
+        right: 20px;
+
+        .info-box {
+            display: flex;
+            justify-content: space-between;
+            padding: 8px 16px;
+            margin-bottom: 15px;
+
+            p {
+                margin-right: 20px;
+            }
+
+            .el-icon-close {
+                cursor: pointer;
+            }
+        }
+
+        .error-info {
+            background: #fef0f0;
+            color: #f56c6c;
+        }
+
+        .success-info {
+            background-color: #f0f9eb;
+            color: #67c23a;
+        }
     }
 }
 

文件差异内容过多而无法显示
+ 1065 - 247
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/admin/ActivityManage.vue


+ 264 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/admin/TestPaper.less

@@ -0,0 +1,264 @@
+.paper-body {
+    width: 98%;
+    height: 88%;
+    padding-left: 20px;
+    position: relative;
+
+    .back-to-top {
+        position: absolute;
+        right: 10px;
+        bottom: 10px;
+        height: 40px;
+        width: 40px;
+        background: #979797;
+        z-index: 99999;
+        border-radius: 50%;
+        cursor: pointer;
+        text-align: center;
+        line-height: 40px;
+
+        &:hover {
+            background: rgb(128, 128, 128);
+        }
+
+        .el-icon-arrow-up {
+            font-size: 22px;
+            color: white;
+        }
+    }
+
+    .paper-base-info {
+        position: sticky;
+        top: -2px;
+        display: flex;
+        flex-direction: row;
+        justify-content: space-between;
+        align-items: center;
+        background-color: #fff;
+        border-bottom: 1px dashed #cfcfcf;
+        z-index: 99;
+        padding: 10px 0 20px 0;
+
+        .base-info-btn:not(:last-child) {
+            margin-right: 10px;
+        }
+
+        .analysis-info {
+            font-weight: bold;
+            color: #69baec;
+            margin: 0 5px;
+        }
+
+        .base-info-item {
+            margin-right: 15px;
+        }
+    }
+
+
+    .components-el-container {
+        .paper-header {
+            margin-top: 20px;
+        }
+
+        /*向垂直水平居中*/
+        .flex-col-center {
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+            align-items: center;
+        }
+
+        .paper-title {
+            font-size: 30px;
+            margin: 30px 0;
+            font-weight: bold;
+            vertical-align: middle;
+            text-align: center;
+            cursor: pointer;
+        }
+
+        .no-data-text {
+            width: 100%;
+            padding: 30px;
+            background: #fff;
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+            align-items: center;
+            margin-top: 10px;
+            font-size: 16px;
+        }
+
+
+        .content-wrap {
+            position: relative;
+            width: 100%;
+            height: auto;
+            display: flex;
+            flex-direction: column;
+
+            .type-name {
+                font-size: 18px;
+                font-weight: bold;
+                margin-top: 20px;
+            }
+
+            .exercise-item {
+                position: relative;
+                width: calc(100% - 45px);
+                height: auto;
+                padding: 10px 20px 10px 20px;
+                margin-bottom: 10px;
+                margin-top: 30px;
+                font-size: 14px;
+                background: #fff;
+                border: 2px solid transparent;
+                cursor: pointer;
+
+                p {
+                    width: 92%;
+                }
+
+                &:hover {
+                    border: 2px solid #01b4ef;
+                    box-shadow: none !important;
+
+                    .item-tools-bind {
+                        display: unset;
+                    }
+                }
+
+                table,
+                td {
+                    border: 1px solid rgb(128, 128, 128);
+                    border-collapse: collapse;
+                    text-align: center;
+                    padding: 5px 10px;
+                }
+
+                .item-question {
+                    position: relative;
+                    cursor: pointer;
+
+                    .item-question-order {
+                        display: inline-block;
+                        vertical-align: top;
+                        width: 32px;
+                    }
+
+                    .item-question-text {
+                        display: inline-block;
+                        width: calc(90% - 30px);
+                    }
+                }
+
+                .item-options {
+                    margin-top: 10px;
+
+                    .item-option-content {
+                        margin: 7px 0;
+                    }
+
+                    .item-option-order {
+                        display: inline-block;
+                        vertical-align: top;
+                        width: 30px;
+                    }
+
+                    .item-option-text {
+                        display: inline-block;
+                        width: calc(100% - 30px);
+                    }
+                }
+
+                .item-btn-toggle {
+                    position: absolute;
+                    right: 10px;
+                    top: 8px;
+                    width: 15%;
+                    display: flex;
+                    justify-content: center;
+                    align-items: center;
+                }
+
+                .toggle-area {
+                    border-top: 1px #c3c3c34d dashed;
+                    padding-top: 10px;
+                    margin-top: 10px;
+                }
+
+                .item-explain {
+                    margin-top: 10px;
+                    cursor: pointer;
+                    font-size: 14px;
+
+                    .explain-title {
+                        width: 12%;
+                        max-width: 100px;
+                        display: inline-block;
+                        color: rgb(16, 171, 231);
+                    }
+
+                    .item-explain-details {
+                        vertical-align: top;
+                        display: inline-block;
+                        width: calc(100% - 100px);
+
+                        // width: 90%;
+                        .item-point-tag {
+                            padding: 0 10px;
+                            border: 1px solid #d6d6d6;
+                            margin-left: 10px;
+                            border-radius: 4px;
+
+                            &:first-child {
+                                margin-left: 0;
+                            }
+                        }
+
+                        .repair-item {
+                            display: inline-flex;
+                            margin-right: 20px;
+                            color: #21b1ff;
+                            text-decoration: underline;
+                            align-items: flex-end;
+
+                            &-link {
+                                margin-left: 5px;
+                            }
+
+                        }
+                    }
+                }
+
+                .item-answer,
+                .item-explain {
+                    line-height: 26px;
+                }
+
+                img {
+                    vertical-align: middle;
+                    max-width: 100%;
+                }
+
+                video {
+                    max-width: 100%;
+                }
+
+                .item-answer-item {
+                    margin-left: 25px;
+                    padding: 0 25px;
+                    border-bottom: 2px solid rgb(128, 128, 128);
+
+                    p {
+                        display: inline-block;
+                    }
+
+                    &:first-child {
+                        margin-left: 0;
+                    }
+                }
+            }
+        }
+    }
+}

+ 315 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/admin/TestPaper.vue

@@ -0,0 +1,315 @@
+<template>
+    <div class="paper-body">
+        <div class="back-to-top flex-col-center" title="返回顶部" v-if="isShowBackToTop" @click="handleBackToTop">
+            <i class="el-icon-arrow-up" />
+        </div>
+        <vuescroll ref="paperRef" @handle-scroll="handleScroll">
+            <div class="paper-base-info">
+                <div style="display: flex;">
+                    <span class="base-info-item">总分:<span class="analysis-info" style="cursor: pointer;">{{ paperInfo.score }}</span>分</span>
+                    <!-- <span class="base-info-item" @click="onShowScoreTable" style="cursor: pointer;">已配:<span class="analysis-info">{{ allocatedScore || 0 }}</span>分</span> -->
+                    <span class="base-info-item">题量:<span class="analysis-info">{{ paperInfo.item ? paperInfo.item.length : 0 }}</span></span>
+                    <span class="base-info-item" style="display: flex;">难度:
+                        <el-rate v-model="paperInfo.item.length ? +paperDiff : 0" allow-half disabled></el-rate>
+                    </span>
+                </div>
+                <div>
+                    <el-button class="base-info-btn" type="primary" size="small" @click="onHandleToggle()" v-show="paperInfo.item.length">{{ isAllOpen ? '全部折叠' : '全部展开' }}</el-button>
+                    <el-button class="base-info-btn" type="primary" size="small" @click="onViewModelChange()" v-show="paperInfo.item.length">{{ viewModel === 'type' ? '题号排序' : '题型排序' }}</el-button>
+                </div>
+            </div>
+            <div class="components-el-container">
+                <div class="paper-header flex-col-center">
+                    <p class="paper-title">{{ paperInfo.name }}</p>
+                </div>
+                <div v-if="!exerciseList.length" class="no-data-text">
+                    <img src="@/assets/icon/no_data.svg" width="120" />
+                    <span style="margin-top: 15px; color: #808080">暂无数据</span>
+                </div>
+                <div class="content-wrap" ref="mathJaxContainer" v-else>
+                    <div class="list-view" :key="typeIndex" v-for="(typeItem, typeIndex) in listData">
+                        <p v-show="viewModel === 'type' && typeItem.list.length" class="type-name">
+                            {{ $tools.getChineseByNum(getLatestTypeIndex(typeItem.type) + 1) }}: {{ exersicesType[typeItem.type] }}
+                            <span style="font-size: 14px; font-weight: 600">(共{{ typeItem.list.length }}题,总计{{ typeItem.score || 0 }}分)</span>
+                        </p>
+                        <div v-for="(item, index) of typeItem.list" :key="index" class="exercise-item" :data-id="item.id">
+                            <div @click="onQuestionToggle(exerciseList.indexOf(item), item.id, $event, typeItem.list)" style="max-width: 85%">
+                                <!-- 题干部分 -->
+                                <div class="item-question">
+                                    <div v-if="viewModel === 'list'">
+                                        <div class="item-question-order">{{ index + 1 }} :</div>
+                                        <div class="item-question-text" v-html="item.question"></div>
+                                    </div>
+                                    <div v-else>
+                                        <div class="item-question-order">{{ exerciseList.indexOf(item) + 1 }} :</div>
+                                        <div class="item-question-text" v-html="item.question"></div>
+                                    </div>
+                                </div>
+                                <!-- 选项部分 -->
+                                <div v-for="(option, optionIndex) in item.option" :key="optionIndex" class="item-options">
+                                    <div class="item-option-content">
+                                        <div class="item-option-order">{{ String.fromCharCode(64 + parseInt(optionIndex + 1)) }} :</div>
+                                        <div class="item-option-text" v-html="option.value"></div>
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="exercise-item-children" v-if="item.children.length">
+                                <!-- <BaseChild :children="item.children" :totalScore="item.score" :isChangePaper="isChangePaper" :isShowScore="!isExamPaper" :canFix="canFix" @onEditChildFinish="onEditChildFinish"> </BaseChild> -->
+                            </div>
+                            <div class="item-btn-toggle">
+                                <span style="margin-right: 10px; font-size: 16px">
+                                    <span style="font-weight: bold; color: #0086e6">{{ item.score }}</span> 分
+                                </span>
+                                <i @click.stop="onQuestionToggle(exerciseList.indexOf(item), item.id, $event, typeItem.list)" :class="collapseList.indexOf(exerciseList.indexOf(item)) > -1 ? 'el-icon-jiantouxiangshang' : 'el-icon-jiantouxiangxia'" style="font-size: 20px;" v-if="item.type !== 'compose'" />
+                            </div>
+                            <!-- 答案以及解析 -->
+                            <transition name="slide" v-if="item.type !== 'compose'">
+                                <!-- <div v-show="collapseList.indexOf(exerciseList.indexOf(item)) > -1" class="toggle-area"> -->
+                                <div v-show="collapseList.indexOf(exerciseList.indexOf(item)) > -1" class="toggle-area">
+                                    <div>
+                                        <!-- 答案展示部分 -->
+                                        <div class="item-explain">
+                                            <span class="explain-title">【题型】</span>
+                                            <div class="item-explain-details">{{ exersicesType[item.type] }}</div>
+                                        </div>
+                                        <div class="item-explain">
+                                            <span class="explain-title">【答案】</span>
+                                            <div class="item-explain-details">
+                                                <!-- 问答题答案 -->
+                                                <div v-if="item.type === 'subjective' || item.type === 'complete' || item.type === 'connector' || item.type === 'correct'">
+                                                    <span v-for="(answer, index) in item.answer" :key="index" v-html="item.answer.length ? answer : '暂无'"></span>
+                                                </div>
+                                                <!-- 问答题答案 -->
+                                                <div v-else-if="item.type === 'judge'">
+                                                    <span>{{ item.answer.length ? (item.answer[0] === "A" ? '正确' : '错误') : '答案' }}</span>
+                                                </div>
+                                                <!-- 其余题型答案 -->
+                                                <div v-else>
+                                                    <span :class="[item.type === 'complete' ? 'item-answer-item' : '']" v-for="(answer, index) in item.answer" :key="index">{{ answer }}</span>
+                                                </div>
+                                            </div>
+                                        </div>
+                                        <!-- 解析部分 -->
+                                        <div class="item-explain">
+                                            <span class="explain-title">【解析】</span>
+                                            <div class="item-explain-details">
+                                                <span v-html="item.explain || '暂无'"></span>
+                                            </div>
+                                        </div>
+                                        <!-- 知识点部分 -->
+                                        <div class="item-explain">
+                                            <span class="explain-title">【知识点】</span>
+                                            <div class="item-explain-details">
+                                                <span v-if="!item.knowledge || !_.compact(item.knowledge).length">暂无</span>
+                                                <div v-else>
+                                                    <span v-for="(point, index) in item.knowledge" :key="index" class="item-point-tag">
+                                                        {{ point }}
+                                                    </span>
+                                                </div>
+                                            </div>
+                                        </div>
+                                        <!-- 认知层次部分 -->
+                                        <div class="item-explain">
+                                            <span class="explain-title">【认知层次】</span>
+                                            <div class="item-explain-details">
+                                                {{ item.field ? exersicesField[item.field - 1] : exersicesField[0] }}
+                                            </div>
+                                        </div>
+                                        <!-- 补救资源部分 -->
+                                        <div class="item-explain">
+                                            <span class="explain-title">【补救资源】</span>
+                                            <div class="item-explain-details">
+                                                <div v-if="item.repair && item.repair.length" style="display: flex; flex-wrap: wrap">
+                                                    <div class="repair-item" v-for="(link, index) in item.repair" :key="index">
+                                                        <img :src="$tools.getFileThum(link.type, link.name)" width="20" />
+                                                        <span class="repair-item-link" @click.stop="onRepairLinkClick(link)">{{ link.name }}</span>
+                                                    </div>
+                                                </div>
+                                                <span v-else>暂无</span>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </transition>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </vuescroll>
+    </div>
+</template>
+
+<script>
+export default {
+    props: {
+        paperInfo: {
+            type: Object,
+            default: undefined
+        }
+    },
+    data() {
+        return {
+            isShowBackToTop: false,
+            typeList: ["single", "multiple", "judge", "complete", "subjective", "connector", "correct", "compose"],
+            exersicesType: {
+                single: '单选题',
+                multiple: '多选题',
+                judge: '判断题',
+                complete: '填空题',
+                subjective: '问答题',
+                connector: '连线题',
+                correct: '改错题',
+                compose: '综合题'
+            },
+            exersicesField: ['记忆', '理解', '应用', '分析', '评价', '创造'],
+            paperDiff: 0,
+            exerciseList: [],
+            groupList: [],
+            orderList: [],
+            isAllOpen: false,
+            viewModel: 'type',
+            collapseList: [],
+        }
+    },
+    mounted() {
+        this.viewModel = this.paperInfo.itemSort === 1 ? 'list' : 'type'
+        this.paperDiff = this.paperInfo.item ? this.handleDiffCalc(this.paperInfo.item) : 0
+
+        if (this.paperInfo.item && this.paperInfo.item.length) {
+            let that = this;
+            this.groupList = []; // 题型排序的数据
+            this.orderList = []; //顺序排列的数据
+            this.exerciseList = [];
+            that.typeSummaryInfo = this.paperInfo.typeSummaryInfo || that.typeSummaryInfo;
+            this.paperInfo = this.paperInfo;
+            this.multipleRule = this.paperInfo.multipleRule || 1; // 配分规则
+            if (this.paperInfo.item.length) {
+                this.paperInfo.item.forEach((i) => {
+                    if (!i.score) i.score = 0;
+                    // 如果有综合题 则将小题的分数进行累加作为综合题的分数
+                    if (i.type === "compose" && i.children.length) {
+                        i.score = i.children.reduce((a, b) => a + b.score, 0);
+                    }
+                });
+                // 给顺序题目排序
+                this.orderList.push({
+                    list: this.paperInfo.item
+                });
+                /* 处理试卷内题目按照题型排序 */
+                this.typeList.forEach((item) => {
+                    this._.mapKeys(this._.groupBy(this.paperInfo.item, "type"), function (value, key) {
+                        if (key === item) {
+                            /* 按照题型排序,并且计算每种题型的总分 */
+                            let typeInfo = {
+                                type: key,
+                                list: value,
+                                score: value.reduce((p, e) => p + e.score, 0)
+                            };
+                            that.groupList.push(typeInfo);
+                            that.exerciseList = that.exerciseList.concat(value);
+                        }
+                    });
+                });
+            }
+            // 重新赋值
+            // this.originData = this.exerciseList;
+            
+            /* window.MathJax.startup.promise.then(() => {
+                window.MathJax.typesetPromise([this.$refs.mathJaxContainer])
+            }) */
+        }
+    },
+    computed: {
+        listData() {
+            return this.viewModel === "type" ? this.groupList : this.orderList;
+        },
+    },
+    methods: {
+        /**
+         * 计算试卷题目平均难度
+         * @param arr 试题集合
+         */
+        handleDiffCalc(arr) {
+            let levelArr = arr.map(i => i.level)
+            return this._.meanBy(levelArr).toFixed(1)
+        },
+        /**
+         * 全部展开与全部折叠
+         */
+        onHandleToggle() {
+            if (this.isAllOpen) {
+                this.collapseList = [];
+            } else {
+                this.collapseList = [...this.exerciseList.keys()];
+            }
+            this.isAllOpen = !this.isAllOpen
+        },
+        onViewModelChange() {
+            this.viewModel = this.viewModel === 'type' ? 'list' : 'type'
+        },
+        /* 获取当前题型的说明文本 */
+        getTypeSummary(typeItem) {
+            let that = this;
+            return (typeItem) => {
+                if (that.typeSummaryInfo && that.typeSummaryInfo[typeItem.type]) {
+                    return that.typeSummaryInfo[typeItem.type];
+                } else {
+                    return `共 ${typeItem.list.length}题,总计${typeItem.score || 0}分`;
+                }
+            };
+        },
+        /**
+         * 题干展开与收缩
+         * @param index
+         * @param id
+         */
+        onQuestionToggle(index, id, e) {
+            e.stopPropagation();
+            this.curEditItemId = null;
+            let listIndex = this.collapseList.indexOf(index);
+            if (listIndex > -1) {
+                this.collapseList.splice(listIndex, 1);
+            } else {
+                /** 如果是首次展开 则需要获取详细数据 否则直接展开 */
+                if (!this.exerciseList[index].answer.length) {
+                    this.collapseList.push(index);
+                } else {
+                    this.collapseList.push(index);
+                }
+            }
+        },
+        /* 补救资源点击事件 */
+        onRepairLinkClick(link) {
+            window.open(/^(http:|https:)/i.test(link.blobUrl) ? link.blobUrl : "http://" + link.blobUrl);
+        },
+        getLatestTypeIndex(type) {
+            let arr = [];
+            this.groupList.forEach((i) => {
+                if (i.list.length) {
+                    arr.push(i.type);
+                }
+            });
+            return arr.indexOf(type);
+        },
+        // 判断容器滚动距离
+        handleScroll(vertical, horizontal, nativeEvent) {
+            this.isShowBackToTop = vertical.scrollTop > 400
+        },
+        handleBackToTop() {
+            console.log(this)
+            this.$nextTick(() => {
+                if (this.$refs.paperRef) {
+                    this.$refs['paperRef'].scrollTo({
+                        y: '0'
+                    },
+                    500
+                )}
+            })
+        },
+    }
+}
+</script>
+
+<style lang="less" scoped>
+@import './TestPaper.less';
+</style>

+ 35 - 11
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/login/Admin.vue

@@ -25,14 +25,19 @@
                         <el-button type="primary" size="medium" @click="showPrivacy('smspin')">登录</el-button>
                     </div>
                     <div class="right-input" v-show="isQRCode">
-                        <h1 style="margin-bottom: 20px;">HiTA扫码登录</h1>
+                        <h1 style="margin-bottom: 20px;">HiTA5或微信扫码登录</h1>
                         <div style="text-align: center; position: relative; min-height: 236px;">
                             <img :src="qrCodeImg.qrcode" alt="" style="width: 230px;">
-                            <span class="refresh-mask" v-show="!ttlChange"><i class="el-icon-refresh-right" @click="getCode('qrcode')"></i></span>
+                            <span class="refresh-mask" v-show="!ttlChange && !qrCodeToken"><i class="el-icon-refresh-right" @click="getCode('qrcode')"></i></span>
+                            <span class="refresh-mask" v-show="qrCodeToken"><i class="el-icon-circle-check"></i></span>
                         </div>
                         <p style="text-align: center; margin-bottom: 20px;">
-                            <span v-show="ttlChange"><span style="color: #fb3636;">{{ ttl }}</span> 后过期</span>
-                            <span v-show="!ttlChange" style="color: #fb3636;">二维码已过期</span>
+                            <span v-show="ttlChange && !qrCodeToken"><span style="color: #fb3636;">{{ ttl }}</span> 后过期</span>
+                            <span v-show="!ttlChange && !qrCodeToken" style="color: #fb3636;">二维码已过期</span>
+                            <span v-show="qrCodeToken" style="color: #13b81b;">
+                                <i class="el-icon-circle-check"></i>
+                                已扫码
+                            </span>
                         </p>
                         <el-button type="primary" size="medium" v-show="qrCodeToken" @click="showPrivacy('qrcode')">扫码成功,请登录</el-button>
                     </div>
@@ -45,8 +50,8 @@
                     </div>
                     <div class="network-info">
                         <span @click="showDevice()">
-                            <i v-show="hybridType" class="el-icon-success" style="color: #0eb90e;"></i>
-                            <i v-show="!hybridType" class="el-icon-error" style="color: #fb3636;"></i>
+                            <i v-show="hybridType" class="el-icon-yunduanziyuan" style="color: #0eb90e;"></i>
+                            <i v-show="!hybridType" class="el-icon-yunduan-guanbi-xianxing" style="color: #fb3636;"></i>
                             服务端信息
                         </span>
                     </div>
@@ -108,8 +113,8 @@
                 内的所有条款。如您同意以上协议内容,请点击“同意并继续”。
             </p>
             <span slot="footer" class="dialog-footer">
-                <el-button @click="isPrivacy = false">不同意</el-button>
                 <el-button type="primary" @click="toLogin(loginType)">同意并继续</el-button>
+                <el-button @click="isPrivacy = false">不同意</el-button>
             </span>
         </el-dialog>
         <el-dialog title="请绑定学校" width="30%" :visible.sync="isBindSchool" :show-close="bindSchoolType" :close-on-press-escape="bindSchoolType" :close-on-click-modal="bindSchoolType">
@@ -183,6 +188,8 @@ export default {
                     item.showHZ = item.hz ? (item.hz / 1000) : 0
                 });
                 if(!res.data.server.school) this.isBindSchool = true
+                localStorage.setItem('deviceId', res.data.device)
+                localStorage.setItem('schoolInfo', JSON.stringify(res.data.server.school))
                 this.deviceInfo = res.data
                 this.hybridType = res.data?.hybrid
             })
@@ -303,7 +310,7 @@ export default {
                     res.data.server.cpuInfos.forEach(item => {
                         item.showHZ = item.hz ? (item.hz / 1000) : 0
                     });
-                    localStorage.setItem('deviceId', res.data.device)
+                    localStorage.setItem('schoolInfo', JSON.stringify(res.data.server.school))
                     this.deviceInfo = res.data
                     this.hybridType = res.data?.hybrid
                     this.isBindSchool = false
@@ -362,7 +369,7 @@ export default {
         },
     },
     beforeDestroy() {
-        this.signalR.stopSignalR()
+        if(this.signalR) this.signalR.stopSignalR()
     },
 }
 </script>
@@ -376,6 +383,7 @@ export default {
     background-position: center;
     background-size: cover;
     background: #F4F7FF;
+
     .login-body {
         display: flex;
         align-items: center;
@@ -442,11 +450,17 @@ export default {
                         transform: translate(-50%, -50%);
 
                         .el-icon-refresh-right {
-                            font-size: 40px;
+                            font-size: 130px;
                             font-weight: bold;
                             color: rgb(255, 255, 255);
                             cursor: pointer;
                         }
+
+                        .el-icon-circle-check {
+                            font-size: 120px;
+                            font-weight: bold;
+                            color: #45ce4c;
+                        }
                     }
                 }
 
@@ -529,11 +543,21 @@ export default {
 
     .footer-info-item {
         color: #919191;
-        margin-right: 40px;
+        margin: 0 20px;
         font-size: 12px;
     }
 }
 
+@media screen and (max-width: 1366px) {
+    .login .login-body .login-box {
+        .body-right {
+            .network-info {
+                bottom: -3%;
+            }
+        }
+    }
+}
+
 @media screen and (max-width: 1280px) {
     .login .login-body .login-box {
         .body-left {

+ 225 - 4
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/login/Student.vue

@@ -1,13 +1,234 @@
 <template>
-    <div>学生端
+    <div class="login">
+        <!-- 学生端
         会获取到学校信息、正在进行的活动,此处会展示活动名称、学校信息、账号密码
-        若没有活动,则不展示账号密码,提示没有活动
+        若没有活动,则不展示账号密码,提示没有活动 -->
+            <div class="activity-school" v-if="evaluationInfo">
+                <h2>{{ evaluationInfo.name }}</h2>
+                <p>
+                    <img :src="evaluationInfo.ownerPicture" alt="">
+                    <span>{{ evaluationInfo.ownerName }}</span>
+                </p>
+            </div>
+        <div class="login-body">
+            <div class="login-box">
+                <div class="body-left">
+                    <i class="el-icon-s-custom" style="font-size: 10em; color: #67C23A;"></i>
+                </div>
+                <div class="body-right">
+                    <div class="right-input">
+                        <h1>学生账号登录</h1>
+                        <el-input v-model="loginForm.phone" placeholder="账号" size="medium" />
+                        <el-input v-model="loginForm.smspin" placeholder="密码" size="medium" type="password" />
+                        <el-button type="success" size="medium" @click="toLogin()">登录</el-button>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="login-footer">
+            <a class="footer-info-item">蜀ICP备18027363号</a>
+            <span class="footer-info-item">© 2021 HABOOK Group 醍摩豆</span>
+        </div>
     </div>
 </template>
 
 <script>
-export default {}
+export default {
+    data() {
+        return {
+            evaluationInfo: undefined,
+            loginForm: {
+                id: '',
+                password: '',
+            },
+            schoolInfo: {
+                id: 'hbcn',
+                name: '醍摩豆学校',
+                picture: 'https://teammodelos.blob.core.chinacloudapi.cn/0-public/school/9b6c5b5c-aa9b-4529-8d27-1a674c58040c.png'
+            },
+        }
+    },
+    created() {
+        this.viewNetworkInfo()
+    },
+    methods: {
+        async viewNetworkInfo() {
+            let params = {
+                fp: await this.$tools.getFingerprint()
+            }
+            this.$api.getDevice(params).then(res => {
+                /* res.data.server.host = []
+                res.data.server.uris.forEach(item => {
+                    res.data.server.networks.forEach(network => {
+                        // https://192.168.8.140:5001/login/student
+                        let url = `${item.protocol}://${network.ip}:${item.port}/login/student`
+                        res.data.server.host.push(url)
+                    })
+                })
+                res.data.server.shwoRam = (res.data.server.ram / 1024 / 1024 / 1024).toFixed(1)
+                res.data.server.cpuInfos.forEach(item => {
+                    item.showHZ = item.hz ? (item.hz / 1000) : 0
+                });
+                if(!res.data.server.school) this.isBindSchool = true
+                localStorage.setItem('deviceId', res.data.device)
+                localStorage.setItem('schoolInfo', JSON.stringify(res.data.server.school))
+                this.deviceInfo = res.data
+                this.hybridType = res.data?.hybrid */
+
+                this.stuGetEvaluation()
+            })
+        },
+        stuGetEvaluation() {
+            this.$api.stuGetEvaluation({}).then(res => {
+                if(res.code === 200) {
+                    this.evaluationInfo = res.evaluationClient
+                } else {
+                    this.$message({
+                        message: '当前无评测可以作答',
+                        type: 'warning'
+                    });
+                }
+            })
+        },
+        toLogin() {},
+    }
+}
 </script>
+<style lang="less" scoped>
+.login {
+    width: 100%;
+    height: 100%;
+    background: #F4F7FF;
+    position: relative;
+    background-image: url(./background.jpg);
+    background-repeat: no-repeat;
+    background-attachment: fixed;
+    background-position: 50%;
+    background-size: cover;
+
+    .activity-school {
+        position: absolute;
+        top: 40px;
+        left: auto;
+        font-size: 30px;
+        color: #4f4f4f;
+        padding-bottom: 20px;
+        margin-bottom: 10px;
+        text-align: center;
+        width: 100%;
+
+        p {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            margin-top: 10px;
+            color: #686868;
+
+            img {
+                width: 50px;
+                margin-right: 10px;
+            }
+        }
+    }
+    
+    .login-body {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        height: 100%;
+
+        .login-box {
+            border-radius: 20px;
+            background: #fff;
+            overflow: hidden;
+            display: flex;
+            justify-content: center;
+            // align-items: center;
+            margin-top: -50px;
+            box-shadow: 0px 2px 30px rgba(0, 0, 0, 0.15);
+            height: 45%;
+            width: 45%;
+            position: relative;
+
+            .body-left {
+                // width: 800px;
+                width: 35%;
+                text-align: center;
+                position: relative;
+                border-right: 1px solid #ccc;
+                box-shadow: 2px 0 15px 5px rgba(18, 19, 19, .3);
+                display: flex;
+                align-items: center;
+                justify-content: center;
+            }
+
+            .body-right {
+                width: calc(65% - 100px);
+                height: auto;
+                position: relative;
+                margin: 6% 50px;
+
+                h1{
+                    margin-bottom: 50px;
+                    color: #5b5b5b;
+                }
+                .el-input {
+                    margin-bottom: 20px;
+                }
+
+                .el-button {
+                    width: 100%;
+                    margin: 10px 0;
+                }
+            }
+        }
+    }
+}
+
+.login-footer {
+    width: 100%;
+    position: fixed;
+    bottom: 10px;
+    z-index: 100;
+    left: 0;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    .footer-info-item {
+        color: #626262;
+        margin: 0 20px;
+        font-size: 12px;
+    }
+}
+
+@media screen and (max-width: 1366px) {
+    .login .login-body .login-box {
+        .body-right {
+            
+        }
+    }
+}
+
+@media screen and (max-width: 1280px) {
+    .login .login-body .login-box {
+        .body-left {
+            display: none;
+        }
+        .body-right {
+            width: 100%;
+        }
+    }
+}
+
+@media screen and (max-width: 768px) {
+    .login .login-body .login-box {
+        width: 60%;
+        height: 35%;
 
-<style>
+        .body-right {
+            margin: 6% 25px;
+        }
+    }
+}
 </style>

二进制
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/login/background.jpg


+ 9 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/vue.config.js

@@ -58,7 +58,15 @@ module.exports = defineConfig({
                 pathRewrite: {
                     '^/api': ''
                 }
-            }
+            },
+            '/package': {
+                target: 'https://localhost:6001', //后端接口
+                secure: false,
+                changeOrigin: false,
+                pathRewrite: {
+                    '^/package': '/package'
+                }
+            },
         },
         historyApiFallback: true,
     },

+ 64 - 1
TEAMModelOS.Function/CosmosDBTriggers/TriggerExam.cs

@@ -235,7 +235,6 @@ namespace TEAMModelOS.CosmosDBTriggers
                                             var sresponse = await client.GetContainer(Constant.TEAMModelOS, "School").ReadItemStreamAsync(cla, new PartitionKey($"Class-{info.school}"));
                                             if (sresponse.StatusCode == System.Net.HttpStatusCode.OK)
                                             {
-
                                                 using var json = await JsonDocument.ParseAsync(sresponse.Content);
                                                 Class classroom = json.ToObject<Class>();
 
@@ -393,6 +392,14 @@ namespace TEAMModelOS.CosmosDBTriggers
                                         await client.GetContainer(Constant.TEAMModelOS, "Common").ReplaceItemAsync<ExamInfo>(info, info.id, new PartitionKey(info.code));
                                     }
                                 }*/
+
+                                //統測活動處理
+                                if(!string.IsNullOrWhiteSpace(info.jointExamId))
+                                {
+                                    //老師活動進行階段狀態更新
+                                    JointEventGroupDb jointCourse = await SetJointCourseScheduleStatus(client, info, true);
+                                    //發送階段完成通知 [待]
+                                }
                             }
                             catch (Exception e)
                             {
@@ -554,6 +561,14 @@ namespace TEAMModelOS.CosmosDBTriggers
                                     await client.GetContainer(Constant.TEAMModelOS, "Common").ReplaceItemAsync<ExamInfo>(info, info.id, new PartitionKey(info.code));
                                 }
                                 await SetLearnRecordContent(info, data, _azureStorage, _azureCosmos);
+
+                                //統測活動處理
+                                if (!string.IsNullOrWhiteSpace(info.jointExamId))
+                                {
+                                    //老師活動進行階段狀態更新
+                                    JointEventGroupDb jointCourse = await SetJointCourseScheduleStatus(client, info, true);
+                                    //發送階段完成通知 [待]
+                                }
                             }
                             catch (Exception e)
                             {
@@ -797,6 +812,54 @@ namespace TEAMModelOS.CosmosDBTriggers
             }
         }
 
+        /// <summary>
+        /// 更新統測活動 各老師活動進行狀態
+        /// </summary>
+        /// <param name="info">評量</param>
+        /// <param name="updFlg">是否更新 false:只取得報名班級狀態</param>
+        /// <returns></returns>
+        private static async Task<JointEventGroupDb> SetJointCourseScheduleStatus(CosmosClient client, ExamInfo info, bool updFlg=false)
+        {
+            //進行階段狀態更新(update JointCourse)
+            JointEventGroupDb jointCourse = new JointEventGroupDb();
+            string jointEventId = string.Empty;
+            string jointGroupId = string.Empty;
+            string examCreatorId = string.Empty;
+            ///取得統測活動、老師報名班級
+            var jexamResponse = await client.GetContainer(Constant.TEAMModelOS, Constant.Common).ReadItemStreamAsync(info.jointExamId, new PartitionKey($"JointExam"));
+            if (jexamResponse.StatusCode == System.Net.HttpStatusCode.OK)
+            {
+                using var json = await JsonDocument.ParseAsync(jexamResponse.Content);
+                JointExam jointExam = json.ToObject<JointExam>();
+                jointEventId = jointExam.jointEventId;
+                jointGroupId = jointExam.jointGroupId;
+                examCreatorId = info.creatorId;
+                string jcSql = $"SELECT * FROM c WHERE c.jointEventId = '{jointEventId}' AND c.jointGroupId = '{jointGroupId}' AND c.creatorId = '{examCreatorId}' AND c.type = 'regular' ";
+                await foreach (var item in client.GetContainer(Constant.TEAMModelOS, Constant.Teacher).GetItemQueryStreamIteratorSql(queryText: jcSql, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"JointCourse") }))
+                {
+                    using var jsonJc = await JsonDocument.ParseAsync(item.Content);
+                    if (jsonJc.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
+                    {
+                        foreach (var obj in jsonJc.RootElement.GetProperty("Documents").EnumerateArray())
+                        {
+                            jointCourse = obj.ToObject<JointEventGroupDb>();
+                        }
+                    }
+                }
+            }
+            ///更新 JointCourse
+            if (!string.IsNullOrWhiteSpace(jointCourse.id))
+            {
+                jointCourse = await JointService.CalJointCourseGroupScheduleStatusAsync(client, jointEventId, jointGroupId, examCreatorId, jointCourse, null); //各Schedule Status計算
+                if(updFlg)
+                {
+                    await client.GetContainer(Constant.TEAMModelOS, Constant.Teacher).ReplaceItemAsync<JointEventGroupDb>(jointCourse, jointCourse.id, new PartitionKey(jointCourse.code));
+                }
+                
+            }
+
+            return jointCourse;
+        }
 
         /// <summary>
         /// 取得學習紀錄的actor

+ 20 - 2
TEAMModelOS.Function/IESTimerTrigger.cs

@@ -11,6 +11,9 @@ using System.Globalization;
 using TEAMModelOS.SDK.Models;
 using HTEX.Lib.ETL.Lesson;
 using System.Linq;
+using TEAMModelOS.SDK.DI.IPIP;
+using static TEAMModelOS.SDK.Extension.GeoRegion;
+using System.Text.Json;
 
 namespace TEAMModelOS.Function
 {
@@ -31,7 +34,8 @@ namespace TEAMModelOS.Function
         private readonly CoreAPIHttpService _coreAPIHttpService;
         //private IPSearcher _ipSearcher;
         private readonly IConfiguration _configuration;
-        public IESTimerTrigger(ILoggerFactory loggerFactory, IConfiguration configuration, CoreAPIHttpService coreAPIHttpService, IHttpClientFactory httpClient, SnowflakeId snowflakeId,   AzureCosmosFactory azureCosmos, DingDing dingDing, AzureStorageFactory azureStorage, AzureRedisFactory azureRedis)
+        private readonly City _city;
+        public IESTimerTrigger(ILoggerFactory loggerFactory, IConfiguration configuration, CoreAPIHttpService coreAPIHttpService, IHttpClientFactory httpClient, SnowflakeId snowflakeId,   AzureCosmosFactory azureCosmos, DingDing dingDing, AzureStorageFactory azureStorage, AzureRedisFactory azureRedis, City city)
         {
             _logger = loggerFactory.CreateLogger<IESTimerTrigger>();
             _azureCosmos = azureCosmos;
@@ -44,6 +48,7 @@ namespace TEAMModelOS.Function
             //  _ipSearcher = ipSearcher;
             _coreAPIHttpService = coreAPIHttpService;
             _configuration= configuration;
+            _city = city;
         }
 
         //0 0 20 * * 0
@@ -140,8 +145,21 @@ namespace TEAMModelOS.Function
             var y = $"{datetime.Year}";
             var m = datetime.Month >= 10 ? $"{datetime.Month}" : $"0{datetime.Month}";
             var d = datetime.Day >= 10 ? $"{datetime.Day}" : $"0{datetime.Day}";
+            string? local = Environment.GetEnvironmentVariable("Option:Location");
+            List<regionrow> region_gl = new List<regionrow>();
+            using (StreamReader r = new StreamReader("JsonFile/Region/region_gl.json"))
+            {
+                string json_g = r.ReadToEnd();
+                region_gl = JsonSerializer.Deserialize<List<regionrow>>(json_g);
+            }
+            List<regionrow> region_cn = new List<regionrow>();
+            using (StreamReader r = new StreamReader("JsonFile/Region/region_cn.json"))
+            {
+                string json_c = r.ReadToEnd();
+                region_cn = JsonSerializer.Deserialize<List<regionrow>>(json_c);
+            }
             //生成學校IOT數據
-            await BIProdAnalysis.BICreatDailyAnalData(_azureRedis, _azureCosmosClient, _azureCosmosClientCsv2, _azureCosmosClientCsv2CnRead, _dingDing, y, m, d);
+            await BIProdAnalysis.BICreatDailyAnalData(_azureRedis, _azureCosmosClient, _azureCosmosClientCsv2, _azureCosmosClientCsv2CnRead, _dingDing, _city, local, y, m, d, region_gl, region_cn);
             //刪除三個月以前的Redis數據 [待做]
         }
     }

文件差异内容过多而无法显示
+ 13614 - 0
TEAMModelOS.Function/JsonFile/Region/region_cn.json


文件差异内容过多而无法显示
+ 1880 - 0
TEAMModelOS.Function/JsonFile/Region/region_en.json


文件差异内容过多而无法显示
+ 1896 - 0
TEAMModelOS.Function/JsonFile/Region/region_gl.json


+ 2 - 1
TEAMModelOS.Function/Program.cs

@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
 using TEAMModelOS.SDK;
 using TEAMModelOS.SDK.DI;
+using TEAMModelOS.SDK.DI.IPIP;
 using TEAMModelOS.SDK.DI.Multiple;
 var host = new HostBuilder()
     .ConfigureFunctionsWebApplication()
@@ -33,7 +34,7 @@ var host = new HostBuilder()
         List<(string name, string connectionString)> storageConnects = new();
         storageConnects.Add(("Default", context.Configuration.GetSection("Azure:Storage:ConnectionString").Get<string>()));
         services.AddMultipleAzureStorage(storageConnects);
-
+        services.AddSingleton(new City(@"Services/ipip.ipdb"));
         services.AddSingleton<BackgroundWorkerQueue>();
         services.AddHostedService<LongRunningService>();
     })

二进制
TEAMModelOS.Function/Services/ipip.ipdb


+ 11 - 3
TEAMModelOS.Function/TEAMModelOS.Function.csproj

@@ -5,9 +5,9 @@
     <OutputType>Exe</OutputType>
     <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
-	<Version>5.2502.5</Version>
-	<AssemblyVersion>5.2502.5.1</AssemblyVersion>
-	<FileVersion>5.2502.5.1</FileVersion>
+	<Version>5.2502.19</Version>
+	<AssemblyVersion>5.2502.19.1</AssemblyVersion>
+	<FileVersion>5.2502.19.1</FileVersion>
 	<PackageId>TEAMModelOS.FunctionV4</PackageId>
 	<Authors>teammodel</Authors>
 	<Company>醍摩豆(成都)信息技术有限公司</Company>
@@ -63,6 +63,14 @@
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToPublishDirectory>Never</CopyToPublishDirectory>
     </None>
+	<None Update="Services\ipip.ipdb">
+	  <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+	  <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
+	</None>
+	<None Update="JsonFile\Region\*.json">
+	  <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+	  <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
+	</None>
   </ItemGroup>
   <ItemGroup>
     <Using Include="System.Threading.ExecutionContext" Alias="ExecutionContext" />

+ 68 - 0
TEAMModelOS.SDK/DI/IPIP/BaseStation.cs

@@ -0,0 +1,68 @@
+using System.Collections.Generic;
+
+namespace TEAMModelOS.SDK.DI.IPIP
+{
+
+    public class BaseStation
+    {
+
+        /**
+         * @var Reader
+         */
+        private readonly Reader reader;
+
+        public BaseStation(string name)  {
+            reader = new Reader(name);
+        }
+
+        public string[] find(string addr, string language) {
+            return reader.find(addr, language);
+        }
+
+        public Dictionary<string, string> findMap(string addr, string language)  {
+            var data = reader.find(addr, language);
+            if (data == null) {
+                return null;
+            }
+            var m = new Dictionary<string, string>();
+
+            var fields = reader.getSupportFields();
+
+            for (int i = 0, l = data.Length; i<l; i++) {
+                m.Add(fields[i], data[i]);
+            }
+
+            return m;
+        }
+
+        public BaseStationInfo findInfo(string addr, string language)  {
+
+            var data = reader.find(addr, language);
+            if (data == null) {
+                return null;
+            }
+
+            return new BaseStationInfo(data);
+        }
+
+        public bool isIPv4()
+        {
+            return (reader.getMeta().IPVersion & 0x01) == 0x01;
+        }
+
+        public bool isIPv6()
+        {
+            return (reader.getMeta().IPVersion & 0x02) == 0x02;
+        }
+
+        public string[] fields()
+        {
+            return reader.getSupportFields();
+        }
+
+        public int buildTime()
+        {
+            return reader.getBuildUTCTime();
+        }
+    }
+}

+ 72 - 0
TEAMModelOS.SDK/DI/IPIP/BaseStationInfo.cs

@@ -0,0 +1,72 @@
+using System.Text;
+
+namespace TEAMModelOS.SDK.DI.IPIP
+{
+
+    public class BaseStationInfo
+    {
+
+        private readonly string[] data;
+
+        public BaseStationInfo(string[] data)
+        {
+            this.data = data;
+        }
+
+        public string getCountryName()
+        {
+            return data[0];
+        }
+
+        public string getRegionName()
+        {
+            return data[1];
+        }
+
+        public string getCityName()
+        {
+            return data[2];
+        }
+
+        public string getOwnerDomain()
+        {
+            return data[3];
+        }
+
+        public string getIspDomain()
+        {
+            return data[4];
+        }
+
+        public string getBaseStation()
+        {
+            return data[5];
+        }
+
+        public override string ToString()
+        {
+            var sb = new StringBuilder();
+
+            sb.Append("country_name:");
+            sb.Append(getCountryName());
+            sb.Append("\n");
+            sb.Append("region_name:");
+            sb.Append(getRegionName());
+            sb.Append("\n");
+            sb.Append("city_name:");
+            sb.Append(getCityName());
+            sb.Append("\n");
+            sb.Append("owner_domain:");
+            sb.Append(getOwnerDomain());
+            sb.Append("\n");
+            sb.Append("isp_domain:");
+            sb.Append(getIspDomain());
+            sb.Append("\n");
+            sb.Append("base_station:");
+            sb.Append(getBaseStation());
+
+            return sb.ToString();
+        }
+    }
+
+}

+ 66 - 0
TEAMModelOS.SDK/DI/IPIP/City.cs

@@ -0,0 +1,66 @@
+using System.Collections.Generic;
+
+namespace TEAMModelOS.SDK.DI.IPIP
+{
+
+    public class City
+    {
+
+        /**
+         * @var Reader
+         */
+        private readonly Reader reader;
+
+        public City(string name) {
+            reader = new Reader(name);
+        }
+
+        public string[] find(string addr, string language)  {
+            return reader.find(addr, language);
+        }
+
+        public Dictionary<string, string> findMap(string addr, string language)  {
+            var data = reader.find(addr, language);
+            if (data == null) {
+                return null;
+            }
+            var fields = reader.getSupportFields();
+            var m = new Dictionary<string, string>();
+            for (int i = 0, l = data.Length; i<l; i++) {
+                m.Add(fields[i], data[i]);
+            }
+
+            return m;
+        }
+
+        public CityInfo findInfo(string addr, string language)  {
+
+            var data = reader.find(addr, language);
+            if (data == null) {
+                return null;
+            }
+
+            return new CityInfo(data);
+        }
+
+        public bool isIPv4()
+        {
+            return (reader.getMeta().IPVersion & 0x01) == 0x01;
+        }
+
+        public bool isIPv6()
+        {
+            return (reader.getMeta().IPVersion & 0x02) == 0x02;
+        }
+
+        public string[] fields()
+        {
+            return reader.getSupportFields();
+        }
+
+        public int buildTime()
+        {
+            return reader.getBuildUTCTime();
+        }
+    }
+}

+ 199 - 0
TEAMModelOS.SDK/DI/IPIP/CityInfo.cs

@@ -0,0 +1,199 @@
+using System.Text;
+
+namespace TEAMModelOS.SDK.DI.IPIP
+{
+
+    public class CityInfo
+    {
+
+        private readonly string[] data;
+
+        public CityInfo(string[] data)
+        {
+            this.data = data;
+        }
+
+        public string getCountryName()
+        {
+            return data[0];
+        }
+
+        public string getRegionName()
+        {
+            return data[1];
+        }
+
+        public string getCityName()
+        {
+            return data[2];
+        }
+
+        public string getOwnerDomain()
+        {
+            return data[3];
+        }
+
+        public string getIspDomain()
+        {
+            return data[4];
+        }
+
+        public string getLatitude()
+        {
+            return data[5];
+        }
+
+        public string getLongitude()
+        {
+            return data[6];
+        }
+
+        public string getTimezone()
+        {
+            return data[7];
+        }
+
+        public string getUtcOffset()
+        {
+            return data[8];
+        }
+
+        public string getChinaAdminCode()
+        {
+            return data[9];
+        }
+
+        public string getIddCode()
+        {
+            return data[10];
+        }
+
+        public string getCountryCode()
+        {
+            return data[11];
+        }
+
+        public string getContinentCode()
+        {
+            return data[12];
+        }
+
+        public string getIDC()
+        {
+            return data[13];
+        }
+
+        public string getBaseStation()
+        {
+            return data[14];
+        }
+
+        public string getCountryCode3()
+        {
+            return data[15];
+        }
+
+        public string getEuropeanUnion()
+        {
+            return data[16];
+        }
+
+        public string getCurrencyCode()
+        {
+            return data[17];
+        }
+
+        public string getCurrencyName()
+        {
+            return data[18];
+        }
+
+        public string getAnycast()
+        {
+            return data[19];
+        }
+
+        public override string ToString()
+        {
+
+            var sb = new StringBuilder();
+
+            sb.Append("country_name:");
+            sb.Append(getCountryName());
+            sb.Append("\n");
+            sb.Append("region_name:");
+            sb.Append(getRegionName());
+            sb.Append("\n");
+            sb.Append("city_name:");
+            sb.Append(getCityName());
+            sb.Append("\n");
+            sb.Append("owner_domain:");
+            sb.Append(getOwnerDomain());
+            sb.Append("\n");
+            sb.Append("isp_domain:");
+            sb.Append(getIspDomain());
+            sb.Append("\n");
+            sb.Append("latitude:");
+            sb.Append(getLatitude());
+            sb.Append("\n");
+            sb.Append("longitude:");
+            sb.Append(getLongitude());
+            sb.Append("\n");
+
+            sb.Append("timezone:");
+            sb.Append(getTimezone());
+            sb.Append("\n");
+
+            sb.Append("utc_offset:");
+            sb.Append(getUtcOffset());
+            sb.Append("\n");
+
+            sb.Append("china_admin_code:");
+            sb.Append(getChinaAdminCode());
+            sb.Append("\n");
+
+            sb.Append("idd_code:");
+            sb.Append(getIddCode());
+            sb.Append("\n");
+
+            sb.Append("country_code:");
+            sb.Append(getCountryCode());
+            sb.Append("\n");
+
+            sb.Append("continent_code:");
+            sb.Append(getContinentCode());
+            sb.Append("\n");
+
+            sb.Append("idc:");
+            sb.Append(getIDC());
+            sb.Append("\n");
+
+            sb.Append("base_station:");
+            sb.Append(getBaseStation());
+            sb.Append("\n");
+
+            sb.Append("country_code3:");
+            sb.Append(getCountryCode3());
+            sb.Append("\n");
+
+            sb.Append("european_union:");
+            sb.Append(getEuropeanUnion());
+            sb.Append("\n");
+
+            sb.Append("currency_code:");
+            sb.Append(getCurrencyCode());
+            sb.Append("\n");
+
+            sb.Append("currency_name:");
+            sb.Append(getCurrencyName());
+            sb.Append("\n");
+
+            sb.Append("anycast:");
+            sb.Append(getAnycast());
+
+            return sb.ToString();
+
+        }
+    }
+
+}

+ 69 - 0
TEAMModelOS.SDK/DI/IPIP/District.cs

@@ -0,0 +1,69 @@
+using System.Collections.Generic;
+
+namespace TEAMModelOS.SDK.DI.IPIP
+{
+
+    public class District
+    {
+
+        /**
+         * @var Reader
+         */
+        private readonly Reader reader;
+
+        public District(string name)  {
+            reader = new Reader(name);
+        }
+
+        public string[] find(string addr, string language)  {
+            return reader.find(addr, language);
+        }
+
+        public Dictionary<string, string> findMap(string addr, string language)  {
+            var data = reader.find(addr, language);
+            if (data == null) {
+                return null;
+            }
+
+            var m = new Dictionary<string, string>();
+
+            var fields = reader.getSupportFields();
+
+            for (int i = 0, l = data.Length; i<l; i++) {
+                m.Add(fields[i], data[i]);
+            }
+
+            return m;
+        }
+
+        public DistrictInfo findInfo(string addr, string language)  {
+
+            var data = reader.find(addr, language);
+            if (data == null) {
+                return null;
+            }
+
+            return new DistrictInfo(data);
+        }
+
+        public bool isIPv4()
+        {
+            return (reader.getMeta().IPVersion & 0x01) == 0x01;
+        }
+
+        public bool isIPv6()
+        {
+            return (reader.getMeta().IPVersion & 0x02) == 0x02;
+        }
+
+        public string[] fields()
+        {
+            return reader.getSupportFields();
+        }
+
+        public int buildTime()
+        {
+            return reader.getBuildUTCTime();
+        }
+    }
+}

+ 88 - 0
TEAMModelOS.SDK/DI/IPIP/DistrictInfo.cs

@@ -0,0 +1,88 @@
+using System.Text;
+
+namespace TEAMModelOS.SDK.DI.IPIP
+{
+
+    public class DistrictInfo
+    {
+
+        private readonly string[] data;
+
+        public DistrictInfo(string[] data)
+        {
+            this.data = data;
+        }
+
+        public string getCountryName()
+        {
+            return data[0];
+        }
+
+        public string getRegionName()
+        {
+            return data[1];
+        }
+
+        public string getCityName()
+        {
+            return data[2];
+        }
+
+        public string getDistrictName()
+        {
+            return data[3];
+        }
+
+        public string getChinaAdminCode()
+        {
+            return data[4];
+        }
+
+        public string getCoveringRadius()
+        {
+            return data[5];
+        }
+
+        public string getLatitude()
+        {
+            return data[7];
+        }
+
+        public string getLongitude()
+        {
+            return data[6];
+        }
+
+        public override string ToString()
+        {
+            var sb = new StringBuilder();
+
+            sb.Append("country_name:");
+            sb.Append(getCountryName());
+            sb.Append("\n");
+            sb.Append("region_name:");
+            sb.Append(getRegionName());
+            sb.Append("\n");
+            sb.Append("city_name:");
+            sb.Append(getCityName());
+            sb.Append("\n");
+            sb.Append("district_name:");
+            sb.Append(getDistrictName());
+            sb.Append("\n");
+            sb.Append("china_admin_code:");
+            sb.Append(getChinaAdminCode());
+            sb.Append("\n");
+            sb.Append("covering_radius:");
+            sb.Append(getCoveringRadius());
+            sb.Append("\n");
+            sb.Append("latitude:");
+            sb.Append(getLatitude());
+            sb.Append("\n");
+            sb.Append("longitude:");
+            sb.Append(getLongitude());
+
+            return sb.ToString();
+        }
+    }
+
+}

+ 68 - 0
TEAMModelOS.SDK/DI/IPIP/IDC.cs

@@ -0,0 +1,68 @@
+using System.Collections.Generic;
+
+namespace TEAMModelOS.SDK.DI.IPIP
+{
+
+    public class IDC
+    {
+
+        /**
+         * @var Reader
+         */
+        private readonly Reader reader;
+
+        public IDC(string name)  {
+            reader = new Reader(name);
+        }
+
+        public string[] find(string addr, string language)  {
+            return reader.find(addr, language);
+        }
+
+        public Dictionary<string, string> findMap(string addr, string language)  {
+            var data = reader.find(addr, language);
+            if (data == null) {
+                return null;
+            }
+
+            var m = new Dictionary<string, string>();
+
+            var fields = reader.getSupportFields();
+
+            for (int i = 0, l = data.Length; i<l; i++) {
+                m.Add(fields[i], data[i]);
+            }
+
+            return m;
+        }
+
+        public IDCInfo findInfo(string addr, string language)  {
+
+            var data = reader.find(addr, language);
+            if (data == null) {
+                return null;
+            }
+            return new IDCInfo(data);
+        }
+
+        public bool isIPv4()
+        {
+            return (reader.getMeta().IPVersion & 0x01) == 0x01;
+        }
+
+        public bool isIPv6()
+        {
+            return (reader.getMeta().IPVersion & 0x02) == 0x02;
+        }
+
+        public string[] fields()
+        {
+            return reader.getSupportFields();
+        }
+
+        public int buildTime()
+        {
+            return reader.getBuildUTCTime();
+        }
+    }
+}

+ 71 - 0
TEAMModelOS.SDK/DI/IPIP/IDCInfo.cs

@@ -0,0 +1,71 @@
+using System.Text;
+
+namespace TEAMModelOS.SDK.DI.IPIP
+{
+    public class IDCInfo
+    {
+
+        private readonly string[] data;
+
+        public IDCInfo(string[] data)
+        {
+            this.data = data;
+        }
+
+        public string getCountryName()
+        {
+            return data[0];
+        }
+
+        public string getRegionName()
+        {
+            return data[1];
+        }
+
+        public string getCityName()
+        {
+            return data[2];
+        }
+
+        public string getOwnerDomain()
+        {
+            return data[3];
+        }
+
+        public string getIspDomain()
+        {
+            return data[4];
+        }
+
+        public string getIDC()
+        {
+            return data[5];
+        }
+
+        public override string ToString()
+        {
+            var sb = new StringBuilder();
+
+            sb.Append("country_name:");
+            sb.Append(getCountryName());
+            sb.Append("\n");
+            sb.Append("region_name:");
+            sb.Append(getRegionName());
+            sb.Append("\n");
+            sb.Append("city_name:");
+            sb.Append(getCityName());
+            sb.Append("\n");
+            sb.Append("owner_domain:");
+            sb.Append(getOwnerDomain());
+            sb.Append("\n");
+            sb.Append("isp_domain:");
+            sb.Append(getIspDomain());
+            sb.Append("\n");
+            sb.Append("idc:");
+            sb.Append(getIDC());
+
+            return sb.ToString();
+        }
+    }
+
+}

+ 13 - 0
TEAMModelOS.SDK/DI/IPIP/IPFormatException.cs

@@ -0,0 +1,13 @@
+using System;
+
+namespace TEAMModelOS.SDK.DI.IPIP
+{
+    public class IPFormatException : Exception
+    {
+
+        public IPFormatException(string name) : base(name)
+        {
+        }
+    }
+
+}

+ 11 - 0
TEAMModelOS.SDK/DI/IPIP/InvalidDatabaseException.cs

@@ -0,0 +1,11 @@
+using System.IO;
+
+namespace TEAMModelOS.SDK.DI.IPIP
+{
+    public class InvalidDatabaseException : IOException
+    {
+        public InvalidDatabaseException(string message) : base(message)
+        {
+        }
+    }
+}

+ 18 - 0
TEAMModelOS.SDK/DI/IPIP/MetaData.cs

@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using J = Newtonsoft.Json.JsonPropertyAttribute;
+
+namespace TEAMModelOS.SDK.DI.IPIP
+{
+    /**
+     * @copyright IPIP.net
+     */
+    public class MetaData
+    {
+        [J("build")]public int Build;
+        [J("ip_version")] public int IPVersion;
+        [J("node_count")] public int nodeCount;
+        [J("languages")] public Dictionary<string, int> Languages;
+        [J("fields")] public string[] Fields;
+        [J("total_size")] public int totalSize;
+    }
+}

+ 12 - 0
TEAMModelOS.SDK/DI/IPIP/NotFoundException.cs

@@ -0,0 +1,12 @@
+using System;
+
+namespace TEAMModelOS.SDK.DI.IPIP
+{
+    public class NotFoundException : Exception
+    {
+
+        public NotFoundException(string name) : base(name)
+        {
+        }
+    }
+}

+ 241 - 0
TEAMModelOS.SDK/DI/IPIP/Reader.cs

@@ -0,0 +1,241 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Text;
+using Newtonsoft.Json;
+
+namespace TEAMModelOS.SDK.DI.IPIP
+{
+    public class Reader
+    {
+        private readonly int fileSize;
+        private readonly int nodeCount;
+
+        private readonly MetaData meta;
+        private readonly byte[] data;
+
+        private readonly int v4offset;
+
+        public Reader(string name)
+        {
+
+            var file = new FileInfo(name);
+            fileSize = (int)file.Length;
+
+            data = File.ReadAllBytes(name);
+
+            var metaLength = bytesToLong(
+                data[0],
+                data[1],
+                data[2],
+                data[3]
+            );
+
+            var metaBytes = new byte[metaLength];
+            Array.Copy(data, 4, metaBytes, 0, metaLength);
+
+            var meta = JsonConvert.DeserializeObject<MetaData>(Encoding.UTF8.GetString(metaBytes));
+
+            nodeCount = meta.nodeCount;
+            this.meta = meta;
+
+            if ((meta.totalSize + (int)metaLength + 4) != data.Length)
+            {
+                throw new InvalidDatabaseException("database file size error");
+            }
+
+            data = data.Skip((int)metaLength + 4).ToArray();
+
+            if (v4offset == 0)
+            {
+                var node = 0;
+                for (var i = 0; i < 96 && node < nodeCount; i++)
+                {
+                    if (i >= 80)
+                    {
+                        node = readNode(node, 1);
+                    }
+                    else
+                    {
+                        node = readNode(node, 0);
+                    }
+                }
+
+                v4offset = node;
+            }
+        }
+
+        public string[] find(string addr, string language)
+        {
+
+            int off;
+            try
+            {
+                off = meta.Languages[language];
+            }
+            catch (NullReferenceException)
+            {
+                return null;
+            }
+
+            byte[] ipv;
+
+            if (addr.IndexOf(":") > 0)
+            {
+                try
+                {
+                    ipv = IPAddress.Parse(addr).GetAddressBytes();
+                }
+                catch (Exception)
+                {
+                    return null;
+                    //throw new IPFormatException("ipv6 format error");
+                }
+
+                if ((meta.IPVersion & 0x02) != 0x02)
+                {
+                    return null;
+                    //throw new IPFormatException("no support ipv6");
+                }
+
+            }
+            else if (addr.IndexOf(".") > 0)
+            {
+                try
+                {
+                    ipv = IPAddress.Parse(addr).GetAddressBytes();
+                }
+                catch (Exception)
+                {
+                    return null;
+                    //throw new IPFormatException("ipv4 format error");
+                }
+
+                if ((meta.IPVersion & 0x01) != 0x01)
+                {
+                    return null;
+                    //throw new IPFormatException("no support ipv4");
+                }
+            }
+            else
+            {
+                return null;
+                //throw new IPFormatException("ip format error");
+            }
+
+            int node;
+            try
+            {
+                node = findNode(ipv);
+            }
+            catch (NotFoundException)
+            {
+                return null;
+            }
+
+            var data = resolve(node);
+            var dst = new string[meta.Fields.Length];
+            Array.Copy(data.Split('\t'), off, dst, 0, meta.Fields.Length);
+            return dst;
+        }
+
+        private int findNode(byte[] binary)
+        {
+
+            var node = 0;
+
+            var bit = binary.Length * 8;
+
+            if (bit == 32)
+            {
+                node = v4offset;
+            }
+
+            for (var i = 0; i < bit; i++)
+            {
+                if (node > nodeCount)
+                {
+                    break;
+                }
+
+                node = readNode(node, 1 & ((0xFF & binary[i / 8]) >> 7 - (i % 8)));
+            }
+
+            if (node > nodeCount)
+            {
+                return node;
+            }
+
+            throw new NotFoundException("ip not found");
+        }
+
+        private string resolve(int node)
+        {
+            var resoloved = node - nodeCount + nodeCount * 8;
+            if (resoloved >= fileSize)
+            {
+                throw new InvalidDatabaseException("database resolve error");
+            }
+
+            byte b = 0;
+            var size = (int)(bytesToLong(
+                b,
+                b,
+                data[resoloved],
+                data[resoloved + 1]
+            ));
+
+            if (data.Length < (resoloved + 2 + size))
+            {
+                throw new InvalidDatabaseException("database resolve error");
+            }
+
+            return Encoding.UTF8.GetString(data, resoloved + 2, size);
+        }
+
+        private int readNode(int node, int index)
+        {
+            var off = node * 8 + index * 4;
+
+            return (int)(bytesToLong(
+                data[off],
+                data[off + 1],
+                data[off + 2],
+                data[off + 3]
+            ));
+        }
+
+        private static long bytesToLong(byte a, byte b, byte c, byte d)
+        {
+            return int2long((((a & 0xff) << 24) | ((b & 0xff) << 16) | ((c & 0xff) << 8) | (d & 0xff)));
+        }
+
+        private static long int2long(int i)
+        {
+            var l = i & 0x7fffffffL;
+            if (i < 0)
+            {
+                l |= 0x080000000L;
+            }
+
+            return l;
+        }
+
+        public MetaData getMeta()
+        {
+            return meta;
+        }
+
+        public int getBuildUTCTime()
+        {
+            return meta.Build;
+        }
+
+        public string[] getSupportFields()
+        {
+            return meta.Fields;
+        }
+    }
+
+}

+ 150 - 0
TEAMModelOS.SDK/Extension/GeoRegion.cs

@@ -0,0 +1,150 @@
+using Microsoft.IdentityModel.Tokens;
+using System;
+using System.Collections.Generic;
+using System.Drawing.Drawing2D;
+using System.IdentityModel.Tokens.Jwt;
+using System.IO;
+using System.Text;
+using System.Text.Json;
+
+namespace TEAMModelOS.SDK.Extension
+{
+    public static class GeoRegion
+    {
+        //取得國省市區地理資料架構
+        public static regiondata GetRegionData(List<regionrow> region_gl, List<regionrow> region_cn)
+        {
+            regiondata region = new regiondata();
+            //國際
+            if(region_gl.Count > 0)
+            {
+                foreach (regionrow itemcy in region_gl)
+                {
+                    //country
+                    string countryCode = itemcy.code;
+                    if (!region.country.ContainsKey(countryCode))
+                    {
+                        region.country.Add(countryCode, new regionbase() { code = countryCode, name = itemcy.name });
+                    }
+                    //province 無
+                    //city
+                    if (itemcy.children != null)
+                    {
+                        foreach (JsonElement itemcyChild in itemcy.children)
+                        {
+                            regionrow itemct = itemcyChild.ToObject<regionrow>();
+                            string provinceCode = "tw"; //台灣的省代碼用"tw"
+                            string cityCode = itemct.code;
+                            if (!region.city.ContainsKey(countryCode)) region.city.Add(countryCode, new Dictionary<string, Dictionary<string, regionbase>>());
+                            if (!region.city[countryCode].ContainsKey(provinceCode)) region.city[countryCode].Add(provinceCode, new Dictionary<string, regionbase>());
+                            if (!region.city[countryCode][provinceCode].ContainsKey(cityCode)) region.city[countryCode][provinceCode].Add(cityCode, new regionbase() { code = cityCode, name = itemct.name });
+                            //dist
+                            if (itemct.children != null)
+                            {
+                                foreach (JsonElement itemctChild in itemct.children)
+                                {
+                                    regionrow itemds = itemctChild.ToObject<regionrow>();
+                                    string distCode = itemds.code;
+                                    if (!region.dist.ContainsKey(countryCode)) region.dist.Add(countryCode, new Dictionary<string, Dictionary<string, Dictionary<string, regionbase>>>());
+                                    if (!region.dist[countryCode].ContainsKey(provinceCode)) region.dist[countryCode].Add(provinceCode, new Dictionary<string, Dictionary<string, regionbase>>());
+                                    if (!region.dist[countryCode][provinceCode].ContainsKey(cityCode)) region.dist[countryCode][provinceCode].Add(cityCode, new Dictionary<string, regionbase>());
+                                    if (!region.dist[countryCode][provinceCode][cityCode].ContainsKey(distCode)) region.dist[countryCode][provinceCode][cityCode].Add(distCode, new regionbase() { code = distCode, name = itemds.name });
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            //大陸
+            if (region_cn.Count > 0)
+            {
+                //country
+                string countryCode = "CN";
+                string countryName = "中国";
+                if (!region.country.ContainsKey(countryCode))
+                {
+                    region.country.Add(countryCode, new regionbase() { code = countryCode, name = countryName });
+                }
+                //province
+                foreach (regionrow itempv in region_cn)
+                {
+                    string provinceCode = itempv.code.Replace("0000", "");
+                    if (!region.province.ContainsKey(countryCode)) region.province.Add(countryCode, new Dictionary<string, regionbase>());
+                    if (!region.province[countryCode].ContainsKey(provinceCode)) region.province[countryCode].Add(provinceCode, new regionbase() { code = provinceCode, name = itempv.name });
+                    //city
+                    if (itempv.children != null)
+                    {
+                        foreach (JsonElement itempvChild in itempv.children)
+                        {
+                            regionrow itemct = itempvChild.ToObject<regionrow>();
+                            string cityCode = itemct.code;
+                            if (!region.city.ContainsKey(countryCode)) region.city.Add(countryCode, new Dictionary<string, Dictionary<string, regionbase>>());
+                            if (!region.city[countryCode].ContainsKey(provinceCode)) region.city[countryCode].Add(provinceCode, new Dictionary<string, regionbase>());
+                            if (!region.city[countryCode][provinceCode].ContainsKey(cityCode)) region.city[countryCode][provinceCode].Add(cityCode, new regionbase() { code = cityCode, name = itemct.name });
+                            //dist
+                            if (itemct.children != null)
+                            {
+                                foreach (JsonElement itemctChild in itemct.children)
+                                {
+                                    regionrow itemds = itemctChild.ToObject<regionrow>();
+                                    string distCode = itemds.code;
+                                    if (!region.dist.ContainsKey(countryCode)) region.dist.Add(countryCode, new Dictionary<string, Dictionary<string, Dictionary<string, regionbase>>>());
+                                    if (!region.dist[countryCode].ContainsKey(provinceCode)) region.dist[countryCode].Add(provinceCode, new Dictionary<string, Dictionary<string, regionbase>>());
+                                    if (!region.dist[countryCode][provinceCode].ContainsKey(cityCode)) region.dist[countryCode][provinceCode].Add(cityCode, new Dictionary<string, regionbase>());
+                                    if (!region.dist[countryCode][provinceCode][cityCode].ContainsKey(distCode)) region.dist[countryCode][provinceCode][cityCode].Add(distCode, new regionbase() { code = distCode, name = itemds.name });
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            return region;
+        }
+
+        //取得EN版國省市區地理資料架構 ※EN版只取國
+        public static regiondata GetRegionDataEn()
+        {
+            regiondata region = new regiondata();
+            //國際
+            var regionEn = new List<regionrow>();
+            using (StreamReader r = new StreamReader("JsonFile/Region/region_en.json"))
+            {
+                string json = r.ReadToEnd();
+                regionEn = JsonSerializer.Deserialize<List<regionrow>>(json);
+                foreach (regionrow itemcy in regionEn)
+                {
+                    //country
+                    string countryCode = itemcy.code;
+                    if (!region.country.ContainsKey(countryCode))
+                    {
+                        region.country.Add(countryCode, new regionbase() { code = countryCode, name = itemcy.name });
+                    }
+                }
+            }
+            return region;
+        }
+
+        public class regiondata
+        {
+            public Dictionary<string, regionbase> country { get; set; } = new();
+            public Dictionary<string, Dictionary<string, regionbase>> province { get; set; } = new();
+            public Dictionary<string, Dictionary<string, Dictionary<string, regionbase>>> city { get; set; } = new();
+            public Dictionary<string, Dictionary<string, Dictionary<string, Dictionary<string, regionbase>>>> dist { get; set; } = new();
+        }
+        public class regionbase
+        {
+            public string code { get; set; } //代碼
+            public string name { get; set; } //名稱
+        }
+        public class regionrow : regionbase
+        {
+            public List<object> children { get; set; }
+        }
+
+        //地區要去除的特殊字
+        public static List<string> comeRemoveStr = new List<string>() { "省", "市", "区", "自治州", "县", "旗", "盟", "回族", "藏族", "羌族", "哈尼族", "彝族", "壮族", "苗族", "自治", "特别行政", "地區", "區", "縣" };
+        //大陸直轄市的省ID
+        public static List<string> municipalityId = new List<string>() { "11", "12", "31", "50", "81", "82" };
+    }
+}

+ 0 - 1
TEAMModelOS.SDK/Extension/HttpContextExtensions.cs

@@ -248,7 +248,6 @@ namespace TEAMModelOS.SDK.Extension
             httpContext?.Items.TryGetValue("Name", out name);
             httpContext?.Items.TryGetValue("Picture", out picture);
             httpContext?.Items.TryGetValue("School", out school);
-
             return (id?.ToString(), name?.ToString(), picture?.ToString(), school?.ToString());
         }
         /// <summary>

+ 34 - 0
TEAMModelOS.SDK/Models/Cosmos/Common/GeoAnalysis.cs

@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using TEAMModelOS.SDK.Models.Cosmos;
+using static TEAMModelOS.SDK.Models.Cosmos.IotTeachingData;
+
+namespace TEAMModelOS.SDK.Models
+{
+    public class GeoAnalysis : ProdAnalysisCalItem
+    {
+        public string geoId { get; set; } //形式: $"{countryId}-{provinceId}-{cityId}-{distId}" [例]"TW--30-"、"CN-41-410100-"、"MY---"
+        public Geo geo { get; set; }
+    }
+    public class GeoAnalysisCosmos : GeoAnalysis
+    {
+        public GeoAnalysisCosmos()
+        {
+            pk = "GeoAnalysis";
+            code = "GeoAnalysis";
+        }
+        public string pk { get; set; }
+        public string code { get; set; }
+        public string id { get; set; }
+        public string dateUnit { get; set; } //統計日期單位 "year":年統計 "month":月統計 "day":日統計
+        public string date { get; set; } //统计日期 ※依据dateUnit变化 [例]"year":2023 "month":202302 "day":20230210
+        public long dateTime { get; set; } //timestamp UTC milisecond 比較時間用
+        public int year { get; set; } //统计日期:年
+        public int month { get; set; } //统计日期:月
+        public int day { get; set; } //统计日期:日
+        public long createDate { get; set; } //統計時間
+        public int? ttl { get; set; } = -1;
+    }
+}

+ 0 - 0
TEAMModelOS.SDK/Models/Cosmos/Common/IotTeachingData.cs


部分文件因为文件数量过多而无法显示