修复用户和文件管理,增加文件预览

This commit is contained in:
2025-11-03 16:18:30 +08:00
parent 2023fe4b97
commit 88d2f527f4
22 changed files with 4821 additions and 1095 deletions
+443 -21
View File
@@ -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
}
+60
View File
@@ -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{}{