/** * package service * 作用:附件管理服务 */ package service import ( "errors" "fmt" "io" "mime" "os" "path/filepath" "strings" "time" "xk-websocket-v2/internal/model" "gorm.io/gorm" ) // AttachmentService 附件服务结构体 type AttachmentService struct { DB *gorm.DB } // AttachmentSvc 全局单例 var AttachmentSvc *AttachmentService /** * InitAttachmentService * 功能:初始化附件服务 */ func InitAttachmentService(db *gorm.DB) { AttachmentSvc = &AttachmentService{DB: db} // 创建上传目录 os.MkdirAll("./uploads/images", os.ModePerm) os.MkdirAll("./uploads/videos", os.ModePerm) } /** * UploadFile * 功能:上传文件 * @param userID 上传者ID * @param fileName 文件名 * @param fileType 文件类型(image/video) * @param fileSize 文件大小 * @param fileData 文件数据 * @returns 附件信息和错误 */ func (s *AttachmentService) UploadFile(userID, fileName, fileType string, fileSize int64, fileData io.Reader) (*model.Attachment, error) { // 验证文件类型 if fileType != "image" && fileType != "video" { return nil, errors.New("文件类型必须是image或video") } // 验证文件大小 if fileType == "image" && fileSize > model.MaxImageSize { return nil, fmt.Errorf("图片大小不能超过%dMB", model.MaxImageSize/(1024*1024)) } if fileType == "video" && fileSize > model.MaxVideoSize { return nil, fmt.Errorf("视频大小不能超过%dMB", model.MaxVideoSize/(1024*1024)) } // 验证文件扩展名 ext := strings.ToLower(filepath.Ext(fileName)) allowedExts := s.getAllowedExtensions(fileType) if !contains(allowedExts, ext) { return nil, fmt.Errorf("不支持的文件类型,允许的类型: %v", allowedExts) } // 生成唯一文件名 timestamp := time.Now().UnixNano() randomStr := fmt.Sprintf("%d", timestamp%1000000) newFileName := fmt.Sprintf("%d_%s%s", timestamp, randomStr, ext) // 确定保存路径 var saveDir string if fileType == "image" { saveDir = "./uploads/images" } else { saveDir = "./uploads/videos" } filePath := filepath.Join(saveDir, newFileName) // 保存文件 file, err := os.Create(filePath) if err != nil { return nil, fmt.Errorf("创建文件失败: %v", err) } defer file.Close() _, err = io.Copy(file, fileData) if err != nil { os.Remove(filePath) // 删除失败的文件 return nil, fmt.Errorf("保存文件失败: %v", err) } // 生成访问URL fileURL := fmt.Sprintf("/uploads/%s/%s", fileType+"s", newFileName) // 获取MIME类型 mimeType := mime.TypeByExtension(ext) if mimeType == "" { mimeType = "application/octet-stream" } // 创建附件记录 attachment := model.Attachment{ UploaderID: userID, FileName: fileName, FileType: fileType, FileSize: fileSize, FilePath: filePath, FileURL: fileURL, MimeType: mimeType, } if err := s.DB.Create(&attachment).Error; err != nil { os.Remove(filePath) // 删除文件 return nil, fmt.Errorf("保存附件记录失败: %v", err) } return &attachment, nil } /** * GetAttachment * 功能:获取附件信息 */ func (s *AttachmentService) GetAttachment(attachmentID uint) (*model.Attachment, error) { var attachment model.Attachment result := s.DB.First(&attachment, attachmentID) if result.Error != nil { return nil, result.Error } return &attachment, nil } /** * DeleteAttachment * 功能:删除附件(验证上传者权限) */ func (s *AttachmentService) DeleteAttachment(attachmentID uint, userID string) error { var attachment model.Attachment if err := s.DB.First(&attachment, attachmentID).Error; err != nil { return err } // 验证权限 if attachment.UploaderID != userID { return errors.New("无权删除此附件") } // 删除文件 if _, err := os.Stat(attachment.FilePath); err == nil { os.Remove(attachment.FilePath) } // 删除记录 return s.DB.Delete(&attachment).Error } /** * GetUserAttachments * 功能:获取用户附件列表 */ func (s *AttachmentService) GetUserAttachments(userID, fileType string, page, pageSize int) ([]model.Attachment, int64, error) { var attachments []model.Attachment var total int64 query := s.DB.Where("uploader_id = ?", userID) if fileType != "" { query = query.Where("file_type = ?", fileType) } // 获取总数 query.Model(&model.Attachment{}).Count(&total) // 分页查询 offset := (page - 1) * pageSize result := query.Order("created_at DESC"). Offset(offset). Limit(pageSize). Find(&attachments) return attachments, total, result.Error } // 辅助函数:获取允许的文件扩展名 func (s *AttachmentService) getAllowedExtensions(fileType string) []string { if fileType == "image" { return []string{".jpg", ".jpeg", ".png", ".gif", ".webp"} } return []string{".mp4", ".avi", ".mov", ".wmv", ".flv", ".mkv"} } // 辅助函数:检查字符串是否在切片中 func contains(slice []string, item string) bool { for _, s := range slice { if s == item { return true } } return false }