修复用户和文件管理,增加文件预览
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{}{
|
||||
|
||||
@@ -60,8 +60,9 @@ func JWTAuthMiddleware() web.FilterFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户信息存储在上下文
|
||||
ctx.Input.SetData("userId", claims.UserID)
|
||||
ctx.Input.SetData("username", claims.Username)
|
||||
}
|
||||
// 将用户信息存储在上下文
|
||||
ctx.Input.SetData("userId", claims.UserID)
|
||||
ctx.Input.SetData("username", claims.Username)
|
||||
ctx.Input.SetData("tenantId", claims.TenantId)
|
||||
}
|
||||
}
|
||||
|
||||
+98
-44
@@ -1,42 +1,46 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
// FileInfo 文件信息模型
|
||||
// 对应 yz_files 表结构
|
||||
type FileInfo struct {
|
||||
ID int64 `orm:"column(id);auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
|
||||
ID int64 `orm:"column(id);auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
|
||||
// 用户关联信息(通过JWT认证获取)
|
||||
UserID int `orm:"column(user_id);default(0)" json:"user_id"`
|
||||
|
||||
UserID int `orm:"column(user_id);default(0)" json:"user_id"`
|
||||
|
||||
// 文件基础信息
|
||||
FileName string `orm:"column(file_name);size(255)" json:"file_name"`
|
||||
OriginalName string `orm:"column(original_name);size(255)" json:"original_name"`
|
||||
FilePath string `orm:"column(file_path);size(500)" json:"file_path"`
|
||||
FileURL string `orm:"column(file_url);size(500);null" json:"file_url"`
|
||||
FileSize int64 `orm:"column(file_size);default(0)" json:"file_size"`
|
||||
FileType string `orm:"column(file_type);size(50)" json:"file_type"`
|
||||
FileExt string `orm:"column(file_ext);size(20)" json:"file_ext"`
|
||||
|
||||
FileName string `orm:"column(file_name);size(255)" json:"file_name"`
|
||||
OriginalName string `orm:"column(original_name);size(255)" json:"original_name"`
|
||||
FilePath string `orm:"column(file_path);size(500)" json:"file_path"`
|
||||
FileURL string `orm:"column(file_url);size(500);null" json:"file_url"`
|
||||
FileSize int64 `orm:"column(file_size);default(0)" json:"file_size"`
|
||||
FileType string `orm:"column(file_type);size(50)" json:"file_type"`
|
||||
FileExt string `orm:"column(file_ext);size(20)" json:"file_ext"`
|
||||
MD5 string `orm:"column(md5);size(32);null" json:"md5"`
|
||||
|
||||
// 分类信息
|
||||
Category string `orm:"column(category);size(100)" json:"category"`
|
||||
SubCategory string `orm:"column(sub_category);size(100);null" json:"sub_category"`
|
||||
|
||||
Category string `orm:"column(category);size(100)" json:"category"`
|
||||
SubCategory string `orm:"column(sub_category);size(100);null" json:"sub_category"`
|
||||
|
||||
// 状态信息
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"`
|
||||
IsPublic int8 `orm:"column(is_public);default(0)" json:"is_public"`
|
||||
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"`
|
||||
IsPublic int8 `orm:"column(is_public);default(0)" json:"is_public"`
|
||||
|
||||
// 上传信息
|
||||
UploadBy string `orm:"column(upload_by);size(100)" json:"upload_by"`
|
||||
UploadTime time.Time `orm:"column(upload_time);type(datetime);auto_now_add" json:"upload_time"`
|
||||
|
||||
UploadBy string `orm:"column(upload_by);size(100)" json:"upload_by"`
|
||||
UploadTime time.Time `orm:"column(upload_time);type(datetime);auto_now_add" json:"upload_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time,omitempty"`
|
||||
|
||||
// 关联的用户信息(非数据库字段)
|
||||
User *User `orm:"-" json:"user,omitempty"`
|
||||
User *User `orm:"-" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
// TableName 设置表名
|
||||
@@ -44,6 +48,31 @@ func (f *FileInfo) TableName() string {
|
||||
return "yz_files"
|
||||
}
|
||||
|
||||
// CanPreview 判断文件是否可以在线预览
|
||||
func (f *FileInfo) CanPreview() bool {
|
||||
previewableExts := map[string]bool{
|
||||
// 图片格式
|
||||
".jpg": true,
|
||||
".jpeg": true,
|
||||
".png": true,
|
||||
".gif": true,
|
||||
".bmp": true,
|
||||
".webp": true,
|
||||
".svg": true,
|
||||
// 文档格式(仅支持 .docx,不支持旧的 .doc、Excel、PPT)
|
||||
".pdf": true,
|
||||
".txt": true,
|
||||
".docx": true,
|
||||
// 视频格式
|
||||
".mp4": true,
|
||||
".webm": true,
|
||||
// 音频格式
|
||||
".mp3": true,
|
||||
".wav": true,
|
||||
}
|
||||
return previewableExts[strings.ToLower(f.FileExt)]
|
||||
}
|
||||
|
||||
// GetAllFiles 获取所有文件信息
|
||||
func GetAllFiles() ([]*FileInfo, error) {
|
||||
o := orm.NewOrm()
|
||||
@@ -109,7 +138,7 @@ func UpdateFile(file *FileInfo) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteFile 删除文件信息(软删除,设置状态为0)
|
||||
// DeleteFile 删除文件信息(软删除,设置状态为0并记录删除时间)
|
||||
func DeleteFile(id int64) error {
|
||||
o := orm.NewOrm()
|
||||
file := &FileInfo{ID: id}
|
||||
@@ -117,7 +146,9 @@ func DeleteFile(id int64) error {
|
||||
return err
|
||||
}
|
||||
file.Status = 0
|
||||
_, err := o.Update(file, "Status")
|
||||
now := time.Now()
|
||||
file.DeleteTime = &now
|
||||
_, err := o.Update(file, "Status", "DeleteTime")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -131,50 +162,73 @@ func HardDeleteFile(id int64) error {
|
||||
// GetFileStatistics 获取文件统计信息
|
||||
func GetFileStatistics(tenantID string) (map[string]interface{}, error) {
|
||||
o := orm.NewOrm()
|
||||
|
||||
|
||||
// 总文件数
|
||||
totalCount, err := o.QueryTable("yz_files").Filter("tenant_id", tenantID).Filter("status", 1).Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
// 总文件大小
|
||||
var totalSize int64
|
||||
err = o.Raw("SELECT COALESCE(SUM(file_size), 0) FROM yz_files WHERE tenant_id = ? AND status = 1", tenantID).QueryRow(&totalSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
// 按分类统计
|
||||
var categoryStats []orm.Params
|
||||
_, err = o.Raw("SELECT category, COUNT(*) as count, COALESCE(SUM(file_size), 0) as size FROM yz_files WHERE tenant_id = ? AND status = 1 GROUP BY category", tenantID).Values(&categoryStats)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_count": totalCount,
|
||||
"total_size": totalSize,
|
||||
"total_count": totalCount,
|
||||
"total_size": totalSize,
|
||||
"category_stats": categoryStats,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SearchFiles 搜索文件
|
||||
// SearchFiles 搜索文件(通过原始文件名搜索)
|
||||
func SearchFiles(keyword string, tenantID string) ([]*FileInfo, error) {
|
||||
o := orm.NewOrm()
|
||||
var files []*FileInfo
|
||||
|
||||
// 构建查询条件
|
||||
|
||||
// 构建查询条件 - 只通过原始文件名搜索
|
||||
qs := o.QueryTable("yz_files").Filter("tenant_id", tenantID).Filter("status", 1)
|
||||
|
||||
// 搜索文件名、原始文件名、分类(使用or条件)
|
||||
cond := orm.NewCondition()
|
||||
cond = cond.Or("file_name__icontains", keyword).
|
||||
Or("original_name__icontains", keyword).
|
||||
Or("category__icontains", keyword)
|
||||
|
||||
qs = qs.SetCond(cond)
|
||||
|
||||
|
||||
// 搜索原始文件名
|
||||
qs = qs.Filter("original_name__icontains", keyword)
|
||||
|
||||
_, err := qs.OrderBy("-upload_time").All(&files)
|
||||
return files, err
|
||||
}
|
||||
}
|
||||
|
||||
// GetFileByMD5 根据MD5获取文件信息(获取唯一的文件记录)
|
||||
func GetFileByMD5(md5 string) (*FileInfo, error) {
|
||||
o := orm.NewOrm()
|
||||
var file FileInfo
|
||||
err := o.QueryTable("yz_files").Filter("md5", md5).Filter("status", 1).OrderBy("upload_time").One(&file)
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &file, nil
|
||||
}
|
||||
|
||||
// GetFileByMD5AndTenant 根据MD5和租户ID获取文件信息
|
||||
func GetFileByMD5AndTenant(md5 string, tenantID string) (*FileInfo, error) {
|
||||
o := orm.NewOrm()
|
||||
var file FileInfo
|
||||
err := o.QueryTable("yz_files").Filter("md5", md5).Filter("tenant_id", tenantID).Filter("status", 1).OrderBy("upload_time").One(&file)
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &file, nil
|
||||
}
|
||||
|
||||
+35
-4
@@ -24,6 +24,8 @@ type User struct {
|
||||
Email string
|
||||
Avatar string
|
||||
Nickname string
|
||||
Status int `orm:"column(status);default(1)" json:"status"`
|
||||
Role int `orm:"column(role);default(0)" json:"role"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"`
|
||||
LastLoginTime *time.Time `orm:"column(last_login_time);null;type(datetime)" json:"last_login_time"`
|
||||
}
|
||||
@@ -147,6 +149,20 @@ func GetAllUsers(tenantId int) []*User {
|
||||
return users
|
||||
}
|
||||
|
||||
// GetTenantUsers 获取指定租户下的所有用户(排除已删除的用户)
|
||||
func GetTenantUsers(tenantId int) ([]*User, error) {
|
||||
o := orm.NewOrm()
|
||||
var users []*User
|
||||
|
||||
// 查询指定租户下未删除的用户
|
||||
_, err := o.Raw("SELECT * FROM yz_users WHERE tenant_id = ? AND delete_time IS NULL ORDER BY id DESC", tenantId).QueryRows(&users)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询租户用户失败: %v", err)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// GetUserInfo 根据用户ID或用户名获取用户
|
||||
func GetUserInfo(userId int, username string, tenantId int) (*User, error) {
|
||||
o := orm.NewOrm()
|
||||
@@ -221,7 +237,7 @@ func ValidateUser(username, password string, tenantName string) (*User, error) {
|
||||
}
|
||||
|
||||
// AddUser 向数据库添加新用户
|
||||
func AddUser(username, password, email, nickname, avatar string, tenantId int) (*User, error) {
|
||||
func AddUser(username, password, email, nickname, avatar string, tenantId, role int) (*User, error) {
|
||||
// 1. 验证租户是否存在且有效
|
||||
o := orm.NewOrm()
|
||||
var tenantExists bool
|
||||
@@ -258,11 +274,12 @@ func AddUser(username, password, email, nickname, avatar string, tenantId int) (
|
||||
user := &User{
|
||||
TenantId: tenantId,
|
||||
Username: username,
|
||||
Password: hashedPassword, // 存储加密后的密码
|
||||
Salt: salt, // 存储盐值(用于后续验证)
|
||||
Password: hashedPassword,
|
||||
Salt: salt,
|
||||
Email: email,
|
||||
Nickname: nickname,
|
||||
Avatar: avatar,
|
||||
Role: role, // 设置角色ID
|
||||
}
|
||||
|
||||
// 5. 插入数据库(使用之前定义的 o)
|
||||
@@ -276,7 +293,7 @@ func AddUser(username, password, email, nickname, avatar string, tenantId int) (
|
||||
}
|
||||
|
||||
// EditUser 更新用户信息
|
||||
func EditUser(id int, username, email, nickname, avatar string) (*User, error) {
|
||||
func EditUser(id int, username, email, nickname, avatar, status string, roleId int) (*User, error) {
|
||||
// 根据ID查询用户
|
||||
o := orm.NewOrm()
|
||||
user := &User{}
|
||||
@@ -307,6 +324,20 @@ func EditUser(id int, username, email, nickname, avatar string) (*User, error) {
|
||||
user.Avatar = avatar
|
||||
}
|
||||
|
||||
// 更新状态(将字符串转换为数字)
|
||||
if status != "" {
|
||||
if status == "active" {
|
||||
user.Status = 1
|
||||
} else if status == "inactive" {
|
||||
user.Status = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 更新角色ID
|
||||
if roleId > 0 {
|
||||
user.Role = roleId
|
||||
}
|
||||
|
||||
// 执行数据库更新
|
||||
_, err = o.Update(user)
|
||||
if err != nil {
|
||||
|
||||
Generated
+1640
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"vue-office": "^0.0.5"
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package routers
|
||||
import (
|
||||
"server/controllers"
|
||||
"server/middleware"
|
||||
"strings"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
@@ -44,6 +45,11 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否为公开预览接口(/api/files/public-preview/:id)
|
||||
if strings.HasPrefix(path, "/api/files/public-preview/") {
|
||||
skipAuth = true
|
||||
}
|
||||
|
||||
if !skipAuth {
|
||||
middleware.JWTAuthMiddleware()(ctx)
|
||||
}
|
||||
@@ -63,6 +69,7 @@ func init() {
|
||||
beego.Router("/api/deleteUser/:id", &controllers.UserController{}, "delete:DeleteUser")
|
||||
beego.Router("/api/changePassword/:id", &controllers.UserController{}, "post:ChangePassword")
|
||||
beego.Router("/api/reset-password", &controllers.UserController{}, "post:ResetPassword")
|
||||
beego.Router("/api/tenantUsers/:tenantId", &controllers.UserController{}, "get:GetTenantUsers")
|
||||
|
||||
// 认证路由
|
||||
beego.Router("/api/login", &controllers.AuthController{}, "post:Login")
|
||||
@@ -84,6 +91,9 @@ func init() {
|
||||
beego.Router("/api/files", &controllers.FileController{}, "get:GetAllFiles")
|
||||
beego.Router("/api/files", &controllers.FileController{}, "post:Post")
|
||||
beego.Router("/api/files/my", &controllers.FileController{}, "get:GetMyFiles")
|
||||
beego.Router("/api/files/download/:id", &controllers.FileController{}, "get:DownloadFile")
|
||||
beego.Router("/api/files/preview/:id", &controllers.FileController{}, "get:PreviewFile")
|
||||
beego.Router("/api/files/public-preview/:id", &controllers.FileController{}, "get:PublicPreviewFile")
|
||||
beego.Router("/api/files/:id", &controllers.FileController{}, "get:GetFileById")
|
||||
beego.Router("/api/files/tenant", &controllers.FileController{}, "get:GetFilesByTenant")
|
||||
beego.Router("/api/files/:id", &controllers.FileController{}, "put:UpdateFile")
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"runtime"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/beego/beego/v2/core/logs"
|
||||
|
||||
_ "server/routers"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func init() {
|
||||
_, file, _, _ := runtime.Caller(0)
|
||||
apppath, _ := filepath.Abs(filepath.Dir(filepath.Join(file, ".." + string(filepath.Separator))))
|
||||
beego.TestBeegoInit(apppath)
|
||||
}
|
||||
|
||||
|
||||
// TestBeego is a sample to run an endpoint test
|
||||
func TestBeego(t *testing.T) {
|
||||
r, _ := http.NewRequest("GET", "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
beego.BeeApp.Handlers.ServeHTTP(w, r)
|
||||
|
||||
logs.Trace("testing", "TestBeego", "Code[%d]\n%s", w.Code, w.Body.String())
|
||||
|
||||
Convey("Subject: Test Station Endpoint\n", t, func() {
|
||||
Convey("Status Code Should Be 200", func() {
|
||||
So(w.Code, ShouldEqual, 200)
|
||||
})
|
||||
Convey("The Result Should Not Be Empty", func() {
|
||||
So(w.Body.Len(), ShouldBeGreaterThan, 0)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"server/models"
|
||||
)
|
||||
|
||||
func TestFileModel(t *testing.T) {
|
||||
// 测试创建文件信息
|
||||
file := &models.FileInfo{
|
||||
TenantID: "test-tenant-001",
|
||||
FileName: "test-file.txt",
|
||||
OriginalName: "original-test-file.txt",
|
||||
FilePath: "/uploads/test/test-file.txt",
|
||||
FileURL: "http://localhost:8080/uploads/test/test-file.txt",
|
||||
FileSize: 1024,
|
||||
FileType: "text/plain",
|
||||
FileExt: "txt",
|
||||
Category: "test",
|
||||
SubCategory: "unit-test",
|
||||
Status: 1,
|
||||
IsPublic: 0,
|
||||
UploadBy: "test-user",
|
||||
}
|
||||
|
||||
// 测试添加文件
|
||||
id, err := models.AddFile(file)
|
||||
if err != nil {
|
||||
t.Errorf("添加文件失败: %v", err)
|
||||
}
|
||||
t.Logf("文件添加成功,ID: %d", id)
|
||||
|
||||
// 测试根据ID获取文件
|
||||
retrievedFile, err := models.GetFileById(id)
|
||||
if err != nil {
|
||||
t.Errorf("获取文件失败: %v", err)
|
||||
}
|
||||
if retrievedFile.FileName != file.FileName {
|
||||
t.Errorf("文件名不匹配,期望: %s, 实际: %s", file.FileName, retrievedFile.FileName)
|
||||
}
|
||||
|
||||
// 测试更新文件
|
||||
retrievedFile.FileName = "updated-test-file.txt"
|
||||
err = models.UpdateFile(retrievedFile)
|
||||
if err != nil {
|
||||
t.Errorf("更新文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 测试根据租户获取文件
|
||||
files, err := models.GetFilesByTenant("test-tenant-001")
|
||||
if err != nil {
|
||||
t.Errorf("根据租户获取文件失败: %v", err)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
t.Error("根据租户获取文件为空")
|
||||
}
|
||||
|
||||
// 测试根据分类获取文件
|
||||
filesByCategory, err := models.GetFilesByCategory("test")
|
||||
if err != nil {
|
||||
t.Errorf("根据分类获取文件失败: %v", err)
|
||||
}
|
||||
if len(filesByCategory) == 0 {
|
||||
t.Error("根据分类获取文件为空")
|
||||
}
|
||||
|
||||
// 测试搜索文件
|
||||
searchFiles, err := models.SearchFiles("test", "test-tenant-001")
|
||||
if err != nil {
|
||||
t.Errorf("搜索文件失败: %v", err)
|
||||
}
|
||||
if len(searchFiles) == 0 {
|
||||
t.Error("搜索文件结果为空")
|
||||
}
|
||||
|
||||
// 测试文件统计
|
||||
stats, err := models.GetFileStatistics("test-tenant-001")
|
||||
if err != nil {
|
||||
t.Errorf("获取文件统计失败: %v", err)
|
||||
}
|
||||
t.Logf("文件统计: %+v", stats)
|
||||
|
||||
// 测试软删除文件
|
||||
err = models.DeleteFile(id)
|
||||
if err != nil {
|
||||
t.Errorf("软删除文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证软删除后的状态
|
||||
deletedFile, err := models.GetFileById(id)
|
||||
if err != nil {
|
||||
t.Errorf("获取软删除后的文件失败: %v", err)
|
||||
}
|
||||
if deletedFile.Status != 0 {
|
||||
t.Errorf("软删除后文件状态应为0,实际为: %d", deletedFile.Status)
|
||||
}
|
||||
|
||||
// 测试硬删除文件
|
||||
err = models.HardDeleteFile(id)
|
||||
if err != nil {
|
||||
t.Errorf("硬删除文件失败: %v", err)
|
||||
}
|
||||
|
||||
t.Log("文件模型测试完成")
|
||||
}
|
||||
Reference in New Issue
Block a user