瀏覽代碼

Merge branch 'develop' of http://52.130.252.100:10000/TEAMMODEL/TEAMModelOS into develop

jeff 9 月之前
父節點
當前提交
f537b03235
共有 37 個文件被更改,包括 1372 次插入447 次删除
  1. 64 9
      TEAMModelOS.Extension/HTEX.Complex/Controllers/ScreenController.cs
  2. 2 0
      TEAMModelOS.Extension/HTEX.Complex/HTEX.Complex.csproj
  3. 24 4
      TEAMModelOS.Extension/HTEX.Complex/Program.cs
  4. 46 20
      TEAMModelOS.Extension/HTEX.Complex/Services/SignalRScreenServerHub.cs
  5. 10 7
      TEAMModelOS.Extension/HTEX.Complex/Services/TaskService.cs
  6. 0 1
      TEAMModelOS.Extension/HTEX.Complex/appsettings.Development.json
  7. 2 3
      TEAMModelOS.Extension/HTEX.Complex/appsettings.json
  8. 0 35
      TEAMModelOS.Extension/HTEX.ScreenClient/Controllers/WeatherForecastController.cs
  9. 4 1
      TEAMModelOS.Extension/HTEX.ScreenClient/HTEX.ScreenClient.csproj
  10. 13 13
      TEAMModelOS.Extension/HTEX.ScreenClient/Program.cs
  11. 9 156
      TEAMModelOS.Extension/HTEX.ScreenClient/Services/SignalRScreenClientHub.cs
  12. 0 13
      TEAMModelOS.Extension/HTEX.ScreenClient/WeatherForecast.cs
  13. 1 1
      TEAMModelOS.Extension/HTEX.ScreenClient/appsettings.Development.json
  14. 11 6
      TEAMModelOS.Function/IESServiceBusTrigger.cs
  15. 52 0
      TEAMModelOS.SDK/DI/Device/CoreDevice.cs
  16. 174 0
      TEAMModelOS.SDK/Helper/Common/DeviceHelper.cs
  17. 2 1
      TEAMModelOS.SDK/Models/Cosmos/Student/StudentArtResult.cs
  18. 7 2
      TEAMModelOS.SDK/Models/Service/ArtService.cs
  19. 75 59
      TEAMModelOS.SDK/Models/Service/GenPDFService.cs
  20. 1 0
      TEAMModelOS.SDK/TEAMModelOS.SDK.csproj
  21. 12 0
      TEAMModelOS/ClientApp/public/lang/en-US.js
  22. 12 0
      TEAMModelOS/ClientApp/public/lang/zh-CN.js
  23. 12 0
      TEAMModelOS/ClientApp/public/lang/zh-TW.js
  24. 22 5
      TEAMModelOS/ClientApp/src/common/BaseQuickPaper.vue
  25. 9 0
      TEAMModelOS/ClientApp/src/components/student-web/WrongQusetion/QuesList.less
  26. 12 2
      TEAMModelOS/ClientApp/src/components/student-web/WrongQusetion/QuesList.vue
  27. 1 1
      TEAMModelOS/ClientApp/src/view/evaluation/components/BaseImport.vue
  28. 4 3
      TEAMModelOS/ClientApp/src/view/htcommunity/htMgtExam.vue
  29. 21 29
      TEAMModelOS/ClientApp/src/view/htcommunity/htMgtHome.vue
  30. 588 0
      TEAMModelOS/ClientApp/src/view/learnactivity/byStu/htByStuMark.vue
  31. 50 23
      TEAMModelOS/ClientApp/src/view/learnactivity/tabs/htAnswerTable.vue
  32. 2 0
      TEAMModelOS/ClientApp/src/view/syllabus/Syllabus.vue
  33. 89 36
      TEAMModelOS/Controllers/School/ArtReviewController.cs
  34. 35 11
      TEAMModelOS/Controllers/System/GenPDFController.cs
  35. 4 4
      TEAMModelOS/TEAMModelOS.csproj
  36. 1 1
      TEAMModelOS/appsettings.Development.json
  37. 1 1
      TEAMModelOS/appsettings.json

+ 64 - 9
TEAMModelOS.Extension/HTEX.Complex/Controllers/ScreenController.cs

@@ -5,6 +5,8 @@ using Microsoft.AspNetCore.SignalR;
 using HTEX.Complex.Services;
 using System.Text.Json;
 using TEAMModelOS.SDK.Extension;
+using TEAMModelOS.SDK.DI.Device;
+using Microsoft.Azure.Amqp.Framing;
 
 namespace HTEX.Complex.Controllers
 {
@@ -19,8 +21,10 @@ namespace HTEX.Complex.Controllers
         private readonly AzureRedisFactory _azureRedis;
         private readonly IHubContext<SignalRScreenServerHub> _screenServerHub;
         private readonly ILogger<ScreenController> _logger;
+        private readonly CoreDevice _device;
+
         public ScreenController(AzureRedisFactory azureRedis,  IHttpClientFactory httpClient, IConfiguration configuration, AzureStorageFactory azureStorage,
-            DingDing dingDing, IHubContext<SignalRScreenServerHub> screenServerHub , ILogger<ScreenController> logger)
+            DingDing dingDing, IHubContext<SignalRScreenServerHub> screenServerHub , ILogger<ScreenController> logger, CoreDevice device)
         {
             _httpClient = httpClient;
             _configuration = configuration;
@@ -29,8 +33,21 @@ namespace HTEX.Complex.Controllers
             _azureRedis=azureRedis;
             _screenServerHub = screenServerHub;
             _logger = logger;
+            _device = device;
+        }
+        /// <summary>
+        /// 添加任务
+        /// </summary>
+        /// <returns></returns>
+        [HttpPost("push-task")]
+        public async Task<IActionResult> AddTask(JsonElement json) 
+        {
+            GenPDFData  genPDFData = json.ToObject<GenPDFData>();
+            ClientDevice serverDevice =  await _device.GetCoreDevice();
+            var addData = await GenPDFService.AddGenPdfQueue(_azureRedis, genPDFData, serverDevice);
+            await ManualSendTask();
+            return Ok(new { add= addData .add ,total= addData .total});
         }
-
         /// <summary>
         /// 外部触发任务发送
         /// </summary>
@@ -38,8 +55,15 @@ namespace HTEX.Complex.Controllers
         [HttpPost("send-task")]
         public async Task<IActionResult> SendTask(JsonElement json)
         {
-            var values =  await _azureRedis.GetRedisClient(8).HashGetAllAsync("ScreenApi:clients");
-            if (values!=null) 
+            await ManualSendTask(); 
+            return Ok();
+        }
+
+        private async Task ManualSendTask()
+        {
+            ClientDevice serverDevice = await _device.GetCoreDevice();
+            var values = await _azureRedis.GetRedisClient(8).HashGetAllAsync($"ScreenApi:clients:{serverDevice.deviceId}");
+            if (values!=null)
             {
                 foreach (var value in values)
                 {
@@ -51,14 +75,45 @@ namespace HTEX.Complex.Controllers
                     }
                     if (screenClient.status!.Equals(ScreenConstant.idle) && !string.IsNullOrWhiteSpace(screenClient.connid))
                     {
+                        ////////////////////
+                        ///检擦是否在线
+                        //var cts = new CancellationTokenSource();
+                        //try
+                        //{
+                        //    await _screenServerHub.Clients.Client(screenClient.connid!).SendAsync("ReceiveConnection", new ConnectionMessage
+                        //    {
+                        //        connid=screenClient.connid!,
+                        //        clientid = screenClient.clientid,
+                        //        status = screenClient.status,
+                        //        grant_type = screenClient.grant_type,
+                        //        message_type= MessageType.conn_success,
+                        //        content = $"检查连接!"
+                        //    }, cts);
+                        //}
+                        //catch (OperationCanceledException)
+                        //{
+                        //    Console.WriteLine("Operation was canceled.");
+                        //}
+                        //catch (HubException ex) 
+                        //{
+                        //    if (ex.Message.Contains("disconnected"))
+                        //    {
+                        //        _logger.LogInformation($"客户端连接异常=>{screenClient.name},{screenClient.region},{screenClient.clientid},连接失败......{ex.Message},{ex.StackTrace}");  // 客户端不在线
+                        //    }
+                        //    _logger.LogInformation($"客户端连接异常=>{screenClient.name},{screenClient.region},{screenClient.clientid},连接失败......{ex.Message},{ex.StackTrace}");
+                        //}
+                        //catch (Exception ex)
+                        //{
+                        //    _logger.LogInformation($"客户端连接异常=>{screenClient.name},{screenClient.region},{screenClient.clientid},连接失败......{ex.Message},{ex.StackTrace}");
+                        //}
                         _logger.LogInformation($"客户端当前空闲=>{screenClient.name},{screenClient.region},{screenClient.clientid},分发任务......");
                         //连接成功,马上分发任务。
-                        var task = await TaskService.SentTask(_azureRedis, _azureStorage);
+                        var task = await TaskService.SentTask(_azureRedis, _azureStorage, _logger, serverDevice);
                         if (task.genQueue!=null && task.genRedis!=null  && !string.IsNullOrWhiteSpace(task.genQueue.cntName))
                         {
                             screenClient.status =  ScreenConstant.busy;
                             screenClient.last_time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
-                            await _screenServerHub.Clients.Client(screenClient.connid!).SendAsync ("ReceiveMessage", new ScreenProcessMessage
+                            await _screenServerHub.Clients.Client(screenClient.connid!).SendAsync("ReceiveMessage", new ScreenProcessMessage
                             {
                                 connid = screenClient.connid,
                                 clientid =screenClient.clientid,
@@ -67,10 +122,11 @@ namespace HTEX.Complex.Controllers
                                 message_type= MessageType.task_send_success,
                                 content =$"{task.genQueue.ToJsonString()}",//从Redis中获取任务信息
                             });
+                            _logger.LogInformation($"分发任务:{task.genQueue.ToJsonString()}");
                         }
                         else
                         {
-                            _logger.LogInformation($"客户端当前空闲=>{screenClient.name},{screenClient.region},{screenClient.clientid},暂无任务可领取的任务......");
+                            //_logger.LogInformation($"客户端当前空闲=>{screenClient.name},{screenClient.region},{screenClient.clientid},暂无任务可领取的任务......");
                             if (task.genRedis!=null)
                             {
                                 string msgError = $"分发任务异常原因=>{screenClient.name},{screenClient.region},{screenClient.clientid}:{task.msg}\ngenQueue:{task.genQueue?.ToJsonString()}\ngenRedis:{task.genRedis?.ToJsonString()}";
@@ -92,10 +148,9 @@ namespace HTEX.Complex.Controllers
                             });
                         }
                     }
-                    await _azureRedis.GetRedisClient(8).HashSetAsync($"ScreenApi:clients", screenClient.clientid, screenClient.ToJsonString());
+                    await _azureRedis.GetRedisClient(8).HashSetAsync($"ScreenApi:clients:{serverDevice.deviceId}", screenClient.clientid, screenClient.ToJsonString());
                 }
             }
-            return Ok();
         }
     }
 }

+ 2 - 0
TEAMModelOS.Extension/HTEX.Complex/HTEX.Complex.csproj

@@ -15,6 +15,8 @@
 	<PackageReference Include="Hangfire.Redis.StackExchange" Version="1.9.3" />
 	<PackageReference Include="MQTTnet.AspNetCore" Version="4.3.6.1152" />
 	<PackageReference Include="MQTTnet.AspNetCore.Routing" Version="0.4.37" />
+	<PackageReference Include="Serilog.AspNetCore" Version="8.0.2" />
+	<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\TEAMModelOS.SDK\TEAMModelOS.SDK.csproj" />

+ 24 - 4
TEAMModelOS.Extension/HTEX.Complex/Program.cs

@@ -18,13 +18,35 @@ using System.Text.Json;
 using Microsoft.Extensions.Hosting;
 using MQTTnet.AspNetCore.Routing;
 using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Hosting.Server.Features;
+using System.Security.Policy;
+using System.Net.Http;
+using TEAMModelOS.SDK.DI.Device;
+using Serilog;
 namespace HTEX.Complex
 {
     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();
+
             //builder.WebHost.ConfigureKestrel(options =>
             //{
             //    //options.ListenAnyIP(4001, options => {
@@ -87,16 +109,13 @@ namespace HTEX.Complex
             builder.Services.AddAzureStorage(StorageConnectionString, "Default");
             //Redis
             builder.Services.AddAzureRedis(RedisConnectionString, "Default");
-            string? StorageConnectionStringTest = builder.Configuration.GetValue<string>("Azure:Storage:ConnectionString-Test");
-            //Storage
-            builder.Services.AddAzureStorage(StorageConnectionString, "Test");
-
             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(
@@ -155,6 +174,7 @@ namespace HTEX.Complex
                 //server.ClientAcknowledgedPublishPacketAsync += events._mqttServer_ClientAcknowledgedPublishPacketAsync;//已确认发布数据包
             });
             app.Run();
+           
         }
     }
 }

+ 46 - 20
TEAMModelOS.Extension/HTEX.Complex/Services/SignalRScreenServerHub.cs

@@ -7,6 +7,12 @@ using System.Web;
 using System.Text;
 
 using StackExchange.Redis;
