Files
2026-08-15 17:04:47 +08:00

174 lines
5.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}