using IES.ExamLibrary.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 _logger; private readonly LiteDBFactory _liteDBFactory; private readonly CenterServiceConnectionService _connectionService; private readonly int DelayMicro = 10;//微观数据延迟 private readonly int DelayMacro = 100;//宏观数据延迟 private readonly IHubContext _signalRExamServerHub; public ManageController(LiteDBFactory liteDBFactory,ILogger logger, IConfiguration configuration, IHttpClientFactory httpClientFactory, IMemoryCache memoryCache, CenterServiceConnectionService connectionService, IHubContext signalRExamServerHub) { _logger = logger; _configuration=configuration; _httpClientFactory=httpClientFactory; _memoryCache=memoryCache; _liteDBFactory=liteDBFactory; _connectionService=connectionService; _signalRExamServerHub=signalRExamServerHub; } ///通过线上回传数据需要鉴权验证等操作。 ///通过离线包回传数据需要加密操作 /// /// 清理缓存,列出缓存占用空间,type =list列出,type=clear清理,不能清理近期及正在激活的数据,并且提示清理中暂未上传或者导出的数据。 /// /// /// [HttpPost("clean-cache")] [AuthToken("admin", "teacher", "visitor")] public async Task CleanCache(JsonNode json) { return Ok(); } [HttpPost("download-package")] [AuthToken("admin","teacher")] public async Task 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().FindOne(x => x.id!.Equals(token.id)); if (teacher != null) { string? CenterUrl = _configuration.GetValue("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(); 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 CheckShortCode(JsonNode json) { string shortCode = $"{json["shortCode"]}"; string evaluationId = $"{json["evaluationId"]}"; string deviceId = $"{json["deviceId"]}"; string centerCode= string.Empty, centerMsg= string.Empty; Expression> 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( 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? evaluationClients = _liteDBFactory.GetLiteDatabase().GetCollection().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) || token.scope.Equals(ExamConstant.ScopeVisitor))//由于已经绑定学校,访客教师也可以访问中心。 { if (_connectionService.centerIsConnected) { Teacher? teacher = _liteDBFactory.GetLiteDatabase().GetCollection().FindOne(x => x.id!.Equals(token.id)); if (teacher != null) { string? CenterUrl = _configuration.GetValue("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(); if (jsonNode != null) { centerCode = $"{jsonNode["code"]}"; centerMsg = $"{jsonNode["msg"]}"; if ($"{jsonNode["code"]}".Equals("200")) { evaluationCloud = jsonNode["evaluation"]?.ToObject(); } } 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().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(); if (evaluation_data==null) { blob=1; data=1; } else { EvaluationClient? evaluationClient = evaluation_data["evaluationClient"]?.ToObject(); 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? evaluationExams = evaluation_data["evaluationExams"]?.ToObject>(); 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 contents = new List(); 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(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, }); } /// /// 激活或者取消激活考试 /// /// /// [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().FindOne(x => x.id!.Equals(id) && !string.IsNullOrWhiteSpace(x.shortCode) && x.shortCode.Equals(shortCode)); if (evaluationClient != null) { IEnumerable evaluationClients = _liteDBFactory.GetLiteDatabase().GetCollection().Find(x=>x.activate==1); if (evaluationClients != null && evaluationClients.Count() > 0) { foreach(EvaluationClient item in evaluationClients) { item.activate = 0; _liteDBFactory.GetLiteDatabase().GetCollection().Upsert(item); } } int activate = 0; if (int.TryParse(activateStr, out int _activate)) { activate = _activate; } evaluationClient.activate = activate; _liteDBFactory.GetLiteDatabase().GetCollection().Upsert(evaluationClient); } } return Ok(); } /// /// 加载本地的活动列表 /// /// /// [HttpPost("list-local-evaluation")] [AuthToken("admin", "teacher", "visitor")] public IActionResult ListLocalEvaluation(JsonNode json) { IEnumerable? evaluationClients = _liteDBFactory.GetLiteDatabase().GetCollection().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(); foreach (var property in properties) { if (!property.Name.Equals("password") && !property.Name.Equals("openCode")) { anonymousObject[property.Name] = property.GetValue(client); } } return anonymousObject; }); return Ok(new { code = 200, evaluation = result }); } else { return Ok(new { code = 200, evaluation = new Dictionary() }); } } } }