Преглед изворни кода

Merge branch 'develop5.0-tmd' of http://106.12.23.251:10000/TEAMMODEL/TEAMModelOS into develop5.0-tmd

OnePsycho пре 4 година
родитељ
комит
4fa587b5eb
34 измењених фајлова са 1182 додато и 67 уклоњено
  1. 16 0
      Client/Client.csproj
  2. 67 0
      Client/Program.cs
  3. 143 0
      Client/SSE/ConnectedState.cs
  4. 55 0
      Client/SSE/ConnectingState.cs
  5. 37 0
      Client/SSE/DisconnectedState.cs
  6. 116 0
      Client/SSE/EventSource.cs
  7. 14 0
      Client/SSE/EventSourceState.cs
  8. 15 0
      Client/SSE/IConnectionState.cs
  9. 17 0
      Client/SSE/IServerResponse.cs
  10. 15 0
      Client/SSE/IWebRequester.cs
  11. 12 0
      Client/SSE/IWebRequesterFactory.cs
  12. 39 0
      Client/SSE/ServerResponse.cs
  13. 26 0
      Client/SSE/ServerSentEvent.cs
  14. 17 0
      Client/SSE/ServerSentEventReceivedEventArgs.cs
  15. 16 0
      Client/SSE/StateChangedEventArgs.cs
  16. 47 0
      Client/SSE/StringSplitter.cs
  17. 38 0
      Client/SSE/WatchDog.cs
  18. 30 0
      Client/SSE/WebRequester.cs
  19. 16 0
      Client/SSE/WebRequesterFactory.cs
  20. 1 0
      TEAMModelOS.SDK/TEAMModelOS.SDK.csproj
  21. 7 1
      TEAMModelOS.sln
  22. 1 0
      TEAMModelOS/ClientApp/src/assets/login/icon_student.svg
  23. 1 0
      TEAMModelOS/ClientApp/src/assets/login/icon_teacher.svg
  24. 1 0
      TEAMModelOS/ClientApp/src/assets/login/ies5_logo_2.svg
  25. BIN
      TEAMModelOS/ClientApp/src/assets/login/login_bg.jpg
  26. 1 1
      TEAMModelOS/ClientApp/src/components/public/frontEndMain/Index.less
  27. 23 10
      TEAMModelOS/ClientApp/src/components/public/frontEndMain/Index.vue
  28. 5 6
      TEAMModelOS/ClientApp/src/view/login/page/Student.vue
  29. 1 1
      TEAMModelOS/ClientApp/src/view/login/page/Teacher.less
  30. 4 5
      TEAMModelOS/ClientApp/src/view/login/page/Teacher.vue
  31. 172 15
      TEAMModelOS/ClientApp/src/view/login/test.vue
  32. 161 0
      TEAMModelOS/Controllers/Client/HiScanController.cs
  33. 27 28
      TEAMModelOS/Controllers/School/StudentController.cs
  34. 41 0
      TEAMModelOS/Startup.cs

+ 16 - 0
Client/Client.csproj

@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.3" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
+    <PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="5.0.0" />
+    <PackageReference Include="Twilio" Version="5.55.0" />
+  </ItemGroup>
+
+</Project>

+ 67 - 0
Client/Program.cs

@@ -0,0 +1,67 @@
+using Client.SSE;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net.Http;
+using System.Security.Policy;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+
+
+namespace Client
+{
+    class Program
+    {    
+        static void Main(string[] args)
+        {
+            //SSE
+            try
+            {
+                var cts = new CancellationTokenSource();
+                var header = new Dictionary<string, string>() { { "X-Auth-Name", "IES5" } };
+                //var sse = new EventSource(new Uri("https://localhost:5001/service/sse"), headeer, 5000);
+                var sse = new EventSource(new Uri("https://api2.teammodel.net/service/sse"), header, 5000);
+
+                sse.StateChanged += Sse_StateChanged;
+                sse.EventReceived += Sse_EventReceived;
+                sse.Start(cts.Token);
+
+                //cts?.Cancel(); 中斷SSE Client連線
+            }
+            catch (TaskCanceledException ex)
+            {
+                Console.WriteLine(ex.Message);
+            }
+            catch (AggregateException ex)
+            {
+                Console.WriteLine(ex.Message);
+            }
+            catch (Exception ex)
+            {
+                Console.WriteLine(ex.Message);
+            }
+            Console.ReadLine();
+        }
+
+        private static void Sse_EventReceived(object sender, ServerSentEventReceivedEventArgs e)
+        {
+            var mm = e.Message;
+            Console.WriteLine(mm.Data);
+        }
+
+
+        private static void Sse_StateChanged(object sender, StateChangedEventArgs e)
+        {
+            //CONNECTING 0 連線中
+            //OPEN 1 開啟
+            //CLOSED 2 關閉
+            var aa = e.State;
+
+            Console.WriteLine(aa.ToString());
+        }
+
+    }
+}

+ 143 - 0
Client/SSE/ConnectedState.cs