+using Microsoft.Extensions.Logging;
+using TEAMModelOS.SDK.Models;
+using Google.Protobuf.WellKnownTypes;
+using Microsoft.Extensions.DependencyInjection;
+using TEAMModelOS.SDK.DI.Device;
+
 namespace HTEX.Complex.Services
 {
     public class SignalRScreenServerHub : Hub<IClient>
@@ -16,12 +22,14 @@ namespace HTEX.Complex.Services
         private readonly AzureRedisFactory _azureRedis;
         private readonly AzureStorageFactory _azureStorage;
         private readonly DingDing _dingDing;
-        public SignalRScreenServerHub(AzureRedisFactory azureRedis, ILogger<SignalRScreenServerHub> logger ,AzureStorageFactory azureStorage, DingDing dingDing)
+        private readonly CoreDevice _device;
+        public SignalRScreenServerHub(AzureRedisFactory azureRedis,   ILogger<SignalRScreenServerHub> logger,  AzureStorageFactory azureStorage, DingDing dingDing, CoreDevice device)
         {
             _logger = logger;
             _azureRedis = azureRedis;
             _azureStorage = azureStorage;
             _dingDing = dingDing;
+            _device=device;
         }
 
         /// <summary>
@@ -30,6 +38,9 @@ namespace HTEX.Complex.Services
         /// <returns></returns>
         public override async Task OnConnectedAsync()
         {
+            var serverDevice = await  _device.GetCoreDevice();
+            
+             
             var connid = Context.ConnectionId;
             var httpContext = Context.GetHttpContext();
             if (httpContext != null)
@@ -49,15 +60,16 @@ namespace HTEX.Complex.Services
                     {
                         connid = connid,
                         grant_type = grant_type,
-                        clientid= clientid
+                        clientid= clientid,
+                        serverid=serverDevice.deviceId,
                     };
-                    await _azureRedis.GetRedisClient(8).HashSetAsync($"SignalRClient:connects", connid, client.ToJsonString());
-                    ClientDevice device = HttpUtility.UrlDecode(_device, Encoding.Unicode).ToObject<ClientDevice>();
+                    await _azureRedis.GetRedisClient(8).HashSetAsync($"SignalRClient:connects:{serverDevice.deviceId}", connid, client.ToJsonString());
+                    ScreenClient device = HttpUtility.UrlDecode(_device, Encoding.Unicode).ToObject<ScreenClient>();
                     switch (true) 
                     {
                         case bool when grant_type.Equals(ScreenConstant.grant_type):
                             ScreenClient screenClient ;
-                            var value = await _azureRedis.GetRedisClient(8).HashGetAsync($"ScreenApi:clients", client.clientid);
+                            var value = await _azureRedis.GetRedisClient(8).HashGetAsync($"ScreenApi:clients:{serverDevice.deviceId}", client.clientid);
                             if (value!=default  && value.HasValue)
                             {
                                 screenClient = value.ToString().ToObject<ScreenClient>();
@@ -93,6 +105,7 @@ namespace HTEX.Complex.Services
                             screenClient.cpu = device.cpu;
                             screenClient.ram = device.ram;
                             screenClient.last_time= DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+                            screenClient.deviceId = serverDevice.deviceId;
                             //连接成功,发送消息给客户端。
                             await SendConnection(connid, new ConnectionMessage
                             {
@@ -107,7 +120,7 @@ namespace HTEX.Complex.Services
                             if (screenClient.status!.Equals(ScreenConstant.idle)) {
                                 _logger.LogInformation($"客户端当前空闲=>{screenClient.name},{screenClient.region},{clientid},分发任务......");
                                 //连接成功,马上分发任务。
-                                var task = await TaskService.SentTask(_azureRedis,_azureStorage);
+                                var task = await TaskService.SentTask(_azureRedis,_azureStorage, _logger, serverDevice);
                                 if (task.genQueue!=null && task.genRedis!=null  && !string.IsNullOrWhiteSpace(task.genQueue.cntName))
                                 {
                                     screenClient.status =  ScreenConstant.busy;
@@ -121,9 +134,10 @@ namespace HTEX.Complex.Services
                                         message_type= MessageType.task_send_success,
                                         content =$"{task.genQueue.ToJsonString()}",//从Redis中获取任务信息
                                     });
+                                    _logger.LogInformation($"分发任务:{task.genQueue.ToJsonString()}");
                                 }
                                 else {
-                                    _logger.LogInformation($"客户端当前空闲=>{screenClient.name},{screenClient.region},{clientid},暂无任务可领取的任务......");
+                                   // _logger.LogInformation($"客户端当前空闲=>{screenClient.name},{screenClient.region},{clientid},暂无任务可领取的任务......");
                                     if (task.genRedis!=null)
                                     {
                                         string msgError = $"分发任务异常原因=>{screenClient.name},{screenClient.region},{clientid}:{task.msg}\ngenQueue:{task.genQueue?.ToJsonString()}\ngenRedis:{task.genRedis?.ToJsonString()}";
@@ -144,7 +158,7 @@ namespace HTEX.Complex.Services
                                     });
                                 }
                             }
-                            await _azureRedis.GetRedisClient(8).HashSetAsync($"ScreenApi:clients", client.clientid, screenClient.ToJsonString());
+                            await _azureRedis.GetRedisClient(8).HashSetAsync($"ScreenApi:clients:{serverDevice.deviceId}", client.clientid, screenClient.ToJsonString());
                             break;
                     }
                 }
@@ -165,30 +179,40 @@ namespace HTEX.Complex.Services
 
         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", connid);
+            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", connid);
+                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($"ScreenApi:clients", client.clientid);
+                    var value =  await _azureRedis.GetRedisClient(8).HashGetAsync($"ScreenApi:clients:{serverDevice.deviceId}", client.clientid);
                     if (value!=default  && value.HasValue) 
                     {
                         ScreenClient screenClient = value.ToString().ToObject<ScreenClient>() ;
                         _logger.LogInformation($"客户端断开连接=>{connid},{screenClient.name},{screenClient.region},{screenClient.clientid} ");
                         long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
                         // 判断是否过期
-                        if (screenClient.status!.Equals(ScreenConstant.busy )  &&    screenClient.last_time+screenClient.timeout+screenClient.delay+ ScreenConstant.time_excess <=now)
+                        if (screenClient.status!.Equals(ScreenConstant.busy))
                         {
+                            //超时了
+                            if (screenClient.last_time+screenClient.timeout+screenClient.delay+ ScreenConstant.time_excess <=now)
+                            {
+                                screenClient.status=ScreenConstant.offline;
+                                screenClient.connid= string.Empty;
+                            }
+                        }
+                        else {
                             screenClient.status=ScreenConstant.offline;
                             screenClient.connid= string.Empty;
-                            await _azureRedis.GetRedisClient(8).HashSetAsync($"ScreenApi:clients", client.clientid, screenClient.ToJsonString());
                         }
+                        await _azureRedis.GetRedisClient(8).HashSetAsync($"ScreenApi:clients:{serverDevice.deviceId}", client.clientid, screenClient.ToJsonString());
                     }
                 }
             }
@@ -200,13 +224,14 @@ namespace HTEX.Complex.Services
             ////接收消息
             //如果是超时,放回队列。
             ///分发新任务。
+            var serverDevice = await _device.GetCoreDevice();
             long nowNew = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
             var connid = Context.ConnectionId;
             PDFGenQueue? task =  message.content?.ToObject<PDFGenQueue>();
             ScreenClient screenClient = null;
             try
             {  //释放客户端的忙碌状态。
-                var redisData = await _azureRedis.GetRedisClient(8).HashGetAsync($"SignalRClient:connects", connid);
+                var redisData = await _azureRedis.GetRedisClient(8).HashGetAsync($"SignalRClient:connects:{serverDevice.deviceId}", connid);
                 var client = redisData.ToString().ToObject<SignalRClient>();
                 await SendConnection(connid, new ConnectionMessage
                 {
@@ -217,14 +242,14 @@ namespace HTEX.Complex.Services
                     message_type= MessageType.conn_success,
                     content = $"客户端空闲,等待任务分发......"
                 });
-                var value = await _azureRedis.GetRedisClient(8).HashGetAsync($"ScreenApi:clients", client.clientid);
+                var value = await _azureRedis.GetRedisClient(8).HashGetAsync($"ScreenApi:clients:{serverDevice.deviceId}", client.clientid);
                 if (value!=default  && value.HasValue)
                 {
                     screenClient = value.ToString().ToObject<ScreenClient>();
                     screenClient.status=ScreenConstant.idle;
                     screenClient.last_time=nowNew;
                     screenClient.taskComplete++;
-                    await _azureRedis.GetRedisClient(8).HashSetAsync($"ScreenApi:clients", client.clientid,screenClient.ToJsonString());
+                    await _azureRedis.GetRedisClient(8).HashSetAsync($"ScreenApi:clients:{serverDevice.deviceId}", client.clientid,screenClient.ToJsonString());
                     _logger.LogInformation($"客户端空闲,等待任务分发......=>{connid},{screenClient.name},{screenClient.region},{screenClient.clientid} ");
                 }
                 
@@ -248,12 +273,12 @@ namespace HTEX.Complex.Services
                 ///如果是超时,放回队列。
                 if (message.result==4)
                 {
-                    await _azureRedis.GetRedisClient(8).ListLeftPushAsync($"PDFGen:Queue", task.ToJsonString());
+                    await _azureRedis.GetRedisClient(8).ListLeftPushAsync($"PDFGen:Queue:{serverDevice.deviceId}", task.ToJsonString());
                 }
             }
             if (screenClient!=null && screenClient.status!.Equals(ScreenConstant.idle))
             {
-                var taskData = await TaskService.SentTask(_azureRedis, _azureStorage);
+                var taskData = await TaskService.SentTask(_azureRedis, _azureStorage, _logger, serverDevice);
                 if (taskData.genQueue!=null && taskData.genRedis!=null  && !string.IsNullOrWhiteSpace(taskData.genQueue.cntName))
                 {
                     screenClient.status =  ScreenConstant.busy;
@@ -267,10 +292,11 @@ namespace HTEX.Complex.Services
                         message_type= MessageType.task_send_success,
                         content =$"{taskData.genQueue.ToJsonString()}",//从Redis中获取任务信息
                     });
+                    _logger.LogInformation($"分发任务:{taskData.genQueue.ToJsonString()}");
                 }
                 else
                 {
-                    _logger.LogInformation($"客户端当前空闲=>{screenClient.name},{screenClient.region},{screenClient.clientid},暂无任务可领取的任务......");
+                    //_logger.LogInformation($"客户端当前空闲=>{screenClient.name},{screenClient.region},{screenClient.clientid},暂无任务可领取的任务......");
                     if (taskData.genRedis!=null)
                     {
                         string msgError = $"分发任务异常原因=>{screenClient.name},{screenClient.region},{screenClient.clientid}:{taskData.msg}\ngenQueue:{taskData.genQueue?.ToJsonString()}\ngenRedis:{taskData.genRedis?.ToJsonString()}";
@@ -291,7 +317,7 @@ namespace HTEX.Complex.Services
                         content = taskData.msg
                     });
                 }
-                await _azureRedis.GetRedisClient(8).HashSetAsync($"ScreenApi:clients", screenClient.clientid, screenClient.ToJsonString());
+                await _azureRedis.GetRedisClient(8).HashSetAsync($"ScreenApi:clients:{serverDevice.deviceId}", screenClient.clientid, screenClient.ToJsonString());
             }
         }
         public async Task SendConnection(string connectionId, MessageBody msg)

+ 10 - 7
TEAMModelOS.Extension/HTEX.Complex/Services/TaskService.cs

