波比 10 mēneši atpakaļ
vecāks
revīzija
52191b2780

+ 23 - 0
Tech/210key/server.crt

@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIIDxzCCAq+gAwIBAgIUb40FnscX57Yky6ZQdiHpVYGcT9AwDQYJKoZIhvcNAQEL
+BQAwczELMAkGA1UEBhMCTVkxCzAJBgNVBAgMAk1ZMQswCQYDVQQHDAJNWTELMAkG
+A1UECgwCTVkxCzAJBgNVBAsMAk1ZMRYwFAYDVQQDDA0xOTIuMTY4LjEuMjEwMRgw
+FgYJKoZIhvcNAQkBFgkxMUBxcS5jb20wHhcNMjUxMTEyMDY0OTAxWhcNMzUxMTEw
+MDY0OTAxWjBzMQswCQYDVQQGEwJNWTELMAkGA1UECAwCTVkxCzAJBgNVBAcMAk1Z
+MQswCQYDVQQKDAJNWTELMAkGA1UECwwCTVkxFjAUBgNVBAMMDTE5Mi4xNjguMS4y
+MTAxGDAWBgkqhkiG9w0BCQEWCTExQHFxLmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAMRxrVj3Uv8DaZmZ8+W1AhQ0MzVi49D8dh7xqNOh4jJwCPXm
+RxUiKybjI4wW6Vf5ADiWb17obHoARldtH/81Ja4Af+H1vi1gE4MPXhhKDzmeBflp
+74KAOFq4Ii/1U+1DAjRaalYiqaWLkuvqRfJHK5voCRMaMBP9NQJzggq1drMcOg1a
+SJPnxOq+tXeNha6TUjEX7Xxak9KEa7sGT5HiZJBh9TZ/T/oDRbK2KHBe9jEgXRyM
+5XTiP5x+X4Mvuasp7fBR5oHo0eZ7ByCaOgQJIeyw2qSKWlNFQEBS+MVLICKdzN46
+GrcmlvGazryp6YxpD6HT+lLikW9L46DSUwgx+h8CAwEAAaNTMFEwHQYDVR0OBBYE
+FKthVSN9viF7V3UWk1ztxpK3zE5rMB8GA1UdIwQYMBaAFKthVSN9viF7V3UWk1zt
+xpK3zE5rMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAILDele5
+vusd1WPETNy0S2lbmQfV0tmKjtKj75AXkp232XdhsxhweCclsI/Yz7Hg7AnsyJGg
+SJ5MyrGt3UsvIquxh6vI/IvIToRzeWehgjetEMBSqqWOa4yYNTu3cg71MLpt0u4W
+K+K10gqBISps+O82FFBCWpS2z3dBn23PH61QcrmscytFUXRH0UuKosC9doTZziUR
+/ZnpVtIoshgrmquAC5Ck1KF3LMvk5oYeCoGk7y0Kyscnyt7tizaSA0VbUKeDp68W
+w2p5/yx6AoBrv7kfbCJhLS+937NbVor7EmJ3eb4JOjOjStB0rmoo4qyNLc13CRKt
+N3ChwcTuZpCiY7s=
+-----END CERTIFICATE-----

+ 28 - 0
Tech/210key/server.key

@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDEca1Y91L/A2mZ
+mfPltQIUNDM1YuPQ/HYe8ajToeIycAj15kcVIism4yOMFulX+QA4lm9e6Gx6AEZX
+bR//NSWuAH/h9b4tYBODD14YSg85ngX5ae+CgDhauCIv9VPtQwI0WmpWIqmli5Lr
+6kXyRyub6AkTGjAT/TUCc4IKtXazHDoNWkiT58TqvrV3jYWuk1IxF+18WpPShGu7
+Bk+R4mSQYfU2f0/6A0WytihwXvYxIF0cjOV04j+cfl+DL7mrKe3wUeaB6NHmewcg
+mjoECSHssNqkilpTRUBAUvjFSyAinczeOhq3Jpbxms68qemMaQ+h0/pS4pFvS+Og
+0lMIMfofAgMBAAECggEABPIs1b+8ZbA+PYrxGqY5DT5fv7HBpB3+m6D6I2hafdDf
+6fKmQcj12dhVQv06jiNYe1iGS1LXcnxAwJrks4uGSSSF8Z7UqqMi9lzLbG8ihYhj
+Wfx2oR1BvOmqmZ+3/OeAAS0Kq2yn3PKSFhbsAJbii1Vcme3siF3okICAacJYRzV6
+qHfBb0wR9RhB98gAvi/mDH88du6Dy9u9OrGwRrxy2tHqQnxRR7S5kxoNgjceRUCl
+kYAVGEEUR8J2bq1H8DxjKqPtL0hkU3vNqgia8AQIyDyHgu44VyFITollif4DJX+q
+NKVOT8/PgERJsKYwMd+w8ZgzG7dxCjVZtICLc/wyiQKBgQD1JjG96SRmGhW7VjIU
+KOvirypIqI+aiOrS6OMVdkz0NGj9LzRQ3DeppEjjavBGpz54kyfim6iZEnUPyB3H
+kyLjfF+FJZB6gPUk7x2d3SU9e0OQimW9zuK84qdtANH+peU9WQ5J2S4tjCocxZ3U
+Tgzx8wbZjEaUG5L+Z4wnRpkiZQKBgQDNI5nQFMiTJVYEMCGt2r1WJapuOJ6qRWE0
+Hbdn1zphYdzFkzzNCCgwEjVH//JG5Hq3x8PzUBSx29kf5LGJOoMldEi1p6VYAWSR
+A/zGVnuX/KMPf+iB6Yvb25czKRuhDROgAq/h0ltbLuyEpC//WR/ixsEP/ePj/+TD
+YjGVvd2gMwKBgCMRfqYbFtDgRGGTGADYRb4crqDMNBa4JVdcZzenv6RjZu1o9kPm
+7LOAw8h8gLiqrf4Tlu4MtFaJVGOYjeIBw0jlfsiOMhfEIM1Sw1rUysH7wxOHKLy5
+PqgkiCzIpEGYO63F8oxP3BI57cBF6Kj5LVLjIAS401PWifWInHz11WwdAoGAUI6S
+cGw498/Jn4tnLmafw5y2+OGzLRphuZ2R0G1st6E+mTfgwINbhMUht0yDT+OZbWI9
+SOdBuTakZSnhrgoURjkL9IoJHC4v1+LA+QItNto3DnckTx/vlD7/eU/DUdNYyZVE
+jGGXDo+2wb/YOWY+Z0e1uRnyzC5JH5EH982/opECgYEA3TtDOZXj+LxGfwS2qCuJ
+a5p8/FLuoubGfYt7v4h3FCYUgP/3i+35EdFsrcS5VinhkDK8v7nDbYz7vAhV/e9H
+pGVbjG52KeLELnXbusbDPaeMHB8KV64FnJ0KS0ly8FRuE0MsRLSQopwOIViGVgFK
+XMjKuSq9KJCHQJIH3yuKa50=
+-----END PRIVATE KEY-----

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1027 - 0
Tech/common/信息化接口定义.go


+ 26 - 3
Tech/conf/app.conf

@@ -11,12 +11,35 @@ ElectricalPort = 7214
 
 # 数据库配置
 DB.Adapter = mysql
-DB.URL = dbadmin:dbadmin@987@tcp(124.222.28.5:7312)/CCGD_project?charset=utf8
+DB.URL = yd_fms:fms@123@tcp(124.222.28.5:7212)/CCGD_project?charset=utf8
+
+# 信息化服务器和地址
+ESYSIP = 124.222.28.5
+ESYSPort = 7321
+
+# 信息化请求的TOKEN
+SYSTOKEN = 6000-0009-8720-2301
+# 设备号
+SYSDEVICEID = M0026
+
+SYSCLLX = 牵引变电实训室
+SYSSYCX = 牵引变电实训系统
+
+# 配电实训设备-设备号:M0007 token:6000-0009-8720-2301
+# 牵引变电实训设备-设备号:M0026 token:6000-0009-8720-2301
+
+
+maxmemory = 32 << 20  # 32MB
+maxUploadSize = 32 << 20  # 10MB
+
+
+EGetAllDataInterval = 10 #电调获取全量数据的间隔时间
 
 
 # HTTPS配置
 EnableHTTPS = true
-EnableHttpTLS = true
+EnableHttpTLS = true  
 HTTPSPort = 8443
 HTTPSCertFile = "conf/server.crt"
-HTTPSKeyFile = "conf/server.key"
+HTTPSKeyFile = "conf/server.key"
+

+ 133 - 28
Tech/controllers/用户.go

@@ -2,12 +2,13 @@
 package controllers
 
 import (
-	"Tech/models"
+	"Tech/common"
 	"crypto/rand"
 	"encoding/base64"
 	"encoding/json"
+	"sync"
 
-	"github.com/beego/beego/v2/client/orm"
+	"github.com/beego/beego/v2/core/logs"
 	beego "github.com/beego/beego/v2/server/web"
 )
 
@@ -15,7 +16,63 @@ type UserController struct {
 	beego.Controller
 }
 
+// 用户信息缓存,key为userName,value为用户信息
+var userInfoCache = make(map[string]common.PersonalInfo)
+var cacheMutex sync.RWMutex // 保护缓存的读写锁
+
 var simpleTokenStore = make(map[string]string) // token -> userId
+
+// GetUserInfoFromCache 通过用户名从缓存中获取用户信息
+func GetUserInfoFromCache(userName string) (common.PersonalInfo, bool) {
+	// 先尝试读取缓存
+	cacheMutex.RLock()
+	userInfo, exists := userInfoCache[userName]
+	defer cacheMutex.RUnlock()
+
+	if exists {
+		return userInfo, true
+	}
+
+	return common.PersonalInfo{}, false
+}
+
+// GetUserInfoFromCache 通过人员GUID查询
+func GetUserInfoFromCache_RYEMPID(RYEMPID string) (common.PersonalInfo, bool) {
+	// 先尝试读取缓存
+	cacheMutex.RLock()
+	defer cacheMutex.RUnlock()
+
+	// 遍历缓存查找匹配的RYEMPID
+	for _, userInfo := range userInfoCache {
+		if userInfo.RYEMPID == RYEMPID {
+			return userInfo, true
+		}
+	}
+
+	return common.PersonalInfo{}, false
+}
+
+// initUserInfoCache 初始化用户信息缓存
+func InitUserInfoCache() {
+	logs.Info("开始初始化用户信息缓存...")
+
+	personalInfos, err := common.GetAllPersionInfo()
+	if err != nil {
+		logs.Error("获取所有用户信息失败: ", err)
+		return
+	}
+
+	// 加锁更新缓存
+	cacheMutex.Lock()
+	defer cacheMutex.Unlock()
+
+	for _, info := range personalInfos {
+		// 使用RYEMPID作为用户名缓存用户信息
+		userInfoCache[info.RYID] = info
+	}
+
+	logs.Info("用户信息缓存初始化完成,共缓存%d个用户", len(personalInfos))
+}
 func generateShortToken(userId string) string {
 	b := make([]byte, 6)
 	rand.Read(b)
@@ -61,49 +118,97 @@ func (c *UserController) UserLogin() {
 		return
 	}
 
-	o := orm.NewOrm()
-	var user models.UserData
-	err := o.QueryTable(new(models.UserData)).Filter("用户名称", req.UserName).One(&user)
+	// 使用信息化系统进行身份验证
+	loginResp, err := common.LoginToInformationSystem(req.UserName, req.Password)
 	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.Ctx.Output.SetStatus(500)
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "信息化系统验证失败: " + err.Error(),
 		}
 		c.ServeJSON()
 		return
 	}
 
