소스 검색

Merge branch 'develop' into TPE/Daniel

jeff 5 달 전
부모
커밋
5a2be27358
100개의 변경된 파일4967개의 추가작업 그리고 340개의 파일을 삭제
  1. 4 0
      TEAMModelBI/ClientApp/src/api/index.js
  2. 4 2
      TEAMModelBI/ClientApp/src/language/lang/zh-cn.js
  3. 9 3
      TEAMModelBI/ClientApp/src/language/lang/zh-tw.js
  4. 4 4
      TEAMModelBI/ClientApp/src/router/index.js
  5. 4 4
      TEAMModelBI/ClientApp/src/view/common/aside.vue
  6. 1 1
      TEAMModelBI/ClientApp/src/view/htcommunity/adminpanel.vue
  7. 527 0
      TEAMModelBI/ClientApp/src/view/product/authorization.vue
  8. 1 1
      TEAMModelBI/Controllers/BINormal/AbilityTaskMgmtController.cs
  9. 1 1
      TEAMModelBI/Controllers/BINormal/BatchAreaController.cs
  10. 62 74
      TEAMModelBI/Controllers/BIProductAnalysis/ProductAnalysisController.cs
  11. 1 1
      TEAMModelBI/Controllers/BISchool/SchoolController.cs
  12. 1 1
      TEAMModelBI/Controllers/BITest/TestController.cs
  13. 1 1
      TEAMModelBI/Controllers/Census/LessonSticsController.cs
  14. 1 1
      TEAMModelBI/Controllers/RepairApi/InitialAreaController.cs
  15. 3 3
      TEAMModelBI/TEAMModelBI.csproj
  16. 1 1
      TEAMModelOS.Extension/HTEX.Complex/Services/SignalRScreenServerHub.cs
  17. 1 1
      TEAMModelOS.Extension/HTEX.DataETL/Controllers/LessonRecordController.cs
  18. 1 1
      TEAMModelOS.Extension/HTEX.Lib/ETL/Lesson/LessonETLService.cs
  19. 1 1
      TEAMModelOS.Extension/HTEX.Lib/HTEX.Lib.csproj
  20. 1 1
      TEAMModelOS.Extension/HTEX.Lib/summary.xml
  21. 2 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/package.json
  22. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/public/favicon.ico
  23. 1 1
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/public/index.html
  24. 21 3
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/App.vue
  25. 3 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/api/index.js
  26. 539 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/assets/iconfont/demo.css
  27. 510 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/assets/iconfont/demo_index.html
  28. 71 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/assets/iconfont/iconfont.css
  29. 1 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/assets/iconfont/iconfont.js
  30. 107 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/assets/iconfont/iconfont.json
  31. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/assets/iconfont/iconfont.ttf
  32. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/assets/iconfont/iconfont.woff
  33. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/assets/iconfont/iconfont.woff2
  34. 54 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/assets/reset.css
  35. 15 7
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/main.js
  36. 16 10
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/router/index.js
  37. 34 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/view/admin/ActivityManage.less
  38. 34 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/view/admin/ActivityManage.vue
  39. 159 3
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/view/login/Admin.vue
  40. 4 1
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/view/login/Student.vue
  41. 26 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/view/student/ActivityAnswer.less
  42. 50 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/view/student/ActivityAnswer.vue
  43. 150 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/view/student/ActivityInfo.less
  44. 223 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/view/student/ActivityInfo.vue
  45. 15 4
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/BaseController.cs
  46. 24 11
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/IndexController.cs
  47. 369 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/ManageController.cs
  48. 6 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/StudentController.cs
  49. 0 39
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/WeatherForecastController.cs
  50. 244 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/CustomFileLoggerProvider.cs
  51. 18 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/DataCenterConnectionService.cs
  52. 120 48
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/SignalRHost/SignalRExamServerHub.cs
  53. 29 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Filters/AspNetCoreBuilderServiceCollectionExtensions.cs
  54. 114 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Filters/AuthTokenAttribute.cs
  55. 2 2
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/CollectionHelper.cs
  56. 16 2
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/Constant.cs
  57. 68 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/ExpressionHelper.cs
  58. 37 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/FileHelper.cs
  59. 320 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/HttpContextExtensions.cs
  60. 67 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/JwtAuthExtension.cs
  61. 51 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/ProcessHelper.cs
  62. 12 1
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/IES.ExamServer.csproj
  63. 109 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Models/SignalRClient.cs
  64. 4 11
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Models/Teacher.cs
  65. 139 8
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Program.cs
  66. 163 31
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Services/IndexService.cs
  67. 0 15
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/WeatherForecast.cs
  68. 10 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/app.manifest
  69. 3 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/appsettings.json
  70. 12 0
      TEAMModelOS.Extension/IES.ExamLib/IES.ExamLib.csproj
  71. 274 0
      TEAMModelOS.Extension/IES.ExamLib/Models/EvaluationCommon.cs
  72. 14 0
      TEAMModelOS.Extension/IES.ExamLib/Models/ExamConstant.cs
  73. 16 5
      TEAMModelOS.Function/CosmosDBTriggers/TriggerArt.cs
  74. 4 4
      TEAMModelOS.Function/CosmosDBTriggers/TriggerCorrect.cs
  75. 1 1
      TEAMModelOS.Function/CosmosDBTriggers/TriggerExam.cs
  76. 1 1
      TEAMModelOS.Function/CosmosDBTriggers/TriggerExamImport.cs
  77. 1 1
      TEAMModelOS.Function/CosmosDBTriggers/TriggerExamLite.cs
  78. 1 1
      TEAMModelOS.Function/CosmosDBTriggers/TriggerHomework.cs
  79. 1 1
      TEAMModelOS.Function/CosmosDBTriggers/TriggerQuotaImport.cs
  80. 1 1
      TEAMModelOS.Function/CosmosDBTriggers/TriggerStudy.cs
  81. 2 2
      TEAMModelOS.Function/CosmosDBTriggers/TriggerSurvey.cs
  82. 2 2
      TEAMModelOS.Function/CosmosDBTriggers/TriggerVote.cs
  83. 30 4
      TEAMModelOS.Function/IESServiceBusTrigger.cs
  84. 3 3
      TEAMModelOS.Function/TEAMModelOS.Function.csproj
  85. 1 1
      TEAMModelOS.SDK/Models/Cosmos/Common/ArtEvaluation.cs
  86. 1 1
      TEAMModelOS.SDK/Models/Cosmos/Common/ArtExam.cs
  87. 1 1
      TEAMModelOS.SDK/Models/Cosmos/Common/ArtMusic.cs
  88. 1 1
      TEAMModelOS.SDK/Models/Cosmos/Common/ArtRecord.cs
  89. 1 1
      TEAMModelOS.SDK/Models/Cosmos/Common/ExamLite.cs
  90. 1 1
      TEAMModelOS.SDK/Models/Cosmos/Common/Inner/AbilityTaskTree.cs
  91. 1 1
      TEAMModelOS.SDK/Models/Cosmos/Common/Inner/CourseChange.cs
  92. 1 1
      TEAMModelOS.SDK/Models/Cosmos/Common/Inner/SurveyRecord.cs
  93. 1 1
      TEAMModelOS.SDK/Models/Cosmos/Common/Inner/SyllabusTree.cs
  94. 1 1
      TEAMModelOS.SDK/Models/Cosmos/Common/IotTeachingData.cs
  95. 1 1
      TEAMModelOS.SDK/Models/Cosmos/Common/LearnRecord.cs
  96. 1 1
      TEAMModelOS.SDK/Models/Cosmos/Common/LessonCount.cs
  97. 1 1
      TEAMModelOS.SDK/Models/Cosmos/Common/Scoring.cs
  98. 1 1
      TEAMModelOS.SDK/Models/Cosmos/Common/Snode.cs
  99. 1 1
      TEAMModelOS.SDK/Models/Cosmos/Common/StuCourse.cs
  100. 0 0
      TEAMModelOS.SDK/Models/Cosmos/Common/StudentScoreRecord.cs

+ 4 - 0
TEAMModelBI/ClientApp/src/api/index.js

@@ -624,4 +624,8 @@ export default {
     getCsSchoolByGeo(data) {
         return post('/schoolcheck/get-school-geo', data)
     },
+    //取得學校或TMID產品授權狀況
+    getSchoolProdauth(data) {
+        return post('/schoolcheck/get-school-prodauth', data)
+    },
 }

+ 4 - 2
TEAMModelBI/ClientApp/src/language/lang/zh-cn.js

