黄贺彬 il y a 3 mois
Parent
commit
2546561696

+ 1 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/.npmrc

@@ -0,0 +1 @@
+electron_mirror=https://npmmirror.com/mirrors/electron/

+ 7 - 2
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/constants.js

@@ -17,7 +17,7 @@ if (app.isPackaged) {
     serverPath = __dirname;
 }
 
-const baseUrl = 'https://exam.habook.local:8888/login/admin';
+const baseUrl = 'https://exam.habook.local:8888';
 const remoteVersionsUrl = 'https://teammodelos.blob.core.chinacloudapi.cn/0-public/exam-server/versions.json';
 const remoteZipBaseUrl = 'https://teammodelos.blob.core.chinacloudapi.cn/0-public/exam-server';
 
@@ -26,5 +26,10 @@ module.exports = {
     baseUrl,
     remoteVersionsUrl,
     remoteZipBaseUrl,
-    agent
+    agent,
+    VIRTUAL_KEYWORDS: [
+        'virtual', 'hyper-v', 'virtualbox', 'vmware', 'veth', 'virbr', 'tunnel', 'docker', 'loopback', 'vpn',
+        'ppp', 'slip', 'bridge', 'vnic', 'vif', 'tap', 'vlan', 'vswitch', 'vxlan', 'gre', 'ipsec', 'vrf', 'vport', 'vnet', 'vmac', 'vxnet'
+    ],
+    VIRTUAL_TYPES: ['Loopback', 'Tunnel', 'Ppp', 'Slip', 'Unknown']
 };

+ 22 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/installer.nsh

@@ -0,0 +1,22 @@
+Unicode true
+Section "Install"
+    ; 安装路径
+    SetOutPath "$INSTDIR"
+
+    ; 安装文件
+    File /r "dist\win-ia32-unpacked\*.*"
+
+    ; 检查 server 目录是否存在,若不存在则创建
+    IfFileExists "$INSTDIR\server" +2
+    CreateDirectory "$INSTDIR\server"
+SectionEnd
+
+Section "Uninstall"
+    ; 删除文件,但保留 server 目录
+    Delete "$INSTDIR\*.*"
+    RMDir /r "$INSTDIR\subdir1"
+    RMDir /r "$INSTDIR\subdir2"
+
+    ; 跳过 server 目录
+    ; RMDir /r "$INSTDIR\server" ; 注释掉这一行
+SectionEnd

+ 69 - 8
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/main.js

@@ -1,18 +1,23 @@
-const { app, BrowserWindow } = require('electron');
+const { app, BrowserWindow, Tray, Menu } = require('electron');
+const path = require('path');
 const serverManager = require('./serverManager');
 const menuManager = require('./menuManager');
 const updateManager = require('./updateManager');
 const constants = require('./constants');
+const { getNetworkInterfaces } = require('./networkService');
 //app.disableHardwareAcceleration(); //使用windows7 ,或者虚拟机的时候 需要验证 禁用 GPU 加速
 //app.commandLine.appendSwitch('ignore-certificate-errors')
 let win = null;
-
+let tray = null;
+app.isQuitting = false; // 添加标志位
 // 创建 Electron 窗口的函数
 const createWindow = async () => {
     try {
-       
+        const networks = getNetworkInterfaces();
+        console.log('Available networks:', networks);
         const isServerRunning = await serverManager.checkServerHealth();
         if (!isServerRunning) {
+            console.log('Server is not running, starting it......................................');
             await serverManager.startServer(); // 启动 Web API
         }
         win = new BrowserWindow({
@@ -26,16 +31,65 @@ const createWindow = async () => {
         //win.webContents.session.setCertificateVerifyProc((request, callback) => {
         //    // 始终返回 0 表示验证通过
         //    callback(0);
-        //});
+        //});///login/admin
         win.maximize();
-        win.loadURL(constants.baseUrl, {
+        win.loadURL(`${constants.baseUrl}/login/admin`, {
             agent: constants.agent
         });
+        // 监听窗口关闭事件,隐藏窗口而不是关闭
+        win.on('close', (event) => {
+            if (!app.isQuitting) {
+                event.preventDefault();
+                win.hide();
+            }
+        });
 
     } catch (error) {
         console.error('Error starting server or loading window:', error);
     }
 };
+
+
+const createTray = () => {
+    const iconPath = path.join(__dirname, 'logo.ico'); // 你的托盘图标路径
+    tray = new Tray(iconPath);
+
+    const contextMenu = Menu.buildFromTemplate([
+        {
+            label: '显示',
+            click: () => {
+                if (win) {
+                    win.show();
+                }
+            }
+        },
+        {
+            label: '退出',
+            click: () => {
+                app.isQuitting = true;
+                app.quit();
+            }
+        }
+    ]);
+
+    tray.setToolTip('评测教师端');
+    tray.setContextMenu(contextMenu);
+
+    // 监听双击托盘图标事件,恢复窗口
+    tray.on('double-click', () => {
+        if (win) {
+            if (win.isVisible()) {
+                win.focus(); // 如果窗口已经显示,则聚焦窗口
+            } else {
+                win.show(); // 如果窗口隐藏,则显示窗口
+            }
+        }
+    });
+};
+
+
+
+
 // 定义回调函数
 const checkForUpdatesHandler = () => {
     updateManager.checkForUpdates(win, () => {
@@ -46,6 +100,7 @@ const checkForUpdatesHandler = () => {
 app.whenReady().then(() => {
     process.env.NODE_OPTIONS = '--tls-min-v1.2';
     createWindow();
+    createTray();
     // 创建菜单并传递回调函数
     menuManager.createMenu(checkForUpdatesHandler);
 
@@ -57,13 +112,19 @@ app.whenReady().then(() => {
 
     // 监听 before-quit 事件,关闭 IES.ExamServer.exe 进程
     app.on('before-quit', async (event) => {
+        if (app.isQuitting) {
+            return; // 如果已经在退出流程中,则直接返回
+        }
+        app.isQuitting = true; // 标记正在退出
         event.preventDefault(); // 阻止默认的退出行为
-        await serverManager.shutdownServer();
-        //app.quit(); // 关闭 Electron 应用程序
+        await serverManager.shutdownServer(); // 关闭服务器
+        app.quit(); // 触发退出流程
     });
 });
 
 // 当所有窗口关闭时退出应用(macOS 除外)
 app.on('window-all-closed', function () {
-    app.quit();
+    if (process.platform !== 'darwin') {
+        app.quit();
+    }
 });

+ 31 - 24
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/menuManager.js

@@ -21,7 +21,7 @@ const createMenu = (checkForUpdatesCallback) => {
                 const win = BrowserWindow.getFocusedWindow(); // 获取当前获得焦点的窗口
                 console.log('win', win);
                 if (win) {
-                    win.loadURL(constants.baseUrl, {
+                    win.loadURL(`${constants.baseUrl}/login/admin`, {
                         agent: constants.agent
                     });
                 } else {
@@ -43,34 +43,41 @@ const createMenu = (checkForUpdatesCallback) => {
                     click: () => {
                         openDevTools();
                     }
-                }
-            ]
-        },
-        { label: '帮助' },
-        {
-            label: '版本',
-            submenu: [
+                },
                 {
-                    label: '检查更新服务端',
+                    label: '退出程序',
                     click: () => {
-                        if (typeof checkForUpdatesCallback === 'function') {
-                            checkForUpdatesCallback(); // 确保回调函数是函数
-                        } else {
-                            console.error('checkForUpdatesCallback is not a function');
-                        }
+                        require('electron'). app.quit();
                     }
-                },
-                {
-                    label: `服务端版本: ${localVersion}`
-                },
-                {
-                    label: `应用程序版本: ${appVersion}`
-                },
-                {
-                    label: `浏览器内核版本: ${chromeVersion}`
                 }
             ]
         }
+        //,
+        //{ label: '帮助' },
+        //{
+        //    label: '版本',
+        //    submenu: [
+        //        {
+        //            label: '检查更新服务端',
+        //            click: () => {
+        //                if (typeof checkForUpdatesCallback === 'function') {
+        //                    checkForUpdatesCallback(); 
+        //                } else {
+        //                    console.error('checkForUpdatesCallback is not a function');
+        //                }
+        //            }
+        //        },
+        //        {
+        //            label: `服务端版本: ${localVersion}`
+        //        },
+        //        {
+        //            label: `应用程序版本: ${appVersion}`
+        //        },
+        //        {
+        //            label: `浏览器内核版本: ${chromeVersion}`
+        //        }
+        //    ]
+        //}
     ];
 
     const menu = Menu.buildFromTemplate(template);
@@ -112,7 +119,7 @@ const clearCache = () => {
         session.defaultSession.clearCache(() => {
             console.log('缓存已清理');
         });
-        win.loadURL(constants.baseUrl, {
+        win.loadURL(`${constants.baseUrl}/login/admin`, {
             agent: constants.agent
         });
     } else {

BIN
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/networkService.js


+ 6 - 2
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/package.json

@@ -23,11 +23,13 @@
       "output": "dist"
     },
     "win": {
+      "requestedExecutionLevel": "requireAdministrator",
       "target": "nsis",
       "sign": false,
       "icon": "logo.ico"
     },
     "nsis": {
+      "unicode": true,
       "runAfterFinish": false,
       "oneClick": false,
       "perMachine": true,
@@ -35,7 +37,8 @@
       "createDesktopShortcut": true,
       "createStartMenuShortcut": true,
       "installerHeader": "header.bmp",
-      "installerSidebar": "sidebar.bmp"
+      "installerSidebar": "sidebar.bmp",
+      "allowElevation": true
     },
     "extraFiles": [
       {
@@ -49,6 +52,7 @@
   },
   "dependencies": {
     "adm-zip": "^0.5.16",
-    "axios": "^1.7.9"
+    "axios": "^1.7.9",
+    "network-interfaces": "^1.1.0"
   }
 }

+ 44 - 30
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/serverManager.js

@@ -10,40 +10,54 @@ let serverProcess;
 // 启动 Web API 的函数
 const startServer = () => {
     return new Promise((resolve, reject) => {
-        serverProcess = exec(path.join(constants.serverPath, 'server', 'IES.ExamServer.exe'), {
-            cwd: `${constants.serverPath}/server`, // 设置工作目录为 server 目录
-            stdio: 'inherit'
-        });
+        try {
+            console.log('Starting server...Promise');
+            const serverExePath = path.join(constants.serverPath, 'server', 'IES.ExamServer.exe');
+            if (!fs.existsSync(serverExePath)) {
+                throw new Error(`IES.ExamServer.exe not found at ${serverExePath}`);
+            }
+            // 使用双引号包裹路径
+            const serverCommand = `"${serverExePath}"`;
+            console.log(`Server command: ${serverCommand}`);
+            serverProcess = exec(serverCommand, {
+                cwd: `${constants.serverPath}/server`, // 设置工作目录为 server 目录
+                stdio: 'inherit'
+            });
 
-        serverProcess.stdout.on('data', (data) => {
-            console.log(`Server stdout: ${data}`);
-        });
+            serverProcess.stdout.on('data', (data) => {
+                console.log(`Server stdout: ${data}`);
+            });
 
-        serverProcess.stderr.on('data', (data) => {
-            console.log(`Server stderr: ${data}`);
-        });
+            serverProcess.stderr.on('data', (data) => {
+                console.log(`Server stderr: ${data}`);
+            });
 
-        serverProcess.on('close', (data) => {
-            console.log(`Server process exited with code ${data}`);
-            reject(new Error(`Server process exited with code ${data}`));
-        });
+            serverProcess.on('close', (data) => {
+                console.log(`Server process exited with code ${data}`);
+                reject(new Error(`Server process exited with code ${data}`));
+            });
 
-        // 等待 Web API 启动成功
-        const checkHealth = async () => {
-            try {
-                const response = await axios.get(`${constants.baseUrl}/index/health`, {
-                httpsAgent: constants.agent
-                });
-                if (response.status === 200) {
-                    console.log('Server is up and running!');
-                    resolve();
+            // 等待 Web API 启动成功
+            const checkHealth = async () => {
+                try {
+                    const response = await axios.get(`${constants.baseUrl}/index/health`, {
+                        httpsAgent: constants.agent
+                    });
+                    if (response.status === 200) {
+                        console.log('Server is up and running!');
+                        resolve();
+                    }
+                } catch (error) {
+                    console.log('Waiting for server to start...', error);
+                    setTimeout(checkHealth, 1000); // 每隔 1 秒检查一次
                 }
-            } catch (error) {
-                console.log('Waiting for server to start...', error);
-                setTimeout(checkHealth, 1000); // 每隔 1 秒检查一次
-            }
-        };
-        checkHealth();
+            };
+            checkHealth();
+        } catch (error)
+        {
+            console.error('Error starting server:', error);
+            reject(error);
+        }
     });
 };
 
@@ -68,7 +82,7 @@ const shutdownServer = async () => {
    
     try {
         console.log('index/shutdown api ...');
-        const response = await axios.get(`${constants.baseUrl}/index/shutdown`, {
+        const response = await axios.get(`${constants.baseUrl}/index/shutdown?delay=0`, {
             httpsAgent: constants.agent
         });
         if (response.status === 200) {

+ 1 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/updateManager.js

@@ -153,7 +153,7 @@ const updateServer = async (latestVersion, win, createMenuCallback) => {
         } catch (error) {
             console.error('Error Shutting down IES.ExamServer...', error);
         }
-        await utils.delay(1000);
+        await utils.delay(2000);
 
         // 3. 解压 IES.ExamServer.zip
         console.log('Extracting IES.ExamServer.zip...');

Fichier diff supprimé car celui-ci est trop grand
+ 2733 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/yarn.lock


+ 1 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/.config/dotnet-tools.json

@@ -3,7 +3,7 @@
   "isRoot": true,
   "tools": {
     "dotnet-ef": {
-      "version": "9.0.1",
+      "version": "9.0.2",
       "commands": [
         "dotnet-ef"
       ],

+ 42 - 12
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/IndexController.cs

@@ -248,27 +248,52 @@ namespace IES.ExamServer.Controllers
              */
             return Ok(content);
         }
+
+
+        private static bool _isShuttingDown = false;
         /// <summary>
         /// 关闭当前进程的接口
         /// </summary>
         /// <returns></returns>
         [HttpGet("shutdown")]
-        public IActionResult Shutdown(int delayMilliseconds = 200)
+        public IActionResult Shutdown(int delay = 200)
         {
             try
             {
-                _ = Task.Run(async () =>
+                if (_isShuttingDown)
                 {
-                    try
-                    {
-                        await Task.Delay(delayMilliseconds);
-                        _hostApplicationLifetime.StopApplication();
-                    }
-                    catch (Exception ex)
-                    {
-                        Console.Error.WriteLine($"Error during shutdown: {ex.Message}");
-                    }
-                });
+                    return Ok("Shutdown process is already initiated.");
+                }
+
+                _isShuttingDown = true;
+
+                //_ = Task.Delay(delay).ContinueWith(_ =>
+                //{
+                //    try
+                //    {
+                //        // 关闭前清理资源
+                //        CleanupResources();
+
+                //        // 停止应用程序
+                //        _hostApplicationLifetime.StopApplication();
+                //    }
+                //    catch (Exception ex)
+                //    {
+                //        Console.Error.WriteLine($"Error during shutdown: {ex.Message}");
+                //    }
+                //});
+                try
+                {
+                    // 关闭前清理资源
+                    CleanupResources();
+
+                    // 停止应用程序
+                    _hostApplicationLifetime.StopApplication();
+                }
+                catch (Exception ex)
+                {
+                    Console.Error.WriteLine($"Error during shutdown: {ex.Message}");
+                }
 
                 return Ok("Shutdown process has been initiated.");
             }
@@ -277,6 +302,11 @@ namespace IES.ExamServer.Controllers
                 return StatusCode(500, $"Failed to initiate the shutdown process: {ex.Message}");
             }
         }
+
+        private void CleanupResources()
+        {
+           // Console.WriteLine("Cleaning up resources...");
+        }
         [HttpGet("nowtime")]
         public async Task<IActionResult> NowTime()
         {

+ 2 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/ManageController.cs

@@ -362,13 +362,14 @@ namespace IES.ExamServer.Controllers
                                             Directory.CreateDirectory(path_paper);
                                         }
                                         //最多开启10个线程并行下载
-                                        var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = 20 };
+                                        var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = 5 };
                                         // 使用 Parallel.ForEachAsync 并行处理每个 blob
                                         await Parallel.ForEachAsync(evaluationPaper.blobs, parallelOptions, async (blob, cancellationToken) =>
                                         {
                                             try
                                             {
                                                 // 下载 Blob 文件到本地
+                                                httpClient.Timeout = TimeSpan.FromSeconds(300);
                                                 HttpResponseMessage blobMessage = await httpClient.GetAsync($"{url}/{cnt}/{blob.path}?{sas}", cancellationToken);
                                                 if (blobMessage.IsSuccessStatusCode)
                                                 {

+ 2 - 17
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Services/IndexService.cs

@@ -277,22 +277,6 @@ namespace IES.ExamServer.Services
                 int physical = 0;
                 if (nic.OperationalStatus == OperationalStatus.Up)
                 {
-                    //该方法并不能判断是否是物理网卡
-                    //if (nic.NetworkInterfaceType== NetworkInterfaceType.Ethernet//- 以太网接口,通常用于有线网络连接。
-                    //    ||nic.NetworkInterfaceType== NetworkInterfaceType.TokenRing//- 令牌环网接口,一种较早的局域网技术。
-                    //    ||nic.NetworkInterfaceType== NetworkInterfaceType.Fddi//- 光纤分布式数据接口,用于光纤网络。
-                    //    ||nic.NetworkInterfaceType== NetworkInterfaceType.Ethernet3Megabit//- 3兆比特以太网接口,一种较早的以太网标准。
-                    //    ||nic.NetworkInterfaceType== NetworkInterfaceType.FastEthernetT// 快速以太网接口(100Base-T),用于双绞线网络。
-                    //    ||nic.NetworkInterfaceType== NetworkInterfaceType.FastEthernetFx//快速以太网接口(100Base-FX),用于光纤网络。
-                    //    ||nic.NetworkInterfaceType== NetworkInterfaceType.Wireless80211//无线局域网接口(Wi-Fi)。
-                    //    ||nic.NetworkInterfaceType== NetworkInterfaceType.GigabitEthernet //千兆以太网接口,提供1 Gbps的数据速率。
-                    //    ||nic.NetworkInterfaceType== NetworkInterfaceType.Wman// 移动宽带接口,用于WiMax设备。
-                    //    ||nic.NetworkInterfaceType== NetworkInterfaceType.Wwanpp//移动宽带接口,用于GSM设备。
-                    //      ||nic.NetworkInterfaceType== NetworkInterfaceType.Wwanpp2//移动宽带接口,用于CDMA设备。
-                    //    )
-                    //{
-                    //    physical=1;
-                    //}
                     if (IsPhysicalNetworkInterface(nic)) 
                     {
                         physical = 1;
@@ -333,11 +317,12 @@ namespace IES.ExamServer.Services
             var networks = device.networks.ToList();
             if (device.networks.IsNotEmpty()) 
             {
-               var order=  device.networks.OrderByDescending(x => x.physical).ToList();
+                var order=  device.networks.OrderByDescending(x => x.physical).ToList();
                 for (int i=0; i<order.Count();i++) 
                 {
                     order[i].domain="exam.habook.local";
                 }
+                device.networks=order;
                 //优先以物理网卡来生成hash,如果没有则以所有网卡生成hash
                 var physical = order.FindAll(x => x.physical==1);
                 if (physical.IsNotEmpty())

+ 18 - 1
TEAMModelOS.Function/IESServiceBusTrigger.cs

@@ -1765,7 +1765,24 @@ namespace TEAMModelOS.Function
                                         {
                                             string owner = lessonRecord.scope.Equals("school") ? lessonRecord.school : lessonRecord.tmdid;
                                             examDatas = await LessonETLService.GetExamInfo(lessonRecord, timeLineData, _azureStorage, owner);
-
+                                            try {
+                                                foreach (var examData in examDatas)
+                                                {
+                                                    ResponseMessage responseMessage = await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Common).ReadItemStreamAsync(examData.exam.id, new PartitionKey($"Exam-{owner}"));
+                                                    if (responseMessage.IsSuccessStatusCode)
+                                                    {
+                                                        ExamInfo examInfo = JsonDocument.Parse(responseMessage.Content).RootElement.ToObject<ExamInfo>();
+                                                        if (examInfo!=null)
+                                                        {
+                                                            examInfo.lessonRecordId = lessonRecord.id;
+                                                            await _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Common).UpsertItemAsync(examInfo, new PartitionKey(examInfo.code));
+                                                        }
+                                                    }
+                                                }
+                                            }
+                                            catch (Exception ex) { 
+                                              await  _dingDing.SendBotMsg($"更新考试信息失败,{examDatas.ToJsonString()}{ex.Message}{ex.StackTrace}", GroupNames.成都开发測試群組);
+                                            }
                                             sokratesDatas= sokratesDatas.IsNotEmpty() ? sokratesDatas : timeLineData!=null ? timeLineData.events : new List<TimeLineEvent>();
                                         }
                                         LessonLocal lessonLocal = new LessonLocal()

+ 1 - 0
TEAMModelOS.SDK/Models/Cosmos/School/ExamInfo.cs

@@ -25,6 +25,7 @@ namespace TEAMModelOS.SDK.Models
         public string school { get; set; }
         public string creatorId { get; set; }
         public int stuCount { get; set; }
+        public string lessonRecordId { get; set; }
 /*        //实际考试人数
         public int realCount { get; set; }
         //平均分