-	if user.Password != req.Password {
+	// 检查登录结果
+	if loginResp.ResultCode != "0" {
 		c.Ctx.Output.SetStatus(401)
 		c.Data["json"] = map[string]interface{}{
 			"code": 401,
-			"msg":  "用户名或密码错误",
+			"msg":  loginResp.ResultDesc,
 		}
 		c.ServeJSON()
 		return
 	}
 
-	tokenString := generateShortToken(user.UserName)
+	// 登录成功,获取用户详细信息
+	personalInfoResp, err := common.GetPersonalInfoFromInformationSystem(loginResp.RYEMPID)
+	if err != nil {
+		c.Ctx.Output.SetStatus(500)
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "获取用户信息失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 检查获取用户信息的结果
+	if personalInfoResp.ResultCode != "0" {
+		c.Ctx.Output.SetStatus(500)
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "获取用户信息失败: " + personalInfoResp.ResultDesc,
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 获取第一个用户的信息(通常只有一个)
+	var personalInfo common.PersonalInfo
+	if len(personalInfoResp.PersonalInfos) > 0 {
+		personalInfo = personalInfoResp.PersonalInfos[0]
+	}
+
+	// 将用户信息存入缓存
+	cacheMutex.Lock()
+	userInfoCache[req.UserName] = personalInfo
+	cacheMutex.Unlock()
+
+	tokenString := generateShortToken(req.UserName)
+
+	// 构建返回给前端的用户信息
+	userInfo := map[string]interface{}{
+		"token":          tokenString,
+		"teacherId":      personalInfo.TeacherId,
+		"orgid":          personalInfo.Orgid,
+		"RYID":           personalInfo.RYID,
+		"RYEMPID":        personalInfo.RYEMPID,
+		"KQID":           personalInfo.KQID,
+		"XM":             personalInfo.XM,
+		"XB":             personalInfo.XB,
+		"CSRQ":           personalInfo.CSRQ,
+		"MM":             personalInfo.MM,
+		"SSDW":           personalInfo.SSDW,
+		"JG":             personalInfo.JG,
+		"Fingerprints":   personalInfo.Fingerprints,
+		"SF":             personalInfo.SF,
+		"SFZH":           personalInfo.SFZH,
+		"SJH":            personalInfo.SJH,
+		"FaceData":       personalInfo.FaceData,
+		"FaceDataLength": personalInfo.FaceDataLength,
+		"ZW":             personalInfo.ZW,
+		"SSJG":           personalInfo.SSJG,
+		"GZ":             personalInfo.GZ,
+		"JSDJ":           personalInfo.JSDJ,
+		"XL":             personalInfo.XL,
+		"Count":          personalInfo.Count,
+	}
 
 	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": "登录成功",
+		"data": userInfo,
+		"msg":  "登录成功",
 	}
 	c.ServeJSON()
 }

+ 206 - 0
Tech/controllers/电子考评.go

@@ -3,11 +3,14 @@ package controllers
 import (
 	// 根据实际项目结构调整导入路径
 
+	"Tech/common"
 	"Tech/models"
 	"bytes"
 	"encoding/json"
 	"fmt"
 	"io/ioutil"
+	"mime/multipart"
+	"net/http"
 	"os"
 	"os/exec"
 	"time"
@@ -1478,3 +1481,206 @@ func (c *EvaLogController) generateOperHTMLContent(courseRunLog models.CourseRun
 
 	return buf.String()
 }
+
+// /执行上传对应电子考评表到信息还系统
+func DoUploadTMS(RunLogID int) error {
+	o := orm.NewOrm()
+
+	// 查询课程运行记录信息
+	var courseRunLog models.CourseRunLog
+	err := o.Raw(`SELECT * FROM CourseRunLog WHERE 运行记录编号 = ?`, RunLogID).QueryRow(&courseRunLog)
+	if err != nil {
+		return fmt.Errorf("查询课程运行记录失败: %v", err)
+	}
+
+	// 查询该课程运行记录下的所有评价项
+	var evaLogs []models.CourseEvaLog
+	_, err = o.Raw(`
+		SELECT * 
+		FROM CourseEvaLog 
+		WHERE 运行记录编号 = ?
+		ORDER BY 序号
+	`, RunLogID).QueryRows(&evaLogs)
+
+	if err != nil {
+		return fmt.Errorf("查询评价记录列表失败: %v", err)
+	}
+
+	// 查询所有角色信息用于关联角色名称
+	var roleInfos []*models.RoleInfo
+	_, err = o.QueryTable(new(models.RoleInfo)).All(&roleInfos)
+	if err != nil {
+		return fmt.Errorf("查询角色信息失败: %v", err)
+	}
+
+	// 创建角色ID到角色名称的映射
+	roleMap := make(map[int]string)
+	for _, role := range roleInfos {
+		roleMap[role.RoleID] = role.RoleName
+	}
+
+	// 生成HTML内容
+	controller := &EvaLogController{}
+	htmlContent := controller.generateHTMLContent(courseRunLog, evaLogs, roleMap)
+
+	// 创建临时HTML文件
+	htmlFile, err := os.CreateTemp("", "course_evaluation_report_*.html")
+	if err != nil {
+		return fmt.Errorf("创建HTML文件失败: %v", err)
+	}
+	defer os.Remove(htmlFile.Name())
+
+	// 写入HTML内容
+	_, err = htmlFile.WriteString(htmlContent)
+	if err != nil {
+		htmlFile.Close()
+		return fmt.Errorf("写入HTML文件失败: %v", err)
+	}
+	htmlFile.Close()
+
+	// 创建临时PDF文件
+	pdfFile, err := os.CreateTemp("", "course_evaluation_report_*.pdf")
+	if err != nil {
+		return fmt.Errorf("创建PDF文件失败: %v", err)
+	}
+	defer os.Remove(pdfFile.Name())
+	pdfFile.Close()
+
+	// 使用wkhtmltopdf将HTML转换为PDF
+	cmd := exec.Command("wkhtmltopdf", "--encoding", "utf-8", htmlFile.Name(), pdfFile.Name())
+	var stderr bytes.Buffer
+	cmd.Stderr = &stderr
+
+	err = cmd.Run()
+	if err != nil {
+		return fmt.Errorf("HTML转PDF失败: %v; %s", err, stderr.String())
+	}
+
+	// 准备上传到信息化系统的数据
+	// 学员GUID(可能有多个,用逗号分隔)
+	traineeGUIDs := courseRunLog.TraineeGUID
+
+	// 课程得分
+	score := fmt.Sprintf("%.2f", courseRunLog.CourseScore)
+
+	// 构造请求URL
+	url := common.GetAPIEndpoint(common.SVR_API_UPLOAD_AbilityScore)
+
+	// 创建multipart表单数据
+	var buf bytes.Buffer
+	writer := multipart.NewWriter(&buf)
+
+	// 添加表单字段
+	_ = writer.WriteField("PXJLGUID", courseRunLog.TrainingRecordGUID)
+	_ = writer.WriteField("XYEMPID", traineeGUIDs)
+	_ = writer.WriteField("DF", score)
+
+	// 添加文件字段
+	fileWriter, _ := writer.CreateFormFile("FILE", fmt.Sprintf("course_evaluation_report_%d.pdf", RunLogID))
+	pdfData, _ := ioutil.ReadFile(pdfFile.Name())
+	_, _ = fileWriter.Write(pdfData)
+
+	// 添加Content-Type字段
+	_ = writer.WriteField("Content-Type", "multipart/form-data")
+
+	// 关闭multipart writer
+	writer.Close()
+
+	// 创建请求
+	req, err := http.NewRequest("POST", url, &buf)
+	if err != nil {
+		return fmt.Errorf("创建请求失败: %v", err)
+	}
+
+	// 设置请求头
+	req.Header.Set("Content-Type", writer.FormDataContentType())
+
+	// 从配置中获取token
+	token, err := beego.AppConfig.String("SYSTOKEN")
+	if err == nil && token != "" {
+		req.Header.Set("token", token)
+	}
+
+	// 发送请求
+	client := &http.Client{}
+	resp, err := client.Do(req)
+	if err != nil {
+		return fmt.Errorf("发送请求失败: %v", err)
+	}
+	defer resp.Body.Close()
+
+	// 读取响应
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return fmt.Errorf("读取响应失败: %v", err)
+	}
+
+	// 检查状态码
+	if resp.StatusCode != http.StatusOK {
+		return fmt.Errorf("信息化系统返回错误状态码: %d, 响应内容: %s", resp.StatusCode, string(body))
+	}
+
+	// 解析响应
+	var result struct {
+		ResultCode string `json:"ResultCode"`
+		ResultDesc string `json:"ResultDesc"`
+	}
+	err = json.Unmarshal(body, &result)
+	if err != nil {
+		return fmt.Errorf("解析响应失败: %v", err)
+	}
+
+	// 检查结果
+	if result.ResultCode != "0" {
+		return fmt.Errorf("信息化系统处理失败: %s", result.ResultDesc)
+	}
+
+	return nil
+}
+
+// /evalog/uploadtms 课程评价记录上传信息化系统
+func (c *EvaLogController) UploadTMS() {
+	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":  "缺少运行记录ID参数",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	err := DoUploadTMS(req.RunLogID)
+	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":  "上传信息还系统成功",
+	}
+	c.ServeJSON()
+
+}

+ 409 - 467
Tech/controllers/电调.go

@@ -15,140 +15,140 @@ import (
 )
 
 func init() {
-	// 启动后台协程定期更新电调模型数据
-	go func() {
-		// 初始化延迟1秒执行,避免系统启动时的资源竞争
-		time.Sleep(1 * time.Second)
-
-		// 定时每5秒获取一次数据
-		ticker := time.NewTicker(5 * time.Second)
-		defer ticker.Stop()
-
-		for {
-			select {
-			case <-ticker.C:
-				updateElectricalData()
-			}
-		}
-	}()
+	// // 启动后台协程定期更新电调模型数据
+	// go func() {
+	// 	// 初始化延迟1秒执行,避免系统启动时的资源竞争
+	// 	time.Sleep(1 * time.Second)
+
+	// 	// 定时每5秒获取一次数据
+	// 	ticker := time.NewTicker(5 * time.Second)
+	// 	defer ticker.Stop()
+
+	// 	for {
+	// 		select {
+	// 		case <-ticker.C:
+	// 			updateElectricalData()
+	// 		}
+	// 	}
+	// }()
 }
 
