Files

174 lines
5.5 KiB
Go
Raw Permalink Normal View History

2026-08-15 17:04:47 +08:00
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 来自 JWTteamID/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
}