文件管理增加数据库表。
This commit is contained in:
+32
-27
@@ -30,35 +30,40 @@ type AdminMenuGroup struct {
|
||||
Items []AdminMenuItem `json:"items"`
|
||||
}
|
||||
|
||||
var adminMenus = []AdminMenuGroup{
|
||||
{Group: "", Items: []AdminMenuItem{
|
||||
{Key: "dashboard", Name: "仪表盘"},
|
||||
}},
|
||||
{Group: "内容管理", Items: []AdminMenuItem{
|
||||
{Key: "posts", Name: "文章管理"},
|
||||
{Key: "categories", Name: "分类管理"},
|
||||
{Key: "tags", Name: "标签管理"},
|
||||
{Key: "pages", Name: "页面管理"},
|
||||
{Key: "comments", Name: "评论管理"},
|
||||
}},
|
||||
{Group: "系统管理", Items: []AdminMenuItem{
|
||||
{Key: "users", Name: "用户管理"},
|
||||
{Key: "files", Name: "文件管理"},
|
||||
{Key: "themes", Name: "主题管理"},
|
||||
{Key: "settings", Name: "基础设置"},
|
||||
}},
|
||||
}
|
||||
var userMenus = []AdminMenuGroup{
|
||||
{Group: "", Items: []AdminMenuItem{
|
||||
{Key: "dashboard", Name: "仪表盘"},
|
||||
{Key: "posts", Name: "文章管理"},
|
||||
{Key: "files", Name: "文件管理"},
|
||||
{Key: "comments", Name: "评论管理"},
|
||||
}},
|
||||
}
|
||||
|
||||
// GetMenusByRole 根据角色返回后台菜单
|
||||
func GetMenusByRole(role string) []AdminMenuGroup {
|
||||
if role == "admin" {
|
||||
return []AdminMenuGroup{
|
||||
{Group: "", Items: []AdminMenuItem{
|
||||
{Key: "dashboard", Name: "仪表盘"},
|
||||
}},
|
||||
{Group: "内容管理", Items: []AdminMenuItem{
|
||||
{Key: "posts", Name: "文章管理"},
|
||||
{Key: "categories", Name: "分类管理"},
|
||||
{Key: "tags", Name: "标签管理"},
|
||||
{Key: "pages", Name: "页面管理"},
|
||||
{Key: "comments", Name: "评论管理"},
|
||||
}},
|
||||
{Group: "系统管理", Items: []AdminMenuItem{
|
||||
{Key: "users", Name: "用户管理"},
|
||||
{Key: "files", Name: "文件管理"},
|
||||
{Key: "themes", Name: "主题管理"},
|
||||
{Key: "settings", Name: "基础设置"},
|
||||
}},
|
||||
}
|
||||
}
|
||||
// 普通用户:仪表盘 + 文章管理 + 评论管理
|
||||
return []AdminMenuGroup{
|
||||
{Group: "", Items: []AdminMenuItem{
|
||||
{Key: "dashboard", Name: "仪表盘"},
|
||||
{Key: "posts", Name: "文章管理"},
|
||||
{Key: "comments", Name: "评论管理"},
|
||||
}},
|
||||
switch role {
|
||||
case "admin":
|
||||
return adminMenus
|
||||
default:
|
||||
return userMenus
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+90
-61
@@ -6,15 +6,17 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"goblog/database"
|
||||
"goblog/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
// uploadDir 上传文件保存目录
|
||||
// uploadDir 上传文件保存根目录
|
||||
uploadDir = "./static/uploads"
|
||||
// uploadURLPrefix 上传文件对外访问的 URL 前缀
|
||||
uploadURLPrefix = "/static/uploads/"
|
||||
@@ -39,14 +41,6 @@ var allowedImageMIMEs = map[string]bool{
|
||||
"image/webp": true,
|
||||
}
|
||||
|
||||
// UploadedFile 上传文件信息
|
||||
type UploadedFile struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Size int64 `json:"size"`
|
||||
ModTime string `json:"mod_time"`
|
||||
}
|
||||
|
||||
// sanitizeBaseName 清理文件名(去除扩展名和不安全字符)
|
||||
func sanitizeBaseName(filename string) string {
|
||||
base := strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
|
||||
@@ -70,7 +64,7 @@ func sanitizeBaseName(filename string) string {
|
||||
return res
|
||||
}
|
||||
|
||||
// UploadImage 上传图片(校验格式与大小)
|
||||
// UploadImage 上传图片(校验格式与大小,写入 files 表,按年月目录存储)
|
||||
func UploadImage(c *gin.Context) {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
@@ -115,15 +109,19 @@ func UploadImage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 确保上传目录存在
|
||||
if err := os.MkdirAll(uploadDir, 0o755); err != nil {
|
||||
// 按年月生成子目录:uploads/202608/
|
||||
now := time.Now()
|
||||
yearMonth := now.Format("200601")
|
||||
subDir := filepath.Join(uploadDir, yearMonth)
|
||||
|
||||
if err := os.MkdirAll(subDir, 0o755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建上传目录失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// 生成唯一文件名:时间戳_原始名.扩展名
|
||||
filename := fmt.Sprintf("%d_%s%s", time.Now().UnixNano(), sanitizeBaseName(file.Filename), ext)
|
||||
dstPath := filepath.Join(uploadDir, filename)
|
||||
filename := fmt.Sprintf("%d_%s%s", now.UnixNano(), sanitizeBaseName(file.Filename), ext)
|
||||
dstPath := filepath.Join(subDir, filename)
|
||||
|
||||
dst, err := os.Create(dstPath)
|
||||
if err != nil {
|
||||
@@ -137,79 +135,110 @@ func UploadImage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前用户 ID
|
||||
userID, _ := c.Get("userID")
|
||||
|
||||
// 相对路径(用于 URL 拼接和数据库存储)
|
||||
relPath := filepath.Join(yearMonth, filename)
|
||||
|
||||
// 写入 files 表
|
||||
fileRecord := models.File{
|
||||
UserID: userID.(uint),
|
||||
FileName: filename,
|
||||
OrigName: file.Filename,
|
||||
FilePath: relPath,
|
||||
FileSize: file.Size,
|
||||
MimeType: contentType,
|
||||
}
|
||||
if err := database.DB.Create(&fileRecord).Error; err != nil {
|
||||
// 文件已保存但数据库写入失败,记录日志但不阻断返回
|
||||
fmt.Printf("写入文件记录失败: %v\n", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"url": uploadURLPrefix + filename,
|
||||
"url": uploadURLPrefix + strings.ReplaceAll(relPath, "\\", "/"),
|
||||
"name": filename,
|
||||
"size": file.Size,
|
||||
})
|
||||
}
|
||||
|
||||
// ListUploads 列出已上传的图片
|
||||
// ListUploads 列出已上传的图片(从数据库读取,管理员看全部,普通用户看自己的)
|
||||
func ListUploads(c *gin.Context) {
|
||||
if err := os.MkdirAll(uploadDir, 0o755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取上传目录失败"})
|
||||
userID, _ := c.Get("userID")
|
||||
role, _ := c.Get("role")
|
||||
|
||||
var files []models.File
|
||||
query := database.DB.Order("created_at DESC")
|
||||
|
||||
if role != "admin" {
|
||||
query = query.Where("user_id = ?", userID)
|
||||
}
|
||||
|
||||
if err := query.Find(&files).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询文件列表失败"})
|
||||
return
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(uploadDir)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取上传目录失败"})
|
||||
return
|
||||
// 转换为前端需要的格式
|
||||
type FileItem struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
OrigName string `json:"orig_name"`
|
||||
URL string `json:"url"`
|
||||
Size int64 `json:"size"`
|
||||
MimeType string `json:"mime_type"`
|
||||
UserID uint `json:"user_id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
files := make([]UploadedFile, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(e.Name()))
|
||||
if !allowedImageExts[ext] {
|
||||
continue
|
||||
}
|
||||
info, err := e.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
files = append(files, UploadedFile{
|
||||
Name: e.Name(),
|
||||
URL: uploadURLPrefix + e.Name(),
|
||||
Size: info.Size(),
|
||||
ModTime: info.ModTime().Format("2006-01-02 15:04:05"),
|
||||
items := make([]FileItem, 0, len(files))
|
||||
for _, f := range files {
|
||||
items = append(items, FileItem{
|
||||
ID: f.ID,
|
||||
Name: f.FileName,
|
||||
OrigName: f.OrigName,
|
||||
URL: uploadURLPrefix + strings.ReplaceAll(f.FilePath, "\\", "/"),
|
||||
Size: f.FileSize,
|
||||
MimeType: f.MimeType,
|
||||
UserID: f.UserID,
|
||||
CreatedAt: f.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
|
||||
// 按修改时间倒序(最新的在前)
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
return files[i].ModTime > files[j].ModTime
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": files})
|
||||
c.JSON(http.StatusOK, gin.H{"data": items})
|
||||
}
|
||||
|
||||
// DeleteUpload 删除已上传的图片
|
||||
// DeleteUpload 删除已上传的图片(同时删除数据库记录和物理文件)
|
||||
func DeleteUpload(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
|
||||
// 防止路径穿越
|
||||
if name == "" || strings.Contains(name, "..") || strings.ContainsAny(name, `/\`) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "非法的文件名"})
|
||||
fileID := c.Param("id")
|
||||
if fileID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少文件ID"})
|
||||
return
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(name))
|
||||
if !allowedImageExts[ext] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "非法的文件类型"})
|
||||
var file models.File
|
||||
if err := database.DB.First(&file, fileID).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "文件记录不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
path := filepath.Join(uploadDir, name)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在"})
|
||||
// 权限校验:管理员可删所有,普通用户只能删自己的
|
||||
userID, _ := c.Get("userID")
|
||||
role, _ := c.Get("role")
|
||||
if role != "admin" && file.UserID != userID.(uint) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权删除此文件"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Remove(path); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除文件失败"})
|
||||
// 删除物理文件
|
||||
physPath := filepath.Join(uploadDir, file.FilePath)
|
||||
if _, err := os.Stat(physPath); err == nil {
|
||||
os.Remove(physPath)
|
||||
}
|
||||
|
||||
// 删除数据库记录
|
||||
if err := database.DB.Delete(&file).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除文件记录失败"})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user