@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Net;
+using System.IO;
+using System.Threading.Tasks;
+using System.Threading;
+using System.Diagnostics;
+
+namespace Client.SSE
+{
+    public class ConnectedState : IConnectionState
+    {    
+
+        private IWebRequesterFactory mWebRequesterFactory;
+        private ServerSentEvent mSse = null;
+        private string mRemainingText = string.Empty;   // the text that is not ended with a lineending char is saved for next call.
+        private IServerResponse mResponse;
+        private Dictionary<string, string> headers;
+
+        public EventSourceState State { get { return EventSourceState.OPEN; } }
+
+        public ConnectedState(IServerResponse response, IWebRequesterFactory webRequesterFactory, Dictionary<string, string> headers)
+        {
+            mResponse = response;
+            mWebRequesterFactory = webRequesterFactory;
+            this.headers = headers;
+        }
+
+        public Task<IConnectionState> Run(Action<ServerSentEvent> msgReceived, CancellationToken cancelToken, Dictionary<string, string> headers)
+        {   
+            Task<IConnectionState> t = new Task<IConnectionState>(() =>
+            {
+                //using (mResponse)
+                {
+                    //using (var stream = mResponse.GetResponseStream())
+                    var stream = mResponse.GetResponseStream();
+                    {
+                        byte[] buffer = new byte[1024 * 8];
+                        var taskRead = stream.ReadAsync(buffer, 0, buffer.Length, cancelToken);
+
+                        try
+                        {
+                            taskRead.Wait(cancelToken);
+                        }
+                        catch (Exception ex)
+                        {
+                            Trace.WriteLine(ex, "ConnectedState.Run");
+                        }
+                        if (!cancelToken.IsCancellationRequested)
+                        {
+                            int bytesRead = taskRead.Result;
+                            if (bytesRead > 0) // stream has not reached the end yet
+                            {
+                                //Console.WriteLine("ReadCallback {0} bytesRead", bytesRead);
+                                string text = Encoding.UTF8.GetString(buffer, 0, bytesRead);
+                                text = mRemainingText + text;
+                                string[] lines = StringSplitter.SplitIntoLines(text, out mRemainingText);
+                                foreach (string line in lines)
+                                {
+                                    if (cancelToken.IsCancellationRequested) break;
+
+                                    // Dispatch message if empty lne
+                                    if (string.IsNullOrEmpty(line.Trim()) && mSse != null)
+                                    {
+                                        Trace.WriteLine("SSE Message received");
+                                        msgReceived(mSse);
+                                        mSse = null;
+                                    }
+                                    else if (line.StartsWith(":"))
+                                    {
+                                        // This a comment, just log it.
+                                        Trace.WriteLine("SSE A comment was received: " + line);
+                                    }
+                                    else
+                                    {
+                                        string fieldName = String.Empty;
+                                        string fieldValue = String.Empty;
+                                        if (line.Contains(':'))
+                                        {
+                                            int index = line.IndexOf(':');
+                                            fieldName = line.Substring(0, index);
+                                            fieldValue = line.Substring(index + 1).TrimStart();
+                                        }
+                                        else
+                                            fieldName = line;
+
+                                        if (String.Compare(fieldName, "event", true) == 0)
+                                        {
+                                            mSse = mSse ?? new ServerSentEvent();
+                                            mSse.EventType = fieldValue;
+                                        }
+                                        else if (String.Compare(fieldName, "data", true) == 0)
+                                        {
+                                            mSse = mSse ?? new ServerSentEvent();
+                                            mSse.Data = fieldValue + '\n';
+                                        }
+                                        else if (String.Compare(fieldName, "id", true) == 0)
+                                        {
+                                            mSse = mSse ?? new ServerSentEvent();
+                                            mSse.LastEventId = fieldValue;
+                                        }
+                                        else if (String.Compare(fieldName, "retry", true) == 0)
+                                        {
+                                            int parsedRetry;
+                                            if (int.TryParse(fieldValue, out parsedRetry))
+                                            {
+                                                mSse = mSse ?? new ServerSentEvent();
+                                                mSse.Retry = parsedRetry;
+                                            }
+                                        }
+                                        else
+                                        {
+                                            // Ignore this, just log it
+                                            Trace.WriteLine("SSE A unknown line was received: " + line);
+                                        }
+                                    }
+                                }
+
+                                if (!cancelToken.IsCancellationRequested)
+                                    return this;
+                            }
+                            else // end of the stream reached
+                            {
+                                Trace.WriteLine("SSE No bytes read. End of stream.");
+                            }
+                        }
+
+                        //stream.Dispose()
+                        //stream.Close();
+                        //mResponse.Close();
+                        //mResponse.Dispose();
+                        return new DisconnectedState(mResponse.ResponseUri, mWebRequesterFactory, headers);
+                    }
+                }
+            });
+
+            t.Start();
+            return t;
+        }
+    }
+}

+ 55 - 0
Client/SSE/ConnectingState.cs

