Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,21 @@
data/

# 需求文档
assignment.pdf
assignment.pdf

# 压缩文件
*.zip
*.rar
*.7z
*.tar
*.gz
*.bz2
*.xz
*.iso
*.dmg
*.pkg
*.deb
*.rpm

# 服务器使用的 docker compose 文件
docker_compose_cloud.yaml
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion server/config/docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@ baidu:
ali:
apikey: "sk-9ef56cbc5d944b65b1bb612fd7864877"
deepseek:
apiKey: "sk-884a6ecd756644cdb4ead4240b0b73cd"
apiKey: "sk-884a6ecd756644cdb4ead4240b0b73cd"
mcp:
serverURL: "http://mcp:7777"
4 changes: 3 additions & 1 deletion server/config/server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@ baidu:
ali:
apikey: "sk-9ef56cbc5d944b65b1bb612fd7864877"
deepseek:
apiKey: "sk-884a6ecd756644cdb4ead4240b0b73cd"
apiKey: "sk-884a6ecd756644cdb4ead4240b0b73cd"
mcp:
serverURL: "http://localhost:7777"
19 changes: 10 additions & 9 deletions server/model/user_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 注册请求
Expand Down
13 changes: 11 additions & 2 deletions server/service/mcp_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -36,7 +45,7 @@ func NewMCPService(db *gorm.DB) *MCPService {
return &MCPService{
db: db,
openaiClient: client,
mcpServerURL: "http://localhost:7777",
mcpServerURL: getMCPServerURL(),
}
}

Expand Down
35 changes: 31 additions & 4 deletions server/service/user_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down
49 changes: 49 additions & 0 deletions server/test/user_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
}

// 测试解封用户
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"})
Expand Down
Loading
Loading