diff --git a/.gitignore b/.gitignore index c1bdc6d..74ec837 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,21 @@ data/ # 需求文档 -assignment.pdf \ No newline at end of file +assignment.pdf + +# 压缩文件 +*.zip +*.rar +*.7z +*.tar +*.gz +*.bz2 +*.xz +*.iso +*.dmg +*.pkg +*.deb +*.rpm + +# 服务器使用的 docker compose 文件 +docker_compose_cloud.yaml \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index dd46af6..de0e74a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,7 +21,7 @@ services: image: redis:latest container_name: imagehub-redis restart: unless-stopped - command: redis-server --requirepass 123456 + command: redis-server --requirepass 123456 --notify-keyspace-events Ex ports: - "6379:6379" networks: diff --git a/server/config/docker.yaml b/server/config/docker.yaml index 1fa690f..45b627d 100644 --- a/server/config/docker.yaml +++ b/server/config/docker.yaml @@ -35,4 +35,6 @@ baidu: ali: apikey: "sk-9ef56cbc5d944b65b1bb612fd7864877" deepseek: - apiKey: "sk-884a6ecd756644cdb4ead4240b0b73cd" \ No newline at end of file + apiKey: "sk-884a6ecd756644cdb4ead4240b0b73cd" +mcp: + serverURL: "http://mcp:7777" \ No newline at end of file diff --git a/server/config/server.yaml b/server/config/server.yaml index f017c4a..99bee60 100644 --- a/server/config/server.yaml +++ b/server/config/server.yaml @@ -35,4 +35,6 @@ baidu: ali: apikey: "sk-9ef56cbc5d944b65b1bb612fd7864877" deepseek: - apiKey: "sk-884a6ecd756644cdb4ead4240b0b73cd" \ No newline at end of file + apiKey: "sk-884a6ecd756644cdb4ead4240b0b73cd" +mcp: + serverURL: "http://localhost:7777" \ No newline at end of file diff --git a/server/model/user_model.go b/server/model/user_model.go index b73a6d0..2944666 100644 --- a/server/model/user_model.go +++ b/server/model/user_model.go @@ -6,15 +6,16 @@ import ( // User 用户信息 type User struct { - ID int `json:"id" gorm:"primaryKey;autoIncrement"` // 主键,用户 ID - Username string `json:"username" gorm:"uniqueIndex;type:varchar(64);not null"` // 用户名 - PasswordHash string `json:"-" gorm:"type:varchar(64);not null"` // 密码哈希 - Email string `json:"email" gorm:"type:varchar(64);uniqueIndex;not null"` // 邮箱 - ParseExif bool `json:"parse_exif" gorm:"default:true"` // 是否解析EXIF - IsAdmin bool `json:"is_admin" gorm:"default:false"` // 是否为管理员 - IsSuspended bool `json:"is_suspended" gorm:"default:false"` // 是否被封禁 - CreatedTime time.Time `json:"created_time" gorm:"autoCreateTime"` // 创建时间 - UpdatedTime time.Time `json:"updated_time" gorm:"autoUpdateTime"` // 更新时间 + ID int `json:"id" gorm:"primaryKey;autoIncrement"` // 主键,用户 ID + Username string `json:"username" gorm:"uniqueIndex;type:varchar(64);not null"` // 用户名 + PasswordHash string `json:"-" gorm:"type:varchar(64);not null"` // 密码哈希 + Email string `json:"email" gorm:"type:varchar(64);uniqueIndex;not null"` // 邮箱 + ParseExif bool `json:"parse_exif" gorm:"default:true"` // 是否解析EXIF + IsAdmin bool `json:"is_admin" gorm:"default:false"` // 是否为管理员 + IsSuspended bool `json:"is_suspended" gorm:"default:false"` // 是否被封禁 + SuspendedUntil *time.Time `json:"suspended_until,omitempty"` // 封禁到期时间 + CreatedTime time.Time `json:"created_time" gorm:"autoCreateTime"` // 创建时间 + UpdatedTime time.Time `json:"updated_time" gorm:"autoUpdateTime"` // 更新时间 } // RegisterRequest 注册请求 diff --git a/server/service/mcp_service.go b/server/service/mcp_service.go index 43af839..7aed62a 100644 --- a/server/service/mcp_service.go +++ b/server/service/mcp_service.go @@ -23,7 +23,16 @@ type MCPService struct { } func getDeepseekAPIKey() string { - return viper.GetString("deepseek.api_key") + return viper.GetString("deepseek.apiKey") +} + +func getMCPServerURL() string { + url := viper.GetString("mcp.serverURL") + if url == "" { + // 默认值,用于向后兼容 + return "http://localhost:7777" + } + return url } func NewMCPService(db *gorm.DB) *MCPService { @@ -36,7 +45,7 @@ func NewMCPService(db *gorm.DB) *MCPService { return &MCPService{ db: db, openaiClient: client, - mcpServerURL: "http://localhost:7777", + mcpServerURL: getMCPServerURL(), } } diff --git a/server/service/user_service.go b/server/service/user_service.go index 94e4040..46f19e7 100644 --- a/server/service/user_service.go +++ b/server/service/user_service.go @@ -84,7 +84,22 @@ func (s *UserService) LoginUser(loginRequest *model.LoginRequest) (*model.User, } // 检查用户是否被封禁(数据库字段) if user.IsSuspended { - return nil, "", fmt.Errorf("当前用户已被封禁") + // 检查封禁是否已到期 + if user.SuspendedUntil != nil && time.Now().After(*user.SuspendedUntil) { + // 封禁已到期,自动解封 + err = s.UnsuspendUser(user.ID) + if err != nil { + return nil, "", fmt.Errorf("自动解封失败: %v", err) + } + // 重新获取用户信息 + user, err = s.GetUserByUsername(loginRequest.Username) + if err != nil { + return nil, "", fmt.Errorf("查询用户失败: %v", err) + } + } else { + // 封禁未到期(SuspendedUntil为nil表示永久封禁,或时间未到) + return nil, "", fmt.Errorf("当前用户已被封禁") + } } // 生成token token, err := utils.CreateToken(user.ID, user.Username, user.Email, user.IsAdmin) @@ -215,13 +230,19 @@ func (s *UserService) IsAdmin(id int) (bool, error) { // SuspendUser 封禁用户 func (s *UserService) SuspendUser(id int, expiration time.Duration) error { + // 计算封禁到期时间 + suspendedUntil := time.Now().Add(expiration) + // 将用户ID加到Redis中 err := s.redisClient.Set(context.Background(), fmt.Sprintf("user%d_isSuspended", id), true, expiration).Err() if err != nil { return fmt.Errorf("添加被封禁用户ID到Redis失败: %v", err) } // 同时更新数据库字段 - err = s.db.Model(&model.User{}).Where("id = ?", id).Update("is_suspended", true).Error + err = s.db.Model(&model.User{}).Where("id = ?", id).Updates(map[string]interface{}{ + "is_suspended": true, + "suspended_until": suspendedUntil, + }).Error if err != nil { return fmt.Errorf("更新用户封禁状态到数据库失败: %v", err) } @@ -236,7 +257,10 @@ func (s *UserService) UnsuspendUser(id int) error { return fmt.Errorf("删除封禁用户ID从Redis失败: %v", err) } // 同时更新数据库字段 - err = s.db.Model(&model.User{}).Where("id = ?", id).Update("is_suspended", false).Error + err = s.db.Model(&model.User{}).Where("id = ?", id).Updates(map[string]interface{}{ + "is_suspended": false, + "suspended_until": nil, + }).Error if err != nil { return fmt.Errorf("更新用户解封状态到数据库失败: %v", err) } @@ -274,7 +298,10 @@ func (s *UserService) StartSuspendExpirationListener(ctx context.Context) { continue } // 更新数据库,将用户解封 - err = s.db.Model(&model.User{}).Where("id = ?", userID).Update("is_suspended", false).Error + err = s.db.Model(&model.User{}).Where("id = ?", userID).Updates(map[string]interface{}{ + "is_suspended": false, + "suspended_until": nil, + }).Error if err != nil { // 记录错误,但不中断监听 fmt.Printf("自动解封用户失败 (用户ID: %d): %v\n", userID, err) diff --git a/server/test/user_service_test.go b/server/test/user_service_test.go index 05c0f49..7584c36 100644 --- a/server/test/user_service_test.go +++ b/server/test/user_service_test.go @@ -352,6 +352,9 @@ func TestSuspendUser(t *testing.T) { userAfterSuspend, err := userService.GetUserByID(user.ID) assert.NoError(t, err) assert.Equal(t, true, userAfterSuspend.IsSuspended) + // 验证封禁到期时间已设置 + assert.NotNil(t, userAfterSuspend.SuspendedUntil) + assert.True(t, userAfterSuspend.SuspendedUntil.After(time.Now())) } // 测试解封用户 @@ -396,6 +399,8 @@ func TestUnsuspendUser(t *testing.T) { userAfterUnsuspend, err := userService.GetUserByID(user.ID) assert.NoError(t, err) assert.Equal(t, false, userAfterUnsuspend.IsSuspended) + // 验证封禁到期时间已清除 + assert.Nil(t, userAfterUnsuspend.SuspendedUntil) } // 测试设置是否解析EXIF @@ -571,6 +576,50 @@ func TestLoginSuspendedUser(t *testing.T) { assert.Contains(t, err.Error(), "当前用户已被封禁") } +// 测试封禁到期后自动解封 +func TestLoginSuspendedUserExpired(t *testing.T) { + err := ClearTables(dbConfig, []string{"users"}) + if err != nil { + panic(err) + } + // 先注册一个用户 + registerRequest := &model.RegisterRequest{ + Username: "封禁到期测试用户", + Email: "expired_suspended@test.com", + Password: "123456", + } + user, err := userService.RegisterUser(registerRequest) + assert.NoError(t, err) + + // 封禁用户(封禁1秒,用于测试) + err = userService.SuspendUser(user.ID, time.Second) + assert.NoError(t, err) + + // 验证用户已被封禁 + userAfterSuspend, err := userService.GetUserByID(user.ID) + assert.NoError(t, err) + assert.Equal(t, true, userAfterSuspend.IsSuspended) + assert.NotNil(t, userAfterSuspend.SuspendedUntil) + + // 等待封禁到期 + time.Sleep(2 * time.Second) + + // 尝试登录,应该自动解封并成功登录 + loginRequest := &model.LoginRequest{ + Username: "封禁到期测试用户", + Password: "123456", + } + _, token, err := userService.LoginUser(loginRequest) + assert.NoError(t, err) + assert.NotEmpty(t, token) + + // 验证用户已被自动解封 + userAfterLogin, err := userService.GetUserByID(user.ID) + assert.NoError(t, err) + assert.Equal(t, false, userAfterLogin.IsSuspended) + assert.Nil(t, userAfterLogin.SuspendedUntil) +} + // 测试根据ID获取不存在的用户 func TestGetUserByIDNotFound(t *testing.T) { err := ClearTables(dbConfig, []string{"users"}) diff --git a/web/src/components/ImageEidtor.vue b/web/src/components/ImageEidtor.vue index 49641c1..83dd530 100644 --- a/web/src/components/ImageEidtor.vue +++ b/web/src/components/ImageEidtor.vue @@ -1,4 +1,4 @@ -