@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Net;
+using System.IO;
+using System.Threading;
+using System.Diagnostics;
+
+namespace Client.SSE
+{
+    public class ConnectingState : IConnectionState
+    {      
+
+        private Uri mUrl;
+        private IWebRequesterFactory mWebRequesterFactory;
+        private Dictionary<string, string> headers;
+
+        public EventSourceState State { get { return EventSourceState.CONNECTING; } }
+
+        public ConnectingState(Uri url, IWebRequesterFactory webRequesterFactory, Dictionary<string, string> headers)
+        {
+            if (url == null) throw new ArgumentNullException("Url cant be null");
+            if (webRequesterFactory == null) throw new ArgumentNullException("Factory cant be null");
+            mUrl = url;
+            mWebRequesterFactory = webRequesterFactory;
+            this.headers = headers;
+        }
+
+        public Task<IConnectionState> Run(Action<ServerSentEvent> donothing, CancellationToken cancelToken, Dictionary<string, string> headers)
+        {
+            IWebRequester requester = mWebRequesterFactory.Create();
+            var taskResp = requester.Get(mUrl, headers);
+
+            return taskResp.ContinueWith<IConnectionState>(tsk => 
+            {
+                if (tsk.Status == TaskStatus.RanToCompletion && !cancelToken.IsCancellationRequested)
+                {
+                    IServerResponse response = tsk.Result;
+                    if (response.StatusCode == HttpStatusCode.OK)
+                    {
+                        return new ConnectedState(response, mWebRequesterFactory, headers);
+                    }
+                    else
+                    {
+                        Trace.WriteLine("Failed to connect to: " + mUrl.ToString() + response ?? (" Http statuscode: " + response.StatusCode));
+                    }
+                }
+
+                return new DisconnectedState(mUrl, mWebRequesterFactory, headers);
+            });
+        }
+    }
+}

+ 37 - 0
Client/SSE/DisconnectedState.cs

@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Client.SSE
+{
+    public class DisconnectedState : IConnectionState 
+    {
+        private Uri mUrl;
+        private IWebRequesterFactory mWebRequesterFactory;
+        private Dictionary<string, string> headers;
+
+        public EventSourceState State
+        {
+            get { return EventSourceState.CLOSED; }
+        }
+
+        public DisconnectedState(Uri url, IWebRequesterFactory webRequesterFactory, Dictionary<string, string> headers)
+        {
+            if (url == null) throw new ArgumentNullException("Url cant be null");
+            mUrl = url;
+            mWebRequesterFactory = webRequesterFactory;
+            this.headers = headers;
+        }
+
+        public Task<IConnectionState> Run(Action<ServerSentEvent> donothing, CancellationToken cancelToken, Dictionary<string, string> headers)
+        {
+            if(cancelToken.IsCancellationRequested)
+                return Task.Factory.StartNew<IConnectionState>(() => { return new DisconnectedState(mUrl, mWebRequesterFactory, headers); });
+            else
+                return Task.Factory.StartNew<IConnectionState>(() => { return new ConnectingState(mUrl, mWebRequesterFactory, headers); });
+        }
+    }
+}

+ 116 - 0
Client/SSE/EventSource.cs

@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+using System.Threading;
+
+namespace Client.SSE
+{
+    public class EventSource
+    {   
+        public event EventHandler<StateChangedEventArgs> StateChanged;
+        public event EventHandler<ServerSentEventReceivedEventArgs> EventReceived;
+
+        public CancellationTokenSource CancellationToken { get; set; }
+
+        private IWebRequesterFactory _webRequesterFactory = new WebRequesterFactory();
+        private int _timeout = 0;
+        public Uri Url { get; private set; }
+        public EventSourceState State { get { return CurrentState.State; } }
+        public string LastEventId { get; private set; }
+        private IConnectionState mCurrentState = null;
+        private CancellationToken mStopToken;
+        private CancellationTokenSource mTokenSource = new CancellationTokenSource();
+        private Dictionary<string, string> _headers;
+
+        private IConnectionState CurrentState
+        {
+            get { return mCurrentState; }
+            set
+            {
+                if (!value.Equals(mCurrentState))
+                {
+                    StringBuilder sb = new StringBuilder("State changed from ");
+                    sb.Append(mCurrentState == null ? "Unknown" : mCurrentState.State.ToString());
+                    sb.Append(" to ");
+                    sb.Append(value == null ? "Unknown" : value.State.ToString());
+                    Trace.WriteLine(sb.ToString());
+                    mCurrentState = value;
+                    OnStateChanged(mCurrentState.State);
+                }
+            }
+        }
+
+        public EventSource(Uri url, int timeout)
+        {
+            Initialize(url, timeout);
+        }
+
+        public EventSource(Uri url, Dictionary<string, string> headers, int timeout)
+        {
+            _headers = headers;
+            Initialize(url, timeout);
+        }
+
+        /// <summary>
+        /// Constructor for testing purposes
+        /// </summary>
+        /// <param name="factory">The factory that generates the WebRequester to use.</param>
+        public EventSource(Uri url, IWebRequesterFactory factory)
+        {
+            _webRequesterFactory = factory;
+            Initialize(url, 0);
+        }
+
+        private void Initialize(Uri url, int timeout)
+        {
+            _timeout = timeout;
+            Url = url;
+            CurrentState = new DisconnectedState(Url, _webRequesterFactory, _headers);
+            Trace.WriteLine("SSE EventSource created for " + url.ToString());
+        }
+
+
+        /// <summary>
+        /// Start the EventSource. 
+        /// </summary>
+        /// <param name="stopToken">Cancel this token to stop the EventSource.</param>
+        public void Start(CancellationToken stopToken)
+        {
+            if (State == EventSourceState.CLOSED)
+            {
+                mStopToken = stopToken;
+                mTokenSource = CancellationTokenSource.CreateLinkedTokenSource(stopToken);
+                Run();
+            }
+        }
+
+        protected void Run()
+        {
+            if (mTokenSource.IsCancellationRequested && CurrentState.State == EventSourceState.CLOSED)
+                return;
+
+            mCurrentState.Run(this.OnEventReceived, mTokenSource.Token, _headers).ContinueWith(cs =>
+            {
+                CurrentState = cs.Result;
+                Run();
+            });
+        }
+
+        protected void OnEventReceived(ServerSentEvent sse)
+        {
+            if (EventReceived != null)
+            {
+                EventReceived(this, new ServerSentEventReceivedEventArgs(sse));
+            }
+        }
+
+        protected void OnStateChanged(EventSourceState newState)
+        {
+            if (StateChanged != null)
+            {
+                StateChanged(this, new StateChangedEventArgs(newState));
+            }
+        }
+    }
+}

