CrazyIter_Bin 4 miesięcy temu
rodzic
commit
4a1610bfce
100 zmienionych plików z 7747 dodań i 0 usunięć
  1. 8 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamLib/IES.ExamLibrary.csproj
  2. 372 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamLib/Models/EvaluationCommon.cs
  3. 14 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamLib/Models/ExamConstant.cs
  4. 22 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam0/cert.pem
  5. 74 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam0/certificate.bat
  6. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam0/certificate.cer
  7. 28 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam0/key.pem
  8. 22 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam1/cert.pem
  9. 74 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam1/certificate.bat
  10. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam1/certificate.cer
  11. 28 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam1/key.pem
  12. 22 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam2/cert.pem
  13. 74 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam2/certificate.bat
  14. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam2/certificate.cer
  15. 28 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam2/key.pem
  16. 22 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam3/cert.pem
  17. 74 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam3/certificate.bat
  18. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam3/certificate.cer
  19. 28 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam3/key.pem
  20. 22 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam4/cert.pem
  21. 74 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam4/certificate.bat
  22. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam4/certificate.cer
  23. 28 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam4/key.pem
  24. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/logo.png
  25. 22 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/template/cert.pem
  26. 74 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/template/certificate.bat
  27. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/template/certificate.cer
  28. 28 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/template/key.pem
  29. 104 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/BaseController.cs
  30. 416 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/IndexController.cs
  31. 651 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/ManageController.cs
  32. 6 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/StudentController.cs
  33. 23 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/CenterServiceConnectionService.cs
  34. 248 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/CustomFileLoggerProvider.cs
  35. 59 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/LiteDBFactory.cs
  36. 158 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/ServiceInitializer.cs
  37. 143 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/SignalRCloudClientHub.cs
  38. 219 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/SignalRHost/SignalRExamServerHub.cs
  39. 29 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Filters/AspNetCoreBuilderServiceCollectionExtensions.cs
  40. 114 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Filters/AuthTokenAttribute.cs
  41. 38 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/CollectionHelper.cs
  42. 31 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/Constant.cs
  43. 68 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/ExpressionHelper.cs
  44. 37 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/FileHelper.cs
  45. 320 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/HttpContextExtensions.cs
  46. 51 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/JsonExtensions.cs
  47. 67 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/JwtAuthExtension.cs
  48. 51 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/ProcessHelper.cs
  49. 124 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/QRCodeHelper.cs
  50. 134 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/ShaHashHelper.cs
  51. 40 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/IES.ExamServer.csproj
  52. 9 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Models/ErrorViewModel.cs
  53. 77 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Models/ServerDevice.cs
  54. 117 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Models/SignalRClient.cs
  55. 36 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Models/Teacher.cs
  56. 123 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Program.cs
  57. 34 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Properties/launchSettings.json
  58. 461 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Services/IndexService.cs
  59. 89 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Services/MemoryInfo.cs
  60. 10 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/app.manifest
  61. 15 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/appsettings.Development.json
  62. 65 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/appsettings.json
  63. 24 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/nlog.config
  64. 9 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/.editorconfig
  65. 1 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/.gitattributes
  66. 30 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/.gitignore
  67. 11 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/IES.ExamViews.esproj
  68. 35 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/README.md
  69. 5 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/babel.config.js
  70. 17 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/eslint.config.js
  71. 13 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/index.html
  72. 19 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/jsconfig.json
  73. 55 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/package.json
  74. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/public/favicon.ico
  75. 17 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/public/index.html
  76. 129 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/public/signalr.html
  77. 45 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/App.vue
  78. 596 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/api/http.js
  79. 43 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/api/index.js
  80. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/fengjing.jpg
  81. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/icon/icon_play.png
  82. 539 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/demo.css
  83. 579 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/demo_index.html
  84. 83 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.css
  85. 1 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.js
  86. 128 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.json
  87. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.ttf
  88. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.woff
  89. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.woff2
  90. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/image/tmd_logo.png
  91. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/logo.png
  92. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/qrCode.png
  93. 63 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/reset.css
  94. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/source/audio.png
  95. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/source/excel.png
  96. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/source/folder.png
  97. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/source/image.png
  98. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/source/item.png
  99. BIN
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/source/link.png
  100. 0 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/source/pdf.png

+ 8 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamLib/IES.ExamLibrary.csproj

@@ -0,0 +1,8 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.1</TargetFramework>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+</Project>

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

@@ -0,0 +1,372 @@
+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>
+        /// 数据源的id
+        /// </summary>
+        public string? ownerId { get; set; }
+        /// <summary>
+        /// 数据源的名称
+        /// </summary>
+        public string? ownerName { get; set; }
+        /// <summary>
+        /// 数据源的logo
+        /// </summary>
+        public string? ownerPicture { 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>
+        /// 扩展活动项目,投票评选Vote,问卷调查Survey
+        /// </summary>
+        public List<ExtendActivity> activities { get; set; } = new List<ExtendActivity>();
+        /// <summary>
+        /// AI音乐评测
+        /// </summary>
+        public AIMusic? music { get; set; }
+        /// <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? grouplistHash {  get; set; }
+        /// <summary>
+        /// 数据哈希值
+        /// </summary>
+        public string? dataHash { get; set; }
+        /// <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; }
+       
+        /// <summary>
+        /// 倒计时类型 0 未设置,1统一以服务器时间为基准介绍,2,以开始作答为基准,开始作答向局域网端发送请求,返回开始作答时间。
+        /// </summary>
+        public int countdownType {  get; set; }
+        /// <summary>
+        /// 倒计时,时长,按毫秒为单位
+        /// </summary>
+        public long countdown { get; set; }
+        /// <summary>
+        /// 截至时间,countdownType=1 时有值
+        /// </summary>
+        public long deadline {  get; set; }
+
+    }
+    public class SubjectExam
+    {
+        public string? examId { get; set; }
+        public string? examName { get; set; }
+        public string? subjectId { get; set; }
+        public string? subjectName { get; set; }
+        public List<SubjectExamPaper> papers { get; set; } = new List<SubjectExamPaper>();
+    }
+    /// <summary>
+    /// 扩展活动
+    /// </summary>
+    public class ExtendActivity
+    {
+        public string? activityId { get; set; }
+        public string? activityName { get; set; }
+        /// <summary>
+        /// 投票评选Vote,问卷调查Survey
+        /// </summary>
+        public string? type { get; set; }
+        public List<SubjectExamPaper> papers { get; set; } = new List<SubjectExamPaper>();
+    }
+    /// <summary>
+    /// AI音乐评测
+    /// </summary>
+    public class AIMusic
+    {
+        /// <summary>
+        /// 关联的评测
+        /// </summary>
+        public string? examId { get; set; }
+        /// <summary>
+        /// 关联的区级评测id
+        /// </summary>
+        public string? pid {  get; set; }
+        /// <summary>
+        /// 评测名称
+        /// </summary>
+        public string? examName { get; set; }
+        /// <summary>
+        /// AI 音乐评测题目id
+        /// </summary>
+        public string? questionId {  get; set; }
+        /// <summary>
+        /// AI 音乐评测题目名称
+        /// </summary>
+        public string? questionName {  get; set; }
+        /// <summary>
+        /// 必唱歌曲
+        /// </summary>
+        public List<AISong> mustSong {  get; set; }= new List<AISong>();
+        /// <summary>
+        /// 选唱歌曲
+        /// </summary>
+        public List<AISong> optionSong { get; set; } = new List<AISong>();
+    }
+
+    public class AISong 
+    {
+        /// <summary>
+        /// 歌曲id
+        /// </summary>
+        public string? songId { get; set; }
+        /// <summary>
+        /// 歌曲名称
+        /// </summary>
+        public string? songName { get; set; }
+    }
+    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; }
+        /// <summary>
+        /// 评测类型Exam,投票评选Vote,问卷调查Survey
+        /// </summary>
+        public string? type { 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.Exam/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=";
+    }
+}

+ 22 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam0/cert.pem

@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDozCCAougAwIBAgIUXVG28xNwGPdDM2SRRiCQhDTZG60wDQYJKoZIhvcNAQEL
+BQAwazELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB1NpQ2h1YW4xEDAOBgNVBAcMB0No
+ZW5nRHUxDzANBgNVBAoMBmhhYm9vazELMAkGA1UECwwCcmQxGjAYBgNVBAMMEWV4
+YW0uaGFib29rLmxvY2FsMB4XDTI1MDEyMTA0MzM0NVoXDTI2MDEyMTA0MzM0NVow
+azELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB1NpQ2h1YW4xEDAOBgNVBAcMB0NoZW5n
+RHUxDzANBgNVBAoMBmhhYm9vazELMAkGA1UECwwCcmQxGjAYBgNVBAMMEWV4YW0u
+aGFib29rLmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxAl2
+7myycEOgiO0OEohPYDV/1raik81QuU3485caFoxs2UUXJoW0zkG0TKHVqVJGcNbg
+xncN7gjThRAKy4PSLTrn9A9fPJVqRpB0ElJlBsChhfXxpWzrFl/wHOxdmYz5rUjs
+gjq0zdlodSQ2yWE0VdZhI6VqIbvz16rHXj19B1kKcGVzlnwuRxuqldDEJSUD3UTe
+epPGaSbdqI4riUcOaksEMEAgll7HqixTv5tB3/aKJ0w+nxZFimocdA/XFXIXMO1M
+lLP2CCck1Jv7vlAL/TP22wQnfINifYDisTmsdtFvq6hgkFgh34lDX+moM92MPR5/
+m5D4GQi6BYtFpOoC+wIDAQABoz8wPTAcBgNVHREEFTATghFleGFtLmhhYm9vay5s
+b2NhbDAdBgNVHQ4EFgQUXr4ImISHw4eBN6r8MO3LiM0o+5gwDQYJKoZIhvcNAQEL
+BQADggEBAIFTbpGUwtpMa//mrHGghAcF+jw3+6G/I6K0bsfcFfYVQJCwJ+HcNWDJ
++nCn64rdu5sQvFmpPSlXBMbFoW/ZAxs2g7jhk90MUrR0MFBu91qAu2gqwE55S6xr
+O7fg4sd0aVvR2tE2e6wxMaLGTfNtF9Uo0tKFehDUQXIiCSAQkLGwsWneZ+GRE8Dc
+CwGjJXpwvBfmcQoNMLqZpZ8P8AdOUVjp7PIEkWJWSGcaa+RCrP4qhVwNv+ZloBbc
+XBNxCrftg2epupYOGvsq5ACX7u3nLbRfuKu3X+E0F7V/Qwmjfc9+WS50LouCMIx/
+TvVawMxytPaTUlQe45aNANW4reHWezQ=
+-----END CERTIFICATE-----

+ 74 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam0/certificate.bat

@@ -0,0 +1,74 @@
+@echo off
+echo Configuring hosts file
+
+net session >nul 2>&1
+if %errorLevel% neq 0 (
+    echo Please run this script as an administrator
+    pause
+    exit /b
+)
+
+set "hostsFile=C:\Windows\System32\drivers\etc\hosts"
+set "newEntry=192.168.8.132 exam.habook.local"
+
+if not exist "%hostsFile%" (
+    echo hosts file does not exist:%hostsFile%
+    pause
+    exit /b
+)
+
+
+findstr /v /i /c:"exam.habook.local" "%hostsFile%" > "%hostsFile%.tmp"
+if %errorLevel% equ 0 (
+    move /y "%hostsFile%.tmp" "%hostsFile%" >nul 2>&1
+    echo Removed all entries containing exam.habook.local
+) else (
+    echo Failed to remove entries containing exam.habook.local
+    pause
+    exit /b
+)
+
+
+echo %newEntry% >> "%hostsFile%"
+if %errorLevel% equ 0 (
+    echo Hosts file configured successfully
+) else (
+    echo Hosts file configuration failed
+    pause
+    exit /b
+)
+
+:ImportCert
+echo Importing certificate
+
+if not exist "%~dp0certificate.cer" (
+    echo Certificate file does not exist:%~dp0certificate.cer
+    pause
+    exit /b
+)
+
+set "certSubject="
+for /f "tokens=*" %%i in ('certutil -dump "%~dp0certificate.cer" ^| findstr /i "CN="') do (
+    set "certSubject=%%i"
+)
+
+if defined certSubject (
+    echo Deleting existing certificate with the same name
+    certutil -delstore "Root" "%certSubject%"
+    if %errorLevel% equ 0 (
+        echo Existing certificate deleted successfully
+    ) else (
+        echo Failed to delete existing certificate (may not exist)
+    )
+)
+
+echo Importing new certificate
+certutil -addstore -f "Root" "%~dp0certificate.cer"
+if %errorLevel% equ 0 (
+    echo Certificate imported successfully
+) else (
+    echo Certificate import failed
+)
+
+echo All operations completed
+pause

BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam0/certificate.cer


+ 28 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam0/key.pem

@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDECXbubLJwQ6CI
+7Q4SiE9gNX/WtqKTzVC5TfjzlxoWjGzZRRcmhbTOQbRModWpUkZw1uDGdw3uCNOF
+EArLg9ItOuf0D188lWpGkHQSUmUGwKGF9fGlbOsWX/Ac7F2ZjPmtSOyCOrTN2Wh1
+JDbJYTRV1mEjpWohu/PXqsdePX0HWQpwZXOWfC5HG6qV0MQlJQPdRN56k8ZpJt2o
+jiuJRw5qSwQwQCCWXseqLFO/m0Hf9oonTD6fFkWKahx0D9cVchcw7UyUs/YIJyTU
+m/u+UAv9M/bbBCd8g2J9gOKxOax20W+rqGCQWCHfiUNf6agz3Yw9Hn+bkPgZCLoF
+i0Wk6gL7AgMBAAECggEABMSeKepm5KggbXQmlXjPRW3HsDc8+Q9TWU52MaaJMec0
+doxhpcQ2w5WBhyOgiL/BitkPGoSmyBVa36+mM5D/Oa5nGd6N2HFh5ll5GUD7yBBD
+XXi/6eAeT6sBshJlrGhrYjWV2w0GfMM+8SpyUq+UkEnoju9lB3EE20gCFV61fco3
+pK7en/hHCK0/jLDJYxEhEk5FMKA0BAUdsoD2WRVCnakCSaKRJzJEafL9CHQaL/oq
+pPVIYO3tx0ryNtdcLsc0j01rLMJ8n9lG59+3zAu5nCEVHYKO7oPuj8kpwdqxIj9J
+ZnSHX9ayJYNOdJDGD8zQYKhZQdquOYOAE9NnbINAMQKBgQD+OCf02IIuK4NcU1Ef
+wlx/sRjvCIWq39S1KNQ0kPtI4dWUNLI5DJqVQgylHIkVC/FNbTcaW1A6nVqukJfJ
+Nu3HrZoN+585QRKesTGNd45Zmwwy8JuV1OblV6SbI5xZ3g3S4SVkWnldOz36FThn
+wdHf2lg/unGzX4ojgYfuV1VpfwKBgQDFaPsd7Lo9oDUEZH4GDjk8onsIWwad6Nut
+3CCdBn3tiSlSAddVbZfu60VtM25CA9K1UbPsV1FU7ZYX1wKma019r1VqkwhPMWoq
+FsnhoHMOl69vV/3brjfHZDflbCMbMt/vYc91kRS7nVDAeqz5sDf4mJ7BSs70ZGth
+oAqHLQfMhQKBgQDhg71mRX5OKMmR6FMpwkg9+kNtIHk7GO5feoWs0AQqJjRKEekc
+FKM4zuvauJKeegaoMb9VATYNmTMtchVEKRcMMGNeDh20M5ap8fRMU4eS06khszHB
+26isQHBEM3XqfsJylMmP2XaaDwiuxY5Q9K4ST2ZDukhM3+7yCmEkPJMHTwKBgEEK
+DXIWhGW5Wr5PvZWRKhpoDdD67HsqNPZbCAO0F9kiz5JNOPzUVrJIoV8RCsqFJ+7F
+NFoxioJIpKLGHAFoaOd31NSADMTKqwei6nCDxGSSZSJyAxlVlNsEkcXsksRrRow/
+1XIOkp4dfnVr9YFuJYKqBeP5GaY7T4WijNVsaJ1hAoGAGQD8EyQXcfsaIEjaPQ65
+io+DloCFqwJd5EkCFPyv7Qz+5A9Jv6/RTznXSXtNBv+j6t7MG/o6A8ZX0ZR5UjuG
+WB/U87QpUokC+wzNTCdbDWaF/GvOU8lziksDrMMzrV8Yy+CVtGNiscAZyj6QJ1Am
+WrPltUEZoXxKkRRcmvNeti8=
+-----END PRIVATE KEY-----

+ 22 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam1/cert.pem

@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDpjCCAo6gAwIBAgIUOJhd4SHuDg1Y/zNtpBHCVtmwcSMwDQYJKoZIhvcNAQEL
+BQAwbDELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB1NpQ2h1YW4xEDAOBgNVBAcMB0No
+ZW5nRHUxDzANBgNVBAoMBmhhYm9vazELMAkGA1UECwwCcmQxGzAZBgNVBAMMEmV4
+YW0xLmhhYm9vay5sb2NhbDAeFw0yNTAxMjIwMTU2MzlaFw0yNjAxMjIwMTU2Mzla
+MGwxCzAJBgNVBAYTAkNOMRAwDgYDVQQIDAdTaUNodWFuMRAwDgYDVQQHDAdDaGVu
+Z0R1MQ8wDQYDVQQKDAZoYWJvb2sxCzAJBgNVBAsMAnJkMRswGQYDVQQDDBJleGFt
+MS5oYWJvb2subG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0
+qV10i/hFgxYUCQ2fz230qf2dpjvLlOXC/6VR9j7sRPoLlPrS7ux1i5/ysqjBxSvr
+4vAAyTRqtr8xGusj0pP3KrZ1s1ETFhfs1I1+VwRVBdrQYCtc18a2H6KTl1QJhH53
+i8otPG6xe3+x/CN+f67VYhuVLGmvfT+a/VehDu4INqZF35xzNDYwyT7xGZLFZiCD
+GrcVmZrpkIcBe0NWdkZyfx2U0fuuDxJHe3j+HNB/xZMFJWlTtqOcx1c4iQHwFGyR
+niPkzhjGt+9d4bnQ0wgKMs/ACSACl0QK+R5SMGqKiex2WOmd1GMkhm7B3Y5g9MbI
+koRLPW1iHgycvFt/Dy65AgMBAAGjQDA+MB0GA1UdEQQWMBSCEmV4YW0xLmhhYm9v
+ay5sb2NhbDAdBgNVHQ4EFgQUPxmKwaVyZjfo+c5KUbIieL2BjZIwDQYJKoZIhvcN
+AQELBQADggEBAKIqBNcwHswBt9OE8H9jGxS13JYmpwV3W01bbJP7Uf0uiElZFKZo
+A9l4/snc7dNbF65mQMWnRiTZKRdm5rO4VgRAjiZ+n9/dGFdfgJZp7vsGfzfrPvax
+eMu0R++KKT7NdECwF24CgZfzyR1rqmDbofrdOr6zhWC8dRtxcGXlrdn8Y9cuXHVE
+uCK2u52RK7F+z86G5LTNCXmY/HjJwB4zEdL/T2f0MZxumrdPNyzekSQsMwe1ogGQ
+Zt/4EuS/+Qc6cvjTph7x1AXtRE9PfGDPKpohJiQpq/QG2dpnPe23JxKLIRcWOKq9
+qwoamlyjFKFhfc9AfAle3z0ehR/2X8mxxWs=
+-----END CERTIFICATE-----

+ 74 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam1/certificate.bat

@@ -0,0 +1,74 @@
+@echo off
+echo Configuring hosts file
+
+net session >nul 2>&1
+if %errorLevel% neq 0 (
+    echo Please run this script as an administrator
+    pause
+    exit /b
+)
+
+set "hostsFile=C:\Windows\System32\drivers\etc\hosts"
+set "newEntry=192.168.8.132 exam1.habook.local"
+
+if not exist "%hostsFile%" (
+    echo hosts file does not exist:%hostsFile%
+    pause
+    exit /b
+)
+
+
+findstr /v /i /c:"exam.habook.local" "%hostsFile%" > "%hostsFile%.tmp"
+if %errorLevel% equ 0 (
+    move /y "%hostsFile%.tmp" "%hostsFile%" >nul 2>&1
+    echo Removed all entries containing exam.habook.local
+) else (
+    echo Failed to remove entries containing exam.habook.local
+    pause
+    exit /b
+)
+
+
+echo %newEntry% >> "%hostsFile%"
+if %errorLevel% equ 0 (
+    echo Hosts file configured successfully
+) else (
+    echo Hosts file configuration failed
+    pause
+    exit /b
+)
+
+:ImportCert
+echo Importing certificate
+
+if not exist "%~dp0certificate.cer" (
+    echo Certificate file does not exist:%~dp0certificate.cer
+    pause
+    exit /b
+)
+
+set "certSubject="
+for /f "tokens=*" %%i in ('certutil -dump "%~dp0certificate.cer" ^| findstr /i "CN="') do (
+    set "certSubject=%%i"
+)
+
+if defined certSubject (
+    echo Deleting existing certificate with the same name
+    certutil -delstore "Root" "%certSubject%"
+    if %errorLevel% equ 0 (
+        echo Existing certificate deleted successfully
+    ) else (
+        echo Failed to delete existing certificate (may not exist)
+    )
+)
+
+echo Importing new certificate
+certutil -addstore -f "Root" "%~dp0certificate.cer"
+if %errorLevel% equ 0 (
+    echo Certificate imported successfully
+) else (
+    echo Certificate import failed
+)
+
+echo All operations completed
+pause

BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam1/certificate.cer


+ 28 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam1/key.pem

