CrazyIter_Bin vor 4 Jahren
Ursprung
Commit
202c787393

+ 1 - 0
TEAMModelOS.SDK/TEAMModelOS.SDK.csproj

@@ -21,6 +21,7 @@
     <PackageReference Include="ClouDASLibx" Version="1.2.7" />
     <PackageReference Include="DocumentFormat.OpenXml" Version="2.12.3" />
     <PackageReference Include="HtmlAgilityPack" Version="1.11.32" />
+    <PackageReference Include="Lib.AspNetCore.ServerSentEvents" Version="6.0.0" />
     <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.10" />
     <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
     <PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="5.0.4" />

+ 161 - 0
TEAMModelOS/Controllers/Client/HiScanController.cs

@@ -0,0 +1,161 @@
+using Azure.Cosmos;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using System;
+using System.Collections.Generic;
+using System.IdentityModel.Tokens.Jwt;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using TEAMModelOS.Models.Dto;
+using TEAMModelOS.SDK.Models;
+using TEAMModelOS.SDK;
+using TEAMModelOS.SDK.Context.Constant.Common;
+using TEAMModelOS.SDK.DI;
+using TEAMModelOS.SDK.DI.AzureCosmos.Inner;
+using TEAMModelOS.SDK.Extension;
+using TEAMModelOS.SDK.Helper.Common.CollectionHelper;
+using TEAMModelOS.SDK.Helper.Common.StringHelper;
+using TEAMModelOS.Models;
+using Microsoft.Extensions.Options;
+using TEAMModelOS.SDK.Models.Cosmos;
+using Microsoft.AspNetCore.Authorization;
+using TEAMModelOS.Filter;
+using StackExchange.Redis;
+using TEAMModelOS.SDK.Models.Cosmos.Common.Inner;
+using TEAMModelOS.Services.Common;
+using System.IO;
+using System.Dynamic;
+using Azure.Storage.Blobs.Models;
+using Azure.Storage.Sas;
+using Lib.AspNetCore.ServerSentEvents;
+
+namespace TEAMModelOS.Controllers.Core
+{
+    [ProducesResponseType(StatusCodes.Status200OK)]
+    [ProducesResponseType(StatusCodes.Status400BadRequest)]
+    //[Authorize(Roles = "HiTool")]
+    [Route("hiscan")]
+    [ApiController]
+    public class HiScanController : ControllerBase
+    {
+        private readonly AzureRedisFactory _azureRedis;
+        private readonly AzureCosmosFactory _azureCosmos;
+        private readonly SnowflakeId _snowflakeId;
+        private readonly AzureServiceBusFactory _serviceBus;
+        private readonly DingDing _dingDing;
+        private readonly Option _option;
+        private readonly AzureStorageFactory _azureStorage;
+        private readonly ServerSentEventsService _sse;
+        public HiScanController(AzureCosmosFactory azureCosmos, AzureServiceBusFactory serviceBus, SnowflakeId snowflakeId, DingDing dingDing, IOptionsSnapshot<Option> option,
+           AzureRedisFactory azureRedis, AzureStorageFactory azureStorage, ServerSentEventsService sse)
+        {
+            _azureCosmos = azureCosmos;
+            _serviceBus = serviceBus;
+            _snowflakeId = snowflakeId;
+            _dingDing = dingDing;
+            _option = option?.Value;
+            _azureRedis = azureRedis;
+            _azureStorage = azureStorage;
+            _sse = sse;
+        }
+
+        ///<summary>
+        ///查询教师的阅卷任务列表
+        /// </summary>
+        /// <data>
+        ///    ! "code":"tmdid"
+        /// </data>
+        /// <param name="request"></param>
+        /// <returns></returns>
+        [ProducesDefaultResponseType]
+        [HttpPost("verify-qrcode")]
+       // [AuthToken(Roles = "teacher,admin")]
+        public async Task<IActionResult> VerifyQrcode(JsonElement request)
+        {
+            try {
+                if (!request.TryGetProperty("sid", out JsonElement sid)) return BadRequest();
+                if (!request.TryGetProperty("id_token", out JsonElement id_token)) return BadRequest();
+                if (!request.TryGetProperty("dev", out JsonElement dev)) return BadRequest();
+                IServerSentEventsClient sseClient;
+                if (Guid.TryParse($"{sid}", out Guid guid) && (sseClient = _sse.GetClient(guid)) != null) {
+                    var clientName = sseClient.GetProperty<string>("NAME");
+                    //var clientDID= sseClient.GetProperty<string>("DID");
+                    var isHiTeach = clientName.Contains("HiScan", StringComparison.OrdinalIgnoreCase);
+                    var jwt = new JwtSecurityToken(id_token.GetString());
+                    //TODO 此驗證IdToken先簡單檢查,後面需向Core ID新API,驗證Token
+                    if (!jwt.Payload.Iss.Equals("account.teammodel", StringComparison.OrdinalIgnoreCase)) return BadRequest();
+                    var id = jwt.Payload.Sub;
+                    jwt.Payload.TryGetValue("name", out object name);
+                    jwt.Payload.TryGetValue("picture", out object picture);
+                    List<object> schools = new List<object>();
+                    // object schools = null;
+                    string defaultschool = null;
+                    //TODO 取得Teacher 個人相關數據(課程清單、虛擬教室清單、歷史紀錄清單等),學校數據另外API處理,多校切換時不同
+                    var client = _azureCosmos.GetCosmosClient();
+                    var response = await client.GetContainer("TEAMModelOS", "Teacher").ReadItemStreamAsync(id, new PartitionKey("Base"));
+                    int size = 0;
+                    //老師個人資料(含初始化)
+                    if (response.Status == 200)
+                    {
+                        var json = await JsonDocument.ParseAsync(response.ContentStream);
+                        if (json.RootElement.TryGetProperty("schools", out JsonElement value))
+                        {
+                            if (json.RootElement.TryGetProperty("size", out JsonElement _size) && _size.ValueKind.Equals(JsonValueKind.Number))
+                            {
+                                size = _size.GetInt32();
+                            }
+                            foreach (var obj in value.EnumerateArray())
+                            {
+                                string statusNow = obj.GetProperty("status").ToString();
+                                //正式加入才会有
+                                if (statusNow == "join")
+                                {
+                                    dynamic schoolExtobj = new ExpandoObject();
+                                    var schoolJson = await client.GetContainer("TEAMModelOS", "School").ReadItemStreamAsync($"{obj.GetProperty("schoolId")}", new PartitionKey("Base"));
+                                    var school = await JsonDocument.ParseAsync(schoolJson.ContentStream);
+                                    schoolExtobj.schoolId = obj.GetProperty("schoolId");
+                                    schoolExtobj.name = obj.GetProperty("name");
+                                    schoolExtobj.status = obj.GetProperty("status");
+                                    if (obj.TryGetProperty("time", out JsonElement time))
+                                    {
+                                        schoolExtobj.time = obj.GetProperty("time");
+                                    }
+
+                                    schoolExtobj.picture = school.RootElement.GetProperty("picture");
+                                    schools.Add(schoolExtobj);
+                                    //如果有申请或者加入学校,但是未分配空间则都可以得到1G免费空间
+                                    if (size == 0)
+                                    {
+                                        size = 1;
+                                        Teacher tech = await client.GetContainer("TEAMModelOS", "Teacher").ReadItemAsync<Teacher>(id, new PartitionKey("Base"));
+                                        tech.size = size;
+                                        await client.GetContainer("TEAMModelOS", "Teacher").ReplaceItemAsync<Teacher>(tech, id, new PartitionKey("Base"));
+                                    }
+                                }
+                            }
+                        }
+                        //預設學校ID
+                        if (json.RootElement.TryGetProperty("defaultSchool", out JsonElement valueD) && !string.IsNullOrEmpty(valueD.ToString()))
+                        {
+                            defaultschool = valueD.ToString();
+                        }
+                        //換取AuthToken,提供給前端
+                        var auth_token = JwtAuthExtension.CreateAuthToken(_option.HostName, id, name?.ToString(), picture?.ToString(), _option.JwtSecretKey, roles: new[] { "teacher" });
+                        await sseClient.SendEventAsync(new { auth_token, schools }.ToJsonString());
+                        return Ok(new { auth_token, schools });
+                    }
+                    else
+                    {
+                        return Ok(new { status = 404 });
+                    }
+                }
+            } catch (Exception ex ) {
+                await _dingDing.SendBotMsg($"IES5,{_option.Location},hiscan/verify-qrcode()\n{ex.Message}{ex.StackTrace}", GroupNames.醍摩豆服務運維群組);
+                return BadRequest();
+            }
+           return Ok();
+        }
+    }
+}

