diff --git a/cmd/server/wire.go b/cmd/server/wire.go index 043c50b..2c350fb 100644 --- a/cmd/server/wire.go +++ b/cmd/server/wire.go @@ -44,6 +44,8 @@ func ProvideRouter( adminCommentHandler *handler.AdminCommentHandler, adminGroupHandler *handler.AdminGroupHandler, adminDashboardHandler *handler.AdminDashboardHandler, + adminLogHandler *handler.AdminLogHandler, + logService *service.LogService, activityService service.UserActivityService, casbinService service.CasbinService, ) *router.Router { @@ -68,6 +70,8 @@ func ProvideRouter( adminCommentHandler, adminGroupHandler, adminDashboardHandler, + adminLogHandler, + logService, activityService, casbinService, ) diff --git a/cmd/server/wire_gen.go b/cmd/server/wire_gen.go index 294428a..95be523 100644 --- a/cmd/server/wire_gen.go +++ b/cmd/server/wire_gen.go @@ -42,15 +42,24 @@ func InitializeApp() (*App, error) { userService := wire.ProvideUserService(userRepository, systemMessageService, emailService, cache) userActivityRepository := wire.ProvideUserActivityRepository(db, cache) userActivityService := wire.ProvideUserActivityService(userActivityRepository) - userHandler := wire.ProvideUserHandler(userService, userActivityService) gorseClient := wire.ProvideGorseClient(config) openaiClient := wire.ProvideOpenAIClient(config) postAIService := wire.ProvidePostAIService(openaiClient) transactionManager := wire.ProvideTransactionManager(db) postService := wire.ProvidePostService(postRepository, systemMessageService, gorseClient, postAIService, cache, transactionManager) - postHandler := wire.ProvidePostHandler(postService, userService) commentRepository := repository.NewCommentRepository(db) commentService := wire.ProvideCommentService(commentRepository, postRepository, systemMessageService, gorseClient, postAIService, cache) + operationLogRepository := repository.NewOperationLogRepository(db) + loginLogRepository := repository.NewLoginLogRepository(db) + dataChangeLogRepository := repository.NewDataChangeLogRepository(db) + logger := wire.ProvideLogger() + asyncLogManager := wire.ProvideAsyncLogManager(db, operationLogRepository, loginLogRepository, dataChangeLogRepository, logger, config) + operationLogService := wire.ProvideOperationLogService(asyncLogManager, operationLogRepository) + loginLogService := wire.ProvideLoginLogService(asyncLogManager, loginLogRepository) + dataChangeLogService := wire.ProvideDataChangeLogService(asyncLogManager, dataChangeLogRepository) + logService := wire.ProvideLogService(operationLogService, loginLogService, dataChangeLogService) + userHandler := wire.ProvideUserHandler(userService, userActivityService, postService, commentService, logService) + postHandler := wire.ProvidePostHandler(postService, userService, logService) commentHandler := handler.NewCommentHandler(commentService) chatService := wire.ProvideChatService(db, messageRepository, userRepository, hub, cache) messageService := wire.ProvideMessageService(db, messageRepository, cache) @@ -79,7 +88,6 @@ func InitializeApp() (*App, error) { voteHandler := handler.NewVoteHandler(voteService, postService) scheduleRepository := repository.NewScheduleRepository(db) scheduleService := wire.ProvideScheduleService(scheduleRepository) - logger := wire.ProvideLogger() server := wire.ProvideGRPCServer(config, logger) taskManager := wire.ProvideTaskManager(server) scheduleSyncService := wire.ProvideScheduleSyncService(taskManager, scheduleRepository, logger) @@ -99,7 +107,8 @@ func InitializeApp() (*App, error) { adminGroupHandler := handler.NewAdminGroupHandler(adminGroupService) adminDashboardService := wire.ProvideAdminDashboardService(db, userRepository, postRepository, commentRepository, groupRepository, userActivityRepository) adminDashboardHandler := handler.NewAdminDashboardHandler(adminDashboardService) - router := ProvideRouter(userHandler, postHandler, commentHandler, messageHandler, notificationHandler, uploadHandler, jwtService, pushHandler, systemMessageHandler, groupHandler, stickerHandler, gorseHandler, voteHandler, scheduleHandler, roleHandler, adminUserHandler, adminPostHandler, adminCommentHandler, adminGroupHandler, adminDashboardHandler, userActivityService, casbinService) + adminLogHandler := handler.NewAdminLogHandler(operationLogService, loginLogService, dataChangeLogService) + router := ProvideRouter(userHandler, postHandler, commentHandler, messageHandler, notificationHandler, uploadHandler, jwtService, pushHandler, systemMessageHandler, groupHandler, stickerHandler, gorseHandler, voteHandler, scheduleHandler, roleHandler, adminUserHandler, adminPostHandler, adminCommentHandler, adminGroupHandler, adminDashboardHandler, adminLogHandler, logService, userActivityService, casbinService) app := NewApp(config, db, router, pushService, server) return app, nil } @@ -128,6 +137,8 @@ func ProvideRouter( adminCommentHandler *handler.AdminCommentHandler, adminGroupHandler *handler.AdminGroupHandler, adminDashboardHandler *handler.AdminDashboardHandler, + adminLogHandler *handler.AdminLogHandler, + logService *service.LogService, activityService service.UserActivityService, casbinService service.CasbinService, ) *router.Router { @@ -152,6 +163,8 @@ func ProvideRouter( adminCommentHandler, adminGroupHandler, adminDashboardHandler, + adminLogHandler, + logService, activityService, casbinService, ) diff --git a/internal/cache/conversation_cache.go b/internal/cache/conversation_cache.go index 9114c7e..b46fd81 100644 --- a/internal/cache/conversation_cache.go +++ b/internal/cache/conversation_cache.go @@ -45,8 +45,6 @@ type MessageCacheData struct { Category string `json:"category"` SystemType string `json:"system_type,omitempty"` ExtraData json.RawMessage `json:"extra_data,omitempty"` - MentionUsers string `json:"mention_users"` - MentionAll bool `json:"mention_all"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } @@ -126,8 +124,6 @@ func (m *MessageCacheData) ToModel() *model.Message { Category: model.MessageCategory(m.Category), SystemType: model.SystemMessageType(m.SystemType), ExtraData: parseExtraData(m.ExtraData), - MentionUsers: m.MentionUsers, - MentionAll: m.MentionAll, CreatedAt: m.CreatedAt, UpdatedAt: m.UpdatedAt, } @@ -146,8 +142,6 @@ func MessageCacheDataFromModel(msg *model.Message) *MessageCacheData { Category: string(msg.Category), SystemType: string(msg.SystemType), ExtraData: serializeExtraData(msg.ExtraData), - MentionUsers: msg.MentionUsers, - MentionAll: msg.MentionAll, CreatedAt: msg.CreatedAt, UpdatedAt: msg.UpdatedAt, } diff --git a/internal/config/config.go b/internal/config/config.go index d3114a8..34b6227 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -27,6 +27,7 @@ type Config struct { ConversationCache ConversationCacheConfig `mapstructure:"conversation_cache"` GRPC GRPCConfig `mapstructure:"grpc"` Casbin CasbinConfig `mapstructure:"casbin"` + Encryption EncryptionConfig `mapstructure:"encryption"` } // Load 加载配置文件 diff --git a/internal/config/encryption.go b/internal/config/encryption.go new file mode 100644 index 0000000..b1d2255 --- /dev/null +++ b/internal/config/encryption.go @@ -0,0 +1,14 @@ +package config + +// EncryptionConfig 消息加密配置 +type EncryptionConfig struct { + // Enabled 是否启用消息加密 + Enabled bool `mapstructure:"enabled"` + + // Key 加密密钥(32字节,AES-256) + // 生产环境应通过环境变量 APP_ENCRYPTION_KEY 设置 + Key string `mapstructure:"key"` + + // KeyVersion 密钥版本,用于密钥轮换 + KeyVersion int `mapstructure:"key_version"` +} diff --git a/internal/model/message.go b/internal/model/message.go index fac1eea..cfec4a0 100644 --- a/internal/model/message.go +++ b/internal/model/message.go @@ -9,6 +9,7 @@ import ( "gorm.io/gorm" + "carrot_bbs/internal/pkg/crypto" "carrot_bbs/internal/pkg/utils" ) @@ -138,30 +139,35 @@ func (s *MessageSegments) Scan(value interface{}) error { // Message 消息实体 // 使用雪花算法ID(string类型)和seq机制实现消息排序和增量同步 +// 支持消息内容加密存储 type Message struct { - ID string `gorm:"primaryKey;size:20" json:"id"` // 雪花算法ID(string类型) - ConversationID string `gorm:"not null;size:20;index:idx_msg_conversation_seq,priority:1" json:"conversation_id"` // 会话ID(string类型) - SenderID string `gorm:"column:sender_id;type:varchar(50);index;not null" json:"sender_id"` // 发送者ID (UUID格式) - Seq int64 `gorm:"not null;index:idx_msg_conversation_seq,priority:2" json:"seq"` // 会话内序号,用于排序和增量同步 - Segments MessageSegments `gorm:"type:json" json:"segments"` // 消息链(结构体数组) - ReplyToID *string `json:"reply_to_id,omitempty"` // 回复的消息ID(string类型) - Status MessageStatus `gorm:"type:varchar(20);default:'normal'" json:"status"` // 消息状态 + ID string `gorm:"primaryKey;size:20" json:"id"` // 雪花算法ID(string类型) + ConversationID string `gorm:"not null;size:20;index:idx_msg_conversation_seq,priority:1" json:"conversation_id"` // 会话ID(string类型) + SenderID string `gorm:"column:sender_id;type:varchar(50);index;not null" json:"sender_id"` // 发送者ID (UUID格式) + Seq int64 `gorm:"not null;index:idx_msg_conversation_seq,priority:2" json:"seq"` // 会话内序号,用于排序和增量同步 + + // 消息内容字段 + Segments MessageSegments `gorm:"-" json:"segments"` // 消息链(内存中使用,不映射到数据库) + SegmentsEncrypted string `gorm:"column:segments;type:text" json:"-"` // 加密后的消息链(存储在数据库) + SegmentsKeyVersion int `gorm:"column:segments_key_version;default:0" json:"segments_key_version"` // 密钥版本,用于密钥轮换 + + ReplyToID *string `json:"reply_to_id,omitempty"` // 回复的消息ID(string类型) + Status MessageStatus `gorm:"type:varchar(20);default:'normal'" json:"status"` // 消息状态 // 新增字段:消息分类和系统消息类型 Category MessageCategory `gorm:"type:varchar(20);default:'chat'" json:"category"` // 消息分类 SystemType SystemMessageType `gorm:"type:varchar(30)" json:"system_type,omitempty"` // 系统消息类型 ExtraData *ExtraData `gorm:"type:json" json:"extra_data,omitempty"` // 额外数据(JSON格式) - // @相关字段 - MentionUsers string `gorm:"type:text" json:"mention_users"` // @的用户ID列表,JSON数组 - MentionAll bool `gorm:"default:false" json:"mention_all"` // 是否@所有人 - // 软删除 DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 时间戳 CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` + + // 内部标记,表示是否已解密 + decrypted bool `gorm:"-"` } // SenderIDStr 返回发送者ID字符串(保持兼容性) @@ -169,7 +175,7 @@ func (m *Message) SenderIDStr() string { return m.SenderID } -// BeforeCreate 创建前生成雪花算法ID +// BeforeCreate 创建前生成雪花算法ID并加密消息内容 func (m *Message) BeforeCreate(tx *gorm.DB) error { if m.ID == "" { id, err := utils.GetSnowflake().GenerateID() @@ -178,6 +184,107 @@ func (m *Message) BeforeCreate(tx *gorm.DB) error { } m.ID = strconv.FormatInt(id, 10) } + + // 加密消息内容 + return m.encryptSegments() +} + +// BeforeUpdate 更新前加密消息内容 +func (m *Message) BeforeUpdate(tx *gorm.DB) error { + // 只有当 Segments 被修改时才重新加密 + if tx.Statement.Changed("Segments") || len(m.Segments) > 0 { + return m.encryptSegments() + } + return nil +} + +// AfterFind 查询后自动解密消息内容 +func (m *Message) AfterFind(tx *gorm.DB) error { + return m.decryptSegments() +} + +// AfterCreate 创建后解密消息内容(保持内存中的数据可读) +func (m *Message) AfterCreate(tx *gorm.DB) error { + m.decrypted = true + return nil +} + +// encryptSegments 加密消息段 +func (m *Message) encryptSegments() error { + // 如果没有消息内容或加密器未初始化,跳过加密 + if len(m.Segments) == 0 { + return nil + } + + encryptor := crypto.GetMessageEncryptor() + if encryptor == nil { + // 加密器未初始化时,直接存储JSON(兼容模式) + data, err := json.Marshal(m.Segments) + if err != nil { + return err + } + m.SegmentsEncrypted = string(data) + m.SegmentsKeyVersion = 0 + return nil + } + + // 序列化消息段 + plaintext, err := json.Marshal(m.Segments) + if err != nil { + return err + } + + // 加密 + ciphertext, err := encryptor.Encrypt(plaintext) + if err != nil { + return err + } + + m.SegmentsEncrypted = ciphertext + m.SegmentsKeyVersion = encryptor.GetKeyVersion() + return nil +} + +// decryptSegments 解密消息段 +func (m *Message) decryptSegments() error { + // 如果已经解密过,跳过 + if m.decrypted { + return nil + } + + // 如果没有加密内容,跳过 + if m.SegmentsEncrypted == "" { + m.decrypted = true + return nil + } + + encryptor := crypto.GetMessageEncryptor() + if encryptor == nil { + // 加密器未初始化,尝试直接解析JSON(兼容旧数据) + if err := json.Unmarshal([]byte(m.SegmentsEncrypted), &m.Segments); err != nil { + return err + } + m.decrypted = true + return nil + } + + // 尝试解密 + plaintext, err := encryptor.Decrypt(m.SegmentsEncrypted) + if err != nil { + // 解密失败,可能是未加密的旧数据,尝试直接解析 + if parseErr := json.Unmarshal([]byte(m.SegmentsEncrypted), &m.Segments); parseErr != nil { + return err // 返回解密错误 + } + m.decrypted = true + return nil + } + + // 解析解密后的数据 + if err := json.Unmarshal(plaintext, &m.Segments); err != nil { + return err + } + + m.decrypted = true return nil } diff --git a/internal/pkg/crypto/message_encryptor.go b/internal/pkg/crypto/message_encryptor.go new file mode 100644 index 0000000..285de3a --- /dev/null +++ b/internal/pkg/crypto/message_encryptor.go @@ -0,0 +1,173 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "errors" + "io" + "sync" +) + +var ( + ErrInvalidKey = errors.New("invalid encryption key: must be 32 bytes") + ErrInvalidCiphertext = errors.New("invalid ciphertext: too short") + ErrDecryptFailed = errors.New("decryption failed") +) + +// MessageEncryptor 消息加密器 +// 使用 AES-256-GCM 算法进行加密 +type MessageEncryptor struct { + key []byte + gcm cipher.AEAD + keyVersion int + mu sync.RWMutex +} + +// globalEncryptor 全局加密器实例 +var globalEncryptor *MessageEncryptor +var encryptorOnce sync.Once + +// InitMessageEncryptor 初始化全局消息加密器 +// key 必须是32字节(256位)的密钥 +func InitMessageEncryptor(key string, keyVersion int) error { + keyBytes := []byte(key) + if len(keyBytes) != 32 { + return ErrInvalidKey + } + + block, err := aes.NewCipher(keyBytes) + if err != nil { + return err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return err + } + + encryptorOnce.Do(func() { + globalEncryptor = &MessageEncryptor{ + key: keyBytes, + gcm: gcm, + keyVersion: keyVersion, + } + }) + + return nil +} + +// GetMessageEncryptor 获取全局加密器实例 +func GetMessageEncryptor() *MessageEncryptor { + return globalEncryptor +} + +// Encrypt 加密数据 +// 返回 base64 编码的密文(格式:nonce + ciphertext + tag) +func (e *MessageEncryptor) Encrypt(plaintext []byte) (string, error) { + if e == nil { + return "", errors.New("encryptor not initialized") + } + + if len(plaintext) == 0 { + return "", nil + } + + e.mu.RLock() + defer e.mu.RUnlock() + + // 生成随机nonce(12字节) + nonce := make([]byte, e.gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return "", err + } + + // 加密数据,nonce附加在密文前面 + ciphertext := e.gcm.Seal(nonce, nonce, plaintext, nil) + + // 返回base64编码的密文 + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +// Decrypt 解密数据 +// 输入 base64 编码的密文 +func (e *MessageEncryptor) Decrypt(ciphertextBase64 string) ([]byte, error) { + if e == nil { + return nil, errors.New("encryptor not initialized") + } + + if ciphertextBase64 == "" { + return nil, nil + } + + e.mu.RLock() + defer e.mu.RUnlock() + + // base64解码 + ciphertext, err := base64.StdEncoding.DecodeString(ciphertextBase64) + if err != nil { + return nil, err + } + + // 检查密文长度 + nonceSize := e.gcm.NonceSize() + if len(ciphertext) < nonceSize { + return nil, ErrInvalidCiphertext + } + + // 提取nonce和实际密文 + nonce := ciphertext[:nonceSize] + actualCiphertext := ciphertext[nonceSize:] + + // 解密 + plaintext, err := e.gcm.Open(nil, nonce, actualCiphertext, nil) + if err != nil { + return nil, ErrDecryptFailed + } + + return plaintext, nil +} + +// GetKeyVersion 获取当前密钥版本 +func (e *MessageEncryptor) GetKeyVersion() int { + if e == nil { + return 0 + } + e.mu.RLock() + defer e.mu.RUnlock() + return e.keyVersion +} + +// RotateKey 密钥轮换(用于密钥升级) +// 新密钥必须也是32字节 +func (e *MessageEncryptor) RotateKey(newKey string, newVersion int) error { + keyBytes := []byte(newKey) + if len(keyBytes) != 32 { + return ErrInvalidKey + } + + block, err := aes.NewCipher(keyBytes) + if err != nil { + return err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return err + } + + e.mu.Lock() + defer e.mu.Unlock() + + e.key = keyBytes + e.gcm = gcm + e.keyVersion = newVersion + + return nil +} + +// IsInitialized 检查加密器是否已初始化 +func IsInitialized() bool { + return globalEncryptor != nil +} diff --git a/internal/pkg/crypto/message_encryptor_test.go b/internal/pkg/crypto/message_encryptor_test.go new file mode 100644 index 0000000..b9773bf --- /dev/null +++ b/internal/pkg/crypto/message_encryptor_test.go @@ -0,0 +1,168 @@ +package crypto + +import ( + "encoding/json" + "sync" + "testing" +) + +func TestMessageEncryptor_EncryptDecrypt(t *testing.T) { + // 初始化加密器(32字节密钥) + key := "12345678901234567890123456789012" // 32 bytes + err := InitMessageEncryptor(key, 1) + if err != nil { + t.Fatalf("InitMessageEncryptor failed: %v", err) + } + + encryptor := GetMessageEncryptor() + if encryptor == nil { + t.Fatal("GetMessageEncryptor returned nil") + } + + tests := []struct { + name string + plaintext []byte + }{ + { + name: "simple text", + plaintext: []byte("Hello, World!"), + }, + { + name: "empty", + plaintext: []byte(""), + }, + { + name: "json data", + plaintext: []byte(`{"type":"text","data":{"content":"这是一条测试消息"}}`), + }, + { + name: "long message", + plaintext: []byte("这是一条很长的消息,用于测试加密长文本的性能和正确性。" + + "加密后的数据应该能够正确解密,并且每次加密的结果应该不同(因为有随机nonce)。"), + }, + { + name: "complex message segments", + plaintext: mustMarshal([]map[string]interface{}{ + {"type": "text", "data": map[string]interface{}{"content": "你好"}}, + {"type": "image", "data": map[string]interface{}{"url": "https://example.com/image.png"}}, + }), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 空字符串特殊处理 + if len(tt.plaintext) == 0 { + ciphertext, err := encryptor.Encrypt(tt.plaintext) + if err != nil { + t.Errorf("Encrypt() error = %v", err) + } + if ciphertext != "" { + t.Errorf("Encrypt() for empty input should return empty string, got %s", ciphertext) + } + return + } + + // 加密 + ciphertext, err := encryptor.Encrypt(tt.plaintext) + if err != nil { + t.Errorf("Encrypt() error = %v", err) + return + } + + // 解密 + decrypted, err := encryptor.Decrypt(ciphertext) + if err != nil { + t.Errorf("Decrypt() error = %v", err) + return + } + + // 比较 + if string(decrypted) != string(tt.plaintext) { + t.Errorf("Decrypt() = %s, want %s", decrypted, tt.plaintext) + } + }) + } +} + +func TestMessageEncryptor_DifferentNonce(t *testing.T) { + key := "12345678901234567890123456789012" + _ = InitMessageEncryptor(key, 1) + encryptor := GetMessageEncryptor() + + plaintext := []byte("same message") + + // 加密两次相同的内容,密文应该不同(因为随机nonce) + ciphertext1, _ := encryptor.Encrypt(plaintext) + ciphertext2, _ := encryptor.Encrypt(plaintext) + + if ciphertext1 == ciphertext2 { + t.Error("Same plaintext should produce different ciphertext due to random nonce") + } + + // 但两个都应该能正确解密 + decrypted1, _ := encryptor.Decrypt(ciphertext1) + decrypted2, _ := encryptor.Decrypt(ciphertext2) + + if string(decrypted1) != string(plaintext) || string(decrypted2) != string(plaintext) { + t.Error("Both ciphertexts should decrypt to the same plaintext") + } +} + +func TestMessageEncryptor_InvalidKey(t *testing.T) { + tests := []struct { + name string + key string + wantErr bool + }{ + {"valid 32 bytes", "12345678901234567890123456789012", false}, + {"invalid 16 bytes", "1234567890123456", true}, + {"invalid 24 bytes", "123456789012345678901234", true}, + {"invalid 31 bytes", "1234567890123456789012345678901", true}, + {"invalid 33 bytes", "123456789012345678901234567890123", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 重置全局加密器 + globalEncryptor = nil + encryptorOnce = sync.Once{} + + err := InitMessageEncryptor(tt.key, 1) + if (err != nil) != tt.wantErr { + t.Errorf("InitMessageEncryptor() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestMessageEncryptor_KeyVersion(t *testing.T) { + // 重置 + globalEncryptor = nil + encryptorOnce = sync.Once{} + + err := InitMessageEncryptor("12345678901234567890123456789012", 1) + if err != nil { + t.Fatal(err) + } + + encryptor := GetMessageEncryptor() + if encryptor.GetKeyVersion() != 1 { + t.Errorf("GetKeyVersion() = %d, want 1", encryptor.GetKeyVersion()) + } + + // 测试密钥轮换 + err = encryptor.RotateKey("abcdefghijklmnopqrstuvwxyz123456", 2) + if err != nil { + t.Fatal(err) + } + + if encryptor.GetKeyVersion() != 2 { + t.Errorf("GetKeyVersion() after rotation = %d, want 2", encryptor.GetKeyVersion()) + } +} + +func mustMarshal(v interface{}) []byte { + data, _ := json.Marshal(v) + return data +} diff --git a/internal/router/router.go b/internal/router/router.go index 269fd9e..a3f59a1 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -110,7 +110,6 @@ func (r *Router) setupRoutes() { c.JSON(200, gin.H{"status": "ok"}) }) - // API v1 v1 := r.engine.Group("/api/v1") { @@ -132,9 +131,9 @@ func (r *Router) setupRoutes() { // === 访问日志中间件 === if r.adminLogHandler != nil && r.logService != nil { accessLogConfig := &middleware.AccessLogConfig{ - Enable: true, - SkipPaths: []string{"/health", "/api/v1/health"}, - ServerIP: "0.0.0.0", + Enable: true, + SkipPaths: []string{"/health", "/api/v1/health"}, + ServerIP: "0.0.0.0", ServerPort: 8080, } r.engine.Use(middleware.AccessLogMiddleware( @@ -142,11 +141,6 @@ func (r *Router) setupRoutes() { accessLogConfig, )) } - r.engine.Use(middleware.AccessLogMiddleware( - r.logService, - accessLogConfig, - )) - } // Casbin 权限中间件(暂不全局启用,可根据需要在特定路由组上使用) // casbinMiddleware := middleware.CasbinAuth(r.casbinService) @@ -478,6 +472,7 @@ func (r *Router) setupRoutes() { } } } +} // Engine 获取引擎 func (r *Router) Engine() *gin.Engine {