@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC0qV10i/hFgxYU
+CQ2fz230qf2dpjvLlOXC/6VR9j7sRPoLlPrS7ux1i5/ysqjBxSvr4vAAyTRqtr8x
+Gusj0pP3KrZ1s1ETFhfs1I1+VwRVBdrQYCtc18a2H6KTl1QJhH53i8otPG6xe3+x
+/CN+f67VYhuVLGmvfT+a/VehDu4INqZF35xzNDYwyT7xGZLFZiCDGrcVmZrpkIcB
+e0NWdkZyfx2U0fuuDxJHe3j+HNB/xZMFJWlTtqOcx1c4iQHwFGyRniPkzhjGt+9d
+4bnQ0wgKMs/ACSACl0QK+R5SMGqKiex2WOmd1GMkhm7B3Y5g9MbIkoRLPW1iHgyc
+vFt/Dy65AgMBAAECggEAJhdXpw0kCbP74bmO79USf/Wfja/RB7mbQCQavU9IMRTS
+C8Mbp4HMxXPtA4T74+8otZGhvOivbpidc0MTtPffTsvosKRgQb/0x3CBbNP4C90y
+J6E/Q1ITia7d3nSXuXIAIECPHj6RsEYzxFk/Opq9SCxpWGiG2AN2d71WzpSRR5Wy
+4ljWxyqrpq+9yBW+6a6ZESNTG3wnscysy1EENt5TrHrpSaH90aLDNGYFfY1x+d3h
+lCNzuAvCv9F0DIzhvsICfrWYVmLPZHFMVdR6oYiAkOQ+FwqYbvrBttCXs7Mqod+e
+JI2hzyDgxHQ0p0SFjGocTaZ+lirnwkzct/wZwDotKQKBgQDyEvIxnxZDi0maYkIW
+feJpOJP9bClkziVc2ng+HdlcUW9zAvsmnQOYqzNemjJ1N+UWvcVjVfHh8K9ISLUV
+bmygIGxHGP1wUhLojlJqHTRJ2tMvgxeO5xDmgY/te/WSaqCj/bauPy1xUEEfado2
+kTuu0zL7RPehG4iyAN0dfSudgwKBgQC/Df2Wswnsi2yptinyfUmjzlqgotHJlFDr
+iGQSOpqR9v68wLAMPt5nIvJwNpUILvFa20Wk6+GuJy5BcK5rrfBAVLx+8OJMpuA5
+G6beWx3CWKK/fLMH6F6YMyVDSKu9KvLOMVkCG8uqjSXU0aL9Ymwx7+leU7kbznIY
++Nw1cSkqEwKBgQDpUtmeeng7IkYPSCXrB+rzAFWkLly3jPr2RJ0hQiP/l36UnIr2
+7OBKhrk8teNsmDN3d/KJjI1X+WT5hxsDTSvmK1oyLSQa3wDaplNJdFyx0vk7El/i
+nVTs2HShsplARwYPCrzJtptWXMRoQt7ROasNFwRMrG0CHEflSm/VvJANgQKBgCnK
+5a4RfBpCZYK6VnK09WbfvPNmqn1t9EWRkFJsf2NupEql29zR49Sr0Fy7k942ZpV5
+YcKx1qxB5dxqybzET8nCv3kFOHuMBYB4jAgX5mqc+PzqSj9wlUC173DIdMjsnDB+
+mvnbGrI0LhJfyUekQAasdGAt8FAk2NCdn44RMvABAoGABmHKJ+mRl/T+x/XeY5ED
+CRpi5q6H2t/90g9lP0S7DHRVOkbCFvDCzdwYgmaxJatGo8TOH7PziuZDpgULjL8O
+/250oj5nNMyMsst5Uk4NopBsb7+f2TdM7Br/tOAdm+Tzl7KCHwsh+fDBlcJRq+Bb
+knsLLVKfkMLMm3WuWiIN/7Y=
+-----END PRIVATE KEY-----

+ 22 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam2/cert.pem

@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDpjCCAo6gAwIBAgIUDfoU75/CWmzBsSuNiuRxG7V2j8owDQYJKoZIhvcNAQEL
+BQAwbDELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB1NpQ2h1YW4xEDAOBgNVBAcMB0No
+ZW5nRHUxDzANBgNVBAoMBmhhYm9vazELMAkGA1UECwwCcmQxGzAZBgNVBAMMEmV4
+YW0yLmhhYm9vay5sb2NhbDAeFw0yNTAxMjIwMTU3NTBaFw0yNjAxMjIwMTU3NTBa
+MGwxCzAJBgNVBAYTAkNOMRAwDgYDVQQIDAdTaUNodWFuMRAwDgYDVQQHDAdDaGVu
+Z0R1MQ8wDQYDVQQKDAZoYWJvb2sxCzAJBgNVBAsMAnJkMRswGQYDVQQDDBJleGFt
+Mi5oYWJvb2subG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCl
+Zx/kmGkyrPtpAfDgfbYhRImryj4Ez+CFloyZO49bdlFJ4qNrwx3WSdvay4Vk/2jQ
+wAZaRU6ojxt4rBbL7VKU/xHd1CPLjoiq8pAVdfoYiJTb3Nj4xsAer+xaCuwfC96a
+J/Wgw40JByzrBBR2b14GEOx2A6M99e+V2/Wvhc+pUu9h2F6vrND43TNy2UGtAyWQ
+Oya4RIubb4zRSLjPN4wECjtbZ2Rm1lTB+tOW+Q/PVgVj6YddVONVpaEorpoae5lX
+zsjpJchegY0UJrVUZqII1HWmItzuu5ZZbydBLxKcMcfteRK7awqgUYWTA1RbEkyb
+k69alkylL84hD9XOO+0FAgMBAAGjQDA+MB0GA1UdEQQWMBSCEmV4YW0yLmhhYm9v
+ay5sb2NhbDAdBgNVHQ4EFgQUtfCBPHkTSDDMBerflEj46zlTjrYwDQYJKoZIhvcN
+AQELBQADggEBAHCuW3w/nWm9Cek2CPWcHS+nP65UK8ql7H3nX7vBws0cmWlTHD7j
+cyJf6YHP/T2SdpaPJJIp9AxzlvQUPE0UKf92+hQci5E+422zIz1C++GLtkMwoGll
+Aj7SpsCAYIXlOd6Xe2k7EF2A3wJD0UU5v4GBQ2iZSKhSTTnv/slkBKvOLcCmnIXG
+Z9W40M3fXNzk1+oHfXXrXzL6ysL4xpmaHn5INB9wOgL77hLsDoqLG+HS1WOaPtQ3
+Ck4CSsJ8msJybCjPV3SJ2+9bUgBJlal7WP/0HyvIjF6Lh0IFRlpHr4eY+OV2PaVy
+i1gCnF5/vBOWWaAq2oyD+kHiJslX3XUPi/Y=
+-----END CERTIFICATE-----

+ 74 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam2/certificate.bat

@@ -0,0 +1,74 @@
+@echo off
+echo Configuring hosts file
+
+net session >nul 2>&1
+if %errorLevel% neq 0 (
+    echo Please run this script as an administrator
+    pause
+    exit /b
+)
+
+set "hostsFile=C:\Windows\System32\drivers\etc\hosts"
+set "newEntry=192.168.8.132 exam2.habook.local"
+
+if not exist "%hostsFile%" (
+    echo hosts file does not exist:%hostsFile%
+    pause
+    exit /b
+)
+
+
+findstr /v /i /c:"exam.habook.local" "%hostsFile%" > "%hostsFile%.tmp"
+if %errorLevel% equ 0 (
+    move /y "%hostsFile%.tmp" "%hostsFile%" >nul 2>&1
+    echo Removed all entries containing exam.habook.local
+) else (
+    echo Failed to remove entries containing exam.habook.local
+    pause
+    exit /b
+)
+
+
+echo %newEntry% >> "%hostsFile%"
+if %errorLevel% equ 0 (
+    echo Hosts file configured successfully
+) else (
+    echo Hosts file configuration failed
+    pause
+    exit /b
+)
+
+:ImportCert
+echo Importing certificate
+
+if not exist "%~dp0certificate.cer" (
+    echo Certificate file does not exist:%~dp0certificate.cer
+    pause
+    exit /b
+)
+
+set "certSubject="
+for /f "tokens=*" %%i in ('certutil -dump "%~dp0certificate.cer" ^| findstr /i "CN="') do (
+    set "certSubject=%%i"
+)
+
+if defined certSubject (
+    echo Deleting existing certificate with the same name
+    certutil -delstore "Root" "%certSubject%"
+    if %errorLevel% equ 0 (
+        echo Existing certificate deleted successfully
+    ) else (
+        echo Failed to delete existing certificate (may not exist)
+    )
+)
+
+echo Importing new certificate
+certutil -addstore -f "Root" "%~dp0certificate.cer"
+if %errorLevel% equ 0 (
+    echo Certificate imported successfully
+) else (
+    echo Certificate import failed
+)
+
+echo All operations completed
+pause

BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam2/certificate.cer


+ 28 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam2/key.pem

@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQClZx/kmGkyrPtp
+AfDgfbYhRImryj4Ez+CFloyZO49bdlFJ4qNrwx3WSdvay4Vk/2jQwAZaRU6ojxt4
+rBbL7VKU/xHd1CPLjoiq8pAVdfoYiJTb3Nj4xsAer+xaCuwfC96aJ/Wgw40JByzr
+BBR2b14GEOx2A6M99e+V2/Wvhc+pUu9h2F6vrND43TNy2UGtAyWQOya4RIubb4zR
+SLjPN4wECjtbZ2Rm1lTB+tOW+Q/PVgVj6YddVONVpaEorpoae5lXzsjpJchegY0U
+JrVUZqII1HWmItzuu5ZZbydBLxKcMcfteRK7awqgUYWTA1RbEkybk69alkylL84h
+D9XOO+0FAgMBAAECggEABfHUm5z1j4b8ojYZ7sC6eeMLlDkK30zUyGYPpUkWuDY+
+IR0cKV2h4Xd6q/OUn8SlFXcIMSHO16JWOUgRhRvvCsBt1RoL6rqzkQy7UPfbYdHG
+2iSm/on4bbQ08nCrMrcHERxzH4wMRHWn2MXwMcPMoLsvjZv/SfmpbQX78fi8EV07
+/99u5lFcIxkF837PLgbXtX0OQY3hjZqbPo429jDWGvGdSdR+Zu+QKh09fGF2brBz
+FXp+TfbZmWFyTI9I6qNPyq1++amlTGidxemSEToCzCMNC5BxPFHyT7VBj4UpmIz/
+0jdvbbIJQ6Xhr1OX5HQdWg2CRWJIVuYEC/pPbcocIQKBgQDh6hpJsw/Upj5NbiHZ
+ecnx3fZk1jxlpe23VAAnH/3ACF8rGSUnsnCSaxONj7cznjf94Nta+i5SX4uSW+KF
+fiY5hjA4GzcFzuRQGu1IJsW9uyYCBsY6hk4CEZEEBSLgUj5N6mUAKG+KMLKlytSd
+3SAReGe2Lrc2FSxQGYkvs1KtewKBgQC7bg219gB18C5fgMiBgq9y8jdusCVfdnvH
+k1vX4c1EDrP7Q+2fs4AwlMtekf3b7UuL5fFRWkSsQ0deI7KE0CrBC+vFEJEX23xV
+Ws09jLQ0P6GHhcyGMO6o2B/U9YKSrVlKJs7VB1VrcpH/Yv9ZVAYoNT6B/Ix8iTyO
+1Ge4+XSHfwKBgBu9MfMgsG3s57N8NV3NrXUsSufAwnO8tv6OquIi9HZS0NSq5rwE
+Ffx0d1cncVg+MiPTKzv1giCNKMcUzzCS98CScHNDLDNjXvdTBxWX5SnRw+31xPtE
+qGlqnMLAmrKuhoXspPArBt8R8a2XxRmJIDnk7d8Zx1c1rFY9fHHF8/3nAoGAAh8+
+3H6KhstPWxl0K7M1FVIUupYX9jq7MAlFEu6lik24T/H28MXxf5tPqiRxAVpwbocN
+8mPZPzILzs8MqGBK+6CM7NBBNEnx4G2EwVukdqr5wzUKmcJYEWVRBvI4pjx8NFC4
+KsVIfEzxxjhyt6ox36aqrIIVfRt4qg1Rl1CLtzUCgYBRpu52mx6JKsWmHcTLCTNU
+Fkl5DVr1qsaWtRUmnlOtS0ZSne+pQERMMqkoDsBa1toGueK4Xy/CI0Kksn/jM/fE
+FDqWHRUjVzw9RMMNrILb+uWuIuPyL4Ro1iVeINnSOLaGQYuYbyW3tb/BEX4lL4ev
++7G4gILX8EFF1TRmfuGGnQ==
+-----END PRIVATE KEY-----

+ 22 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam3/cert.pem

@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDpjCCAo6gAwIBAgIUA3r/m8KYXj7zWeGVj+JSAKZ4npowDQYJKoZIhvcNAQEL
+BQAwbDELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB1NpQ2h1YW4xEDAOBgNVBAcMB0No
+ZW5nRHUxDzANBgNVBAoMBmhhYm9vazELMAkGA1UECwwCcmQxGzAZBgNVBAMMEmV4
+YW0zLmhhYm9vay5sb2NhbDAeFw0yNTAxMjIwMTU4MDZaFw0yNjAxMjIwMTU4MDZa
+MGwxCzAJBgNVBAYTAkNOMRAwDgYDVQQIDAdTaUNodWFuMRAwDgYDVQQHDAdDaGVu
+Z0R1MQ8wDQYDVQQKDAZoYWJvb2sxCzAJBgNVBAsMAnJkMRswGQYDVQQDDBJleGFt
+My5oYWJvb2subG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC1
+yRuBicfR66FpVq0EC9lhAaBgjjJtsZW7t8R0G6xnmrHGHThawLhM9vXHD9nFxIkT
+R+AT9UioJ7+kvoD7LeZ4HcqrV000GYNyV3Sa+vlwt6O3jXtVkGdF/vW/07FMhsUk
+I8nDiwkj2bOmC0/eTDFBfuomVmS1f+a771PzbF82WSfxK+4gl2RKm0lRm075wjzA
+3Bd+7ve4pCasw6ln64+tcUt9SnRe9B/BBULwmHuPOm5xvY9Pg2Juj2lTqfRdm+K0
+zbNf67Ic+nhS6pWb6gx4qAIWtz1kyDavAPo4bHv8GMrNkzUUunOXjAAk9jf6Jyeb
+SbHQr8EKJCiswdvH0F35AgMBAAGjQDA+MB0GA1UdEQQWMBSCEmV4YW0zLmhhYm9v
+ay5sb2NhbDAdBgNVHQ4EFgQUpSH12rGZ955/Nrt2dkYk4GsTyQIwDQYJKoZIhvcN
+AQELBQADggEBAAnuCoDG7o/TNMEqFXIWCuINSpKhkb5N+wsE23W/fJulmAtxifZj
+kBUYfIZW/OdT9xBOtgCCrbu4hjWVjcIfFhyoZ9bI1kWvX6fPcnCNT3qQPCbgZjBc
+sJe7bTl9qLMs58+988KaHl2VIm1z6xDDs+3N1DI2okssKTkEJtTWT8k8SJFczLEn
+2YAmufHfGcyH1KmN3tv2i8lcmhrJju5ljf6ovaYALV/LfcjjspelKdPn8wmTkpFA
+ZqYwUftHHnnm7qy8a485yYA08uHvJXTpLxm8yagr2AKP5vr9YJ+cGrF55jp/NXCd
+HriFs4skM94BJtoZRjVdGZjpG9aYjqNxIjg=
+-----END CERTIFICATE-----

+ 74 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam3/certificate.bat

@@ -0,0 +1,74 @@
+@echo off
+echo Configuring hosts file
+
+net session >nul 2>&1
+if %errorLevel% neq 0 (
+    echo Please run this script as an administrator
+    pause
+    exit /b
+)
+
+set "hostsFile=C:\Windows\System32\drivers\etc\hosts"
+set "newEntry=192.168.8.132 exam3.habook.local"
+
+if not exist "%hostsFile%" (
+    echo hosts file does not exist:%hostsFile%
+    pause
+    exit /b
+)
+
+
+findstr /v /i /c:"exam.habook.local" "%hostsFile%" > "%hostsFile%.tmp"
+if %errorLevel% equ 0 (
+    move /y "%hostsFile%.tmp" "%hostsFile%" >nul 2>&1
+    echo Removed all entries containing exam.habook.local
+) else (
+    echo Failed to remove entries containing exam.habook.local
+    pause
+    exit /b
+)
+
+
+echo %newEntry% >> "%hostsFile%"
+if %errorLevel% equ 0 (
+    echo Hosts file configured successfully
+) else (
+    echo Hosts file configuration failed
+    pause
+    exit /b
+)
+
+:ImportCert
+echo Importing certificate
+
+if not exist "%~dp0certificate.cer" (
+    echo Certificate file does not exist:%~dp0certificate.cer
+    pause
+    exit /b
+)
+
+set "certSubject="
+for /f "tokens=*" %%i in ('certutil -dump "%~dp0certificate.cer" ^| findstr /i "CN="') do (
+    set "certSubject=%%i"
+)
+
+if defined certSubject (
+    echo Deleting existing certificate with the same name
+    certutil -delstore "Root" "%certSubject%"
+    if %errorLevel% equ 0 (
+        echo Existing certificate deleted successfully
+    ) else (
+        echo Failed to delete existing certificate (may not exist)
+    )
+)
+
+echo Importing new certificate
+certutil -addstore -f "Root" "%~dp0certificate.cer"
+if %errorLevel% equ 0 (
+    echo Certificate imported successfully
+) else (
+    echo Certificate import failed
+)
+
+echo All operations completed
+pause

BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam3/certificate.cer


+ 28 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam3/key.pem

@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1yRuBicfR66Fp
+Vq0EC9lhAaBgjjJtsZW7t8R0G6xnmrHGHThawLhM9vXHD9nFxIkTR+AT9UioJ7+k
+voD7LeZ4HcqrV000GYNyV3Sa+vlwt6O3jXtVkGdF/vW/07FMhsUkI8nDiwkj2bOm
+C0/eTDFBfuomVmS1f+a771PzbF82WSfxK+4gl2RKm0lRm075wjzA3Bd+7ve4pCas
+w6ln64+tcUt9SnRe9B/BBULwmHuPOm5xvY9Pg2Juj2lTqfRdm+K0zbNf67Ic+nhS
+6pWb6gx4qAIWtz1kyDavAPo4bHv8GMrNkzUUunOXjAAk9jf6JyebSbHQr8EKJCis
+wdvH0F35AgMBAAECggEAE2SWdh3MPuVLzXCNOZrIAHTLdrGEyLYCuslygE55eH4E
+zBVO70OTBcbs1mUm/tWmJ/PpgEeRDjtbUwhtux4c8aCAAAJqvo2gO8D/tA7lMHSu
+1wSVbT3f/pQiBGphhj/0ZRQaUK2S9ouhgiu/w//N22ZeNWPPD5vK8i6ofpYHnET9
+AuHm/7Toh3eUSpOCoCxKSAtyWbg/9Vr1CAjAp4mAlFYHtO8rm7/yGszUIofWz1hc
++EntN2FV7auXZdJyo5a5tbOKd9Ii0NlEHAXxmFHcunjl68OonScNEFfRDF9YJTxq
+TEtDP5cTRU2TnkfEVHw92XGaXmRU34DxSP6D1ZnaxwKBgQD9LuG02IKZIVB6n4xb
+kNzJcwoGNo948LTXCJE2yKU+iK99lz5vNJyyFaLfYY5xbuQZRf7dcPgTyUuRS+ur
+oS6ruX5Z9j+FdCJwAUdjOYXNQwtqwCAq6IorbNTrTY8WA6cAmzUlZdlZsa/gScZN
+IwEsTW0RBeocFqKR6fHAjIAOZwKBgQC3zt7jfI4hSVPAIpFn5YDU5DGVenELyPHg
+3D7fNtvVbKX5gmtr24rJmlxCFCOREUNyF4S2nj6oql9I9PJadPBmCSju2OhnD/zj
+TtWc0SyuN3AWpU+2WS02B5v/PSm5NrnUkddU3835de5Ajzah38AfTyB3RRSBWSie
+ui3LXkG0nwKBgQDmZOEz5sqBetV3oPTi5mJkV3FJ0iChV7nY7Izoo5Hr5Ap+aUGB
+hQkK7bF3QZmUE+syLIYPERxPNSC1KbdeSaDk2Dnot7N6SAKGlkNWQiRGc7pR/F/J
+bC8/8RuYvM12pQA2BwdH7vTpME7x9XClBTOuEw8rLIyr3tU1nwRFyhVGBQKBgG+R
+njEvFYaW38MsOF58LCsNKBu3ao7gthDrHy6WOfHeNRCyXUnmxOOCUqW5W61ecTX7
+mI7QlvACGjxKkiDeAl2tCa+Q8eA9EB9ZZsf9H8XP2LWmOjBRCORW0hWnpn/J/BpP
+PAlEn93f+3Ise9jd29wlR2ud/shUuhc+ozViTTe5AoGBAIIh/D7tbB4Zhxz9gcGG
+bEFMruveLIDxrs5Bqt33tcaZdYmnQ+OFhuMjDtZ4cJAERHSrnBVWb2x+cl/lxgOX
+1VAWCWFPAquxMFGkh2Xdcpd4UKgrv0YJEd1r8J9s6574fqYvJkzNP/Ph3VOYUMxg
+m51TzcO13938amX0MiII0PVk
+-----END PRIVATE KEY-----

+ 22 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam4/cert.pem