+ 27 - 28
TEAMModelOS/Controllers/School/StudentController.cs

@@ -129,7 +129,7 @@ namespace TEAMModelOS.Controllers
         /// <param name="students"></param>
         /// <returns></returns>
         private (Dictionary<string, (string name, string no, int year, string salt, string pw, string classNo, string className, string periodId, int gradeIndex)> studs,
-            Dictionary<string, (string className, string periodId, int gradeIndex, int year)> classInfo,
+            Dictionary<string, (string className, string periodId, int gradeIndex, int year, string no )> classInfo,
             Dictionary<string, List<(string id, string no)>> classStudNo,
             List<string> errorYear,
             List<string> duplId) doSortImpStuds(string schoolId, JsonElement.ArrayEnumerator students)
@@ -140,7 +140,7 @@ namespace TEAMModelOS.Controllers
             Dictionary<string, (string name, string no, int  year, string salt, string pw, string classNo, string className, string periodId, int gradeIndex)> dicStuds = new Dictionary<string, (string name, string no, int year, string salt, string pw, string classNo, string className, string periodId, int gradeIndex)>();
 
             //存放教室資訊用 key:classNo value:className
-            Dictionary<string, (string className, string periodId, int gradeIndex,int year)> dicClassInfo = new Dictionary<string, (string className, string periodId, int gradeIndex, int year)>();
+            Dictionary<string, (string className, string periodId, int gradeIndex,int year,string no)> dicClassInfo = new Dictionary<string, (string className, string periodId, int gradeIndex, int year,string no)>();
             //存放欲加入該間教室的學生座號清單 key:classNo value:no list
             Dictionary<string, List<(string id, string no)>> dicClassStudNo = new Dictionary<string, List<(string id, string no)>>();
             //存放輸入id重複
