修复用户和文件管理,增加文件预览
This commit is contained in:
+443
-21
@@ -1,7 +1,12 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@@ -455,6 +460,12 @@ func (c *FileController) Post() {
|
||||
username = "unknown"
|
||||
}
|
||||
|
||||
// 从JWT中间件获取租户ID
|
||||
tenantID := "default"
|
||||
if tid, ok := c.Ctx.Input.GetData("tenantId").(int); ok && tid > 0 {
|
||||
tenantID = strconv.Itoa(tid)
|
||||
}
|
||||
|
||||
// 获取上传的文件
|
||||
file, header, err := c.GetFile("file")
|
||||
if err != nil {
|
||||
@@ -473,18 +484,91 @@ func (c *FileController) Post() {
|
||||
fileExt := strings.ToLower(filepath.Ext(originalName))
|
||||
fileName := strings.TrimSuffix(originalName, fileExt)
|
||||
|
||||
// 读取文件内容到内存以计算MD5(对于大文件可能需要优化)
|
||||
fileData := make([]byte, fileSize)
|
||||
n, err := file.Read(fileData)
|
||||
if err != nil && err != io.EOF {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "读取文件失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if int64(n) != fileSize {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "读取文件不完整",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 计算文件MD5值
|
||||
hash := md5.New()
|
||||
hash.Write(fileData)
|
||||
fileMD5 := hex.EncodeToString(hash.Sum(nil))
|
||||
|
||||
// 检查是否已存在相同MD5的文件
|
||||
existingFile, err := models.GetFileByMD5AndTenant(fileMD5, tenantID)
|
||||
if err == nil && existingFile != nil {
|
||||
// 文件已存在,不保存文件,只创建数据库记录
|
||||
// 生成日期路径(年/月/日)
|
||||
now := time.Now()
|
||||
|
||||
// 创建文件信息记录(使用已存在的文件路径)
|
||||
fileInfo := models.FileInfo{
|
||||
TenantID: tenantID,
|
||||
UserID: userID,
|
||||
FileName: fileName,
|
||||
OriginalName: originalName,
|
||||
FilePath: existingFile.FilePath, // 使用已存在文件的路径
|
||||
FileURL: existingFile.FileURL, // 使用已存在文件的URL
|
||||
FileSize: fileSize,
|
||||
FileType: getFileTypeByExt(fileExt),
|
||||
FileExt: fileExt,
|
||||
MD5: fileMD5,
|
||||
Category: c.GetString("category"),
|
||||
Status: 1,
|
||||
UploadBy: username,
|
||||
UploadTime: now,
|
||||
}
|
||||
|
||||
// 如果分类为空,使用默认分类
|
||||
if fileInfo.Category == "" {
|
||||
fileInfo.Category = "未分类"
|
||||
}
|
||||
|
||||
// 保存到数据库(只保存记录,不保存文件)
|
||||
id, err := models.AddFile(&fileInfo)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "保存文件信息失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
fileInfo.ID = id
|
||||
|
||||
// 返回成功响应
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "文件上传成功(重复文件,使用已有文件)",
|
||||
"data": fileInfo,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 文件不存在,正常上传流程
|
||||
// 获取分类(可选)
|
||||
category := c.GetString("category")
|
||||
if category == "" {
|
||||
category = "未分类"
|
||||
}
|
||||
|
||||
// 获取租户ID(可选,从请求参数或中间件获取)
|
||||
tenantID := c.GetString("tenant_id")
|
||||
if tenantID == "" {
|
||||
tenantID = "default"
|
||||
}
|
||||
|
||||
// 生成日期路径(年/月/日)
|
||||
now := time.Now()
|
||||
datePath := now.Format("2006/01/02")
|
||||
@@ -513,8 +597,8 @@ func (c *FileController) Post() {
|
||||
// 计算相对路径(用于存储到数据库)
|
||||
relativePath := path.Join("uploads", datePath, uniqueFileName)
|
||||
|
||||
// 保存文件
|
||||
if err := c.SaveToFile("file", savePath); err != nil {
|
||||
// 保存文件(将已读取的数据写入文件)
|
||||
if err := os.WriteFile(savePath, fileData, 0644); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "保存文件失败: " + err.Error(),
|
||||
@@ -524,19 +608,7 @@ func (c *FileController) Post() {
|
||||
}
|
||||
|
||||
// 获取文件类型
|
||||
fileType := "other"
|
||||
switch fileExt {
|
||||
case ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp":
|
||||
fileType = "image"
|
||||
case ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".txt":
|
||||
fileType = "document"
|
||||
case ".mp4", ".avi", ".mov", ".wmv":
|
||||
fileType = "video"
|
||||
case ".mp3", ".wav", ".flac":
|
||||
fileType = "audio"
|
||||
case ".zip", ".rar", ".7z":
|
||||
fileType = "archive"
|
||||
}
|
||||
fileType := getFileTypeByExt(fileExt)
|
||||
|
||||
// 构造文件URL(相对路径)
|
||||
fileURL := "/" + relativePath
|
||||
@@ -552,7 +624,9 @@ func (c *FileController) Post() {
|
||||
FileSize: fileSize,
|
||||
FileType: fileType,
|
||||
FileExt: fileExt,
|
||||
MD5: fileMD5,
|
||||
Category: category,
|
||||
Status: 1, // 设置为正常状态
|
||||
UploadBy: username,
|
||||
UploadTime: now,
|
||||
}
|
||||
@@ -580,3 +654,351 @@ func (c *FileController) Post() {
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DownloadFile 下载文件
|
||||
func (c *FileController) DownloadFile() {
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "参数错误",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取文件信息
|
||||
file, err := models.GetFileById(id)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "文件不存在",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取实际的文件记录(如果文件不存在,通过MD5查找)
|
||||
actualFile, err := getActualFile(file)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "获取文件信息失败",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件是否存在(尝试多个可能的路径)
|
||||
var filePath string
|
||||
possiblePaths := []string{
|
||||
actualFile.FilePath, // 直接使用相对路径
|
||||
filepath.Join("server", actualFile.FilePath), // server目录前缀
|
||||
filepath.Join(".", actualFile.FilePath), // 当前目录
|
||||
}
|
||||
|
||||
for _, path := range possiblePaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
filePath = path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if filePath == "" {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "文件不存在于服务器",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
c.Ctx.Output.Header("Content-Description", "File Transfer")
|
||||
c.Ctx.Output.Header("Content-Type", "application/octet-stream")
|
||||
c.Ctx.Output.Header("Content-Disposition", "attachment; filename="+url.QueryEscape(actualFile.OriginalName))
|
||||
c.Ctx.Output.Header("Content-Transfer-Encoding", "binary")
|
||||
c.Ctx.Output.Header("Expires", "0")
|
||||
c.Ctx.Output.Header("Cache-Control", "must-revalidate")
|
||||
c.Ctx.Output.Header("Pragma", "public")
|
||||
|
||||
// 输出文件
|
||||
http.ServeFile(c.Ctx.ResponseWriter, c.Ctx.Request, filePath)
|
||||
}
|
||||
|
||||
// PreviewFile 预览文件
|
||||
func (c *FileController) PreviewFile() {
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "参数错误",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取文件信息
|
||||
file, err := models.GetFileById(id)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "文件不存在",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取实际的文件记录(如果文件不存在,通过MD5查找)
|
||||
actualFile, err := getActualFile(file)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "获取文件信息失败",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件是否可预览
|
||||
if !actualFile.CanPreview() {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "该文件类型不支持预览",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件是否存在(尝试多个可能的路径)
|
||||
var filePath string
|
||||
possiblePaths := []string{
|
||||
actualFile.FilePath, // 直接使用相对路径
|
||||
filepath.Join("server", actualFile.FilePath), // server目录前缀
|
||||
filepath.Join(".", actualFile.FilePath), // 当前目录
|
||||
}
|
||||
|
||||
for _, path := range possiblePaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
filePath = path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if filePath == "" {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "文件不存在于服务器",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 设置正确的 Content-Type
|
||||
contentType := getContentType(actualFile.FileExt)
|
||||
c.Ctx.Output.Header("Content-Type", contentType)
|
||||
c.Ctx.Output.Header("Content-Disposition", "inline; filename="+url.QueryEscape(actualFile.OriginalName))
|
||||
|
||||
// 打开文件
|
||||
fileHandle, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "打开文件失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
defer fileHandle.Close()
|
||||
|
||||
// 复制文件内容到响应
|
||||
io.Copy(c.Ctx.ResponseWriter, fileHandle)
|
||||
}
|
||||
|
||||
// PublicPreviewFile 公开预览文件(用于 Office Online Viewer,无需认证但仅用于预览)
|
||||
func (c *FileController) PublicPreviewFile() {
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "参数错误",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取文件信息
|
||||
file, err := models.GetFileById(id)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "文件不存在",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取实际的文件记录
|
||||
actualFile, err := getActualFile(file)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "获取文件信息失败",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件是否可预览
|
||||
if !actualFile.CanPreview() {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "该文件类型不支持预览",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
var filePath string
|
||||
possiblePaths := []string{
|
||||
actualFile.FilePath,
|
||||
filepath.Join("server", actualFile.FilePath),
|
||||
filepath.Join(".", actualFile.FilePath),
|
||||
}
|
||||
|
||||
for _, path := range possiblePaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
filePath = path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if filePath == "" {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "文件不存在于服务器",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 设置正确的 Content-Type
|
||||
contentType := getContentType(actualFile.FileExt)
|
||||
c.Ctx.Output.Header("Content-Type", contentType)
|
||||
c.Ctx.Output.Header("Content-Disposition", "inline; filename="+url.QueryEscape(actualFile.OriginalName))
|
||||
// 允许跨域访问(Office Online Viewer 需要)
|
||||
c.Ctx.Output.Header("Access-Control-Allow-Origin", "*")
|
||||
|
||||
// 打开文件
|
||||
fileHandle, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "打开文件失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
defer fileHandle.Close()
|
||||
|
||||
// 复制文件内容到响应
|
||||
io.Copy(c.Ctx.ResponseWriter, fileHandle)
|
||||
}
|
||||
|
||||
// getFileTypeByExt 根据文件扩展名获取文件类型
|
||||
func getFileTypeByExt(ext string) string {
|
||||
ext = strings.ToLower(ext)
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp":
|
||||
return "image"
|
||||
case ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".txt":
|
||||
return "document"
|
||||
case ".mp4", ".avi", ".mov", ".wmv":
|
||||
return "video"
|
||||
case ".mp3", ".wav", ".flac":
|
||||
return "audio"
|
||||
case ".zip", ".rar", ".7z":
|
||||
return "archive"
|
||||
default:
|
||||
return "other"
|
||||
}
|
||||
}
|
||||
|
||||
// getContentType 根据文件扩展名获取 Content-Type
|
||||
func getContentType(ext string) string {
|
||||
ext = strings.ToLower(ext)
|
||||
contentTypes := map[string]string{
|
||||
// 图片
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".gif": "image/gif",
|
||||
".bmp": "image/bmp",
|
||||
".webp": "image/webp",
|
||||
".svg": "image/svg+xml",
|
||||
// 文档
|
||||
".pdf": "application/pdf",
|
||||
".txt": "text/plain; charset=utf-8",
|
||||
".doc": "application/msword",
|
||||
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
".xls": "application/vnd.ms-excel",
|
||||
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
".ppt": "application/vnd.ms-powerpoint",
|
||||
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
// 视频
|
||||
".mp4": "video/mp4",
|
||||
".webm": "video/webm",
|
||||
// 音频
|
||||
".mp3": "audio/mpeg",
|
||||
".wav": "audio/wav",
|
||||
}
|
||||
|
||||
if contentType, ok := contentTypes[ext]; ok {
|
||||
return contentType
|
||||
}
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
// getActualFile 获取实际的文件记录(如果当前记录的文件不存在,通过MD5查找唯一文件)
|
||||
func getActualFile(file *models.FileInfo) (*models.FileInfo, error) {
|
||||
// 检查当前记录的文件是否存在(尝试多个可能的路径)
|
||||
possiblePaths := []string{
|
||||
file.FilePath, // 直接使用相对路径
|
||||
filepath.Join("server", file.FilePath), // server目录前缀
|
||||
filepath.Join(".", file.FilePath), // 当前目录
|
||||
}
|
||||
|
||||
for _, path := range possiblePaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
// 文件存在,返回当前记录
|
||||
return file, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 文件不存在,通过MD5查找唯一文件
|
||||
if file.MD5 == "" {
|
||||
return file, nil
|
||||
}
|
||||
|
||||
actualFile, err := models.GetFileByMD5(file.MD5)
|
||||
if err == nil && actualFile != nil {
|
||||
// 检查找到的文件是否存在
|
||||
possiblePaths = []string{
|
||||
actualFile.FilePath,
|
||||
filepath.Join("server", actualFile.FilePath),
|
||||
filepath.Join(".", actualFile.FilePath),
|
||||
}
|
||||
for _, path := range possiblePaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return actualFile, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找不到,返回原始记录
|
||||
return file, nil
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ func (c *UserController) GetAllUsers() {
|
||||
"avatar": user.Avatar,
|
||||
"nickname": user.Nickname,
|
||||
"tenant_id": user.TenantId,
|
||||
"status": user.Status,
|
||||
"role": user.Role,
|
||||
"lastLoginTime": user.LastLoginTime,
|
||||
})
|
||||
}
|
||||
@@ -37,6 +39,56 @@ func (c *UserController) GetAllUsers() {
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetTenantUsers 获取指定租户下的所有用户(排除已删除的用户)
|
||||
func (c *UserController) GetTenantUsers() {
|
||||
// 从URL参数获取租户ID
|
||||
tenantId, err := c.GetInt(":tenantId")
|
||||
if err != nil || tenantId <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "租户ID无效",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 调用模型层方法查询
|
||||
users, err := models.GetTenantUsers(tenantId)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 格式化返回数据
|
||||
userList := make([]map[string]interface{}, 0)
|
||||
for _, user := range users {
|
||||
userList = append(userList, map[string]interface{}{
|
||||
"id": user.Id,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"avatar": user.Avatar,
|
||||
"nickname": user.Nickname,
|
||||
"tenant_id": user.TenantId,
|
||||
"status": user.Status,
|
||||
"role": user.Role,
|
||||
"last_login_time": user.LastLoginTime,
|
||||
})
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 0,
|
||||
"message": "获取租户用户列表成功",
|
||||
"data": userList,
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// ChangePassword 修改用户密码
|
||||
func (c *UserController) ChangePassword() {
|
||||
// 从URL获取用户ID
|
||||
@@ -144,6 +196,8 @@ func (c *UserController) GetUserInfo() {
|
||||
"avatar": user.Avatar,
|
||||
"nickname": user.Nickname,
|
||||
"tenant_id": user.TenantId,
|
||||
"role": user.Role,
|
||||
"status": user.Status,
|
||||
},
|
||||
}
|
||||
c.ServeJSON()
|
||||
@@ -159,6 +213,7 @@ func (c *UserController) AddUser() {
|
||||
Nickname string `json:"nickname"`
|
||||
Avatar string `json:"avatar"`
|
||||
TenantId int `json:"tenant_id"`
|
||||
Role int `json:"role"` // 角色ID
|
||||
}
|
||||
|
||||
// 解析请求体JSON数据
|
||||
@@ -210,6 +265,7 @@ func (c *UserController) AddUser() {
|
||||
userData.Nickname,
|
||||
userData.Avatar,
|
||||
userData.TenantId,
|
||||
userData.Role, // 添加 role 参数
|
||||
)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
@@ -243,6 +299,8 @@ func (c *UserController) EditUser() {
|
||||
Email string `json:"email"`
|
||||
Nickname string `json:"nickname"`
|
||||
Avatar string `json:"avatar"`
|
||||
Status string `json:"status"`
|
||||
Role int `json:"role"` // 改为 role,存储角色ID
|
||||
}
|
||||
|
||||
// 解析请求体JSON
|
||||
@@ -275,6 +333,8 @@ func (c *UserController) EditUser() {
|
||||
updateData.Email,
|
||||
updateData.Nickname,
|
||||
updateData.Avatar,
|
||||
updateData.Status,
|
||||
updateData.Role, // 改为 Role
|
||||
)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
|
||||
Reference in New Issue
Block a user