@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDpjCCAo6gAwIBAgIUWa2IOFQksnU2Vb1Z/YDmaKflvFAwDQYJKoZIhvcNAQEL
+BQAwbDELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB1NpQ2h1YW4xEDAOBgNVBAcMB0No
+ZW5nRHUxDzANBgNVBAoMBmhhYm9vazELMAkGA1UECwwCcmQxGzAZBgNVBAMMEmV4
+YW00LmhhYm9vay5sb2NhbDAeFw0yNTAxMjIwMTU4MjVaFw0yNjAxMjIwMTU4MjVa
+MGwxCzAJBgNVBAYTAkNOMRAwDgYDVQQIDAdTaUNodWFuMRAwDgYDVQQHDAdDaGVu
+Z0R1MQ8wDQYDVQQKDAZoYWJvb2sxCzAJBgNVBAsMAnJkMRswGQYDVQQDDBJleGFt
+NC5oYWJvb2subG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDp
+u91Jfyvh421jTABrq0M/gX1GQXZ6DZE81M9853TmX9S6Hh59nFFYTZ/h62LjKQyY
+6kMWzOvCQeSPmsbOCT7iUuvYa8jMUdxQYGd974uu0fk1TS58ZGyH9M02iMceRdmR
+Z1zc6ojfhLMgCcKAkC8yd5RuqGzqAPXD/lf/p6RBICwdRFMLxvCigP2wsDCkGm+u
+EAdQFIQGdoUdJDr73H9almIomJ/rAwXBPbeYp2TgWzzD1TlsWS2MgfWPZICiAz0I
+aO+a2518jeLGQHi80LIvr1RFwXS75nlSk0WDYevpXr6946VML+wUY1X9/vrlmBvo
+QRzuwfvXHTWGDu679I1hAgMBAAGjQDA+MB0GA1UdEQQWMBSCEmV4YW00LmhhYm9v
+ay5sb2NhbDAdBgNVHQ4EFgQUt44zMPIaFyMJP6m/DoAVwd6cskgwDQYJKoZIhvcN
+AQELBQADggEBABp8wbwAdrKC3JkcgiywrGLtIQ0+6bunZ1lCc655zoVfFU5Xln3b
+mO6hXMMhN/u5ySItmaSX3T/zEA8j57L9XMvZk3+scS5JHOD9NW8pqsnnVLFU3mFO
+MGfAOqispu+HRI4hpjK7J+LlGSn1ftng6QHrgp/j1YAmrG0SxOuPHVpnvk9Lfl8c
+COCGkR4q1w8YVBn75ca6tX8TNhqZz26H21OM70XfdFe/cK3XrDOwFk62fsCyeofW
+0izDItZCi9lPHKuBOwUcKnkk3d2qJ5SyoDgQ7KvRu+JgnARi0vVS5CopuibTSmyC
+IVJ7C+L5c+zQeQk/uKLGC9pOJW9+VHoulpc=
+-----END CERTIFICATE-----

+ 74 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam4/certificate.bat

@@ -0,0 +1,74 @@
+@echo off
+echo Configuring hosts file
+
+net session >nul 2>&1
+if %errorLevel% neq 0 (
+    echo Please run this script as an administrator
+    pause
+    exit /b
+)
+
+set "hostsFile=C:\Windows\System32\drivers\etc\hosts"
+set "newEntry=192.168.8.132 exam4.habook.local"
+
+if not exist "%hostsFile%" (
+    echo hosts file does not exist:%hostsFile%
+    pause
+    exit /b
+)
+
+
+findstr /v /i /c:"exam.habook.local" "%hostsFile%" > "%hostsFile%.tmp"
+if %errorLevel% equ 0 (
+    move /y "%hostsFile%.tmp" "%hostsFile%" >nul 2>&1
+    echo Removed all entries containing exam.habook.local
+) else (
+    echo Failed to remove entries containing exam.habook.local
+    pause
+    exit /b
+)
+
+
+echo %newEntry% >> "%hostsFile%"
+if %errorLevel% equ 0 (
+    echo Hosts file configured successfully
+) else (
+    echo Hosts file configuration failed
+    pause
+    exit /b
+)
+
+:ImportCert
+echo Importing certificate
+
+if not exist "%~dp0certificate.cer" (
+    echo Certificate file does not exist:%~dp0certificate.cer
+    pause
+    exit /b
+)
+
+set "certSubject="
+for /f "tokens=*" %%i in ('certutil -dump "%~dp0certificate.cer" ^| findstr /i "CN="') do (
+    set "certSubject=%%i"
+)
+
+if defined certSubject (
+    echo Deleting existing certificate with the same name
+    certutil -delstore "Root" "%certSubject%"
+    if %errorLevel% equ 0 (
+        echo Existing certificate deleted successfully
+    ) else (
+        echo Failed to delete existing certificate (may not exist)
+    )
+)
+
+echo Importing new certificate
+certutil -addstore -f "Root" "%~dp0certificate.cer"
+if %errorLevel% equ 0 (
+    echo Certificate imported successfully
+) else (
+    echo Certificate import failed
+)
+
+echo All operations completed
+pause

BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam4/certificate.cer


+ 28 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/exam4/key.pem

@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDpu91Jfyvh421j
+TABrq0M/gX1GQXZ6DZE81M9853TmX9S6Hh59nFFYTZ/h62LjKQyY6kMWzOvCQeSP
+msbOCT7iUuvYa8jMUdxQYGd974uu0fk1TS58ZGyH9M02iMceRdmRZ1zc6ojfhLMg
+CcKAkC8yd5RuqGzqAPXD/lf/p6RBICwdRFMLxvCigP2wsDCkGm+uEAdQFIQGdoUd
+JDr73H9almIomJ/rAwXBPbeYp2TgWzzD1TlsWS2MgfWPZICiAz0IaO+a2518jeLG
+QHi80LIvr1RFwXS75nlSk0WDYevpXr6946VML+wUY1X9/vrlmBvoQRzuwfvXHTWG
+Du679I1hAgMBAAECggEACcxDgjd3xDQXq4wtz1WiT8jykq2fVQ35CuMD4KFGO/sD
+7JpjHlJdcZVtcJfAsTOREN7vGYdVEbNX7LIpB82I6o1d0BhyligPU3o7FGMkiJtK
+NlMKMhI2n3tiV9kjebAlZuw9jxAw1SfXp23pTqDcraGoIfxi9ms8Yi8pCrFBGC63
+zx1JzAvFSrfybWT8ExjslID3VX9AhYahlJyXZ6fayTwqFn5FQtqzpa070gnEBshB
+mXJ8Lb88pVvkPjWjHMlcUa3whTtZ+FvIozz2Fe0aRLICplitYzQKpqgIAuyaiWeI
+MDbYYBKgmGHCwM0S6EGBWPAw19BwTxT9Hm4ehVdoQQKBgQD6c+POW6bd1Lg31Sjl
+GiabN2bb2S9hI2WnfsYlYBX2mW4/WPvRK5ckgluPai5tt9zQ71FOIZ7U0HV4tMZG
+Fcu+dn+mBgRmcEjqI+/Uxy0SxZmZYbT4Row178Mc66iplUNkRNfZNQdo+RWQc3e7
+pUgoNT5I9hWNUZZTuCb6T4c+UQKBgQDu6Sz+6hRY7QZ4b7MMQl0oc9B8PDRRVi47
+aLlbT3rrtSn4Ng7vNfR80+tQBATmDe7twaM9kzumRzFDO8YO5eiyiicknjG/cyrn
+QM/aaa6xiMNYsClOCmJEcGyfUDeqvZW4AbiBbzGU+U4V0D8CjsIXxEsK8xRd+r9+
+94ONsRZKEQKBgQDlbTIbAERs44dZxg+vR3RkLa0w6dSINIfTlsNmy41zF/wxVY9g
+foD3Nd8wEkGzyoEieIhPfWblZoyl5VvYKfE5aY3nhY2UVXnF2uPBVC/LCW56XlIM
+OXwJkLh4jrwzlQNmH9ZnKDfAhqSlbdEZ+P+Pra5/4cW+biW6TXCPTjkC8QKBgQCW
+XP0rxuldenhLDooS2iXFkvaRalbHJkVcsLGQLXrApLmwdV164mtosPv11UG8BOLu
+kqOJ2oN9SGtR1Gn38G0/CUJPgpzu0K4c86ad4UvIgrnntJ+adWCZkGRc9GDEviNf
+tv2HwRLknu+tPO6bTnwL90f/sTONXoZtg5wxblYH8QKBgQDHHWcdtcxWdOAZQ5Tp
+sPRF0tRH3M261qt1OivhH1o0foKnCQKygThkYN8JoeAG8i1Z/b9D9A9CvDlGfaI9
+YV9V/FjHPrWEWO+5fCquIvO3kI0gpt77pdVp0G67IhqAwYQqniSSNIrfR9J32oEL
+kSQjjBj3xAi7wVNiXEH8Eyinng==
+-----END PRIVATE KEY-----

BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/logo.png


+ 22 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/template/cert.pem

@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDozCCAougAwIBAgIUXVG28xNwGPdDM2SRRiCQhDTZG60wDQYJKoZIhvcNAQEL
+BQAwazELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB1NpQ2h1YW4xEDAOBgNVBAcMB0No
+ZW5nRHUxDzANBgNVBAoMBmhhYm9vazELMAkGA1UECwwCcmQxGjAYBgNVBAMMEWV4
+YW0uaGFib29rLmxvY2FsMB4XDTI1MDEyMTA0MzM0NVoXDTI2MDEyMTA0MzM0NVow
+azELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB1NpQ2h1YW4xEDAOBgNVBAcMB0NoZW5n
+RHUxDzANBgNVBAoMBmhhYm9vazELMAkGA1UECwwCcmQxGjAYBgNVBAMMEWV4YW0u
+aGFib29rLmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxAl2
+7myycEOgiO0OEohPYDV/1raik81QuU3485caFoxs2UUXJoW0zkG0TKHVqVJGcNbg
+xncN7gjThRAKy4PSLTrn9A9fPJVqRpB0ElJlBsChhfXxpWzrFl/wHOxdmYz5rUjs
+gjq0zdlodSQ2yWE0VdZhI6VqIbvz16rHXj19B1kKcGVzlnwuRxuqldDEJSUD3UTe
+epPGaSbdqI4riUcOaksEMEAgll7HqixTv5tB3/aKJ0w+nxZFimocdA/XFXIXMO1M
+lLP2CCck1Jv7vlAL/TP22wQnfINifYDisTmsdtFvq6hgkFgh34lDX+moM92MPR5/
+m5D4GQi6BYtFpOoC+wIDAQABoz8wPTAcBgNVHREEFTATghFleGFtLmhhYm9vay5s
+b2NhbDAdBgNVHQ4EFgQUXr4ImISHw4eBN6r8MO3LiM0o+5gwDQYJKoZIhvcNAQEL
+BQADggEBAIFTbpGUwtpMa//mrHGghAcF+jw3+6G/I6K0bsfcFfYVQJCwJ+HcNWDJ
++nCn64rdu5sQvFmpPSlXBMbFoW/ZAxs2g7jhk90MUrR0MFBu91qAu2gqwE55S6xr
+O7fg4sd0aVvR2tE2e6wxMaLGTfNtF9Uo0tKFehDUQXIiCSAQkLGwsWneZ+GRE8Dc
+CwGjJXpwvBfmcQoNMLqZpZ8P8AdOUVjp7PIEkWJWSGcaa+RCrP4qhVwNv+ZloBbc
+XBNxCrftg2epupYOGvsq5ACX7u3nLbRfuKu3X+E0F7V/Qwmjfc9+WS50LouCMIx/
+TvVawMxytPaTUlQe45aNANW4reHWezQ=
+-----END CERTIFICATE-----

+ 74 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/template/certificate.bat

@@ -0,0 +1,74 @@
+@echo off
+echo Configuring hosts file
+
+net session >nul 2>&1
+if %errorLevel% neq 0 (
+    echo Please run this script as an administrator
+    pause
+    exit /b
+)
+
+set "hostsFile=C:\Windows\System32\drivers\etc\hosts"
+set "newEntry=192.168.8.132 exam.habook.local"
+
+if not exist "%hostsFile%" (
+    echo hosts file does not exist:%hostsFile%
+    pause
+    exit /b
+)
+
+
+findstr /v /i /c:"exam.habook.local" "%hostsFile%" > "%hostsFile%.tmp"
+if %errorLevel% equ 0 (
+    move /y "%hostsFile%.tmp" "%hostsFile%" >nul 2>&1
+    echo Removed all entries containing exam.habook.local
+) else (
+    echo Failed to remove entries containing exam.habook.local
+    pause
+    exit /b
+)
+
+
+echo %newEntry% >> "%hostsFile%"
+if %errorLevel% equ 0 (
+    echo Hosts file configured successfully
+) else (
+    echo Hosts file configuration failed
+    pause
+    exit /b
+)
+
+:ImportCert
+echo Importing certificate
+
+if not exist "%~dp0certificate.cer" (
+    echo Certificate file does not exist:%~dp0certificate.cer
+    pause
+    exit /b
+)
+
+set "certSubject="
+for /f "tokens=*" %%i in ('certutil -dump "%~dp0certificate.cer" ^| findstr /i "CN="') do (
+    set "certSubject=%%i"
+)
+
+if defined certSubject (
+    echo Deleting existing certificate with the same name
+    certutil -delstore "Root" "%certSubject%"
+    if %errorLevel% equ 0 (
+        echo Existing certificate deleted successfully
+    ) else (
+        echo Failed to delete existing certificate (may not exist)
+    )
+)
+
+echo Importing new certificate
+certutil -addstore -f "Root" "%~dp0certificate.cer"
+if %errorLevel% equ 0 (
+    echo Certificate imported successfully
+) else (
+    echo Certificate import failed
+)
+
+echo All operations completed
+pause

BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/template/certificate.cer


+ 28 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/template/key.pem

@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDECXbubLJwQ6CI
+7Q4SiE9gNX/WtqKTzVC5TfjzlxoWjGzZRRcmhbTOQbRModWpUkZw1uDGdw3uCNOF
+EArLg9ItOuf0D188lWpGkHQSUmUGwKGF9fGlbOsWX/Ac7F2ZjPmtSOyCOrTN2Wh1
+JDbJYTRV1mEjpWohu/PXqsdePX0HWQpwZXOWfC5HG6qV0MQlJQPdRN56k8ZpJt2o
+jiuJRw5qSwQwQCCWXseqLFO/m0Hf9oonTD6fFkWKahx0D9cVchcw7UyUs/YIJyTU
+m/u+UAv9M/bbBCd8g2J9gOKxOax20W+rqGCQWCHfiUNf6agz3Yw9Hn+bkPgZCLoF
+i0Wk6gL7AgMBAAECggEABMSeKepm5KggbXQmlXjPRW3HsDc8+Q9TWU52MaaJMec0
+doxhpcQ2w5WBhyOgiL/BitkPGoSmyBVa36+mM5D/Oa5nGd6N2HFh5ll5GUD7yBBD
+XXi/6eAeT6sBshJlrGhrYjWV2w0GfMM+8SpyUq+UkEnoju9lB3EE20gCFV61fco3
+pK7en/hHCK0/jLDJYxEhEk5FMKA0BAUdsoD2WRVCnakCSaKRJzJEafL9CHQaL/oq
+pPVIYO3tx0ryNtdcLsc0j01rLMJ8n9lG59+3zAu5nCEVHYKO7oPuj8kpwdqxIj9J
+ZnSHX9ayJYNOdJDGD8zQYKhZQdquOYOAE9NnbINAMQKBgQD+OCf02IIuK4NcU1Ef
+wlx/sRjvCIWq39S1KNQ0kPtI4dWUNLI5DJqVQgylHIkVC/FNbTcaW1A6nVqukJfJ
+Nu3HrZoN+585QRKesTGNd45Zmwwy8JuV1OblV6SbI5xZ3g3S4SVkWnldOz36FThn
+wdHf2lg/unGzX4ojgYfuV1VpfwKBgQDFaPsd7Lo9oDUEZH4GDjk8onsIWwad6Nut
+3CCdBn3tiSlSAddVbZfu60VtM25CA9K1UbPsV1FU7ZYX1wKma019r1VqkwhPMWoq
+FsnhoHMOl69vV/3brjfHZDflbCMbMt/vYc91kRS7nVDAeqz5sDf4mJ7BSs70ZGth
+oAqHLQfMhQKBgQDhg71mRX5OKMmR6FMpwkg9+kNtIHk7GO5feoWs0AQqJjRKEekc
+FKM4zuvauJKeegaoMb9VATYNmTMtchVEKRcMMGNeDh20M5ap8fRMU4eS06khszHB
+26isQHBEM3XqfsJylMmP2XaaDwiuxY5Q9K4ST2ZDukhM3+7yCmEkPJMHTwKBgEEK
+DXIWhGW5Wr5PvZWRKhpoDdD67HsqNPZbCAO0F9kiz5JNOPzUVrJIoV8RCsqFJ+7F
+NFoxioJIpKLGHAFoaOd31NSADMTKqwei6nCDxGSSZSJyAxlVlNsEkcXsksRrRow/
+1XIOkp4dfnVr9YFuJYKqBeP5GaY7T4WijNVsaJ1hAoGAGQD8EyQXcfsaIEjaPQ65
+io+DloCFqwJd5EkCFPyv7Qz+5A9Jv6/RTznXSXtNBv+j6t7MG/o6A8ZX0ZR5UjuG
+WB/U87QpUokC+wzNTCdbDWaF/GvOU8lziksDrMMzrV8Yy+CVtGNiscAZyj6QJ1Am
+WrPltUEZoXxKkRRcmvNeti8=
+-----END PRIVATE KEY-----

+ 104 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/BaseController.cs

@@ -0,0 +1,104 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace IES.ExamServer.Controllers
+{
+    public class BaseController : ControllerBase
+    {
+        public BaseController()
+        {
+
+
+        }
+        public string GetIP()
+        {
+            var IpPort = HttpContext.Request.Headers["X-Forwarded-For"].FirstOrDefault();
+
+            if (string.IsNullOrEmpty(IpPort))
+            {
+                IpPort = $"{HttpContext.Connection.RemoteIpAddress}";
+            }
+            if (IpPort.Contains("::"))
+            {
+                IpPort = "127.0.0.1";
+            }
+            return IpPort;
+        }
+        public string GetCookie(string key)
+        {
+            IRequestCookieCollection cookies = HttpContext.Request.Cookies;
+            string value = "";
+            if (cookies != null)
+            {
+                foreach (var ck in cookies)
+                {
+                    if (ck.Key.Equals(key))
+                    {
+                        value = ck.Value;
+                        break;
+                    }
+                }
+            }
+            return value;
+        }
+        /// <summary>
+        /// 取得AuthToken權杖資訊
+        /// </summary>        
+        /// <param name="key">Key Name</param>
+        /// <returns></returns>
+        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}",$"{scope}",$"{timeZone}", rolse, $"{keyData}");
+        }
+
+       
+        /// <summary>
+        /// 取得驗證金鑰,Authorization
+        /// </summary>        
+        public string GetToken()
+        {
+            return HttpContext.Request.Headers["Authorization"].ToString();
+        }
+
+        /// <summary>
+        /// 取得JWT驗證金鑰,Authorization Bearer
+        /// </summary>
+        /// <param name="httpContext"></param>
+        /// <returns></returns>
+        public string GetJwtToken()
+        {
+            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;
+        }
+
+
+
+        public   int code = 0;
+        public   string msg = "OK";
+
+
+    }
+}

+ 416 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/IndexController.cs