@@ -220,8 +220,8 @@ namespace TEAMModelOS.Controllers
                         {
                            
                             studentInfo.className = tmpClassName.GetString();
-                            if (!dicClassInfo.ContainsKey(tmpClassNo.GetString()))
-                            { dicClassInfo.Add(tmpClassNo.GetString(), (tmpClassName.GetString(), studentInfo.periodId, studentInfo.gradeIndex, year)); }
+                            if (!dicClassInfo.ContainsKey($"{studentInfo.periodId}_{year}_{tmpClassNo.GetString()}"))
+                            { dicClassInfo.Add($"{studentInfo.periodId}_{year}_{tmpClassNo.GetString()}", (tmpClassName.GetString(), studentInfo.periodId, studentInfo.gradeIndex, year, tmpClassNo.GetString())); }
                         }
                     }
                     
@@ -246,9 +246,9 @@ namespace TEAMModelOS.Controllers
             {
                 var sortedImpData = doSortImpStuds(schoolId, students);
 
-                List<string> classNos = sortedImpData.classInfo.Select(o => o.Key).ToList();
+                //var   classNos = sortedImpData.classInfo.Select(o => new {key= o.Key, periodId=o.Value.periodId,index= o.Value.gradeIndex,year = o.Value.year }).ToList();
                 //抓到教室資訊
-                var classInfos = await getClassInfoUseNo(schoolId, classNos);
+                var classInfos = await getClassInfoUseNo(schoolId, sortedImpData.classInfo);
                 //取出已存在教室的classId,後面查座號要用。
 
                 List<Task> tasks = new List<Task>();
@@ -259,22 +259,30 @@ namespace TEAMModelOS.Controllers
                 Dictionary<string, (string classId, string className, string periodId, string gradeId, int year)> classNoId = new Dictionary<string, (string classId, string className, string periodId, string gradeId, int year)>();
                 foreach (var classInfo in classInfos)
                 {
-                    string classGradeId = (classInfo.Value.TryGetProperty("gradeId", out JsonElement classGradeIdJson)) ? classGradeIdJson.GetString() : null;
-                    int classYear = (classInfo.Value.TryGetProperty("year", out JsonElement classYearJson)) ? classYearJson.GetInt32() : 0;
+                    string classGradeId = classInfo.Value.gradeId;
+                    int classYear = classInfo.Value.year;
                     classNoId.Add(classInfo.Key,
-                        (classInfo.Value.GetProperty("id").GetString(), classInfo.Value.GetProperty("name").GetString(), classInfo.Value.GetProperty("periodId").GetString(), classGradeId, classYear));
+                        (classInfo.Value.id, classInfo.Value.name, classInfo.Value.periodId, classGradeId, classYear));
                     tasks.Add(
                         Task.Run(
                             async () =>
                             {
                                 //(id,no)
-                                var studNo = await checkStudNo(schoolId, classInfo.Value.GetProperty("id").GetString());
+                                var studNo = await checkStudNo(schoolId, classInfo.Value.id);
                                 classStudNos.Add(classInfo.Key, studNo);
                             }));
                 }
 
                 //這邊整理出不存在的教室,之後創建新教室用(比對classNo)。