-// updateElectricalData 从电调服务获取数据并更新模型
-func updateElectricalData() {
-	defer func() {
-		if r := recover(); r != nil {
-			fmt.Printf("更新电调数据时发生错误: %v\n", r)
-		}
-	}()
-
-	// 获取 ElectricModelManager 单例实例
-	manager := models.GetInstance()
-
-	// 获取模型列表副本,避免长时间持有锁
-	manager.Mutex.RLock()
-	modelsCopy := make(map[string]models.EViewMapModel)
-	for k, v := range manager.Models {
-		modelsCopy[k] = v
-	}
-	manager.Mutex.RUnlock()
-
-	// 如果 Models 为空,不进行任何处理
-	if len(modelsCopy) == 0 {
-		return
-	}
-
-	// 遍历已有模型并分别更新
-	for _, viewMapModel := range modelsCopy {
-		if len(viewMapModel.Models) > 0 && time.Since(viewMapModel.LastBatchUpdate) >= 5*time.Second {
-			// 获取第一个模型作为参考来构建请求参数
-			firstModel := viewMapModel.Models[0]
-
-			// 从配置获取电调服务地址
-			electricalIP, err := beego.AppConfig.String("ElectricalIP")
-			if err != nil {
-				fmt.Printf("读取电调服务IP配置失败: %v\n", err)
-				continue
-			}
-
-			electricalPort, err := beego.AppConfig.Int("ElectricalPort")
-			if err != nil {
-				fmt.Printf("读取电调服务端口配置失败: %v\n", err)
-				continue
-			}
-
-			// 构造电调服务地址
-			electricalAddr := fmt.Sprintf("http://%s:%d/ServerCtrl/AllEqpmtList", electricalIP, electricalPort)
-
-			// 准备请求数据
-			type RequestData struct {
-				LineID    int `json:"lineID"`
-				StationID int `json:"StationID"`
-				ViewID    int `json:"ViewID"`
-			}
-			reqData := RequestData{
-				LineID: firstModel.LineID,
-				// StationID: firstModel.StationID,
-				// ViewID:    firstModel.ViewID,
-			}
-
-			// 将请求数据转换为JSON
-			reqBody, err := json.Marshal(reqData)
-			if err != nil {
-				fmt.Printf("构造请求数据失败: %v\n", err)
-				continue
-			}
-
-			// 发送POST请求获取数据
-			resp, err := http.Post(electricalAddr, "application/json", bytes.NewBuffer(reqBody))
-			if err != nil {
-				fmt.Printf("调用电调服务获取所有设备列表接口失败: %v\n", err)
-				continue
-			}
+// // updateElectricalData 从电调服务获取数据并更新模型
+// func updateElectricalData() {
+// 	defer func() {
+// 		if r := recover(); r != nil {
+// 			fmt.Printf("更新电调数据时发生错误: %v\n", r)
+// 		}
+// 	}()
 
-			// 读取响应体
-			bodyBytes, err := io.ReadAll(resp.Body)
-			resp.Body.Close()
-			if err != nil {
-				fmt.Printf("读取电调服务响应失败: %v\n", err)
-				continue
-			}
+// 	// 获取 ElectricModelManager 单例实例
+// 	manager := models.GetInstance()
 
-			// 检查响应状态
-			if resp.StatusCode != http.StatusOK {
-				fmt.Printf("电调服务返回异常状态码: %d,响应内容: %s\n", resp.StatusCode, string(bodyBytes))
-				continue
-			}
+// 	// 获取模型列表副本,避免长时间持有锁
+// 	manager.Mutex.RLock()
+// 	modelsCopy := make(map[string]models.EViewMapModel)
+// 	for k, v := range manager.Models {
+// 		modelsCopy[k] = v
+// 	}
+// 	manager.Mutex.RUnlock()
 
-			// 解析JSON响应
-			var result map[string]interface{}
-			if err := json.Unmarshal(bodyBytes, &result); err != nil {
-				fmt.Printf("解析电调服务响应失败: %v\n", err)
-				continue
-			}
+// 	// 如果 Models 为空,不进行任何处理
+// 	if len(modelsCopy) == 0 {
+// 		return
+// 	}
 
-			// 检查是否有data字段
-			if data, exists := result["data"]; exists {
-				// 解析data中的设备列表并更新到模型管理器
-				var equipments []models.DiandiaoModel
-				if equipBytes, err := json.Marshal(data); err == nil {
-					if err := json.Unmarshal(equipBytes, &equipments); err == nil {
-						// 更新总包设备数据 (在更新时加锁)
-						manager.UpdateBatchData(equipments)
-						fmt.Printf("成功更新线路%d的电调模型数据\n", firstModel.LineID)
-					} else {
-						fmt.Printf("解析设备列表失败: %v\n", err)
-					}
-				}
-			}
-		}
-	}
-}
+// 	// 遍历已有模型并分别更新
+// 	for _, viewMapModel := range modelsCopy {
+// 		if len(viewMapModel.Models) > 0 && time.Since(viewMapModel.LastBatchUpdate) >= 5*time.Second {
+// 			// 获取第一个模型作为参考来构建请求参数
+// 			firstModel := viewMapModel.Models[0]
+
+// 			// 从配置获取电调服务地址
+// 			electricalIP, err := beego.AppConfig.String("ElectricalIP")
+// 			if err != nil {
+// 				fmt.Printf("读取电调服务IP配置失败: %v\n", err)
+// 				continue
+// 			}
+
+// 			electricalPort, err := beego.AppConfig.Int("ElectricalPort")
+// 			if err != nil {
+// 				fmt.Printf("读取电调服务端口配置失败: %v\n", err)
+// 				continue
+// 			}
+
+// 			// 构造电调服务地址
+// 			electricalAddr := fmt.Sprintf("http://%s:%d/ServerCtrl/AllEqpmtList", electricalIP, electricalPort)
+
+// 			// 准备请求数据
+// 			type RequestData struct {
+// 				LineID    int `json:"lineID"`
+// 				StationID int `json:"StationID"`
+// 				ViewID    int `json:"ViewID"`
+// 			}
+// 			reqData := RequestData{
+// 				LineID: firstModel.LineID,
+// 				// StationID: firstModel.StationID,
+// 				// ViewID:    firstModel.ViewID,
+// 			}
+
+// 			// 将请求数据转换为JSON
+// 			reqBody, err := json.Marshal(reqData)
+// 			if err != nil {
+// 				fmt.Printf("构造请求数据失败: %v\n", err)
+// 				continue
+// 			}
+
+// 			// 发送POST请求获取数据
+// 			resp, err := http.Post(electricalAddr, "application/json", bytes.NewBuffer(reqBody))
+// 			if err != nil {
+// 				fmt.Printf("调用电调服务获取所有设备列表接口失败: %v\n", err)
+// 				continue
+// 			}
+
+// 			// 读取响应体
+// 			bodyBytes, err := io.ReadAll(resp.Body)
+// 			resp.Body.Close()
+// 			if err != nil {
+// 				fmt.Printf("读取电调服务响应失败: %v\n", err)
+// 				continue
+// 			}
+
+// 			// 检查响应状态
+// 			if resp.StatusCode != http.StatusOK {
+// 				fmt.Printf("电调服务返回异常状态码: %d,响应内容: %s\n", resp.StatusCode, string(bodyBytes))
+// 				continue
+// 			}
+
+// 			// 解析JSON响应
+// 			var result map[string]interface{}
+// 			if err := json.Unmarshal(bodyBytes, &result); err != nil {
+// 				fmt.Printf("解析电调服务响应失败: %v\n", err)
+// 				continue
+// 			}
+
+// 			// 检查是否有data字段
+// 			if data, exists := result["data"]; exists {
+// 				// 解析data中的设备列表并更新到模型管理器
+// 				var equipments []models.DiandiaoModel
+// 				if equipBytes, err := json.Marshal(data); err == nil {
+// 					if err := json.Unmarshal(equipBytes, &equipments); err == nil {
+// 						// 更新总包设备数据 (在更新时加锁)
+// 						manager.UpdateBatchData(equipments)
+// 						fmt.Printf("成功更新线路%d的电调模型数据\n", firstModel.LineID)
+// 					} else {
+// 						fmt.Printf("解析设备列表失败: %v\n", err)
+// 					}
+// 				}
+// 			}
+// 		}
+// 	}
+// }
 
 type EDispatchController struct {
 	beego.Controller
 }
 
