package app import ( "fmt" "io" "log" "os" "path/filepath" "strings" "verification-platform-backend/internal/database" "verification-platform-backend/internal/model" "verification-platform-backend/internal/service" "verification-platform-backend/pkg/response" "github.com/gin-gonic/gin" "github.com/google/uuid" ) func SetupUserRoutes(r *gin.RouterGroup) { r.POST("/logout", handleAppLogout) r.GET("/user-info", handleAppGetUserInfo) r.POST("/avatar", handleAppUploadAvatar) } func handleAppLogout(c *gin.Context) { appKey := c.Param("appKey") var app model.Application if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil { response.Error(c, 404, "应用不存在") return } if service.GetApplicationDisabledStatus(app.ID) { response.Error(c, 403, "该应用已被禁用") return } userID, exists := c.Get("user_id") if !exists { response.Error(c, 401, "未授权") return } var req struct { DeviceID string `json:"device_id" binding:"required"` InstanceID string `json:"instance_id" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, 400, "device_id 和 instance_id 为必填参数") return } // 获取用户在该应用下的所有设备 var devices []model.UserDevice if err := database.DB.Where("user_id = ? AND application_id = ?", userID, app.ID).Find(&devices).Error; err != nil { response.Error(c, 500, "获取设备失败") return } if len(devices) == 0 { response.Success(c, gin.H{"message": "登出成功"}) return } // 查找指定设备 var targetDeviceID uint for _, d := range devices { if d.DeviceID == req.DeviceID { targetDeviceID = d.ID break } } if targetDeviceID == 0 { response.Error(c, 404, "设备不存在") return } // 清除指定实例 result := database.DB.Where("device_id = ? AND instance_id = ?", targetDeviceID, req.InstanceID).Delete(&model.DeviceSession{}) deletedCount := result.RowsAffected log.Printf("[INFO] Logout: user %d, device %s, instance %s, deleted %d", userID, req.DeviceID, req.InstanceID, deletedCount) service.LogVerification(c, &app.ID, uintPtr(userID.(uint)), "logout", fmt.Sprintf("用户登出,清除 %d 个实例", deletedCount), req.DeviceID, nil) response.Success(c, gin.H{ "message": "登出成功", "deleted_count": deletedCount, }) } func handleAppGetUserInfo(c *gin.Context) { appKey := c.Param("appKey") var app model.Application if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil { response.Error(c, 404, "应用不存在") return } if service.GetApplicationDisabledStatus(app.ID) { response.Error(c, 403, "该应用已被禁用") return } userID, exists := c.Get("user_id") if !exists { response.Error(c, 401, "未授权") return } var user model.AppUser if err := database.DB.First(&user, userID).Error; err != nil { response.Error(c, 404, "用户不存在") return } result := gin.H{ "user_id": user.ID, "username": user.Username, "email": user.Email, "avatar": user.Avatar, "status": user.Status, } if user.Balance == -1 { result["is_permanent"] = true } else if app.BillingType == "subscription" { if user.ExpiryAt != nil { result["expiry_at"] = user.ExpiryAt.Format("2006-01-02 15:04:05") result["expiry_timestamp"] = user.ExpiryAt.Unix() } } else if app.BillingType != "free" { result["balance"] = user.Balance } response.Success(c, result) } func handleAppUploadAvatar(c *gin.Context) { appKey := c.Param("appKey") var app model.Application if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil { response.Error(c, 404, "应用不存在") return } if service.GetApplicationDisabledStatus(app.ID) { response.Error(c, 403, "该应用已被禁用") return } userID, exists := c.Get("user_id") if !exists { response.Error(c, 401, "未授权") return } file, header, err := c.Request.FormFile("avatar") if err != nil { response.Error(c, 400, "请上传头像文件") return } defer file.Close() if !isValidImageFile(header.Filename) { response.Error(c, 400, "只支持JPG、PNG、GIF格式的图片") return } const maxSize = 2 << 20 if header.Size > maxSize { response.Error(c, 400, "头像文件大小不能超过2MB") return } avatarDir := filepath.Join("uploads", "avatars") if err := os.MkdirAll(avatarDir, 0755); err != nil { response.Error(c, 500, "创建目录失败") return } ext := filepath.Ext(header.Filename) filename := fmt.Sprintf("avatar_%d_%s%s", userID.(uint), uuid.New().String()[:8], ext) dst := filepath.Join(avatarDir, filename) dstFile, err := os.Create(dst) if err != nil { response.Error(c, 500, "创建文件失败") return } defer dstFile.Close() if _, err := io.Copy(dstFile, file); err != nil { response.Error(c, 500, "保存文件失败") return } avatarURL := fmt.Sprintf("/uploads/avatars/%s", filename) var user model.AppUser if err := database.DB.First(&user, userID).Error; err != nil { response.Error(c, 404, "用户不存在") return } oldAvatar := user.Avatar user.Avatar = avatarURL if err := database.DB.Save(&user).Error; err != nil { response.Error(c, 500, "保存头像失败") return } if oldAvatar != "" && strings.HasPrefix(oldAvatar, "/uploads/avatars/") { oldPath := filepath.Join(".", oldAvatar) os.Remove(oldPath) } response.Success(c, gin.H{ "avatar_url": avatarURL, }) } func isValidImageFile(filename string) bool { ext := strings.ToLower(filepath.Ext(filename)) validExts := map[string]bool{ ".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true, } return validExts[ext] } func uintPtr(v uint) *uint { return &v }