+ 14 - 0
Client/SSE/EventSourceState.cs

@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Client.SSE
+{
+    public enum EventSourceState
+    {
+        CONNECTING,
+        OPEN,
+        CLOSED 
+    }
+}

+ 15 - 0
Client/SSE/IConnectionState.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Client.SSE
+{
+    public interface IConnectionState
+    {
+        EventSourceState State { get; }
+        Task<IConnectionState> Run(Action<ServerSentEvent> MsgReceivedCallback, CancellationToken cancelToken, Dictionary<string, string> headers);
+    }
+}

+ 17 - 0
Client/SSE/IServerResponse.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+
+namespace Client.SSE
+{
+    public interface IServerResponse
+    {
+        HttpStatusCode StatusCode { get; }
+
+        System.IO.Stream GetResponseStream();
+
+        Uri ResponseUri { get; }
+    }
+}

+ 15 - 0
Client/SSE/IWebRequester.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Client.SSE
+{
+    public interface IWebRequester
+    {
+        Task<IServerResponse> Get(Uri url, Dictionary<string, string> headers = null);
+
+    }
+}

+ 12 - 0
Client/SSE/IWebRequesterFactory.cs

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Client.SSE
+{
+    public interface IWebRequesterFactory
+    {
+        IWebRequester Create();
+    }
+}

+ 39 - 0
Client/SSE/ServerResponse.cs

@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+
+namespace Client.SSE
+{
+    public class ServerResponse : IServerResponse
+    {
+        private System.Net.HttpWebResponse mHttpResponse;
+
+        public ServerResponse(System.Net.WebResponse webResponse)
+        {
+            this.mHttpResponse = webResponse as HttpWebResponse;
+        }
+
+        public HttpStatusCode StatusCode
+        {
+            get
+            {
+                return mHttpResponse.StatusCode;
+            }
+        }
+
+        public System.IO.Stream GetResponseStream()
+        {
+            return mHttpResponse.GetResponseStream();
+        }
+
+        public Uri ResponseUri
+        {
+            get
+            {
+                return mHttpResponse.ResponseUri;
+            }
+        }
+    }
+}

+ 26 - 0
Client/SSE/ServerSentEvent.cs

@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Client.SSE
+{
+    public class ServerSentEvent
+    {
+        public string LastEventId { get; set; }
+        public string EventType { get; set; }
+        public string Data { get; set; }
+        public int? Retry { get; set; }
+
+        public override string ToString()
+        {
+            StringBuilder sb = new StringBuilder();
+            sb.Append("EventType: ").Append(EventType).AppendLine();
+            sb.Append("Data: ").Append(Data).AppendLine();
+            sb.Append("LastEventId: ").Append(LastEventId).AppendLine();
+            if(Retry.HasValue)
+                sb.Append("Retry: ").Append(Retry.Value).AppendLine();
+            return sb.ToString();
+        }
+    }
+}

+ 17 - 0
Client/SSE/ServerSentEventReceivedEventArgs.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Client.SSE
+{
+    public class ServerSentEventReceivedEventArgs : EventArgs
+    {
+        public ServerSentEvent Message { get; private set; }
+        public ServerSentEventReceivedEventArgs(ServerSentEvent message)
+        {
+            Message = message;
+        }
+
+    }
+}

+ 16 - 0
Client/SSE/StateChangedEventArgs.cs

@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Client.SSE
+{
+    public class StateChangedEventArgs : EventArgs
+    {
+        public EventSourceState State { get; private set; }
+        public StateChangedEventArgs(EventSourceState state)
+        {
+            State = state;
+        }
+    }
+}

