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

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{}{
+5 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+1640
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
{
"dependencies": {
"vue-office": "^0.0.5"
}
}
+10
View File
@@ -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")
-42
View File
@@ -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)
})
})
}
-105
View File
@@ -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("文件模型测试完成")
}