Преглед на файлове

Merge branch 'develop' into TPE/Daniel

jeff преди 5 месеца
родител
ревизия
f89666a46b
променени са 84 файла, в които са добавени 46470 реда и са изтрити 745 реда
  1. 80 0
      HTEX.DataETL/Controllers/LessonRecordController.cs
  2. 33 0
      IESHybridCloud/Controllers/WeatherForecastController.cs
  3. 25 0
      IESHybridCloud/IESHybridCloud.csproj
  4. 6 0
      IESHybridCloud/IESHybridCloud.http
  5. BIN
      IESHybridCloud/JsonFiles/ip2region.db
  6. 42708 0
      IESHybridCloud/JsonFiles/latlng.json
  7. 89 0
      IESHybridCloud/Program.cs
  8. 41 0
      IESHybridCloud/Properties/launchSettings.json
  9. 163 0
      IESHybridCloud/Services/SignalRIESHybridClientHub.cs
  10. 13 0
      IESHybridCloud/WeatherForecast.cs
  11. 27 0
      IESHybridCloud/appsettings.Development.json
  12. 25 0
      IESHybridCloud/appsettings.json
  13. 3 2
      TEAMModelBI/ClientApp/src/language/lang/zh-cn.js
  14. 3 2
      TEAMModelBI/ClientApp/src/language/lang/zh-tw.js
  15. 3 2
      TEAMModelBI/ClientApp/src/view/product/index.vue
  16. 49 8
      TEAMModelBI/ClientApp/src/view/userInquire/details.vue
  17. 7 4
      TEAMModelBI/Controllers/BITmid/TmidController.cs
  18. 4 4
      TEAMModelBI/Controllers/Census/SchoolController.cs
  19. 3 3
      TEAMModelBI/TEAMModelBI.csproj
  20. 1 1
      TEAMModelOS.Extension/HTEX.Complex/Services/SignalRScreenServerHub.cs
  21. 1 1
      TEAMModelOS.Extension/HTEX.ScreenClient/Services/SignalRScreenClientHub.cs
  22. 16 1
      TEAMModelOS.Extension/HTEX.Test/Program.cs
  23. 10 1
      TEAMModelOS.Function/CosmosDBTriggers/TriggerExam.cs
  24. 2 3
      TEAMModelOS.Function/IESHttpTrigger.cs
  25. 15 1
      TEAMModelOS.Function/IESServiceBusTrigger.cs
  26. 0 1
      TEAMModelOS.Function/Program.cs
  27. 3 3
      TEAMModelOS.Function/TEAMModelOS.Function.csproj
  28. 1 0
      TEAMModelOS.SDK/DI/AzureSignalR/AzureSignalRFactory.cs
  29. 2 1
      TEAMModelOS.Function/DI/BackgroundWorkerQueue.cs
  30. 452 0
      TEAMModelOS.SDK/Helper/Security/Hash/MurmurHash3.cs
  31. 4 0
      TEAMModelOS.SDK/Models/Cosmos/Common/ArtEvaluation.cs
  32. 49 9
      TEAMModelOS.SDK/Models/Service/GenPDFService.cs
  33. 1 1
      TEAMModelOS.SDK/Models/Service/SchoolService.cs
  34. 248 209
      TEAMModelOS.SDK/Models/Service/StudentService.cs
  35. 3 3
      TEAMModelOS.SDK/TEAMModelOS.SDK.csproj
  36. 8 1
      TEAMModelOS.sln
  37. 2 0
      TEAMModelOS/ClientApp/public/lang/en-US.js
  38. 2 0
      TEAMModelOS/ClientApp/public/lang/zh-CN.js
  39. 2 0
      TEAMModelOS/ClientApp/public/lang/zh-TW.js
  40. 103 3
      TEAMModelOS/ClientApp/src/common/BaseQuickPaper.vue
  41. 585 0
      TEAMModelOS/ClientApp/src/common/MarkCanvas.vue
  42. 25 28
      TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseKnowledgeDetail.vue
  43. 34 5
      TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseLevelDetail.vue
  44. 3 3
      TEAMModelOS/ClientApp/src/components/student-analysis/total/htBasePie.vue
  45. 40 8
      TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseRadar.vue
  46. 82 4
      TEAMModelOS/ClientApp/src/components/student-web/EventView/EventContentTypeTemplate/PaperViewBox/ArtTestReport.vue
  47. 1 1
      TEAMModelOS/ClientApp/src/components/student-web/EventView/EventContentTypeTemplate/PaperViewBox/ArtView.vue
  48. 11 2
      TEAMModelOS/ClientApp/src/components/student-web/EventView/EventContentTypeTemplate/PaperViewBox/PaperTest.vue
  49. 2 2
      TEAMModelOS/ClientApp/src/components/student-web/EventView/EventList.vue
  50. 40 0
      TEAMModelOS/ClientApp/src/store/module/totalAnalysis.js
  51. 80 2
      TEAMModelOS/ClientApp/src/utils/evTools.js
  52. 124 1
      TEAMModelOS/ClientApp/src/utils/public.js
  53. 24 2
      TEAMModelOS/ClientApp/src/view/artexam/Create.vue
  54. 4 3
      TEAMModelOS/ClientApp/src/view/artexam/Mgt.vue
  55. 1 1
      TEAMModelOS/ClientApp/src/view/artexam/SelectMusic.vue
  56. 1 1
      TEAMModelOS/ClientApp/src/view/artexam/ZYMusicDetail.vue
  57. 8 5
      TEAMModelOS/ClientApp/src/view/evaluation/bank/ExerciseList.vue
  58. 28 4
      TEAMModelOS/ClientApp/src/view/evaluation/bank/ExerciseNoFilter.vue
  59. 46 8
      TEAMModelOS/ClientApp/src/view/evaluation/bank/TestPaperList.vue
  60. 91 31
      TEAMModelOS/ClientApp/src/view/evaluation/bank/index.vue
  61. 5 5
      TEAMModelOS/ClientApp/src/view/evaluation/components/BaseExerciseList.vue
  62. 83 10
      TEAMModelOS/ClientApp/src/view/evaluation/components/BasePoints.vue
  63. 1 1
      TEAMModelOS/ClientApp/src/view/learnactivity/tabs/htCloudDAS.vue
  64. 6 4
      TEAMModelOS/ClientApp/src/view/student-account/import/StuImport.vue
  65. 23 16
      TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/KnowledgeAnalysis/htKnowledgeAnalysis.vue
  66. 52 11
      TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/KnowledgeAnalysis/htScoreDetails.vue
  67. 24 14
      TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/LevelAnalysis/htLevelAnalysis.vue
  68. 122 66
      TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/LevelAnalysis/htScoreDetails.vue
  69. 5 3
      TEAMModelOS/ClientApp/src/view/student-web/AppNew.vue
  70. 2 1
      TEAMModelOS/ClientApp/src/view/syllabus/Syllabus.vue
  71. 124 97
      TEAMModelOS/Controllers/Both/GroupListController.cs
  72. 4 4
      TEAMModelOS/Controllers/Client/AClassONEController.cs
  73. 2 2
      TEAMModelOS/Controllers/Common/ArtController.cs
  74. 4 2
      TEAMModelOS/Controllers/School/SchoolTeacherController.cs
  75. 69 1
      TEAMModelOS/Controllers/System/BlobController.cs
  76. 2 1
      TEAMModelOS/Controllers/Third/IRS/ThirdIRSController.cs
  77. 128 0
      TEAMModelOS/Controllers/Third/XunFeiJYY/XunFeiJYYService.cs
  78. 152 89
      TEAMModelOS/Controllers/Third/XunFeiJYY/XunFeilJYYController.cs
  79. 187 0
      TEAMModelOS/Filter/IESHybridCloudHub.cs
  80. 4 1
      TEAMModelOS/Startup.cs
  81. 4 4
      TEAMModelOS/TEAMModelOS.csproj
  82. 29 29
      TEAMModelOS/appsettings.Development.json
  83. 1 1
      TEAMModelOS/appsettings.json
  84. 0 8
      nuget.config

+ 80 - 0
HTEX.DataETL/Controllers/LessonRecordController.cs

@@ -52,7 +52,87 @@ namespace HTEX.DataETL.Controllers
             _azureRedis = azureRedis;
 
         }
+        [HttpPost("schools")]
+        public async Task<IActionResult> Schools(JsonElement json) 
+        {
 
+            List<string> ids = new List<string> { "cdscxx",
+  "sdsyxq",
+  "cdsxxx",
+  "cdssz",
+  "cdsshx",
+  "ptszx",
+  "cdwwsz",
+  "csdswz",
+  "cdfczx",
+  "sdsy",
+  "qyszgh",
+  "sdsydq",
+  "sslzxq",
+  "cd37z",
+  "cdqysz",
+  "cd11z",
+  "xhlxx",
+  "cdpxjj",
+  "xcfxqy",
+  "csdswx",
+  "thgjxx",
+  "cdwwsx",
+  "cdsjy",
+  "cdsxwy",
+  "cdsxzq",
+  "cdsxmd",
+  "cdsxcf",
+  "cdsxqh",
+  "cdsyxx",
+  "cdsxx",
+  "sxgkxx",
+  "cdqbxx",
+  "qyszfx",
+  "cdpxxq",
+  "cdpxlz",
+  "cdpx",
+  "cdkhxx",
+  "cdhmxx",
+  "cdjsxx",
+  "cdhhxx",
+  "cdhygj",
+  "hygjqb",
+  "cdglxx",
+  "cddpxx",
+  "cddcg",
+  "cdctzm",
+  "cdctxx",
+  "cdchxx",
+  "cdctxq",
+  "sdsyqb",
+  "sslzsx",
+  "sslzjs",
+  "cdjsxc",
+  "cdjsxb",
+  "yfsxwy",
+  "sdyzhy",
+  "yxmdfs",
+  "cdcgxc",
+  "cdcgxa" };
+            foreach (string id in ids) {
+                var res = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Student).GetList<Student>("select value c  from c ", $"Base-{id}");
+                if (res.list.IsNotEmpty()) 
+                {
+                   var change=  res.list.FindAll(x => x.graduate==1);
+                    if (change.IsNotEmpty()) 
+                    {
+                        foreach (var item in change)
+                        {
+                            item.graduate=0;
+                            await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Student).UpsertItemAsync(item, new PartitionKey(item.code));
+                        }
+                    }
+                    
+                }
+            }
+            return Ok(new {  });
+        }
         [HttpPost("process-lesson")]
         public async Task<IActionResult> ProcessLesson(JsonElement json)
         {

+ 33 - 0
IESHybridCloud/Controllers/WeatherForecastController.cs

@@ -0,0 +1,33 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace IESHybridCloud.Controllers
+{
+    [ApiController]
+    [Route("[controller]")]
+    public class WeatherForecastController : ControllerBase
+    {
+        private static readonly string[] Summaries = new[]
+        {
+            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
+        };
+
+        private readonly ILogger<WeatherForecastController> _logger;
+
+        public WeatherForecastController(ILogger<WeatherForecastController> logger)
+        {
+            _logger = logger;
+        }
+
+        [HttpGet]
+        public IEnumerable<WeatherForecast> Get()
+        {
+            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
+            {
+                Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
+                TemperatureC = Random.Shared.Next(-20, 55),
+                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
+            })
+            .ToArray();
+        }
+    }
+}

+ 25 - 0
IESHybridCloud/IESHybridCloud.csproj

@@ -0,0 +1,25 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>net8.0</TargetFramework>
+    <Nullable>enable</Nullable>
+    <ImplicitUsings>enable</ImplicitUsings>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Hangfire" Version="1.8.17" />
+    <PackageReference Include="Hangfire.Dashboard.BasicAuthorization" Version="1.0.2" />
+    <PackageReference Include="Hangfire.Redis.StackExchange" Version="1.9.4" />
+    <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.8" />
+    <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
+    <PackageReference Include="MQTTnet.AspNetCore" Version="4.3.7.1207" />
+    <PackageReference Include="MQTTnet.AspNetCore.Routing" Version="0.4.37" />
+    <PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
+    <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\TEAMModelOS.SDK\TEAMModelOS.SDK.csproj" />
+  </ItemGroup>
+
+</Project>

+ 6 - 0
IESHybridCloud/IESHybridCloud.http

@@ -0,0 +1,6 @@
+@IESHybridCloud_HostAddress = http://localhost:5251
+
+GET {{IESHybridCloud_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###

BIN
IESHybridCloud/JsonFiles/ip2region.db


Файловите разлики са ограничени, защото са твърде много
+ 42708 - 0
IESHybridCloud/JsonFiles/latlng.json


+ 89 - 0
IESHybridCloud/Program.cs

@@ -0,0 +1,89 @@
+using IESHybridCloud.Services;
+using MathNet.Numerics;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Serilog;
+using System.Configuration;
+using System.Text.Json;
+using TEAMModelOS.SDK;
+using TEAMModelOS.SDK.DI;
+using TEAMModelOS.SDK.DI.Device;
+
+namespace IESHybridCloud
+{
+    public class Program
+    {
+        public static void Main(string[] args)
+        {
+            var builder = WebApplication.CreateBuilder(args);
+            //防止编译后的appsettings.json 文件内容,在重新部署的时候,因为不同的环境导致被覆盖的问题,
+            //所以在正式环境中指定appsettings-prod.json一个本地开发环境不存在的文件,以达到不会被覆盖的问题,
+            //即使在生产环境中未配置appsettings-prod.json 也不影响启动,因为会按照appsettings.json的配置启动
+#if !DEBUG
+            builder.Host.ConfigureAppConfiguration((context, config) => {
+                config.SetBasePath(Directory.GetCurrentDirectory());
+                config.AddJsonFile("appsettings-prod.json", optional: true, reloadOnChange: true);
+            });
+#endif
+            Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console(outputTemplate: "{Timestamp:HH:mm:ss.fff zzz} [{Level:u3}] ({ThreadId}) {Message}{NewLine}{Exception}")
+           .WriteTo.File("logs/log-.log", rollingInterval: RollingInterval.Day).CreateLogger();
+            builder.Host.UseSerilog();
+            // Add services to the container.
+
+            builder.Services.AddControllers();
+            builder.Services.AddHttpClient();
+
+            string? StorageConnectionString = builder.Configuration.GetValue<string>("Azure:Storage:ConnectionString");
+            string? RedisConnectionString = builder.Configuration.GetValue<string>("Azure:Redis:ConnectionString");
+            //Storage
+            builder.Services.AddAzureStorage(StorageConnectionString, "Default");
+            //Redis
+            builder.Services.AddAzureRedis(RedisConnectionString, "Default");
+            builder.Services.AddSignalR();
+            builder.Services.AddHttpContextAccessor();
+            builder.Services.AddHttpClient<DingDing>();
+            string path = $"{builder.Environment.ContentRootPath}/JsonFiles";
+            builder.Services.TryAddSingleton(new Region2LongitudeLatitudeTranslator(path));
+            builder.Services.AddIPSearcher(path);
+            builder.Services.AddSingleton<CoreDevice>();
+            builder.Services.AddCors(options =>
+            {
+                options.AddDefaultPolicy(
+                builder =>
+                {
+
+                    builder.AllowAnyOrigin()
+                            .AllowAnyHeader()
+                            .AllowAnyMethod();
+                });
+            });
+            builder.Services.AddControllersWithViews();
+            builder.Services.AddSingleton<BackgroundWorkerQueue>();
+            builder.Services.AddHostedService<SignalRIESHybridClientHub>();
+            builder.Services.AddAzureSignalR(builder.Configuration.GetValue<string>("Azure:SignalR:ConnectionString"));
+            ////MQTT  服务端API 发送消息到MQTT客户端 https://www.cnblogs.com/weskynet/p/16441219.html
+            //#region MQTT配置
+            //builder.Services.AddSingleton<MQTTEvents>();
+            //builder.Services.AddSingleton(new JsonSerializerOptions(JsonSerializerDefaults.Web));
+            //builder.Services.AddHostedMqttServerWithServices(x =>
+            //{
+            //    x.WithDefaultEndpoint()
+            //    .WithConnectionBacklog(1000)
+            //    .WithPersistentSessions(true).WithKeepAlive()
+            //    .WithDefaultCommunicationTimeout(TimeSpan.FromMilliseconds(30));
+            //}).AddMqttConnectionHandler().AddConnections().AddMqttControllers();
+            //#endregion
+            var app = builder.Build();
+
+            // Configure the HTTP request pipeline.
+
+            app.UseHttpsRedirection();
+
+            app.UseAuthorization();
+
+
+            app.MapControllers();
+
+            app.Run();
+        }
+    }
+}

+ 41 - 0
IESHybridCloud/Properties/launchSettings.json

@@ -0,0 +1,41 @@
+{
+  "$schema": "http://json.schemastore.org/launchsettings.json",
+  "iisSettings": {
+    "windowsAuthentication": false,
+    "anonymousAuthentication": true,
+    "iisExpress": {
+      "applicationUrl": "http://localhost:37858",
+      "sslPort": 44395
+    }
+  },
+  "profiles": {
+    "http": {
+      "commandName": "Project",
+      "dotnetRunMessages": true,
+      "launchBrowser": true,
+      "launchUrl": "weatherforecast",
+      "applicationUrl": "http://localhost:5251",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      }
+    },
+    "https": {
+      "commandName": "Project",
+      "dotnetRunMessages": true,
+      "launchBrowser": true,
+      "launchUrl": "weatherforecast",
+      "applicationUrl": "https://localhost:7222;http://localhost:5251",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      }
+    },
+    "IIS Express": {
+      "commandName": "IISExpress",
+      "launchBrowser": true,
+      "launchUrl": "weatherforecast",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      }
+    }
+  }
+}

+ 163 - 0
IESHybridCloud/Services/SignalRIESHybridClientHub.cs

@@ -0,0 +1,163 @@
+using Microsoft.AspNetCore.SignalR;
+using Microsoft.AspNetCore.SignalR.Client;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Web;
+using TEAMModelOS.SDK;
+using TEAMModelOS.SDK.DI;
+using TEAMModelOS.SDK.DI.Device;
+using TEAMModelOS.SDK.Extension;
+using static Azure.Core.HttpHeader;
+
+namespace IESHybridCloud.Services
+{
+    public class SignalRIESHybridClientHub : BackgroundService, IDisposable
+    {
+        private readonly IConfiguration _configuration;
+        private readonly ILogger<SignalRIESHybridClientHub> _logger;
+        private TEAMModelOS.SDK.HybridClient? device;
+        private readonly CoreDevice _device;
+        private readonly IHttpClientFactory _httpClientFactory;
+        private readonly BackgroundWorkerQueue _backgroundWorkerQueue;
+        private readonly AzureSignalRFactory _azureSignalRFactory;
+        public SignalRIESHybridClientHub(AzureSignalRFactory azureSignalRFactory,BackgroundWorkerQueue backgroundWorkerQueue, IConfiguration configuration, ILogger<SignalRIESHybridClientHub> logger, IHttpClientFactory httpClientFactory, CoreDevice device)
+        {
+
+            _configuration=configuration;
+            _logger=logger;
+            _httpClientFactory=httpClientFactory;
+            _device = device;
+            _backgroundWorkerQueue=backgroundWorkerQueue;
+            _azureSignalRFactory=azureSignalRFactory;
+        }
+        protected async override Task ExecuteAsync(CancellationToken stoppingToken)
+        {
+            var coreDevice = await _device.GetCoreDevice();
+            device=coreDevice.ToJsonString().ToObject<TEAMModelOS.SDK.HybridClient>();
+            string clientid = device.deviceId!;
+            string? CenterUrl = _configuration.GetSection("HybridClient:CenterUrl").Value;
+            long Timeout = _configuration.GetValue<long>("HybridClient:Timeout");
+            long Delay = _configuration.GetValue<long>("HybridClient:Delay");
+            device.timeout = Timeout;
+            device.delay = Delay;
+            await StartHubConnectionAsync(clientid, CenterUrl);
+        }
+        private async Task StartHubConnectionAsync(string clientid, string? CenterUrl)
+        { 
+            
+            //重写重连策略,防止服务端更新,断线后,客户端达到最大连接次数,依然连线不上服务端。
+            var reconnectPolicy = new ExponentialBackoffReconnectPolicy(TimeSpan.FromSeconds(10), _logger); // 尝试重连的最大次数,这里使用 int.MaxValue 表示无限次
+            reconnectPolicy.MaxRetryCount = int.MaxValue;
+           
+         
+            //HubConnection hubConnection = new HubConnectionBuilder()
+            //   .WithUrl($"https://localhost:5001/hub/hybrid-cloud?grant_type={HybridConstant.grant_type}&clientid={clientid}&device={HttpUtility.UrlEncode(device.ToJsonString(), Encoding.Unicode)}") //only one slash
+            //   .WithAutomaticReconnect(reconnectPolicy)
+            //   .ConfigureLogging(logging =>
+            //   {
+            //       logging.SetMinimumLevel(LogLevel.Information);
+            //       logging.AddConsole();
+            //   })
+            //   .Build();
+
+            
+
+
+            try
+            {
+                //hubConnection.On<ConnectionMessage>("ReceiveConnection", (message) =>
+                //{
+                //    _logger.LogInformation($"连接成功:{message.ToJsonString()}");
+                //    //重置重连次数。
+                //    reconnectPolicy.Reset();
+                //});
+                //hubConnection.On<JsonElement>("ReceiveMessage", async (message) =>
+                //{
+                //    {
+                //        _logger.LogInformation($"获得推送数据,{message.ToJsonString()}");
+                //    }
+                //});ieshybridcloudhub
+                //await hubConnection.StartAsync();
+                var hub = _azureSignalRFactory.GetServiceManager().GetHubContext($"/ieshybridcloudhub");
+                await hub.Clients.User(device.deviceId).SendCoreAsync("ReceiveMessage", new[]{new {
+                                connectionId = "1",
+                                to = "2",
+                                groupname ="3",
+                                sender = device.deviceId,
+                                text = device.ToJsonString()
+                            }});
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError("初次启动连接SignalR失败,等待重连......");
+                int retryCount = 0;
+                const int maxRetries = 360;
+                const int retryDelaySeconds = 10;
+                while (retryCount < maxRetries)
+                {
+                    try
+                    {
+                        await Task.Delay(retryDelaySeconds * 1000); // 等待一段时间后重试  
+                       // await hubConnection.StartAsync();
+                        _logger.LogInformation("SignalR连接成功(重试后)!");
+                        break; // 连接成功,退出循环  
+                    }
+                    catch (Exception retryEx)
+                    {
+                        retryCount++;
+                        _logger.LogInformation($"SignalR连接重试失败: {retryEx.Message}。重试次数: {retryCount}/{maxRetries}");
+                        // 可以在这里决定是否因为某种原因停止重试  
+                        if (retryCount == maxRetries)
+                        {
+                            _logger.LogInformation("达到最大重试次数,停止重试。");
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+        public async Task<(int status, string msg, JsonNode? data)> ReceiveMessage(JsonNode data) 
+        {
+            int status = 0;
+            string msg = string.Empty;
+             _backgroundWorkerQueue.QueueBackgroundWorkItem(async task =>
+            {
+               var pk= data["pk"];
+            });
+            return (status, msg, data);
+
+        }
+    }
+    public class ExponentialBackoffReconnectPolicy : IRetryPolicy
+    {
+        private readonly TimeSpan _retryInterval;
+
+        private int _retryCount;
+        public int MaxRetryCount { get; set; } = int.MaxValue;
+        public readonly ILogger<SignalRIESHybridClientHub> _logger;
+        public ExponentialBackoffReconnectPolicy(TimeSpan retryInterval, ILogger<SignalRIESHybridClientHub> logger)
+        {
+            _retryInterval = retryInterval;
+            _retryCount = 0;
+            _logger = logger;
+        }
+
+        public TimeSpan? NextRetryDelay(RetryContext retryContext)
+        {
+            _logger.LogInformation($"重连次数: {_retryCount}");
+            if (_retryCount < MaxRetryCount)
+            {
+                _retryCount++;
+                // 计算下一次重连的延迟时间
+                return _retryInterval;
+            }
+            return null; // 达到最大重连次数后不再重连
+        }
+
+        public void Reset()
+        {
+            _retryCount = 0;
+        }
+    }
+}

+ 13 - 0
IESHybridCloud/WeatherForecast.cs

@@ -0,0 +1,13 @@
+namespace IESHybridCloud
+{
+    public class WeatherForecast
+    {
+        public DateOnly Date { get; set; }
+
+        public int TemperatureC { get; set; }
+
+        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
+
+        public string? Summary { get; set; }
+    }
+}

+ 27 - 0
IESHybridCloud/appsettings.Development.json

@@ -0,0 +1,27 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Information",
+      "Microsoft.AspNetCore": "Warning"
+    }
+  },
+  "Azure": {
+    "Storage": {
+      "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=teammodelos;AccountKey=Dl04mfZ9hE9cdPVO1UtqTUQYN/kz/dD/p1nGvSq4tUu/4WhiKcNRVdY9tbe8620nPXo/RaXxs+1F9sVrWRo0bg==;EndpointSuffix=core.chinacloudapi.cn"
+    },
+    "Cosmos": {
+      "ConnectionString": "AccountEndpoint=https://teammodelos.documents.azure.cn:443/;AccountKey=clF73GwPECfP1lKZTCvs8gLMMyCZig1HODFbhDUsarsAURO7TcOjVz6ZFfPqr1HzYrfjCXpMuVD5TlEG5bFGGg==;"
+    },
+    "Redis": {
+      "ConnectionString": "CoreRedisCN.redis.cache.chinacloudapi.cn:6380,password=LyJWP1ORJdv+poXWofAF97lhCEQPg1wXWqvtzXGXQuE=,ssl=True,abortConnect=False"
+    },
+    "SignalR": {
+      "ConnectionString": "Endpoint=https://channel.signalr.azure.cn;AccessKey=AtcB7JYFNUbUXb1rGxa3PVksQ2X5YSv3JOHZR9J88tw=;Version=1.0;"
+    }
+  },
+  "HybridClient": {
+    "Timeout": 30000,
+    "Delay": 500,
+    "CenterUrl": "https://channel.signalr.azure.cn"
+  }
+}

+ 25 - 0
IESHybridCloud/appsettings.json

@@ -0,0 +1,25 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Information",
+      "Microsoft.AspNetCore": "Warning"
+    }
+  },
+  "Azure": {
+    "Storage": {
+      "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=teammodelos;AccountKey=Dl04mfZ9hE9cdPVO1UtqTUQYN/kz/dD/p1nGvSq4tUu/4WhiKcNRVdY9tbe8620nPXo/RaXxs+1F9sVrWRo0bg==;EndpointSuffix=core.chinacloudapi.cn"
+    },
+    "Cosmos": {
+      "ConnectionString": "AccountEndpoint=https://teammodelos.documents.azure.cn:443/;AccountKey=clF73GwPECfP1lKZTCvs8gLMMyCZig1HODFbhDUsarsAURO7TcOjVz6ZFfPqr1HzYrfjCXpMuVD5TlEG5bFGGg==;"
+    },
+    "Redis": {
+      "ConnectionString": "CoreRedisCN.redis.cache.chinacloudapi.cn:6380,password=LyJWP1ORJdv+poXWofAF97lhCEQPg1wXWqvtzXGXQuE=,ssl=True,abortConnect=False"
+    }
+  },
+  "AllowedHosts": "*",
+  "HybridClient": {
+    "Timeout": 30000,
+    "Delay": 500,
+    "CenterUrl": "https://channel.signalr.azure.cn"
+  }
+}

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

