Просмотр исходного кода

Merge branch 'master' of https://gowone.dy.takin.cc/liyuezong/SCADATrainSys

zhanghaijun месяцев назад: 11
Родитель
Сommit
1b7232caac

+ 8 - 0
.gitignore

@@ -0,0 +1,8 @@
+/Tech/Tech
+/Tech/Tech.exe
+/Tech/Tech.tar.gz
+/Tech/__debug_bin2435896766.exe
+/Tech/__debug_bin2573719945.exe
+/Tech/__debug_bin2686917779.exe
+/Tech/__debug_bin4190657557.exe
+/Tech/static/upload/

+ 13 - 0
Tech/ .txt

@@ -0,0 +1,13 @@
+# 创建新的 screen 会话
+screen -S tech_service
+
+# 在会话中运行服务
+./Tech
+
+# 按 Ctrl+A 然后按 D 分离会话
+# 重新连接:screen -r tech_service
+
+
+screen -ls
+
+pkill screen

+ 16 - 0
Tech/.vscode/launch.json

@@ -0,0 +1,16 @@
+{
+    // 使用 IntelliSense 了解相关属性。 
+    // 悬停以查看现有属性的描述。
+    // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+    
+        {
+            "name": "Launch Package",
+            "type": "go",
+            "request": "launch",
+            "mode": "auto",
+            "program": "${workspaceFolder}/main.go"
+        }
+    ]
+}

+ 1 - 0
Tech/buildlinux.bat

@@ -0,0 +1 @@
+bee pack -be GOOS=linux

+ 13 - 0
Tech/common/commondefine.go

@@ -0,0 +1,13 @@
+package common
+
+// 使用 iota 定义枚举
+type NoteTypeDef int
+
+const (
+	NoteTypeDef_UnKnow    NoteTypeDef = iota // 0
+	NoteTypeDef_Start                        // 1
+	NoteTypeDef_End                          // 2
+	NoteTypeDef_operation                    // 3
+	NoteTypeDef_Fault                        // 4
+	NoteTypeDef_Voice                        // 5
+)

+ 14 - 0
Tech/conf/app.conf

@@ -0,0 +1,14 @@
+appname = Tech
+httpport = 8080
+runmode = dev
+
+
+copyrequestbody = true
+
+# 电调服务IP和端口
+ElectricalIP = 124.222.28.5
+ElectricalPort = 7214
+
+# 数据库配置
+DB.Adapter = mysql
+DB.URL = yd_fms:fms@123@tcp(124.222.28.5:7212)/CCGD_project?charset=utf8

+ 17 - 0
Tech/controllers/default.go

@@ -0,0 +1,17 @@
+package controllers
+
+import (
+	beego "github.com/beego/beego/v2/server/web"
+	_ "github.com/go-sql-driver/mysql"
+)
+
+type MainController struct {
+	beego.Controller
+}
+
+func (c *MainController) Get() {
+	c.Data["Website"] = "beego.vip"
+	c.Data["Email"] = "astaxie@gmail.com"
+	c.TplName = "index.tpl"
+
+}

+ 23 - 0
Tech/controllers/test.json

@@ -0,0 +1,23 @@
+{
+    "process": {
+        "nodeName": "开始",
+        "nodeType": "start",
+        "nodeData": {},
+        "childNode": {
+            "nodeName": "故障触发",
+            "nodeType": "fault",
+            "nodeData": {},
+            "childNode": {
+                "nodeName": "语音交互",
+                "nodeType": "voice",
+                "nodeData": {},
+                "childNode": {
+                    "nodeName": "基本操作",
+                    "nodeType": "operation",
+                    "nodeData": {},
+                    "childNode": null
+                }
+            }
+        }
+    }
+}

+ 170 - 0
Tech/controllers/用户.go

