package services import ( "fmt" "net/url" "path/filepath" "strings" "time" "server/models" ) // FileUploadParams 创建文件记录所需参数 type FileUploadParams struct { Source string // backend / platform Scope string // tenant / user Tid uint64 Tuid uint64 // 归属用户,0 表示租户共享 Uid uint64 // 上传者 Name string Ext string Cate uint64 Size uint64 Src string ObjectKey string Storage string MD5 string } // FindDuplicate 按「来源端 + 归属范围 + 租户 + 归属用户 + MD5」精确查重。 // // 规则: // - 租户共享文件:同一 tid 内 MD5 相同才算重复(tuid 为 NULL) // - 用户个人文件:同一 tid + 同一 tuid 内 MD5 相同才算重复 // - 因此「租户已有 a 文件」与「用户 c 上传同样文件」互不冲突,可并存 // // 未命中时返回 (nil, nil),调用方按 err == nil && file == nil 处理即可。 func FindDuplicate(source, scope string, tid, tuid uint64, md5Str string) (*models.SystemFile, error) { if md5Str == "" { return nil, nil } models.EnsureSystemFileStorageColumns() qs := models.Orm.QueryTable(new(models.SystemFile)). Filter("source", source). Filter("scope", scope). Filter("tid", tid). Filter("md5", md5Str). Filter("delete_time__isnull", true) if scope == ScopeUser { qs = qs.Filter("tuid", tuid) } else { qs = qs.Filter("tuid__isnull", true) } var f models.SystemFile if err := qs.OrderBy("-id").One(&f); err != nil { return nil, nil } return &f, nil } // CreateFileRecord 写入文件记录(同步写入归属与存储字段) func CreateFileRecord(p FileUploadParams) (uint64, error) { models.EnsureSystemFileStorageColumns() uid := p.Uid row := &models.SystemFile{ Tid: p.Tid, Uid: &uid, Name: p.Name, Type: DetectFileType(p.Ext), Cate: p.Cate, Size: p.Size, Src: p.Src, Uploader: p.Uid, Md5: p.MD5, Source: p.Source, Scope: p.Scope, Storage: p.Storage, ObjectKey: p.ObjectKey, } if p.Tuid > 0 { tuid := p.Tuid row.Tuid = &tuid } if row.Source == "" { row.Source = SourceBackend } if row.Scope == "" { row.Scope = ScopeTenant } id, err := models.Orm.Insert(row) if err != nil { return 0, err } return uint64(id), nil } // RemovePhysical 删除物理文件。优先用 object_key,老数据则从 src 反推。 func RemovePhysical(svc StorageService, objectKey, src string) error { if svc == nil { return fmt.Errorf("存储服务未初始化") } key := strings.TrimSpace(objectKey) if key == "" { key = KeyFromSrc(src, svc) } if key == "" { return fmt.Errorf("无法解析文件路径: %s", src) } return svc.Delete(key) } // KeyFromSrc 从访问 URL/相对路径中解析出存储 key(object_key) func KeyFromSrc(src string, svc StorageService) string { src = strings.TrimSpace(src) if src == "" { return "" } switch s := svc.(type) { case *LocalStorage: base := filepath.ToSlash(s.BaseDir) rel := src if i := strings.Index(rel, base+"/"); i >= 0 { rel = rel[i+len(base)+1:] } rel = strings.TrimPrefix(rel, "/") // 去掉可能的查询串 if i := strings.IndexAny(rel, "?#"); i >= 0 { rel = rel[:i] } return rel case *QiniuStorage: domain := strings.TrimRight(s.Domain, "/") rel := src if domain != "" && strings.HasPrefix(rel, domain) { rel = strings.TrimPrefix(strings.TrimPrefix(rel, domain), "/") } else if u, err := url.Parse(rel); err == nil && u.Host != "" { rel = strings.TrimPrefix(u.Path, "/") } rel = strings.TrimPrefix(rel, "/") if i := strings.IndexAny(rel, "?#"); i >= 0 { rel = rel[:i] } return rel } return "" } // 文件类型与扩展名白名单(两端共用同一套规则) var fileTypeByCategory = map[string]uint8{ "image": 1, "document": 2, "video": 3, "audio": 4, "appsupgrade": 2, } var allowedExtByCategory = map[string][]string{ "image": {"jpg", "jpeg", "png", "gif", "bmp", "webp"}, "document": {"pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt"}, "video": {"mp4", "webm", "mov"}, "audio": {"mp3", "wav", "ogg"}, "appsupgrade": {"zip", "exe", "dmg", "msi", "msix", "apk", "deb", "rpm", "7z", "tar", "gz", "pkg"}, } // FileExt 取小写扩展名(不含点) func FileExt(name string) string { name = strings.TrimSpace(name) if i := strings.LastIndex(name, "."); i >= 0 && i < len(name)-1 { return strings.ToLower(name[i+1:]) } return "" } // DetectFileType 根据扩展名推断文件类型:1图片 2文档 3视频 4音频,未匹配返回 2(文档/其他) func DetectFileType(ext string) uint8 { ext = strings.ToLower(strings.TrimPrefix(ext, ".")) for cat, exts := range allowedExtByCategory { for _, e := range exts { if e == ext { if t, ok := fileTypeByCategory[cat]; ok { return t } return 2 } } } return 2 } // SoftDeleteFiles 软删除(标记 delete_time) func SoftDeleteFiles(ids []uint64) (int64, error) { if len(ids) == 0 { return 0, nil } models.EnsureSystemFileStorageColumns() return models.Orm.QueryTable(new(models.SystemFile)). Filter("id__in", ids). Filter("delete_time__isnull", true). Update(map[string]interface{}{"delete_time": time.Now()}) } // DeleteFilesPermanently 彻底删除数据库记录(物理文件由调用方先删) func DeleteFilesPermanently(ids []uint64) (int64, error) { if len(ids) == 0 { return 0, nil } models.EnsureSystemFileStorageColumns() return models.Orm.QueryTable(new(models.SystemFile)). Filter("id__in", ids). Delete() }