@@ -0,0 +1,416 @@
+using IES.ExamServer.Models;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Caching.Memory;
+using System.Diagnostics;
+using System.Text.Json.Nodes;
+using System.Text.Json;
+using IES.ExamServer.Helper;
+using System.DrawingCore.Imaging;
+using System.DrawingCore;
+using System.IdentityModel.Tokens.Jwt;
+using IES.ExamServer.Services;
+using IES.ExamServer.DI;
+using IES.ExamServer.Helpers;
+using IES.ExamLib.Models;
+
+namespace IES.ExamServer.Controllers
+{
+    [ApiController]
+    [Route("index")]
+  
+    public class IndexController : BaseController
+    {
+        private readonly IConfiguration _configuration;
+        private readonly IHttpClientFactory _httpClientFactory;
+        private readonly IMemoryCache _memoryCache;
+        private readonly ILogger<IndexController> _logger;
+        private readonly CenterServiceConnectionService _connectionService;
+        private readonly LiteDBFactory _liteDBFactory;
+        public IndexController(ILogger<IndexController> logger, IConfiguration configuration, IHttpClientFactory httpClientFactory, IMemoryCache memoryCache,CenterServiceConnectionService connectionService, LiteDBFactory liteDBFactory)
+        {
+            _logger = logger;
+            _configuration=configuration;
+            _httpClientFactory=httpClientFactory;
+            _memoryCache=memoryCache;
+            _connectionService=connectionService;
+            _liteDBFactory=liteDBFactory;
+        }
+        [HttpPost("generate-certificate")]
+        public async Task<IActionResult> GenerateCertificate(JsonNode json) 
+        {
+            try {
+                ServerDevice serverDevice = _memoryCache.Get<ServerDevice>(Constant._KeyServerDevice);
+                if (serverDevice!=null && serverDevice.networks.IsNotEmpty())
+                {
+                    string mac = $"{json["mac"]}";
+                    var network = serverDevice.networks.Find(x => mac.Equals(x.mac));
+                    if (network!=null)
+                    {
+                        string pathBat = Path.Combine(Directory.GetCurrentDirectory(), "Configs", "template", "certificate.bat");
+                        string pathCer = Path.Combine(Directory.GetCurrentDirectory(), "Configs", "template" ,"certificate.cer");
+                        string text = await System.IO.File.ReadAllTextAsync(pathBat);
+                        text = text.Replace("192.168.8.132", network.ip);
+                        string pathCertificateBat = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "package", "certificate.bat");
+                        await System.IO.File.WriteAllTextAsync(pathCertificateBat, text);
+                        string pathCertificateCer = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "package", "certificate.cer");
+                        System.IO.File.Copy(pathCer, pathCertificateCer);
+                        return Ok(new { code = 200, msg = "生成成功。", bat = "package/certificate.bat", cer = "package/certificate.cer" });
+                    }
+                    else
+                    {
+                        msg="未找到匹配的网卡设备。";
+                    }
+                }
+                else
+                {
+                    msg="服务端设备未找到,或网卡设备不存在。";
+                }
+            } catch (Exception ex) 
+            {
+                _logger.LogError($"生成证书和自定义域名映射脚本错误。{ex.Message},{ex.StackTrace}");
+                msg=$"服务端错误,{ex.Message}";
+            }
+            return Ok(new { code = 400, msg = msg });
+        }
+        [HttpPost("list-schools")]
+        public async Task<IActionResult> ListSchool(JsonNode json) 
+        {
+            string filePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "package", "schools.json");
+            string schoolText = await System.IO.File.ReadAllTextAsync(filePath);
+            JsonNode? node = schoolText.ToObject<JsonNode>();
+            return Ok(new {code =200, schools= node?["schools"] });
+        }
+        [HttpPost("bind-school")]
+        public async Task<IActionResult> BindSchool(JsonNode json) 
+        {
+            string id=$"{json["id"]}";
+            string name= $"{json["name"]}";
+            string fp = $"{json["fp"]}";
+            if (!string.IsNullOrWhiteSpace(id) && !string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(fp))
+            {
+                string filePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "package", "schools.json");
+                string schoolText = await System.IO.File.ReadAllTextAsync(filePath);
+                List<School>? schools = schoolText.ToObject<JsonNode>()?["schools"]?.ToObject<List<School>>();
+                School? school = schools?.Find(x => id.Equals(x.id)  && name.Equals(x.name));
+                if (school!=null)
+                {
+                    _liteDBFactory.GetLiteDatabase().GetCollection<School>().DeleteAll();
+                    _liteDBFactory.GetLiteDatabase().GetCollection<School>().Upsert(school);
+                    IEnumerable<School> schoolsDb = _liteDBFactory.GetLiteDatabase().GetCollection<School>().FindAll();
+                    School? schoolDb = schools?.FirstOrDefault();
+                    _memoryCache.TryGetValue(Constant._KeyServerDevice, out ServerDevice? server);
+                    if (server!=null)
+                    {
+                        server.school = school;
+                    }
+                    string ip = GetIP();
+                    var device = IndexService.GetDeviceInit(HttpContext, $"{fp}", ip, _memoryCache);
+                    int hybrid = 0;
+                    _memoryCache.Set(Constant._KeyServerDevice, server);
+                    _memoryCache.TryGetValue(Constant._KeyServerCenter, out JsonNode? data);
+                    _memoryCache.TryGetValue(Constant._KeyServerDevice, out server);
+                    if (data!=null)
+                    {
+                        hybrid=1;
+                        msg="云端服务连接成功!";
+                        return Ok(new { code = 200, msg, data = new { hybrid, device, centerUrl = data["centerUrl"], region = data["region"], ip = data["ip"], nowtime = DateTimeOffset.Now.ToUnixTimeMilliseconds(), server } });
+                    }
+                    else
+                    {
+                        msg="云端服务未连接!";
+                        return Ok(new { code = 200, msg, data = new { hybrid, device, centerUrl = "", region = "局域网·内网", ip = ip, nowtime = DateTimeOffset.Now.ToUnixTimeMilliseconds(), server } });
+                    }
+                }
+                else
+                {
+                    return Ok(new { code = 400, msg = "绑定失败!" });
+                }
+            }
+            else {
+                return Ok(new { code = 400, msg = "参数错误!" });
+            }
+        }
+        [HttpPost("device")]
+        public IActionResult Device(JsonElement json )
+        {
+            try
+            {
+                string ip=   GetIP();
+                json.TryGetProperty("fp", out JsonElement fp);
+                var device = IndexService.GetDeviceInit(HttpContext, $"{fp}", ip, _memoryCache);
+                int hybrid = 0, notify = 0 ;
+                _memoryCache.TryGetValue(Constant._KeyServerCenter, out JsonNode? data);
+                _memoryCache.TryGetValue(Constant._KeyServerDevice, out ServerDevice? server);
+                if (_connectionService.notifyIsConnected)
+                { 
+                    notify=1;
+                }
+                if (_connectionService.centerIsConnected)
+                {
+                    hybrid=1;
+                }
+                msg="服务端检测成功!";
+                return Ok(new { code = 200, msg, data = new {
+                    notify,
+                    hybrid,
+                    device,
+                    centerUrl = hybrid==1 ? _connectionService.centerUrl : "",
+                    notifyUrl = notify==1 ? _connectionService.notifyUrl : "",
+                    region = data?["region"],
+                    ip = data!=null ? data?["ip"] : ip,
+                    nowtime = DateTimeOffset.Now.ToUnixTimeMilliseconds(),
+                    server
+                } });
+            }
+            catch (Exception ex)
+            {
+                code=500;
+                msg="服务端异常!";
+            }
+            return Ok(new { code, msg });
+        }
+
+
+        /**
+        {
+            "type":"sms",//qrcode二维码扫码登录:randomCode必传;  sms 短信验证登录:randomCode必传,mobile必传
+            "randomCode",
+            "mobile":"1528377****"
+        }
+        **/
+        /// <summary>
+        /// 登录验证
+        /// </summary>
+        /// <param name="randomCode"></param>
+        /// <returns></returns>
+        [HttpPost("login-check")]
+        public async Task<IActionResult> LoginCheck(JsonNode json)
+        {
+            try
+            {
+                var type = json["type"];
+                string? CenterUrl = _configuration.GetValue<string>("ExamServer:CenterUrl");
+                if (!string.IsNullOrWhiteSpace($"{type}"))
+                {
+                    TmdidImplicit? token = null;
+                    string x_auth_token = string.Empty;
+                    List<School>? schools = null;
+                    JsonNode? jsonNode = null;
+                    long time = DateTimeOffset.Now.ToUnixTimeMilliseconds();
+                    _memoryCache.TryGetValue(Constant._KeyServerDevice, out ServerDevice? server);
+                    School? school = null;
+                    if (server!=null) 
+                    {
+                    school = server.school;
+                    }
+                    switch (true)
+                    {
+                        //跳过忽略,但是仍然要以访客身份登录
+                        case bool when $"{type}".Equals(ExamConstant.ScopeVisitor):
+                            {
+                                string id = $"{DateTimeOffset.Now.ToUnixTimeSeconds()}";
+                                string name = $"{school?.name}教师-{Random.Shared.Next(100, 999)}";
+                                x_auth_token = JwtAuthExtension.CreateAuthToken("www.teammodel.cn",id ,name,picture: school?.picture
+                                    , ExamConstant.JwtSecretKey,ExamConstant.ScopeVisitor,8,schoolID:school?.id,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"]}";
+                                System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12;
+                                var rc= _memoryCache.Get<string>($"Login:ExamServer:{school?.id}:{randomCode}");
+                                if (!string.IsNullOrWhiteSpace(rc))
+                                {
+                                    var response = await _httpClientFactory.CreateClient().GetAsync($"{CenterUrl}/core/qrcode/check?randomcode={randomCode}&school={school?.id}&client=ExamServer");
+                                    if (response.IsSuccessStatusCode)
+                                    {
+                                        string content = await response.Content.ReadAsStringAsync();
+                                        if (!string.IsNullOrWhiteSpace(content))
+                                        {
+                                            jsonNode = content.ToObject<JsonNode>();
+                                        }
+                                        else
+                                        {
+                                            code=400;
+                                            msg="随机码验证失败";
+                                        }
+                                    }
+                                    else
+                                    {
+                                        code=400;
+                                        msg="随机码验证错误";
+                                    }
+                                }
+                                else
+                                {
+                                    code=400;
+                                    msg="二维码过期";
+                                }
+                                break;
+                            }
+                        case bool when $"{type}".Equals("smspin"):
+                            {
+                                string pin_code = $"{json["pin_code"]}";
+                                string account = $"{json["account"]}";
+                                var response = await _httpClientFactory.CreateClient().PostAsJsonAsync($"{CenterUrl}/core/sendsms/check", new { pin_code, account,school=school?.id });
+                                if (response.IsSuccessStatusCode)
+                                {
+                                    string content = await response.Content.ReadAsStringAsync();
+                                    if (!string.IsNullOrWhiteSpace(content))
+                                    {
+                                        jsonNode = content.ToObject<JsonNode>();
+                                    }
+                                    else
+                                    {
+                                        code=400;
+                                        msg="短信验证返回结果为空";
+                                    }
+                                }
+                                else
+                                {
+                                    code=400;
+                                    msg="短信验证错误";
+                                }
+                                break;
+                            }
+
+                    }
+                    if (jsonNode != null  && $"{jsonNode["code"]}".Equals("200"))
+                    {
+                        token =jsonNode["implicit_token"]?.ToObject<TmdidImplicit>(); 
+                        x_auth_token = $"{jsonNode["x_auth_token"]}";
+                        schools =jsonNode["schools"]?.ToObject<List<School>>(); 
+                        var jwt = new JwtSecurityToken(token?.id_token);
+                        var id = jwt.Payload.Sub;
+                        jwt.Payload.TryGetValue("name", out object? name);
+                        jwt.Payload.TryGetValue("picture", out object? picture);
+                        //_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
+                    {
+                        code=400;
+                        msg="验证失败";
+                    }
+                }
+                else
+                {
+                    code=400;
+                    msg="参数错误";
+                }
+            }
+            catch (Exception ex)
+            {
+                code=500;
+                msg="异常错误";
+            }
+            return Ok(new { code = code,msg });
+        }
+        /*
+     
+         
+         
+         */
+        /// <summary>
+        /// 登录模式初始化
+        /// </summary>
+        /// <returns></returns>
+        [HttpPost("login-init")]
+        public async Task<IActionResult> LoginInit(JsonNode json)
+        {
+            var type = json["type"];
+            string qrcode = string.Empty;
+            string randomcode = "";
+            _memoryCache.TryGetValue(Constant._KeyServerDevice, out ServerDevice? server);
+            School? school = null;
+            if (server != null)
+            {
+                school = server.school;
+            }
+            if (school != null)
+            {
+                switch (true)
+                {
+                    case bool when $"{type}".Equals("qrcode"):
+                        {
+                            //.NET Core使用SkiaSharp快速生成二维码  https://cloud.tencent.com/developer/article/2336486
+
+                            // 生成二维码图片
+                            Random random = new Random();
+                            randomcode = $"{random.Next(1000, 9999)}";
+                            string? CenterUrl = _configuration.GetValue<string>("ExamServer:CenterUrl");
+                            CenterUrl = CenterUrl.Equals("https://localhost:5001") ? "https://www.teammodel.cn" : CenterUrl;
+                            string content = $"{CenterUrl}/qrcodelogin?randomcode=Login:ExamServer:{school?.id}:{randomcode}&m=%E6%89%AB%E7%A0%81%E7%99%BB%E5%BD%95&o=1";
+                            var str = QRCodeHelper.GenerateQRCode(content, 300, 300, QRCodeHelper.logo);
+                            qrcode = $"data:image/png;base64,{str}";
+                            int ttl = 60;
+                            string  key = $"Login:ExamServer:{school?.id}:{randomcode}";
+                            _memoryCache.Set(key, randomcode, TimeSpan.FromSeconds(ttl));
+                            var device =IndexService.GetDevice(HttpContext,_memoryCache);
+
+                            _memoryCache.Set($"device:{key}", device);
+                            try {
+                                if (_connectionService.notifyIsConnected && _connectionService.serverDevice!=null) 
+                                {
+                                    string url = $"{_connectionService.notifyUrl!}/third/ies/qrcode-login-register";
+                                    //扫描登录,远端设备登记临时随机码
+                                    await _httpClientFactory.CreateClient().PostAsJsonAsync(url,new { randomcode= key, deviceid = _connectionService .serverDevice.deviceId});
+                                }
+                            } catch (Exception e) {
+                                _logger.LogError($"{e.Message},{e.StackTrace}");
+                            }
+                            return Ok(new { ttl,code = 200, randomCode = randomcode, qrcode, type });
+                        }
+                    case bool when $"{type}".Equals("xqrcode"):
+                        {
+                            // 生成二维码图片
+                            Random random = new Random();
+                            randomcode = $"{random.Next(1000, 9999)}";
+                            string? CenterUrl = _configuration.GetValue<string>("ExamServer:CenterUrl");
+                            CenterUrl = CenterUrl.Equals("https://localhost:5001") ? "https://www.teammodel.cn" : CenterUrl;
+                            string content = $"{CenterUrl}/qrcodelogin?randomcode=Login:ExamServer:{school?.id}:{randomcode}&m=%E6%89%AB%E7%A0%81%E7%99%BB%E5%BD%95&o=1";
+                            Bitmap qrCodeImage = QRCodeHelper.GetBitmap(content, 200, 200);
+                            using (MemoryStream stream = new MemoryStream())
+                            {
+                                qrCodeImage.Save(stream, ImageFormat.Png);
+                                byte[] data = stream.ToArray();
+                                qrcode = $"data:image/png;base64,{Convert.ToBase64String(data)}";
+                            }
+                            int ttl = 60;
+                            _memoryCache.Set($"Login:ExamServer:{school?.id}:{randomcode}", randomcode, TimeSpan.FromSeconds(ttl));
+                            return Ok(new {ttl, code = 200, randomCode = randomcode, qrcode, type });
+                        }
+                    case bool when $"{type}".Equals("smspin"):
+                        {
+                            int send = 0;
+                            if (!string.IsNullOrWhiteSpace($"{json["area"]}") && !string.IsNullOrWhiteSpace($"{json["to"]}"))
+                            {
+                                string? CenterUrl = _configuration.GetValue<string>("ExamServer:CenterUrl");
+                                string url = $"{CenterUrl}/core/sendsms/pin";
+                                HttpResponseMessage message = await _httpClientFactory.CreateClient().PostAsJsonAsync(url, new { area = json["area"], to = json["to"], lang = "zh-cn" });
+                                if (message.IsSuccessStatusCode)
+                                {
+                                    string content = await message.Content.ReadAsStringAsync();
+                                    JsonNode? jsonNode = content?.ToObject<JsonNode>();
+                                    if (jsonNode != null && int.TryParse($"{jsonNode["send"]}", out int s))
+                                    {
+                                        send = s;
+                                    }
+                                }
+                            }
+                            return Ok(new { code = 200, send, type });
+                        }
+                }
+            }
+            else if (school == null) 
+            {
+                return Ok(new { code = 400,msg="未绑定学校" });
+            }
+            return Ok(new { code = 400 });
+        }
+
+    }
+}

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

