package service import ( "fmt" "io" "os" "path/filepath" "sort" "strings" "time" "gotest/internal/config" "gotest/internal/storage" ) // UploadService 文件上传服务 type UploadService struct { storage storage.Storage cfg *config.Config } // NewUploadService 创建上传服务实例 func NewUploadService(s storage.Storage, cfg *config.Config) *UploadService { return &UploadService{storage: s, cfg: cfg} } // allowedModules 允许的模块列表 var allowedModules = map[string]string{ "enterprise": "enterprise", "shop": "shop", "food": "food", "game": "game", "app": "app", "music": "music", "common": "common", } // Upload 上传文件,返回访问 URL // module 指定上传到的模块(enterprise/shop/food/game/app/music/common) // filename 原始文件名,contentType MIME 类型,fileSize 文件大小,reader 文件内容流 func (s *UploadService) Upload(module string, filename string, contentType string, fileSize int64, reader io.Reader) (string, error) { // 校验文件大小 if fileSize > s.cfg.Upload.MaxSize { return "", fmt.Errorf("文件大小超过限制(最大 %d 字节)", s.cfg.Upload.MaxSize) } // 校验文件类型 allowed := false for _, t := range s.cfg.Upload.AllowedTypes { if t == contentType { allowed = true break } } if !allowed { return "", fmt.Errorf("不支持的文件类型: %s", contentType) } // 校验并规范化模块名 mod := allowedModules[module] if mod == "" { mod = "common" } // 生成存储 key:模块名/日期/时间戳文件名 ext := filepath.Ext(filename) key := fmt.Sprintf("%s/%s%s", mod, time.Now().Format("2006/01/02/150405.000000"), ext) url, err := s.storage.Upload(key, reader, contentType) if err != nil { return "", fmt.Errorf("文件上传失败: %w", err) } return url, nil } // FileInfo 文件信息 type FileInfo struct { Name string `json:"name"` URL string `json:"url"` Size int64 `json:"size"` ModTime string `json:"mod_time"` IsDir bool `json:"is_dir"` } // ListFiles 列出指定模块下的所有文件(递归遍历子目录) func (s *UploadService) ListFiles(module string) ([]FileInfo, error) { mod := allowedModules[module] if mod == "" { mod = "common" } dirPath := filepath.Join(s.cfg.Upload.Path, mod) var result []FileInfo // 递归遍历 err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { if err != nil { return nil // 跳过错误 } if info.IsDir() { return nil // 跳过目录 } // 计算相对路径作为 key relPath, err := filepath.Rel(s.cfg.Upload.Path, path) if err != nil { return nil } relPath = filepath.ToSlash(relPath) // 统一用 / url := s.storage.GetURL(relPath) result = append(result, FileInfo{ Name: info.Name(), URL: url, Size: info.Size(), ModTime: info.ModTime().Format("2006-01-02 15:04:05"), IsDir: false, }) return nil }) if err != nil { if os.IsNotExist(err) { return []FileInfo{}, nil } return nil, fmt.Errorf("读取目录失败: %w", err) } // 按修改时间倒序 sort.Slice(result, func(i, j int) bool { return result[i].ModTime > result[j].ModTime }) return result, nil } // DeleteFile 删除指定模块下的文件 // filename 可以是文件名或完整相对路径(如 2026/07/10/xxx.png) func (s *UploadService) DeleteFile(module, filename string) error { mod := allowedModules[module] if mod == "" { mod = "common" } // 防止路径穿越:去掉模块名前缀(如果有的话)和 .. cleanName := strings.ReplaceAll(filename, "..", "") cleanName = strings.TrimPrefix(cleanName, mod+"/") cleanName = strings.TrimPrefix(cleanName, mod) key := filepath.Join(mod, cleanName) key = filepath.ToSlash(key) return s.storage.Delete(key) } // GetModules 返回所有支持的模块 func (s *UploadService) GetModules() []map[string]string { return []map[string]string{ {"key": "enterprise", "label": "企业官网"}, {"key": "shop", "label": "跨境电商"}, {"key": "food", "label": "外卖平台"}, {"key": "game", "label": "游戏平台"}, {"key": "app", "label": "应用商店"}, {"key": "music", "label": "音乐平台"}, {"key": "common", "label": "公共资源"}, } } // EnsureUploadDir 确保上传目录和各模块子目录存在 func (s *UploadService) EnsureUploadDir() { basePath := s.cfg.Upload.Path for mod := range allowedModules { os.MkdirAll(filepath.Join(basePath, mod), 0755) } } // sanitizeFilename 清理文件名中的危险字符 func sanitizeFilename(name string) string { name = strings.ReplaceAll(name, "..", "") name = strings.ReplaceAll(name, "/", "_") name = strings.ReplaceAll(name, "\\", "_") return name }