-// /courserun/eqpmtvalue 更新电调设备状态
+// /courserun/eqpmtstatus 更新电调设备状态
 func (c *EDispatchController) Eqpmtvalue() {
 	var req models.DiandiaoModel
 
@@ -392,82 +392,15 @@ func (c *EDispatchController) UpdateTestStatus() {
 
 }
 
-// // /serverCtrl/alleqpmtlist 获取所有设备列表
-// func (c *EDispatchController) AllEqpmtList() {
-// 	// 定义与testStatus.json文件匹配的结构体
-// 	type jsonData struct {
-// 		DataID   string `json:"dataId"`
-// 		DataName string `json:"dataName"`
-// 		Value    string `json:"value"`
-// 		ID       string `json:"id"`
-// 	}
-
-// 	// 读取testStatus.json文件
-// 	filePath := "static/testStatus.json"
-// 	data, err := os.ReadFile(filePath)
-// 	if err != nil {
-// 		c.Ctx.Output.SetStatus(500)
-// 		c.Data["json"] = map[string]interface{}{
-// 			"code": 500,
-// 			"msg":  "读取文件失败: " + err.Error(),
-// 		}
-// 		c.ServeJSON()
-// 		return
-// 	}
-
-// 	// 解析JSON数据
-// 	var jsonDataList []jsonData
-// 	err = json.Unmarshal(data, &jsonDataList)
-// 	if err != nil {
-// 		c.Ctx.Output.SetStatus(500)
-// 		c.Data["json"] = map[string]interface{}{
-// 			"code": 500,
-// 			"msg":  "解析JSON失败: " + err.Error(),
-// 		}
-// 		c.ServeJSON()
-// 		return
-// 	}
-
-// 	// 转换为EquipmentData结构体
-// 	type EquipmentData struct {
-// 		DataID   string `json:"dataId"`
-// 		DataName string `json:"dataName"`
-// 		Value    string `json:"value"`
-// 		ID       string `json:"id"`
-// 	}
-// 	//动态生成值 1-3改变
-
-// 	// 初始化随机数种子
-// 	rand.Seed(time.Now().UnixNano())
-
-// 	// 生成1-3之间的随机数
-// 	nValue := rand.Intn(3) + 1
-
-// 	var equipmentDataList []EquipmentData
-// 	for _, item := range jsonDataList {
-// 		equipment := EquipmentData{
-// 			DataID:   item.DataID,
-// 			DataName: item.DataName,
-// 			Value:    fmt.Sprintf("%d", nValue),
-// 			ID:       item.ID,
-// 		}
-// 		equipmentDataList = append(equipmentDataList, equipment)
-// 	}
-
-// 	// 返回结果
-// 	c.Data["json"] = equipmentDataList
-// 	c.ServeJSON()
-
-// }
-
 // /serverCtrl/alleqpmtlist 获取所有设备列表
 func (c *EDispatchController) AllEqpmtList() {
 	// 转换缓存数据为指定格式
 	type EquipmentData struct {
-		DataID   string `json:"dataId"`
-		DataName string `json:"dataName"`
-		Value    string `json:"value"`
-		ID       string `json:"id"`
+		DataID      string `json:"dataId"`
+		DataName    string `json:"dataName"`
+		Value       string `json:"value"`
+		ID          string `json:"id"`
+		EqpmtTypeID int    `json:"eqpmtTypeID"`
 	}
 
 	// 从配置文件读取电调服务IP和端口
@@ -491,6 +424,12 @@ func (c *EDispatchController) AllEqpmtList() {
 		return
 	}
 
+	// 获取全量数据更新间隔配置,默认10秒
+	fullDataIntervalSeconds := 10
+	if interval, err := beego.AppConfig.Int("EGetAllDataInterval"); err == nil {
+		fullDataIntervalSeconds = interval
+	}
+
 	// 构造电调服务地址
 	electricalAddr := fmt.Sprintf("http://%s:%d/ServerCtrl/AllEqpmtList", electricalIP, electricalPort)
 
@@ -533,48 +472,18 @@ func (c *EDispatchController) AllEqpmtList() {
 		}
 	}
 
-	// 生成请求key
-	model := models.DiandiaoModel{
-		LineID: lineID,
-	}
-	key := model.GenerateModelKey()
+	// 使用客户端IP作为客户端标识
+	clientID := c.Ctx.Input.IP()
 
 	// 获取 ElectricModelManager 单例实例
 	manager := models.GetInstance()
 
-	// 检查缓存中是否存在对应key的数据,且最后更新时间在5秒内
-	if viewMapModel, exists := manager.GetModelByKey(key); exists {
-		// 检查最后总包更新时间是否在2秒内
-		if time.Since(viewMapModel.LastBatchUpdate) <= 2*time.Second || time.Since(viewMapModel.LastDeviceUpdate) <= 2*time.Second {
+	// 注册客户端
+	manager.RegisterClient(clientID)
+	// 注意:不再在函数退出时注销客户端,而是由 ElectricModelManager 内部定期清理长时间未访问的客户端
 
-			var equipmentsData []EquipmentData
-			for _, model := range viewMapModel.Models {
-				equip := EquipmentData{
-					DataID:   model.EqpmtStrID,
-					ID:       model.EqpmtStrID,
-					DataName: model.EqpmtStdName,
-				}
-
-				switch model.CMDType {
-				case 0: //设备状态
-					equip.Value = fmt.Sprintf("%d", model.EqpmtStatus)
-				case 1: //设备数据控制
-					equip.Value = fmt.Sprintf("%d", model.FValue)
-				}
-				equipmentsData = append(equipmentsData, equip)
-			}
-			c.Data["json"] = equipmentsData
-
-			// // 使用缓存数据
-			// c.Data["json"] = map[string]interface{}{
-			// 	"code": 200,
-			// 	"msg":  "获取所有设备列表成功(缓存)",
-			// 	"data": equipmentsData,
-			// }
-			c.ServeJSON()
-			return
-		}
-	}
+	// 检查是否应该发送全量数据
+	shouldSendFullData := manager.ShouldSendFullData(fullDataIntervalSeconds)
 
 	// 准备请求数据
 	type RequestData struct {
@@ -654,12 +563,16 @@ func (c *EDispatchController) AllEqpmtList() {
 				}
 			}
 
+			// 构建当前设备数据映射
+			currentDataMapping := make(map[string]string)
 			var equipmentsData []EquipmentData
+			// isFirst := true
 			for _, model := range equipments {
 				equip := EquipmentData{
-					DataID:   model.EqpmtStrID,
-					ID:       model.EqpmtStrID,
-					DataName: model.EqpmtStdName,
+					DataID:      model.EqpmtStrID,
+					ID:          model.EqpmtStrID,
+					DataName:    model.EqpmtStdName,
+					EqpmtTypeID: model.EqpmtTypeID,
 				}
 
 				if model.CMDType == 0 { //设备状态
@@ -667,17 +580,43 @@ func (c *EDispatchController) AllEqpmtList() {
 				} else if model.CMDType == 1 { //设备数据控制
 					equip.Value = fmt.Sprintf("%d", model.FValue)
 				}
+				// if isFirst {
+				// 	// 生成0到100之间的随机整数
+				// 	equip.Value = fmt.Sprintf("%d", rand.Intn(101))
+				// 	isFirst = false
+
+				// }
+
+				// 添加到当前数据映射
+				currentDataMapping[model.EqpmtStrID] = equip.Value
 				equipmentsData = append(equipmentsData, equip)
 			}
 
-			c.Data["json"] = equipmentsData
+			// 如果应该发送全量数据
+			if shouldSendFullData {
+				c.Data["json"] = equipmentsData
+				manager.UpdateFullDataTimestamp()
+				manager.UpdateClientData(clientID, currentDataMapping)
+			} else {
+				// 否则只返回变化的数据
+				changedData := manager.CompareWithClientData(clientID, currentDataMapping)
+
+				// 构建变化的设备数据列表
+				var changedEquipmentsData []EquipmentData
+				for _, equip := range equipmentsData {
+					if _, changed := changedData[equip.ID]; changed {
+						changedEquipmentsData = append(changedEquipmentsData, equip)
+					}
+				}
 
-			// // 返回响应
-			// c.Data["json"] = map[string]interface{}{
-			// 	"code": 200,
-			// 	"msg":  "获取所有设备列表成功",
-			// 	"data": equipmentsData,
-			// }
+				// 确保不会返回nil,即使没有变化也返回空数组
+				if changedEquipmentsData == nil {
+					changedEquipmentsData = []EquipmentData{}
+				}
+				c.Data["json"] = changedEquipmentsData
+				// 更新客户端数据记录
+				manager.UpdateClientData(clientID, currentDataMapping)
+			}
 		} else {
 			// 如果没有data字段,返回整个响应
 			c.Data["json"] = map[string]interface{}{
@@ -690,236 +629,236 @@ func (c *EDispatchController) AllEqpmtList() {
 	c.ServeJSON()
 }
 
-// /serverCtrl/alleqpmtlist2 获取所有设备列表  测试使用
-func (c *EDispatchController) AllEqpmtList2() {
-	// 转换缓存数据为指定格式
-	type EquipmentData struct {
-		DataID   string `json:"dataId"`
-		DataName string `json:"dataName"`
-		Value    string `json:"value"`
-		ID       string `json:"id"`
-	}
-
-	// 从配置文件读取电调服务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
-		}
-	}
-
-	// 生成请求key
-	model := models.DiandiaoModel{
-		LineID: lineID,
-	}
-	key := model.GenerateModelKey()
-
-	// 获取 ElectricModelManager 单例实例
-	manager := models.GetInstance()
-
-	// 检查缓存中是否存在对应key的数据,且最后更新时间在5秒内
-	if viewMapModel, exists := manager.GetModelByKey(key); exists {
-		// 检查最后总包更新时间是否在5秒内
-		if time.Since(viewMapModel.LastBatchUpdate) <= 5*time.Second || time.Since(viewMapModel.LastDeviceUpdate) <= 5*time.Second {
+// // /serverCtrl/alleqpmtlist2 获取所有设备列表  测试使用
+// func (c *EDispatchController) AllEqpmtList2() {
+// 	// 转换缓存数据为指定格式
+// 	type EquipmentData struct {
+// 		DataID   string `json:"dataId"`
+// 		DataName string `json:"dataName"`
+// 		Value    string `json:"value"`
+// 		ID       string `json:"id"`
+// 	}
 
-			var equipmentsData []EquipmentData
-			for _, model := range viewMapModel.Models {
-				equip := EquipmentData{
-					DataID:   model.EqpmtStrID,
-					ID:       model.EqpmtStrID,
-					DataName: model.EqpmtStdName,
-				}
+// 	// 从配置文件读取电调服务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
+// 	}
 
-				switch model.CMDType {
-				case 0: //设备状态
-					equip.Value = fmt.Sprintf("%d", model.EqpmtStatus)
-				case 1: //设备数据控制
-					equip.Value = fmt.Sprintf("%d", model.FValue)
-				}
-				equipmentsData = append(equipmentsData, equip)
-			}
+// 	electricalPort, err := beego.AppConfig.Int("ElectricalPort")
+// 	if err != nil {
+// 		c.Data["json"] = map[string]interface{}{
+// 			"code": 500,
+// 			"msg":  "读取电调服务端口配置失败: " + err.Error(),
+// 		}
+// 		c.ServeJSON()
+// 		return
+// 	}
 
-			c.Data["json"] = equipmentsData
+// 	// 构造电调服务地址
+// 	electricalAddr := fmt.Sprintf("http://%s:%d/ServerCtrl/AllEqpmtList", electricalIP, electricalPort)
 
-			// 使用缓存数据
-			// c.Data["json"] = map[string]interface{}{
-			// 	"code": 200,
-			// 	"msg":  "获取所有设备列表成功(缓存)",
-			// 	"data": equipmentsData,
-			// }
-			c.ServeJSON()
-			return
-		}
-	}
+// 	// 获取最新的课程运行记录以获取线路号
+// 	o := orm.NewOrm()
+// 	var latestCourse models.CourseRunLog
+// 	err = o.QueryTable(new(models.CourseRunLog)).
+// 		OrderBy("-RunLogID").
+// 		One(&latestCourse)
 
-	// 准备请求数据
-	type RequestData struct {
-		LineID    int `json:"lineID"`
-		StationID int `json:"StationID"`
-		ViewID    int `json:"ViewID"`
-	}
-	reqData := RequestData{
-		LineID:    lineID,
-		StationID: 0,
-		ViewID:    0,
-	}
+// 	if err != nil && err != orm.ErrNoRows {
+// 		c.Data["json"] = map[string]interface{}{
+// 			"code": 500,
+// 			"msg":  "查询最新课程运行记录失败: " + err.Error(),
+// 		}
+// 		c.ServeJSON()
+// 		return
+// 	}
 
-	// 将请求数据转换为JSON
-	reqBody, err := json.Marshal(reqData)
-	if err != nil {
-		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
+// 		}
 
-	// 调用电调服务获取所有设备列表接口,使用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 err != orm.ErrNoRows {
+// 			lineID = courseInfo.LineNo
+// 		}
+// 	}
 
-	// 检查响应状态
-	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
-	}
+// 	// 生成请求key
+// 	model := models.DiandiaoModel{
+// 		LineID: lineID,
+// 	}
+// 	key := model.GenerateModelKey()
+
+// 	// 获取 ElectricModelManager 单例实例
+// 	manager := models.GetInstance()
+
+// 	// 检查缓存中是否存在对应key的数据,且最后更新时间在5秒内
+// 	if viewMapModel, exists := manager.GetModelByKey(key); exists {
+// 		// 检查最后总包更新时间是否在5秒内
+// 		if time.Since(viewMapModel.LastBatchUpdate) <= 5*time.Second || time.Since(viewMapModel.LastDeviceUpdate) <= 5*time.Second {
+
+// 			var equipmentsData []EquipmentData
+// 			for _, model := range viewMapModel.Models {
+// 				equip := EquipmentData{
+// 					DataID:   model.EqpmtStrID,
+// 					ID:       model.EqpmtStrID,
+// 					DataName: model.EqpmtStdName,
+// 				}
+
+// 				switch model.CMDType {
+// 				case 0: //设备状态
+// 					equip.Value = fmt.Sprintf("%d", model.EqpmtStatus)
+// 				case 1: //设备数据控制
+// 					equip.Value = fmt.Sprintf("%d", model.FValue)
+// 				}
+// 				equipmentsData = append(equipmentsData, equip)
+// 			}
+
+// 			c.Data["json"] = equipmentsData
+
+// 			// 使用缓存数据
+// 			// c.Data["json"] = map[string]interface{}{
+// 			// 	"code": 200,
+// 			// 	"msg":  "获取所有设备列表成功(缓存)",
+// 			// 	"data": equipmentsData,
+// 			// }
+// 			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
-	}
+// 	// 准备请求数据
+// 	type RequestData struct {
+// 		LineID    int `json:"lineID"`
+// 		StationID int `json:"StationID"`
+// 		ViewID    int `json:"ViewID"`
+// 	}
+// 	reqData := RequestData{
+// 		LineID:    lineID,
+// 		StationID: 0,
+// 		ViewID:    0,
+// 	}
 
-	// 尝试解析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中的设备列表并更新到模型管理器
-			var equipments []models.DiandiaoModel
-			if equipBytes, err := json.Marshal(data); err == nil {
-				if err := json.Unmarshal(equipBytes, &equipments); err == nil {
-					// 更新总包设备数据
-					manager.UpdateBatchData(equipments)
-				}
-			}
+// 	// 将请求数据转换为JSON
+// 	reqBody, err := json.Marshal(reqData)
+// 	if err != nil {
+// 		c.Data["json"] = map[string]interface{}{
+// 			"code": 500,
+// 			"msg":  "构造请求数据失败: " + err.Error(),
+// 		}
+// 		c.ServeJSON()
+// 		return
+// 	}
 
-			var equipmentsData []EquipmentData
-			for _, model := range equipments {
-				equip := EquipmentData{
-					DataID:   model.EqpmtStrID,
-					ID:       model.EqpmtStrID,
-					DataName: model.EqpmtStdName,
-				}
+// 	// 调用电调服务获取所有设备列表接口,使用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 model.CMDType == 0 { //设备状态
-					equip.Value = fmt.Sprintf("%d", model.EqpmtStatus)
-				} else if model.CMDType == 1 { //设备数据控制
-					equip.Value = fmt.Sprintf("%d", model.FValue)
-				}
-				equipmentsData = append(equipmentsData, equip)
-			}
+// 	// 检查响应状态
+// 	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
+// 	}
 
-			c.Data["json"] = equipmentsData
+// 	// 读取并解析响应体
+// 	bodyBytes, err := io.ReadAll(resp.Body)
+// 	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": equipmentsData,
-			// }
-		} else {
-			// 如果没有data字段,返回整个响应
-			c.Data["json"] = map[string]interface{}{
-				"code": 500,
-				"msg":  "获取所有设备列表失败",
-			}
-		}
-	}
+// 	// 尝试解析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中的设备列表并更新到模型管理器
+// 			var equipments []models.DiandiaoModel
+// 			if equipBytes, err := json.Marshal(data); err == nil {
+// 				if err := json.Unmarshal(equipBytes, &equipments); err == nil {
+// 					// 更新总包设备数据
+// 					manager.UpdateBatchData(equipments)
+// 				}
+// 			}
+
+// 			var equipmentsData []EquipmentData
+// 			for _, model := range equipments {
+// 				equip := EquipmentData{
+// 					DataID:   model.EqpmtStrID,
+// 					ID:       model.EqpmtStrID,
+// 					DataName: model.EqpmtStdName,
+// 				}
+
+// 				if model.CMDType == 0 { //设备状态
+// 					equip.Value = fmt.Sprintf("%d", model.EqpmtStatus)
+// 				} else if model.CMDType == 1 { //设备数据控制
+// 					equip.Value = fmt.Sprintf("%d", model.FValue)
+// 				}
+// 				equipmentsData = append(equipmentsData, equip)
+// 			}
+
+// 			c.Data["json"] = equipmentsData
+
+// 			// // 返回响应
+// 			// c.Data["json"] = map[string]interface{}{
+// 			// 	"code": 200,
+// 			// 	"msg":  "获取所有设备列表成功",
+// 			// 	"data": equipmentsData,
+// 			// }
+// 		} else {
+// 			// 如果没有data字段,返回整个响应
+// 			c.Data["json"] = map[string]interface{}{
+// 				"code": 500,
+// 				"msg":  "获取所有设备列表失败",
+// 			}
+// 		}
+// 	}
 