@@ -0,0 +1,170 @@
+// controllers/用户.go
+package controllers
+
+import (
+	"Tech/models"
+	"crypto/rand"
+	"encoding/base64"
+	"encoding/json"
+
+	"github.com/beego/beego/v2/client/orm"
+	beego "github.com/beego/beego/v2/server/web"
+)
+
+type UserController struct {
+	beego.Controller
+}
+
+var simpleTokenStore = make(map[string]string) // token -> userId
+func generateShortToken(userId string) string {
+	b := make([]byte, 6)
+	rand.Read(b)
+	token := base64.RawURLEncoding.EncodeToString(b)
+	simpleTokenStore[token] = userId
+	return token
+}
+
+func validateShortToken(token string) (string, bool) {
+	userId, ok := simpleTokenStore[token]
+	return userId, ok
+}
+
+func deleteShortToken(token string) {
+	delete(simpleTokenStore, token)
+}
+
+// UserLogin 用户登录认证并生成Token
+func (c *UserController) UserLogin() {
+	var req struct {
+		UserName string `json:"userName"`
+		Password string `json:"password"`
+	}
+	requestBody := c.Ctx.Input.RequestBody
+	println("Request Body:", string(requestBody))
+	if err := json.Unmarshal(c.Ctx.Input.RequestBody, &req); err != nil {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "请求参数格式错误: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	if req.UserName == "" || req.Password == "" {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "用户名和密码不能为空",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	o := orm.NewOrm()
+	var user models.UserData
+	err := o.QueryTable(new(models.UserData)).Filter("用户名称", req.UserName).One(&user)
+	if err != nil {
+		if err == orm.ErrNoRows {
+			c.Ctx.Output.SetStatus(401)
+			c.Data["json"] = map[string]interface{}{
+				"code": 401,
+				"msg":  "用户名或密码错误",
+			}
+		} else {
+			c.Ctx.Output.SetStatus(500)
+			c.Data["json"] = map[string]interface{}{
+				"code": 500,
+				"msg":  "服务器内部错误: " + err.Error(),
+			}
+		}
+		c.ServeJSON()
+		return
+	}
+
+	if user.Password != req.Password {
+		c.Ctx.Output.SetStatus(401)
+		c.Data["json"] = map[string]interface{}{
+			"code": 401,
+			"msg":  "用户名或密码错误",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	tokenString := generateShortToken(user.UserName)
+
+	c.Data["json"] = map[string]interface{}{
+		"code": 200,
+		"data": map[string]interface{}{
+			"username":    user.UserName,
+			"userid":      user.UserID,
+			"userGroupID": user.UserGroupID,
+			"userType":    user.UserType,
+			"token":       tokenString,
+		},
+		"msg": "登录成功",
+	}
+	c.ServeJSON()
+}
+
+// UserLogOut 用户登出
+func (c *UserController) UserLogOut() {
+	authHeader := c.Ctx.Input.Header("Authorization")
+	if authHeader == "" {
+		c.Ctx.Output.SetStatus(401)
+		c.Data["json"] = map[string]interface{}{
+			"code": 401,
+			"msg":  "未提供认证信息",
+		}
+		c.ServeJSON()
+		return
+	}
+	tokenString := authHeader
+	if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
+		tokenString = authHeader[7:]
+	}
+	deleteShortToken(tokenString)
+	c.DestroySession()
+	c.Data["json"] = map[string]interface{}{
+		"code": 200,
+		"msg":  "登出成功",
+	}
+	c.ServeJSON()
+}
+
+// ValidateToken 简单token校验
+func (c *UserController) ValidateToken() {
+	authHeader := c.Ctx.Input.Header("Authorization")
+	if authHeader == "" {
+		c.Ctx.Output.SetStatus(401)
+		c.Data["json"] = map[string]interface{}{
+			"code": 401,
+			"msg":  "未提供认证信息",
+		}
+		c.ServeJSON()
+		return
+	}
+	tokenString := authHeader
+	if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
+		tokenString = authHeader[7:]
+	}
+	userId, ok := validateShortToken(tokenString)
+	if !ok {
+		c.Ctx.Output.SetStatus(401)
+		c.Data["json"] = map[string]interface{}{
+			"code": 401,
+			"msg":  "Token无效或已过期",
+		}
+		c.ServeJSON()
+		return
+	}
+	c.Data["json"] = map[string]interface{}{
+		"code": 200,
+		"data": map[string]interface{}{
+			"userid": userId,
+		},
+		"msg": "Token验证成功",
+	}
+	c.ServeJSON()
+}

+ 301 - 0
Tech/controllers/电调.go

@@ -0,0 +1,301 @@
+// controllers/用户.go
+package controllers
+
+import (
+	"Tech/models"
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+
+	"github.com/beego/beego/v2/client/orm"
+	beego "github.com/beego/beego/v2/server/web"
+)
+
+type EDispatchController struct {
+	beego.Controller
+}
+
+// /courserun/eqpmtstatus 更新电调设备状态
+func (c *EDispatchController) Eqpmtstatus() {
+	var req struct {
+		UserName string `json:"userName"`
+		Password string `json:"password"`
+	}
+	requestBody := c.Ctx.Input.RequestBody
+	println("Request Body:", string(requestBody))
+	if err := json.Unmarshal(c.Ctx.Input.RequestBody, &req); err != nil {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "请求参数格式错误: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	c.Data["json"] = map[string]interface{}{
+		"code": 200,
+		"msg":  "更新电调设备状态",
+	}
+	c.ServeJSON()
+}
+
+// /courserun/add_ctrl_log 添加电调控制日志
+func (c *EDispatchController) Addctrllog() {
+	var req struct {
+		UserName string `json:"userName"`
+		Password string `json:"password"`
+	}
+	requestBody := c.Ctx.Input.RequestBody
+	println("Request Body:", string(requestBody))
+	if err := json.Unmarshal(c.Ctx.Input.RequestBody, &req); err != nil {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "请求参数格式错误: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+	c.Data["json"] = map[string]interface{}{
+		"code": 200,
+		"msg":  "添加操作日志成功",
+	}
+	c.ServeJSON()
+}
+
+// /serverCtrl/subsysstatus 获取子系统状态
+func (c *EDispatchController) SubSysStatus() {
+	// 从配置文件读取电调服务IP和端口
+	electricalIP, err := beego.AppConfig.String("ElectricalIP")
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "读取电调服务IP配置失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	electricalPort, err := beego.AppConfig.Int("ElectricalPort")
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "读取电调服务端口配置失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 构造电调服务地址
+	electricalAddr := fmt.Sprintf("http://%s:%d/ServerCtrl/SubSysStatus", electricalIP, electricalPort)
+
+	// 调用电调服务获取子系统状态接口
+	resp, err := http.Get(electricalAddr)
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "调用电调服务获取子系统状态接口失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+	defer resp.Body.Close()
+
+	// 检查响应状态
+	if resp.StatusCode != http.StatusOK {
+		bodyBytes, _ := io.ReadAll(resp.Body)
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  fmt.Sprintf("电调服务返回异常状态码: %d,响应内容: %s", resp.StatusCode, string(bodyBytes)),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 读取并解析响应体
+	bodyBytes, err := io.ReadAll(resp.Body)
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "读取电调服务响应失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 尝试解析JSON响应
+	var result interface{}
+	if err := json.Unmarshal(bodyBytes, &result); err != nil {
+		// 如果不是JSON格式,作为字符串返回
+		c.Data["json"] = map[string]interface{}{
+			"code": 200,
+			"msg":  "获取子系统状态成功",
+			"data": string(bodyBytes),
+		}
+	} else {
+		// 如果是JSON格式,直接返回解析结果
+		c.Data["json"] = map[string]interface{}{
+			"code": 200,
+			"msg":  "获取子系统状态成功",
+			"data": result,
+		}
+	}
+
+	c.ServeJSON()
+}
+
+// /serverCtrl/alleqpmtlist 获取所有设备列表
+func (c *EDispatchController) AllEqpmtList() {
+	// 从配置文件读取电调服务IP和端口
+	electricalIP, err := beego.AppConfig.String("ElectricalIP")
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "读取电调服务IP配置失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	electricalPort, err := beego.AppConfig.Int("ElectricalPort")
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "读取电调服务端口配置失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 构造电调服务地址
+	electricalAddr := fmt.Sprintf("http://%s:%d/ServerCtrl/AllEqpmtList", electricalIP, electricalPort)
+
+	// 获取最新的课程运行记录以获取线路号
+	o := orm.NewOrm()
+	var latestCourse models.CourseRunLog
+	err = o.QueryTable(new(models.CourseRunLog)).
+		OrderBy("-RunLogID").
+		One(&latestCourse)
+
+	if err != nil && err != orm.ErrNoRows {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "查询最新课程运行记录失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 如果没有找到运行记录,默认线路号为0
+	lineID := 0
+	if err != orm.ErrNoRows {
+		// 根据运行课程ID查询课程信息以获取线路号
+		var courseInfo models.CourseInfo
+		err = o.QueryTable(new(models.CourseInfo)).
+			Filter("CourseID", latestCourse.RunCourseID).
+			One(&courseInfo)
+
+		if err != nil && err != orm.ErrNoRows {
+			c.Data["json"] = map[string]interface{}{
+				"code": 500,
+				"msg":  "查询课程信息失败: " + err.Error(),
+			}
+			c.ServeJSON()
+			return
+		}
+
+		if err != orm.ErrNoRows {
+			lineID = courseInfo.LineNo
+		}
+	}
+
+	// 准备请求数据
+	type RequestData struct {
+		LineID    int `json:"lineID"`
+		StationID int `json:"StationID"`
+		ViewID    int `json:"ViewID"`
+	}
+	reqData := RequestData{
+		LineID:    lineID,
+		StationID: 0,
+		ViewID:    0,
+	}
+
+	// 将请求数据转换为JSON
+	reqBody, err := json.Marshal(reqData)
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "构造请求数据失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 调用电调服务获取所有设备列表接口,使用POST方法
+	resp, err := http.Post(electricalAddr, "application/json", bytes.NewBuffer(reqBody))
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "调用电调服务获取所有设备列表接口失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+	defer resp.Body.Close()
+
+	// 检查响应状态
+	if resp.StatusCode != http.StatusOK {
+		bodyBytes, _ := io.ReadAll(resp.Body)
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  fmt.Sprintf("电调服务返回异常状态码: %d,响应内容: %s", resp.StatusCode, string(bodyBytes)),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 读取并解析响应体
+	bodyBytes, err := io.ReadAll(resp.Body)
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "读取电调服务响应失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 尝试解析JSON响应
+	var result map[string]interface{}
+	if err := json.Unmarshal(bodyBytes, &result); err != nil {
+		// 如果不是JSON格式,作为字符串返回
+		c.Data["json"] = map[string]interface{}{
+			"code": 200,
+			"msg":  "获取所有设备列表成功",
+			"data": string(bodyBytes),
+		}
+	} else {
+		// 如果是JSON格式,检查是否有data字段
+		if data, exists := result["data"]; exists {
+			// 如果电调返回的数据中有data字段,直接使用它
+			c.Data["json"] = map[string]interface{}{
+				"code": 200,
+				"msg":  "获取所有设备列表成功",
+				"data": data,
+			}
+		} else {
+			// 如果没有data字段,返回整个响应
+			c.Data["json"] = map[string]interface{}{
+				"code": 200,
+				"msg":  "获取所有设备列表成功",
+				"data": result,
+			}
+		}
+	}
+
+	c.ServeJSON()
+}

+ 330 - 0
Tech/controllers/节点解析存储.go

@@ -0,0 +1,330 @@
+package controllers
+
+import (
+	"Tech/common"
+	"Tech/models"
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"github.com/beego/beego/v2/client/orm"
+)
+
+////  { "process": { "nodeName": "开始", "nodeType": "start", "nodeData": {}, "childNode": { "nodeName": "故障触发", "nodeType": "fault", "nodeData": {}, "childNode": { "nodeName": "语音交互", "nodeType": "voice", "nodeData": {}, "childNode": { "nodeName": "基本操作", "nodeType": "operation", "nodeData": {}, "childNode": null } } } } }
+
+// 静态函数 - 解析保存节点数据
+func ParserAndSaveNode(courseId int, strJson string) (bool, string) {
+	// 1. 解析JSON字符串到Node结构
+	var rootNode models.Node
+	err := json.Unmarshal([]byte(strJson), &rootNode)
+	if err != nil {
+		return false, "JSON解析失败"
+	}
+
+	//1.清除 课程 CourseActionNode CourseOperationNode CourseVoiceNode CourseFaultNode CourseNodeTrigger 数据
+	ClearCourse(courseId)
+
+	// 2. 保存节点数据到数据库
+	success, msg := saveNodeToDB(courseId, 0, &rootNode)
+	if !success {
+		return false, fmt.Sprintf("节点保存失败: %s", msg)
+	}
+
+	return true, "节点解析并保存成功"
+}
+
+// 1.清除 课程 CourseActionNode CourseOperationNode CourseVoiceNode CourseFaultNode CourseNodeTrigger 数据
+func ClearCourse(courseId int) (bool, string) {
+	o := orm.NewOrm()
+
+	// 清除CourseNodeTrigger数据
+	_, err := o.Raw("DELETE FROM CourseNodeTrigger WHERE 课程ID = ?", courseId).Exec()
+	if err != nil {
+		return false, fmt.Sprintf("清除CourseNodeTrigger数据失败: %v", err)
+	}
+
+	// 清除CourseFaultNode数据
+	_, err = o.Raw("DELETE FROM CourseFaultNode WHERE 课程ID = ?", courseId).Exec()
+	if err != nil {
+		return false, fmt.Sprintf("清除CourseFaultNode数据失败: %v", err)
+	}
+
+	// 清除CourseVoiceNode数据
+	_, err = o.Raw("DELETE FROM CourseVoiceNode WHERE 课程ID = ?", courseId).Exec()
+	if err != nil {
+		return false, fmt.Sprintf("清除CourseVoiceNode数据失败: %v", err)
+	}
+
+	// 清除CourseOperationNode数据
+	_, err = o.Raw("DELETE FROM CourseOperationNode WHERE 课程ID = ?", courseId).Exec()
+	if err != nil {
+		return false, fmt.Sprintf("清除CourseOperationNode数据失败: %v", err)
+	}
+
+	// 清除CourseActionNode数据
+	_, err = o.Raw("DELETE FROM CourseActionNode WHERE 课程ID = ?", courseId).Exec()
+	if err != nil {
+		return false, fmt.Sprintf("清除CourseActionNode数据失败: %v", err)
+	}
+
+	// 清除CourseEvaList数据
+	_, err = o.Raw("DELETE FROM CourseEvaList WHERE 课程ID = ?", courseId).Exec()
+	if err != nil {
+		return false, fmt.Sprintf("清除CourseEvaList数据失败: %v", err)
+	}
+
+	return true, "课程数据清除成功"
+}
+
+// saveNodeToDB 递归保存节点到数据库
+func saveNodeToDB(classID int, parentID int, node *models.Node) (bool, string) {
+	if node == nil {
+		return true, ""
+	}
+
+	// 保存当前节点
+	_, err := SaveCourseActionNode(classID, parentID, node)
+	if err != nil {
+		return false, fmt.Sprintf("插入节点失败: %v", err)
+	}
+
+	// // 保存节点数据
+	if node.NodeData != nil {
+		success, msg := saveNodeData(classID, parentID, node.NodeData)
+		if !success {
+			return false, msg
+		}
+	}
+
+	// 递归保存子节点
+	if node.ChildNode != nil {
+		success, msg := saveNodeToDB(classID, node.NodeData.NodeID, node.ChildNode)
+		if !success {
+			return false, msg
+		}
+	}
+
+	return true, ""
+}
+
+// SaveCourseActionNode 插入节点基本信息到数据库
+func SaveCourseActionNode(classID int, parentID int, node *models.Node) (bool, error) {
+	o := orm.NewOrm()
+
+	// 将子节点ID列表转换为JSON字符串
+	var childNodesStr string
+	if node.ChildNode != nil && node.ChildNode.NodeData != nil {
+		// ChildNode 是单个节点而不是数组
+		childNodesStr = fmt.Sprintf("%d", node.ChildNode.NodeData.NodeID)
+	}
+
+	nodedata := node.NodeData
+	if nodedata == nil {
+		return false, fmt.Errorf("插入节点失败: %v", "节点数据为空")
+	}
+	var nodeType common.NoteTypeDef = common.NoteTypeDef_UnKnow
+	switch strings.ToLower(node.NodeType) {
+	case "start":
+		nodeType = 1
+	case "end":
+		nodeType = 2
+	case "operation":
+		nodeType = 3
+	case "fault":
+		nodeType = 4
+	case "voice":
+		nodeType = 5
+	}
+
+	// 创建CourseActionNode对象
+	courseActionNode := &models.CourseActionNode{
+		CourseID:         classID,
+		NodeID:           nodedata.NodeID,
+		NodeName:         node.NodeName,
+		NodeType:         int(nodeType),
+		ParentNode:       fmt.Sprintf("%d", parentID),
+		ChildNode:        childNodesStr,
+		BindRole:         nodedata.BindRole,
+		NodeDetail:       nodedata.NodeDetail,
+		DelaySkip:        nodedata.DelaySkip,
+		LoseItem:         nodedata.LoseItem,
+		NodeJoinType:     nodedata.NodeJoinType,
+		GuideDesc:        nodedata.GuideDesc,
+		GuideFilePath:    nodedata.GuideFilePath,
+		VirtualOperation: nodedata.VirtualOperation,
+	}
+	println(courseActionNode)
+
+	// 插入数据并获取ID
+	_, err := o.Insert(courseActionNode)
+	if err != nil {
+		return false, fmt.Errorf("插入CourseActionNode失败,节点重复: %v", err)
+	}
+
+	if nodeType != common.NoteTypeDef_operation && nodeType != common.NoteTypeDef_Voice {
+		return true, nil //语音节点和操作节点才需要保存评价
+	}
+
+	if nodedata.OperationNode == nil && nodedata.VoiceNode == nil {
+		fmt.Println("没有节点数据,不需要保存评价")
+		return true, nil // 没有节点数据,不需要保存评价
+	}
+
+	// 查询课程信息,检查虚拟角色设置
+	courseInfo := models.CourseInfo{CourseID: classID}
+	err = o.Read(&courseInfo, "CourseID")
+	if err != nil {
+		return false, fmt.Errorf("查询课程信息失败: %v", err)
+	}
+
+	// isVirtualRole := false
+	// // 检查节点绑定角色是否为真实角色(不在虚拟角色列表中)
+	// // 如果是真实角色,添加到CourseEvaList评价列表中
+	// if courseInfo.VirtualRole != "" {
+	// 	virtualRoles := strings.Split(courseInfo.VirtualRole, ",")
+
+	// 	for _, role := range virtualRoles {
+	// 		if strings.TrimSpace(role) == fmt.Sprintf("%d", nodedata.BindRole) {
+	// 			isVirtualRole = true
+	// 			break
+	// 		}
+	// 	}
+	// }
+
+	// 如果不是虚拟角色,则添加到评价列表
+	if true {
+		// 查询当前最大的序号
+		var maxSeqNo int
+		err = o.Raw("SELECT COALESCE(MAX(序号), 0) FROM CourseEvaList WHERE 课程ID = ?", classID).QueryRow(&maxSeqNo)
+		if err != nil {
+			return false, fmt.Errorf("查询评价序号失败: %v", err)
+		}
+
+		// 创建CourseEvaList对象
+		courseEva := &models.CourseEvaList{
+			CourseID:      classID,
+			SeqNo:         maxSeqNo + 1,
+			EvaTargetRole: nodedata.BindRole,
+			EvaName:       node.NodeName,
+			EvaMapID:      nodedata.NodeID, // 评价映射ID就是NodeID
+			EvaType:       0,               // 0表示主观评价
+			EvaSource:     0,               // 0表示来自流程图
+		}
+		if node.NodeData.OperationNode != nil {
+			courseEva.EvaScore = nodedata.OperationNode.Score
+			courseEva.EvaDeduction = nodedata.OperationNode.Deduction
+		}
+		if node.NodeData.VoiceNode != nil {
+			courseEva.EvaScore = 0
+			courseEva.EvaDeduction = 0
+		}
+
+		// 插入评价记录
+		_, err = o.Insert(courseEva)
+		if err != nil {
+			return false, fmt.Errorf("插入CourseEvaList失败: %v", err)
+		}
+	}
+
+	return true, nil
+}
+
+// saveNodeData 保存节点数据到数据库
+func saveNodeData(classID int, nodeID int, nodeData *models.NodeData) (bool, string) {
+	// 保存操作节点数据
+	if nodeData.OperationNode != nil {
+		success, msg := saveOperationNode(classID, nodeID, nodeData.OperationNode)
+		if !success {
+			return false, msg
+		}
+	}
+
+	// 保存故障节点数据
+	if nodeData.FaultNode != nil {
+		success, msg := saveFaultNode(classID, nodeID, nodeData.FaultNode)
+		if !success {
+			return false, msg
+		}
+	}
+
+	// 保存语音节点数据
+	if nodeData.VoiceNode != nil {
+		success, msg := saveVoiceNode(classID, nodeID, nodeData.VoiceNode)
+		if !success {
+			return false, msg
+		}
+	}
+
+	// 保存节点触发器数据
+	if nodeData.NodeTrigger != nil {
+		success, msg := saveNodeTrigger(classID, nodeID, nodeData.NodeTrigger)
+		if !success {
+			return false, msg
+		}
+	}
+
+	return true, ""
+}
+
+// saveOperationNode 保存操作节点数据
+func saveOperationNode(classID int, nodeID int, opNode *models.CourseOperationNode) (bool, string) {
+	// 实际实现需要根据数据库表结构进行调整
+	o := orm.NewOrm()
+	// 设置课程ID
+	opNode.CourseID = classID
+	opNode.NodeID = nodeID
+	_, err := o.Insert(opNode)
+	if err != nil {
+		return false, fmt.Sprintf("保存操作节点失败,节点重复: %v", err)
+	}
+
+	return true, ""
+}
+
+// saveFaultNode 保存故障节点数据
+func saveFaultNode(classID int, nodeID int, faultNode *models.CourseFaultNode) (bool, string) {
+	o := orm.NewOrm()
+
+	// 设置课程ID
+	faultNode.CourseID = classID
+	faultNode.NodeID = nodeID
+
+	_, err := o.Insert(faultNode)
+	if err != nil {
+		return false, fmt.Sprintf("保存故障节点失败,节点重复: %v", err)
+	}
+
+	return true, ""
+}
+
+// saveVoiceNode 保存语音节点数据
+func saveVoiceNode(classID int, nodeID int, voiceNode *models.CourseVoiceNode) (bool, string) {
+	o := orm.NewOrm()
+
+	// 设置课程ID
+	voiceNode.CourseID = classID
+	voiceNode.NodeID = nodeID
+
+	_, err := o.Insert(voiceNode)
+	if err != nil {
+		return false, fmt.Sprintf("保存语音节点失败,节点重复: %v", err)
+	}
+
+	return true, ""
+}
+
+// saveNodeTrigger 保存节点触发器数据
+func saveNodeTrigger(classID int, nodeID int, trigger *models.CourseNodeTrigger) (bool, string) {
+	o := orm.NewOrm()
+
+	// 设置课程ID
+	trigger.CourseID = classID
+	trigger.NodeID = nodeID
+
+	_, err := o.Insert(trigger)
+	if err != nil {
+		return false, fmt.Sprintf("保存节点触发器失败,节点重复: %v", err)
+	}
+
+	return true, ""
+}

+ 505 - 0
Tech/controllers/评价.go

@@ -0,0 +1,505 @@
+package controllers
+
+import (
+	// 根据实际项目结构调整导入路径
+
+	"Tech/models"
+	"encoding/json"
+
+	"github.com/beego/beego/v2/client/orm"
+	beego "github.com/beego/beego/v2/server/web"
+	// 新增导入
+)
+
+type EvaController struct {
+	beego.Controller
+}
+
+// /eva/list 获取课程评价列表
+func (c *EvaController) List() {
+	type Request struct {
+		CourseID int `json:"courseID"`
+	}
+
+	var req Request
+	requestBody := c.Ctx.Input.RequestBody
+	println("Request Body:", string(requestBody))
+
+	if err := json.Unmarshal(requestBody, &req); err != nil {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "请求参数格式错误: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	if req.CourseID == 0 {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "缺少课程ID参数",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	o := orm.NewOrm()
+	var evaList []*models.CourseEvaList
+
+	// 根据课程ID查询评价列表
+	count, err := o.QueryTable(new(models.CourseEvaList)).
+		Filter("CourseID", req.CourseID).
+		All(&evaList)
+
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "查询评价列表失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 获取所有角色信息用于关联角色名称
+	var roleInfos []*models.RoleInfo
+	_, err = o.QueryTable(new(models.RoleInfo)).All(&roleInfos)
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "查询角色信息失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 创建角色ID到角色名称的映射
+	roleMap := make(map[int]string)
+	for _, role := range roleInfos {
+		roleMap[role.RoleID] = role.RoleName
+	}
+
+	// 为每个评价项添加角色名称
+	type EvaWithRoleName struct {
+		*models.CourseEvaList
+		RoleName string `json:"roleName"`
+	}
+
+	evaWithRoles := make([]EvaWithRoleName, len(evaList))
+	for i, eva := range evaList {
+		evaWithRoles[i] = EvaWithRoleName{
+			CourseEvaList: eva,
+			RoleName:      roleMap[eva.EvaTargetRole],
+		}
+	}
+
+	c.Data["json"] = map[string]interface{}{
+		"code":  200,
+		"count": count,
+		"msg":   "成功",
+		"data":  evaWithRoles,
+	}
+	c.ServeJSON()
+}
+
+// /eva/add 添加课程评价
+func (c *EvaController) Add() {
+	var eva models.CourseEvaList
+	requestBody := c.Ctx.Input.RequestBody
+	println("Request Body:", string(requestBody))
+
+	if err := json.Unmarshal(requestBody, &eva); err != nil {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "请求参数格式错误: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	if eva.CourseID == 0 {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "缺少课程ID参数",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	o := orm.NewOrm()
+
+	// 如果SeqNo为0,则查询该课程下最大的SeqNo并加1
+	if eva.SeqNo == 0 {
+		var maxSeqNo int
+		err := o.Raw("SELECT COALESCE(MAX(序号), 0) FROM CourseEvaList WHERE 课程ID = ?", eva.CourseID).QueryRow(&maxSeqNo)
+		if err != nil {
+			c.Data["json"] = map[string]interface{}{
+				"code": 500,
+				"msg":  "查询序号失败: " + err.Error(),
+			}
+			c.ServeJSON()
+			return
+		}
+		eva.SeqNo = maxSeqNo + 1
+	} else {
+		// 如果指定了SeqNo,检查该SeqNo是否已存在
+		var count int64
+		count, err := o.QueryTable(new(models.CourseEvaList)).
+			Filter("CourseID", eva.CourseID).
+			Filter("SeqNo", eva.SeqNo).
+			Count()
+
+		if err != nil {
+			c.Data["json"] = map[string]interface{}{
+				"code": 500,
+				"msg":  "查询序号失败: " + err.Error(),
+			}
+			c.ServeJSON()
+			return
+		}
+
+		// 如果SeqNo已存在,则将该SeqNo及之后的所有序号都加1
+		if count > 0 {
+			// 使用倒序更新,避免主键冲突
+			_, err := o.Raw("UPDATE CourseEvaList SET 序号 = 序号 + 1 WHERE 课程ID = ? AND 序号 >= ? ORDER BY 序号 DESC",
+				eva.CourseID, eva.SeqNo).Exec()
+			if err != nil {
+				c.Data["json"] = map[string]interface{}{
+					"code": 500,
+					"msg":  "调整序号失败: " + err.Error(),
+				}
+				c.ServeJSON()
+				return
+			}
+		}
+	}
+
+	// 设置评价来源为教员自定义
+	eva.EvaSource = 1
+
+	_, err := o.Insert(&eva)
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "添加评价失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	c.Data["json"] = map[string]interface{}{
+		"code":  200,
+		"msg":   "添加评价成功",
+		"seqNo": eva.SeqNo,
+	}
+	c.ServeJSON()
+}
+
+// /eva/delete 删除课程评价
+func (c *EvaController) Delete() {
+	type Request struct {
+		CourseID int `json:"courseID"`
+		SeqNo    int `json:"seqNo"`
+	}
+
+	var req Request
+	requestBody := c.Ctx.Input.RequestBody
+	println("Request Body:", string(requestBody))
+
+	if err := json.Unmarshal(requestBody, &req); err != nil {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "请求参数格式错误: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	if req.CourseID == 0 || req.SeqNo == 0 {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "缺少必要参数",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	o := orm.NewOrm()
+
+	// 先查询评价记录,判断是否来自流程图
+	eva := models.CourseEvaList{CourseID: req.CourseID, SeqNo: req.SeqNo}
+	err := o.Read(&eva, "CourseID", "SeqNo")
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "查询评价记录失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 如果评价来自流程图(EvaSource=0),则不允许删除
+	if eva.EvaSource == 0 {
+		c.Data["json"] = map[string]interface{}{
+			"code": 403,
+			"msg":  "来自流程图的评价不能删除",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 执行删除操作(仅对教员自定义的评价)
+	n, err := o.Delete(&models.CourseEvaList{CourseID: req.CourseID, SeqNo: req.SeqNo})
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "删除评价失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	if n == 0 {
+		c.Data["json"] = map[string]interface{}{
+			"code": 404,
+			"msg":  "未找到对应的评价记录",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	c.Data["json"] = map[string]interface{}{
+		"code": 200,
+		"msg":  "删除评价成功",
+	}
+	c.ServeJSON()
+}
+
+// /eva/update 更新课程评价
+func (c *EvaController) Update() {
+	var eva models.CourseEvaList
+	requestBody := c.Ctx.Input.RequestBody
+	println("Request Body:", string(requestBody))
+
+	if err := json.Unmarshal(requestBody, &eva); err != nil {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "请求参数格式错误: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	if eva.CourseID == 0 || eva.SeqNo == 0 {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "缺少必要参数",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	o := orm.NewOrm()
+
+	// 先尝试读取记录,判断是否存在
+	existingEva := models.CourseEvaList{CourseID: eva.CourseID, SeqNo: eva.SeqNo}
+	err := o.Read(&existingEva, "CourseID", "SeqNo")
+	if err != nil {
+		if err == orm.ErrNoRows {
+			c.Data["json"] = map[string]interface{}{
+				"code": 404,
+				"msg":  "未找到对应的评价记录",
+			}
+			c.ServeJSON()
+			return
+		} else {
+			c.Data["json"] = map[string]interface{}{
+				"code": 500,
+				"msg":  "查询评价记录失败: " + err.Error(),
+			}
+			c.ServeJSON()
+			return
+		}
+	}
+
+	// 使用原生SQL更新记录
+	res, err := o.Raw("UPDATE CourseEvaList SET 评价目标角色=?, 评价名称=?, 评价映射ID=?, 评价类型=?, 评价赋分=?, 评价失分=?, 评价来源=? WHERE 课程ID=? AND 序号=?",
+		eva.EvaTargetRole, eva.EvaName, eva.EvaMapID, eva.EvaType, eva.EvaScore, eva.EvaDeduction, eva.EvaSource,
+		eva.CourseID, eva.SeqNo).Exec()
+
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "更新评价失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 获取影响的行数
+	num, _ := res.RowsAffected()
+	if num == 0 {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "更新评价失败: 无记录被更新",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	c.Data["json"] = map[string]interface{}{
+		"code": 200,
+		"msg":  "更新评价成功",
+		"data": eva,
+	}
+	c.ServeJSON()
+}
+
+// /eva/setscore 设置评价分数
+func (c *EvaController) SetScore() {
+	type Request struct {
+		RunLogID  int     `json:"runLogID"`
+		RunOperID int     `json:"runOperID"`
+		OperScore float64 `json:"operScore"`
+	}
+
+	var req Request
+	requestBody := c.Ctx.Input.RequestBody
+	println("Request Body:", string(requestBody))
+
+	if err := json.Unmarshal(requestBody, &req); err != nil {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "请求参数格式错误: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	if req.RunLogID == 0 || req.RunOperID == 0 {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "缺少必要参数",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	o := orm.NewOrm()
+
+	// 先检查记录是否存在
+	var count int64
+	err := o.Raw("SELECT COUNT(*) FROM CourseEvaLog WHERE 运行记录编号 = ? AND 运行操作ID = ?",
+		req.RunLogID, req.RunOperID).QueryRow(&count)
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "查询评价记录失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	if count == 0 {
+		c.Data["json"] = map[string]interface{}{
+			"code": 404,
+			"msg":  "未找到对应的评价记录",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 使用原生SQL更新操作得分字段
+	res, err := o.Raw("UPDATE CourseEvaLog SET 操作得分 = ? WHERE 运行记录编号 = ? AND 运行操作ID = ?",
+		req.OperScore, req.RunLogID, req.RunOperID).Exec()
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "更新操作得分失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 获取影响的行数
+	num, _ := res.RowsAffected()
+	if num == 0 {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "更新操作得分失败: 无记录被更新",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	c.Data["json"] = map[string]interface{}{
+		"code": 200,
+		"msg":  "设置操作得分成功",
+	}
+	c.ServeJSON()
+}
+
+// /eva/record/list 获取课程评价记录列表
+func (c *EvaController) EvaRecordList() {
+	type Request struct {
+		RunLogID int `json:"runLogID"`
+	}
+
+	var req Request
+	requestBody := c.Ctx.Input.RequestBody
+	println("Request Body:", string(requestBody))
+
+	if err := json.Unmarshal(requestBody, &req); err != nil {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "请求参数格式错误: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	if req.RunLogID == 0 {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "缺少运行记录编号参数",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	o := orm.NewOrm()
+	var evaLogList []*models.CourseEvaLog
+
+	// 根据运行记录编号查询评价记录列表
+	count, err := o.QueryTable(new(models.CourseEvaLog)).
+		Filter("RunLogID", req.RunLogID).
+		All(&evaLogList)
+
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "查询评价记录列表失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	c.Data["json"] = map[string]interface{}{
+		"code":  200,
+		"count": count,
+		"msg":   "成功",
+		"data":  evaLogList,
+	}
+	c.ServeJSON()
+}

Разница между файлами не показана из-за своего большого размера
+ 1453 - 0
Tech/controllers/课程.go


+ 781 - 0
Tech/controllers/课程运行.go

@@ -0,0 +1,781 @@
+package controllers
+
+import (
+	// 根据实际项目结构调整导入路径
+
+	"Tech/models"
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/beego/beego/v2/client/orm"
+	beego "github.com/beego/beego/v2/server/web"
+	// 新增导入
+)
+
+var (
+	dtLastPauseTime time.Time // 全局变量记录上次暂停时间
+)
+
+type CourseRunController struct {
+	beego.Controller
+}
+
+func (c *CourseRunController) CourseRun() {
+	type Request struct {
+		CourseID int `json:"courseID"`
+	}
+
+	var req Request
+	requestBody := c.Ctx.Input.RequestBody
+	println("Request Body:", string(requestBody))
+
+	if err := json.Unmarshal(requestBody, &req); err != nil {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "请求参数格式错误: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	if req.CourseID == 0 {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "缺少课程ID参数",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	o := orm.NewOrm()
+
+	// 查询是否存在正在运行的课程(课程用时为0表示正在运行)
+	var runningCourse models.CourseRunLog
+	err := o.QueryTable(new(models.CourseRunLog)).
+		Filter("CourseTime", 0).
+		OrderBy("-RunLogID").
+		One(&runningCourse)
+
+	if err == nil {
+		// 存在正在运行的课程
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "有课程正在运行中,无法开始新课程",
+		}
+		c.ServeJSON()
+		return
+	} else if err != orm.ErrNoRows {
+		// 查询出错
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "查询运行课程状态失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 查询课程信息
+	var course models.CourseInfo
+	err = o.QueryTable(new(models.CourseInfo)).
+		Filter("CourseID", req.CourseID).
+		One(&course)
+
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "查询课程信息失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 获取最大的课程运行ID号,然后ID号加1
+	var maxRunLogID int
+	err = o.Raw("SELECT COALESCE(MAX(运行记录编号), 0) FROM CourseRunLog").QueryRow(&maxRunLogID)
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "查询最大运行日志ID失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+	nextRunLogID := maxRunLogID + 1
+
+	// 创建新的课程运行记录
+	newRunLog := models.CourseRunLog{
+		RunLogID:      nextRunLogID, // 使用计算出的ID
+		RunCourseID:   req.CourseID,
+		RunCourseName: course.CourseName,
+		RunDateTime:   time.Now().Format("2006-01-02 15:04:05"),
+		CourseTime:    0, // 初始为0,表示正在运行
+		CourseScore:   0,
+	}
+
+	// 插入课程运行记录
+	id, err := o.Insert(&newRunLog)
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "创建课程运行记录失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 通知电调开课
+	isret, err := c.tellEStartCourse(req.CourseID, int(id))
+	if !isret || err != nil {
+		// 如果通知电调开课失败,删除刚刚创建的课程运行记录
+		o.Delete(&newRunLog)
+
+		// 错误信息已经在 tellEStartCourse 中设置
+		c.ServeJSON()
+		return
+	}
+
+	// 开课成功则继续后续操作
+	// 根据课程评价列表生成评价日志记录
+	var evaList []*models.CourseEvaList
+	_, err = o.QueryTable(new(models.CourseEvaList)).
+		Filter("CourseID", req.CourseID).
+		OrderBy("SeqNo").
+		All(&evaList)
+
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "查询课程评价列表失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 为每个评价项创建评价日志记录
+	for _, eva := range evaList {
+		evaLog := models.CourseEvaLog{
+			RunLogID:    int(id), // 使用运行记录ID作为关联标识
+			RunCourseID: req.CourseID,
+			RunOperID:   eva.SeqNo,   // 使用评价序号作为操作ID
+			OperName:    eva.EvaName, // 使用评价名称作为操作名称
+			OperTime:    0,           // 初始化时间为0
+			OperScore:   0,           // 初始化得分为0
+			OperParam1:  "",          // 参数初始化为空字符串
+			OperParam2:  "",
+			OperParam3:  "",
+			OperParam4:  "",
+			OperParam5:  "",
+		}
+
+		_, err = o.Insert(&evaLog)
+		if err != nil {
+			c.Data["json"] = map[string]interface{}{
+				"code": 500,
+				"msg":  "创建课程评价日志失败: " + err.Error(),
+			}
+			c.ServeJSON()
+			return
+		}
+	}
+
+	// 成功返回记录ID
+	c.Data["json"] = map[string]interface{}{
+		"code": 200,
+		"msg":  "课程开始运行",
+		"data": map[string]interface{}{
+			"runLogID": id,
+		},
+	}
+
+	c.ServeJSON()
+}
+
+// / 通知电调服务课程开始
+func (c *CourseRunController) tellEStartCourse(courseID int, runLogID int) (bool, error) {
+
+	// 从配置文件读取电调服务IP和端口
+	electricalIP, err := beego.AppConfig.String("ElectricalIP")
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "读取电调服务IP配置失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return false, err
+	}
+
+	electricalPort, err := beego.AppConfig.Int("ElectricalPort")
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "读取电调服务端口配置失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return false, err
+	}
+
+	// 构造电调服务地址
+	electricalAddr := fmt.Sprintf("http://%s:%d/ServerCtrl/Run", electricalIP, electricalPort)
+
+	// 准备请求数据
+	type CourseStartReq struct {
+		CourseID    int    `json:"CourseID"`
+		CourseRunID int    `json:"CourseRunID"`
+		SubParam1   string `json:"subParam1"`
+		SubParam2   string `json:"subParam2"`
+	}
+	startReq := CourseStartReq{
+		CourseID:    courseID,
+		CourseRunID: runLogID,
+		SubParam1:   "",
+		SubParam2:   "",
+	}
+	reqBody, err := json.Marshal(startReq)
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "构造通知电调服务请求体失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return false, err
+	}
+
+	// 调用电调服务CourseStart接口
+	resp, err := http.Post(electricalAddr, "application/json", bytes.NewBuffer(reqBody))
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "调用电调服务开课接口失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return false, err
+	}
+	defer resp.Body.Close()
+
+	// 检查响应状态
+	if resp.StatusCode != http.StatusOK {
+		bodyBytes, _ := io.ReadAll(resp.Body)
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  fmt.Sprintf("电调服务返回异常状态码: %d,响应内容: %s", resp.StatusCode, string(bodyBytes)),
+		}
+		c.ServeJSON()
+		return false, fmt.Errorf("电调服务返回异常状态码: %d", resp.StatusCode)
+	}
+
+	// 读取响应体(可根据实际需求解析响应内容)
+	_, _ = io.ReadAll(resp.Body)
+	return true, nil
+}
+
+// / 通知电调服务课程结束
+func (c *CourseRunController) tellEStopCourse(courseID int, runLogID int) (bool, error) {
+
+	// 从配置文件读取电调服务IP和端口
+	electricalIP, err := beego.AppConfig.String("ElectricalIP")
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "读取电调服务IP配置失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return false, err
+	}
+
+	electricalPort, err := beego.AppConfig.Int("ElectricalPort")
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "读取电调服务端口配置失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return false, err
+	}
+
+	// 构造电调服务地址
+	electricalAddr := fmt.Sprintf("http://%s:%d/ServerCtrl/Stop", electricalIP, electricalPort)
+
+	// 准备请求数据
+	type CourseStopReq struct {
+		CourseID    int `json:"CourseID"`
+		CourseRunID int `json:"CourseRunID"`
+		CtrlStatus  int `json:"CtrlStatus"`
+	}
+	stopReq := CourseStopReq{
+		CourseID:    courseID,
+		CourseRunID: runLogID,
+		CtrlStatus:  0,
+	}
+	reqBody, err := json.Marshal(stopReq)
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "构造通知电调服务请求体失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return false, err
+	}
+
+	// 调用电调服务CourseStop接口
+	resp, err := http.Post(electricalAddr, "application/json", bytes.NewBuffer(reqBody))
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "调用电调服务停课接口失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return false, err
+	}
+	defer resp.Body.Close()
+
+	// 检查响应状态
+	if resp.StatusCode != http.StatusOK {
+		bodyBytes, _ := io.ReadAll(resp.Body)
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  fmt.Sprintf("电调服务返回异常状态码: %d,响应内容: %s", resp.StatusCode, string(bodyBytes)),
+		}
+		c.ServeJSON()
+		return false, fmt.Errorf("电调服务返回异常状态码: %d", resp.StatusCode)
+	}
+
+	// 读取响应体(可根据实际需求解析响应内容)
+	_, _ = io.ReadAll(resp.Body)
+	return true, nil
+}
+func (c *CourseRunController) CoursePause() {
+	type Request struct {
+		SetFlag int `json:"setFlag"` // 1表示暂停,2表示恢复
+	}
+
+	var req Request
+	if err := json.Unmarshal(c.Ctx.Input.RequestBody, &req); err != nil {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "请求参数格式错误: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	if req.SetFlag != 1 && req.SetFlag != 2 {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "参数值错误,SetFlag应为1或2",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	o := orm.NewOrm()
+
+	// 查询正在进行的课程(课程用时为0表示正在运行)
+	var runningCourse models.CourseRunLog
+	err := o.QueryTable(new(models.CourseRunLog)).
+		Filter("CourseTime", 0).
+		OrderBy("-RunLogID").
+		One(&runningCourse)
+
+	if err != nil {
+		if err == orm.ErrNoRows {
+			c.Data["json"] = map[string]interface{}{
+				"code": 500,
+				"msg":  "没有正在运行的课程",
+			}
+			c.ServeJSON()
+			return
+		}
+
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "查询运行课程状态失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	switch req.SetFlag {
+	case 1: // 暂停课程
+		if runningCourse.PauseFlag == 1 {
+			c.Data["json"] = map[string]interface{}{
+				"code": 500,
+				"msg":  "课程已在暂停状态",
+			}
+			c.ServeJSON()
+			return
+		}
+
+		// 设置暂停标记并记录暂停开始时间
+		runningCourse.PauseFlag = 1
+		dtLastPauseTime = time.Now()
+
+	case 2: // 恢复课程
+		if runningCourse.PauseFlag != 1 {
+			c.Data["json"] = map[string]interface{}{
+				"code": 500,
+				"msg":  "课程未处于暂停状态",
+			}
+			c.ServeJSON()
+			return
+		}
+
+		// 如果有暂停开始时间,则计算暂停时长并累加到总暂停时长
+		if !dtLastPauseTime.IsZero() {
+			pauseDuration := int(time.Since(dtLastPauseTime).Seconds())
+			if pauseDuration > 0 {
+				runningCourse.PauseTotalTime += pauseDuration
+			}
+			// 清除暂停开始时间
+			dtLastPauseTime = time.Time{}
+		}
+
+		// 清除暂停标记
+		runningCourse.PauseFlag = 0
+	}
+
+	// 更新课程运行记录,修改PauseFlag和PauseTotalTime字段
+	_, err = o.Update(&runningCourse, "PauseFlag", "PauseTotalTime")
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "更新课程暂停状态失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	var msg string
+	if req.SetFlag == 1 {
+		msg = "课程已暂停"
+	} else {
+		msg = "课程已恢复"
+	}
+
+	c.Data["json"] = map[string]interface{}{
+		"code": 200,
+		"msg":  msg,
+		"data": runningCourse, // 返回完整的CourseRunLog数据
+	}
+	c.ServeJSON()
+}
+
+// /course/stop 停止课程运行
+func (c *CourseRunController) CourseStop() {
+	o := orm.NewOrm()
+
+	// 查询最新的课程运行记录
+	var latestCourse models.CourseRunLog
+	err := o.QueryTable(new(models.CourseRunLog)).
+		OrderBy("-RunLogID").
+		One(&latestCourse)
+
+	if err != nil {
+		if err == orm.ErrNoRows {
+			c.Data["json"] = map[string]interface{}{
+				"code": 500,
+				"msg":  "没有找到任何课程运行记录",
+			}
+			c.ServeJSON()
+			return
+		}
+
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "查询课程运行记录失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 检查课程是否已经结束
+	if latestCourse.CourseTime > 0 {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "课程已经结束,无法再次结束",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 计算课程用时(从开始时间到当前时间的秒数)
+	startTime, err := time.ParseInLocation("2006-01-02 15:04:05", latestCourse.RunDateTime, time.Local)
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code":      500,
+			"msg":       "解析课程开始时间失败: " + err.Error(),
+			"runStatus": 0,
+			"data":      nil,
+		}
+		c.ServeJSON()
+		return
+	}
+
+	courseDuration := int(time.Since(startTime).Seconds())
+
+	// 更新课程记录
+	latestCourse.CourseTime = courseDuration
+	latestCourse.PauseFlag = -1
+
+	_, err = o.Update(&latestCourse, "CourseTime", "PauseFlag")
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "更新课程运行记录失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	c.Data["json"] = map[string]interface{}{
+		"code": 200,
+		"msg":  "课程运行已停止",
+		"data": map[string]interface{}{
+			"runLogID":  latestCourse.RunLogID,
+			"courseID":  latestCourse.RunCourseID,
+			"duration":  courseDuration,
+			"startTime": latestCourse.RunDateTime,
+		},
+	}
+
+	// 通知电调服务课程结束
+	_, err = c.tellEStopCourse(latestCourse.RunCourseID, latestCourse.RunLogID)
+	if err != nil {
+		fmt.Println("通知电调服务停止课程失败: " + err.Error())
+		// c.Data["json"] = map[string]interface{}{
+		// 	"code": 500,
+		// 	"msg":  "通知电调服务停止课程失败: " + err.Error(),
+		// }
+		// c.ServeJSON()
+		// return
+	}
+
+	c.ServeJSON()
+}
+
+// /course/status 查询课程运行状态
+func (c *CourseRunController) CourseStatus() {
+	o := orm.NewOrm()
+
+	// 查询最新的课程运行记录
+	var latestCourse models.CourseRunLog
+	err := o.QueryTable(new(models.CourseRunLog)).
+		OrderBy("-RunLogID").
+		One(&latestCourse)
+
+	if err != nil {
+		if err == orm.ErrNoRows {
+			// 没有课程运行记录
+			c.Data["json"] = map[string]interface{}{
+				"code": 200,
+				"msg":  "成功",
+				"data": map[string]interface{}{
+					"runStatus": 0, // 0表示无记录/未运行
+				},
+			}
+			c.ServeJSON()
+			return
+		}
+
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "查询课程运行记录失败: " + err.Error(),
+			"data": map[string]interface{}{
+				"runStatus": 0,
+			},
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 判断课程是否结束
+	if latestCourse.CourseTime > 0 {
+		// 课程已结束,返回空data
+		c.Data["json"] = map[string]interface{}{
+			"code": 200,
+			"msg":  "成功",
+			"data": map[string]interface{}{
+				"runStatus": 0, // 0表示未运行(课程已结束)
+			},
+		}
+	} else {
+		// 课程正在进行中,计算运行时间并返回详细信息
+		startTime, err := time.ParseInLocation("2006-01-02 15:04:05", latestCourse.RunDateTime, time.Local)
+		if err != nil {
+			c.Data["json"] = map[string]interface{}{
+				"code": 500,
+				"msg":  "解析课程开始时间失败: " + err.Error(),
+				"data": map[string]interface{}{
+					"runStatus": 0,
+				},
+			}
+			c.ServeJSON()
+			return
+		}
+
+		// 获取当前时间
+		now := time.Now()
+
+		fmt.Printf("目标时间: %v\n", startTime)
+		fmt.Printf("当前时间: %v\n", now)
+
+		// 计算时间差(秒)
+		diff := int(now.Sub(startTime).Seconds())
+
+		// 确定运行状态
+		runStatus := 1 // 默认为正在运行
+		if latestCourse.PauseFlag == 1 {
+			runStatus = 2 // 2表示暂停中
+		}
+		// 计算calTime为开始时间加上暂停时间后的格式化时间
+		calTime := startTime.Add(time.Duration(latestCourse.PauseTotalTime) * time.Second).Format("2006-01-02 15:04:05")
+
+		// 课程正在进行中,返回详细信息
+		c.Data["json"] = map[string]interface{}{
+			"code": 200,
+			"msg":  "成功",
+			"data": map[string]interface{}{
+				"runLogID":       latestCourse.RunLogID,
+				"runCourseID":    latestCourse.RunCourseID,
+				"runCourseName":  latestCourse.RunCourseName,
+				"runDateTime":    latestCourse.RunDateTime,
+				"courseTime":     diff, // 实时计算的运行时间
+				"courseScore":    latestCourse.CourseScore,
+				"pauseFlag":      latestCourse.PauseFlag,
+				"pauseTotalTime": latestCourse.PauseTotalTime,
+				"calTime":        calTime,   // 开始时间+暂停时间后的时间
+				"runStatus":      runStatus, // 0表示未运行,1表示正在运行,2表示暂停中
+			},
+		}
+	}
+
+	c.ServeJSON()
+}
+
+// /runrecord/list 获取课程运行记录列表 查询课程运行时间
+func (c *CourseRunController) RunRecordList() {
+	// 定义请求参数结构体
+	type Request struct {
+		CourseName string `json:"courseName"`
+		TypeID     int    `json:"typeID"`
+		CourseID   int    `json:"courseID"`
+		LineNo     int    `json:"lineNo"`
+		Creator    string `json:"creator"`
+	}
+
+	var req Request
+	// 解析请求参数
+	if err := json.Unmarshal(c.Ctx.Input.RequestBody, &req); err != nil {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "请求参数格式错误: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	o := orm.NewOrm()
+
+	// 先查询所有运行记录
+	var runRecords []models.CourseRunLog
+	qs := o.QueryTable(new(models.CourseRunLog)).OrderBy("-RunLogID")
+	_, err := qs.All(&runRecords)
+	if err != nil {
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "查询课程运行记录失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 构建结果
+	type RunRecordDetail struct {
+		RunLogID       int     `json:"runLogID"`
+		RunCourseID    int     `json:"runCourseID"`
+		RunCourseName  string  `json:"runCourseName"`
+		RunDateTime    string  `json:"runDateTime"`
+		CourseTime     int     `json:"courseTime"`
+		CourseScore    float64 `json:"courseScore"`
+		PauseFlag      int     `json:"pauseFlag"`
+		PauseTotalTime int     `json:"pauseTotalTime"`
+	}
+
+	var results []RunRecordDetail
+
+	// 遍历运行记录并获取课程详细信息及应用筛选
+	for _, record := range runRecords {
+		// 查询对应的课程信息
+		var courseInfo models.CourseInfo
+		err = o.QueryTable(new(models.CourseInfo)).Filter("CourseID", record.RunCourseID).One(&courseInfo)
+		if err != nil && err != orm.ErrNoRows {
+			c.Data["json"] = map[string]interface{}{
+				"code": 500,
+				"msg":  "查询课程信息失败: " + err.Error(),
+			}
+			c.ServeJSON()
+			return
+		}
+
+		// 检查是否符合筛选条件
+		match := true
+
+		// 如果有筛选条件,检查是否匹配
+		if req.CourseName != "" && (err == orm.ErrNoRows || !strings.Contains(courseInfo.CourseName, req.CourseName)) {
+			match = false
+		}
+
+		if req.TypeID != 0 && (err == orm.ErrNoRows || courseInfo.TypeID != req.TypeID) {
+			match = false
+		}
+
+		if req.CourseID != 0 && record.RunCourseID != req.CourseID {
+			match = false
+		}
+
+		if req.LineNo != 0 && (err == orm.ErrNoRows || courseInfo.LineNo != req.LineNo) {
+			match = false
+		}
+
+		if req.Creator != "" && (err == orm.ErrNoRows || !strings.Contains(courseInfo.Creator, req.Creator)) {
+			match = false
+		}
+
+		// 如果匹配筛选条件,则添加到结果中
+		if match {
+			detail := RunRecordDetail{
+				RunLogID:       record.RunLogID,
+				RunCourseID:    record.RunCourseID,
+				RunCourseName:  record.RunCourseName,
+				RunDateTime:    record.RunDateTime,
+				CourseTime:     record.CourseTime,
+				CourseScore:    record.CourseScore,
+				PauseFlag:      record.PauseFlag,
+				PauseTotalTime: record.PauseTotalTime,
+			}
+
+			if err != orm.ErrNoRows {
+
+			}
+
+			results = append(results, detail)
+		}
+	}
+
+	c.Data["json"] = map[string]interface{}{
+		"code":  200,
+		"msg":   "查询成功",
+		"count": len(results),
+		"data":  results,
+	}
+	c.ServeJSON()
+}

+ 41 - 0
Tech/go.mod

@@ -0,0 +1,41 @@
+module Tech
+
+go 1.24
+
+require github.com/beego/beego/v2 v2.1.0
+
+require github.com/smartystreets/goconvey v1.6.4
+
+require (
+	filippo.io/edwards25519 v1.1.0 // indirect
+	github.com/deckarep/golang-set v1.8.0 // indirect
+	github.com/go-sql-driver/mysql v1.9.3 // indirect
+)
+
+require (
+	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/cespare/xxhash/v2 v2.2.0 // indirect
+	github.com/golang-jwt/jwt/v4 v4.5.2
+	github.com/golang/protobuf v1.5.3 // indirect
+	github.com/google/uuid v1.6.0
+	github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect
+	github.com/hashicorp/golang-lru v0.5.4 // indirect
+	github.com/jtolds/gls v4.20.0+incompatible // indirect
+	github.com/kr/text v0.2.0 // indirect
+	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
+	github.com/mitchellh/mapstructure v1.5.0 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/polardb/polardbx-connector-go v1.0.0
+	github.com/prometheus/client_golang v1.15.1 // indirect
+	github.com/prometheus/client_model v0.3.0 // indirect
+	github.com/prometheus/common v0.42.0 // indirect
+	github.com/prometheus/procfs v0.9.0 // indirect
+	github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
+	github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
+	golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect
+	golang.org/x/net v0.7.0 // indirect
+	golang.org/x/sys v0.6.0 // indirect
+	golang.org/x/text v0.7.0 // indirect
+	google.golang.org/protobuf v1.30.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)

+ 91 - 0
Tech/go.sum

@@ -0,0 +1,91 @@
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/beego/beego/v2 v2.1.0 h1:Lk0FtQGvDQCx5V5yEu4XwDsIgt+QOlNjt5emUa3/ZmA=
+github.com/beego/beego/v2 v2.1.0/go.mod h1:6h36ISpaxNrrpJ27siTpXBG8d/Icjzsc7pU1bWpp0EE=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4=
+github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo=
+github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
+github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
+github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
+github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
+github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
+github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
+github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
+github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/polardb/polardbx-connector-go v1.0.0 h1:+xPdpz+RNq1ge0LxwpxaKZ1BLNvzHZBmIVAvcdZBGiY=
+github.com/polardb/polardbx-connector-go v1.0.0/go.mod h1:aEEyVPHgnXt4xP5N+oPKDapKGwhk/KSForTePVLy6OQ=
+github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
+github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
+github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
+github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
+github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
+github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
+github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
+github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
+github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
+github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
+github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 h1:DAYUYH5869yV94zvCES9F51oYtN5oGlwjxJJz7ZCnik=
+github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38=
+golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
+google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 35 - 0
Tech/main.go

@@ -0,0 +1,35 @@
+package main
+
+import (
+	_ "Tech/routers"
+
+	"github.com/beego/beego/v2/client/orm"
+	"github.com/beego/beego/v2/server/web"
+	beego "github.com/beego/beego/v2/server/web"
+)
+
+func init() {
+	// 注册驱动(可选的,mysql/sqlite3/postgres 默认已注册):cite[4]
+	// orm.RegisterDriver("mysql", orm.DR_MySQL)
+
+	// 从配置文件读取数据库配置
+	dbAdapter, _ := beego.AppConfig.String("DB.Adapter")
+	dbURL, _ := beego.AppConfig.String("DB.URL")
+
+	// 注册数据库
+	orm.RegisterDataBase("default", dbAdapter, dbURL)
+
+	// 注册模型(也可以在主程序入口或模型自身的 init 函数中)
+	// orm.RegisterModel(new(User))
+}
+func main() {
+	// 自动建表。参数说明:数据库别名、是否强制重建(false 则只创建新表)、是否打印日志
+	// orm.RunSyncdb("default", false, true)
+
+	// 可选:开启 ORM 调试模式,打印 SQL 日志
+	// 将 URL 路径 /static/upload 映射到本地目录 static/upload
+	web.SetStaticPath("/static/upload", "static/upload")
+	orm.Debug = true
+	beego.Run()
+	
+}

+ 330 - 0
Tech/models/教员模型.go

@@ -0,0 +1,330 @@
+package models
+
+import (
+	"github.com/beego/beego/v2/client/orm"
+	_ "github.com/go-sql-driver/mysql"
+)
+
+// 1. CourseType - 课程分类表
+type CourseType struct {
+	TypeID    int    `orm:"column(分类ID);pk" json:"typeID"`
+	TypeName  string `orm:"column(分类名称);type(text)" json:"typeName"`
+	ParentID  int    `orm:"column(父类ID)" json:"parentID"`
+	TypeParam int    `orm:"column(类型参数)" json:"typeParam"`
+}
+
+func (c *CourseType) TableName() string { return "CourseType" }
+
+// 2. CourseInfo - 课程数据表
+type CourseInfo struct {
+	CourseID         int    `orm:"column(课程ID);pk" json:"courseID"`
+	CourseGUID       string `orm:"column(课程GUID);type(text)" json:"courseGUID"`
+	CourseName       string `orm:"column(课程名称);type(text)" json:"courseName"`
+	TypeID           int    `orm:"column(分类ID)" json:"typeID"`
+	ExternalTypeID   int    `orm:"column(外部分类ID)" json:"externalTypeID"`
+	VirtualStartTime int    `orm:"column(虚拟起始时间)" json:"virtualStartTime"`
+	TotalDuration    int    `orm:"column(课程总时长)" json:"totalDuration"`
+	LineNo           int    `orm:"column(线路号)" json:"lineNo"`
+	RunningMap       int    `orm:"column(运行图)" json:"runningMap"`
+	CourseDesc       string `orm:"column(课程描述);type(text)" json:"courseDesc"`
+	Creator          string `orm:"column(创建人);type(text)" json:"creator"`
+	CreateTime       string `orm:"column(创建时间);type(text)" json:"createTime"`
+	ModifyTime       string `orm:"column(修改时间);type(text)" json:"modifyTime"`
+	GuideDesc        string `orm:"column(指导描述);type(text);null" json:"guideDesc"`
+	GuideFilePath    string `orm:"column(指导文件地址);type(text);null" json:"guideFilePath"`
+	VirtualRole      string `orm:"column(虚拟角色);type(text)" json:"virtualRole"`
+	CourseDataStream string `orm:"column(课程数据流);type(text);null" json:"courseDataStream"`
+}
+
+func (c *CourseInfo) TableName() string { return "CourseInfo" }
+
+// 3. CourseEvaList - 课程评价表
+type CourseEvaList struct {
+	CourseID      int    `orm:"column(课程ID);pk" json:"courseID"`
+	SeqNo         int    `orm:"column(序号)" json:"seqNo"`
+	EvaTargetRole int    `orm:"column(评价目标角色)" json:"evaTargetRole"`
+	EvaName       string `orm:"column(评价名称);type(text)" json:"evaName"`
+	EvaMapID      int    `orm:"column(评价映射ID)" json:"evaMapID"`
+	EvaType       int    `orm:"column(评价类型)" json:"evaType"` // 0主观评价1客观评价
+	EvaScore      int    `orm:"column(评价赋分)" json:"evaScore"`
+	EvaDeduction  int    `orm:"column(评价失分)" json:"evaDeduction"`
+	EvaSource     int    `orm:"column(评价来源)" json:"evaSource"` // 0来自流程图,1.来自教员自定义
+}
+
+func (c *CourseEvaList) TableName() string { return "CourseEvaList" }
+
+// 4. CourseActionNode - 课程节点表
+type CourseActionNode struct {
+	CourseID int    `orm:"column(课程ID);pk" json:"courseID"`
+	NodeID   int    `orm:"column(节点ID)" json:"nodeID"`
+	NodeName string `orm:"column(节点名称);type(text)" json:"nodeName"`
+	//NodeType 1:开始2:结束3:操作4:故障5:语音
+	NodeType         int    `orm:"column(节点类型)" json:"nodeType"`
+	ParentNode       string `orm:"column(父节点);type(text)" json:"parentNode"`
+	ChildNode        string `orm:"column(子节点);type(text)" json:"childNode"`
+	BindRole         int    `orm:"column(绑定角色)" json:"bindRole"`
+	NodeDetail       string `orm:"column(节点详细信息);type(text)" json:"nodeDetail"`
+	DelaySkip        int    `orm:"column(延时跳过)" json:"delaySkip"`
+	LoseItem         int    `orm:"column(失格项)" json:"loseItem"`
+	NodeJoinType     int    `orm:"column(节点接入与或关系)" json:"nodeJoinType"` // 0无关系1或关系2与关系
+	GuideDesc        string `orm:"column(指导描述);type(text);null" json:"guideDesc"`
+	GuideFilePath    string `orm:"column(指导文件地址);type(text);null" json:"guideFilePath"`
+	VirtualOperation int    `orm:"column(虚拟操作)" json:"virtualOperation"` // 1虚拟角色0真实角色
+}
+
+func (c *CourseActionNode) TableName() string { return "CourseActionNode" }
+
+// 5. CourseOperationNode - 课程操作类型节点表
+type CourseOperationNode struct {
+	CourseID   int    `orm:"column(课程ID);pk" json:"courseID"`
+	NodeID     int    `orm:"column(节点ID)" json:"nodeID"`
+	TemplateID int    `orm:"column(模板号)" json:"templateID"`
+	TextParam1 string `orm:"column(文本参数1);type(text)" json:"textParam1"`
+	TextParam2 string `orm:"column(文本参数2);type(text)" json:"textParam2"`
+	TextParam3 string `orm:"column(文本参数3);type(text)" json:"textParam3"`
+	TextParam4 string `orm:"column(文本参数4);type(text)" json:"textParam4"`
+	TextParam5 string `orm:"column(文本参数5);type(text)" json:"textParam5"`
+	Score      int    `orm:"column(赋分)" json:"score"`
+	Deduction  int    `orm:"column(失分)" json:"deduction"`
+}
+
+func (c *CourseOperationNode) TableName() string { return "CourseOperationNode" }
+
+// 6. CourseFaultNode - 课程故障类型节点表
+type CourseFaultNode struct {
+	CourseID   int    `orm:"column(课程ID);pk" json:"courseID"`
+	NodeID     int    `orm:"column(节点ID)" json:"nodeID"`
+	TemplateID int    `orm:"column(模板号)" json:"templateID"`
+	TextParam1 string `orm:"column(文本参数1);type(text)" json:"textParam1"`
+	TextParam2 string `orm:"column(文本参数2);type(text)" json:"textParam2"`
+	TextParam3 string `orm:"column(文本参数3);type(text)" json:"textParam3"`
+	TextParam4 string `orm:"column(文本参数4);type(text)" json:"textParam4"`
+	TextParam5 string `orm:"column(文本参数5);type(text)" json:"textParam5"`
+	RuleID     int    `orm:"column(规则编号)" json:"ruleID"`
+}
+
+func (c *CourseFaultNode) TableName() string { return "CourseFaultNode" }
+
+// 7. CourseVoiceNode - 课程语音节点表
+type CourseVoiceNode struct {
+	CourseID     int    `orm:"column(课程ID);pk" json:"courseID"`
+	NodeID       int    `orm:"column(节点ID)" json:"nodeID"`
+	TargetRole   int    `orm:"column(通讯角色目标)" json:"targetRole"`
+	VoiceContent string `orm:"column(语音内容);type(text)" json:"voiceContent"`
+	EvalContent  string `orm:"column(评价内容);type(text)" json:"evalContent"` // (AAA,BBB);CCC 映射(AAA∪BBB)∩CCC
+}
+
+func (c *CourseVoiceNode) TableName() string { return "CourseVoiceNode" }
+
+// 8. CourseNodeTrigger - 课程节点触发条件表
+type CourseNodeTrigger struct {
+	CourseID     int    `orm:"column(课程ID);pk" json:"courseID"`
+	NodeID       int    `orm:"column(节点ID)" json:"nodeID"`
+	TriggerMode  int    `orm:"column(触发方式)" json:"triggerMode"`            // 0:教员手动触发,1.课程起始时间触发,2.流程延迟时间触发.3,任意流程推进到立即出发,4.条件流程触发.
+	TextParam1   string `orm:"column(文本参数1);type(text)" json:"textParam1"` // 1,2:填写秒数,4.(4∪5)∩3
+	TextParam2   string `orm:"column(文本参数2);type(text)" json:"textParam2"`
+	Event        string `orm:"column(事件);type(text);null" json:"event"`
+	TriggerDelay int    `orm:"column(触发延时)" json:"triggerDelay"`
+}
+
+func (c *CourseNodeTrigger) TableName() string { return "CourseNodeTrigger" }
+
+// 9. TemplateOperation - 操作类模板表
+type TemplateOperation struct {
+	TemplateID      int    `orm:"column(模板ID);pk" json:"templateID"`
+	Category        int    `orm:"column(分类)" json:"category"`
+	CategoryName    string `orm:"column(分类名称);type(text)" json:"categoryName"`
+	SubCategory     int    `orm:"column(子分类)" json:"subCategory"`
+	SubCategoryName string `orm:"column(子分类名称);type(text)" json:"subCategoryName"`
+	TemplateName    string `orm:"column(操作模板名称);type(text)" json:"templateName"`
+	TemplateType    int    `orm:"column(操作模板类型)" json:"templateType"`
+	Param1Type      int    `orm:"column(参数1类型)" json:"param1Type"`
+	Param1Desc      string `orm:"column(参数1说明);type(text)" json:"param1Desc"`
+	Param2Type      int    `orm:"column(参数2类型)" json:"param2Type"`
+	Param2Desc      string `orm:"column(参数2说明);type(text)" json:"param2Desc"`
+	Param3Type      int    `orm:"column(参数3类型)" json:"param3Type"`
+	Param3Desc      string `orm:"column(参数3说明);type(text)" json:"param3Desc"`
+	Param4Type      int    `orm:"column(参数4类型)" json:"param4Type"`
+	Param4Desc      string `orm:"column(参数4说明);type(text)" json:"param4Desc"`
+	Param5Type      int    `orm:"column(参数5类型)" json:"param5Type"`
+	Param5Desc      string `orm:"column(参数5说明);type(text)" json:"param5Desc"`
+}
+
+func (t *TemplateOperation) TableName() string { return "TemplateOperation" }
+
+// 10. TemplateFault - 故障类模板表
+type TemplateFault struct {
+	TemplateID      int    `orm:"column(模板ID);pk" json:"templateID"`
+	Category        int    `orm:"column(分类)" json:"category"`
+	CategoryName    string `orm:"column(分类名称);type(text)" json:"categoryName"`
+	SubCategory     int    `orm:"column(子分类)" json:"subCategory"`
+	SubCategoryName string `orm:"column(子分类名称);type(text)" json:"subCategoryName"`
+	TemplateName    string `orm:"column(模板名称);type(text)" json:"templateName"`
+	FaultLogicMap   int    `orm:"column(故障逻辑映射)" json:"faultLogicMap"`
+	Param1Type      int    `orm:"column(参数1类型)" json:"param1Type"`
+	Param1Desc      string `orm:"column(参数1说明);type(text)" json:"param1Desc"`
+	Param2Type      int    `orm:"column(参数2类型)" json:"param2Type"`
+	Param2Desc      string `orm:"column(参数2说明);type(text)" json:"param2Desc"`
+	Param3Type      int    `orm:"column(参数3类型)" json:"param3Type"`
+	Param3Desc      string `orm:"column(参数3说明);type(text)" json:"param3Desc"`
+	Param4Type      int    `orm:"column(参数4类型)" json:"param4Type"`
+	Param4Desc      string `orm:"column(参数4说明);type(text)" json:"param4Desc"`
+	Param5Type      int    `orm:"column(参数5类型)" json:"param5Type"`
+	Param5Desc      string `orm:"column(参数5说明);type(text)" json:"param5Desc"`
+}
+
+func (t *TemplateFault) TableName() string { return "TemplateFault" }
+
+// 11. TemplateVoice - 语音类模板表
+type TemplateVoice struct {
+	TemplateID      int    `orm:"column(模板ID);pk" json:"templateID"`
+	Category        int    `orm:"column(分类)" json:"category"`
+	CategoryName    string `orm:"column(分类名称);type(text)" json:"categoryName"`
+	SubCategory     int    `orm:"column(子分类)" json:"subCategory"`
+	SubCategoryName string `orm:"column(子分类名称);type(text)" json:"subCategoryName"`
+	TemplateName    string `orm:"column(模板名称);type(text)" json:"templateName"`
+	VoiceContent    string `orm:"column(语音内容);type(text)" json:"voiceContent"`
+	EvalTemplate    string `orm:"column(评价模板);type(text)" json:"evalTemplate"`
+	DestRoleID      int    `orm:"column(目标角色ID);" json:"destRoleID"`
+}
+
+func (t *TemplateVoice) TableName() string { return "TemplateVoice" }
+
+// 12. NodePicPath - 节点图片数据表
+type NodePicPath struct {
+	TemplateType int    `orm:"column(模板类型)" json:"templateType"`
+	TemplateID   int    `orm:"column(模板ID);pk" json:"templateID"`
+	TemplateName string `orm:"column(模板名称);type(text)" json:"templateName"`
+	NodeStatus   int    `orm:"column(节点状态)" json:"nodeStatus"`
+	NodePicPath  string `orm:"column(节点图片地址);type(text)" json:"nodePicPath"`
+}
+
+func (n *NodePicPath) TableName() string { return "NodePicPath" }
+
+// 13. CourseRunLog - 课程运行记录表
+type CourseRunLog struct {
+	RunLogID      int     `orm:"column(运行记录编号);pk;" json:"runLogID"`
+	RunCourseID   int     `orm:"column(运行课程ID)" json:"runCourseID"`
+	RunCourseName string  `orm:"column(运行课程名称);type(text)" json:"runCourseName"`
+	RunDateTime   string  `orm:"column(运行日期时间);type(text)" json:"runDateTime"`
+	CourseTime    int     `orm:"column(课程用时)" json:"courseTime"`
+	CourseScore   float64 `orm:"column(课程得分)" json:"courseScore"`
+	//1:已经暂停,1运行中,-1课程结束
+	PauseFlag      int `orm:"column(暂停标记)" json:"pauseFlag"`
+	PauseTotalTime int `orm:"column(暂停累计时长)" json:"pauseTotalTime"`
+}
+
+func (c *CourseRunLog) TableName() string { return "CourseRunLog" }
+
+// 14. CourseOperLog - 课程操作记录表
+type CourseOperLog struct {
+	RunLogID    string  `orm:"column(运行记录编号);pk;type(text)" json:"runLogID"`
+	RunCourseID int     `orm:"column(运行课程ID)" json:"runCourseID"`
+	RunOperID   string  `orm:"column(运行操作ID);type(text)" json:"runOperID"`
+	OperName    string  `orm:"column(操作名称);type(text)" json:"operName"`
+	OperTime    int     `orm:"column(操作时间)" json:"operTime"`
+	OperScore   float64 `orm:"column(操作得分)" json:"operScore"`
+	OperParam1  string  `orm:"column(操作参数1);type(text)" json:"operParam1"`
+	OperParam2  string  `orm:"column(操作参数2);type(text)" json:"operParam2"`
+	OperParam3  string  `orm:"column(操作参数3);type(text)" json:"operParam3"`
+	OperParam4  string  `orm:"column(操作参数4);type(text)" json:"operParam4"`
+	OperParam5  string  `orm:"column(操作参数5);type(text)" json:"operParam5"`
+}
+
+func (c *CourseOperLog) TableName() string { return "CourseOperLog" }
+
+// 15. CourseEvaLog - 课程评价记录表
+type CourseEvaLog struct {
+	RunLogID    int     `orm:"column(运行记录编号);pk;" json:"runLogID"`
+	RunCourseID int     `orm:"column(运行课程ID)" json:"runCourseID"`
+	RunOperID   int     `orm:"column(运行操作ID);" json:"runOperID"`
+	OperName    string  `orm:"column(操作名称);type(text)" json:"operName"`
+	OperTime    int     `orm:"column(操作时间)" json:"operTime"`
+	OperScore   float64 `orm:"column(操作得分)" json:"operScore"`
+	OperParam1  string  `orm:"column(操作参数1);type(text)" json:"operParam1"`
+	OperParam2  string  `orm:"column(操作参数2);type(text)" json:"operParam2"`
+	OperParam3  string  `orm:"column(操作参数3);type(text)" json:"operParam3"`
+	OperParam4  string  `orm:"column(操作参数4);type(text)" json:"operParam4"`
+	OperParam5  string  `orm:"column(操作参数5);type(text)" json:"operParam5"`
+}
+
+func (c *CourseEvaLog) TableName() string { return "CourseEvaLog" }
+
+// 16. SystemLink - 系统链接目标表
+type SystemLink struct {
+	DeviceID   int    `orm:"column(设备ID);pk" json:"deviceID"`
+	DeviceName int    `orm:"column(设备名称)" json:"deviceName"`
+	IP         string `orm:"column(IP);type(text)" json:"ip"`
+	Port       string `orm:"column(Port);type(text)" json:"port"`
+	DeviceType int    `orm:"column(设备类型)" json:"deviceType"`
+}
+
+func (s *SystemLink) TableName() string { return "SystemLink" }
+
+// 17. RoleInfo - 角色数据表
+type RoleInfo struct {
+	RoleID   int    `orm:"column(角色ID);pk" json:"roleID"`
+	RoleName string `orm:"column(角色名称);type(text)" json:"roleName"`
+	RoleType int    `orm:"column(角色类型)" json:"roleType"`
+	RoleData string `orm:"column(角色数据);type(text)" json:"roleData"`
+}
+
+func (r *RoleInfo) TableName() string { return "RoleInfo" }
+
+// 18. UserData - 用户信息表
+type UserData struct {
+	UserID      int    `orm:"column(用户ID);pk" json:"userID"`
+	UserName    string `orm:"column(用户名称);type(text)" json:"userName"`
+	UserGroupID int    `orm:"column(用户所属分组)" json:"userGroupID"`
+	UserType    int    `orm:"column(用户类型)" json:"userType"` // 0管理员1教员2学员
+	Password    string `orm:"column(密码);type(text)" json:"password"`
+}
+
+func (u *UserData) TableName() string { return "UserData" }
+
+// 19. UserType - 用户分类信息表
+type UserType struct {
+	GroupID     int    `orm:"column(分组ID);pk" json:"groupID"`
+	GroupName   string `orm:"column(分组名称);type(text)" json:"groupName"`
+	ParentGroup int    `orm:"column(父分组ID)" json:"parentGroup"`
+}
+
+func (u *UserType) TableName() string { return "UserType" }
+
+// 20. TypeMapList - 数据字典表
+type TypeMapList struct {
+	DictionaryType        int    `orm:"column(字典类型)" json:"dictionaryType"`
+	DictionaryTypeName    string `orm:"column(字典类型名称);type(text)" json:"dictionaryTypeName"`
+	DictionarySubType     int    `orm:"column(字典子类型)" json:"dictionarySubType"`
+	DictionarySubTypeName string `orm:"column(字典子类型名称);type(text)" json:"dictionarySubTypeName"`
+	KeyParam              int    `orm:"column(KeyParam)" json:"keyParam"`
+	ValueParam            string `orm:"column(ValueParam);type(text)" json:"valueParam"`
+}
+
+func (t *TypeMapList) TableName() string { return "TypeMapList" }
+
+// 注册模型
+func init() {
+	orm.RegisterModel(
+		new(CourseType),
+		new(CourseInfo),
+		new(CourseEvaList),
+		new(CourseActionNode),
+		new(CourseOperationNode),
+		new(CourseFaultNode),
+		new(CourseVoiceNode),
+		new(CourseNodeTrigger),
+		new(TemplateOperation),
+		new(TemplateFault),
+		new(TemplateVoice),
+		new(NodePicPath),
+		new(CourseRunLog),
+		new(CourseOperLog),
+		new(CourseEvaLog),
+		new(SystemLink),
+		new(RoleInfo),
+		new(UserData),
+		new(UserType),
+		new(TypeMapList),
+	)
+}

+ 27 - 0
Tech/models/节点模型.go

@@ -0,0 +1,27 @@
+package models
+
+// Node 节点模型
+type Node struct {
+	NodeName string `json:"nodeName"`
+	// 节点类型 start end operation fault voice
+	NodeType  string    `json:"nodeType"`
+	NodeData  *NodeData `json:"nodeData"`
+	ChildNode *Node     `json:"childNode"`
+}
+
+// NodeData 节点数据
+type NodeData struct {
+	OperationNode    *CourseOperationNode `json:"operationNode"`
+	FaultNode        *CourseFaultNode     `json:"faultNode"`
+	VoiceNode        *CourseVoiceNode     `json:"voiceNode"`
+	NodeTrigger      *CourseNodeTrigger   `json:"nodeTrigger"`
+	NodeID           int                  `json:"nodeId"`
+	BindRole         int                  `json:"bindRole"`
+	NodeDetail       string               `json:"nodeDetail"`
+	DelaySkip        int                  `json:"delaySkip"`
+	LoseItem         int                  `json:"loseItem"`
+	NodeJoinType     int                  `json:"nodeJoinType"`
+	GuideDesc        string               `json:"guideDesc"`
+	GuideFilePath    string               `json:"guideFilePath"`
+	VirtualOperation int                  `json:"virtualOperation"`
+}

+ 64 - 0
Tech/routers/router.go

@@ -0,0 +1,64 @@
+package routers
+
+import (
+	"Tech/controllers"
+
+	beego "github.com/beego/beego/v2/server/web"
+)
+
+func init() {
+	beego.Router("/", &controllers.MainController{})
+
+	//用户登陆
+	beego.Router("/auth/login", &controllers.UserController{}, "post:UserLogin")
+	beego.Router("/auth/logout", &controllers.UserController{}, "get:UserLogOut")
+
+	// 课程相关接口
+	beego.Router("/course/list", &controllers.CourseController{}, "post:List")
+	beego.Router("/course/add", &controllers.CourseController{}, "post:Add")
+	beego.Router("/course/update", &controllers.CourseController{}, "post:Update")
+	beego.Router("/course/delete", &controllers.CourseController{}, "post:Delete")
+
+	beego.Router("/course/detail/get", &controllers.CourseController{}, "post:GetDetail")
+	beego.Router("/course/detail/set", &controllers.CourseController{}, "post:SetDetail")
+	beego.Router("/coursetype/list", &controllers.CourseController{}, "post:GetCourseTypeList")
+
+	beego.Router("/course/copy", &controllers.CourseController{}, "post:CourseCopy")
+	beego.Router("/course/import", &controllers.CourseController{}, "post:CourseImport")
+	beego.Router("/course/export", &controllers.CourseController{}, "post:CourseExport")
+
+	beego.Router("/role/list", &controllers.CourseController{}, "post:GetRoleList")
+	beego.Router("/templateoperation/list", &controllers.CourseController{}, "post:GetTemplateOperationList")
+	beego.Router("/templatefault/list", &controllers.CourseController{}, "post:GetTemplateFaultList")
+	beego.Router("/templatevoice/list", &controllers.CourseController{}, "post:GetTemplateVoiceList")
+
+	beego.Router("/static/upload", &controllers.CourseController{}, "post:Upload")
+
+	beego.Router("/dict/list", &controllers.CourseController{}, "post:GetDictList")
+	beego.Router("/dict/listall", &controllers.CourseController{}, "post:GetDictListAll")
+
+	beego.Router("/course/run", &controllers.CourseRunController{}, "post:CourseRun")
+	beego.Router("/course/stop", &controllers.CourseRunController{}, "post:CourseStop")
+	beego.Router("/course/pause", &controllers.CourseRunController{}, "post:CoursePause")
+	beego.Router("/course/status", &controllers.CourseRunController{}, "post:CourseStatus")
+
+	beego.Router("/eva/list", &controllers.EvaController{}, "post:List")
+	beego.Router("/eva/add", &controllers.EvaController{}, "post:Add")
+	beego.Router("/eva/delete", &controllers.EvaController{}, "post:Delete")
+	beego.Router("/eva/update", &controllers.EvaController{}, "post:Update")
+
+	beego.Router("/eva/record/list", &controllers.EvaController{}, "post:EvaRecordList")
+	beego.Router("/eva/setscore", &controllers.EvaController{}, "post:SetScore")
+
+	//电调
+	beego.Router("/courserun/eqpmtstatus", &controllers.EDispatchController{}, "post:Eqpmtstatus")
+	beego.Router("/courserun/add_ctrl_log", &controllers.EDispatchController{}, "post:Addctrllog")
+
+	//获取设备状态 UI->Server->电调
+
+	beego.Router("/serverctrl/subsysstatus", &controllers.EDispatchController{}, "post:SubSysStatus")
+	beego.Router("/serverctrl/alleqpmtList", &controllers.EDispatchController{}, "post:AllEqpmtList")
+
+	//运行记录
+	beego.Router("/runrecord/list ", &controllers.CourseRunController{}, "post:RunRecordList")
+}

+ 18 - 0
Tech/run.sh

@@ -0,0 +1,18 @@
+#!/bin/bash
+
+# 切换到svr目录
+cd svr || {
+    echo "错误:无法切换到svr目录"
+    exit 1
+}
+
+# 杀死名为tech_service的screen会话
+pkill screen
+
+# 等待一段时间确保screen进程完全终止
+sleep 1
+
+# 创建新的screen会话并运行Tech程序
+screen -S tech_service -d -m ./Tech
+
+echo "tech_service已重新启动"

+ 1 - 0
Tech/static/js/reload.min.js

@@ -0,0 +1 @@
+function b(a){var c=new WebSocket(a);c.onclose=function(){setTimeout(function(){b(a)},2E3)};c.onmessage=function(){location.reload()}}try{if(window.WebSocket)try{b("ws://localhost:12450/reload")}catch(a){console.error(a)}else console.log("Your browser does not support WebSockets.")}catch(a){console.error("Exception during connecting to Reload:",a)};

+ 42 - 0
Tech/tests/default_test.go

@@ -0,0 +1,42 @@
+package test
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+	"runtime"
+	"path/filepath"
+
+    "github.com/beego/beego/v2/core/logs"
+
+	_ "Tech/routers"
+
+	beego "github.com/beego/beego/v2/server/web"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func init() {
+	_, file, _, _ := runtime.Caller(0)
+	apppath, _ := filepath.Abs(filepath.Dir(filepath.Join(file, ".." + string(filepath.Separator))))
+	beego.TestBeegoInit(apppath)
+}
+
+
+// TestBeego is a sample to run an endpoint test
+func TestBeego(t *testing.T) {
+	r, _ := http.NewRequest("GET", "/", nil)
+	w := httptest.NewRecorder()
+	beego.BeeApp.Handlers.ServeHTTP(w, r)
+
+	logs.Trace("testing", "TestBeego", "Code[%d]\n%s", w.Code, w.Body.String())
+
+	Convey("Subject: Test Station Endpoint\n", t, func() {
+	        Convey("Status Code Should Be 200", func() {
+	                So(w.Code, ShouldEqual, 200)
+	        })
+	        Convey("The Result Should Not Be Empty", func() {
+	                So(w.Body.Len(), ShouldBeGreaterThan, 0)
+	        })
+	})
+}
+

+ 190 - 0
Tech/views/index.html

@@ -0,0 +1,190 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>跨智能流程图功能页面</title>
+    <style>
+        #flowchart-container {
+            width: 100vw;
+            height: 100vh;
+            border: 1px solid #ccc;
+            position: relative;
+            overflow: hidden;
+        }
+        .node {
+            position: absolute;
+            width: 120px;
+            height: 60px;
+            border: 2px solid #3498db;
+            background-color: #f0f8ff;
+            border-radius: 5px;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            cursor: move;
+            user-select: none;
+        }
+        .edge {
+            position: absolute;
+            height: 2px;
+            background-color: #7f8c8d;
+            transform-origin: 0 0;
+        }
+        .toolbar {
+            position: fixed;
+            top: 10px;
+            left: 10px;
+            background-color: #fff;
+            padding: 10px;
+            border: 1px solid #ccc;
+            border-radius: 5px;
+            z-index: 100;
+        }
+        button {
+            margin: 5px;
+            padding: 5px 10px;
+        }
+    </style>
+</head>
+<body>
+    <div class="toolbar">
+        <button id="add-node">添加节点</button>
+        <button id="connect-nodes">连接节点</button>
+        <button id="save-diagram">保存流程图</button>
+    </div>
+    <div id="flowchart-container"></div>
+
+    <script>
+        // 获取DOM元素
+        const container = document.getElementById('flowchart-container');
+        const addNodeBtn = document.getElementById('add-node');
+        const connectNodesBtn = document.getElementById('connect-nodes');
+        const saveDiagramBtn = document.getElementById('save-diagram');
+
+        // 存储节点和边
+        let nodes = [];
+        let edges = [];
+        let isConnecting = false;
+        let startNode = null;
+
+        // 添加节点函数
+        function addNode(x, y) {
+            const node = document.createElement('div');
+            node.className = 'node';
+            node.textContent = `节点 ${nodes.length + 1}`;
+            node.style.left = x + 'px';
+            node.style.top = y + 'px';
+            container.appendChild(node);
+
+            const nodeData = {
+                element: node,
+                x: x,
+                y: y,
+                id: nodes.length
+            };
+            nodes.push(nodeData);
+
+            // 添加拖拽功能
+            let isDragging = false;
+            let offsetX, offsetY;
+
+            node.addEventListener('mousedown', (e) => {
+                isDragging = true;
+                offsetX = e.clientX - x;
+                offsetY = e.clientY - y;
+                node.style.zIndex = 10;
+
+                if (isConnecting && !startNode) {
+                    startNode = nodeData;
+                }
+            });
+
+            document.addEventListener('mousemove', (e) => {
+                if (isDragging) {
+                    x = e.clientX - offsetX;
+                    y = e.clientY - offsetY;
+                    node.style.left = x + 'px';
+                    node.style.top = y + 'px';
+                    nodeData.x = x;
+                    nodeData.y = y;
+                    updateEdges();
+                }
+            });
+
+            document.addEventListener('mouseup', () => {
+                isDragging = false;
+                node.style.zIndex = 1;
+
+                if (isConnecting && startNode && startNode !== nodeData) {
+                    addEdge(startNode, nodeData);
+                    startNode = null;
+                    isConnecting = false;
+                }
+            });
+        }
+
+        // 添加边函数
+        function addEdge(fromNode, toNode) {
+            const edge = document.createElement('div');
+            edge.className = 'edge';
+            container.appendChild(edge);
+
+            const edgeData = {
+                element: edge,
+                from: fromNode,
+                to: toNode
+            };
+            edges.push(edgeData);
+
+            updateEdgePosition(edgeData);
+        }
+
+        // 更新边的位置
+        function updateEdgePosition(edgeData) {
+            const fromX = edgeData.from.x + edgeData.from.element.offsetWidth / 2;
+            const fromY = edgeData.from.y + edgeData.from.element.offsetHeight / 2;
+            const toX = edgeData.to.x + edgeData.to.element.offsetWidth / 2;
+            const toY = edgeData.to.y + edgeData.to.element.offsetHeight / 2;
+
+            const dx = toX - fromX;
+            const dy = toY - fromY;
+            const length = Math.sqrt(dx * dx + dy * dy);
+            const angle = Math.atan2(dy, dx) * (180 / Math.PI);
+
+            edgeData.element.style.left = fromX + 'px';
+            edgeData.element.style.top = fromY + 'px';
+            edgeData.element.style.width = length + 'px';
+            edgeData.element.style.transform = `rotate(${angle}deg)`;
+        }
+
+        // 更新所有边的位置
+        function updateEdges() {
+            edges.forEach(edge => {
+                updateEdgePosition(edge);
+            });
+        }
+
+        // 按钮事件监听
+        addNodeBtn.addEventListener('click', () => {
+            addNode(100 + nodes.length * 150, 100);
+        });
+
+        connectNodesBtn.addEventListener('click', () => {
+            isConnecting = true;
+        });
+
+        saveDiagramBtn.addEventListener('click', () => {
+            // 这里可以实现保存流程图数据的逻辑
+            console.log('保存流程图数据:', { nodes, edges });
+        });
+
+        // 点击空白处添加节点
+        container.addEventListener('click', (e) => {
+            if (e.target === container) {
+                addNode(e.clientX, e.clientY);
+            }
+        });
+    </script>
+</body>
+</html>

Разница между файлами не показана из-за своего большого размера
+ 95 - 0
Tech/views/index.tpl


+ 13 - 0
Tech/新建 文本文档.txt

@@ -0,0 +1,13 @@
+# 创建新的 screen 会话
+screen -S tech_service
+
+# 在会话中运行服务
+./Tech
+
+# 按 Ctrl+A 然后按 D 分离会话
+# 重新连接:screen -r tech_service
+
+
+screen -ls
+
+pkill screen