yaojianjun преди 9 месеца
родител
ревизия
997b7a0353
променени са 10 файла, в които са добавени 1261 реда и са изтрити 12 реда
  1. 561 0
      Tech/common/logger.go
  2. 433 0
      Tech/controllers/日志.go
  3. 108 0
      Tech/controllers/用户.go
  4. 48 0
      Tech/controllers/课程.go
  5. 5 1
      Tech/go.mod
  6. 9 0
      Tech/go.sum
  7. 14 0
      Tech/log/app.log
  8. 9 0
      Tech/log/info.log
  9. 67 11
      Tech/main.go
  10. 7 0
      Tech/routers/router.go

+ 561 - 0
Tech/common/logger.go

@@ -0,0 +1,561 @@
+package common
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"time"
+
+	"github.com/sirupsen/logrus"
+	"gopkg.in/natefinch/lumberjack.v2"
+)
+
+var (
+	Logger *logrus.Logger
+	
+	// 不同级别的日志文件writer
+	infoLogger  *lumberjack.Logger
+	warnLogger  *lumberjack.Logger
+	errorLogger *lumberjack.Logger
+	mainLogger  *lumberjack.Logger
+)
+
+// 日志级别常量
+const (
+	LogLevelTrace = "trace"
+	LogLevelDebug = "debug"
+	LogLevelInfo  = "info"
+	LogLevelWarn  = "warn"
+	LogLevelError = "error"
+	LogLevelFatal = "fatal"
+	LogLevelPanic = "panic"
+)
+
+// LogConfig 日志配置结构体
+type LogConfig struct {
+	LogDir      string   // 日志目录
+	LogLevel    string   // 日志级别
+	MaxSize     int      // 每个日志文件最大大小(MB)
+	MaxBackups  int      // 保留的旧日志文件数量
+	MaxAge      int      // 保留日志文件的最大天数
+	Compress    bool     // 是否压缩旧日志文件
+	LogModules  []string // 需要记录日志的模块
+	EnableCaller bool    // 是否显示调用者信息
+}
+
+// InitLogger 初始化日志系统
+func InitLogger(config *LogConfig) error {
+	if config == nil {
+		// 使用默认配置
+		config = getDefaultLogConfig()
+	}
+
+	// 确保日志目录存在
+	if err := os.MkdirAll(config.LogDir, 0755); err != nil {
+		return fmt.Errorf("创建日志目录失败: %v", err)
+	}
+
+	// 创建logrus实例
+	Logger = logrus.New()
+
+	// 设置日志级别
+	level, err := logrus.ParseLevel(config.LogLevel)
+	if err != nil {
+		level = logrus.InfoLevel
+	}
+	Logger.SetLevel(level)
+
+	// 设置日志格式
+	Logger.SetFormatter(&logrus.JSONFormatter{
+		TimestampFormat: "2006-01-02 15:04:05",
+	})
+
+	// 设置调用者信息
+	Logger.SetReportCaller(config.EnableCaller)
+
+	// 设置日志输出
+	if err := setLogOutput(config); err != nil {
+		return err
+	}
+
+	// 记录系统启动日志
+	Logger.WithFields(logrus.Fields{
+		"event": "system_startup",
+		"pid":   os.Getpid(),
+	}).Info("SCADA培训系统启动")
+
+	return nil
+}
+
+// setLogOutput 设置日志输出
+func setLogOutput(config *LogConfig) error {
+	// 创建不同级别的日志文件
+	infoLogPath := filepath.Join(config.LogDir, "info.log")
+	warnLogPath := filepath.Join(config.LogDir, "warn.log")
+	errorLogPath := filepath.Join(config.LogDir, "error.log")
+
+	// 创建主日志文件
+	mainLogPath := filepath.Join(config.LogDir, "app.log")
+	
+	// 为主日志创建writer
+	mainWriter := &lumberjack.Logger{
+		Filename:   mainLogPath,
+		MaxSize:    config.MaxSize,
+		MaxBackups: config.MaxBackups,
+		MaxAge:     config.MaxAge,
+		Compress:   config.Compress,
+	}
+
+	// 同时输出到控制台和文件
+	// 使用MultiWriter同时写入控制台和文件
+	Logger.SetOutput(mainWriter)
+	
+	// 保存writer引用,用于后续使用
+	mainLogger = mainWriter
+	infoLogger = &lumberjack.Logger{
+		Filename:   infoLogPath,
+		MaxSize:    config.MaxSize,
+		MaxBackups: config.MaxBackups,
+		MaxAge:     config.MaxAge,
+		Compress:   config.Compress,
+	}
+	warnLogger = &lumberjack.Logger{
+		Filename:   warnLogPath,
+		MaxSize:    config.MaxSize,
+		MaxBackups: config.MaxBackups,
+		MaxAge:     config.MaxAge,
+		Compress:   config.Compress,
+	}
+	errorLogger = &lumberjack.Logger{
+		Filename:   errorLogPath,
+		MaxSize:    config.MaxSize,
+		MaxBackups: config.MaxBackups,
+		MaxAge:     config.MaxAge,
+		Compress:   config.Compress,
+	}
+
+	// 启动日志清理协程
+	go startLogCleaner(config)
+
+	return nil
+}
+
+// startLogCleaner 启动日志清理协程
+func startLogCleaner(config *LogConfig) {
+	// 每天凌晨2点执行日志清理
+	ticker := time.NewTicker(24 * time.Hour)
+	defer ticker.Stop()
+
+	// 计算到下一个凌晨2点的时间
+	now := time.Now()
+	next := time.Date(now.Year(), now.Month(), now.Day(), 2, 0, 0, 0, now.Location())
+	if next.Before(now) {
+		next = next.Add(24 * time.Hour)
+	}
+	initialDelay := next.Sub(now)
+	
+	// 先等待到下一个凌晨2点
+	time.Sleep(initialDelay)
+
+	// 执行日志清理
+	cleanOldLogs(config)
+
+	for range ticker.C {
+		cleanOldLogs(config)
+	}
+}
+
+// cleanOldLogs 清理旧的日志文件
+func cleanOldLogs(config *LogConfig) {
+	if config.MaxAge <= 0 {
+		return // 不启用自动清理
+	}
+
+	// 清理info级别的日志
+	cleanOldLogsByPattern(config.LogDir, "info-*.log.gz", config.MaxAge)
+	cleanOldLogsByPattern(config.LogDir, "info-*.log", config.MaxAge)
+	
+	// 清理warn级别的日志
+	cleanOldLogsByPattern(config.LogDir, "warn-*.log.gz", config.MaxAge)
+	cleanOldLogsByPattern(config.LogDir, "warn-*.log", config.MaxAge)
+	
+	// 清理error级别的日志
+	cleanOldLogsByPattern(config.LogDir, "error-*.log.gz", config.MaxAge)
+	cleanOldLogsByPattern(config.LogDir, "error-*.log", config.MaxAge)
+
+	// 记录日志清理操作
+	Logger.WithFields(logrus.Fields{
+		"event": "log_cleanup",
+		"max_age_days": config.MaxAge,
+	}).Info("执行日志清理操作")
+}
+
+// cleanOldLogsByPattern 根据模式清理旧的日志文件
+func cleanOldLogsByPattern(logDir, pattern string, maxAge int) {
+	matches, err := filepath.Glob(filepath.Join(logDir, pattern))
+	if err != nil {
+		Logger.WithFields(logrus.Fields{
+			"event": "log_cleanup_error",
+			"error": err.Error(),
+			"pattern": pattern,
+		}).Error("获取日志文件列表失败")
+		return
+	}
+
+	cutoffTime := time.Now().AddDate(0, 0, -maxAge)
+	
+	for _, filePath := range matches {
+		fileInfo, err := os.Stat(filePath)
+		if err != nil {
+			continue
+		}
+
+		if fileInfo.ModTime().Before(cutoffTime) {
+			if err := os.Remove(filePath); err != nil {
+				Logger.WithFields(logrus.Fields{
+					"event": "log_file_delete_error",
+					"error": err.Error(),
+					"file": filePath,
+				}).Error("删除旧日志文件失败")
+			} else {
+				Logger.WithFields(logrus.Fields{
+					"event": "log_file_deleted",
+					"file": filePath,
+					"mod_time": fileInfo.ModTime().Format("2006-01-02 15:04:05"),
+				}).Debug("已删除旧日志文件")
+			}
+		}
+	}
+}
+
+// SetLogLevel 动态设置日志级别
+func SetLogLevel(level string) error {
+	if Logger == nil {
+		return fmt.Errorf("日志系统未初始化")
+	}
+	
+	logLevel, err := logrus.ParseLevel(level)
+	if err != nil {
+		return fmt.Errorf("无效的日志级别: %s", level)
+	}
+	
+	Logger.SetLevel(logLevel)
+	Logger.WithFields(logrus.Fields{
+		"event": "log_level_changed",
+		"new_level": level,
+	}).Info("日志级别已更新")
+	
+	return nil
+}
+
+// GetLogFilesInfo 获取日志文件信息
+func GetLogFilesInfo() ([]map[string]interface{}, error) {
+	if Logger == nil {
+		return nil, fmt.Errorf("日志系统未初始化")
+	}
+
+	// 获取log目录
+	logDir := filepath.Dir(getDefaultLogConfig().LogDir)
+	
+	// 查找所有日志文件
+	patterns := []string{"info*.log", "warn*.log", "error*.log"}
+	var result []map[string]interface{}
+	
+	for _, pattern := range patterns {
+		matches, err := filepath.Glob(filepath.Join(logDir, pattern))
+		if err != nil {
+			continue
+		}
+		
+		for _, filePath := range matches {
+			fileInfo, err := os.Stat(filePath)
+			if err != nil {
+				continue
+			}
+			
+			fileInfoMap := map[string]interface{}{
+				"name":     filepath.Base(filePath),
+				"path":     filePath,
+				"size":     fileInfo.Size(),
+				"mod_time": fileInfo.ModTime().Format("2006-01-02 15:04:05"),
+				"is_compressed": strings.HasSuffix(filePath, ".gz"),
+			}
+			
+			result = append(result, fileInfoMap)
+		}
+	}
+	
+	return result, nil
+}
+
+// getDefaultLogConfig 获取默认日志配置
+func getDefaultLogConfig() *LogConfig {
+	exePath, err := os.Executable()
+	if err != nil {
+		exePath = "."
+	}
+	exeDir := filepath.Dir(exePath)
+
+	return &LogConfig{
+		LogDir:      filepath.Join(exeDir, "log"),
+		LogLevel:    "info",
+		MaxSize:     100, // 100MB
+		MaxBackups:  30,  // 保留30个旧文件
+		MaxAge:      90,  // 保留90天
+		Compress:    true,
+		EnableCaller: true,
+	}
+}
+
+// GetLogger 获取日志实例
+func GetLogger() *logrus.Logger {
+	if Logger == nil {
+		// 如果未初始化,使用默认配置初始化
+		InitLogger(nil)
+	}
+	return Logger
+}
+
+// LogWithFields 带字段的日志记录
+func LogWithFields(fields logrus.Fields) *logrus.Entry {
+	return GetLogger().WithFields(fields)
+}
+
+// LogInfo 记录信息级别日志
+func LogInfo(msg string, fields ...logrus.Fields) {
+	logger := GetLogger()
+	
+	// 写入主日志文件
+	if len(fields) > 0 {
+		entry := logger.WithFields(fields[0])
+		entry.Info(msg)
+		// 同时写入info级别的日志文件
+		writeToLevelFile("info", msg, fields[0])
+	} else {
+		logger.Info(msg)
+		// 同时写入info级别的日志文件
+		writeToLevelFile("info", msg, nil)
+	}
+}
+
+// LogWarn 记录警告级别日志
+func LogWarn(msg string, fields ...logrus.Fields) {
+	logger := GetLogger()
+	
+	// 写入主日志文件
+	if len(fields) > 0 {
+		entry := logger.WithFields(fields[0])
+		entry.Warn(msg)
+		// 同时写入warn级别的日志文件
+		writeToLevelFile("warn", msg, fields[0])
+	} else {
+		logger.Warn(msg)
+		// 同时写入warn级别的日志文件
+		writeToLevelFile("warn", msg, nil)
+	}
+}
+
+// LogError 记录错误级别日志
+func LogError(msg string, err error, fields ...logrus.Fields) {
+	logger := GetLogger()
+	logFields := logrus.Fields{}
+	if err != nil {
+		logFields["error"] = err.Error()
+	}
+	if len(fields) > 0 {
+		for k, v := range fields[0] {
+			logFields[k] = v
+		}
+	}
+	
+	// 写入主日志文件
+	entry := logger.WithFields(logFields)
+	entry.Error(msg)
+	
+	// 同时写入error级别的日志文件
+	writeToLevelFile("error", msg, logFields)
+}
+
+// LogDebug 记录调试级别日志
+func LogDebug(msg string, fields ...logrus.Fields) {
+	logger := GetLogger()
+	
+	// 写入主日志文件(debug级别不单独记录到debug.log)
+	if len(fields) > 0 {
+		entry := logger.WithFields(fields[0])
+		entry.Debug(msg)
+	} else {
+		logger.Debug(msg)
+	}
+}
+
+// LogTrace 记录跟踪级别日志
+func LogTrace(msg string, fields ...logrus.Fields) {
+	logger := GetLogger()
+	
+	// 写入主日志文件(trace级别不单独记录到trace.log)
+	if len(fields) > 0 {
+		entry := logger.WithFields(fields[0])
+		entry.Trace(msg)
+	} else {
+		logger.Trace(msg)
+	}
+}
+
+// writeToLevelFile 将日志写入对应的级别日志文件
+func writeToLevelFile(level, msg string, fields logrus.Fields) {
+	var writer *lumberjack.Logger
+	switch level {
+	case "info":
+		writer = infoLogger
+	case "warn":
+		writer = warnLogger
+	case "error":
+		writer = errorLogger
+	default:
+		return
+	}
+	
+	if writer == nil {
+		return
+	}
+	
+	// 创建文件专用的logger
+	fileLogger := logrus.New()
+	fileLogger.SetFormatter(&logrus.JSONFormatter{
+		TimestampFormat: "2006-01-02 15:04:05",
+	})
+	fileLogger.SetOutput(writer)
+	
+	// 根据级别设置日志级别
+	switch level {
+	case "info":
+		fileLogger.SetLevel(logrus.InfoLevel)
+		if fields != nil {
+			fileLogger.WithFields(fields).Info(msg)
+		} else {
+			fileLogger.Info(msg)
+		}
+	case "warn":
+		fileLogger.SetLevel(logrus.WarnLevel)
+		if fields != nil {
+			fileLogger.WithFields(fields).Warn(msg)
+		} else {
+			fileLogger.Warn(msg)
+		}
+	case "error":
+		fileLogger.SetLevel(logrus.ErrorLevel)
+		if fields != nil {
+			fileLogger.WithFields(fields).Error(msg)
+		} else {
+			fileLogger.Error(msg)
+		}
+	}
+}
+
+// LogRequest 记录HTTP请求日志
+func LogRequest(method, path, ip, userAgent string, statusCode int, duration time.Duration) {
+	GetLogger().WithFields(logrus.Fields{
+		"event":      "http_request",
+		"method":     method,
+		"path":       path,
+		"ip":         ip,
+		"user_agent": userAgent,
+		"status":     statusCode,
+		"duration":   duration.Milliseconds(),
+	}).Info("HTTP请求")
+}
+
+// LogResponse 记录HTTP响应日志
+func LogResponse(data interface{}, fields ...logrus.Fields) {
+	logFields := logrus.Fields{
+		"event": "http_response",
+		"data":  data,
+	}
+	if len(fields) > 0 {
+		for k, v := range fields[0] {
+			logFields[k] = v
+		}
+	}
+	GetLogger().WithFields(logFields).Info("HTTP响应")
+}
+
+// LogDBOperation 记录数据库操作日志
+func LogDBOperation(operation, table string, duration time.Duration, fields ...logrus.Fields) {
+	logFields := logrus.Fields{
+		"event":     "db_operation",
+		"operation": operation,
+		"table":     table,
+		"duration":  duration.Milliseconds(),
+	}
+	if len(fields) > 0 {
+		for k, v := range fields[0] {
+			logFields[k] = v
+		}
+	}
+	GetLogger().WithFields(logFields).Info("数据库操作")
+}
+
+// LogUserAction 记录用户操作日志
+func LogUserAction(userID, userName, action, module string, fields ...logrus.Fields) {
+	logFields := logrus.Fields{
+		"event":     "user_action",
+		"user_id":   userID,
+		"user_name": userName,
+		"action":    action,
+		"module":    module,
+	}
+	if len(fields) > 0 {
+		for k, v := range fields[0] {
+			logFields[k] = v
+		}
+	}
+	GetLogger().WithFields(logFields).Info("用户操作")
+}
+
+// LogSystemError 记录系统错误日志
+func LogSystemError(component, operation string, err error, fields ...logrus.Fields) {
+	logFields := logrus.Fields{
+		"event":     "system_error",
+		"component": component,
+		"operation": operation,
+		"error":     err.Error(),
+	}
+	
+	// 添加调用者信息
+	if pc, file, line, ok := runtime.Caller(1); ok {
+		funcName := runtime.FuncForPC(pc).Name()
+		logFields["caller"] = fmt.Sprintf("%s:%d:%s", filepath.Base(file), line, funcName)
+	}
+	
+	if len(fields) > 0 {
+		for k, v := range fields[0] {
+			logFields[k] = v
+		}
+	}
+	GetLogger().WithFields(logFields).Error("系统错误")
+}
+
+// LogBusinessEvent 记录业务事件日志
+func LogBusinessEvent(eventType, description string, fields ...logrus.Fields) {
+	logFields := logrus.Fields{
+		"event":       "business_event",
+		"event_type":  eventType,
+		"description": description,
+	}
+	if len(fields) > 0 {
+		for k, v := range fields[0] {
+			logFields[k] = v
+		}
+	}
+	GetLogger().WithFields(logFields).Info("业务事件")
+}
+
+// CloseLogger 关闭日志系统
+func CloseLogger() {
+	if Logger != nil {
+		Logger.Info("SCADA培训系统关闭")
+	}
+}