@@ -0,0 +1,651 @@
+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.AspNetCore.SignalR;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Configuration;
+using System.Linq.Expressions;
+using System.Net.Http;
+using System.Net.Http.Json;
+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 CenterServiceConnectionService _connectionService;
+        private readonly int DelayMicro = 10;//微观数据延迟
+        private readonly int DelayMacro = 100;//宏观数据延迟
+        private readonly IHubContext<SignalRExamServerHub> _signalRExamServerHub;
+        public ManageController(LiteDBFactory liteDBFactory,ILogger<ManageController> logger, IConfiguration configuration,
+            IHttpClientFactory httpClientFactory, IMemoryCache memoryCache, CenterServiceConnectionService connectionService, IHubContext<SignalRExamServerHub> signalRExamServerHub)
+        {
+            _logger = logger;
+            _configuration=configuration;
+            _httpClientFactory=httpClientFactory;
+            _memoryCache=memoryCache;
+            _liteDBFactory=liteDBFactory;
+            _connectionService=connectionService;
+            _signalRExamServerHub=signalRExamServerHub;
+        }
+        ///通过线上回传数据需要鉴权验证等操作。
+        ///通过离线包回传数据需要加密操作
+        
+        /// <summary>
+        /// 清理缓存,列出缓存占用空间,type =list列出,type=clear清理,不能清理近期及正在激活的数据,并且提示清理中暂未上传或者导出的数据。
+        /// </summary>
+        /// <param name="json"></param>
+        /// <returns></returns>
+        [HttpPost("clean-cache")]
+        [AuthToken("admin", "teacher", "visitor")]
+        public async Task<IActionResult> CleanCache(JsonNode json) 
+        {
+            return Ok();
+        }
+        [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
+            /* int data = 0,blob=0,webview=0, groupList=0
+             {
+                "evaluationId":"idssss",
+                "shortCode":"1234567890",
+                "ownerId":"hbcn/tmdid",
+                "data":1,
+                "blob":1,
+                "webview":1,
+                "groupList":1
+            }
+             */
+            //如果要访问中心,则需要教师登录联网。  
+            var token = GetAuthTokenInfo();
+            if (token.scope.Equals(ExamConstant.ScopeTeacher))
+            {
+                if (_connectionService.centerIsConnected) 
+                {
+                    Teacher? teacher = _liteDBFactory.GetLiteDatabase().GetCollection<Teacher>().FindOne(x => x.id!.Equals(token.id));
+                    if (teacher != null)
+                    {
+                        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}/blob/sas-read", new { containerName = $"{json["ownerId"]}" });
+                        string sas = string.Empty;
+                        string url = string.Empty;
+                        string cnt = string.Empty;
+                        if (message.IsSuccessStatusCode)
+                        {
+                            //url  sas timeout name
+                            string content = await message.Content.ReadAsStringAsync();
+                            JsonNode? jsonNode = content.ToObject<JsonNode>();
+                            if (jsonNode != null)
+                            {
+                                sas = $"{jsonNode["sas"]}";
+                                cnt = $"{jsonNode["name"]}";
+                            }
+                        }
+                        var httpClient= _httpClientFactory.CreateClient();
+                        if ($"{json["data"]}".Equals("1")) 
+                        {
+                           await httpClient.GetAsync($"{url}/{cnt}/exam/{json["evaluationId"]}/package/evaluation.json");
+                        }
+                        if ($"{json["groupList"]}".Equals("1"))
+                        {
+                            await httpClient.GetAsync($"{url}/{cnt}/exam/{json["evaluationId"]}/package/grouplist.json");
+                        }
+                        if ($"{json["blob"]}".Equals("1"))
+                        {
+                            await httpClient.GetAsync($"{url}/{cnt}/exam/{json["evaluationId"]}/paper");
+                        }
+                        if ($"{json["webview"]}".Equals("1"))
+                        {
+                            //await httpClient.GetAsync($"{url}/{cnt}/exam/{json["evaluationId"]}/evaluation.json");
+                        }
+                    }
+                }
+            }
+           return Ok();
+        }
+        [HttpPost("check-short-code")]
+        [AuthToken("admin", "teacher", "visitor")]
+        public async Task<IActionResult> CheckShortCode(JsonNode json)
+        {
+           
+            string shortCode = $"{json["shortCode"]}";
+            string evaluationId = $"{json["evaluationId"]}";
+            string deviceId = $"{json["deviceId"]}";
+            string centerCode= string.Empty, centerMsg= string.Empty;
+            Expression<Func<EvaluationClient, bool>> predicate = x => true;
+            int checkTotal = 0, checkSuccess = 0, checkError = 0, checkWarning = 0;
+            int msg_status = Constant._Message_status_info;
+            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!=null && evaluationClients.Count()>0)
+            {
+                evaluationLocal= evaluationClients.First();
+            }
+            //如果要访问中心,则需要教师登录联网。  
+            var token = GetAuthTokenInfo();
+            if (token.scope.Equals(ExamConstant.ScopeTeacher))
+            {
+                if (_connectionService.centerIsConnected)
+                {
+                    Teacher? teacher = _liteDBFactory.GetLiteDatabase().GetCollection<Teacher>().FindOne(x => x.id!.Equals(token.id));
+                    if (teacher != null)
+                    {
+                        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);
+                        try {
+                            HttpResponseMessage message = await client.PostAsJsonAsync($"{CenterUrl}/evaluation-sync/find-sync-info", new { shortCode, evaluationId });
+                            if (message.IsSuccessStatusCode)
+                            {
+                                string content = await message.Content.ReadAsStringAsync();
+                                JsonNode? jsonNode = content.ToObject<JsonNode>();
+                                if (jsonNode != null)
+                                {
+                                    centerCode = $"{jsonNode["code"]}";
+                                    centerMsg = $"{jsonNode["msg"]}";
+                                    if ($"{jsonNode["code"]}".Equals("200"))
+                                    {
+                                        evaluationCloud = jsonNode["evaluation"]?.ToObject<EvaluationClient>();
+                                    }
+                                }
+                                else
+                                {
+                                    centerCode = "500";
+                                    centerMsg = "数据转换异常";
+                                }
+                            }
+                            else
+                            {
+                                centerCode = $"{message.StatusCode}";
+                                centerMsg = "数据中心访问异常";
+                            }
+                        } catch (Exception ex) {
+                            centerCode = $"500";
+                            centerMsg = $"数据中心访问异常:{ex.Message}";
+                        }
+                    }
+                    else
+                    {
+                        centerCode = $"401";
+                        centerMsg = "当前登录账号未找到";
+                    }
+                }
+                else 
+                {
+                    centerCode = $"404";
+                    centerMsg = "云端数据中心未连接";
+                }
+                if (centerCode.Equals("200"))
+                {
+                    msg_status=Constant._Message_status_success;
+                    checkTotal++;
+                    checkSuccess++;
+                }
+                else 
+                {
+                    msg_status=Constant._Message_status_warning;
+                    checkTotal++;
+                    checkWarning++;
+                }
+                await _signalRExamServerHub.SendMessage(_memoryCache, _logger, deviceId, Constant._Message_grant_type_check_file,
+                               new MessageContent { dataId=evaluationCloud?.id, dataName=evaluationCloud?.name, messageType=Constant._Message_type_message, status=msg_status, content=$"云端数据检测结果:{centerMsg},状态:{centerCode}" });
+            }
+
+            //数据,文件,页面 0 没有更新,1 有更新
+            int data = 0,blob=0,webview=0, groupList=0, status=0;
+            long dataSize = 0, blobSize=0 , webviewSize=0, studentCount=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)
+                    ||(evaluationLocal.paperCount!= evaluationCloud.paperCount)
+                    )
+                {
+                    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;
+                }
+                if ((evaluationLocal.studentCount!= evaluationCloud.studentCount)||(!$"{evaluationLocal.grouplistHash}".Equals(evaluationCloud.grouplistHash)))  
+                {
+                    groupList=1;
+                    studentCount=evaluationCloud.studentCount;
+                }
+            }
+            else if (evaluationLocal!=null && evaluationCloud==null)
+            {
+                //线下有数据,线上没有数据,可能没联网。
+                status = 3;
+            }
+            else if (evaluationLocal==null && evaluationCloud!=null)
+            {
+                //线下没有数据,线上有数据
+                evaluationLocal= evaluationCloud;
+                blob=1;
+                data=1;
+                webview=1;
+                groupList=1;
+                blobSize=evaluationCloud.blobSize;
+                dataSize=evaluationCloud.dataSize;
+                webviewSize=evaluationCloud.webviewSize;
+                studentCount=evaluationCloud.studentCount;
+                status = 4;
+                _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationClient>().Insert(evaluationLocal);
+            }
+            
+            if (evaluationLocal!=null)
+            {
+                await _signalRExamServerHub.SendMessage(_memoryCache, _logger, deviceId, Constant._Message_grant_type_check_file, 
+                    new MessageContent {dataId=evaluationLocal.id,dataName=evaluationLocal.name,messageType=Constant._Message_type_message, status=0, content="开始检查评测信息文件.." });
+                 
+                //校验本地文件数据
+                string packagePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "package");
+                if (!Directory.Exists(packagePath))
+                {
+                    Directory.CreateDirectory(packagePath);
+                }
+                string evaluationPath = Path.Combine(packagePath, evaluationLocal.id!);
+                string evaluationDataPath = Path.Combine(evaluationPath,"data");
+                // await Task.Delay(DelayMacro);
+
+                //await Task.Delay(DelayMacro);
+                string path_groupList = Path.Combine(evaluationDataPath, "groupList.json");
+                msg_status =Constant._Message_status_info;
+                if (!System.IO.File.Exists(path_groupList))
+                {
+                    groupList=1;
+                    msg_status=Constant._Message_status_error;
+                    checkTotal++;
+                    checkError++;
+                }
+                else
+                {
+                    msg_status=Constant._Message_status_success;
+                    checkTotal++;
+                    checkSuccess++;
+                }
+                await _signalRExamServerHub.SendMessage(_memoryCache, _logger, deviceId, Constant._Message_grant_type_check_file, 
+                    new MessageContent { dataId=evaluationLocal.id, dataName=evaluationLocal.name, messageType= Constant._Message_type_check, status=msg_status, content=$"评测名单文件:{path_groupList}" });
+                //await Task.Delay(DelayMacro);
+                string path_source = Path.Combine(evaluationDataPath, "source.json");
+                msg_status = Constant._Message_status_info;
+                if (!System.IO.File.Exists(path_source))
+                {
+                    data=1;
+                    msg_status=Constant._Message_status_error;
+                    checkTotal++;
+                    checkError++;
+                }
+                else
+                {
+                    msg_status=Constant._Message_status_success;
+                    checkTotal++;
+                    checkSuccess++;
+                }
+                await _signalRExamServerHub.SendMessage(_memoryCache, _logger, deviceId, Constant._Message_grant_type_check_file,
+                    new MessageContent { dataId=evaluationLocal.id, dataName=evaluationLocal.name, messageType= Constant._Message_type_check, status=msg_status, content=$"评测原始数据:{path_source}" });
+                msg_status =Constant._Message_status_info;
+                try {
+                    //TODO 重整本地文件路径。 文件可能不存在D:\VisualStudioProjects\TEAMModelOS\TEAMModelOS.Extension\IES.Exam\IES.ExamServer\wwwroot\package\exam\6af32bbd-144e-4366-8bc0-61ba4c85677c\evaluation.json
+                    string path_evaluation = Path.Combine(evaluationDataPath, "evaluation.json");
+                    if (!System.IO.File.Exists(path_evaluation))
+                    {
+                        blob=1;
+                        data=1;
+                        msg_status=Constant._Message_status_error;
+                        checkTotal++;
+                        checkError++;
+                    }
+                    else
+                    {
+                        msg_status=Constant._Message_status_success;
+                        checkTotal++;
+                        checkSuccess++;
+                    }
+                    //数据格式:  [消息][信息/错误/警告][15:43]=>[开始检查评测信息文件...]
+                    //数据格式:  [检查][成功/失败][15:43]=>[评测数据文件:/wwwroot/package/623a9fe6-5445-0938-ff77-aeb80066ef27/evaluation.json]
+                    //数据格式:  [下载][成功/失败][15:43]=>[评测数据文件:/wwwroot/package/623a9fe6-5445-0938-ff77-aeb80066ef27/evaluation.json][1024kb][15ms]
+                    await _signalRExamServerHub.SendMessage(_memoryCache, _logger, deviceId, Constant._Message_grant_type_check_file,
+                        new MessageContent { dataId=evaluationLocal.id, dataName=evaluationLocal.name, messageType= Constant._Message_type_check, status=msg_status, content=$"评测数据文件:{path_evaluation}" });
+                    if (System.IO.File.Exists(path_evaluation)) 
+                    {
+                        string evaluation_str = await System.IO.File.ReadAllTextAsync(path_evaluation);
+                        JsonNode? evaluation_data = evaluation_str.ToObject<JsonNode>();
+                        if (evaluation_data==null)
+                        {
+                            blob=1;
+                            data=1;
+                        }
+                        else
+                        {
+                            EvaluationClient? evaluationClient = evaluation_data["evaluationClient"]?.ToObject<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=Constant._Message_status_info;
+                                }
+                                else
+                                {
+                                    data=1;
+                                    msg_status=Constant._Message_status_error;
+                                }
+                                await _signalRExamServerHub.SendMessage(_memoryCache, _logger, deviceId, Constant._Message_grant_type_check_file,
+                                    new MessageContent { dataId=evaluationLocal.id, dataName=evaluationLocal.name, messageType=Constant._Message_type_message, status=msg_status, content="校验本地试卷文件..." });
+                            }
+                            else { data=1; }
+
+                            List<EvaluationExam>? evaluationExams = evaluation_data["evaluationExams"]?.ToObject<List<EvaluationExam>>();
+                            if (evaluationExams.IsEmpty())
+                            {
+                                blob=1;
+                                data=1;
+                            }
+                            else 
+                            {
+                                
+                                foreach (var evaluationExam in evaluationExams!)
+                                {
+                                    string path_papers = Path.Combine(evaluationPath,  "exams");
+                                    var papers_files = FileHelper.ListAllFiles(path_papers);
+                                    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
+                                            {
+                                                dataId=evaluationLocal.id,
+                                                dataName=evaluationLocal.name,
+                                                messageType=Constant._Message_type_check,
+                                                status=msg_status,
+                                                content=$"试卷文件信息:{paper.paperName}"
+                                            });
+                                        }
+                                        int paper_msg_status = Constant._Message_status_info;
+                                        if (paper_error_count>0)
+                                        {
+                                            blob=1;
+                                            paper_msg_status=Constant._Message_status_error;
+                                        }
+                                        else
+                                        {
+                                            paper_msg_status=Constant._Message_status_success;
+                                        }
+                                        await _signalRExamServerHub.SendMessage(_memoryCache, _logger, deviceId, Constant._Message_grant_type_check_file,
+                                            new MessageContent
+                                            {
+                                                dataId=evaluationLocal.id,
+                                                dataName=evaluationLocal.name,
+                                                messageType=Constant._Message_type_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) {
+                    _logger.LogData<object>(new {code=500,msg=e.Message,data = new { content= e.StackTrace } }, evaluationLocal.id!);
+                }
+
+                //检查需要更新的项目:
+                if (data==1) 
+                {
+                    await _signalRExamServerHub.SendMessage(_memoryCache, _logger, deviceId, Constant._Message_grant_type_check_file,
+                        new MessageContent
+                        {
+                            dataId=evaluationLocal.id,
+                            dataName=evaluationLocal.name,
+                            messageType=Constant._Message_type_message,
+                            status=Constant._Message_status_warning,
+                            content=$"检查到评测数据需要更新。[{dataSize}]"
+                        });
+                }
+                if (blob==1)
+                {
+                    await _signalRExamServerHub.SendMessage(_memoryCache, _logger, deviceId, Constant._Message_grant_type_check_file,
+                        new MessageContent
+                        {
+                            dataId=evaluationLocal.id,
+                            dataName=evaluationLocal.name,
+                            messageType=Constant._Message_type_message,
+                            status=Constant._Message_status_warning,
+                            content=$"检查到评测试卷需要更新。[{blobSize}]"
+                        });
+                }
+                if (webview==1) 
+                {
+                    await _signalRExamServerHub.SendMessage(_memoryCache, _logger, deviceId, Constant._Message_grant_type_check_file,
+                        new MessageContent
+                        {
+                            dataId=evaluationLocal.id,
+                            dataName=evaluationLocal.name,
+                            messageType=Constant._Message_type_message,
+                            status=Constant._Message_status_warning,
+                            content=$"检查到评测作答页面需要更新。[{webviewSize}]"
+                        });
+                }
+                if (groupList==1) 
+                {
+                    await _signalRExamServerHub.SendMessage(_memoryCache, _logger, deviceId, Constant._Message_grant_type_check_file,
+                        new MessageContent
+                        {
+                            dataId=evaluationLocal.id,
+                            dataName=evaluationLocal.name,
+                            messageType=Constant._Message_type_message,
+                            status=Constant._Message_status_warning,
+                            content=$"检查到评测名单需要更新。[{studentCount}]"
+                        });
+                }
+            }
+            int finalStatus = Constant._Message_status_success;
+            if (checkWarning>0) 
+            {
+                finalStatus = Constant._Message_status_warning;
+            }
+            if (checkError>0)
+            {
+                finalStatus = Constant._Message_status_error;
+            }
+            await _signalRExamServerHub.SendMessage(_memoryCache, _logger, deviceId, Constant._Message_grant_type_check_file,
+                new MessageContent {
+                    dataId=evaluationLocal?.id,
+                    dataName=evaluationLocal?.name,
+                    messageType=Constant._Message_type_message,
+                    status=Constant._Message_status_warning,
+                    content=$"最终检测结果:总数({checkTotal}),成功({checkSuccess}),警告({checkWarning}),异常({checkError})。"
+                });
+            return Ok(new {
+                code = 200,
+                evaluation = evaluationLocal,
+                data,
+                blob,
+                webview,
+                dataSize,
+                blobSize,
+                webviewSize,
+                status,
+                groupList,
+                studentCount,
+                checkTotal,
+                checkSuccess,
+                checkError,
+                checkWarning,
+            });
+        }
+
+        /// <summary>
+        /// 激活或者取消激活考试
+        /// </summary>
+        /// <param name="json"></param>
+        /// <returns></returns>
+        [HttpPost("activate-evaluation")]
+        [AuthToken("admin", "teacher", "visitor")]
+        public IActionResult ActivateEvaluation(JsonNode json)
+        {
+            string id = $"{json["id"]}";
+            string shortCode = $"{json["shortCode"]}";
+            string activateStr = $"{json["activate"]}";
+            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)
+                {
+                    IEnumerable<EvaluationClient> evaluationClients = _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationClient>().Find(x=>x.activate==1);
+                    if (evaluationClients != null && evaluationClients.Count() > 0) 
+                    {
+                        foreach(EvaluationClient item in evaluationClients)
+                        {
+                            item.activate = 0;
+                            _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationClient>().Upsert(item);
+                        }
+                    }
+                    int activate = 0;
+                    if (int.TryParse(activateStr, out int _activate)) {
+                        activate = _activate;
+                    }
+                    evaluationClient.activate = activate;
+                    _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationClient>().Upsert(evaluationClient);
+                }
+            }
+            return Ok();
+        }
+        /// <summary>
+        /// 加载本地的活动列表
+        /// </summary>
+        /// <param name="json"></param>
+        /// <returns></returns>
+        [HttpPost("list-local-evaluation")]
+        [AuthToken("admin", "teacher", "visitor")]
+        public IActionResult ListLocalEvaluation(JsonNode json) 
+        {
+
+            IEnumerable<EvaluationClient>? evaluationClients = _liteDBFactory.GetLiteDatabase().GetCollection<EvaluationClient>().FindAll().OrderByDescending(x=>x.activate).ThenByDescending(x=>x.stime);
+
+            if (evaluationClients != null)
+            {
+                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 });
+            }
+            else {
+                return Ok(new { code = 200, evaluation = new Dictionary<string, object?>() });
+            }
+            
+        }
+    }
+}

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

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

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

@@ -0,0 +1,23 @@
+using IES.ExamServer.Models;
+
+namespace IES.ExamServer.DI
+{
+    public class CenterServiceConnectionService
+    {
+        private bool _centerIsConnected;
+        private bool _notifyIsConnected;
+        public string? centerUrl { get; set; }
+        public string? notifyUrl { get; set; }
+        public ServerDevice? serverDevice {  get; set; }
+        public bool notifyIsConnected
+        {
+            get { return string.IsNullOrWhiteSpace(notifyUrl) ? false : _notifyIsConnected; }
+            set { _notifyIsConnected = value; }
+        }
+        public bool centerIsConnected
+        {
+            get { return string.IsNullOrWhiteSpace(centerUrl) ? false : _centerIsConnected; }
+            set { _centerIsConnected = value; }
+        }
+    }
+}

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

@@ -0,0 +1,248 @@
+using Microsoft.Extensions.Logging;
+using System;
+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));
+       
+        
+
+        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);
+            }
+            if (!string.IsNullOrWhiteSpace(id))
+            {
+                var logFilePath = Path.Combine(logDirectory, $"{id}.log");
+                var logMessage = data?.ToJsonString();
+                _fileLogChannel.Writer.TryWrite($"{logFilePath}|||{DateTime.Now:yyyy-MM-dd HH:mm:ss} [Data] {logMessage}{Environment.NewLine}");
+            }
+            else {
+                _consoleLogChannel.Writer.TryWrite($"日志id为空,日志数据:{data?.ToJsonString()}");
+            }
+            //_ = 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); // 输出到控制台
+            }
+        }
+    }
+}

+ 59 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/LiteDBFactory.cs

@@ -0,0 +1,59 @@
+using LiteDB;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace IES.ExamServer.DI
+{
+    public class LiteDBFactory
+    {
+        private readonly IServiceProvider _services;
+        private readonly IOptionsMonitor<LiteDBFactoryOptions> _optionsMonitor;
+        private readonly ILogger _logger;
+        private ConcurrentDictionary<string, LiteDatabase> LiteDatabases { get; } = new ConcurrentDictionary<string, LiteDatabase>();
+        public LiteDBFactory(IServiceProvider services, IOptionsMonitor<LiteDBFactoryOptions> optionsMonitor, ILogger<LiteDBFactory> logger)
+        {
+            if (services == null) throw new ArgumentNullException(nameof(services));
+            if (optionsMonitor == null) throw new ArgumentNullException(nameof(optionsMonitor));
+
+            _services = services;
+            _optionsMonitor = optionsMonitor;
+            _logger = logger;
+        }
+
+        public LiteDatabase GetLiteDatabase(string name = "Master")
+        {
+            return LiteDatabases.GetOrAdd(name, x => new LiteDatabase(_optionsMonitor.Get(name).Connectionstring));
+        }
+    }
+    public class LiteDBFactoryOptions
+    {
+        public string? Name { get; set; }
+        public string? Connectionstring { get; set; }
+    }
+    public static class LiteDBFactoryExtensions
+    {
+        public static IServiceCollection AddLiteDB(this IServiceCollection services, List<LiteDBFactoryOptions> connectionstrings)
+        {
+            if (services == null) throw new ArgumentNullException(nameof(services));
+            if (connectionstrings == null) throw new ArgumentNullException(nameof(connectionstrings));
+
+
+            services.TryAddSingleton<LiteDBFactory>();
+            //多个连接字符串注入
+            connectionstrings.ForEach(connection =>
+            {
+                services.Configure<LiteDBFactoryOptions>(connection.Name, o => { o.Name = connection.Name; o.Connectionstring = connection.Connectionstring; });
+            });
+
+            return services;
+        }
+    }
+}

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

@@ -0,0 +1,158 @@
+using IES.ExamServer.Services;
+using Microsoft.AspNetCore.Hosting.Server.Features;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.Extensions.Caching.Memory;
+using System.Text.Encodings.Web;
+using System.Text.Json.Nodes;
+using System.Text.Json;
+using System.Text.Unicode;
+using IES.ExamServer.Helper;
+using IES.ExamServer.Models;
+using System.Security.Policy;
+using IES.ExamServer.Helpers;
+
+namespace IES.ExamServer.DI
+{
+    public class ServiceInitializer
+    {
+        private readonly IMemoryCache _cache;
+        private readonly IHttpClientFactory _clientFactory;
+        private readonly LiteDBFactory _liteDBFactory;
+        private readonly IConfiguration _configuration;
+        private readonly CenterServiceConnectionService _connectionService;
+        private readonly IHostApplicationLifetime _lifetime;
+        private readonly IServer _server;
+        private readonly ILogger<ServiceInitializer> _logger;
+
+        public ServiceInitializer(IMemoryCache cache,
+        IHttpClientFactory clientFactory,
+        LiteDBFactory liteDBFactory,
+        IConfiguration configuration,
+        CenterServiceConnectionService connectionService,
+        IHostApplicationLifetime lifetime,
+        IServer server,
+        ILogger<ServiceInitializer> logger)
+        {
+            _cache = cache;
+            _clientFactory = clientFactory;
+            _liteDBFactory = liteDBFactory;
+            _configuration = configuration;
+            _connectionService = connectionService;
+            _lifetime = lifetime;
+            _server = server;
+            _logger = logger;
+        }
+
+        public async Task InitializeAsync()
+        {
+            JsonNode? data = null;
+            int hybrid = 0, notify=0;
+            string remote = "127.0.0.1";
+            string region = "局域网·内网";
+            string? centerUrl = _configuration.GetValue<string>("ExamServer:CenterUrl");
+            try
+            {
+                
+                var httpClient = _clientFactory.CreateClient();
+                httpClient.Timeout = TimeSpan.FromSeconds(10);
+                HttpResponseMessage message = await httpClient.PostAsJsonAsync($"{centerUrl}/core/system-info", new { });
+
+                if (message.IsSuccessStatusCode)
+                {
+                    string content = await message.Content.ReadAsStringAsync();
+                    data = JsonSerializer.Deserialize<JsonNode>(content);
+                    data!["centerUrl"] = centerUrl;
+                    _cache.Set(Constant._KeyServerCenter, data);
+                    remote = $"{data["ip"]}";
+                    region = $"{data["region"]}";
+                    hybrid = 1;
+                }
+            }
+            catch (Exception ex)
+            {
+                // 云端服务连接失败
+                hybrid = 0;
+            }
+            string? notifyUrl = _configuration.GetValue<string>("ExamServer:NotifyUrl");
+            try
+            {
+                
+                var httpClient = _clientFactory.CreateClient();
+                httpClient.Timeout = TimeSpan.FromSeconds(10);
+                HttpResponseMessage message = await httpClient.PostAsJsonAsync($"{notifyUrl}/index/device-init", new { fp= Guid.NewGuid().ToString() });
+                if (message.IsSuccessStatusCode)
+                {
+                    notify = 1;
+                }
+            }
+            catch (Exception ex)
+            {
+                // 云端服务连接失败
+                notify = 0;
+            }
+            if (hybrid==1)
+            {
+              
+                var httpClient = _clientFactory.CreateClient();
+                httpClient.Timeout = TimeSpan.FromSeconds(10);
+                HttpResponseMessage message = await httpClient.GetAsync("https://teammodelos.blob.core.chinacloudapi.cn/0-public/schools.json");
+                if (message.IsSuccessStatusCode)
+                {
+                    // 读取响应内容
+                    string content = await message.Content.ReadAsStringAsync();
+                    // 保存文件的路径
+                    string filePath = Path.Combine(Directory.GetCurrentDirectory(),"wwwroot", "package", "schools.json");
+                    // 确保目录存在
+                    Directory.CreateDirectory(Path.GetDirectoryName(filePath)!);
+                    // 将内容写入文件
+                    await File.WriteAllTextAsync(filePath, content);
+                }
+                else
+                {
+                    throw new Exception($"Failed to download data. Status code: {message.StatusCode}");
+                }
+            }
+            _connectionService.notifyUrl = notify == 1 ? notifyUrl : null;
+            _connectionService.notifyIsConnected = notify == 1;
+            // 单例模式存储云端数据中心连接状态
+            _connectionService.centerUrl = hybrid == 1 ?centerUrl : null;
+            _connectionService.centerIsConnected = hybrid == 1;
+            ServerDevice serverDevice = IndexService.GetServerDevice(remote, region);
+            IEnumerable<School> schools = _liteDBFactory.GetLiteDatabase().GetCollection<School>().FindAll();
+            School? school = schools?.FirstOrDefault();
+            serverDevice.school = school;
+            _cache.Set(Constant._KeyServerDevice, serverDevice);
+           
+            _liteDBFactory.GetLiteDatabase().GetCollection<ServerDevice>().Upsert(serverDevice);
+            _connectionService.serverDevice = serverDevice;
+            _lifetime.ApplicationStarted.Register(() =>
+            {
+               var serverDevice=  _cache.Get<ServerDevice>(Constant._KeyServerDevice);
+                var _url = _server.Features.Get<IServerAddressesFeature>()?.Addresses;
+                if (_url!.IsNotEmpty())
+                {
+                    List<UriInfo> ports = new List<UriInfo>();
+                    foreach (var url in _url!)
+                    {
+                        Uri uri = new Uri(url);
+                        serverDevice.uris.Add(new UriInfo { port= uri.Port, protocol= uri.Scheme });
+                    }
+
+                }
+                else
+                {
+                    throw new Exception("未获取到端口信息!");
+                }
+                _logger.LogInformation($"服务端设备信息:{JsonSerializer.Serialize(serverDevice, options: new JsonSerializerOptions { Encoder = JavaScriptEncoder.Create(UnicodeRanges.All) })}");
+                _cache.Set(Constant._KeyServerDevice, serverDevice);
+            });
+
+            // 退出程序
+            _lifetime.ApplicationStopping.Register(() =>
+            {
+                Console.WriteLine("The application is stopping. Performing cleanup...");
+                // 在这里添加清理资源、保存数据等逻辑
+            });
+        }
+    }
+}

+ 143 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/SignalRCloudClientHub.cs