@@ -9,12 +9,12 @@ namespace HTEX.Complex.Services
 {
     public class TaskService
     {
-        public static async Task<(PDFGenRedis genRedis, PDFGenQueue genQueue, string msg)> SentTask(AzureRedisFactory _azureRedis, AzureStorageFactory _azureStorage)
+        public static async Task<(PDFGenRedis genRedis, PDFGenQueue genQueue, string msg)> SentTask(AzureRedisFactory _azureRedis, AzureStorageFactory _azureStorage, ILogger logger,ClientDevice serverDevice)
         {
             string msg = string.Empty;
             //从尾部弹出元素,队列先进先出
             long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
-            var queueValue = await _azureRedis.GetRedisClient(8).ListRightPopAsync("PDFGen:Queue");
+            var queueValue = await _azureRedis.GetRedisClient(8).ListRightPopAsync($"PDFGen:Queue:{serverDevice.deviceId}");
             PDFGenRedis genRedis = null;
             PDFGenQueue genQueue = null;
             if (queueValue!=default && queueValue.HasValue)
@@ -38,8 +38,8 @@ namespace HTEX.Complex.Services
                             url=  HttpUtility.UrlDecode(url);
                             uri = new Uri(url);
                             string host = uri.Host;
-                            string azure_region = genQueue.env.Equals(ScreenConstant.env_develop) ? "Test" : "Default";
-                            var blobServiceClient = _azureStorage.GetBlobServiceClient("Test");
+                            
+                            var blobServiceClient = _azureStorage.GetBlobServiceClient();
                             if (host.Equals(blobServiceClient.Uri.Host))
                             {
                                 // 获取容器名,它是路径的第一个部分
@@ -49,15 +49,16 @@ namespace HTEX.Complex.Services
                                 string directoryPath = string.Join("", uri.Segments, 2, uri.Segments.Length - 3);
                                 string? fileName = Path.GetFileNameWithoutExtension(uri.AbsolutePath);
                                 string blobPath = $"{directoryPath}{fileName}.pdf";
-                                var urlSas = _azureStorage.GetBlobSAS(containerName, blobPath, BlobSasPermissions.Write|BlobSasPermissions.Read, hour: 1, name: azure_region);
+                                var urlSas = _azureStorage.GetBlobSAS(containerName, blobPath, BlobSasPermissions.Write|BlobSasPermissions.Read, hour: 1);
                                 genQueue.blobSas =  urlSas.sas;
                                 genQueue.blobName=blobPath;
                                 genQueue.cntName=containerName;
                                 genQueue.blobFullUrl=urlSas.fullUri;
+                                
                             }
                             else
                             {
-                                msg="数据地址与服务提供的站点不一致!";
+                                msg=$"数据地址与服务提供的站点不一致!{host},{blobServiceClient.Uri.Host}";
                                 genRedis.status=3;
                                 genRedis.msg = msg;
 
@@ -73,7 +74,9 @@ namespace HTEX.Complex.Services
                     }
                     catch (Exception ex)
                     {
-                        msg=$"数据地址处理异常,异常信息:{ex.Message}";
+                        
+                        msg =$"数据地址处理异常,异常信息:{ex.Message}";
+                        logger.LogInformation($"{msg},{ex.StackTrace}");
                         genRedis.status=3;
                         genRedis.msg = ex.Message;
                     }

+ 0 - 1
TEAMModelOS.Extension/HTEX.Complex/appsettings.Development.json

@@ -19,7 +19,6 @@
   "Azure": {
     "Storage": {
       "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=teammodeltest;AccountKey=O2W2vadCqexDxWO+px+QK7y1sHwsYj8f/WwKLdOdG5RwHgW/Dupz9dDUb4c1gi6ojzQaRpFUeAAmOu4N9E+37A==;EndpointSuffix=core.chinacloudapi.cn",
-      "ConnectionString-Test": "DefaultEndpointsProtocol=https;AccountName=teammodeltest;AccountKey=O2W2vadCqexDxWO+px+QK7y1sHwsYj8f/WwKLdOdG5RwHgW/Dupz9dDUb4c1gi6ojzQaRpFUeAAmOu4N9E+37A==;EndpointSuffix=core.chinacloudapi.cn"
     },
     "Redis": {
       "ConnectionString": "52.130.252.100:6379,password=habook,ssl=false,abortConnect=False,writeBuffer=10240"

+ 2 - 3
TEAMModelOS.Extension/HTEX.Complex/appsettings.json

@@ -9,17 +9,16 @@
   "Kestrel": {
     "Endpoints": {
       "Http": {
-        "Url": "http://*:8807"
+        "Url": "http://*:8907"
       },
       "Https": {
-        "Url": "https://*:8808"
+        "Url": "https://*:8908"
       }
     }
   },
   "Azure": {
     "Storage": {
       "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=teammodelos;AccountKey=Dl04mfZ9hE9cdPVO1UtqTUQYN/kz/dD/p1nGvSq4tUu/4WhiKcNRVdY9tbe8620nPXo/RaXxs+1F9sVrWRo0bg==;EndpointSuffix=core.chinacloudapi.cn",
-      "ConnectionString-Test": "DefaultEndpointsProtocol=https;AccountName=teammodeltest;AccountKey=O2W2vadCqexDxWO+px+QK7y1sHwsYj8f/WwKLdOdG5RwHgW/Dupz9dDUb4c1gi6ojzQaRpFUeAAmOu4N9E+37A==;EndpointSuffix=core.chinacloudapi.cn"
     },
     "Redis": {
       "ConnectionString": "CoreRedisCN.redis.cache.chinacloudapi.cn:6380,password=LyJWP1ORJdv+poXWofAF97lhCEQPg1wXWqvtzXGXQuE=,ssl=True,abortConnect=False"

+ 0 - 35
TEAMModelOS.Extension/HTEX.ScreenClient/Controllers/WeatherForecastController.cs

@@ -1,35 +0,0 @@
-using Microsoft.AspNetCore.Hosting.Server.Features;
-using Microsoft.AspNetCore.Mvc;
-
-namespace HTEX.ScreenClient.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();
-        }
-    }
-}

+ 4 - 1
TEAMModelOS.Extension/HTEX.ScreenClient/HTEX.ScreenClient.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk.Web">
+<Project Sdk="Microsoft.NET.Sdk.Web">
 
   <PropertyGroup>
     <TargetFramework>net8.0</TargetFramework>
@@ -10,10 +10,13 @@
     <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.8" />
     <PackageReference Include="System.Diagnostics.PerformanceCounter" Version="8.0.0" />
     <PackageReference Include="System.Management" Version="8.0.0" />
+	<PackageReference Include="Serilog.AspNetCore" Version="8.0.2" />
+	<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
   </ItemGroup>
 
   <ItemGroup>
     <ProjectReference Include="..\..\TEAMModelOS.SDK\TEAMModelOS.SDK.csproj" />
   </ItemGroup>
 
+
 </Project>

+ 13 - 13
TEAMModelOS.Extension/HTEX.ScreenClient/Program.cs

@@ -5,7 +5,8 @@ using System.Net;
 using System.Net.Sockets;
 using System.Runtime.InteropServices;
 using System.Text.RegularExpressions;
-
+using TEAMModelOS.SDK.DI.Device;
+using Serilog;
 namespace HTEX.ScreenClient
 {
     public class Program
@@ -14,23 +15,22 @@ namespace HTEX.ScreenClient
         {
            
             var builder = WebApplication.CreateBuilder(args);
-   
-            builder.Services.AddControllers();
-            builder.Services.AddHttpClient();
-            builder.Services.AddHttpContextAccessor();
-            //CheckOrNewPort(1883)
-            //CheckOrNewPort(5000)
-            builder.WebHost.UseKestrel(options => {
-                //options.ListenAnyIP(4001, options => {
-                //   // options.UseHttps("Crt/iteden.pfx", "iteden"); 
-                //});
-                options.ListenAnyIP(CheckOrNewPort(1883), options => {/*options.UseHttps("Crt/iteden.pfx", "iteden");*/ });
+            builder.WebHost.UseKestrel(options =>
+            {
+                // options.ListenAnyIP(CheckOrNewPort(1883), options => {/*options.UseHttps("Crt/iteden.pfx", "iteden");*/ });
                 options.ListenAnyIP(CheckOrNewPort(5000), options => {/* options.UseHttps("Configs/Crt/iteden.pfx", "iteden"); */}); // Default HTTP pipeline
             });
+            //写在端口配置之前,并且在用到的DI之前。否则会导致DI注入失败
+            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();
 
+            builder.Services.AddControllers();
+            builder.Services.AddHttpClient();
+            builder.Services.AddHttpContextAccessor();
             builder.Services.AddHostedService<SignalRScreenClientHub>();
+            builder.Services.AddSingleton<CoreDevice>();
             var app = builder.Build();
-            // Configure the HTTP request pipeline.
 
             app.UseHttpsRedirection();
 

+ 9 - 156
TEAMModelOS.Extension/HTEX.ScreenClient/Services/SignalRScreenClientHub.cs

@@ -18,6 +18,7 @@ using System.Text.RegularExpressions;
 using System.Threading.Tasks;
 using System.Web;
 using TEAMModelOS.SDK;
+using TEAMModelOS.SDK.DI.Device;
 using TEAMModelOS.SDK.Extension;
 
 namespace HTEX.ScreenClient.Services
@@ -26,28 +27,23 @@ namespace HTEX.ScreenClient.Services
     {
         private readonly IConfiguration _configuration; 
         private readonly ILogger<SignalRScreenClientHub> _logger;
+        private TEAMModelOS.SDK.ScreenClient? device;
+        private readonly CoreDevice _device;
         private readonly IHttpClientFactory _httpClientFactory;
-        private readonly IServiceProvider _services;
-        private IEnumerable<string>? _url = new List<string>();
-        private ClientDevice? device;
-        public SignalRScreenClientHub(IConfiguration configuration,ILogger<SignalRScreenClientHub> logger,IHttpClientFactory httpClientFactory, IServiceProvider services,IHostApplicationLifetime lifetime) 
+        public SignalRScreenClientHub(IConfiguration configuration,ILogger<SignalRScreenClientHub> logger,IHttpClientFactory httpClientFactory, CoreDevice device) 
         {
                 
             _configuration=configuration;
             _logger=logger;
             _httpClientFactory=httpClientFactory;
-            _services=services;
-            lifetime.ApplicationStarted.Register(() => {
-                var server = _services.GetService<IServer>();
-                _url = server?.Features.Get<IServerAddressesFeature>()?.Addresses;
-            });
+            _device = device;
         }
         
         protected async override Task ExecuteAsync(CancellationToken stoppingToken)
         {
-            device =  await GetClientInfo();
-            string hashData = $"{device.name}-{device.remote}-{device.port}-{device.os}-{string.Join(",",device.networks.Select(x=>$"{x.mac}-{x.ip}"))}";
-            string clientid =  ShaHashHelper.GetSHA256(hashData);
+            var coreDevice =  await _device.GetCoreDevice();
+            device=coreDevice.ToJsonString().ToObject<TEAMModelOS.SDK.ScreenClient>();
+            string clientid = device.deviceId!;
             string? CenterUrl = _configuration.GetSection("ScreenClient:CenterUrl").Value;
             string? ScreenUrl = _configuration.GetSection("ScreenClient:ScreenUrl").Value;
             long Timeout = _configuration.GetValue<long>("ScreenClient:Timeout");
@@ -220,150 +216,7 @@ namespace HTEX.ScreenClient.Services
             return (status, msg, task);
         }
 
-        public async Task<ClientDevice> GetClientInfo()
-        {
-            string hostName =$"{Environment.UserName}-{Dns.GetHostName()}" ;
-            string os = RuntimeInformation.OSDescription;
-            //获取当前客户端的服务端口
-            var _httpClient = _httpClientFactory.CreateClient();
-            ClientDevice device = new ClientDevice { name =hostName, os= os };
-            HttpResponseMessage message = await _httpClient.PostAsJsonAsync("https://www.teammodel.cn/core/system-info", new { });
-            if (message.IsSuccessStatusCode)
-            {
-                JsonNode? json = JsonSerializer.Deserialize<JsonNode>(await message.Content.ReadAsStringAsync());
-                var ip =  json?["ip"];
-                var region = json?["region"];
-                _logger.LogInformation($"远程地址:{ip}");
-                _logger.LogInformation($"所属地区:{region}");
-                device.remote=ip?.ToString();
-                device.region=region?.ToString();
-            }
-            _logger.LogInformation($"计算机名:{hostName}");
-            _logger.LogInformation($"系统名称:{RuntimeInformation.OSDescription}");
-            int CpuCoreCount = 0;
-            long MenemorySize = 0;
-            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
-            {
-                // 获取CPU核心数
-                //int processorCount = Environment.ProcessorCount;
-               //Console.WriteLine("CPU 核心数: " + processorCount);
-                using (ManagementClass managementClass = new ManagementClass("Win32_Processor"))
-                {
-                    using (ManagementObjectCollection managementObjectCollection = managementClass.GetInstances())
-                    {
-                        foreach (ManagementObject managementObject in managementObjectCollection)
-                        {
-                            CpuCoreCount += Convert.ToInt32(managementObject.Properties["NumberOfLogicalProcessors"].Value);
-                        }
-                    }
-                }
-                using (ManagementClass mc = new ManagementClass("Win32_ComputerSystem"))
-                {
-                    using (ManagementObjectCollection moc = mc.GetInstances())
-                    {
-                        foreach (ManagementObject mo in moc)
-                        {
-                            if (mo["TotalPhysicalMemory"]!= null)
-                            {
-                                MenemorySize = Convert.ToInt64(mo["TotalPhysicalMemory"]);
-                            }
-                        }
-                    }
-                }
-            }
-            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
-            {
-                //int processorCount = Environment.ProcessorCount;
-               // Console.WriteLine("CPU 核心数: " + processorCount);
-                string[] cpu_lines = File.ReadAllLines("/proc/cpuinfo");
-                CpuCoreCount= cpu_lines.Count(line => line.StartsWith("processor", StringComparison.OrdinalIgnoreCase));
-                string[] mem_lines = File.ReadAllLines("/proc/meminfo");
-                var match = mem_lines.FirstOrDefault(line => line.StartsWith("MemTotal:"));
-                if (match != null)
-                {
-                    var matchResult = Regex.Match(match, @"\d+");
-                    if (matchResult.Success)
-                    {
-                        MenemorySize=  long.Parse(matchResult.Value);
-                    }
-                }
-            }
-            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
-            {
-                using (var process = new Process())
-                {
-                    process.StartInfo.FileName = "/usr/sbin/sysctl";
-                    process.StartInfo.Arguments = "-n hw.ncpu";
-                    process.StartInfo.RedirectStandardOutput = true;
-                    process.StartInfo.UseShellExecute = false;
-                    process.Start();
-                    string output = process.StandardOutput.ReadToEnd().Trim();
-                    int coreCount;
-                    if (int.TryParse(output, out coreCount))
-                    {
-                        CpuCoreCount= coreCount;
-                    }
-                }
-                using (var process = new Process())
-                {
-                    process.StartInfo.FileName = "/usr/sbin/sysctl";
-                    process.StartInfo.Arguments = "-n hw.memsize";
-                    process.StartInfo.RedirectStandardOutput = true;
-                    process.StartInfo.UseShellExecute = false;
-                    process.Start();
-                    string output = process.StandardOutput.ReadToEnd().Trim();
-                    long memorySize;
-                    if (long.TryParse(output, out memorySize))
-                    {
-                        MenemorySize=  memorySize;
-                    }
-                }
-            }
-
-            //Console.WriteLine("CPU 核心数: " + CpuCoreCount+",RAM 大小:"+MenemorySize);
-            _logger.LogInformation($"内存大小:{MenemorySize}");
-            _logger.LogInformation($"核心数量:{CpuCoreCount}");
-            device.cpu=CpuCoreCount;
-            device.ram=MenemorySize;
-            var nics = NetworkInterface.GetAllNetworkInterfaces();
-            foreach (var nic in nics)
-            {
-                if (nic.OperationalStatus == OperationalStatus.Up)
-                {
-                    var name = $"{nic.Name}-{nic.Description}";
-                    var mac = nic.GetPhysicalAddress().ToString();
-                    var properties = nic.GetIPProperties();
-                    var unicastAddresses = properties.UnicastAddresses;
-                    foreach (var unicast in unicastAddresses)
-                    {
-                        if (unicast.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
-                        {
-                            var ip = unicast.Address.ToString();
-                            Network network= new Network() { mac=mac, ip=ip ,name= name};
-                            if (!string.IsNullOrWhiteSpace(mac.ToString())  && !mac.Equals("000000000000"))
-                            {
-                                device.networks.Add(network);
-                                _logger.LogInformation($"网卡名称: {name}");
-                                _logger.LogInformation($"网卡地址: {mac}");
-                                _logger.LogInformation($"内网地址: {ip}");
-                            }
-                        }
-                    }
-                }
-            }
-            if (_url!=null) 
-            {
-                List<int> ports = new List<int>();
-                foreach (var url in _url)
-                {
-                    Uri uri = new Uri(url);
-                    ports.Add(uri.Port);
-                }
-                device.port= string.Join(",", ports);
-                _logger.LogInformation($"占用端口: {device.port}");
-            }
-            return device ;
-        }
+       
        
     }
     public class ExponentialBackoffReconnectPolicy : IRetryPolicy

+ 0 - 13
TEAMModelOS.Extension/HTEX.ScreenClient/WeatherForecast.cs

@@ -1,13 +0,0 @@
-namespace HTEX.ScreenClient
-{
-    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; }
-    }
-}

+ 1 - 1
TEAMModelOS.Extension/HTEX.ScreenClient/appsettings.Development.json

@@ -10,6 +10,6 @@
     "ScreenUrl": "http://52.130.252.100:13000",
     "Timeout": 30000,
     "Delay": 500,
-    "CenterUrl": "http://52.130.252.100:8807"
+    "CenterUrl": "http://127.0.0.1:8807"
   }
 }

+ 11 - 6
TEAMModelOS.Function/IESServiceBusTrigger.cs

@@ -100,25 +100,30 @@ namespace TEAMModelOS.Function
             _logger.LogInformation("Message ID: {id}", message.MessageId);
             _logger.LogInformation("Message Body: {body}", message.Body);
             _logger.LogInformation("Message Content-Type: {contentType}", message.ContentType);
-            var json = JsonDocument.Parse(message.Body);
+            var json = JsonDocument.Parse(message.Body).RootElement;
 
             try
             {
                 switch (true)
                 {
-                    case bool when json.RootElement.TryGetProperty("bizType", out JsonElement _bizType) && $"{_bizType}".Equals("OfflineRecord"):
+                    case bool when json.TryGetProperty("bizType", out JsonElement _bizType) && $"{_bizType}".Equals("OfflineRecord"):
                         //处理教师线下研修报告的生成。
                         //await GenOfflineRecordPdf(element, msg);
                         break;
-                    case bool when json.RootElement.TryGetProperty("bizType", out JsonElement _bizType) && $"{_bizType}".Equals("ArtStudentPdf"):
-                        await GenPDFService.GenArtStudentPdf(_azureRedis, _azureCosmos,
-                  _coreAPIHttpService, _dingDing, _azureStorage,_configuration, json.RootElement);
+                    case bool when json.TryGetProperty("bizType", out JsonElement _bizType) && $"{_bizType}".Equals("ArtStudentPdf"):
+                        json.TryGetProperty("studentIds", out JsonElement _studentIds);
+                        json.TryGetProperty("artId", out JsonElement _artId);
+                        json.TryGetProperty("schoolCode", out JsonElement _schoolId);
+                        json.TryGetProperty("headLang", out JsonElement headLang);
+                        List<string> studentIds = _studentIds.ToObject<List<string>>();
+                        var data =  await GenPDFService.GenArtStudentPdf(_azureRedis, _azureCosmos,  _coreAPIHttpService, _dingDing, _azureStorage,_configuration, studentIds,$"{_artId}",$"{_schoolId}",$"{headLang}");
+                        await GenPDFService. PushScreenTask(_azureRedis, _configuration, $"{_artId}",data. art,data. studentPdfs,_httpClient,_dingDing);
                         break;
                 }
             }
             catch (Exception ex)
             {
-                await _dingDing.SendBotMsg($"{Environment.GetEnvironmentVariable("Option:Location")}-ServiceBus,GenPDF()\n{ex.Message}\n{ex.StackTrace}\n\n{json.RootElement.ToJsonString()}", GroupNames.醍摩豆服務運維群組);
+                await _dingDing.SendBotMsg($"{Environment.GetEnvironmentVariable("Option:Location")}-ServiceBus,GenPDF()\n{ex.Message}\n{ex.StackTrace}\n\n{json.ToJsonString()}", GroupNames.醍摩豆服務運維群組);
             }
             finally
             {  // Complete the message

+ 52 - 0
TEAMModelOS.SDK/DI/Device/CoreDevice.cs

@@ -0,0 +1,52 @@
+using Microsoft.AspNetCore.Hosting.Server.Features;
+using Microsoft.Azure.Cosmos;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Hosting.Server;
+namespace TEAMModelOS.SDK.DI.Device
+{
+    public class CoreDevice
+    {
+        private readonly IServiceProvider _services;
+        private readonly ILogger _logger;
+        private readonly IHttpClientFactory _httpClientFactory;
+        private readonly IHostApplicationLifetime _lifetime;
+        private ConcurrentDictionary<string, Task <ClientDevice>> clientDevice { get; } = new ConcurrentDictionary<string, Task<ClientDevice>>();
+        public CoreDevice(IServiceProvider services,  ILogger<CoreDevice> logger, IHostApplicationLifetime lifetime, IHttpClientFactory httpClientFactory)
+        {
+            _services = services;
+            _logger = logger;
+            _services=services;
+            _httpClientFactory=httpClientFactory;
+            _lifetime = lifetime;
+           
+          
+        }
+        public   Task<ClientDevice> GetCoreDevice()
+        {
+            try
+            {
+                var server = _services.GetService<IServer>();
+                var d = server?.Features.Get<IServerAddressesFeature>();
+                IEnumerable<string>? _url = server?.Features.Get<IServerAddressesFeature>()?.Addresses;
+                var device = clientDevice.GetOrAdd("Device", x => DeviceHelper.GetClientInfo(_httpClientFactory, _logger, _url));
+                return device;
+            }
+            catch (Exception e)
+            {
+                _logger?.LogWarning(e, e.Message);
+                throw;
+            }
+        }
+    }
+}

+ 174 - 0
TEAMModelOS.SDK/Helper/Common/DeviceHelper.cs

@@ -0,0 +1,174 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Net.Http;
+using System.Net.NetworkInformation;
+using System.Net;
+using System.Runtime.InteropServices;
+using System.Security.Policy;
+using System.Text;
+using System.Text.Json.Nodes;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using System.Text.Json;
+using Microsoft.Extensions.Logging;
+using System.IO;
+using Microsoft.Extensions.Configuration;
+using System.Management;
+using System.Net.Http.Json;
+namespace TEAMModelOS.SDK
+{
+    public static class DeviceHelper
+    {
+        public static async Task<ClientDevice> GetClientInfo(IHttpClientFactory _httpClientFactory, ILogger _logger, IEnumerable<string>? _url)
+        {
+            string hostName = $"{Environment.UserName}-{Dns.GetHostName()}";
+            string os = RuntimeInformation.OSDescription;
+            //获取当前客户端的服务端口
+            var _httpClient = _httpClientFactory.CreateClient();
+            ClientDevice device = new ClientDevice { name =hostName, os= os };
+            HttpResponseMessage message = await _httpClient.PostAsJsonAsync("https://www.teammodel.cn/core/system-info", new { });
+            if (message.IsSuccessStatusCode)
+            {
+                JsonNode? json = JsonSerializer.Deserialize<JsonNode>(await message.Content.ReadAsStringAsync());
+                var ip = json?["ip"];
+                var region = json?["region"];
+                _logger.LogInformation($"远程地址:{ip}");
+                _logger.LogInformation($"所属地区:{region}");
+                device.remote=ip?.ToString();
+                device.region=region?.ToString();
+            }
+            _logger.LogInformation($"计算机名:{hostName}");
+            _logger.LogInformation($"系统名称:{RuntimeInformation.OSDescription}");
+            int CpuCoreCount = 0;
+            long MenemorySize = 0;
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                // 获取CPU核心数
+                //int processorCount = Environment.ProcessorCount;
+                //Console.WriteLine("CPU 核心数: " + processorCount);
+                using (ManagementClass managementClass = new ManagementClass("Win32_Processor"))
+                {
+                    using (ManagementObjectCollection managementObjectCollection = managementClass.GetInstances())
+                    {
+                        foreach (ManagementObject managementObject in managementObjectCollection)
+                        {
+                            CpuCoreCount += Convert.ToInt32(managementObject.Properties["NumberOfLogicalProcessors"].Value);
+                        }
+                    }
+                }
+                using (ManagementClass mc = new ManagementClass("Win32_ComputerSystem"))
+                {
+                    using (ManagementObjectCollection moc = mc.GetInstances())
+                    {
+                        foreach (ManagementObject mo in moc)
+                        {
+                            if (mo["TotalPhysicalMemory"]!= null)
+                            {
+                                MenemorySize = Convert.ToInt64(mo["TotalPhysicalMemory"]);
+                            }
+                        }
+                    }
+                }
+            }
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+            {
+                //int processorCount = Environment.ProcessorCount;
+                // Console.WriteLine("CPU 核心数: " + processorCount);
+                string[] cpu_lines = File.ReadAllLines("/proc/cpuinfo");
+                CpuCoreCount= cpu_lines.Count(line => line.StartsWith("processor", StringComparison.OrdinalIgnoreCase));
+                string[] mem_lines = File.ReadAllLines("/proc/meminfo");
+                var match = mem_lines.FirstOrDefault(line => line.StartsWith("MemTotal:"));
+                if (match != null)
+                {
+                    var matchResult = Regex.Match(match, @"\d+");
+                    if (matchResult.Success)
+                    {
+                        MenemorySize=  long.Parse(matchResult.Value);
+                    }
+                }
+            }
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+            {
+                using (var process = new Process())
+                {
+                    process.StartInfo.FileName = "/usr/sbin/sysctl";
+                    process.StartInfo.Arguments = "-n hw.ncpu";
+                    process.StartInfo.RedirectStandardOutput = true;
+                    process.StartInfo.UseShellExecute = false;
+                    process.Start();
+                    string output = process.StandardOutput.ReadToEnd().Trim();
+                    int coreCount;
+                    if (int.TryParse(output, out coreCount))
+                    {
+                        CpuCoreCount= coreCount;
+                    }
+                }
+                using (var process = new Process())
+                {
+                    process.StartInfo.FileName = "/usr/sbin/sysctl";
+                    process.StartInfo.Arguments = "-n hw.memsize";
+                    process.StartInfo.RedirectStandardOutput = true;
+                    process.StartInfo.UseShellExecute = false;
+                    process.Start();
+                    string output = process.StandardOutput.ReadToEnd().Trim();
+                    long memorySize;
+                    if (long.TryParse(output, out memorySize))
+                    {
+                        MenemorySize=  memorySize;
+                    }
+                }
+            }
+
+            //Console.WriteLine("CPU 核心数: " + CpuCoreCount+",RAM 大小:"+MenemorySize);
+            _logger.LogInformation($"内存大小:{MenemorySize}");
+            _logger.LogInformation($"核心数量:{CpuCoreCount}");
+            device.cpu=CpuCoreCount;
+            device.ram=MenemorySize;
+            var nics = NetworkInterface.GetAllNetworkInterfaces();
+            foreach (var nic in nics)
+            {
+                if (nic.OperationalStatus == OperationalStatus.Up)
+                {
+                    var name = $"{nic.Name}-{nic.Description}";
+                    var mac = nic.GetPhysicalAddress().ToString();
+                    var properties = nic.GetIPProperties();
+                    var unicastAddresses = properties.UnicastAddresses;
+                    foreach (var unicast in unicastAddresses)
+                    {
+                        if (unicast.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
+                        {
+                            var ip = unicast.Address.ToString();
+                            Network network = new Network() { mac=mac, ip=ip, name= name };
+                            if (!string.IsNullOrWhiteSpace(mac.ToString())  && !mac.Equals("000000000000"))
+                            {
+                                device.networks.Add(network);
+                                _logger.LogInformation($"网卡名称: {name}");
+                                _logger.LogInformation($"网卡地址: {mac}");
+                                _logger.LogInformation($"内网地址: {ip}");
+                            }
+                        }
+                    }
+                }
+            }
+            if (_url!.IsNotEmpty())
+            {
+                List<int> ports = new List<int>();
+                foreach (var url in _url)
+                {
+                    Uri uri = new Uri(url);
+                    ports.Add(uri.Port);
+                }
+                device.port= string.Join(",", ports);
+                _logger.LogInformation($"占用端口: {device.port}");
+            }
+            else {
+                throw new Exception("未获取到端口信息!");
+            }
+            string hashData = ShaHashHelper.GetSHA1($"{device.name}-{device.remote}-{device.port}-{device.os}-{string.Join(",", device.networks.Select(x => $"{x.mac}-{x.ip}"))}");
+            device.deviceId=hashData;
+            return device;
+        }
+    }
+}

+ 2 - 1
TEAMModelOS.SDK/Models/Cosmos/Student/StudentArtResult.cs

@@ -24,7 +24,7 @@ namespace TEAMModelOS.SDK.Models
         public List<ArtSubjectScore> subjectScores { get; set; } = new List<ArtSubjectScore>();
         public List<ArtQuotaResult> results { get; set; } = new List<ArtQuotaResult>();
         public string blob { get; set; }
-
+        public string comment { get; set; }
         public Attachment pdf { get; set; }= new Attachment();
 
         /// <summary>
@@ -38,6 +38,7 @@ namespace TEAMModelOS.SDK.Models
     public class ArtSubjectScore {
         public string subjectId { get; set; }
         public double score { get; set; } = 0;
+        public string comment { get; set; }
     }
     public class ArtQuotaResult
     {

+ 7 - 2
TEAMModelOS.SDK/Models/Service/ArtService.cs

@@ -421,15 +421,20 @@ namespace TEAMModelOS.SDK.Models.Service
                             {
                                 comment.Append(comment5.Replace("{pointLow}", string.Join("、", pointLow)));
                             }
+                            string custom_comment = string.Empty;
                             if (artSubjectPdf.subjectId.Equals("subject_music"))
                             {
                                 comment.Append(comment_subject_music);
+                                var dbcomment= x.subjectScores.Find(d => d.subjectId.Equals("subject_music"));
+                                custom_comment=dbcomment?.comment;
                             }
                             if (artSubjectPdf.subjectId.Equals("subject_painting"))
                             {
                                 comment.Append(comment_subject_painting);
+                                var dbcomment = x.subjectScores.Find(d => d.subjectId.Equals("subject_painting"));
+                                custom_comment=dbcomment?.comment;
                             }
-                            artSubjectPdf.comment = comment.ToString();
+                            artSubjectPdf.comment = string.IsNullOrWhiteSpace(custom_comment)? comment.ToString():custom_comment;
                             subjectPdfs.Add(artSubjectPdf);
                         }
                     }
@@ -449,7 +454,7 @@ namespace TEAMModelOS.SDK.Models.Service
                     level = level,
                     score = allScore,
                     allSubjectQuotas = allSubjectQuotas.ToList(),
-                    comment = comment.ToString(),
+                    comment =string.IsNullOrWhiteSpace(x.comment)? comment.ToString():x.comment,
                     subjectPdfs = subjectPdfs,
                 };
                 studentPdfs.Add(studentPdf);

+ 75 - 59
TEAMModelOS.SDK/Models/Service/GenPDFService.cs

@@ -26,6 +26,7 @@ using TEAMModelOS.SDK.Models.Service;
 using Azure.Core;
 using TEAMModelOS.SDK.Models.Cosmos.Common;
 using System.Configuration;
+using Google.Protobuf.WellKnownTypes;
 
 namespace TEAMModelOS.SDK
 {
@@ -367,7 +368,7 @@ namespace TEAMModelOS.SDK
         /// <param name="azureRedis"></param>
         /// <param name="data"></param>
         /// <returns></returns>
-        public static async Task<(int total,int add )> AddGenPdfQueue( AzureRedisFactory azureRedis, GenPDFData data) 
+        public static async Task<(int total,int add )> AddGenPdfQueue( AzureRedisFactory azureRedis, GenPDFData data,ClientDevice device) 
         {
             long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
             List<PDFGenRedis> genRedis = new List<PDFGenRedis>();
@@ -404,12 +405,12 @@ namespace TEAMModelOS.SDK
                 if (dbData!=null)
                 {
                     
-                    if (dbData.status==0|| dbData.status==1)
-                    {
-                        //不变的
-                        countProcess+=1;
-                    }
-                    else
+                    //if (dbData.status==0|| dbData.status==1)
+                    //{
+                    //    //不变的
+                    //    countProcess+=1;
+                    //}
+                    //else
                     {
                         //需要变更的
                         dbData.status = 0;
@@ -458,7 +459,7 @@ namespace TEAMModelOS.SDK
                 };
                 //string message = JsonSerializer.Serialize(genQueue);
                 //从头部压入元素,队列先进先出
-                await azureRedis.GetRedisClient(8).ListLeftPushAsync($"PDFGen:Queue",  genQueue.ToJsonString());
+                await azureRedis.GetRedisClient(8).ListLeftPushAsync($"PDFGen:Queue:{device.deviceId}",  genQueue.ToJsonString());
 
                 //var serviceBusMessage = new ServiceBusMessage(message);
                 //serviceBusMessage.ApplicationProperties.Add("name", "BlobRoot");
@@ -470,17 +471,12 @@ namespace TEAMModelOS.SDK
           
             return ( countProcess , genRedis.Count() ); 
         }
-
-        public static async Task GenArtStudentPdf( AzureRedisFactory _azureRedis, AzureCosmosFactory _azureCosmos, 
-            CoreAPIHttpService _coreAPIHttpService, DingDing _dingDing, AzureStorageFactory _azureStorage, IConfiguration _configuration, JsonElement json)
+       
+        public static async Task<(List<ArtStudentPdf> studentPdfs, List<StudentArtResult> artResults, ArtEvaluation art)> GenArtStudentPdf( AzureRedisFactory _azureRedis, AzureCosmosFactory _azureCosmos, 
+            CoreAPIHttpService _coreAPIHttpService, DingDing _dingDing, AzureStorageFactory _azureStorage, IConfiguration _configuration, List<string> studentIds,string _artId,string _schoolId,string headLang)
         {
             try
             {
-                json.TryGetProperty("studentIds", out JsonElement _studentIds);
-                json.TryGetProperty("artId", out JsonElement _artId);
-                json.TryGetProperty("schoolCode", out JsonElement _schoolId);
-                json.TryGetProperty("headLang", out JsonElement headLang);
-                List<string> studentIds = _studentIds.ToObject<List<string>>();
                 string _schoolCode = $"{_schoolId}";
                 ArtEvaluation art = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, "Common").ReadItemAsync<ArtEvaluation>($"{_artId}", new PartitionKey($"Art-{_schoolId}"));
                 (List<ArtStudentPdf> studentPdfs, List<StudentArtResult> artResults) = await ArtService.GenStuArtPDF(studentIds, $"{_artId}", art, $"{_schoolId}", $"{headLang}", _azureCosmos, _coreAPIHttpService, _dingDing);
@@ -506,52 +502,66 @@ namespace TEAMModelOS.SDK
                 //2个小时。
                 await _azureRedis.GetRedisClient(8).KeyExpireAsync($"ArtPDF:{_artId}:{_schoolCode}", new TimeSpan(2, 0, 0));
                 List<Task<string>> uploads = new List<Task<string>>();
-                studentPdfs.ForEach(x => {
+                studentPdfs.ForEach(x =>
+                {
                     x.blob = $"art/{x.artId}/report/{x.studentId}.json";
                     var urlSas = _azureStorage.GetBlobSAS($"{_schoolCode}", x.blob, BlobSasPermissions.Write|BlobSasPermissions.Read, hour: 24);
                     x.blobFullUrl=urlSas.fullUri;
                     uploads.Add(_azureStorage.GetBlobContainerClient($"{_schoolCode}").UploadFileByContainer(x.ToJsonString(), "art", $"{x.artId}/report/{x.studentId}.json", true));
                 });
                 var uploadJsonUrls = await Task.WhenAll(uploads);
-                var list = uploadJsonUrls.ToList();
-                //List<string> urls = new List<string>();
-                //(string uri, string sas) = _azureStorage.GetBlobContainerSAS($"{_schoolCode}", Azure.Storage.Sas.BlobContainerSasPermissions.Read);
-                //studentPdfs.ForEach(x => {
-                //    string atrUrl = "https://teammodeltest.blob.core.chinacloudapi.cn/0-public/bookjs/art/index.html";
-                //    var s = _azureStorage.GetBlobSAS($"{_schoolCode}", x.blob, BlobSasPermissions.Read);
-                //    s.fullUri = $"{HttpUtility.UrlEncode($"{s}", Encoding.UTF8)}";
-                //    string url = $"{atrUrl}?url={s.fullUri}";
-                //    urls.Add(url);
-                //});
-                string env = ScreenConstant.env_release;
-                if (_configuration.GetValue<string>("Option:Location").Contains("Test", StringComparison.OrdinalIgnoreCase) ||
-                    _configuration.GetValue<string>("Option:Location").Contains("Dep", StringComparison.OrdinalIgnoreCase))
-                {
-                    env = ScreenConstant.env_develop;
-                }
-               var addData = await GenPDFService.AddGenPdfQueue(_azureRedis,
-                    new GenPDFData
-                    {
-                        env =env,
-                        timeout=30000,
-                        delay=1000,
-                        checkPageCompleteJs=true,
-                        sessionId=$"{_artId}",
-                        taskName = art.name,
-                        taskType="Art",
-                        owner=art.owner,
-                        scope=art.scope,
-                        pageUrl="https://teammodeltest.blob.core.chinacloudapi.cn/0-public/bookjs/art/index.html",
-                        datas= studentPdfs.Select(x => new PDFData{ id= x.studentId, name=x.studentName, url =x.blobFullUrl }).ToList()
-                    });
-                Console.WriteLine($"{addData.total},{addData.add}");
+                return (studentPdfs, artResults,art);
+
             }
             catch (Exception ex)
             {
                 await _dingDing.SendBotMsg($"{ex.Message}{ex.StackTrace}", GroupNames.成都开发測試群組);
             }
+            return (null, null, null);
         }
 
+        public static async Task PushScreenTask(AzureRedisFactory _azureRedis, IConfiguration _configuration, string _artId, 
+            ArtEvaluation art, List<ArtStudentPdf> studentPdfs,IHttpClientFactory _httpClient,DingDing dingDing)
+        {
+            string env = ScreenConstant.env_release;
+            if (_configuration.GetValue<string>("Option:Location").Contains("Test", StringComparison.OrdinalIgnoreCase) ||
+                _configuration.GetValue<string>("Option:Location").Contains("Dep", StringComparison.OrdinalIgnoreCase))
+            {
+                env = ScreenConstant.env_develop;
+            }
+            string ComplexAPI= _configuration.GetValue<string>("HTEX.Complex.API");
+            try {
+
+                var client=   _httpClient.CreateClient();
+                client.Timeout= new TimeSpan(0, 0, 30);
+                HttpResponseMessage message = await client.PostAsJsonAsync($"{ComplexAPI}/api/screen/push-task", new GenPDFData
+                {
+                    env =env,
+                    timeout=30000,
+                    delay=1000,
+                    checkPageCompleteJs=true,
+                    sessionId=$"{_artId}",
+                    taskName = art.name,
+                    taskType="Art",
+                    owner=art.owner,
+                    scope=art.scope,
+                    pageUrl="https://teammodeltest.blob.core.chinacloudapi.cn/0-public/bookjs/art/index.html",
+                    datas= studentPdfs.Select(x => new PDFData { id= x.studentId, name=x.studentName, url =x.blobFullUrl }).ToList()
+                });
+                if (message.IsSuccessStatusCode)
+                {
+
+                }
+                else {
+                    await dingDing.SendBotMsg($"艺术评测任务添加接口状态返回异常,状态:{message.StatusCode}", GroupNames.成都开发測試群組);
+                }
+            }
+            catch(Exception ex){
+                await dingDing.SendBotMsg($"艺术评测任务添加异常:{ex.Message},{ex.StackTrace}", GroupNames.成都开发測試群組);
+            }
+             
+            //Console.WriteLine($"{addData.total},{addData.add}");
+        }
     }
 
     public class GenPDFData
@@ -788,6 +798,18 @@ namespace TEAMModelOS.SDK
         /// 任务完成数
         /// </summary>
         public int taskComplete { get; set; }
+        /// <summary>
+        /// 超时时间,单位毫秒
+        /// </summary>
+        public long timeout { get; set; } = 30000;
+        /// <summary>
+        /// 延迟时间,单位毫秒
+        /// </summary>
+        public long delay { get; set; } = 3000;
+        /// <summary>
+        /// PDF服务地址
+        /// </summary>
+        public string? screenUrl { get; set; }
     }
 
     public class SignalRClient
@@ -804,6 +826,7 @@ namespace TEAMModelOS.SDK
         /// SignalR的连接ID 不建议暴露。
         /// </summary>
         public string? connid { get; set; }
+        public string? serverid { get; set;}
     }
     public interface IClient
     {
@@ -930,18 +953,11 @@ namespace TEAMModelOS.SDK
         /// 网卡 IP信息
         /// </summary>
         public List<Network> networks { get; set; } = new List<Network>();
+       
         /// <summary>
-        /// 超时时间,单位毫秒
-        /// </summary>
-        public long timeout { get; set; } = 30000;
-        /// <summary>
-        /// 延迟时间,单位毫秒
-        /// </summary>
-        public long delay { get; set; } = 3000;
-        /// <summary>
-        /// PDF服务地址
+        /// 设备id
         /// </summary>
-        public string? screenUrl { get; set; } 
+        public string? deviceId { get; set; }
     }
     public class Network
     {

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

@@ -50,5 +50,6 @@
 		<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.22.0" />
 		<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Abstractions" Version="1.3.0" />
 		<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.2.0" />
+		<PackageReference Include="System.Management" Version="8.0.0" />
 	</ItemGroup>
 </Project>

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

@@ -8022,6 +8022,18 @@ const LANG_EN_US = {
         course: 'Course',
         courseGroup: 'Class',
         schoolName: 'School',
+        Warning01: 'For designated review, please enter the TEAM Model user number and check to confirm it is the correct TEAM Model ID',
+        check: 'check',
+        Warning02: 'The ongoing schedule cannot be edited',
+        jointTime: 'Registration time',
+        pleaseEnterEventName: 'Please enter the event name',
+        pleaseEnterSchedule: 'Please enter the schedule',
+        accountNotExist: 'Account does not exist',
+        pleaseEnterAccount: 'Please enter account number',
+        confirm: 'OK',
+        Warning03: 'Are you sure you want to delete the group?',
+        notice: 'hint',
+        Warning04: 'Are you sure you want to delete the schedule?',
     },
     activity: {
         scoreWord: {

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

@@ -8023,6 +8023,18 @@ const LANG_ZH_CN = {
         course: '课程',
         courseGroup: '课程名单',
         schoolName: '学校',
+        Warning01: '指定评审,请输入醍摩豆用户编号,检查确认为正确醍摩豆ID',
+        check: '检查',
+        Warning02: '进行中时程不可编辑',
+        jointTime: '报名时间',
+        pleaseEnterEventName: '请输入活动名称',
+        pleaseEnterSchedule: '请输入时程',
+        accountNotExist: '帐号不存在',
+        pleaseEnterAccount: '请输入帐号',
+        confirm: '确定',
+        Warning03: '确定要删除组别?',
+        notice: '提示',
+        Warning04: '确定要删除时程?',
     },
     activity: {
         scoreWord: {

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

@@ -8023,6 +8023,18 @@ const LANG_ZH_TW = {
         course: '課程',
         courseGroup: '課程名單',
         schoolName: '學校',
+        Warning01: '指定評審,請輸入醍摩豆用戶編號,檢查確認為正確醍摩豆ID',
+        check: '檢查',
+        Warning02: '進行中時程不可編輯',
+        jointTime: '報名時間',
+        pleaseEnterEventName: '請輸入活動名稱',
+        pleaseEnterSchedule: '請輸入時程',
+        accountNotExist: '帳號不存在',
+        pleaseEnterAccount: '請輸入帳號',
+        confirm: '確定',
+        Warning03: '確定要刪除組別?',
+        notice: '提示',
+        Warning04: '確定要刪除時程?',
     },
     activity: {
         scoreWord: {

+ 22 - 5
TEAMModelOS/ClientApp/src/common/BaseQuickPaper.vue

@@ -66,10 +66,12 @@
 			</p>
 			<p class="upper-tip" v-if="answerInputMode === 'code'">* {{ $t("evaluation.quickPaper.onlyUpper") }}</p>
 			<div class="ans-input-wrap">
-				<div class="answer-item" v-if="paperInfo.items[0].count" style="display: flex; align-items: center">
-					<span class="type">{{ paperInfo.items[0].count > 1 ? `1 ~ ${paperInfo.items[0].count}` : "1" }}</span>
-					<Input v-model="singleAns" @mousewheel.native.prevent :type="answerInputMode === 'number' ? 'number' : 'text'" @on-change="removeChinese($event, 0, 'single')" @on-keydown="onSingleInput" :placeholder="$t('evaluation.quickPaper.tip2')" />
-				</div>
+				<template v-if="paperInfo.items[0].count">
+					<div class="answer-item" v-for="i in singleNum" :key="i" style="display: flex; align-items: center">
+						<span class="type">{{ i }}</span>
+						<Input v-model="singleAns" @mousewheel.native.prevent :type="answerInputMode === 'number' ? 'number' : 'text'" @on-change="removeChinese($event, 0, 'single')" @on-keydown="onSingleInput" :placeholder="$t('evaluation.quickPaper.tip2')" />
+					</div>
+				</template>
 				<div class="answer-item" v-if="paperInfo.items[1].count">
 					<div v-for="(item, index) in paperInfo.items[1].count" :key="index" style="display: flex; align-items: center">
 						<span class="type">{{ item + paperInfo.items[0].count }}</span>
@@ -995,7 +997,22 @@
 			},
 			isEnglish() {
 				return localStorage.local === "en-us";
-			}
+			},
+			singleNum() {
+				let arr = this.paperInfo.items[0].count === 1 ? ['1'] : []
+				if(this.paperInfo.items[0].count > 1) {
+					let num = Math.floor(this.paperInfo.items[0].count / 5)
+					num = (num * 5) === this.paperInfo.items[0].count ? num : num + 1
+					for (let i = 0; i < num; i++) {
+						if(i+1 === num && this.paperInfo.items[0].count - i * 5 === 1) {
+							arr.push(`${this.paperInfo.items[0].count}`)
+						} else {
+							arr.push(`${i*5+1}~${i+1 === num ? this.paperInfo.items[0].count : (i+1)*5}`)
+						}
+					}
+				}
+				return arr
+			},
 		}
 	};
 </script>

+ 9 - 0
TEAMModelOS/ClientApp/src/components/student-web/WrongQusetion/QuesList.less

@@ -198,6 +198,15 @@
                     margin-right: 10px;
                     min-width: 60px;
                 }
+
+                .item-explain-details{
+                    vertical-align: top;
+                    display: inline-block;
+                    width: calc(100% - 120px);
+                    img {
+                        max-width: 100%;
+                    }
+                }
             }
 
             .question {

+ 12 - 2
TEAMModelOS/ClientApp/src/components/student-web/WrongQusetion/QuesList.vue

@@ -99,7 +99,12 @@
                                         </div>
                                         <div class="answer-area">
                                             <span class="answer-tip">【{{ $t('evaluation.answer') }}】</span>
-                                            <p v-html="child.answer[0]"></p>
+                                            <div v-if="child.answer.length" class="item-explain-details">
+                                                <span v-for="(answerC, indexCA) in child.answer" :key="indexCA" v-html="answerC"></span>
+                                            </div>
+                                            <div v-if="!child.answer.length" class="item-explain-details">
+                                                <span>{{ $t('studentWeb.exam.report.noAns') }}</span>
+                                            </div>
                                         </div>
                                         <div class="answer-area">
                                             <span class="answer-tip">【{{ $t('evaluation.explain') }}】</span>
@@ -110,7 +115,12 @@
                                 <template v-else>
                                     <div class="answer-area">
                                         <span class="answer-tip">【{{ $t('evaluation.answer') }}】</span>
-                                        <p v-html="item.answer[0]"></p>
+                                        <div v-if="item.answer.length" class="item-explain-details">
+                                            <span v-for="(answerC, indexCA) in item.answer" :key="indexCA" v-html="answerC"></span>
+                                        </div>
+                                        <div v-if="!item.answer.length" class="item-explain-details">
+                                            <span>{{ $t('studentWeb.exam.report.noAns') }}</span>
+                                        </div>
                                     </div>
                                     <div class="answer-area">
                                         <span class="answer-tip">【{{ $t('evaluation.explain') }}】</span>

+ 1 - 1
TEAMModelOS/ClientApp/src/view/evaluation/components/BaseImport.vue

@@ -243,7 +243,7 @@ export default {
 
     /* 获取答案 */
     getItemAnswer(answer) {
-      let answerArr = answer.split(',')
+      let answerArr = answer.split('')
       let result = []
       answerArr.forEach(i => {
         var patternNum = new RegExp("[0-9]+");

+ 4 - 3
TEAMModelOS/ClientApp/src/view/htcommunity/htMgtExam.vue

@@ -13,10 +13,10 @@
 			</el-tabs>
 		</div> -->
 		<div style="margin: 10px">
-			<el-select v-model="filter.schedule" placeholder="請選擇時程" style="margin-right: 30px;" @change="findEvaluation">
+			<el-select v-model="filter.schedule"  style="margin-right: 30px;" @change="findEvaluation">
 				<el-option v-for="item in propSchedules" :key="item.id" :label="item.name" :value="item.id" />
 			</el-select>
-			<el-select v-model="filter.group" placeholder="請選擇組別" @change="findEvaluation">
+			<el-select v-model="filter.group"  @change="findEvaluation">
 				<el-option v-for="item in propGroups" :key="item.id" :label="item.name" :value="item.id" />
 			</el-select>
 		</div>
@@ -674,8 +674,9 @@ export default {
 		},
 		// 點擊左側個人評量列表項目的動作
 		selectEvaluation(index) {
+			debugger
 			this.checkScoreSave(this.toEvaluation, index);
-			this.$EventBus.$emit("onEvaChange", this.evaListShow[index]);
+			this.$EventBus.$emit("onEvaChange", this.htEvaListShow[index]);
 		},
 		// 
 		toEvaluation(index) {

+ 21 - 29
TEAMModelOS/ClientApp/src/view/htcommunity/htMgtHome.vue

@@ -22,18 +22,7 @@
                     <!-- <el-button type="text" @click="return false">{{$t("htcommunity.view")}}</el-button> -->
                 </template>
             </el-table-column>
-        </el-table>
-
-        <!-- 詳細內容彈出視窗 -->
-        <el-dialog :title="this.$t('htcommunity.details')" :visible.sync="showDetailsModal" width="50%">
-            <p>{{this.$t("htcommunity.activityName")}}: {{ selectedActivity.name }}</p>
-            <p>{{this.$t("htcommunity.schedule")}}: {{ selectedActivity.time }}</p>
-            <p>{{this.$t("htcommunity.administrator")}}: {{ selectedActivity.admin }}</p>
-            <p>原始資料: {{ selectedActivity.originalData }}</p>
-            <span slot="footer" class="dialog-footer">
-                <el-button @click="showDetailsModal = false">{{this.$t("htcommunity.cancel")}}</el-button>
-            </span>
-        </el-dialog>
+        </el-table>       
 
         <!-- 新增活動彈出視窗 -->
         <el-dialog :title="this.$t('htcommunity.addEvent')" :visible.sync="showAddModal" width="90%"
@@ -64,7 +53,9 @@
                         <el-form-item :label="this.$t('htcommunity.group')">
                             <!-- <el-tooltip content="指定評審,請檢查確認為正確醍摩豆ID" placement="bottom" effect="light"> -->
                             <el-tooltip placement="bottom" effect="light">
-                                <div slot="content" style="color: red;font-size: 1.2em;">指定評審,請輸入醍摩豆用戶編號,檢查確認為正確醍摩豆ID</div>
+                                <div slot="content" style="color: red;font-size: 1.2em;">
+                                  {{$t("htcommunity.Warning01")}}
+                                  </div>
                                 <i class="el-icon-warning-outline" style="font-size: 2em;"></i>
                             </el-tooltip>
                             <!-- <span>指定評審請輸入<span class="point">手機號碼</span>、<span class="point">醍摩豆用戶編號</span>或<span class="point">電子信箱</span>等資訊進行搜尋</span> -->
@@ -79,7 +70,7 @@
                                     <template slot-scope="scope">
                                         <el-input v-model="scope.row.groupJudge" style="width: 200px;margin-right: 10px"></el-input>
                                         <el-button type="primary" icon="el-icon-search" size="mini" @click="checkTmdid(scope)"
-                                                   style="margin-top: 5px;">檢查</el-button>
+                                                   style="margin-top: 5px;">{{$t("htcommunity.check")}}</el-button>
                                         <span v-if="scope.row.valid">✔️</span>
 
                                     </template>
@@ -112,7 +103,9 @@
                                 <el-date-picker v-model="scope.row.schedule" type="datetimerange" start-placeholder="Start"
                                                 end-placeholder="End" style="width: 100%;" :disabled="scheduleDisabled(scope, '')">
                                 </el-date-picker>
-                                <span style="color:red;font-size: small;" v-show="scope.row.progress==='going'">進行中時程不可編輯</span>
+                                <span style="color:red;font-size: small;" v-show="scope.row.progress==='going'">                                  
+                                  {{$t("htcommunity.Warning02")}}
+                                  </span>
                             </template>
                         </el-table-column>
                         <el-table-column prop="name" :label="this.$t('htcommunity.stage')" min-width="100px">
@@ -210,8 +203,7 @@ export default {
             { code: 'join', name: this.$t("htcommunity.signup") },
             { code: 'exam', name: this.$t("htcommunity.exam") },
             { code: 'other', name: this.$t("htcommunity.other") }
-        ],
-      showDetailsModal: false,
+        ],      
       showAddModal: false,
       selectedActivity: {},
       newActivity: {
@@ -225,18 +217,18 @@ export default {
         ],
         requireOrderCompletion: false,
         planContent: [
-          {id:'', step: 1, schedule: [], name: '報名時間', location: '', description: '', blobs: '', type: true, examType: false, evaluation: false }
+          {id:'', step: 1, schedule: [], name: this.$t("htcommunity.jointTime"), location: '', description: '', blobs: '', type: true, examType: false, evaluation: false }
         ],
        
       },
       // 表單驗證
       rules: {
         name: [
-          { required: true, message: '請輸入活動名稱' }
+          { required: true, message: this.$t("htcommunity.pleaseEnterEventName") }
         ],
         time: [
           {
-            required: true, message: '請輸入時程'
+            required: true, message: this.$t("htcommunity.pleaseEnterSchedule")
           }
         ],
       },
@@ -341,7 +333,7 @@ export default {
             else {
               this.$message({
                 showClose: true,
-                message: '帳號不存在',
+                message: this.$t('htcommunity.accountNotExist'),
                 type: 'error'
               });
               scope.row.valid = false;
@@ -355,7 +347,7 @@ export default {
       } else {
         this.$message({
           showClose: true,
-          message: '請輸入帳號',
+          message: this.$t('htcommunity.pleaseEnterAccount'),
           type: 'error'
         });
         scope.row.valid = false;
@@ -694,7 +686,7 @@ export default {
         ],
         requireOrderCompletion: false,
         planContent: [
-          {id:'', step: 1, schedule: [], name: '報名時間', location: '', description: '', blobs: '',  type: true, examType: false, evaluation: false }
+          {id:'', step: 1, schedule: [], name: this.$t('htcommunity.jointTime'), location: '', description: '', blobs: '',  type: true, examType: false, evaluation: false }
         ]
       };      
     },
@@ -704,9 +696,9 @@ export default {
     },
     //移除分組
     removeGroup(scope) {
-      this.$confirm('確定要刪除組別?', '提示', {
-        confirmButtonText: '確定',
-        cancelButtonText: '取消',
+      this.$confirm(this.$t('htcommunity.Warning03'), this.$t('htcommunity.notice'), {
+        confirmButtonText: this.$t('htcommunity.confirm'),
+        cancelButtonText: this.$t('htcommunity.cancel'),
         type: 'warning'
       }).then(() => {
         try {
@@ -741,9 +733,9 @@ export default {
     },
     //移除時程
     removePlanContent(scope) {      
-      this.$confirm('確定要刪除時程?', '提示', {
-        confirmButtonText: '確定',
-        cancelButtonText: '取消',
+     this.$confirm(this.$t('htcommunity.Warning04'), this.$t('htcommunity.notice'), {
+        confirmButtonText: this.$t('htcommunity.confirm'),
+        cancelButtonText: this.$t('htcommunity.cancel'),
         type: 'warning'
       }).then(() => {
         try {

+ 588 - 0
TEAMModelOS/ClientApp/src/view/learnactivity/byStu/htByStuMark.vue

@@ -0,0 +1,588 @@
+<template>
+  <div class="paper-score-container">
+    <Loading :top="200" type="1" style="text-align:center" v-show="dataLoading"></Loading>
+    <div class="scoring-paper-header">
+      <div>
+        <span class="base-info-item">
+          {{$t('learnActivity.score.stuName')}}
+          <span class="analysis-info">{{studentAnswer.name}}</span>
+        </span>
+        <span class="base-info-item">
+          {{$t('learnActivity.score.score')}}
+          <span class="analysis-info">
+            {{scoreTotal}}{{$t('learnActivity.score.scoreUnit')}}
+          </span>
+        </span>
+        <!-- 保存分数 -->
+        <span :class="['base-info-btn', isUpd ? 'base-info-btn-active' : '']" type="success" @click="saveScore">
+          <Icon type="ios-albums-outline" />
+          {{$t('learnActivity.score.saveScore')}}
+        </span>
+        <!-- 未阅满分 -->
+        <span class="base-info-btn" @click="batchScore('zero')">
+          <Icon type="ios-create" />
+          {{$t('learnActivity.score.fastScore4')}}
+        </span>
+        <!-- 未阅零分 -->
+        <span class="base-info-btn" @click="batchScore('full')">
+          <Icon type="ios-create" />
+          {{$t('learnActivity.score.fastScore5')}}
+        </span>
+        <span v-show="examInfo.source === '2'" class="base-info-btn" type="success" @click="viewOriginal">
+          <Icon type="md-eye" />
+          {{$t('learnActivity.score.viewOrigin')}}
+        </span>
+        <span class="base-info-btn" @click="config.showAnswer = !config.showAnswer">
+          <Icon :type="config.showAnswer ? 'md-eye-off':'md-eye'" />
+          {{ config.showAnswer ? $t('learnActivity.score.hideAns') : $t('learnActivity.score.showAns')}}
+        </span>
+        <span class="base-info-btn" @click="config.showQu = !config.showQu">
+          <Icon :type="config.showQu ? 'md-eye-off':'md-eye'" />
+          {{ config.showQu ? $t('learnActivity.score.hideQu') : $t('learnActivity.score.showQu')}}
+        </span>
+      </div>
+      <!-- 题号 -->
+      <div class="question-index-box" v-if="studentAnswer.scores">
+        <div class="base-info-item" style="white-space: nowrap;">{{$t('learnActivity.score.quIndex')}}</div>
+        <div>
+          <span v-for="quNoItem in quNoList" :key="quNoItem.label" :class="studentAnswer.scores[quNoItem.index] >= 0 ? 'qu-order-tag':'qu-order-tag-def'">
+            {{quNoItem.label}}
+          </span>
+        </div>
+      </div>
+      <!-- 渲染题目和打分相关UI -->
+      <template v-if="!isComplete && studentAnswer.stuAnswers && studentAnswer.stuAnswers.length">
+        <!-- 默认题目顺序 -->
+        <template v-if="itemSort == 1">
+          <div v-for="(quItem) in defaultList" :key="quItem.quNo" ref="mathJaxContainer">
+            <template v-if="quItem.children && quItem.children.length">
+              <QuAndScore :ref="'QuAndScore'+ child.index" :stuMark="studentAnswer.mark[child.index]" @click.native="config.activeIndex = child.index" v-model="studentAnswer.scores[child.index]" v-for="(child) in quItem.children" :key="child.quNo" :stuAnswer="studentAnswer.stuAnswers[child.index]" :questionItem="child" :config="config" @on-save-mark="saveMark" @on-del-mark="delMark"></QuAndScore>
+            </template>
+            <template v-else>
+              <QuAndScore :ref="'QuAndScore'+ quItem.index" :stuMark="studentAnswer.mark[quItem.index]" @click.native="config.activeIndex = quItem.index" v-model="studentAnswer.scores[quItem.index]" :questionItem="quItem" :config="config" :stuAnswer="studentAnswer.stuAnswers[quItem.index]" @on-save-mark="saveMark" @on-del-mark="delMark"></QuAndScore>
+            </template>
+          </div>
+        </template>
+        <!-- 按题型排序 -->
+        <template v-else>
+          <div v-for="(typeItem) in groupList" :key="typeItem.type" ref="mathJaxContainer">
+            <p class="type-name">
+              {{ $tools.getChineseByNum(getLatestTypeIndex(typeItem.type) + 1) }}
+              {{ exersicesType[typeItem.type] }}
+              ({{ typeItem.score || 0 }} {{$t('learnActivity.score.scoreUnit')}})
+            </p>
+            <div v-for="(quItem) in typeItem.list" :key="quItem.quNo">
+              <template v-if="quItem.children && quItem.children.length">
+                <QuAndScore :ref="'QuAndScore'+ child.index" :stuMark="studentAnswer.mark[child.index]" @click.native="config.activeIndex = child.index" v-model="studentAnswer.scores[child.index]" v-for="(child) in quItem.children" :key="child.quNo" :stuAnswer="studentAnswer.stuAnswers[child.index]" :questionItem="child" :config="config" @on-save-mark="saveMark" @on-del-mark="delMark"></QuAndScore>
+              </template>
+              <template v-else>
+                <QuAndScore :ref="'QuAndScore'+ quItem.index" :stuMark="studentAnswer.mark[quItem.index]" @click.native="config.activeIndex = quItem.index" v-model="studentAnswer.scores[quItem.index]" :questionItem="quItem" :config="config" :stuAnswer="studentAnswer.stuAnswers[quItem.index]" @on-save-mark="saveMark" @on-del-mark="delMark"></QuAndScore>
+              </template>
+            </div>
+          </div>
+        </template>
+      </template>
+      <div v-show="isComplete" class="complete-score-box">
+        <Icon class="complete-icon" type="md-checkmark-circle" />
+        <p class="complete-stu-info">{{studentAnswer.name}} ({{scoreTotal}}{{$t('learnActivity.score.scoreUnit')}})</p>
+        <p class="complete-next" @click="nextStu">{{$t('learnActivity.score.nextStu')}}</p>
+      </div>
+    </div>
+    <Modal v-model="originalStatus" :title="$t('learnActivity.score.viewOrigin')" :width="900" footer-hide>
+      <img v-for="item in sourceList" :src="item.url" :key="item.name" width="850">
+      <EmptyData v-show="!sourceList.length"></EmptyData>
+    </Modal>
+  </div>
+</template>
+
+<script>
+import QuAndScore from "./QuAndScore.vue"
+import BlobTool from '@/utils/blobTool.js'
+export default {
+  components: {
+    QuAndScore
+  },
+  props: {
+    examId: {
+      type: String,
+      default: '',
+      required: true
+    },
+    examInfo: {
+      type: Object,
+      default: () => {
+        return {}
+      }
+    },
+    subjectId: {
+      type: String,
+      default: '',
+      required: true
+    },
+    paper: {
+      type: Object,
+      default: () => { }
+    },
+    studentInfo: {
+      type: Object,
+      default: () => { }
+    },
+    defaultIndex: {
+      type: Number,
+      default: 0
+    }
+  },
+  data() {
+    let _this = this
+    return {
+      originalStatus: false,
+      isComplete: false,
+      exersicesType: this.$GLOBAL.EXERCISE_TYPES(),
+      typeList: ['single', 'multiple', 'judge', 'complete', 'subjective', 'connector', 'correct', 'compose'],
+      itemSort: 1,
+      quNoList: [],
+      sourceList: [],
+      dataLoading: false,
+      paperScore: [],
+      groupList: [],
+      defaultList: [],
+      studentAnswer: {},
+      config: {
+        showAnswer: false,
+        showQu: false,
+        activeIndex: -1,
+        examInfo: _this.examInfo,
+        stuInfo: _this.studentInfo,
+        subjectId: _this.subjectId,
+      },
+      isUpd: false
+    }
+  },
+  computed: {
+    scoreTotal() {
+      if (this.studentAnswer && this.studentAnswer.scores) {
+        let s = this.studentAnswer.scores.reduce((prev, cur) => {
+          cur = cur < 0 ? 0 : cur
+          return prev + cur
+        }, 0)
+        return s
+      }
+      return 0
+    },
+  },
+  watch: {
+    examInfo: {
+      immediate: true,
+      handler(n, o) {
+        if (n) {
+          this.config.examInfo = n
+          this.config.subjectId = this.subjectId
+        }
+      }
+    },
+    paper: {
+      handler(newPaper, oldValue) {
+        this.handleInitPaper(newPaper)
+      },
+      deep: true,
+      immediate: true
+    },
+    studentInfo: {
+      async handler(newValue, oldValue) {
+        this.studentAnswer = {}
+        this.studentAnswer = this._.cloneDeep(newValue || {})
+        this.isComplete = false
+        //处理默认显示批注
+        this.markFlag = {}
+        if (this.studentAnswer.mark) {
+          //后端阅卷批注数据异常,暂时这里过滤处理
+          for (let i = 0; i < this.studentAnswer.mark.length; i++) {
+            if (this.studentAnswer.mark[i]) {
+              this.studentAnswer.mark[i] = this.studentAnswer.mark[i].filter(m => {
+                return !!m.mark
+              })
+            }
+          }
+        }
+        if (!this.studentAnswer.status && this.studentAnswer.answers && this.studentAnswer.scores) {
+          if (this.studentAnswer.answers.length) {
+            let sourceBlob = this.studentAnswer.answers[0].replace('ans.json', 'source')
+            this.$set(this.studentAnswer, 'sourceBlob', sourceBlob)
+            try {
+              let sas = this.examInfo.scope == 'school' ? this.$store.state.user.schoolProfile.blob_sas : this.$store.state.user.userProfile.blob_sas
+              let blobUrl = this.examInfo.scope == 'school' ? JSON.parse(decodeURIComponent(localStorage.school_profile, "utf-8")).blob_uri : JSON.parse(decodeURIComponent(localStorage.user_profile, "utf-8")).blob_uri
+              /**
+               * 1、批注逻辑调整,每个题目的批注单独保存成图片;
+               * 2、这里就直接读取原始作答数据;
+               * 3、在渲染的时候再判断当前题目是否有批注数据。
+               */
+              let urlPrefix = `${blobUrl}/exam/${this.studentAnswer.answers[0].replace('/ans.json', '')}`
+              let fullUrl = `${blobUrl}/exam/${this.studentAnswer.answers[0]}?${sas}`
+              let ansRes = await this.$jsFn.handleStudentAnswer(fullUrl, urlPrefix, sas)
+              // 问答题:课中的地址需截取文件名称重新拼接地址,因此统一重新拼接处理
+              ansRes  = ansRes.map((item, index) => {
+                if(this.quNoList[index].type === 'subjective' && this.quNoList[index].answerType && this.quNoList[index].answerType != 'text' && item.length) {
+                  let name = item[0].substr(item[0].lastIndexOf(`/${this.studentInfo.id}/`) + (this.studentInfo.id.length + 2))
+                  // 暂时处理切换学生带来的字符串重复拼接问题
+                  if(name.includes('/')){
+                    return
+                  }
+                  item = this.quNoList[index].answerType === 'file' ? [name] : [`${blobUrl}/exam/${this.examId}/${this.subjectId}/${this.studentInfo.id}/${name}?${sas}`]
+                }
+                return item
+              })
+              this.$set(this.studentAnswer, 'stuAnswers', ansRes)
+            } catch (e) {
+              let full = []
+              let i = 0
+              while (i < this.paperInfo.item.length) {
+                full.push(`<p style="color:red">${this.$t('learnActivity.score.ansErr')}</p>`)
+                i++
+              }
+              this.$set(this.studentAnswer, 'stuAnswers', full)
+            }
+          } else {
+            let a = this.studentAnswer.scores.map(item => {
+              return []
+            })
+            this.$set(this.studentAnswer, 'stuAnswers', a)
+          }
+          this.studentAnswer.status = true
+        }
+        // 切换学生后,上面步骤会触发studentAnswer.scores的监控事件,需重置编辑(isUpd)状态,否则会下一次切换学生时保存成绩
+        this.isUpd = false
+      },
+      deep: true,
+      immediate: true
+    },
+    "studentAnswer.scores": {
+      handler(n, o) {
+        if (n && o && n.length === o.length) {
+          this.isUpd = true
+        }
+      }
+    },
+    'config.showQu' : {
+      handler(n, o) {
+        if(n) {
+          window.MathJax.startup.promise.then(() => {
+						window.MathJax.typesetPromise([this.$refs.mathJaxContainer])
+					})
+        }
+      }
+    },
+  },
+  methods: {
+    handleInitPaper(newPaper) {
+      this.paperInfo = this._.cloneDeep(newPaper)
+      let that = this
+      this.groupList = []
+      this.defaultList = []
+      this.paperScore = []
+      this.quNoList = []
+      this.itemSort = newPaper?.itemSort
+      if (newPaper && newPaper.item) {
+        this.dataLoading = true
+        if (newPaper.item.length) {
+          let index = 0 //题目索引
+          let quIndex = 1 // 题号
+          newPaper.item.forEach((i) => {
+            //记录题目原始位置
+            if (i.type == 'compose') {
+              i.children.forEach((cItem, cIndex) => {
+                cItem.quNo = `${quIndex}-${cIndex + 1}`
+                cItem.index = index++
+                this.paperScore.push(cItem.score || 0)
+              })
+              quIndex++
+            } else {
+              i.quNo = (quIndex++).toString()
+              i.index = index++
+              this.paperScore.push(i.score || 0)
+            }
+            if (!i.score) i.score = 0
+          })
+          //默认排序
+          if (this.itemSort == 1) {
+            this.defaultList = newPaper.item
+            this.defaultList.forEach(item => {
+              if (item.children && item.children.length) {
+                item.children.forEach(child => {
+                  this.quNoList.push({
+                    label: child.quNo,
+                    index: child.index,
+                    type: child.type,
+                    answerType: child?.answerType || undefined
+                  })
+                })
+              } else {
+                this.quNoList.push({
+                  label: item.quNo,
+                  index: item.index,
+                  type: item.type,
+                  answerType: item?.answerType || undefined
+                })
+              }
+            })
+          }
+
+          // 处理试卷内题目按照题型排序
+          else {
+            this.typeList.forEach(item => {
+              this._.mapKeys(this._.groupBy(newPaper.item, 'type'), function (value, key) {
+                if (key === item) { /* 按照题型排序,并且计算每种题型的总分 */
+                  that.groupList.push({
+                    type: key,
+                    list: value,
+                    score: value.reduce((p, e) => parseFloat(p) + parseFloat(e.score), 0)
+                  })
+                }
+              })
+            })
+            this.groupList.forEach((typeItem, typeIndex) => {
+              if (typeItem.list) {
+                typeItem.list.forEach((quItem, quIndex) => {
+                  if (quItem.children && quItem.children.length) {
+                    quItem.children.forEach(child => {
+                      this.quNoList.push({
+                        label: child.quNo,
+                        index: child.index,
+                        type: child.type,
+                        answerType: child?.answerType || undefined
+                      })
+                    })
+                  } else {
+                    this.quNoList.push({
+                      label: quItem.quNo,
+                      index: quItem.index,
+                      type: quItem.type,
+                      answerType: quItem?.answerType || undefined
+                    })
+                  }
+                })
+              }
+            })
+          }
+        }
+        this.dataLoading = false
+      }
+    },
+    getLatestTypeIndex(type) {
+      let arr = []
+      this.groupList.forEach(i => {
+        if (i.list.length) {
+          arr.push(i.type)
+        }
+      })
+      return arr.indexOf(type)
+    },
+    //点评下一位学生
+    nextStu() {
+      this.$emit('nextStu')
+    },
+    // 查阅原卷
+    viewOriginal() {
+      console.log(`output->this.studentAnswer`, this.studentAnswer)
+      if (!this.studentAnswer.sourceBlob) {
+        this.$Message.warning(this.$t('learnActivity.score.noStuAnswer'))
+        return
+      }
+      let sas = this.examInfo.scope == 'school' ? this.$store.state.user.schoolProfile.blob_sas : this.$store.state.user.userProfile.blob_sas
+      let blobUrl = this.examInfo.scope == 'school' ? JSON.parse(decodeURIComponent(localStorage.school_profile, "utf-8")).blob_uri : JSON.parse(decodeURIComponent(localStorage.user_profile, "utf-8")).blob_uri
+      let host = blobUrl.substring(0, blobUrl.lastIndexOf('/'))
+      let cont = blobUrl.substring(blobUrl.lastIndexOf('/') + 1)
+      let blobTool = new BlobTool(host, cont, '?' + sas, this.examInfo.scope)
+      let path = `exam/${this.studentAnswer.sourceBlob}` //原卷路径
+      this.sourceList = []
+      blobTool.listBlob({
+        prefix: path
+      }).then(
+        res => {
+          if (res.blobList && res.blobList.length) {
+            res.blobList.forEach(item => {
+              item.url = item.url + '?' + sas
+            })
+            this.sourceList = res.blobList
+          }
+        },
+        err => {
+          console.log(`output->err`, err)
+        }
+      )
+      console.log(`output->sourceList`, this.sourceList)
+      this.originalStatus = true
+    },
+    /**
+     * type  full未阅满分 zero未阅零分
+     */
+    batchScore(type) {
+      if (!this.studentAnswer.scores.includes(-1)) {
+        this.$Message.warning({
+          content: this.$t('learnActivity.score.noQuMark'),
+          duration: 2
+        })
+        return
+      }
+      if (type === 'full') {
+        for (let i = 0; i < this.studentAnswer.scores.length; i++) {
+          if (this.studentAnswer.scores[i] === -1) {
+            this.$set(this.studentAnswer.scores, i, this.paperScore[i] || 0)
+          }
+        }
+      } else if (type === 'zero') {
+        for (let i = 0; i < this.studentAnswer.scores.length; i++) {
+          if (this.studentAnswer.scores[i] === -1) {
+            this.$set(this.studentAnswer.scores, i, 0)
+          }
+        }
+      }
+    },
+    //保存学生分数
+    saveScore() {
+      if (!this.isUpd) {
+        this.$Message.warning(this.$t('learnActivity.score.noUpd'))
+        return
+      }
+      let d = this._.cloneDeep(this.studentAnswer)
+      this.examInfo.stuLists
+      let requestData = {
+        "id": this.examId,
+        "code": this.examInfo.owner == 'school' ? this.$store.state.userInfo.schoolCode : this.$store.state.userInfo.TEAMModelId,
+        "point": [this.studentAnswer.scores],
+        "studentId": [{
+          id: this.studentAnswer.id,
+          type: this.studentAnswer.type
+        }],
+        "classId": this.studentAnswer.classId,
+        "school": this.examInfo.scope == 'school' ? this.$store.state.userInfo.schoolCode : this.$store.state.userInfo.TEAMModelId,
+        // "school": this.$store.state.userInfo.schoolCode,
+        "subjectId": this.subjectId
+      }
+      this.$api.learnActivity.UpsertAllRecord(requestData).then(res => {
+        if (!res.error) {
+          this.$Message.success(this.$t('learnActivity.score.saveScoreOk'))
+          this.isUpd = false
+          this.$emit('updScore', [d])
+          // 切换学生时保存成绩,不需要展示下一位学生页面
+          if (!this.studentAnswer.scores.includes(-1) && this.studentInfo.id === requestData.studentId[0].id) {
+            this.isComplete = true
+          }
+          if (res.isScore) this.$EventBus.$emit('onStatusChange')
+        } else {
+          this.$Message.error(this.$t('learnActivity.score.saveSocreErr'))
+        }
+      })
+    },
+    delMark(index) {
+      this.$Modal.confirm({
+        title: this.$t('learnActivity.score.delMark'),
+        content: this.$t('learnActivity.score.delMarkContent'),
+        onOk: () => {
+          let mark = this.studentAnswer.mark[index]?.find(item => {
+            return item.tmdId == this.$store.state.userInfo.TEAMModelId
+          })
+          let blob = mark ? mark.mark : ""
+          //保存批注数据
+          this.$api.learnActivity.delAnswer({
+            "id": this.examId,
+            "studentId": this.studentAnswer.id,
+            "subjectId": this.subjectId,
+            "classId": this.studentAnswer.classId,
+            "code": this.examInfo.scope == 'school' ? this.$store.state.userInfo.schoolCode : this.$store.state.userInfo.TEAMModelId,
+            "tmdId": this.$store.state.userInfo.TEAMModelId,
+            "index": index,//题号
+            "blob": blob
+          }).then(
+            res => {
+              this.$Message.success(this.$t('learnActivity.mark.deleteOk'))
+              let i = this.studentAnswer.mark[index].findIndex(item => item.tmdId === this.$store.state.userInfo.TEAMModelId)
+              if (i > -1) {
+                this.studentAnswer.mark[index].splice(i, 1)
+                this.$set(this.studentAnswer.mark, index, this._.cloneDeep(this.studentAnswer.mark[index]))
+              }
+              console.log('**********', this.studentAnswer)
+            },
+            err => {
+              this.$Message.error(this.$t('learnActivity.mark.deleteErr'))
+            }
+          )
+        }
+      })
+    },
+    // 保存批注
+    // data: {markInfo,quIndex}
+    saveMark(data) {
+      let { markInfo, quIndex } = data
+      let fileName, hasMarked
+      if (this.studentAnswer.mark && this.studentAnswer.mark[quIndex]) {
+        let d = this._.cloneDeep(this.studentAnswer.mark[quIndex])
+        let markData = d.find(item => {
+          return item.tmdId == this.$store.state.userInfo.TEAMModelId
+        })
+        if (markData) {
+          let blob = markData.mark
+          fileName = blob.substring(blob.lastIndexOf('/') + 1, blob.length)
+          hasMarked = true
+        } else {
+          fileName = this.$jsFn.uuid() + '.png'
+        }
+      } else {
+        fileName = this.$jsFn.uuid() + '.png'
+      }
+      let markPng = this.$jsFn.dataURLtoFile(markInfo.base64, fileName)
+      //保存批注图片
+      let sas = this.examInfo.scope == 'school' ? this.$store.state.user.schoolProfile.blob_sas : this.$store.state.user.userProfile.blob_sas
+      let blobUrl = this.examInfo.scope == 'school' ? JSON.parse(decodeURIComponent(localStorage.school_profile, "utf-8")).blob_uri : JSON.parse(decodeURIComponent(localStorage.user_profile, "utf-8")).blob_uri
+      let host = blobUrl.substring(0, blobUrl.lastIndexOf('/'))
+      let cont = blobUrl.substring(blobUrl.lastIndexOf('/') + 1)
+      let blobTool = new BlobTool(host, cont, '?' + sas, this.examInfo.scope)
+      let path = `exam/${this.examId}/${this.subjectId}/${this.studentAnswer.id}`
+      blobTool.upload(markPng, { path }).then(
+        res => {
+          if (!hasMarked) {
+            this.studentAnswer.mark[quIndex].push({
+              mark: path + '/' + fileName,
+              tmdId: this.$store.state.userInfo.TEAMModelId
+            })
+          }
+          //保存批注数据
+          this.$api.learnActivity.upsertAnswer({
+            "id": this.examId,
+            "studentId": this.studentAnswer.id,
+            "subjectId": this.subjectId,
+            "classId": this.studentAnswer.classId,
+            "code": this.examInfo.scope == 'school' ? this.$store.state.userInfo.schoolCode : this.$store.state.userInfo.TEAMModelId,
+            "tmdId": this.$store.state.userInfo.TEAMModelId,
+            "index": quIndex,//题号
+            "mark": {
+              sc: 0,//分数
+              tmdId: this.$store.state.userInfo.TEAMModelId,
+              mark: path + '/' + fileName,//批注BLOB地址
+              identity: 'admin',//老师身份,这是是固定管理员身份
+              index: quIndex //题号
+            }
+          }).then(
+            res => {
+              this.$Message.success(this.$t('learnActivity.score.markOk'))
+              let r = 'QuAndScore' + quIndex
+              console.log(this.$refs['QuAndScore5'])
+              console.log(this.$refs[r])
+              this.$refs[r][0]?.saveMarkOk()
+            },
+            err => {
+              this.$Message.error(this.$t('learnActivity.score.markErr'))
+            }
+          )
+        },
+        err => {
+          this.$Message.error(this.$t('learnActivity.mark.saveErr'))
+        }
+      )
+    },
+  }
+}
+</script>
+
+<style scoped lang="less">
+@import "./ByStuMark.less";
+</style>

+ 50 - 23
TEAMModelOS/ClientApp/src/view/learnactivity/tabs/htAnswerTable.vue

@@ -137,9 +137,9 @@
 						</Option>
 					</Select>
 					<!-- 导出表格 -->
-					<Button size="small" @click="exportData" :loading="exportLoading" custom-icon="iconfont icon-download" type="primary" v-show="examInfo.progress != 'pending' && !isMarkView" class="export-btn">
+					<!-- <Button size="small" @click="exportData" :loading="exportLoading" custom-icon="iconfont icon-download" type="primary" v-show="examInfo.progress != 'pending' && !isMarkView" class="export-btn">
 						{{ exportLoading ? $t("learnActivity.score.exporting") : $t("learnActivity.score.exportData") }}
-					</Button>
+					</Button> -->
 					<!-- 切换打分UI -->
 					<span class="common-icon-text" @click="toggleScoreStatus">
 						<Icon :custom="isMarkView ? 'iconfont icon-table' : 'iconfont icon-scoring'" size="14" />
@@ -152,9 +152,9 @@
 					</span>
 				</div>
 				<!-- 按人批阅 -->
-				<ByStuMark ref="byStuMark" v-if="markType == 'byStu'" :examInfo="examInfo" :defaultIndex="defaultIndex" :paper="paperInfo" :studentInfo="chooseStudent" :subjectId="chooseSubject" @updScore="updScore" @nextStu="getNextStu"></ByStuMark>
+				<ByStuMark ref="byStuMark" v-if="markType == 'byStu'" :examInfo="examInfo" :defaultIndex="defaultIndex" :paper="paperInfo" :studentInfo="chooseStudent" :subjectId="subjectIdForMark" @updScore="updScore" @nextStu="getNextStu" :examId="examIdForMark"></ByStuMark>
 				<!-- 按题批阅 -->
-				<ByQuMark v-else-if="markType == 'byQu'" @updScore="updScore" :paper="paperInfo" :examInfo="examInfo" ref="byQuMark" :stusInfo="studentScore" :classId="chooseClass" :subjectId="chooseSubject"></ByQuMark>
+				<ByQuMark v-else-if="markType == 'byQu'" @updScore="updScore" :paper="paperInfo" :examInfo="examInfo" ref="byQuMark" :stusInfo="studentScore" :classId="chooseClass" :subjectId="subjectIdForMark" :examId="examIdForMark" ></ByQuMark>
 			</div>
 		</vuescroll>
 		
@@ -255,7 +255,7 @@
 <script>
 	import StuReport from "../StuReport.vue";
 	import CorrectRate from "../echarts/CorrectRate.vue";
-	import ByStuMark from "../byStu/ByStuMark.vue";
+	import ByStuMark from "../byStu/htByStuMark.vue";
 	import ByQuMark from "../ByQuMark.vue";
 	import excel from "@/utils/excel.js";
 	export default {
@@ -282,6 +282,8 @@
 		},
 		data() {
 			return {
+				examIdForMark:"",
+				subjectIdForMark:"",
 				groupSelectValue: "",
 				courseSelectValue: "",
 				teacherSelectValue:"",
@@ -612,12 +614,14 @@
 			// 課程下拉選單切換值 
 			changeCourseSelectData(){				
 				this.groupSelectData = [];
-				// 取得課程的班級
+				// 取得課程的班級  // 根據目前選中的課程班級  設定 examId 跟 subjectId
 				let stu = this.examInfo.stuLists.find((item) =>  item.creatorId == this.teacherSelectValue );
 				let course = stu.courseLists.find((item) =>  item.courseId == this.courseSelectValue );
 				course.groupLists.forEach(item =>{
 					this.groupSelectData.push({name: item.name, id: item.id});
 				})
+				this.examIdForMark = course.examId;
+				this.subjectIdForMark = course.subjectId;
 			},
 			// 課程下拉選單切換值 
 			changeGroupSelectData(){	
@@ -849,24 +853,46 @@
 				this.exportLoading = true;
 				//TODO 导出数据
 				let promises = [];
-				this.examInfo.subjects.forEach((subject) => {
-					let cList = [];
-					if (this.examInfo.classes.length) {
-						cList = this.examInfo.classes;
-					} else {
-						cList = this.examInfo.stuLists;
-					}
-					cList.forEach((c) => {
-						let requestData = {
-							id: this.examInfo.id,
-							code: this.examInfo.scope == "school" ? this.$store.state.userInfo.schoolCode : this.examInfo.code.substr(this.examInfo.code.lastIndexOf("-") + 1),
-							subjectId: subject.id,
-							classId: c,
+				// this.examInfo.stuLists.courseLists.forEach((course) => {
+				// 	let cList = [];
+				// 	// if (this.examInfo.classes.length) {
+				// 	// 	cList = this.examInfo.classes;
+				// 	// } else {
+				// 	// 	cList = this.examInfo.stuLists;
+				// 	// }
+
+				// 	cList.forEach((c) => {
+				// 		let requestData = {
+				// 			id: this.examInfo.id,
+				// 			code: this.examInfo.scope == "school" ? this.$store.state.userInfo.schoolCode : this.examInfo.code.substr(this.examInfo.code.lastIndexOf("-") + 1),
+				// 			subjectId: course.subjectId,
+				// 			classId: c,
+				// 			startTime: this.examInfo.startTime
+				// 		};
+				// 		promises.push(this.$api.learnActivity.FindAllStudent(requestData));
+				// 	});
+				// });
+
+				this.examInfo.stuLists.forEach(stu => {
+					stu.courseLists.forEach(course => {
+						course.groupLists.forEach(group => {
+							let requestData = {
+							id: course.examId,
+							code: stu.creatorId,
+							subjectId: course.subjectId,
+							classId: group.id,
 							startTime: this.examInfo.startTime
-						};
-						promises.push(this.$api.learnActivity.FindAllStudent(requestData));
-					});
-				});
+						    };
+							promises.push(this.$api.learnActivity.FindAllStudent(requestData));
+						})
+					})
+				})
+
+
+
+
+
+
 				// 20240407 处理导出学生各题作答内容
 				let sas = this.examInfo.scope == "school" ? this.$store.state.user.schoolProfile.blob_sas : this.$store.state.user.userProfile.blob_sas;
 				let blobUrl = this.examInfo.scope == "school" ? JSON.parse(decodeURIComponent(localStorage.school_profile, "utf-8")).blob_uri : JSON.parse(decodeURIComponent(localStorage.user_profile, "utf-8")).blob_uri;
@@ -1164,6 +1190,7 @@
 				// 	startTime: this.examInfo.startTime
 				// };	
 				//debugger
+				// 根據選中的老師>課程 取得需要的參數
 				let stu = this.examInfo.stuLists.find((item) =>  item.creatorId == this.teacherSelectValue );
 				let course = stu.courseLists.find((item) =>  item.courseId == this.courseSelectValue );
 				let requestData = {

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

@@ -1506,12 +1506,14 @@ export default {
           // 获取关联的试题数据
           itemNodeArr.forEach(async item => {
             promiseArr.push(new Promise(async (r, j) => {
+              let blob = this.$evTools.getBlobHost() + '/' + item.cntr + item.link + sas
               let indexJsonFile = await this.$tools.getFile(this.$evTools.getBlobHost() + '/' + item.cntr + item.link + sas)
               let jsonData = JSON.parse(indexJsonFile)
               jsonData.exercise.question = jsonData.item[0].question
               jsonData.exercise.option = jsonData.item[0].option
               jsonData.exercise.id = jsonData.id
               jsonData.exercise.pid = jsonData.pid
+              jsonData.exercise.blob = blob
               jsonData.exercise = await this.$evTools.doAddHost(jsonData.exercise, null, null, true)
               r(jsonData.exercise)
             }))

+ 89 - 36
TEAMModelOS/Controllers/School/ArtReviewController.cs

@@ -152,8 +152,8 @@ namespace TEAMModelOS.Controllers
 
 
 #if !DEBUG
-        [AuthToken(Roles = "teacher,admin")]
-        [Authorize(Roles = "IES")]
+        //[AuthToken(Roles = "teacher,admin")]
+        //[Authorize(Roles = "IES")]
 #endif
         public async Task<IActionResult> GenPDF(JsonElement request)
         {
@@ -187,54 +187,107 @@ namespace TEAMModelOS.Controllers
             return Ok(new {code=0,msg="加入PDF报告生成队列中。" });
         }
 
+
+        /**
+        {
+           "artId": "99a946a7-f475-463f-846f-834a276e1b34",
+           "schoolId": "hbcn",
+           "schoolCode":"hbcn",
+           "opt": "gen-pdf",
+           "headLang":"zh-cn",
+           "studentIds": [
+               "202206001", "202206002"
+           ]
+}
+        
+        */
+
         [ProducesDefaultResponseType]
         //[AuthToken(Roles = "teacher,admin")]
-        [HttpPost("get-pdf")]
-        public async Task<IActionResult> getPdf(JsonElement request)
+        [HttpPost("update-custom-comment")]
+        public async Task<IActionResult> UpdateCustomComment(JsonElement json) 
         {
-            string head_lang = "zh-cn";
-            List<string> schools = new List<string>();
-            if (request.TryGetProperty("schools", out JsonElement _schools) && _schools.ValueKind.Equals(JsonValueKind.Array))
+            string head_lang = "";
+            if (HttpContext.Request.Headers.TryGetValue("lang", out var _lang))
             {
-                schools = _schools.ToObject<List<string>>();
+                head_lang = $"{_lang}";
             }
-            var client = _azureCosmos.GetCosmosClient();
-            string sql = "select * from c where c.pk = 'Art' and c.pId in ('2f74d38e-80c1-4c55-9dd0-de0d8f6fdf6d','306fa576-7ae4-4baa-ac24-0b5ad4dd1bc2')";           
-            List<ArtEvaluation> infos = new();
-            await foreach (var item in client.GetContainer(Constant.TEAMModelOS, Constant.Common).GetItemQueryIteratorSql<ArtEvaluation>(queryText: sql))
+            if (string.IsNullOrWhiteSpace(head_lang))
             {
-                infos.Add(item);
+                head_lang = _option.Location.Contains("China") ? "zh-cn" : "en-us";
             }
-            //List<Task<ItemResponse<ExamInfo>>> tasks = new List<Task<ItemResponse<ExamInfo>>>();
-            List<string> ids = infos.Select(x => x.id).ToList();
+            if (!json.TryGetProperty("artId", out JsonElement _artId))
+            {
+                return BadRequest();
+            }
+            if (!json.TryGetProperty("schoolId", out JsonElement _schoolId))
+            {
+                return BadRequest();
+            }
+            if (!json.TryGetProperty("studentId", out JsonElement _studentId))
+            {
+                return BadRequest();
+            }
+            json.TryGetProperty("comment", out JsonElement _comment);
+            json.TryGetProperty("comment_music", out JsonElement _comment_music);
+            json.TryGetProperty("comment_painting", out JsonElement _comment_painting);
+
+            string query = $" select value c from c where c.school  = '{_schoolId}' and c.studentId ='{_studentId}'";
             List<StudentArtResult> artResults = new List<StudentArtResult>();
-            await foreach (var item in client.GetContainer(Constant.TEAMModelOS, "Student").GetItemQueryStreamIteratorSql(
-                queryText: $"select value(c) from c where c.pk = 'ArtResult' and c.artId in ({string.Join(",", ids.Select(o => $"'{o}'"))})"))
+            await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Student).GetItemQueryIteratorSql<StudentArtResult>
+              (queryText: query, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"ArtResult-{_artId}") }))
             {
-                using var json = await JsonDocument.ParseAsync(item.Content);
-                if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
-                {
-                    foreach (var obj in json.RootElement.GetProperty("Documents").EnumerateArray())
+                item.comment=$"{_comment}";
+                item.subjectScores.ForEach(x => {
+                    if(x.subjectId.Equals("subject_music"))
                     {
-                        artResults.Add(obj.ToObject<StudentArtResult>());
+                        x.comment=$"{_comment_music}";
                     }
-                }
+                    if (x.subjectId.Equals("subject_painting"))
+                    {
+                        x.comment = $"{_comment_painting}";
+                    }
+                });
+                await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Student).UpsertItemAsync(item, new PartitionKey($"ArtResult-{_artId}"));
+                artResults.Add(item);
             }
-            foreach (var item in infos)
+            if (artResults.IsNotEmpty()) 
             {
-                if (item.classes.Count == 0)
-                {
-                    continue;
-                }
-                else
-                {
-                    
-                    var studentIds = artResults.Where(c => c.artId.Equals(item.id)).Select(z => z.studentId).ToList();
-                    await ArtService.GenArtPDF(studentIds, item.id, item.school, head_lang, _serviceBus, _configuration);
-                    await Task.Delay(30000);
-                }
+                var data = await GenPDFService.GenArtStudentPdf(_azureRedis, _azureCosmos, _coreAPIHttpService, _dingDing, _azureStorage, _configuration, artResults.Select(x=>x.studentId).ToList(), $"{_artId}", $"{_schoolId}", $"{head_lang}");
+                return Ok(new { code = 0, dataFile = data.studentPdfs.Select(x => new { x.blob, x.blobFullUrl }) });
+            }
+            return Ok(new { code = 1, msg = "没有找到学生数据" });
+        }
+
+        [ProducesDefaultResponseType]
+        //[AuthToken(Roles = "teacher,admin")]
+        [HttpPost("get-pdf-data")]
+        public async Task<IActionResult> GetPdfData(JsonElement json)
+        {
+            if (!json.TryGetProperty("artId", out JsonElement _artId))
+            {
+                return BadRequest();
+            }
+            if (!json.TryGetProperty("schoolId", out JsonElement _schoolId))
+            {
+                return BadRequest();
+            }
+            List<string> studentIds = new List<string>();
+            if (json.TryGetProperty("studentIds", out JsonElement _studentIds) && _studentIds.ValueKind.Equals(JsonValueKind.Array))
+            {
+                studentIds = _studentIds.ToObject<List<string>>();
+            }
+            string head_lang = string.Empty;
+            if (HttpContext.Request.Headers.TryGetValue("lang", out var _lang))
+            {
+                head_lang = $"{_lang}";
+            }
+            if (string.IsNullOrWhiteSpace(head_lang))
+            {
+                head_lang = _option.Location.Contains("China") ? "zh-cn" : "en-us";
             }
-            return Ok(new { code = 0, msg = "加入PDF报告生成队列中。", schools= infos.Select(x=>x.school)});
+            var data = await GenPDFService.GenArtStudentPdf(_azureRedis, _azureCosmos, _coreAPIHttpService, _dingDing, _azureStorage, _configuration, studentIds,$"{_artId}",$"{_schoolId}",$"{head_lang}");
+            return Ok(new {code=0, dataFiles= data.studentPdfs.Select(x=>new { x.blob,x.blobFullUrl}) });
         }
 
         /// <summary>

+ 35 - 11
TEAMModelOS/Controllers/System/GenPDFController.cs

@@ -13,7 +13,9 @@ using TEAMModelOS.SDK.Models.Service;
 using DocumentFormat.OpenXml.Vml;
 using TEAMModelOS.Filter;
 using Microsoft.AspNetCore.Authorization;
-
+using FastJSON;
+using Microsoft.Azure.Cosmos.Linq;
+using TEAMModelOS.SDK.Extension;
 namespace TEAMModelOS.Controllers
 {
     [ProducesResponseType(StatusCodes.Status200OK)]
@@ -56,18 +58,18 @@ namespace TEAMModelOS.Controllers
         /// </summary>
         /// <param name="request"></param>
         /// <returns></returns>
-        [ProducesDefaultResponseType]
-        [HttpPost("art-report")]
+      //  [ProducesDefaultResponseType]
+       // [HttpPost("art-report")]
 //#if !DEBUG
 //        [AuthToken(Roles = "teacher,admin")]
 //        [Authorize(Roles = "IES")]
 //#endif
-        public async Task<IActionResult> ArtReport (GenPDFData request) 
-        {
+       // public async Task<IActionResult> ArtReport (GenPDFData request) 
+        //{
             
-            var data = await GenPDFService.AddGenPdfQueue(  _azureRedis, request);
-            return Ok(new { total= data.total,add= data .add});
-        }
+            //var data = await GenPDFService.AddGenPdfQueue(  _azureRedis, request);
+            //return Ok(new { total= data.total,add= data .add});
+       // }
         /// <summary>
         /// 艺术评测报告生成
         /// </summary>
@@ -79,10 +81,32 @@ namespace TEAMModelOS.Controllers
         //        [AuthToken(Roles = "teacher,admin")]
         //        [Authorize(Roles = "IES")]
         //#endif
-        public async Task<IActionResult> GenArtStudentPdf(JsonElement request)
+        public async Task<IActionResult> GenArtStudentPdf(JsonElement json)
         {
-
-            await GenPDFService.GenArtStudentPdf(_azureRedis, _azureCosmos,_coreAPIHttpService, _dingDing, _azureStorage, _configuration, request);
+            if (!json.TryGetProperty("artId", out JsonElement _artId))
+            {
+                return BadRequest();
+            }
+            if (!json.TryGetProperty("schoolId", out JsonElement _schoolId))
+            {
+                return BadRequest();
+            }
+            List<string> studentIds = new List<string>();
+            if (json.TryGetProperty("studentIds", out JsonElement _studentIds) && _studentIds.ValueKind.Equals(JsonValueKind.Array))
+            {
+                studentIds = _studentIds.ToObject<List<string>>();
+            }
+            string head_lang = string.Empty;
+            if (HttpContext.Request.Headers.TryGetValue("lang", out var _lang))
+            {
+                head_lang = $"{_lang}";
+            }
+            if (string.IsNullOrWhiteSpace(head_lang))
+            {
+                head_lang = _option.Location.Contains("China") ? "zh-cn" : "en-us";
+            }
+            var data =   await GenPDFService.GenArtStudentPdf(_azureRedis, _azureCosmos, _coreAPIHttpService, _dingDing, _azureStorage, _configuration, studentIds, $"{_artId}", $"{_schoolId}", $"{head_lang}");
+            await GenPDFService.PushScreenTask(_azureRedis, _configuration, $"{_artId}", data.art, data.studentPdfs,_httpClient,_dingDing);
             return Ok();
         }
         

+ 4 - 4
TEAMModelOS/TEAMModelOS.csproj

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

+ 1 - 1
TEAMModelOS/appsettings.Development.json

@@ -18,7 +18,7 @@
     "IdTokenSalt": "8263692E2213497BB55E74792B7900B4",
     "HttpTrigger": "https://teammodelosfunction-test.chinacloudsites.cn/api/",
     //"HttpTrigger": "http://localhost:7071/api/"
-    "Version": "5.2408.14.1"
+    "Version": "5.2408.21.1"
   },
   "Azure": {
     // 测试站数据库

+ 1 - 1
TEAMModelOS/appsettings.json

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