@@ -850,6 +850,7 @@ const zh_cn = {
             lastLoginTime: '上次登陆时间',
             noUseRecently: '近期未使用',
             time: '时间',
+            day: '天',
             total: '共',
             school: '学校',
             spaceAndRights: '空间与权益',
@@ -959,8 +960,8 @@ const zh_cn = {
         VDPGJ6NC: '劳动教育服务',
         YPXSJ6NJ: '五育看板',
         IPALB6EY: 'IES5个人空间',
-        Z6ELB6EZ: 'HiTeach5ID授权',
-        VA67B6EZ: 'HiTeach5县市授权',
+        Z6ELB6EZ: 'HiTeach5 ID授权',
+        VA67B6EZ: 'HiTeach5 县市授权',
         VAA7B6EY: 'IES5个人空间县市授权',
     },
     aprule: {

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

@@ -843,6 +843,7 @@ const zh_tw = {
             lastLoginTime: '上次登陸時間',
             noUseRecently: '近期未使用',
             time: '時間',
+            day: '天',
             total: '共',
             school: '學校',
             spaceAndRights: '空間與權益',
@@ -952,8 +953,8 @@ const zh_tw = {
         VDPGJ6NC: '勞動教育服務',
         YPXSJ6NJ: '五育看板',
         IPALB6EY: 'IES5個人空間',
-        Z6ELB6EZ: 'HiTeach5ID授權',
-        VA67B6EZ: 'HiTeach5縣市授權',
+        Z6ELB6EZ: 'HiTeach5 ID授權',
+        VA67B6EZ: 'HiTeach5 縣市授權',
         VAA7B6EY: 'IES5個人空間縣市授權',
     },
     aprule: {

+ 3 - 2
TEAMModelBI/ClientApp/src/view/product/index.vue

@@ -460,7 +460,7 @@ let columns = ref([
   },
 ])
 const popoverRef = ref()
-const siteValue = window.location.host === 'localhost:5001' ? 'cn' : window.location.host === 'bi.teammodel.cn' ? 'cn' : window.location.host === 'bitest.teammodel.cn' ? 'cn' : 'international'
+const siteValue = window.location.host === 'localhost:5001' ? 'international' : window.location.host === 'bi.teammodel.cn' ? 'cn' : window.location.host === 'bitest.teammodel.cn' ? 'cn' : 'international'
 const optionsData = siteValue === 'cn' ? option_cn : option_gl
 //const optionsData = option_gl
 columns.value[0].headerCellRenderer = (props = HeaderCellSlotProps) => {
@@ -716,11 +716,12 @@ let exportList = ref([
   { label: proxy.$t(`product.studentNumberUnique`), key: 'stuShow' },
   { label: proxy.$t(`product.studentJoinHours`), key: 'stuLessonLengMin' },
   { label: proxy.$t(`product.tGreen`), key: 'tGreen' },
+  { label: proxy.$t(`product.tLesson`), key: 'tLesson' },
   { label: proxy.$t(`product.lessonNumber`), key: 'lessonRecord' },
   { label: proxy.$t(`product.lessonHours`), key: 'lessonLengMin' },
   { label: proxy.$t(`product.time`), key: 'date' },
 ])
-let fieldList = ref([proxy.$t(`product.school`) + proxy.$t(`product.name`), proxy.$t(`product.school`) + proxy.$t(`product.simpleCode`), proxy.$t(`product.classroomNumber`), proxy.$t(`product.teacherNumber`), proxy.$t(`product.studentNumberUnique`), proxy.$t(`product.studentJoinHours`), proxy.$t(`product.tGreen`), proxy.$t(`product.lessonNumber`), proxy.$t(`product.lessonHours`), proxy.$t(`product.time`)])
+let fieldList = ref([proxy.$t(`product.school`) + proxy.$t(`product.name`), proxy.$t(`product.school`) + proxy.$t(`product.simpleCode`), proxy.$t(`product.classroomNumber`), proxy.$t(`product.teacherNumber`), proxy.$t(`product.studentNumberUnique`), proxy.$t(`product.studentJoinHours`), proxy.$t(`product.tGreen`), proxy.$t(`product.tLesson`), proxy.$t(`product.lessonNumber`), proxy.$t(`product.lessonHours`), proxy.$t(`product.time`)])
 let exportStandard = ref([
   { title: proxy.$t(`product.joinNumberUnique`), value: 0, key: 'personnum', option: [{ name: proxy.$t(`product.smaller`) + '50', value: '50' }, { name: proxy.$t(`product.bigger`) + '50,' + proxy.$t(`product.smaller`) + '100', value: '50-100' }, { name: proxy.$t(`product.bigger`) + '100', value: '101' }, { name: proxy.$t(`product.bigger`) + '300', value: '301' }] },
   { title: proxy.$t(`product.lessonTime`), value: 0, key: 'classtime', option: [{ name: proxy.$t(`product.smaller`) + proxy.$t(`product.equal`) + '45' + proxy.$t(`product.minute`), value: '45' }, { name: proxy.$t(`product.bigger`) + '45' + proxy.$t(`product.minute`) + ',' + proxy.$t(`product.smaller`) + '90' + proxy.$t(`product.minute`), value: '45-90' }, { name: proxy.$t(`product.smaller`) + '90' + proxy.$t(`product.minute`), value: '91' }] },

+ 49 - 8
TEAMModelBI/ClientApp/src/view/userInquire/details.vue

@@ -230,7 +230,7 @@
                                     <div class="prodlist-item" v-for="item in powerList" :key="item">
                                     <div class="rightsbox-item">
                                         <div class="rightsbox-item-title">
-                                            <!-- <p>[ID授权] {{item.name}}</p> -->
+                                            {{item.name}}
                                         </div>
                                         <div class="rightsbox-item-time">
                                             <p>
@@ -243,7 +243,7 @@
                                                 <span class="rightsbox-item-timenums">{{item.aprule.schoolinfo.name}}</span>
                                             </p>
                                         </div>
-                                        <div class="rightsbox-item-expansion" v-show="item.prodCode ==='Z6ELB6EZ'">
+                                        <div class="rightsbox-item-expansion" v-show="item.prodCode==='Z6ELB6EZ' || item.prodCode==='VA67B6EZ'">
                                             <p class="expansion-title">{{$t(`userInquire.details.apRule`)}}</p>
                                             <!--产品扩充-->
                                             <div v-for="iteml in item.apruleArr" :key="iteml.key">
@@ -901,13 +901,29 @@ function initdata() {
     
     //权益内容
     let {prod,benefits}=transmitData
-    prod.forEach((item)=>{
-        //处理名字
-        item.name = item.prodCode === 'Z6ELB6EZ' ? 'HiTeach 5' : item.prodCode === 'IPALB6EY' ? proxy.$t(`auth.IPALB6EY`) : ''
+    prod.forEach((item) => {
+        //商品名字  IPALB6EY:'IES5個人空間', Z6ELB6EZ: 'HiTeach5ID授權', VA67B6EZ: 'HiTeach5縣市授權', VAA7B6EY: 'IES5個人空間縣市授權',
+        switch (item.prodCode) {
+            case 'IPALB6EY':
+                item.name = proxy.$t(`auth.IPALB6EY`)
+                break;
+            case 'Z6ELB6EZ':
+                item.name = proxy.$t(`auth.Z6ELB6EZ`)
+                break;
+            case 'VA67B6EZ':
+                item.name = proxy.$t(`auth.VA67B6EZ`)
+                break;
+            case 'VAA7B6EY':
+                item.name = proxy.$t(`auth.VAA7B6EY`)
+                break;
+        }
+        //item.name = item.prodCode === 'Z6ELB6EZ' ? proxy.$t(`auth.Z6ELB6EZ`) : item.prodCode === 'IPALB6EY' ? proxy.$t(`auth.IPALB6EY`) : ''
         //处理扩展内容
         let apruleItem=item.aprule
         item.apruleArr=[]
-        if(apruleItem !==null && item.prodCode ==='Z6ELB6EZ'){
+        if (apruleItem !== null && (item.prodCode === 'Z6ELB6EZ' || item.prodCode === 'VA67B6EZ') )
+        {
+            powerExtensionReset()
             apruleItem.hdcam ? powerExtension.value[0].state=true:powerExtension.value[0].state=false //USB錄影支援
             apruleItem.sokapp ? powerExtension.value[1].state=true:powerExtension.value[1].state=false //蘇格拉底議課APP
             apruleItem.sokvdo ? powerExtension.value[2].state=true:powerExtension.value[2].state=false //蘇格拉底影片
@@ -926,7 +942,7 @@ function initdata() {
             apruleItem.sokvtt ? powerExtension.value[15].state = true : powerExtension.value[15].state = false //蘇格拉底語音轉寫
             apruleItem.cowork ? powerExtension.value[16].state = true : powerExtension.value[16].state = false //協作
             apruleItem.aigpt ? powerExtension.value[17].state = true : powerExtension.value[17].state = false //AI GPT服務
-            item.apruleArr=powerExtension.value
+            item.apruleArr = JSON.parse(JSON.stringify(powerExtension.value))
         }
         //处理时间
         let startDates=new Date(item.startDate);let endDates=new Date(item.endDate)
@@ -936,7 +952,7 @@ function initdata() {
         console.log(days,'天数')
         item.intervalTime=days
         item.startDateText=item.startDate ? proxy.$common.timestampToTime(item.startDate):0
-        item.endDateText= item.endDate? proxy.$common.timestampToTime(item.endDate):0
+        item.endDateText = item.endDate ? proxy.$common.timestampToTime(item.endDate) : 0
         powerList.value.push(item)  
     })
     console.log(powerList.value,'权益内容')
@@ -985,6 +1001,31 @@ function initdata() {
     // advancevalue.value.next=detailsData.value[transferNum.value+1]
     
 }
+//HiTeach擴充項 reset
+function powerExtensionReset() {
+    powerExtension.value[0].state = false //USB錄影支援
+    powerExtension.value[1].state = false //蘇格拉底議課APP
+    powerExtension.value[2].state = false //蘇格拉底影片
+    powerExtension.value[3].state = false //蘇格拉底桌面
+    powerExtension.value[4].state = false //蘇格拉底報告
+    powerExtension.value[5].state = false //蘇格拉底小數據
+    powerExtension.value[6].state = false //AI智能終端
+    powerExtension.value[7].state = false //AI文句分析
+    powerExtension.value[8].state = false //電子學生證
+    powerExtension.value[9].state = false  //雲端診斷分析系統
+    powerExtension.value[10].value = 0 //分組數
+    powerExtension.value[10].state = false //分組數
+    powerExtension.value[11].value = 0 //IRS連線授權數
+    powerExtension.value[11].state = false //IRS連線授權數
+    powerExtension.value[12].value = 0 //議課人數
+    powerExtension.value[12].state = true //議課人數
+    powerExtension.value[13].value = 0 //智慧評分系統
+    powerExtension.value[13].state = false //智慧評分系統
+    powerExtension.value[14].state = true  //IRS硬體遙控器
+    powerExtension.value[15].state = false //蘇格拉底語音轉寫
+    powerExtension.value[16].state = false //協作
+    powerExtension.value[17].state = false //AI GPT服務
+}
 function bytesToGB(bytes) {
   const gb = bytes / (1024 * 1024 * 1024);
   return gb.toFixed(2); // 将结果保留两位小数

+ 7 - 4
TEAMModelBI/Controllers/BITmid/TmidController.cs

@@ -194,10 +194,13 @@ namespace TEAMModelBI.Controllers.BITmid
                             string city = doc.GetProperty("city").GetString();
                             string district = string.Empty;
                             if (!string.IsNullOrWhiteSpace(country) && country.Equals("TW"))
-                            {
-                                district = city;
-                                city = province;
-                                province = string.Empty;
+                            {   //TW有兩種格式 (1)無province => 無須變更 (2)有province => city = province,district = city
+                                if(!string.IsNullOrWhiteSpace(province))
+                                {
+                                    district = city;
+                                    city = province;
+                                    province = string.Empty;
+                                }
                             }
                             tmidStics.country = (!string.IsNullOrWhiteSpace(country) && regionData.country.ContainsKey(country)) ? regionData.country[country].name.Replace("地區", "").Replace("地区", "") : country;
                             tmidStics.province = (!string.IsNullOrWhiteSpace(country) && regionData.country.ContainsKey(country) && !string.IsNullOrWhiteSpace(province) && regionData.province.ContainsKey(country) && regionData.province[country].ContainsKey(province)) ? regionData.province[country][province].name : province;

+ 4 - 4
TEAMModelBI/Controllers/Census/SchoolController.cs

@@ -1117,7 +1117,7 @@ namespace TEAMModelBI.Controllers.Census
                 areaScStats.resourceCnt = statsInfo.resourceCnt;
                 areaScStats.size = statsInfo.size;
                 //areaScStats.useSize = statsInfo.useSize;
-                areaScStats.lessStats.open = statsInfo.lesson.all;
+                areaScStats.lessStats.all = statsInfo.lesson.all;
                 areaScStats.lessStats.open = statsInfo.lesson.open;
                 areaScStats.lessStats.less = statsInfo.lesson.less;
                 areaScStats.lessStats.lastDay = statsInfo.lesson.lastDay;
@@ -1282,7 +1282,7 @@ namespace TEAMModelBI.Controllers.Census
                 allScStats.size = statsInfo.size;
                 allScStats.scCreateTime = statsInfo.scCreateTime;
                 allScStats.upTime = statsInfo.upTime;
-                allScStats.lessStats.open = statsInfo.lesson.all;
+                allScStats.lessStats.all = statsInfo.lesson.all;
                 allScStats.lessStats.open = statsInfo.lesson.open;
                 allScStats.lessStats.less = statsInfo.lesson.less;
                 allScStats.lessStats.lastDay = statsInfo.lesson.lastDay;
@@ -1306,8 +1306,8 @@ namespace TEAMModelBI.Controllers.Census
                 allScStats.actStats.dayCnt = statsInfo.activity.dayCnt;
                 allScStats.actStats.lastWeek = statsInfo.activity.lastWeek;
                 allScStats.actStats.week = statsInfo.activity.week;
-                allScStats.actStats.lastTerm = statsInfo.activity.lastTerm;
-                allScStats.actStats.term = statsInfo.activity.term;
+                allScStats.actStats.lastTerm = statsInfo.activity.lastTerm; //上學期活動
+                allScStats.actStats.term = statsInfo.activity.term; //本學期活動
                 allScStats.actStats.lastMonth = statsInfo.activity.lastMonth;
                 allScStats.actStats.month = statsInfo.activity.month;
                 if (statsInfo.study != null)

+ 3 - 3
TEAMModelBI/TEAMModelBI.csproj

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

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

@@ -105,7 +105,7 @@ namespace HTEX.Complex.Services
                             screenClient.cpu = device.cpu;
                             screenClient.ram = device.ram;
                             screenClient.last_time= DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
-                            screenClient.deviceId = serverDevice.deviceId;
+                            screenClient.deviceId = device.deviceId;
                             //连接成功,发送消息给客户端。
                             await SendConnection(connid, new ConnectionMessage
                             {

+ 1 - 1
TEAMModelOS.Extension/HTEX.ScreenClient/Services/SignalRScreenClientHub.cs

@@ -59,7 +59,7 @@ namespace HTEX.ScreenClient.Services
             var reconnectPolicy = new ExponentialBackoffReconnectPolicy(TimeSpan.FromSeconds(10), _logger); // 尝试重连的最大次数,这里使用 int.MaxValue 表示无限次
             reconnectPolicy.MaxRetryCount = int.MaxValue;
             HubConnection hubConnection = new HubConnectionBuilder()
-               .WithUrl($"{CenterUrl}/signalr/screen?grant_type=bookjs_api&clientid={clientid}&device={HttpUtility.UrlEncode(device.ToJsonString(), Encoding.Unicode)}") //only one slash
+               .WithUrl($"{CenterUrl}/signalr/screen?grant_type={ScreenConstant.grant_type}&clientid={clientid}&device={HttpUtility.UrlEncode(device.ToJsonString(), Encoding.Unicode)}") //only one slash
                .WithAutomaticReconnect(reconnectPolicy)
                .ConfigureLogging(logging =>
                {

+ 16 - 1
TEAMModelOS.Extension/HTEX.Test/Program.cs

@@ -30,7 +30,22 @@ namespace HTEX.Test
     {
         public static async Task Main(string[] args)
         {
-           
+
+            string sws = $"{"123".PadLeft(2, '0')}-{"1".PadLeft(2,'0')}";
+
+            List<string> s= new List<string>();
+            s.Add("fbc284072a40da84890e4860c15c453b8638a4b1839455635ab838bbe1a3339d31d8219466cef60f0f4a2622c5dc36c34f8ba143450025d2");
+            s.Add("fbc284072a40da8463057d2118d2978b37eb91e0534cfd525a76c5eed9c793affb417dcd776a68f119af14627d5ee85213f5e3e35f680538");
+            s.Add("fbc284072a40da8463057d2118d2978bf96625e980e310ef69dd62d4778abb46a3341709da302abe3623e3a37dd6796e8222f7143827eaef");
+            s.Add("fbc284072a40da8463057d2118d2978b2a80c26f4ac5aaa6981873f4e062d104a7d20b81890394af238c0acfb0949712980917fe9caee4aa");
+            s.Add("fbc284072a40da84b345d9b6abcc7f857352943f1a7ca844ce0e55c91821ec5b423d1f02b4768ab42017e0a418b25f7925a43125ffd20df7");
+            s.Add("fbc284072a40da8463057d2118d2978b98faee8d973f5641324682352e4aac2ef58c14a14929098788f0cfae40ca5d6a7af1d9c9c219006d");
+            s.Add("fbc284072a40da84c04385f268e6fefff3a0818487e5ccf023de060587c785a3d46ced5c51055055463ea6995045054723b328f9c3b32d27");
+            s.Add("fbc284072a40da84184ab5ec079fb0faea0716bb55bc45fe021d93318b256fb20126f7011f66d683c1b77f835b7def23d6957fb2b861aef0");
+            foreach (var item in s) 
+            {
+                MurmurHash3.Hash32(item);
+            }
             //await MockDataController.MockData();
             var builder = WebApplication.CreateBuilder(args);
 

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

@@ -32,6 +32,8 @@ using HtmlAgilityPack;
 using Azure.Storage.Blobs;
 using System.ComponentModel;
 using TEAMModelOS.Function;
+using DocumentFormat.OpenXml.Spreadsheet;
+using Items = TEAMModelOS.SDK.Models.Cosmos.Student.Items;
 namespace TEAMModelOS.CosmosDBTriggers
 {
     public class TriggerExam
@@ -341,7 +343,14 @@ namespace TEAMModelOS.CosmosDBTriggers
                                                 result.sum.Add(0);
                                                 result.status.Add(1);
                                                 if (info.qamode == 2) {
-                                                    result.paper.Add(blobs[new Random().Next(blobs.Count)]);
+                                                    Random random = new Random();
+                                                    int randomIndex = random.Next(blobs.Count); // 生成一个随机索引
+                                                    string randomBlob = blobs[randomIndex]; // 获取随机索引对应的元素
+                                                    result.paper.Add(randomBlob);
+                                                    blobs.Remove(randomBlob);
+                                                    if (blobs.Count == 0) {
+                                                        blobs = info.papers.Where(c => c.subjectId.Equals(subject.id)).Select(c => c.blob).ToList();
+                                                    }
                                                 }                                              
                                             }
 

+ 2 - 3
TEAMModelOS.Function/IESHttpTrigger.cs

@@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Mvc;
 using Microsoft.Azure.Functions.Worker;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
-using TEAMModelOS.Function.DI;
 using TEAMModelOS.SDK.DI;
 using TEAMModelOS.SDK;
 using TEAMModelOS.Models;
@@ -441,7 +440,7 @@ namespace TEAMModelOS.Function
                     {
                         var ids = graduate_classes.Where(x => !string.IsNullOrWhiteSpace(x.id)).Select(x => $"'{x.id}'");
                         List<Student> students = new List<Student>();
-                        string sql = $"select value c from c where  (c.graduate = 0 or  IS_DEFINED(c.graduate) = false)  and  c.classId in ({string.Join(",", ids)})";
+                        string sql = $"select value c from c where    c.classId in ({string.Join(",", ids)})";
                         await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Student)
                             .GetItemQueryIteratorSql<Student>(queryText: sql, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"Base-{schoolId}") }))
                         {
@@ -468,7 +467,7 @@ namespace TEAMModelOS.Function
                     {
                         var ids = cancel_graduate_classes.Where(x => !string.IsNullOrWhiteSpace(x.id)).Select(x => $"'{x.id}'");
                         List<Student> students = new List<Student>();
-                        string sql = $"select value c from c where  c.graduate =1   and  c.classId in ({string.Join(",", ids)})";
+                        string sql = $"select value c from c where   c.classId in ({string.Join(",", ids)})";
                         await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Student)
                             .GetItemQueryIteratorSql<Student>(queryText: sql, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"Base-{schoolId}") }))
                         {

+ 15 - 1
TEAMModelOS.Function/IESServiceBusTrigger.cs

@@ -5,7 +5,6 @@ using DinkToPdf.Contracts;
 using Microsoft.Azure.Functions.Worker;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
-using TEAMModelOS.Function.DI;
 using TEAMModelOS.SDK.DI;
 using TEAMModelOS.SDK;
 using TEAMModelOS.Models;
@@ -48,6 +47,7 @@ using static TEAMModelOS.SDK.Models.Service.SystemService;
 using System.Xml;
 using System.Reflection;
 using System.Text.RegularExpressions;
+using Microsoft.Azure.Cosmos.Core;
 
 namespace TEAMModelOS.Function
 {
@@ -1280,6 +1280,20 @@ namespace TEAMModelOS.Function
                 prodSum.service = servicesProductSumOrg;
                 prodSum.hard = hardsProductSumOrg;
                 await client.GetContainer(Constant.TEAMModelOS, "School").ReplaceItemAsync<SchoolProductSum>(prodSum, prodSum.id, new PartitionKey($"{prodSum.code}"));
+                ///更新学生端学校列表
+                var res = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.School).GetList<dynamic>("select  c.id,c.city,c.name,c.province,c.picture,c.region from c ", "Base");
+                var schools = new { schools = res.list };
+                var blockBlob = _azureStorage.GetBlobContainerClient("0-public").GetBlobClient("schools.json");
+                string content_type = "application/octet-stream";
+                ContentTypeDict.dict.TryGetValue(".json", out string contenttype);
+                if (!string.IsNullOrEmpty(contenttype))
+                {
+                    content_type = contenttype;
+                }
+                byte[] bytes = System.Text.Encoding.Default.GetBytes(schools.ToJsonString());
+                Stream streamBlob = new MemoryStream(bytes);
+                await blockBlob.UploadAsync(streamBlob, true);
+                blockBlob.SetHttpHeaders(new BlobHttpHeaders { ContentType = content_type });
             }
             catch (CosmosException ex)
             {

+ 0 - 1
TEAMModelOS.Function/Program.cs

@@ -3,7 +3,6 @@ using Microsoft.Azure.Functions.Worker;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
-using TEAMModelOS.Function.DI;
 using TEAMModelOS.SDK;
 using TEAMModelOS.SDK.DI;
 using TEAMModelOS.SDK.DI.Multiple;

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

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

+ 1 - 0
TEAMModelOS.SDK/DI/AzureSignalR/AzureSignalRFactory.cs

@@ -41,5 +41,6 @@ namespace TEAMModelOS.SDK.DI
                 return null;
             }
         }
+
     }
 }

+ 2 - 1
TEAMModelOS.Function/DI/BackgroundWorkerQueue.cs

@@ -4,9 +4,10 @@ using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
+using System.Threading;
 using System.Threading.Tasks;
 
-namespace TEAMModelOS.Function.DI
+namespace TEAMModelOS.SDK.DI
 {
     public class BackgroundWorkerQueue
     {

+ 452 - 0
TEAMModelOS.SDK/Helper/Security/Hash/MurmurHash3.cs

@@ -0,0 +1,452 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TEAMModelOS.SDK
+{
+    public static class MurmurHash3
+    {
+        public static uint Hash32(string input)
+        {
+            var buffer = Encoding.UTF8.GetBytes(input);
+            uint seed = 0;
+            using var stream = new MemoryStream(buffer);
+            var result = MurmurHash3.Hash32(stream, seed);
+            return result;
+        }
+        public static string Hash128(string input)
+        {
+            var buffer = Encoding.UTF8.GetBytes(input);
+            uint seed = 0;
+            using var stream = new MemoryStream(buffer);
+            var result = MurmurHash3.Hash128(stream, seed);
+            return Convert.ToHexString(result);
+        }
+
+        /// <summary>
+        /// Calculate 32-bit MurmurHash3 hash value using x86 version of the algorithm.
+        /// </summary>
+        /// <param name="stream">Input stream.</param>
+        /// <param name="seed">Seed value.</param>
+        /// <returns>Hash value.</returns>
+        public static uint Hash32(Stream stream, uint seed)
+        {
+            const int uintSize = sizeof(uint);
+            const uint final1 = 0x85ebca6b;
+            const uint final2 = 0xc2b2ae35;
+            const uint n = 0xe6546b64;
+            const uint m = 5;
+
+            uint hash = seed;
+            var buffer = new byte[uintSize].AsSpan();
+            uint length = 0;
+            int bytesRead;
+            while ((bytesRead = stream.Read(buffer)) == uintSize)
+            {
+                uint k = Bits.ToUInt32(buffer);
+                round32(ref k, ref hash);
+                hash = Bits.RotateLeft(hash, 13);
+                hash *= m;
+                hash += n;
+                length += (uint)bytesRead;
+            }
+
+            // process remaning bytes
+            if (bytesRead > 0)
+            {
+                uint remaining = Bits.PartialBytesToUInt32(buffer[..bytesRead]);
+                round32(ref remaining, ref hash);
+                length += (uint)bytesRead;
+            }
+
+            hash ^= length;
+
+            // finalization mix
+            hash ^= hash >> 16;
+            hash *= final1;
+            hash ^= hash >> 13;
+            hash *= final2;
+            hash ^= hash >> 16;
+            return hash;
+        }
+
+        /// <summary>
+        /// Calculate 32-bit MurmurHash3 hash value using x86 version of the algorithm.
+        /// </summary>
+        /// <param name="stream">Input stream.</param>
+        /// <param name="seed">Seed value.</param>
+        /// <returns>A <see cref="Task"/> representing the asynchronous hash operation.</returns>
+        public static async Task<uint> Hash32Async(Stream stream, uint seed)
+        {
+            const int uintSize = sizeof(uint);
+            const uint final1 = 0x85ebca6b;
+            const uint final2 = 0xc2b2ae35;
+            const uint n = 0xe6546b64;
+            const uint m = 5;
+
+            uint hash = seed;
+            var buffer = new byte[uintSize].AsMemory();
+            uint length = 0;
+            int bytesRead;
+            while ((bytesRead = await stream.ReadAsync(buffer).ConfigureAwait(false)) == uintSize)
+            {
+                uint k = Bits.ToUInt32(buffer.Span);
+                round32(ref k, ref hash);
+                hash = Bits.RotateLeft(hash, 13);
+                hash *= m;
+                hash += n;
+                length += (uint)bytesRead;
+            }
+
+            // process remaning bytes
+            if (bytesRead > 0)
+            {
+                uint remaining = Bits.PartialBytesToUInt32(buffer[..bytesRead].Span);
+                round32(ref remaining, ref hash);
+                length += (uint)bytesRead;
+            }
+
+            hash ^= length;
+
+            // finalization mix
+            hash ^= hash >> 16;
+            hash *= final1;
+            hash ^= hash >> 13;
+            hash *= final2;
+            hash ^= hash >> 16;
+            return hash;
+        }
+
+        /// <summary>
+        /// Calculate 32-bit MurmurHash3 hash value.
+        /// </summary>
+        /// <param name="buffer">Input buffer.</param>
+        /// <param name="seed">Seed value.</param>
+        /// <returns>Hash value.</returns>
+        public static uint Hash32(ReadOnlySpan<byte> buffer, uint seed)
+        {
+            const int uintSize = sizeof(uint);
+            const uint final1 = 0x85ebca6b;
+            const uint final2 = 0xc2b2ae35;
+            const uint n = 0xe6546b64;
+            const uint m = 5;
+
+            uint hash = seed;
+            int length = buffer.Length;
+            var (numUInts, leftBytes) = Math.DivRem(length, uintSize);
+            int i = 0;
+            for (; i < numUInts * sizeof(uint); i += sizeof(uint))
+            {
+                uint k = Bits.ToUInt32(buffer[i..(i + sizeof(uint))]);
+                round32(ref k, ref hash);
+                hash = Bits.RotateLeft(hash, 13);
+                hash *= m;
+                hash += n;
+            }
+
+            if (leftBytes > 0)
+            {
+                uint remaining = Bits.PartialBytesToUInt32(buffer[i..(i + leftBytes)]);
+                round32(ref remaining, ref hash);
+            }
+
+            hash ^= (uint)length;
+
+            // finalization mix
+            hash ^= hash >> 16;
+            hash *= final1;
+            hash ^= hash >> 13;
+            hash *= final2;
+            hash ^= hash >> 16;
+            return hash;
+        }
+
+        /// <summary>
+        /// Calculate 128-bit MurmurHash3 hash value using 64-bit version of the algorithm.
+        /// </summary>
+        /// <param name="stream">Input stream.</param>
+        /// <param name="seed">Seed value.</param>
+        /// <returns>128-bit hash value in a Span.</returns>
+        public static byte[] Hash128(Stream stream, uint seed)
+        {
+            const int ulongSize = sizeof(ulong);
+            const int blockSize = ulongSize * 2;
+
+            const ulong c1 = 0x87c37b91114253d5UL;
+            const ulong c2 = 0x4cf5ad432745937fUL;
+
+            ulong h1 = seed;
+            ulong h2 = seed;
+            var buffer = new byte[blockSize].AsSpan();
+            int outputLength = 0;
+            int readBytes;
+            while ((readBytes = stream.Read(buffer)) == blockSize)
+            {
+                ulong ik1 = Bits.ToUInt64(buffer);
+                ulong ik2 = Bits.ToUInt64(buffer[ulongSize..]);
+
+                round128(ref ik1, ref h1, c1, c2, h2, 31, 27, 0x52dce729U);
+                round128(ref ik2, ref h2, c2, c1, h1, 33, 31, 0x38495ab5U);
+
+                outputLength += blockSize;
+            }
+
+            ulong k1;
+            ulong k2;
+
+            if (readBytes > ulongSize)
+            {
+                k2 = Bits.PartialBytesToUInt64(buffer[ulongSize..]);
+                tailRound128(ref k2, ref h2, c2, c1, 33);
+                outputLength += readBytes - ulongSize;
+                readBytes = ulongSize;
+            }
+
+            if (readBytes > 0)
+            {
+                k1 = Bits.PartialBytesToUInt64(buffer[..readBytes]);
+                tailRound128(ref k1, ref h1, c1, c2, 31);
+                outputLength += readBytes;
+            }
+
+            h1 ^= (ulong)outputLength;
+            h2 ^= (ulong)outputLength;
+
+            h1 += h2;
+            h2 += h1;
+
+            fmix64(ref h1);
+            fmix64(ref h2);
+
+            h1 += h2;
+            h2 += h1;
+
+            return makeBytes(h1, h2);
+        }
+
+        /// <summary>
+        /// Calculate 128-bit MurmurHash3 hash value using x64 version of the algorithm.
+        /// </summary>
+        /// <param name="buffer">Input buffer.</param>
+        /// <param name="seed">Seed value.</param>
+        /// <returns>128-bit hash value as a Span of bytes.</returns>
+        public static byte[] Hash128(ReadOnlySpan<byte> buffer, uint seed)
+        {
+            const int blockSize = 16;
+            const ulong c1 = 0x87c37b91114253d5UL;
+            const ulong c2 = 0x4cf5ad432745937fUL;
+
+            ulong h1 = seed;
+            ulong h2 = seed;
+
+            int length = buffer.Length;
+            var (numBlocks, leftBytes) = Math.DivRem(length, blockSize);
+
+            int offset = 0;
+            int end = numBlocks * sizeof(ulong) * 2;
+            while (offset != end)
+            {
+                ulong ik1 = Bits.ToUInt64(buffer[offset..(offset + sizeof(ulong))]);
+                offset += sizeof(ulong);
+                ulong ik2 = Bits.ToUInt64(buffer[offset..(offset + sizeof(ulong))]);
+                offset += sizeof(ulong);
+
+                round128(ref ik1, ref h1, c1, c2, h2, 31, 27, 0x52dce729U);
+                round128(ref ik2, ref h2, c2, c1, h1, 33, 31, 0x38495ab5U);
+            }
+
+            int tail = offset;
+            ulong k1;
+            ulong k2;
+
+            if (leftBytes > sizeof(ulong))
+            {
+                offset += sizeof(ulong);
+                k2 = Bits.PartialBytesToUInt64(buffer[offset..]);
+                tailRound128(ref k2, ref h2, c2, c1, 33);
+                leftBytes = sizeof(ulong);
+            }
+
+            if (leftBytes > 0)
+            {
+                k1 = Bits.PartialBytesToUInt64(buffer[tail..(tail + leftBytes)]);
+                tailRound128(ref k1, ref h1, c1, c2, 31);
+            }
+
+            h1 ^= (ulong)length;
+            h2 ^= (ulong)length;
+
+            h1 += h2;
+            h2 += h1;
+
+            fmix64(ref h1);
+            fmix64(ref h2);
+
+            h1 += h2;
+            h2 += h1;
+
+            return makeBytes(h1, h2);
+        }
+
+        private static byte[] makeBytes(ulong h1, ulong h2)
+        {
+            var result = new byte[16];
+            BitConverter.TryWriteBytes(result, h1);
+            BitConverter.TryWriteBytes(result.AsSpan()[8..], h2);
+            return result;
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static void round32(ref uint value, ref uint hash)
+        {
+            const uint c1 = 0xcc9e2d51;
+            const uint c2 = 0x1b873593;
+
+            value *= c1;
+            value = Bits.RotateLeft(value, 15);
+            value *= c2;
+            hash ^= value;
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static void round128(
+            ref ulong k,
+            ref ulong h,
+            ulong c1,
+            ulong c2,
+            ulong hn,
+            int krot,
+            int hrot,
+            uint x)
+        {
+            k *= c1;
+            k = Bits.RotateLeft(k, krot);
+            k *= c2;
+            h ^= k;
+            h = Bits.RotateLeft(h, hrot);
+            h += hn;
+            h = (h * 5) + x;
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static void fmix64(ref ulong h)
+        {
+            h ^= h >> 33;
+            h *= 0xff51afd7ed558ccdUL;
+            h ^= h >> 33;
+            h *= 0xc4ceb9fe1a85ec53UL;
+            h ^= h >> 33;
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static void tailRound128(ref ulong k, ref ulong h, ulong c1, ulong c2, int rot)
+        {
+            k *= c1;
+            k = Bits.RotateLeft(k, rot);
+            k *= c2;
+            h ^= k;
+        }
+    }
+
+    internal static class Bits
+    {
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal static ulong RotateLeft(ulong value, int bits)
+        {
+            return (value << bits) | (value >> (64 - bits));
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal static uint RotateLeft(uint value, int bits)
+        {
+            return (value << bits) | (value >> (32 - bits));
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal static uint RotateRight(uint value, int bits)
+        {
+            return (value >> bits) | (value << (32 - bits));
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal static ulong RotateRight(ulong value, int bits)
+        {
+            return (value >> bits) | (value << (64 - bits));
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal static ulong ToUInt64(ReadOnlySpan<byte> bytes)
+        {
+            return Unsafe.ReadUnaligned<ulong>(ref MemoryMarshal.GetReference(bytes));
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal static uint ToUInt32(ReadOnlySpan<byte> bytes)
+        {
+            return Unsafe.ReadUnaligned<uint>(ref MemoryMarshal.GetReference(bytes));
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal static ulong PartialBytesToUInt64(ReadOnlySpan<byte> remainingBytes)
+        {
+            // a switch/case approach is slightly faster than the loop but .net
+            // refuses to inline it due to larger code size.
+            ulong result = 0;
+
+            // trying to modify leftBytes would invalidate inlining
+            // need to use local variable instead
+            for (int i = 0; i < remainingBytes.Length; i++)
+            {
+                result |= ((ulong)remainingBytes[i]) << (i << 3);
+            }
+
+            return result;
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal static uint PartialBytesToUInt32(ReadOnlySpan<byte> remainingBytes)
+        {
+            int len = remainingBytes.Length;
+            if (len > 3)
+            {
+                return Bits.ToUInt32(remainingBytes);
+            }
+
+            // a switch/case approach is slightly faster than the loop but .net
+            // refuses to inline it due to larger code size.
+            uint result = remainingBytes[0];
+            if (len > 1)
+            {
+                result |= (uint)(remainingBytes[1] << 8);
+            }
+
+            if (len > 2)
+            {
+                result |= (uint)(remainingBytes[2] << 16);
+            }
+
+            return result;
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal static uint SwapBytes32(uint num)
+        {
+            return (Bits.RotateLeft(num, 8) & 0x00FF00FFu)
+                 | (Bits.RotateRight(num, 8) & 0xFF00FF00u);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal static ulong SwapBytes64(ulong num)
+        {
+            num = (Bits.RotateLeft(num, 48) & 0xFFFF0000FFFF0000ul)
+                | (Bits.RotateLeft(num, 16) & 0x0000FFFF0000FFFFul);
+            return (Bits.RotateLeft(num, 8) & 0xFF00FF00FF00FF00ul)
+                 | (Bits.RotateRight(num, 8) & 0x00FF00FF00FF00FFul);
+        }
+    }
+}

+ 4 - 0
TEAMModelOS.SDK/Models/Cosmos/Common/ArtEvaluation.cs

@@ -104,6 +104,10 @@ namespace TEAMModelOS.SDK.Models.Cosmos.Common
         /// </summary>
         public long uploadETime { get; set; }
         /// <summary>
+        /// 考试截止时间
+        /// </summary>
+        public long examDeadline { get;set;}
+        /// <summary>
         /// 上传进度
         /// </summary>
         public string uploadProgress { get; set; }

+ 49 - 9
TEAMModelOS.SDK/Models/Service/GenPDFService.cs

@@ -778,9 +778,52 @@ namespace TEAMModelOS.SDK
         /// </summary>
         public string blobFullUrl { get; set; }
     }
+    public static class HybridConstant
+    {
+        public static readonly string busy = "busy";
+        public static readonly string idle = "idle";
+        public static readonly string error = "error";
+        public static readonly string offline = "offline";
+        public static readonly string grant_type = "hybrid-cloud";
+        public static readonly string env_release = "release";
+        public static readonly string env_develop = "develop";
+        /// <summary>
+        /// 冗余时间
+        /// </summary>
+        public static readonly long time_excess = 5000;
+    }
 
-
-
+    public class HybridClient : ClientDevice 
+    {
+        /// <summary>
+        /// 授权类型,hybrid-cloud 
+        /// </summary>
+        public string? grant_type { get; set; }
+        /// <summary>
+        /// 客户端id
+        /// </summary>
+        public string? clientid { get; set; }
+        /// <summary>
+        /// SignalR的连接ID 不建议暴露。
+        /// </summary>
+        public string? connid { get; set; }
+        /// <summary>
+        /// 状态  busy 忙碌,free 空闲,down 离线,error 错误
+        /// </summary>
+        public string? status { get; set; }
+        /// <summary>
+        /// 最后更新时间
+        /// </summary>
+        public long last_time { get; set; }
+        /// <summary>
+        /// 超时时间,单位毫秒
+        /// </summary>
+        public long timeout { get; set; } = 30000;
+        /// <summary>
+        /// 延迟时间,单位毫秒
+        /// </summary>
+        public long delay { get; set; } = 3000;
+    }
 
     public class ScreenClient  : ClientDevice
     {
@@ -911,15 +954,12 @@ namespace TEAMModelOS.SDK
         public static readonly string error = "error";
         public static readonly string offline = "offline";
         public static readonly string grant_type = "bookjs_api";
-
-
         public static readonly string env_release = "release";
         public static readonly string env_develop = "develop";
-                
-    /// <summary>
-    /// 冗余时间
-    /// </summary>
-    public static readonly long time_excess = 5000;
+        /// <summary>
+        /// 冗余时间
+        /// </summary>
+        public static readonly long time_excess = 5000;
     }
     public enum MessageType {
         conn_success,//连接成功

+ 1 - 1
TEAMModelOS.SDK/Models/Service/SchoolService.cs

@@ -1094,7 +1094,7 @@ namespace TEAMModelOS.SDK
                 string sql = $"SELECT value c  FROM c where  (c.graduate = 0 or  IS_DEFINED(c.graduate) = false) ";
                 if (periodIds.IsNotEmpty())
                 {
-                    sql = $"SELECT value c  FROM c where c.periodId in ({string.Join(",", periodIds.Select(x => $"'{x}'"))}) ";
+                    sql = $"SELECT value c  FROM c where    c.periodId in ({string.Join(",", periodIds.Select(x => $"'{x}'"))}) ";
                 }
                 await foreach (var item in client.GetContainer(Constant.TEAMModelOS, "School").GetItemQueryIteratorSql<Class>
                      //(queryText: $"SELECT value c  FROM c where (c.graduate = 0 or  IS_DEFINED(c.graduate) = false)",

+ 248 - 209
TEAMModelOS.SDK/Models/Service/StudentService.cs

@@ -1152,12 +1152,16 @@ namespace TEAMModelOS.SDK
                 //這邊整理出不存在的教室,之後創建新教室用(比對classNo)。
                 //var nonexistentClassNo = classNos.Except(classInfos.Select(o => o.Key).ToList());
                 List<string> exsitkey = new List<string>();
-                foreach (var classInfo in classInfos)
+                if (classInfos!=null)
                 {
-                    //$"{studentInfo.periodId}_{year}_{tmpClassNo.GetString()}"
-                    var key = $"{classInfo.Value.periodId}_{classInfo.Value.year}_{classInfo.Value.no}";
-                    exsitkey.Add(key);
+                    foreach (var classInfo in classInfos)
+                    {
+                        //$"{studentInfo.periodId}_{year}_{tmpClassNo.GetString()}"
+                        var key = $"{classInfo.Value.periodId}_{classInfo.Value.year}_{classInfo.Value.no}";
+                        exsitkey.Add(key);
+                    }
                 }
+               
                 List<KeyValuePair<string, (string className, string periodId, int year, string no)>> nonexistentClassNo = new List<KeyValuePair<string, (string className, string periodId, int year, string no)>>();
                 foreach (var key in sortedImpData.classInfo.Keys)
                 {
@@ -1753,6 +1757,7 @@ namespace TEAMModelOS.SDK
                 writer.WriteString("style", "smart");
                 writer.WriteString("openType", "1");
                 writer.WriteString("scope", "school");
+                writer.WriteNumber("graduate", 0);
                 writer.WriteEndObject();
                 writer.Flush();
 
@@ -1931,11 +1936,7 @@ namespace TEAMModelOS.SDK
             {
                 //TODO : 進階查詢選項調整、部分地方可用並行處理
                 //以學校學生角度去抓資料
-                Dictionary<string, List<(string id, string name, string picture, int year, string no, string periodId, string irs, string imei, string gender, int graduate, bool hasEduOpenId, List<StudentGuardian> guardians)>> dicClassStuds =
-                    new Dictionary<string, List<(string id, string name, string picture, int year, string no, string periodId, string irs, string imei, string gender, int graduate, bool hasEduOpenId, List<StudentGuardian> guardians)>>();
-                List<(string id, string name, string picture, int year, string no, string periodId, string irs, string imei, string gender, int graduate, bool hasEduOpenId, List<StudentGuardian> guardians)> notJoinClassStuds =
-                    new List<(string id, string name, string picture, int year, string no, string periodId, string irs, string imei, string gender, int graduate, bool hasEduOpenId, List<StudentGuardian> guardians)>();
-
+              
 
                 List<Imei> imeis = new List<Imei>();
                 string imeiQueryText = $"SELECT  *  FROM c WHERE c.school = '{schoolId}'";
@@ -1946,16 +1947,59 @@ namespace TEAMModelOS.SDK
                 }
                 List<Student> graduate_students = new List<Student>();
                 List<Class> school_classes = new List<Class>();
+                List<object> ret = new List<object>();
                 string queryText = "";
                 if (graduate == 0)
                 {
-                    queryText = $"SELECT  *  FROM c WHERE c.code = 'Base-{schoolId}' and (c.graduate = 0 or IS_DEFINED(c.graduate) = false )";
+                    ///查询当年的毕业班
+                    string sql = sql = $"SELECT value c FROM c where  (c.graduate =0 or IS_DEFINED(c.graduate)=false)"; ;
+
+                    var client = _azureCosmos.GetCosmosClient();
+                    await foreach (var item in client.GetContainer(Constant.TEAMModelOS, "School").GetItemQueryIteratorSql<Class>
+                    (queryText: sql,
+                       requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Class-{schoolId}") }))
+                    {
+                        school_classes.Add(item);
+                    }
+
+                    queryText = $"SELECT  value c  FROM c WHERE c.code = 'Base-{schoolId}' and  (c.classId =null or IS_DEFINED(c.classId) = false or c.classId in ( {string.Join(",", school_classes.Select(x => $"'{x.id}'"))}))";
+                    string continuationToken = string.Empty;
+                    var container = _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, "Student");
+                    var studentsRes = await container.GetList<Student>(queryText, $"Base-{schoolId}");
+                    if (studentsRes.list.IsNotEmpty())
+                    {
+                        foreach (var student in studentsRes.list)
+                        {
+                            var imeiObj = imeis.Find(x => x.stuid.Equals(student.id));
+                            Class stuClass = school_classes.Find(x =>!string.IsNullOrWhiteSpace(student.classId) &&  x.id.Equals(student.classId));
+                            ret.Add(new
+                            {
+                                student.id,
+                                student.name,
+                                student.picture,
+                                student.year,
+                                student.no,
+                                classId = stuClass?.id,
+                                classNo = stuClass?.no,
+                                className = stuClass?.name,
+                                gradeId = stuClass?.gradeId,
+                                student.periodId,
+                                classYear = stuClass?.year,
+                                irs = student.irs,
+                                imei = imeiObj?.id,
+                                gender = student.gender,
+                                hasEduOpenId = (!string.IsNullOrWhiteSpace(student.openId)) ? true : false,
+                                guardians = student.guardians,
+                                graduate = !string.IsNullOrWhiteSpace(student.classId) ? 0:student.graduate
+                            });
+
+                        }
+                    }
                 }
                 else
                 {
                     queryText = $"SELECT  *  FROM c WHERE c.code = 'Base-{schoolId}' and  c.graduate = 1 and c.year ={inyear} ";
 
-
                     ///查询当年的毕业班
                     string sql = sql = $"SELECT value c FROM c where  c.graduate =1 and c.year ={inyear}"; ;
 
@@ -1969,215 +2013,209 @@ namespace TEAMModelOS.SDK
                     //如果已经毕业的班级不为空。
                     if (school_classes.IsNotEmpty())
                     {
-                        string classSql = $"SELECT  value c    FROM c WHERE c.code = 'Base-{schoolId}' and  c.graduate = 1  and c.classId in(   {string.Join(",", school_classes.Select(x => $"'{x.id}'"))}  )";
+                        string classSql = $"SELECT  value c    FROM c WHERE c.code = 'Base-{schoolId}' and    c.classId in(   {string.Join(",", school_classes.Select(x => $"'{x.id}'"))}  )";
                         await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, "Student").GetItemQueryIteratorSql<Student>(queryText: classSql,
                         requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Base-{schoolId}") }))
                         {
                             graduate_students.Add(item);
                         }
                     }
-                }
-
-
-                //回傳用ContinuationToken
-                string continuationToken = string.Empty;
-                var container = _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, "Student");
-
-                //進行學生資料的查詢 TEAMModelOS-Student
-                await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, "Student")
-                   .GetItemQueryStreamIteratorSql(
-                       queryText: queryText,
-                       requestOptions: new QueryRequestOptions()
-                       { PartitionKey = new PartitionKey($"Base-{schoolId}"), MaxItemCount = -1 }))
-                {
-                    continuationToken = item.ContinuationToken;
-                    using var json = await JsonDocument.ParseAsync(item.Content);
-                    if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
+                    graduate_students.ForEach(o =>
                     {
-                        List<(string id, string name, string picture, int year, string no, List<StudentGuardian> guardians)> students = new List<(string id, string name, string picture, int year, string no, List<StudentGuardian> guardians)>();
-
-                        var accounts = json.RootElement.GetProperty("Documents").EnumerateArray();
-                        while (accounts.MoveNext())
+                        var imeiObj = imeis.Find(x => x.stuid.Equals(o.id));
+                        Class stuClass = school_classes.Find(x => x.id.Equals(o.classId));
+                        ret.Add(
+                        new
                         {
-                            JsonElement acc = accounts.Current;
-
-                            string classId = acc.GetProperty("classId").GetString();
-                            acc.TryGetProperty("irs", out JsonElement irs);
-                            acc.TryGetProperty("guardians", out JsonElement _guardians);
-                            List<StudentGuardian> guardians = new List<StudentGuardian>();
-
-                            if (_guardians.ValueKind.Equals(JsonValueKind.Array))
-                            {
-                                guardians = _guardians.Deserialize<List<StudentGuardian>>();
-                                if (guardians.Any())
-                                {
-                                    guardians = guardians.FindAll(x => !string.IsNullOrWhiteSpace(x.mobile));
-                                }
-                            }
-
-                            var imeiObj = imeis.Find(x => x.stuid.Equals(acc.GetProperty("id").GetString()));
-                            if (string.IsNullOrWhiteSpace(classId))
-                            {
-                                notJoinClassStuds.Add(
-                                        (
-                                            acc.GetProperty("id").GetString(),
-                                            acc.GetProperty("name").GetString(),
-                                            acc.GetProperty("picture").GetString(),
-                                            acc.GetProperty("year").GetInt32(),
-                                            acc.GetProperty("no").GetString(),
-                                            acc.TryGetProperty("periodId", out JsonElement _periodId) && _periodId.ValueKind.Equals(JsonValueKind.String) ? _periodId.GetString() : null,
-                                            $"{irs}",
-                                            imeiObj?.id,//imei
-                                            acc.TryGetProperty("gender", out JsonElement _gender) && _gender.ValueKind.Equals(JsonValueKind.String) ? _gender.GetString() : null,
-                                            acc.TryGetProperty("graduate", out JsonElement _graduate) && _graduate.ValueKind.Equals(JsonValueKind.Number) ? int.Parse($"{_graduate}") : 0,
-                                            acc.TryGetProperty("openId", out JsonElement _openId) && !string.IsNullOrWhiteSpace(_openId.GetString()) ? true : false,
-                                            guardians
-                                        )
-                                    );
-                            }
-                            else
-                            {
-                                if (dicClassStuds.ContainsKey(classId))
-                                {
-                                    dicClassStuds[classId].Add(
-                                            (
-                                                acc.GetProperty("id").GetString(),
-                                                acc.GetProperty("name").GetString(),
-                                                acc.GetProperty("picture").GetString(),
-                                                acc.GetProperty("year").GetInt32(),
-                                                acc.GetProperty("no").GetString(), acc.TryGetProperty("periodId", out JsonElement _periodId) && _periodId.ValueKind.Equals(JsonValueKind.String) ? _periodId.GetString() : null,
-                                                $"{irs}",
-                                                imeiObj?.id,//imei
-                                                acc.TryGetProperty("gender", out JsonElement _gender) && _gender.ValueKind.Equals(JsonValueKind.String) ? _gender.GetString() : null,
-                                                acc.TryGetProperty("graduate", out JsonElement _graduate) && _graduate.ValueKind.Equals(JsonValueKind.Number) ? int.Parse($"{_graduate}") : 0,
-                                                acc.TryGetProperty("openId", out JsonElement _openId) && !string.IsNullOrWhiteSpace(_openId.GetString()) ? true : false,
-                                                guardians
-                                            )
-                                        );
-                                }
-                                else
-                                {
-                                    dicClassStuds.Add(classId,
-                                            new List<(string id, string name, string picture, int year, string no, string periodId, string irs, string imei, string gender, int graduate, bool hasEduOpenId, List<StudentGuardian> guardians)>()
-                                            {
-                                                (
-                                                    acc.GetProperty("id").GetString(),
-                                                    acc.GetProperty("name").GetString(),
-                                                    acc.GetProperty("picture").GetString(),
-                                                    acc.GetProperty("year").GetInt32(),
-                                                    acc.GetProperty("no").GetString(),  acc.TryGetProperty("periodId",out JsonElement _periodId)&&  _periodId.ValueKind.Equals(JsonValueKind.String) ? _periodId.GetString() : null,
-                                                    $"{irs}"  ,
-                                                    imeiObj?.id,//imei
-                                                    acc.TryGetProperty("gender", out JsonElement _gender) && _gender.ValueKind.Equals(JsonValueKind.String) ? _gender.GetString() : null,
-                                                    acc.TryGetProperty("graduate", out JsonElement _graduate) && _graduate.ValueKind.Equals(JsonValueKind.Number) ? int.Parse($"{_graduate}"): 0,
-                                                    acc.TryGetProperty("openId", out JsonElement _openId) && !string.IsNullOrWhiteSpace(_openId.GetString()) ? true : false,
-                                                    guardians
-                                                )
-                                            }
-                                        );
-                                }
-                            }
-                        }
-                    }
+                            o.id,
+                            o.name,
+                            o.picture,
+                            o.year,
+                            o.no,
+                            classId = stuClass?.id,
+                            classNo = stuClass?.no,
+                            className = stuClass?.name,
+                            gradeId = stuClass?.gradeId,
+                            o.periodId,
+                            classYear = stuClass?.year,
+                            irs = o.irs,
+                            imei = imeiObj?.id,
+                            gender = o.gender,
+                            hasEduOpenId = (!string.IsNullOrWhiteSpace(o.openId)) ? true : false,
+                            guardians = o.guardians,
+                            graduate=1
+                        });
+                    });
                 }
 
-                //查學生所屬的教室及座號
-                List<object> ret = new List<object>();
+                //await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, "Student")
+                //   .GetItemQueryStreamIteratorSql(
+                //       queryText: queryText,
+                //       requestOptions: new QueryRequestOptions()
+                //       { PartitionKey = new PartitionKey($"Base-{schoolId}"), MaxItemCount = -1 }))
+                //{
+                //    continuationToken = item.ContinuationToken;
+                //    using var json = await JsonDocument.ParseAsync(item.Content);
+                //    if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
+                //    {
+
+                //        var accounts = json.RootElement.GetProperty("Documents").EnumerateArray();
+                //        while (accounts.MoveNext())
+                //        {
+                //            JsonElement acc = accounts.Current;
+
+                //            string classId = acc.GetProperty("classId").GetString();
+                //            acc.TryGetProperty("irs", out JsonElement irs);
+                //            acc.TryGetProperty("guardians", out JsonElement _guardians);
+                //            List<StudentGuardian> guardians = new List<StudentGuardian>();
+
+                //            if (_guardians.ValueKind.Equals(JsonValueKind.Array))
+                //            {
+                //                guardians = _guardians.Deserialize<List<StudentGuardian>>();
+                //                if (guardians.Any())
+                //                {
+                //                    guardians = guardians.FindAll(x => !string.IsNullOrWhiteSpace(x.mobile));
+                //                }
+                //            }
+
+                //            var imeiObj = imeis.Find(x => x.stuid.Equals(acc.GetProperty("id").GetString()));
+                //            if (string.IsNullOrWhiteSpace(classId))
+                //            {
+                //                notJoinClassStuds.Add(
+                //                        (
+                //                            acc.GetProperty("id").GetString(),
+                //                            acc.GetProperty("name").GetString(),
+                //                            acc.GetProperty("picture").GetString(),
+                //                            acc.GetProperty("year").GetInt32(),
+                //                            acc.GetProperty("no").GetString(),
+                //                            acc.TryGetProperty("periodId", out JsonElement _periodId) && _periodId.ValueKind.Equals(JsonValueKind.String) ? _periodId.GetString() : null,
+                //                            $"{irs}",
+                //                            imeiObj?.id,//imei
+                //                            acc.TryGetProperty("gender", out JsonElement _gender) && _gender.ValueKind.Equals(JsonValueKind.String) ? _gender.GetString() : null,
+                //                            acc.TryGetProperty("graduate", out JsonElement _graduate) && _graduate.ValueKind.Equals(JsonValueKind.Number) ? int.Parse($"{_graduate}") : 0,
+                //                            acc.TryGetProperty("openId", out JsonElement _openId) && !string.IsNullOrWhiteSpace(_openId.GetString()) ? true : false,
+                //                            guardians
+                //                        )
+                //                    );
+                //            }
+                //            else
+                //            {
+                //                if (dicClassStuds.ContainsKey(classId))
+                //                {
+                //                    dicClassStuds[classId].Add(
+                //                            (
+                //                                acc.GetProperty("id").GetString(),
+                //                                acc.GetProperty("name").GetString(),
+                //                                acc.GetProperty("picture").GetString(),
+                //                                acc.GetProperty("year").GetInt32(),
+                //                                acc.GetProperty("no").GetString(), acc.TryGetProperty("periodId", out JsonElement _periodId) && _periodId.ValueKind.Equals(JsonValueKind.String) ? _periodId.GetString() : null,
+                //                                $"{irs}",
+                //                                imeiObj?.id,//imei
+                //                                acc.TryGetProperty("gender", out JsonElement _gender) && _gender.ValueKind.Equals(JsonValueKind.String) ? _gender.GetString() : null,
+                //                                acc.TryGetProperty("graduate", out JsonElement _graduate) && _graduate.ValueKind.Equals(JsonValueKind.Number) ? int.Parse($"{_graduate}") : 0,
+                //                                acc.TryGetProperty("openId", out JsonElement _openId) && !string.IsNullOrWhiteSpace(_openId.GetString()) ? true : false,
+                //                                guardians
+                //                            )
+                //                        );
+                //                }
+                //                else
+                //                {
+                //                    dicClassStuds.Add(classId,
+                //                            new List<(string id, string name, string picture, int year, string no, string periodId, string irs, string imei, string gender, int graduate, bool hasEduOpenId, List<StudentGuardian> guardians)>()
+                //                            {
+                //                                (
+                //                                    acc.GetProperty("id").GetString(),
+                //                                    acc.GetProperty("name").GetString(),
+                //                                    acc.GetProperty("picture").GetString(),
+                //                                    acc.GetProperty("year").GetInt32(),
+                //                                    acc.GetProperty("no").GetString(),  acc.TryGetProperty("periodId",out JsonElement _periodId)&&  _periodId.ValueKind.Equals(JsonValueKind.String) ? _periodId.GetString() : null,
+                //                                    $"{irs}"  ,
+                //                                    imeiObj?.id,//imei
+                //                                    acc.TryGetProperty("gender", out JsonElement _gender) && _gender.ValueKind.Equals(JsonValueKind.String) ? _gender.GetString() : null,
+                //                                    acc.TryGetProperty("graduate", out JsonElement _graduate) && _graduate.ValueKind.Equals(JsonValueKind.Number) ? int.Parse($"{_graduate}"): 0,
+                //                                    acc.TryGetProperty("openId", out JsonElement _openId) && !string.IsNullOrWhiteSpace(_openId.GetString()) ? true : false,
+                //                                    guardians
+                //                                )
+                //                            }
+                //                        );
+                //                }
+                //            }
+                //        }
+                //    }
+                //}
+
+              
 
                 //查教室的資訊,用以取得gradeId,periodId資訊。
-                var classInfos = await getClassInfoUseId(_azureCosmos, _dingDing, _option, schoolId, dicClassStuds.Keys.ToList());
+              //  var classInfos = await getClassInfoUseId(_azureCosmos, _dingDing, _option, schoolId, dicClassStuds.Keys.ToList());
 
                 //輪循所有教室學生的資料
-                foreach (var classStud in dicClassStuds)
-                {
-                    string classId = null, classNo = null, className = null, gradeId = null, periodId = null;
-                    int classYear = -1;
-                    if (classInfos.ContainsKey(classStud.Key))
-                    {
-                        classId = classInfos[classStud.Key].GetProperty("id").GetString();
-                        classNo = classInfos[classStud.Key].GetProperty("no").GetString();
-                        className = classInfos[classStud.Key].GetProperty("name").GetString();
-                        periodId = classInfos[classStud.Key].TryGetProperty("periodId", out JsonElement _periodId) && _periodId.ValueKind.Equals(JsonValueKind.String) ? _periodId.GetString() : null;
-                        if (classInfos[classStud.Key].TryGetProperty("year", out JsonElement year))
-                        {
-                            if (year.ValueKind.Equals(JsonValueKind.Number))
-                            {
-                                classYear = classInfos[classStud.Key].GetProperty("year").GetInt32();
-                            }
-                        }
-                    }
-
-                    var tmp = classStud.Value.Select(o =>
-                                     new
-                                     {
-                                         o.id,
-                                         o.name,
-                                         o.picture,
-                                         o.year,
-                                         o.no,
-                                         classId,
-                                         classNo,
-                                         className,
-                                         gradeId,
-                                         periodId = string.IsNullOrEmpty(periodId) ? o.periodId : periodId,
-                                         classYear,
-                                         irs = o.irs,
-                                         imei = o.imei,
-                                         gender = o.gender,
-                                         graduate = o.graduate,
-                                         hasEduOpenId = o.hasEduOpenId,
-                                         guardians = o.guardians,
-                                     });
-                    ret.AddRange(tmp);
-                }
-
-                //彙整沒有加入教室的學生
-                notJoinClassStuds.ForEach(o => ret.Add(
-                    new
-                    {
-                        o.id,
-                        o.name,
-                        o.picture,
-                        o.year,
-                        o.no,
-                        classId = (string)null,
-                        classNo = (string)null,
-                        className = (string)null,
-                        gradeId = (string)null,
-                        o.periodId,
-                        classYear = -1,
-                        irs = o.irs,
-                        imei = o.imei,
-                        gender = o.gender,
-                        hasEduOpenId = o.hasEduOpenId,
-                        guardians = o.guardians,
-                    }));
-
-                graduate_students.ForEach(o =>
-                {
-                    var imeiObj = imeis.Find(x => x.stuid.Equals(o.id));
-                    Class stuClass = school_classes.Find(x => x.id.Equals(o.classId));
-                    ret.Add(
-                    new
-                    {
-                        o.id,
-                        o.name,
-                        o.picture,
-                        o.year,
-                        o.no,
-                        classId = stuClass?.id,
-                        classNo = stuClass?.no,
-                        className = stuClass?.name,
-                        gradeId = stuClass?.gradeId,
-                        o.periodId,
-                        classYear = stuClass?.year,
-                        irs = o.irs,
-                        imei = imeiObj?.id,
-                        gender = o.gender,
-                        hasEduOpenId = (!string.IsNullOrWhiteSpace(o.openId)) ? true : false,
-                        guardians = o.guardians,
-                    });
-                });
+                //foreach (var classStud in dicClassStuds)
+                //{
+                //    string classId = null, classNo = null, className = null, gradeId = null, periodId = null;
+                //    int classYear = -1;
+                //    if (classInfos.ContainsKey(classStud.Key))
+                //    {
+                //        classId = classInfos[classStud.Key].GetProperty("id").GetString();
+                //        classNo = classInfos[classStud.Key].GetProperty("no").GetString();
+                //        className = classInfos[classStud.Key].GetProperty("name").GetString();
+                //        periodId = classInfos[classStud.Key].TryGetProperty("periodId", out JsonElement _periodId) && _periodId.ValueKind.Equals(JsonValueKind.String) ? _periodId.GetString() : null;
+                //        if (classInfos[classStud.Key].TryGetProperty("year", out JsonElement year))
+                //        {
+                //            if (year.ValueKind.Equals(JsonValueKind.Number))
+                //            {
+                //                classYear = classInfos[classStud.Key].GetProperty("year").GetInt32();
+                //            }
+                //        }
+                //    }
+
+                //    var tmp = classStud.Value.Select(o =>
+                //                     new
+                //                     {
+                //                         o.id,
+                //                         o.name,
+                //                         o.picture,
+                //                         o.year,
+                //                         o.no,
+                //                         classId,
+                //                         classNo,
+                //                         className,
+                //                         gradeId,
+                //                         periodId = string.IsNullOrEmpty(periodId) ? o.periodId : periodId,
+                //                         classYear,
+                //                         irs = o.irs,
+                //                         imei = o.imei,
+                //                         gender = o.gender,
+                //                         graduate = o.graduate,
+                //                         hasEduOpenId = o.hasEduOpenId,
+                //                         guardians = o.guardians,
+                //                     });
+                //    ret.AddRange(tmp);
+                //}
+
+                ////彙整沒有加入教室的學生
+                //notJoinClassStuds.ForEach(o => ret.Add(
+                //    new
+                //    {
+                //        o.id,
+                //        o.name,
+                //        o.picture,
+                //        o.year,
+                //        o.no,
+                //        classId = (string)null,
+                //        classNo = (string)null,
+                //        className = (string)null,
+                //        gradeId = (string)null,
+                //        o.periodId,
+                //        classYear = -1,
+                //        irs = o.irs,
+                //        imei = o.imei,
+                //        gender = o.gender,
+                //        hasEduOpenId = o.hasEduOpenId,
+                //        guardians = o.guardians,
+                //    }));
+
+                
                 return ret;
             }
             catch (Exception ex)
@@ -2470,9 +2508,10 @@ namespace TEAMModelOS.SDK
         /// <returns></returns>
         private static async Task<Dictionary<string, Class>> getClassInfoUseNo(AzureCosmosFactory _azureCosmos, DingDing _dingDing, Option _option, string schoolId, Dictionary<string, (string className, string periodId, int year, string no)> classNos)
         {
+            Dictionary<string, Class> dicClassInfo = new Dictionary<string, Class>();
             try
             {
-                Dictionary<string, Class> dicClassInfo = new Dictionary<string, Class>();
+               
                 if (!(classNos == null || classNos.Count == 0))
                 {
 
@@ -2489,7 +2528,7 @@ namespace TEAMModelOS.SDK
                     }
                     return dicClassInfo;
                 }
-                else return null;
+                else return dicClassInfo;
 
             }
             catch (CosmosException ex)
@@ -2500,7 +2539,7 @@ namespace TEAMModelOS.SDK
             {
                 await _dingDing.SendBotMsg($"IES5,{_option.Location},StudentController/getClassInfoUseId()\n{ex.Message}\n{ex.StackTrace}", GroupNames.醍摩豆服務運維群組);
             }
-            return null;
+            return dicClassInfo;
         }
 
         /// <summary>

+ 3 - 3
TEAMModelOS.SDK/TEAMModelOS.SDK.csproj

@@ -1,9 +1,9 @@
 <Project Sdk="Microsoft.NET.Sdk">
 	<PropertyGroup>
 		<TargetFramework>net8.0</TargetFramework>
-		<Version>5.2411.27</Version>
-		<AssemblyVersion>5.2411.27.1</AssemblyVersion>
-		<FileVersion>5.2411.27.1</FileVersion>
+		<Version>5.2412.4</Version>
+		<AssemblyVersion>5.2412.4.1</AssemblyVersion>
+		<FileVersion>5.2412.4.1</FileVersion>
 		<PackageReleaseNotes>发版</PackageReleaseNotes>
 	</PropertyGroup>
 

+ 8 - 1
TEAMModelOS.sln

@@ -27,7 +27,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HTEX.ScreenClient", "TEAMMo
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HTEX.Test", "TEAMModelOS.Extension\HTEX.Test\HTEX.Test.csproj", "{0AB3F94D-9698-4DB1-B532-5E6C8E56F770}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HTEX.DataETL", "HTEX.DataETL\HTEX.DataETL.csproj", "{1F30D9C2-3E52-4E7C-AE46-8C13916153D2}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HTEX.DataETL", "HTEX.DataETL\HTEX.DataETL.csproj", "{1F30D9C2-3E52-4E7C-AE46-8C13916153D2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IESHybridCloud", "IESHybridCloud\IESHybridCloud.csproj", "{A76FCB84-C9FF-46C6-8B82-FC7538779A98}"
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -85,6 +87,10 @@ Global
 		{1F30D9C2-3E52-4E7C-AE46-8C13916153D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{1F30D9C2-3E52-4E7C-AE46-8C13916153D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{1F30D9C2-3E52-4E7C-AE46-8C13916153D2}.Release|Any CPU.Build.0 = Release|Any CPU
+		{A76FCB84-C9FF-46C6-8B82-FC7538779A98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{A76FCB84-C9FF-46C6-8B82-FC7538779A98}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{A76FCB84-C9FF-46C6-8B82-FC7538779A98}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{A76FCB84-C9FF-46C6-8B82-FC7538779A98}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -98,6 +104,7 @@ Global
 		{BB3DD2CC-CAFA-4DE9-97FA-866465A274F1} = {9B74B53F-20E8-46CC-903B-62AEB1583DD7}
 		{0AB3F94D-9698-4DB1-B532-5E6C8E56F770} = {9B74B53F-20E8-46CC-903B-62AEB1583DD7}
 		{1F30D9C2-3E52-4E7C-AE46-8C13916153D2} = {9B74B53F-20E8-46CC-903B-62AEB1583DD7}
+		{A76FCB84-C9FF-46C6-8B82-FC7538779A98} = {9B74B53F-20E8-46CC-903B-62AEB1583DD7}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {76440725-5E50-4288-851F-BA5C0BC8E8C6}

+ 2 - 0
TEAMModelOS/ClientApp/public/lang/en-US.js

@@ -2799,6 +2799,7 @@ const LANG_EN_US = {
         delOldPoint2: 'After deleting old key concepts and categories, they cannot be retrieved. Do you confirm to delete?',
         delOldPoint3: 'Deleted successfully',
         delOldPoint4: 'Deleted failed',
+        preFormat: 'Display style',
     },
     // 活动模块
     learnActivity: {
@@ -5901,6 +5902,7 @@ const LANG_EN_US = {
         chooseVolumeTip: 'Please select an existing book or create a new one!',
         playFailTip: 'Resource temporarily does not support playing',
         shareSyllabus: 'Share Personal Syllabus',
+        shareSyllabusSchool: 'Share school syllabus',
         chooseShareChapter: 'Select a chapter to share',
         allShare: 'Share the whole book',
         chooseTeacher: 'Choose the teachers to share with',

+ 2 - 0
TEAMModelOS/ClientApp/public/lang/zh-CN.js

@@ -2798,6 +2798,7 @@ const LANG_ZH_CN = {
         delOldPoint2: '清除旧知识点和旧知识块后将无法找回,是否确认清除?',
         delOldPoint3: '清除成功',
         delOldPoint4: '清除失败',
+        preFormat: '展现方式',
     },
     // 活动模块
     learnActivity: {
@@ -5899,6 +5900,7 @@ const LANG_ZH_CN = {
         chooseVolumeTip: '请选择已创建的册别或者新建册别!',
         playFailTip: '资源暂不支持播放',
         shareSyllabus: '分享个人课纲',
+        shareSyllabusSchool: '分享学校课纲',
         chooseShareChapter: '选择分享的章节',
         allShare: '整册分享',
         chooseTeacher: '选择分享的教师',

+ 2 - 0
TEAMModelOS/ClientApp/public/lang/zh-TW.js

@@ -2801,6 +2801,7 @@ const LANG_ZH_TW = {
         delOldPoint2: '清除舊知識點和知識塊後將無法找回,是否確認清除? ',
         delOldPoint3: '清除成功',
         delOldPoint4: '清除失敗',
+        preFormat: '展現方式',
     },
     // 活动模块
     learnActivity: {
@@ -5901,6 +5902,7 @@ const LANG_ZH_TW = {
         chooseVolumeTip: '請選擇已新增的冊別或者新增冊別!',
         playFailTip: '資源暫不支援播放',
         shareSyllabus: '分享個人課綱',
+        shareSyllabusSchool: '分享學校課綱',
         chooseShareChapter: '選擇分享的章節',
         allShare: '整冊分享',
         chooseTeacher: '選擇分享的教師',

+ 103 - 3
TEAMModelOS/ClientApp/src/common/BaseQuickPaper.vue

@@ -33,6 +33,7 @@
 				<Upload action="" style="display: inline-block" :format="['jpg', 'jpeg', 'png', 'pdf']" :max-size="2048" multiple :before-upload="handleBeforeUpload" :show-upload-list="false">
 					<span class="btn-upload">{{ $t("evaluation.quickPaper.upload") }}</span>
 				</Upload>
+				<span @click="screenshotChange()" style="cursor: pointer" v-if="buildMode === 'order' && attachments.length">切图</span>
 			</p>
 			<div class="img-list">
 				<div v-if="!attachments.length" style="margin: 20px 30px; color: #9999">{{ $t("evaluation.quickPaper.empty") }}</div>
@@ -166,11 +167,29 @@
 						<Input v-if="item.type !== 'subjective' || item.answerType === 'audio'" v-model="item.answer" :type="(item.type === 'single' || item.type === 'multiple') && answerInputMode === 'number' ? 'number' : 'text'" @mousewheel.native.prevent @on-change="removeChinese($event, index, item.type)" @on-keydown="onOrderInputRule($event, item, index)" :placeholder="$t('evaluation.quickPaper.tip6')" style="width: 220px; margin: 0 10px" />
 						<span style="margin: 0 10px">{{ $t("syllabus.score") }}</span>
 						<InputNumber v-model="item.score" :min="1" :max="100"></InputNumber>
+						<img :src="item.img" alt="" style="width: 100px;" @click="$hevueImgPreview(item.img)" v-show="item.img">
 						<span style="margin-left: 10px; cursor: pointer" @click="doRemoveItem(index)"><Icon type="md-trash" color="#FA8C16" size="18" /></span>
 					</div>
 				</div>
 			</div>
 		</div>
+		<Modal v-model="isScreenshot" :mask-closable="false" title="框图取题" width="800" @on-ok="saveMark()" @on-cancel="closeMark()">
+			<div>
+				<Select v-model="screenType" :placeholder="$t('evaluation.newExercise.gradePlaceholder')" style="width: 250px;">
+					<Option v-for="(type, typeIndex) in Object.keys(typeMap)" :value="type" :key="typeIndex">
+						{{ typeMap[type] }}
+					</Option>
+				</Select>
+				<Button type="primary" style="margin-left: 15px;" @click="changImg('up')" v-show="markIndex">上一页</Button>
+				<Button type="primary" style="margin-left: 15px;" @click="changImg('down')" v-show="markIndex + 1 != attachments.length">下一页</Button>
+				<Button type="primary" style="margin-left: 15px;" @click="delMark()">撤销</Button>
+			</div>
+			<div style="margin-top: 10px">
+			</div>
+			<div style="margin-top: 10px;" v-if="isScreenshot">
+				<MarkCanvas ref="markCanvasRef" :markIndex="markIndex" :list="markList" :markImgUrl="markImgUrl" :quesNum="orderItemsArr.length"></MarkCanvas>
+			</div>
+		</Modal>
 	</div>
 </template>
 
@@ -227,7 +246,12 @@
 				subjectList: [],
 				orderItemsArr: [],
 				paperInfo: {},
-				oldPaperName: ""
+				oldPaperName: "",
+				isScreenshot: false,
+				markListOld: [],
+				markList: [],
+				screenType: 'single',
+				markIndex: 0,
 			};
 		},
 		created() {
@@ -284,6 +308,8 @@
 						}
 						Promise.all(promiseArr).then(result => {
 							that.attachments.push(...result);
+							let arr = Array(result.length).fill([])
+							that.markListOld.push(...arr)
 							that.nowFileCount += result.length;
 						})
 					});
@@ -385,6 +411,17 @@
 			/* 移除试题 */
 			doRemoveItem(index) {
 				this.orderItemsArr.splice(index, 1);
+				// 被移除的试题需从切图数组(markListOld对应图片数组下)中删除,并更新切图传的qIndex
+				let markIndex = this.markListOld.findIndex(item => item.find(img => img.qIndex === index))
+				if(markIndex !== -1) {
+					let imgIndex = this.markListOld[markIndex].findIndex(img => img.qIndex === index)
+					this.markListOld[markIndex].splice(imgIndex, 1)
+					this.markListOld.forEach((item, mIndex) => {
+						item.forEach((img, gIndex) => {
+							if(imgIndex < img.qIndex) img.qIndex = img.qIndex - 1
+						})
+					})
+				}
 			},
 			/* 回显编辑试卷 */
 			async doRenderEditPaper() {
@@ -658,6 +695,7 @@
 					const reader = new FileReader();
 					reader.addEventListener("load", () => {
 						that.attachments.push(reader.result);
+						that.markListOld.push([])
 					});
 					reader.readAsDataURL(img);
 				}
@@ -670,7 +708,18 @@
 			/* 删除附件 */
 			doDelete(imgIndex) {
 				this.attachments.splice(imgIndex, 1);
+				this.markListOld.splice(imgIndex, 1)
+				// 删除图片后,清理从图片中切图产生的题目 暂不放出
+				/* this.orderItemsArr = this.orderItemsArr.filter(item => item.imgIndex !== imgIndex)
+				this.orderItemsArr.forEach(item => {
+					if(item.imgIndex > imgIndex) item.imgIndex -= 1
+				}) */
 				this.nowFileCount--;
+				if(this.attachments.length && this.markIndex === this.attachments.length) {
+					this.markIndex -= 1
+				} else if(!this.attachments.length) {
+					this.markIndex = 0
+				}
 			},
 			/* 修改题目数量 */
 			onItemCountChange(type) {
@@ -1029,7 +1078,7 @@
 							this.$evTools.createBlobItem({
 								id: this.$tools.guid(),
 								type: item.type,
-								question: "No." + (blobItemPromiseArr.length + 1),
+								question: item.img ? `<img src="${item.img}">` : "No." + (blobItemPromiseArr.length + 1),
 								option: this.getItemOptions(item),
 								answer: this.getOrderItemAnswer(item),
 								score: item.score,
@@ -1102,7 +1151,55 @@
 					console.log("最终结果", result);
 					this.$emit("finish");
 				});
-			}
+			},
+			screenshotChange() {
+				this.markList = this._.cloneDeep(this.markListOld[this.markIndex]) || []
+				this.isScreenshot = true
+			},
+			saveMark() {
+				let markList = this.$refs.markCanvasRef.cavList
+				// this.orderItemsArr = []
+				let arr = this.markListOld[this.markIndex].length ? markList.slice(this.markListOld[this.markIndex].length, markList.length) : [...markList]
+				arr.forEach(item => {
+					// 创建一个 canvas 元素来进行裁剪
+					const cvs = document.createElement('canvas')
+					cvs.width = item.w
+      				cvs.height = item.h
+					const ctx = cvs.getContext('2d')
+					// 将裁剪位置转换为原始图像上的坐标
+					const cropX = item.x * this.$refs.markCanvasRef.cutImgScale.xScale
+					const cropY = item.y * this.$refs.markCanvasRef.cutImgScale.yScale
+					const cropW = item.w * this.$refs.markCanvasRef.cutImgScale.xScale
+					const cropH = item.h * this.$refs.markCanvasRef.cutImgScale.yScale
+					// 明天处理截图的位置问题
+					ctx.drawImage(this.$refs.markCanvasRef.$refs.originImageEl, cropX, cropY, cropW, cropH, 0, 0, cvs.width, cvs.height)
+					let url = cvs.toDataURL('image/png')
+					item.qIndex = this.orderItemsArr.length
+					this.orderItemsArr.push({
+						type: this.screenType,
+						options: ["single", "multiple"].includes(this.screenType) ? 4 : 0,
+						answer: "",
+						answerType: "text",
+						answerLang: "zh",
+						score: 10,
+						useAutoScore: false,
+						img: url,
+						imgIndex: this.markIndex
+					})
+				})
+				markList.forEach(item => item.oldQues = true)
+				this.markListOld.splice(this.markIndex, 1, this._.cloneDeep(markList))
+			},
+			closeMark() {
+				// this.markList = [...this.markListOld]
+			},
+			delMark() {
+				this.$refs.markCanvasRef.delDraw(this.$refs.markCanvasRef.checkIndex, true)
+			},
+			changImg(type) {
+				this.markIndex = type === 'up' ? this.markIndex - 1 : this.markIndex + 1
+				this.markList = this._.cloneDeep(this.markListOld[this.markIndex]) || []
+			},
 		},
 		computed: {
 			isSchool() {
@@ -1141,6 +1238,9 @@
 					gradeName: this.gradeCode.length ? this.gradeCode.map((i) => this.gradeList[i]).join(",") : this.$t("evaluation.filter.allGrades")
 				}
 			},
+			markImgUrl() {
+				return this.attachments.length ? this.attachments[this.markIndex] : ''
+			},
 		}
 	};
 </script>

+ 585 - 0
TEAMModelOS/ClientApp/src/common/MarkCanvas.vue

@@ -0,0 +1,585 @@
+<template>
+    <div ref="markImg" id="imgContent" style="position: relative;">
+        <img ref="originImageEl" id="bigImg" :src="markImgUrl" style="margin: auto; width: 100%; height: auto;" />
+        <canvas id="canvas" ref="markCanvasRef" style="position: absolute; top: 0; left: 0;"></canvas>
+    </div>
+        <!-- <canvas id="canvas" :width="width" :height="height" ref="markCanvasRef" style="position: absolute; top: 0; left: 0;"></canvas> -->
+</template>
+
+<script>
+export default {
+    props: {
+        markIndex: {
+            type: Number,
+            default: 0
+        },
+        list: {
+            type: Array,
+            default: () => []
+        },
+        quesNum: {
+            type: Number,
+            default: 0
+        },
+        markImgUrl: {
+            type: String,
+            default: ''
+        },
+    },
+    data() {
+        return {
+            cavList: [],
+            canvas: undefined,
+            contentText: undefined,
+            cutImgScale: {},
+            checkIndex: -1,
+        }
+    },
+    mounted() {
+        this.$nextTick(() => {
+            this.cavList = this._.cloneDeep(this.list)
+            this.canvas = this.$refs.markCanvasRef
+            this.canvas.width = document.getElementById('bigImg').offsetWidth
+            this.canvas.height = document.getElementById('bigImg').offsetHeight
+            this.canvas.style.cursor = 'crosshair'
+            
+            this.contentText = this.canvas.getContext('2d')
+            this.contentText.strokeStyle = 'blue'
+            this.contentText.lineWidth = 4
+            this.contentText.font = '16px Arial'
+            this.draw(undefined, true)
+            this.setImgScale()
+        })
+    },
+    computed: {
+    },
+    watch: {
+        markIndex: {
+            handler(n, o) {
+                this.cavList = [...this.list]
+                this.$nextTick(() => {
+                    this.canvas.width = document.getElementById('bigImg').offsetWidth
+                    this.canvas.height = document.getElementById('bigImg').offsetHeight
+                    this.contentText = this.canvas.getContext('2d')
+                    this.contentText.strokeStyle = 'blue'
+                    this.contentText.lineWidth = 4
+                    this.contentText.font = '16px Arial'
+                    this.setImgScale()
+                    this.reDraw()
+                })
+            },
+        },
+        /* list: {
+            handler(n, o) {
+                this.contentText.clearRect(0, 0, this.canvas.width, this.canvas.height)
+                this.cavList = [...n]
+            }
+        },
+        markImgUrl: {
+            handler(n, o) {
+                this.$nextTick(() => {
+                    this.canvas.width = document.getElementById('bigImg').offsetWidth
+                    this.canvas.height = document.getElementById('bigImg').offsetHeight
+                    this.reDraw()
+                    this.setImgScale()
+                })
+            },
+        } */
+    },
+    methods: {
+        setImgScale() {
+            const displayWidth = this.$refs.originImageEl.clientWidth
+            const displayHeight = this.$refs.originImageEl.clientHeight
+            // 获取图片的原始尺寸
+            const originalWidth = this.$refs.originImageEl.naturalWidth
+            const originalHeight = this.$refs.originImageEl.naturalHeight
+            // 计算缩放比例
+            const xScale = originalWidth / displayWidth
+            const yScale = originalHeight / displayHeight
+            this.cutImgScale = {xScale, yScale}
+        },
+        draw(i, isRepaint) {
+            let that = this
+            /* this.contentText = this.canvas.getContext('2d')
+            this.contentText.strokeStyle = 'blue'
+            this.contentText.lineWidth = 2
+            this.contentText.font = '16px Arial' */
+            // 初始化鼠标坐标
+            let sX = 0 // 鼠标X坐标
+            let sY = 0 // 鼠标Y坐标
+            console.log('draw', i);
+            if(isRepaint) {
+                this.contentText.clearRect(0, 0, this.canvas.width, this.canvas.height)
+                this.reDraw()
+            }
+            // 鼠标移动进行第一层判断, 区分情况: 无矩形, 已有矩形无选中, 已有选中矩形
+            // list中有oldQues,就不能选中移动了
+            this.canvas.onmousemove = (em) => {
+                // if(i && this.cavList[i]?.oldQues) return
+                sX = em.offsetX
+                sY = em.offsetY
+                let iem = undefined //鼠标移动时临时存储当前鼠标所在矩形的下标
+                if (!that.cavList.length) { //无矩形
+                    that.newDraw()
+                } else if (i === undefined) {// 已有矩形无选中
+                    // 判断鼠标位置并绘制
+                    that.cavList.forEach((value, index, array) => {
+                        // 这个条件判断鼠标是否位于一个宽高均为正的矩形框的右下方。如果是,则设置 iem 为当前矩形框的索引,并调用 judgeDraw() 方法进行后续操作
+                        if (value.w > 0 && value.h > 0 && sX > value.x && sX < value.x + value.w && sY > value.y && sY < value.y + value.h) {
+                            // 鼠标在右下方向生成的矩形中
+                            iem = index
+                            that.judgeDraw(iem)
+                        }
+                        if (value.w < 0 && value.h > 0 && sX < value.x && sX > value.x + value.w && sY > value.y && sY < value.y + value.h) {
+                            // 鼠标在左下方向生成的矩形中
+                            iem = index
+                            that.judgeDraw(iem)
+                        }
+                        if (value.w > 0 && value.h < 0 && sX > value.x && sX < value.x + value.w && sY < value.y && sY > value.y + value.h) {
+                            // 鼠标在右上方向生成的矩形中
+                            iem = index
+                            that.judgeDraw(iem)
+                        }
+                        if (value.w < 0 && value.h < 0 && sX < value.x && sX > value.x + value.w && sY < value.y && sY > value.y + value.h) {
+                            // 鼠标在左上方向生成的矩形中
+                            iem = index
+                            that.judgeDraw(iem)
+                        }
+                        // 如果以上所有条件都不满足,则意味着鼠标不在任何一个矩形框内,此时调用 newDraw() 方法。
+                        if (iem === undefined) {
+                            // 鼠标不在矩形中
+                            that.checkIndex = that.cavList.length - 1
+                            that.newDraw()
+                        }
+                    })
+                } else if(!that.cavList[i]?.oldQues) {// 已有选中矩形
+                    for (let index = 0; index < that.cavList.length; index++) {
+                        let value = that.cavList[index]
+                        // 判断鼠标位置并绘制
+                        // 如果鼠标在矩形的四个角上,表示可能需要改变矩形的大小或位置。因此,通过判断鼠标位置,确定鼠标所在的角,并根据角的位置信息调整矩形。
+                        // 如果鼠标在矩形的边上,表示可能需要调整矩形的大小或者移动整个矩形。根据鼠标位置判断是水平边还是垂直边,并确定是左上、右上、左下还是右下角,以便进行相应的操作。
+                        // 如果鼠标在矩形的内部,表示可能需要移动整个矩形。这时,根据鼠标位置确定是哪个矩形,以便移动对应的矩形。
+                        // 如果以上所有条件都不满足,则表示鼠标不在任何矩形中,可能需要绘制新的矩形或者执行其他操作。
+                        if (sX < value.x + 5 && sX > value.x - 5 && sY < value.y + 5 && sY > value.y - 5) {
+                            // ***  鼠标在起点角  ***
+                            if (index === i) {
+                                that.changeDraw(i, 1)
+                                break
+                            }
+                        } else if (sX < value.x + value.w + 5 && sX > value.x + value.w - 5 && sY < value.y + 5 && sY > value.y - 5) {
+                            // ***  鼠标在起点横向角  ***
+                            if (index === i) {
+                                that.changeDraw(i, 2)
+                                break
+                            }
+                        } else if (sX < value.x + 5 && sX > value.x - 5 && sY < value.y + value.h + 5 && sY > value.y + value.h - 5) {
+                            // ***  鼠标在起点纵向角  ***
+                            if (index === i) {
+                                that.changeDraw(i, 3)
+                                break
+                            }
+                        } else if (sX < value.x + value.w + 5 && sX > value.x + value.w - 5 && sY < value.y + value.h + 5 && sY > value.y + value.h - 5) {
+                            // ***  鼠标在终点角  ***
+                            if (index === i) {
+                                that.changeDraw(i, 4)
+                                break
+                            }
+                        } else if (value.w > 0 && value.h > 0 && sX > value.x && sX < value.x + value.w && sY > value.y && sY < value.y + value.h) {
+                            // ***  鼠标在右下方向生成的矩形中  ***
+                            iem = index
+                            that.judgeDraw(index)
+                            break
+                        } else if (value.w < 0 && value.h > 0 && sX < value.x && sX > value.x + value.w && sY > value.y && sY < value.y + value.h) {
+                            // ***  鼠标在左下方向生成的矩形中  ***
+                            iem = index
+                            that.judgeDraw(index)
+                            break
+                        } else if (value.w > 0 && value.h < 0 && sX > value.x && sX < value.x + value.w && sY < value.y && sY > value.y + value.h) {
+                            // ***  鼠标在右上方向生成的矩形中  ***
+                            iem = index
+                            that.judgeDraw(index)
+                            break
+                        } else if (value.w < 0 && value.h < 0 && sX < value.x && sX > value.x + value.w && sY < value.y && sY > value.y + value.h) {
+                            // ***  鼠标在左上方向生成的矩形中  ***
+                            iem = index
+                            that.judgeDraw(index)
+                            break
+                        } else {
+                            if (iem === undefined) {
+                                // *** 鼠标不在矩形中 ***
+                                that.newDraw()
+                            }
+                        }
+                    }
+                }
+                this.canvas.onmouseout = (eo) => {
+                    if (i) {
+                        that.draw(i)
+                    }
+                }
+            }
+            // return this.cavList
+        },
+        changeDraw(i, site) {
+            let that = this
+            // 将鼠标样式改变为指针形状
+            this.canvas.style.cursor = 'pointer'
+            console.log('编辑矩形四个角', i);
+
+            // site: 操作矩形角的位置, 1-起点 2-起点横向 3-起点纵向 4-终点
+            let mark = this.cavList[i];
+
+            /* 按下鼠标左键 */
+            this.canvas.onmousedown = function (ed) {
+                // console.log('按下鼠标左键');
+                // 保存鼠标落下位置的X, Y坐标, firefox中鼠标移动后ed.offsetX ed.offsetY会变成 0, 需要使用临时参数存储起来
+                let sX = ed.offsetX; // 起点X坐标
+                let sY = ed.offsetY; // 起点Y坐标
+
+                /* 移动鼠标 */
+                that.canvas.onmousemove = function (em) {
+                    // 计算绘制数据
+                    let iframe = {}
+                    // 判断矩形变化的类型,site 可能的取值是 1、2、3、4,分别对应矩形的不同位置和方向。根据 site 的取值,计算矩形的新位置和大小,并将其保存在名为 iframe 的对象中。
+                    //  1 左上角;2 右上角;3 左下角;4 右下角。
+                    switch (site) {
+                        case 1:
+                            iframe = {
+                                x: em.offsetX,
+                                y: em.offsetY,
+                                w: mark.w - (em.offsetX - sX),
+                                h: mark.h - (em.offsetY - sY),
+                                addIndex: mark.addIndex
+                            }
+                            break;
+                        case 2:
+                            iframe = {
+                                x: mark.x,
+                                y: mark.y + (em.offsetY - sY),
+                                w: mark.w + (em.offsetX - sX),
+                                h: mark.h - (em.offsetY - sY),
+                                addIndex: mark.addIndex
+                            }
+                            break;
+                        case 3:
+                            iframe = {
+                                x: mark.x + (em.offsetX - sX),
+                                y: mark.y,
+                                w: mark.w - (em.offsetX - sX),
+                                h: mark.h + (em.offsetY - sY),
+                                addIndex: mark.addIndex
+                            }
+                            break;
+                        case 4:
+                            iframe = {
+                                x: mark.x,
+                                y: mark.y,
+                                w: mark.w + (em.offsetX - sX),
+                                h: mark.h + (em.offsetY - sY),
+                                addIndex: mark.addIndex
+                            }
+                            break;
+                    }
+                    that.cavList.splice(i, 1, iframe);// 替换列表中的选定矩形数据
+
+                    // 重新绘制
+                    that.reDraw(i);
+                }
+
+                /* 鼠标离开矩形区 */
+                that.canvas.onmouseout = function (eo) {
+                    // console.log('鼠标离开矩形区');
+                    // 重新绘制
+                    that.reDraw();
+                    // 初始化
+                    that.draw()
+                };
+
+                /* 监听键盘, 点击后可以控制删除, 由于移动矩形事件已经监听了onmousemove, 所以在移动矩形方法中仍有一次调用 */
+                that.delDraw(i);
+            }
+        },
+        newDraw() {
+            let that = this
+            // 设置鼠标样式为十字架
+            this.canvas.style.cursor = 'crosshair'
+            // 初始化变量
+            let start = false; // 画框状态, false时不执行画框操作
+            let sX = 0; // 起点X坐标
+            let sY = 0; // 起点Y坐标
+
+            /* 按下鼠标左键 */
+            this.canvas.onmousedown = function (ed) {
+                /* 使用变量 */
+                start = true;
+                sX = ed.offsetX;
+                sY = ed.offsetY;
+
+                /* 重置按键监听, 防止选中取消后仍可删除 */
+                that.delDraw()
+
+                /* 鼠标移动 */
+                that.canvas.onmousemove = function (em) {
+                    if (start) {
+                        // 重新绘制
+                        that.reDraw();
+                        // 设置边框为虚线
+                        // 这个方法开始一条路径,或者重置当前的路径。在绘制新形状之前,你通常会调用这个方法来确保当前路径是新的,而不是继续绘制之前的路径。
+                        that.contentText.beginPath();
+                        //  这个方法设置当前线条样式为虚线。在这里,它将线条设置为了一种样式,这个样式是一个数组,数组中的第一个数字表示实线的长度,
+                        //  第二个数字表示空白的长度。所以 [8, 4] 表示每隔 8 个像素绘制一段实线,然后跳过 4 个像素绘制下一段实线,以此类推。
+                        that.contentText.setLineDash([8, 4]);
+                        // 这个方法用于创建一个新的矩形路径。它接受四个参数:起始点的 x 坐标和 y 坐标,以及矩形的宽度和高度。
+                        that.contentText.rect(sX, sY, em.offsetX - sX, em.offsetY - sY);
+                        // 这个方法绘制当前路径。在这里,它会按照之前设置的线条样式绘制矩形的轮廓。
+                        that.contentText.stroke();
+                    }
+                }
+
+                /* 鼠标抬起 */
+                that.canvas.onmouseup = function (eu) {
+                    
+                    // 如果绘制状态为 true,并且鼠标水平或垂直方向上移动的距离都大于 10 像素,那么才执行绘制矩形的操作。这样做是为了避免用户误操作导致绘制过小的矩形。
+                    if (start && Math.abs(eu.offsetX - sX) > 10 && Math.abs(eu.offsetY - sY) > 10) {
+                        // 改变矩形数组
+                        let frame = {
+                            x: sX, y: sY, w: eu.offsetX - sX, h: eu.offsetY - sY, addIndex: that.cavList.length - that.list.length + 1 + that.quesNum
+                        };
+                        that.cavList.push(frame);
+                        // 重新绘制
+                        that.reDraw();
+                        // 改变画框状态
+                        start = false
+                        // 初始化
+                        that.draw()
+                    } else {
+                        // 重新绘制
+                        that.reDraw();
+                        // 改变画框状态
+                        start = false
+                        // 初始化
+                        that.draw()
+                    }
+                };
+
+                /* 鼠标离开矩形区 */
+                that.canvas.onmouseout = function (eo) {
+                    if (start && Math.abs(eo.offsetX - sX) > 10 && Math.abs(eo.offsetY - sY) > 10) {
+                        // 改变矩形数组
+                        let frame = {
+                            x: sX, y: sY, w: eo.offsetX - sX, h: eo.offsetY - sY, addIndex: that.cavList.length - that.list.length + 1 + that.quesNum
+                        };
+                        that.cavList.push(frame);
+                        // 重新绘制
+                        that.reDraw();
+                        // 改变画框状态
+                        start = false;
+                        // 初始化
+                        that.draw()
+                    } else {
+                        // 重新绘制
+                        that.reDraw();
+                        // 改变画框状态
+                        start = false
+                        // 初始化
+                        that.draw()
+                    }
+                };
+            }
+        },
+        judgeDraw(iem) {
+            let that = this
+            console.log('选中矩形, 重绘矩形, 并分发后续事件', iem);
+            this.canvas.style.cursor = 'default';
+            // 初始化变量
+            let sX = 0; // 起点X坐标
+            let sY = 0; // 起点Y坐标
+
+            /* 按下鼠标左键 */
+            this.canvas.onmousedown = function (ed) {
+                sX = ed.offsetX;
+                sY = ed.offsetY;
+                if(that.cavList[iem].oldQues) return
+                that.checkIndex = iem
+                // 更改选中状态, 重绘矩形
+                that.reDraw(iem);
+
+                /* 当仅点击选中矩形便抬起鼠标后, 重新初始化画布 */
+                that.canvas.onmouseup = function () {
+                    // 重绘矩形
+                    that.reDraw(iem);
+
+                    // 初始化
+                    that.draw(iem);
+                };
+
+                /* 按住拖动鼠标, 移动选中矩形*/
+                that.moveDraw(iem, sX, sY);
+
+                /* 监听键盘, 点击后可以控制删除, 由于移动矩形事件已经监听了onmousemove, 所以在移动矩形方法中仍有一次调用 */
+                that.delDraw(iem);
+            }
+        },
+        moveDraw(i, sX, sY) {
+            let that = this
+            console.log('移动矩形', i);
+            // 获取选中的矩形
+            let mark = this.cavList[i]
+            // 监听鼠标移动事件
+            this.canvas.onmousemove = function (em) {
+                // 计算移动后矩形的新位置和大小
+                let iframe = {
+                    x: mark.x + (em.offsetX - sX),
+                    y: mark.y + (em.offsetY - sY),
+                    w: mark.w,
+                    h: mark.h,
+                    addIndex: mark.addIndex
+                }
+                // 更新矩形数组中选中矩形的数据
+                that.cavList.splice(i, 1, iframe);
+                /* 监听键盘, 使矩形在移动后仍可删除, 在点击未移动过的矩形时仍有一次监听 */
+                that.delDraw(i);
+                // 重新绘制
+                that.reDraw(i);
+            }
+            // 监听鼠标抬起事件
+            this.canvas.onmouseup = function () {
+                // 重绘矩形
+                that.reDraw(i);
+
+                // 初始化画布,使画布可以继续接收其他事件
+                that.draw(i);
+            };
+        },
+        delDraw(i, isBtn) {
+            let that = this
+            /* 按键事件 */
+            console.log('删除矩形', i);
+            if (i === null) {
+                // i为null时阻止按键监听事件冒泡
+                document.onkeydown = function (k) {
+                    return false;
+                }
+            } else {
+                if(isBtn) {
+                    if(that.checkIndex != -1 && !that.cavList[that.checkIndex].oldQues) {
+                        if (that.cavList.length >= 1) {
+                            // 删除数组元素
+                            that.cavList.splice(i, 1);
+                            // 重绘矩形
+                            that.reDraw();
+                        } else {
+                            /* 矩形数组长度为0, 已将矩形框全部删除 */
+                            that.contentText.clearRect(0, 0, that.canvas.width, that.canvas.height);
+                        }
+                        that.cavList.forEach((item, cIndex) => {
+                            if(!item.oldQues && cIndex >= that.checkIndex) {
+                                item.addIndex = item.addIndex - 1
+                            }
+                        })
+                        that.checkIndex = that.cavList.length - 1
+                        // 重置监听状态, 防止删除完毕后, 按键监听不消失
+                        that.delDraw()
+                        // 重绘矩形
+                        that.reDraw();
+                        // 初始化
+                        that.draw();
+                    }
+                } else {
+                    // 监听按键事件
+                    document.onkeydown = function (k) {
+                        let key = k.keyCode || k.which;
+                        if ((key == 46 || key == 8) && i !== null) {
+                            if (that.cavList.length >= 1) {
+                                // 删除数组元素
+                                that.cavList.splice(i, 1);
+                                // 重绘矩形
+                                that.reDraw();
+                            } else {
+                                /* 矩形数组长度为0, 已将矩形框全部删除 */
+                                that.contentText.clearRect(0, 0, that.canvas.width, that.canvas.height);
+                            }
+                            that.cavList.forEach((item, cIndex) => {
+                                if(!item.oldQues && cIndex >= i) {
+                                    item.addIndex = item.addIndex - 1
+                                }
+                            })
+                            // 重置监听状态, 防止删除完毕后, 按键监听不消失
+                            that.delDraw()
+                            // 重绘矩形
+                            that.reDraw();
+                            // 初始化
+                            that.draw();
+                        }
+                    }
+                }
+            }
+        },
+        reDraw(i) {
+            console.log('重绘所有矩形', i);
+            this.contentText.setLineDash([8, 0]); // 设置边框为实线
+            this.contentText.clearRect(0, 0, this.canvas.width, this.canvas.height);
+            // 绘制未选中部分
+            this.cavList.forEach((value, index, array) => {
+                if (i === undefined || index != i) {
+                    this.contentText.beginPath();
+                    this.contentText.strokeStyle = 'blue';
+                    this.contentText.rect(value.x, value.y, value.w, value.h);
+                    this.contentText.stroke();
+                    this.contentText.fillStyle = 'blue'
+                    this.contentText.fillText(`第${value.qIndex !== undefined ? value.qIndex + 1 : value.addIndex}题`, value.x + 5, value.y + 20)
+                }
+            });
+            // 绘制已选中部分
+            this.cavList.forEach((value, index, array) => {
+                if (index === i) {
+                    /* 绘制方框 */
+                    //  开始一个新的绘图路径。
+                    this.contentText.beginPath();
+                    // 设置绘制路径的边框颜色为红色。
+                    this.contentText.strokeStyle = 'red';
+                    // 绘制一个矩形路径,起始点为 (value.x, value.y),宽度为 value.w,高度为 value.h。
+                    this.contentText.rect(value.x, value.y, value.w, value.h);
+                    // 设置矩形的填充色为带有透明度的灰色。
+                    this.contentText.fillStyle = 'RGBA(102,102,102,0.2)'
+                    // 填充矩形的内部区域,以实现选中效果。
+                    this.contentText.fillRect(value.x, value.y, value.w, value.h);
+                    // 绘制矩形路径的边框。
+                    this.contentText.stroke();
+                    // 绘制四个角的圆圈
+                    this.contentText.beginPath();
+                    this.contentText.strokeStyle = 'red';
+                    // 绘制一个圆弧路径,圆心为 (value.x, value.y),半径为 4,起始角度为 0,终止角度为 2π(360度),即绘制一个完整的圆。
+                    this.contentText.arc(value.x, value.y, 4, 0, Math.PI * 2)
+                    this.contentText.fillStyle = "red";
+                    this.contentText.fill();// 画起点实心圆
+                    this.contentText.stroke();
+                    this.contentText.beginPath();
+                    // 绘制另一个圆弧路径,圆心为 (value.x, value.y + value.h),参数同上。
+                    this.contentText.arc(value.x, value.y + value.h, 4, 0, Math.PI * 2);
+                    this.contentText.fillStyle = "red";
+                    this.contentText.fill();// 画起点纵向实心圆
+                    this.contentText.stroke();
+                    this.contentText.beginPath();
+                    // 绘制另一个圆弧路径,圆心为 (value.x + value.w, value.y + value.h),参数同上。
+                    this.contentText.arc(value.x + value.w, value.y + value.h, 4, 0, Math.PI * 2);
+                    this.contentText.fillStyle = "red";
+                    this.contentText.fill();// 画起点横向实心圆
+                    this.contentText.stroke();
+                    this.contentText.beginPath();
+                    // 绘制另一个圆弧路径,圆心为 (value.x + value.w, value.y),参数同上。
+                    this.contentText.arc(value.x + value.w, value.y, 4, 0, Math.PI * 2);
+                    this.contentText.fillStyle = "red";
+                    this.contentText.fill();// 画终点实心圆
+                    this.contentText.stroke();
+                    this.contentText.fillText(`第${value.qIndex !== undefined ? value.qIndex + 1 : value.addIndex}题`, value.x + 5, value.y + 20)
+                }
+            })
+        }
+    }
+}
+</script>
+
+<style>
+
+</style>

+ 25 - 28
TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseKnowledgeDetail.vue

@@ -220,30 +220,11 @@ export default {
     doRender(data, classIndexs) {
       let classIndex = this.getCurClassIndex
       // 区分全部班级以及单个班级的数据
-      this.x = data.pointList
-      //this.y = classIndex === -1 ? data.stupercent.grade.map(item => (Number(item)).toFixed(2)) : data.pointList.map(pointName => data.classpercent[pointName][classIndex].toFixed(2))
-      if(classIndex === -1){
-      // 全部班級的分率統計需要另外計算        
-      const questionCount = this.$store.state.totalAnalysis.analysisJsonJoint.knowPer[0].value.length;
-      const averages = new Array(questionCount).fill(0);
-
-      // 累加各班級每一題的分數
-      this.$store.state.totalAnalysis.analysisJsonJoint.knowAllper.forEach((classItem) => {
-        classItem.value.forEach((score, index) => {
-          averages[index] += score;
-        });
-      });
-
-      // 計算每題的平均得分率
-      let averageScores = averages.map((sum, index) => {
-        const total = this.$store.state.totalAnalysis.analysisJsonJoint.knowPer[0].value[index] * 100;
-        const classCount = this.$store.state.totalAnalysis.analysisJsonJoint.knowAllper.length;
-        return total > 0 ? ((sum / (total * classCount)) * 100).toFixed(2) : 0;
-      });
-
-      this.y = averageScores;   
+      this.x = data.pointList      
+      if(classIndex === -1){         
+        this.y = data.stupercent.grade.map(item => (Number(item)).toFixed(2)) 
       }else{
-        this.y = data.pointList.map(pointName => data.classpercent[pointName][classIndex].toFixed(2));
+        this.y = data.pointList.map(pointName => data.classpercent[pointName][0].toFixed(2));
       }
       this.drawLine()
     }
@@ -269,12 +250,28 @@ export default {
     // 获取最新散点图数据
     getKnowledgeData() {
       
-      let curclassIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.id).indexOf(this.$store.state.totalAnalysis.currentSubjectJoint)
-      let curJson = this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curclassIndex].pointKey
-      
-
-
+      let curClassIndex = this.$store.state.totalAnalysis.curClassIndex
+      //let curJson = this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curclassIndex].pointKey
+      let curJson = {}
 
+      if(curClassIndex !== -1){// 取指定班級的資料
+        curJson = this.$store.state.totalAnalysis.analysisJsonJoint.classes[curClassIndex].pointLevelKey.pointKey        
+      }else{
+        // 取全部班級的資料 // 初始化平均陣列 都先塞 0
+        let stupercent = { grade: new Array(this.$store.state.totalAnalysis.analysisJsonJoint.classes[0].knowWrong.length).fill(0) }        
+        // 用classes的knowWrong的   每一題的[3]加總之後除以classes.len
+        this.$store.state.totalAnalysis.analysisJsonJoint.classes.forEach(classItem => {
+          classItem.knowWrong.forEach((knowWrongItem, index) => {
+            stupercent.grade[index] += parseFloat(knowWrongItem[3])
+          })
+        })
+        // 計算百分比
+        stupercent.grade = stupercent.grade.map(item => (Number((item / this.$store.state.totalAnalysis.analysisJsonJoint.classes.length).toFixed(2)) * 100))                  
+        curJson = {
+          pointList: this.$store.state.totalAnalysis.analysisJsonJoint.classes[0].pointLevelKey.pointKey.pointList,
+          stupercent: stupercent
+        }
+      }           
       return curJson
     },
     getAnalysisJson() {

+ 34 - 5
TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseLevelDetail.vue

@@ -218,11 +218,16 @@ export default {
     },
 
     doRender(data, classIndexs) {
-      let classIndex = this.getCurClassIndex
+      let classIndex = this.getCurClassIndex      
       console.log(classIndex)
       // 区分全部班级以及单个班级的数据
+      
       this.x = data.pointList
-      this.y = classIndex === -1 ? data.stupercent.grade.map(item => (Number(item)).toFixed(2)) : data.pointList.map(pointName => data.classpercent[pointName][classIndex].toFixed(2))
+      if(classIndex === -1){         
+        this.y = data.stupercent.grade.map(item => (Number(item)).toFixed(2)) 
+      }else{
+        this.y = data.pointList.map(pointName => data.classpercent[pointName][0].toFixed(2));
+      }      
       this.drawLine()
     },
 
@@ -261,14 +266,38 @@ export default {
   computed: {
     // 获取最新散点图数据
     getKnowledgeData() {
-      let curclassIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.id).indexOf(this.$store.state.totalAnalysis.currentSubjectJoint)
-      let curJson = this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curclassIndex].levelKey
+      let curclassIndex = this.$store.state.totalAnalysis.curClassIndex
+      //let curclassIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.id).indexOf(this.$store.state.totalAnalysis.currentSubjectJoint)
+      let curJson = {}
+      if(curclassIndex !== -1){
+        curJson = this.$store.state.totalAnalysis.analysisJsonJoint.classes[curclassIndex].pointLevelKey.levelKey
+      }else{
+         // 取全部班級的資料 // 初始化平均陣列 都先塞 0
+        let stupercent = { grade: new Array(this.$store.state.totalAnalysis.analysisJsonJoint.classes[0].fieldWrong.length).fill(0) }        
+        // 用classes的fieldWrong的   每一題的[3]加總之後除以classes.len
+        this.$store.state.totalAnalysis.analysisJsonJoint.classes.forEach(classItem => {
+          classItem.fieldWrong.forEach((fieldWrongItem, index) => {
+            stupercent.grade[index] += parseFloat(fieldWrongItem[3])
+          })
+        })
+        // 計算百分比
+        stupercent.grade = stupercent.grade.map(item => (Number((item / this.$store.state.totalAnalysis.analysisJsonJoint.classes.length).toFixed(2)) * 100))                  
+        curJson = {
+          pointList: this.$store.state.totalAnalysis.analysisJsonJoint.classes[0].pointLevelKey.levelKey.pointList,
+          stupercent: stupercent
+        }
+
+      }
+      
+      //let curJson = this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curclassIndex].levelKey
       let transArr = this.$GLOBAL.EXERCISE_LEVELS()
       curJson.pointList = curJson.pointList.map((i, index) => transArr[index])
       for (let key in curJson.classpercent) {
         if (!isNaN(key)) {
           let newKey = transArr[+key - 1]
-          curJson.classpercent[newKey] = curJson.classpercent[key]
+          if(curJson.classpercent){
+            curJson.classpercent[newKey] = curJson.classpercent[key]
+          }          
           curJson.stupercent[newKey] = curJson.stupercent[key]
         }
       }

+ 3 - 3
TEAMModelOS/ClientApp/src/components/student-analysis/total/htBasePie.vue

@@ -79,9 +79,9 @@
         },
         computed: {
             // 获取最新知识点占比饼图数据
-            getPieData() {
-                let curSubjectIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.id).indexOf(this.$store.state.totalAnalysis.currentSubjectJoint)
-                return this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].pointKey
+            getPieData() {       
+                // 活動版全部的題目跟配分都一樣 所以取第一筆資料即可                                  
+                return this.$store.state.totalAnalysis.analysisJsonJoint.classes[0].pointLevelKey.pointKey
             }
         },
         watch: {

+ 40 - 8
TEAMModelOS/ClientApp/src/components/student-analysis/total/htBaseRadar.vue

@@ -122,10 +122,20 @@ export default {
       })
     },
 
-    doRender(data, classIndexs) {
-      let classIndex = this.getCurClassIndex
-      let indicator = []
-      let rateArr = classIndex === -1 ? data.stupercent.grade.map(item => (Number(item)).toFixed(2)) : data.pointList.map(pointName => data.classpercent[pointName][classIndex].toFixed(2))
+    doRender(data, classIndex) {            
+      let indicator = [] 
+      let rateArr = []
+      if(classIndex === -1){
+        // 全部班級
+        if(data.stupercent.grade){
+          rateArr = data.stupercent.grade.map(item => (Number(item)).toFixed(2))
+          }        
+      }else{
+        // 指定班級
+        if(data.classpercent){
+          rateArr = data.pointList.map(pointName => data.classpercent[pointName][0].toFixed(2))
+          }        
+      }         
       console.log(data)
       // 获取最大值 +5 设置到雷达边界最大值
       let maxNum = Math.max(...rateArr.map(i => Number(i))) + 5
@@ -147,10 +157,32 @@ export default {
 
   computed: {
     // 获取最新雷达图数据
-    getKnowledgeData() {
-      let curSubjectIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.id).indexOf(this.$store.state.totalAnalysis.currentSubjectJoint)
-      let curJson = this.echartsId === 'knowRadar' ? this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].pointKey :
-        this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].levelKey
+    getKnowledgeData() {            
+      let curClassIndex = this.$store.state.totalAnalysis.curClassIndex
+      let curJson = {}
+      if(curClassIndex !== -1){// 取指定班級的資料
+        curJson = this.echartsId === 'knowRadar' ? this.$store.state.totalAnalysis.analysisJsonJoint.classes[curClassIndex].pointLevelKey.pointKey :
+        this.$store.state.totalAnalysis.analysisJsonJoint.classes[curClassIndex].pointLevelKey.levelKey
+      }else{
+        // 取全部班級的資料
+        let stupercent = { grade: new Array(this.$store.state.totalAnalysis.analysisJsonJoint.classes[0].knowWrong.length).fill(0) }        
+        // 用classes的knowWrong的   每一題的[3]加總之後除以classes.len
+        this.$store.state.totalAnalysis.analysisJsonJoint.classes.forEach(classItem => {
+          classItem.knowWrong.forEach((knowWrongItem, index) => {
+            stupercent.grade[index] += parseFloat(knowWrongItem[3])
+          })
+        })
+        // 計算百分比
+        stupercent.grade = stupercent.grade.map(item => (Number((item / this.$store.state.totalAnalysis.analysisJsonJoint.classes.length).toFixed(2)) * 100))                  
+        curJson = {
+          pointList: this.$store.state.totalAnalysis.analysisJsonJoint.classes[0].pointLevelKey.pointKey.pointList,
+          stupercent: stupercent
+        }
+      }    
+      // data.classpercent
+                // data.pointList
+                // data.stupercent.grade    
+
       if (this.echartsId !== 'knowRadar') {
         let transArr = this.$GLOBAL.EXERCISE_LEVELS()
         curJson.pointList = curJson.pointList.map((i, index) => transArr[index])

+ 82 - 4
TEAMModelOS/ClientApp/src/components/student-web/EventView/EventContentTypeTemplate/PaperViewBox/ArtTestReport.vue

@@ -1,6 +1,14 @@
 <template>
     <div class="art-paper-content">
-        <Loading v-show="isLoad" bgColor="rgba(0, 0, 0, 0.3)"></Loading>
+        <div v-show="isLoad">
+            <Spin fix style="background-color: rgba(0, 0, 0, 0.3); color: #1e79da; font-size: 30px;">
+                <i-circle :percent="quesProcess" stroke-color="#1e79da" trail-color="#d4d4d4" :size="160">
+                    <span class="demo-Circle-inner" style="font-size: 34px">{{ quesProcess }}%</span>
+                </i-circle>
+                <div>试题加载中</div>
+            </Spin>
+        </div>
+        <!-- <Loading v-show="isLoad" bgColor="rgba(0, 0, 0, 0.3)"></Loading> -->
         <vuescroll>
             <EventBasicInfo :info="nowActive" />
             <div class="art-box">
@@ -87,6 +95,8 @@ export default {
             previewStatus: false,
             previewFile: null,
             testState: 0, //1:未作答  2:未评分  3:已评分
+            processNum: 0,
+            artQuesTotal: 0, //本次评测的总试题数量
         }
     },
     computed: {
@@ -97,6 +107,10 @@ export default {
         nowActive() {
             return this.getItemTitle
         },
+        quesProcess() {
+            let process = this.artQuesTotal ? Math.round(this.processNum / this.artQuesTotal * 100) : 0
+            return process > 100 ? 100 : process
+        },
     },
     watch: {
         nowActive: {
@@ -132,6 +146,8 @@ export default {
         },
         async getArtInfo() {
             this.isLoad = true
+            this.artQuesTotal = 0
+            this.processNum = 0
             let params = {
                 id: this.getItemTitle.id,
                 code: this.getItemTitle.school
@@ -272,6 +288,7 @@ export default {
                                             })
                                             paperArt.source = this.getItemTitle.source || null
                                             paperArt.qamode = this.getItemTitle.qamode
+                                            this.artQuesTotal += paperArt.point.length
                                             item.examInfo.push(paperArt)
                                         }
                                         for (let i = 0; i < item.examInfo.length; i++) {
@@ -323,6 +340,7 @@ export default {
         // 获取试卷信息
         getPaper() {
             return new Promise((resolve, reject) => {
+                let date = (new Date()).getTime() //当前时间
                 let promiseArr = []
                 this.artExam.forEach(item => {
                     promiseArr.push(new Promise(async (r, j) => {
@@ -358,7 +376,7 @@ export default {
                                     k++
                                 }
                             }
-                            item.testState = k ? (item.examInfo[0].stuAns ? 1 : 2) : 3
+                            item.testState = k ? (item.examInfo[0].stuAns ? (this.artInfo.examDeadline && this.artInfo.examDeadline < date) ? 2 : 1 : 2) : 3
                             console.error('试卷信息', item.paperInfo)
                             r(true)
                         } else {
@@ -378,8 +396,68 @@ export default {
         getStuPaper(code) {
             return new Promise(async(r, j) => {
                 try {
-                    let a = await this.$evTools.getStuPaper(code)
-                    r(a)
+                    let curScope = code.scope
+                    // let blobHost = this.getBlobHost()
+                    // 根据试卷的Blob地址 去读取JSON文件
+                    let cntr = code.code
+                    let sas = await this.$tools.getBlobSas(cntr)
+                    let sasString = "?" + sas.sas
+                    let blobHost = sas.url
+                    let paperBlobPath = blobHost + '/' + cntr + code.blob
+                    let fullPath = paperBlobPath + '/index.json' + sasString
+                    console.log(fullPath);
+                    try {
+                        let jsonInfo = await this.$tools.getFile(fullPath)
+                        let jsonData = JSON.parse(jsonInfo)
+                        jsonData.scope = curScope
+                        jsonData.code = code.code
+                        jsonData.sheet = code.sheet || null
+                        code.tags = code.tags || jsonData.tags
+                        // 获取试卷包含的试题数据并包装好
+                        if (jsonData.slides && jsonData.slides.length) {
+                            jsonData.item = []
+                            let promiseArr = []
+                            jsonData.slides.forEach((item, index) => {
+                                promiseArr.push(new Promise(async (resolve, reject) => {
+                                    // 获取题目JSON并且包装成完整试题对象
+                                    let itemFullPath = paperBlobPath + '/' + item.url + sasString
+                                    let itemJson = JSON.parse(await this.$tools.getFile(itemFullPath))
+                                    itemJson.exercise.question = itemJson.item[0].question
+                                    itemJson.exercise.option = itemJson.item[0].option
+                                    itemJson.exercise.id = itemJson.id
+                                    itemJson.exercise.pid = itemJson.pid
+                                    itemJson.exercise.scope = curScope
+                                    itemJson.exercise.score = item.scoring ? item.scoring.score : 0
+                                    // jsonData.item.push(itemJson.exercise)
+                                    try {
+                                        itemJson.exercise = await this.$evTools.doAddHost(itemJson.exercise, code, code.code, null, sasString)
+                                        this.processNum++
+                                        resolve(itemJson.exercise)
+                                    } catch (e) {
+                                        reject(e)
+                                    }
+                                }))
+                            })
+                            Promise.all(promiseArr).then(res => {
+                                res.forEach((resItem, resIndex) => {
+                                    resItem.children = []
+                                    if (resItem.pid) {
+                                        let pItem = res.filter(i => i.id === resItem.pid)[0]
+                                        pItem.children.push(resItem)
+                                        pItem.score = pItem.score + resItem.score
+                                    }
+                                })
+                                jsonData.item = res.filter(i => !i.pid)
+                                r(jsonData)
+                            }).catch(e => {
+                                j(e)
+                            })
+                        }
+                    } catch (e) {
+                        j(e)
+                    }
+                    /* let a = await this.$evTools.getStuPaper(code)
+                    r(a) */
                 } catch(e) {
                     j(undefined)
                 }

+ 1 - 1
TEAMModelOS/ClientApp/src/components/student-web/EventView/EventContentTypeTemplate/PaperViewBox/ArtView.vue

@@ -484,7 +484,7 @@ export default {
     // 查看智音作答详情
     zyModule(busType, isUrl) {
       // const url = this.$store.state.config.srvAdrType === 'test' ? 'https://tstamesopen.aimusic.art' : 'https://amesopen.aimusic.art'
-      const url = 'https://tmdopen.yosocloud.com'
+      const url = this.$store.state.config.srvAdrType === 'test' ? 'https://tmdopen.yosocloud.com' : 'https://musicopen.winteach.cn'
       // 智音提供的APP_ID
       const appid = '8a68f563f3384662acbc268336b98ae2'
       //第三方token

+ 11 - 2
TEAMModelOS/ClientApp/src/components/student-web/EventView/EventContentTypeTemplate/PaperViewBox/PaperTest.vue

@@ -1290,7 +1290,11 @@
                     this.$api.studentWeb.SaveStuExamPaper(req).then(res => {
                         if (res.classResult) {
                             this.$Message.success(this.$t('studentWeb.exam.submitSuccess'))
-                            this.$router.go(-1)
+                            this.$router.push({
+                                path: '/studentWeb/examView',
+                                query: {aId: this.getItemTitle.id, subIds: this.getItemTitle.subjects.map(sub => sub.id)}
+                            })
+                            // this.$router.go(-1)
                             this.removeLocal()
                         } else {
                             this.isLoading = false
@@ -1308,7 +1312,12 @@
             quitTest(type) {
                 this.showMessageNum = type ? 7 : this.showMessageNum
                 this.isLoading = true
-                this.$router.go(-1)
+                
+                this.$router.push({
+                    path: '/studentWeb/examView',
+                    query: {aId: this.getItemTitle.id, subIds: this.getItemTitle.subjects.map(sub => sub.id)}
+                })
+                // this.$router.go(-1)
             },
             //处理学生作答数据blob地址
             formUrl(data) { 

+ 2 - 2
TEAMModelOS/ClientApp/src/components/student-web/EventView/EventList.vue

@@ -754,7 +754,7 @@ import ArtTestReport from "./EventContentTypeTemplate/PaperViewBox/ArtTestReport
                             }, 1000)
                             this.isListNoItem = false
                             let ids = JSON.parse(decodeURIComponent(sessionStorage.getItem("ids")))
-                            if(ids) {
+                            /* if(ids) {
                                 let nowIn = this.eventShow.findIndex(item => {
                                     return item.id === ids.examId
                                 })
@@ -763,7 +763,7 @@ import ArtTestReport from "./EventContentTypeTemplate/PaperViewBox/ArtTestReport
                                 } else if(!this.nowActivity) {
                                     this.sentSelectedEventTitle(this.eventShow[0])
                                 }
-                            } else if(this.$route.query.aId) {
+                            } else  */if(this.$route.query.aId) {
                                 let nowIn = this.eventShow.findIndex(item => {
                                     return item.id === this.$route.query.aId
                                 })

+ 40 - 0
TEAMModelOS/ClientApp/src/store/module/totalAnalysis.js

@@ -128,6 +128,46 @@ export default {
                 }				
 			})
 			val.pointLevelKey = pointLevelKey
+            //===================以班級為單位 start===================            
+            val.classes.forEach((classItem) => {
+
+                // 活動版fieldName fieldPerItem都一樣取第一個即可
+                let fieldNameItem = val.fieldName[0].value
+                let fieldPerItem = val.fieldPer[0].value
+                let fieldwrongItem = classItem.fieldWrong
+                let knowNameItem = classItem.knowName
+                let knowPerItem = classItem.knowPer
+                let wrongItem = classItem.knowWrong
+                if(fieldNameItem && fieldPerItem && fieldwrongItem && knowNameItem && knowPerItem && wrongItem){
+                    //classItem.pointLevelKey = {}
+                    classItem.pointLevelKey = {
+                        classId:classItem.classId,
+                        levelKey:{
+                            level:fieldNameItem,
+                            pointList:fieldNameItem,
+                            per:fieldPerItem,
+                            wrong:{
+                                datas:fieldwrongItem,
+                                keys:val.wrongKey
+                            },
+                            stupercent:tools.getLevelPercentJointClass(val,classItem).stuResult,
+                            classpercent:tools.getLevelPercentJointClass(val,classItem).classResult
+                        },
+                        pointKey:{
+                            level:knowNameItem,
+                            pointList:knowNameItem,
+                            per:knowPerItem,
+                            wrong:{
+                                datas:wrongItem,
+                                keys:val.wrongKey
+                            },
+                            stupercent:tools.getKnowPercentJointClass(val,classItem).stuResult,
+                            classpercent:tools.getKnowPercentJointClass(val,classItem).classResult
+                        }
+                    }
+                }				
+			})
+            //===================以班級為單位 end=====================
 			state.currentSubject = null
 			state.indexSubject = null
 			state.curClassIndex = -1

+ 80 - 2
TEAMModelOS/ClientApp/src/utils/evTools.js

@@ -337,18 +337,29 @@ export default {
 							let src = decodeURI(srcList[i])
 							/* 如果是来自试卷的题目 则需要匹配试卷HOST */
 							if (paperItem && paperItem.examId) {
-								console.error(paperItem)
 								// 如果是学生端 examId为学校编码
 								let examContainer = paperItem.examScope ? (paperItem.examScope === 'school' ? store.state.userInfo.schoolCode : store.state.userInfo.TEAMModelId) : paperItem.examId
 								let blobUrl = src.includes(curHost) ? src.split('?')[0] : (curHost + '/' + examContainer + paperItem.blob + '/' + src)
 								try {
-									let addSasUrl = sasString ? { url: blobUrl + sasString } : await $tools.getFileSas(blobUrl)
+									let addSasUrl = ''
+									// 处理青羊区艺术评测题目中,音视频文件名称重复导致组卷时,blob只有一个相同名称的音视频
+									if(paperItem.tags.includes('2024青羊区艺术测评五年级美术试卷') || paperItem.tags.includes('2024青羊区艺术测评八年级美术试卷') || paperItem.tags.includes('2024青羊区艺术测评五年级音乐试卷') || paperItem.tags.includes('2024青羊区艺术测评八年级音乐试卷')){
+										blobUrl = curHost + '/hbcn/item/' + exerciseItem.id  + '/' + src
+										// 其他学校学生作答醍摩豆学段试卷时,音视频获取的是当前学校code下的题库,此处改为固定获取醍摩豆学校(hbcn)的
+										addSasUrl = container === 'hbcn' && sasString ? { url: blobUrl + sasString } : await $tools.getFileSas(blobUrl)
+									} else {
+										addSasUrl = sasString ? { url: blobUrl + sasString } : await $tools.getFileSas(blobUrl)
+									}
 									richTextObj[key] = richTextObj[key].replaceAll(`src="${src}"`, `src="${addSasUrl.url}"`);
 								} catch (e) {
 									j(500)
 								}
 							} else if (paperItem && paperItem.scope) { //必须是编辑试卷时 试题内的多媒体才是从paper中读取 (如果编辑时从题库新增多媒体试题 还未处理)
 								let blobUrl = src.includes(curHost) ? src.split('?')[0] : (curHost + '/' + container + '/paper/' + paperItem.name + '/' + src)
+								if(paperItem.tags.includes('2024青羊区艺术测评五年级音乐试卷(测试用卷)') || paperItem.tags.includes('2024青羊区艺术测评八年级音乐试卷(测试用卷)')){
+									blobUrl = curHost + '/' + container + '/item/'  + exerciseItem.id  + '/' + src
+								}
+								
 								try {
 									let addSasUrl = await $tools.getFileSas(blobUrl)
 									richTextObj[key] = richTextObj[key].replaceAll(`src="${src}"`, `src="${addSasUrl.url}"`);
@@ -625,6 +636,7 @@ export default {
 				jsonData.scope = curScope
 				jsonData.code = paper.code
 				jsonData.sheet = paper.sheet || null
+				paper.tags = paper.tags || jsonData.tags
 				// 获取试卷包含的试题数据并包装好
 				if (jsonData.slides && jsonData.slides.length) {
 					jsonData.item = []
@@ -669,6 +681,72 @@ export default {
 			}
 		})
 	},
+	// 艺术测评专用获取试卷方案
+	getStuPaperForArt(paper, examScope, sas) {
+		let curScope = examScope || paper.scope
+		console.log(...arguments);
+		return new Promise(async (r, j) => {
+			// let blobHost = this.getBlobHost()
+			// 根据试卷的Blob地址 去读取JSON文件
+			let cntr = paper.code
+			let sasString = "?" + sas.sas
+			let blobHost = sas.url
+			let paperBlobPath = blobHost + '/' + cntr + paper.blob
+			let fullPath = paperBlobPath + '/index.json' + sasString
+			console.log(fullPath);
+			try {
+				let jsonInfo = await $tools.getFile(fullPath)
+				let jsonData = JSON.parse(jsonInfo)
+				jsonData.scope = curScope
+				jsonData.code = paper.code
+				jsonData.sheet = paper.sheet || null
+				paper.tags = paper.tags || jsonData.tags
+				r(jsonData)
+				// 获取试卷包含的试题数据并包装好
+				// if (jsonData.slides && jsonData.slides.length) {
+				// 	jsonData.item = []
+				// 	let promiseArr = []
+				// 	jsonData.slides.forEach((item, index) => {
+				// 		promiseArr.push(new Promise(async (resolve, reject) => {
+				// 			// 获取题目JSON并且包装成完整试题对象
+				// 			let itemFullPath = paperBlobPath + '/' + item.url + sasString
+				// 			let itemJson = JSON.parse(await $tools.getFile(itemFullPath))
+				// 			itemJson.exercise.question = itemJson.item[0].question
+				// 			itemJson.exercise.option = itemJson.item[0].option
+				// 			itemJson.exercise.id = itemJson.id
+				// 			itemJson.exercise.pid = itemJson.pid
+				// 			itemJson.exercise.scope = curScope
+				// 			itemJson.exercise.score = item.scoring ? item.scoring.score : 0
+				// 			// jsonData.item.push(itemJson.exercise)
+				// 			try {
+				// 				itemJson.exercise = await this.doAddHost(itemJson.exercise, paper, paper.code, null, sasString)
+				// 				resolve(itemJson.exercise)
+				// 			} catch (e) {
+				// 				reject(e)
+				// 			}
+				// 		}))
+				// 	})
+				// 	Promise.all(promiseArr).then(res => {
+				// 		res.forEach((resItem, resIndex) => {
+				// 			resItem.children = []
+				// 			if (resItem.pid) {
+				// 				let pItem = res.filter(i => i.id === resItem.pid)[0]
+				// 				pItem.children.push(resItem)
+				// 				pItem.score = pItem.score + resItem.score
+				// 			}
+				// 		})
+				// 		jsonData.item = res.filter(i => !i.pid)
+				// 		r(jsonData)
+				// 	}).catch(e => {
+				// 		j(e)
+				// 	})
+				// }
+			} catch (e) {
+				j(e)
+			}
+		})
+	},
+
 	/* 获取完整的试卷数据 */
 	getComposeItem(paper) {
 		return new Promise(async (r, j) => {

+ 124 - 1
TEAMModelOS/ClientApp/src/utils/public.js

@@ -1932,7 +1932,130 @@ export default {
 		})
 		return result
 	},
-	/* 学情知识点年级班级得分率数据转换 活動版*/
+	//--------------------------------------------
+	/* 学情认知层次年级班级得分率数据转换 活動Class版 */
+	getLevelPercentJointClass(val, classItem) {
+		let stuResult = {}
+		let classResult = {}
+		//認知層次都是固定項目 [1, 2, 3, 4, 5, 6]
+		let fieldNameItem = val.fieldName[0]
+		// let fieldAllPerItem = val.fieldAllPer.find(i => i.key === subject.id)
+		// let fieldwrongItem = val.fieldwrong.find(i => i.key === subject.id)
+		fieldNameItem.value.forEach((item, index) => {
+			stuResult[item] = this.getLevelStuPercentJointClass(val, classItem, index)
+			classResult[item] = this.getLevelClassPercentJointClass(val, classItem, index)
+		})
+		// stuResult.grade = fieldAllPerItem.value.map((score, index) => (fieldwrongItem.value[
+		// 	index][1] == 0 ? 0 : Number(score / fieldwrongItem.value[index][1]).toFixed(2)) * 100)
+		stuResult.keys = val.knowKey
+		classResult.keys = val.knowkey
+		classResult.className = val.classes.map(i => i.className)
+		let obj = {
+			stuResult: stuResult,
+			classResult: classResult
+		}
+		return obj
+	},
+	/* 学情认知层次模块数据转换 活動Class版 */
+	getLevelStuPercentJointClass(val, classItem, index) {
+		let fScoreItem = classItem.fScore
+		let result = []
+		val.students.forEach(stu => {
+			// if(stu.name==="沈仕程"){
+			// 	console.log("沈仕程 : "+JSON.stringify(stu));
+			// }
+			// 如果該學生的課程/科目存在  而且 id 相同才加入
+			//let fScoresItem = val.fScores.find(i => i.key === subject.id)			
+			let fieldStuPer = classItem.fieldStuPer.find(i => i.key === stu.id)					
+			if(fieldStuPer && stu.classId === classItem.classId){
+				let fScoreItemValue = 0
+				if(fieldStuPer.value[index]!==0 || fScoreItem[index] !== 0){
+					fScoreItemValue = (fieldStuPer.value[index] / fScoreItem[index]).toFixed(2)
+				}
+				result.push([
+					stu.name,
+					stu.className,
+					stu.no || '-',
+					fScoreItem[index],
+					fieldStuPer.value[index],
+					fScoreItemValue
+				])
+			}			
+		})
+		return result
+	},
+	/* 学情认知层次年级班级得分率数据转换 活動Class版 */
+	getLevelClassPercentJointClass(val, classItem, index) {
+		let result = []
+		let classfieldPer = classItem.fieldWrong.find(i => i[0] === val.fieldName[0].value[index].toString())
+		result.push(parseFloat(classfieldPer[3]) * 100)
+
+
+		// val.classes.forEach(classItem => {
+		// 	// 如果該班級的課程/科目存在  而且 id 相同才加入
+		// 	let classSubjectItem = classItem.subjects.find(i => i.id === subject.id)
+		// 	let fieldwrongItem = val.fieldwrong.find(i => i.key === subject.id)
+		// 	if(classSubjectItem){			
+		// 	   result.push(classSubjectItem.field.map((score, index) => 
+		// 		fieldwrongItem.value[index][1] == 0 ? 0 : Number(score / fieldwrongItem.value[index][1]) * 100)[index])
+		// 	}
+		// })
+		return result
+	},
+	//--------------------------------------------	
+	/* 学情知识点年级班级得分率数据转换 活動class版*/
+	getKnowPercentJointClass(val, classItem) {
+		let stuResult = {}
+		let classResult = {}
+		let knowNameItem = classItem.knowName
+		//let knowAllperItem = val.knowAllper.find(i => i.key === subject.id)
+		//let wrongItem = val.wrong.find(i => i.key === subject.id)
+		knowNameItem.forEach((item, index) => {
+			stuResult[item] = this.getKnowStuPercentJointClass(val, classItem, index)
+			classResult[item] = this.getKnowClassPercentJointClass(val, classItem, index)
+		})
+		// 取当前年级在每个知识点的得分 除以知识点的总分 得到年级在该知识点的得分率
+		//stuResult.grade = knowAllperItem.value.map((score, index) => wrongItem.value[index][1] == 0 ? 0 : Number(score / wrongItem.value[index][1]) * 100)
+		stuResult.keys = val.knowKey
+		classResult.keys = val.knowkey
+		classResult.className = val.classes.map(i => i.className)
+		let obj = {
+			stuResult: stuResult,
+			classResult: classResult
+		}
+		return obj
+	},   
+	/* 学情知识点模块数据转换 活動class版 */
+	getKnowStuPercentJointClass(val, classItem, index) {		
+		let kScoresItem = classItem.kScore
+		let result = []
+		val.students.forEach(stu => {
+			// 如果該學生的課程/科目存在  而且 id 相同才加入			
+			let knowStuPer = classItem.knowStuPer.find(i => i.key === stu.id)			
+			if (stu.classId === classItem.classId) {
+			    result.push([
+			    	stu.name,
+			    	stu.className,
+			    	stu.no || '-',
+			    	kScoresItem[index],			    	
+			    	knowStuPer.value[index],
+			    	kScoresItem[index] == 0 ? 0 :  (knowStuPer.value[index] / kScoresItem[index]).toFixed(2)
+			    ])
+			}
+		})
+		return result
+	},
+	/* 学情知识点班级得分率数据转换 活動class版 */
+	getKnowClassPercentJointClass(val, classItem, index) {
+		let result = []		
+		// 取得目標知識點得分率
+		let classKnowPer = classItem.knowWrong.find(i => i[0] === classItem.knowName[index])
+		result.push(parseFloat(classKnowPer[3]) * 100)
+		
+		return result
+	},
+	
+	/* 学情知识点年级班级得分率数据转换 活動subject版*/
 	getKnowPercentJoint(val, subject) {
 		let stuResult = {}
 		let classResult = {}

+ 24 - 2
TEAMModelOS/ClientApp/src/view/artexam/Create.vue

@@ -51,6 +51,9 @@
           <FormItem :label="`正式测评`"  prop="time" style="margin-top: 30px">
             <DatePicker transfer type="datetimerange" v-model="artInfo.time" placement="bottom-start" :placeholder="`请设置学生正式测评时间范围`" @on-change="handleSetTime" style="width: 100%"></DatePicker>
           </FormItem>
+          <FormItem :label="`评测时间`" style="margin-top: 30px">
+            <DatePicker :options="dateOptions" transfer type="datetime" v-model="artInfo.examTime" :placeholder="`请设置评测作答的结束时间`" @on-change="handleExamEndTime" style="width: 100%" />
+          </FormItem>
           <FormItem label="学段" prop="periodType" style="margin-top: 30px" v-if="isArea">
             <Select v-model="artInfo.periodType" placeholder="请选择施测适用学段">
               <Option v-for="item in pdTypeList" :value="item.value" :key="item.value">{{ item.label }}</Option>
@@ -182,6 +185,8 @@ export default {
         startTime: 0,
         endTime: 0,
         time: [],
+        examDeadline: 0,
+        examTime: '',
         uploadTime:[],
         uploadSTime:0,
         uploadETime:0,
@@ -248,7 +253,8 @@ export default {
       },
       trainRule: {},
       allList: [],
-      quotas: []
+      quotas: [],
+      dateOptions: {},
     }
   },
   computed: {
@@ -327,7 +333,17 @@ export default {
           this.getTargetList()
         }
       }
-    }
+    },
+    'artInfo.time': {
+      handler(n, o) {
+        let that = this
+        this.dateOptions = {
+            disabledDate (date) {
+                return date && (date.valueOf() < that.artInfo.startTime || date.valueOf() > that.artInfo.endTime)
+            }
+        }
+      },
+    },
   },
   methods: {
     goBack(){
@@ -348,6 +364,11 @@ export default {
       let end = value[1]
       this.artInfo.startTime = start ? new Date(start).getTime() : 0
       this.artInfo.endTime = end ? new Date(end).getTime() : 0
+      this.artInfo.examTime = ''
+      this.artInfo.examDeadline = 0
+    },
+    handleExamEndTime(value) {
+      this.artInfo.examDeadline = value ? new Date(value).getTime() : 0
     },
     handleSetUploadTime(value) {
       let start = value[0]
@@ -517,6 +538,7 @@ export default {
           endTime: this.artInfo.endTime,
           uploadSTime: this.artInfo.uploadSTime,
           uploadETime: this.artInfo.uploadETime,
+          examDeadline: this.artInfo.examDeadline,
           subjects: this.subjectList.filter(item => this.artInfo.subjects.includes(item.id))
         }
         let promise

+ 4 - 3
TEAMModelOS/ClientApp/src/view/artexam/Mgt.vue

@@ -196,6 +196,7 @@
 				let resourcePaper = [];
 				let taskOrder = [];
 				if (!this.artInfo?.settings?.length) return;
+				let sas = await this.$tools.getBlobSas('hbcn')
 				for (let quota of this.artInfo.settings) {
 					for (let task of quota.task) {
 						if (task.type === 1 && task.infoId) {
@@ -203,7 +204,7 @@
 								let simplePapers = this.examInfoList.filter((item) => item.infoId === task.infoId);
 								let paperPromiseArr = [];
 								simplePapers.forEach((paper) => {
-									paperPromiseArr.push(this.getPaperInfo(paper));
+									paperPromiseArr.push(this.getPaperInfo(paper, sas));
 								});
 								let apiPapers = await Promise.all(paperPromiseArr);
 								let requestData = {
@@ -316,12 +317,12 @@
 					});
 			},
 			//拼接API需要的paper数据
-			getPaperInfo(simplePaper) {
+			getPaperInfo(simplePaper, sas) {
 				return new Promise(async (r, j) => {
 					try {
 						simplePaper.scope = "school";
 						simplePaper.code = "hbcn";
-						let fullPaper = await this.$evTools.getStuPaper(simplePaper);
+						let fullPaper = await this.$evTools.getStuPaperForArt(simplePaper, undefined, sas);
 						if (fullPaper) {
 							let apiPaper = {};
 							apiPaper.id = fullPaper.id;

+ 1 - 1
TEAMModelOS/ClientApp/src/view/artexam/SelectMusic.vue

@@ -25,7 +25,7 @@ export default {
   created() {
     // 链接
     // const url = this.$store.state.config.srvAdrType === 'test' ? 'https://tstamesopen.aimusic.art' : 'https://amesopen.aimusic.art'
-    const url = 'https://tmdopen.yosocloud.com'
+    const url = this.$store.state.config.srvAdrType === 'test' ? 'https://tmdopen.yosocloud.com' : 'https://musicopen.winteach.cn'
     // 智音提供的APP_ID
     const appid = '8a68f563f3384662acbc268336b98ae2'
     //第三方token

+ 1 - 1
TEAMModelOS/ClientApp/src/view/artexam/ZYMusicDetail.vue

@@ -32,7 +32,7 @@ export default {
 
     // 链接
     // const url = this.$store.state.config.srvAdrType === 'test' ? 'https://tstamesopen.aimusic.art' : 'https://amesopen.aimusic.art'
-    const url = 'https://tmdopen.yosocloud.com'
+    const url = this.$store.state.config.srvAdrType === 'test' ? 'https://tmdopen.yosocloud.com' : 'https://musicopen.winteach.cn'
     // 智音提供的APP_ID
     const appid = '8a68f563f3384662acbc268336b98ae2'
     //第三方token

+ 8 - 5
TEAMModelOS/ClientApp/src/view/evaluation/bank/ExerciseList.vue

@@ -221,7 +221,7 @@
           </div>
         </transition>
         <!-- 底部题目操作栏 -->
-        <div class="item-tools">
+        <div class="item-tools" v-if="artExamEdit">
           <span class="item-tools-info">{{$t('evaluation.filter.type')}}:{{ exersicesType[item.type] }}</span>
           <span class="item-tools-info">{{$t('evaluation.filter.diff')}}:{{ exersicesDiff[item.level - 1] }}</span>
           <span class="item-tools-info">{{$t('evaluation.filter.level')}}:{{ exersicesField[item.field - 1] }}</span>
@@ -336,7 +336,7 @@ export default {
     onSelectAll() {
       this.selectedArr = this.isSelectAll ? this._.cloneDeep(this.exerciseList) : []
       console.log(this.isSelectAll, this.ids)
-      this.$emit('chooseQuChange', this.selectedArr)
+      if(this.artExamEdit) this.$emit('chooseQuChange', this.selectedArr)
     },
     /** 获取区班校信息 */
     getSchoolInfo() {
@@ -386,7 +386,7 @@ export default {
         }
       }
       this.isSelectAll = this.selectedArr.length === this.exerciseList.length
-      this.$emit('chooseQuChange', this.selectedArr)
+      if(this.artExamEdit) this.$emit('chooseQuChange', this.selectedArr)
     },
 
     /* 手动保存 */
@@ -424,7 +424,7 @@ export default {
     },
 
     getTags() {
-      this.selectTags = []
+      // this.selectTags = []
       let params = {
         // "@ASC": this.filterSort,
         code: (this.isShowSchoolBank || (this.filterOrigin === this.schoolCode)) ? this.$store.state.userInfo.schoolCode :
@@ -1019,7 +1019,10 @@ export default {
     //已选题目的id
     ids() {
       return this.selectedArr.map(i => i.id)
-    }
+    },
+    artExamEdit() {
+      return this.$store.state.config.srvAdrType === "product" && this.$store.state.userInfo.hasSchool && this.$store.state.userInfo.schoolCode === 'hbcn' && this.subjectList.length && (this.subjectList[this.filterSubject].id === '9d6581ab-1819-973f-e8cc-5a797b2806e4' || this.subjectList[this.filterSubject].id === '0787a5e1-f5a4-8dc5-48dd-b97ee2f1bd1d') ? this.$store.state.userInfo.TEAMModelId === '1595321354' : true
+    },
   },
 
 };

+ 28 - 4
TEAMModelOS/ClientApp/src/view/evaluation/bank/ExerciseNoFilter.vue

@@ -32,7 +32,7 @@
                 <div class="action-tool">
                     <Checkbox v-model="isAllOpen" @on-change="onHandleToggle">{{ $t('evaluation.exerciseList.showAllAnswer') }}</Checkbox>
                 </div>
-                <!-- <div class="action-tool">
+                <div class="action-tool">
                     <div style="display: flex; align-items: center;">
                         <span style="margin-right: 10px;">{{ $t('evaluation.exerciseList.relatePoints') }}:</span>
                         <Tag v-for="(item,index) in relatePoints" :key="item" :name="item" closable @on-close="handleClosePoint(index)">{{ item }}</Tag>
@@ -46,7 +46,7 @@
                         <Option v-for="item in tags" :value="item" :key="item">{{ item }}</Option>
                         </Select>
                     </div>
-                </div> -->
+                </div>
             </div>
             <div class="action-tools" style="display:flex; align-items: center;">
                 <div class="action-tool">
@@ -165,6 +165,9 @@
                 <Button type="success" :loading="editLoading" @click="doSaveEdit">{{$t('evaluation.confirm')}}</Button>
             </div>
         </Modal>
+        <Modal v-model="selectPointsModal" :title="$t('evaluation.newExercise.choosePoint')" ref="pointRef" footer-hide width="600px" class="related-point-modal" style="z-index: 99999">
+            <BasePoints v-if="selectPointsModal" :period="periodList[filterPeriod].id" :subject="subjectList[filterSubject].id" @onCheckChange="onCheckChange" @onCancel="selectPointsModal = false" :points="relatePoints" scope="school"></BasePoints>
+        </Modal>
     </div>
 </template>
 
@@ -199,6 +202,9 @@ export default {
             editLoading: false,
             filterParams: {},
             itemIndex: -1,
+            relatePoints: [],
+            selectPointsModal: false,
+            selectTags: []
         }
     },
     created() {
@@ -253,9 +259,9 @@ export default {
                 type: [],
                 field: [],
                 scope: 'school',
-                "knowledge[*]": [],
+                "knowledge[*]": this.relatePoints,
                 pid: null,
-                "tags[*]": []
+                "tags[*]": this.selectTags
             };
             if (this.isDesc) {
                 this.filterParams['@DESC'] = 'createTime'
@@ -318,6 +324,7 @@ export default {
             this.dataLoading = false
         },
         getTags() {
+            this.selectTags = []
             let params = {
                 '@DESC': 'createTime',
                 // "@ASC": this.filterSort,
@@ -452,6 +459,23 @@ export default {
             })
 
         },
+        onCheckChange(val) {
+            this.relatePoints = val;
+            this.selectPointsModal = false
+            this.doFilter()
+            // this.getTags()
+        },
+        onRelatePoint() {
+            this.selectPointsModal = true
+        },
+        handleClosePoint(index) {
+            this.relatePoints.splice(index, 1)
+            this.doFilter()
+            // this.getTags()
+        },
+        changeTag(val) {
+            this.doFilter()
+        },
     }
 }
 </script>

+ 46 - 8
TEAMModelOS/ClientApp/src/view/evaluation/bank/TestPaperList.vue

@@ -28,10 +28,12 @@
             @on-change="onSearchChange" />
         </div>
       </div>
-      <div class="action-tools">
-        <span>{{ $t('evaluation.exerciseList.totalTip1') }}<span
-            style="font-size: 18px;color: #ff0206;margin: 0 10px;font-weight: bold;">{{ totalNum }}</span>{{
-              $t('unit.text3') }}</span>
+      <div class="action-tools check-all" style="display:flex; align-items: center;">
+        <Checkbox v-model="isSelectAll" @on-change="onSelectAll" v-if="$access.can('admin.*||exercise-upd')">{{ $t('evaluation.choosePageItems') }}</Checkbox>
+        <span style="margin-left:20px">{{ $t('evaluation.exerciseList.totalTip1') }}
+          <span style="font-size: 18px;color: #ff0206;margin: 0 10px;font-weight: bold;">{{ totalNum }}</span>
+          {{ $t('unit.text3') }}
+        </span>
       </div>
     </div>
 
@@ -79,7 +81,7 @@
 							<span class="info-bold" v-for="(tag,tagIndex) in paper.tags"><Tag color="blue">{{ tag }}</Tag></span>
 						</span> -->
           </div>
-          <div class="paper-item-tools" v-if="!chooseModel">
+          <div class="paper-item-tools" v-if="!chooseModel && artExamEdit">
             <span class="paper-item-tools-edit" @click.stop="goToDownload(paper)">
               <Icon type="md-download" />
               <span>{{ $t('syllabus.download') }}</span>
@@ -98,6 +100,10 @@
               <Icon type="md-trash" />
               <span>{{ $t('evaluation.deleteItem') }}</span>
             </span>
+            <Button :type="ids.indexOf(paper.id) > -1 ? 'warning' : 'info'" @click.stop="handleChoose(paper)"
+              v-if="($access.can('admin.*||exercise-upd') || !isSchool)" style="margin-left: 20px;">
+              {{ ids.indexOf(paper.id) > -1 ? $t('evaluation.removeItem') : $t('auth.select') }}
+            </Button>
           </div>
           <div v-if="chooseModel" class="paper-item-select">
             <div v-if="!singleChoose">
@@ -234,7 +240,9 @@ export default {
         subjectCountArr: [],
         periodCountArr: []
       },
-      isShowSchoolBank: false
+      isShowSchoolBank: false,
+      delPaperList: [],
+      isSelectAll: false
     }
   },
   created() {
@@ -604,7 +612,27 @@ export default {
       })
       console.log('要下载的试卷', fullPaperJson)
     },
-
+    
+    onSelectAll() {
+      this.delPaperList = this.isSelectAll ? this._.cloneDeep(this.paperList) : []
+      console.log(this.isSelectAll, this.ids)
+      if(this.artExamEdit) this.$emit('chooseQuChange', this.delPaperList)
+    },
+    /* 选题操作 */
+    handleChoose(item) {
+      if (this.ids.indexOf(item.id) == -1) {
+        this.delPaperList.push(item) //添加题目
+      } else {
+        for (let i = 0; i < this.delPaperList.length; i++) {
+          if (this.delPaperList[i].id == item.id) {
+            this.delPaperList.splice(i, 1) //移除
+            break
+          }
+        }
+      }
+      this.isSelectAll = this.delPaperList.length === this.paperList.length
+      if(this.artExamEdit) this.$emit('chooseQuChange', this.delPaperList)
+    },
     /**
      * 删除试卷
      * @param item
@@ -742,7 +770,14 @@ export default {
   computed: {
     isSchool() {
       return this.filterParams.scope === 'school' ? true : false
-    }
+    },
+    //已选题目的id
+    ids() {
+      return this.delPaperList.map(i => i.id)
+    },
+    artExamEdit() {
+      return this.$store.state.config.srvAdrType === "product" && this.$store.state.userInfo.hasSchool && this.$store.state.userInfo.schoolCode === 'hbcn' && (this.$refs.baseFilter.filterParams.subjectId[0] === '9d6581ab-1819-973f-e8cc-5a797b2806e4' || this.$refs.baseFilter.filterParams.subjectId[0] === '0787a5e1-f5a4-8dc5-48dd-b97ee2f1bd1d') ? this.$store.state.userInfo.TEAMModelId === '1595321354' : true
+    },
   }
 }
 </script>
@@ -768,6 +803,9 @@ export default {
 .pl-container .ivu-checkbox-inner {
   display: none !important;
 }
+.check-all .ivu-checkbox-inner {
+  display: inline-block !important;
+}
 
 .random-pick-modal .question-condition-wrap .question-condition-item .condition-label,
 .random-pick-modal .question-condition-wrap .question-condition-item {

+ 91 - 31
TEAMModelOS/ClientApp/src/view/evaluation/bank/index.vue

@@ -9,7 +9,7 @@
 				<ExerciseList ref="exList" @chooseQuChange="onItemCheckChange"></ExerciseList>
 			</TabPane>
 			<TabPane :label="$t('evaluation.index.paper')" name="paper" tab="listTab">
-				<PaperList ref="paperList" @onPaperClick="isShowBackList = true" @onBackToTop="onBackToTop" @editQuickPaper="editQuickPaper"></PaperList>
+				<PaperList ref="paperList" @chooseQuChange="onItemCheckChange" @onPaperClick="isShowBackList = true" @onBackToTop="onBackToTop" @editQuickPaper="editQuickPaper"></PaperList>
 			</TabPane>
 		</Tabs>
 		<div class="ev-list-operation">
@@ -19,7 +19,7 @@
 						<Icon type="md-arrow-back" size="16" />
 						<span>{{ $t("evaluation.index.backList") }}</span>
 					</span>
-					<span @click="onEditPaper" class="bank-tools-btn" v-if="isShowBackList">
+					<span @click="onEditPaper" class="bank-tools-btn" v-if="isShowBackList && !artExamEdit">
 						<Icon type="ios-create" size="16" />
 						<span>{{ $t("evaluation.paperList.editPaper") }}</span>
 					</span>
@@ -46,7 +46,7 @@
 						</span>
 					</div>
 				</div>
-				<span @click="doBatchDelete" v-show="currentTab === 'exercise' && ($access.can('admin.*||exercise-upd') || !isSchool) && checkItemArr.length" class="bank-tools-btn" style="color: #ff4346 !important">
+				<span @click="doBatchDelete" v-show="($access.can('admin.*||exercise-upd') || !isSchool) && checkItemArr.length" class="bank-tools-btn" style="color: #ff4346 !important">
 					<Icon type="md-trash" size="16" />
 					<span style="display: inline-block">{{ $t("teachContent.delBatch") }}</span>
 				</span>
@@ -180,45 +180,99 @@
 			},
 			/* 批量删除 */
 			doBatchDelete() {
-				let exListVm = this.$refs.exList;
+				if(this.currentTab === 'exercise') {
+					let exListVm = this.$refs.exList;
 
-				this.$Modal.confirm({
-					title: this.$t("evaluation.newExercise.modalTip"),
-					content: this.$t("stuAccount.tips2Content3"),
-					onOk: () => {
-						let ids = this.checkItemArr.map((i) => i.id);
-						let item = this.checkItemArr[0];
-						exListVm.dataLoading = true;
-						this.$api.newEvaluation
-							.DeleteExamItem({
-								ids: ids,
+					this.$Modal.confirm({
+						title: this.$t("evaluation.newExercise.modalTip"),
+						content: this.$t("stuAccount.tips2Content3"),
+						onOk: () => {
+							let ids = this.checkItemArr.map((i) => i.id);
+							let item = this.checkItemArr[0];
+							exListVm.dataLoading = true;
+							this.$api.newEvaluation
+								.DeleteExamItem({
+									ids: ids,
+									code: item.code,
+									scope: item.scope
+								})
+								.then((res) => {
+									if (res.code == 200) {
+										exListVm.isSelectAll = false;
+										exListVm.selectedArr = [];
+										exListVm.doFilter();
+										this.checkItemArr = [];
+										this.$Message.success(this.$t("evaluation.deleteSuc"));
+									} else {
+										this.$Message.warning(this.$t("evaluation.deleteFail"));
+										exListVm.dataLoading = false;
+									}
+								})
+								.catch((err) => {
+									console.log(err);
+									this.$Message.warning(this.$t("evaluation.deleteFail"));
+									exListVm.dataLoading = false;
+								});
+						}
+					});
+				} else {
+					let paperListVm = this.$refs.paperList
+					paperListVm.dataLoading = true;
+					let promiseArr = []
+					this.checkItemArr.forEach(item => {
+						promiseArr.push(new Promise((r, j) => {
+							this.$api.learnActivity.DeleteExamPaper({
+								id: item.id,
 								code: item.code,
 								scope: item.scope
-							})
-							.then((res) => {
-								if (res.code == 200) {
-									exListVm.isSelectAll = false;
-									exListVm.selectedArr = [];
-									exListVm.doFilter();
-									this.checkItemArr = [];
-									this.$Message.success(this.$t("evaluation.deleteSuc"));
+							}).then(async res => {
+								if (!res.error) {
+									this.$api.blob.deletePrefix({
+										"cntr": item.scope === 'school' ? this.$store.state.userInfo.schoolCode : this.$store.state.userInfo.TEAMModelId,
+										"prefix": "paper/" + item.name
+									}).then((res) => {
+										if (!res.error) {
+											r(true)
+										} else {
+											j(false)
+										}
+									},(err) => {
+										j(false)
+									})
 								} else {
-									this.$Message.warning(this.$t("evaluation.deleteFail"));
-									exListVm.dataLoading = false;
+									j(false)
 								}
+							}).catch(err => {
+								j(false)
 							})
-							.catch((err) => {
-								console.log(err);
-								this.$Message.warning(this.$t("evaluation.deleteFail"));
-								exListVm.dataLoading = false;
-							});
-					}
-				});
+						}))
+					})
+					Promise.allSettled(promiseArr).then(result => {
+						paperListVm.isSelectAll = false;
+						paperListVm.delPaperList = [];
+						paperListVm.doFilter();
+						this.checkItemArr = [];
+						this.$Message.success(this.$t("evaluation.deleteSuc"));
+						paperListVm.dataLoading = false;
+					}).catch(e => {
+						console.log(e);
+						this.$Message.warning(this.$t("evaluation.deleteFail"));
+						paperListVm.dataLoading = false;
+					})
+				}
 			},
 			onBackToTop() {
 				this.$refs.bankContainer.scrollIntoView();
 			},
 			onTabClick(val) {
+				if(this.currentTab === 'exercise') {
+					this.$refs.exList.isSelectAll = false;
+					this.$refs.exList.selectedArr = [];
+				} else {
+					this.$refs.paperList.isSelectAll = false;
+					this.$refs.paperList.delPaperList = [];
+				}
+				this.checkItemArr = []
 				this.currentTab = val;
 				this.onBackToTop();
 			},
@@ -387,6 +441,9 @@
 			if (this.$route.params.tabName) {
 				this.currentTab = this.$route.params.tabName;
 				this.tabName = this.$route.params.tabName;
+			} else if(this.$route.name === "personalBank") {
+				this.currentTab = 'paper'
+				this.tabName = 'paper'
 			}
 
 			if (this.$route.name === "schoolBank" && this.$route.params.activePeriod) {
@@ -423,6 +480,9 @@
 			showType() {
 				return this.$store.state.config.srvAdrType === 'test' && this.$store.state.userInfo.TEAMModelId === '1595321354' && this.$store.state.userInfo.schoolCode === 'hbcn'
 			},
+			artExamEdit() {
+				return this.$refs?.paperList.curPaper?.subjectId === '9d6581ab-1819-973f-e8cc-5a797b2806e4' || this.$refs?.paperList.curPaper?.subjectId === '0787a5e1-f5a4-8dc5-48dd-b97ee2f1bd1d'
+			},
 		},
 		beforeRouteLeave(to, from, next) {
 			// 如果是从试卷库预览跳转到生成答题卡 则需要保留试卷库页面的缓存状态

+ 5 - 5
TEAMModelOS/ClientApp/src/view/evaluation/components/BaseExerciseList.vue

@@ -811,11 +811,11 @@
 					this.modifyItems[existIndex] = item;
 				}
 
-				if (this.viewModel === "list" && this.orderList.length) {
-					let preOrderList = JSON.parse(JSON.stringify(this.orderList[0].list));
-					preOrderList.splice(this.curOrderIndex, 1, item);
-					this.paper.item = preOrderList;
-				}
+				//if (this.viewModel === "list" && this.orderList.length) {
+				//	let preOrderList = JSON.parse(JSON.stringify(this.orderList[0].list));
+				//	preOrderList.splice(this.curOrderIndex, 1, item);
+				//	this.paper.item = preOrderList;
+				//}
 				/* 如果编辑好的题目是之前错误的题目 则移除掉 */
 				let isErrorItemIndex = this.errorList.findIndex((i) => i.id === item.id);
 				if (isErrorItemIndex > -1) {

+ 83 - 10
TEAMModelOS/ClientApp/src/view/evaluation/components/BasePoints.vue

@@ -29,14 +29,27 @@
       <Icon v-if="treeList.length" type="md-add-circle" color="#40A8F0" size="16" :title="$t('knowledge.addPoint')" style="cursor:pointer; margin-left: 5px;" @click="getKnowledgeTree()" />
       <span v-else>{{ $t('knowledge.addTreeTips') }}</span>
     </div>
-
-    <div style="margin-top: 20px;">
-      <Loading hideMask v-show="isLoading"></Loading>
-      <Input v-special-char icon="ios-close" v-model="searchSchoolPoint" :placeholder="$t('evaluation.points.searchPoint')" autofocus style="width: 98%;margin-left:1%;margin-bottom:10px" @on-change="onSearchSchoolChange" @on-enter="onSearchSchoolChange" />
-      <div v-if="schoolPointList.length === 0" style="margin:5px">{{$t('evaluation.points.noPoint')}}</div>
-      <CheckboxGroup v-model="checkedList" @on-change="onCheckChange" v-else>
-        <Checkbox v-for="(item,index) in schoolPointList" :label="item" :key="item">{{ item }}</Checkbox>
-      </CheckboxGroup>
+    <div>
+      <div style="margin-top: 7px; margin-left: 5px;">
+        <span>{{ $t('knowledge.preFormat') }}:</span>
+        <span>{{ $t('knowledge.layFlat') }}</span>
+        <i-switch size="small" v-model="pointShowType" @on-change="typeChange" style="margin: 0 5px;" />
+        <span>{{ $t('knowledge.tree') }}</span>
+      </div>
+      <div style="margin-top: 10px;" v-show="!pointShowType">
+        <Loading hideMask v-show="isLoading"></Loading>
+        <Input v-special-char icon="ios-close" v-model="searchSchoolPoint" :placeholder="$t('evaluation.points.searchPoint')" autofocus style="width: 98%;margin-left:1%;margin-bottom:10px" @on-change="onSearchSchoolChange()" @on-enter="onSearchSchoolChange()" />
+        <div v-if="schoolPointList.length === 0" style="margin:5px">{{$t('evaluation.points.noPoint')}}</div>
+        <CheckboxGroup v-model="checkedList" @on-change="onCheckChange" v-else>
+          <Checkbox v-for="(item,index) in schoolPointList" :label="item" :key="item">{{ item }}</Checkbox>
+        </CheckboxGroup>
+      </div>
+      <div style="margin-top: 20px;" v-show="pointShowType">
+        <Loading hideMask v-show="isLoading"></Loading>
+        <Input v-special-char icon="ios-close" v-model="searchSchoolPointTree" :placeholder="$t('evaluation.points.searchPoint')" autofocus style="width: 98%;margin-left:1%;margin-bottom:10px" @on-change="onSearchSchoolChange(true)" @on-enter="onSearchSchoolChange(true)" />
+        <div v-if="schoolPointTree.length === 0" style="margin:5px">{{$t('evaluation.points.noPoint')}}</div>
+        <el-tree ref="treeRef" :data="schoolPointTree" :props="defaultProps" :filter-node-method="filterNode" @check-change="handleCheckChange" node-key="id" accordion check-strictly show-checkbox v-else></el-tree>
+      </div>
     </div>
 
     <div class="points-checked-wrap">
@@ -145,6 +158,9 @@ export default {
           children: 'children',
           label: 'name'
       },
+      pointShowType: false,
+      schoolPointTree: [],
+      searchSchoolPointTree: '',
     }
   },
   created() {
@@ -255,12 +271,14 @@ export default {
           if (!res.error && res.knowledgeTrees.length) {
             this.curPointResponse = res.knowledgeTrees[0]
             this.schoolPointList = res.knowledgeTrees[0].points
+            this.schoolPointTree = res.knowledgeTrees[0].tree
             this.originSchoolList = res.knowledgeTrees[0].points
             this.originPointList = this.originPointList.concat(res.knowledgeTrees[0].points)
           } else {
             this.$Message.warning(this.$t('evaluation.noData'),)
             this.uuid = Math.uuid()
             this.schoolPointList = []
+            this.schoolPointTree = []
             this.originSchoolList = []
             this.originPointList = []
             this.curPointResponse = {
@@ -379,8 +397,12 @@ export default {
       this.checkedList.splice(index, 1)
     },
 
-    onSearchSchoolChange() {
-      this.schoolPointList = this.originSchoolList.filter(item => item.toUpperCase().indexOf(this.searchSchoolPoint.toUpperCase()) > -1 || item.toLowerCase().indexOf(this.searchSchoolPoint.toLowerCase()) > -1 || item.indexOf(this.searchSchoolPoint) > -1)
+    onSearchSchoolChange(isTree) {
+      if(!isTree) {
+        this.schoolPointList = this.originSchoolList.filter(item => item.toUpperCase().indexOf(this.searchSchoolPoint.toUpperCase()) > -1 || item.toLowerCase().indexOf(this.searchSchoolPoint.toLowerCase()) > -1 || item.indexOf(this.searchSchoolPoint) > -1)
+      } else if(this.schoolPointTree.length) {
+        this.$refs.treeRef.filter(this.searchSchoolPointTree)
+      }
     },
     getKnowledgeTree() {
       let params = {
@@ -418,6 +440,57 @@ export default {
         })
         this.newPointName = ''
     },
+    typeChange(value) {
+      this.getPointPing(this.checkedList)
+    },
+    handleCheckChange(data, checked, indeterminate) {
+      if(checked && !this.checkedList.includes(data.name)) {
+        this.checkedList.push(data.name)
+      } else if(!checked) {
+        let arr = this.$refs.treeRef.getCheckedNodes()
+        if(!arr.find(item => item.name === data.name)) {
+          this.checkedList = this.checkedList.filter(item => item !== data.name)
+        }
+      }
+    },
+    flattenObjects(arr) {
+      return arr.reduce((acc, val) => {
+        if (val.children && val.children.length) {
+          // 如果当前元素是数组,则递归地进行平铺
+          acc.push(...this.flattenObjects(val.children));
+        } else {
+          // 如果当前元素是对象,则直接添加到结果数组中
+          let info = {
+            allcids: val.allcids,
+            id: val.id,
+            level: val.level,
+            link: val.link,
+            name: val.name,
+            pid: val.pid,
+            subcids: val.subcids,
+            tid: val.tid,
+            used: val.used
+          }
+          acc.push(info);
+        }
+        return acc;
+      }, []);
+    },
+    getPointPing(points) {
+      if(!this.schoolPointTree.length) return
+      let arr = this.flattenObjects(this.schoolPointTree)
+      let checkNodes = []
+      arr.forEach(item => {
+        if(points.includes(item.name)) {
+          checkNodes.push(item)
+        }
+      });
+      this.$refs.treeRef.setCheckedNodes(checkNodes)
+    },
+    filterNode(value, data) {
+      if (!value) return true
+      return data.name.indexOf(value) !== -1
+    }
   },
   mounted() {
     if (this.points.length) {

+ 1 - 1
TEAMModelOS/ClientApp/src/view/learnactivity/tabs/htCloudDAS.vue

@@ -35,7 +35,7 @@
           </div>
         </div>
         <div class="class-tab" v-if="!(activeBar === 2 || isShowQuestionList)">
-          <span v-for="(item,index) in classList" :value="index" :key="item" @click="onClassChange(item,index)" :class="['class-tab-item',activeClassIndex == index ? 'data-select-active' : '']">{{ item }}</span>
+          <span v-for="(item,index) in classList" :value="index" :key="item + index" @click="onClassChange(item,index)" :class="['class-tab-item',activeClassIndex == index ? 'data-select-active' : '']">{{ item }}</span>
         </div>
         <AchievementAnalysis v-if="activeBar == 0 && activeClassIndex == 0"></AchievementAnalysis>
         <SingleClassView v-else-if="activeBar == 0 && activeClassIndex > 0"></SingleClassView>

+ 6 - 4
TEAMModelOS/ClientApp/src/view/student-account/import/StuImport.vue

@@ -350,6 +350,7 @@ export default {
     doConfirm() {
       if (this.importLoading) return
       this.importLoading = true
+      this.importing = false
       //整理TtableData
       let temp = this.tableData.map((item) => {
         item.classNo = item.classId
@@ -383,16 +384,17 @@ export default {
               }
             }, (err) => {
 
+            }).finally(() => {
+              this.importLoading = false
             })
           } else {
+            this.importLoading = false
             this.$Message.info(this.$t('stuAccount.importErr'))
           }
         }, (err) => {
-
+          this.importLoading = false
         }
-      ).finally(() => {
-        this.importLoading = false
-      })
+      )
     },
     async importAgain() {
       this.viewStatus = 'tips'

+ 23 - 16
TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/KnowledgeAnalysis/htKnowledgeAnalysis.vue

@@ -106,14 +106,14 @@
                 for (let i = 0; i < pointList.length; i++) {
                     let o = {}
                     o.name = pointList[i]
-                    o.gradeRate = data.stupercent.grade[i]
+                    //o.gradeRate = data.stupercent.grade[i]
                     for (let j = 0; j < origin.className.length; j++) {
                         o[origin.className[j]] = origin[pointList[i]][j]
                     }
                     arr.push(o)
                 }
                 this.renderClassColumns(this.getAnalysisJson,origin)
-				console.log(arr)
+				console.log(arr)                
                 this.knowledgeData = arr
             }
 
@@ -133,22 +133,29 @@
 
         computed: {
             // 获取最新散点图数据
-            getKnowledgeData() {       
-                // 因應結構不同 重新統計得分率
-				let curSubjectIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.id).indexOf(this.$store.state.totalAnalysis.currentSubjectJoint)
-                let AllpointKey = this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].pointKey
-                let pointList = this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[6].pointKey.pointList                
-                let pointLevelKey = this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey
-                for(let i = 0;i<pointLevelKey.length;i++){
-                    for(let j = 0;j<pointList.length;j++){
-                        if(pointLevelKey[i].pointKey.classpercent[pointList[j]] && pointLevelKey[i].pointKey.classpercent[pointList[j]].length>0){
-                            AllpointKey.classpercent[pointList[j]].push(pointLevelKey[i].pointKey.classpercent[pointList[j]][0]);                       
-                        }                      
-                   }
+            getKnowledgeData() {                  
+                // 初始資料架構
+                let AllpointKey = {
+                    classpercent:{
+                        className:[]
+                        },
+                    pointList:this.$store.state.totalAnalysis.analysisJsonJoint.classes[0].pointLevelKey.pointKey.pointList 
                 }
+                // 加入每個知識點為一個陣列
+                AllpointKey.pointList.forEach(pointListItem => {                                               
+                        AllpointKey.classpercent[pointListItem] = []
+                        })
 
-                //return this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].pointKey
-                return AllpointKey
+                this.$store.state.totalAnalysis.analysisJsonJoint.classes.forEach((classItem, index) => {
+                    AllpointKey.classpercent.className.push(classItem.className)
+                    AllpointKey.pointList.forEach(pointListItem => {
+                        // 取得目標知識點得分率
+                        let classKnowPer = classItem.knowWrong.find(i => i[0] === pointListItem)
+                        AllpointKey.classpercent[pointListItem].push(parseFloat(classKnowPer[3]) * 100)
+                    })
+                })
+                
+                return AllpointKey              
             },
 			
 			getAnalysisJson() {

+ 52 - 11
TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/KnowledgeAnalysis/htScoreDetails.vue

@@ -232,8 +232,6 @@ export default {
             //let kScoresItem = classesItem.kScore
             let knowStuPer = classesItem.knowStuPer.find(i => i.key === stu.id)
             if (stu.classId === classesItem.classId) {
-
-
               result.push([
                 stu.name,
                 stu.className,
@@ -245,12 +243,8 @@ export default {
               ])
             }
           })
-
-
           stuResult[item] = result
-
         })
-
       }
       
       // 取当前年级在每个知识点的得分 除以知识点的总分 得到年级在该知识点的得分率
@@ -271,10 +265,16 @@ export default {
     },
 
     doRenderWrong(data) {
-      let classIndex = this.$store.state.totalAnalysis.curClassIndex
-      let origin = data.wrong
+      // let classIndex = this.$store.state.totalAnalysis.curClassIndex
+      // let origin = data.wrong
+      // let allWrongData = this.$tools.jsonTransform({ datas: origin.datas, keys: origin.keys })
+      
+      // this.numData = classIndex === -1 ? allWrongData : this.getClassWrongData(allWrongData, classIndex)
+      
+      let origin = data
       let allWrongData = this.$tools.jsonTransform({ datas: origin.datas, keys: origin.keys })
-      this.numData = classIndex === -1 ? allWrongData : this.getClassWrongData(allWrongData, classIndex)
+      
+      this.numData = allWrongData
     },
 
     getClassWrongData(allWrongData) {
@@ -308,8 +308,49 @@ export default {
   computed: {
     // 获取最新散点图数据
     getKnowledgeData() {      
-      let curSubjectIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.id).indexOf(this.$store.state.totalAnalysis.currentSubjectJoint)
-      return this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].pointKey
+      // 如果class = -1  要重新統計全部班級的knowWrong
+      let classIndex = this.$store.state.totalAnalysis.curClassIndex
+      // 初始物件
+      let knowWrongAll = {
+          datas : [],
+          pointList : this.$store.state.totalAnalysis.analysisJsonJoint.classes[0].knowName,
+          keys: [
+              "name",
+              "point",
+              "itemNO",
+              "persent",
+              "wrong",
+              "rhw",
+              "rlw"
+            ]
+        }
+      if(classIndex === -1){ // 全部班級
+        knowWrongAll.datas = this.$store.state.totalAnalysis.analysisJsonJoint.classes[0].knowWrong
+        //  平均得分率 跟 錯題人次 都先歸零  其他通用
+        knowWrongAll.datas.forEach(item => {
+          item[3] = 0
+          item[4] = 0
+        })
+        // 匯總 平均得分率 跟 錯題人次
+        this.$store.state.totalAnalysis.analysisJsonJoint.classes.forEach(classItem => {
+          knowWrongAll.datas.forEach(knowWrongAllItem => {            
+            let knowWrongItem = classItem.knowWrong.find(k => k[0] == knowWrongAllItem[0])
+            knowWrongAllItem[3] = parseFloat(knowWrongAllItem[3]) + parseFloat(knowWrongItem[3])
+            knowWrongAllItem[4] = parseFloat(knowWrongAllItem[4]) + parseFloat(knowWrongItem[4])
+          })         
+        })
+        // 平均得分率總和再平均一次班級
+        knowWrongAll.datas.forEach(knowWrongAllItem => {            
+            knowWrongAllItem[3] = knowWrongAllItem[3] / this.$store.state.totalAnalysis.analysisJsonJoint.classes.length
+            
+          })        
+      }else{
+        knowWrongAll.datas = this.$store.state.totalAnalysis.analysisJsonJoint.classes[classIndex].knowWrong
+      }
+
+      return knowWrongAll    
+      // let curSubjectIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.id).indexOf(this.$store.state.totalAnalysis.currentSubjectJoint)
+      // return this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].pointKey
     },
     getAnalysisJson() {
       return this.$store.state.totalAnalysis.analysisJsonJoint

+ 24 - 14
TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/LevelAnalysis/htLevelAnalysis.vue

@@ -98,7 +98,7 @@
                 for (let i = 0; i < pointList.length; i++) {
                     let o = {}
                     o.name = pointList[i]
-                    o.gradeRate = data.stupercent.grade[i]
+                    //o.gradeRate = data.stupercent.grade[i]
                     for (let j = 0; j < origin.className.length; j++) {
                         o[origin.className[j]] = origin[pointList[i]][j]
                     }
@@ -125,19 +125,29 @@
 
         computed: {
             // 获取最新散点图数据
-            getLevelData() {
-				let curSubjectIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.id).indexOf(this.$store.state.totalAnalysis.currentSubjectJoint)
-				let levelJson = this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].levelKey
-				let transArr = this.$GLOBAL.EXERCISE_LEVELS()
-				levelJson.pointList = levelJson.pointList.map((i,index) => transArr[index])
-				for(let key in levelJson.classpercent){
-					if(!isNaN(key)){
-						let newKey = transArr[+key - 1]
-						levelJson.classpercent[newKey] = levelJson.classpercent[key]
-						levelJson.stupercent[newKey] = levelJson.stupercent[key]
-					}
-				}
-				return levelJson
+            getLevelData() {			
+                 // 初始資料架構
+                let AllpointKey = {
+                    classpercent:{
+                        className:[]
+                        },
+                    pointList:this.$store.state.totalAnalysis.analysisJsonJoint.classes[0].pointLevelKey.levelKey.pointList 
+                }
+                // 加入每個認知層次為一個陣列
+                AllpointKey.pointList.forEach(pointListItem => {                                               
+                        AllpointKey.classpercent[pointListItem] = []
+                        })
+
+                this.$store.state.totalAnalysis.analysisJsonJoint.classes.forEach((classItem, index) => {
+                    AllpointKey.classpercent.className.push(classItem.className)
+                    AllpointKey.pointList.forEach(pointListItem => {
+                        // 取得目標認知層次得分率
+                        let classLevelPer = classItem.fieldWrong.find(i => i[0] === pointListItem.toString())
+                        AllpointKey.classpercent[pointListItem].push(parseFloat(classLevelPer[3]) * 100)
+                    })
+                })
+                
+                return AllpointKey              
             },
 			getAnalysisJson() {
 			    return this.$store.state.totalAnalysis.analysisJsonJoint

+ 122 - 66
TEAMModelOS/ClientApp/src/view/student-analysis/total-analysis/LevelAnalysis/htScoreDetails.vue

@@ -167,73 +167,99 @@ export default {
       this.doRender(this.getLevelData, this.currentPoint, this.curClassIndex)
     },
 
+    // doRender(data, point) {
+    //   let classIndex = this.$store.state.totalAnalysis.curClassIndex
+    //   let origin = data.stupercent
+    //   let keys = origin.keys
+    //   let datas = classIndex === -1 ? (origin[point] || []) : origin[point].filter(i => i[1] === this.classList[classIndex + 1])
+    //   this.tableData = this.$tools.jsonTransform({
+    //     datas: datas,
+    //     keys: keys
+    //   })
+    // },
     doRender(data, point) {
-      //data = this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].pointKey
-     
-      let classIndex = this.$store.state.totalAnalysisJoint.curClassIndex
-      let classesItem = this.$store.state.totalAnalysisJoint.classes[classIndex]
-      //===========================================
-      let val = this.$store.state.totalAnalysis.analysisJsonJoint
-      let stuResult = {}
-      let knowNameItem = classesItem.knowName
-      //let knowAllperItem = classesItem.knowStuPer
-      let wrongItem = classesItem.knowWrong
-      knowNameItem.value.forEach((item, index) => {
-        //stuResult[item] = this.getKnowStuPercentJoint(val, subject, index)
-        let result = []
-        val.students.forEach(stu => {
-          // 如果該學生的課程/科目存在  而且 id 相同才加入
-          //let stuSubjectItem = stu.subjects.find(i => i.id === subject.id)
-          //let kScoresItem = classesItem.kScore
-          let knowStuPer = classesItem.knowStuPer.find(i => i.key === stu.id)
-          if (stu.classId === classesItem.classId) {
+     let stuResult = {}      
+     this.currentPoint = point
+     this.pointName= this.$GLOBAL.EXERCISE_LEVELS()[point-1]
 
-            
-            result.push([
-              stu.name,
-              stu.className,
-              stu.no || '-',
-              kScoresItem[index],
-              //stuSubjectItem.point[index],
-              knowStuPer[index],
-              kScoresItem[index] == 0 ? 0 : (knowStuPer[index] / kScoresItem[index]).toFixed(2)
-            ])
-          }
+      let val = this.$store.state.totalAnalysis.analysisJsonJoint
+      let classIndex = this.$store.state.totalAnalysis.curClassIndex
+      if (classIndex === -1) {
+        this.$store.state.totalAnalysis.analysisJsonJoint.classes.forEach((classesItem, classesindex) => {         
+          let levelNameItem = classesItem.pointLevelKey.levelKey.level  
+          let fScoresItem = classesItem.fScore
+          levelNameItem.forEach((item, index) => {            
+            let result = []
+            val.students.forEach(stu => {             
+              let fieldStuPer = classesItem.fieldStuPer.find(i => i.key === stu.id)
+              if (stu.classId === classesItem.classId) {
+                result.push([
+                  stu.name,
+                  stu.className,
+                  stu.no || '-',
+                  fScoresItem[index],                  
+                  fieldStuPer.value[index],
+                  fScoresItem[index] == 0 ? 0 : (fieldStuPer.value[index] / fScoresItem[index]).toFixed(2)
+                ])
+              }
+            })
+            if(stuResult[item]){
+              stuResult[item].push(...result)
+            }else{
+              stuResult[item] = result
+            }            
+          })
         })
+      } else {
+        let classesItem = this.$store.state.totalAnalysis.analysisJsonJoint.classes[classIndex]
+        //===========================================
+        let val = this.$store.state.totalAnalysis.analysisJsonJoint
 
-
-        stuResult[item] = result
-
-      })
-      // 取当前年级在每个知识点的得分 除以知识点的总分 得到年级在该知识点的得分率
-      //stuResult.grade = knowAllperItem.value.map((score, index) => wrongItem.value[index][1] == 0 ? 0 : Number(score / wrongItem.value[index][1]) * 100)
-      stuResult.keys = val.knowKey
-
-      //===========================================
-      
+        let levelNameItem = classesItem.pointLevelKey.levelKey.level        
+        let fScoresItem = classesItem.fScore
+        levelNameItem.forEach((item, index) => {          
+          let result = []
+          val.students.forEach(stu => {           
+            let fieldStuPer = classesItem.fieldStuPer.find(i => i.key === stu.id)
+            if (stu.classId === classesItem.classId) {
+              result.push([
+                stu.name,
+                stu.className,
+                stu.no || '-',
+                fScoresItem[index],                
+                fieldStuPer.value[index],
+                fScoresItem[index] == 0 ? 0 : (fieldStuPer.value[index] / fScoresItem[index]).toFixed(2)
+              ])
+            }
+          })
+          stuResult[item] = result
+        })
+      }
       
-      //let origin = data.stupercent
+      // 取当前年级在每个知识点的得分 除以知识点的总分 得到年级在该知识点的得分率      
+      stuResult.keys = val.knowKey     
       let origin = stuResult
       let keys = origin.keys
-      let datas = classIndex === -1 ? (origin[point] || []) : origin[point].filter(i => i[1] === this.classList[classIndex + 1])
+      let transArr = this.$GLOBAL.EXERCISE_LEVELS()
+      //origin = origin.map((i, index) => transArr[index])
+      //let datas = classIndex === -1 ? (origin[point] || []) : origin[point].filter(i => i[1] === this.classList[classIndex + 1])
+      transArr.forEach((item,index) =>{
+        origin[item] = origin[index + 1]
+      })
       this.tableData = this.$tools.jsonTransform({
-        datas: datas,
+        datas: origin[point],
         keys: keys
       })
     },
 
-    doRenderWrong(data) {
-      let classIndex = this.$store.state.totalAnalysis.curClassIndex
-      let origin = data.wrong
-      let keys = origin.keys
-      let datas = origin.datas
-      console.log(datas)
-      datas.forEach((i, index) => {
-        i[0] = this.transArr[index]
-      })
+    doRenderWrong(data) {     
+      let keys = data.keys
+      let datas = data.datas           
       let allWrongData = this.$tools.jsonTransform({ datas: datas, keys: keys })
-      this.numData = classIndex === -1 ? allWrongData : this.getClassWrongData(allWrongData, classIndex)
-
+      allWrongData.forEach((i, index) => {
+        i.name = this.transArr[index]
+      })      
+      this.numData = allWrongData
     },
 
     getClassWrongData(allWrongData) {
@@ -262,20 +288,50 @@ export default {
   computed: {
     // 获取最新散点图数据
     getLevelData() {
-      let curSubjectIndex = this.$store.state.totalAnalysis.analysisJsonJoint.subjects.map(i => i.id).indexOf(this
-        .$store.state
-        .totalAnalysis.currentSubjectJoint)
-      let levelJson = this.$store.state.totalAnalysis.analysisJsonJoint.pointLevelKey[curSubjectIndex].levelKey
+      // 如果class = -1  要重新統計全部班級的fieldWrong
+
+      let classIndex = this.$store.state.totalAnalysis.curClassIndex
+      // 初始物件
+      let initPointList = this.$store.state.totalAnalysis.analysisJsonJoint.classes[0].pointLevelKey.levelKey.pointList
       let transArr = this.$GLOBAL.EXERCISE_LEVELS()
-      levelJson.pointList = levelJson.pointList.map((i, index) => transArr[index])
-      for (let key in levelJson.classpercent) {
-        if (!isNaN(key)) {
-          let newKey = transArr[+key - 1]
-          levelJson.classpercent[newKey] = levelJson.classpercent[key]
-          levelJson.stupercent[newKey] = levelJson.stupercent[key]
+      initPointList = initPointList.map((i, index) => transArr[index])
+      let fieldWrongAll = {
+          datas : [],
+          pointList : initPointList,
+          keys: [
+              "name",
+              "point",
+              "itemNO",
+              "persent",
+              "wrong",
+              "rhw",
+              "rlw"
+            ]
         }
+      if(classIndex === -1){ // 全部班級
+        fieldWrongAll.datas = this.$store.state.totalAnalysis.analysisJsonJoint.classes[0].fieldWrong
+        //  平均得分率 跟 錯題人次 都先歸零  其他通用
+        fieldWrongAll.datas.forEach(item => {
+          item[3] = 0
+          item[4] = 0
+        })
+        // 匯總 平均得分率 跟 錯題人次
+        this.$store.state.totalAnalysis.analysisJsonJoint.classes.forEach(classItem => {
+          fieldWrongAll.datas.forEach(fieldWrongAllItem => {            
+            let fieldWrongItem = classItem.fieldWrong.find(k => k[0] == fieldWrongAllItem[0])
+            fieldWrongAllItem[3] = parseFloat(fieldWrongAllItem[3]) + parseFloat(fieldWrongItem[3])
+            fieldWrongAllItem[4] = parseFloat(fieldWrongAllItem[4]) + parseFloat(fieldWrongItem[4])
+          })         
+        })
+        // 平均得分率總和再平均一次班級
+        fieldWrongAll.datas.forEach(fieldWrongItem => {            
+            fieldWrongItem[3] = fieldWrongItem[3] / this.$store.state.totalAnalysis.analysisJsonJoint.classes.length
+            
+          })        
+      }else{
+        fieldWrongAll.datas = this.$store.state.totalAnalysis.analysisJsonJoint.classes[classIndex].fieldWrong
       }
-      return levelJson
+      return fieldWrongAll
     },
     // 获取最新散点图数据
     getKnowledgeData() {
@@ -299,7 +355,7 @@ export default {
     // },
     classIndex(n, o) {
       this.curClassIndex = n - 1
-      this.doRender(this.getLevelData, this.getLevelData.pointList[0], n - 1)
+      this.doRender(this.getLevelData, 1, n - 1)
       this.doRenderWrong(this.getLevelData, n - 1)
     },
     immediate: true

+ 5 - 3
TEAMModelOS/ClientApp/src/view/student-web/AppNew.vue

@@ -746,9 +746,11 @@ export default {
                 }
                 this.getCourseTop.forEach(item => {
                     let index = list.findIndex(course => item.id === course.id && item.groupId === course.groupId)
-                    let courseInfo = list.splice(index, 1)[0]
-                    courseInfo.top = true
-                    list.unshift(courseInfo)
+                    if(index != -1) {
+                        let courseInfo = list.splice(index, 1)[0]
+                        courseInfo.top = true
+                        list.unshift(courseInfo)
+                    }
                 })
                 this.$store.commit("setAllCourse", list)
                 this.courseList = this.getAllCourse

+ 2 - 1
TEAMModelOS/ClientApp/src/view/syllabus/Syllabus.vue

@@ -301,7 +301,7 @@
     <!-- 选择分享弹窗 -->
     <Modal v-model="isShareModal" width="1200" footer-hide class="share-syllabus-modal" @on-visible-change="onShareModalChange">
       <div class="modal-header" slot="header">
-        {{ inviteType === 'tree' ? $t('syllabus.chooseCoTeacher') : $t('syllabus.shareSyllabus') }}
+        {{ inviteType === 'tree' ? $t('syllabus.chooseCoTeacher') : (isSchool ? $t('syllabus.shareSyllabusSchool') : $t('syllabus.shareSyllabus')) }}
       </div>
       <div class="share-wrap">
         <div class="share-left">
@@ -1579,6 +1579,7 @@ export default {
       let { creatorName, creatorId, name, id, scope, school } = this.curVolume
       let host = window.location.origin
       let callbackUrl = `${host}/shareSyllabus?creatorName=${creatorName}&creatorId=${creatorId}&name=${name}&id=${id}&scope=${scope}&school=${school}&m=${this.$t('qrCode.tip4')}&o=1`
+      this.qrConfig.title = this.isSchool ? this.$t('syllabus.shareSyllabusSchool') : this.$t('syllabus.shareSyllabus')
       this.qrConfig.url = callbackUrl
       this.qrConfig.shareContent = `${this.$t('syllabus.shareContentText1')}\n\n${this.$t('syllabus.shareContentText2')}:${creatorName}\n${this.$t('syllabus.shareContentText3')}:${name}\n\n${this.$t('syllabus.shareContentText4')}\n${callbackUrl}`
       this.shareQrModal = true

+ 124 - 97
TEAMModelOS/Controllers/Both/GroupListController.cs

@@ -24,6 +24,7 @@ using TEAMModelOS.Controllers.Both;
 using System.Net.Http;
 using System.Security;
 using Microsoft.Azure.Cosmos;
+using TEAMModelOS.SDK.Models.Dtos;
 namespace TEAMModelOS.Controllers
 {
     [ProducesResponseType(StatusCodes.Status200OK)]
@@ -54,7 +55,7 @@ namespace TEAMModelOS.Controllers
             _azureStorage = azureStorage;
             _coreAPIHttpService = coreAPIHttpService;
             _environment = environment;
-            _azureRedis=azureRedis;
+            _azureRedis = azureRedis;
         }
         //学生获取自己已经加入的名单。
         [ProducesDefaultResponseType]
@@ -82,14 +83,14 @@ namespace TEAMModelOS.Controllers
                 memberType = 1;
             }
             List<GroupListGrp> groups = null;
-            if (json.TryGetProperty("time", out JsonElement _time)  && long.TryParse($"{_time}", out long time)  && time>0)
+            if (json.TryGetProperty("time", out JsonElement _time) && long.TryParse($"{_time}", out long time) && time > 0)
             {
-                groups = await GroupListService.GetMemberInGroupList(_coreAPIHttpService, client, _dingDing, userid, memberType, school, new List<string> { "class", "teach" },time:time);
+                groups = await GroupListService.GetMemberInGroupList(_coreAPIHttpService, client, _dingDing, userid, memberType, school, new List<string> { "class", "teach" }, time: time);
             }
             else {
-               groups = await GroupListService.GetMemberInGroupList(_coreAPIHttpService, client, _dingDing, userid, memberType, school, new List<string> { "class", "teach" });
+                groups = await GroupListService.GetMemberInGroupList(_coreAPIHttpService, client, _dingDing, userid, memberType, school, new List<string> { "class", "teach" });
             }
-           
+
             return Ok(new { groups = groups });
         }
         [ProducesDefaultResponseType]
@@ -106,24 +107,24 @@ namespace TEAMModelOS.Controllers
                 await foreach (var item in client.GetContainer(Constant.TEAMModelOS, "School").GetItemQueryIteratorSql<GroupList>(queryText: queryNo,
                 requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"GroupList-{school}") }))
                 {
-                    groupList=item;
+                    groupList = item;
                     break;
                 }
             }
-            if (groupList==null) {
+            if (groupList == null) {
                 await foreach (var item in client.GetContainer(Constant.TEAMModelOS, "Teacher").GetItemQueryIteratorSql<GroupList>(queryText: queryNo,
                               requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"GroupList") }))
                 {
-                    groupList=item;
+                    groupList = item;
                     break;
                 }
             }
-            if (groupList!=null)
+            if (groupList != null)
             {
-                return Ok(new {code=200, nos = groupList.members.Select(x => x.irs),optNo= groupList.optNo , limitCount=groupList.limitCount});
+                return Ok(new { code = 200, nos = groupList.members.Select(x => x.irs), optNo = groupList.optNo, limitCount = groupList.limitCount });
             }
             else {
-                return Ok(new {code=404,  nos =new List<string>() , optNo =0, limitCount =0});
+                return Ok(new { code = 404, nos = new List<string>(), optNo = 0, limitCount = 0 });
             }
         }
 
@@ -140,9 +141,9 @@ namespace TEAMModelOS.Controllers
             json.TryGetProperty("school", out JsonElement school);
             json.TryGetProperty("courseId", out JsonElement _courseId);
             int seatNo = 0;
-            if (json.TryGetProperty("seatNo", out JsonElement _seatNo)  ) {
+            if (json.TryGetProperty("seatNo", out JsonElement _seatNo)) {
                 if (int.TryParse($"{_seatNo}", out int __seatNo)) {
-                    seatNo=__seatNo;
+                    seatNo = __seatNo;
                 }
             }
             var client = _azureCosmos.GetCosmosClient();
@@ -172,17 +173,17 @@ namespace TEAMModelOS.Controllers
                         //创建账号并第一次登录IES5则默认赠送1G
                         defaultSchool = null,
                         schools = new List<TmdUser.School>(),
-                        lang= lang?.ToString(),
+                        lang = lang?.ToString(),
                     };
                     await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, "Student").CreateItemAsync<TmdUser>(tmduser, new PartitionKey("Base"));
                 }
             }
             int year = DateTimeOffset.UtcNow.Year;
-            (int status, GroupList stuList, Member member) data = await GroupListService.CodeJoinList(client, _azureRedis, $"{_stuListNo}", id, type: 1, $"{school}", year, $"{name}", $"{picture}", $"{lang}", seatNo, $"{_courseId}","hita");
+            (int status, GroupList stuList, Member member) data = await GroupListService.CodeJoinList(client, _azureRedis, $"{_stuListNo}", id, type: 1, $"{school}", year, $"{name}", $"{picture}", $"{lang}", seatNo, $"{_courseId}", "hita");
             //没有TmdUser时
             if (data.status == 0)
             {
-                await GroupListService.UpsertList(data.stuList, _azureCosmos, _configuration, _serviceBus,_client: "hita");
+                await GroupListService.UpsertList(data.stuList, _azureCosmos, _configuration, _serviceBus, _client: "hita");
 
                 List<string> ids = new List<string>();
                 if (data.stuList.scope.Equals("private") && !string.IsNullOrEmpty(data.stuList.creatorId))
@@ -238,7 +239,7 @@ namespace TEAMModelOS.Controllers
             }
             if (!json.TryGetProperty("stuListNo", out JsonElement _stuListNo)) return BadRequest();
             var (userid, _name, _picture, school) = HttpContext.GetAuthTokenInfo();
-          
+
             int type = 0;
             string no = null;
             var client = _azureCosmos.GetCosmosClient();
@@ -261,8 +262,8 @@ namespace TEAMModelOS.Controllers
             {
                 type = 1;
             }
-            int seatNo =0;
-            (int status, GroupList stuList, Member member) data = await GroupListService.CodeJoinList(client, _azureRedis, $"{_stuListNo}", userid, type, school, year, _name, _picture, head_lang,seatNo,_client:"web");
+            int seatNo = 0;
+            (int status, GroupList stuList, Member member) data = await GroupListService.CodeJoinList(client, _azureRedis, $"{_stuListNo}", userid, type, school, year, _name, _picture, head_lang, seatNo, _client: "web");
             if (data.status == 0)
             {
 
@@ -408,9 +409,9 @@ namespace TEAMModelOS.Controllers
                                 school = $"{schoolId}",
                                 type = "class",
                                 year = item.year,
-                                expire=0,
+                                expire = 0,
                                 groupName = groupNames,
-                                scount= scount
+                                scount = scount
                             };
                             groupLists.Add(group);
                         }
@@ -456,7 +457,7 @@ namespace TEAMModelOS.Controllers
                         }
                     }
                     return Ok(new { groupLists });
-                    //弃用 
+                //弃用 
                 case bool when $"{opt}".Equals("teach", StringComparison.OrdinalIgnoreCase):
                     //我执教的
                     //从学校的课程和个人课程搜寻与我相关的课程对应的名单。
@@ -507,7 +508,7 @@ namespace TEAMModelOS.Controllers
                                 scope = "school",
                                 school = $"{schoolId}",
                                 type = "class",
-                                expire=0,
+                                expire = 0,
                                 year = item.year,
                                 groupName = groupNames
                             };
@@ -597,7 +598,7 @@ namespace TEAMModelOS.Controllers
             json.TryGetProperty("periodId", out JsonElement periodId);
             long nowtime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
             json.TryGetProperty("time", out JsonElement _time);
-           
+
             switch (true)
             {
                 //我管理的
@@ -605,20 +606,20 @@ namespace TEAMModelOS.Controllers
                     {
                         List<GroupListGrp> groupLists = new List<GroupListGrp>();
                         var authData = HttpContext.GetAuthTokenKey("Roles");
-                        object _roles = null, _permissions= null;
-                        HttpContext?.Items.TryGetValue("Roles", out   _roles);
+                        object _roles = null, _permissions = null;
+                        HttpContext?.Items.TryGetValue("Roles", out _roles);
                         HttpContext?.Items.TryGetValue("Permissions", out _permissions);
                         List<string> rolse = new List<string>();
-                        if (_roles!= null) {
-                            rolse= (List<string>)_roles;
+                        if (_roles != null) {
+                            rolse = (List<string>)_roles;
                         }
                         List<string> permissions = new List<string>();
-                        if (_permissions!= null)
+                        if (_permissions != null)
                         {
-                            permissions= (List<string>)_permissions;
+                            permissions = (List<string>)_permissions;
                         }
 
-                        if (rolse.Contains("admin")||permissions.Contains("course-upd")||permissions.Contains("course-read"))
+                        if (rolse.Contains("admin") || permissions.Contains("course-upd") || permissions.Contains("course-read"))
                         {
                             //包含,学校的行政班,教学班
                             json.TryGetProperty("type", out JsonElement _type);
@@ -664,10 +665,10 @@ namespace TEAMModelOS.Controllers
                                         school = $"{school}",
                                         type = "class",
                                         year = item.year,
-                                        expire=0,
-                                        no=item.no,
+                                        expire = 0,
+                                        no = item.no,
                                         groupName = groupNames,
-                                        scount= scount
+                                        scount = scount
                                     };
                                     groupLists.Add(group);
                                 }
@@ -713,22 +714,22 @@ namespace TEAMModelOS.Controllers
                                 }
                             }
                         }
-                       
+
                         //long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
                         return Ok(new { groupLists });
                     }
                 case bool when $"{opt}".Equals("teach", StringComparison.OrdinalIgnoreCase):
                     {
-                        string sql  = $"SELECT distinct value c FROM c  join b in c.schedules where c.pk='CourseTask' and (ARRAY_CONTAINS(b.assistants,'{tmdid}')or  b.teacherId  ='{tmdid}' )";
-                        List<KeyValuePair<string,CourseTask >> schoolTeacherTask = new List<KeyValuePair<string, CourseTask>>();
-                        List<KeyValuePair<string,CourseTask >> schoolAssistantTask = new  List<KeyValuePair<string,CourseTask >>();
+                        string sql = $"SELECT distinct value c FROM c  join b in c.schedules where c.pk='CourseTask' and (ARRAY_CONTAINS(b.assistants,'{tmdid}')or  b.teacherId  ='{tmdid}' )";
+                        List<KeyValuePair<string, CourseTask>> schoolTeacherTask = new List<KeyValuePair<string, CourseTask>>();
+                        List<KeyValuePair<string, CourseTask>> schoolAssistantTask = new List<KeyValuePair<string, CourseTask>>();
 
-                        List<KeyValuePair<string,CourseTask >> privateTeacherTask = new  List<KeyValuePair<string,CourseTask >>();
-                        List<KeyValuePair<string,CourseTask >> privateAssistantTask = new  List<KeyValuePair<string,CourseTask >>();
+                        List<KeyValuePair<string, CourseTask>> privateTeacherTask = new List<KeyValuePair<string, CourseTask>>();
+                        List<KeyValuePair<string, CourseTask>> privateAssistantTask = new List<KeyValuePair<string, CourseTask>>();
                         List<CourseDto> schoolCourses = new List<CourseDto>();
                         List<CourseDto> teahcerCourses = new List<CourseDto>();
                         List<string> groupIds = new List<string>();
-                        if (!string.IsNullOrWhiteSpace(school)) 
+                        if (!string.IsNullOrWhiteSpace(school))
                         {
                             School schoolBase = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.School).ReadItemAsync<School>(school, new PartitionKey("Base"));
                             JsonElement _year = default, _semesterId = default, _periodId = default;
@@ -745,58 +746,58 @@ namespace TEAMModelOS.Controllers
                             //string date = SchoolService.GetOpensByStudyYearAndSemester(period.semesters, int.Parse($"{_year}"), $"{_semesterId}");
                             sql = $"{sql} and  c.year={_year} and c.semesterId='{_semesterId}'";
                             HashSet<string> courseIds = new HashSet<string>();
-                            var resultSchool= await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.School).GetList<CourseTask>(sql, $"CourseTask-{school}");
+                            var resultSchool = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.School).GetList<CourseTask>(sql, $"CourseTask-{school}");
                             if (resultSchool.list.IsNotEmpty()) {
                                 resultSchool.list.ForEach(x => {
-                                   var schedulesTeacher =   x.schedules.Where(z =>!string.IsNullOrWhiteSpace(z.teacherId)   && z.teacherId.Equals(tmdid));
-                                    if (schedulesTeacher.Any()) 
+                                    var schedulesTeacher = x.schedules.Where(z => !string.IsNullOrWhiteSpace(z.teacherId) && z.teacherId.Equals(tmdid));
+                                    if (schedulesTeacher.Any())
                                     {
                                         courseIds.Add(x.courseId);
                                         CourseTask courseTask = x.ToJsonString().ToObject<CourseTask>();
-                                        courseTask.schedules=schedulesTeacher.ToList();
-                                        schoolTeacherTask.Add(new KeyValuePair<string,CourseTask  >(x.courseId, courseTask));
+                                        courseTask.schedules = schedulesTeacher.ToList();
+                                        schoolTeacherTask.Add(new KeyValuePair<string, CourseTask>(x.courseId, courseTask));
                                         groupIds.AddRange(schedulesTeacher.Where(z => !string.IsNullOrWhiteSpace(z.groupId)).Select(x => x.groupId));
                                     }
-                                    var schedulesAssistant = x.schedules.Where(z => z.assistants.Contains(tmdid) );
+                                    var schedulesAssistant = x.schedules.Where(z => z.assistants.Contains(tmdid));
                                     if (schedulesAssistant.Any())
                                     {
                                         courseIds.Add(x.courseId);
                                         CourseTask courseTask = x.ToJsonString().ToObject<CourseTask>();
-                                        courseTask.schedules=schedulesAssistant.ToList();
+                                        courseTask.schedules = schedulesAssistant.ToList();
                                         schoolAssistantTask.Add(new KeyValuePair<string, CourseTask>(x.courseId, courseTask));
                                         groupIds.AddRange(schedulesAssistant.Where(z => !string.IsNullOrWhiteSpace(z.groupId)).Select(x => x.groupId));
                                     }
                                 });
                             }
                             if (courseIds.Any()) {
-                                string sqlCourse = $"select value c from c where c.id in ({string.Join(",",courseIds.Select(b=>$"'{b}'"))})";
+                                string sqlCourse = $"select value c from c where c.id in ({string.Join(",", courseIds.Select(b => $"'{b}'"))})";
                                 var result = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.School).GetList<CourseBase>(sqlCourse, $"CourseBase-{school}");
-                                if (result.list.IsNotEmpty()) 
+                                if (result.list.IsNotEmpty())
                                 {
-                                    foreach (var item in result.list) 
+                                    foreach (var item in result.list)
                                     {
                                         List<CourseTaskDto> courseTaskDtos = new List<CourseTaskDto>();
-                                        var teacher = schoolTeacherTask.Where(x => x.Key.Equals(item.id)).Select(z => new CourseTaskDto { courseTask=z.Value, type="teacher" });
+                                        var teacher = schoolTeacherTask.Where(x => x.Key.Equals(item.id)).Select(z => new CourseTaskDto { courseTask = z.Value, type = "teacher" });
                                         if (teacher.Any()) {
                                             courseTaskDtos.AddRange(teacher.ToList());
                                         }
-                                        var assistant= schoolAssistantTask.Where(x => x.Key.Equals(item.id)).Select(z => new CourseTaskDto {  courseTask=z.Value, type="assistant" });
+                                        var assistant = schoolAssistantTask.Where(x => x.Key.Equals(item.id)).Select(z => new CourseTaskDto { courseTask = z.Value, type = "assistant" });
                                         if (assistant.Any()) {
                                             courseTaskDtos.AddRange(assistant.ToList());
                                         }
-                                        schoolCourses.Add(new CourseDto { courseBase=item, courseTasks=courseTaskDtos });
+                                        schoolCourses.Add(new CourseDto { courseBase = item, courseTasks = courseTaskDtos });
                                     }
                                 }
                             }
                         }
-                      
+
                         {
                             HashSet<string> courseIds = new HashSet<string>();
                             string sqlCoursePrivate = $"select value c.id from c where  c.creatorId='{tmdid}'";
                             var resultCourseBasePrivate = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Teacher).GetList<string>(sqlCoursePrivate, $"CourseBase");
                             if (resultCourseBasePrivate.list.IsNotEmpty())
                             {
-                                courseIds=new HashSet<string>(resultCourseBasePrivate.list);
+                                courseIds = new HashSet<string>(resultCourseBasePrivate.list);
                             }
                             string sqlprivate = $"SELECT distinct value c FROM c  join b in c.schedules where c.pk='CourseTask' and (ARRAY_CONTAINS(b.assistants,'{tmdid}')or  b.teacherId  ='{tmdid}' )";
                             var resultTeacher = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Teacher).GetList<CourseTask>(sqlprivate, $"CourseTask");
@@ -804,12 +805,12 @@ namespace TEAMModelOS.Controllers
                             {
 
                                 resultTeacher.list.ForEach(x => {
-                                    var schedulesTeacher = x.schedules.Where(z => !string.IsNullOrWhiteSpace(z.teacherId)  &&  z.teacherId.Equals(tmdid));
+                                    var schedulesTeacher = x.schedules.Where(z => !string.IsNullOrWhiteSpace(z.teacherId) && z.teacherId.Equals(tmdid));
                                     if (schedulesTeacher.Any())
                                     {
                                         courseIds.Add(x.courseId);
                                         CourseTask courseTask = x.ToJsonString().ToObject<CourseTask>();
-                                        courseTask.schedules=schedulesTeacher.ToList();
+                                        courseTask.schedules = schedulesTeacher.ToList();
                                         privateTeacherTask.Add(new KeyValuePair<string, CourseTask>(x.courseId, courseTask));
                                         groupIds.AddRange(schedulesTeacher.Where(z => !string.IsNullOrWhiteSpace(z.groupId)).Select(x => x.groupId));
                                     }
@@ -818,12 +819,12 @@ namespace TEAMModelOS.Controllers
                                     {
                                         courseIds.Add(x.courseId);
                                         CourseTask courseTask = x.ToJsonString().ToObject<CourseTask>();
-                                        courseTask.schedules=schedulesAssistant.ToList();
+                                        courseTask.schedules = schedulesAssistant.ToList();
                                         privateAssistantTask.Add(new KeyValuePair<string, CourseTask>(x.courseId, courseTask));
-                                        groupIds.AddRange(schedulesAssistant.Where(z=>!string.IsNullOrWhiteSpace(z.groupId)).Select(x => x.groupId));
+                                        groupIds.AddRange(schedulesAssistant.Where(z => !string.IsNullOrWhiteSpace(z.groupId)).Select(x => x.groupId));
                                     }
                                 });
-                               
+
                             }
                             if (courseIds.Any())
                             {
@@ -835,46 +836,46 @@ namespace TEAMModelOS.Controllers
 
                                     {
                                         List<CourseTaskDto> courseTaskDtos = new List<CourseTaskDto>();
-                                        var teacher = privateTeacherTask.Where(x => x.Key.Equals(item.id)).Select(z => new CourseTaskDto { courseTask=z.Value, type="teacher" });
+                                        var teacher = privateTeacherTask.Where(x => x.Key.Equals(item.id)).Select(z => new CourseTaskDto { courseTask = z.Value, type = "teacher" });
                                         if (teacher.Any())
                                         {
                                             courseTaskDtos.AddRange(teacher.ToList());
                                         }
-                                        var assistant = privateAssistantTask.Where(x => x.Key.Equals(item.id)).Select(z => new CourseTaskDto { courseTask=z.Value, type="assistant" });
+                                        var assistant = privateAssistantTask.Where(x => x.Key.Equals(item.id)).Select(z => new CourseTaskDto { courseTask = z.Value, type = "assistant" });
                                         if (assistant.Any())
                                         {
                                             courseTaskDtos.AddRange(assistant.ToList());
                                         }
-                                        teahcerCourses.Add(new CourseDto { courseBase=item, courseTasks=courseTaskDtos });
+                                        teahcerCourses.Add(new CourseDto { courseBase = item, courseTasks = courseTaskDtos });
                                     }
                                 }
                             }
                             List<CourseGroupList> courseGroupLists = new List<CourseGroupList>();
-                            _coreAPIHttpService.check=false;
-                            var groupListDatas =  await GroupListService.GetMemberByListids(_coreAPIHttpService, _azureCosmos.GetCosmosClient(), _dingDing, groupIds.ToHashSet().ToList(), school);
+                            _coreAPIHttpService.check = false;
+                            var groupListDatas = await GroupListService.GetMemberByListids(_coreAPIHttpService, _azureCosmos.GetCosmosClient(), _dingDing, groupIds.ToHashSet().ToList(), school);
 
                             foreach (var z in teahcerCourses)
                             {
-                                CourseGroupList courseGroupList=   new CourseGroupList
+                                CourseGroupList courseGroupList = new CourseGroupList
                                 {
-                                    scope="private",
-                                    id=z.courseBase.id,
-                                    name=z.courseBase.name,
-                                    periodId=z.courseBase.period?.id,
-                                    period=z.courseBase.period?.name,
-                                    subject=z.courseBase.subject?.name,
-                                    subjectId= z.courseBase.subject?.id
+                                    scope = "private",
+                                    id = z.courseBase.id,
+                                    name = z.courseBase.name,
+                                    periodId = z.courseBase.period?.id,
+                                    period = z.courseBase.period?.name,
+                                    subject = z.courseBase.subject?.name,
+                                    subjectId = z.courseBase.subject?.id
                                 };
                                 foreach (var x in z.courseTasks)
                                 {
                                     foreach (var y in x.courseTask.schedules)
                                     {
                                         var data = groupListDatas.groups.Find(m => m.id.Equals(y.groupId));
-                                        if (data!= null)
+                                        if (data != null)
                                         {
                                             HashSet<string> groupName = data.members.Where(x => !string.IsNullOrEmpty(x.groupName)).Select(y => y.groupName).ToHashSet();
                                             GroupListGrp groupListGrp = new GroupListGrp(data, groupName);
-                                            groupListGrp.teachType=x.type;
+                                            groupListGrp.teachType = x.type;
                                             courseGroupList.groups.Add(groupListGrp);
                                         }
                                     }
@@ -885,32 +886,32 @@ namespace TEAMModelOS.Controllers
                             {
                                 CourseGroupList courseGroupList = new CourseGroupList
                                 {
-                                    scope="school",
-                                    id=z.courseBase.id,
-                                    name=z.courseBase.name,
-                                    periodId=z.courseBase.period?.id,
-                                    period=z.courseBase.period?.name,
-                                    subject=z.courseBase.subject?.name,
-                                    subjectId= z.courseBase.subject?.id
+                                    scope = "school",
+                                    id = z.courseBase.id,
+                                    name = z.courseBase.name,
+                                    periodId = z.courseBase.period?.id,
+                                    period = z.courseBase.period?.name,
+                                    subject = z.courseBase.subject?.name,
+                                    subjectId = z.courseBase.subject?.id
                                 };
                                 foreach (var x in z.courseTasks)
                                 {
                                     foreach (var y in x.courseTask.schedules)
                                     {
                                         var data = groupListDatas.groups.Find(m => m.id.Equals(y.groupId));
-                                        if (data!= null) {
+                                        if (data != null) {
                                             HashSet<string> groupName = data.members.Where(x => !string.IsNullOrEmpty(x.groupName)).Select(y => y.groupName).ToHashSet();
                                             GroupListGrp groupListGrp = new GroupListGrp(data, groupName);
-                                            groupListGrp.teachType=x.type;
+                                            groupListGrp.teachType = x.type;
                                             courseGroupList.groups.Add(groupListGrp);
                                         }
                                     }
                                 }
                                 courseGroupLists.Add(courseGroupList);
                             }
-                            long now =  DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+                            long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
                             foreach (var groups in courseGroupLists) {
-                                groups.groups.RemoveAll(z => z.expire>0 && z.expire<now);
+                                groups.groups.RemoveAll(z => z.expire > 0 && z.expire < now);
                             }
                             return Ok(new { groupLists = courseGroupLists });
                         }
@@ -954,9 +955,9 @@ namespace TEAMModelOS.Controllers
                 }
             }
             List<string> listids = ids.ToObject<List<string>>();
-            if (json.TryGetProperty("time", out JsonElement _time) &&  long.TryParse($"{_time}", out long time)  && time>0)
+            if (json.TryGetProperty("time", out JsonElement _time) && long.TryParse($"{_time}", out long time) && time > 0)
             {
-                (List<RMember> members, List<RGroupList> groups) = await GroupListService.GetMemberByListids(_coreAPIHttpService, client, _dingDing, listids, $"{schoolId}", ps,time:time);
+                (List<RMember> members, List<RGroupList> groups) = await GroupListService.GetMemberByListids(_coreAPIHttpService, client, _dingDing, listids, $"{schoolId}", ps, time: time);
                 return Ok(new { groups, members });
             }
             else
@@ -964,7 +965,7 @@ namespace TEAMModelOS.Controllers
                 (List<RMember> members, List<RGroupList> groups) = await GroupListService.GetMemberByListids(_coreAPIHttpService, client, _dingDing, listids, $"{schoolId}", ps);
                 return Ok(new { groups, members });
             }
-           
+
         }
         /// <summary>
         /// 根据任意名单id获取名单摘要信息。
@@ -982,9 +983,9 @@ namespace TEAMModelOS.Controllers
             var client = _azureCosmos.GetCosmosClient();
             if (!json.TryGetProperty("ids", out JsonElement ids)) return BadRequest();
             json.TryGetProperty("schoolId", out JsonElement schoolId);
-            List<string> listids = ids.ToObject<HashSet<string>>().ToList() ;
+            List<string> listids = ids.ToObject<HashSet<string>>().ToList();
             List<GroupListDto> groups = null;
-            if (json.TryGetProperty("time", out JsonElement _time) &&  long.TryParse($"{_time}", out long time)  && time>0)
+            if (json.TryGetProperty("time", out JsonElement _time) && long.TryParse($"{_time}", out long time) && time > 0)
             {
                 groups = await GroupListService.GetGroupListByListids(client, _dingDing, listids, $"{schoolId}", SummarySql, time: time);
             }
@@ -992,7 +993,7 @@ namespace TEAMModelOS.Controllers
             {
                 groups = await GroupListService.GetGroupListByListids(client, _dingDing, listids, $"{schoolId}", SummarySql);
             }
-              
+
             return Ok(new { groups });
         }
         /// <summary>
@@ -1015,18 +1016,18 @@ namespace TEAMModelOS.Controllers
                 if (!json.TryGetProperty("type", out JsonElement type)) return BadRequest();
                 json.TryGetProperty("periodId", out JsonElement periodId);
                 json.TryGetProperty("no", out JsonElement no);
-               
+
                 List<RGroupList> groups = new List<RGroupList>();
 
                 string sqlSemester = "";
-                if (json.TryGetProperty("time", out JsonElement _time) &&  long.TryParse($"{_time}", out long time)  && time>0)
+                if (json.TryGetProperty("time", out JsonElement _time) && long.TryParse($"{_time}", out long time) && time > 0)
                 {
                     var groupListSemester = await GetGroupListSemester(client, null, $"{_schoolId}", $"{type}", $"{periodId}", $"{no}", time);
                     if (groupListSemester.groupListDtos.IsNotEmpty())
                     {
                         groups.AddRange(groupListSemester.rgroupList);
                         //排除掉已有的数据
-                        sqlSemester =$" and c.id not in ({string.Join(",", groupListSemester.groupListDtos.Select(z => $"'{z.id}'"))})";
+                        sqlSemester = $" and c.id not in ({string.Join(",", groupListSemester.groupListDtos.Select(z => $"'{z.id}'"))})";
                     }
                 }
                 StringBuilder sql = new StringBuilder($"SELECT distinct value(c) FROM c where c.type='{type}'");
@@ -1049,12 +1050,31 @@ namespace TEAMModelOS.Controllers
                 {
                     sql.Append($" and c.creatorId='{_tmdid}'");
                 }
+                StringBuilder groupIds = new StringBuilder();
+
                 await foreach (var item in client.GetContainer(Constant.TEAMModelOS, tbname).GetItemQueryIteratorSql<RGroupList>(queryText: sql.ToString(),
                  requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey(code) }))
                 {
                     groups.Add(item);
+                    // 取出該老師還有連結課程的名單id
+                    groupIds.Append($"'{item.id}',");
                 }
-                (List<RGroupList> groupsData, List<RMember> members) = await GroupListService.GetGroupListMemberInfo(_coreAPIHttpService, client, $"{type}", groups, tbname, _dingDing, $"{_schoolId}");
+                List<RGroupList> haveTaskGroups = new List<RGroupList>();
+                if (groupIds.Length > 0)
+                {
+                    groupIds = groupIds.Remove(groupIds.Length - 1, 1);
+                    string courseTasksql = $"SELECT distinct b.groupId from c join b in c.schedules where b.groupId in ( {groupIds} )";
+
+                    await foreach (var item in client.GetContainer(Constant.TEAMModelOS, tbname).GetItemQueryIteratorSql<GroupId>(queryText: courseTasksql.ToString(),
+                    requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey("CourseTask") }))
+                    {
+                        var groupItem = groups.Find(z => z.id == item.groupId);  
+                        // 篩選出有效的名單
+                        haveTaskGroups.Add(groupItem);
+                    }
+                }
+
+                (List<RGroupList> groupsData, List<RMember> members) = await GroupListService.GetGroupListMemberInfo(_coreAPIHttpService, client, $"{type}", haveTaskGroups, tbname, _dingDing, $"{_schoolId}");
                 return Ok(new { groups = groupsData, members });
             }
             catch (CosmosException ex)
@@ -1067,6 +1087,13 @@ namespace TEAMModelOS.Controllers
             }
             return Ok();
         }
+        public class GroupId
+        {
+            /// <summary>
+            /// 分组id
+            /// </summary>
+            public string groupId { get; set; }
+        }
         /// <summary>
         /// 获取名单列表
         /// </summary>

+ 4 - 4
TEAMModelOS/Controllers/Client/AClassONEController.cs

@@ -196,7 +196,7 @@ namespace TEAMModelOS.Controllers
         [ProducesDefaultResponseType]
         [HttpPost("get-school-info")]
 #if !DEBUG
-        [Authorize(Roles = "AClassONE")]
+        [Authorize(Roles = "AClassONE,IES")]
 #endif
         [AuthToken(Roles = "teacher,admin")]
         public async Task<IActionResult> GetSchoolInfo(JsonElement json) {
@@ -411,9 +411,8 @@ namespace TEAMModelOS.Controllers
         /// <returns></returns>
         [ProducesDefaultResponseType]
         [HttpPost("get-teach-info")]
-#if !DEBUG
-        [Authorize(Roles = "AClassONE")]
-#endif
+
+        [Authorize(Roles = "AClassONE,IES")]
         [AuthToken(Roles = "teacher,admin")]
         public async Task<IActionResult> GetTeachInfo(JsonElement json) {
             (string tmdid, _, _, string school) = HttpContext.GetAuthTokenInfo();
@@ -1038,6 +1037,7 @@ namespace TEAMModelOS.Controllers
                     c.period,
                     c.periodType,
                     c.zymusicstds,c.code,
+                    c.examDeadline,
                     quotStus.Where(z => z.artId.Equals(c.id)).FirstOrDefault().quotas,
                     count = artAttachments.Where(z => z.artId.Equals(c.id)).ToList().Count
 

+ 2 - 2
TEAMModelOS/Controllers/Common/ArtController.cs

@@ -556,7 +556,7 @@ namespace TEAMModelOS.Controllers.Common
                     return BadRequest();
                 }
                 var client = _azureCosmos.GetCosmosClient();
-                StringBuilder stringBuilder = new($"select c.id,c.img,c.name,c.classes,c.code,c.type,c.startTime,c.endTime,c.presenter,c.topic,c.address,c.owner,c.progress,c.uploadSTime,c.uploadETime from c where (c.status<>404 or IS_DEFINED(c.status) = false )");
+                StringBuilder stringBuilder = new($"select c.id,c.img,c.name,c.classes,c.code,c.type,c.startTime,c.endTime,c.presenter,c.topic,c.address,c.owner,c.progress,c.uploadSTime,c.uploadETime,c.examDeadline from c where (c.status<>404 or IS_DEFINED(c.status) = false )");
                 string continuationToken = string.Empty;
                 string token = default;
                 
@@ -988,7 +988,7 @@ namespace TEAMModelOS.Controllers.Common
                         //string base64Str = Convert.ToBase64String(inputBytes);
                         var base64Str = HttpUtility.UrlEncode(Convert.ToBase64String(Encoding.UTF8.GetBytes(data.ToJsonString())));
                         //var date = Convert.FromBase64String(base64Str);
-                        StringBuilder url = new("https://tmdopen.yosocloud.com?appid=8a68f563f3384662acbc268336b98ae2");
+                        StringBuilder url = new("https://musicopen.winteach.cn?appid=8a68f563f3384662acbc268336b98ae2");
                         url.Append($"&data={base64Str}");
                         url.Append($"&thirdToken={token}");
                         zyUrl.Add((artResult.studentId, url.ToString(), artResult.zyanswer.time));

+ 4 - 2
TEAMModelOS/Controllers/School/SchoolTeacherController.cs

@@ -450,6 +450,7 @@ namespace TEAMModelOS.Controllers
             //对应年份
             if (!request.TryGetProperty("year", out JsonElement year)) return BadRequest();
             if (!request.TryGetProperty("periodId", out JsonElement periodId)) return BadRequest();
+            if (!request.TryGetProperty("semesterId", out JsonElement semesterId)) return BadRequest();
 
             List<ScheduleTask> schedules = new();
             try {
@@ -474,7 +475,7 @@ namespace TEAMModelOS.Controllers
                     }
                 }
                 //获取当前学期下面所有得课程安排内容
-                var queryCourse = $"select c.schedules from c where c.year = {year} ";
+                var queryCourse = $"select c.schedules from c where c.year = {year} and c.semesterId = '{semesterId}' ";
                 await foreach (var item in client.GetContainer(Constant.TEAMModelOS, "School").GetItemQueryStreamIteratorSql(queryText: queryCourse, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"CourseTask-{code}") }))
                 {
                     using var json = await JsonDocument.ParseAsync(item.Content);
@@ -498,7 +499,7 @@ namespace TEAMModelOS.Controllers
                 });
                 //获取所有的教学班Id集合
                 List<string> classIds = teacherInfo.Select(c => c.groupId).ToList();
-                var queryClass = $"select c.id,c.year from c where c.year <= {year} and c.graduate = 0 and c.id in ({string.Join(",", classIds.Select(o => $"'{o}'"))})";
+                var queryClass = $"select c.id,c.year from c where c.year <= {year} and c.graduate = 0 and c.periodId = '{periodId}' and c.id in ({string.Join(",", classIds.Select(o => $"'{o}'"))})";
                 List<(string cId, int year)> values = new();
                 await foreach (var item in client.GetContainer(Constant.TEAMModelOS, "School").GetItemQueryStreamIteratorSql(queryText: queryClass, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Class-{code}") }))
                 {
@@ -523,6 +524,7 @@ namespace TEAMModelOS.Controllers
                 var teacher = teacherGrades.GroupBy(c => c.teacherId).Select(z => new { 
                     tmdId = z.Key,
                     teacs.Where(k => k.id.Equals(z.Key))?.FirstOrDefault().name,
+                    periodId = periodId.GetString(),
                     grade = z.ToList().Select(k => new { k.gradeName,k.grade }).ToList().Distinct()
             });
                 Dictionary<string, int> gradeCount = [];

+ 69 - 1
TEAMModelOS/Controllers/System/BlobController.cs

@@ -39,6 +39,7 @@ using Microsoft.AspNetCore.Cors;
 using TEAMModelOS.Controllers.Third.LePei;
 using TEAMModelOS.SDK.Models.Table;
 using Microsoft.Azure.Cosmos.Table;
+using System.Text.Json.Nodes;
 
 namespace TEAMModelOS.Controllers
 {
@@ -479,7 +480,7 @@ namespace TEAMModelOS.Controllers
                 return BadRequest();
             }
         }
-
+        
         /*
          新增 编辑接口
         {
@@ -1460,5 +1461,72 @@ namespace TEAMModelOS.Controllers
             }
             return Ok(new { status = 1, totalCount, totalSize, summary, unLinks, ttl });
         }
+        [HttpPost("copy-blobs")]
+        [AuthToken(Roles = "teacher,admin")]
+        [Authorize(Roles = "IES")]
+        public async Task<ActionResult> CopyBlobs(JsonNode request)
+        {
+            BlobServiceClient blobServiceClient = _azureStorage.GetBlobServiceClient();
+            //原始容器
+            var scnt = request["scnt"];
+            //目标容器
+            var tcnt = request["tcnt"];
+            var blobs = request["blobs"];
+            List<BlobCopyDto> copyDtos = new List<BlobCopyDto>();
+            if (!string.IsNullOrWhiteSpace($"{scnt}") && !string.IsNullOrWhiteSpace($"{tcnt}") && blobs!=null)
+            {
+                // 确保目标容器存在
+                await blobServiceClient.GetBlobContainerClient($"{tcnt}").CreateIfNotExistsAsync();
+                copyDtos = JsonSerializer.Deserialize<List<BlobCopyDto>>(blobs);
+
+                var pages =copyDtos.Page(200);
+                foreach (var page in pages) {
+                    await Parallel.ForEachAsync(page, async (copyDto, token) =>
+                    {
+                        try
+                        {
+                            BlobClient sBlobClient = blobServiceClient.GetBlobContainerClient($"{scnt}").GetBlobClient(copyDto.s);
+                            BlobClient tBlobClient = blobServiceClient.GetBlobContainerClient($"{tcnt}").GetBlobClient(copyDto.t);
+                            await tBlobClient.StartCopyFromUriAsync(sBlobClient.Uri);
+                            // 检查复制是否成功
+                            if (await tBlobClient.ExistsAsync())
+                            {
+                                copyDto.c=1;
+                            }
+                            else
+                            {
+                                copyDto.c = 2;
+                            }
+                        }
+                        catch (Exception ex)
+                        {
+                            copyDto.c=2;
+                        }
+                    });
+                }
+            }
+            return Ok(new {
+                blobs = copyDtos,
+                count_0 = copyDtos.Count(x => x.c == 0),
+                count_1 = copyDtos.Count(x => x.c == 1),
+                count_2 = copyDtos.Count(x => x.c == 2),
+                count_d = copyDtos.Count(),
+            });
+        }
+    }
+    public class BlobCopyDto
+    {
+        /// <summary>
+        /// 源文件
+        /// </summary>
+        public string s { get; set; }
+        /// <summary>
+        /// 目标文件
+        /// </summary>
+        public string t { get; set; }
+        /// <summary>
+        /// 复制状态 0未复制,1成功,2失败
+        /// </summary>
+        public int c{ get; set; }
     }
 }

+ 2 - 1
TEAMModelOS/Controllers/Third/IRS/ThirdIRSController.cs

@@ -217,7 +217,7 @@ namespace TEAMModelOS.Controllers
                             {
                                 // 處理單例復用,提高請求效率,避免ServiceTransportType.Persistent websocket斷線在Azure偶發的消息遺失
                                 // TODO 尚須注意釋放的問題,後續須處理
-                                var hub = _azureSignalR.GetServiceManager().GetHubContext($"C{stu.channel}");
+                              
                                 // 下面註解寫法會造成Azure上消息偶發遺失,Localhost不會發生
                                 //var serviceManager = _azureSignalR.GetServiceManager();                       
                                 //using var hub = await serviceManager.CreateHubContextAsync($"C{stu.channel}", CancellationToken.None);
@@ -255,6 +255,7 @@ namespace TEAMModelOS.Controllers
                                         }
                                     }
                                 }
+                            var hub = _azureSignalR.GetServiceManager().GetHubContext($"C{stu.channel}");
                             var common = JsonSerializer.Serialize(new
                             {
                                 action = "DirectIRS.Answer",

+ 128 - 0
TEAMModelOS/Controllers/Third/XunFeiJYY/XunFeiJYYService.cs

@@ -11,6 +11,9 @@ using System.Text.Json.Nodes;
 using TEAMModelOS.SDK.Extension;
 using Microsoft.Azure.Cosmos.Linq;
 using System.Text.Json;
+using System.ComponentModel.DataAnnotations;
+using TEAMModelOS.SDK.Context.Attributes.Azure;
+using Microsoft.Azure.Cosmos.Table;
 
 namespace TEAMModelOS.Controllers.Third.XunFeiJYY
 {
@@ -168,4 +171,129 @@ namespace TEAMModelOS.Controllers.Third.XunFeiJYY
             return (result, code, list);
         }
     }
+    public class XunFeiJYYBind
+    {
+        [Required(ErrorMessage = "{0} 必须填写")]
+        public string param { get; set; }
+        public string id_token { get; set; }
+        public string mobile { get; set; }
+    }
+    [TableName(Name = "ScYxpt")]
+    public class XunFeiJYYStudent : TableEntity
+    {
+        public string id { get; set; }
+        public string userName { get; set; }
+        public string userPhoto { get; set; }
+        public string schoolId { get; set; }
+        public string classId { get; set; }
+        /// <summary>
+        /// 班级号
+        /// </summary>
+        public string classNO { get; set; }
+        /// <summary>
+        /// 班级内的学号即座号
+        /// </summary>
+        public string classNo { get; set; }
+        public string genderCode { get; set; }
+        public string userClassId { get; set; }
+        public string tmdClassId { get; set; }
+        /// <summary>
+        ///优先匹配学号,再匹配姓名,如果名字不同,则检查id是否一致,如果id不一致则是两个人
+        /// </summary>
+        public string tmdStudentId { get; set; }
+        /// <summary>
+        /// MurmurHash3.Hash32 处理后的短码id
+        /// </summary>
+        public string hashNo { get; set; }
+
+    }
+    [TableName(Name = "ScYxpt")]
+    public class XunFeiJYYUser : TableEntity
+    {
+        public string userId { get; set; }
+        public string userName { get; set; }
+        public string userPhoto { get; set; }
+        public string schoolId { get; set; }
+        public string schoolName { get; set; }
+        public string province { get; set; }
+        public string city { get; set; }
+        public string district { get; set; }
+        public string mobile { get; set; }
+        public string genderCode { get; set; }
+        public string tmdid { get; set; }
+    }
+    [TableName(Name = "ScYxpt")]
+    public class XunFeiJYYClass : TableEntity
+    {
+        /// <summary>
+        /// 小学2024届5班
+        /// </summary>
+        public string className { get; set; }
+        /// <summary>
+        /// 毕业年份
+        /// </summary>
+        public string graduatedYear { get; set; }
+        /// <summary>
+        /// 学校id
+        /// </summary>
+        public string schoolId { get; set; }
+
+        /// <summary>
+        /// 学段
+        /// </summary>
+        public string phaseCode { get; set; }
+        /// <summary>
+        /// 
+        /// </summary>
+        public string id { get; set; }
+        /// <summary>
+        /// 系统学段code
+        /// </summary>
+        public string systemPhaseCode { get; set; }
+        /// <summary>
+        /// 班级类型,0行政班,1分层班,2选修班,3自由班,4培训机构下的班级
+        /// </summary>
+        public string classTypeCode { get; set; }
+        /// <summary>
+        /// 该班级是否已毕业,0否1是
+        /// </summary>
+        public string isGraduated { get; set; }
+        /// <summary>
+        /// 入学年份
+        /// </summary>
+        public string inYear { get; set; }
+        /// <summary>
+        /// 班级序号,用作排序
+        /// </summary>
+        public string classOrder { get; set; }
+        /// <summary>
+        /// 学区id
+        /// </summary>
+        public string campusId { get; set; }
+        /// <summary>
+        /// 班级的初始学段(用于跨学段升年级使用)
+        /// </summary>
+        public string initPhaseCode { get; set; }
+        /// <summary>
+        /// 醍摩豆班级id
+        /// </summary>
+        public string tmdClassId { get; set; }
+        /// <summary>
+        /// 醍摩豆学段id
+        /// </summary>
+        public string tmdPeriodId { get; set; }
+        /// <summary>
+        /// 醍摩豆学校简码
+        /// </summary>
+        public string tmdSchoolCode { get; set; }
+    }
+
+    public class XunFeiJYYBindDto
+    {
+        public string id { get; set; }
+        public string code { get; set; }
+        public string name { get; set; }
+        public string key { get; set; }
+
+    }
 }

+ 152 - 89
TEAMModelOS/Controllers/Third/XunFeiJYY/XunFeilJYYController.cs

@@ -32,6 +32,11 @@ using DocumentFormat.OpenXml.InkML;
 using System.IdentityModel.Tokens.Jwt;
 using System.Linq;
 using System.Net.Http.Headers;
+using DocumentFormat.OpenXml.VariantTypes;
+using static TEAMModelOS.SDK.SchoolService;
+using System.Security.Claims;
+using static TEAMModelOS.SDK.StudentService;
+using DocumentFormat.OpenXml.Office2010.Excel;
 
 
 namespace TEAMModelOS.Controllers.Third.XunFeiJYY
@@ -71,6 +76,9 @@ namespace TEAMModelOS.Controllers.Third.XunFeiJYY
         private static readonly string getOrgTeacherById = "getOrgTeacherById";
         //根据学生id查询行政学生
         private static readonly string getOrgStudentById = "getOrgStudentById";
+        //根据行政班级分页查询行政学生信息
+        private static readonly string listOrgStudentInOrgClass = "listOrgStudentInOrgClass";
+
         private readonly AzureCosmosFactory _azureCosmos;
         private readonly DingDing _dingDing;
         private readonly Option _option;
@@ -104,7 +112,149 @@ namespace TEAMModelOS.Controllers.Third.XunFeiJYY
                 new Dictionary<string, string>() { { "schoolId", schoolid } ,{ "param", $"{{\"limit\":{pageSize},\"pageIndex\":{pageIndex}}}" } },pageSize);
             if (data.code== 200)
             {
-               
+                List<XunFeiJYYBindDto> dtos = new List<XunFeiJYYBindDto>();
+                dtos.Add(new XunFeiJYYBindDto {
+                    id="fbc284072a40da84352496274afc3bc55db54e6686cf3af22a9f0a295239691707dc038c5d090450c46bdea890254258b8a118406664bdaf",
+                    code="habook",
+                    key="school",
+                    name="厦大附属翔安实验学校"
+                }); 
+                dtos.Add(new XunFeiJYYBindDto
+                {
+                    id="03",
+                    code="50fdecdb-6cbd-4c9a-8dc2-a627f4d6b312",
+                    key="period",
+                    name="小学"
+                }); dtos.Add(new XunFeiJYYBindDto
+                {
+                    id="04",
+                    code="a098e3f2-e295-a385-bfe5-1cfc91a64c24",
+                    key="period",
+                    name="初中"
+                });
+                dtos.Add(new XunFeiJYYBindDto
+                {
+                    id="05",
+                    code="36725f35-03f8-ae37-0729-5aa8a52eb1d8",
+                    key="period",
+                    name="高中"
+                });
+                var table = _azureStorage.GetCloudTableClient().GetTableReference("ScYxpt");
+                List<Class> classes = new List<Class>();
+                var schoolDto = dtos.FindAll(x => x.key.Equals("school")).First();
+                var dbclassrs = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.School).GetList<Class>($"select value c from c ", $"Class-{schoolDto.code}");
+                if (dbclassrs.list.IsNotEmpty()) 
+                {
+                    classes.AddRange(dbclassrs.list);
+                }
+                foreach (XunFeiJYYClass clazz in data.list) 
+                {
+                    var tmdClassNO = clazz.classOrder;
+                    var tmdClassYear = clazz.inYear;
+                    var tmdClassId = MurmurHash3.Hash128(clazz.id);
+                    var periodDto = dtos.Find(x=>x.key.Equals("period") && x.id.Equals(clazz.phaseCode));
+                  
+                    XunFeiJYYClass saveClazz=clazz;
+                    List<XunFeiJYYClass> xfclasses = await table.FindListByDict<XunFeiJYYClass>(new Dictionary<string, object>() { { Constant.PartitionKey, $"XunFeiJYYClass" }, { Constant.RowKey, clazz.id} });
+                    bool dbExists = false;
+                    if (xfclasses.IsNotEmpty())
+                    {
+                        saveClazz=xfclasses.First();
+                        var dbclass= classes.Find(x => x.id.Equals(saveClazz.tmdClassId));
+                        if (dbclass!=null)
+                        {
+                            dbExists = true;
+                        }
+                    }
+                    else {
+                        saveClazz.tmdSchoolCode=schoolDto.code;
+                        saveClazz.tmdPeriodId=periodDto.code;
+                        saveClazz.tmdClassId=tmdClassId;
+                        saveClazz.PartitionKey="XunFeiJYYClass";
+                        saveClazz.RowKey=clazz.id;
+                        dbExists = false;
+                    }
+                    if (!dbExists) 
+                    {
+                        Class @class = new Class
+                        {
+                            id =saveClazz.tmdClassId,
+                            code=$"Class-{schoolDto.code}",
+                            name=saveClazz.className,
+                            pk="Class",
+                            no=tmdClassNO,
+                            periodId=saveClazz.tmdPeriodId,
+                            year=int.Parse(tmdClassYear),
+                            school=schoolDto.code,
+                            graduate=saveClazz.isGraduated.Equals("0") ? 0 : 1,
+                           
+                        };
+                        await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.School).UpsertItemAsync(@class,new PartitionKey($"Class-{schoolDto.code}"));
+                        await table.SaveOrUpdate<XunFeiJYYClass>(saveClazz);
+                    }
+                    List<Student> students = new List<Student>();
+                    var dbstudentsrs= await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Student).GetList<Student>($"select value s from s where s.classId='{saveClazz.tmdClassId}'",$"Base-{schoolDto.code}");
+                    if (dbstudentsrs.list.IsNotEmpty()) 
+                    {
+                        students.AddRange(dbstudentsrs.list);
+                    }
+                    var studentDatas = await XunFeiJYYService.PostRequestPageAll<XunFeiJYYStudent>(_httpClientFactory.CreateClient(), listOrgStudentInOrgClass, new Dictionary<string, string>() { { "classId", clazz.id }, { "param", $"{{\"limit\":{pageSize},\"pageIndex\":{pageIndex}}}" } }, pageSize);
+                    if (studentDatas.code==200) 
+                    {
+                        // id规则 inYrear+{学段order}classNO+classNo;
+
+                        foreach (XunFeiJYYStudent student in studentDatas.list)
+                        {
+                            
+                            XunFeiJYYStudent saveStudent = student;
+                            List<XunFeiJYYStudent> xfstudents = await table.FindListByDict<XunFeiJYYStudent>(new Dictionary<string, object>() { { Constant.PartitionKey, $"XunFeiJYYStudent" }, { Constant.RowKey, student.id } });
+                            dbExists = false;
+                            if (xfstudents.IsNotEmpty())
+                            {
+                                saveStudent=xfstudents.First();
+                                var dbstudent = students.Find(x => x.id.Equals(saveStudent.tmdStudentId));
+                                if (dbstudent!=null)
+                                {
+                                    if(dbstudent.name.Equals(saveStudent.userName))
+                                    dbExists = true;
+                                }
+                            }
+                            else
+                            {
+                                saveStudent.tmdStudentId=$"{tmdClassYear}{clazz.phaseCode}{tmdClassNO.PadLeft(2, '0')}{student.classNo.PadLeft(3, '0')}";
+                                saveStudent.hashNo=  $"{MurmurHash3.Hash32(student.id)}";
+                                saveStudent.RowKey = student.id;
+                                saveStudent.PartitionKey="XunFeiJYYStudent";
+                                saveStudent.tmdClassId=saveClazz.tmdClassId;
+                                dbExists = false;
+                            }
+                            if (!dbExists)
+                            {
+                                var salt = Utils.CreatSaltString(8);
+                                var pw = Utils.HashedPassword(saveStudent.tmdStudentId, salt);
+                                Student studentSave = new Student
+                                {
+                                    id = student.tmdStudentId,
+                                    code=$"Base-{schoolDto.code}",
+                                    pk="Base",
+                                    gender=string.IsNullOrWhiteSpace(saveStudent.genderCode) ? "S" : saveStudent.genderCode.Equals("0") ? "F" : saveStudent.genderCode.Equals("1") ? "M" : "S",
+                                    classId=saveClazz.tmdClassId,
+                                    periodId=saveClazz.tmdPeriodId,
+                                    name=saveStudent.userName,
+                                    picture= saveStudent.userPhoto,
+                                    year=int.Parse(tmdClassYear),
+                                    no=saveStudent.classNo,
+                                    irs=saveStudent.classNo,
+                                    graduate=saveClazz.isGraduated.Equals("0") ? 0 : 1,
+                                    pw= pw,
+                                    salt=salt,
+                                };
+                                await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Student).UpsertItemAsync(studentSave, new PartitionKey($"Base-{schoolDto.code}"));
+                                await table.SaveOrUpdate<XunFeiJYYStudent>(saveStudent);
+                            }
+                        }
+                    }
+                }
                 return Ok(data.list);
             }
             else {
@@ -381,93 +531,6 @@ namespace TEAMModelOS.Controllers.Third.XunFeiJYY
             }
             return Ok(new { code = 400, rurl= rurl.ToString() });
         }
-        public class XunFeiJYYBind
-        {
-            [Required(ErrorMessage = "{0} 必须填写")]
-            public string param { get; set; }
-            public string id_token { get; set; }
-            public string mobile { get; set; }
-        }
-
-        [TableName(Name = "ScYxpt")]
-        public class XunFeiJYYUser : TableEntity
-        {
-            public string userId { get; set; }
-            public string userName { get; set; }
-            public string userPhoto { get; set; }
-            public string schoolId { get; set; }
-            public string schoolName { get; set; }
-            public string province { get; set; }
-            public string city { get; set; }
-            public string district { get; set; }
-            public string mobile { get; set; }
-            public string genderCode { get; set; }
-            public string  tmdid { get; set; }
-        }
-        [TableName(Name = "ScYxpt")]
-        public class XunFeiJYYClass : TableEntity
-        {
-            /// <summary>
-            /// 小学2024届5班
-            /// </summary>
-            public string className { get; set; }
-            /// <summary>
-            /// 毕业年份
-            /// </summary>
-            public string graduatedYear { get; set; }
-            /// <summary>
-            /// 学校id
-            /// </summary>
-            public string schoolId { get; set; }
-
-            /// <summary>
-            /// 学段
-            /// </summary>
-            public string phaseCode { get; set; }
-            /// <summary>
-            /// 
-            /// </summary>
-            public string id { get; set; }
-            /// <summary>
-            /// 系统学段code
-            /// </summary>
-            public string systemPhaseCode { get; set; }
-            /// <summary>
-            /// 班级类型,0行政班,1分层班,2选修班,3自由班,4培训机构下的班级
-            /// </summary>
-            public string classTypeCode { get; set; }
-            /// <summary>
-            /// 该班级是否已毕业,0否1是
-            /// </summary>
-            public string isGraduated { get; set; }
-            /// <summary>
-            /// 入学年份
-            /// </summary>
-            public string inYear { get; set; }
-            /// <summary>
-            /// 班级序号,用作排序
-            /// </summary>
-            public string classOrder { get; set; }
-            /// <summary>
-            /// 学区id
-            /// </summary>
-            public string campusId { get; set; }
-            /// <summary>
-            /// 班级的初始学段(用于跨学段升年级使用)
-            /// </summary>
-            public string initPhaseCode { get; set; }
-            /// <summary>
-            /// 醍摩豆班级id
-            /// </summary>
-            public string classId { get; set;}
-            /// <summary>
-            /// 醍摩豆学段id
-            /// </summary>
-            public string periodId { get; set; }
-            /// <summary>
-            /// 醍摩豆学校简码
-            /// </summary>
-            public string schoolCode { get; set; }
-        }
+        
     }
 }

+ 187 - 0
TEAMModelOS/Filter/IESHybridCloudHub.cs

@@ -0,0 +1,187 @@
+using Microsoft.AspNetCore.SignalR;
+using Microsoft.Extensions.Logging;
+using System.Threading.Tasks;
+using TEAMModelOS.SDK;
+using TEAMModelOS.SDK.DI.Device;
+using TEAMModelOS.SDK.DI;
+using Microsoft.Extensions.Primitives;
+using System.Text;
+using System.Web;
+using TEAMModelOS.SDK.Extension;
+using System;
+using System.Text.Json;
+using TEAMModelOS.SDK.Models;
+
+namespace TEAMModelOS.Filter
+{
+    public class IESHybridCloudHub: Hub<IClient>
+    {
+       
+        private readonly ILogger<IESHybridCloudHub> _logger;
+        private readonly AzureRedisFactory _azureRedis;
+        private readonly AzureStorageFactory _azureStorage;
+        private readonly DingDing _dingDing;
+        private readonly CoreDevice _device;
+        public IESHybridCloudHub(AzureRedisFactory azureRedis, ILogger<IESHybridCloudHub> logger, AzureStorageFactory azureStorage, DingDing dingDing, CoreDevice device)
+        {
+            _logger = logger;
+            _azureRedis = azureRedis;
+            _dingDing = dingDing;
+            _device=device;
+        }
+        public override async Task OnConnectedAsync() 
+        {
+            var serverDevice = await _device.GetCoreDevice();
+
+            var connid = Context.ConnectionId;
+            var httpContext = Context.GetHttpContext();
+            if (httpContext != null) 
+            {
+                //wss://www.winteach.cn/signalr/notify?grant_type=wechat_qrcode&scene=0a75aca57536490ba00fe62e27bb8f6c&id=U2MNiCFNPPuVcw2gUI_gRA
+                //wss://www.winteach.cn/signalr/notify?grant_type=bookjs_api&clientid={clientid}&id=客户端自动生成的
+                httpContext.Request.Query.TryGetValue("grant_type", out StringValues grant_type);
+                httpContext.Request.Query.TryGetValue("clientid", out StringValues clientid);
+                httpContext.Request.Query.TryGetValue("device", out StringValues _device);
+                await Groups.AddToGroupAsync(connid, grant_type!);
+                if (!clientid.Equals(StringValues.Empty) && !grant_type.Equals(StringValues.Empty)) 
+                {
+                    ///连接配置,并且使用钉钉 通知。
+                    var client = new SignalRClient
+                    {
+                        connid = connid,
+                        grant_type = grant_type,
+                        clientid= clientid,
+                        serverid=serverDevice.deviceId,
+                    };
+                    await _azureRedis.GetRedisClient(8).HashSetAsync($"SignalRClient:connects:{serverDevice.deviceId}", connid, client.ToJsonString());
+                    HybridClient device = HttpUtility.UrlDecode(_device, Encoding.Unicode).ToObject<HybridClient>();
+                    switch (true) 
+                    {
+                        case bool when grant_type.Equals(HybridConstant.grant_type):
+                            HybridClient hybridClient;
+                            var value = await _azureRedis.GetRedisClient(8).HashGetAsync($"HybridApi:clients:{serverDevice.deviceId}", client.clientid);
+                            if (value!=default  && value.HasValue)
+                            {
+                                hybridClient = value.ToString().ToObject<HybridClient>();
+
+                                // 这里不强制设置free ,因为如果是重连,可能正在执行任务,需要等待执行完成
+
+
+                                //先检查状态是否是在忙碌,在时间戳范围里,如果不在时间戳范围,强制free。
+                                if (!hybridClient.status!.Equals(ScreenConstant.idle) && hybridClient.last_time  + hybridClient.timeout+ hybridClient.delay + ScreenConstant.time_excess < DateTimeOffset.UtcNow.ToUnixTimeMilliseconds())
+                                {
+                                    hybridClient.status = ScreenConstant.idle;
+                                }
+                            }
+                            else
+                            {
+                                hybridClient = new HybridClient
+                                {
+                                    status = ScreenConstant.idle,
+                                };
+                            }
+                            hybridClient.connid=connid;
+                            hybridClient.grant_type = grant_type;
+                            hybridClient.clientid = clientid;
+                            hybridClient.os = device.os;
+                            hybridClient.port = device.port;
+                            hybridClient.name = device.name;
+                            hybridClient.region = device.region;
+                            hybridClient.remote = device.remote;
+                            hybridClient.networks = device.networks;
+                            hybridClient.delay = device.delay;
+                            hybridClient.timeout = device.timeout;
+                            hybridClient.cpu = device.cpu;
+                            hybridClient.ram = device.ram;
+                            hybridClient.last_time= DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+                            hybridClient.deviceId = device.deviceId;
+                            //连接成功,发送消息给客户端。
+                            await SendConnection(connid, new ConnectionMessage
+                            {
+                                connid=connid,
+                                clientid = clientid,
+                                status = hybridClient.status,
+                                grant_type = grant_type,
+                                message_type= MessageType.conn_success,
+                                content = $"连接成功"
+                            });
+                            _logger.LogInformation($"客户端连接成功=>{hybridClient.name},{hybridClient.region},{clientid}:\n{hybridClient.ToJsonString()}");
+                            await _azureRedis.GetRedisClient(8).HashSetAsync($"HybridApi:clients:{serverDevice.deviceId}", client.clientid, hybridClient.ToJsonString());
+                            break;
+                    }
+                }
+            }
+        }
+
+        public override async Task OnDisconnectedAsync(Exception? exception) 
+        {
+
+            var serverDevice = await _device.GetCoreDevice();
+            var connid = Context.ConnectionId;
+            var redisData = await _azureRedis.GetRedisClient(8).HashGetAsync($"SignalRClient:connects:{serverDevice.deviceId}", connid);
+            _logger.LogInformation($"客户端断开连接=>{connid} ");
+            ///连接配置,并且使用钉钉 离线通知。
+            if (!redisData.IsNullOrEmpty)
+            {
+                var client = redisData.ToString().ToObject<SignalRClient>();
+                await _azureRedis.GetRedisClient(8).HashDeleteAsync($"SignalRClient:connects:{serverDevice.deviceId}", connid);
+                if (client != null)
+                {
+                    await Groups.RemoveFromGroupAsync(connid, client.grant_type!);
+                    var value = await _azureRedis.GetRedisClient(8).HashGetAsync($"HybridApi:clients:{serverDevice.deviceId}", client.clientid);
+                    if (value!=default  && value.HasValue)
+                    {
+                        HybridClient hybridClient = value.ToString().ToObject<HybridClient>();
+                        _logger.LogInformation($"客户端断开连接=>{connid},{hybridClient.name},{hybridClient.region},{hybridClient.clientid} ");
+                        long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+                        // 判断是否过期
+                        if (hybridClient.status!.Equals(HybridConstant.busy))
+                        {
+                            //超时了
+                            if (hybridClient.last_time+hybridClient.timeout+hybridClient.delay+ ScreenConstant.time_excess <=now)
+                            {
+                                hybridClient.status=ScreenConstant.offline;
+                                hybridClient.connid= string.Empty;
+                            }
+                        }
+                        else
+                        {
+                            hybridClient.status=ScreenConstant.offline;
+                            hybridClient.connid= string.Empty;
+                        }
+                        await _azureRedis.GetRedisClient(8).HashSetAsync($"HybridApi:clients:{serverDevice.deviceId}", client.clientid, hybridClient.ToJsonString());
+                    }
+                }
+            }
+        }
+        public async Task ReceiveMessage(JsonElement message) 
+        {
+            ////接收消息
+            //如果是超时,放回队列。
+            ///分发新任务。
+            var serverDevice = await _device.GetCoreDevice();
+            long nowNew = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+            var connid = Context.ConnectionId;
+            _logger.LogInformation($"接收消息:{message.ToJsonString()}");
+
+        }
+        public async Task SendConnection(string connectionId, MessageBody msg)
+        {
+            await Clients.Client(connectionId).ReceiveConnection(msg);
+        }
+        public async Task SendMessage(string connectionId, MessageBody msg)
+        {
+            await Clients.Client(connectionId).ReceiveMessage(msg);
+        }
+        public async Task SendDisConnection(string connectionId, MessageBody msg)
+        {
+            await Clients.Client(connectionId).ReceiveDisConnection(msg);
+        }
+    }
+    public interface IClient
+    {
+        Task ReceiveMessage(MessageBody message);
+        Task ReceiveConnection(MessageBody message);
+        Task ReceiveDisConnection(MessageBody message);
+    }
+}

+ 4 - 1
TEAMModelOS/Startup.cs

@@ -204,6 +204,8 @@ namespace TEAMModelOS
            // services.AddSingleton<ILoggerProvider, BlobLoggerProvider>();
             services.AddMvcFilter<RequestAuditFilter>();
             services.AddNetMail();
+            services.AddSignalR().AddAzureSignalR(o=>o.ConnectionString=Configuration.GetValue<string>("Azure:SignalR:ConnectionString"));
+            //services.AddSignalR();
 #if !DEBUG
  //第一步: 配置gzip与br的压缩等级为最优
             //builder.Services.AddMyResponseCompression();
@@ -278,9 +280,10 @@ namespace TEAMModelOS
             //调用放在之后、UseRouting 和 UseCors,但在 UseEndpoints之前
             app.UseAuthentication();
             app.UseAuthorization();
-            
             app.UseEndpoints(endpoints =>
             {
+                //  endpoints.MapHub<IESHybridCloudHub>("/hub/hybrid-cloud").RequireCors("any");
+                endpoints.MapHub<IESHybridCloudHub>("/hub/hybrid-cloud");
                 endpoints.MapControllers();
                 endpoints.MapServerSentEvents("/service/sse", new ServerSentEventsOptions
                 {

+ 4 - 4
TEAMModelOS/TEAMModelOS.csproj

@@ -79,11 +79,11 @@
 		<SpaRoot>ClientApp\</SpaRoot>
 		<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
 		<UserSecretsId>078b5d89-7d90-4f6a-88fc-7d96025990a8</UserSecretsId>
-		<Version>5.2411.27</Version>
-		<AssemblyVersion>5.2411.27.1</AssemblyVersion>
-		<FileVersion>5.2411.27.1</FileVersion>
+		<Version>5.2412.11</Version>
+		<AssemblyVersion>5.2412.11.1</AssemblyVersion>
+		<FileVersion>5.2412.11.1</FileVersion>
 		<Description>TEAMModelOS(IES5)</Description>
-		<PackageReleaseNotes>IES版本说明版本切换标记5.2411.27.1</PackageReleaseNotes>
+		<PackageReleaseNotes>IES版本说明版本切换标记5.2412.11.1</PackageReleaseNotes>
 		<PackageId>TEAMModelOS</PackageId>
 		<Authors>teammodel</Authors>
 		<Company>醍摩豆(成都)信息技术有限公司</Company>

Файловите разлики са ограничени, защото са твърде много
+ 29 - 29
TEAMModelOS/appsettings.Development.json


+ 1 - 1
TEAMModelOS/appsettings.json

@@ -18,7 +18,7 @@
     "Exp": 86400,
     "IdTokenSalt": "8263692E2213497BB55E74792B7900B4",
     "HttpTrigger": "https://teammodelosfunction.chinacloudsites.cn/api/",
-    "Version": "5.2411.27.1"
+    "Version": "5.2412.11.1"
   },
   "Azure": {
     "Storage": {

+ 0 - 8
nuget.config

@@ -1,13 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <configuration>
-<packageSources>
-
-
-
-<add key="habook" value="http://cdhabook.teammodel.cn:20000/v3/index.json" />
-
-
-</packageSources>
 <packageRestore>
 <add key="enabled" value="True" />
 <add key="automatic" value="True" />