2026-07-03 15:58:29 +08:00
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"crypto/rand"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"errors"
|
2026-07-18 10:52:32 +08:00
|
|
|
"fmt"
|
2026-07-03 15:58:29 +08:00
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"seeyon-filesystem/config"
|
|
|
|
|
"seeyon-filesystem/dto"
|
|
|
|
|
"seeyon-filesystem/model"
|
|
|
|
|
"seeyon-filesystem/repository"
|
|
|
|
|
"seeyon-filesystem/storage"
|
|
|
|
|
"seeyon-filesystem/utils"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ShareService 分享业务服务
|
|
|
|
|
type ShareService struct {
|
|
|
|
|
shareRepo *repository.ShareRepository
|
|
|
|
|
fileRepo *repository.FileRepository
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewShareService 创建分享服务实例
|
|
|
|
|
func NewShareService() *ShareService {
|
|
|
|
|
return &ShareService{
|
|
|
|
|
shareRepo: repository.NewShareRepository(),
|
|
|
|
|
fileRepo: repository.NewFileRepository(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create 创建分享链接
|
|
|
|
|
func (s *ShareService) Create(req *dto.ShareCreateRequest, ownerID uint64) (*dto.ShareVO, error) {
|
|
|
|
|
// 验证文件存在
|
2026-07-10 17:33:33 +08:00
|
|
|
fileID := uint64(req.FileID)
|
|
|
|
|
file, err := s.fileRepo.FindByID(fileID)
|
2026-07-03 15:58:29 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, errors.New("文件不存在")
|
|
|
|
|
}
|
2026-07-18 10:52:32 +08:00
|
|
|
if !s.canAccessFile(file.ID, file.OwnerID, ownerID) {
|
2026-07-03 15:58:29 +08:00
|
|
|
return nil, errors.New("无权分享此文件")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生成分享码
|
|
|
|
|
shareCode, err := generateShareCode()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, errors.New("生成分享码失败")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
share := &model.Share{
|
2026-07-18 10:52:32 +08:00
|
|
|
ID: utils.GenID(),
|
2026-07-10 17:33:33 +08:00
|
|
|
FileID: fileID,
|
2026-07-03 15:58:29 +08:00
|
|
|
OwnerID: ownerID,
|
|
|
|
|
ShareCode: shareCode,
|
|
|
|
|
AllowPreview: 1,
|
|
|
|
|
AllowDownload: 1,
|
|
|
|
|
Status: 1,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 设置密码
|
|
|
|
|
if req.Password != "" {
|
|
|
|
|
hashed, _ := utils.HashPassword(req.Password)
|
|
|
|
|
share.Password = hashed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 设置过期时间
|
|
|
|
|
if req.ExpireHours > 0 {
|
|
|
|
|
expireAt := time.Now().Add(time.Duration(req.ExpireHours) * time.Hour)
|
|
|
|
|
share.ExpireAt = &expireAt
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
share.MaxDownload = req.MaxDownload
|
|
|
|
|
|
|
|
|
|
// 设置权限
|
|
|
|
|
share.AllowPreview = int8(req.AllowPreview)
|
|
|
|
|
share.AllowDownload = int8(req.AllowDownload)
|
|
|
|
|
// 如果都是0(未传), 默认允许
|
|
|
|
|
if share.AllowPreview == 0 && share.AllowDownload == 0 {
|
|
|
|
|
share.AllowPreview = 1
|
|
|
|
|
share.AllowDownload = 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := s.shareRepo.Create(share); err != nil {
|
|
|
|
|
return nil, errors.New("创建分享失败")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return s.toShareVO(share), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetByCode 通过分享码获取分享信息(公开访问)
|
|
|
|
|
func (s *ShareService) GetByCode(code string) (*dto.ShareFileVO, *model.Share, error) {
|
|
|
|
|
share, err := s.shareRepo.FindByShareCode(code)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, errors.New("分享链接不存在或已失效")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查过期
|
|
|
|
|
if share.ExpireAt != nil && share.ExpireAt.Before(time.Now()) {
|
|
|
|
|
return nil, nil, errors.New("分享链接已过期")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查下载次数
|
|
|
|
|
if share.MaxDownload > 0 && share.DownloadCount >= share.MaxDownload {
|
|
|
|
|
return nil, nil, errors.New("下载次数已达上限")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取文件信息
|
|
|
|
|
file, err := s.fileRepo.FindByID(share.FileID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, errors.New("文件不存在")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生成预览URL
|
|
|
|
|
previewURL := ""
|
|
|
|
|
if share.AllowPreview == 1 && file.StorageKey != "" {
|
|
|
|
|
engine, err := storage.GetEngineByPolicyID(file.StoragePolicyID)
|
|
|
|
|
if err == nil {
|
|
|
|
|
if localEngine, ok := engine.(*storage.LocalEngine); ok {
|
|
|
|
|
previewURL = localEngine.GetURL(file.StorageKey)
|
|
|
|
|
} else if minioEngine, ok := engine.(*storage.MinIOEngine); ok {
|
|
|
|
|
previewURL = minioEngine.GetPreviewURL(file.StorageKey)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fileVO := &dto.ShareFileVO{
|
|
|
|
|
Name: file.Name,
|
|
|
|
|
Extension: file.Extension,
|
|
|
|
|
Size: file.Size,
|
|
|
|
|
MimeType: file.MimeType,
|
|
|
|
|
PreviewURL: previewURL,
|
|
|
|
|
AllowPreview: share.AllowPreview == 1,
|
|
|
|
|
AllowDownload: share.AllowDownload == 1,
|
|
|
|
|
CreatedAt: file.CreatedAt,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fileVO, share, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// VerifyPassword 验证分享密码
|
|
|
|
|
func (s *ShareService) VerifyPassword(shareID uint64, password string) bool {
|
|
|
|
|
share, err := s.shareRepo.FindByID(shareID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return utils.CheckPassword(password, share.Password)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ListByOwner 查询用户的分享列表
|
|
|
|
|
func (s *ShareService) ListByOwner(ownerID uint64) ([]dto.ShareVO, error) {
|
|
|
|
|
shares, err := s.shareRepo.ListByOwner(ownerID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vos := make([]dto.ShareVO, 0, len(shares))
|
|
|
|
|
for i := range shares {
|
|
|
|
|
vos = append(vos, *s.toShareVO(&shares[i]))
|
|
|
|
|
}
|
|
|
|
|
return vos, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cancel 取消分享
|
|
|
|
|
func (s *ShareService) Cancel(id uint64, ownerID uint64) error {
|
|
|
|
|
share, err := s.shareRepo.FindByID(id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return errors.New("分享不存在")
|
|
|
|
|
}
|
|
|
|
|
if share.OwnerID != ownerID {
|
|
|
|
|
return errors.New("无权操作此分享")
|
|
|
|
|
}
|
|
|
|
|
return s.shareRepo.Cancel(id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// toShareVO 转换为视图对象
|
|
|
|
|
func (s *ShareService) toShareVO(share *model.Share) *dto.ShareVO {
|
|
|
|
|
// 生成完整分享链接
|
|
|
|
|
publicURL := config.AppConfig.Server.PublicURL
|
|
|
|
|
if publicURL == "" {
|
2026-07-18 10:52:32 +08:00
|
|
|
publicURL = fmt.Sprintf("http://localhost:%d", config.AppConfig.Server.Port)
|
2026-07-03 15:58:29 +08:00
|
|
|
}
|
|
|
|
|
publicURL = strings.TrimRight(publicURL, "/")
|
|
|
|
|
|
|
|
|
|
return &dto.ShareVO{
|
2026-07-10 17:33:33 +08:00
|
|
|
ID: dto.StringID(share.ID),
|
2026-07-03 15:58:29 +08:00
|
|
|
ShareCode: share.ShareCode,
|
|
|
|
|
ShareURL: publicURL + "/s/" + share.ShareCode,
|
|
|
|
|
HasPassword: share.Password != "",
|
|
|
|
|
ExpireAt: share.ExpireAt,
|
|
|
|
|
MaxDownload: share.MaxDownload,
|
|
|
|
|
DownloadCount: share.DownloadCount,
|
|
|
|
|
AllowPreview: share.AllowPreview,
|
|
|
|
|
AllowDownload: share.AllowDownload,
|
|
|
|
|
CreatedAt: share.CreatedAt,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 10:52:32 +08:00
|
|
|
// canAccessFile 检查用户是否有权操作该文件(owner / admin / 被授权)
|
|
|
|
|
func (s *ShareService) canAccessFile(fileID, fileOwnerID, userID uint64) bool {
|
|
|
|
|
if fileOwnerID == userID {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
// 管理员
|
|
|
|
|
var count int64
|
|
|
|
|
config.DB.Model(&model.UserRole{}).
|
|
|
|
|
Joins("JOIN fs_role ON fs_role.id = fs_user_role.role_id").
|
|
|
|
|
Where("fs_user_role.user_id = ? AND fs_role.name = ?", userID, "admin").
|
|
|
|
|
Count(&count)
|
|
|
|
|
if count > 0 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
// 文件级授权
|
|
|
|
|
config.DB.Model(&model.FilePermission{}).
|
|
|
|
|
Where("file_id = ? AND (user_id = ? OR role_id IN (?))",
|
|
|
|
|
fileID, userID,
|
|
|
|
|
config.DB.Model(&model.UserRole{}).Select("role_id").Where("user_id = ?", userID),
|
|
|
|
|
).Count(&count)
|
|
|
|
|
return count > 0
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-03 15:58:29 +08:00
|
|
|
// generateShareCode 生成8字节随机分享码(16位十六进制字符串)
|
|
|
|
|
func generateShareCode() (string, error) {
|
|
|
|
|
bytes := make([]byte, 8)
|
|
|
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return hex.EncodeToString(bytes), nil
|
|
|
|
|
}
|