@@ -0,0 +1,143 @@
+using IES.ExamServer.DI.SignalRHost;
+using IES.ExamServer.Helper;
+using IES.ExamServer.Models;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Hosting.Server.Features;
+using Microsoft.AspNetCore.SignalR;
+using Microsoft.AspNetCore.SignalR.Client;
+using Microsoft.Extensions.Caching.Memory;
+using System.Text;
+using System.Text.Json.Nodes;
+using System.Web;
+
+namespace IES.ExamServer.DI
+{
+    public class SignalRCloudClientHub : BackgroundService, IDisposable
+    {
+        private readonly IConfiguration _configuration;
+        private readonly ILogger<SignalRCloudClientHub> _logger;
+        private readonly IHttpClientFactory _httpClientFactory;
+        private readonly IMemoryCache _memoryCache;
+        private readonly IHubContext<SignalRExamServerHub> _signalRExamServerHub;
+        public SignalRCloudClientHub(IConfiguration configuration, ILogger<SignalRCloudClientHub> logger, IHttpClientFactory httpClientFactory, IMemoryCache memoryCache,
+            IHubContext<SignalRExamServerHub> signalRExamServerHub)
+        {
+
+            _configuration=configuration;
+            _logger=logger;
+            _httpClientFactory=httpClientFactory;
+            _memoryCache=memoryCache;
+            _signalRExamServerHub=signalRExamServerHub;
+        }
+        protected async override Task ExecuteAsync(CancellationToken stoppingToken)
+        {
+            _memoryCache.TryGetValue(Constant._KeyServerDevice, out ServerDevice? server);
+            string? deviceId = server?.deviceId;
+            string? NotifyUrl = _configuration.GetValue<string>("ExamServer:NotifyUrl");
+            await StartHubConnectionAsync(deviceId, NotifyUrl);
+
+        }
+        private async Task StartHubConnectionAsync(string? deviceId,string? NotifyUrl)
+        {
+            var reconnectPolicy = new ExponentialBackoffReconnectPolicy(TimeSpan.FromSeconds(10), _logger); // 尝试重连的最大次数,这里使用 int.MaxValue 表示无限次
+            reconnectPolicy.MaxRetryCount = int.MaxValue;
+            HubConnection hubConnection = new HubConnectionBuilder()
+               .WithUrl($"{NotifyUrl}/signalr/remote/notify?grant_type={Constant._Message_grant_type_ies_qrcode_login}&deviceid={deviceId}&dingding=123") //only one slash
+               .WithAutomaticReconnect(reconnectPolicy)
+               .ConfigureLogging(logging =>
+               {
+                   logging.SetMinimumLevel(LogLevel.Information);
+                   logging.AddConsole();
+               })
+               .Build();
+            try
+            {
+                hubConnection.On<ConnectionMessageBody>("ReceiveConnection", (message) =>
+                {
+                    _logger.LogInformation($"连接成功:{message.ToJsonString()}");
+                    //重置重连次数。
+                    reconnectPolicy.Reset();
+                });
+                hubConnection.On<ConnectionMessageBody>("ReceiveMessage", async (message) =>
+                {
+                    if ($"{Constant._Message_grant_type_ies_qrcode_login}" .Equals(message?.grant_type))
+                    {
+                        string deviceId = string.Empty;
+                        var nodeData= message?.content?.ToObject<JsonNode>();
+                        if (nodeData!=null)
+                        {
+                            string randomcode = $"{nodeData["randomcode"]}";
+                            deviceId=_memoryCache.Get<string>($"device:{randomcode}");
+                        }
+                        await  _signalRExamServerHub.SendMessage(_memoryCache,_logger, deviceId, Constant._Message_grant_type_ies_qrcode_login,
+                          new MessageContent {dataId=deviceId,dataName="",messageType =Constant._Message_type_message, status=1,content=$"{nodeData?.ToJsonString()}"});
+                    }
+                    else
+                    {
+                        _logger.LogInformation($"云端signalr数据格式不匹配,{message?.ToJsonString()}");
+                    }
+                });
+                await hubConnection.StartAsync();
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError("初次启动连接SignalR失败,等待重连......");
+                int retryCount = 0;
+                const int maxRetries = 360;
+                const int retryDelaySeconds = 10;
+                while (retryCount < maxRetries)
+                {
+                    try
+                    {
+                        await Task.Delay(retryDelaySeconds * 1000); // 等待一段时间后重试  
+                        await hubConnection.StartAsync();
+                        _logger.LogInformation("SignalR连接成功(重试后)!");
+                        break; // 连接成功,退出循环  
+                    }
+                    catch (Exception retryEx)
+                    {
+                        retryCount++;
+                        _logger.LogInformation($"SignalR连接重试失败: {retryEx.Message}。重试次数: {retryCount}/{maxRetries}");
+                        // 可以在这里决定是否因为某种原因停止重试  
+                        if (retryCount == maxRetries)
+                        {
+                            _logger.LogInformation("达到最大重试次数,停止重试。");
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+    }
+    public class ExponentialBackoffReconnectPolicy : IRetryPolicy
+    {
+        private readonly TimeSpan _retryInterval;
+
+        private int _retryCount;
+        public int MaxRetryCount { get; set; } = int.MaxValue;
+        public readonly ILogger<SignalRCloudClientHub> _logger;
+        public ExponentialBackoffReconnectPolicy(TimeSpan retryInterval, ILogger<SignalRCloudClientHub> logger)
+        {
+            _retryInterval = retryInterval;
+            _retryCount = 0;
+            _logger = logger;
+        }
+
+        public TimeSpan? NextRetryDelay(RetryContext retryContext)
+        {
+            _logger.LogInformation($"重连次数: {_retryCount}");
+            if (_retryCount < MaxRetryCount)
+            {
+                _retryCount++;
+                // 计算下一次重连的延迟时间
+                return _retryInterval;
+            }
+            return null; // 达到最大重连次数后不再重连
+        }
+
+        public void Reset()
+        {
+            _retryCount = 0;
+        }
+    }
+}

+ 219 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/SignalRHost/SignalRExamServerHub.cs

@@ -0,0 +1,219 @@
+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
+{
+    public static class SignalRExamServerHubExtension
+    {
+        public async static Task SendMessage(this IHubContext<SignalRExamServerHub> hubContext, IMemoryCache _memoryCache, ILogger logger, string clientId, string grant_type, MessageContent content)
+        {
+            //双向检测是否连接。
+            SignalRClient signalRClient = _memoryCache.Get<SignalRClient>($"{Constant._KeySignalRClientClients}:{clientId}");
+            if (signalRClient!=null)
+            {
+                signalRClient = _memoryCache.Get<SignalRClient>($"{Constant._KeySignalRClientConnects}:{signalRClient.connid}");
+            }
+            int code = 0;
+            string msg = "";
+            CheckFileMessageBody messageBody = 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
+            };
+            if (signalRClient != null)
+            {
+
+                try
+                {
+                    switch (true)
+                    {
+                        case bool when grant_type.Equals(Constant._Message_grant_type_ies_qrcode_login):
+                            {
+                                await hubContext.Clients.Client(signalRClient.connid!).SendAsync("ReceiveMessage", messageBody);
+                                code=200;
+                                msg = $"发送成功";
+                                break;
+                            }
+                        default:
+                            break;
+                    }
+                }
+                catch (Exception ex)
+                {
+                    code=500;
+                    msg = $"{ex.Message},{ex.StackTrace}";
+                }
+            }
+            else
+            {
+                code= 400;
+                msg="未连接客户端";
+            }
+            logger.LogData<object>(new { code, msg, data = messageBody }, content.dataId!);
+        }
+    }
+    public  class SignalRExamServerHub : Hub<IClient>
+    {
+        private readonly ILogger<SignalRExamServerHub> _logger;
+        private readonly IMemoryCache _memoryCache;
+      
+        public SignalRExamServerHub(ILogger<SignalRExamServerHub> logger,IMemoryCache memoryCache)
+        {
+            _logger = logger;
+            _memoryCache = memoryCache;
+        }
+
+        // <summary>
+        /// 这需要继承Hub来创建中心,并向中心添加方法,客户端可以调用标识符为public的方法
+        /// </summary>
+        /// <param name="user"></param>
+        /// <param name="message"></param>
+        /// <returns></returns>
+
+        public  void ReceiveMessage(string clientId, string grant_type, string contentmsg )
+        {
+            _logger.LogInformation($"收到客户端的消息{contentmsg}");
+        }
+   
+        /// <summary>
+        /// 客户连接成功时触发
+        /// </summary>
+        /// <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}:{clientid}", client);
+                                _memoryCache.Set($"{Constant._KeySignalRClientConnects}:{connid}", 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;
+                            }
+                        // 扫码登录
+                        case bool when grant_type.Equals(Constant._Message_grant_type_ies_qrcode_login):
+                            {
+                                _memoryCache.Set($"{Constant._KeySignalRClientClients}:{clientid}", client);
+                                _memoryCache.Set($"{Constant._KeySignalRClientConnects}:{connid}", 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;
+                            }
+                        // 下载文件
+                        case bool when grant_type.Equals(Constant._Message_grant_type_download_file):
+                            {
+                                _memoryCache.Set($"{Constant._KeySignalRClientClients}:{clientid}", client);
+                                _memoryCache.Set($"{Constant._KeySignalRClientConnects}:{connid}", 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;
+                            }
+                        // 上传数据
+                        case bool when grant_type.Equals(Constant._Message_grant_type_upload_data):
+                            {
+                                _memoryCache.Set($"{Constant._KeySignalRClientClients}:{clientid}", client);
+                                _memoryCache.Set($"{Constant._KeySignalRClientConnects}:{connid}", 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 interface IClient
+    {
+        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 执行后不需要做任何处理
+        }
+    }
+}

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

@@ -0,0 +1,38 @@
+namespace IES.ExamServer.Helpers
+{
+    public static class CollectionHelper
+    {
+        /// <summary>
+        /// 判断集合是否为空
+        /// </summary>
+        /// <param name="collection"></param>
+        /// <returns></returns>
+        public static bool IsEmpty<T>(this IEnumerable<T>? collection)
+        {
+            if (collection != null && collection.Any())
+            {
+                return false;
+            }
+            else
+            {
+                return true;
+            }
+        }
+        /// <summary>
+        /// 判断集合是否不为空
+        /// </summary>
+        /// <param name="collection"></param>
+        /// <returns></returns>
+        public static bool IsNotEmpty<T>(this IEnumerable<T>? collection)
+        {
+            if (collection != null && collection.Any())
+            {
+                return true;
+            }
+            else
+            {
+                return false;
+            }
+        }
+    }
+}

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

@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace IES.ExamServer.Helper
+{
+    public static class Constant
+    {
+        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_grant_type_ies_qrcode_login = "ies_qrcode_login";
+        public static readonly string _Message_grant_type_download_file = "download_file";
+        public static readonly string _Message_grant_type_upload_data = "upload_data";
+        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);
+        }
+    }
+
+}

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

@@ -0,0 +1,51 @@
+using System.Buffers;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Unicode;
+using System.Xml.Linq;
+
+namespace IES.ExamServer
+{
+    public static class JsonExtensions
+    {
+        private static readonly JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions
+        {
+            Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
+        };
+        public static string ToJsonString(this JsonElement json)
+        {
+            return JsonSerializer.Serialize(json, jsonSerializerOptions);
+        }
+        public static string ToJsonString(this Object obj)
+        {
+            return JsonSerializer.Serialize(obj, jsonSerializerOptions);
+        }
+        public static string ToJsonString(this JsonNode node)
+        {
+            return JsonSerializer.Serialize(node, jsonSerializerOptions);
+        }
+        public static T? ToObject<T>(this JsonElement json) where T : class
+        {
+            return JsonSerializer.Deserialize<T>(json.GetRawText(), jsonSerializerOptions);
+        }
+        public static T? ToObject<T>(this string json) where T : class
+        {
+            try
+            {
+                return JsonSerializer.Deserialize<T>(json, jsonSerializerOptions);
+            }
+            catch (JsonException ex)
+            {
+                return null;
+               // throw new InvalidOperationException("Failed to deserialize JSON.", ex);
+            }
+        }
+
+        public static T? ToObject<T>(this JsonNode json) where T : class
+        {
+            return JsonSerializer.Deserialize<T>(json, jsonSerializerOptions!);
+        }
+    }
+}

+ 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("操作完成。");
+        }
+    }
+}

Plik diff jest za duży
+ 124 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/QRCodeHelper.cs


+ 134 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/ShaHashHelper.cs

@@ -0,0 +1,134 @@
+using System.Security.Cryptography;
+using System.Text;
+
+namespace IES.ExamServer.Helpers
+{
+    public sealed class ShaHashHelper
+    {
+        public static string GetSHA1(string Code)
+        {
+            var resbuffer = Encoding.Default.GetBytes(Code);
+            HashAlgorithm iSha =   SHA1.Create();
+            resbuffer = iSha.ComputeHash(resbuffer);
+            StringBuilder builder = new StringBuilder();
+            for (int i = 0; i < resbuffer.Length; i++)
+            {
+                builder.Append(resbuffer[i].ToString("x2"));
+            }
+            return builder.ToString();
+            //return Convert.ToBase64String(strRes);
+        }
+        public static string GetSHA1(byte[] buffer)
+        {
+            var resbuffer = buffer;
+            HashAlgorithm iSha = SHA1.Create();
+            resbuffer = iSha.ComputeHash(buffer);
+            StringBuilder builder = new StringBuilder();
+            for (int i = 0; i < resbuffer.Length; i++)
+            {
+                builder.Append(resbuffer[i].ToString("x2"));
+            }
+            return builder.ToString();
+            // return Convert.ToBase64String(strRes);
+        }
+        public static string GetSHA1(Stream stream)
+        {
+            byte[] buffer = new byte[stream.Length];
+
+            stream.Read(buffer, 0, buffer.Length);
+            // stream.Close();
+            var resbuffer = buffer;
+            HashAlgorithm iSha = SHA1.Create();
+            resbuffer = iSha.ComputeHash(buffer);
+            StringBuilder builder = new StringBuilder();
+            for (int i = 0; i < resbuffer.Length; i++)
+            {
+                builder.Append(resbuffer[i].ToString("x2"));
+            }
+            return builder.ToString();
+            // return Convert.ToBase64String(strRes);
+        }
+        public static string GetSHA1(string Code, string SecretKey)
+        {
+            HMACSHA1 hmacsha1 = new HMACSHA1(Encoding.UTF8.GetBytes(SecretKey));
+            byte[] resbuffer = hmacsha1.ComputeHash(Encoding.UTF8.GetBytes(Code));
+
+            StringBuilder builder = new StringBuilder();
+            for (int i = 0; i < resbuffer.Length; i++)
+            {
+                builder.Append(resbuffer[i].ToString("x2"));
+            }
+            return builder.ToString();
+            //return Convert.ToBase64String(rstRes); 
+        }
+        public static string GetSHA1(byte[] buffer, string SecretKey)
+        {
+            HMACSHA1 hmacsha1 = new HMACSHA1(Encoding.UTF8.GetBytes(SecretKey));
+            byte[] resbuffer = hmacsha1.ComputeHash(buffer);
+
+            StringBuilder builder = new StringBuilder();
+            for (int i = 0; i < resbuffer.Length; i++)
+            {
+                builder.Append(resbuffer[i].ToString("x2"));
+            }
+            return builder.ToString();
+            //eturn Convert.ToBase64String(buffer);
+        }
+        public static string GetSHA256(byte[] buffer)
+        {
+            var Sha256 = SHA256.Create();
+            byte[] resbuffer = Sha256.ComputeHash(buffer);
+            StringBuilder builder = new StringBuilder();
+            for (int i = 0; i < resbuffer.Length; i++)
+            {
+                builder.Append(resbuffer[i].ToString("x2"));
+            }
+            return builder.ToString();
+            //return Convert.ToBase64String(retval);
+
+        }
+
+        public static string GetSHA256(string Code)
+        {
+            var Sha256 = SHA256.Create();
+            byte[] resbuffer = Sha256.ComputeHash(Encoding.UTF8.GetBytes(Code));
+            StringBuilder builder = new StringBuilder();
+            for (int i = 0; i < resbuffer.Length; i++)
+            {
+                builder.Append(resbuffer[i].ToString("x2"));
+            }
+            return builder.ToString();
+            //return Convert.ToBase64String(retval);
+        }
+
+        public static string GetSHA256(byte[] buffer, string SecretKey)
+        {
+            byte[] messageBytes = buffer;
+            byte[] keyByte = Encoding.UTF8.GetBytes(SecretKey);
+            var hmacsha256 = new HMACSHA256(keyByte);
+            byte[] resbuffer = hmacsha256.ComputeHash(messageBytes);
+            StringBuilder builder = new StringBuilder();
+            for (int i = 0; i < resbuffer.Length; i++)
+            {
+                builder.Append(resbuffer[i].ToString("x2"));
+            }
+            return builder.ToString();
+            //return Convert.ToBase64String(hashmessage);
+        }
+
+        public static string GetSHA256(string Code, string SecretKey)
+        {
+            byte[] messageBytes = Encoding.UTF8.GetBytes(Code);
+            byte[] keyByte = Encoding.UTF8.GetBytes(SecretKey);
+            var hmacsha256 = new HMACSHA256(keyByte);
+            byte[] resbuffer = hmacsha256.ComputeHash(messageBytes);
+            StringBuilder builder = new StringBuilder();
+            for (int i = 0; i < resbuffer.Length; i++)
+            {
+                builder.Append(resbuffer[i].ToString("x2"));
+            }
+            return builder.ToString();
+            //return Convert.ToBase64String(resbuffer);
+        }
+    }
+}

+ 40 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/IES.ExamServer.csproj

@@ -0,0 +1,40 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>net6.0</TargetFramework>
+    <Nullable>enable</Nullable>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <SpaRoot>..\IES.ExamViews</SpaRoot>
+    <SpaProxyLaunchCommand>npm run serve</SpaProxyLaunchCommand>
+    <SpaProxyServerUrl>http://localhost:8081</SpaProxyServerUrl>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\IES.ExamLib\IES.ExamLibrary.csproj" />
+    <ProjectReference Include="..\IES.ExamViews\IES.ExamViews.esproj">
+      <ReferenceOutputAssembly>false</ReferenceOutputAssembly>
+    </ProjectReference>
+  </ItemGroup>
+
+  <ItemGroup>
+	  <PackageReference Include="AutoMapper" Version="13.0.1" />
+	  <PackageReference Include="LiteDB" Version="5.0.21" />
+	  <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.36" />
+	  <PackageReference Include="Microsoft.VisualBasic" Version="10.3.0" />
+	  <PackageReference Include="NLog" Version="5.3.4" />
+	  <PackageReference Include="NLog.Extensions.Logging" Version="5.3.15" />
+	  <PackageReference Include="SkiaSharp.QrCode" Version="0.7.0" />
+	  <PackageReference Include="System.Diagnostics.PerformanceCounter" Version="6.0.2" />
+	  <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
+	  <PackageReference Include="System.Management" Version="6.0.2" />
+	  <PackageReference Include="ZXing.Net.Bindings.ZKWeb.System.Drawing" Version="0.16.7" />
+    <PackageReference Include="Microsoft.AspNetCore.SpaProxy">
+      <Version>6.*-*</Version>
+    </PackageReference>
+  </ItemGroup>
+
+  <ItemGroup>
+    <Folder Include="Logs\DataLogs\" />
+  </ItemGroup>
+
+</Project>

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

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

+ 77 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Models/ServerDevice.cs

@@ -0,0 +1,77 @@
+using IES.ExamServer.Services;
+
+namespace IES.ExamServer.Models
+{
+   
+    public class ServerDevice
+    {
+        public string? id { get; set; }
+        public School? school { get; set; }
+        /// <summary>
+        /// 设备id
+        /// </summary>
+        public string? deviceId { get; set; }
+        public string? userName { get; set; }
+        /// <summary>
+        /// 机器名
+        /// </summary>
+        public string? name { get; set; }
+        /// <summary>
+        /// 操作系统
+        /// </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 List<CPUInfo> cpuInfos { get; set; } = new List<CPUInfo>();
+        /// <summary>
+        /// 内存大小
+        /// </summary>
+        public long ram { get; set; }
+        /// <summary>
+        /// 远程ip
+        /// </summary>
+        public string? remote { get; set; }
+        /// <summary>
+        /// 端口,可能有多个端口
+        /// </summary>
+        public List<UriInfo> uris { get; set; } = new List<UriInfo>();
+        /// <summary>
+        /// 地区
+        /// </summary>
+        public string? region { get; set; }
+        /// <summary>
+        /// 网卡 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
+    {
+        public string? name { get; set; }
+        public string? mac { get; set; }
+        public string? ip { get; set; }
+    }
+}

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

@@ -0,0 +1,117 @@
+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>();
+        /// <summary>
+        /// 数据id
+        /// </summary>
+        public string? dataId {  get; set; }
+        /// <summary>
+        /// 数据名称
+        /// </summary>
+        public string? dataName {  get; set; }
+    }
+ 
+    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; }
+    }
+}

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

@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace IES.ExamServer.Models
+{
+    public class Teacher
+    {
+        public string? id {  get; set; }
+        public string? name { get; set; }
+        public string? picture {  get; set; }
+        public string? x_auth_token {  get; set; }
+        public List<School>? schools { get; set; }= new List<School>();
+        public TmdidImplicit? implicit_token { get; set; }
+        public long loginTime { get; set; }
+    }
+
+    public class School
+    {
+        public string? id { get; set; }
+        public string? name { get; set; }
+        public string? picture { get; set; }
+    }
+    [Serializable]
+    public class TmdidImplicit
+    {
+        public string? id_token { get; set; }
+        public string? access_token { get; set; }
+        public string? expires_in { get; set; }
+        public string? token_type { get; set; }
+    }
+
+   
+}

+ 123 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Program.cs