+ 47 - 0
Client/SSE/StringSplitter.cs

@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Client.SSE
+{
+    public class StringSplitter
+    {
+        public static string[] SplitIntoLines(string text, out string remainingText)
+        {
+            List<string> lines = new List<string>();
+
+            //bool endFound = false;
+            //bool searchingForFirstChar = true;
+            int lineLength = 0;
+            char previous = char.MinValue;
+            for (int i = 0; i < text.Length; i++)
+            {
+                char c = text[i];
+                if (c == '\n' || c == '\r')
+                {
+                    bool isCRLFPair = previous=='\r' && c == '\n';
+
+                    if (!isCRLFPair)
+                    {
+                        string line = text.Substring(i - lineLength, lineLength);
+                        lines.Add(line);
+                    }
+
+                    lineLength = 0;
+                }
+                else
+                {
+                    lineLength++;
+                }
+                previous = c;
+            }
+
+            // Save the last chars that is not followed by a lineending.
+            remainingText = text.Substring(text.Length - lineLength);
+
+            return lines.ToArray();
+        }
+    }
+}

+ 38 - 0
Client/SSE/WatchDog.cs

@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Client.SSE
+{
+    public class Watchdog
+    {
+        private long _timeout;
+        private Timer _timer;
+        public event EventHandler TimerExpired;
+
+        public void Start()
+        {
+            _timer = new Timer(new TimerCallback(OnTimerExpired), null, 0, _timeout);
+        }
+
+        public void Reset()
+        {
+            _timer.Change(0, _timeout);
+        }
+
+        private void OnTimerExpired(object State)
+        {
+            _timer.Change(Timeout.Infinite, Timeout.Infinite);
+            if (TimerExpired != null)
+                TimerExpired(this, new EventArgs());
+        }
+
+        public Watchdog(long timeout)
+        {
+            if (timeout < 1) throw new ArgumentOutOfRangeException("timeout", "timeout muste be greater than zero.");
+        }
+    }
+}

+ 30 - 0
Client/SSE/WebRequester.cs

@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace Client.SSE
+{
+    public class WebRequester : IWebRequester
+    {
+        public Task<IServerResponse> Get(Uri url, Dictionary<string, string> headers = null)
+        {
+            var wreq = (HttpWebRequest)WebRequest.Create(url);
+            wreq.Method = "GET";
+            wreq.Proxy = null;
+
+            if (headers != null)
+            {
+                foreach (var header in headers)
+                {
+                    wreq.Headers.Add(header.Key, header.Value);
+                }
+            }
+
+            var taskResp = Task.Factory.FromAsync<WebResponse>(wreq.BeginGetResponse,
+                wreq.EndGetResponse, null).ContinueWith<IServerResponse>(t => new ServerResponse(t.Result));
+            return taskResp;
+
+        }
+    }
+}

+ 16 - 0
Client/SSE/WebRequesterFactory.cs

@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Client.SSE
+{
+    public class WebRequesterFactory : IWebRequesterFactory
+    {
+        public IWebRequester Create()
+        {
+            return new WebRequester();
+        }
+    }
+}

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

+ 7 - 1
TEAMModelOS.sln

