文件管理增加数据库表。

This commit is contained in:
2026-08-18 17:29:06 +08:00
parent f1780de632
commit f1a6c5abaa
6 changed files with 157 additions and 98 deletions
+32 -27
View File
@@ -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
View File
@@ -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
}