@@ -0,0 +1,123 @@
+using IES.ExamServer.DI;
+using IES.ExamServer.DI.SignalRHost;
+using IES.ExamServer.Filters;
+using Microsoft.AspNetCore.StaticFiles;
+using NLog.Extensions.Logging;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Text.Unicode;
+
+namespace IES.ExamServer.Server
+{
+    public class Program
+    {
+        public async static Task Main(string[] args)
+        {
+            Console.OutputEncoding = Encoding.UTF8;
+            var builder = WebApplication.CreateBuilder(args);
+            string path = $"{builder.Environment.ContentRootPath}/Configs";
+            // Add services to the container.
+            builder.Configuration.SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
+            // Add services to the container.
+            builder.Services.AddControllers().AddJsonOptions(options =>
+            {
+                // 设置 JSON 序列化选项
+                options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All); // 允许所有 Unicode 字符
+                options.JsonSerializerOptions.WriteIndented = true; // 格式化输出(可选)
+            }); ;
+            builder.Services.AddHttpClient();
+            builder.Services.AddSignalR();
+            builder.Services.AddHttpContextAccessor();
+            //此处能在Linux及MacOS运行,
+            //Windows的路径是LocalApplicationData Path: C:\Users\john\AppData\Local
+            //Linux的路径是LocalApplicationData Path: /home/john/.local/share,
+            //MacOS 的路径LocalApplicationData Path: /Users/john/Library/Application Support
+            string localAppDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+            //string dbpath = $"{localAppDataPath}\\ExamServer\\LiteDB";
+            string dbpath = Path.Combine(localAppDataPath, "ExamServer", "LiteDB");
+            if (!System.IO.Directory.Exists(dbpath))
+            {
+                System.IO.Directory.CreateDirectory(dbpath);
+            }
+            string liteDBPath = $"Filename={dbpath}/data.db;Connection=shared";
+
+            var connections_LiteDB = new List<LiteDBFactoryOptions>
+            {
+                new LiteDBFactoryOptions { Name = "Master", Connectionstring = liteDBPath}
+            };
+            builder.Services.AddLiteDB(connections_LiteDB);
+            builder.Services.AddMemoryCache();
+            // 注册 ConnectionService 为单例
+            builder.Services.AddSingleton<CenterServiceConnectionService>();
+            builder.Services.AddSingleton<ServiceInitializer>();
+            builder.Services.AddCors(options =>
+            {
+                //options.AddDefaultPolicy(
+                //builder =>
+                //{
+
+                //    builder.AllowAnyOrigin()
+                //            .AllowAnyHeader()
+                //            .AllowAnyMethod();
+                //});
+                options.AddPolicy("any", builder =>
+                {
+                    builder.SetIsOriginAllowed(x => true)
+                    .AllowAnyMethod()
+                    .AllowAnyHeader()
+                    .AllowCredentials();
+                });
+
+            });
+            builder.Services.AddMvcFilter<AuthTokenActionFilter>();
+            // 添加自定义日志提供程序
+            //builder.Logging.ClearProviders();
+            //bool enableConsoleOutput = true;
+            //builder.Logging.AddProvider(new CustomFileLoggerProvider(Path.Combine(Directory.GetCurrentDirectory(), "Logs"), enableConsoleOutput));
+            // 添加日志服务
+            builder.Logging.ClearProviders();
+            builder.Logging.AddNLog();
+            builder.Services.AddHostedService<SignalRCloudClientHub>();
+            var app = builder.Build();
+
+            app.UseDefaultFiles();
+
+            var contentTypeProvider = new FileExtensionContentTypeProvider();
+            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();
+
+            // Configure the HTTP request pipeline.
+
+            app.UseHttpsRedirection();
+            app.UseRouting();
+            app.UseCors("any");
+            app.UseAuthorization();
+
+
+            app.MapControllers();
+            app.MapHub<SignalRExamServerHub>("/signalr/exam").RequireCors("any");
+
+            app.MapFallbackToFile("/index.html");
+
+            // 获取 ServiceInitializer 实例并初始化
+            var connectionManager = app.Services.GetRequiredService<ServiceInitializer>();
+            await connectionManager.InitializeAsync();
+            await app.RunAsync();
+        }
+    }
+}

+ 34 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Properties/launchSettings.json

@@ -0,0 +1,34 @@
+{
+  "$schema": "https://json.schemastore.org/launchsettings.json",
+  "iisSettings": {
+    "windowsAuthentication": false,
+    "anonymousAuthentication": true,
+    "iisExpress": {
+      "applicationUrl": "http://localhost:57655",
+      "sslPort": 44395
+    }
+  },
+  "profiles": {
+    "IES.ExamServer": {
+      "commandName": "Project",
+      "dotnetRunMessages": true,
+      "launchBrowser": true,
+      "launchUrl": "",
+      "applicationUrl": "https://localhost:7113;http://localhost:5049",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development",
+        "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
+      }
+    },
+    "IIS Express": {
+      "commandName": "IISExpress",
+      "launchBrowser": true,
+      "launchUrl": "",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development",
+        "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
+      }
+    }
+  }
+}
+

+ 461 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Services/IndexService.cs

@@ -0,0 +1,461 @@
+using IES.ExamServer.Helpers;
+using Microsoft.Extensions.Caching.Memory;
+using System.Diagnostics;
+using System.Net.NetworkInformation;
+using System.Net;
+using System.Runtime.InteropServices;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.RegularExpressions;
+using System.Management;
+using IES.ExamServer.Models;
+using System.CodeDom.Compiler;
+ 
+namespace IES.ExamServer.Services
+{
+    public static class IndexService
+    {
+        public static ServerDevice GetServerDevice( string remote,string region)
+        {
+            string hostName = $"{Environment.UserName}-{Dns.GetHostName()}";
+            string os = RuntimeInformation.OSDescription;
+            //获取当前客户端的服务端口
+            string currentUserName = Environment.UserName;
+            string MachineName =Environment.MachineName;
+            //var s = Environment;
+            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))
+            {
+                CpuCoreCount = Environment.ProcessorCount;
+                MenemorySize = MemoryInfo.GetTotalPhysicalMemory();
+                //using (PerformanceCounter pc = new PerformanceCounter("Memory", "Available Bytes"))
+                //{
+                //    // 获取可用内存
+                //    AvailableMemory = (long)pc.NextValue();
+
+                //}
+                //using (PerformanceCounter pc = new PerformanceCounter("Memory", "Committed Bytes"))
+                //{
+                //    // 获取可用内存
+                //    CommittedMemory= (long)pc.NextValue();
+
+                //}
+                //MenemorySize=AvailableMemory+CommittedMemory;
+                //
+
+                // 获取CPU核心数
+                //int processorCount = Environment.ProcessorCount;
+                //Console.WriteLine("CPU 核心数: " + processorCount);
+                //using (ManagementClass managementClass = new ManagementClass("Win32_Processor"))
+                //{
+                //    using (ManagementObjectCollection managementObjectCollection = managementClass.GetInstances())
+                //    {
+                //        foreach (ManagementObject managementObject in managementObjectCollection)
+                //        {
+                //            CpuCoreCount += Convert.ToInt32(managementObject.Properties["NumberOfLogicalProcessors"].Value);
+                //        }
+                //    }
+                //}
+                //using (ManagementObjectSearcher searcher1 = new ManagementObjectSearcher("SELECT TotalPhysicalMemory FROM Win32_ComputerSystem"))
+                //{
+                //    foreach (ManagementObject obj in searcher1.Get())
+                //    {
+                //       long  MenemorySizes1 = Convert.ToInt64(obj["TotalPhysicalMemory"]);
+                //        break; // 只需要第一个结果
+                //    }
+                //}
+
+                //using (ManagementClass mc = new ManagementClass("Win32_ComputerSystem"))
+                //{
+                //    using (ManagementObjectCollection moc = mc.GetInstances())
+                //    {
+                //        foreach (ManagementObject mo in moc)
+                //        {
+                //            if (mo["TotalPhysicalMemory"]!= null)
+                //            {
+                //                MenemorySize = Convert.ToInt64(mo["TotalPhysicalMemory"]);
+                //            }
+                //        }
+                //    }
+                //}
+                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 });
+                //}
+                string cpuName =  MemoryInfo.GetCpuName();
+                var clockSpeed = MemoryInfo.GetCpuSpeed();
+                device.cpuInfos.Add(new CPUInfo { name = cpuName, hz = $"{clockSpeed}" });
+            }
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+            {
+                //int processorCount = Environment.ProcessorCount;
+                // Console.WriteLine("CPU 核心数: " + processorCount);
+                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)
+                {
+                    var matchResult = Regex.Match(match, @"\d+");
+                    if (matchResult.Success)
+                    {
+                        MenemorySize=  long.Parse(matchResult.Value);
+                    }
+                }
+            }
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+            {
+                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
+                {
+                    using (var process = new Process())
+                    {
+                        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;
+                        }
+                    }
+                }
+                catch (Exception ex) { }
+                try
+                {
+                    using (var process = new Process())
+                    {
+
+
+                        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();
+            foreach (var nic in nics)
+            {
+                if (nic.OperationalStatus == OperationalStatus.Up)
+                {
+                    var name = $"{nic.Name}-{nic.Description}";
+                    var mac = nic.GetPhysicalAddress().ToString();
+                    var properties = nic.GetIPProperties();
+                    var unicastAddresses = properties.UnicastAddresses;
+                    foreach (var unicast in unicastAddresses)
+                    {
+                        if (unicast.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
+                        {
+                            var ip = unicast.Address.ToString();
+                            Network network = new Network() { mac=mac, ip=ip, name= name };
+                            if (!string.IsNullOrWhiteSpace(mac.ToString())  && !mac.Equals("000000000000"))
+                            {
+                                device.networks.Add(network);
+                            }
+                        }
+                    }
+                }
+            }
+            //if (_url!.IsNotEmpty())
+            //{
+            //    List<UriInfo> ports = new List<UriInfo>();
+            //    foreach (var url in _url!)
+            //    {
+            //        Uri uri = new Uri(url);
+            //        device.uris.Add(new UriInfo { port= uri.Port, protocol= uri.Scheme });
+            //    }
+                 
+            //}
+            //else
+            //{
+            //    throw new Exception("未获取到端口信息!");
+            //}
+            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;
+            device.id= hashData;
+            return device;
+        }
+
+
+        /// <summary>
+        /// 直接获取设备。
+        /// </summary>
+        /// <param name="httpContext"></param>
+        /// <param name="IP"></param>
+        /// <param name="device_timeSpan"></param>
+        /// <param name="_azureRedis"></param>
+        /// <returns></returns>
+        public static string GetDevice(HttpContext httpContext, IMemoryCache _memoryCache)
+        {
+            string IP= GetIP(httpContext);
+            var cookie = httpContext.Request.Cookies;
+            string device =string.Empty;
+            if (cookie != null)
+            {
+                ///设备是否存在
+                foreach (var ck in cookie)
+                {
+                    if (ck.Key.Equals("device"))
+                    {
+                        //redis如果存在则
+                        var fingerprint = ck.Value.Split("-");
+                        if (fingerprint.Length == 2 && IP.Equals(fingerprint[1]))
+                        {
+                            if (!_memoryCache.TryGetValue<string>($"device:{fingerprint[0]}:{IP}", out device)) 
+                            {
+                                _memoryCache.Set($"device:{fingerprint[0]}:{IP}", $"{fingerprint[0]}-{IP}");
+                                device = $"{fingerprint[0]}-{IP}";
+                            }
+                            
+                        }
+                    }
+                }
+            }
+            return device;
+        }
+        public static string GetIP(HttpContext httpContext)
+        {
+            var IpPort = httpContext.Request.Headers["X-Forwarded-For"].FirstOrDefault();
+            if (string.IsNullOrEmpty(IpPort))
+            {
+                IpPort = $"{httpContext.Connection.RemoteIpAddress}";
+            }
+            if (IpPort.Contains("::"))
+            {
+                IpPort = "127.0.0.1";
+            }
+            return IpPort;
+        }
+
+        /// <summary>
+        /// 初始化设备
+        /// </summary>
+        /// <param name="httpContext"></param>
+        /// <param name="fingerprint">浏览器指纹</param>
+        /// <param name="IP"></param>
+        /// <param name="device_timeSpan"></param>
+        /// <param name="_azureRedis"></param>
+        /// <returns></returns>
+        public static   string GetDeviceInit(this HttpContext httpContext, string fingerprint, string IP, IMemoryCache _memoryCache)
+        {
+            string device = $"{fingerprint}-{IP}";
+            List<string> cookieString = new List<string>();
+            var cookie = httpContext.Request.Cookies;
+            int status = 1;
+            if (cookie != null)
+            {
+                ///设备是否存在
+                foreach (var ck in cookie)
+                {
+                    if (ck.Key.Equals("device"))
+                    {
+                        if (device.Contains("-") && device.Contains("."))
+                        {
+                            //如果匹配的是fingerprint-IP 则是已经存在的。
+                            if (ck.Value.Equals(device))
+                            {
+                                //redis如果存在则
+                                _memoryCache.TryGetValue<string>($"device:{fingerprint}:{IP}", out string device_exist);
+                                // 返回的则应该是ck.Value=exist_device_exist的数据
+                                if (device_exist!=null)
+                                {
+                                    if (!string.IsNullOrWhiteSpace($"{device_exist}"))
+                                    {
+                                        //0是代表指纹和IP匹配,正常返回的
+                                        status = 1;
+                                    }
+                                    else
+                                    {
+                                        //需要新建 fingerprint-IP
+                                        status = 1;
+                                    }
+                                }
+                                else
+                                {
+                                    status = 1;
+                                }
+
+                            }
+                            else
+                            {
+                                string ck_ip = ck.Value.Split("-")[1];
+                                if (ck_ip.Equals(IP))
+                                {
+                                    //传入的指纹和cookie的不一致,仍然以cookie的为准。
+                                    status = 1;
+                                    fingerprint = ck.Value.Split("-").First();
+                                    device = ck.Value;
+                                }
+                            }
+                        }
+
+                        else
+                        {
+                            //如果匹配的是fingerprint则是一个新的设备。
+                            if (ck.Value.Equals(fingerprint))
+                            {
+                                //检查设备是否被占用
+                                //var device_exist = _azureRedis.GetRedisClient(8).HashExists($"device:{fingerprint}", IP);
+                                _memoryCache.TryGetValue<JsonNode>($"device:{fingerprint}:{IP}", out JsonNode device_exist);
+                                if (device_exist!=null)
+                                {
+                                    //需要新建 sha1(fingerprint+uuid)-IP
+                                    status = 2;
+                                }
+                                else
+                                {
+                                    //0是代表指纹和IP匹配,正常返回的
+                                    status = 1;
+
+                                }
+                            }
+                            else
+                            {
+                                //匹配的都不是,新设备。
+                                status = 1;
+                            }
+                        }
+                    }
+                    else
+                    {
+                        cookieString.Add($"{ck.Key}{ck.Value}");
+                    }
+                }
+            }
+            /*
+                httpContext.Request.Headers.TryGetValue("accept-language", out var accept_language);
+                httpContext.Request.Headers.TryGetValue("sec-ch-ua", out var chua);
+                httpContext.Request.Headers.TryGetValue("sec-ch-ua-platform", out var platform);
+                httpContext.Request.Headers.TryGetValue("user-agent", out var useragent);
+                httpContext.Request.Headers.TryGetValue("accept", out var accept);
+                httpContext.Request.Headers.TryGetValue("accept-encoding", out var accept_encoding);
+                device = ShaHashHelper.GetSHA1($"{IP}{accept_language}{chua}{platform}{useragent}{accept}{accept_encoding}{string.Join("", cookieString)}");
+             */
+            if (status == 2)
+            {
+                fingerprint = ShaHashHelper.GetSHA1(fingerprint + Guid.NewGuid().ToString());
+                device = $"{fingerprint}-{IP}";
+            }
+            //else if (status == 1)
+            //{
+            //    device = $"{fingerprint}-{IP}";
+            //}
+            //await _azureRedis.GetRedisClient(8).HashSetAsync($"device:{fingerprint}", IP, new { device }.ToJsonString());
+            //await _azureRedis.GetRedisClient(8).KeyExpireAsync($"device:{fingerprint}", device_timeSpan);
+            _memoryCache.Set($"device:{fingerprint}:{IP}", device);
+            httpContext.Response.Cookies.Append("device", device, new CookieOptions { HttpOnly = true, MaxAge = new TimeSpan(24 * 7, 0, 0) });
+            return device;
+        }
+    }
+
+    public class UriInfo 
+    {
+        public string? protocol { get; set; }
+        public int port { get; set; }
+    }
+   
+}

+ 89 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Services/MemoryInfo.cs

@@ -0,0 +1,89 @@
+using Microsoft.Win32;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace IES.ExamServer.Services
+{
+    public static class MemoryInfo
+    {
+        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
+        private class MEMORYSTATUSEX
+        {
+            public uint dwLength;
+            public uint dwMemoryLoad;
+            public ulong ullTotalPhys;
+            public ulong ullAvailPhys;
+            public ulong ullTotalPageFile;
+            public ulong ullAvailPageFile;
+            public ulong ullTotalVirtual;
+            public ulong ullAvailVirtual;
+            public ulong ullAvailExtendedVirtual;
+
+            public MEMORYSTATUSEX()
+            {
+                dwLength = (uint)Marshal.SizeOf(typeof(MEMORYSTATUSEX));
+            }
+        }
+        // 从注册表获取 CPU 名称
+        public static string GetCpuNameFromRegistry()
+        {
+            using (RegistryKey key = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0"))
+            {
+                if (key != null)
+                {
+                    object cpuName = key.GetValue("ProcessorNameString");
+                    if (cpuName != null)
+                    {
+                        return cpuName.ToString();
+                    }
+                }
+            }
+            throw new Exception("Failed to retrieve CPU name from registry.");
+        }
+
+        // 从注册表获取 CPU 主频
+        public static uint GetCpuSpeedFromRegistry()
+        {
+            using (RegistryKey key = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0"))
+            {
+                if (key != null)
+                {
+                    object cpuSpeed = key.GetValue("~MHz");
+                    if (cpuSpeed != null)
+                    {
+                        return Convert.ToUInt32(cpuSpeed);
+                    }
+                }
+            }
+            throw new Exception("Failed to retrieve CPU speed from registry.");
+        }
+        // 从注册表获取 CPU 主频
+       
+
+        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
+        private static extern bool GlobalMemoryStatusEx([In, Out] MEMORYSTATUSEX lpBuffer);
+        public static string GetCpuName()
+        {
+            return GetCpuNameFromRegistry();
+        }
+
+        // 获取 CPU 主频
+        public static uint GetCpuSpeed()
+        {
+            return GetCpuSpeedFromRegistry();
+        }
+        /// <summary>
+        /// 获取系统的总物理内存(以字节为单位)
+        /// </summary>
+        public static long GetTotalPhysicalMemory()
+        {
+            MEMORYSTATUSEX memStatus = new MEMORYSTATUSEX();
+            if (GlobalMemoryStatusEx(memStatus))
+            {
+                return (long)memStatus.ullTotalPhys;
+            }
+            throw new System.ComponentModel.Win32Exception();
+        }
+    }
+
+}

+ 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>

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

@@ -0,0 +1,15 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Information",
+      "Microsoft.AspNetCore": "Warning"
+    }
+  },
+  "Kestrel": {
+    "Endpoints": {
+      "Https": {
+        "Url": "https://*:6001"
+      }
+    }
+  }
+}

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

@@ -0,0 +1,65 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Information",
+      "Microsoft.AspNetCore": "Warning",
+      "Microsoft": "Warning",
+      "Microsoft.Hosting.Lifetime": "Information"
+    },
+    "File": {
+      "Path": "Logs/app-.log",
+      "Append": true,
+      "MaxFileSize": 20971520, // 10 MB
+      "MaxRollingFiles": 10 // 保留最近7个文件
+    }
+  },
+  "AllowedHosts": "*",
+  "Kestrel": {
+    "Endpoints": {
+      "Https0": {
+        "Url": "https://*:8000",
+        "Certificate": {
+          "Path": "Configs/exam0/cert.pem",
+          "KeyPath": "Configs/exam0/key.pem"
+        }
+      },
+      "Https1": {
+        "Url": "https://*:8001",
+        "Certificate": {
+          "Path": "Configs/exam1/cert.pem",
+          "KeyPath": "Configs/exam1/key.pem"
+        }
+      },
+      "Https2": {
+        "Url": "https://*:8002",
+        "Certificate": {
+          "Path": "Configs/exam2/cert.pem",
+          "KeyPath": "Configs/exam2/key.pem"
+        }
+      },
+      "Https3": {
+        "Url": "https://*:8003",
+        "Certificate": {
+          "Path": "Configs/exam3/cert.pem",
+          "KeyPath": "Configs/exam3/key.pem"
+        }
+      },
+      "Https4": {
+        "Url": "https://*:8004",
+        "Certificate": {
+          "Path": "Configs/exam4/cert.pem",
+          "KeyPath": "Configs/exam4/key.pem"
+        }
+      }
+    }
+  },
+  "ExamServer": {
+    "Timeout": 30000,
+    "Delay": 500,
+    "CenterUrl": "https://www.teammodel.cn", //https://www.teammodel.cn,https://localhost:5001
+    "NotifyUrl": "https://www.winteach.cn"
+  },
+  "ExamClient": {
+    "Domain": "exam.habook.local"
+  }
+}

+ 24 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/nlog.config

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      autoReload="true"
+      throwConfigExceptions="true">
+	<targets>
+		<target name="file"
+				xsi:type="File"
+				fileName="Logs/${shortdate}.log"
+		layout="${longdate} ${level:uppercase=true} ${message} ${exception:format=ToString}"
+		archiveFileName="Logs/{#}.log"
+		archiveEvery="Day"
+		archiveNumbering="Date"
+		maxArchiveFiles="30"
+		keepFileOpen="false"
+		concurrentWrites="true" />
+		<target name="console"
+				xsi:type="Console"
+				layout="${longdate} ${level:uppercase=true} ${message} ${exception:format=ToString}" />
+	</targets>
+	<rules>
+		<logger name="*" minlevel="Info" writeTo="file,console" />
+	</rules>
+</nlog>

+ 9 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/.editorconfig