-	c.ServeJSON()
-}
+// 	c.ServeJSON()
+// }
 
 // /serverctrl/seteqpmtoper 设置电调设备操作
 func (c *EDispatchController) Seteqpmtoper() {
@@ -1350,17 +1289,19 @@ func (c *EDispatchController) GetEqpmtFault() {
 
 	// 转换为UI需要的格式
 	type UIFaultData struct {
-		SeqNo     int    `json:"SeqNo"`
-		EqpmtID   string `json:"EqpmtID"`
-		EqpmtName string `json:"EqpmtName"`
-		FaultID   int    `json:"FaultID"`
-		FaultTime int    `json:"FaultTime"`
+		SeqNo       int    `json:"SeqNo"`
+		EqpmtID     string `json:"EqpmtID"`
+		EqpmtName   string `json:"EqpmtName"`
+		FaultID     int    `json:"FaultID"`
+		FaultTime   int    `json:"FaultTime"`
+		EqpmtTypeID int    `json:"EqpmtTypeID"`
 	}
 
 	var uiData []UIFaultData
 	seqNo := 1
 	for _, item := range faultData {
 		eqpmtID, _ := item["EqpmtID"].(string)
+		EqpmtTypeID, _ := item["EqpmtTypeID"].(int)
 
 		// 解析设备故障数组
 		eqpmtFault, ok := item["EqpmtFault"].([]interface{})
@@ -1388,11 +1329,12 @@ func (c *EDispatchController) GetEqpmtFault() {
 		secondsAgo := int(time.Since(faultTime).Seconds())
 
 		uiFault := UIFaultData{
-			SeqNo:     seqNo,
-			EqpmtID:   eqpmtID,
-			EqpmtName: "", // 原始数据中未提供设备名称,留空
-			FaultID:   int(faultID),
-			FaultTime: secondsAgo,
+			SeqNo:       seqNo,
+			EqpmtID:     eqpmtID,
+			EqpmtName:   "", // 原始数据中未提供设备名称,留空
+			FaultID:     int(faultID),
+			FaultTime:   secondsAgo,
+			EqpmtTypeID: int(EqpmtTypeID),
 		}
 
 		uiData = append(uiData, uiFault)

+ 522 - 3
Tech/controllers/课程.go

@@ -1,11 +1,13 @@
 package controllers
 
 import (
+	"Tech/common"
 	"Tech/models" // 根据实际项目结构调整导入路径
 	"encoding/json"
 	"fmt"
 	"os"
 	"path"
+	"strconv"
 	"time"
 
 	"github.com/beego/beego/v2/client/orm"
@@ -17,6 +19,78 @@ type CourseController struct {
 	beego.Controller
 }
 
+// GetCourseGUIDsByJXJHID 根据培训计划ID获取课程GUID列表
+func GetCourseGUIDsByJXJHID(jxjhID, userName string) ([]string, error) {
+	// 定义请求结构体
+	type Request struct {
+		SBID    string `json:"SBID"`    // 设备编号
+		JYEMPID string `json:"JYEMPID"` // 教员GUID
+		XYEMPID string `json:"XYEMPID"` // 学员GUID
+	}
+	var req Request
+
+	// 从app.conf中读取SYSDEVICEID作为SBID的值
+	sbid, err := beego.AppConfig.String("SYSDEVICEID")
+	if err != nil {
+		return nil, fmt.Errorf("获取设备编号失败: %v", err)
+	}
+	req.SBID = sbid
+
+	// 从缓存中获取用户信息
+	userInfo, exists := GetUserInfoFromCache(userName)
+	if !exists {
+		return nil, fmt.Errorf("用户信息不存在: ")
+	}
+
+	switch userInfo.SF {
+	case "1": // 如果是教员,则使用当前用户的RYEMPID
+		req.XYEMPID = userInfo.RYEMPID
+	case "2": // 如果是学员,则使用当前用户的RYEMPID
+		req.JYEMPID = userInfo.RYEMPID
+	}
+	// 调用信息化接口获取培训计划
+	response, err := common.GetTeachPlanFromInformationSystem(req.SBID, req.JYEMPID, req.XYEMPID)
+	if err != nil {
+		return nil, fmt.Errorf("获取培训计划失败: %v", err)
+	}
+
+	// 判断请求是否成功
+	if response.ResultCode != "0" {
+		return nil, fmt.Errorf("获取培训计划失败: %s", response.ResultDesc)
+	}
+
+	// 收集匹配的培训计划下的所有课程GUID
+	var courseGUIDs []string
+	courseGUIDSet := make(map[string]bool) // 用于去重
+
+	// 遍历所有培训实施计划
+	for _, pxss := range response.PXSS {
+		// 遍历所有计划
+		for _, jh := range pxss.JH {
+			// 检查是否是目标培训计划
+			if jh.ID == jxjhID {
+				// 添加SDKC中的KCGUID
+				for _, sdkc := range jh.SDKC {
+					if sdkc.KCGUID != "" && !courseGUIDSet[sdkc.KCGUID] {
+						courseGUIDs = append(courseGUIDs, sdkc.KCGUID)
+						courseGUIDSet[sdkc.KCGUID] = true
+					}
+				}
+
+				// 添加TESTST中的KCGUID
+				for _, testst := range jh.TESTST {
+					if testst.KCGUID != "" && !courseGUIDSet[testst.KCGUID] {
+						courseGUIDs = append(courseGUIDs, testst.KCGUID)
+						courseGUIDSet[testst.KCGUID] = true
+					}
+				}
+			}
+		}
+	}
+
+	return courseGUIDs, nil
+}
+
 // /course/list 获取所有课程信息列表
 func (c *CourseController) List() {
 	type Query struct {
@@ -25,6 +99,8 @@ func (c *CourseController) List() {
 		TypeID     int    `form:"typeID"`
 		Creator    string `form:"creator"`
 		LineNo     int    `form:"lineNo"`
+
+		JXJHID string `form:"JXJHID"` // 培训计划ID
 	}
 	var query Query
 	requestBody := c.Ctx.Input.RequestBody
@@ -67,8 +143,48 @@ func (c *CourseController) List() {
 		qs = qs.Filter("LineNo", query.LineNo)
 	}
 
+	// 如果提供了培训计划ID(JXJHID),则从信息化系统获取对应的课程GUID列表
+	var courseGUIDs []string
+	if query.JXJHID != "" {
+
+		// 从Header中获取userName
+		authHeader := c.Ctx.Input.Header("userName")
+		if authHeader == "" {
+			c.Ctx.Output.SetStatus(401)
+			c.Data["json"] = map[string]interface{}{
+				"code": 401,
+				"msg":  "未提供认证信息",
+			}
+			c.ServeJSON()
+			return
+		}
+
+		var err error
+		courseGUIDs, err = GetCourseGUIDsByJXJHID(query.JXJHID, authHeader)
+		if err != nil {
+			c.Data["json"] = map[string]interface{}{
+				"code": 500,
+				"msg":  "获取培训计划课程列表失败: " + err.Error(),
+			}
+			c.ServeJSON()
+			return
+		}
+
+		// 如果找到了课程GUID列表,则添加过滤条件
+		if len(courseGUIDs) > 0 {
+			qs = qs.Filter("CourseGUID__in", courseGUIDs)
+		} else {
+			// 如果没有找到匹配的课程,则返回空结果
+			var courses []models.CourseInfo
+			c.Data["json"] = map[string]interface{}{"code": 200, "count": 0, "msg": "", "data": courses}
+			c.ServeJSON()
+			return
+		}
+	}
+
 	var courses []models.CourseInfo
-	count, err := qs.OrderBy("-CreateTime").All(&courses, "课程ID", "课程GUID", "课程名称", "分类ID", "外部分类ID", "虚拟起始时间", "课程总时长", "线路号", "运行图", "课程描述", "创建人", "创建时间", "修改时间", "指导描述", "指导文件地址", "虚拟角色")
+	count, err := qs.OrderBy("-CreateTime").All(&courses, "课程ID", "课程GUID", "课程名称", "分类ID", "外部分类ID", "虚拟起始时间",
+		"课程总时长", "线路号", "运行图", "课程描述", "创建人", "创建时间", "修改时间", "指导描述", "指导文件地址", "虚拟角色", "课程分类ID", "课程分类名称", "创建人ID")
 	if err != nil {
 		c.Data["json"] = map[string]interface{}{"code": 500, "error": "查询失败: " + err.Error()}
 		c.ServeJSON()
@@ -156,7 +272,7 @@ func (c *CourseController) ListPage() {
 	var courses []models.CourseInfo
 	_, err = qs.OrderBy("-CreateTime").Limit(req.PageSize).Offset((req.PageNum-1)*req.PageSize).All(&courses,
 		"课程ID", "课程GUID", "课程名称", "分类ID", "外部分类ID", "虚拟起始时间", "课程总时长", "线路号", "运行图",
-		"课程描述", "创建人", "创建时间", "修改时间", "指导描述", "指导文件地址", "虚拟角色")
+		"课程描述", "创建人", "创建时间", "修改时间", "指导描述", "指导文件地址", "虚拟角色", "课程分类ID", "课程分类名称", "创建人ID")
 	if err != nil {
 		c.Data["json"] = map[string]interface{}{
 			"code": 500,
@@ -253,7 +369,8 @@ func (c *CourseController) Update() {
 	o := orm.NewOrm()
 
 	course.ModifyTime = time.Now().Format("2006-01-02 15:04:05")
-	_, err := o.Update(&course, "课程名称", "分类ID", "外部分类ID", "虚拟起始时间", "课程总时长", "线路号", "运行图", "课程描述", "创建人", "修改时间", "指导描述", "指导文件地址")
+	_, err := o.Update(&course, "课程名称", "分类ID", "外部分类ID", "虚拟起始时间", "课程总时长", "线路号", "运行图", "课程描述", "创建人",
+		"修改时间", "指导描述", "指导文件地址", "课程分类ID", "课程分类名称", "创建人ID")
 	if err != nil {
 		c.Data["json"] = map[string]interface{}{"code": 500, "msg": "更新失败: " + err.Error()}
 		c.ServeJSON()
@@ -1586,3 +1703,405 @@ func (c *CourseController) CourseExport() {
 	c.Ctx.Output.Header("Content-Length", fmt.Sprintf("%d", len(jsonData)))
 	c.Ctx.Output.Body(jsonData)
 }
+
+// /gettmscoursetype 获取课程分类列表
+func (c *CourseController) GetTMSCourseType() {
+	// 调用信息化接口获取课程分类列表,传入FZXTLX参数为1
+	response, err := common.GetAllCourseTypeEx(1)
+	if err != nil {
+		c.Ctx.Output.SetStatus(500)
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "获取课程分类列表失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 判断请求是否成功
+	if response.ResultCode != "0" {
+		c.Ctx.Output.SetStatus(500)
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "获取课程分类列表失败: " + response.ResultDesc,
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 返回成功数据
+	c.Data["json"] = map[string]interface{}{
+		"code": 200,
+		"msg":  "获取课程分类列表成功",
+		"data": response.CourseTypes,
+	}
+	c.ServeJSON()
+}
+
+// /course/uploadtms 上传课程到信息化平台
+func (c *CourseController) UploadTMS() {
+	// 解析请求中的课程ID数组
+	var req []struct {
+		CourseID int `json:"courseId"`
+	}
+
+	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 len(req) == 0 {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "请提供要上传的课程ID",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	o := orm.NewOrm()
+
+	// 准备要上传的课程信息
+	var coursesToUpload []common.UploadCourseInfo
+
+	// 提取课程ID列表
+	courseIds := make([]int, len(req))
+	for i, item := range req {
+		courseIds[i] = item.CourseID
+	}
+
+	// 查询课程信息
+	var courses []models.CourseInfo
+	_, err := o.QueryTable(new(models.CourseInfo)).Filter("课程ID__in", courseIds).All(&courses)
+	if err != nil {
+		c.Ctx.Output.SetStatus(500)
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "查询课程信息失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 如果没有找到任何课程
+	if len(courses) == 0 {
+		c.Data["json"] = map[string]interface{}{
+			"code": 200,
+			"msg":  "没有找到指定的课程",
+			"data": []common.UploadCourseInfo{},
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 从配置文件中读取CLLX和SYCX
+	cllx, _ := beego.AppConfig.String("SYSCLLX")
+	if cllx == "" {
+		cllx = "牵引变电实训室" // 默认值
+	}
+
+	sycx, _ := beego.AppConfig.String("SYSSYCX")
+	if sycx == "" {
+		sycx = "SCADA系统" // 默认值
+	}
+
+	// 将课程信息转换为上传格式
+	for _, course := range courses {
+		// 从数据库中查询线路名称
+		var typeMapList []models.TypeMapList
+		lineName := "" // 默认线路名称
+		o := orm.NewOrm()
+		err := o.QueryTable(new(models.TypeMapList)).
+			Filter("DictionaryTypeName", "线路号").
+			Filter("KeyParam", course.LineNo).
+			One(&typeMapList)
+
+		if err == nil && len(typeMapList) > 0 {
+			lineName = typeMapList[0].ValueParam
+		}
+
+		uploadCourse := common.UploadCourseInfo{
+			KCGUID: course.CourseGUID,
+			KCMC:   course.CourseName,
+			KCMS:   course.CourseDesc,
+			KCNR:   course.CourseDesc,       // 课程内容暂时使用课程描述
+			KCFLID: course.CourseCategoryID, // 课程分类ID
+			CJRID:  course.CreatorID,        // 创建人作为创建人ID
+			CJSJ:   course.CreateTime,       // 创建时间
+			XGRID:  course.CreatorID,        // 创建人作为修改人ID
+			XGSJ:   course.ModifyTime,       // 修改时间
+			PXMS:   "1",                     // 排序模式,默认为1
+			Alone: common.Alone{
+				FZXTLX: "1", // 独立系统类型,默认为1
+				MJ: common.MJ{
+					CLLX: cllx,                             // 车辆类型
+					SYCX: sycx,                             // 适用车型
+					XLBH: fmt.Sprintf("%d", course.LineNo), // 线路编号
+					XLMC: lineName,                         // 线路名称,这里使用固定值
+				},
+			},
+		}
+		coursesToUpload = append(coursesToUpload, uploadCourse)
+	}
+
+	// 调用信息化接口上传课程
+	response, err := common.UploadCourseToInformationSystem(coursesToUpload)
+	if err != nil {
+		c.Ctx.Output.SetStatus(500)
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "上传课程到信息化平台失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 判断请求是否成功
+	// 根据返回的SucceedCount判断是否全部成功
+	succeedCount := 0
+	if response.SucceedCount != "" {
+		count, err := strconv.Atoi(response.SucceedCount)
+		if err == nil {
+			succeedCount = count
+		}
+	}
+
+	totalCount := len(response.CourseResults)
+
+	// 如果有失败的课程,返回详细信息
+	if succeedCount < totalCount {
+		c.Ctx.Output.SetStatus(500)
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  fmt.Sprintf("上传课程部分失败: 成功%d个,失败%d个", succeedCount, totalCount-succeedCount),
+			"data": response.CourseResults,
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 返回成功数据
+	c.Data["json"] = map[string]interface{}{
+		"code": 200,
+		"msg":  "上传课程到信息化平台成功",
+		"data": response.CourseResults,
+	}
+	c.ServeJSON()
+}
+
+// /course/tmsplan 获取信息化系统的课程计划
+func (c *CourseController) GetTMSCoursePlan() {
+	// 定义请求结构体
+	type Request struct {
+		SBID    string `json:"SBID"`    // 设备编号
+		JYEMPID string `json:"JYEMPID"` // 教员GUID
+		XYEMPID string `json:"XYEMPID"` // 学员GUID
+	}
+	var req Request
+
+	// 从app.conf中读取SYSDEVICEID作为SBID的值
+	sbid, err := beego.AppConfig.String("SYSDEVICEID")
+	if err != nil {
+		c.Ctx.Output.SetStatus(500)
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "获取设备编号失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+	req.SBID = sbid
+
+	// 解析请求中的参数 Header 中的 userName 判断用户缓存中的是学员还是教员,将对应人员guid 传入req
+
+	// 从Header中获取userName
+	authHeader := c.Ctx.Input.Header("userName")
+	if authHeader == "" {
+		c.Ctx.Output.SetStatus(401)
+		c.Data["json"] = map[string]interface{}{
+			"code": 401,
+			"msg":  "未提供认证信息",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 从缓存中获取用户信息
+	userInfo, exists := GetUserInfoFromCache(authHeader)
+	if !exists {
+		c.Ctx.Output.SetStatus(401)
+		c.Data["json"] = map[string]interface{}{
+			"code": 401,
+			"msg":  "用户信息不存在",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	switch userInfo.SF {
+	case "1": // 如果是教员,则使用当前用户的RYEMPID
+		req.XYEMPID = userInfo.RYEMPID
+	case "2": // 如果是学员,则使用当前用户的RYEMPID
+		req.JYEMPID = userInfo.RYEMPID
+	}
+
+	// 调用信息化接口获取培训计划
+	response, err := common.GetTeachPlanFromInformationSystem(req.SBID, req.JYEMPID, req.XYEMPID)
+	if err != nil {
+		c.Ctx.Output.SetStatus(500)
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "获取培训计划失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 判断请求是否成功
+	if response.ResultCode != "0" {
+		c.Ctx.Output.SetStatus(500)
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "获取培训计划失败: " + response.ResultDesc,
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 提取需要返回给前端的数据
+	type PlanInfo struct {
+		JXJHID      string   `json:"JXJHID"`      // 培训计划ID
+		KMMC        string   `json:"KMMC"`        // 科目名称
+		SDKCGUIDs   []string `json:"SDKCGUIDs"`   // 培训课程GUID列表
+		TESTSTGUIDs []string `json:"TESTKCGUIDs"` // 测试课程GUID列表
+	}
+
+	// 提取需要返回给前端的数据
+	type PlanDoInfo struct {
+		PXSSID string     `json:"PXSSID"`   // 培训实施ID
+		PXSSMC string     `json:"PXSSMC"`   // 培训实施名称
+		PXInfo []PlanInfo `json:"PlanInfo"` // 培训计划信息
+	}
+
+	var plans []PlanDoInfo
+
+	// 遍历所有培训实施计划
+	for _, pxss := range response.PXSS {
+		PlanDoInfo := PlanDoInfo{PXSSID: pxss.PXSSID, PXSSMC: pxss.PXSSMC}
+
+		// 遍历所有计划
+		for _, jh := range pxss.JH {
+			plan := PlanInfo{
+				JXJHID:      jh.ID,
+				KMMC:        jh.KMMC,
+				SDKCGUIDs:   []string{},
+				TESTSTGUIDs: []string{},
+			}
+
+			// 添加SDKC中的KCGUID
+			for _, sdkc := range jh.SDKC {
+				if sdkc.KCGUID != "" {
+					plan.SDKCGUIDs = append(plan.SDKCGUIDs, sdkc.KCGUID)
+				}
+			}
+
+			// 添加TESTST中的KCGUID
+			for _, testst := range jh.TESTST {
+				if testst.KCGUID != "" {
+					plan.TESTSTGUIDs = append(plan.TESTSTGUIDs, testst.KCGUID)
+				}
+			}
+			PlanDoInfo.PXInfo = append(PlanDoInfo.PXInfo, plan)
+		}
+
+		plans = append(plans, PlanDoInfo)
+	}
+
+	// 返回成功数据
+	c.Data["json"] = map[string]interface{}{
+		"code": 200,
+		"msg":  "获取培训计划成功",
+		"data": plans,
+	}
+	c.ServeJSON()
+}
+
+// /course/tmsplanstudent 获取TMS课程计划参与的学生
+func (c *CourseController) GetTMSCoursePlanStudent() {
+	// 获取请求参数
+	var req struct {
+		PXSSID string `json:"PXSSID"`
+	}
+
+	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.PXSSID == "" {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "PXSSID不能为空",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 调用信息化系统获取培训学生信息
+	trainingStudentResp, err := common.GetTrainingStudentFromInformationSystem(req.PXSSID)
+	if err != nil {
+		c.Ctx.Output.SetStatus(500)
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "获取培训学生信息失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 检查返回结果
+	if trainingStudentResp.ResultCode != "0" {
+		c.Ctx.Output.SetStatus(500)
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "获取培训学生信息失败: " + trainingStudentResp.ResultDesc,
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 构造简化版的学生信息列表,只包含XYEMPID和XYXM
+	type SimpleStudentInfo struct {
+		XYEMPID string `json:"XYEMPID"`
+		XYXM    string `json:"XYXM"`
+	}
+
+	simpleStudents := make([]SimpleStudentInfo, 0)
+	for _, student := range trainingStudentResp.Students {
+		simpleStudents = append(simpleStudents, SimpleStudentInfo{
+			XYEMPID: student.XYEMPID,
+			XYXM:    student.XYXM,
+		})
+	}
+
+	// 返回成功数据
+	c.Data["json"] = map[string]interface{}{
+		"code":  200,
+		"msg":   "获取培训学生信息成功",
+		"data":  simpleStudents,
+		"count": trainingStudentResp.Count,
+	}
+	c.ServeJSON()
+}

+ 61 - 9
Tech/controllers/课程运行.go

@@ -3,6 +3,7 @@ package controllers
 import (
 	// 根据实际项目结构调整导入路径
 
+	"Tech/common"
 	"Tech/models"
 	"bytes"
 	"encoding/json"
@@ -27,7 +28,11 @@ type CourseRunController struct {
 
 func (c *CourseRunController) CourseRun() {
 	type Request struct {
-		CourseID int `json:"courseID"`
+		CourseID                 int    `json:"courseID"`
+		TraineeGUID              string `json:"traineeGUID"`
+		TrainingImplementationID string `json:"trainingImplementationID"`
+		TrainingPlanID           string `json:"trainingPlanID"`
+		TrainingPlanName         string `json:"trainingPlanName"`
 	}
 
 	var req Request
@@ -109,14 +114,32 @@ func (c *CourseRunController) CourseRun() {
 	}
 	nextRunLogID := maxRunLogID + 1
 
+	// 从用户信息缓存中通过TraineeGUID查询TraineeName
+	traineeName := ""
+	if req.TraineeGUID != "" {
+		// 从缓存中获取用户信息
+		userInfo, exists := GetUserInfoFromCache_RYEMPID(req.TraineeGUID)
+		if exists {
+			traineeName = userInfo.XM
+		}
+	}
+
+	// 生成唯一的TrainingRecordGUID,使用时间戳+随机数确保唯一性
+	trainingRecordGUID := fmt.Sprintf("%s-%d", time.Now().Format("20060102150405"), time.Now().UnixNano()%1000000)
+
 	// 创建新的课程运行记录
 	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,
+		RunLogID:                 nextRunLogID, // 使用计算出的ID
+		RunCourseID:              req.CourseID,
+		RunCourseName:            course.CourseName,
+		RunDateTime:              time.Now().Format("2006-01-02 15:04:05"),
+		CourseTime:               0, // 初始为0,表示正在运行
+		CourseScore:              0,
+		TraineeGUID:              req.TraineeGUID,
+		TraineeName:              traineeName,
+		TrainingRecordGUID:       trainingRecordGUID,
+		TrainingImplementationID: req.TrainingImplementationID,
+		TrainingPlanID:           req.TrainingPlanID,
 	}
 
 	// 插入课程运行记录
@@ -130,6 +153,22 @@ func (c *CourseRunController) CourseRun() {
 		return
 	}
 
+	if req.TrainingImplementationID != "" && req.TrainingPlanID != "" {
+		// 通知信息化系统开始培训
+		err = common.NotifyTMSStartTraining(int(id), course, req.TraineeGUID, req.TrainingImplementationID, req.TrainingPlanID, "")
+		if err != nil {
+			// 如果通知信息化系统失败,删除刚刚创建的课程运行记录
+			o.Delete(&newRunLog)
+
+			c.Data["json"] = map[string]interface{}{
+				"code": 500,
+				"msg":  "通知信息化系统开始培训失败: " + err.Error(),
+			}
+			c.ServeJSON()
+			return
+		}
+	}
+
 	// 通知电调开课
 	isret, err := c.tellEStartCourse(req.CourseID, int(id), course.LineNo)
 	if !isret || err != nil {
@@ -193,7 +232,8 @@ func (c *CourseRunController) CourseRun() {
 		"code": 200,
 		"msg":  "课程开始运行",
 		"data": map[string]interface{}{
-			"runLogID": id,
+			"runLogID":         id,
+			"TrainingPlanName": req.TrainingPlanName,
 		},
 	}
 
@@ -529,8 +569,9 @@ func (c *CourseRunController) CourseStop() {
 	// 更新课程记录
 	latestCourse.CourseTime = courseDuration
 	latestCourse.PauseFlag = -1
+	latestCourse.FinishDateTime = time.Now().Format("2006-01-02 15:04:05")
 
-	_, err = o.Update(&latestCourse, "CourseTime", "PauseFlag")
+	_, err = o.Update(&latestCourse, "CourseTime", "PauseFlag", "FinishDateTime")
 	if err != nil {
 		c.Data["json"] = map[string]interface{}{
 			"code": 500,
@@ -551,6 +592,13 @@ func (c *CourseRunController) CourseStop() {
 		},
 	}
 
+	// 通知信息化系统结束培训
+	err = common.NotifyTMSStopTraining(latestCourse.TrainingRecordGUID)
+	if err != nil {
+		// 记录错误日志,但不中断操作
+		fmt.Println("通知信息化系统结束培训失败: " + err.Error())
+	}
+
 	// 通知电调服务课程结束
 	_, err = c.tellEStopCourse(latestCourse.RunCourseID, latestCourse.RunLogID)
 	if err != nil {
@@ -563,6 +611,10 @@ func (c *CourseRunController) CourseStop() {
 		// return
 	}
 
+	err = DoUploadTMS(latestCourse.RunLogID)
+	if err != nil {
+		fmt.Println("上传电子考评表失败: " + err.Error())
+	}
 	c.ServeJSON()
 }
 

+ 81 - 0
Tech/main.go

@@ -1,9 +1,13 @@
 package main
 
 import (
+	"Tech/common"
+	"Tech/controllers"
 	_ "Tech/routers"
+	"time"
 
 	"github.com/beego/beego/v2/client/orm"
+	"github.com/beego/beego/v2/core/logs"
 	"github.com/beego/beego/v2/server/web"
 	beego "github.com/beego/beego/v2/server/web"
 	"github.com/beego/beego/v2/server/web/filter/cors"
@@ -20,9 +24,80 @@ func init() {
 	// 注册数据库
 	orm.RegisterDataBase("default", dbAdapter, dbURL)
 
+	// 启动数据库连接监控协程
+	go monitorDatabaseConnection(dbAdapter, dbURL)
 	// 注册模型(也可以在主程序入口或模型自身的 init 函数中)
 	// orm.RegisterModel(new(User))
 }
+
+// sendHeartbeatPeriodically 定时发送心跳到信息化服务器
+func sendHeartbeatPeriodically() {
+	ticker := time.NewTicker(30 * time.Second) // 每30秒发送一次心跳
+	defer ticker.Stop()
+
+	for range ticker.C {
+		err := func() error {
+			defer func() {
+				if r := recover(); r != nil {
+					logs.Error("发送心跳时发生异常: %v", r)
+				}
+			}()
+
+			err := common.SendHeartbeat()
+			if err != nil {
+				logs.Error("发送心跳失败: %v", err)
+				return err
+			}
+
+			logs.Info("成功发送心跳到信息化服务器")
+			return nil
+		}()
+
+		if err != nil {
+			logs.Error("发送心跳任务出现错误: %v", err)
+		}
+	}
+}
+
+// monitorDatabaseConnection 定时检查数据库连接并尝试重连
+// monitorDatabaseConnection 定时检查数据库连接并尝试重连
+func monitorDatabaseConnection(dbAdapter, dbURL string) {
+	ticker := time.NewTicker(10 * time.Second) // 每10秒检查一次
+	defer ticker.Stop()
+
+	for range ticker.C {
+		// 使用匿名函数包装可能产生异常的操作,并在每次循环都进行异常处理
+		func() {
+			defer func() {
+				if r := recover(); r != nil {
+					// 即使NewOrm发生异常,也要尝试重新注册数据库连接
+					logs.Error("监控协程发生异常: %v", r)
+					err := orm.RegisterDataBase("default", dbAdapter, dbURL)
+					if err != nil {
+						logs.Error("数据库重连失败: ", err)
+					} else {
+						logs.Info("数据库重连成功")
+					}
+				}
+			}()
+
+			// 尝试执行一个简单的查询来测试连接
+			o := orm.NewOrm()
+			_, err := o.Raw("SELECT 1").Exec()
+			if err != nil {
+				logs.Error("数据库连接异常: ", err)
+				// 尝试重新注册数据库连接
+				err = orm.RegisterDataBase("default", dbAdapter, dbURL)
+				if err != nil {
+					logs.Error("数据库重连失败: ", err)
+				} else {
+					logs.Info("数据库重连成功")
+				}
+			}
+		}()
+	}
+}
+
 func main() {
 	// 自动建表。参数说明:数据库别名、是否强制重建(false 则只创建新表)、是否打印日志
 	// orm.RunSyncdb("default", false, true)
@@ -41,6 +116,12 @@ func main() {
 
 	web.SetStaticPath("/static/upload", "static/upload")
 	orm.Debug = true
+
+	// 初始化用户信息缓存
+	controllers.InitUserInfoCache()
+	// 启动定时发送心跳任务
+	go sendHeartbeatPeriodically()
+
 	beego.Run()
 
 }

+ 28 - 17
Tech/models/教员模型.go

@@ -17,23 +17,26 @@ 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"`
+	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"`
+	CourseCategoryID   string `orm:"column(课程分类ID);type(text);null" json:"courseCategoryID"`
+	CourseCategoryName string `orm:"column(课程分类名称);type(text);null" json:"courseCategoryName"`
+	CreatorID          string `orm:"column(创建人ID);type(text);null" json:"creatorID"`
 }
 
 func (c *CourseInfo) TableName() string { return "CourseInfo" }
@@ -212,6 +215,14 @@ type CourseRunLog struct {
 	//1:已经暂停,0运行中,-1课程结束
 	PauseFlag      int `orm:"column(暂停标记)" json:"pauseFlag"`
 	PauseTotalTime int `orm:"column(暂停累计时长)" json:"pauseTotalTime"`
+
+	//、、新增字段
+	FinishDateTime           string `orm:"column(课程结束时间);type(text)" json:"finishDateTime"`
+	TraineeName              string `orm:"column(培训学员姓名);size(255)" json:"traineeName"`
+	TraineeGUID              string `orm:"column(培训学员GUID);size(255)" json:"traineeGUID"`
+	TrainingRecordGUID       string `orm:"column(培训记录GUID);size(50)" json:"trainingRecordGUID"`
+	TrainingImplementationID string `orm:"column(培训实施ID);size(255)" json:"trainingImplementationID"`
+	TrainingPlanID           string `orm:"column(实训计划ID);size(255)" json:"trainingPlanID"`
 }
 
 func (c *CourseRunLog) TableName() string { return "CourseRunLog" }

+ 93 - 1
Tech/models/电调模型.go

@@ -18,6 +18,7 @@ type DiandiaoModel struct {
 	OpeMark      int    `json:"OpeMark"`
 	FValue       int    `json:"fValue"` //浮点数值
 	NValueType   int    `json:"nValueType"`
+	EqpmtTypeID  int    `json:"EqpmtTypeID"`
 }
 
 // ... existing code ...
@@ -42,6 +43,11 @@ type ElectricModelManager struct {
 	Models map[string]EViewMapModel // key: modelKey, value: 单图元电调模型
 	Mutex  sync.RWMutex             // 读写锁保护并发访问
 	once   sync.Once                // 确保初始化只执行一次
+
+	// 用于跟踪客户端连接和上次发送的数据
+	ClientData         map[string]map[string]string // clientID -> (eqpmtStrID -> value)
+	ClientLastFullData time.Time                    // 上次发送全量数据的时间
+	ClientLastAccess   map[string]time.Time         // clientID -> 最后访问时间
 }
 
 // 使用 sync.Once 确保线程安全的单例实例创建
@@ -54,9 +60,15 @@ var (
 func GetInstance() *ElectricModelManager {
 	once.Do(func() {
 		instance = &ElectricModelManager{
-			Models: make(map[string]EViewMapModel),
+			Models:           make(map[string]EViewMapModel),
+			ClientData:       make(map[string]map[string]string),
+			ClientLastAccess: make(map[string]time.Time),
 		}
 	})
+
+	// 定期清理长时间未访问的客户端数据
+	go instance.cleanupInactiveClients()
+
 	return instance
 }
 
@@ -149,3 +161,83 @@ func (em *ElectricModelManager) UpdateSingleDevice(model DiandiaoModel) {
 		}
 	}
 }
+
+// RegisterClient 注册客户端连接
+func (em *ElectricModelManager) RegisterClient(clientID string) {
+	em.Mutex.Lock()
+	defer em.Mutex.Unlock()
+
+	em.ClientLastAccess[clientID] = time.Now()
+
+	if _, exists := em.ClientData[clientID]; !exists {
+		em.ClientData[clientID] = make(map[string]string)
+	}
+}
+
+// ShouldSendFullData 判断是否应该发送全量数据
+func (em *ElectricModelManager) ShouldSendFullData(intervalSeconds int) bool {
+	em.Mutex.RLock()
+	defer em.Mutex.RUnlock()
+
+	return time.Since(em.ClientLastFullData) >= time.Duration(intervalSeconds)*time.Second
+}
+
+// UpdateFullDataTimestamp 更新全量数据发送时间戳
+func (em *ElectricModelManager) UpdateFullDataTimestamp() {
+	em.Mutex.Lock()
+	defer em.Mutex.Unlock()
+
+	em.ClientLastFullData = time.Now()
+}
+
+// CompareWithClientData 比较当前数据与客户端数据的差异
+func (em *ElectricModelManager) CompareWithClientData(clientID string, currentData map[string]string) map[string]string {
+	em.Mutex.RLock()
+	defer em.Mutex.RUnlock()
+
+	clientData, exists := em.ClientData[clientID]
+	if !exists {
+		// 客户端尚未注册或无历史数据,返回全部当前数据
+		return currentData
+	}
+
+	changedData := make(map[string]string)
+	for id, value := range currentData {
+		if clientValue, ok := clientData[id]; !ok || clientValue != value {
+			changedData[id] = value
+		}
+	}
+
+	return changedData
+}
+
+// UpdateClientData 更新客户端数据记录
+func (em *ElectricModelManager) UpdateClientData(clientID string, data map[string]string) {
+	em.Mutex.Lock()
+	defer em.Mutex.Unlock()
+
+	em.ClientData[clientID] = data
+	em.ClientLastAccess[clientID] = time.Now()
+}
+
+// cleanupInactiveClients 定期清理长时间未访问的客户端数据
+func (em *ElectricModelManager) cleanupInactiveClients() {
+	ticker := time.NewTicker(1 * time.Minute)
+	defer ticker.Stop()
+
+	for {
+		select {
+		case <-ticker.C:
+			em.Mutex.Lock()
+			now := time.Now()
+			for clientID, lastAccess := range em.ClientLastAccess {
+				// 如果客户端超过5分钟未访问,则清理其数据
+				if now.Sub(lastAccess) > 5*time.Minute {
+					delete(em.ClientData, clientID)
+					delete(em.ClientLastAccess, clientID)
+				}
+			}
+			em.Mutex.Unlock()
+		}
+	}
+}

+ 16 - 2
Tech/routers/router.go

@@ -20,6 +20,11 @@ func init() {
 	beego.Router("/course/update", &controllers.CourseController{}, "post:Update")
 	beego.Router("/course/delete", &controllers.CourseController{}, "post:Delete")
 
+	// 获取课程分类列表
+	beego.Router("/gettmscoursetype", &controllers.CourseController{}, "post:GetTMSCourseType")
+	// 课程上传
+	beego.Router("/course/uploadtms", &controllers.CourseController{}, "post:UploadTMS")
+
 	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")
@@ -52,7 +57,7 @@ func init() {
 	beego.Router("/eva/orderupdate", &controllers.EvaController{}, "post:Orderupdate")
 
 	//电调
-	beego.Router("/courserun/eqpmtvalue", &controllers.EDispatchController{}, "post:Eqpmtvalue")
+	beego.Router("/courserun/eqpmtstatus", &controllers.EDispatchController{}, "post:Eqpmtvalue")
 	beego.Router("/courserun/add_ctrl_log", &controllers.EDispatchController{}, "post:Addctrllog")
 
 	//设置设备状态 UI->Server->电调
@@ -64,7 +69,7 @@ func init() {
 	beego.Router("/serverctrl/subsysstatus", &controllers.EDispatchController{}, "post:SubSysStatus")
 	beego.Router("/serverctrl/alleqpmtList", &controllers.EDispatchController{}, "post:AllEqpmtList")
 
-	beego.Router("/serverctrl/alleqpmtList2", &controllers.EDispatchController{}, "get:AllEqpmtList2")
+	// beego.Router("/serverctrl/alleqpmtList2", &controllers.EDispatchController{}, "get:AllEqpmtList2")
 
 	//运行记录
 	beego.Router("/runrecord/list ", &controllers.CourseRunController{}, "post:RunRecordList")
@@ -97,4 +102,13 @@ func init() {
 
 	// /serverctrl/geteqpmtfault 获取电调故障列表信息
 	beego.Router("/serverctrl/geteqpmtfault", &controllers.EDispatchController{}, "post:GetEqpmtFault")
+
+	// /course/tmsplan 获取TMS课程计划
+	beego.Router("/course/tmsplan", &controllers.CourseController{}, "post:GetTMSCoursePlan")
+
+	// /course/tmsplanstudent 获取TMS课程计划参与的学生
+	beego.Router("/course/tmsplanstudent", &controllers.CourseController{}, "post:GetTMSCoursePlanStudent")
+
+	// /evalog/uploadtms 课程评价记录上传信息化系统
+	beego.Router("/evalog/uploadtms", &controllers.EvaLogController{}, "post:UploadTMS")
 }

+ 41 - 216
Tech/test.json

@@ -1,217 +1,42 @@
-{
-    "nodeId": "935741",
-    "nodeName": "开始",
-    "nodeType": "start",
-    "nodeData": {},
-    "childNode": {
-        "nodeId": "330600",
-        "nodeName": "语音交互",
-        "nodeType": "voice",
-        "nodeData": {
-            "nodeID": 330600,
-            "nodeName": "2222",
-            "bindRole": 104,
-            "nodeDetail": "",
-            "delaySkip": 0,
-            "loseItem": 1,
-            "virtualOperation": 0,
-            "nodeTrigger": {
-                "nodeID": 330600,
-                "triggerMode": 0,
-                "textParam1": "",
-                "textParam2": "",
-                "event": "",
-                "triggerDelay": 0
-            },
-            "voiceNode": {
-                "templateID": 3002,
-                "categoryName": "目标分类",
-                "subCategoryName": "远端人员",
-                "bindRole": 102,
-                "voiceContent": "222",
-                "evalContent": "333"
-            }
-        },
-        "childNode": {
-            "nodeId": "390371",
-            "nodeName": "基本操作",
-            "nodeType": "operation",
-            "nodeData": {
-                "nodeID": 390371,
-                "nodeName": "名称",
-                "bindRole": 102,
-                "nodeDetail": "",
-                "delaySkip": 0,
-                "loseItem": 1,
-                "virtualOperation": 0,
-                "nodeTrigger": {
-                    "nodeID": 390371,
-                    "triggerMode": 0,
-                    "textParam1": "",
-                    "textParam2": "",
-                    "event": "",
-                    "triggerDelay": 0
-                },
-                "operationNode": {
-                    "templateID": 1002,
-                    "categoryName": "电力设备控制",
-                    "subCategoryName": "分合闸",
-                    "param1Type": 2,
-                    "param2Type": 2,
-                    "param3Type": 2,
-                    "param4Type": 2,
-                    "param5Type": 2,
-                    "param1Desc": "111",
-                    "param2Desc": "222",
-                    "param3Desc": "",
-                    "param4Desc": "",
-                    "param5Desc": "",
-                    "score": 10,
-                    "deduction": 0
-                }
-            },
-            "childNode": {
-                "nodeId": "918257",
-                "nodeName": "语音交互",
-                "nodeType": "voice",
-                "nodeData": {
-                    "nodeID": 918257,
-                    "name": "根据工作票向路局供电调度申请准许作业命令",
-                    "bindRole": 103,
-                    "nodeDetail": "",
-                    "delaySkip": 0,
-                    "loseItem": 1,
-                    "virtualOperation": 0,
-                    "nodeTrigger": {
-                        "nodeID": 918257,
-                        "triggerMode": 0,
-                        "textParam1": "",
-                        "textParam2": "",
-                        "event": "",
-                        "triggerDelay": 0
-                    },
-                    "voiceNode": {
-                        "templateID": 3001,
-                        "categoryName": "目标分类",
-                        "subCategoryName": "远端人员",
-                        "bindRole": 101,
-                        "voiceContent": "供电调度请求批准准许作业",
-                        "evalContent": "准许作业"
-                    },
-                    "nodeName": "111"
-                },
-                "childNode": {
-                    "nodeId": "534140",
-                    "nodeName": "基本操作",
-                    "nodeType": "operation",
-                    "nodeData": {
-                        "nodeID": 534140,
-                        "name": "根据申请远方操作断开供电臂互供的两变(配)电所对应馈出柜的断路器及隔离开关",
-                        "bindRole": 101,
-                        "nodeDetail": "",
-                        "delaySkip": 0,
-                        "loseItem": 1,
-                        "virtualOperation": 0,
-                        "nodeTrigger": {
-                            "nodeID": 534140,
-                            "triggerMode": 0,
-                            "textParam1": "",
-                            "textParam2": "",
-                            "event": "",
-                            "triggerDelay": 0
-                        },
-                        "operationNode": {
-                            "templateID": 1001,
-                            "categoryName": "电力设备控制",
-                            "subCategoryName": "分合闸",
-                            "param1Type": 2,
-                            "param2Type": 2,
-                            "param3Type": 2,
-                            "param4Type": 2,
-                            "param5Type": 2,
-                            "param1Desc": "34234324",
-                            "param2Desc": "324324",
-                            "param3Desc": "",
-                            "param4Desc": "",
-                            "param5Desc": "",
-                            "score": 10,
-                            "deduction": 0
-                        },
-                        "nodeName": "222"
-                    },
-                    "childNode": {
-                        "nodeId": "405697",
-                        "nodeName": "语音交互",
-                        "nodeType": "voice",
-                        "nodeData": {
-                            "nodeID": 405697,
-                            "name": "确认停电后,路局供电调度下令相邻两个变配电所将已停电高压柜“远方/就地”开关打到“就地”位,确认",
-                            "bindRole": 101,
-                            "nodeDetail": "",
-                            "delaySkip": 0,
-                            "loseItem": 1,
-                            "virtualOperation": 0,
-                            "nodeTrigger": {
-                                "nodeID": 405697,
-                                "triggerMode": 0,
-                                "textParam1": "",
-                                "textParam2": "",
-                                "event": "",
-                                "triggerDelay": 0
-                            },
-                            "voiceNode": {
-                                "templateID": 3002,
-                                "categoryName": "目标分类",
-                                "subCategoryName": "远端人员",
-                                "bindRole": 102,
-                                "voiceContent": "将变配电所将已停电高压柜“远方/就地”开关打到“就地”位",
-                                "evalContent": "“远方/就地”,就地”位"
-                            },
-                            "nodeName": "333"
-                        },
-                        "childNode": {
-                            "nodeId": "134762",
-                            "nodeName": "基本操作",
-                            "nodeType": "operation",
-                            "nodeData": {
-                                "nodeID": 134762,
-                                "name": "验明无电,开锁,合上相邻两个变配电所停电高压柜的接地开关,确认,加锁;",
-                                "bindRole": 102,
-                                "nodeDetail": "",
-                                "delaySkip": 0,
-                                "loseItem": 1,
-                                "virtualOperation": 0,
-                                "nodeTrigger": {
-                                    "nodeID": 134762,
-                                    "triggerMode": 0,
-                                    "textParam1": "",
-                                    "textParam2": "",
-                                    "event": "",
-                                    "triggerDelay": 0
-                                },
-                                "operationNode": {
-                                    "templateID": 1012,
-                                    "categoryName": "电力设备控制",
-                                    "subCategoryName": "权限控制",
-                                    "param1Type": 2,
-                                    "param2Type": 2,
-                                    "param3Type": 2,
-                                    "param4Type": 2,
-                                    "param5Type": 2,
-                                    "param1Desc": "423423",
-                                    "param2Desc": "",
-                                    "param3Desc": "",
-                                    "param4Desc": "",
-                                    "param5Desc": "",
-                                    "score": 15,
-                                    "deduction": 0
-                                },
-                                "nodeName": "4444111111"
-                            }
-                        }
-                    }
-                }
-            }
-        }
+[
+    {
+        "nodeId": "1",
+        "nodeName": "步骤1",
+        "nodeResult": "OK"
+    },
+    {
+        "nodeId": "2",
+        "nodeName": "步骤2",
+        "nodeResult": "OK"
+    },
+    {
+        "nodeId": "3",
+        "nodeName": "步骤3",
+        "nodeResult": "OK"
+    },
+    {
+        "nodeId": "4",
+        "nodeName": "步骤4",
+        "nodeResult": "OK"
+    },
+    {
+        "nodeId": "5",
+        "nodeName": "步骤5",
+        "nodeResult": "OK"
+    },
+    {
+        "nodeId": "6",
+        "nodeName": "步骤6",
+        "nodeResult": "OK"
     }
-}
+        {
+        "nodeId": "7",
+        "nodeName": "步骤7",
+        "nodeResult": "OK"
+    },
+    {
+        "nodeId": "8",
+        "nodeName": "步骤8",
+        "nodeResult": "OK"
+    }
+]

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1086 - 0
Tech/实训设备多人培训与信息化系统交互接口协议(带能力项)V1.0.0(1).pdf


+ 1 - 0
Tech/生成key.txt

@@ -0,0 +1 @@
+openssl req -x509 -newkey rsa:2048 -nodes -keyout server.key -out server.crt -days 3650

+ 55 - 0
frpc.toml

@@ -0,0 +1,55 @@
+serverAddr = "124.222.28.5"
+serverPort = 7000
+
+[[proxies]]
+name = "test-tcp-aaa-22_2"
+type = "tcp"
+localIP = "127.0.0.1"
+localPort = 22
+remotePort = 7311
+
+[[proxies]]
+name = "test-tcp-aaa-8527_2"
+type = "tcp"
+localIP = "127.0.0.1"
+localPort = 8527
+remotePort = 7312
+
+[[proxies]]
+name = "test-tcp-aaa-7070_2"
+type = "tcp"
+localIP = "127.0.0.1"
+localPort = 7070
+remotePort = 7313
+
+
+[[proxies]]
+name = "test-tcp-aaa-6090_2"
+type = "tcp"
+localIP = "127.0.0.1"
+localPort = 6090
+remotePort = 7314
+
+
+[[proxies]]
+name = "test-tcp-aaa-9001_2"
+type = "tcp"
+localIP = "127.0.0.1"
+localPort = 9001
+remotePort = 7315
+
+
+
+[[proxies]]
+name = "test-tcp-aaa-8443_2"
+type = "tcp"
+localIP = "127.0.0.1"
+localPort = 8443
+remotePort = 7317
+
+[[proxies]]
+name = "test-tcp-aaa-84438094_2"
+type = "tcp"
+localIP = "192.168.1.251"
+localPort = 8094
+remotePort = 7321