package service import ( "crypto/sha256" "encoding/hex" "io" "net/http" "os" "path/filepath" "time" "gorm.io/gorm" "nl-pms-api/internal/commonservice" "nl-pms-api/internal/config" "nl-pms-api/internal/model" ) // FileService 图片上传与素材库管理。 type FileService struct { DB *gorm.DB Cfg *config.Config } var extByMime = map[string]string{ "image/jpeg": ".jpg", "image/png": ".png", "image/gif": ".gif", "image/webp": ".webp", } // UploadResult 上传成功响应。 type UploadResult struct { ID int64 `json:"id"` Name string `json:"name"` URL string `json:"url"` Size int64 `json:"size"` Mime string `json:"mime"` TeamID int64 `json:"teamId"` } // FileItem 素材库列表项。 type FileItem struct { ID int64 `json:"id"` Name string `json:"name"` Original string `json:"original"` Mime string `json:"mime"` Size int64 `json:"size"` UserID int64 `json:"userId"` TeamID int64 `json:"teamId"` Kind string `json:"kind"` CreatedAt string `json:"createdAt"` Username string `json:"username"` URL string `json:"url" gorm:"-"` } // Upload 保存图片;userID 来自 JWT,teamID/kind 来自表单。 func (s *FileService) Upload(userID, teamID int64, kind, original string, r io.Reader, sizeHint int64, requestHost string) (*UploadResult, error) { if sizeHint > s.Cfg.MaxUploadBytes() { return nil, &commonservice.AppError{Code: "FILE_TOO_LARGE", Status: http.StatusRequestEntityTooLarge} } data, err := io.ReadAll(io.LimitReader(r, s.Cfg.MaxUploadBytes()+1)) if err != nil || int64(len(data)) > s.Cfg.MaxUploadBytes() { return nil, &commonservice.AppError{Code: "FILE_TOO_LARGE", Status: http.StatusRequestEntityTooLarge} } mime := http.DetectContentType(data) ext, ok := extByMime[mime] if !ok { return nil, &commonservice.AppError{Code: "UNSUPPORTED_TYPE", Status: http.StatusUnsupportedMediaType} } sum := hex.EncodeToString(func() []byte { h := sha256.Sum256(data); return h[:] }()) var rec model.File if s.DB.Where("sha256 = ? AND user_id = ? AND team_id = ?", sum, userID, teamID).First(&rec).Error == nil { return s.toUploadResult(rec, requestHost), nil } name := commonservice.StoredName(ext) full := filepath.Join(s.Cfg.StorageDir, filepath.FromSlash(name)) if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil { return nil, commonservice.Internal("SAVE_FAILED") } if err := os.WriteFile(full, data, 0644); err != nil { return nil, commonservice.Internal("SAVE_FAILED") } rec = model.File{ Name: name, Original: commonservice.Clip(filepath.Base(original), 255), Mime: mime, Size: int64(len(data)), SHA256: sum, UserID: userID, TeamID: teamID, Kind: commonservice.NormalizeKind(kind), CreatedAt: time.Now().UTC().Format(time.RFC3339), } if err := s.DB.Create(&rec).Error; err != nil { _ = os.Remove(full) return nil, commonservice.Internal("SAVE_FAILED") } return s.toUploadResult(rec, requestHost), nil } func (s *FileService) toUploadResult(f model.File, host string) *UploadResult { return &UploadResult{ ID: f.ID, Name: f.Name, URL: commonservice.PublicURL(s.Cfg, host, f.Name), Size: f.Size, Mime: f.Mime, TeamID: f.TeamID, } } // List 素材库分页;scope=mine|team|all。 func (s *FileService) List(userID int64, scope string, teamID, page, size int64, requestHost string) (int64, []FileItem, error) { if userID <= 0 { return 0, nil, commonservice.BadRequest("USER_REQUIRED") } var where func(*gorm.DB) *gorm.DB switch scope { case "mine": where = func(db *gorm.DB) *gorm.DB { return db.Where("pms_files.user_id = ?", userID) } case "team": if !commonservice.IsTeamAdmin(s.DB, teamID, userID) { return 0, nil, commonservice.Forbidden("FORBIDDEN") } where = func(db *gorm.DB) *gorm.DB { return db.Where("pms_files.team_id = ?", teamID) } case "all": if userID != commonservice.AdminUserID { return 0, nil, commonservice.Forbidden("FORBIDDEN") } where = func(db *gorm.DB) *gorm.DB { return db } default: return 0, nil, commonservice.BadRequest("BAD_SCOPE") } if page < 1 { page = 1 } if size < 1 || size > 100 { size = 24 } var total int64 if err := s.DB.Table("pms_files").Scopes(where).Count(&total).Error; err != nil { return 0, nil, commonservice.Internal("QUERY_FAILED") } items := []FileItem{} err := s.DB.Table("pms_files").Scopes(where). Select("pms_files.id, pms_files.name, pms_files.original, pms_files.mime, pms_files.size, pms_files.user_id, pms_files.team_id, pms_files.kind, pms_files.created_at, COALESCE(u.username,'') AS username"). Joins("LEFT JOIN users u ON u.id = pms_files.user_id"). Order("pms_files.id DESC").Limit(int(size)).Offset(int((page - 1) * size)). Scan(&items).Error if err != nil { return 0, nil, commonservice.Internal("QUERY_FAILED") } for i := range items { items[i].URL = commonservice.PublicURL(s.Cfg, requestHost, items[i].Name) } return total, items, nil } // Delete 删除记录与磁盘文件。 func (s *FileService) Delete(userID, fileID int64) error { var rec model.File if s.DB.First(&rec, fileID).Error != nil { return commonservice.NotFound("NOT_FOUND") } allowed := userID == commonservice.AdminUserID || (userID > 0 && rec.UserID == userID) || (rec.TeamID > 0 && commonservice.IsTeamAdmin(s.DB, rec.TeamID, userID)) if !allowed { return commonservice.Forbidden("FORBIDDEN") } if err := s.DB.Delete(&model.File{}, fileID).Error; err != nil { return commonservice.Internal("DELETE_FAILED") } _ = os.Remove(filepath.Join(s.Cfg.StorageDir, filepath.FromSlash(rec.Name))) return nil }