提交
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"seeyon-filesystem/dto"
|
||||
"seeyon-filesystem/model"
|
||||
"seeyon-filesystem/repository"
|
||||
"seeyon-filesystem/storage"
|
||||
"seeyon-filesystem/utils"
|
||||
)
|
||||
|
||||
@@ -55,6 +56,7 @@ func (s *AdminService) CreateUser(req *dto.UserCreateRequest) error {
|
||||
}
|
||||
|
||||
user := &model.User{
|
||||
ID: utils.GenID(),
|
||||
Username: req.Username,
|
||||
Password: hashed,
|
||||
Email: req.Email,
|
||||
@@ -77,7 +79,11 @@ func (s *AdminService) CreateUser(req *dto.UserCreateRequest) error {
|
||||
|
||||
// 分配角色
|
||||
if len(req.RoleIDs) > 0 {
|
||||
s.userRoleRepo.SetUserRoles(user.ID, req.RoleIDs)
|
||||
roleIDs := make([]uint64, len(req.RoleIDs))
|
||||
for i, id := range req.RoleIDs {
|
||||
roleIDs[i] = uint64(id)
|
||||
}
|
||||
s.userRoleRepo.SetUserRoles(user.ID, roleIDs)
|
||||
} else {
|
||||
// 未指定角色时, 自动分配默认角色(user)
|
||||
var defaultRole model.Role
|
||||
@@ -99,7 +105,7 @@ func (s *AdminService) ListUsers(page, size int) ([]dto.UserVO, int64, error) {
|
||||
vos := make([]dto.UserVO, 0, len(users))
|
||||
for _, u := range users {
|
||||
vo := dto.UserVO{
|
||||
ID: u.ID,
|
||||
ID: dto.StringID(u.ID),
|
||||
Username: u.Username,
|
||||
Email: u.Email,
|
||||
Nickname: u.Nickname,
|
||||
@@ -112,7 +118,7 @@ func (s *AdminService) ListUsers(page, size int) ([]dto.UserVO, int64, error) {
|
||||
// 获取用户角色
|
||||
roles, _ := s.userRoleRepo.GetUserRoles(u.ID)
|
||||
for _, r := range roles {
|
||||
vo.Roles = append(vo.Roles, dto.RoleVO{ID: r.ID, Name: r.Name, DisplayName: r.DisplayName})
|
||||
vo.Roles = append(vo.Roles, dto.RoleVO{ID: dto.StringID(r.ID), Name: r.Name, DisplayName: r.DisplayName})
|
||||
}
|
||||
vos = append(vos, vo)
|
||||
}
|
||||
@@ -146,7 +152,11 @@ func (s *AdminService) UpdateUser(userID uint64, req *dto.UserUpdateRequest) err
|
||||
|
||||
// 更新角色
|
||||
if req.RoleIDs != nil {
|
||||
s.userRoleRepo.SetUserRoles(userID, req.RoleIDs)
|
||||
roleIDs := make([]uint64, len(req.RoleIDs))
|
||||
for i, id := range req.RoleIDs {
|
||||
roleIDs[i] = uint64(id)
|
||||
}
|
||||
s.userRoleRepo.SetUserRoles(userID, roleIDs)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -255,7 +265,13 @@ func (s *AdminService) UpdateStoragePolicy(id uint64, req *dto.StoragePolicyUpda
|
||||
}
|
||||
}
|
||||
|
||||
return s.policyRepo.Update(policy)
|
||||
if err := s.policyRepo.Update(policy); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 清除引擎缓存, 下次访问时用新配置重建
|
||||
storage.InvalidateEngine(id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteStoragePolicy 删除存储策略
|
||||
@@ -277,6 +293,7 @@ func (s *AdminService) CreateDepartment(req *dto.DeptCreateRequest) error {
|
||||
}
|
||||
|
||||
dept := &model.Department{
|
||||
ID: utils.GenID(),
|
||||
Name: req.Name,
|
||||
Code: req.Code,
|
||||
ParentID: req.ParentID,
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"seeyon-filesystem/config"
|
||||
"seeyon-filesystem/dto"
|
||||
@@ -13,6 +14,8 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const maxSessionsPerUser = 3 // 每用户最大同时登录数
|
||||
|
||||
// AuthService 认证服务
|
||||
type AuthService struct {
|
||||
userRepo *repository.UserRepository
|
||||
@@ -46,7 +49,11 @@ func (s *AuthService) Login(req *dto.LoginRequest) (*dto.LoginResponse, error) {
|
||||
return nil, errors.New("用户名或密码错误")
|
||||
}
|
||||
|
||||
// 生成JWT Token
|
||||
return s.generateLoginResponse(user, "", "")
|
||||
}
|
||||
|
||||
// generateLoginResponse 生成登录响应(统一处理Token生成和会话创建)
|
||||
func (s *AuthService) generateLoginResponse(user *model.User, device, ip string) (*dto.LoginResponse, error) {
|
||||
token, err := utils.GenerateToken(
|
||||
user.ID,
|
||||
user.Username,
|
||||
@@ -58,6 +65,9 @@ func (s *AuthService) Login(req *dto.LoginRequest) (*dto.LoginResponse, error) {
|
||||
return nil, errors.New("生成Token失败")
|
||||
}
|
||||
|
||||
// 创建会话(限制最大同时登录数)
|
||||
s.createSession(user.ID, token, device, ip)
|
||||
|
||||
return &dto.LoginResponse{
|
||||
Token: token,
|
||||
Username: user.Username,
|
||||
@@ -67,6 +77,59 @@ func (s *AuthService) Login(req *dto.LoginRequest) (*dto.LoginResponse, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Logout 退出登录, 删除当前会话
|
||||
func (s *AuthService) Logout(userID uint64, token string) {
|
||||
config.DB.Where("user_id = ? AND token = ?", userID, token).Delete(&model.UserSession{})
|
||||
}
|
||||
|
||||
// GetSessions 获取用户的活跃会话列表
|
||||
func (s *AuthService) GetSessions(userID uint64) []map[string]interface{} {
|
||||
var sessions []model.UserSession
|
||||
config.DB.Where("user_id = ? AND expires_at > ?", userID, time.Now()).
|
||||
Order("created_at DESC").Find(&sessions)
|
||||
|
||||
result := make([]map[string]interface{}, 0, len(sessions))
|
||||
for _, sess := range sessions {
|
||||
result = append(result, map[string]interface{}{
|
||||
"id": sess.ID,
|
||||
"device": sess.Device,
|
||||
"ip": sess.IP,
|
||||
"created_at": sess.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
"expires_at": sess.ExpiresAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// createSession 创建登录会话, 超过上限时踢掉最旧的
|
||||
func (s *AuthService) createSession(userID uint64, token, device, ip string) {
|
||||
// 清理该用户过期的会话
|
||||
config.DB.Where("user_id = ? AND expires_at < ?", userID, time.Now()).Delete(&model.UserSession{})
|
||||
|
||||
// 统计当前活跃会话数
|
||||
var count int64
|
||||
config.DB.Model(&model.UserSession{}).Where("user_id = ?", userID).Count(&count)
|
||||
|
||||
// 超过上限, 删除最旧的会话
|
||||
if count >= maxSessionsPerUser {
|
||||
var oldest model.UserSession
|
||||
config.DB.Where("user_id = ?", userID).Order("created_at ASC").First(&oldest)
|
||||
if oldest.ID > 0 {
|
||||
config.DB.Delete(&oldest)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新会话
|
||||
expiresAt := time.Now().Add(time.Duration(config.AppConfig.JWT.Expiration) * time.Hour)
|
||||
config.DB.Create(&model.UserSession{
|
||||
UserID: userID,
|
||||
Token: token,
|
||||
Device: device,
|
||||
IP: ip,
|
||||
ExpiresAt: expiresAt,
|
||||
})
|
||||
}
|
||||
|
||||
// validateAppLogin 校验 appId 登录
|
||||
func (s *AuthService) validateAppLogin(appID string, userID uint64) error {
|
||||
// 1. appId 是否存在且启用
|
||||
@@ -104,25 +167,7 @@ func (s *AuthService) AppLogin(req *dto.AppLoginRequest) (*dto.LoginResponse, er
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 生成JWT Token
|
||||
token, err := utils.GenerateToken(
|
||||
user.ID,
|
||||
user.Username,
|
||||
user.Role,
|
||||
config.AppConfig.JWT.Secret,
|
||||
config.AppConfig.JWT.Expiration,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.New("生成Token失败")
|
||||
}
|
||||
|
||||
return &dto.LoginResponse{
|
||||
Token: token,
|
||||
Username: user.Username,
|
||||
Nickname: user.Nickname,
|
||||
Role: user.Role,
|
||||
MustChangePwd: user.MustChangePwd == 1,
|
||||
}, nil
|
||||
return s.generateLoginResponse(user, "", "")
|
||||
}
|
||||
|
||||
// Register 用户注册
|
||||
@@ -138,6 +183,7 @@ func (s *AuthService) Register(req *dto.RegisterRequest) error {
|
||||
}
|
||||
|
||||
user := &model.User{
|
||||
ID: utils.GenID(),
|
||||
Username: req.Username,
|
||||
Password: hashedPassword,
|
||||
Email: req.Email,
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"seeyon-filesystem/config"
|
||||
"seeyon-filesystem/dto"
|
||||
"seeyon-filesystem/model"
|
||||
"seeyon-filesystem/utils"
|
||||
)
|
||||
|
||||
// BackupService 备份服务
|
||||
@@ -33,6 +34,7 @@ func (s *BackupService) CreatePolicy(req *dto.BackupPolicyCreateRequest) error {
|
||||
}
|
||||
|
||||
policy := &model.BackupPolicy{
|
||||
ID: utils.GenID(),
|
||||
Name: req.Name,
|
||||
BackupType: req.BackupType,
|
||||
TargetType: req.TargetType,
|
||||
@@ -105,6 +107,7 @@ func (s *BackupService) ExecuteBackup(policyID uint64) (*model.BackupLog, error)
|
||||
|
||||
// 创建备份日志
|
||||
log := &model.BackupLog{
|
||||
ID: utils.GenID(),
|
||||
PolicyID: policyID,
|
||||
PolicyName: policy.Name,
|
||||
BackupType: policy.BackupType,
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -38,7 +39,7 @@ func NewFileService() *FileService {
|
||||
|
||||
// Upload 上传文件
|
||||
// 流程: 校验大小 -> 计算MD5 -> 检查秒传 -> 存储文件 -> 保存元数据
|
||||
func (s *FileService) Upload(fileHeader *multipart.FileHeader, folderID uint64, ownerID uint64, storagePolicyID uint64) (*dto.FileVO, error) {
|
||||
func (s *FileService) Upload(fileHeader *multipart.FileHeader, folderID uint64, ownerID uint64, storagePolicyID uint64, bizID string) (*dto.FileVO, error) {
|
||||
// 校验文件大小
|
||||
maxSize := s.getMaxFileSize(ownerID)
|
||||
if maxSize > 0 && fileHeader.Size > maxSize {
|
||||
@@ -51,11 +52,19 @@ func (s *FileService) Upload(fileHeader *multipart.FileHeader, folderID uint64,
|
||||
if err != nil {
|
||||
return nil, errors.New("目标文件夹不存在")
|
||||
}
|
||||
if folder.OwnerID != ownerID {
|
||||
if !s.canAccessFile(folder.ID, folder.OwnerID, ownerID) {
|
||||
return nil, errors.New("无权上传到此文件夹")
|
||||
}
|
||||
}
|
||||
|
||||
// 检查biz_id唯一性
|
||||
if bizID != "" {
|
||||
var existingBiz model.File
|
||||
if config.DB.Where("biz_id = ? AND status = 1", bizID).First(&existingBiz).Error == nil {
|
||||
return nil, fmt.Errorf("业务编号 %s 已存在", bizID)
|
||||
}
|
||||
}
|
||||
|
||||
// 确定存储策略: 优先使用指定策略, 否则根据用户分配自动确定
|
||||
var engine storage.Engine
|
||||
var policyID uint64
|
||||
@@ -68,7 +77,7 @@ func (s *FileService) Upload(fileHeader *multipart.FileHeader, folderID uint64,
|
||||
engine, engineErr := storage.GetEngineByPolicyID(policyID)
|
||||
if engineErr != nil {
|
||||
engine = storage.GetDefaultEngine()
|
||||
policyID = uint64(config.AppConfig.Storage.DefaultPolicyID)
|
||||
// policyID 保留原值, 不覆盖, 确保文件记录正确的存储策略
|
||||
}
|
||||
if engine == nil {
|
||||
return nil, errors.New("存储引擎未初始化")
|
||||
@@ -106,6 +115,7 @@ func (s *FileService) Upload(fileHeader *multipart.FileHeader, folderID uint64,
|
||||
if existing != nil {
|
||||
newFile := &model.File{
|
||||
ID: utils.GenID(),
|
||||
BizID: bizID,
|
||||
Name: fileHeader.Filename,
|
||||
Extension: utils.GetExtension(fileHeader.Filename),
|
||||
FolderID: folderID,
|
||||
@@ -140,6 +150,7 @@ func (s *FileService) Upload(fileHeader *multipart.FileHeader, folderID uint64,
|
||||
|
||||
newFile := &model.File{
|
||||
ID: utils.GenID(),
|
||||
BizID: bizID,
|
||||
Name: fileHeader.Filename,
|
||||
Extension: ext,
|
||||
FolderID: folderID,
|
||||
@@ -158,6 +169,12 @@ func (s *FileService) Upload(fileHeader *multipart.FileHeader, folderID uint64,
|
||||
}
|
||||
|
||||
s.userRepo.UpdateStorageUsed(ownerID, size)
|
||||
|
||||
// 异步检测视频编码, H.265自动转码为H.264
|
||||
if ext == "mp4" || ext == "mkv" || ext == "avi" || ext == "mov" {
|
||||
go s.asyncTranscode(engine, newFile)
|
||||
}
|
||||
|
||||
return s.toFileVO(newFile), nil
|
||||
}
|
||||
|
||||
@@ -350,36 +367,48 @@ func GenerateFileURL(file *model.File, urlType string, token ...string) (string,
|
||||
|
||||
// 根据存储策略的实际类型决定URL生成方式
|
||||
policyType := getStoragePolicyType(file.StoragePolicyID)
|
||||
log.Printf("[URL生成] 文件ID=%d, storage_policy_id=%d, 策略类型=%s", file.ID, file.StoragePolicyID, policyType)
|
||||
|
||||
switch policyType {
|
||||
case "minio":
|
||||
engine, err := storage.GetEngineByPolicyID(file.StoragePolicyID)
|
||||
if err != nil {
|
||||
// MinIO不可用, 回退到API代理
|
||||
log.Printf("[URL生成] MinIO引擎获取失败, 回退API代理: %v", err)
|
||||
return buildAPIURL(file.ID, urlType, tk), nil
|
||||
}
|
||||
minioEngine := engine.(*storage.MinIOEngine)
|
||||
var presignedURL string
|
||||
var fileURL string
|
||||
var genErr error
|
||||
if urlType == "preview" {
|
||||
presignedURL, genErr = minioEngine.GetPresignedPreviewURL(file.StorageKey, file.MimeType, 24*time.Hour)
|
||||
fileURL, genErr = minioEngine.GetPresignedPreviewURL(file.StorageKey, file.MimeType, 24*time.Hour)
|
||||
} else {
|
||||
presignedURL, genErr = minioEngine.GetPresignedURL(file.StorageKey, 24*time.Hour)
|
||||
fileURL, genErr = minioEngine.GetPresignedURL(file.StorageKey, 24*time.Hour)
|
||||
}
|
||||
if genErr != nil {
|
||||
return buildAPIURL(file.ID, urlType, tk), nil
|
||||
// 预签名失败(如匿名凭证/公开桶), 用直接URL
|
||||
log.Printf("[URL生成] 预签名失败, 使用直接URL: %v", genErr)
|
||||
fileURL = minioEngine.GetURL(file.StorageKey)
|
||||
// 预览模式追加inline头参数, 避免触发下载
|
||||
if urlType == "preview" {
|
||||
fileURL += "?response-content-disposition=inline"
|
||||
}
|
||||
}
|
||||
policy := getStoragePolicyConfig(file.StoragePolicyID)
|
||||
if policy != nil && policy.NginxEndpoint != "" {
|
||||
presignedURL = replaceMinIOEndpoint(presignedURL, minioEngine.Endpoint, policy.NginxEndpoint)
|
||||
fileURL = replaceMinIOEndpoint(fileURL, minioEngine.Endpoint, policy.NginxEndpoint)
|
||||
}
|
||||
return presignedURL, nil
|
||||
return fileURL, nil
|
||||
|
||||
case "local":
|
||||
// 本地存储: 有CDN就用CDN, 否则用API路径
|
||||
cfg := config.AppConfig.Storage
|
||||
if cfg.CDNHost != "" {
|
||||
prefix := strings.TrimRight(cfg.CDNPathPrefix, "/")
|
||||
// 策略分类子目录(hot/cold/public/slave)
|
||||
category := getStoragePolicyCategory(file.StoragePolicyID)
|
||||
if category != "" {
|
||||
prefix = prefix + "/" + category
|
||||
}
|
||||
key := strings.TrimLeft(file.StorageKey, "/")
|
||||
return fmt.Sprintf("%s%s/%s", cfg.CDNHost, prefix, key), nil
|
||||
}
|
||||
@@ -400,6 +429,15 @@ func getStoragePolicyType(policyID uint64) string {
|
||||
return policy.Type
|
||||
}
|
||||
|
||||
// getStoragePolicyCategory 获取存储策略分类(hot/cold/public/slave)
|
||||
func getStoragePolicyCategory(policyID uint64) string {
|
||||
var policy model.StoragePolicy
|
||||
if config.DB.Select("policy_type").First(&policy, policyID).Error != nil {
|
||||
return ""
|
||||
}
|
||||
return policy.PolicyType
|
||||
}
|
||||
|
||||
// buildAPIURL 构建完整的API文件访问地址
|
||||
func buildAPIURL(fileID uint64, urlType string, token ...string) string {
|
||||
publicURL := config.AppConfig.Server.PublicURL
|
||||
@@ -476,9 +514,97 @@ func (s *FileService) SearchByType(ownerID uint64, extensions []string) ([]dto.F
|
||||
return vos, nil
|
||||
}
|
||||
|
||||
// ListTrashed 查询回收站
|
||||
// asyncTranscode 异步转码视频(H.265→H.264), 支持本地存储和MinIO
|
||||
func (s *FileService) asyncTranscode(engine storage.Engine, file *model.File) {
|
||||
if !utils.FFmpegAvailable() {
|
||||
return
|
||||
}
|
||||
|
||||
// 根据存储引擎类型处理
|
||||
switch e := engine.(type) {
|
||||
case *storage.LocalEngine:
|
||||
s.transcodeLocal(e, file)
|
||||
case *storage.MinIOEngine:
|
||||
s.transcodeMinIO(e, file)
|
||||
}
|
||||
}
|
||||
|
||||
// transcodeLocal 本地存储转码
|
||||
func (s *FileService) transcodeLocal(localEngine *storage.LocalEngine, file *model.File) {
|
||||
filePath := localEngine.GetFullPath(file.StorageKey)
|
||||
if !utils.NeedsTranscode(filePath) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[转码] 检测到H.265视频, 开始转码: %s", file.Name)
|
||||
utils.TranscodeToH264(filePath)
|
||||
|
||||
// 更新文件大小(转码后大小可能变化)
|
||||
if info, err := os.Stat(filePath); err == nil {
|
||||
s.fileRepo.UpdateSize(file.ID, info.Size())
|
||||
}
|
||||
}
|
||||
|
||||
// transcodeMinIO MinIO存储转码: 下载→转码→重新上传
|
||||
func (s *FileService) transcodeMinIO(minioEngine *storage.MinIOEngine, file *model.File) {
|
||||
// 下载到临时文件
|
||||
tmpInput := os.TempDir() + "/transcode_in_" + file.MD5 + filepath.Ext(file.Name)
|
||||
reader, err := minioEngine.Download(file.StorageKey)
|
||||
if err != nil {
|
||||
log.Printf("[转码] MinIO下载失败: %v", err)
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
outFile, err := os.Create(tmpInput)
|
||||
if err != nil {
|
||||
log.Printf("[转码] 创建临时文件失败: %v", err)
|
||||
return
|
||||
}
|
||||
io.Copy(outFile, reader)
|
||||
outFile.Close()
|
||||
defer os.Remove(tmpInput)
|
||||
|
||||
// 检测是否需要转码
|
||||
if !utils.NeedsTranscode(tmpInput) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[转码] 检测到H.265视频(MinIO), 开始转码: %s", file.Name)
|
||||
|
||||
// 转码
|
||||
tmpOutput := os.TempDir() + "/transcode_out_" + file.MD5 + ".mp4"
|
||||
utils.TranscodeToH264(tmpInput) // 会生成 tmpInput 的 _h264.mp4 版本并替换
|
||||
// TranscodeToH264 会替换原文件, 所以用 tmpInput 作为转码结果
|
||||
|
||||
// 重新上传到 MinIO
|
||||
uploadFile, err := os.Open(tmpInput)
|
||||
if err != nil {
|
||||
log.Printf("[转码] 打开转码文件失败: %v", err)
|
||||
return
|
||||
}
|
||||
defer uploadFile.Close()
|
||||
defer os.Remove(tmpInput)
|
||||
|
||||
newSize, _ := uploadFile.Seek(0, io.SeekEnd)
|
||||
uploadFile.Seek(0, io.SeekStart)
|
||||
|
||||
if err := minioEngine.Upload(file.StorageKey, uploadFile, newSize); err != nil {
|
||||
log.Printf("[转码] MinIO上传失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新文件大小
|
||||
s.fileRepo.UpdateSize(file.ID, newSize)
|
||||
log.Printf("[转码] MinIO转码完成: %s, 新大小: %d", file.Name, newSize)
|
||||
|
||||
os.Remove(tmpOutput)
|
||||
}
|
||||
|
||||
// ListTrashed 查询回收站(管理员看全部, 普通用户看自己的)
|
||||
func (s *FileService) ListTrashed(ownerID uint64) ([]dto.FileVO, error) {
|
||||
files, err := s.fileRepo.ListTrashed(ownerID)
|
||||
isAdmin := s.isAdmin(ownerID)
|
||||
files, err := s.fileRepo.ListTrashed(ownerID, isAdmin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -499,6 +625,7 @@ func (s *FileService) GetDownloadPath(storageKey string) string {
|
||||
func (s *FileService) toFileVO(file *model.File) *dto.FileVO {
|
||||
vo := &dto.FileVO{
|
||||
ID: dto.StringID(file.ID),
|
||||
BizID: file.BizID,
|
||||
Name: file.Name,
|
||||
Extension: file.Extension,
|
||||
FolderID: dto.StringID(file.FolderID),
|
||||
@@ -666,6 +793,7 @@ func (s *FileService) GrantPermission(fileID uint64, grantBy uint64, req *dto.Gr
|
||||
}
|
||||
|
||||
return config.DB.Create(&model.FilePermission{
|
||||
ID: utils.GenID(),
|
||||
FileID: fileID,
|
||||
UserID: userID,
|
||||
RoleID: roleID,
|
||||
|
||||
@@ -48,6 +48,14 @@ func (s *FolderService) Create(req *dto.CreateFolderRequest, ownerID uint64) (*d
|
||||
return nil, errors.New("同名文件夹已存在")
|
||||
}
|
||||
|
||||
// 检查biz_id唯一性
|
||||
if req.BizID != "" {
|
||||
existingBiz, _ := s.folderRepo.FindByBizID(req.BizID, ownerID)
|
||||
if existingBiz != nil {
|
||||
return nil, fmt.Errorf("业务编号 %s 已存在", req.BizID)
|
||||
}
|
||||
}
|
||||
|
||||
folderID := utils.GenID()
|
||||
folder := &model.Folder{
|
||||
ID: folderID,
|
||||
@@ -253,6 +261,7 @@ func (s *FolderService) getUserRoleIDs(userID uint64) []uint64 {
|
||||
func (s *FolderService) toFileVO(file *model.File) *dto.FileVO {
|
||||
vo := &dto.FileVO{
|
||||
ID: dto.StringID(file.ID),
|
||||
BizID: file.BizID,
|
||||
Name: file.Name,
|
||||
Extension: file.Extension,
|
||||
FolderID: dto.StringID(file.FolderID),
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"seeyon-filesystem/config"
|
||||
"seeyon-filesystem/dto"
|
||||
"seeyon-filesystem/model"
|
||||
"seeyon-filesystem/utils"
|
||||
)
|
||||
|
||||
// LogService 日志服务
|
||||
@@ -16,6 +17,7 @@ func NewLogService() *LogService {
|
||||
// Record 记录操作日志
|
||||
func (s *LogService) Record(userID uint64, username, action, resource, detail, ip, ua string, status int) {
|
||||
log := &model.OperationLog{
|
||||
ID: utils.GenID(),
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
Action: action,
|
||||
|
||||
@@ -33,6 +33,7 @@ func (s *OpenAPIService) CreateApp(appName string) (*dto.AppVO, error) {
|
||||
appSecret := generateRandomHex(32)
|
||||
|
||||
app := &model.App{
|
||||
ID: utils.GenID(),
|
||||
AppID: appID,
|
||||
AppSecret: appSecret,
|
||||
AppName: appName,
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -37,7 +38,7 @@ func (s *ShareService) Create(req *dto.ShareCreateRequest, ownerID uint64) (*dto
|
||||
if err != nil {
|
||||
return nil, errors.New("文件不存在")
|
||||
}
|
||||
if file.OwnerID != ownerID {
|
||||
if !s.canAccessFile(file.ID, file.OwnerID, ownerID) {
|
||||
return nil, errors.New("无权分享此文件")
|
||||
}
|
||||
|
||||
@@ -48,6 +49,7 @@ func (s *ShareService) Create(req *dto.ShareCreateRequest, ownerID uint64) (*dto
|
||||
}
|
||||
|
||||
share := &model.Share{
|
||||
ID: utils.GenID(),
|
||||
FileID: fileID,
|
||||
OwnerID: ownerID,
|
||||
ShareCode: shareCode,
|
||||
@@ -176,7 +178,7 @@ func (s *ShareService) toShareVO(share *model.Share) *dto.ShareVO {
|
||||
// 生成完整分享链接
|
||||
publicURL := config.AppConfig.Server.PublicURL
|
||||
if publicURL == "" {
|
||||
publicURL = "http://localhost:8080"
|
||||
publicURL = fmt.Sprintf("http://localhost:%d", config.AppConfig.Server.Port)
|
||||
}
|
||||
publicURL = strings.TrimRight(publicURL, "/")
|
||||
|
||||
@@ -194,6 +196,29 @@ func (s *ShareService) toShareVO(share *model.Share) *dto.ShareVO {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// generateShareCode 生成8字节随机分享码(16位十六进制字符串)
|
||||
func generateShareCode() (string, error) {
|
||||
bytes := make([]byte, 8)
|
||||
|
||||
@@ -295,6 +295,7 @@ func (s *SSOService) findOrCreateUser(provider *model.SSOProvider, ssoUserData s
|
||||
localUsername := fmt.Sprintf("sso_%s_%s", provider.Type, username)
|
||||
|
||||
newUser := &model.User{
|
||||
ID: utils.GenID(),
|
||||
Username: localUsername,
|
||||
Password: "_SSO_NO_PASSWORD_", // SSO用户无本地密码
|
||||
Email: ssoUserData["email"],
|
||||
@@ -323,6 +324,7 @@ func (s *SSOService) findOrCreateUser(provider *model.SSOProvider, ssoUserData s
|
||||
// saveSession 保存SSO会话
|
||||
func (s *SSOService) saveSession(userID, providerID uint64, externalID, accessToken, refreshToken string, expiresAt time.Time) {
|
||||
session := &model.SSOSession{
|
||||
ID: utils.GenID(),
|
||||
UserID: userID,
|
||||
ProviderID: providerID,
|
||||
ExternalID: externalID,
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"seeyon-filesystem/config"
|
||||
"seeyon-filesystem/dto"
|
||||
"seeyon-filesystem/model"
|
||||
"seeyon-filesystem/utils"
|
||||
)
|
||||
|
||||
// StorageAssignService 存储策略分配服务
|
||||
@@ -245,6 +246,7 @@ func (s *StorageAssignService) SetDefault(policyID uint64) error {
|
||||
|
||||
// 创建新记录
|
||||
return config.DB.Create(&model.StorageAssignment{
|
||||
ID: utils.GenID(),
|
||||
PolicyID: policyID,
|
||||
IsDefault: 1,
|
||||
Status: 1,
|
||||
@@ -265,6 +267,7 @@ func (s *StorageAssignService) AssignToRole(roleID, policyID uint64, quota int64
|
||||
}
|
||||
|
||||
return config.DB.Create(&model.StorageAssignment{
|
||||
ID: utils.GenID(),
|
||||
GroupID: roleID,
|
||||
PolicyID: policyID,
|
||||
StorageQuota: quota,
|
||||
@@ -286,6 +289,7 @@ func (s *StorageAssignService) AssignToUser(userID, policyID uint64, quota int64
|
||||
}
|
||||
|
||||
return config.DB.Create(&model.StorageAssignment{
|
||||
ID: utils.GenID(),
|
||||
UserID: userID,
|
||||
PolicyID: policyID,
|
||||
StorageQuota: quota,
|
||||
|
||||
Reference in New Issue
Block a user