@@ -70,6 +70,7 @@ const zh_cn = {
         NewMessage: '消息推送',
         productAuth: '产品授权',
         productAnalysis: '产品分析',
+        productAuthorization: '产品授权',
         userRelated: '用户相关',
         issueCoupon: '发优惠券',
         jointPurchase: '统购设定',
@@ -903,7 +904,7 @@ const zh_cn = {
         space: '空间',
         inputSpace: '请输入容量',
         unit: '单位',
-        expirationDate: '使用期限',
+        expirationDate: '起讫日期筛选',
         startDate: '开始时间',
         endDate: '结束时间',
         quota: '名额',
@@ -963,7 +964,8 @@ const zh_cn = {
         IPALB6EY: 'IES5个人空间',
         Z6ELB6EZ: 'HiTeach5 ID授权',
         VA67B6EZ: 'HiTeach5 县市授权',
-        VAA7B6EY: 'IES5个人空间县市授权',
+        VAA7B6EY: 'IES5个人空间县市授权',        
+        export: '汇出',
     },
     aprule: {
         hdcam: 'USB录影支援',

+ 9 - 3
TEAMModelBI/ClientApp/src/language/lang/zh-tw.js

@@ -69,7 +69,7 @@ const zh_tw = {
         sendMessage: '消息推送',
         NewMessage: '新消息推送',
         productAnalysis: '產品分析',
-        productAuth: '產品授權',
+        productAuthorization: '產品授權',
         userRelated: '用戶相關',
         issueCoupon: '發優惠券',
         jointPurchase: '統購設定',
@@ -896,7 +896,7 @@ const zh_tw = {
         space: '空間',
         inputSpace: '請輸入容量',
         unit: '單位',
-        expirationDate: '使用期限',
+        expirationDate: '起訖日期篩選',
         startDate: '開始時間',
         endDate: '結束時間',
         quota: '名額',
@@ -936,6 +936,11 @@ const zh_tw = {
         message6: '請輸入數字',
         message7: '學校已移除',
     },
+    productAuthorize:
+    {
+        filterType: '篩選條件',
+
+    },
     auth:{
         YMPCVCIM: '學情分析模組',
         IPDYZYLC: '智慧學校管理服務',
@@ -956,7 +961,8 @@ const zh_tw = {
         IPALB6EY: 'IES5個人空間',
         Z6ELB6EZ: 'HiTeach5 ID授權',
         VA67B6EZ: 'HiTeach5 縣市授權',
-        VAA7B6EY: 'IES5個人空間縣市授權',
+        VAA7B6EY: 'IES5個人空間縣市授權',        
+        export: '匯出',
     },
     aprule: {
         hdcam: 'USB錄影支援',

+ 4 - 4
TEAMModelBI/ClientApp/src/router/index.js

@@ -89,14 +89,14 @@ const routes = [
                 isShow: true,
                 component: () => require.ensure([], (require) => require(`@/view/product/index.vue`))
             },
-            //品授權
+            //品授權
             {
-                name: "auth",
-                path: "auth",
+                name: "productAuthorization",
+                path: "productAuthorization",
                 permission: "teacher-read|teacher-upd",
                 roles: ['admin'],
                 isShow: true,
-                component: () => require.ensure([], (require) => require(`@/view/product/index.vue`))
+                component: () => require.ensure([], (require) => require(`@/view/product/authorization.vue`))
             },
             // 統購平台
             {

+ 4 - 4
TEAMModelBI/ClientApp/src/view/common/aside.vue

@@ -145,12 +145,12 @@ export default {
             sort: 7,
           },
           {
-            name: proxy.$t(`menu.productAuth`),
-            router: '/home/auth',
-            icon: '#icon-fenxi1',
+            name: proxy.$t(`menu.productAuthorization`),
+            router: '/home/productAuthorization',
+            icon: '#icon-fuzhi',
             permission: [],
             isShow: true,
-            sort: 7,
+            sort: 9,
           },
           {
             name: proxy.$t(`menu.userRelated`),

+ 1 - 1
TEAMModelBI/ClientApp/src/view/htcommunity/adminpanel.vue

@@ -826,7 +826,7 @@ EEEE    120
           let sheetName = proxy.$t(`purchase.sheetSummary`);
           XLSX.utils.book_append_sheet(wb, ws, sheetName);
           //分頁2 已領取清單
-          let titleTch = [proxy.$t(`purchase.schoolName`), proxy.$t(`purchase.schoolCode`), proxy.$t(`purchase.receiverName`), proxy.$t(`purchase.receiverId`)];
+          let titleTch = [proxy.$t(`purchase.schoolName`), proxy.$t(`purchase.schoolCode`), proxy.$t(`purchase.receiverId`), proxy.$t(`purchase.receiverName`)];
           let keyTch = ['schoolName', 'shortCode', 'tmid', 'name'];
           let dataTch = [];
           Object.entries(res.school).forEach(([purchaseIdNow, purchaseSchoolData]) => {

+ 527 - 0
TEAMModelBI/ClientApp/src/view/product/authorization.vue

@@ -0,0 +1,527 @@
+<template>
+  <el-container> 
+    <el-main class="header-select">
+      <el-collapse v-model="activeName" accordion >
+        <el-collapse-item :title="$t(`productAuthorize.filterType`)" name="1">
+      <!-- 篩選區域 -->
+      <div class="filtratebox">
+        <el-form label-position="top" style="line-height: 40px">
+          <!-- 篩選產品 -->
+          <el-form-item label="篩選產品" >
+            <el-button type="primary" block @click="selectAll(true)" plain size="small">全選</el-button>
+            <el-button type="primary" block @click="selectAll(false)" plain size="small">全不選</el-button>
+            <el-checkbox-group v-model="selectedProducts" style="text-align: left;">
+              <el-checkbox  
+                v-for="product in productData"
+                :key="product.prodcode"
+                :label="product.prodcode"  >
+                {{ product.name }}
+              </el-checkbox>
+            </el-checkbox-group>
+          </el-form-item>
+
+          <!-- 篩選日期 -->
+           
+          <el-form-item label="篩選日期">            
+            <el-input-number v-model="dateFilter.number" :min="1" /> &nbsp;&nbsp;
+            <el-select v-model="dateFilter.unit" placeholder="請選擇單位">
+              <el-option
+                v-for="unit in dateUnits"
+                :key="unit.value"
+                :label="unit.label"
+                :value="unit.value"
+              />
+            </el-select>&nbsp;&nbsp;
+            <el-select v-model="dateFilter.direction" placeholder="請選擇方向">
+              <el-option
+                v-for="direction in dateDirections"
+                :key="direction.value"
+                :label="direction.label"
+                :value="direction.value"
+              />
+            </el-select>
+          </el-form-item>
+
+          <!-- 起訖日期篩選 -->                           
+          <el-form-item :label="$t('purchase.expirationDate')" :label-width="formLabelWidth" prop="time" style="width: 500px">
+              <el-date-picker v-model="dateRange" type="daterange" range-separator="To" :start-placeholder="$t('purchase.startDate')" :end-placeholder="$t('purchase.endDate')" :size="size" format="YYYY/MM/DD" value-format="YYYY-MM-DD" />
+          </el-form-item>
+          <div style="float: left;width: 100%;text-align: left; color: red;">
+            [篩選日期] 與 [起訖日期篩選] 以 [篩選日期] 為優先條件
+          </div>          
+
+          <!-- 篩選對象 -->
+          <el-form-item label="篩選對象">            
+            <el-select v-model="selectedaccountType" placeholder="請選擇對象">
+              <el-option
+                v-for="type in accountTypes"
+                :key="type.value"
+                :label="type.label"
+                :value="type.value"
+              />
+            </el-select>
+          </el-form-item>
+          
+          <!-- 篩選按鈕 -->          
+          <el-button type="primary" block @click="filterData">篩選</el-button>
+          <el-button type="primary" block @click="clearFilter">清除篩選</el-button>
+        </el-form>
+      </div>
+        </el-collapse-item>           
+      </el-collapse>
+
+      <!-- 表格 -->
+      <div style="width:100%;margin-bottom:0px;line-height: 70px !important;">
+        <div style="width:10%;float: left;">
+          資料筆數:{{ filteredData.length }}
+        </div>
+        <!-- margin-right: 1%;
+  line-height: 50px !important;
+  float: right; -->
+        <div style="float: right;">
+          <el-button type="primary" @click="exportExcel">{{$t(`auth.export`)}}</el-button>
+        </div>
+      </div>
+
+      <div class="table-container" v-loading="loading" :element-loading-text="$t(`product.prepareData`)+'...'">
+        <el-table  :data="filteredData" border style="width:100%"  :default-sort="{ prop: 'school', order: 'ascending' }" class="custom-table" >
+          <el-table-column prop="name" label="名稱" sortable align="center" />
+          <el-table-column prop="product" label="產品名稱" sortable align="center" />
+          <el-table-column prop="startDate" label="開始日期" sortable align="center" />
+          <el-table-column prop="endDate" label="結束日期" sortable align="center" />
+        </el-table>
+      </div>
+    </el-main>
+  </el-container>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted, getCurrentInstance } from "vue";
+  let { proxy } = getCurrentInstance()
+  let loading = ref(false)
+  const activeName = ref('1')
+  // 模擬的產品清單
+  const products = ["產品 A", "產品 B", "產品 C", "產品 D"];
+  let productData = ref([
+    {
+      name: 'ezStation 2',
+      prodcode: '3222NIYD',
+    },
+    {
+      name: 'HiTeach STD',
+      prodcode: 'J223IZ6M',
+    },
+    {
+      name: 'HiTeach TBL',
+      prodcode: '3222C6D2',
+    },
+    {
+      name: 'HiTeach PRO',
+      prodcode: 'J223IZAM',
+    },
+    {
+      name: 'HiTeach Lite',
+      prodcode: 'J2236ZCX',
+    },
+    {
+      name: 'HiTeach Mobile',
+      prodcode: '3222DNG2',
+    },
+    {
+      name: 'HiTeach Premium',
+      prodcode: '3222IAVN',
+    },
+    {
+      name: 'HiTeach5',
+      prodcode: 'BYJ6LZ6Z',
+    },
+    {
+      name: 'HiTeachCC',
+      prodcode: 'LZLL6ZEI',
+    },
+    {
+      name: proxy.$t(`auth.YMPCVCIM`),
+      prodcode: 'YMPCVCIM',
+    },
+    {
+      name: proxy.$t(`auth.IPDYZYLC`),
+      prodcode: 'IPDYZYLC',
+    },
+    {
+      name: proxy.$t(`auth._3CLYJ6NP`),
+      prodcode: '3CLYJ6NP',
+    },
+    {
+      name: proxy.$t(`auth.IPALJ6NY`),
+      prodcode: 'IPALJ6NY',
+    },
+    {
+      name: proxy.$t(`auth.VABAJ6NV`),
+      prodcode: 'VABAJ6NV',
+    },
+    {
+      name: proxy.$t(`auth._0VPBDZPG`),
+      prodcode: '0VPBDZPG',
+    },
+    {
+      name: proxy.$t(`auth.B9GPJ6NY`),
+      prodcode: 'B9GPJ6NY',
+    },
+    {
+      name: proxy.$t(`auth.LY9AJ6NY`),
+      prodcode: 'LY9AJ6NY',
+    },
+    {
+      name: proxy.$t(`auth.YL9CJ6NY`),
+      prodcode: 'YL9CJ6NY',
+    },
+    {
+      name: proxy.$t(`auth.LL9MJ6NY`),
+      prodcode: 'LL9MJ6NY',
+    },
+    {
+      name: proxy.$t(`auth.B6V5J6NP`),
+      prodcode: 'B6V5J6NP',
+    },
+    {
+      name: proxy.$t(`auth.LSZYJ6NA`),
+      prodcode: 'LSZYJ6NA',
+    },
+    {
+      name: proxy.$t(`auth.CVGPJ6NN`),
+      prodcode: 'CVGPJ6NN',
+    },
+    {
+      name: proxy.$t(`auth.VDPGJ6NC`),
+      prodcode: 'VDPGJ6NC',
+    },
+    {
+      name: proxy.$t(`auth.YPXSJ6NJ`),
+      prodcode: 'YPXSJ6NJ',
+    },
+    {
+      name: proxy.$t(`auth.VLY6J6N6`),
+      prodcode: 'VLY6J6N6',
+    },
+  ])
+
+
+  // 模擬的表格資料
+  const tableData = reactive([
+    {
+      accountType: "school",
+      name: "學校甲",
+      product: "產品 A",
+      startDate: "2024-01-01",
+      endDate: "2024-12-31",
+    },
+    {
+      accountType: "school",
+      name: "學校乙",
+      product: "產品 B",
+      startDate: "2024-03-01",
+      endDate: "2024-09-30",
+    },
+    {
+      accountType: "school",
+      name: "學校丙",
+      product: "產品 C",
+      startDate: "2024-02-01",
+      endDate: "2024-08-31",
+    },
+    {
+      accountType: "school",
+      name: "學校丁",
+      product: "產品 A",
+      startDate: "2024-05-01",
+      endDate: "2025-04-30",
+    },
+    {
+      accountType: "tmid",
+      name: "ID01",
+      product: "產品 B",
+      startDate: "2024-05-01",
+      endDate: "2025-04-30",
+    },
+    {
+      accountType: "tmid",
+      name: "ID01",
+      product: "產品 A",
+      startDate: "2024-05-01",
+      endDate: "2025-04-30",
+    },
+    {
+      accountType: "tmid",
+      name: "ID02",
+      product: "產品 B",
+      startDate: "2024-05-01",
+      endDate: "2025-04-30",
+    },
+    {
+      accountType: "tmid",
+      name: "ID03",
+      product: "產品 C",
+      startDate: "2024-05-01",
+      endDate: "2025-04-30",
+    },
+  ]);
+
+  // 篩選條件
+  const selectedProducts = ref([]);
+  const selectedaccountType = ref('school');
+  const dateFilter = reactive({
+    number: 0,
+    unit: "",
+    direction: "",
+  });
+  const startDate = ref(null); // 起始日期
+  const endDate = ref(null); // 結束日期
+  let dateRange = ref([]);
+  let filteredData = reactive([]);
+
+  // 日期單位與方向選項
+  const dateUnits = [
+    { label: "請選擇單位", value: "" },
+    { label: "年", value: "year" },
+    { label: "月", value: "month" },
+    { label: "週", value: "week" },
+    { label: "日", value: "day" },
+  ];
+
+  const accountTypes = [
+    { label: "學校", value: "school" },
+    { label: "醍摩豆ID", value: "tmid" },
+  ];
+  const dateDirections = [
+    { label: "請選擇方向", value: "" },
+    { label: "之前", value: "before" },
+    { label: "之後", value: "after" },
+  ];
+
+  //getfilteredData()
+
+  onMounted(() => {
+
+    getfilteredData()
+
+  })
+
+  function getfilteredData() {
+    loading.value = true
+
+    filteredData.splice(0, filteredData.length);
+    let filtered = JSON.parse(JSON.stringify(tableData));
+
+    // 按產品篩選
+    if (selectedProducts.value && selectedProducts.value.length) {
+      filtered = filtered.filter((item) =>
+        selectedProducts.value.includes(item.product)
+      );
+    }
+
+    // 按篩選日期
+    if (dateFilter.number && dateFilter.unit && dateFilter.direction) {
+      const today = new Date();
+      const compareDate = new Date(today);
+      const unitMap = {
+        year: "FullYear",
+        month: "Month",
+        week: "Date",
+        day: "Date",
+      };
+
+      if (dateFilter.unit === "week") {
+        compareDate.setDate(
+          today.getDate() +
+          (dateFilter.direction === "before" ? -7 : 7) * dateFilter.number
+        );
+      } else {
+        compareDate[`set${unitMap[dateFilter.unit]}`](
+          today[`get${unitMap[dateFilter.unit]}`]() +
+          (dateFilter.direction === "before" ? -1 : 1) * dateFilter.number
+        );
+      }
+
+      filtered = filtered.filter((item) => {
+        const endDate = new Date(item.endDate);
+        return dateFilter.direction === "before"
+          ? endDate <= compareDate
+          : endDate >= compareDate;
+      });
+    }
+
+    // 起始/結束日期篩選
+    if (startDate.value) {
+      filtered = filtered.filter(
+        (item) => new Date(item.startDate) >= new Date(startDate.value)
+      );
+    }
+    if (endDate.value) {
+      filtered = filtered.filter(
+        (item) => new Date(item.endDate) <= new Date(endDate.value)
+      );
+    }
+    // 按對象篩選
+    if (selectedaccountType.value) {
+      filtered = filtered.filter((item) =>
+        selectedaccountType.value.includes(item.accountType)
+      );
+    }
+
+
+    // filtered.forEach(element => {
+    //   filteredData.push(element)
+    // });
+
+    let authEndYMWD = dateFilter.number ? dateFilter.number : 0
+    if (dateFilter.direction === "before") {
+      authEndYMWD = authEndYMWD * -1
+    }
+    let authStart = 0
+    let authEnd = 0
+    if (dateRange.value && dateRange.value.length > 0) {
+      authStart = new Date(dateRange.value[0]).getTime() / 1000
+      authEnd = new Date(dateRange.value[1]).getTime() / 1000
+    }
+
+    let data = {
+      idType: selectedaccountType.value, //ID類型 school:學校 tmid:TMID ※預設:school
+      prodCode: selectedProducts.value, //產品代碼
+      authEndYMWD: dateFilter.unit, //日期單位:year:年 month:月 week:週 day:日
+      authEndPeriod: authEndYMWD, //日期計算數 [例] -3:從現在起往前算三個日期單位 3:從現在起往後算三個日期單位
+      authStart: authStart, //授權起始日 ※「授權起始日、授權終止日」與「日期單位、日期計算數」擇一成立
+      authEnd: authEnd, //授權終止日
+    }
+    
+    try {
+      //取得學校或TMID產品授權狀況
+      proxy.$api.getSchoolProdauth(data).then((res) => {
+        loading.value = false
+        if (typeof res.err === "string" && res.err.length === 0 && (res.serial.length > 0 || res.service.length > 0)) {
+          let serial = JSON.parse(JSON.stringify(res.serial));
+          let service = JSON.parse(JSON.stringify(res.service));
+          service.forEach(element => {
+            //  if(!element.startDate || !element.endDate){
+            //   let aaa = element
+
+            // }
+            // 轉換產品名稱
+            let product = productData.value.find(item => {
+              return item.prodcode === element.prodCode
+            })
+            let filteredItem = {
+              name: element.saleClient.name,
+              product: product.name,
+              startDate: proxy.$common.timestampToTime(element.startDate, '', true),
+              endDate: proxy.$common.timestampToTime(element.endDate, '', true),
+            }
+            filteredData.push(filteredItem)
+          });
+          serial.forEach(element => {
+            // if(!element.startDate || !element.endDate){
+            //   let aaa = element
+
+            // }
+            //轉換產品名稱
+            let product = productData.value.find(item => {
+              return item.prodcode === element.prodCode
+            })
+            let filteredItem = {
+              name: element.saleClient.name,
+              product: product.name,
+              startDate: proxy.$common.timestampToTime(element.authSysStartDate, '', true),
+              endDate: proxy.$common.timestampToTime(element.authSysEndDate, '', true),
+            }
+            filteredData.push(filteredItem)
+          });
+        }
+      })
+    } catch (error) {
+      console.log(error)
+    } finally {
+      
+    }
+   
+  }
+
+
+
+  function filterData() {
+    getfilteredData()
+  };
+
+  function clearFilter() {
+    selectedProducts.value = [];
+    selectedaccountType.value = 'school';
+
+    dateFilter.number = 0
+    dateFilter.unit = ""
+    dateFilter.direction = ""
+
+    startDate.value = 0; // 起始日期
+    endDate.value = 0; // 結束日期
+    dateRange.value = [];
+  };
+  function selectAll(flag) {
+    if (flag) {
+      productData.value.forEach(item => {
+        selectedProducts.value.push(item.prodcode)
+      })
+    } else {
+      selectedProducts.value = [];
+    }
+  };
+  //导出日志
+    function exportExcel () {
+      require.ensure([], () => {
+        const { export_json_to_excel } = require('../../until/excel/Export2Excel')
+        const tHeader = ['名稱', '產品名稱', '開始日期', '結束日期'] // excel文档第一行显示的标题
+        const filterVal = ['name', 'product', 'startDate', 'endDate'] // id,version等都是上面标题所对应的数据
+        const list = filteredData
+        const data = formatJson(filterVal, list)
+        export_json_to_excel(tHeader, data, '產品授權') //标题,数据,文件名
+      })
+    }
+    function formatJson (filterVal, jsonData) {
+      return jsonData.map((v) => filterVal.map((j) => v[j]))
+    }
+
+
+</script>
+
+<style>
+.filtratebox {
+  width: 100%;
+  padding: 10px 20px 0px 20px;
+  background: #fff;
+  border-top: 1px solid #ccc;
+  position: relative;
+}
+
+.table-container {
+  margin: 0 auto;
+  width: 98%;
+  margin-bottom: 150px;
+  margin-top: 10px;
+  line-height: 10px
+}
+
+.custom-table >>> .el-table__header,
+.custom-table >>> .el-table__body,
+.custom-table >>> .el-table__row {
+  border-color: black !important;
+}
+.el-form-item {
+  margin-bottom: 5px;
+}
+.header-select,
+.middlebox {
+  /* width: 100%; */
+  /* background-color: #fff; */
+  margin: 10px 20px;
+}
+.header-select .el-collapse-item__header {
+  font-size: 16px;
+  padding-left: 0.5%;
+}
+</style>

+ 1 - 1
TEAMModelBI/Controllers/BINormal/AbilityTaskMgmtController.cs

@@ -10,7 +10,7 @@ using TEAMModelOS.Models;
 using Microsoft.Extensions.Options;
 using System.Text.Json;
 using TEAMModelOS.SDK.Models;
-using TEAMModelOS.SDK.Models.Cosmos.Common;
+using TEAMModelOS.SDK.Models.Cosmos;
 using System.Text;
 using TEAMModelBI.Filter;
 using TEAMModelOS.SDK.Services;

+ 1 - 1
TEAMModelBI/Controllers/BINormal/BatchAreaController.cs

@@ -11,7 +11,7 @@ using TEAMModelOS.SDK.DI;
 using TEAMModelOS.SDK.Models;
 using Microsoft.Azure.Cosmos;
 using System.Text.Json;
-using TEAMModelOS.SDK.Models.Cosmos.Common;
+using TEAMModelOS.SDK.Models.Cosmos;
 using TEAMModelOS.SDK.Models.Cosmos.BI;
 using Azure.Messaging.ServiceBus;
 using TEAMModelOS.SDK.Extension;

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 62 - 74
TEAMModelBI/Controllers/BIProductAnalysis/ProductAnalysisController.cs


+ 1 - 1
TEAMModelBI/Controllers/BISchool/SchoolController.cs

@@ -32,7 +32,7 @@ using TEAMModelOS.SDK.DI;
 using TEAMModelOS.SDK.Extension;
 using TEAMModelOS.SDK.Models;
 using TEAMModelOS.SDK.Models.Cosmos.BI;
-using TEAMModelOS.SDK.Models.Cosmos.Common;
+using TEAMModelOS.SDK.Models.Cosmos;
 using TEAMModelOS.SDK.Models.Service.BI;
 using JsonSerializer = System.Text.Json.JsonSerializer;
 using static Microsoft.Azure.Amqp.Serialization.SerializableType;

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

@@ -24,7 +24,7 @@ using TEAMModelOS.SDK.DI;
 using TEAMModelOS.SDK.Extension;
 using TEAMModelOS.SDK.Models;
 using TEAMModelOS.SDK.Models.Cosmos.BI;
-using TEAMModelOS.SDK.Models.Cosmos.Common;
+using TEAMModelOS.SDK.Models.Cosmos;
 using TEAMModelOS.SDK.Models.Service;
 using TEAMModelOS.SDK.Models.Table;
 

+ 1 - 1
TEAMModelBI/Controllers/Census/LessonSticsController.cs

@@ -9,7 +9,7 @@ using TEAMModelOS.SDK.DI;
 using Microsoft.Extensions.Options;
 using Microsoft.Azure.Cosmos;
 using System.Text.Json;
-using TEAMModelOS.SDK.Models.Cosmos.Common;
+using TEAMModelOS.SDK.Models.Cosmos;
 using TEAMModelOS.SDK.Models;
 using TEAMModelBI.Models;
 using TEAMModelOS.SDK.Extension;

+ 1 - 1
TEAMModelBI/Controllers/RepairApi/InitialAreaController.cs

@@ -17,7 +17,7 @@ using TEAMModelOS.SDK.Models;
 using Microsoft.Azure.Cosmos;
 using System;
 using System.Collections.Generic;
-using TEAMModelOS.SDK.Models.Cosmos.Common;
+using TEAMModelOS.SDK.Models.Cosmos;
 using System.Linq;
 using Pipelines.Sockets.Unofficial.Arenas;
 using TEAMModelBI.Models;

+ 3 - 3
TEAMModelBI/TEAMModelBI.csproj

@@ -65,9 +65,9 @@
 		<SpaRoot>ClientApp\</SpaRoot>
 		<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
 		<UserSecretsId>078b5d89-7d90-4f6a-88fc-7d96025990a8</UserSecretsId>
-		<Version>5.2412.25</Version>
-		<AssemblyVersion>5.2412.25.1</AssemblyVersion>
-		<FileVersion>5.2412.25.1</FileVersion>
+		<Version>5.2501.8</Version>
+		<AssemblyVersion>5.2501.8.1</AssemblyVersion>
+		<FileVersion>5.2501.8.1</FileVersion>
 		<Description>TEAMModelBI(BI)</Description>
 		<PackageReleaseNotes>BI版本说明版本切换标记2022000908</PackageReleaseNotes>
 		<PackageId>TEAMModelBI</PackageId>

+ 1 - 1
TEAMModelOS.Extension/HTEX.Complex/Services/SignalRScreenServerHub.cs

@@ -65,7 +65,7 @@ namespace HTEX.Complex.Services
                     };
                     await _azureRedis.GetRedisClient(8).HashSetAsync($"SignalRClient:connects:{serverDevice.deviceId}", connid, client.ToJsonString());
                     ScreenClient device = HttpUtility.UrlDecode(_device, Encoding.Unicode).ToObject<ScreenClient>();
-                    switch (true) 
+                    switch (true)  
                     {
                         case bool when grant_type.Equals(ScreenConstant.grant_type):
                             ScreenClient screenClient ;

+ 1 - 1
TEAMModelOS.Extension/HTEX.DataETL/Controllers/LessonRecordController.cs

@@ -21,7 +21,7 @@ using TEAMModelOS.SDK.Extension;
 using TEAMModelOS.SDK.Helper.Common.FileHelper;
 using TEAMModelOS.SDK.Models;
 using TEAMModelOS.SDK.Models.Cosmos.BI;
-using TEAMModelOS.SDK.Models.Cosmos.Common;
+using TEAMModelOS.SDK.Models.Cosmos;
 using TEAMModelOS.SDK.Models.Cosmos.OpenEntity;
 using static TEAMModelOS.SDK.Models.Service.SystemService;
 

+ 1 - 1
TEAMModelOS.Extension/HTEX.Lib/ETL/Lesson/LessonETLService.cs

@@ -27,7 +27,7 @@ using TEAMModelOS.SDK;
 using TEAMModelOS.SDK.DI;
 using TEAMModelOS.SDK.Extension;
 using TEAMModelOS.SDK.Models;
-using TEAMModelOS.SDK.Models.Cosmos.Common;
+using TEAMModelOS.SDK.Models.Cosmos;
 using TEAMModelOS.SDK.Models.Cosmos.OpenEntity;
 using TEAMModelOS.SDK.Models.Dtos;
 using static TEAMModelOS.SDK.Models.ThirdService;

+ 1 - 1
TEAMModelOS.Extension/HTEX.Lib/HTEX.Lib.csproj

@@ -15,7 +15,7 @@
 	  <PackageReference Include="EPPlus" Version="7.4.1" />
 	  <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
 	  <PackageReference Include="System.Drawing.Common" Version="8.0.6" />
-	  <PackageReference Include="System.Text.Json" Version="8.0.3" />
+	  <PackageReference Include="System.Text.Json" Version="8.0.5" />
   </ItemGroup>
 
   <ItemGroup>

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
TEAMModelOS.Extension/HTEX.Lib/summary.xml


+ 2 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/package.json

@@ -24,6 +24,8 @@
     "@vue/cli-service": "~5.0.0",
     "eslint": "^7.32.0",
     "eslint-plugin-vue": "^8.0.3",
+    "less": "^4.2.1",
+    "less-loader": "^12.2.0",
     "vue-template-compiler": "^2.6.14"
   },
   "eslintConfig": {

BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/public/favicon.ico


+ 1 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/public/index.html

@@ -5,7 +5,7 @@
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta name="viewport" content="width=device-width,initial-scale=1.0">
     <link rel="icon" href="<%= BASE_URL %>favicon.ico">
-    <title><%= htmlWebpackPlugin.options.title %></title>
+    <title>局域网评测</title>
   </head>
   <body>
     <noscript>

+ 21 - 3
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/App.vue

@@ -17,11 +17,29 @@ export default {
 
 <style>
 #app {
-    font-family: Avenir, Helvetica, Arial, sans-serif;
+    font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",'Microsoft JhengHei',Arial,sans-serif;
     -webkit-font-smoothing: antialiased;
     -moz-osx-font-smoothing: grayscale;
-    text-align: center;
+    /* text-align: center;
     color: #2c3e50;
-    margin-top: 60px;
+    margin-top: 60px; */
+}
+html,
+body,
+#app {
+    height: 100%;
+    margin: 0;
+}
+.el-button:focus {
+    outline: none;
+}
+.el-empty {
+    margin: auto;
+}
+.el-container {
+    height: 100%;
+}
+[class*=" el-icon-"], [class^=el-icon-] {
+    vertical-align: -.125em !important;
 }
 </style>

+ 3 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/api/index.js

@@ -0,0 +1,3 @@
+import { fetch, post } from '@/api/http'
+
+export default {}

+ 539 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/assets/iconfont/demo.css

@@ -0,0 +1,539 @@
+/* Logo 字体 */
+@font-face {
+  font-family: "iconfont logo";
+  src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
+  src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
+    url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
+    url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
+    url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
+}
+
+.logo {
+  font-family: "iconfont logo";
+  font-size: 160px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+/* tabs */
+.nav-tabs {
+  position: relative;
+}
+
+.nav-tabs .nav-more {
+  position: absolute;
+  right: 0;
+  bottom: 0;
+  height: 42px;
+  line-height: 42px;
+  color: #666;
+}
+
+#tabs {
+  border-bottom: 1px solid #eee;
+}
+
+#tabs li {
+  cursor: pointer;
+  width: 100px;
+  height: 40px;
+  line-height: 40px;
+  text-align: center;
+  font-size: 16px;
+  border-bottom: 2px solid transparent;
+  position: relative;
+  z-index: 1;
+  margin-bottom: -1px;
+  color: #666;
+}
+
+
+#tabs .active {
+  border-bottom-color: #f00;
+  color: #222;
+}
+
+.tab-container .content {
+  display: none;
+}
+
+/* 页面布局 */
+.main {
+  padding: 30px 100px;
+  width: 960px;
+  margin: 0 auto;
+}
+
+.main .logo {
+  color: #333;
+  text-align: left;
+  margin-bottom: 30px;
+  line-height: 1;
+  height: 110px;
+  margin-top: -50px;
+  overflow: hidden;
+  *zoom: 1;
+}
+
+.main .logo a {
+  font-size: 160px;
+  color: #333;
+}
+
+.helps {
+  margin-top: 40px;
+}
+
+.helps pre {
+  padding: 20px;
+  margin: 10px 0;
+  border: solid 1px #e7e1cd;
+  background-color: #fffdef;
+  overflow: auto;
+}
+
+.icon_lists {
+  width: 100% !important;
+  overflow: hidden;
+  *zoom: 1;
+}
+
+.icon_lists li {
+  width: 100px;
+  margin-bottom: 10px;
+  margin-right: 20px;
+  text-align: center;
+  list-style: none !important;
+  cursor: default;
+}
+
+.icon_lists li .code-name {
+  line-height: 1.2;
+}
+
+.icon_lists .icon {
+  display: block;
+  height: 100px;
+  line-height: 100px;
+  font-size: 42px;
+  margin: 10px auto;
+  color: #333;
+  -webkit-transition: font-size 0.25s linear, width 0.25s linear;
+  -moz-transition: font-size 0.25s linear, width 0.25s linear;
+  transition: font-size 0.25s linear, width 0.25s linear;
+}
+
+.icon_lists .icon:hover {
+  font-size: 100px;
+}
+
+.icon_lists .svg-icon {
+  /* 通过设置 font-size 来改变图标大小 */
+  width: 1em;
+  /* 图标和文字相邻时,垂直对齐 */
+  vertical-align: -0.15em;
+  /* 通过设置 color 来改变 SVG 的颜色/fill */
+  fill: currentColor;
+  /* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
+      normalize.css 中也包含这行 */
+  overflow: hidden;
+}
+
+.icon_lists li .name,
+.icon_lists li .code-name {
+  color: #666;
+}
+
+/* markdown 样式 */
+.markdown {
+  color: #666;
+  font-size: 14px;
+  line-height: 1.8;
+}
+
+.highlight {
+  line-height: 1.5;
+}
+
+.markdown img {
+  vertical-align: middle;
+  max-width: 100%;
+}
+
+.markdown h1 {
+  color: #404040;
+  font-weight: 500;
+  line-height: 40px;
+  margin-bottom: 24px;
+}
+
+.markdown h2,
+.markdown h3,
+.markdown h4,
+.markdown h5,
+.markdown h6 {
+  color: #404040;
+  margin: 1.6em 0 0.6em 0;
+  font-weight: 500;
+  clear: both;
+}
+
+.markdown h1 {
+  font-size: 28px;
+}
+
+.markdown h2 {
+  font-size: 22px;
+}
+
+.markdown h3 {
+  font-size: 16px;
+}
+
+.markdown h4 {
+  font-size: 14px;
+}
+
+.markdown h5 {
+  font-size: 12px;
+}
+
+.markdown h6 {
+  font-size: 12px;
+}
+
+.markdown hr {
+  height: 1px;
+  border: 0;
+  background: #e9e9e9;
+  margin: 16px 0;
+  clear: both;
+}
+
+.markdown p {
+  margin: 1em 0;
+}
+
+.markdown>p,
+.markdown>blockquote,
+.markdown>.highlight,
+.markdown>ol,
+.markdown>ul {
+  width: 80%;
+}
+
+.markdown ul>li {
+  list-style: circle;
+}
+
+.markdown>ul li,
+.markdown blockquote ul>li {
+  margin-left: 20px;
+  padding-left: 4px;
+}
+
+.markdown>ul li p,
+.markdown>ol li p {
+  margin: 0.6em 0;
+}
+
+.markdown ol>li {
+  list-style: decimal;
+}
+
+.markdown>ol li,
+.markdown blockquote ol>li {
+  margin-left: 20px;
+  padding-left: 4px;
+}
+
+.markdown code {
+  margin: 0 3px;
+  padding: 0 5px;
+  background: #eee;
+  border-radius: 3px;
+}
+
+.markdown strong,
+.markdown b {
+  font-weight: 600;
+}
+
+.markdown>table {
+  border-collapse: collapse;
+  border-spacing: 0px;
+  empty-cells: show;
+  border: 1px solid #e9e9e9;
+  width: 95%;
+  margin-bottom: 24px;
+}
+
+.markdown>table th {
+  white-space: nowrap;
+  color: #333;
+  font-weight: 600;
+}
+
+.markdown>table th,
+.markdown>table td {
+  border: 1px solid #e9e9e9;
+  padding: 8px 16px;
+  text-align: left;
+}
+
+.markdown>table th {
+  background: #F7F7F7;
+}
+
+.markdown blockquote {
+  font-size: 90%;
+  color: #999;
+  border-left: 4px solid #e9e9e9;
+  padding-left: 0.8em;
+  margin: 1em 0;
+}
+
+.markdown blockquote p {
+  margin: 0;
+}
+
+.markdown .anchor {
+  opacity: 0;
+  transition: opacity 0.3s ease;
+  margin-left: 8px;
+}
+
+.markdown .waiting {
+  color: #ccc;
+}
+
+.markdown h1:hover .anchor,
+.markdown h2:hover .anchor,
+.markdown h3:hover .anchor,
+.markdown h4:hover .anchor,
+.markdown h5:hover .anchor,
+.markdown h6:hover .anchor {
+  opacity: 1;
+  display: inline-block;
+}
+
+.markdown>br,
+.markdown>p>br {
+  clear: both;
+}
+
+
+.hljs {
+  display: block;
+  background: white;
+  padding: 0.5em;
+  color: #333333;
+  overflow-x: auto;
+}
+
+.hljs-comment,
+.hljs-meta {
+  color: #969896;
+}
+
+.hljs-string,
+.hljs-variable,
+.hljs-template-variable,
+.hljs-strong,
+.hljs-emphasis,
+.hljs-quote {
+  color: #df5000;
+}
+
+.hljs-keyword,
+.hljs-selector-tag,
+.hljs-type {
+  color: #a71d5d;
+}
+
+.hljs-literal,
+.hljs-symbol,
+.hljs-bullet,
+.hljs-attribute {
+  color: #0086b3;
+}
+
+.hljs-section,
+.hljs-name {
+  color: #63a35c;
+}
+
+.hljs-tag {
+  color: #333333;
+}
+
+.hljs-title,
+.hljs-attr,
+.hljs-selector-id,
+.hljs-selector-class,
+.hljs-selector-attr,
+.hljs-selector-pseudo {
+  color: #795da3;
+}
+
+.hljs-addition {
+  color: #55a532;
+  background-color: #eaffea;
+}
+
+.hljs-deletion {
+  color: #bd2c00;
+  background-color: #ffecec;
+}
+
+.hljs-link {
+  text-decoration: underline;
+}
+
+/* 代码高亮 */
+/* PrismJS 1.15.0
+https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
+/**
+ * prism.js default theme for JavaScript, CSS and HTML
+ * Based on dabblet (http://dabblet.com)
+ * @author Lea Verou
+ */
+code[class*="language-"],
+pre[class*="language-"] {
+  color: black;
+  background: none;
+  text-shadow: 0 1px white;
+  font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
+  text-align: left;
+  white-space: pre;
+  word-spacing: normal;
+  word-break: normal;
+  word-wrap: normal;
+  line-height: 1.5;
+
+  -moz-tab-size: 4;
+  -o-tab-size: 4;
+  tab-size: 4;
+
+  -webkit-hyphens: none;
+  -moz-hyphens: none;
+  -ms-hyphens: none;
+  hyphens: none;
+}
+
+pre[class*="language-"]::-moz-selection,
+pre[class*="language-"] ::-moz-selection,
+code[class*="language-"]::-moz-selection,
+code[class*="language-"] ::-moz-selection {
+  text-shadow: none;
+  background: #b3d4fc;
+}
+
+pre[class*="language-"]::selection,
+pre[class*="language-"] ::selection,
+code[class*="language-"]::selection,
+code[class*="language-"] ::selection {
+  text-shadow: none;
+  background: #b3d4fc;
+}
+
+@media print {
+
+  code[class*="language-"],
+  pre[class*="language-"] {
+    text-shadow: none;
+  }
+}
+
+/* Code blocks */
+pre[class*="language-"] {
+  padding: 1em;
+  margin: .5em 0;
+  overflow: auto;
+}
+
+:not(pre)>code[class*="language-"],
+pre[class*="language-"] {
+  background: #f5f2f0;
+}
+
+/* Inline code */
+:not(pre)>code[class*="language-"] {
+  padding: .1em;
+  border-radius: .3em;
+  white-space: normal;
+}
+
+.token.comment,
+.token.prolog,
+.token.doctype,
+.token.cdata {
+  color: slategray;
+}
+
+.token.punctuation {
+  color: #999;
+}
+
+.namespace {
+  opacity: .7;
+}
+
+.token.property,
+.token.tag,
+.token.boolean,
+.token.number,
+.token.constant,
+.token.symbol,
+.token.deleted {
+  color: #905;
+}
+
+.token.selector,
+.token.attr-name,
+.token.string,
+.token.char,
+.token.builtin,
+.token.inserted {
+  color: #690;
+}
+
+.token.operator,
+.token.entity,
+.token.url,
+.language-css .token.string,
+.style .token.string {
+  color: #9a6e3a;
+  background: hsla(0, 0%, 100%, .5);
+}
+
+.token.atrule,
+.token.attr-value,
+.token.keyword {
+  color: #07a;
+}
+
+.token.function,
+.token.class-name {
+  color: #DD4A68;
+}
+
+.token.regex,
+.token.important,
+.token.variable {
+  color: #e90;
+}
+
+.token.important,
+.token.bold {
+  font-weight: bold;
+}
+
+.token.italic {
+  font-style: italic;
+}
+
+.token.entity {
+  cursor: help;
+}

+ 510 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/assets/iconfont/demo_index.html

@@ -0,0 +1,510 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8"/>
+  <title>iconfont Demo</title>
+  <link rel="shortcut icon" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg" type="image/x-icon"/>
+  <link rel="icon" type="image/svg+xml" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg"/>
+  <link rel="stylesheet" href="https://g.alicdn.com/thx/cube/1.3.2/cube.min.css">
+  <link rel="stylesheet" href="demo.css">
+  <link rel="stylesheet" href="iconfont.css">
+  <script src="iconfont.js"></script>
+  <!-- jQuery -->
+  <script src="https://a1.alicdn.com/oss/uploads/2018/12/26/7bfddb60-08e8-11e9-9b04-53e73bb6408b.js"></script>
+  <!-- 代码高亮 -->
+  <script src="https://a1.alicdn.com/oss/uploads/2018/12/26/a3f714d0-08e6-11e9-8a15-ebf944d7534c.js"></script>
+  <style>
+    .main .logo {
+      margin-top: 0;
+      height: auto;
+    }
+
+    .main .logo a {
+      display: flex;
+      align-items: center;
+    }
+
+    .main .logo .sub-title {
+      margin-left: 0.5em;
+      font-size: 22px;
+      color: #fff;
+      background: linear-gradient(-45deg, #3967FF, #B500FE);
+      -webkit-background-clip: text;
+      -webkit-text-fill-color: transparent;
+    }
+  </style>
+</head>
+<body>
+  <div class="main">
+    <h1 class="logo"><a href="https://www.iconfont.cn/" title="iconfont 首页" target="_blank">
+      <img width="200" src="https://img.alicdn.com/imgextra/i3/O1CN01Mn65HV1FfSEzR6DKv_!!6000000000514-55-tps-228-59.svg">
+      
+    </a></h1>
+    <div class="nav-tabs">
+      <ul id="tabs" class="dib-box">
+        <li class="dib active"><span>Unicode</span></li>
+        <li class="dib"><span>Font class</span></li>
+        <li class="dib"><span>Symbol</span></li>
+      </ul>
+      
+      <a href="https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=4795944" target="_blank" class="nav-more">查看项目</a>
+      
+    </div>
+    <div class="tab-container">
+      <div class="content unicode" style="display: block;">
+          <ul class="icon_lists dib-box">
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe71f;</span>
+                <div class="name">物品-书笔</div>
+                <div class="code-name">&amp;#xe71f;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe6b6;</span>
+                <div class="name">意见反馈、记笔记</div>
+                <div class="code-name">&amp;#xe6b6;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe600;</span>
+                <div class="name">错误</div>
+                <div class="code-name">&amp;#xe600;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe619;</span>
+                <div class="name">勾</div>
+                <div class="code-name">&amp;#xe619;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe61b;</span>
+                <div class="name">勾选</div>
+                <div class="code-name">&amp;#xe61b;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe650;</span>
+                <div class="name">勾</div>
+                <div class="code-name">&amp;#xe650;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe714;</span>
+                <div class="name">勾1</div>
+                <div class="code-name">&amp;#xe714;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe62e;</span>
+                <div class="name">叉叉</div>
+                <div class="code-name">&amp;#xe62e;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe620;</span>
+                <div class="name">退出</div>
+                <div class="code-name">&amp;#xe620;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe7ed;</span>
+                <div class="name">退出</div>
+                <div class="code-name">&amp;#xe7ed;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe61c;</span>
+                <div class="name">勾勾</div>
+                <div class="code-name">&amp;#xe61c;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe7d3;</span>
+                <div class="name">点击</div>
+                <div class="code-name">&amp;#xe7d3;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe6bb;</span>
+                <div class="name">已完成</div>
+                <div class="code-name">&amp;#xe6bb;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe666;</span>
+                <div class="name">未作答</div>
+                <div class="code-name">&amp;#xe666;</div>
+              </li>
+          
+          </ul>
+          <div class="article markdown">
+          <h2 id="unicode-">Unicode 引用</h2>
+          <hr>
+
+          <p>Unicode 是字体在网页端最原始的应用方式,特点是:</p>
+          <ul>
+            <li>支持按字体的方式去动态调整图标大小,颜色等等。</li>
+            <li>默认情况下不支持多色,直接添加多色图标会自动去色。</li>
+          </ul>
+          <blockquote>
+            <p>注意:新版 iconfont 支持两种方式引用多色图标:SVG symbol 引用方式和彩色字体图标模式。(使用彩色字体图标需要在「编辑项目」中开启「彩色」选项后并重新生成。)</p>
+          </blockquote>
+          <p>Unicode 使用步骤如下:</p>
+          <h3 id="-font-face">第一步:拷贝项目下面生成的 <code>@font-face</code></h3>
+<pre><code class="language-css"
+>@font-face {
+  font-family: 'element-icons';
+  src: url('iconfont.woff2?t=1735287119647') format('woff2'),
+       url('iconfont.woff?t=1735287119647') format('woff'),
+       url('iconfont.ttf?t=1735287119647') format('truetype');
+}
+</code></pre>
+          <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
+<pre><code class="language-css"
+>.element-icons {
+  font-family: "element-icons" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+</code></pre>
+          <h3 id="-">第三步:挑选相应图标并获取字体编码,应用于页面</h3>
+<pre>
+<code class="language-html"
+>&lt;span class="element-icons"&gt;&amp;#x33;&lt;/span&gt;
+</code></pre>
+          <blockquote>
+            <p>"element-icons" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。</p>
+          </blockquote>
+          </div>
+      </div>
+      <div class="content font-class">
+        <ul class="icon_lists dib-box">
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-my-note"></span>
+            <div class="name">
+              物品-书笔
+            </div>
+            <div class="code-name">.el-icon-my-note
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-myNote"></span>
+            <div class="name">
+              意见反馈、记笔记
+            </div>
+            <div class="code-name">.el-icon-myNote
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-cuowu"></span>
+            <div class="name">
+              错误
+            </div>
+            <div class="code-name">.el-icon-cuowu
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-gou"></span>
+            <div class="name">
+              勾
+            </div>
+            <div class="code-name">.el-icon-gou
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-gouxuan"></span>
+            <div class="name">
+              勾选
+            </div>
+            <div class="code-name">.el-icon-gouxuan
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-gou1"></span>
+            <div class="name">
+              勾
+            </div>
+            <div class="code-name">.el-icon-gou1
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-gou11"></span>
+            <div class="name">
+              勾1
+            </div>
+            <div class="code-name">.el-icon-gou11
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-chacha"></span>
+            <div class="name">
+              叉叉
+            </div>
+            <div class="code-name">.el-icon-chacha
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-tuichu"></span>
+            <div class="name">
+              退出
+            </div>
+            <div class="code-name">.el-icon-tuichu
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-tuichu2"></span>
+            <div class="name">
+              退出
+            </div>
+            <div class="code-name">.el-icon-tuichu2
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-gougou1"></span>
+            <div class="name">
+              勾勾
+            </div>
+            <div class="code-name">.el-icon-gougou1
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-dianji"></span>
+            <div class="name">
+              点击
+            </div>
+            <div class="code-name">.el-icon-dianji
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-yiwancheng"></span>
+            <div class="name">
+              已完成
+            </div>
+            <div class="code-name">.el-icon-yiwancheng
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-weizuoda"></span>
+            <div class="name">
+              未作答
+            </div>
+            <div class="code-name">.el-icon-weizuoda
+            </div>
+          </li>
+          
+        </ul>
+        <div class="article markdown">
+        <h2 id="font-class-">font-class 引用</h2>
+        <hr>
+
+        <p>font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。</p>
+        <p>与 Unicode 使用方式相比,具有如下特点:</p>
+        <ul>
+          <li>相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。</li>
+          <li>因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。</li>
+        </ul>
+        <p>使用步骤如下:</p>
+        <h3 id="-fontclass-">第一步:引入项目下面生成的 fontclass 代码:</h3>
+<pre><code class="language-html">&lt;link rel="stylesheet" href="./iconfont.css"&gt;
+</code></pre>
+        <h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
+<pre><code class="language-html">&lt;span class="element-icons el-icon-xxx"&gt;&lt;/span&gt;
+</code></pre>
+        <blockquote>
+          <p>"
+            element-icons" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。</p>
+        </blockquote>
+      </div>
+      </div>
+      <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-my-note"></use>
+                </svg>
+                <div class="name">物品-书笔</div>
+                <div class="code-name">#el-icon-my-note</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-myNote"></use>
+                </svg>
+                <div class="name">意见反馈、记笔记</div>
+                <div class="code-name">#el-icon-myNote</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-cuowu"></use>
+                </svg>
+                <div class="name">错误</div>
+                <div class="code-name">#el-icon-cuowu</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-gou"></use>
+                </svg>
+                <div class="name">勾</div>
+                <div class="code-name">#el-icon-gou</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-gouxuan"></use>
+                </svg>
+                <div class="name">勾选</div>
+                <div class="code-name">#el-icon-gouxuan</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-gou1"></use>
+                </svg>
+                <div class="name">勾</div>
+                <div class="code-name">#el-icon-gou1</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-gou11"></use>
+                </svg>
+                <div class="name">勾1</div>
+                <div class="code-name">#el-icon-gou11</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-chacha"></use>
+                </svg>
+                <div class="name">叉叉</div>
+                <div class="code-name">#el-icon-chacha</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-tuichu"></use>
+                </svg>
+                <div class="name">退出</div>
+                <div class="code-name">#el-icon-tuichu</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-tuichu2"></use>
+                </svg>
+                <div class="name">退出</div>
+                <div class="code-name">#el-icon-tuichu2</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-gougou1"></use>
+                </svg>
+                <div class="name">勾勾</div>
+                <div class="code-name">#el-icon-gougou1</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-dianji"></use>
+                </svg>
+                <div class="name">点击</div>
+                <div class="code-name">#el-icon-dianji</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-yiwancheng"></use>
+                </svg>
+                <div class="name">已完成</div>
+                <div class="code-name">#el-icon-yiwancheng</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-weizuoda"></use>
+                </svg>
+                <div class="name">未作答</div>
+                <div class="code-name">#el-icon-weizuoda</div>
+            </li>
+          
+          </ul>
+          <div class="article markdown">
+          <h2 id="symbol-">Symbol 引用</h2>
+          <hr>
+
+          <p>这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇<a href="">文章</a>
+            这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:</p>
+          <ul>
+            <li>支持多色图标了,不再受单色限制。</li>
+            <li>通过一些技巧,支持像字体那样,通过 <code>font-size</code>, <code>color</code> 来调整样式。</li>
+            <li>兼容性较差,支持 IE9+,及现代浏览器。</li>
+            <li>浏览器渲染 SVG 的性能一般,还不如 png。</li>
+          </ul>
+          <p>使用步骤如下:</p>
+          <h3 id="-symbol-">第一步:引入项目下面生成的 symbol 代码:</h3>
+<pre><code class="language-html">&lt;script src="./iconfont.js"&gt;&lt;/script&gt;
+</code></pre>
+          <h3 id="-css-">第二步:加入通用 CSS 代码(引入一次就行):</h3>
+<pre><code class="language-html">&lt;style&gt;
+.icon {
+  width: 1em;
+  height: 1em;
+  vertical-align: -0.15em;
+  fill: currentColor;
+  overflow: hidden;
+}
+&lt;/style&gt;
+</code></pre>
+          <h3 id="-">第三步:挑选相应图标并获取类名,应用于页面:</h3>
+<pre><code class="language-html">&lt;svg class="icon" aria-hidden="true"&gt;
+  &lt;use xlink:href="#icon-xxx"&gt;&lt;/use&gt;
+&lt;/svg&gt;
+</code></pre>
+          </div>
+      </div>
+
+    </div>
+  </div>
+  <script>
+  $(document).ready(function () {
+      $('.tab-container .content:first').show()
+
+      $('#tabs li').click(function (e) {
+        var tabContent = $('.tab-container .content')
+        var index = $(this).index()
+
+        if ($(this).hasClass('active')) {
+          return
+        } else {
+          $('#tabs li').removeClass('active')
+          $(this).addClass('active')
+
+          tabContent.hide().eq(index).fadeIn()
+        }
+      })
+    })
+  </script>
+</body>
+</html>

+ 71 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/assets/iconfont/iconfont.css

@@ -0,0 +1,71 @@
+@font-face {
+  font-family: "element-icons"; /* Project id 4795944 */
+  src: url('iconfont.woff2?t=1735287119647') format('woff2'),
+       url('iconfont.woff?t=1735287119647') format('woff'),
+       url('iconfont.ttf?t=1735287119647') format('truetype');
+}
+
+.element-icons {
+  font-family: "element-icons" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.el-icon-my-note:before {
+  content: "\e71f";
+}
+
+.el-icon-myNote:before {
+  content: "\e6b6";
+}
+
+.el-icon-cuowu:before {
+  content: "\e600";
+}
+
+.el-icon-gou:before {
+  content: "\e619";
+}
+
+.el-icon-gouxuan:before {
+  content: "\e61b";
+}
+
+.el-icon-gou1:before {
+  content: "\e650";
+}
+
+.el-icon-gou11:before {
+  content: "\e714";
+}
+
+.el-icon-chacha:before {
+  content: "\e62e";
+}
+
+.el-icon-tuichu:before {
+  content: "\e620";
+}
+
+.el-icon-tuichu2:before {
+  content: "\e7ed";
+}
+
+.el-icon-gougou1:before {
+  content: "\e61c";
+}
+
+.el-icon-dianji:before {
+  content: "\e7d3";
+}
+
+.el-icon-yiwancheng:before {
+  content: "\e6bb";
+}
+
+.el-icon-weizuoda:before {
+  content: "\e666";
+}
+

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/assets/iconfont/iconfont.js


+ 107 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/assets/iconfont/iconfont.json

@@ -0,0 +1,107 @@
+{
+  "id": "4795944",
+  "name": "ExamServer",
+  "font_family": "element-icons",
+  "css_prefix_text": "el-icon-",
+  "description": "",
+  "glyphs": [
+    {
+      "icon_id": "6536038",
+      "name": "物品-书笔",
+      "font_class": "my-note",
+      "unicode": "e71f",
+      "unicode_decimal": 59167
+    },
+    {
+      "icon_id": "24591518",
+      "name": "意见反馈、记笔记",
+      "font_class": "myNote",
+      "unicode": "e6b6",
+      "unicode_decimal": 59062
+    },
+    {
+      "icon_id": "1108",
+      "name": "错误",
+      "font_class": "cuowu",
+      "unicode": "e600",
+      "unicode_decimal": 58880
+    },
+    {
+      "icon_id": "386336",
+      "name": "勾",
+      "font_class": "gou",
+      "unicode": "e619",
+      "unicode_decimal": 58905
+    },
+    {
+      "icon_id": "579391",
+      "name": "勾选",
+      "font_class": "gouxuan",
+      "unicode": "e61b",
+      "unicode_decimal": 58907
+    },
+    {
+      "icon_id": "782338",
+      "name": "勾",
+      "font_class": "gou1",
+      "unicode": "e650",
+      "unicode_decimal": 58960
+    },
+    {
+      "icon_id": "844603",
+      "name": "勾1",
+      "font_class": "gou11",
+      "unicode": "e714",
+      "unicode_decimal": 59156
+    },
+    {
+      "icon_id": "8988248",
+      "name": "叉叉",
+      "font_class": "chacha",
+      "unicode": "e62e",
+      "unicode_decimal": 58926
+    },
+    {
+      "icon_id": "6626202",
+      "name": "退出",
+      "font_class": "tuichu",
+      "unicode": "e620",
+      "unicode_decimal": 58912
+    },
+    {
+      "icon_id": "16921565",
+      "name": "退出",
+      "font_class": "tuichu2",
+      "unicode": "e7ed",
+      "unicode_decimal": 59373
+    },
+    {
+      "icon_id": "17622335",
+      "name": "勾勾",
+      "font_class": "gougou1",
+      "unicode": "e61c",
+      "unicode_decimal": 58908
+    },
+    {
+      "icon_id": "15900229",
+      "name": "点击",
+      "font_class": "dianji",
+      "unicode": "e7d3",
+      "unicode_decimal": 59347
+    },
+    {
+      "icon_id": "17814604",
+      "name": "已完成",
+      "font_class": "yiwancheng",
+      "unicode": "e6bb",
+      "unicode_decimal": 59067
+    },
+    {
+      "icon_id": "6910350",
+      "name": "未作答",
+      "font_class": "weizuoda",
+      "unicode": "e666",
+      "unicode_decimal": 58982
+    }
+  ]
+}

BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/assets/iconfont/iconfont.ttf


BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/assets/iconfont/iconfont.woff


BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/assets/iconfont/iconfont.woff2


+ 54 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/assets/reset.css

@@ -0,0 +1,54 @@
+/* http://meyerweb.com/eric/tools/css/reset/ 
+   v2.0 | 20110126
+   License: none (public domain)
+*/
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed, 
+figure, figcaption, footer, header, hgroup, 
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+	margin: 0;
+	padding: 0;
+	border: 0;
+	/*font-size: 100%;*/
+	/*font: inherit;*/
+	vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure, 
+footer, header, hgroup, menu, nav, section {
+	display: block;
+}
+body {
+	line-height: 1.5;
+	color: #515a6e;
+	font-size: 14px;
+}
+ol, ul {
+	list-style: none;
+}
+blockquote, q {
+	quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+	content: '';
+	content: none;
+}
+table {
+	border-collapse: collapse;
+	border-spacing: 0;
+}
+a {
+	text-decoration: none;
+  	outline: none;
+}

+ 15 - 7
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/main.js

@@ -1,21 +1,29 @@
 import Vue from 'vue'
 import App from './App.vue'
-import router from './router/index'
-import apiTools from '@/api/http'
+import router from './router/router'
+import apiTools from '@/api'
+import { fetch, post } from '@/api/http'
 
 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"
 
 Vue.use(ElementUI)
 Vue.config.productionTip = false
 
 Vue.prototype.$api = apiTools
 Vue.prototype.$axios = axios
+Vue.prototype.$post = post
+Vue.prototype.$get = fetch
 
+const app = new Vue({
+    el: '#app',
+    router,
+    ...App
+})
 
-new Vue({
-  el: '#app',
-  router,
-  render: h => h(App),
-}).$mount('#app')
+export {
+    app
+}

+ 16 - 10
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/router/index.js

@@ -6,16 +6,7 @@ Vue.use(VueRouter)
 const routes = [
     {
         path: '/',
-        name: 'home',
-        component: () => import('@/view/login/Admin.vue')
-    },
-    {
-        path: '/about',
-        name: 'about',
-        // route level code-splitting
-        // this generates a separate chunk (about.[hash].js) for this route
-        // which is lazy-loaded when the route is visited.
-        component: () => import('@/view/login/Student.vue')
+        redirect: '/login/admin',
     },
     {
         path: '/login',
@@ -32,6 +23,21 @@ const routes = [
                 component: () => import('@/view/login/Student.vue')
             },
         ]
+    },
+    {
+        path: '/admin',
+        name: 'admin',
+        component: () => import('@/view/admin/ActivityManage.vue')
+    },
+    {
+        path: '/info',
+        name: 'info',
+        component: () => import('@/view/student/ActivityInfo.vue')
+    },
+    {
+        path: '/studentAns',
+        name: 'studentAns',
+        component: () => import('@/view/student/ActivityAnswer.vue')
     }
 ]
 

+ 34 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/view/admin/ActivityManage.less

@@ -0,0 +1,34 @@
+.el-container {
+    height: 100%;
+
+    .el-header,
+    .el-footer {
+        background-color: #b3c0d1;
+        color: #333;
+        /* text-align: center; */
+        line-height: 60px;
+    }
+
+    .el-aside {
+        background-color: #d3dce6;
+        color: #333;
+        text-align: center;
+        line-height: 200px;
+    }
+
+    .el-main {
+        background-color: #e9eef3;
+        color: #333;
+        text-align: center;
+        line-height: 160px;
+    }
+}
+
+.el-container:nth-child(5) .el-aside,
+.el-container:nth-child(6) .el-aside {
+    line-height: 260px;
+}
+
+.el-container:nth-child(7) .el-aside {
+    line-height: 320px;
+}

+ 34 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/view/admin/ActivityManage.vue

@@ -0,0 +1,34 @@
+<template>
+    <el-container>
+        <el-aside width="200px">Aside</el-aside>
+        <el-container>
+            <el-header>
+                <div>
+                    <el-dropdown>
+                        <el-avatar icon="el-icon-user-solid"></el-avatar>
+                        <el-dropdown-menu slot="dropdown">
+                            <el-dropdown-item icon="el-icon-switch-button">退出</el-dropdown-item>
+                        </el-dropdown-menu>
+                    </el-dropdown>
+                </div>
+            </el-header>
+            <el-main>
+                活动列表
+                活动信息(激活按钮、信息框、学生作答框)
+                    信息框中展示基本信息、试卷下载情况等
+                    学生作答框展示哪些电脑在作答,是否作答完成
+            </el-main>
+        </el-container>
+    </el-container>
+</template>
+
+<script>
+export default {}
+</script>
+
+<style lang="less" scoped>
+@import "./ActivityManage.less";
+</style>
+
+<style lang="less" scoped>
+</style>

+ 159 - 3
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/view/login/Admin.vue

@@ -1,10 +1,166 @@
 <template>
-    <div>教师端</div>
+    <div class="login">
+        <div class="login-body">
+            <div class="login-box">
+                <div class="body-left">
+                    <img src="@/assets/fengjing.jpg" alt="">
+                </div>
+                <div class="body-right">
+                    <div class="right-input" v-show="!isQRCode">
+                        <h1>手机号登录</h1>
+                        <el-input v-model="loginForm.id" placeholder="手机号码" size="medium" />
+                        <div>
+                            <el-input v-model="loginForm.password" type="password" placeholder="密码" size="medium" style="width: 60%;" />
+                            <el-button type="primary" size="medium" @click="login()" style="width: calc(40% - 20px); margin-left: 20px;">获取验证码</el-button>
+                        </div>
+                        <el-button type="primary" size="medium" @click="login()">登录</el-button>
+                    </div>
+                    <div class="right-input" v-show="isQRCode">
+                        <h1>扫码登录</h1>
+                        <el-button type="primary" size="medium" @click="login()">登录</el-button>
+                    </div>
+                    <div>试卷验证码登录</div>
+                </div>
+                <div class="qr-code" @click="isQRCode = !isQRCode">
+                    <img src="@/assets/qrCode.png" alt="">
+                </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 {
+            loginForm: {
+                id: '',
+                password: '',
+            },
+            isQRCode: false,
+        }
+    },
+}
 </script>
 
-<style>
+<style lang="less" scoped>
+.login {
+    width: 100%;
+    height: 100%;
+    background-repeat: no-repeat;
+    background-attachment: fixed;
+    background-position: center;
+    background-size: cover;
+    background: #F4F7FF;
+    .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: 60%;
+            width: 60%;
+            position: relative;
+
+            .body-left {
+                // width: 800px;
+                width: 60%;
+                img {
+                    width: 100%;
+                    height: 100%;
+                }
+            }
+            .body-right {
+                width: calc(40% - 100px);
+                height: auto;
+                position: relative;
+                margin: 6% 50px;
+
+                /* .right-input {
+                    margin: 30px 100px;
+                } */
+
+                h1{
+                    margin-bottom: 50px;
+                    color: #5b5b5b;
+                }
+                .el-input {
+                    margin-bottom: 20px;
+                }
+
+                .el-button {
+                    // padding: 0 50px;
+                    // border-radius: 50px;
+                    width: 100%;
+                    margin: 10px 0;
+                }
+
+                .register-forget {
+                    font-size: 13px;
+                    color: #919191;
+                    text-align: right;
+                    margin-bottom: 10px;
+
+                    &>span {
+                        cursor: pointer;
+                        &:hover {
+                            color: #409EFF;
+                        }
+                    }
+                }
+            }
+        }
+    }
+                .qr-code {
+                    position: absolute;
+                    top: 20px;
+                    right: 20px;
+                    cursor: pointer;
+
+                    img {
+                        width: 75px;
+                    }
+                }
+}
+.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: #919191;
+        margin-right: 40px;
+        font-size: 12px;
+    }
+}
+</style>
+
+<style lang="less">
+.login {
+    .el-divider__text {
+        color: #919191;
+    }
+    .el-input__wrapper {
+        background-color: #ededed;
+        border: none;
+    }
+} 
 </style>

+ 4 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/view/login/Student.vue

@@ -1,5 +1,8 @@
 <template>
-    <div>学生端</div>
+    <div>学生端
+        会获取到学校信息、正在进行的活动,此处会展示活动名称、学校信息、账号密码
+        若没有活动,则不展示账号密码,提示没有活动
+    </div>
 </template>
 
 <script>

+ 26 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/view/student/ActivityAnswer.less

@@ -0,0 +1,26 @@
+.el-header {
+    width: 100%;
+    height: 40px !important;
+    line-height: 40px;
+    background-color: #ffffff;
+    color: #24b880;
+    font-size: 20px;
+    // padding: 5px 15px;
+    position: relative;
+    border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+    box-sizing: content-box;
+}
+
+.el-main {
+    background-color: #ffffff;
+    color: #333;
+    display: flex;
+    .question-content {
+        width: 80%;
+        background-color: #B3C0D1;
+    }
+    .answer-sheet {
+        width: 20%;
+        background-color: antiquewhite;
+    }
+}

+ 50 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/view/student/ActivityAnswer.vue

@@ -0,0 +1,50 @@
+<template>
+    <el-container>
+        <el-header>
+            <i class="el-icon-tuichu2"></i>
+            <span>{{ activityInfo.name }}</span>
+            <span class="count-down" v-if="needCountDown && showExam.length">
+                <span>作答时间&nbsp;</span>
+                <span :style="{'color': diffSeconds < 60 ? 'red' : ''}">{{ surplus }}</span>
+            </span>
+        </el-header>
+        <el-main>
+            <div class="question-content">作答区</div>
+            <div class="answer-sheet">答题卡</div>
+        </el-main>
+    </el-container>
+</template>
+
+<script>
+export default {
+    data() {
+        return {
+            loading: false,
+            activityInfo: {
+                name: '艺术评测'
+            },
+            needCountDown: true,
+            showExam: [{}],
+            surplus: "", //倒计时的字符
+            diffSeconds: 0, //秒数
+        }
+    },
+    methods: {
+        startLoading() {
+            this.loading = this.$loading({
+                lock: true,
+                text: 'Loading',
+                spinner: 'el-icon-loading',
+                background: 'rgba(0, 0, 0, 0.8)'
+            })
+        },
+        closeLoading() {
+            this.loading.close()
+        },
+    }
+}
+</script>
+
+<style lang="less" scoped>
+@import "./ActivityAnswer.less";
+</style>

+ 150 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/view/student/ActivityInfo.less

@@ -0,0 +1,150 @@
+.el-container {
+    background-color: #ffffff;
+    position: relative;
+
+    .el-header {
+        background-color: #b3c0d1;
+        // color: #333;
+        line-height: 60px;
+    }
+
+    .el-main {
+        // background-color: #ffffff;
+        // color: #333;
+        width: 65%;
+        height: 100%;
+        margin: auto;
+        padding: 40px;
+
+        .header-box {
+            display: flex;
+            justify-content: space-between;
+
+            .base-info {
+                font-size: 14px;
+
+                p {
+                    margin-bottom: 5px;
+
+                    &>span {
+                        font-weight: bold;
+                        color: #00ad6c;
+                        margin-right: 5px;
+                    }
+                }
+            }
+        }
+
+        .title-mark {
+            background: #00ad6c;
+            padding: 4px 13px;
+            margin-right: 10px;
+            color: #fff;
+            border-radius: 18px;
+        }
+
+        .paper-subject {
+            font-weight: bolder;
+            line-height: 40px;
+            margin-right: 20px;
+            cursor: pointer;
+            display: inline-flex;
+
+            &>span:first-child {
+                background-color: #00ad6c;
+                padding: 0 15px;
+                color: #fff;
+                border-radius: 5px 0 0 5px;
+            }
+
+            &>span:nth-of-type(2) {
+                padding: 0 15px;
+                border: 1px solid #ababab;
+                border-radius: 0 5px 5px 0;
+                border-color: #00ad6c;
+            }
+
+            .el-icon-circle-check {
+                color: #00ad6c;
+                font-size: 18px;
+                margin-left: 15px;
+            }
+        }
+
+        .paper-content {
+            margin-top: 50px;
+        }
+
+
+        .art-content {
+            margin: 10px 0;
+            // height: 100%;
+
+            .subject-content {
+                margin-bottom: 50px;
+
+                .subject-title {
+                    width: calc(100% - 20px);
+                    background-color: #79b29c;
+                    color: #fff;
+                    font-size: 20px;
+                    padding: 10px;
+                    border-radius: 5px;
+                    margin: 20px 0;
+                }
+
+                .paper-answer {
+                    .scoreboard {
+                        // width: 100%;
+                        background: rgb(240, 240, 240);
+                        border-radius: 8px;
+                        padding: 15px;
+
+                        .ivu-card {
+                            margin-bottom: 20px;
+                        }
+
+                        .to-answer {
+                            // font-size: 25px;
+                            font-weight: 800;
+                            padding: 17px;
+                            cursor: pointer;
+
+                            .answer-type {
+                                margin-left: 50px;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    .loading-container {
+        position: absolute;
+        width: 100%;
+        height: 100%;
+        right: 0;
+        top: 0;
+        display: flex;
+        flex-direction: row;
+        justify-content: center;
+        /*background: rgba(103, 103, 103, 0.27);*/
+        z-index: 1000;
+
+        #loadingBox {
+            height: 200px;
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+            align-items: center;
+            background: #ffffff;
+            padding: 50px;
+            border-radius: 10px;
+
+            .el-progress__text {
+                font-size: 30px;
+            }
+        }
+    }
+}

+ 223 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/ClientApp/src/view/student/ActivityInfo.vue

@@ -0,0 +1,223 @@
+<template>
+    <el-container>
+        <div class="loading-container" style="background: rgba(0, 0, 0, 0.7)" v-show="isLoadQues">
+            <div id="loadingBox" style="margin: auto;">
+                <el-progress type="circle" :stroke-width="8" :width="150" define-back-color="#fff" text-color="#07ac88" color="#07ac88" :percentage="80"></el-progress>
+                <div style="color: #00ad6c; margin: 30px 0px 15px; font-size: 20px;">试题载入中,请耐心等待</div>
+                <span style="font-size: 12px; color: #666 !important;">若长时间卡住,请 <a href="#" @click.prevent="handleLink()" style="color: #1487ff;">刷新页面</a> 后重试 </span>
+            </div>
+        </div>
+        <el-header>Header</el-header>
+        <el-main>
+            <div class="header-box">
+                <div style="font-weight: bold; font-size: 1.5em;">
+                    <span class="title-mark">评测</span>
+                    <span>评测名称</span>
+                </div>
+                <div class="base-info">
+                    <p>
+                        <i class="el-icon-user"></i>
+                        课程老师:<span>罗老师</span>
+                    </p>
+                    <p>
+                        <i class="el-icon-postcard"></i>
+                        学生名单:<span>2021级3班</span>
+                    </p>
+                    <p>
+                        <i class="el-icon-user"></i>
+                        学生姓名:<span>鹿晗</span>
+                    </p>
+                    <p>
+                        <i class="el-icon-time"></i>
+                        活动时间:<span>2024-12-20 11:00 ~ 2025-01-20 11:00</span>
+                    </p>
+                </div>
+            </div>
+            <template v-if="activityInfo.type === 'Exam' || (activityInfo.type === 'Art' && activityInfo.progress === 'finish')">
+                <div style="margin-top: 30px;">
+                    <div v-for="(item, index) in subjectList" :key="index" class="paper-subject">
+                        <span>{{ item }}</span>
+                        <span>
+                            {{ item }}科试卷
+                            <i class="el-icon-circle-check"></i>
+                        </span>
+                    </div>
+                </div>
+                <div class="paper-content">
+                    <el-tabs v-model="activeName">
+                        <el-tab-pane label="评测内容" name="exam">
+                            
+                        </el-tab-pane>
+                        <el-tab-pane label="评测内容" name="111">用户管理</el-tab-pane>
+                    </el-tabs>
+                </div>
+            </template>
+            <template v-if="activityInfo.type === 'Art' && activityInfo.progress === 'going'">
+                <div class="art-content">
+                    <div class="subject-content" v-for="item in artExam" :key="item.id">
+                        <p class="subject-title">{{ item.subject.name }}</p>
+                        <div class="paper-answer" v-if="item.exam[0]">
+                            <div class="scoreboard">
+                                <div v-show="item.testState === 1" @click="onLoadQues(item)" class="to-answer">
+                                    <i class="el-icon-dianji" style="font-size: 35px; color: #03966a;"></i>
+                                    <span style="color: #03966a; margin-left: 10px; font-size: 25px;">前往作答</span>
+                                    <i class="el-icon-weizuoda answer-type" style="font-size: 50px;"></i>
+                                </div>
+                                <div v-show="item.testState === 2 || item.testState === 3" class="to-answer">
+                                    <i class="el-icon-gougou1" style="color: green;"></i>
+                                    <span style="margin-left: 5px;">成绩尚未结算,请等待老师批改试卷,统计成绩</span>
+                                    <i class="el-icon-yiwancheng answer-type" style="font-size: 50px; color: #01adff;"></i>
+                                </div>
+                            </div>
+                            <template v-if="item.homework.length">
+                                <div v-for="(hw, index) in item.homework" :key="index">
+                                    <template v-if="hw">
+                                        <template v-if="hw.subject === 'subject_music' && hw.quotaId === 'quota_22'">
+                                            <div class="scoreboard">
+                                                <!-- <div v-if="hw.overTime && !hw.isAnswer" class="to-answer"> -->
+                                                    <div class="to-answer">
+                                                    <i class="el-icon-warning-outline"></i>
+                                                    <span style="margin-left: 5px;">已结束</span>
+                                                    <i class="el-icon-weizuoda answer-type" style="font-size: 50px;"></i>
+                                                </div>
+                                            </div>
+                                                <div class="scoreboard">
+                                                <!-- <div v-else-if="!hw.overTime && hw.isAnswer" class="to-answer"> -->
+                                                    <div class="to-answer">
+                                                    <i class="el-icon-gougou1" style="color: green;"></i>
+                                                    <span style="margin-left: 5px;">已完成作答</span>
+                                                    <i class="el-icon-yiwancheng answer-type" style="font-size: 50px; color: #01adff;"></i>
+                                                </div></div>
+                                                <div class="scoreboard">
+                                                <!-- <div v-else-if="hw.overTime && hw.isAnswer" class="to-answer"> -->
+                                                    <div class="to-answer">
+                                                    <i class="el-icon-dianji" style="font-size: 35px; color: #03966a;"></i>
+                                                    <span style="color: #03966a; margin-left: 10px; font-size: 25px;">查看详情</span>
+                                                </div></div>
+                                                <!-- <div v-else class="to-answer"> -->
+                                                    <div class="scoreboard">
+                                                    <div class="to-answer">
+                                                    <i class="el-icon-dianji" style="font-size: 35px; color: #03966a;"></i>
+                                                    <span style="color: #03966a; margin-left: 10px; font-size: 25px;">开始演唱</span>
+                                                    <i class="el-icon-weizuoda answer-type" style="font-size: 50px;"></i>
+                                                </div>
+                                            </div>
+                                        </template>
+                                    </template>
+                                </div>
+                            </template>
+                        </div>
+                        <div v-else class="paper-answer">
+                            暂无科目相关内容
+                        </div>
+                    </div>
+                </div>
+            </template>
+        </el-main>
+    </el-container>
+</template>
+
+<script>
+export default {
+    data() {
+        return {
+            isLoadQues: false,
+            loading: undefined,
+            subjectList: ['语文', '数学'],
+            activeName: 'exam',
+            activityInfo: {
+                type: 'Art',
+                progress: 'going'
+            },
+            artExam: [
+                {
+                    subject: {name: '语文'},
+                    exam: [{}],
+                    testState: 2,
+                    homework: [],
+                },
+                {
+                    subject: {name: '音乐'},
+                    exam: [{}],
+                    testState: 1,
+                    homework: [
+                        {
+                            subject: 'subject_music',
+                            quotaId: 'quota_22',
+                            overTime: '',
+                            isAnswer: true
+                        }
+                    ],
+                }
+            ],
+            processNum: 0,
+            isUpload: false
+        }
+    },
+    methods: {
+        startLoading() {
+            this.loading = this.$loading({
+                lock: true,
+                text: 'Loading',
+                spinner: 'el-icon-loading',
+                background: 'rgba(0, 0, 0, 0.8)'
+            })
+        },
+        closeLoading() {
+            this.loading.close()
+        },
+        handleLink() {
+            window.location.reload()
+        },
+        async onLoadQues(art) {
+            this.processNum = 0
+            this.isLoadQues = true
+            /* let infos = await this.getPaper(art.subject.id)
+            this.showTest(art) */
+        },
+    }
+}
+</script>
+
+<style lang="less" scoped>
+@import "./ActivityInfo.less";
+</style>
+
+<style lang="less">
+.paper-content {
+    .el-tabs__active-bar {
+        display: none;
+    }
+
+    .el-tabs__item.is-active {
+        color: #24b880;
+        font-weight: bolder;
+        border-bottom: 4px solid #24b880 !important;
+        margin: 0 10px;
+        width: auto;
+        text-align: center !important;
+        font-size: 24px;
+    }
+
+    .el-tabs__item {
+        text-align: center !important;
+        font-size: 24px;
+        margin: 0 10px;
+        font-weight: bolder;
+        color: #5a5a5a;
+
+        &:hover {
+            color: #24b880;
+        }
+    }
+
+    .el-tabs--top .el-tabs__item.is-top{
+        padding: 5px 20px;
+        height: 100%;
+    }
+}
+
+.loading-container .el-progress__text {
+    font-size: 30px !important;
+}
+</style>

+ 15 - 4
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/BaseController.cs

@@ -45,20 +45,31 @@ namespace IES.ExamServer.Controllers
         /// </summary>        
         /// <param name="key">Key Name</param>
         /// <returns></returns>
-        public (string id, string name, string picture, string school, string keyData) GetAuthTokenInfo(string? key = null)
+        public (string id, string? name, string picture, string school,string scope ,string timeZone,List<string> rolse, string keyData) GetAuthTokenInfo(string? key = null)
         {
             object? keyData = null;
             HttpContext.Items.TryGetValue("ID", out object? id);
             HttpContext.Items.TryGetValue("Name", out object? name);
             HttpContext.Items.TryGetValue("Picture", out object? picture);
             HttpContext.Items.TryGetValue("School", out object? school);
+            HttpContext.Items.TryGetValue("Scope", out object? scope);
+            HttpContext.Items.TryGetValue("TimeZone", out object? timeZone);
+            List<string> rolse= new List<string>();
+            if (HttpContext.Items.TryGetValue("Roles", out object? _roles)) 
+            {
+                if (_roles is List<string> s)
+                    {
+                    rolse=s;
+                }
+            }
             if (!string.IsNullOrWhiteSpace(key))
             {
                 HttpContext.Items.TryGetValue(key, out keyData);
             }
-            return ($"{id}", $"{name}", $"{picture}", $"{school}", $"{keyData}");
+            return ($"{id}", $"{name}", $"{picture}", $"{school}",$"{scope}",$"{timeZone}", rolse, $"{keyData}");
         }
 
+       
         /// <summary>
         /// 取得驗證金鑰,Authorization
         /// </summary>        
@@ -85,8 +96,8 @@ namespace IES.ExamServer.Controllers
 
 
 
-        public readonly int code = 0;
-        public readonly string msg = "OK";
+        public   int code = 0;
+        public   string msg = "OK";
 
 
     }

+ 24 - 11
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/IndexController.cs

@@ -9,6 +9,8 @@ using System.DrawingCore.Imaging;
 using System.DrawingCore;
 using System.IdentityModel.Tokens.Jwt;
 using IES.ExamServer.Services;
+using IES.ExamServer.DI;
+using IES.ExamLib.Models;
 
 namespace IES.ExamServer.Controllers
 {
@@ -21,19 +23,20 @@ namespace IES.ExamServer.Controllers
         private readonly IHttpClientFactory _httpClientFactory;
         private readonly IMemoryCache _memoryCache;
         private readonly ILogger<IndexController> _logger;
-
-        public IndexController(ILogger<IndexController> logger, IConfiguration configuration, IHttpClientFactory httpClientFactory, IMemoryCache memoryCache)
+        private readonly DataCenterConnectionService _connectionService;
+        private readonly LiteDBFactory _liteDBFactory;
+        public IndexController(ILogger<IndexController> logger, IConfiguration configuration, IHttpClientFactory httpClientFactory, IMemoryCache memoryCache,DataCenterConnectionService connectionService, LiteDBFactory liteDBFactory)
         {
             _logger = logger;
             _configuration=configuration;
             _httpClientFactory=httpClientFactory;
             _memoryCache=memoryCache;
+            _connectionService=connectionService;
+            _liteDBFactory=liteDBFactory;
         }
         [HttpPost("device")]
-        public async Task<IActionResult> Device(JsonElement json )
+        public IActionResult Device(JsonElement json )
         {
-            int code = 0;
-            string msg = string.Empty;
             try
             {
                 string ip=   GetIP();
@@ -78,8 +81,6 @@ namespace IES.ExamServer.Controllers
         [HttpPost("login-check")]
         public async Task<IActionResult> LoginCheck(JsonNode json)
         {
-            int code = 0;
-            string msg = string.Empty;
             try
             {
                 var type = json["type"];
@@ -90,8 +91,20 @@ namespace IES.ExamServer.Controllers
                     string x_auth_token = string.Empty;
                     List<School>? schools = null;
                     JsonNode? jsonNode = null;
+                    long time = DateTimeOffset.Now.ToUnixTimeMilliseconds();
                     switch (true)
                     {
+                        //跳过忽略,但是仍然要以访客身份登录
+                        case bool when $"{type}".Equals(ExamConstant.ScopeVisitor):
+                            {
+                                string id = $"{DateTimeOffset.Now.ToUnixTimeSeconds()}";
+                                string name = $"访客教师-{Random.Shared.Next(100, 999)}";
+                                x_auth_token = JwtAuthExtension.CreateAuthToken("www.teammodel.cn",id ,name,null
+                                    ,ExamConstant.JwtSecretKey,ExamConstant.ScopeVisitor,8,null,new string[] { "visitor" }, expire: 1);
+                                //  _memoryCache.Set($"Teacher:{id}", new Teacher { id = id, name = $"{name}", implicit_token = token, picture = null, schools = schools, x_auth_token = x_auth_token });
+                                _liteDBFactory.GetLiteDatabase().GetCollection<Teacher>().Upsert(new Teacher { id = id, name = $"{name}", implicit_token = token, picture = null, schools = schools, x_auth_token = x_auth_token,loginTime=time });
+                                return Ok(new { code = 200,x_auth_token = x_auth_token });
+                            }
                         case bool when $"{type}".Equals("qrcode"):
                             {
                                 string randomCode = $"{json["randomCode"]}";
@@ -104,7 +117,6 @@ namespace IES.ExamServer.Controllers
                                     if (!string.IsNullOrWhiteSpace(content))
                                     {
                                         jsonNode = JsonSerializer.Deserialize<JsonNode>(content);
-
                                     }
                                     else
                                     {
@@ -155,8 +167,9 @@ namespace IES.ExamServer.Controllers
                         var id = jwt.Payload.Sub;
                         jwt.Payload.TryGetValue("name", out object? name);
                         jwt.Payload.TryGetValue("picture", out object? picture);
-                        _memoryCache.Set($"Teacher:{id}", new LoginTeacher { id=id, name=$"{name}", implicit_token= token, picture=$"{picture}", schools=schools, x_auth_token=x_auth_token });
-                        return Ok(new { code=200, implicit_token = token, x_auth_token = x_auth_token, schools = schools });
+                        //_memoryCache.Set($"Teacher:{id}", new Teacher { id=id, name=$"{name}", implicit_token= token, picture=$"{picture}", schools=schools, x_auth_token=x_auth_token });
+                        _liteDBFactory.GetLiteDatabase().GetCollection<Teacher>().Upsert(new Teacher { id=id, name=$"{name}", implicit_token= token, picture=$"{picture}", schools=schools, x_auth_token=x_auth_token ,loginTime=time });
+                        return Ok(new { code=200,/* implicit_token = token, schools = schools , */ x_auth_token = x_auth_token });
                     }
                     else
                     {
@@ -175,7 +188,7 @@ namespace IES.ExamServer.Controllers
                 code=500;
                 msg="异常错误";
             }
-            return Ok(new { code = code });
+            return Ok(new { code = code,msg });
         }
         /*
      

+ 369 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/ManageController.cs

@@ -0,0 +1,369 @@
+using IES.ExamLib.Models;
+using IES.ExamServer.DI;
+using IES.ExamServer.DI.SignalRHost;
+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.Configuration;
+using System.Linq.Expressions;
+using System.Net.Http;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+
+namespace IES.ExamServer.Controllers
+{
+    [ApiController]
+    [Route("manage")]
+    public class ManageController:BaseController
+    {
+        private readonly IConfiguration _configuration;
+        private readonly IHttpClientFactory _httpClientFactory;
+        private readonly IMemoryCache _memoryCache;
+        private readonly ILogger<ManageController> _logger;
+        private readonly LiteDBFactory _liteDBFactory;
+        private readonly DataCenterConnectionService _connectionService;
+        private readonly int DelayMicro = 10;//微观数据延迟
+        private readonly int DelayMacro = 100;//宏观数据延迟
+        private readonly SignalRExamServerHub _signalRExamServerHub;
+        public ManageController(LiteDBFactory liteDBFactory,ILogger<ManageController> logger, IConfiguration configuration,
+            IHttpClientFactory httpClientFactory, IMemoryCache memoryCache, DataCenterConnectionService connectionService,SignalRExamServerHub signalRExamServerHub)
+        {
+            _logger = logger;
+            _configuration=configuration;
+            _httpClientFactory=httpClientFactory;
+            _memoryCache=memoryCache;
+            _liteDBFactory=liteDBFactory;
+            _connectionService=connectionService;
+            _signalRExamServerHub=signalRExamServerHub;
+        }
+        [HttpPost("download-package")]
+        [AuthToken("admin","teacher")]
+        public async Task<IActionResult> DownloadPackage(JsonNode json)
+        {
+
+            //C#.NET 6 后端与前端流式通信
+            //https://www.doubao.com/chat/collection/687687510791426?type=Thread
+            //下载日志记录:1.步骤,检查,2.获取描述信息,3.分类型,4下载文件,5.前端处理,6.返回结果 , 正在下载...==> [INFO]https://www.doubao.com/chat/collection/687687510791426?type=Thread [Size=180kb] Ok...
+            //进度条 展示下载文件总大小和已下载,末尾展示 文件总个数和已下载个数
+            //https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/8.0.7/signalr.min.js
+            return Ok();
+        }
+        [HttpPost("check-short-code")]
+        public async Task<IActionResult> CheckShortCode(JsonNode json)
+        {
+           
+            string shortCode = $"{json["shortCode"]}";
+            string evaluationId = $"{json["evaluationId"]}";
+            string deviceId = $"{json["deviceId"]}";
+            Expression<Func<EvaluationClient, bool>> predicate = x => true;
+
+            if (!string.IsNullOrEmpty(shortCode))
+            {
+                var codePredicate = ExpressionHelper.Or<EvaluationClient>(
+                    x => !string.IsNullOrWhiteSpace(x.shortCode) &&  x.shortCode == shortCode,
+                    x => !string.IsNullOrWhiteSpace(x.password) &&  x.password == shortCode
+                );
+                predicate= predicate.And(codePredicate);
+            }
+            else {
+                return Ok(new { code = 400,msg="必须输入开卷码" });
+            }
+            if (!string.IsNullOrWhiteSpace(evaluationId))
+            {
+                predicate= predicate.And(x => x.id!.Equals(evaluationId));
+            }
+            IEnumerable<EvaluationClient> evaluationClients = _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationClient>().Find(predicate);
+            EvaluationClient? evaluationLocal = null;
+            EvaluationClient? evaluationCloud = null;
+            if (evaluationClients.Count()>0)
+            {
+                evaluationLocal= evaluationClients.First();
+            }
+            //如果要访问中心,则需要教师登录联网。  
+            var token = GetAuthTokenInfo();
+            if (token.scope.Equals(ExamConstant.ScopeTeacher))
+            {
+                if (  _connectionService.dataCenterIsConnected)
+                {
+                    Teacher teacher=  _liteDBFactory.GetLiteDatabase().GetCollection<Teacher>().FindOne(x => x.id!.Equals(token.id));
+                    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, teacher.x_auth_token);
+                    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 =JsonSerializer.Deserialize<JsonNode>(content);
+                        if (jsonNode!=null) 
+                        {
+                            evaluationCloud= JsonSerializer.Deserialize<EvaluationClient>(jsonNode["evaluation"]);
+                        }
+                    }
+                }
+            }
+            //数据,文件,页面 0 没有更新,1 有更新
+            int data = 0,blob=0,webview=0,status=0;
+            long dataSize = 0, blobSize=0 , webviewSize=0;
+            if (evaluationLocal== null && evaluationCloud==null)
+            {
+                //线上线下没有数据
+                status=1;
+                
+            }
+            else if (evaluationLocal!=null && evaluationCloud!=null) 
+            {
+                //线上线下有数据
+                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;
+                }
+                if ((evaluationLocal.dataTime<evaluationCloud.dataTime)
+                    ||(evaluationLocal.dataSize!=evaluationCloud.dataSize))
+                {
+                    data=1;
+                    dataSize=evaluationCloud.dataSize;
+                }
+                if ((evaluationLocal.webviewCount!=evaluationCloud.webviewCount)
+                    ||(evaluationLocal.webviewSize!= evaluationCloud.webviewSize)
+                    ||(evaluationLocal.webviewTime!= evaluationCloud.webviewTime)
+                    ||(!string.IsNullOrWhiteSpace(evaluationLocal.webviewPath)&&  !evaluationLocal.webviewPath.Equals(evaluationCloud.webviewPath)))
+                {
+                    webview=1;
+                    webviewSize=evaluationCloud.webviewSize;
+                }
+            }
+            else if (evaluationLocal!=null && evaluationCloud==null)
+            {
+                //线下有数据,线上没有数据,可能没联网。
+                status = 3;
+            }
+            else if (evaluationLocal==null && evaluationCloud!=null)
+            {
+                //线下没有数据,线上有数据
+                evaluationLocal= evaluationCloud;
+                blob=1;
+                data=1;
+                webview=1;
+                blobSize=evaluationCloud.blobSize;
+                dataSize=evaluationCloud.dataSize;
+                webviewSize=evaluationCloud.webviewSize;
+                status = 4;
+                _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationClient>().Insert(evaluationLocal);
+            }
+            List<string> file_error = new List<string>();
+            if (evaluationLocal!=null)
+            {
+
+
+                await _signalRExamServerHub.SendMessage(deviceId, Constant._Message_grant_type_check_file, 
+                    new MessageContent {messageType=Constant._Message_type_message, status=0, content="开始检查评测信息文件.." });
+                //校验本地文件数据
+                string packagePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "package");
+                if (!Directory.Exists(packagePath))
+                    Directory.CreateDirectory(packagePath);
+                string evaluationPath = Path.Combine(packagePath, evaluationLocal.id!);
+               // await Task.Delay(DelayMacro);
+                int msg_status = Constant._Message_status_info;
+                string path_evaluation = Path.Combine(evaluationPath, "evaluation.json");
+                if (!System.IO.File.Exists(path_evaluation))
+                {
+                    file_error.Add("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(deviceId, Constant._Message_grant_type_check_file, 
+                    new MessageContent { messageType= Constant._Message_type_check, status=msg_status,content=$"评测数据文件:{path_evaluation}" });
+                //await Task.Delay(DelayMacro);
+                string path_groupList = Path.Combine(evaluationPath, "groupList.json");
+                msg_status =Constant._Message_status_info;
+                if (!System.IO.File.Exists(path_groupList))
+                {
+                    file_error.Add("groupList");
+                    msg_status=Constant._Message_status_error;
+                }
+                else
+                {
+                    msg_status=Constant._Message_status_success;
+                }
+                await _signalRExamServerHub.SendMessage(deviceId, Constant._Message_grant_type_check_file, 
+                    new MessageContent { messageType= Constant._Message_type_check, status=msg_status, content=$"评测名单文件:{path_groupList}" });
+                //await Task.Delay(DelayMacro);
+                string path_source = Path.Combine(evaluationPath, "source.json");
+                msg_status = Constant._Message_status_info;
+                if (!System.IO.File.Exists(path_source))
+                {
+                    file_error.Add("source");
+                    msg_status=Constant._Message_status_error;
+                }
+                else
+                {
+                    msg_status=Constant._Message_status_success;
+                }
+                await _signalRExamServerHub.SendMessage(deviceId, Constant._Message_grant_type_check_file,
+                    new MessageContent { messageType= Constant._Message_type_check, status=msg_status, content=$"评测原始数据:{path_source}" });
+               // await Task.Delay(DelayMacro);
+                msg_status =Constant._Message_status_info;
+                try {
+
+                    string evaluation_str = await System.IO.File.ReadAllTextAsync(path_evaluation);
+                    JsonNode? evaluation_data = JsonSerializer.Deserialize<JsonNode>(evaluation_str);
+                    
+                    if (evaluation_data!=null) 
+                    {
+                        EvaluationClient? evaluationClient = JsonSerializer.Deserialize<EvaluationClient>(evaluation_data["evaluationClient"]);
+                        if (evaluationClient!=null) 
+                        {
+                            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)&&(evaluationLocal.webviewCount==evaluationClient.webviewCount)
+                                &&(evaluationLocal.webviewSize== evaluationClient.webviewSize)
+                                &&(evaluationLocal.webviewTime== evaluationClient.webviewTime)
+                                &&(!string.IsNullOrWhiteSpace(evaluationLocal.webviewPath)&&  evaluationLocal.webviewPath.Equals(evaluationClient.webviewPath)))
+                            {
+                                msg_status=1;
+                            }
+                            else
+                            {
+                                msg_status=Constant._Message_status_error;
+                            }
+                            await _signalRExamServerHub.SendMessage(deviceId, Constant._Message_grant_type_check_file,
+                                new MessageContent { messageType=Constant._Message_type_message, status=msg_status, content="校验本地数据文件..." });
+                        }
+
+                        List<EvaluationExam>? evaluationExams = JsonSerializer.Deserialize<List<EvaluationExam>>(evaluation_data["evaluationExams"]);
+                        if (evaluationExams.IsNotEmpty()) 
+                        {
+                            string path_papers = Path.Combine(evaluationPath, "papers");
+                            var papers_files = FileHelper.ListAllFiles(path_papers);
+                            foreach (var evaluationExam in evaluationExams!)
+                            {
+
+                                int paperIndex = 0;
+                                foreach (var paper in evaluationExam.papers) 
+                                {
+                                    paperIndex++;
+                                    List<MessageContent> contents = new List<MessageContent>();
+                                    int paper_error_count = 0;
+                                    foreach (var blobInfo in paper.blobs)
+                                    {
+                                        msg_status=Constant._Message_status_info;
+                                        if (!string.IsNullOrWhiteSpace(blobInfo.path))
+                                        {
+                                           
+                                            var file = papers_files.Find(x => x.Contains(blobInfo.path));
+                                            if (file!=null)
+                                            {
+                                                msg_status=1;
+                                                msg_status=Constant._Message_status_success;
+                                            }
+                                            else {
+                                                msg_status=Constant._Message_status_error;
+                                                paper_error_count++;
+                                            }
+
+                                        }
+                                        else {
+                                            msg_status=Constant._Message_status_warning; ;
+                                            paper_error_count++;
+                                        }
+                                        contents.Add(new MessageContent { messageType=Constant._Message_type_check, status=msg_status, content=$"试卷文件信息:{paper.paperName}" });
+                                    }
+                                    int paper_msg_status = Constant. _Message_status_info;
+                                    if (paper_error_count>0)
+                                    {
+                                        paper_msg_status=Constant._Message_status_error;
+                                    }
+                                    else {
+                                        paper_msg_status=Constant._Message_status_success;
+                                    }
+                                    await _signalRExamServerHub.SendMessage(deviceId, Constant._Message_grant_type_check_file, 
+                                        new MessageContent {
+                                            messageType=Constant._Message_type_message,
+                                            status=paper_msg_status,
+                                            content=$"试卷名称:[{paperIndex}]{evaluationExam.examName}-{evaluationExam.subjectName}-{paper.paperName}\r\n文件数量:{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) { 
+                
+                }
+                await _signalRExamServerHub.SendMessage(deviceId, Constant._Message_grant_type_check_file, 
+                    new MessageContent { messageType=Constant._Message_type_message, status=0, content="提取评测数据文件..." });
+                
+
+            }
+
+            return Ok(new {code=200, evaluation= evaluationLocal,data,blob,webview,dataSize,blobSize,webviewSize,status });
+        }
+
+        /// <summary>
+        /// 激活考试
+        /// </summary>
+        /// <param name="json"></param>
+        /// <returns></returns>
+        [HttpPost("activate-evaluation")]
+        public IActionResult ActivateEvaluation(JsonNode json)
+        {
+            string id = $"{json["id"]}";
+            string shortCode = $"{json["shortCode"]}";
+            if (!string.IsNullOrWhiteSpace(id) && !string.IsNullOrWhiteSpace(shortCode)) 
+            {
+                EvaluationClient evaluationClient = _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationClient>().FindOne(x => x.id!.Equals(id) && !string.IsNullOrWhiteSpace(x.shortCode) && x.shortCode.Equals(shortCode));
+                if (evaluationClient != null)
+                {
+
+                }
+            }
+            return Ok();
+        }
+        /// <summary>
+        /// 加载本地的活动列表
+        /// </summary>
+        /// <param name="json"></param>
+        /// <returns></returns>
+        [HttpPost("list-local-evaluation")]
+        public IActionResult ListLocalEvaluation(JsonNode json) 
+        {
+
+            IEnumerable<EvaluationClient> evaluationClients = _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationClient>().FindAll().OrderByDescending(x=>x.activate).ThenByDescending(x=>x.stime);
+            var result = evaluationClients.Select(client =>
+            {
+                var properties = client.GetType().GetProperties();
+                var anonymousObject = new Dictionary<string, object?>();
+                foreach (var property in properties)
+                {
+                    if (!property.Name .Equals("password")  && !property.Name.Equals("shortCode"))
+                    {
+                        anonymousObject[property.Name] = property.GetValue(client);
+                    }
+                }
+                return anonymousObject;
+            });
+            return Ok(new {code=200, evaluation= result });
+        }
+    }
+}

+ 6 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/StudentController.cs

@@ -0,0 +1,6 @@
+namespace IES.ExamServer.Controllers
+{
+    public class StudentController
+    {
+    }
+}

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

@@ -1,39 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Logging;
-
-namespace VueCliSample.Controllers
-{
-    [ApiController]
-    [Route("api/[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()
-        {
-            var rng = new Random();
-            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
-            {
-                Date = DateTime.Now.AddDays(index),
-                TemperatureC = rng.Next(-20, 55),
-                Summary = Summaries[rng.Next(Summaries.Length)]
-            })
-            .ToArray();
-        }
-    }
-}

+ 244 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/CustomFileLoggerProvider.cs

@@ -0,0 +1,244 @@
+using Microsoft.Extensions.Logging;
+using System.Collections.Concurrent;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using System.Text.Unicode;
+using System.Threading.Channels;
+
+namespace IES.ExamServer.DI
+{
+    public class CustomFileLoggerProvider : ILoggerProvider
+    {
+        private readonly string _logDirectory;
+        private readonly bool _enableConsoleOutput;
+        private readonly string _timestamp;
+        private readonly ConcurrentDictionary<string, CustomFileLogger> _loggers = new ConcurrentDictionary<string, CustomFileLogger>();
+
+        public CustomFileLoggerProvider(string logDirectory, bool enableConsoleOutput)
+        {
+            _logDirectory = logDirectory;
+            _enableConsoleOutput = enableConsoleOutput;
+            _timestamp = DateTime.Now.ToString("yyMMddHH"); // 生成时间戳
+            if (!Directory.Exists(_logDirectory))
+            {
+                Directory.CreateDirectory(_logDirectory);
+            }
+        }
+
+        public ILogger CreateLogger(string categoryName)
+        {
+            return _loggers.GetOrAdd(categoryName, name => new CustomFileLogger(_logDirectory, name, _enableConsoleOutput, _timestamp));
+        }
+
+        public void Dispose()
+        {
+            _loggers.Clear();
+        }
+    }
+    public class CustomFileLogger : ILogger
+    {
+        private readonly string _logDirectory;
+        private readonly string _categoryName;
+        private readonly bool _enableConsoleOutput;
+        private static readonly SemaphoreSlim _fileLock = new SemaphoreSlim(1, 1);
+        private readonly List<string> _logBuffer = new List<string>();
+      //  private readonly Timer _flushTimer;
+        private readonly string _timestamp;
+        private readonly Channel<string> _consoleLogChannel;
+        private readonly Channel<string> _fileLogChannel;
+
+        public CustomFileLogger(string logDirectory, string categoryName, bool enableConsoleOutput, string timestamp)
+        {
+            _logDirectory = logDirectory;
+            _categoryName = categoryName;
+            _enableConsoleOutput = enableConsoleOutput;
+            // 每 5 秒刷新一次日志缓冲区
+           // _flushTimer = new Timer(FlushLogs, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
+            _timestamp=timestamp;
+            // 创建无界 Channel
+            _consoleLogChannel = Channel.CreateUnbounded<string>();
+            // 创建无界 Channel
+            _fileLogChannel = Channel.CreateUnbounded<string>();
+            // 启动后台任务处理控制台日志
+            _ = Task.Run(ProcessConsoleLogs);
+            // 启动后台任务处理文件日志
+            _ = Task.Run(ProcessFileLogs);
+        }
+
+        public IDisposable BeginScope<TState>(TState state) => null;
+
+        public bool IsEnabled(LogLevel logLevel) => true;
+
+        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
+        {
+            var logMessage = formatter(state, exception);
+            var logFileName = $"{logLevel}_{_timestamp}.log"; // 按时间戳命名文件
+            var logFilePath = Path.Combine(_logDirectory, logFileName);
+
+            // 异步写入文件
+           // _ = WriteToFileAsync(logFilePath, logLevel, logMessage);
+            // 将日志消息写入 Channel(不阻塞)
+            _fileLogChannel.Writer.TryWrite($"{logFilePath}|||{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{logLevel}] {logMessage}{Environment.NewLine}");
+            // 输出到控制台
+            if (_enableConsoleOutput)
+            {
+                // Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{logLevel}] {logMessage}");
+                // _ = Task.Run(() => Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{logLevel}] {logMessage}")); 
+                _consoleLogChannel.Writer.TryWrite($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{logLevel}] {logMessage}");
+            }
+        }
+        private async Task ProcessConsoleLogs()
+        {
+            await foreach (var logMessage in _consoleLogChannel.Reader.ReadAllAsync())
+            {
+                Console.WriteLine(logMessage); // 输出到控制台
+            }
+        }
+        private async Task ProcessFileLogs()
+        {
+            await foreach (var logEntry in _fileLogChannel.Reader.ReadAllAsync())
+            {
+                try
+                {
+                    // 解析日志条目
+                    var parts = logEntry.Split("|||");
+                    var logFilePath = parts[0];
+                    var logMessage = parts[1];
+
+                    // 异步写入文件
+                    await File.AppendAllTextAsync(logFilePath, logMessage);
+                }
+                catch (Exception ex)
+                {
+                    // 处理文件写入异常
+                    // Console.WriteLine($"Failed to write log to file: {ex.Message}");
+                    _consoleLogChannel.Writer.TryWrite($"Failed to write log to file: {ex.Message}");
+                }
+            }
+        }
+     
+
+        //private async void FlushLogs(object state)
+        //{
+        //    List<string> logsToWrite;
+        //    lock (_logBuffer)
+        //    {
+        //        if (_logBuffer.Count == 0) return;
+        //        logsToWrite = new List<string>(_logBuffer);
+        //        _logBuffer.Clear();
+        //    }
+
+        //    var logFileName = $"{_categoryName}_{_timestamp}.log";
+        //    var logFilePath = Path.Combine(_logDirectory, logFileName);
+
+        //    await _fileLock.WaitAsync(); // 获取锁
+        //    try
+        //    {
+        //        await File.AppendAllLinesAsync(logFilePath, logsToWrite);
+        //    }
+        //    finally
+        //    {
+        //        _fileLock.Release(); // 释放锁
+        //    }
+        //}
+        public void Dispose()
+        {
+           // _flushTimer?.Change(Timeout.Infinite, 0); // 停止定时器
+           // FlushLogs(null); // 最后一次刷新日志
+        }
+    }
+    public static class LoggerExtensions
+    {
+        //private static readonly SemaphoreSlim _dataFileLock = new SemaphoreSlim(1, 1);
+        //private static readonly Channel<string> _fileLogChannel = Channel.CreateUnbounded<string>();
+        //private static readonly Channel<string> _consoleLogChannel = Channel.CreateUnbounded<string>();
+        private static readonly Channel<string> _fileLogChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(10000)
+        {
+            FullMode = BoundedChannelFullMode.DropWrite // 
+        });
+
+        private static readonly Channel<string> _consoleLogChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(10000)
+        {
+            //DropOldest: 此模式会移除并丢弃通道中最旧的那个数据项,以便为正在写入的新数据腾出空间。通道里最早进入的数据会被舍弃,从而让新的数据可以进入通道。
+            //DropNewest:  在这种模式下,为了给要写入的新数据项腾出空间,会移除并丢弃通道中最新的那个数据项。也就是说,新的数据会覆盖掉原本在通道里最新进入的元素,保证新数据能被写入。
+            //Wait:  当使用这种模式时,如果尝试向已满的有界通道写入数据,调用写入操作的线程会等待,直到通道中有空间可用,然后再完成写入操作。这意味着线程会被阻塞,直到可以成功放入新的数据项。
+            FullMode = BoundedChannelFullMode.DropWrite // 直接丢弃当前正要写入的这个数据项,通道中的内容保持不变,相当于放弃这次写入操作。
+        });
+        //懒加载
+        private static readonly Lazy<Task> _fileLogProcessor = new Lazy<Task>(() => Task.Run(ProcessFileLogs));
+        private static readonly Lazy<Task> _consoleLogProcessor = new Lazy<Task>(() => Task.Run(ProcessConsoleLogs));
+        private static readonly JsonSerializerOptions jsonSerializerOptions ;
+        static LoggerExtensions(){
+            jsonSerializerOptions= new JsonSerializerOptions { Encoder=JavaScriptEncoder.Create(UnicodeRanges.All) };
+        }
+
+        public static void LogData<T>(this ILogger logger, T data, string id)
+        {
+            // 确保后台任务已启动,现在就要用确保启动
+            _ = _fileLogProcessor.Value;
+            _ = _consoleLogProcessor.Value;
+            var logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Logs", "DataLogs");
+            if (!Directory.Exists(logDirectory))
+            {
+                Directory.CreateDirectory(logDirectory);
+            }
+
+            var logFilePath = Path.Combine(logDirectory, $"{id}.log");
+            var logMessage = System.Text.Json.JsonSerializer.Serialize(data, jsonSerializerOptions);
+            _fileLogChannel.Writer.TryWrite($"{logFilePath}|||{DateTime.Now:yyyy-MM-dd HH:mm:ss} [Data] {logMessage}{Environment.NewLine}");
+            //_ = Task.Run(async () =>
+            //{
+            //    await _dataFileLock.WaitAsync(); // 获取锁
+            //    try
+            //    {
+            //        await File.AppendAllTextAsync(logFilePath, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [Data] {logMessage}{Environment.NewLine}");
+            //    }
+            //    finally
+            //    {
+            //        _dataFileLock.Release(); // 释放锁
+            //    }
+            //});
+        }
+        static async Task ProcessFileLogs()
+        {
+            try
+            {
+                await foreach (var logEntry in _fileLogChannel.Reader.ReadAllAsync())
+                {
+                   
+                    try
+                    {
+                        // 解析日志条目
+                        var parts = logEntry.Split("|||");
+                        var logFilePath = parts[0];
+                        var logMessage = parts[1];
+
+                        // 异步写入文件
+                        await File.AppendAllTextAsync(logFilePath, logMessage, Encoding.UTF8);
+                    }
+                    catch (Exception ex)
+                    {
+                        // 处理文件写入异常
+                        // Console.WriteLine($"Failed to write log to file: {ex.Message}");
+                        _consoleLogChannel.Writer.TryWrite($"Failed to write log to file: {ex.Message}");
+                    }
+                }
+            }
+            catch (Exception ex) {
+                _consoleLogChannel.Writer.TryWrite($"Failed to write log to file: {ex.Message}");
+            }
+            
+            // 循环结束后,检查是否有剩余的日志条目
+          
+        }
+         
+        static async Task ProcessConsoleLogs()
+        {
+            await foreach (var logMessage in _consoleLogChannel.Reader.ReadAllAsync())
+            {
+                Console.WriteLine(logMessage); // 输出到控制台
+            }
+        }
+    }
+}

+ 18 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/DataCenterConnectionService.cs

@@ -0,0 +1,18 @@
+namespace IES.ExamServer.DI
+{
+    public class DataCenterConnectionService
+    {
+        
+
+
+
+        private bool _dataCenterIsConnected;
+        public string? centerUrl { get; set; }
+
+        public bool dataCenterIsConnected
+        {
+            get { return string.IsNullOrWhiteSpace(centerUrl) ? false : _dataCenterIsConnected; }
+            set { _dataCenterIsConnected = value; }
+        }
+    }
+}

+ 120 - 48
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/SignalRHost/SignalRExamServerHub.cs

@@ -1,5 +1,11 @@
-using Microsoft.AspNetCore.SignalR;
+using IES.ExamServer.Helper;
+using IES.ExamServer.Models;
+using IES.ExamServer.Services;
+using Microsoft.AspNetCore.SignalR;
+using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Primitives;
+using System.Text.Json;
 
 
 namespace IES.ExamServer.DI.SignalRHost
@@ -7,62 +13,128 @@ namespace IES.ExamServer.DI.SignalRHost
     public  class SignalRExamServerHub : Hub<IClient>
     {
         private readonly ILogger<SignalRExamServerHub> _logger;
+        private readonly IMemoryCache _memoryCache;
       
-        public SignalRExamServerHub(ILogger<SignalRExamServerHub> logger)
+        public SignalRExamServerHub(ILogger<SignalRExamServerHub> logger,IMemoryCache memoryCache)
         {
             _logger = logger;
-           
+            _memoryCache = memoryCache;
         }
-    }
-    public interface IClient
-    {
-        Task ReceiveMessage(MessageBody message);
-        Task ReceiveConnection(MessageBody message);
-        Task ReceiveDisConnection(MessageBody message);
-    }
-    public abstract class MessageBody
-    {
-        public MessageBody()
+
+        // <summary>
+        /// 这需要继承Hub来创建中心,并向中心添加方法,客户端可以调用标识符为public的方法
+        /// </summary>
+        /// <param name="user"></param>
+        /// <param name="message"></param>
+        /// <returns></returns>
+
+        public async Task SendMessage(string clientId, string grant_type, MessageContent content )
         {
-            time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+            //双向检测是否连接。
+            SignalRClient signalRClient = _memoryCache.Get<SignalRClient>($"{Constant._KeySignalRClientClients}:{clientId}");
+            if (signalRClient!=null)
+            {
+                signalRClient = _memoryCache.Get<SignalRClient>($"{Constant._KeySignalRClientConnects}:{signalRClient.connid}");
+            }
+            if (signalRClient != null)
+            {
+                try {
+                    switch (true)
+                    {
+                        case bool when grant_type.Equals(Constant._Message_grant_type_check_file):
+                            {
+                                await Clients.Client(signalRClient.connid!).ReceiveMessage(new CheckFileMessageBody
+                                {
+                                    content = content.content,
+                                    status = content.status,
+                                    clientid = clientId,
+                                    connid = signalRClient.connid,
+                                    grant_type = grant_type,
+                                    time = DateTimeOffset.Now.ToUnixTimeMilliseconds(),
+                                    type = Constant._Message_type_message,
+                                    contents = content.contents
+                                });
+                                break;
+                            }
+                        default:
+                            break;
+                    }
+                } catch { }
+            }
         }
+
         /// <summary>
-        /// 连接id
-        /// </summary>
-        public virtual string? connid { get; set; }
-        /// <summary>
-        /// 客户端id
-        /// </summary>
-        public virtual string? clientid { get; set; }
-        /// <summary>
-        /// 状态  busy 忙碌,free 空闲,down 离线,error 错误
-        /// </summary>
-        public virtual string? status { get; set; }
-        /// <summary>
-        /// 消息内容
-        /// </summary>
-        public virtual string? content { get; set; }
-        /// <summary>
-        /// 消息创建时间
-        /// </summary>
-        public virtual long time { get; }
-        /// <summary>
-        /// 授权类型,bookjs_api 
-        /// </summary>
-        public virtual string? grant_type { get; set; }
-        /// <summary>
-        /// 消息类型
+        /// 客户连接成功时触发
         /// </summary>
-        public virtual MessageType message_type { get; set; }
-
+        /// <returns></returns>
+        public override async Task OnConnectedAsync() 
+        {
+            ServerDevice device = _memoryCache.Get<ServerDevice>(Constant._KeyServerDevice);
+            var connid = Context.ConnectionId;
+            var httpContext = Context.GetHttpContext();
+            if (httpContext != null) 
+            {
+                //wss://www.winteach.cn/signalr/notify?grant_type=wechat_qrcode&scene=0a75aca57536490ba00fe62e27bb8f6c&id=U2MNiCFNPPuVcw2gUI_gRA
+                //wss://www.winteach.cn/signalr/notify?grant_type=bookjs_api&clientid={clientid}&id=客户端自动生成的
+                httpContext.Request.Query.TryGetValue("grant_type", out StringValues grant_type);
+                httpContext.Request.Query.TryGetValue("clientid", out StringValues clientid);
+                await Groups.AddToGroupAsync(connid, grant_type!);
+                if (!clientid.Equals(StringValues.Empty) && !grant_type.Equals(StringValues.Empty)) 
+                {
+                    var client = new SignalRClient
+                    {
+                        connid = connid,
+                        grant_type = grant_type,
+                        clientid= clientid,//浏览器生成的客户端设备id
+                        serverid=device.deviceId,//服务器设备id
+                    };
+                   
+                    switch (true) 
+                    {
+                        // 检查文件
+                        case bool when grant_type.Equals(Constant._Message_grant_type_check_file):
+                            {
+                                _memoryCache.Set($"{Constant._KeySignalRClientClients}:{Constant._Message_grant_type_check_file}:{clientid}", JsonSerializer.Serialize(client));
+                                _memoryCache.Set($"{Constant._KeySignalRClientConnects}:{Constant._Message_grant_type_check_file}:{connid}", JsonSerializer.Serialize(client));
+                                await SendConnection(connid, new ConnectionMessageBody
+                                {
+                                    connid=connid,
+                                    clientid = clientid,
+                                    grant_type = grant_type,
+                                    content = $"连接成功",
+                                    type=Constant._Message_type_message,
+                                    status = Constant._Message_status_success,
+                                    time = DateTimeOffset.Now.ToUnixTimeMilliseconds(),
+                                    
+                                });
+                                break;
+                            }
+                    }
+                }
+            }
+        }
+        public async Task SendConnection(string connectionId, ConnectionMessageBody msg)
+        {
+            await Clients.Client(connectionId).ReceiveConnection(msg);
+        }
+        public async override Task OnDisconnectedAsync(Exception? exception)
+        {
+            var connid = Context.ConnectionId;
+            SignalRClient  signalRClient =   _memoryCache.Get<SignalRClient>($"{Constant._KeySignalRClientConnects}:{connid}");
+            if (signalRClient!=null)
+            {
+                _memoryCache.Remove($"{Constant._KeySignalRClientConnects}:{connid}");
+                _memoryCache.Remove($"{Constant._KeySignalRClientClients}:{signalRClient.clientid}");
+                await Groups.RemoveFromGroupAsync(connid, signalRClient.grant_type!);
+            }
+        }
     }
-    public enum MessageType
+    public interface IClient
     {
-        conn_success,//连接成功
-        conn_error,// 连接失败
-        task_send_success,// 任务发送成功
-        task_send_error,// 任务发送失败
-        task_execute_success,// 任务执行成功
-        task_execute_error,// 任务执行失败
+        Task ReceiveMessage(MessageBody message);
+        Task ReceiveConnection(MessageBody message);
+        Task ReceiveDisConnection(MessageBody message);
     }
+  
+    
 }

+ 29 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Filters/AspNetCoreBuilderServiceCollectionExtensions.cs

@@ -0,0 +1,29 @@
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.AspNetCore.Mvc;
+
+namespace IES.ExamServer.Filters
+{
+    public static    class AspNetCoreBuilderServiceCollectionExtensions
+    {
+        /// <summary>
+        /// 注册 Mvc 过滤器
+        /// </summary>
+        /// <typeparam name="TFilter"></typeparam>
+        /// <param name="services"></param>
+        /// <param name="configure"></param>
+        /// <returns></returns>
+        public static IServiceCollection AddMvcFilter<TFilter>(this IServiceCollection services, Action<MvcOptions> configure = default)
+            where TFilter : IFilterMetadata
+        {
+            services.Configure<MvcOptions>(options =>
+            {
+                options.Filters.Add<TFilter>();
+
+                // 其他额外配置
+                configure?.Invoke(options);
+            });
+
+            return services;
+        }
+    }
+}

+ 114 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Filters/AuthTokenAttribute.cs

@@ -0,0 +1,114 @@
+using IES.ExamLib.Models;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.Extensions.Primitives;
+using System.IdentityModel.Tokens.Jwt;
+using System.Security;
+using ZXing.QrCode.Internal;
+using static System.Formats.Asn1.AsnWriter;
+
+namespace IES.ExamServer.Filters
+{
+    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
+    public class AuthTokenAttribute : Attribute 
+    {
+        public string[]? Roles { get; set; }
+        public AuthTokenAttribute(params string[] roles)
+        {
+            Roles = roles;
+        }
+    }
+    public class AuthTokenActionFilter : IActionFilter
+    {
+        public     void OnActionExecuting(ActionExecutingContext context)
+        {
+            var authtoken = context.HttpContext.GetXAuth("AuthToken");
+            var TimeZone = 8;
+            var authTokenAttribute = context.ActionDescriptor.EndpointMetadata.OfType<AuthTokenAttribute>().FirstOrDefault();
+            bool needParse = false;
+            if (authTokenAttribute!=null)
+            {
+
+                if (string.IsNullOrWhiteSpace(authtoken) ||  !JwtAuthExtension.ValidateAuthToken(authtoken, ExamConstant.JwtSecretKey))
+                {
+                    context.Result = new Microsoft.AspNetCore.Mvc.UnauthorizedResult();
+                }
+                else {
+                    needParse=true;
+                }
+            }
+            else { needParse=true; }
+            if (needParse)
+            {
+                if (!string.IsNullOrWhiteSpace(authtoken) && JwtAuthExtension.ValidateAuthToken(authtoken, ExamConstant.JwtSecretKey))
+                {
+                    //string msg = "";
+                    //int code = 0;
+
+                    string? id = string.Empty, name = string.Empty, picture = string.Empty, school = string.Empty, scope = string.Empty, timzone = string.Empty;
+                    if (context.HttpContext.Request.Headers.TryGetValue("Time-Zone", out StringValues value)  && int.TryParse($"{value}", out int tz))
+                    {
+                        TimeZone=tz;
+                    }
+                    var jwt = new JwtSecurityTokenHandler().ReadJwtToken(authtoken);
+                    id = jwt.Payload.Sub;
+                    //school = jwt.Payload.Azp;
+                    name = jwt.Claims.FirstOrDefault(claim => claim.Type.Equals("name"))?.Value;
+                    picture = jwt.Claims.FirstOrDefault(claim => claim.Type.Equals("picture"))?.Value;
+                    scope = jwt.Claims.FirstOrDefault(claim => claim.Type.Equals("scope"))?.Value;
+                    timzone = jwt.Claims.FirstOrDefault(claim => claim.Type.Equals("timzone"))?.Value;
+                    bool pass = false;
+                    List<string>? _roles = jwt.Claims.Where(c => c.Type.Equals("roles")).Select(x => x.Value)?.ToList();
+
+
+                    if (authTokenAttribute!=null)
+                    {
+                        if (_roles!=null && authTokenAttribute.Roles!=null&& authTokenAttribute.Roles.Intersect(_roles).Any())
+                        {
+                            pass = true;
+                        }
+                    }
+                    else
+                    {
+                        pass=true;
+                    }
+                    if (pass)
+                    {
+                        //未标记
+                        context.HttpContext.Items.Add("ID", id);
+                        context.HttpContext.Items.Add("Name", name);
+                        context.HttpContext.Items.Add("Picture", picture);
+                        //context.HttpContext.Items.Add("School", school);
+                        context.HttpContext.Items.Add("Roles", _roles);
+                        context.HttpContext.Items.Add("Scope", scope);
+                        context.HttpContext.Items.Add("TimeZone", TimeZone);
+                    }
+                    else
+                    {
+                        context.Result = new Microsoft.AspNetCore.Mvc.UnauthorizedResult();
+                    }
+                }
+                else 
+                {
+                    var _roles = new List<string> { "visitor" }; // 默认角色,访客角色
+                    context.HttpContext.Items.Add("Roles", _roles);
+                    context.HttpContext.Items.Add("TimeZone", TimeZone);
+                    context.HttpContext.Items.Add("ID", $"{DateTimeOffset.Now.ToUnixTimeSeconds()}");
+                    context.HttpContext.Items.Add("Name", $"访客{Random.Shared.Next(100,999)}");
+                    context.HttpContext.Items.Add("Picture", null);
+                    context.HttpContext.Items.Add("Scope", "visitor");
+                }
+            }
+            else 
+            { 
+                context.Result = new Microsoft.AspNetCore.Mvc.UnauthorizedResult();
+            }
+             
+        }
+
+        public void OnActionExecuted(ActionExecutedContext context)
+        {
+            // 在 Action 执行后不需要做任何处理
+        }
+    }
+}

+ 2 - 2
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/CollectionHelper.cs

@@ -7,7 +7,7 @@
         /// </summary>
         /// <param name="collection"></param>
         /// <returns></returns>
-        public static bool IsEmpty<T>(this IEnumerable<T> collection)
+        public static bool IsEmpty<T>(this IEnumerable<T>? collection)
         {
             if (collection != null && collection.Any())
             {
@@ -23,7 +23,7 @@
         /// </summary>
         /// <param name="collection"></param>
         /// <returns></returns>
-        public static bool IsNotEmpty<T>(this IEnumerable<T> collection)
+        public static bool IsNotEmpty<T>(this IEnumerable<T>? collection)
         {
             if (collection != null && collection.Any())
             {

+ 16 - 2
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/Constant.cs

@@ -8,7 +8,21 @@ namespace IES.ExamServer.Helper
 {
     public static class Constant
     {
-        public static string _KeyServerCenter = "Server:Center:Data";
-        public static string _KeyServerDevice = "Server:Device:Info";
+        public static readonly string _KeyServerCenter = "Server:Center:Data";
+        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 _X_Auth_AuthToken = "X-Auth-AuthToken";
+        public static readonly string _Message_grant_type_check_file = "check_file";
+        public static readonly string _Message_type_message = "message";
+        public static readonly string _Message_type_check = "check";
+        public static readonly string _Message_type_download = "download";
+        public static readonly string _Message_type_upload = "upload";
+
+        public static readonly int _Message_status_error = -1;
+        public static readonly int _Message_status_info = 0;
+        public static readonly int _Message_status_success = 1;
+        public static readonly int _Message_status_warning = 2;
     }
 }

+ 68 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/ExpressionHelper.cs

@@ -0,0 +1,68 @@
+using System.Linq.Expressions;
+
+namespace IES.ExamServer
+{
+    public static class ExpressionHelper
+    {
+       
+
+        public static Expression<Func<T, bool>> And<T>(  this Expression<Func<T, bool>> first,    Expression<Func<T, bool>> second)
+        {
+            // 创建一个新的参数表达式
+            var parameter = Expression.Parameter(typeof(T), "x");
+
+            // 替换第一个表达式中的参数
+            var leftVisitor = new ReplaceParameterVisitor(first.Parameters[0], parameter);
+            var left = leftVisitor.Visit(first.Body);
+
+            // 替换第二个表达式中的参数
+            var rightVisitor = new ReplaceParameterVisitor(second.Parameters[0], parameter);
+            var right = rightVisitor.Visit(second.Body);
+
+            // 组合两个表达式
+            var combined = Expression.AndAlso(left, right);
+
+            // 创建新的 lambda 表达式
+            return Expression.Lambda<Func<T, bool>>(combined, parameter);
+        }
+        public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
+        {
+            // 创建一个新的参数表达式
+            var parameter = Expression.Parameter(typeof(T), "x");
+
+            // 替换第一个表达式中的参数
+            var leftVisitor = new ReplaceParameterVisitor(first.Parameters[0], parameter);
+            var left = leftVisitor.Visit(first.Body);
+
+            // 替换第二个表达式中的参数
+            var rightVisitor = new ReplaceParameterVisitor(second.Parameters[0], parameter);
+            var right = rightVisitor.Visit(second.Body);
+
+            // 组合两个表达式
+            var combined = Expression.OrElse(left, right);
+
+            // 创建新的 lambda 表达式
+            return Expression.Lambda<Func<T, bool>>(combined, parameter);
+        }
+
+        private class ReplaceParameterVisitor : ExpressionVisitor
+        {
+            private readonly ParameterExpression _oldParameter;
+            private readonly ParameterExpression _newParameter;
+
+            public ReplaceParameterVisitor(ParameterExpression oldParameter, ParameterExpression newParameter)
+            {
+                _oldParameter = oldParameter;
+                _newParameter = newParameter;
+            }
+
+            protected override Expression VisitParameter(ParameterExpression node)
+            {
+                // 如果遇到旧的参数,替换为新的参数
+                if (node == _oldParameter)
+                    return _newParameter;
+                return base.VisitParameter(node);
+            }
+        }
+    }
+}

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

@@ -0,0 +1,37 @@
+namespace IES.ExamServer.Helpers
+{
+    public class FileHelper
+    {
+        /// <summary>
+        /// 列出文件夹下的所有子文件及子文件夹的子文件
+        /// </summary>
+        /// <param name="directoryPath"></param>
+        /// <param name="filter">获取指定匹配模式的文件,如后缀, local.json</param>
+        /// <returns></returns>
+        public static List<string> ListAllFiles(string directoryPath, string filter = null)
+        {
+            List<string> filePaths = new List<string>();
+            DirectoryInfo dirInfo = new DirectoryInfo(directoryPath);
+
+            // 获取目录下的所有文件(包括子目录中的文件)
+            FileInfo[] files = dirInfo.GetFiles("*", SearchOption.AllDirectories);
+
+            foreach (FileInfo file in files)
+            {
+                if (string.IsNullOrWhiteSpace(filter))
+                {
+                    filePaths.Add(file.FullName);
+                    continue;
+                }
+                else
+                {
+                    if (file.FullName.Contains(filter))
+                    {
+                        filePaths.Add(file.FullName);
+                    }
+                }
+            }
+            return filePaths;
+        }
+    }
+}

+ 320 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/HttpContextExtensions.cs

@@ -0,0 +1,320 @@
+using Microsoft.AspNetCore.Mvc.Controllers;
+using Microsoft.Extensions.Primitives;
+using System.Text;
+
+namespace IES.ExamServer
+{
+    public static class HttpContextExtensions
+    {
+        /// <summary>
+        /// 取得驗證金鑰,Authorization
+        /// </summary>        
+        public static string GetToken(this HttpContext httpContext)
+        {
+            return httpContext.Request.Headers["Authorization"].ToString();
+        }
+        /// <summary>
+        /// 获取 Action 特性
+        /// </summary>
+        /// <typeparam name="TAttribute"></typeparam>
+        /// <param name="httpContext"></param>
+        /// <returns></returns>
+        public static TAttribute GetMetadata<TAttribute>(this HttpContext httpContext)
+            where TAttribute : class
+        {
+            return httpContext.GetEndpoint()?.Metadata?.GetMetadata<TAttribute>();
+        }
+        /// <summary>
+        /// 获取 控制器/Action 描述器
+        /// </summary>
+        /// <param name="httpContext"></param>
+        /// <returns></returns>
+        public static ControllerActionDescriptor GetControllerActionDescriptor(this HttpContext httpContext)
+        {
+            return httpContext.GetEndpoint()?.Metadata?.FirstOrDefault(u => u is ControllerActionDescriptor) as ControllerActionDescriptor;
+        }
+        /// <summary>
+        /// 获取本机 IPv4地址
+        /// </summary>
+        /// <param name="context"></param>
+        /// <returns></returns>
+        public static string GetLocalIpAddressToIPv4(this HttpContext context)
+        {
+            return context.Connection.LocalIpAddress?.MapToIPv4()?.ToString();
+        }
+
+        /// <summary>
+        /// 获取本机 IPv6地址
+        /// </summary>
+        /// <param name="context"></param>
+        /// <returns></returns>
+        public static string GetLocalIpAddressToIPv6(this HttpContext context)
+        {
+            return context.Connection.LocalIpAddress?.MapToIPv6()?.ToString();
+        }
+
+        /// <summary>
+        /// 获取远程 IPv4地址
+        /// </summary>
+        /// <param name="context"></param>
+        /// <returns></returns>
+        public static string GetRemoteIpAddressToIPv4(this HttpContext context)
+        {
+            return context.Connection.RemoteIpAddress?.MapToIPv4()?.ToString();
+        }
+
+        /// <summary>
+        /// 获取远程 IPv6地址
+        /// </summary>
+        /// <param name="context"></param>
+        /// <returns></returns>
+        public static string GetRemoteIpAddressToIPv6(this HttpContext context)
+        {
+            return context.Connection.RemoteIpAddress?.MapToIPv6()?.ToString();
+        }
+
+        /// <summary>
+        /// 获取完整请求地址
+        /// </summary>
+        /// <param name="request"></param>
+        /// <returns></returns>
+        public static string GetRequestUrlAddress(this HttpRequest request)
+        {
+            return new StringBuilder()
+                    .Append(request.Scheme)
+                    .Append("://")
+                    .Append(request.Host)
+                    .Append(request.PathBase)
+                    .Append(request.Path)
+                    .Append(request.QueryString)
+                    .ToString();
+        }
+
+        /// <summary>
+        /// 获取来源地址
+        /// </summary>
+        /// <param name="request"></param>
+        /// <param name="refererHeaderKey"></param>
+        /// <returns></returns>
+        public static string GetRefererUrlAddress(this HttpRequest request, string refererHeaderKey = "Referer")
+        {
+            return request.Headers[refererHeaderKey].ToString();
+        }
+
+        /// <summary>
+        /// 读取 Body 内容
+        /// </summary>
+        /// <param name="httpContext"></param>
+        /// <remarks>需先在 Startup 的 Configure 中注册 app.EnableBuffering()</remarks>
+        /// <returns></returns>
+        public static async Task<string> ReadBodyContentAsync(this HttpContext httpContext)
+        {
+            if (httpContext == null) return default;
+            return await httpContext.Request.ReadBodyContentAsync();
+        }
+
+        /// <summary>
+        /// 读取 Body 内容
+        /// </summary>
+        /// <param name="request"></param>
+        /// <remarks>需先在 Startup 的 Configure 中注册 app.EnableBuffering()</remarks>
+        /// <returns></returns>
+        public static async Task<string> ReadBodyContentAsync(this HttpRequest request)
+        {
+            request.Body.Seek(0, SeekOrigin.Begin);
+
+            using var reader = new StreamReader(request.Body, Encoding.UTF8, true, 1024, true);
+            var body = await reader.ReadToEndAsync();
+
+            // 回到顶部,解决此类问题 https://gitee.com/dotnetchina/Furion/issues/I6NX9E
+            request.Body.Seek(0, SeekOrigin.Begin);
+            return body;
+        }
+
+
+        /// <summary>
+        /// 判断是否是 WebSocket 请求
+        /// </summary>
+        /// <param name="context"></param>
+        /// <returns></returns>
+        public static bool IsWebSocketRequest(this HttpContext context)
+        {
+            return context.WebSockets.IsWebSocketRequest || context.Request.Path == "/ws";
+        }
+        /// <summary>
+        /// 设置响应头 Tokens
+        /// </summary>
+        /// <param name="httpContext"></param>
+        /// <param name="accessToken"></param>
+        /// <param name="refreshToken"></param>
+        public static void SetTokensOfResponseHeaders(this HttpContext httpContext, string accessToken, string refreshToken = null)
+        {
+            httpContext.Response.Headers["access-token"] = accessToken;
+            if (!string.IsNullOrWhiteSpace(refreshToken))
+            {
+                httpContext.Response.Headers["x-access-token"] = refreshToken;
+            }
+        }
+        /// <summary>
+        /// 取得JWT驗證金鑰,Authorization Bearer
+        /// </summary>
+        /// <param name="httpContext"></param>
+        /// <returns></returns>
+        public static string GetJwtToken(this HttpContext httpContext)
+        {
+            var token = string.Empty;
+            string authorization = httpContext.Request.Headers["Authorization"].ToString();
+            if (!string.IsNullOrWhiteSpace(authorization) && authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
+            {
+                token = authorization.Substring("Bearer ".Length).Trim();
+            }
+            return token;
+        }
+
+        /// <summary>
+        /// 取得遠端呼叫的IP
+        /// </summary>        
+        public static string GetRemoteIP(this HttpContext httpContext)
+        {
+            return httpContext?.Connection?.RemoteIpAddress?.ToString();
+        }
+
+        /// <summary>
+        /// 取得X-Auth-Key值
+        /// </summary>        
+        /// <param name="key">Key Name</param>
+        /// <returns></returns>
+        public static string GetXAuth(this HttpContext httpContext, string key = null)
+        {
+            try
+            {
+                if (httpContext.Request.Headers.TryGetValue($"X-Auth-{key}", out StringValues value))
+                    return value.ToString();
+                else
+                    return null;
+            }
+            catch
+            {
+                return null;
+            }
+        }
+        /// <summary>
+        /// 取得X-Auth-Key值
+        /// </summary>        
+        /// <param name="key">Key Name</param>
+        /// <returns></returns>
+        public static string GetAuthorization(this HttpContext httpContext)
+        {
+            try
+            {
+                if (httpContext.Request.Headers.TryGetValue("Authorization", out StringValues value))
+                    return value.ToString();
+                else
+                    return null;
+            }
+            catch
+            {
+                return null;
+            }
+        }
+        /// <summary>
+        /// 取得AuthToken權杖資訊
+        /// </summary>        
+        /// <param name="key">Key Name</param>
+        /// <returns></returns>
+        public static (string id, string school) GetApiTokenInfo(this HttpContext httpContext, string key = null)
+        {
+            object id = null, school = null;
+            httpContext?.Items.TryGetValue("ID", out id);
+            httpContext?.Items.TryGetValue("School", out school);
+            return (id?.ToString(), school?.ToString());
+        }
+        /// <summary>
+        /// 取得AuthToken權杖資訊
+        /// </summary>        
+        /// <param name="key">Key Name</param>
+        /// <returns></returns>
+        public static (string id, string name, string picture, string school) GetAuthTokenInfo(this HttpContext httpContext, string key = null)
+        {
+            object id = null, name = null, picture = null, school = null;
+            httpContext?.Items.TryGetValue("ID", out id);
+            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>
+        /// 取得AuthToken權杖資訊
+        /// </summary>        
+        /// <param name="key">Key Name</param>
+        /// <returns></returns>
+        public static (string id, string name, string picture, string school, string area, string keyData) GetAuthTokenKey(this HttpContext httpContext, string key = null)
+        {
+            object id = null, name = null, picture = null, school = null, area = null, keyData = null;
+            httpContext?.Items.TryGetValue("ID", out id);
+            httpContext?.Items.TryGetValue("Name", out name);
+            httpContext?.Items.TryGetValue("Picture", out picture);
+            httpContext?.Items.TryGetValue("School", out school);
+            httpContext?.Items.TryGetValue("Area", out area);
+            if (!string.IsNullOrWhiteSpace(key))
+            {
+                httpContext?.Items.TryGetValue(key, out keyData);
+            }
+            return (id?.ToString(), name?.ToString(), picture?.ToString(), school?.ToString(), area?.ToString(), keyData?.ToString());
+        }
+        /// <summary>
+        /// 取得User-Agent值
+        /// </summary>       
+        public static string GetUserAgent(this HttpContext httpContext)
+        {
+            try
+            {
+                return httpContext.Request.Headers["User-Agent"].ToString();
+            }
+            catch
+            {
+                return null;
+            }
+        }
+
+        /// <summary>
+        /// 取得Scheme值
+        /// </summary>      
+        public static string GetScheme(this HttpContext httpContext)
+        {
+            return httpContext?.Request?.Scheme;
+        }
+
+        /// <summary>
+        /// 取得HostName值
+        /// </summary>        
+        public static string GetHostName(this HttpContext httpContext)
+        {
+            return httpContext?.Request?.Host.ToString();
+        }
+
+        /// <summary>
+        /// 设置本地cookie
+        /// </summary>
+        /// <param name="key">键</param>
+        /// <param name="value">值</param>  
+        /// <param name="minutes">过期时长,单位:分钟</param>      
+        public static void SetCookies(HttpResponse Response, string key, string value, int minutes = 30)
+        {
+            Response.Cookies.Append(key, value, new CookieOptions
+            {
+                Expires = DateTimeOffset.Now.AddMinutes(minutes)
+            });
+        }
+        /// <summary>
+        /// 删除指定的cookie
+        /// </summary>
+        /// <param name="key">键</param>
+        public static void DeleteCookies(HttpContext httpContext, string key)
+        {
+            httpContext.Response.Cookies.Delete(key);
+        }
+    }
+
+}

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

@@ -0,0 +1,67 @@
+using Microsoft.IdentityModel.Tokens;
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using System.Text;
+
+namespace IES.ExamServer
+{
+    public class JwtAuthExtension
+    {
+        public static bool ValidateAuthToken(string token, string salt)
+        {
+            try
+            {
+                var handler = new JwtSecurityTokenHandler();
+                var validationParameters = new TokenValidationParameters
+                {
+                    RequireExpirationTime = false,
+                    ValidateIssuer = false,
+                    ValidateAudience = false,
+                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(salt)),
+                    ValidateLifetime = false,
+                    //LifetimeValidator = LifetimeValidator,
+                    ClockSkew = TimeSpan.Zero
+                };
+                ClaimsPrincipal principal = handler.ValidateToken(token, validationParameters, out SecurityToken securityToken);
+                return true;
+            }
+            catch (Exception)
+            {
+                //Trace.WriteLine(ex.Message);
+                return false;
+            }
+        }
+
+
+        public static string CreateAuthToken(string issuer, string id, string name, string picture, string salt, string scope,   int timezone,   string schoolID =null,string[] roles = null,   int expire = 1, int year = -1)
+        {
+            // 設定要加入到 JWT Token 中的聲明資訊(Claims)  
+            var payload = new JwtPayload {
+                { JwtRegisteredClaimNames.Iss, issuer }, //發行者
+                { JwtRegisteredClaimNames.Azp,schoolID}, // 學校簡碼,如果有的話
+                { JwtRegisteredClaimNames.Sub, id }, // 用戶ID                  
+                { JwtRegisteredClaimNames.Exp,DateTimeOffset.UtcNow.AddHours(expire).ToUnixTimeSeconds()},  // 到期的時間,必須為數字
+                { "name",name}, // 用戶的顯示名稱
+                { "picture",picture}, // 用戶頭像
+                { "roles",roles}, // 登入者的角色,角色類型 (Admin、Teacher、Student) 
+                { "scope",scope},  //登入者的入口类型。 (teacher 教师端登录的醍摩豆ID、tmduser学生端登录的醍摩豆ID、student学生端登录校内账号的学生ID)
+                { "timezone",timezone},
+               
+            };
+            // 建立一組對稱式加密的金鑰,主要用於 JWT 簽章之用
+            var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(salt));
+            // HmacSha256 有要求必須要大於 128 bits,所以 salt 不能太短,至少要 16 字元以上
+            // https://stackoverflow.com/questions/47279947/idx10603-the-algorithm-hs256-requires-the-securitykey-keysize-to-be-greater
+            //var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);
+            var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
+            var header = new JwtHeader(signingCredentials);
+            var secToken = new JwtSecurityToken(header, payload);
+            // 產出所需要的 JWT securityToken 物件,並取得序列化後的 Token 結果(字串格式)
+            var tokenHandler = new JwtSecurityTokenHandler();
+            //var securityToken = tokenHandler.CreateToken(tokenDescriptor);
+            var serializeToken = tokenHandler.WriteToken(secToken);
+
+            return serializeToken;
+        }
+    }
+}

+ 51 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/ProcessHelper.cs

@@ -0,0 +1,51 @@
+using System.Diagnostics;
+using System.Text.Json;
+
+namespace IES.ExamServer.Helpers
+{
+    public class ProcessHelper
+    {
+        /// <summary>
+        ///  获取所有名为 "conhost.exe" 的进程
+        /// </summary>
+        public static void CloseConhost() 
+        {
+            Process[] conhostProcesses = Process.GetProcessesByName("conhost");
+             
+            var CurrentProcess = Process.GetCurrentProcess();
+            var stime = CurrentProcess.StartTime;
+            
+            if (conhostProcesses.Length == 0)
+            {
+                //Console.WriteLine("没有找到 conhost.exe 进程。");
+                return;
+            }
+
+            // 遍历并关闭每个 conhost.exe 进程
+            foreach (Process process in conhostProcesses)
+            {
+                TimeSpan difference = stime- process.StartTime ;
+
+                if (CurrentProcess.ProcessName.Equals(process.ProcessName)) 
+                {
+                    if (difference.Seconds>10)
+                    {
+                        continue;
+                    }
+                    try
+                    {
+                        // 关闭进程
+                        process.Kill();
+                        //Console.WriteLine($"已关闭进程 ID: {process.Id},{process.MachineName},{process.ProcessName}");
+                    }
+                    catch (Exception ex)
+                    {
+                        //Console.WriteLine($"无法关闭进程 ID: {process.Id}, 错误: {ex.Message}");
+                    }
+                }
+            }
+
+            //Console.WriteLine("操作完成。");
+        }
+    }
+}

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

@@ -16,7 +16,8 @@
 	</ItemGroup>
 	
 	<ItemGroup>
-		<Folder Include="wwwroot\" />
+		<Folder Include="Logs\DataLogs\" />
+		<Folder Include="wwwroot\package\" />
 	</ItemGroup>
 	
 	<ItemGroup>
@@ -31,6 +32,7 @@
 	  </None>
 	</ItemGroup>
 	<ItemGroup>
+		<PackageReference Include="AutoMapper" Version="13.0.1" />
 		<PackageReference Include="LiteDB" Version="5.0.21" />
 		<PackageReference Include="SkiaSharp.QrCode" Version="0.7.0" />
 		<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
@@ -38,12 +40,21 @@
 		<PackageReference Include="VueCliMiddleware" Version="6.0.0" />
 		<PackageReference Include="ZXing.Net.Bindings.ZKWeb.System.Drawing" Version="0.16.7" />
 	</ItemGroup>
+	<ItemGroup>
+	  <ProjectReference Include="..\..\IES.ExamLib\IES.ExamLib.csproj" />
+	</ItemGroup>
 
 	<ItemGroup>
 	  <Content Update="wwwroot">
 	    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
 	  </Content>
 	</ItemGroup>
+
+	<ItemGroup>
+	  <None Update="Configs\cert.pfx">
+	    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+	  </None>
+	</ItemGroup>
 	<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build">
 		<!-- Build Target:  Ensure Node.js is installed -->
 		<Exec Command="node --version" ContinueOnError="true">

+ 109 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Models/SignalRClient.cs

@@ -0,0 +1,109 @@
+namespace IES.ExamServer.Models
+{
+    public abstract class MessageBody
+    {
+        public MessageBody()
+        {
+            time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+        }
+        /// <summary>
+        /// 连接id
+        /// </summary>
+        public virtual string? connid { get; set; }
+        /// <summary>
+        /// 客户端id
+        /// </summary>
+        public virtual string? clientid { get; set; }
+        /// <summary>
+        ///-1 error(红),0 info(黑、白),1 success(绿),2 warning(黄)
+        /// </summary>
+        public int status { get; set; }
+        /// <summary>
+        /// 内容
+        /// </summary>
+        public string? content { get; set; }
+        /// <summary>
+        /// 批量消息
+        /// </summary>
+        public List<MessageContent> contents { get; set; } = new List<MessageContent>();    
+        /// <summary>
+        /// 消息创建时间
+        /// </summary>
+        public virtual long time { get; set; }
+        /// <summary>
+        /// 授权类型,bookjs_api 
+        /// </summary>
+        public virtual string? grant_type { get; set; }
+        /// <summary>
+        /// 类型message 消息,check检查,download下载,upload上传数据
+        /// </summary>
+        public string? type { get; set; }
+
+    }
+    public class ConnectionMessageBody : MessageBody
+    {
+
+    }
+    public class DisConnectionMessageBody : MessageBody
+    {
+
+    }
+    public class DownloadUplodaFileMessageBody : MessageBody
+    {
+
+        /// <summary>
+        /// 文件大小
+        /// </summary>
+        public long size { get; set; }
+        /// <summary>
+        /// 消耗时间
+        /// </summary>
+        public long cost { get; set; }
+    }
+
+    public class CheckFileMessageBody : MessageBody
+    {
+       
+       
+    }
+    public class MessageContent
+    {
+        /// <summary>
+        ///-1 error(红),0 info(黑、白),1 success(绿),2 warning(黄)
+        /// </summary>
+        public int status { get; set; }
+        /// <summary>
+        /// 内容
+        /// </summary>
+        public string? content { get; set; }
+        /// <summary>
+        /// 消耗时间
+        /// </summary>
+        public long cost { get; set; }
+        /// <summary>
+        /// 消息类型
+        /// </summary>
+        public string? messageType {  get; set; }
+        /// <summary>
+        /// 批量信息
+        /// </summary>
+        public List<MessageContent> contents { get; set; } = new List<MessageContent>();
+    }
+ 
+    public class SignalRClient
+    {
+        /// <summary>
+        /// 授权类型,bookjs_api 
+        /// </summary>
+        public string? grant_type { get; set; }
+        /// <summary>
+        /// 客户端id
+        /// </summary>
+        public string? clientid { get; set; }
+        /// <summary>
+        /// SignalR的连接ID 不建议暴露。
+        /// </summary>
+        public string? connid { get; set; }
+        public string? serverid { get; set; }
+    }
+}

+ 4 - 11
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Models/Teacher.cs

@@ -12,8 +12,9 @@ namespace IES.ExamServer.Models
         public string? name { get; set; }
         public string? picture {  get; set; }
         public string? x_auth_token {  get; set; }
-        public string? access_token { get; set; }
-        public List<School> schools { get; set; }= new List<School>();
+        public List<School>? schools { get; set; }= new List<School>();
+        public TmdidImplicit? implicit_token { get; set; }
+        public long loginTime { get; set; }
     }
 
     public class School
@@ -31,13 +32,5 @@ namespace IES.ExamServer.Models
         public string? token_type { get; set; }
     }
 
-    public class LoginTeacher 
-    {
-        public string? id { get; set; }
-        public string? name { get; set; }
-        public string? picture { get; set; }
-        public string? x_auth_token { get;set; }
-        public TmdidImplicit? implicit_token { get; set; }
-        public List<School>? schools{ get;set;}= new List<School>();
-    }
+   
 }

+ 139 - 8
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Program.cs

@@ -10,22 +10,54 @@ using System.Text.Json;
 using System.Text.Json.Nodes;
 using VueCliMiddleware;
 using IES.ExamServer.Services;
+using System.Text;
+using IES.ExamServer.Filters;
+using IES.ExamServer.Helpers;
+using Microsoft.Extensions.Hosting;
+using System.Security.Principal;
+using Microsoft.Extensions.FileProviders;
+using System.Text.Encodings.Web;
+using System.Text.Unicode;
 
 namespace IES.ExamServer
 {
-    public class Program
+    public class Program: IDisposable
     {
         public async static Task Main(string[] args)
         {
+            
+            //var mutex = new Mutex(true, "IES.ExamServer", out var createdNew);
+            //if (!createdNew)
+            //{
+            //    // 防止多开,重复启动
+            //    Console.WriteLine("The application is already running.");
+            //    return;
+            //}
+            //ProcessHelper.CloseConhost();
+            //AppDomain.CurrentDomain.ProcessExit += OnExit;
             var builder = WebApplication.CreateBuilder(args);
+
+            string path = $"{builder.Environment.ContentRootPath}/Configs";
+            //builder.WebHost.UseKestrel(options =>
+            //{
+            //    options.ListenAnyIP(5001, listenOptions =>
+            //    {
+            //        listenOptions.UseHttps($"{path}/cert.pfx", "cdhabook") ;
+            //    });
+            //});
             builder.Configuration.SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
             builder.Services.AddSpaStaticFiles(opt => opt.RootPath = "ClientApp/dist");
             // Add services to the container.
-            builder.Services.AddControllersWithViews();
+            builder.Services.AddControllersWithViews().AddJsonOptions(options =>
+            {
+                // 设置 JSON 序列化选项
+                options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All); // 允许所有 Unicode 字符
+                options.JsonSerializerOptions.WriteIndented = true; // 格式化输出(可选)
+            }); ;
             builder.Services.AddHttpClient();
             builder.Services.AddSignalR();
             builder.Services.AddHttpContextAccessor();
-
+           
             string localAppDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
             string dbpath = $"{localAppDataPath}\\ExamServer\\LiteDB";
             if (!System.IO.Directory.Exists(dbpath))
@@ -40,7 +72,8 @@ namespace IES.ExamServer
             };
             builder.Services.AddLiteDB(connections_LiteDB);
             builder.Services.AddMemoryCache();
-            string path = $"{builder.Environment.ContentRootPath}/Configs";
+            // 注册 ConnectionService 为单例
+            builder.Services.AddSingleton<DataCenterConnectionService>();
             builder.Services.AddCors(options =>
             {
                 options.AddDefaultPolicy(
@@ -52,6 +85,11 @@ namespace IES.ExamServer
                             .AllowAnyMethod();
                 });
             });
+            builder.Services.AddMvcFilter<AuthTokenActionFilter>();
+            // 添加自定义日志提供程序
+            //builder.Logging.ClearProviders();
+            //bool enableConsoleOutput = true;
+            //builder.Logging.AddProvider(new CustomFileLoggerProvider(Path.Combine(Directory.GetCurrentDirectory(), "Logs"), enableConsoleOutput));
             var app = builder.Build();
 
             // Configure the HTTP request pipeline.
@@ -66,9 +104,27 @@ namespace IES.ExamServer
             app.UseHttpsRedirection();
             app.UseDefaultFiles();
             var contentTypeProvider = new FileExtensionContentTypeProvider();
-            contentTypeProvider.Mappings[".glb"] = "model/gltf-binary";
+            contentTypeProvider.Mappings[".txt"] = "text/plain";
+            contentTypeProvider.Mappings[".jpg"] = "image/jpeg";
+            contentTypeProvider.Mappings[".jpeg"] = "image/jpeg";
+            contentTypeProvider.Mappings[".png"] = "image/png";
+            contentTypeProvider.Mappings[".html"] = "text/html";
+            contentTypeProvider.Mappings[".js"] = "application/javascript";
+            contentTypeProvider.Mappings[".css"] = "text/css";
+            contentTypeProvider.Mappings[".mp4"] = "video/mp4";
+            contentTypeProvider.Mappings[".mp3"] = "audio/mpeg";
+            contentTypeProvider.Mappings[".json"] = "application/json";
+            contentTypeProvider.Mappings[".pdf"] = "application/pdf";
+            string packagePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "package");
+            if (!Directory.Exists(packagePath))
+            {
+                Directory.CreateDirectory(packagePath);
+            }
             app.UseStaticFiles(new StaticFileOptions
             {
+                FileProvider = new PhysicalFileProvider(
+                Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "package")),
+                RequestPath = "/package",
                 ContentTypeProvider = contentTypeProvider,
             });
 
@@ -79,7 +135,7 @@ namespace IES.ExamServer
             
             app.UseEndpoints(endpoints =>
             {
-                endpoints.MapHub<SignalRExamServerHub>("/signalr/screen").RequireCors("any");
+                endpoints.MapHub<SignalRExamServerHub>("/signalr/exam").RequireCors("any");
                 endpoints.MapControllers();
 
                 // NOTE: VueCliProxy is meant for developement and hot module reload
@@ -134,17 +190,92 @@ namespace IES.ExamServer
                 //云端服务连接失败
                 hybrid = 0;
             }
+            //单例模式存储云端数据中心连接状态
+            DataCenterConnectionService connectionService=  app.Services.GetRequiredService<DataCenterConnectionService>();
+            connectionService.centerUrl = hybrid == 1 ? $"{data?["centerUrl"]}" : null;
+            connectionService.dataCenterIsConnected = hybrid==1 ? true : false;
             var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
             lifetime.ApplicationStarted.Register(() =>
             {
-
+               
                 var server = app.Services.GetService<IServer>();
+                var logger = app.Services.GetRequiredService<ILogger<Program>>();
                 var d = server?.Features.Get<IServerAddressesFeature>();
                 IEnumerable<string>? _url = server?.Features.Get<IServerAddressesFeature>()?.Addresses;
                 ServerDevice serverDevice = IndexService.GetServerDevice(remote, region, _url);
+                //int domainStatus =0;
+                //string domain = builder.Configuration.GetValue<string>("ExamClient:Domain");
+                //foreach (var network in serverDevice.networks) 
+                //{
+                //    try
+                //    {
+                //        string domain_entry = $"{network.ip}    {domain}";
+                //        string hostsFilePath = @"C:\Windows\System32\drivers\etc\hosts";
+                //        string content = File.ReadAllText(hostsFilePath, Encoding.UTF8);
+                //        if (!content.Contains(domain_entry))
+                //        {
+                //            content += Environment.NewLine + domain_entry;
+                //            // 使用管理员权限运行此程序,不然会抛出UnauthorizedAccessException
+                //            File.WriteAllText(hostsFilePath, content, Encoding.UTF8);
+                //            domainStatus=1;
+                //            // Console.WriteLine("Hosts file updated successfully.");
+                //        }
+                //        else
+                //        {
+                //            domainStatus=1;
+                //            //Console.WriteLine("The entry already exists in the hosts file.");
+                //        }
+                //    }
+                //    catch (UnauthorizedAccessException)
+                //    {
+                //        domainStatus=2;
+                //        // Console.WriteLine("You need to run this program with administrative privileges to modify the hosts file.");
+                //    }
+                //    catch (Exception ex)
+                //    {
+                //        domainStatus=0;
+                //        // Console.WriteLine($"An error occurred: {ex.Message}");
+                //    }
+                //}
+                //serverDevice.domainStatus=domainStatus;
+                //serverDevice.domain=domain;
+                logger.LogInformation($"服务端设备信息:{JsonSerializer.Serialize(serverDevice,options: new JsonSerializerOptions { Encoder =JavaScriptEncoder.Create(UnicodeRanges.All)})}");
                 cache.Set(Constant._KeyServerDevice, serverDevice);
             });
-            app.Run();
+
+            // 退出程序
+            lifetime.ApplicationStopping.Register(() =>
+            {
+                Console.WriteLine("The application is stopping. Performing cleanup...");
+                // 在这里添加清理资源、保存数据等逻辑
+            });
+            app.MapGet("/hello",   (ILogger<Program> logger) =>
+            {
+                logger.LogInformation("This is an information log.");
+                logger.LogError("This is an error log.");
+
+                var data = new { Id = 123, Name = "Test Data服务端设备信息" };
+                  logger.LogData(data, data.Id.ToString());
+
+                return "Hello World!";
+            });
+            await app.RunAsync();
+        }
+        //static void OnExit(object sender, EventArgs e)
+        //{
+        //    Console.WriteLine("正在退出程序...");
+
+        //    // 执行任何需要的清理工作
+        //    // 例如: 保存状态,关闭文件和数据库连接等
+
+        //    // 通过调用 Environment.Exit 来结束进程
+        //    Environment.Exit(0);
+        //}
+        public void Dispose()
+        {
+            // 清理代码
+            //Console.WriteLine("正在退出...");
+            // 释放资源、关闭连接等
         }
     }
     public class SystemInfo

+ 163 - 31
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Services/IndexService.cs

@@ -18,8 +18,9 @@ namespace IES.ExamServer.Services
             string hostName = $"{Environment.UserName}-{Dns.GetHostName()}";
             string os = RuntimeInformation.OSDescription;
             //获取当前客户端的服务端口
-          
-            ServerDevice device = new ServerDevice { name =hostName, os= os,region=region,remote=remote };
+            string currentUserName = Environment.UserName;
+           
+            ServerDevice device = new ServerDevice { name =hostName, userName=currentUserName, os= os,region=region,remote=remote };
             int CpuCoreCount = 0;
             long MenemorySize = 0;
             if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
@@ -50,13 +51,50 @@ namespace IES.ExamServer.Services
                         }
                     }
                 }
+                if (Environment.Is64BitOperatingSystem)
+                {
+                    device.bit="64";
+                }
+                else
+                {
+                    device.bit="32";
+                }
+                ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT Name, MaxClockSpeed FROM Win32_Processor");
+                foreach (ManagementObject mo in searcher.Get())
+                {
+                    string? cpuName = mo["Name"].ToString();
+                    string? clockSpeed = mo["MaxClockSpeed"].ToString();
+                    //Console.WriteLine($"CPU 名称: {cpuName}");
+                    //Console.WriteLine($"CPU 主频: {clockSpeed} MHz");
+                    device.cpuInfos.Add(new CPUInfo { name = cpuName, hz = clockSpeed });
+                }
             }
             else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
             {
                 //int processorCount = Environment.ProcessorCount;
                 // Console.WriteLine("CPU 核心数: " + processorCount);
-                string[] cpu_lines = File.ReadAllLines("/proc/cpuinfo");
-                CpuCoreCount= cpu_lines.Count(line => line.StartsWith("processor", StringComparison.OrdinalIgnoreCase));
+                try {
+                    string cpuInfo = File.ReadAllText("/proc/cpuinfo");
+                    string[] cpu_lines = cpuInfo.Split('\n');
+                   
+                    CpuCoreCount= cpu_lines.Count(line => line.StartsWith("processor", StringComparison.OrdinalIgnoreCase));
+                    string? cpuNameLine = cpuInfo.Split('\n').FirstOrDefault(line => line.StartsWith("model name"));
+                    string? clockSpeedLine = cpuInfo.Split('\n').FirstOrDefault(line => line.StartsWith("cpu MHz"));
+                    string cpuName = string.Empty;
+                    string clockSpeed = string.Empty;
+                    if (cpuNameLine!= null)
+                    {
+                          cpuName = cpuNameLine.Split(':').Last().Trim();
+                    }
+                    if (clockSpeedLine!= null)
+                    {
+                         clockSpeed = clockSpeedLine.Split(':').Last().Trim();
+                    }
+                    device.cpuInfos.Add(new CPUInfo { name = cpuName, hz = clockSpeed });
+                } catch (Exception ex)
+                {
+                    
+                }
                 string[] mem_lines = File.ReadAllLines("/proc/meminfo");
                 var match = mem_lines.FirstOrDefault(line => line.StartsWith("MemTotal:"));
                 if (match != null)
@@ -70,38 +108,101 @@ namespace IES.ExamServer.Services
             }
             else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
             {
-                using (var process = new Process())
+                try {
+                    using (var process = new Process())
+                    {
+                        process.StartInfo.FileName = "/usr/sbin/sysctl";
+                        process.StartInfo.Arguments = "-n hw.ncpu";
+                        process.StartInfo.RedirectStandardOutput = true;
+                        process.StartInfo.UseShellExecute = false;
+                        process.Start();
+                        string output = process.StandardOutput.ReadToEnd().Trim();
+                        int coreCount;
+                        if (int.TryParse(output, out coreCount))
+                        {
+                            CpuCoreCount= coreCount;
+                        }
+                    }
+                }
+                catch (Exception ex) { }
+               try
                 {
-                    process.StartInfo.FileName = "/usr/sbin/sysctl";
-                    process.StartInfo.Arguments = "-n hw.ncpu";
-                    process.StartInfo.RedirectStandardOutput = true;
-                    process.StartInfo.UseShellExecute = false;
-                    process.Start();
-                    string output = process.StandardOutput.ReadToEnd().Trim();
-                    int coreCount;
-                    if (int.TryParse(output, out coreCount))
+                    using (var process = new Process())
                     {
-                        CpuCoreCount= coreCount;
+                        process.StartInfo.FileName = "/usr/sbin/sysctl";
+                        process.StartInfo.Arguments = "-n hw.memsize";
+                        process.StartInfo.RedirectStandardOutput = true;
+                        process.StartInfo.UseShellExecute = false;
+                        process.Start();
+                        string output = process.StandardOutput.ReadToEnd().Trim();
+                        long memorySize;
+                        if (long.TryParse(output, out memorySize))
+                        {
+                            MenemorySize=  memorySize;
+                        }
                     }
                 }
-                using (var process = new Process())
+                catch (Exception ex) { }
+                try
                 {
-                    process.StartInfo.FileName = "/usr/sbin/sysctl";
-                    process.StartInfo.Arguments = "-n hw.memsize";
-                    process.StartInfo.RedirectStandardOutput = true;
-                    process.StartInfo.UseShellExecute = false;
-                    process.Start();
-                    string output = process.StandardOutput.ReadToEnd().Trim();
-                    long memorySize;
-                    if (long.TryParse(output, out memorySize))
+                    using (var process = new Process())
                     {
-                        MenemorySize=  memorySize;
+
+
+                        process.StartInfo.FileName = "/usr/sbin/sysctl";
+                        process.StartInfo.Arguments = "-n machdep.cpu.brand_string";
+                        process.StartInfo.RedirectStandardOutput = true;
+                        process.StartInfo.UseShellExecute = false;
+                        process.Start();
+                        string cpuName = process.StandardOutput.ReadToEnd().Trim();
+
+                        process.StartInfo.FileName = "/usr/sbin/sysctl";
+                        process.StartInfo.Arguments = "-n hw.cpu.frequency";
+                        process.StartInfo.RedirectStandardOutput = true;
+                        process.StartInfo.UseShellExecute = false;
+                        process.Start();
+                        string clockSpeed = process.StandardOutput.ReadToEnd().Trim();
+                        //Console.WriteLine($"CPU 名称: {cpuName}");
+                        //Console.WriteLine($"CPU 主频: {clockSpeed} Hz");
+                        device.cpuInfos.Add(new CPUInfo { name = cpuName, hz = clockSpeed });
                     }
                 }
+                catch (Exception ex)
+                {
+                    Console.WriteLine($"出现错误: {ex.Message}");
+                }
+                if (Environment.Is64BitOperatingSystem)
+                {
+                    device.bit="64";
+                }
+                else
+                {
+                    device.bit="32";
+                }
             }
+            if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
+            {
+                device.arch="ARM64";
+            }
+            else if (RuntimeInformation.ProcessArchitecture == Architecture.Arm)
+            {
+                device.arch="ARM32";
+            }
+            else if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
 
+            {
+                device.arch="X64";
+            }
+            else if (RuntimeInformation.ProcessArchitecture == Architecture.X86) 
+            {
+                device.arch="X86";
+            }
+            else
+            {
+                device.arch=$"未知({device.arch})";
+            }
             //Console.WriteLine("CPU 核心数: " + CpuCoreCount+",RAM 大小:"+MenemorySize);
-            
+
             device.cpu=CpuCoreCount;
             device.ram=MenemorySize;
             var nics = NetworkInterface.GetAllNetworkInterfaces();
@@ -129,19 +230,19 @@ namespace IES.ExamServer.Services
             }
             if (_url!.IsNotEmpty())
             {
-                List<int> ports = new List<int>();
+                List<UriInfo> ports = new List<UriInfo>();
                 foreach (var url in _url)
                 {
                     Uri uri = new Uri(url);
-                    ports.Add(uri.Port);
+                    device.uris.Add(new UriInfo { port= uri.Port, protocol= uri.Scheme });
                 }
-                device.port= string.Join(",", ports);
+                 
             }
             else
             {
                 throw new Exception("未获取到端口信息!");
             }
-            string hashData = ShaHashHelper.GetSHA1($"{device.name}-{device.remote}-{device.port}-{device.os}-{string.Join(",", device.networks.Select(x => $"{x.mac}-{x.ip}"))}");
+            string hashData = ShaHashHelper.GetSHA1($"{device.name}-{device.remote}-{string.Join(",", device.uris.Select(x => $"{x.port}-{x.protocol}"))}-{device.os}-{string.Join(",", device.networks.Select(x => $"{x.mac}-{x.ip}"))}");
             device.deviceId=hashData;
             return device;
         }
@@ -270,6 +371,13 @@ namespace IES.ExamServer.Services
             return device;
         }
     }
+
+    public class UriInfo 
+    {
+        public string? protocol { get; set; }
+        public int port { get; set; }
+       
+    }
     public class ServerDevice
     { 
         
@@ -277,6 +385,7 @@ namespace IES.ExamServer.Services
         /// 设备id
         /// </summary>
         public string? deviceId { get; set; }
+        public string? userName {  get; set; }
         /// <summary>
         /// 机器名
         /// </summary>
@@ -286,9 +395,19 @@ namespace IES.ExamServer.Services
         /// </summary>
         public string? os { get; set; }
         /// <summary>
+        /// 操作系统位数 64位/32位
+        /// </summary>
+        public string? bit {  get; set; }
+        /// <summary>
+        /// 操作系统指令架构 x86/x64, arm arm64 其他
+        /// </summary>
+        public  string? arch { get; set; }
+        /// <summary>
         /// CPU核心数量
         /// </summary>
-        public int cpu { get; set; } 
+        public int cpu { get; set; }
+
+        public List<CPUInfo> cpuInfos { get; set; } = new List<CPUInfo>();
         /// <summary>
         /// 内存大小
         /// </summary>
@@ -300,7 +419,7 @@ namespace IES.ExamServer.Services
         /// <summary>
         /// 端口,可能有多个端口
         /// </summary>
-        public string? port { get; set; }
+        public List<UriInfo> uris { get; set; } = new List<UriInfo>();
         /// <summary>
         /// 地区
         /// </summary>
@@ -309,6 +428,19 @@ namespace IES.ExamServer.Services
         /// 网卡 IP信息
         /// </summary>
         public List<Network> networks { get; set; } = new List<Network>();
+        /// <summary>
+        /// 本地域名
+        /// </summary>
+       // public string? domain {  get; set; }
+        /// <summary>
+        /// 域名状态 0:未注册,1:正常,2:未授权,需要手动设置或管理员运行
+        /// </summary>
+        //public int domainStatus { get; set; }
+    }
+    public class CPUInfo 
+    {
+        public string? name { get; set; }
+        public string? hz { get; set; }
     }
     public class Network
     {

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

@@ -1,15 +0,0 @@
-using System;
-
-namespace VueCliSample
-{
-    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; }
-    }
-}

+ 10 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/app.manifest

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
+  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
+    <security>
+      <requestedPrivileges>
+		  <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
+      </requestedPrivileges>
+    </security>
+  </trustInfo>
+</assembly>

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

@@ -20,5 +20,8 @@
     "Timeout": 30000,
     "Delay": 500,
     "CenterUrl": "https://www.teammodel.cn"
+  },
+  "ExamClient": {
+    "Domain": "edge-exam.habook.cn"
   }
 }

+ 12 - 0
TEAMModelOS.Extension/IES.ExamLib/IES.ExamLib.csproj

@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.1</TargetFramework>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
+  </ItemGroup>
+
+</Project>

+ 274 - 0
TEAMModelOS.Extension/IES.ExamLib/Models/EvaluationCommon.cs

@@ -0,0 +1,274 @@
+using System.Collections.Generic;
+
+namespace IES.ExamServer.Models
+{
+    public class EvaluationMain
+    {
+        
+        /// <summary>
+        /// 评测id
+        /// </summary>
+        public string? id { get; set; }
+        /// <summary>
+        /// 区级活动的id
+        /// </summary>
+        public string? pid { get; set; }
+        /// <summary>
+        /// 评测名称
+        /// </summary>
+        public string? name { get; set; }
+        /// <summary>
+        /// 类型: Exam 普通评测, Art艺术评测
+        /// </summary>
+        public string? type { get; set; }
+
+        /// <summary>
+        /// 源数据的发布层级 类型 区级area  校级 school 教师个人 teacher
+        /// </summary>
+        public string? owner { get; set; }
+        /// <summary>
+        /// 数据范围
+        /// </summary>
+        public string? scope { get; set; }
+      
+        /// <summary>
+        /// 源code
+        /// </summary>
+        public string? scode { get; set; }
+        /// <summary>
+        /// 科目
+        /// </summary>
+        public List<SubjectExam> subjects { get; set; } = new List<SubjectExam>();
+
+        /// <summary>
+        /// 活动数据包生成最新时间戳
+        /// </summary>
+        public long dataTime { get; set; }
+        public long dataSize { get; set; }
+        /// <summary>
+        /// 活动文件包生成最新时间戳
+        /// </summary>
+        public long blobTime { get; set; }
+        /// <summary>
+        /// 活动文件包大小
+        /// </summary>
+        public long blobSize { get; set; }
+        /// <summary>
+        /// 活动文件包数量
+        /// </summary>
+
+        public long blobCount { get; set; }
+        /// <summary>
+        /// 活动文件包哈希值
+        /// </summary>
+        public string? blobHash { get; set; }
+        /// <summary>
+        /// 活动文件包哈希值(上次)
+        /// </summary>
+        public string? blobLastHash { get; set; }
+        /// <summary>
+        /// 活动页面代码文件生成最新时间戳
+        /// </summary>
+        public long webviewTime { get; set; }
+        /// <summary>
+        /// 活动页面代码文件数量
+        /// </summary>
+        public long webviewCount { get; set; }
+        /// <summary>
+        /// 活动页面代码文件大小
+        /// </summary>
+        public long webviewSize { get; set; }
+        public string? webviewPath { get; set; }
+        /// <summary>
+        /// 学生数量
+        /// </summary>
+        public int studentCount { get; set; }
+        /// <summary>
+        /// 试卷数量
+        /// </summary>
+        public int paperCount { get; set; }
+        /// <summary>
+        /// 名单集合
+        /// </summary>
+        public List<string> grouplist { get; set; } = new List<string>();
+
+        /// <summary>
+        /// 开卷码
+        /// </summary>
+        public string? shortCode { get; set; }
+    }
+
+
+    public class EvaluationClient: EvaluationMain 
+    {
+        /// <summary>
+        /// 开始时间
+        /// </summary>
+        public long stime { get; set; }
+        /// <summary>
+        /// 结束时间
+        /// </summary>
+        public long etime { get; set; }
+        /// <summary>
+        /// 临时密码
+        /// </summary>
+        public string? password { get; set; }
+        /// <summary>
+        /// 记录地址
+        /// </summary>
+        public string? recordUrl { get; set; }
+        /// <summary>
+        /// 激活状态0未激活,1 激活
+        /// </summary>
+        public int activate {  get; set; }
+
+    }
+    public class SubjectExam
+    { 
+        public string? examId { get; set; }
+        public string? subjectId { get; set; }
+        public string? subjectName { get; set; }
+        public List<SubjectExamPaper> papers { get; set; } = new List<SubjectExamPaper>();
+
+    }
+    
+    public class EvaluationExam 
+    {
+
+        /// <summary>
+        /// 评测的id
+        /// </summary>
+        public string? examId { get; set; }
+        public string? examName { get; set; }
+        /// <summary>
+        /// 评测的id
+        /// </summary>
+        public string? evaluationId { get; set; }
+        /// <summary>
+        /// 评测的科目id
+        /// </summary>
+        public string? subjectId { get; set; }
+        /// <summary>
+        /// 评测的科目名称
+        /// </summary>
+        public string? subjectName { get; set; }
+        /// <summary>
+        /// 评测的试卷列表
+        /// </summary>
+        public List<EvaluationPaper> papers { get; set; } = new List<EvaluationPaper>();
+        /// <summary>
+        /// 评测的班级列表
+        /// </summary>
+        public List<string> classes { get; set; } = new List<string>();
+        public string? owner { get; set; }
+      
+        
+        public string? scope {  get; set; }
+        public long stime { get; set; }
+        public long etime { get; set; }
+    }
+    public class SubjectExamPaper
+    {
+        /// <summary>
+        /// 试卷id
+        /// </summary>
+        public string? paperId { get; set; }
+        /// <summary>
+        /// 试卷名称
+        /// </summary>
+        public string? paperName { get; set; }
+        /// <summary>
+        /// 试卷存储路径
+        /// </summary>
+        public string? blob { get; set; }
+        /// <summary>
+        /// 试卷哈希值
+        /// </summary>
+        public string? paperHash { get; set; }
+    }
+    public class EvaluationPaper: SubjectExamPaper
+    {
+        /// <summary>
+        /// 配分列表
+        /// </summary>
+        public List<double> point { get; set; } = new List<double>();
+        //public List<List<string>> answers { get; set; } = new List<List<string>>();  不显示答案
+        /// <summary>
+        /// 知识点列表
+        /// </summary>
+        public List<List<string>> knowledge { get; set; } = new List<List<string>>();
+        /// <summary>
+        /// 题型列表
+        /// </summary>
+        public List<string> type { get; set; } = new List<string>();
+        /// <summary>
+        /// 认知层次
+        /// </summary>
+        public List<int> field { get; set; } = new List<int>();
+        public List<BlobHashInfo> blobs { get; set; } = new List<BlobHashInfo>();
+    }
+    public class BlobHashInfo 
+    {
+        /// <summary>
+        /// 文件路径
+        /// </summary>
+        public string? path { get; set; }
+        /// <summary>
+        /// 文件大小
+        /// </summary>
+        public long size {  get; set; }
+        /// <summary>
+        /// 文件哈希
+        /// </summary>
+        public string? hash {  get; set; }
+        /// <summary>
+        /// 文件最后修改时间
+        /// </summary>
+        public long last { get; set; }
+
+    }
+
+
+    /// <summary>
+    /// 操作记录
+    /// </summary>
+    public class OperationRecord 
+    {
+        /// <summary>
+        /// 记录id
+        /// </summary>
+        public string? id {  get; set; }
+        /// <summary>
+        /// 评测id
+        /// </summary>
+        public string? examId { get; set; }
+        /// <summary>
+        /// 评测名称
+        /// </summary>
+        public string? examName { get; set; }
+        /// <summary>
+        /// 操作时间
+        /// </summary>
+        public string? optTime { get; set; }
+        /// <summary>
+        /// 操作类型,开启重复作答,开启考前倒计时,开启作答倒计时,强制结束作答,
+        /// </summary>
+        public string? optType { get; set; }
+        /// <summary>
+        /// 操作前值
+        /// </summary>
+        public string? optPerval { get; set; }
+        /// <summary>
+        /// 操作后值
+        /// </summary>
+        public string? optAftval { get; set; }
+        /// <summary>
+        /// 操作用户
+        /// </summary>
+        public string? optUser{ get; set; }
+        /// <summary>
+        /// 设备id
+        /// </summary>
+        public string? deviceId {  get; set; }
+    }
+}

+ 14 - 0
TEAMModelOS.Extension/IES.ExamLib/Models/ExamConstant.cs

@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace IES.ExamLib.Models
+{
+    public class ExamConstant
+    {
+        public static readonly string ScopeTeacher = "teacher";
+        public static readonly string ScopeStudent = "student";
+        public static readonly string ScopeVisitor = "visitor";
+        public static readonly string JwtSecretKey = "fXO6ko/qyXeYrkecPeKdgXnuLXf9vMEtnBC9OB3s+aA=";
+    }
+}

+ 16 - 5
TEAMModelOS.Function/CosmosDBTriggers/TriggerArt.cs

@@ -11,7 +11,7 @@ using TEAMModelOS.SDK.Extension;
 using TEAMModelOS.SDK.Models;
 using TEAMModelOS.SDK;
 using Microsoft.Azure.Cosmos;
-using TEAMModelOS.SDK.Models.Cosmos.Common;
+using TEAMModelOS.SDK.Models.Cosmos;
 
 using TEAMModelOS.Function;
 using static TEAMModelOS.SDK.Models.Cosmos.Student.StudentAnalysis;
@@ -674,14 +674,25 @@ namespace TEAMModelOS.CosmosDBTriggers
                                     {
                                         foreach (var res in rs.results)
                                         {
-                                            if (res.quotaId.Equals("quota_21") && res.score > -1 && res.score < 95)
+                                           /* if (res.quotaId.Equals("quota_21") && res.score > -1 && res.subjectId.Equals("subject_painting") && res.score >= 60 && res.score <= 70)                                         
                                             {
-                                                /*res.score *= 1.5;
+                                                res.score += 7.5;
+                                                *//*res.score *= 1.5;
                                                 if (res.score >= 95) {
                                                     res.score = new Random().Next(90, 99);
-                                                }*/
+                                                }*//*
                                                 //res.score = Math.Round(res.score);
                                             }
+                                            if (res.quotaId.Equals("quota_21") && res.score > -1 && res.subjectId.Equals("subject_music") && res.score >= 70 && res.score < 80)
+                                            {
+                                                res.score += 10;
+                                                *//*res.score *= 1.5;
+                                                if (res.score >= 95) {
+                                                    res.score = new Random().Next(90, 99);
+                                                }*//*
+                                                //res.score = Math.Round(res.score);
+                                            }*/
+
                                         }
 
                                         //if (rs.totalScore == 0)
@@ -785,7 +796,7 @@ namespace TEAMModelOS.CosmosDBTriggers
                                                 totalScore = 100,
                                                 id = sj.id,
                                                 type = sj.id,
-                                                block = stuBlocks.Where(c => c.subjectId.Equals(sj.id)).SelectMany(x => x.dim).Where(z => z.stuId.Equals(rs.studentId))?.FirstOrDefault().blk,
+                                                block = stuBlocks.Where(c => c.subjectId.Equals(sj.id)).SelectMany(x => x.dim).ToList().Count > 0 ? stuBlocks.Where(c => c.subjectId.Equals(sj.id)).SelectMany(x => x.dim).Where(z => z.stuId.Equals(rs.studentId))?.FirstOrDefault().blk : null,
                                                 kno = studentScores.Where(c => c.stuId.Equals(rs.studentId)).SelectMany(c => c.studentScore).Where(
                                                     p => p.subject.Equals(sj.id)).Select(z => new
                                                     {

+ 4 - 4
TEAMModelOS.Function/CosmosDBTriggers/TriggerCorrect.cs

@@ -6,7 +6,7 @@ using TEAMModelOS.SDK.Extension;
 using TEAMModelOS.SDK;
 using TEAMModelOS.SDK.Models;
 using TEAMModelOS.SDK.Models.Cosmos;
-using TEAMModelOS.SDK.Models.Cosmos.Common;
+using TEAMModelOS.SDK.Models.Cosmos;
  
 using TEAMModelOS.Function;
 
@@ -271,7 +271,7 @@ namespace TEAMModelOS.CosmosDBTriggers
                                             classResults.Add(item);
                                         }
                                     }
-                                    List<Task<ItemResponse<SDK.Models.Cosmos.Common.Scoring>>> tasks = new List<Task<ItemResponse<SDK.Models.Cosmos.Common.Scoring>>>();
+                                    List<Task<ItemResponse<SDK.Models.Cosmos.Scoring>>> tasks = new List<Task<ItemResponse<SDK.Models.Cosmos.Scoring>>>();
                                     //初始化老师阅卷记录
                                     //List<string> tmds = new List<string>();
                                     /*                                List<string> marks = new List<string>();
@@ -328,7 +328,7 @@ namespace TEAMModelOS.CosmosDBTriggers
                                                     }
                                                     n++;
                                                 }
-                                                SDK.Models.Cosmos.Common.Scoring sc = new SDK.Models.Cosmos.Common.Scoring
+                                                SDK.Models.Cosmos.Scoring sc = new SDK.Models.Cosmos.Scoring
                                                 {
                                                     id = Guid.NewGuid().ToString(),
                                                     code = "Scoring-" + info.school,
@@ -345,7 +345,7 @@ namespace TEAMModelOS.CosmosDBTriggers
                                                     model = sub.model
 
                                                 };
-                                                tasks.Add(client.GetContainer(Constant.TEAMModelOS, "Teacher").CreateItemAsync<SDK.Models.Cosmos.Common.Scoring>(sc, new PartitionKey(sc.code)));
+                                                tasks.Add(client.GetContainer(Constant.TEAMModelOS, "Teacher").CreateItemAsync<SDK.Models.Cosmos.Scoring>(sc, new PartitionKey(sc.code)));
                                             }
                                             //tasks.Add(redisClient.HashSetAsync($"Exam:Scoring:{eid}-{subjectId}", stuId, new { tmdId = tmds, ans = examClass.studentAnswers[index].Count > 0 ? examClass.studentAnswers[index][0] : "", score = examClass.studentScores[index] }.ToJsonString()));
                                         }

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

@@ -19,7 +19,7 @@ using System.Text.Json.Nodes;
 using Newtonsoft.Json.Linq;
 using TEAMModelOS.SDK.Models.Cosmos.Student;
 using TEAMModelOS.Models.Dto;
-using TEAMModelOS.SDK.Models.Cosmos.Common;
+using TEAMModelOS.SDK.Models.Cosmos;
 using System.Net.Http;
 using Newtonsoft.Json;
 using System.Net;

+ 1 - 1
TEAMModelOS.Function/CosmosDBTriggers/TriggerExamImport.cs

@@ -9,7 +9,7 @@ using TEAMModelOS.SDK.DI;
 using TEAMModelOS.SDK;
 using Microsoft.Azure.Cosmos;
 using System.Net.Http;
-using TEAMModelOS.SDK.Models.Cosmos.School;
+using TEAMModelOS.SDK.Models.Cosmos    ;
 using TEAMModelOS.SDK.Extension;
 using TEAMModelOS.SDK.Models;
 using TEAMModelOS.Function;

+ 1 - 1
TEAMModelOS.Function/CosmosDBTriggers/TriggerExamLite.cs

@@ -12,7 +12,7 @@ using TEAMModelOS.SDK;
 using TEAMModelOS.SDK.DI;
 using TEAMModelOS.SDK.Extension;
 using TEAMModelOS.SDK.Models;
-using TEAMModelOS.SDK.Models.Cosmos.Common;
+using TEAMModelOS.SDK.Models.Cosmos;
 using TEAMModelOS.SDK.Models.Service;
 using TEAMModelOS.Function;
 

+ 1 - 1
TEAMModelOS.Function/CosmosDBTriggers/TriggerHomework.cs

@@ -12,7 +12,7 @@ using TEAMModelOS.SDK;
 using TEAMModelOS.SDK.DI;
 using TEAMModelOS.SDK.Extension;
 using TEAMModelOS.SDK.Models;
-using TEAMModelOS.SDK.Models.Cosmos.Common;
+using TEAMModelOS.SDK.Models.Cosmos;
 using TEAMModelOS.SDK.Models.Service;
 using TEAMModelOS.SDK.Models.Service.BI;
 using TEAMModelOS.Function;

+ 1 - 1
TEAMModelOS.Function/CosmosDBTriggers/TriggerQuotaImport.cs

@@ -9,7 +9,7 @@ using System.Threading.Tasks;
 using TEAMModelOS.SDK.DI;
 using TEAMModelOS.SDK;
 using Microsoft.Azure.Cosmos;
-using TEAMModelOS.SDK.Models.Cosmos.School;
+using TEAMModelOS.SDK.Models.Cosmos;
 using TEAMModelOS.SDK.Models;
 using TEAMModelOS.SDK.Extension;
 using Microsoft.OData.Edm;

+ 1 - 1
TEAMModelOS.Function/CosmosDBTriggers/TriggerStudy.cs

@@ -13,7 +13,7 @@ using TEAMModelOS.SDK;
 using TEAMModelOS.SDK.DI;
 using TEAMModelOS.SDK.Extension;
 using TEAMModelOS.SDK.Models;
-using TEAMModelOS.SDK.Models.Cosmos.Common;
+using TEAMModelOS.SDK.Models.Cosmos;
 using TEAMModelOS.SDK.Models.Service;
 using TEAMModelOS.SDK.Models.Service.BI;
 using TEAMModelOS.Function;

+ 2 - 2
TEAMModelOS.Function/CosmosDBTriggers/TriggerSurvey.cs

@@ -14,8 +14,8 @@ using TEAMModelOS.SDK.Extension;
 using TEAMModelOS.SDK;
 using TEAMModelOS.SDK.Models;
 using TEAMModelOS.SDK.Models.Cosmos;
-using TEAMModelOS.SDK.Models.Cosmos.Common;
-using TEAMModelOS.SDK.Models.Cosmos.Common.Inner;
+using TEAMModelOS.SDK.Models.Cosmos;
+using TEAMModelOS.SDK.Models.Cosmos.Inner;
 using TEAMModelOS.SDK.Module.AzureBlob.Configuration;
 using TEAMModelOS.SDK.Models.Service;
 using Microsoft.Extensions.Configuration;

+ 2 - 2
TEAMModelOS.Function/CosmosDBTriggers/TriggerVote.cs

@@ -12,8 +12,8 @@ using TEAMModelOS.SDK.Extension;
 using TEAMModelOS.SDK;
 using TEAMModelOS.SDK.Models;
 using TEAMModelOS.SDK.Models.Cosmos;
-using TEAMModelOS.SDK.Models.Cosmos.Common;
-using TEAMModelOS.SDK.Models.Cosmos.Common.Inner;
+using TEAMModelOS.SDK.Models.Cosmos;
+using TEAMModelOS.SDK.Models.Cosmos.Inner;
 using TEAMModelOS.SDK.Models.Service;
  
 using Microsoft.Extensions.Configuration;

+ 30 - 4
TEAMModelOS.Function/IESServiceBusTrigger.cs

@@ -15,7 +15,7 @@ using TEAMModelOS.SDK.Models;
 using Microsoft.Azure.Cosmos;
 using TEAMModelOS.SDK.Extension;
 using TEAMModelOS.SDK.Models.Cosmos;
-using TEAMModelOS.SDK.Models.Cosmos.Common;
+using TEAMModelOS.SDK.Models.Cosmos;
 using StackExchange.Redis;
 using static TEAMModelOS.SDK.StatisticsService;
 using Azure;
@@ -2554,15 +2554,40 @@ namespace TEAMModelOS.Function
             var json = JsonDocument.Parse(message.Body);
             try
             {
+                var db = _azureCosmos.GetCosmosClient();
                 //await _dingDing.SendBotMsg($"IES5,{Environment.GetEnvironmentVariable("Option:Location")},Imei AF call\n{msg.ToJsonString()}", GroupNames.成都开发測試群組);
-
                 if (json.RootElement.TryGetProperty("channel", out JsonElement channel) &&
                 json.RootElement.TryGetProperty("userid", out JsonElement userid) &&
                 json.RootElement.TryGetProperty("school", out JsonElement school) &&
                 json.RootElement.TryGetProperty("stus", out JsonElement stus))
                 {
+                    try {
+                        List<string>? stuids = System.Text.Json.JsonSerializer.Deserialize<List<string>>(stus);
+                        if (stuids.IsNotEmpty())
+                        {
+                            string sqls = $"SELECT value c from c where c.channel='{channel.GetString()}' and c.school='{school.GetString()}' and  c.stuid not in ({string.Join(",", stuids!.Select(x => $"'{x}'"))}) ";
+                            var rs = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Student).GetList<Imei>(sqls, "Imei");
+                            if (rs.list.IsNotEmpty()) 
+                            {
+                                foreach (var imei in rs.list) 
+                                {
+                                    imei.channel=null;
+                                    imei.lessonId=null;
+                                    await db.GetContainer(Constant.TEAMModelOS, Constant.Student).ReplaceItemAsync<Imei>(imei, imei.id, new PartitionKey(imei.code));
+                                }
+                                //await _dingDing.SendBotMsg($"{_option.Location},清理占用频道号的电子学生证,清理成功,频道号{channel},清理人数{rs.list.Count()}", GroupNames.成都开发測試群組);
+                            }
+                           
+                        }
+                    } catch (Exception ex)
+                    {
+                        await _dingDing.SendBotMsg($"{_option.Location},清理占用频道号的电子学生证,出现异常", GroupNames.成都开发測試群組);
+                    }
+
+
                     json.RootElement.TryGetProperty("imeiType", out JsonElement imeiType);
-                    var db = _azureCosmos.GetCosmosClient();
+                    json.RootElement.TryGetProperty("lessonId", out JsonElement lessonId);
+                    
                     foreach (var stu in stus.EnumerateArray())
                     {
                         await foreach (var item in db.GetContainer(Constant.TEAMModelOS, Constant.Student).GetItemQueryStreamIteratorSql(
@@ -2576,6 +2601,7 @@ namespace TEAMModelOS.Function
                                 var imei = doc.ToObject<Imei>();
                                 imei.channel = channel.GetString();
                                 imei.userid = userid.GetString();
+                                imei.lessonId = $"{lessonId}";
                                 if (!string.IsNullOrWhiteSpace($"{imeiType}"))
                                 {
                                     imei.imeiType = $"{imeiType}";
@@ -2591,7 +2617,7 @@ namespace TEAMModelOS.Function
                                         imei.imeiType = "tianbo";
                                     }
                                 }
-                                await db.GetContainer(Constant.TEAMModelOS, Constant.Student).ReplaceItemAsync<Imei>(imei, imei.id);
+                                await db.GetContainer(Constant.TEAMModelOS, Constant.Student).ReplaceItemAsync<Imei>(imei, imei.id,new PartitionKey(imei.code));
                             }
                         }
                     }

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

@@ -5,9 +5,9 @@
     <OutputType>Exe</OutputType>
     <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
-	<Version>5.2412.25</Version>
-	<AssemblyVersion>5.2412.25.1</AssemblyVersion>
-	<FileVersion>5.2412.25.1</FileVersion>
+	<Version>5.2501.8</Version>
+	<AssemblyVersion>5.2501.8.1</AssemblyVersion>
+	<FileVersion>5.2501.8.1</FileVersion>
 	<PackageId>TEAMModelOS.FunctionV4</PackageId>
 	<Authors>teammodel</Authors>
 	<Company>醍摩豆(成都)信息技术有限公司</Company>

+ 1 - 1
TEAMModelOS.SDK/Models/Cosmos/Common/ArtEvaluation.cs

@@ -6,7 +6,7 @@ using System.Text.Json;
 using System.Text.Json.Serialization;
 using System.Threading.Tasks;
 
-namespace TEAMModelOS.SDK.Models.Cosmos.Common
+namespace TEAMModelOS.SDK.Models.Cosmos
 {
     /// <summary>
     /// 艺术评测基础信息

+ 1 - 1
TEAMModelOS.SDK/Models/Cosmos/Common/ArtExam.cs

@@ -5,7 +5,7 @@ using System.Text;
 using System.Text.Json;
 using System.Threading.Tasks;
 
-namespace TEAMModelOS.SDK.Models.Cosmos.Common
+namespace TEAMModelOS.SDK.Models.Cosmos
 {
     public  class ArtExam : CosmosEntity
     {

+ 1 - 1
TEAMModelOS.SDK/Models/Cosmos/Common/ArtMusic.cs

@@ -5,7 +5,7 @@ using System.Text;
 using System.Text.Json;
 using System.Threading.Tasks;
 
-namespace TEAMModelOS.SDK.Models.Cosmos.Common
+namespace TEAMModelOS.SDK.Models.Cosmos
 {
     public class ArtMusic : CosmosEntity
     {

+ 1 - 1
TEAMModelOS.SDK/Models/Cosmos/Common/ArtRecord.cs

@@ -4,7 +4,7 @@ using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 
-namespace TEAMModelOS.SDK.Models.Cosmos.Common
+namespace TEAMModelOS.SDK.Models.Cosmos
 {
     public class ArtRecord : CosmosEntity
     {

+ 1 - 1
TEAMModelOS.SDK/Models/Cosmos/Common/ExamLite.cs

@@ -2,7 +2,7 @@ using System;
 using System.Collections.Generic;
 using System.Text;
 using System.Text.Json;
-using TEAMModelOS.SDK.Models.Cosmos.Common;
+using TEAMModelOS.SDK.Models.Cosmos;
 
 namespace TEAMModelOS.SDK.Models
 {

+ 1 - 1
TEAMModelOS.SDK/Models/Cosmos/Common/Inner/AbilityTaskTree.cs

@@ -1,7 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Text;
-using TEAMModelOS.SDK.Models.Cosmos.Common;
+using TEAMModelOS.SDK.Models.Cosmos;
 
 namespace TEAMModelOS.SDK.Models
 {

+ 1 - 1
TEAMModelOS.SDK/Models/Cosmos/Common/Inner/CourseChange.cs

@@ -2,7 +2,7 @@ using System;
 using System.Collections.Generic;
 using System.Text;
 
-namespace TEAMModelOS.SDK.Models.Cosmos.Common
+namespace TEAMModelOS.SDK.Models.Cosmos
 {
     public class CourseChange
     {

+ 1 - 1
TEAMModelOS.SDK/Models/Cosmos/Common/Inner/SurveyRecord.cs

@@ -2,7 +2,7 @@ using System;
 using System.Collections.Generic;
 using System.Text;
 
-namespace TEAMModelOS.SDK.Models.Cosmos.Common.Inner
+namespace TEAMModelOS.SDK.Models.Cosmos.Inner
 {
     public  class SurveyRecord
     {

+ 1 - 1
TEAMModelOS.SDK/Models/Cosmos/Common/Inner/SyllabusTree.cs

@@ -1,5 +1,5 @@
 using System.Collections.Generic;
-using TEAMModelOS.SDK.Models.Cosmos.Common;
+using TEAMModelOS.SDK.Models.Cosmos;
 
 namespace TEAMModelOS.SDK.Models
 {

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

@@ -4,7 +4,7 @@ using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 
-namespace TEAMModelOS.SDK.Models.Cosmos.Common
+namespace TEAMModelOS.SDK.Models.Cosmos
 {
     /// <summary>
     /// CS IOT TeachingData (Redis)

+ 1 - 1
TEAMModelOS.SDK/Models/Cosmos/Common/LearnRecord.cs

@@ -6,7 +6,7 @@ using System.Text;
 using System.Text.Json;
 using System.Threading.Tasks;
 
-namespace TEAMModelOS.SDK.Models.Cosmos.Common
+namespace TEAMModelOS.SDK.Models.Cosmos
 {
     /// <summary>
     /// 儲存學習記錄blob的格式

+ 1 - 1
TEAMModelOS.SDK/Models/Cosmos/Common/LessonCount.cs

@@ -2,7 +2,7 @@
 using System.Collections.Generic;
 using System.Text;
 
-namespace TEAMModelOS.SDK.Models.Cosmos.Common
+namespace TEAMModelOS.SDK.Models.Cosmos
 {
     public class LessonCount : CosmosEntity
     {

+ 1 - 1
TEAMModelOS.SDK/Models/Cosmos/Common/Scoring.cs

@@ -3,7 +3,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 
-namespace TEAMModelOS.SDK.Models.Cosmos.Common
+namespace TEAMModelOS.SDK.Models.Cosmos
 {
     public class Scoring : CosmosEntity
     {

+ 1 - 1
TEAMModelOS.SDK/Models/Cosmos/Common/Snode.cs

@@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations;
 using System.Text;
 using System.Text.Json;
 
-namespace TEAMModelOS.SDK.Models.Cosmos.Common
+namespace TEAMModelOS.SDK.Models.Cosmos
 {
     /// <summary>
     /// 课纲节点父类

+ 1 - 1
TEAMModelOS.SDK/Models/Cosmos/Common/StuCourse.cs

@@ -2,7 +2,7 @@ using System;
 using System.Collections.Generic;
 using System.Text;
 
-namespace TEAMModelOS.SDK.Models.Cosmos.Common
+namespace TEAMModelOS.SDK.Models.Cosmos
 {
     /// <summary>
     /*

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


이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.