@@ -0,0 +1,9 @@
+[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
+charset = utf-8
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+end_of_line = lf
+max_line_length = 100

+ 1 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/.gitattributes

@@ -0,0 +1 @@
+* text=auto eol=lf

+ 30 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/.gitignore

@@ -0,0 +1,30 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+*.tsbuildinfo

+ 11 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/IES.ExamViews.esproj

@@ -0,0 +1,11 @@
+<Project Sdk="Microsoft.VisualStudio.JavaScript.Sdk/0.5.128-alpha">
+  <PropertyGroup>
+    <StartupCommand>npm run serve</StartupCommand>
+    <JavaScriptTestRoot>.\</JavaScriptTestRoot>
+    <JavaScriptTestFramework>Jest</JavaScriptTestFramework>
+    <!-- Allows the build (or compile) script located on package.json to run on Build -->
+    <ShouldRunBuildScript>false</ShouldRunBuildScript>
+    <!-- Folder where production build objects will be placed -->
+    <BuildOutputFolder>$(MSBuildProjectDirectory)\dist</BuildOutputFolder>
+  </PropertyGroup>
+</Project>

+ 35 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/README.md

@@ -0,0 +1,35 @@
+# ies.examserver.client
+
+This template should help get you started developing with Vue 3 in Vite.
+
+## Recommended IDE Setup
+
+[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
+
+## Customize configuration
+
+See [Vite Configuration Reference](https://vite.dev/config/).
+
+## Project Setup
+
+```sh
+npm install
+```
+
+### Compile and Hot-Reload for Development
+
+```sh
+npm run dev
+```
+
+### Compile and Minify for Production
+
+```sh
+npm run build
+```
+
+### Lint with [ESLint](https://eslint.org/)
+
+```sh
+npm run lint
+```

+ 5 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/babel.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+  presets: [
+    '@vue/cli-plugin-babel/preset'
+  ]
+}

+ 17 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/eslint.config.js

@@ -0,0 +1,17 @@
+import js from '@eslint/js'
+import pluginVue from 'eslint-plugin-vue'
+
+export default [
+  {
+    name: 'app/files-to-lint',
+    files: ['**/*.{js,mjs,jsx,vue}'],
+  },
+
+  {
+    name: 'app/files-to-ignore',
+    ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
+  },
+
+  js.configs.recommended,
+  ...pluginVue.configs['flat/essential'],
+]

+ 13 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="">
+  <head>
+    <meta charset="UTF-8">
+    <link rel="icon" href="/favicon.ico">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Vite App</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 19 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/jsconfig.json

@@ -0,0 +1,19 @@
+{
+  "compilerOptions": {
+    "target": "es5",
+    "module": "esnext",
+    "baseUrl": "./",
+    "moduleResolution": "node",
+    "paths": {
+      "@/*": [
+        "src/*"
+      ]
+    },
+    "lib": [
+      "esnext",
+      "dom",
+      "dom.iterable",
+      "scripthost"
+    ]
+  }
+}

+ 55 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/package.json

@@ -0,0 +1,55 @@
+{
+  "name": "ies.examwebview",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "@azure/storage-blob": "^12.26.0",
+    "@fingerprintjs/fingerprintjs": "^4.5.1",
+    "@microsoft/signalr": "^8.0.7",
+    "axios": "^1.7.9",
+    "clean-webpack-plugin": "^4.0.0",
+    "compression-webpack-plugin": "^11.1.0",
+    "core-js": "^3.8.3",
+    "element-ui": "^2.15.14",
+    "js-audio-recorder": "^1.0.7",
+    "qrcodejs2": "^0.0.2",
+    "vue": "^2.6.14",
+    "vue-router": "^3.6.5"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.12.16",
+    "@babel/eslint-parser": "^7.12.16",
+    "@vue/cli-plugin-babel": "~5.0.0",
+    "@vue/cli-plugin-eslint": "~5.0.0",
+    "@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": {
+    "root": true,
+    "env": {
+      "node": true
+    },
+    "extends": [
+      "plugin:vue/essential",
+      "eslint:recommended"
+    ],
+    "parserOptions": {
+      "parser": "@babel/eslint-parser"
+    },
+    "rules": {}
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead"
+  ]
+}

BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/public/favicon.ico


+ 17 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/public/index.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="">
+  <head>
+    <meta charset="utf-8">
+    <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>局域网评测</title>
+  </head>
+  <body>
+    <noscript>
+      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+    </noscript>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 129 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/public/signalr.html

@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+    <meta charset="utf-8" />
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/8.0.7/signalr.min.js"></script>
+    <title></title>
+    <style type="text/css">
+        .content {
+            width: 100%;
+            height: 100%;
+            display: flex;
+            justify-content: left;
+            border: solid #4cff00 1px;
+            margin-top: 2rem;
+        }
+
+        .badge {
+            font-size: large;
+            font-weight: bolder;
+            color: aquamarine;
+        }
+
+        #messagesList {
+            width: 100%;
+        }
+    </style>
+</head>
+<body>
+
+    <div class="container">
+        <textarea rows="3" id="text" cols="120" class="text" placeholder="输入要发送的文字信息"></textarea>
+        <button id="send" class="btn btn-primary">发送</button>
+        <div class="content">
+            <ul id="messagesList" class="list-group">
+            </ul>
+        </div>
+    </div>
+    <script>
+        window.onload = function () {
+            const clientid = '9f4ad914254c028f0b724fe0800ee2ab-127.0.0.1';
+            const data = {
+                clientid: clientid,
+                grant_type: 'ies_qrcode_login'
+            }
+            if (data.clientid) {
+                const url = `/signalr/exam?grant_type=${data.grant_type}&clientid=${data.clientid}`
+
+                // 自定义重连策略
+                const reconnectPolicy = {
+                    nextRetryDelayInMilliseconds: function (retryContext) {
+                        const maxRetries = 360;
+                        const retryDelaySeconds = 10;
+
+                        if (retryContext.previousRetryCount >= maxRetries) {
+                            return null; // 达到最大重试次数后不再重连
+                        }
+
+                        return retryDelaySeconds * 1000; // 返回下一次重连的延迟时间(毫秒)
+                    }
+                };
+                const connection = new signalR.HubConnectionBuilder()
+                    .withUrl(url)
+                    .withAutomaticReconnect(reconnectPolicy) // 使用自定义重连策略
+                    .configureLogging(signalR.LogLevel.Information)
+                    .build();
+                connection.start().then(function () {
+                    console.log(`${data.clientid}连接成功......`);
+                    appendContent(`${data.clientid}连接成功......`);
+                });
+                connection.on("ReceiveConnection", (message) => {
+                    console.log('ReceiveConnection-MESSAGE', message);
+                    appendContent(message.content);
+                });
+                connection.on("ReceiveMessage", (message) => {
+                    console.log('ReceiveMessage-MESSAGE', message);
+                    appendContent(message.content, message.clientid);
+                });
+                connection.onreconnecting((error) => {
+                    console.log(`连接断开,正在尝试重新连接... 错误: ${error}`);
+                });
+
+                connection.onreconnected((connectionId) => {
+                    console.log(`重新连接成功,新的连接ID: ${connectionId}`);
+                });
+
+                connection.onclose((error) => {
+                    console.log(`连接关闭,错误: ${error}`);
+                });
+                const btn = document.getElementById("send")
+                btn.addEventListener('click', function () {
+                    const target = document.getElementById('text')
+                    connection.invoke("ReceiveMessage", data.clientid, data.grant_type,target.value)
+                        .then(_ => {
+                            target.value = '';
+                        })
+                        .catch(err => console.error(err.toString()));
+                })
+            }
+        }
+        function appendContent(content, badge) {
+            let li = document.createElement("li");
+            li.className = 'list-group-item';
+            if (badge) {
+                const span = document.createElement('span');
+                span.className = 'badge';
+                span.innerText = badge + ':';
+                li.appendChild(span);
+            }
+            li.innerHTML += content;
+            document.getElementById("messagesList").appendChild(li);
+        }
+
+        //async function start() {
+        //    try {
+        //        await connection.start();
+        //        console.log("已经连接 SignalR Connected.");
+        //    } catch (err) {
+        //        console.log(err);
+        //        setTimeout(start, 5000);
+        //    }
+        //};
+        //connection.onclose(async () => {
+        //    await start();
+        //});
+        //start();
+    </script>
+</body>
+</html>

+ 45 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/App.vue

@@ -0,0 +1,45 @@
+<template>
+    <div id="app">
+        <router-view></router-view>
+    </div>
+</template>
+
+<script>
+import HelloWorld from "./components/HelloWorld.vue"
+
+export default {
+    name: "App",
+    components: {
+        HelloWorld,
+    },
+}
+</script>
+
+<style>
+#app {
+    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;
+    color: #2c3e50;
+    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>

+ 596 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/api/http.js

@@ -0,0 +1,596 @@
+import axios from 'axios'
+
+import { app } from '@/main';
+
+import { Loading } from 'element-ui';
+
+
+
+// 不需携带access_token
+
+const NO_ACCESS_API = [
+
+  '/index/device',
+
+  '/index/login-init',
+
+  '/index/login-check',
+
+  '/index/list-schools',
+
+  '/index/bind-school',
+
+]
+
+// 需要携带access_token 不需要auth-token
+
+const NO_AUTH_API = []
+
+// 不进行错误提示白名单
+
+const NO_WARNING = []
+
+
+
+
+
+// token刷新中,挂载的请求
+
+let requestStack = []
+
+// 记录token刷新中发起的请求
+
+function track(prms) {
+
+  requestStack.push(prms)
+
+}
+
+// token刷新完成触发挂起的请求继续请求
+
+function trigger() {
+
+  let index = requestStack.length
+
+  while (index > 0) {
+
+    requestStack[index - 1]()
+
+    requestStack.pop()
+
+    index--
+
+  }
+
+}
+
+
+
+let refreshing = false //是否刷新token中
+
+axios.defaults.timeout = 30000 //设置超时时长
+
+axios.defaults.baseURL = ''
+
+
+
+let loading = undefined
+
+/* Loading.service({
+
+    lock: true,
+
+    text: '加载中',
+
+    background: 'rgba(0, 0, 0, 0.7)'
+
+}) */
+
+
+
+// http request 拦截器
+
+axios.interceptors.request.use(config => {
+
+  loading = Loading.service({
+
+    lock: true,
+
+    text: '加载中',
+
+    background: 'rgba(0, 0, 0, 0.7)'
+
+  })
+
+  let isNeedAccess = true
+
+  // 1. 登录及登录前,不检查API是否需要accesstoken
+
+  for (let apiUrl of NO_ACCESS_API) {
+
+    if (config.url.includes(apiUrl)) {
+
+      isNeedAccess = false
+
+      break
+
+    }
+
+  }
+
+  if (!isNeedAccess) return config
+
+
+
+  // 2. 登录后,
+
+  // 检查操作时间
+
+  let webEndTime = localStorage.getItem('webEndTime')
+
+  let time_now = new Date().getTime()
+
+  if (webEndTime && time_now > webEndTime) {
+
+    loginOut()
+
+    sessionStorage.setItem('loginOut', '长时间未操作,重新登录')
+
+    return
+
+  }
+
+  // 检查是否有access_token
+
+  let access_token = localStorage.getItem('access_token')
+
+  if (!access_token) {
+
+    loginOut()
+
+    sessionStorage.setItem('loginOut', 'token无效')
+
+    return
+
+  }
+
+  // 检查是否快到期
+
+  let isExpired = checkToken()
+
+  // token未过期
+
+  if (!isExpired) {
+
+    return handleHeader(config)
+
+  }
+
+  // 刷新token
+
+  let handleRefresh = new Promise((resolve, reject) => {
+
+    track(() => {
+
+      handleHeader(config)
+
+      resolve(config)
+
+    })
+
+  })
+
+  sessionStorage.setItem('apiCount', requestStack.length)
+
+  if (!refreshing) {
+
+    refreshing = true
+
+    loginOut()
+
+    sessionStorage.setItem('loginOut', 'Token验证失败')
+
+    // refreshToken()
+
+  }
+
+  return handleRefresh
+
+}, error => {
+
+  return Promise.reject(error)
+
+})
+
+
+
+// http response 拦截器
+
+axios.interceptors.response.use(response => {
+
+  if (response.data.errCode === 2) {
+
+    /* router.push({
+
+        path: '/login',
+
+        query: {
+
+            redirect: router.currentRoute.fullPath
+
+        }
+
+    }) */
+
+    console.log('errCode');
+
+  }
+
+  // 保存最新的服务端时间
+
+  if (response.headers.date) {
+
+    localStorage.setItem('serverTime', new Date(response.headers.date).getTime())
+
+  }
+
+  // 四小时没操作过则需重新登录
+
+  let endTime = (new Date().getTime() + (4 * 60 * 60 * 1000))
+
+  localStorage.setItem('webEndTime', endTime)
+
+  setTimeout(() => {
+
+    loading.close()
+
+  }, 1500)
+
+  return response
+
+}, error => {
+
+  console.log('vbfbbtfnt', error);
+
+  if (!error.response) {
+
+    if (!NO_WARNING.includes(error.config.url)) {
+
+      app.$message({
+
+        type: 'error',
+
+        message: 'http.error'
+
+      })
+
+    }
+
+  } else if (error.response && error.response.status === 401) {
+
+    localStorage.clear()
+
+    sessionStorage.setItem('loginOut', error.config.url + ':API401,重新登录')
+
+    sessionStorage.setItem('APIInfo', JSON.stringify(error))
+
+    console.log('loginOut', error.config.url + ':API401,重新登录');
+
+    window.location.href = '/login/admin'
+
+    app.$message({
+
+      type: 'error',
+
+      message: 'http.error401'
+
+    })
+
+  } else if (error.response.status === 500) {
+
+    app.$message({
+
+      type: 'error',
+
+      message: 'http.error500'
+
+    })
+
+  } else if (error.response.status === 404) {
+
+    app.$message({
+
+      type: 'error',
+
+      message: 'http.error404'
+
+    })
+
+  } else if (error.response.status === 501) {
+
+    app.$message({
+
+      type: 'error',
+
+      message: 'http.error501'
+
+    })
+
+  } else {
+
+    app.$message({
+
+      type: 'error',
+
+      message: 'http.error400'
+
+    })
+
+  }
+
+  loading.close()
+
+  return Promise.reject(error)
+
+})
+
+
+
+function handleHeader(config) {
+
+  config.headers['Authorization'] = 'Bearer ' + localStorage.getItem('access_token')
+
+  config.headers['Content-Type'] = 'application/json'
+
+  config.headers['lang'] = localStorage.getItem('local') || navigator.language.toLowerCase()
+
+
+
+  let isNeedAuth = true
+
+  for (let apiUrl of NO_AUTH_API) {
+
+    if (config.url.includes(apiUrl)) {
+
+      console.log('auth-token无效', config)
+
+      isNeedAuth = false
+
+      break
+
+    }
+
+  }
+
+  if (!isNeedAuth) return config
+
+
+
+  // 检查auth-token是否存在
+
+  let identity = sessionStorage.getItem('identity')
+
+  // 正式使用要调整为个人和专家
+
+  let auth_token = identity === 'student' ? localStorage.getItem('stu_auth_token') : localStorage.getItem('auth_token')
+
+  if (!auth_token) {
+
+    console.log('auth_token失败', config)
+
+    loginOut()
+
+    sessionStorage.setItem('loginOut', 'localStorage没有auth_token:auth_token失败,重新登录')
+
+    return
+
+  }
+
+  // 通过验证,设置对应参数
+
+  config.headers['X-Auth-AuthToken'] = auth_token
+
+  return config
+
+}
+
+
+
+// 检查token是否快过期
+
+function checkToken() {
+
+  if (!localStorage.getItem('expires_in')) return false
+
+
+
+  var nowTime = new Date()
+
+  var offset = nowTime.getTimezoneOffset() / 60
+
+  let cT = Date.parse(nowTime)
+
+  let eT = Date.parse(localStorage.getItem('expires_in'))
+
+  let oT = 0
+
+  let btw = eT - oT - cT
+
+  if (btw > 10 * 60 * 1000) {
+
+    return false
+
+  } else {
+
+    return true
+
+  }
+
+}
+
+
+
+// 刷新token
+
+function refreshToken() {
+
+  refreshing = true
+
+  let areaRoute = window.location.pathname.split('/')
+
+  axios.post('/activity/login-portal', {
+
+    "route": areaRoute[1],
+
+    "token": localStorage.getItem('auth_token')
+
+  }).then(res => {
+
+    if (res.data.code === 200) {
+
+      localStorage.setItem("auth_token", res.data.token)
+
+      localStorage.setItem("access_token", res.data.auth_token.access_token)
+
+      localStorage.setItem("expires_in", res.data.auth_token.expires_in)
+
+      // token刷新完成,触发挂载的API
+
+      trigger()
+
+      refreshing = false
+
+    } else {
+
+      refreshing = false
+
+      requestStack = []
+
+      loginOut()
+
+      sessionStorage.setItem('loginOut', 'Token验证失败')
+
+    }
+
+  }, err => {
+
+    refreshing = false
+
+    requestStack = []
+
+    loginOut()
+
+    sessionStorage.setItem('loginOut', 'token刷新失败,退出重新登录')
+
+  })
+
+}
+
+
+
+// 超时退出重新登录
+
+function loginOut() {
+
+  localStorage.clear()
+
+  console.log('超时退出');
+
+  // router.push({path: '/home/homePage'})
+
+  window.location.href = window.location.origin + '/login/admin'
+
+}
+
+
+
+/**
+
+ * 封装get方法
+
+ * @param url
+
+ * @param data
+
+ * @returns {Promise}
+
+ */
+
+export function fetch(url, params) {
+
+  let data = {}
+
+  data.method = url
+
+  data.params = params
+
+  data.lang = localStorage.getItem('local')
+
+  return new Promise((resolve, reject) => {
+
+    axios.get(url, data).then(response => {
+
+      resolve(response.data)
+
+      app.$message({
+
+        type: 'success',
+
+        message: '数据访问成功!'
+
+      })
+
+    }).catch(err => {
+
+      reject(err)
+
+    })
+
+  })
+
+}
+
+
+
+/**
+
+ * 封装post请求
+
+ * @param url
+
+ * @param data
+
+ * @returns {Promise}
+
+ */
+
+export function post(url, params) {
+
+  let data = {}
+
+  data.method = url
+
+  data.params = params
+
+  data.lang = localStorage.getItem('local')
+
+  return new Promise((resolve, reject) => {
+
+    axios.post(url, params).then(response => {
+
+      if (response) {
+
+        resolve(response.data)
+
+      }
+
+    }, err => {
+
+      reject(err)
+
+    })
+
+  })
+
+}
+

+ 43 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/api/index.js

@@ -0,0 +1,43 @@
+import { fetch, post } from '@/api/http'
+
+export default {
+    /**
+     * 获取当前浏览器信息
+     * @param {String} fp - 浏览器指纹
+     */
+    getDevice: function(data) {
+        return post('/index/device', data)
+    },
+
+    // 登录页面
+    /**
+     * 获取二维码 / 短信验证码
+     * @param {String} type - qrcode(二维码)  smspin(短信验证码)
+     * @param {String} area - 短信验证码:区号
+     * @param {String} to - 短信验证码:手机号
+     */
+    getCode: function(data) {
+        return post('/index/login-init', data)
+    },
+    /**
+     * 二维码登录 / 短信验证码登录
+     * @param {String} type - qrcode(二维码)  smspin(短信验证码)
+     * @param {String} randomCode - 
+     * @param {String} pin_code - 短信验证码
+     * @param {String} account - +区号-手机号
+     */
+    loginCheck: function(data) {
+        return post('/index/login-check', data)
+    },
+    getSchoolList: function(data) {
+        return post('/index/list-schools', data)
+    },
+    bindSchool: function(data) {
+        return post('/index/bind-school', data)
+    },
+
+    // 管理页面
+
+
+    // 学生页面
+}

BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/fengjing.jpg


BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/icon/icon_play.png


+ 539 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/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;
+}

+ 579 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/demo_index.html

@@ -0,0 +1,579 @@
+<!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">&#xe647;</span>
+                <div class="name">切换</div>
+                <div class="code-name">&amp;#xe647;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe806;</span>
+                <div class="name">激活</div>
+                <div class="code-name">&amp;#xe806;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon element-icons">&#xe809;</span>
+                <div class="name">激活</div>
+                <div class="code-name">&amp;#xe809;</div>
+              </li>
+          
+            <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=1737019993465') format('woff2'),
+       url('iconfont.woff?t=1737019993465') format('woff'),
+       url('iconfont.ttf?t=1737019993465') 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-qiehuan"></span>
+            <div class="name">
+              切换
+            </div>
+            <div class="code-name">.el-icon-qiehuan
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-jihuo"></span>
+            <div class="name">
+              激活
+            </div>
+            <div class="code-name">.el-icon-jihuo
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon element-icons el-icon-jihuo1"></span>
+            <div class="name">
+              激活
+            </div>
+            <div class="code-name">.el-icon-jihuo1
+            </div>
+          </li>
+          
+          <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-qiehuan"></use>
+                </svg>
+                <div class="name">切换</div>
+                <div class="code-name">#el-icon-qiehuan</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-jihuo"></use>
+                </svg>
+                <div class="name">激活</div>
+                <div class="code-name">#el-icon-jihuo</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#el-icon-jihuo1"></use>
+                </svg>
+                <div class="name">激活</div>
+                <div class="code-name">#el-icon-jihuo1</div>
+            </li>
+          
+            <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>

+ 83 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.css

@@ -0,0 +1,83 @@
+@font-face {
+  font-family: "element-icons"; /* Project id 4795944 */
+  src: url('iconfont.woff2?t=1737019993465') format('woff2'),
+       url('iconfont.woff?t=1737019993465') format('woff'),
+       url('iconfont.ttf?t=1737019993465') format('truetype');
+}
+
+.element-icons {
+  font-family: "element-icons" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.el-icon-qiehuan:before {
+  content: "\e647";
+}
+
+.el-icon-jihuo:before {
+  content: "\e806";
+}
+
+.el-icon-jihuo1:before {
+  content: "\e809";
+}
+
+.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";
+}
+

Plik diff jest za duży
+ 1 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.js


+ 128 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.json

@@ -0,0 +1,128 @@
+{
+  "id": "4795944",
+  "name": "ExamServer",
+  "font_family": "element-icons",
+  "css_prefix_text": "el-icon-",
+  "description": "",
+  "glyphs": [
+    {
+      "icon_id": "9568050",
+      "name": "切换",
+      "font_class": "qiehuan",
+      "unicode": "e647",
+      "unicode_decimal": 58951
+    },
+    {
+      "icon_id": "10082384",
+      "name": "激活",
+      "font_class": "jihuo",
+      "unicode": "e806",
+      "unicode_decimal": 59398
+    },
+    {
+      "icon_id": "10087237",
+      "name": "激活",
+      "font_class": "jihuo1",
+      "unicode": "e809",
+      "unicode_decimal": 59401
+    },
+    {
+      "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.ExamViews/src/assets/iconfont/iconfont.ttf


BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.woff


BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/iconfont/iconfont.woff2


BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/image/tmd_logo.png


BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/logo.png


BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/qrCode.png


+ 63 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/reset.css

@@ -0,0 +1,63 @@
+/* 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;
+}
+
+a {
+    -moz-transition: color .2s ease-in-out;
+    -webkit-transition: color .2s ease-in-out;
+    -ms-transition: color .2s ease-in-out;
+    transition: color .2s ease-in-out;
+    color: #2a6abb;
+    text-decoration: none;
+}

BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/source/audio.png


BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/source/excel.png


BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/source/folder.png


BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/source/image.png


BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/source/item.png


BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/source/link.png


+ 0 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/assets/source/pdf.png


Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików