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

Merge branch 'develop' into develop_local

jeff преди 2 месеца
родител
ревизия
337a86e7c8
променени са 49 файла, в които са добавени 1587 реда и са изтрити 4751 реда
  1. 6 2
      TEAMModelBI/Controllers/BIProductAnalysis/ProductAnalysisController.cs
  2. 165 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamClient/app.js
  3. 3 2
      TEAMModelOS.Extension/IES.Exam/IES.ExamClient/constants.js
  4. 106 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamClient/index.html
  5. 203 73
      TEAMModelOS.Extension/IES.Exam/IES.ExamClient/main.js
  6. 3 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamClient/package.json
  7. 9 3
      TEAMModelOS.Extension/IES.Exam/IES.ExamClient/serverManager.js
  8. 1 1
      TEAMModelOS.Extension/IES.Exam/IES.ExamClient/updateManager.js
  9. 6 62
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/IndexController.cs
  10. 1 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/ServiceInitializer.cs
  11. 152 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/SystemScriptHelper.cs
  12. 4 1
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Models/ServerDevice.cs
  13. 106 75
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Services/IndexService.cs
  14. 1 1
      TEAMModelOS.Extension/IES.Exam/IES.ExamServer/appsettings.json
  15. 2 1
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/api/http.js
  16. 8 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/api/index.js
  17. 92 10
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/login/Admin.vue
  18. 8 0
      TEAMModelOS.Extension/IES.Exam/IES.ExamViews/vue.config.js
  19. 1 0
      TEAMModelOS.SDK/Models/Cosmos/Teacher/JointEvent.cs
  20. 2 1
      TEAMModelOS/ClientApp/src/common/BaseQuickPaper.vue
  21. 8 6
      TEAMModelOS/ClientApp/src/components/coursemgt/StudentList.vue
  22. 24 28
      TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/content/index.vue
  23. 0 24
      TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/bank/ExerciseList.vue
  24. 0 2
      TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/bank/PaperDownload.vue
  25. 0 24
      TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/bank/TestPaperList.vue
  26. 0 205
      TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/bank/index.vue
  27. 1 1
      TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/components/BaseChild.vue
  28. 1 3
      TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/components/BaseExerciseList.vue
  29. 2 2
      TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/components/BaseImport.vue
  30. 0 78
      TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/components/BasePasteTool.vue
  31. 0 1
      TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/index/CreateExercises.vue
  32. 0 22
      TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/index/CreatePaper.vue
  33. 0 138
      TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/index/DfPage.less
  34. 0 1602
      TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/index/DfPage.vue
  35. 0 3
      TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/index/TestPaper.vue
  36. 0 3
      TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/index/htTestPaper.vue
  37. 1 16
      TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/index/index.vue
  38. 103 2347
      TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/syllabus/index.vue
  39. 10 10
      TEAMModelOS/ClientApp/src/utils/callDesktopAppMethods.js
  40. 0 1
      TEAMModelOS/ClientApp/src/view/hiTeachSideMenu/index.vue
  41. 55 0
      TEAMModelOS/Controllers/HiTeachSideMenu/Private/ContentController.cs
  42. 54 0
      TEAMModelOS/Controllers/HiTeachSideMenu/School/ContentController.cs
  43. 3 1
      TEAMModelOS/Controllers/Teacher/JointEventController.cs
  44. 14 0
      TEAMModelOS/Models/Request/HiTeachSideMenu/UploadPrivateContentRequest.cs
  45. 21 0
      TEAMModelOS/Models/Request/HiTeachSideMenu/UploadSchoolContentRequest.cs
  46. 113 0
      TEAMModelOS/Properties/ServiceDependencies/teammodelos-rc - Web Deploy/profile.arm.json
  47. 160 0
      TEAMModelOS/Services/ContentService.cs
  48. 137 0
      TEAMModelOS/Services/FileExtractService.cs
  49. 1 2
      TEAMModelOS/TEAMModelOS.csproj

+ 6 - 2
TEAMModelBI/Controllers/BIProductAnalysis/ProductAnalysisController.cs

@@ -77,6 +77,7 @@ namespace TEAMModelBI.Controllers.ProductAnalysis
                 string geoUnit = (jsonElement.TryGetProperty("geoUnit", out JsonElement geoUnitJobj)) ? (!string.IsNullOrWhiteSpace(Convert.ToString(geoUnitJobj))) ? Convert.ToString(geoUnitJobj).ToLower() : "city" : "city"; //地理統計單位 region:國 province:省 city:市 dist:區 預設值:市
                 Geo geo = (jsonElement.TryGetProperty("geo", out JsonElement geoJobj)) ? geoJobj.ToObject<Geo>() : null;
                 string target = (jsonElement.TryGetProperty("target", out JsonElement targetJobj)) ? (!string.IsNullOrWhiteSpace(Convert.ToString(targetJobj))) ? Convert.ToString(targetJobj).ToLower() : "school" : "school"; //統計對象 school:學校 tmid:TMID
+                string extra = (jsonElement.TryGetProperty("extra", out JsonElement extraJobj)) ? (!string.IsNullOrWhiteSpace(Convert.ToString(extraJobj))) ? Convert.ToString(extraJobj).ToLower() : string.Empty : string.Empty; //額外參數 rmvNoAll:不取 noschoolid、allschool
                 List<string> schoolIds = new List<string>();
                 if (target.Equals("school"))
                 {
@@ -330,8 +331,11 @@ namespace TEAMModelBI.Controllers.ProductAnalysis
                     Dictionary<string, ProdAnalysisApiResult> geoDic = new Dictionary<string, ProdAnalysisApiResult>(); //各地理資訊統計資料
                     Dictionary<string, List<string>> geoSchDic = new Dictionary<string, List<string>>(); //地理ID->學校ID 字典
                     string Sql = $"SELECT * FROM c WHERE c.toolType = '{prod}' AND c.dateUnit = '{dateUnit}' AND c.dateTime >= {dateTimeFromSec} AND c.dateTime <= {dateTimeToSec}";
-                    schoolIds.Add("noschoolid");
-                    schoolIds.Add("allschool");
+                    if(!extra.Equals("rmvnoall"))
+                    {
+                        schoolIds.Add("noschoolid");
+                        schoolIds.Add("allschool");
+                    }
                     schIdListStr = JsonSerializer.Serialize(schoolIds);
                     Sql += $" AND ARRAY_CONTAINS({schIdListStr}, c.schoolId, true)";
                     await foreach (var item in cosmosClient.GetContainer("TEAMModelOS", "School").GetItemQueryStreamIteratorSql(queryText: Sql, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"ProdAnalysis") }))

+ 165 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/app.js

@@ -0,0 +1,165 @@
+
+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');
+const { exec } = require('child_process');
+ 
+//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);
+        console.log("运行地址:", networks[0].ip)
+        try {
+            let filePath = path.join(constants.serverPath,'certificate.bat');
+            filePath = `"${filePath}"`;
+            // 使用 execFile 执行 .bat 文件
+     
+            // 执行 .bat 文件
+
+            // 设置超时时间为 60 秒
+            exec(filePath,  (error, stdout, stderr) => {
+                if (error) {
+                    if (error.code === 'ETIMEDOUT') {
+                        console.error('执行 .bat 文件超时');
+                    } else {
+                        console.error(`执行 .bat 文件时出错: ${error.message}`);
+                    }
+                    return;
+                }
+                if (stderr) {
+                    console.error(`.bat 文件执行过程中出现错误: ${stderr}`);
+                    return;
+                }
+                console.log(`.bat 文件执行结果: ${stdout}`);
+            });
+        } catch (error)
+        {
+            console.log(`脚本启动执行错误: ${error}`);
+        }
+
+
+        console.log("开始检查是否启动服务...")
+        const isServerRunning = await serverManager.checkServerHealth();
+        if (!isServerRunning) {
+            console.log('Server is not running, starting it...');
+            await serverManager.startServer(); // 启动 Web API
+        }
+        win = new BrowserWindow({
+            width: 800,
+            height: 600,
+            webPreferences: {
+                nodeIntegration: true,
+                contextIsolation: false,
+            },
+        });
+        //win.webContents.session.setCertificateVerifyProc((request, callback) => {
+        //    // 始终返回 0 表示验证通过
+        //    callback(0);
+        //});///login/admin
+        win.maximize();
+        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: () => {
+                require('electron').app.quit();
+                //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, () => {
+        menuManager.createMenu(checkForUpdatesHandler); // 重新创建菜单并传递回调函数
+    });
+};
+// 当 Electron 应用准备好时创建窗口
+app.whenReady().then(() => {
+    process.env.NODE_OPTIONS = '--tls-min-v1.2';
+    createWindow();
+    createTray();
+    // 创建菜单并传递回调函数
+    menuManager.createMenu(checkForUpdatesHandler);
+
+    app.on('activate', () => {
+        if (BrowserWindow.getAllWindows().length === 0) {
+            createWindow();
+        }
+    });
+
+    // 监听 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(); // 触发退出流程
+    });
+});
+
+// 当所有窗口关闭时退出应用(macOS 除外)
+app.on('window-all-closed', function () {
+    if (process.platform !== 'darwin') {
+        app.quit();
+    }
+});

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

@@ -16,12 +16,13 @@ if (app.isPackaged) {
 } else {
     serverPath = __dirname;
 }
-
-const baseUrl = 'https://exam.habook.local:8888';
+const port = 8326;
+const baseUrl = 'https://exam.habook.local:8326';//端口含义:TEAM  键盘九宫格  T(8) E(3) A(2) M(6)
 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';
 
 module.exports = {
+    port,
     serverPath,
     baseUrl,
     remoteVersionsUrl,

Файловите разлики са ограничени, защото са твърде много
+ 106 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/index.html


+ 203 - 73
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/main.js

@@ -7,53 +7,24 @@ const updateManager = require('./updateManager');
 const constants = require('./constants');
 const { getNetworkInterfaces } = require('./networkService');
 const { exec } = require('child_process');
- 
-//app.disableHardwareAcceleration(); //使用windows7 ,或者虚拟机的时候 需要验证 禁用 GPU 加速
-//app.commandLine.appendSwitch('ignore-certificate-errors')
+const net = require('net');
+const os = require('os');
+const utils = require('./utils');
+
 let win = null;
 let tray = null;
 app.isQuitting = false; // 添加标志位
+
+// 根据操作系统选择命令
+const IS_WINDOWS = os.platform() === 'win32';
+const FIND_PORT_COMMAND = IS_WINDOWS ? `netstat -ano | findstr :${constants.port}` : `lsof -i :${constants.port} -t`;
+const KILL_PROCESS_COMMAND = IS_WINDOWS  ? `taskkill /PID {PID} /F`  : `kill -9 {PID}`;
+
+
 // 创建 Electron 窗口的函数
 const createWindow = async () => {
     try {
-        const networks = getNetworkInterfaces();
-      //  console.log('Available networks:', networks);
-        console.log("运行地址:", networks[0].ip)
-        try {
-            let filePath = path.join(constants.serverPath,'certificate.bat');
-            filePath = `"${filePath}"`;
-            // 使用 execFile 执行 .bat 文件
-     
-            // 执行 .bat 文件
-
-            // 设置超时时间为 60 秒
-            exec(filePath,  (error, stdout, stderr) => {
-                if (error) {
-                    if (error.code === 'ETIMEDOUT') {
-                        console.error('执行 .bat 文件超时');
-                    } else {
-                        console.error(`执行 .bat 文件时出错: ${error.message}`);
-                    }
-                    return;
-                }
-                if (stderr) {
-                    console.error(`.bat 文件执行过程中出现错误: ${stderr}`);
-                    return;
-                }
-                console.log(`.bat 文件执行结果: ${stdout}`);
-            });
-        } catch (error)
-        {
-            console.log(`脚本启动执行错误: ${error}`);
-        }
 
-
-        console.log("开始检查是否启动服务...")
-        const isServerRunning = await serverManager.checkServerHealth();
-        if (!isServerRunning) {
-            console.log('Server is not running, starting it...');
-            await serverManager.startServer(); // 启动 Web API
-        }
         win = new BrowserWindow({
             width: 800,
             height: 600,
@@ -62,14 +33,19 @@ const createWindow = async () => {
                 contextIsolation: false,
             },
         });
+        win.webContents.openDevTools(); // 打开开发者工具
         //win.webContents.session.setCertificateVerifyProc((request, callback) => {
         //    // 始终返回 0 表示验证通过
         //    callback(0);
         //});///login/admin
         win.maximize();
-        win.loadURL(`${constants.baseUrl}/login/admin`, {
-            agent: constants.agent
-        });
+        win.loadFile('index.html');
+
+        // 模拟执行业务过程
+        StartProcess();
+        //win.loadURL(`${constants.baseUrl}/login/admin`, {
+        //    agent: constants.agent
+        //});
         // 监听窗口关闭事件,隐藏窗口而不是关闭
         win.on('close', (event) => {
             if (!app.isQuitting) {
@@ -77,13 +53,128 @@ const createWindow = async () => {
                 win.hide();
             }
         });
-
     } catch (error) {
         console.error('Error starting server or loading window:', error);
     }
 };
 
 
+
+// 当 Electron 应用准备好时创建窗口
+app.whenReady().then(() => {
+    process.env.NODE_OPTIONS = '--tls-min-v1.2';
+    createWindow();
+    createTray();
+    // 创建菜单并传递回调函数
+    menuManager.createMenu(checkForUpdatesHandler);
+
+    app.on('activate', () => {
+        if (BrowserWindow.getAllWindows().length === 0) {
+            createWindow();
+        }
+    });
+
+    // 监听 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(); // 触发退出流程
+    });
+});
+
+// 当所有窗口关闭时退出应用(macOS 除外)
+app.on('window-all-closed', function () {
+    if (process.platform !== 'darwin') {
+        app.quit();
+    }
+});
+
+// 模拟执行业务过程
+async function StartProcess() {
+    //步骤1 开始检查
+    sendLogMessage('检查评测服务是否启动...');
+    //步骤2 检查端口是否占用。
+    let serverStarted = false;
+    try {
+        const inUse = await isPortInUse(constants.port);
+        if (inUse) {
+            serverStarted = true;
+            sendLogMessage(`检查到端口被占用...`);
+
+        } else {
+            sendLogMessage(`评测服务未启动...`);
+        }
+    } catch (err) {
+        sendLogMessage(`检查端口时出错...`);
+      //  console.log(`检查评测服务状态出错...`, err);
+    }
+    //步骤3,尝试优雅关闭,如果不能关闭再通过 关闭进程号关闭端口
+    let needStart = false;
+    if (serverStarted) {
+        //检测是否是.net6的服务在线
+        sendLogMessage('检查是否是评测服务占用端口...');
+        const isServerRunning = await serverManager.checkServerHealth();
+        if (!isServerRunning) {
+            //可能是其他程序占用
+            sendLogMessage('检测到其他程序占用端口...');
+            sendLogMessage('正在查找占用端口的进程...');
+            const pid = await findProcessUsingPort();
+            sendLogMessage(`找到占用端口的进程: ${pid}`);
+            sendLogMessage('正在关闭进程...');
+            const killResult = await killProcess(pid);
+            sendLogMessage(`进程关闭结果:${killResult}`);
+            utils.delay(500);
+            needStart = true;
+        } else {
+            sendLogMessage('评测服务正在运行中...');
+            sendLogMessage('如需强制退出,请点击<设置><退出>按钮...');
+        }
+    } else {
+        needStart = true;
+    }
+    if (needStart) {
+        sendLogMessage('正在启动评测服务...');
+        await serverManager.startServer();
+        const isServerRunning = await serverManager.checkServerHealth();
+        if (isServerRunning) {
+            sendLogMessage('本地IP域名映射配置成功...');
+            sendLogMessage('SSL安全证书安装成功...');
+            sendLogMessage('评测服务启动成功...');
+            sendLogMessage('正在加载登录页面...');
+            utils.delay(2000)
+            win.loadURL(`${constants.baseUrl}/login/admin`, {
+                agent: constants.agent
+            });
+
+        }
+        else {
+            sendLogMessage('评测服务启动失败...');
+            sendLogMessage('请检查hosts文件是否自动映射了exam.habook.local域名...');
+            sendLogMessage("如果没有请手动执行<teacher_manual.bat>脚本文件...");
+        }
+    } else
+    {
+        sendLogMessage('正在加载登录页面...');
+        utils.delay(2000)
+        win.loadURL(`${constants.baseUrl}/login/admin`, {
+            agent: constants.agent
+        });
+    }
+  
+}
+
+
+// 发送消息到渲染进程
+function sendLogMessage(message) {
+    if (win) {
+        win.webContents.send('log-message', message);
+    }
+}
+
 const createTray = () => {
     const iconPath = path.join(__dirname, 'logo.ico'); // 你的托盘图标路径
     tray = new Tray(iconPath);
@@ -122,44 +213,83 @@ const createTray = () => {
     });
 };
 
-
-
-
 // 定义回调函数
 const checkForUpdatesHandler = () => {
     updateManager.checkForUpdates(win, () => {
         menuManager.createMenu(checkForUpdatesHandler); // 重新创建菜单并传递回调函数
     });
 };
-// 当 Electron 应用准备好时创建窗口
-app.whenReady().then(() => {
-    process.env.NODE_OPTIONS = '--tls-min-v1.2';
-    createWindow();
-    createTray();
-    // 创建菜单并传递回调函数
-    menuManager.createMenu(checkForUpdatesHandler);
+function isPortInUse(port) {
+    return new Promise((resolve, reject) => {
+        const server = net.createServer()
+            .once('error', (err) => {
+                if (err.code === 'EADDRINUSE') {
+                    resolve(true); // 端口被占用
+                } else {
+                    reject(err);
+                }
+            })
+            .once('listening', () => {
+                server.close();
+                resolve(false); // 端口未被占用
+            })
+            .listen(port);
+    });
+}
 
-    app.on('activate', () => {
-        if (BrowserWindow.getAllWindows().length === 0) {
-            createWindow();
-        }
+// 查找占用端口的进程
+function findProcessUsingPort() {
+    return new Promise((resolve, reject) => {
+        exec(FIND_PORT_COMMAND, (error, stdout, stderr) => {
+            if (error) {
+                if (error.code === 1) {
+                    // 未找到占用端口的进程
+                    resolve(null);
+                } else {
+                    reject(`查找端口占用失败: ${stderr}`);
+                }
+                return;
+            }
+            // 提取 PID
+            const pid = IS_WINDOWS
+                ? stdout.trim().split(/\s+/).pop() // Windows: 取最后一列
+                : stdout.trim(); // macOS/Linux: 直接输出 PID
+            resolve(pid);
+        });
     });
+}
 
-    // 监听 before-quit 事件,关闭 IES.ExamServer.exe 进程
-    app.on('before-quit', async (event) => {
-        if (app.isQuitting) {
-            return; // 如果已经在退出流程中,则直接返回
+// 关闭进程
+function killProcess(pid) {
+    return new Promise((resolve, reject) => {
+        if (!pid) {
+            resolve('未找到占用端口的进程');
+            return;
         }
-        app.isQuitting = true; // 标记正在退出
-        event.preventDefault(); // 阻止默认的退出行为
-        await serverManager.shutdownServer(); // 关闭服务器
-        app.quit(); // 触发退出流程
+        const command = KILL_PROCESS_COMMAND.replace('{PID}', pid);
+        exec(command, (error, stdout, stderr) => {
+            if (error) {
+                reject(`关闭进程失败: ${stderr}`);
+                return;
+            }
+            resolve(`已关闭进程: ${pid}`);
+        });
     });
-});
+}
+
+//// 启动 .NET Core 应用程序
+//function startDotNetApp() {
+//    return new Promise((resolve, reject) => {
+//        const command = `dotnet run --urls=http://localhost:${PORT}`;
+//        const options = { cwd: DOTNET_PROJECT_PATH }; // 设置工作目录为 .NET 项目路径
+//        exec(command, options, (error, stdout, stderr) => {
+//            if (error) {
+//                reject(`启动 .NET Core 应用程序失败: ${stderr}`);
+//                return;
+//            }
+//            resolve(`.NET Core 应用程序已启动: ${stdout}`);
+//        });
+//    });
+//}
+
 
-// 当所有窗口关闭时退出应用(macOS 除外)
-app.on('window-all-closed', function () {
-    if (process.platform !== 'darwin') {
-        app.quit();
-    }
-});

+ 3 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/package.json

@@ -49,6 +49,9 @@
           "**/*"
         ]
       }
+    ],
+    "extraResources": [
+      "index.html"
     ]
   },
   "dependencies": {

+ 9 - 3
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/serverManager.js

@@ -41,7 +41,8 @@ const startServer = () => {
             const checkHealth = async () => {
                 try {
                     const response = await axios.get(`${constants.baseUrl}/index/health`, {
-                        httpsAgent: constants.agent
+                        httpsAgent: constants.agent,
+                        timeout: 5000 // 设置超时时间为 5 秒
                     });
                     if (response.status === 200) {
                         console.log('Server is up and running!');
@@ -49,7 +50,7 @@ const startServer = () => {
                     }
                 } catch (error) {
                     console.log('Waiting for server to start...', error);
-                    setTimeout(checkHealth, 1000); // 每隔 1 秒检查一次
+                    setTimeout(checkHealth, 10000); // 每隔 10 秒检查一次
                 }
             };
             checkHealth();
@@ -84,8 +85,13 @@ const shutdownServer = async () => {
     try {
         console.log('index/shutdown api ...');
         const response = await axios.get(`${constants.baseUrl}/index/shutdown?delay=0`, {
-            httpsAgent: constants.agent
+            httpsAgent: constants.agent,
+            validateStatus: (status) => status === 200 || status === 404, // 允许 404 状态码
+            timeout: 5000 // 设置超时时间为 5 秒
         });
+        if (response.status === 404) {
+            console.error('API: index/shutdown not found.');
+        }
         if (response.status === 200) {
             console.log('Server is shutdown!');
         }

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

@@ -139,7 +139,7 @@ const updateServer = async (latestVersion, win, createMenuCallback) => {
         try {
             // 2. 关闭 IES.ExamServer.exe
             console.log('Shutting down IES.ExamServer...');
-            const response = await axios.get(`${constants.baseUrl}/index/shutdown`, {
+            const response = await axios.get(`${constants.baseUrl}/index/shutdown?delay=0`, {
                 httpsAgent: constants.agent,
                 validateStatus: (status) => status === 200 || status === 404, // 允许 404 状态码
                 timeout: 5000 // 设置超时时间为 5 秒

+ 6 - 62
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/IndexController.cs

@@ -380,66 +380,8 @@ namespace IES.ExamServer.Controllers
         {
             try
             {
-               
-                string pathCerNew = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "certificate.cer");
-                string pathBatNew = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "install_certificate.bat");
-                if (!System.IO.File.Exists(pathCerNew)|| !System.IO.File.Exists(pathBatNew))
-                {
-                    string pathCer = Path.Combine(Directory.GetCurrentDirectory(), "Configs", "cer", "certificate.cer");
-                    System.IO.File.Copy(pathCer, pathCerNew);
-                    string pathBat = Path.Combine(Directory.GetCurrentDirectory(), "Configs", "cer", "install_certificate.bat");
-                    System.IO.File.Copy(pathBat, pathBatNew);
-                    var res = ProcessHelper.ExecuteProcess(pathBatNew);
-                }
-
-                ServerDevice serverDevice = _memoryCache.Get<ServerDevice>(Constant._KeyServerDevice);
-                if (serverDevice != null && serverDevice.networks.IsNotEmpty())
-                {
-                    Network? network = serverDevice.networks.FirstOrDefault();
-                    if (!string.IsNullOrWhiteSpace(ip))
-                    {
-                        network = serverDevice.networks.FindAll(x => ip.Equals(x.ip))?.FirstOrDefault(); 
-                    }
-                   
-                    if (network != null && !string.IsNullOrWhiteSpace(network.ip))
-                    {
-                        network.primary = 1;
-                        _memoryCache.Set<ServerDevice>(Constant._KeyServerDevice,serverDevice);
-                        string pathBatHosts = Path.Combine(Directory.GetCurrentDirectory(), "Configs", "cer", "modify_hosts.bat");
-                        string text = await System.IO.File.ReadAllTextAsync(pathBatHosts);
-                        // 使用正则表达式替换 IP 地址
-                        string pattern = @"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b";
-                        string result = Regex.Replace(text, pattern, network.ip);
-                        string pathBatHostsNew = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot",  "modify_hosts.bat");
-                        await System.IO.File.WriteAllTextAsync(pathBatHostsNew, result);
-                        string pathBatStudent = Path.Combine(Directory.GetCurrentDirectory(), "Configs", "cer", "student_manual.bat");
-                        string textStudent = await System.IO.File.ReadAllTextAsync(pathBatStudent);
-                        // 使用正则表达式替换 IP 地址
-                        string resultStudent = Regex.Replace(textStudent, pattern, network.ip);
-                        string pathBatStudentNew = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "student_manual.bat");
-                        await System.IO.File.WriteAllTextAsync(pathBatStudentNew, resultStudent);
-                        var resHosts = ProcessHelper.ExecuteProcess(pathBatHostsNew);
-                        return Ok(new {
-                            code = 200,
-                            msg = "成功",
-                            serverDevice,
-                            cer = "certificate.cer",
-                            install_certificate = "install_certificate.bat",
-                            modify_hosts= "modify_hosts.bat",
-                            student_manual= "student_manual.bat"
-                        });
-                    }
-                    else
-                    {
-                        code = 400;
-                        msg = "未找到匹配的IP。";
-                    }
-                }
-                else
-                {
-                    code = 400;
-                    msg = "服务端设备未找到,或网卡设备不存在。";
-                }
+                var data = await IndexService. ModifyHosts(ip,_memoryCache,_liteDBFactory,_connectionService);
+                return Ok(new { data.code,data.code_zip,data.code_cer,data.code_hosts,data.msg});
             }
             catch (Exception ex)
             {
@@ -449,7 +391,10 @@ namespace IES.ExamServer.Controllers
             }
             return Ok(new { code = 400, msg = msg });
         }
-
+        /// <summary>
+        /// 强制重新安装证书
+        /// </summary>
+        /// <returns></returns>
         [HttpGet("install-certificate")]
         public IActionResult InstallCertificate()
         {
@@ -462,7 +407,6 @@ namespace IES.ExamServer.Controllers
                 string pathBat = Path.Combine(Directory.GetCurrentDirectory(), "Configs", "cer", "install_certificate.bat");
                 System.IO.File.Copy(pathBat, pathBatNew);
                 var res = ProcessHelper.ExecuteProcess(pathBatNew);
-
                 return Ok(new {code= res.code, msg= res.msg });
             }
             catch (Exception ex)

+ 1 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/DI/ServiceInitializer.cs

@@ -131,6 +131,7 @@ namespace IES.ExamServer.DI
             _cache.Set(Constant._KeyServerDevice, serverDevice);
             _liteDBFactory.GetLiteDatabase().GetCollection<ServerDevice>().Upsert(serverDevice);
             _connectionService.serverDevice = serverDevice;
+            await  IndexService.ModifyHosts(null, _cache, _liteDBFactory, _connectionService);
             _lifetime.ApplicationStarted.Register(() =>
             {
                var serverDevice=  _cache.Get<ServerDevice>(Constant._KeyServerDevice);

+ 152 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Helpers/SystemScriptHelper.cs

@@ -0,0 +1,152 @@
+using System.Runtime.InteropServices;
+using System.Security.Cryptography.X509Certificates;
+using System.Security.Principal;
+
+namespace IES.ExamServer.Helpers
+{
+    public static class SystemScriptHelper
+    {
+        /// <summary>
+        /// 检查是否管理员身份运行
+        /// </summary>
+        /// <returns></returns>
+        public static bool IsAdministrator()
+        {
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                // 获取当前用户的 Windows 身份
+                WindowsIdentity identity = WindowsIdentity.GetCurrent();
+                // 创建一个 WindowsPrincipal 对象,用于表示当前用户的主体
+                WindowsPrincipal principal = new WindowsPrincipal(identity);
+                // 检查当前用户是否属于管理员组
+                return principal.IsInRole(WindowsBuiltInRole.Administrator);
+            }
+            return false;
+        }
+        /// <summary>
+        /// 根据域名在hosts文件中找到对于的ip地址。
+        /// </summary>
+        /// <param name="domain"></param>
+        /// <returns></returns>
+        public static (string? ip, string msg) FindIpAddressForDomain(string domain)
+        {
+            string? lastMatchingIp = null;
+            try
+            {
+                string filePath = @"C:\Windows\System32\drivers\etc\hosts";
+                string[] lines = File.ReadAllLines(filePath);
+                foreach (string line in lines)
+                {
+                    string trimmedLine = line.Trim();
+                    if (string.IsNullOrEmpty(trimmedLine) || trimmedLine.StartsWith("#"))
+                    {
+                        continue;
+                    }
+                    string[] parts = trimmedLine.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
+                    if (parts.Length >= 2)
+                    {
+                        string ip = parts[0];
+                        for (int i = 1; i < parts.Length; i++)
+                        {
+                            if (parts[i].Equals(domain, StringComparison.OrdinalIgnoreCase))
+                            {
+                                lastMatchingIp = ip;
+                            }
+                        }
+                    }
+                }
+            }
+            catch (Exception ex)
+            {
+                return (null, $"读取文件时出错: {ex.Message}");
+            }
+            return (lastMatchingIp, "匹配结果");
+        }
+        /// <summary>
+        /// 检查证书是否安装,切是否过期,true 已经安装,false 未安装,用于检查证书是否需要重新安装,最终返回 true不用安装。
+        /// 代码中使用的是 CurrentUser 存储位置,如果你需要检查计算机级别的证书存储区,可以将 StoreLocation.CurrentUser 替换为 StoreLocation.LocalMachine,但这可能需要管理员权限。
+        /// </summary>
+        /// <param name="certificate"></param>
+        /// <returns></returns>
+        public static bool CheckCertificate(string certificatePath)
+        {
+            bool installed = false, expired=false;
+            X509Certificate2 certificate = new X509Certificate2(certificatePath);
+            // 定义要检查的证书存储区
+            StoreName[] storeNames = { StoreName.Root, StoreName.CertificateAuthority, StoreName.My };
+            foreach (StoreName storeName in storeNames)
+            {
+                if (IsAdministrator())
+                {
+                    using (X509Store store = new X509Store(storeName, StoreLocation.LocalMachine))
+                    {
+                        try
+                        {
+                            // 打开存储区
+                            store.Open(OpenFlags.ReadOnly);
+                            // 查找匹配的证书
+                            X509Certificate2Collection collection = store.Certificates.Find(X509FindType.FindByThumbprint, certificate.Thumbprint, false);
+                            if (collection.Count > 0)
+                            {
+                                installed = true;
+                                var  certificateInstalled = collection.First();
+                                expired  = CheckCertificateExpired(certificateInstalled);
+                                break;
+                            }
+                        }
+                        catch (Exception ex)
+                        {
+                            Console.WriteLine($"访问 {storeName} 存储区时出错: {ex.Message}");
+                        }
+                        finally
+                        {
+                            // 关闭存储区
+                            store.Close();
+                        }
+                    }
+                }
+                else {
+                    using (X509Store store = new X509Store(storeName, StoreLocation.CurrentUser))
+                    {
+                        try
+                        {
+                            // 打开存储区
+                            store.Open(OpenFlags.ReadOnly);
+                            // 查找匹配的证书
+                            X509Certificate2Collection collection = store.Certificates.Find(X509FindType.FindByThumbprint, certificate.Thumbprint, false);
+                            if (collection.Count > 0)
+                            {
+                                installed = true;
+                                var certificateInstalled = collection.First();
+                                expired = CheckCertificateExpired(certificateInstalled);
+                                break;
+                            }
+                        }
+                        catch (Exception ex)
+                        {
+                            Console.WriteLine($"访问 {storeName} 存储区时出错: {ex.Message}");
+                        }
+                        finally
+                        {
+                            // 关闭存储区
+                            store.Close();
+                        }
+                    }
+                }
+               
+            }
+            return installed && !expired;
+        }
+
+        /// <summary>
+        /// 检查证书是否过期,true  过期,false 未过期
+        /// </summary>
+        /// <param name="certificate"></param>
+        /// <returns></returns>
+        public static bool CheckCertificateExpired(X509Certificate2 certificate)
+        {
+            DateTime now = DateTime.Now;
+            return now < certificate.NotBefore || now > certificate.NotAfter;
+        }
+    }
+}

+ 4 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Models/ServerDevice.cs

@@ -91,12 +91,15 @@ namespace IES.ExamServer.Models
         /// <summary>
         /// 绑定域名
         /// </summary>
-        public string? domain { get; set; }
+        //public string? domain { get; set; }
         public int physical {  get; set; }
         /// <summary>
         /// 当前主站域名
         /// </summary>
         public int primary { get; set; }
+        /// <summary>
+        /// 脚本压缩文件
+        /// </summary>
         public string? batscriptZip { get; set; }
     }
 }

+ 106 - 75
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Services/IndexService.cs

@@ -11,19 +11,30 @@ using System.Text;
 using IES.ExamServer.DI;
 using IES.ExamServer.Helper;
 using System;
+using System.Security.Principal;
 
 namespace IES.ExamServer.Services
 {
     public static class IndexService
     {
-        public static async Task<(int code, string msg)> ModifyHosts(string ip,IMemoryCache _memoryCache,LiteDBFactory _liteDBFactory)
+
+        /// <summary>
+        /// 修改IP域名映射,以及处理证书是否安装的问题。
+        /// </summary>
+        /// <param name="ip"></param>
+        /// <param name="_memoryCache"></param>
+        /// <param name="_liteDBFactory"></param>
+        /// <param name="connectionService"></param>
+        /// <returns></returns>
+        public static async Task<(int code, int code_cer ,int code_hosts,int code_zip, string msg)> ModifyHosts(string? ip,IMemoryCache _memoryCache,LiteDBFactory _liteDBFactory,CenterServiceConnectionService connectionService)
         {
-            int code = 0;
-            string msg = string.Empty;
+           (string? hostsIp,string hostsMsg) = SystemScriptHelper.FindIpAddressForDomain("exam.habook.local");
+            int code = 0, code_cer = 0,code_hosts=0,code_zip=0 ;
+            StringBuilder sb = new StringBuilder();
             try
             {
                 string batscriptPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "batscript");
-                if (!Directory.Exists(batscriptPath)) 
+                if (!Directory.Exists(batscriptPath))
                 {
                     Directory.CreateDirectory(batscriptPath);
                 }
@@ -35,18 +46,36 @@ namespace IES.ExamServer.Services
                     System.IO.File.Copy(pathCer, pathCerNew);
                     string pathBat = Path.Combine(Directory.GetCurrentDirectory(), "Configs", "cer", "install_certificate.bat");
                     System.IO.File.Copy(pathBat, pathBatNew);
-                    var res = ProcessHelper.ExecuteProcess(pathBatNew);
+                }
+                var needInstall = SystemScriptHelper.CheckCertificate(pathCerNew);
+                if (!needInstall)
+                {
+                    if (SystemScriptHelper.IsAdministrator())
+                    {
+                        var res = ProcessHelper.ExecuteProcess(pathBatNew);
+                        sb.Append(res.msg);
+                        code_cer = res.code;
+                    }
+                    else
+                    {
+                        code_cer = 401;
+                        sb.Append("请使用管理员身份运行本程序,如果已经安装过脚本请忽略!");
+                    }
+                }
+                else
+                {
+                    code_cer = 200;
                 }
                 //获取主站配置信息。
                 ServerDevice serverDevice = _memoryCache.Get<ServerDevice>(Constant._KeyServerDevice);
                 var primaryNetworks=   _liteDBFactory.GetLiteDatabase().GetCollection<Network>().FindAll().ToList();
                 Network? primaryNetwork = null;
-                //传入的ip为不为空
+                //传入的ip为不为空,切换
                 if (!string.IsNullOrWhiteSpace(ip) )
                 {
                     if (serverDevice != null && serverDevice.networks.IsNotEmpty()) 
                     {
-                    
+                        primaryNetwork = serverDevice.networks.FindAll(x => ip.Equals(x.ip))?.FirstOrDefault();
                     }
                 }
                 else 
@@ -65,14 +94,6 @@ namespace IES.ExamServer.Services
                                     {
                                         primaryNetwork = network;
                                     }
-                                    else
-                                    {
-                                        _liteDBFactory.GetLiteDatabase().GetCollection<Network>().Delete(network.id);
-                                    }
-                                }
-                                else
-                                {
-                                    _liteDBFactory.GetLiteDatabase().GetCollection<Network>().Delete(network.id);
                                 }
                             }
                         }
@@ -82,79 +103,89 @@ namespace IES.ExamServer.Services
                             primaryNetwork = serverDevice.networks.FirstOrDefault();//第一个是物理网卡
                         }
                     }
-                   
                 }
-
-
-                if (serverDevice != null && serverDevice.networks.IsNotEmpty())
+                if (primaryNetwork != null)
                 {
-                    Network? network = serverDevice.networks.FirstOrDefault();
-                    if (!string.IsNullOrWhiteSpace(ip))
+                    string pathBatHosts = Path.Combine(Directory.GetCurrentDirectory(), "Configs", "cer", "modify_hosts.bat");
+                    string text = await System.IO.File.ReadAllTextAsync(pathBatHosts);
+                    // 使用正则表达式替换 IP 地址
+                    string pattern = @"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b";
+                    string result = Regex.Replace(text, pattern, primaryNetwork.ip!);
+                    string pathBatHostsNew = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "batscript", "modify_hosts.bat");
+                    await System.IO.File.WriteAllTextAsync(pathBatHostsNew, result);
+                    string pathBatStudent = Path.Combine(Directory.GetCurrentDirectory(), "Configs", "cer", "student_manual.bat");
+                    string textStudent = await System.IO.File.ReadAllTextAsync(pathBatStudent);
+                    // 使用正则表达式替换 IP 地址
+                    string resultStudent = Regex.Replace(textStudent, pattern, primaryNetwork.ip!);
+                    string pathBatStudentNew = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "batscript", "student_manual.bat");
+                    await System.IO.File.WriteAllTextAsync(pathBatStudentNew, resultStudent);
+                    if (string.IsNullOrWhiteSpace(hostsIp) || !hostsIp.Equals(primaryNetwork.ip))
+                    {
+                        if (SystemScriptHelper.IsAdministrator())
+                        {
+                            var resHosts = ProcessHelper.ExecuteProcess(pathBatHostsNew);
+                            sb.Append(resHosts.msg);
+                            code_hosts = resHosts.code;
+                        }
+                        else
+                        {
+                            code_hosts = 401;
+                            sb.Append("请使用管理员身份执行本程序!");
+                        }
+                    }
+                    else
                     {
-                        network = serverDevice.networks.FindAll(x => ip.Equals(x.ip))?.FirstOrDefault();
+                        code_hosts = 200;
+                        sb.Append("IP域名映射已存在,无需再次映射!");
                     }
+                    string scriptPath= Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "script");
+                    if (!Directory.Exists(scriptPath)) 
+                    {
+                        Directory.CreateDirectory(scriptPath);
+                    }
+                    string batscriptZipPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "script", "student_script.zip");
 
-                    if (network != null && !string.IsNullOrWhiteSpace(network.ip))
+                    var res = ZipHelper.CreateZip(batscriptPath, batscriptZipPath);
+                    if (res.res)
                     {
-                       
-                        string pathBatHosts = Path.Combine(Directory.GetCurrentDirectory(), "Configs", "cer", "modify_hosts.bat");
-                        string text = await System.IO.File.ReadAllTextAsync(pathBatHosts);
-                        // 使用正则表达式替换 IP 地址
-                        string pattern = @"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b";
-                        string result = Regex.Replace(text, pattern, network.ip);
-                        string pathBatHostsNew = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "batscript", "modify_hosts.bat");
-                        await System.IO.File.WriteAllTextAsync(pathBatHostsNew, result);
-                        string pathBatStudent = Path.Combine(Directory.GetCurrentDirectory(), "Configs", "cer", "student_manual.bat");
-                        string textStudent = await System.IO.File.ReadAllTextAsync(pathBatStudent);
-                        // 使用正则表达式替换 IP 地址
-                        string resultStudent = Regex.Replace(textStudent, pattern, network.ip);
-                        string pathBatStudentNew = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "batscript", "student_manual.bat");
-                        await System.IO.File.WriteAllTextAsync(pathBatStudentNew, resultStudent);
-                        var resHosts = ProcessHelper.ExecuteProcess(pathBatHostsNew);
-                        string batscriptZipPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "student_script.zip");
-                       
-                        var  res=    ZipHelper.CreateZip(batscriptPath, batscriptZipPath);
-                        if (res.res) 
-                        {
-                            serverDevice.networks.ForEach(x => {
-                                x.primary = 0;
-                                x.batscriptZip = null;
-                            });
-                            network.primary = 1;
-                            network.batscriptZip = batscriptZipPath;
-                            _memoryCache.Set<ServerDevice>(Constant._KeyServerDevice, serverDevice);
-                           // _liteDBFactory.GetLiteDatabase().GetCollection<ServerDevice>().Upsert(serverDevice);
-                        }
-                        //return Ok(new
-                        //{
-                        //    code = 200,
-                        //    msg = "成功",
-                        //    serverDevice,
-                        //    cer = "certificate.cer",
-                        //    install_certificate = "install_certificate.bat",
-                        //    modify_hosts = "modify_hosts.bat",
-                        //    student_manual = "student_manual.bat"
-                        //});
+                        code_zip = 200;
+                        sb.Append(res.msg);
+                        
                     }
                     else
                     {
-                        code = 400;
-                        msg = "未找到匹配的IP。";
+                        code_zip = 400;
+                        sb.Append("脚本文件创建异常!");
                     }
-                }
-                else
-                {
-                    code = 400;
-                    msg = "服务端设备未找到,或网卡设备不存在。";
+                    serverDevice!.networks.ForEach(x =>
+                    {
+                        x.primary = 0;
+                        x.batscriptZip = null;
+                        if (x.id!.Equals(primaryNetwork.id))
+                        {
+                            x.primary = code_hosts ==200? 1:0;
+                            x.batscriptZip = res.res? "script/student_script.zip" : null;
+                        }
+                    });
+                    //更新设备的主站设备信息
+                    connectionService.serverDevice = serverDevice;
+                    _memoryCache.Set<ServerDevice>(Constant._KeyServerDevice, serverDevice);
+                    _liteDBFactory.GetLiteDatabase().GetCollection<ServerDevice>().Upsert(serverDevice);
+                    //清理后再保存,保证只有一条主站数据。
+                    _liteDBFactory.GetLiteDatabase().GetCollection<Network>().DeleteAll();
+                    _liteDBFactory.GetLiteDatabase().GetCollection<Network>().Upsert(primaryNetwork);
+                    //所有执行成功
+                    code = 200;
+                    sb.Append("证书安装成功,域名IP绑定成功,脚本文件创建成功!");
                 }
             }
             catch (Exception ex)
             {
                 code = 500;
                 //_logger.LogError($"域名IP绑定错误。{ex.Message},{ex.StackTrace}");
-                msg = $"域名IP绑定错误,{ex.Message}";
+                return (500,code_cer,code_hosts,code_zip, $"域名IP绑定错误,{ex.Message},{ex.StackTrace}");
             }
-            return (code, msg);
+            return (code, code_cer, code_hosts, code_zip, sb.ToString());
         }
 
 
@@ -460,10 +491,10 @@ namespace IES.ExamServer.Services
             if (device.networks.IsNotEmpty()) 
             {
                 var order=  device.networks.OrderByDescending(x => x.physical).ToList();
-                for (int i=0; i<order.Count();i++) 
-                {
-                    order[i].domain="exam.habook.local";
-                }
+                //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);

+ 1 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/appsettings.json

@@ -17,7 +17,7 @@
   "Kestrel": {
     "Endpoints": {
       "Https1": {
-        "Url": "https://*:8888",
+        "Url": "https://*:8326",
         "Certificate": {
           "Path": "Configs/cer/cert.pem",
           "KeyPath": "Configs/cer/key.pem"

+ 2 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/api/http.js

@@ -11,6 +11,7 @@ const NO_ACCESS_API = [
     '/index/bind-school',
     '/student/get-activate-evaluation',
     '/student/login',
+    '/index/modify-hosts'
 ]
 
 // 需要携带access_token 不需要auth-token
@@ -202,7 +203,7 @@ function handleHeader(config) {
     if (!auth_token) {
         console.log('auth_token失败', config)
         loginOut()
-        sessionStorage.setItem('loginOut', 'localStorage没有auth_token:auth_token失败,重新登录')
+        sessionStorage.setItem('loginOut', `localStorage没有auth_token:auth_token失败,重新登录${identity}`)
         return
     }
 

+ 8 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/api/index.js

@@ -50,6 +50,14 @@ export default {
     stuLogin: function(data) {
         return post('/student/login', data)
     },
+    /**
+     * 
+     * @param {String} ip 
+     * @returns 
+     */
+    modifyHost: function(data) {
+        return fetch('/index/modify-hosts', data)
+    },
 
     // 管理页面
     /**

+ 92 - 10
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/src/view/login/Admin.vue

@@ -65,7 +65,7 @@
             <a class="footer-info-item">蜀ICP备18027363号</a>
             <span class="footer-info-item">© 2021 HABOOK Group 醍摩豆</span>
         </div>
-        <el-drawer title="服务端信息" :visible.sync="isDeviceDrawer">
+        <el-drawer title="服务端信息" :visible.sync="isDeviceDrawer" size="35%">
             <el-form ref="form" label-width="120px" v-if="deviceInfo" style="margin-right: 10px;">
                 <el-form-item label="登录用户:">
                     <span>{{ deviceInfo.server.userName }}</span>
@@ -73,6 +73,17 @@
                 <el-form-item label="设备名称:">
                     <span>{{ deviceInfo.server.name }}</span>
                 </el-form-item>
+                <el-form-item label="服务端地址:">
+                    <!-- 展示哪一个是主服务器,有下载学生端配置功能,切换主服务器后需重新下载 -->
+                    <span v-for="(item, index) in deviceInfo.server.host" :key="index" style="display: block;">
+                        {{ item.url }}
+                        <i class="el-icon-copy-document" @click="copyUrl(item.url)" style="cursor: pointer; margin-left: 5px;"></i>
+                        <span style="margin-left: 15px;">
+                            <el-button size="mini" v-show="item.primary" @click="uploadStu()">下载学生端配置</el-button>
+                            <i class="el-icon-qiehuan" @click="checkHost(item)" :title="`切换主站${item.physical ? ',建议设置为主站' : ''}`" v-show="!item.primary" :style="[{'cursor': 'pointer'}, {'font-size': '18px'}, {'color': item.physical ? '#f5912f' : ''}]"></i>
+                        </span>
+                    </span>
+                </el-form-item>
                 <el-form-item label="操作系统:">
                     <span>{{ deviceInfo.server.os }}</span>
                 </el-form-item>
@@ -97,12 +108,6 @@
                 <el-form-item label="数据中心:">
                     <span>{{ deviceInfo.centerUrl }}</span>
                 </el-form-item>
-                <el-form-item label="服务端地址:">
-                    <span v-for="(item, index) in deviceInfo.server.host" :key="index" style="display: block;">
-                        {{ item }}
-                        <i class="el-icon-copy-document" @click="copyUrl(item)" style="cursor: pointer; margin-left: 5px;"></i>
-                    </span>
-                </el-form-item>
             </el-form>
         </el-drawer>
         <el-dialog title="用户协议与隐私政策" width="30%" :visible.sync="isPrivacy">
@@ -162,7 +167,8 @@ export default {
             signalR: null,
             qrCodeToken: undefined,
             ttlTimeOut: undefined,
-            ttlChange: 0
+            ttlChange: 0,
+            stuUrl: '',
         }
     },
     mounted() {
@@ -181,7 +187,13 @@ export default {
                     res.data.server.networks.forEach(network => {
                         // https://192.168.8.140:5001/login/student
                         let url = `${item.protocol}://${network.ip}:${item.port}/login/student`
-                        res.data.server.host.push(url)
+                        if(network.primary) this.stuUrl = network?.batscriptZip
+                        res.data.server.host.push({
+                            url,
+                            primary: network.primary,
+                            physical: item.physical,
+                            ip: network.ip
+                        })
                     })
                 })
                 res.data.server.showRam = (res.data.server.ram / 1024 / 1024 / 1024).toFixed(1)
@@ -305,7 +317,12 @@ export default {
                         res.data.server.networks.forEach(network => {
                             // https://192.168.8.140:5001/login/student
                             let url = `${item.protocol}://${network.ip}:${item.port}/login/student`
-                            res.data.server.host.push(url)
+                            res.data.server.host.push({
+                                url,
+                                primary: network.primary,
+                                physical: item.physical,
+                                ip: network.ip
+                            })
                         })
                     })
                     res.data.server.showRam = (res.data.server.ram / 1024 / 1024 / 1024).toFixed(1)
@@ -350,6 +367,71 @@ export default {
                 }
             })
         },
+        checkHost(data) {
+            this.$api.modifyHost({ip: data.ip}).then(res => {
+                if(res.code === 200) {
+                    this.viewNetworkInfo()
+                    this.$message({
+                        message: '设置成功,请重新下载学生端配置信息',
+                        type: 'success'
+                    });
+                } else {
+                    this.$message({
+                        message: res.msg,
+                        type: 'warning'
+                    });
+                }
+            })
+        },
+        async uploadStu() {
+            let url = `/${this.stuUrl}`
+            try {
+                let data = await this.getFile(url)
+                let objectUrl = window.URL.createObjectURL(new Blob([data], {type: 'application/zip'}));
+                let a = document.createElement('a');
+                a.href = objectUrl;
+                a.download = '学生端配置信息.zip'
+                a.click()
+                a.remove();
+            } catch(e) {
+                this.$message({
+                    message: '下载失败',
+                    type: 'warning'
+                });
+            }
+        },
+        getFile(url) {
+            return new Promise((resolve, reject) => {
+                var xhr = new XMLHttpRequest();
+                var formData = new FormData();
+                xhr.open('get', url); //url填写后台的接口地址,如果是post,在formData append参数(参考原文地址)
+                xhr.responseType = 'blob';
+                xhr.headers = {
+                    'Content-Type': 'application/json; application/octet-stream'
+                }
+                xhr.onload = function (e) {
+                    switch (e.currentTarget.status) {
+                        case 200:
+                            resolve(e.currentTarget.response)
+                            break;
+                        case 404:
+                            // Message.error('有资源丢失')
+                            reject(404)
+                            break;
+                        case 403:
+                            /* app.$message({
+                                type: 'error',
+                                message: '授权过期或授权异常,请稍后重试!'
+                            }) */
+                            reject(403)
+                            break;
+                        default:
+                            break;
+                    }
+                };
+                xhr.send(formData);
+            })
+        },
     },
     computed: {
         bindSchoolType() {

+ 8 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamViews/vue.config.js

@@ -67,6 +67,14 @@ module.exports = defineConfig({
                     '^/package': '/package'
                 }
             },
+            '/script': {
+                target: 'https://localhost:6001', //后端接口
+                secure: false,
+                changeOrigin: false,
+                pathRewrite: {
+                    '^/script': '/script'
+                }
+            },
         },
         historyApiFallback: true,
     },

+ 1 - 0
TEAMModelOS.SDK/Models/Cosmos/Teacher/JointEvent.cs

@@ -95,6 +95,7 @@ namespace TEAMModelOS.SDK.Models
             public string id { get; set; }
             public string name { get; set; }
             public string no { get; set; } //系統安排的流水號 ※各組(jointGroup)的班級從1開始++ 決賽才記入
+            public int expStuNum { get; set; } //預估學生人數
             public List<JointEventGroupCourseGroupSchedule> schedule { get; set; } = new();
             public class JointEventGroupCourseGroupSchedule
             {

+ 2 - 1
TEAMModelOS/ClientApp/src/common/BaseQuickPaper.vue

@@ -468,6 +468,7 @@
 			/* 回显编辑试卷 */
 			async doRenderEditPaper() {
 				this.paperInfo.name = this.editPaper.name;
+				this.paperInfo.score = this.editPaper.score;
 				this.oldPaperName = this.editPaper.name;
 				this.isSecret = this.editPaper.secret === 1
 				let curScope = this.editPaper.scope;
@@ -1250,7 +1251,7 @@
 					let url = cvs.toDataURL('image/png')
 					if(this.editQuesIndex != -1) {
 						item.qIndex = this.editQuesIndex
-						this.orderItemsArr[this.editQuesIndex] = ({
+						this.orderItemsArr.splice(this.editQuesIndex, 1, {
 							type: item.type,
 							options: ["single", "multiple"].includes(item.type) ? 4 : 0,
 							answer: "",

+ 8 - 6
TEAMModelOS/ClientApp/src/components/coursemgt/StudentList.vue

@@ -289,17 +289,19 @@ export default {
                 if (data.results.length) {
                     this.excelFilter = data.results
                     excelResult = this._.cloneDeep(data.results)
-                    this.tableFilterData = stuData.filter(item => {
-                        let eIndex = excelResult.findIndex(results => results?.id && results?.id.toString() === item.id && results?.name === item.name)
-                        if(eIndex != -1) {
-                            excelResult.splice(eIndex, 1)
-                            return true
+                    this.tableFilterData = []
+                    this.notFoundlist = []
+                    excelResult.forEach(results => {
+                        let sData = stuData.find(item => results?.id && results?.id.toString() === item.id && results?.name === item.name)
+                        if(sData) {
+                            this.tableFilterData.push(sData)
+                        } else {
+                            this.notFoundlist.push(results)
                         }
                     })
                     this.pointNum = this.basicCount
                     this.tableShowData = this.tableFilterData.slice(0, this.basicCount)
                     
-                    this.notFoundlist = excelResult
                     this.$nextTick(() => {
                         this.$refs.stuSelection.selectAll(true);
                     });

+ 24 - 28
TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/content/index.vue

@@ -113,18 +113,6 @@
           </div>
 
           <div class="display: flex;">
-              <!--<span v-if="$access.can('admin.*|content-upd') || routerScope == 'private'" class="action-btn-wrap" @click="delFileBatch">
-                <Icon type="md-trash" class="toggle-btn-icon" />
-                {{$t('teachContent.delBatch')}}
-              </span>
-              <span v-if="$access.can('admin.*|content-upd') || routerScope == 'private'" @click="showUpload" class="action-btn-wrap">
-                <Icon type="md-cloud-upload" class="toggle-btn-icon" size="16" />
-                {{$t('teachContent.btnUpload')}}
-              </span> -->
-              <span @click="onWxtClick" class="action-btn-wrap" v-if="hasWxtAuth && !isGlobalSite">
-                <Icon type="md-cloud-upload" class="toggle-btn-icon" size="16" />
-                网校通
-              </span>
               <span @click="changeShowType()" :class="activeType === 'image' || activeType === 'video' ? 'action-btn-wrap' : 'disable-text-icon action-btn-wrap'">
                 <Icon v-show="showType" type="md-list" class="toggle-btn-icon" size="16" />
                 <Icon v-show="!showType" type="md-grid" class="toggle-btn-icon" />
@@ -319,8 +307,16 @@ export default {
   },
   methods: {
       clickCard(event, { value }) {
-          event.preventDefault();
-          callDesktopAppMethods.downloadResource(this.blobContainerName, value.blob);
+        event.preventDefault();
+        let url = value.url;
+        let sas;
+
+        if (value.extension === 'HTEX') {
+          url = url.replace('/index.json', '');
+        }
+        [url, sas] = url.split('?');
+        sas ??= '';
+        callDesktopAppMethods.downloadResource(url, sas);
       },
     onWxtClick() {
       this.$api.auth.xkwOauth({
@@ -795,12 +791,6 @@ export default {
         this.fileColumns.splice(2, 0, ...arr)
       }
       this.contentTypeList = [
-        //{
-        //  label: this.$t('teachContent.recent'),
-        //  type: 'recent',
-        //  icon: 'md-time',
-        //  tips: this.$t('teachContent.recentTips')
-        //},
         {
           label: this.$t('teachContent.filterRes'),
           type: 'res',
@@ -1129,7 +1119,16 @@ export default {
     },
 
     handleResource(row) {
-        callDesktopAppMethods.downloadResource(this.blobContainerName, row.blob);
+      let url = row.url;
+      let sas;
+      console.log(sas)
+
+      if (row.extension === 'HTEX') {
+        url = url.replace('/index.json', '');
+      }
+      [url, sas] = url.split('?');
+      sas ??= '';
+      callDesktopAppMethods.downloadResource(url, sas);
     },
 
     formatDate(timestamp) {
@@ -1230,11 +1229,7 @@ export default {
       this.extFilter = []
       this.selections.length = 0
       this.activeType = this.contentTypeList[index].type
-      //最近上传文件读取本地storage
-      if (this.activeType == 'recent') {
-        this.getCacheFiles()
-        return
-      }
+
       if (this.isFirst[this.activeType]) {
         this.fileListShow.length = 0
         this.findFileList()
@@ -1376,7 +1371,7 @@ export default {
       }
     },
     $route: {
-      handler: function (to, from) {
+      handler: async function (to, from) {
         this.initData()
         if (this.$route.fullPath.includes('school')) {
           this.routerScope = 'school'
@@ -1384,7 +1379,8 @@ export default {
           this.routerScope = 'private'
         }
 
-        this.getSasStr()
+        await this.getSasStr()
+        this.findFileList()
       },
       immediate: true
     },

+ 0 - 24
TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/bank/ExerciseList.vue

@@ -96,12 +96,6 @@
           </div>
         </div>
       </div>
-      <!--<div class="action-tools" style="display:flex;align-items: center;">-->
-        <!-- <div class="action-tool" v-if="isComponent">
-					<Input v-model="searchVal" placeholder="输入关键词搜索试题..." style="width: 300px;" clearable search @on-search="doSearch"></Input>
-				</div> -->
-        <!--<Checkbox v-model="isSelectAll" @on-change="onSelectAll" v-if="(($access.can('admin.*||exercise-upd') ||  (filterOrigin !== schoolCode)))">{{ $t('evaluation.choosePageItems') }}</Checkbox>
-      </div>-->
         <span style="margin-left:20px">{{ $t('evaluation.exerciseList.totalTip1') }}<span style="font-size: 18px;color: #ff0206;margin: 0 10px;font-weight: bold;">{{ totalNum }}</span>{{ $t('evaluation.exerciseList.unit') }}</span>
       <div>
       </div>
@@ -473,24 +467,6 @@ export default {
       })
     },
 
-    /* 搜索试题 */
-    doSearch() {
-      if (!this.searchVal.trim()) {
-        this.$Message.warning('关键词不能为空!')
-        return
-      }
-      this.$api.newEvaluation.SearchItem({
-        "code": this.filterOrigin,
-        "scope": this.curScope,
-        "researchKey": this.searchVal
-      }).then(res => {
-        let list = res.items
-        this.totalNum = list.length;
-        this.originData = list;
-        this.pageChange(1);
-      })
-    },
-
     /* 获取题库数量统计数据 */
     getFilterCount() {
       return new Promise((r, j) => {

+ 0 - 2
TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/bank/PaperDownload.vue

@@ -278,7 +278,6 @@ export default {
       const imageList = document.querySelectorAll("img");
       for (let i = 0; i < imageList.length; i++) {
         if (imageList[i].className === 'xkw-math-img') {
-          this.$Message.warning('学科网来源的试卷暂不支持下载!')
           return
         }
       }
@@ -422,7 +421,6 @@ export default {
               that.printMode = 'A4'
             }
           }).catch(err => {
-            this.$Message.warning('试卷数据异常,无法生成!')
             console.log(err)
             this.isDownloading = false
           }).catch(err => {

+ 0 - 24
TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/bank/TestPaperList.vue

@@ -72,10 +72,6 @@
               1 ? $t('evaluation.paperList.sortByOrder') : $t('evaluation.paperList.sortByType') }}</span></span>
             <span class="info-item">{{ $t('evaluation.updateTime') }}:<span class="info-bold">{{
               $tools.formatTime(paper.createTime) || 0 }} </span></span>
-            <!-- <span class="info-item">
-							<span>标签:</span>
-							<span class="info-bold" v-for="(tag,tagIndex) in paper.tags"><Tag color="blue">{{ tag }}</Tag></span>
-						</span> -->
           </div>
           <div class="paper-item-tools" v-if="!chooseModel">
             <span class="paper-item-tools-edit" @click.stop="onPreviewPaper(paper)" style="cursor: pointer;">
@@ -141,27 +137,7 @@
           <BasePointPie :echartsData="evaluationInfo"></BasePointPie>
         </div>
       </div>
-
-      <!-- 预览答题卡部分 -->
-      <!-- <div class="pl-answersheet-wrap animated fadeIn" v-if="isShowSheet">
-				<AnswerSheet></AnswerSheet>
-			</div> -->
     </div>
-
-    <!-- <Modal v-model="isShowDownLoad" width="950px" title="下载试卷" class-name="preview-modal">
-			<DownloadPaper :paper="fullPaperJson" ref="dpRef"></DownloadPaper>
-			<div slot="footer">
-				<Button @click="">取消</Button>
-				<Button type="success" @click="onDownloadPaper">下载试卷PDF</Button>
-			</div>
-		</Modal> -->
-
-    <!-- 		<Modal v-model="isShowSheet" width="800px"  title="答题卡" class-name="preview-modal" >
-			<div v-if="isShowSheet">
-				<AnswerSheet></AnswerSheet>
-			</div>
-		</Modal> -->
-
   </div>
 </template>
 <script>

+ 0 - 205
TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/bank/index.vue

@@ -1,8 +1,5 @@
 <template>
     <div class="bank-container" ref="bankContainer">
-        <!-- <div class="back-to-top flex-col-center" :title="$t('evaluation.backToTop')" @click="onBackToTop">
-            <Icon type="ios-arrow-up" />
-        </div> -->
         <Tabs :value="tabName" name="listTab" @on-click="onTabClick" :animated="false" style="position: relative;">
 					<button class="editOnExternalBtn" @click="editOnExternal">
               <Icon type="md-create" size="20" color="#42a9f0"/>
@@ -68,46 +65,6 @@
 			onItemCheckChange(arr) {
 				this.checkItemArr = arr;
 			},
-			/* 批量删除 */
-			doBatchDelete() {
-				let exListVm = this.$refs.exList;
-
-				this.$Modal.confirm({
-					title: this.$t("evaluation.newExercise.modalTip"),
-					content: this.$t("stuAccount.tips2Content3"),
-					onOk: () => {
-						let ids = this.checkItemArr.map((i) => i.id);
-						let item = this.checkItemArr[0];
-						exListVm.dataLoading = true;
-						this.$api.newEvaluation
-							.DeleteExamItem({
-								ids: ids,
-								code: item.code,
-								scope: item.scope
-							})
-							.then((res) => {
-								if (res.code == 200) {
-									exListVm.isSelectAll = false;
-									exListVm.selectedArr = [];
-									exListVm.doFilter();
-									this.checkItemArr = [];
-									this.$Message.success(this.$t("evaluation.deleteSuc"));
-								} else {
-									this.$Message.warning(this.$t("evaluation.deleteFail"));
-									exListVm.dataLoading = false;
-								}
-							})
-							.catch((err) => {
-								console.log(err);
-								this.$Message.warning(this.$t("evaluation.deleteFail"));
-								exListVm.dataLoading = false;
-							});
-					}
-				});
-			},
-			onBackToTop() {
-			//	this.$refs.bankContainer.scrollIntoView();
-			},
 			onTabClick(val) {
 				this.currentTab = val;
 				this.$router.replace({
@@ -127,145 +84,7 @@
 				this.isShowBackList = false;
 				this.$refs.bankContainer.scrollIntoView();
 			},
-			/* 编辑当前预览的试卷 */
-			onEditPaper() {
-				this.$refs.paperList.goToPaper(this.$refs.paperList.curPaper);
-			},
-
-			/** 切换全部展开与折叠 */
-			onHandleToggle() {
-				this.$refs.exList.onHandleToggle(this.isAllOpen);
-				this.isAllOpen = !this.isAllOpen;
-			},
-
-			/* 跳转到分享页面 */
-			goShare() {
-				this.$router.push({
-					name: "shareCenter",
-					params: {
-						tabName: this.currentTab
-					}
-				});
-			},
-			/* 确认是否允许携带手机号进行注册 */
-			doConfirmAgree() {
-				return new Promise((r, j) => {
-					this.$Modal.confirm({
-						title: "授权提示",
-						content: "检测到您暂未绑定学科网账号,是否允许以醍摩豆云平台关联手机号进行认证?",
-						okText: "允许",
-						cancelText: "拒绝",
-						onOk: () => {
-							r(1);
-						},
-						onCancel: () => {
-							r(0);
-						}
-					});
-				});
-			},
-
-			/* 跳转学科网 */
-			doXkwAuth() {
-				this.$api.auth.checkBind({}).then(async (res) => {
-					// 判断是否已经绑定学科网
-					let isBind = res.auth.find((i) => i.type === "xkw");
-					// 如果没有绑定 则询问用户是否允许携带手机号进行注册
-					let agree = isBind ? 1 : await this.doConfirmAgree();
-					// 判断资源类型
-					let module = this.currentTab === "exercise" ? "item" : "paper";
-					// 存到本地
-					localStorage.setItem("xkw_module", module);
-					// 发送授权请求
-					this.$api.auth
-						.xkwOauth({
-							module: module,
-							agree: agree
-						})
-						.then((res) => {
-							window.open(res.redirect);
-						});
-				});
-			},
-			// 跳转页面,进行多分题库挑选
-			dodfAuth() {
-				this.$router.push({
-					name: this.isSchool ? 'schoolDf' : 'privateDf',
-				})
-			},
 
-			/**
-			 * exList的collapseList变化
-			 * @param list
-			 */
-			onToggleChange(list) {
-				this.isAllOpen = list.length !== 0;
-			},
-
-			/** 返回创建试题页面 */
-			goCreateExercise() {
-				this.$router.push({
-					name: this.$route.name === "personalBank" ? "newPrivateExercise" : "newSchoolExercise",
-					params: {
-						scope: this.$route.name === "personalBank" ? "private" : "school"
-					}
-				});
-			},
-			/* 快速组卷纸本测验 */
-			goPaperExam() {
-				this.paperExamModal = true;
-				this.fullPaperJson = null
-			},
-
-			goXkwPick() {
-				this.$api.auth
-					.xkwOauth({
-						module: "ezj",
-						agree: 1
-					})
-					.then((res) => {
-						this.$router.push({
-							name: "xkwPage",
-							params: {
-								iframeSrc: res.redirect
-							}
-						});
-					});
-			},
-
-			/** 前往组卷页面 */
-			goCreatePaper(type, isXkwMode) {
-				if (isXkwMode) {
-					// 发送授权请求
-					this.$api.auth
-						.xkwOauth({
-							module: "ezj",
-							agree: 1
-						})
-						.then((res) => {
-							this.$router.push({
-								name: this.$route.name === "personalBank" ? "newPrivatePaper" : "newSchoolPaper",
-								params: {
-									scope: this.$route.name === "personalBank" ? "private" : "school",
-									type: type,
-									isXkwMode: true,
-									iframeSrc: res.redirect,
-									isFromItemBank: this.currentTab === "exercise"
-								}
-							});
-						});
-				} else {
-					this.$router.push({
-						name: this.$route.name === "personalBank" ? "newPrivatePaper" : "newSchoolPaper",
-						params: {
-							scope: this.$route.name === "personalBank" ? "private" : "school",
-							type: type,
-							isXkwMode: isXkwMode,
-							isFromItemBank: this.currentTab === "exercise"
-						}
-					});
-				}
-			},
 			editOnExternal() {
 				const isSchool = this.$route.path.includes('school');
 				const path = `/home/evaluation/${isSchool ? 'schoolBank' : 'personalBank'}`;
@@ -281,7 +100,6 @@
 			}
 		},
 		mounted() {
-
 			if (this.$route.params.tabName) {
 				this.currentTab = this.$route.params.tabName;
 				this.tabName = this.$route.params.tabName;
@@ -303,29 +121,6 @@
 			isSchool() {
 				return this.$route.name === "schoolBank";
 			},
-			hasSchool() {
-				return this.$store.state.userInfo.hasSchool;
-			},
-			paperScrollTop() {
-				return this.$store.state.totalAnalysis.paperScrollTop;
-			},
-			/* 判断是否为国际站 */
-			inGlobalSite() {
-				// return localStorage.getItem("location") === "Global";
-				return this.$store.state.config.srvAdr === 'Global'
-			},
-			hasXkwAuth() {
-				// if (localStorage.school_profile) {
-				//   let schoolProfile = JSON.parse(decodeURIComponent(localStorage.school_profile, "utf-8"))
-				//   return schoolProfile.schoolShows && schoolProfile.schoolShows.find(i => i.type === 'xkw')
-				// } else {
-				//   return false
-				// }
-				return true;
-			},
-			isTestSite() {
-				return (this.$store.state.config.srvAdrType === "test" && this.$store.state.config.srvAdr === 'China') || ((this.$store.state.config.srvAdrType === "product" || this.$store.state.config.srvAdrType === "rc") && (this.$store.state.userInfo.schoolCode === "habook" || this.$store.state.userInfo.schoolCode === "hbcn") && this.$store.state.config.srvAdr === 'China')
-			}
 		},
 		beforeRouteLeave(to, from, next) {
 			// 如果是从试卷库预览跳转到生成答题卡 则需要保留试卷库页面的缓存状态

+ 1 - 1
TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/components/BaseChild.vue

@@ -7,7 +7,7 @@
           {{$t('evaluation.editItem')}}{{$t('evaluation.child')}}
         </div>
         <div class="child-tools-t flex-row-center" v-if="isChangePaper" @click.stop="handleChildEdit(item,index)">
-          调整{{$t('evaluation.child')}}
+          {{$t('evaluation.child')}}
         </div>
         <div class="child-tools-t flex-row-center" v-if="canFix" @click.stop="handleFixChild(item,index)">
           {{ $t('evaluation.fixTip1') }}

+ 1 - 3
TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/components/BaseExerciseList.vue

@@ -18,8 +18,6 @@
 				<div v-for="(item, index) of typeItem.list" :key="index" :class="['exercise-item', isError(item.id) ? 'exercise-item-error' : '']" @mouseenter="exerciseMouseover($event)" @mouseleave="exerciseMouseleave($event)" :data-id="item.id">
 					<!-- 工具栏部分 -->
 					<div class="item-tools-wrap">
-						<!-- <div class="item-tools-t flex-row-center" v-show="isShowTools" @click.stop="handleSetScore(item,exerciseList.indexOf(item),typeItem.list,index)">
-							<Icon type="ios-list-box-outline" />配分</div> -->
 						<div class="item-tools-t flex-row-center" v-show="isShowTools && !isExamPaper" @click.stop="handleToolEdit(typeItem.list, item, index)"><Icon type="md-create" />{{ $t("evaluation.editItem") }}</div>
 						<div class="item-tools-t flex-row-center" v-show="isChangePaper && item.type !== 'compose'" @click.stop="handleToolEdit(typeItem.list, item, index)"><Icon type="md-create" />{{ $t('activity.adjust') }}</div>
 						<div class="item-tools-t flex-row-center" v-show="isAutoMode" @click.stop="handleToolChange(typeItem.list, item, index)"><Icon type="md-repeat" />{{ $t("evaluation.exchange") }}</div>
@@ -316,7 +314,7 @@
 										fontSize: isGroup ? "16px" : "14px"
 									}
 								},
-								isGroup ? that.$tools.getChineseByNum(params.row.groupIndex + 1) + "、" + params.row.type + "(" + params.row.score + ")" : params.row.no
+								isGroup ? that.$tools.getChineseByNum(params.row.groupIndex + 1) + "、" + params.row.type + "(" + params.row.score + ")" : params.row.no
 							);
 						},
 						width: 100

+ 2 - 2
TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/components/BaseImport.vue

@@ -141,8 +141,8 @@ export default {
     async onTemplateSelect(val) {
       let curFile = this.downloadUrls[val]
       const downloadRes = async () => {
-        let response = await fetch(this.hostName + curFile.url); // 内容转变成blob地址
-        let blob = await response.blob(); // 创建隐藏的可下载链接
+        let response = await fetch(this.hostName + curFile.url);
+        let blob = await response.blob();
         let objectUrl = window.URL.createObjectURL(blob);
         let a = document.createElement('a');
         a.href = objectUrl;

+ 0 - 78
TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/components/BasePasteTool.vue

@@ -1,78 +0,0 @@
-<template>
-	<div>
-		<div style="display: flex;justify-content: space-between;align-items: center;padding: 0 10px;">
-			<span style="font-size: 14px;margin-left: 10px;">
-				<span v-if="isHiToolAlive" style="color: #10abe7;">
-					* 已开启醍摩豆教学助手,可以获得更好图文粘贴体验
-				</span>
-				<span v-else style="color: #e72f32;">
-					* 检测到您暂未开启醍摩豆教学助手,启动后可获得更好图文粘贴体验<span style="margin-left: 5px;text-decoration: underline;color: #0074D9;cursor: pointer;" @click="openHiTool">启动</span>
-				</span>
-			</span>
-			<Button type="info" @click="parseHtml">一键生成试题</Button>
-		</div>
-		
-		<div style="margin-top: 10px;">
-			<div style="margin:  0 10px;">
-				<div ref="editor" style="text-align:left" id="pasteEditor"></div>
-			</div>
-		</div>
-	</div>
-</template>
-<script>
-	import axios from 'axios'
-	import E from 'wangeditor'
-	export default {
-		data() {
-			return {
-				stemContent: '',
-				stemEditor: null,
-				answerContent: '',
-				answerEditor: null,
-				isHiToolAlive:false
-			}
-		},
-		methods:{
-			/* 启动HiTool */
-			openHiTool(){
-				window.open("hitools://")
-			},
-			/* 检测是否启动了HiTool */
-			async isToolAlive(){
-				this.isHiToolAlive = await this.$editorTools.checkTools()
-			},
-			/* 解析HTML成试题数据 */
-			parseHtml(){
-				let html = this.stemEditor.txt.html()
-				let noUseTag = '<o:p></o:p>'
-				html = this.replaceAll(html,noUseTag,'')
-				html = this.replaceAll(html,'<font ','<span ')
-				html = this.replaceAll(html,'</font>','</span>')
-				this.$api.SaveAnalyzeHtml({ html : html }).then(response => {
-					if(response.tests.length){
-						this.$emit('addFinish',response.tests)
-					}else{
-						this.$Message.warning('试题生成失败!请检查粘贴内容是否完整!')
-					}
-				}).catch(e => {
-					this.$Message.error('试题生成失败!')
-				})
-			},
-			/* 全局替换 */
-			replaceAll(str,preVal,replaceVal){
-			    return str.replace(new RegExp(preVal,'g'),replaceVal);
-			}
-		},
-		mounted() {
-			this.isToolAlive()
-			let stemEditor = new E(this.$refs.editor)
-			stemEditor.config.onchange = (html) => {
-				this.stemContent = html
-			}
-			this.$editorTools.initMyEditor(stemEditor, this)
-			stemEditor.config.height = 600
-			stemEditor.create()
-			this.stemEditor = stemEditor
-		},
-	}
-</script>

+ 0 - 1
TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/index/CreateExercises.vue

@@ -591,7 +591,6 @@ export default {
             r(res);
           } else {
             j(res);
-            this.$Message.error("服务器繁忙!");
             this.saveLoading = false;
           }
         });

+ 0 - 22
TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/index/CreatePaper.vue

@@ -16,11 +16,6 @@
         <span style="margin-left: 8px;" v-if="isMarkMode">
             <Tag color="cyan">{{ $t('evaluation.markMode.tip4') }}</Tag>
           </span>
-        <!-- <Checkbox v-model="markModel" style="margin-left: 20px;" v-if="isSchool"> 
-          <Tooltip :content="`因應閱卷格式,題序僅依照題型排序,並且不支援含綜合題之試卷`" class="common-toolTip" placement="bottom-end" max-width="200">
-						 <span>阅卷专用</span>
-					</Tooltip>
-        </Checkbox> -->
         <Checkbox v-model="isGeneratePaper" v-if="evaluationInfo.createType === 'import'" style="margin-left: 20px;"
           @on-change="onModeChange('paper')"> {{ $t('evaluation.composePaper') }}
         </Checkbox>
@@ -251,7 +246,6 @@ export default {
           if (res.itemInfos) {
             r(res)
           } else if (res.error === 403) {
-            this.$Message.warning('当前账号学科网授权已过期!组卷失败!')
             r(null)
           }
         }).catch(err => {
@@ -265,22 +259,6 @@ export default {
         this.onImportFinish({
           list: res.itemInfos,
         })
-        let curPeriodName = this.schoolInfo.period[this.evaluationInfo.paperPeriod].name
-        let curSubjectName = this.subjectList[this.evaluationInfo.paperSubject].name
-        if (curPeriodName !== res.periodName) {
-          this.$Notice.info({
-            title: '温馨提示',
-            desc: '题目来源学段与当前所选试题学段不一致,建议您进行调整!',
-            duration: 10
-          });
-        }
-        if (curSubjectName !== res.subjectName) {
-          this.$Notice.info({
-            title: '温馨提示',
-            desc: '题目来源科目与当前所选试题科目不一致,建议您进行调整!',
-            duration: 10
-          });
-        }
       }
     },
     /* 学段切换 */

+ 0 - 138
TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/index/DfPage.less

@@ -1,138 +0,0 @@
-@main-bgColor: rgb(40, 40, 40); //主背景颜色
-@borderColor: var(--border-color);
-@primary-textColor: var(--primary-text-color); //文本主颜色
-@second-textColor: #a5a5a5; //文本副级颜色
-@primary-fontSize: 14px;
-@second-fontSize: 16px;
-
-.df-container {
-    width: 100%;
-    height: 100%;
-    position: relative;
-
-    .new-exercise-header {
-        width: 100%;
-        height: 45px;
-        border-bottom: 1px solid @borderColor;
-        margin-bottom: 15px;
-        display: flex;
-        justify-content: space-between;
-        align-items: center;
-
-        .ev-title {
-            height: 45px;
-            line-height: 45px;
-            color: @primary-textColor;
-            padding-left: 20px;
-            font-size: 16px;
-            display: inline-flex;
-        }
-
-        .btn-save {
-            margin-top: 6px;
-            margin-right: 20px;
-            height: 28px;
-            line-height: 28px;
-        }
-    }
-
-    .exercise-box {
-        display: flex;
-        height: calc(100% - 60px);
-
-        .point-tree {
-            width: calc(30% - 20px);
-            height: 800px;
-            margin: 0 10px;
-            background: #fff;
-
-            .el-tree-node__label {
-                font-size: 16px;
-            }
-        }
-    }
-
-    .create-body {
-        width: 70%;
-        margin: 0px auto;
-        height: 800px;
-        // display: flex;
-
-        .ivu-page {
-            display: flex;
-            flex-direction: row;
-            justify-content: center;
-            margin: 20px 0;
-        }
-
-        .filter-wrap {
-            margin-bottom: 15px;
-        }
-
-        .content-wrap {
-            position: relative;
-            width: 100%;
-            height: auto;
-            display: flex;
-            flex-direction: column;
-
-            .exercise-item {
-                position: relative;
-                width: 100%;
-                height: auto;
-                padding: 10px 20px 10px 20px;
-                margin-bottom: 10px;
-                font-size: 14px;
-                background: #fff;
-                border: 2px solid transparent;
-                cursor: pointer;
-
-                &:hover {
-                    .item-tools-bind {
-                        display: unset;
-                    }
-                }
-
-                .item-tools-t {
-                    height: 100%;
-                    padding: 0 15px;
-                    float: left;
-                    color: white;
-                    cursor: pointer;
-                    font-size: 14px;
-                }
-            }
-        }
-
-        .no-data-text {
-            width: 100%;
-            padding: 30px;
-            background: #fff;
-            display: flex;
-            flex-direction: column;
-            justify-content: center;
-            align-items: center;
-            margin-top: 10px;
-            font-size: 16px;
-        }
-    }
-
-    .question-shopping-car {
-        position: fixed;
-        right: 50px;
-        bottom: 27px;
-        z-index: 1;
-
-        .ivu-poptip-popper {
-            width: 360px !important;
-        }
-    }
-
-    /*横向垂直水平居中*/
-    .flex-row-center {
-        display: flex;
-        flex-direction: row;
-        justify-content: center;
-        align-items: center;
-    }
-}

Файловите разлики са ограничени, защото са твърде много
+ 0 - 1602
TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/index/DfPage.vue


+ 0 - 3
TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/index/TestPaper.vue

@@ -70,9 +70,6 @@
           <BaseCreateChild @addFinish="onAddNewFinish" ref="newEdit" v-if="addNewModal" :curPeriodIndex="paperInfo.paperPeriod" :curSubjectIndex="paperInfo.paperSubject">
           </BaseCreateChild>
         </TabPane>
-        <!-- <TabPane :label="$t('evaluation.quickCreate')" name="name2" tab="newExerciseTab" v-if="isDevEnv">
-          <BasePasteTool v-if="addNewModal" @addFinish="onAddNewFinish"></BasePasteTool>
-        </TabPane> -->
         <TabPane :label="$t('evaluation.cpTip1')" name="name3" tab="newExerciseTab">
           <ManualCreate ref="bankPicker" :subjectCode="subjectCode" :periodCode="periodCode" :gradeCode="gradeCode" :isMarkModel="isMarkMode"></ManualCreate>
         </TabPane>

+ 0 - 3
TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/index/htTestPaper.vue

@@ -66,9 +66,6 @@
           <BaseCreateChild @addFinish="onAddNewFinish" ref="newEdit" v-if="addNewModal" :curPeriodIndex="paperInfo.paperPeriod" :curSubjectIndex="paperInfo.paperSubject">
           </BaseCreateChild>
         </TabPane>
-        <!-- <TabPane :label="$t('evaluation.quickCreate')" name="name2" tab="newExerciseTab" v-if="isDevEnv">
-          <BasePasteTool v-if="addNewModal" @addFinish="onAddNewFinish"></BasePasteTool>
-        </TabPane> -->
         <TabPane :label="$t('evaluation.cpTip1')" name="name3" tab="newExerciseTab">
           <ManualCreate ref="bankPicker" :subjectCode="subjectCode" :periodCode="periodCode" :gradeCode="gradeCode" :isMarkModel="isMarkMode"></ManualCreate>
         </TabPane>

+ 1 - 16
TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/evaluation/index/index.vue

@@ -15,19 +15,6 @@
   export default {
     data() {
       return {
-        syllabusTitle: ' 评测模块',
-        identitydata: [
-          { 'id': 1, 'name': '成都紫藤小学', 'rolename': '管理员', status: '1' },
-          { 'id': 2, 'name': '成都七中小学', 'rolename': '班主任', status: '2' }
-        ],
-        treeData: [],
-        schoolInfo: {},
-        evSubjectList: [],
-        evPeriodsList: [],
-        evVolumesList: [],
-        evSubjectSelect: '',
-        evPeriodSelect: '',
-        evVolumeSelect: '',
         dataLoading: false
       }
     },
@@ -41,10 +28,8 @@
             })
       },
 	  // 判断容器滚动距离
-	  handleScroll(vertical, horizontal, nativeEvent) {
+	  handleScroll(vertical) {
 		this.$EventBus.$emit('evScroll',vertical.scrollTop)
-		// let anchorBarDom = document.getElementById('AnchorBar')
-		// console.log(anchorBarDom.getBoundingClientRect().top)
 	  },
 
       

Файловите разлики са ограничени, защото са твърде много
+ 103 - 2347
TEAMModelOS/ClientApp/src/components/hiTeachSideMenu/syllabus/index.vue


+ 10 - 10
TEAMModelOS/ClientApp/src/utils/callDesktopAppMethods.js

@@ -1,13 +1,13 @@
 export const callDesktopAppMethods = {
-    openLink(url) {
-         window.chrome.webview.hostObjects.bridge.OpenLink(url);
-    },
+  openLink(url) {
+    window.chrome.webview.hostObjects.bridge.OpenLink(url);
+  },
 
-    downloadResource(containerName, blobFileName) {
-         window.chrome.webview.hostObjects.bridge.DownloadResource(containerName, blobFileName);
-    },
+  downloadResource(url, sas) {
+    window.chrome.webview.hostObjects.bridge.DownloadResource(url, sas);
+  },
 
-    openExternal(url) {
-         window.chrome.webview.hostObjects.bridge.OpenExternal(url);
-    },
-}
+  openExternal(url) {
+    window.chrome.webview.hostObjects.bridge.OpenExternal(url);
+  }
+};

+ 0 - 1
TEAMModelOS/ClientApp/src/view/hiTeachSideMenu/index.vue

@@ -74,7 +74,6 @@
             let time_now = new Date().getTime();
             if (webEndTime && time_now > webEndTime) {
                 this.loginOut();
-                sessionStorage.setItem("loginOut", "Home检查长时间未操作");
             }
             this.$store.dispatch("user/checkSchoolCode"); // 設定登入成功的學校簡碼
             this.$store.dispatch("user/checkUserProfile"); // 檢查使用者個人詳細資訊

+ 55 - 0
TEAMModelOS/Controllers/HiTeachSideMenu/Private/ContentController.cs

@@ -0,0 +1,55 @@
+using Azure;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using System;
+using System.Threading.Tasks;
+using TEAMModelOS.Filter;
+using TEAMModelOS.Models.Request.HiTeachSideMenu;
+using TEAMModelOS.SDK.DI;
+using TEAMModelOS.Services;
+
+namespace TEAMModelOS.Controllers.HiTeachSideMenu.Private
+{
+
+    [Route("hiTeachSideMenu/private/[controller]/[action]")]
+    [ApiController]
+    public class ContentController : ControllerBase
+    {
+        private readonly ContentService _contentService;
+
+        public ContentController(AzureStorageFactory azureStorage, AzureCosmosFactory azureCosmos)
+        {
+            _contentService = new ContentService(azureStorage, azureCosmos);
+        }
+
+        [HttpPost]
+        [AuthToken(Roles = "teacher,admin,student")]
+        [Authorize(Roles = "IES,HiTeach")]
+        public async Task<IActionResult> Upload([FromForm] UploadPrivateContentRequest request)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                { 
+                    return BadRequest(ModelState);
+                }
+
+                await _contentService.UploadToPrivate(request.File, request.Id);
+
+                return Ok();
+            }
+            catch (RequestFailedException ex)
+            {
+                if (ex.ErrorCode == "BlobAlreadyExists")
+                {
+                    return BadRequest(new { message = "The file already exists." });
+                }
+                return StatusCode(500);
+            }
+            catch (Exception)
+            {
+                return StatusCode(500);
+            }
+        }
+    }
+}

+ 54 - 0
TEAMModelOS/Controllers/HiTeachSideMenu/School/ContentController.cs

@@ -0,0 +1,54 @@
+using Azure;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using System;
+using System.Threading.Tasks;
+using TEAMModelOS.Filter;
+using TEAMModelOS.Models.Request.HiTeachSideMenu;
+using TEAMModelOS.SDK.DI;
+using TEAMModelOS.Services;
+
+namespace TEAMModelOS.Controllers.HiTeachSideMenu.School
+{
+
+    [Route("hiTeachSideMenu/school/[controller]/[action]")]
+    [ApiController]
+    public class ContentController : ControllerBase
+    {
+        private readonly ContentService _contentService;
+
+        public ContentController(AzureStorageFactory azureStorage, AzureCosmosFactory azureCosmos)
+        {
+            _contentService = new ContentService(azureStorage, azureCosmos);
+        }
+
+        [AuthToken(Roles = "teacher,admin,student")]
+        [Authorize(Roles = "IES,HiTeach")]
+        public async Task<IActionResult> Upload([FromForm] UploadSchoolContentRequest request)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    return BadRequest(ModelState);
+                }
+
+                await _contentService.UploadToSchool(request.File, request.Id, request.PeriodId, request.SubjectId, request.GradeId);
+
+                return Ok();
+            }
+            catch (RequestFailedException ex)
+            {
+                if (ex.ErrorCode == "BlobAlreadyExists")
+                {
+                    return BadRequest(new { message = "The file already exists." });
+                }
+                return StatusCode(500);
+            }
+            catch (Exception)
+            {
+                return StatusCode(500);
+            }
+        }
+    }
+}

+ 3 - 1
TEAMModelOS/Controllers/Teacher/JointEventController.cs

@@ -704,6 +704,7 @@ namespace TEAMModelOS.Controllers.Common
                 {
                     return Ok(new { errCode = "1", err = "Invalid param." });
                 }
+                //取得活動報名資料
                 JointEventGroupDb jointCourse = new JointEventGroupDb();
                 StringBuilder stringBuilder = new($"SELECT * FROM c WHERE c.jointEventId = '{jointEventId}' AND c.jointGroupId = '{jointGroupId}' AND c.creatorId = '{creatorId}' AND ( IS_DEFINED(c.type) = false OR c.type = 'regular' ) ");
                 await foreach (var item in client.GetContainer(Constant.TEAMModelOS, "Teacher").GetItemQueryStreamIteratorSql(queryText: stringBuilder.ToString(), requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey("JointCourse") }))
@@ -1815,7 +1816,8 @@ namespace TEAMModelOS.Controllers.Common
                 public string id { get; set; }
                 public string name { get; set; }
                 public string no { get; set; } //jointGroup所屬班級流水號
-                public int stuNum { get; set; }
+                public int stuNum { get; set; } //實際學生人數
+                public int expStuNum { get; set; } //預估學生人數
                 public bool goFinal { get; set; } //是否可參加決賽
                 public List<object> schedule { get; set; } = new List<object>(); //已完成的行程
             }

+ 14 - 0
TEAMModelOS/Models/Request/HiTeachSideMenu/UploadPrivateContentRequest.cs

@@ -0,0 +1,14 @@
+using Microsoft.AspNetCore.Http;
+using System.ComponentModel.DataAnnotations;
+
+namespace TEAMModelOS.Models.Request.HiTeachSideMenu
+{
+    public class UploadPrivateContentRequest
+    {
+        [Required]
+        public string Id { get; set; }
+
+        [Required]
+        public IFormFile File { get; set; }
+    }
+}

+ 21 - 0
TEAMModelOS/Models/Request/HiTeachSideMenu/UploadSchoolContentRequest.cs

@@ -0,0 +1,21 @@
+using Microsoft.AspNetCore.Http;
+using System.ComponentModel.DataAnnotations;
+
+namespace TEAMModelOS.Models.Request.HiTeachSideMenu
+{
+    public class UploadSchoolContentRequest
+    {
+        [Required]
+        public string Id { get; set; }
+
+        [Required]
+        public string[] PeriodId { get; set; }
+
+        public string[] SubjectId { get; set; } = [];
+
+        public string[] GradeId { get; set; } = [];
+
+        [Required]
+        public IFormFile File { get; set; }
+    }
+}

+ 113 - 0
TEAMModelOS/Properties/ServiceDependencies/teammodelos-rc - Web Deploy/profile.arm.json

@@ -0,0 +1,113 @@
+{
+  "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
+  "contentVersion": "1.0.0.0",
+  "metadata": {
+    "_dependencyType": "compute.appService.windows"
+  },
+  "parameters": {
+    "resourceGroupName": {
+      "type": "string",
+      "defaultValue": "TEAMModelChengdu",
+      "metadata": {
+        "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking."
+      }
+    },
+    "resourceGroupLocation": {
+      "type": "string",
+      "defaultValue": "",
+      "metadata": {
+        "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support."
+      }
+    },
+    "resourceName": {
+      "type": "string",
+      "defaultValue": "rc",
+      "metadata": {
+        "description": "Name of the main resource to be created by this template."
+      }
+    },
+    "resourceLocation": {
+      "type": "string",
+      "defaultValue": "[parameters('resourceGroupLocation')]",
+      "metadata": {
+        "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there."
+      }
+    }
+  },
+  "variables": {
+    "appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]",
+    "appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]"
+  },
+  "resources": [
+    {
+      "type": "Microsoft.Resources/resourceGroups",
+      "name": "[parameters('resourceGroupName')]",
+      "location": "[parameters('resourceGroupLocation')]",
+      "apiVersion": "2019-10-01"
+    },
+    {
+      "type": "Microsoft.Resources/deployments",
+      "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]",
+      "resourceGroup": "[parameters('resourceGroupName')]",
+      "apiVersion": "2019-10-01",
+      "dependsOn": [
+        "[parameters('resourceGroupName')]"
+      ],
+      "properties": {
+        "mode": "Incremental",
+        "template": {
+          "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
+          "contentVersion": "1.0.0.0",
+          "resources": [
+            {
+              "location": "[parameters('resourceLocation')]",
+              "name": "[parameters('resourceName')]",
+              "type": "Microsoft.Web/sites",
+              "apiVersion": "2015-08-01",
+              "tags": {
+                "[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty"
+              },
+              "dependsOn": [
+                "[variables('appServicePlan_ResourceId')]"
+              ],
+              "kind": "app",
+              "properties": {
+                "name": "[parameters('resourceName')]",
+                "kind": "app",
+                "httpsOnly": true,
+                "reserved": false,
+                "serverFarmId": "[variables('appServicePlan_ResourceId')]",
+                "siteConfig": {
+                  "metadata": [
+                    {
+                      "name": "CURRENT_STACK",
+                      "value": "dotnetcore"
+                    }
+                  ]
+                }
+              },
+              "identity": {
+                "type": "SystemAssigned"
+              }
+            },
+            {
+              "location": "[parameters('resourceLocation')]",
+              "name": "[variables('appServicePlan_name')]",
+              "type": "Microsoft.Web/serverFarms",
+              "apiVersion": "2015-08-01",
+              "sku": {
+                "name": "S1",
+                "tier": "Standard",
+                "family": "S",
+                "size": "S1"
+              },
+              "properties": {
+                "name": "[variables('appServicePlan_name')]"
+              }
+            }
+          ]
+        }
+      }
+    }
+  ]
+}

+ 160 - 0
TEAMModelOS/Services/ContentService.cs

@@ -0,0 +1,160 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.Azure.Cosmos;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using TEAMModelOS.SDK.DI;
+using TEAMModelOS.SDK.Models;
+
+namespace TEAMModelOS.Services
+{
+    public class ContentService
+    {
+        private static readonly Dictionary<string, List<string>> fileTypes = new()
+            {
+                { "audio", new List<string> { "mp3", "ogg", "wav", "ape", "cda", "au", "midi", "mac", "aac" }},
+                { "doc", new List<string> { "ppt", "pptx", "doc", "docx", "pdf", "xls", "xlsx", "csv" }},
+                { "image", new List<string> { "jpg", "jpeg", "bmp", "tif", "png", "gif", "svg" }},
+                { "res", new List<string> { "hte", "htex" }},
+                { "video", new List<string> { "rmvb", "wmv", "asf", "avi", "3gp", "mpg", "mkv", "mp4", "dvd", "ogm", "mov", "mpeg2", "mpeg4" }},
+            };
+        private readonly AzureStorageFactory _azureStorage;
+        private readonly AzureCosmosFactory _azureCosmos;
+        private readonly FileExtractService _fileExtractService;
+
+        public ContentService(AzureStorageFactory azureStorage, AzureCosmosFactory azureCosmos)
+        {
+            _azureStorage = azureStorage;
+            _azureCosmos = azureCosmos;
+            _fileExtractService = new FileExtractService(azureStorage);
+        }
+
+        public async Task UploadToSchool(IFormFile file, string id, string[] periodId, string[] subjectId, string[] gradeId)
+        {
+                var fileName = file.FileName;
+                var extension = Path.GetExtension(fileName).ToLower().TrimStart('.');
+                var type = _getFileType(extension);
+                var blobPath = $"{type}/{fileName}";
+
+                await _uploadFile(file, extension, id, blobPath);
+                if (type == "image")
+                {
+                    var thumbnailPath = $"thum/{fileName}";
+                    await _uploadThumbnail(file, id, thumbnailPath);
+                }
+
+                var bloblog = new Bloblog
+                {
+                    id = Guid.NewGuid().ToString(),
+                    pk = "Bloblog",
+                    code = $"Bloblog-{id}",
+                    time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
+                    size = file.Length,
+                    url = blobPath,
+                    type = type,
+                    ext = "." + extension,
+                    periodId = periodId.ToList(),
+                    subjectId = subjectId.ToList(),
+                    gradeId = gradeId.ToList(),
+                };
+                await _writeFileRecord(Constant.School, id, bloblog);
+        }
+
+        public async Task UploadToPrivate(IFormFile file, string id)
+        {
+            var fileName = file.FileName;
+            var extension = Path.GetExtension(fileName).ToLower().TrimStart('.');
+            var type = _getFileType(extension);
+            var blobPath = $"{type}/{fileName}";
+
+            await _uploadFile(file, extension, id, blobPath);
+            if (type == "image")
+            {
+                var thumbnailPath = $"thum/{fileName}";
+                await _uploadThumbnail(file, id, thumbnailPath);
+            }
+
+            var bloblog = new Bloblog
+            {
+                id = Guid.NewGuid().ToString(),
+                pk = "Bloblog",
+                code = $"Bloblog-{id}",
+                time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
+                size = file.Length,
+                url = blobPath,
+                type = type,
+                ext = "." + extension,
+                periodId = new List<string> {},
+                subjectId = new List<string> {},
+                gradeId = new List<string> {},
+            };
+            await _writeFileRecord(Constant.Teacher, id, bloblog);
+        }
+
+        private string _getFileType(string extension)
+        {
+            foreach (var fileType in fileTypes)
+            {
+                if (fileType.Value.Contains(extension))
+                {
+                    return fileType.Key;
+                }
+            }
+
+            return "other";
+        }
+
+        private async Task _uploadFile(IFormFile file, string extension, string id, string path)
+        {
+
+            var fileName = Path.GetFileNameWithoutExtension(file.FileName);
+            var blobClient = _azureStorage.GetBlobContainerClient(id).GetBlobClient(path);
+            using (var stream = file.OpenReadStream())
+            {
+                if (extension == "htex")
+                {
+                    await _fileExtractService.ExtractHTEX(id, fileName, stream);
+                }
+                else
+                {
+                    await blobClient.UploadAsync(stream);
+                }
+            }
+        }
+
+        private async Task _uploadThumbnail(IFormFile file, string id, string path)
+        {
+            int width = 300;
+
+            using (var stream = new MemoryStream())
+            {
+                await file.CopyToAsync(stream);
+                stream.Seek(0, SeekOrigin.Begin);
+
+                using (var image = Image.FromStream(stream))
+                {
+                    int height = (int)(image.Height * (width / (double)image.Width));
+
+                    using (var resizedImage = new Bitmap(image, width, height))
+                    using (var thumbnailStream = new MemoryStream())
+                    {
+                        resizedImage.Save(thumbnailStream, ImageFormat.Jpeg);
+                        thumbnailStream.Position = 0;
+                        var blobClient = _azureStorage.GetBlobContainerClient(id).GetBlobClient(path);
+                        await blobClient.UploadAsync(thumbnailStream);
+                    }
+                }
+            }
+        }
+
+        private async Task _writeFileRecord(string containerName, string id, Bloblog bloblog)
+        {
+            var cosmosContainer = _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, containerName);
+            await cosmosContainer.CreateItemAsync(bloblog, new PartitionKey($"Bloblog-{id}"));
+        }
+    }
+}

+ 137 - 0
TEAMModelOS/Services/FileExtractService.cs

@@ -0,0 +1,137 @@
+using HTEXLib;
+using HTEXLib.Translator;
+using Ionic.Zip;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using TEAMModelOS.Controllers;
+using TEAMModelOS.SDK;
+using TEAMModelOS.SDK.DI;
+
+namespace TEAMModelOS.Services
+{
+    public class FileExtractService
+    {
+        private readonly PPTX2HTEXTranslator _PPTX2HTEXTranslator;
+        private readonly AzureStorageFactory _azureStorage;
+
+        public FileExtractService(AzureStorageFactory azureStorage)
+        {
+            _azureStorage = azureStorage;
+            _PPTX2HTEXTranslator = new PPTX2HTEXTranslator();
+        }
+
+        public async Task<string> ExtractPPTX(string containerid, string FileName, Stream streamFile)
+        {
+            if (string.IsNullOrWhiteSpace(containerid))
+            {
+                containerid = "teammodelos";
+            }
+            string shaCode = Guid.NewGuid().ToString();
+            Htex htex = _PPTX2HTEXTranslator.Translate(streamFile);
+            htex.name = FileName;
+            var slides = htex.slides;
+            List<Task<string>> tasks = new List<Task<string>>();
+            HTEXIndex index = new HTEXIndex() { name = FileName, size = htex.size, thumbnail = htex.thumbnail, id = shaCode };
+
+            List<KeyValuePair<string, string>> blobslidenames = new List<KeyValuePair<string, string>>();
+            foreach (var slide in slides)
+            {
+                string json = JsonHelper.ToJson(slide, ignoreNullValue: false);
+                string guid = Guid.NewGuid().ToString();
+                blobslidenames.Add(new KeyValuePair<string, string>(guid, json));
+            }
+            List<Sld> slds = new List<Sld>();
+            foreach (var key in blobslidenames)
+            {
+                slds.Add(new Sld { type = "normal", url = $"{key.Key}.json", scoring = null }); ;
+                tasks.Add(_azureStorage.GetBlobContainerClient(containerid).UploadFileByContainer(key.Value, "res", $"{FileName}/{key.Key}.json", false));
+            }
+            await Task.WhenAll(tasks);
+            // Dictionary<string, Store> dict = new Dictionary<string, Store>();
+            List<Task> tasksFiles = new List<Task>();
+            foreach (var key in htex.stores.Keys)
+            {
+                if (key.EndsWith(".wdp") || key.EndsWith(".xlsx"))
+                {
+                    htex.stores.Remove(key);
+                    continue;
+                }
+                var store = htex.stores[key];
+                Store str = new Store() { path = key, contentType = store.contentType, isLazy = store.isLazy };
+                if (!store.isLazy && store.contentType != null && ContentTypeDict.extdict.TryGetValue(store.contentType, out string ext) && store.url.Contains(";base64,"))
+                {
+                    string[] strs = store.url.Split(',');
+                    Stream stream = new MemoryStream(Convert.FromBase64String(strs[1]));
+                    // var urlstrs = key.Split("/");
+                    var name = key.Replace("/", "");
+                    str.url = $"{name}";
+                    tasks.Add(_azureStorage.GetBlobContainerClient(containerid).UploadFileByContainer(stream, "res", $"{FileName}/{name}", false));
+                }
+                else
+                {
+                    str.url = System.Web.HttpUtility.UrlDecode(store.url, Encoding.UTF8);
+                }
+                // dict.TryAdd(key, str);
+            }
+            await Task.WhenAll(tasksFiles);
+            // index.stores = dict;
+            index.slides = slds;
+            var BlobUrl =  await _azureStorage.GetBlobContainerClient(containerid).UploadFileByContainer(JsonHelper.ToJson(index, ignoreNullValue: false), "res", FileName + "/" + "index.json", false);
+            return System.Web.HttpUtility.UrlDecode(BlobUrl, Encoding.UTF8);
+        }
+
+        public async Task<string> ExtractHTEX(string containerid, string FileName, Stream stream)
+        {
+            Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+            //处理中文乱码问题
+            Encoding encoding = Encoding.GetEncoding("GB2312");
+            var options = new ReadOptions { Encoding = encoding };
+            string index = "";
+            bool hasindex = false;
+            List<Task<string>> tasks = new List<Task<string>>();
+            ZipFile zip = ZipFile.Read(stream, options);
+            zip.AlternateEncoding = encoding;
+            List<Stream> streams = new List<Stream>();
+            foreach (var f in zip.Entries)
+            {
+                string name = FileName + "/" + f.FileName;
+                if (f.IsDirectory)
+                {
+                    continue;
+                }
+                var uploadStream = f.OpenReader();
+                byte[] buffer = new byte[uploadStream.Length];
+                uploadStream.Read(buffer, 0, buffer.Length);
+                Stream blobstream = new MemoryStream(buffer);
+                streams.Add(blobstream);
+                tasks.Add(_azureStorage.GetBlobContainerClient(containerid).UploadFileByContainer(blobstream, "res", $"{name}", false));
+                if (name.Contains($"{FileName}/index.json"))
+                {
+                    hasindex = true;
+                }
+                uploadStream.Close();
+            }
+
+            zip.Dispose();
+            stream.Close();
+            if (hasindex)
+            {
+                await Task.WhenAll(tasks);
+                foreach (var task in tasks)
+                {
+                    var url = System.Web.HttpUtility.UrlDecode(task.Result, Encoding.UTF8);
+                    if (url.Contains($"{FileName}/index.json"))
+                    {
+                        index = url;
+                    }
+                }
+            }
+            //释放资源
+            streams.ForEach(x => { x.Close(); });
+            return index;
+        }
+    }
+}

+ 1 - 2
TEAMModelOS/TEAMModelOS.csproj

@@ -111,11 +111,10 @@
 			<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
 		</Exec>
 	</Target>
-
+	<!-- 不用每次都安装。-->
 	<Target Name="EnsureNodeModulesInstalled" BeforeTargets="Build" Inputs="package.json" Outputs="packages-lock.json">
 		<!-- Build Target: Restore NPM packages using npm -->
 		<Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
-
 		<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
 	</Target>