@@ -11,7 +11,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TEAMModelGrpc", "TEAMModelG
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TEAMModelFunction", "TEAMModelFunction\TEAMModelFunction.csproj", "{78470113-6261-4F9A-9EF3-E315F060813D}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TEAMModelAPI", "TEAMModelAPI\TEAMModelAPI.csproj", "{2146FEEC-7178-4141-A8C7-CBEBAEE404A6}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TEAMModelAPI", "TEAMModelAPI\TEAMModelAPI.csproj", "{2146FEEC-7178-4141-A8C7-CBEBAEE404A6}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Client", "Client\Client.csproj", "{E1A7F6EA-319E-4582-A800-A04DEB8284D9}"
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -39,6 +41,10 @@ Global
 		{2146FEEC-7178-4141-A8C7-CBEBAEE404A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{2146FEEC-7178-4141-A8C7-CBEBAEE404A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{2146FEEC-7178-4141-A8C7-CBEBAEE404A6}.Release|Any CPU.Build.0 = Release|Any CPU
+		{E1A7F6EA-319E-4582-A800-A04DEB8284D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{E1A7F6EA-319E-4582-A800-A04DEB8284D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{E1A7F6EA-319E-4582-A800-A04DEB8284D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{E1A7F6EA-319E-4582-A800-A04DEB8284D9}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

Разлика између датотеке није приказан због своје велике величине
+ 1 - 0
TEAMModelOS/ClientApp/src/assets/login/icon_student.svg


Разлика између датотеке није приказан због своје велике величине
+ 1 - 0
TEAMModelOS/ClientApp/src/assets/login/icon_teacher.svg


Разлика између датотеке није приказан због своје велике величине
+ 1 - 0
TEAMModelOS/ClientApp/src/assets/login/ies5_logo_2.svg


BIN
TEAMModelOS/ClientApp/src/assets/login/login_bg.jpg


+ 1 - 1
TEAMModelOS/ClientApp/src/components/public/frontEndMain/Index.less

@@ -9,7 +9,7 @@
     &-mark{
         position: fixed;
         left:30px;
-        top:30px;
+        top:20px;
     }
     &-schoolName{
         position: fixed;

+ 23 - 10
TEAMModelOS/ClientApp/src/components/public/frontEndMain/Index.vue

@@ -1,12 +1,22 @@
 <style lang="less" scoped>
 @import "./Index.less";
 .footer-info-item {
-    color: white;
+    color: #b8b8b8;
     margin-right: 40px;
-    font-size: 15px;
+    font-size: 12px;
+}
+.login-title {
+    text-align: left;
+    color: #e0e0e0;
+    font-size: 19px;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    margin-left: 20px;
+    letter-spacing: 1px;
+    display: inline-block;
 }
 </style>
-
 <style lang="less">
 .demo-spin-icon-load {
     animation: ani-demo-spin 1s linear infinite;
@@ -60,7 +70,10 @@
 <template>
     <div id="login" class="login" :style="{backgroundImage:bgImg}">
         <div class="login-mark">
-            <img width="125" src="@/assets/login/1-2.png">
+            <img height="42" src="@/assets/login/ies5_logo_2.svg">
+            <span class="login-title">
+                醍摩豆云平台
+            </span>
         </div>
         <div calss="login-schoolName">
             {{ schoolName }}
@@ -110,7 +123,7 @@ export default {
     data() {
         return {
             schoolName: '',
-            bgImg:''
+            bgImg: ''
         }
     },
     computed: {
@@ -154,23 +167,23 @@ export default {
         this.setLoginSchoolCode()
         // this.bgImg = `url(${require('@/assets/login/1-1.jpg')})`             
     },
-    mounted(){
+    mounted() {
 
     },
     watch: {
         $route: {
             handler(n, o) {
                 if (n.name == 'login') {
-                    this.bgImg = `url(${require('@/assets/login/1-1.jpg')})` 
+                    this.bgImg = `url(${require('@/assets/login/login_bg.jpg')})`
                 } else if (n.name == 'loginTeacher') {
-                    this.bgImg = `url(${require('@/assets/login/3-1.jpg')})` 
+                    this.bgImg = `url(${require('@/assets/login/3-1.jpg')})`
                 } else {
-                    this.bgImg = `url(${require('@/assets/login/2-2.jpg')})` 
+                    this.bgImg = `url(${require('@/assets/login/2-2.jpg')})`
                 }
                 console.log(n)
             },
             deep: true,
-            immediate:true
+            immediate: true
         }
     }
 }

+ 5 - 6
TEAMModelOS/ClientApp/src/view/login/page/Student.vue

@@ -7,7 +7,7 @@
     margin-top: -100px;
 }
 .left-box {
-    padding: 0px 55px;
+    padding: 0px 70px;
     height: 400px;
     display: flex;
     align-items: center;
@@ -23,7 +23,7 @@
     min-width: 600px;
 }
 .student-login-img {
-    width: 180px;
+    width: 140px;
 }
 .teacher-login-title {
     color: white;
@@ -77,10 +77,9 @@
 }
 .client-label {
     color: white;
-    font-size: 30px;
+    font-size: 28px;
     text-align: center;
-    margin-top: -20px;
-    margin-left: -7px;
+    margin-top: 20px;
 }
 </style>
 
@@ -168,7 +167,7 @@
     <div class="loginDiv">
         <div class="left-box">
             <!-- 这张图片需要裁剪顶部 -->
-            <img src="@/assets/login/2-1.png" class="student-login-img" style="margin-top:-30px">
+            <img src="@/assets/login/icon_student.svg" class="student-login-img" style="margin-top:-30px">
             <p class="client-label">学生端</p>
         </div>
         <div class="right-box">

+ 1 - 1
TEAMModelOS/ClientApp/src/view/login/page/Teacher.less

@@ -30,7 +30,7 @@
     }
 }
 .teacher-login-img{
-    width: 180px;
+    width: 110px;
 }
 .title{
     display: flex;

+ 4 - 5
TEAMModelOS/ClientApp/src/view/login/page/Teacher.vue

@@ -7,7 +7,7 @@
     margin-top: -100px;
 }
 .left-box {
-    padding: 0px 55px;
+    padding: 0px 80px;
     height: 370px;
     display: flex;
     flex-direction: column;
@@ -70,10 +70,9 @@
 }
 .client-label {
     color: white;
-    font-size: 30px;
+    font-size: 28px;
     text-align: center;
-    margin-top: -20px;
-    margin-left: -7px;
+    margin-top: 20px;
 }
 </style>
 
@@ -160,7 +159,7 @@
     <div class="loginDiv">
         <div class="left-box">
             <!-- 这张图片需要裁剪顶部 -->
-            <img src="@/assets/login/3-2.png" class="teacher-login-img" style="margin-top:-30px">
+            <img src="@/assets/login/icon_teacher.svg" class="teacher-login-img">
             <p class="client-label">教师端</p>
         </div>
         <div class="right-box" v-show="!qrloginFlag">

+ 172 - 15
TEAMModelOS/ClientApp/src/view/login/test.vue

@@ -71,20 +71,12 @@
 }
 .login-main-wrap {
     display: flex;
-    justify-content: space-evenly;
+    justify-content: center;
     width: 100%;
     height: 600px;
     flex-direction: row;
     align-items: center;
-}
-.login-img-wrap {
-    width: 45%;
-    max-width: 600px;
-    height: 100%;
-    padding: 12px;
-    display: flex;
-    justify-content: center;
-    align-items: center;
+    margin-top: -50px;
 }
 
 .login-main-img {
@@ -101,6 +93,7 @@
     justify-content: space-evenly;
     align-items: center;
     margin-top: 0px;
+    margin-left: 2%;
 }
 .enter-box {
     width: 150px;
@@ -129,28 +122,191 @@
     color: white;
     font-size: 200px;
 }
+.login-img-wrap {
+    width: 45%;
+    max-width: 760px;
+    height: 500px;
+    padding: 12px;
+    margin-right: 5%;
+}
+.swiper-item-box {
+    width: 100%;
+    min-height: 500px;
+    .title {
+        font-size: 40px;
+        color: #fff;
+        line-height: 1.1;
+        letter-spacing: 1px;
+        font-weight: 600;
+    }
+    .cont {
+        margin-top: 30px;
+        margin-left: 15px;
+    }
+    .cont-title {
+        font-size: 26px;
+        color: #fff;
+        line-height: 1.1;
+        font-weight: 500;
+        margin-bottom: 7px;
+        letter-spacing: 1px;
+    }
+    .cont-text {
+        font-size: 16px;
+        margin-bottom: 3px;
+        color: #9e9e9e;
+        letter-spacing: 1px;
+        &::before {
+            content: "|";
+            margin-right: 11px;
+            font-weight: 900;
+        }
+    }
+    .border1::before {
+        color: #de5c53;
+    }
+    .border2::before {
+        color: #fcdc6b;
+    }
+    .border3::before {
+        color: #25ca92;
+    }
+    .border4::before {
+        color: #3eaefe;
+    }
+
+    .link {
+        margin-top: 50px;
+    }
+}
+.logintype {
+    .title {
+        display: block;
+        margin-bottom: 17px;
+        letter-spacing: 1px;
+        color: #9e9e9e;
+        font-size: 13px;
+    }
+    .login-box {
+        min-width: 520px;
+        background-color: rgba(66, 69, 81, 0.9);
+        width: 100%;
+        height: 100px;
+        display: flex;
+        align-items: center;
+        border-left: 5px solid #3dadff;
+        border-radius: 5px;
+        border-right: 5px solid #3dadff;
+        margin-bottom: 25px;
+        cursor: pointer;
+        box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.5);
+        &:hover {
+            background-color: rgba(75, 79, 96, 0.9);
+        }
+    }
+    .login-box-icon {
+        width: 20%;
+        float: left;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        height: 100%;
+        border-right: 1px solid #616161;
+    }
+    .login-box-cont {
+        width: 80%;
+        float: left;
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        justify-content: space-between;
+        padding: 0 17px;
+    }
+    .main {
+        color: #bdbdbd;
+        font-size: 12px;
+    }
+    .subtitle {
+        display: block;
+        font-size: 30px;
+        color: #fff;
+        font-weight: 600;
+        margin-bottom: 6px;
+    }
+}
 </style>
 
 <template>
     <div class="login-main-wrap">
         <div class="login-img-wrap">
-            <img src="@/assets/login/1-3.png" class="login-main-img">
+            <!-- <img src="@/assets/login/1-3.png" class="login-main-img"> -->
+            <!-- 轮播区域 -->
+            <Carousel v-model="curPage" loop dots="outside" arrow="never">
+                <CarouselItem>
+                    <div class="swiper-item-box">
+                        <div class="title">
+                            歡迎來到醍摩豆 5
+                            <br>
+                            新時代智慧教育之旅 就此展開
+                        </div>
+                        <div class="cont">
+                            <div class="cont-title">HiTeach 5 智慧教學系統</div>
+                            <p class="cont-text border1">實踐差異化教學,現代化因材施教</p>
+                            <p class="cont-text border2">線上線下,混和式教學系統首選</p>
+                            <p class="cont-text border3">合作學習,素養導向教學的全面應用</p>
+                            <p class="cont-text border4">看見每個學生思考</p>
+                        </div>
+                        <div class="cont">
+                            <div class="cont-title">醍摩豆雲平台 IES 5</div>
+                            <p class="cont-text border1">新架構與新技術,速度升級更有感</p>
+                            <p class="cont-text border2">兼容各式媒體與檔案,雲端資源庫隨取即用</p>
+                            <p class="cont-text border3">題庫組卷、閱卷系統,評量應用更多元</p>
+                            <p class="cont-text border4">博拉圖學情分析,學生成績精準提升</p>
+                        </div>
+                        <div class="link">
+                            <a target="_block" href="https://www.habook.com/zh-tw/news.php?act=view&amp;id=408">
+                                了解更多關於醍摩豆 5
+                                <i class="ivu-icon ivu-icon-ios-arrow-forward" style="font-size: 19px;"></i>
+                            </a>
+                        </div>
+                    </div>
+
+                </CarouselItem>
+            </Carousel>
         </div>
         <div class="login-enter-wrap">
-            <div class="enter-box" @click="loginTo('teacher')">
+            <div class="logintype">
+                <span class="title">請選擇您的身份進行登入</span>
+                <div class="login-box" @click="loginTo('teacher')">
+                    <div class="login-box-icon"><img width="40" src="@/assets/login/icon_teacher.svg"></div>
+                    <div class="login-box-cont">
+                        <div class="main">
+                            <span class="subtitle">教師身份</span>
+                            以教師身份登入IES 5智慧教學服務,即刻存取雲端服務
+                        </div>
+                        <i class="ivu-icon ivu-icon-ios-arrow-forward" style="font-size: 35px; color: rgb(189, 189, 189);"></i>
+                    </div>
+                </div>
+                <div class="login-box" @click="loginTo('student')">
+                    <div class="login-box-icon"><img width="40" src="@/assets/login/icon_student.svg"></div>
+                    <div class="login-box-cont">
+                        <div class="main"><span class="subtitle">學生身份</span> 以學生身份登入AClass ONE智慧學伴服務,完成學習任務與測驗 </div><i class="ivu-icon ivu-icon-ios-arrow-forward" style="font-size: 35px; color: rgb(189, 189, 189);"></i>
+                    </div>
+                </div>
+                <div class="link" style="text-align: right; letter-spacing: 1px;"><span style="margin-right: 20px;">還沒有帳號嗎?</span>點此<a href="/regist" class="" style="text-decoration: underline; color: rgb(41, 114, 169);">免費註冊</a></div>
+            </div>
+            <!-- <div class="enter-box" @click="loginTo('teacher')">
                 <img style="display: block;width: 100%;" src="@/assets/login/1-5.png">
-                <!-- <Icon custom="iconfont icon-teacher" class="icon-to-img"></Icon> -->
                 <p class="enter-type-btn">
                     教师端
                 </p>
             </div>
             <div class="enter-box" @click="loginTo('student')">
                 <img style="display: block;width: 100%;" src="@/assets/login/1-4.png">
-                <!-- <Icon custom="iconfont icon-student1" class="icon-to-img"></Icon> -->
                 <p class="enter-type-btn">
                     学生端
                 </p>
-            </div>
+            </div> -->
         </div>
         <!-- 提示視窗 -->
         <Modal v-model="identityFlag" width="360" :mask-closable="false" :closable="false" class-name="identityModal">
@@ -169,6 +325,7 @@ import { User } from '@/service/User'
 export default {
     data() {
         return {
+            curPage: 0,
             defaultSchool: {
                 name: '',
                 code: ''

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

+ 41 - 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;
@@ -19,12 +20,14 @@ using Microsoft.AspNetCore.SpaServices;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Primitives;
 using Microsoft.IdentityModel.Tokens;
 using TEAMModelOS.Models;
 using TEAMModelOS.SDK;
 using TEAMModelOS.SDK.Context.Attributes.Azure;
 using TEAMModelOS.SDK.Context.Configuration;
 using TEAMModelOS.SDK.DI;
+using TEAMModelOS.SDK.Extension;
 using TEAMModelOS.SDK.Models.Service;
 using VueCliMiddleware;
 
@@ -110,6 +113,35 @@ namespace TEAMModelOS
             //注入word 標籤解析
             string path = $"{ environment.ContentRootPath}/JsonFile/Core";
             services.AddHtexTranslator(path);
+            services.AddServerSentEvents(o =>
+            {
+                o.KeepaliveMode = ServerSentEventsKeepaliveMode.Always;
+                o.OnClientConnected = async (service, client) =>
+                {
+                    //if (client.Request.Headers.TryGetValue("X-Auth-Name", out StringValues name))
+                    //{
+                    //    client.Client.SetProperty("NAME", name.ToString());
+                    //}
+                    //if (client.Request.Headers.TryGetValue("X-Auth-DID", out StringValues did))
+                    //{
+                    //    client.Client.SetProperty("DID", did.ToString());
+                    //}
+                    //if (client.Request.Headers.TryGetValue("X-Auth-CID", out StringValues cid))
+                    //{
+                    //    client.Client.SetProperty("CID", cid.ToString());
+                    //}
+                    //if (client.Request.Headers.TryGetValue("X-Auth-PIN", out StringValues pin))
+                    //{
+                    //    client.Client.SetProperty("PIN", pin.ToString());
+                    //}
+                    //if (client.Request.Headers.TryGetValue("X-Auth-APP", out StringValues app))
+                    //{
+                    //    client.Client.SetProperty("APP", app.ToString());
+                    //}
+
+                    await client.Client.SendEventAsync(new { sid = client.Client.Id.ToString() }.ToJsonString());
+                };
+            });
         }
 
         // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@@ -139,6 +171,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}",