+ 433 - 0
Tech/controllers/日志.go

@@ -0,0 +1,433 @@
+package controllers
+
+import (
+	"Tech/common"
+	"encoding/json"
+	"os"
+	"path/filepath"
+	"strings"
+
+	beego "github.com/beego/beego/v2/server/web"
+	"github.com/sirupsen/logrus"
+)
+
+type LogController struct {
+	beego.Controller
+}
+
+// LogList 获取日志文件列表
+func (c *LogController) LogList() {
+	// 验证用户权限
+	userName := c.Ctx.Input.Header("userName")
+	if userName == "" {
+		c.Ctx.Output.SetStatus(401)
+		c.Data["json"] = map[string]interface{}{
+			"code": 401,
+			"msg":  "未提供认证信息",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 记录操作日志
+	clientIP := c.Ctx.Input.IP()
+	common.LogUserAction("", userName, "log_list", "system", logrus.Fields{
+		"ip": clientIP,
+	})
+
+	// 获取日志文件信息
+	logFiles, err := common.GetLogFilesInfo()
+	if err != nil {
+		c.Ctx.Output.SetStatus(500)
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "获取日志文件信息失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	c.Data["json"] = map[string]interface{}{
+		"code": 200,
+		"data": logFiles,
+		"msg":  "获取成功",
+	}
+	c.ServeJSON()
+}
+
+// LogView 查看日志文件内容
+func (c *LogController) LogView() {
+	// 验证用户权限
+	userName := c.Ctx.Input.Header("userName")
+	if userName == "" {
+		c.Ctx.Output.SetStatus(401)
+		c.Data["json"] = map[string]interface{}{
+			"code": 401,
+			"msg":  "未提供认证信息",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	type LogViewRequest struct {
+		FileName string `json:"fileName"`
+		Lines    int    `json:"lines"`    // 读取的行数,默认100
+		Offset   int    `json:"offset"`   // 从末尾开始的偏移量,默认0
+	}
+
+	var req LogViewRequest
+	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.Lines <= 0 {
+		req.Lines = 100
+	}
+
+	// 验证文件名,防止路径遍历攻击
+	if strings.Contains(req.FileName, "..") || strings.Contains(req.FileName, "/") || strings.Contains(req.FileName, "\\") {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "非法的文件名",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 检查文件是否是日志文件
+	if !strings.HasPrefix(req.FileName, "info") && !strings.HasPrefix(req.FileName, "warn") && !strings.HasPrefix(req.FileName, "error") || !strings.HasSuffix(req.FileName, ".log") && !strings.HasSuffix(req.FileName, ".log.gz") {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "非日志文件",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 构建文件完整路径
+	logDir := filepath.Join(".", "log")
+	filePath := filepath.Join(logDir, req.FileName)
+
+	// 检查文件是否存在
+	if _, err := os.Stat(filePath); os.IsNotExist(err) {
+		c.Ctx.Output.SetStatus(404)
+		c.Data["json"] = map[string]interface{}{
+			"code": 404,
+			"msg":  "文件不存在",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 读取文件内容
+	var content string
+	var err error
+
+	if strings.HasSuffix(req.FileName, ".gz") {
+		// 压缩文件需要特殊处理,这里简化处理,返回提示信息
+		content = "压缩文件暂不支持在线查看,请下载后查看"
+	} else {
+		content, err = readLastLines(filePath, req.Lines, req.Offset)
+		if err != nil {
+			c.Ctx.Output.SetStatus(500)
+			c.Data["json"] = map[string]interface{}{
+				"code": 500,
+				"msg":  "读取文件失败: " + err.Error(),
+			}
+			c.ServeJSON()
+			return
+		}
+	}
+
+	// 记录操作日志
+	clientIP := c.Ctx.Input.IP()
+	common.LogUserAction("", userName, "log_view", "system", logrus.Fields{
+		"ip":       clientIP,
+		"file_name": req.FileName,
+		"lines":    req.Lines,
+		"offset":   req.Offset,
+	})
+
+	c.Data["json"] = map[string]interface{}{
+		"code": 200,
+		"data": map[string]interface{}{
+			"content": content,
+			"file":    req.FileName,
+		},
+		"msg": "获取成功",
+	}
+	c.ServeJSON()
+}
+
+// LogDownload 下载日志文件
+func (c *LogController) LogDownload() {
+	// 验证用户权限
+	userName := c.Ctx.Input.Header("userName")
+	if userName == "" {
+		c.Ctx.Output.SetStatus(401)
+		c.Data["json"] = map[string]interface{}{
+			"code": 401,
+			"msg":  "未提供认证信息",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	fileName := c.GetString("fileName")
+	if fileName == "" {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "文件名不能为空",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 验证文件名,防止路径遍历攻击
+	if strings.Contains(fileName, "..") || strings.Contains(fileName, "/") || strings.Contains(fileName, "\\") {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "非法的文件名",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 检查文件是否是日志文件
+	if !strings.HasPrefix(fileName, "info") && !strings.HasPrefix(fileName, "warn") && !strings.HasPrefix(fileName, "error") || !strings.HasSuffix(fileName, ".log") && !strings.HasSuffix(fileName, ".log.gz") {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "非日志文件",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 构建文件完整路径
+	logDir := filepath.Join(".", "log")
+	filePath := filepath.Join(logDir, fileName)
+
+	// 检查文件是否存在
+	if _, err := os.Stat(filePath); os.IsNotExist(err) {
+		c.Ctx.Output.SetStatus(404)
+		c.Data["json"] = map[string]interface{}{
+			"code": 404,
+			"msg":  "文件不存在",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 记录操作日志
+	clientIP := c.Ctx.Input.IP()
+	common.LogUserAction("", userName, "log_download", "system", logrus.Fields{
+		"ip":       clientIP,
+		"file_name": fileName,
+	})
+
+	// 设置下载响应头
+	c.Ctx.Output.Download(filePath, fileName)
+}
+
+// LogLevelSet 设置日志级别
+func (c *LogController) LogLevelSet() {
+	// 验证用户权限
+	userName := c.Ctx.Input.Header("userName")
+	if userName == "" {
+		c.Ctx.Output.SetStatus(401)
+		c.Data["json"] = map[string]interface{}{
+			"code": 401,
+			"msg":  "未提供认证信息",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	type LogLevelRequest struct {
+		Level string `json:"level"`
+	}
+
+	var req LogLevelRequest
+	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
+	}
+
+	// 验证日志级别
+	validLevels := map[string]bool{
+		"trace": true,
+		"debug": true,
+		"info":  true,
+		"warn":  true,
+		"error": true,
+		"fatal": true,
+		"panic": true,
+	}
+
+	if !validLevels[req.Level] {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "无效的日志级别",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 设置日志级别
+	err := common.SetLogLevel(req.Level)
+	if err != nil {
+		c.Ctx.Output.SetStatus(500)
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "设置日志级别失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 记录操作日志
+	clientIP := c.Ctx.Input.IP()
+	common.LogUserAction("", userName, "log_level_set", "system", logrus.Fields{
+		"ip":     clientIP,
+		"level":  req.Level,
+	})
+
+	c.Data["json"] = map[string]interface{}{
+		"code": 200,
+		"msg":  "设置成功",
+	}
+	c.ServeJSON()
+}
+
+// LogDelete 删除日志文件
+func (c *LogController) LogDelete() {
+	// 验证用户权限
+	userName := c.Ctx.Input.Header("userName")
+	if userName == "" {
+		c.Ctx.Output.SetStatus(401)
+		c.Data["json"] = map[string]interface{}{
+			"code": 401,
+			"msg":  "未提供认证信息",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	type LogDeleteRequest struct {
+		FileName string `json:"fileName"`
+	}
+
+	var req LogDeleteRequest
+	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 strings.Contains(req.FileName, "..") || strings.Contains(req.FileName, "/") || strings.Contains(req.FileName, "\\") {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "非法的文件名",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 检查文件是否是日志文件
+	if !strings.HasPrefix(req.FileName, "info") && !strings.HasPrefix(req.FileName, "warn") && !strings.HasPrefix(req.FileName, "error") || !strings.HasSuffix(req.FileName, ".log") && !strings.HasSuffix(req.FileName, ".log.gz") {
+		c.Ctx.Output.SetStatus(400)
+		c.Data["json"] = map[string]interface{}{
+			"code": 400,
+			"msg":  "非日志文件",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 构建文件完整路径
+	logDir := filepath.Join(".", "log")
+	filePath := filepath.Join(logDir, req.FileName)
+
+	// 检查文件是否存在
+	if _, err := os.Stat(filePath); os.IsNotExist(err) {
+		c.Ctx.Output.SetStatus(404)
+		c.Data["json"] = map[string]interface{}{
+			"code": 404,
+			"msg":  "文件不存在",
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 删除文件
+	err := os.Remove(filePath)
+	if err != nil {
+		c.Ctx.Output.SetStatus(500)
+		c.Data["json"] = map[string]interface{}{
+			"code": 500,
+			"msg":  "删除文件失败: " + err.Error(),
+		}
+		c.ServeJSON()
+		return
+	}
+
+	// 记录操作日志
+	clientIP := c.Ctx.Input.IP()
+	common.LogUserAction("", userName, "log_delete", "system", logrus.Fields{
+		"ip":       clientIP,
+		"file_name": req.FileName,
+	})
+
+	c.Data["json"] = map[string]interface{}{
+		"code": 200,
+		"msg":  "删除成功",
+	}
+	c.ServeJSON()
+}
+
+// readLastLines 读取文件最后N行
+func readLastLines(filePath string, lines int, offset int) (string, error) {
+	// 读取文件内容
+	content, err := os.ReadFile(filePath)
+	if err != nil {
+		return "", err
+	}
+
+	// 按行分割
+	allLines := strings.Split(string(content), "\n")
+	
+	// 计算起始索引
+	startIndex := len(allLines) - lines - offset
+	if startIndex < 0 {
+		startIndex = 0
+	}
+
+	// 截取需要的行
+	resultLines := allLines[startIndex : len(allLines)-offset]
+	
+	return strings.Join(resultLines, "\n"), nil
+}

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

@@ -13,6 +13,7 @@ import (
 	"github.com/beego/beego/v2/client/orm"
 	"github.com/beego/beego/v2/core/logs"
 	beego "github.com/beego/beego/v2/server/web"
+	"github.com/sirupsen/logrus"
 )
 
 type UserController struct {
@@ -67,10 +68,18 @@ func decryptMD5Password(hash string) string {
 // initUserInfoCache 初始化用户信息缓存
 func InitUserInfoCache() {
 	logs.Info("开始初始化用户信息缓存...")
+	common.LogInfo("开始初始化用户信息缓存...", logrus.Fields{
+		"module": "user",
+		"action": "cache_init",
+	})
 
 	personalInfos, err := common.GetAllPersionInfo()
 	if err != nil {
 		logs.Error("获取所有用户信息失败: ", err)
+		common.LogError("获取所有用户信息失败", err, logrus.Fields{
+			"module": "user",
+			"action": "cache_init",
+		})
 		return
 	}
 
@@ -144,6 +153,11 @@ func InitUserInfoCache() {
 	}
 
 	logs.Info("用户信息缓存初始化完成,共缓存%d个用户到数据库", len(tempUser))
+	common.LogInfo("用户信息缓存初始化完成", logrus.Fields{
+		"module": "user",
+		"action": "cache_init_complete",
+		"user_count": len(tempUser),
+	})
 }
 func generateShortToken(userId string) string {
 	b := make([]byte, 6)
@@ -171,6 +185,10 @@ func (c *UserController) UserLogin() {
 	}
 	requestBody := c.Ctx.Input.RequestBody
 	println("Request Body:", string(requestBody))
+	
+	// 记录用户登录请求
+	clientIP := c.Ctx.Input.IP()
+	
 	if err := json.Unmarshal(c.Ctx.Input.RequestBody, &req); err != nil {
 		c.Ctx.Output.SetStatus(400)
 		c.Data["json"] = map[string]interface{}{
@@ -178,6 +196,13 @@ func (c *UserController) UserLogin() {
 			"msg":  "请求参数格式错误: " + err.Error(),
 		}
 		c.ServeJSON()
+		
+		// 记录登录失败日志
+		common.LogUserAction(req.UserName, req.UserName, "login_failed", "user", logrus.Fields{
+			"reason": "invalid_request_format",
+			"ip":     clientIP,
+			"error":  err.Error(),
+		})
 		return
 	}
 
@@ -188,6 +213,12 @@ func (c *UserController) UserLogin() {
 			"msg":  "用户名和密码不能为空",
 		}
 		c.ServeJSON()
+		
+		// 记录登录失败日志
+		common.LogUserAction(req.UserName, req.UserName, "login_failed", "user", logrus.Fields{
+			"reason": "empty_credentials",
+			"ip":     clientIP,
+		})
 		return
 	}
 
@@ -196,6 +227,14 @@ func (c *UserController) UserLogin() {
 	// 根据loginMode选择验证方式
 	if req.LoginMode == 1 {
 		// 本地数据库验证
+		common.LogDebug("用户登录尝试", logrus.Fields{
+			"module": "user",
+			"action": "local_login_attempt",
+			"username": req.UserName,
+			"login_mode": "local",
+			"ip": clientIP,
+		})
+		
 		o := orm.NewOrm()
 		var userData models.UserData
 		err := o.QueryTable(new(models.UserData)).Filter("人员ID", req.UserName).One(&userData)
@@ -206,6 +245,13 @@ func (c *UserController) UserLogin() {
 				"msg":  "用户名或密码错误",
 			}
 			c.ServeJSON()
+			
+			// 记录登录失败日志
+			common.LogUserAction(req.UserName, req.UserName, "login_failed", "user", logrus.Fields{
+				"reason": "user_not_found",
+				"login_mode": "local",
+				"ip": clientIP,
+			})
 			return
 		}
 
@@ -217,6 +263,13 @@ func (c *UserController) UserLogin() {
 				"msg":  "用户名或密码错误",
 			}
 			c.ServeJSON()
+			
+			// 记录登录失败日志
+			common.LogUserAction(req.UserName, req.UserName, "login_failed", "user", logrus.Fields{
+				"reason": "wrong_password",
+				"login_mode": "local",
+				"ip": clientIP,
+			})
 			return
 		}
 
@@ -236,6 +289,14 @@ func (c *UserController) UserLogin() {
 		CurrentLoginMode = 1
 	} else {
 		// 默认使用信息化系统验证 (req.LoginMode == 0)
+		common.LogDebug("用户登录尝试", logrus.Fields{
+			"module": "user",
+			"action": "info_system_login_attempt",
+			"username": req.UserName,
+			"login_mode": "info_system",
+			"ip": clientIP,
+		})
+		
 		loginResp, err := common.LoginToInformationSystem(req.UserName, req.Password)
 		if err != nil {
 			c.Ctx.Output.SetStatus(500)
@@ -244,6 +305,14 @@ func (c *UserController) UserLogin() {
 				"msg":  "信息化系统验证失败: " + err.Error(),
 			}
 			c.ServeJSON()
+			
+			// 记录登录失败日志
+			common.LogUserAction(req.UserName, req.UserName, "login_failed", "user", logrus.Fields{
+				"reason": "info_system_error",
+				"login_mode": "info_system",
+				"ip": clientIP,
+				"error": err.Error(),
+			})
 			return
 		}
 
@@ -255,6 +324,15 @@ func (c *UserController) UserLogin() {
 				"msg":  loginResp.ResultDesc,
 			}
 			c.ServeJSON()
+			
+			// 记录登录失败日志
+			common.LogUserAction(req.UserName, req.UserName, "login_failed", "user", logrus.Fields{
+				"reason": "info_system_rejected",
+				"login_mode": "info_system",
+				"ip": clientIP,
+				"result_code": loginResp.ResultCode,
+				"result_desc": loginResp.ResultDesc,
+			})
 			return
 		}
 
@@ -325,6 +403,18 @@ func (c *UserController) UserLogin() {
 		"LoginMode":      CurrentLoginMode,
 	}
 
+	// 记录登录成功日志
+	loginModeStr := "local"
+	if CurrentLoginMode == 0 {
+		loginModeStr = "info_system"
+	}
+	common.LogUserAction(personalInfo.RYID, personalInfo.XM, "login_success", "user", logrus.Fields{
+		"login_mode": loginModeStr,
+		"ip": clientIP,
+		"user_type": personalInfo.SF,
+		"organization": personalInfo.SSDW,
+	})
+
 	c.Data["json"] = map[string]interface{}{
 		"code": 200,
 		"data": userInfo,
@@ -335,6 +425,7 @@ func (c *UserController) UserLogin() {
 
 // UserLogOut 用户登出
 func (c *UserController) UserLogOut() {
+	clientIP := c.Ctx.Input.IP()
 	authHeader := c.Ctx.Input.Header("Authorization")
 	if authHeader == "" {
 		c.Ctx.Output.SetStatus(401)
@@ -349,8 +440,25 @@ func (c *UserController) UserLogOut() {
 	if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
 		tokenString = authHeader[7:]
 	}
+	
+	// 从token获取用户信息用于日志记录
+	userId, ok := validateShortToken(tokenString)
+	userName := "unknown"
+	if ok && userId != "" {
+		// 尝试从缓存获取用户名
+		if userInfo, exists := GetUserInfoFromCache(userId); exists {
+			userName = userInfo.XM
+		}
+	}
+	
 	deleteShortToken(tokenString)
 	c.DestroySession()
+	
+	// 记录登出日志
+	common.LogUserAction(userId, userName, "logout", "user", logrus.Fields{
+		"ip": clientIP,
+	})
+	
 	c.Data["json"] = map[string]interface{}{
 		"code": 200,
 		"msg":  "登出成功",

+ 48 - 0
Tech/controllers/课程.go

@@ -13,6 +13,7 @@ import (
 	"github.com/beego/beego/v2/client/orm"
 	beego "github.com/beego/beego/v2/server/web"
 	"github.com/google/uuid" // 新增导入
+	"github.com/sirupsen/logrus"
 )
 
 type CourseController struct {
@@ -105,6 +106,25 @@ func (c *CourseController) List() {
 	var query Query
 	requestBody := c.Ctx.Input.RequestBody
 	println("Request Body:", string(requestBody))
+	
+	clientIP := c.Ctx.Input.IP()
+	userAgent := c.Ctx.Request.UserAgent()
+	
+	// 从Header中获取用户信息用于日志记录
+	authHeader := c.Ctx.Input.Header("userName")
+	userName := "unknown"
+	if authHeader != "" {
+		userName = authHeader
+	}
+	
+	common.LogDebug("课程查询请求", logrus.Fields{
+		"module": "course",
+		"action": "list",
+		"user_name": userName,
+		"ip": clientIP,
+		"user_agent": userAgent,
+	})
+	
 	if err := json.Unmarshal(c.Ctx.Input.RequestBody, &query); err != nil {
 		c.Ctx.Output.SetStatus(400)
 		c.Data["json"] = map[string]interface{}{
@@ -112,6 +132,13 @@ func (c *CourseController) List() {
 			"msg":  "请求参数格式错误: " + err.Error(),
 		}
 		c.ServeJSON()
+		
+		// 记录错误日志
+		common.LogUserAction("", userName, "course_query_failed", "course", logrus.Fields{
+			"reason": "invalid_request_format",
+			"ip": clientIP,
+			"error": err.Error(),
+		})
 		return
 	}
 
@@ -188,9 +215,30 @@ func (c *CourseController) List() {
 	if err != nil {
 		c.Data["json"] = map[string]interface{}{"code": 500, "error": "查询失败: " + err.Error()}
 		c.ServeJSON()
+		
+		// 记录错误日志
+		common.LogUserAction("", userName, "course_query_failed", "course", logrus.Fields{
+			"reason": "database_error",
+			"ip": clientIP,
+			"error": err.Error(),
+		})
 		return
 	}
 
+	// 记录查询成功日志
+	common.LogUserAction("", userName, "course_query_success", "course", logrus.Fields{
+		"ip": clientIP,
+		"result_count": count,
+		"query_params": map[string]interface{}{
+			"course_id": query.CourseID,
+			"course_name": query.CourseName,
+			"type_id": query.TypeID,
+			"creator": query.Creator,
+			"line_no": query.LineNo,
+			"jxjh_id": query.JXJHID,
+		},
+	})
+	
 	c.Data["json"] = map[string]interface{}{"code": 200, "count": count, "msg": "", "data": courses}
 	c.ServeJSON()
 }

+ 5 - 1
Tech/go.mod

@@ -2,7 +2,11 @@ module Tech
 
 go 1.24.0
 
-require github.com/beego/beego/v2 v2.3.8
+require (
+	github.com/beego/beego/v2 v2.3.8
+	github.com/sirupsen/logrus v1.9.3
+	gopkg.in/natefinch/lumberjack.v2 v2.2.1
+)
 
 require (
 	github.com/go-sql-driver/mysql v1.9.3

+ 9 - 0
Tech/go.sum

@@ -7,6 +7,7 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 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/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
@@ -53,10 +54,14 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU
 github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
 github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02 h1:v9ezJDHA1XGxViAUSIoO/Id7Fl63u6d0YmsAm+/p2hs=
 github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02/go.mod h1:RF16/A3L0xSa0oSERcnhd8Pu3IXSDZSK2gmGIMsttFE=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
@@ -72,6 +77,7 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
 golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
 golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -83,5 +89,8 @@ google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
 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/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 14 - 0
Tech/log/app.log

@@ -0,0 +1,14 @@
+{"event":"system_startup","file":"D:/Work/教员/SCADATrainSys/Tech/common/logger.go:87","func":"Tech/common.InitLogger","level":"info","msg":"SCADA培训系统启动","pid":26556,"time":"2025-12-21 23:50:29"}
+{"file":"D:/Work/教员/SCADATrainSys/Tech/common/logger.go:337","func":"Tech/common.LogInfo","level":"info","msg":"日志系统初始化成功","time":"2025-12-21 23:50:29"}
+{"file":"D:/Work/教员/SCADATrainSys/Tech/common/logger.go:337","func":"Tech/common.LogInfo","level":"info","msg":"数据库连接监控任务已启动,间隔10秒","time":"2025-12-21 23:50:29"}
+{"action":"cache_init","file":"D:/Work/教员/SCADATrainSys/Tech/common/logger.go:333","func":"Tech/common.LogInfo","level":"info","module":"user","msg":"开始初始化用户信息缓存...","time":"2025-12-21 23:50:29"}
+{"action":"cache_init_complete","file":"D:/Work/教员/SCADATrainSys/Tech/common/logger.go:333","func":"Tech/common.LogInfo","level":"info","module":"user","msg":"用户信息缓存初始化完成","time":"2025-12-21 23:50:36","user_count":3550}
+{"file":"D:/Work/教员/SCADATrainSys/Tech/common/logger.go:337","func":"Tech/common.LogInfo","level":"info","msg":"SCADA培训系统服务启动中...","time":"2025-12-21 23:50:36"}
+{"file":"D:/Work/教员/SCADATrainSys/Tech/common/logger.go:337","func":"Tech/common.LogInfo","level":"info","msg":"心跳发送任务已启动,间隔30秒","time":"2025-12-21 23:50:36"}
+{"action":"course_query_success","event":"user_action","file":"D:/Work/教员/SCADATrainSys/Tech/common/logger.go:515","func":"Tech/common.LogUserAction","ip":"127.0.0.1","level":"info","module":"course","msg":"用户操作","query_params":{"course_id":0,"course_name":"","creator":"","jxjh_id":"1043104698359754752","line_no":0,"type_id":0},"result_count":2,"time":"2025-12-21 23:50:55","user_id":"","user_name":"00001"}
+{"file":"D:/Work/教员/SCADATrainSys/Tech/common/logger.go:337","func":"Tech/common.LogInfo","level":"info","msg":"成功发送心跳到信息化服务器","time":"2025-12-21 23:51:06"}
+{"file":"D:/Work/教员/SCADATrainSys/Tech/common/logger.go:337","func":"Tech/common.LogInfo","level":"info","msg":"成功发送心跳到信息化服务器","time":"2025-12-21 23:51:36"}
+{"action":"course_query_success","event":"user_action","file":"D:/Work/教员/SCADATrainSys/Tech/common/logger.go:515","func":"Tech/common.LogUserAction","ip":"127.0.0.1","level":"info","module":"course","msg":"用户操作","query_params":{"course_id":0,"course_name":"","creator":"","jxjh_id":"1043104698359754752","line_no":0,"type_id":0},"result_count":2,"time":"2025-12-21 23:51:37","user_id":"","user_name":"00001"}
+{"file":"D:/Work/教员/SCADATrainSys/Tech/common/logger.go:337","func":"Tech/common.LogInfo","level":"info","msg":"成功发送心跳到信息化服务器","time":"2025-12-21 23:52:06"}
+{"file":"D:/Work/教员/SCADATrainSys/Tech/common/logger.go:337","func":"Tech/common.LogInfo","level":"info","msg":"成功发送心跳到信息化服务器","time":"2025-12-21 23:52:36"}
+{"file":"D:/Work/教员/SCADATrainSys/Tech/common/logger.go:337","func":"Tech/common.LogInfo","level":"info","msg":"成功发送心跳到信息化服务器","time":"2025-12-21 23:53:06"}

+ 9 - 0
Tech/log/info.log

@@ -0,0 +1,9 @@
+{"level":"info","msg":"日志系统初始化成功","time":"2025-12-21 23:50:29"}
+{"level":"info","msg":"数据库连接监控任务已启动,间隔10秒","time":"2025-12-21 23:50:29"}
+{"action":"cache_init","level":"info","module":"user","msg":"开始初始化用户信息缓存...","time":"2025-12-21 23:50:29"}
+{"action":"cache_init_complete","level":"info","module":"user","msg":"用户信息缓存初始化完成","time":"2025-12-21 23:50:36","user_count":3550}
+{"level":"info","msg":"SCADA培训系统服务启动中...","time":"2025-12-21 23:50:36"}
+{"level":"info","msg":"心跳发送任务已启动,间隔30秒","time":"2025-12-21 23:50:36"}
+{"level":"info","msg":"成功发送心跳到信息化服务器","time":"2025-12-21 23:51:06"}
+{"level":"info","msg":"成功发送心跳到信息化服务器","time":"2025-12-21 23:51:36"}
+{"level":"info","msg":"成功发送心跳到信息化服务器","time":"2025-12-21 23:52:06"}

+ 67 - 11
Tech/main.go

@@ -4,6 +4,9 @@ import (
 	"Tech/common"
 	"Tech/controllers"
 	_ "Tech/routers"
+	"os"
+	"os/signal"
+	"syscall"
 	"time"
 
 	"github.com/beego/beego/v2/client/orm"
@@ -11,6 +14,7 @@ import (
 	"github.com/beego/beego/v2/server/web"
 	beego "github.com/beego/beego/v2/server/web"
 	"github.com/beego/beego/v2/server/web/filter/cors"
+	"github.com/sirupsen/logrus"
 )
 
 func init() {
@@ -34,36 +38,39 @@ func init() {
 func sendHeartbeatPeriodically() {
 	ticker := time.NewTicker(30 * time.Second) // 每30秒发送一次心跳
 	defer ticker.Stop()
+	common.LogInfo("心跳发送任务已启动,间隔30秒")
 
 	for range ticker.C {
 		err := func() error {
 			defer func() {
 				if r := recover(); r != nil {
-					logs.Error("发送心跳时发生异常: %v", r)
+					common.LogError("发送心跳时发生异常", nil, logrus.Fields{
+						"panic": r,
+					})
 				}
 			}()
 
 			err := common.SendHeartbeat()
 			if err != nil {
-				logs.Error("发送心跳失败: %v", err)
+				common.LogError("发送心跳失败", err)
 				return err
 			}
 
-			logs.Info("成功发送心跳到信息化服务器")
+			common.LogInfo("成功发送心跳到信息化服务器")
 			return nil
 		}()
 
 		if err != nil {
-			logs.Error("发送心跳任务出现错误: %v", err)
+			common.LogError("发送心跳任务出现错误", err)
 		}
 	}
 }
 
-// monitorDatabaseConnection 定时检查数据库连接并尝试重连
 // monitorDatabaseConnection 定时检查数据库连接并尝试重连
 func monitorDatabaseConnection(dbAdapter, dbURL string) {
 	ticker := time.NewTicker(10 * time.Second) // 每10秒检查一次
 	defer ticker.Stop()
+	common.LogInfo("数据库连接监控任务已启动,间隔10秒")
 
 	for range ticker.C {
 		// 使用匿名函数包装可能产生异常的操作,并在每次循环都进行异常处理
@@ -71,27 +78,42 @@ func monitorDatabaseConnection(dbAdapter, dbURL string) {
 			defer func() {
 				if r := recover(); r != nil {
 					// 即使NewOrm发生异常,也要尝试重新注册数据库连接
-					logs.Error("监控协程发生异常: %v", r)
+					common.LogError("监控协程发生异常", nil, logrus.Fields{
+						"panic": r,
+					})
 					err := orm.RegisterDataBase("default", dbAdapter, dbURL)
 					if err != nil {
-						logs.Error("数据库重连失败: ", err)
+						common.LogError("数据库重连失败", err)
 					} else {
-						logs.Info("数据库重连成功")
+						common.LogInfo("数据库重连成功")
 					}
 				}
 			}()
 
 			// 尝试执行一个简单的查询来测试连接
+			startTime := time.Now()
 			o := orm.NewOrm()
 			_, err := o.Raw("SELECT 1").Exec()
+			duration := time.Since(startTime)
+			
 			if err != nil {
-				logs.Error("数据库连接异常: ", err)
+				common.LogError("数据库连接异常", err, logrus.Fields{
+					"adapter":  dbAdapter,
+					"db_check": "SELECT 1",
+				})
 				// 尝试重新注册数据库连接
 				err = orm.RegisterDataBase("default", dbAdapter, dbURL)
 				if err != nil {
-					logs.Error("数据库重连失败: ", err)
+					common.LogError("数据库重连失败", err)
 				} else {
-					logs.Info("数据库重连成功")
+					common.LogInfo("数据库重连成功")
+				}
+			} else {
+				// 每100次检查记录一次成功日志(避免日志过多)
+				if time.Now().Unix()%100 == 0 {
+					common.LogDebug("数据库连接检查正常", logrus.Fields{
+						"duration_ms": duration.Milliseconds(),
+					})
 				}
 			}
 		}()
@@ -99,6 +121,27 @@ func monitorDatabaseConnection(dbAdapter, dbURL string) {
 }
 
 func main() {
+	// 初始化日志系统
+	logConfig := &common.LogConfig{
+		LogDir:       "./log",
+		LogLevel:     "info",
+		MaxSize:      100, // 100MB
+		MaxBackups:   30,  // 保留30个旧文件
+		MaxAge:       90,  // 保留90天
+		Compress:     true,
+		EnableCaller: true,
+	}
+	
+	if err := common.InitLogger(logConfig); err != nil {
+		logs.Error("初始化日志系统失败: %v", err)
+		// 仍然继续运行,但使用默认日志
+	} else {
+		common.LogInfo("日志系统初始化成功")
+	}
+
+	// 设置优雅关闭
+	setupGracefulShutdown()
+
 	// 自动建表。参数说明:数据库别名、是否强制重建(false 则只创建新表)、是否打印日志
 	// orm.RunSyncdb("default", false, true)
 
@@ -122,6 +165,19 @@ func main() {
 	// 启动定时发送心跳任务
 	go sendHeartbeatPeriodically()
 
+	common.LogInfo("SCADA培训系统服务启动中...")
 	beego.Run()
+}
 
+// setupGracefulShutdown 设置优雅关闭
+func setupGracefulShutdown() {
+	c := make(chan os.Signal, 1)
+	signal.Notify(c, os.Interrupt, syscall.SIGTERM)
+	
+	go func() {
+		<-c
+		common.LogInfo("接收到关闭信号,正在优雅关闭服务...")
+		common.CloseLogger()
+		os.Exit(0)
+	}()
 }

+ 7 - 0
Tech/routers/router.go

@@ -117,4 +117,11 @@ func init() {
 
 	// SSE路由
 	beego.Router("/sse/register", &controllers.SSEController{}, "post:Register")
+
+	// 日志管理路由
+	beego.Router("/log/list", &controllers.LogController{}, "post:LogList")
+	beego.Router("/log/view", &controllers.LogController{}, "post:LogView")
+	beego.Router("/log/download", &controllers.LogController{}, "get:LogDownload")
+	beego.Router("/log/level/set", &controllers.LogController{}, "post:LogLevelSet")
+	beego.Router("/log/delete", &controllers.LogController{}, "post:LogDelete")
 }