-                var nonexistentClassNo = classNos.Except(classInfos.Select(o => o.Key).ToList());
+                //var nonexistentClassNo = classNos.Except(classInfos.Select(o => o.Key).ToList());
+                List<string> exsitkey =  new List<string>();
+                foreach (var classInfo in classInfos)
+                {
+                    //$"{studentInfo.periodId}_{year}_{tmpClassNo.GetString()}"
+                    var key = $"{classInfo.Value.periodId}_{classInfo.Value.year}_{classInfo.Value.no}";
+                    exsitkey.Add(key);
+                }
+                var nonexistentClassNo = exsitkey.Except(sortedImpData.classInfo.Select(o => o.Key).ToList());
                 if (nonexistentClassNo.Count() != 0)
                 {
                     var gradesInfo = await getGrades(schoolId);
@@ -1186,29 +1194,20 @@ namespace TEAMModelOS.Controllers
         /// 取得教室資訊,使用classNo進行查詢。
         /// </summary>
         /// <returns></returns>
-        private async Task<Dictionary<string, JsonElement>> getClassInfoUseNo(string schoolId, List<string> classNos)
+        private async Task<Dictionary<string, Class>> getClassInfoUseNo(string schoolId, Dictionary<string, (string className, string periodId, int gradeIndex, int year,string no)> classNos)
         {
             try
             {
+                Dictionary<string, Class> dicClassInfo = new Dictionary<string, Class>();
                 if (!(classNos == null || classNos.Count == 0))
                 {
-                    string queryText = $"SELECT * FROM c WHERE c.code = 'Class-{schoolId}' AND c.no IN ({string.Join(",", classNos.Select(o => $"'{o}'"))})";
-
-                    Dictionary<string, JsonElement> dicClassInfo = new Dictionary<string, JsonElement>();
-
-                    await foreach (Response item in _azureCosmos.GetCosmosClient().GetContainer("TEAMModelOS", "School")
-                        .GetItemQueryStreamIterator(queryText: queryText, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Class-{schoolId}") }))
-                    {
-                        using var json = await JsonDocument.ParseAsync(item.ContentStream);
-                        if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
+                   
+                    foreach (var key in classNos.Keys) {
+                        string queryText = $"SELECT * FROM c WHERE c.code = 'Class-{schoolId}' AND c.no='{classNos[key].no}' and c.year={classNos[key].year} and c.periodId='{classNos[key].periodId}' ";
+                        await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer("TEAMModelOS", "School")
+                       .GetItemQueryIterator<Class>(queryText: queryText, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Class-{schoolId}") }))
                         {
-                            var classInfos = json.RootElement.GetProperty("Documents").EnumerateArray();
-                            while (classInfos.MoveNext())
-                            {
-                                JsonElement account = classInfos.Current;
-                                string no = account.GetProperty("no").GetString();
-                                dicClassInfo.Add(no, account.Clone());
-                            }
+                            dicClassInfo[item.id] = item;
                         }
                     }
                     return dicClassInfo;

+ 11 - 0
TEAMModelOS/Startup.cs

@@ -9,6 +9,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using HTEXLib.Builders;
 using HTEXLib.Translator;
+using Lib.AspNetCore.ServerSentEvents;
 using Microsoft.AspNetCore.Authentication.JwtBearer;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Hosting;
@@ -110,6 +111,7 @@ namespace TEAMModelOS
             //注入word 標籤解析
             string path = $"{ environment.ContentRootPath}/JsonFile/Core";
             services.AddHtexTranslator(path);
+            services.AddServerSentEvents();
         }
 
         // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@@ -139,6 +141,15 @@ namespace TEAMModelOS
             app.UseEndpoints(endpoints =>
             {
                 endpoints.MapControllers();
+                endpoints.MapServerSentEvents("/service/sse", new ServerSentEventsOptions
+                {
+                    //Authorization = ServerSentEventsAuthorization.Default,
+                    OnPrepareAccept = response =>
+                    {
+                        response.Headers.Append("Cache-Control", "no-cache");
+                        response.Headers.Append("X-Accel-Buffering", "no");
+                    }
+                });
 #if DEBUG
                 endpoints.MapToVueCliProxy(
                     "{*path}",