feat: 角色创建与赋权、文件管理单页多级目录与上传可下载、api上传目录可写卷

Made-with: Cursor
This commit is contained in:
whm
2026-03-18 18:26:08 +08:00
parent 07f55e0139
commit 7a97ba8c66
11 changed files with 586 additions and 80 deletions

View File

@@ -5,6 +5,9 @@ import (
"net/http"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
@@ -18,20 +21,35 @@ import (
const uploadDir = "uploads"
// ListSiteAssets 站点功能模块/上传文件列表
// pathPrefix 站点下相对路径前缀,用于多级目录
func pathPrefix(siteID string) string {
return "sites/" + siteID + "/"
}
// ListSiteAssets 站点功能模块/上传文件列表query path 为当前目录相对路径(空为根)
func ListSiteAssets(c *gin.Context) {
siteID := c.Param("site_id")
if siteID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
return
}
path := c.Query("path")
prefix := pathPrefix(siteID)
if path != "" {
prefix = prefix + path
if prefix[len(prefix)-1] != '/' {
prefix += "/"
}
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("site_assets")
// 仅当前目录下直接文件file_path 为 prefix + 不含 / 的文件名)
filter := bson.M{"site_id": siteID, "file_path": bson.M{"$regex": "^" + regexp.QuoteMeta(prefix) + "[^/]+$"}}
opts := options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}})
cursor, err := coll.Find(ctx, bson.M{"site_id": siteID}, opts)
cursor, err := coll.Find(ctx, filter, opts)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
@@ -43,11 +61,60 @@ func ListSiteAssets(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
total, _ := coll.CountDocuments(ctx, bson.M{"site_id": siteID})
c.JSON(http.StatusOK, gin.H{"list": list, "total": total})
total, _ := coll.CountDocuments(ctx, filter)
// 子目录列表:从 file_path 中提取当前 path 下的一级子目录名
subDirs := listSubDirs(c, siteID, path)
c.JSON(http.StatusOK, gin.H{"list": list, "total": total, "sub_dirs": subDirs})
}
// UploadSiteAsset 上传功能模块/文件
func listSubDirs(c *gin.Context, siteID, currentPath string) []string {
prefix := pathPrefix(siteID)
if currentPath != "" {
prefix = prefix + currentPath
if prefix[len(prefix)-1] != '/' {
prefix += "/"
}
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("site_assets")
cursor, err := coll.Find(ctx, bson.M{"site_id": siteID, "file_path": bson.M{"$regex": "^" + regexp.QuoteMeta(prefix)}})
if err != nil {
return nil
}
defer cursor.Close(ctx)
var docs []struct {
FilePath string `bson:"file_path"`
}
_ = cursor.All(ctx, &docs)
seen := make(map[string]bool)
for _, d := range docs {
rel := strings.TrimPrefix(d.FilePath, prefix)
if rel == "" || rel == d.FilePath {
continue
}
parts := strings.SplitN(rel, "/", 2)
if len(parts) > 0 && parts[0] != "" {
seen[parts[0]] = true
}
}
// 再扫描物理目录
baseDir := filepath.Join(uploadDir, filepath.FromSlash(prefix))
entries, _ := os.ReadDir(baseDir)
for _, e := range entries {
if e.IsDir() {
seen[e.Name()] = true
}
}
names := make([]string, 0, len(seen))
for n := range seen {
names = append(names, n)
}
sort.Strings(names)
return names
}
// UploadSiteAsset 上传功能模块/文件form 可选folder当前目录相对路径、downloadabletrue/false
func UploadSiteAsset(c *gin.Context) {
siteID := c.Param("site_id")
if siteID == "" {
@@ -61,19 +128,21 @@ func UploadSiteAsset(c *gin.Context) {
return
}
baseDir := filepath.Join(uploadDir, "sites", siteID)
folder := c.PostForm("folder")
downloadable := c.PostForm("downloadable") == "true" || c.PostForm("downloadable") == "1"
baseDir := filepath.Join(uploadDir, "sites", siteID, filepath.Clean(folder))
if err := os.MkdirAll(baseDir, 0755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建目录失败"})
return
}
// 避免覆盖:加时间戳
name := file.Filename
ext := filepath.Ext(name)
nameNoExt := name[:len(name)-len(ext)]
saveName := nameNoExt + "_" + time.Now().Format("20060102150405") + ext
relPath := filepath.Join("sites", siteID, saveName)
destPath := filepath.Join(uploadDir, relPath)
relPath := filepath.Join("sites", siteID, filepath.Clean(folder), saveName)
relPath = filepath.ToSlash(relPath)
destPath := filepath.Join(uploadDir, filepath.FromSlash(relPath))
if err := c.SaveUploadedFile(file, destPath); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"})
@@ -84,12 +153,13 @@ func UploadSiteAsset(c *gin.Context) {
defer cancel()
doc := models.SiteAsset{
SiteID: siteID,
Name: file.Filename,
FilePath: relPath,
Size: file.Size,
ContentType: file.Header.Get("Content-Type"),
CreatedAt: time.Now().Format(time.RFC3339),
SiteID: siteID,
Name: file.Filename,
FilePath: relPath,
Size: file.Size,
ContentType: file.Header.Get("Content-Type"),
Downloadable: downloadable,
CreatedAt: time.Now().Format(time.RFC3339),
}
res, err := config.GetDB(config.DBName).Collection("site_assets").InsertOne(ctx, bson.M{
"site_id": doc.SiteID,
@@ -97,6 +167,7 @@ func UploadSiteAsset(c *gin.Context) {
"file_path": doc.FilePath,
"size": doc.Size,
"content_type": doc.ContentType,
"downloadable": doc.Downloadable,
"created_at": doc.CreatedAt,
})
if err != nil {
@@ -133,7 +204,7 @@ func DeleteSiteAsset(c *gin.Context) {
return
}
fullPath := filepath.Join(uploadDir, asset.FilePath)
fullPath := filepath.Join(uploadDir, filepath.FromSlash(asset.FilePath))
os.Remove(fullPath)
_, err = coll.DeleteOne(ctx, bson.M{"_id": oid})
@@ -143,3 +214,33 @@ func DeleteSiteAsset(c *gin.Context) {
}
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}
// CreateSiteFolderInput 创建目录
type CreateSiteFolderInput struct {
Path string `json:"path" binding:"required"`
}
// CreateSiteFolder 在站点下创建多级目录
func CreateSiteFolder(c *gin.Context) {
siteID := c.Param("site_id")
if siteID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
return
}
var input CreateSiteFolderInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写目录路径"})
return
}
clean := filepath.Clean(input.Path)
if clean == "." || clean == ".." || strings.HasPrefix(clean, "..") {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的目录路径"})
return
}
baseDir := filepath.Join(uploadDir, "sites", siteID, clean)
if err := os.MkdirAll(baseDir, 0755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建目录失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "创建成功", "path": filepath.ToSlash(clean)})
}

View File

@@ -15,17 +15,9 @@ import (
"github.com/gin-gonic/gin"
)
// 定义角色(与 users.role_id 对应)
var roleMeta = []struct {
RoleID int `json:"role_id"`
RoleName string `json:"role_name"`
}{
{models.RoleIDSuperAdmin, "超级管理员"},
{models.RoleIDSuperUser, "超级用户"},
{models.RoleIDUser, "普通用户"},
}
const customRoleIDStart = 1000 // 定义角色 role_id 从此值起
// GetRolePermissionsList 返回所有角色及其权限(用于角色权限管理页
// GetRolePermissionsList 返回所有角色及其权限(含预定义与自定义
func GetRolePermissionsList(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
@@ -44,23 +36,56 @@ func GetRolePermissionsList(c *gin.Context) {
return
}
permMap := make(map[int][]string)
nameMap := make(map[int]string)
for _, d := range docs {
permMap[d.RoleID] = d.Permissions
if d.RoleName != "" {
nameMap[d.RoleID] = d.RoleName
}
}
list := make([]gin.H, 0, len(roleMeta))
for _, r := range roleMeta {
perms := permMap[r.RoleID]
allKeys := allPermissionKeys()
// 预定义角色固定在前9527, 0, 1再按 role_id 排自定义
predef := []int{models.RoleIDSuperAdmin, models.RoleIDSuperUser, models.RoleIDUser}
seen := make(map[int]bool)
list := make([]gin.H, 0)
for _, rid := range predef {
seen[rid] = true
perms := permMap[rid]
if perms == nil {
perms = []string{}
}
if r.RoleID == models.RoleIDSuperAdmin {
perms = allPermissionKeys()
if rid == models.RoleIDSuperAdmin {
perms = allKeys
}
name := nameMap[rid]
if name == "" {
name = models.DefaultRoleNames[rid]
}
list = append(list, gin.H{
"role_id": r.RoleID,
"role_name": r.RoleName,
"role_id": rid,
"role_name": name,
"permissions": perms,
"is_custom": false,
})
}
for _, d := range docs {
if seen[d.RoleID] {
continue
}
seen[d.RoleID] = true
name := d.RoleName
if name == "" {
name = "角色" + strconv.Itoa(d.RoleID)
}
perms := d.Permissions
if perms == nil {
perms = []string{}
}
list = append(list, gin.H{
"role_id": d.RoleID,
"role_name": name,
"permissions": perms,
"is_custom": true,
})
}
c.JSON(http.StatusOK, gin.H{
@@ -69,11 +94,6 @@ func GetRolePermissionsList(c *gin.Context) {
})
}
// UpdateRolePermissionsInput 更新某角色权限
type UpdateRolePermissionsInput struct {
Permissions []string `json:"permissions"`
}
// UpdateRolePermissions 更新指定角色的权限
func UpdateRolePermissions(c *gin.Context) {
roleIDStr := c.Param("role_id")
@@ -87,7 +107,10 @@ func UpdateRolePermissions(c *gin.Context) {
return
}
var input UpdateRolePermissionsInput
var input struct {
RoleName string `json:"role_name"`
Permissions []string `json:"permissions"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
@@ -98,7 +121,11 @@ func UpdateRolePermissions(c *gin.Context) {
coll := config.GetDB(config.DBName).Collection("role_permissions")
filter := bson.M{"role_id": roleID}
update := bson.M{"$set": bson.M{"role_id": roleID, "permissions": input.Permissions}}
set := bson.M{"role_id": roleID, "permissions": input.Permissions}
if input.RoleName != "" && roleID >= customRoleIDStart {
set["role_name"] = input.RoleName
}
update := bson.M{"$set": set}
opts := options.UpdateOne().SetUpsert(true)
_, err = coll.UpdateOne(ctx, filter, update, opts)
if err != nil {
@@ -107,3 +134,72 @@ func UpdateRolePermissions(c *gin.Context) {
}
c.JSON(http.StatusOK, gin.H{"message": "保存成功", "role_id": roleID, "permissions": input.Permissions})
}
// CreateRoleInput 创建角色
type CreateRoleInput struct {
RoleName string `json:"role_name" binding:"required"`
Permissions []string `json:"permissions"`
}
// CreateRole 创建自定义角色
func CreateRole(c *gin.Context) {
var input CreateRoleInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写角色名称"})
return
}
if input.Permissions == nil {
input.Permissions = []string{}
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("role_permissions")
cursor, _ := coll.Find(ctx, bson.M{"role_id": bson.M{"$gte": customRoleIDStart}}, options.Find().SetSort(bson.D{{Key: "role_id", Value: -1}}).SetLimit(1))
var docs []models.RolePermissionsDoc
_ = cursor.All(ctx, &docs)
cursor.Close(ctx)
nextID := customRoleIDStart
for _, d := range docs {
if d.RoleID >= customRoleIDStart {
nextID = d.RoleID + 1
break
}
}
doc := models.RolePermissionsDoc{
RoleID: nextID,
RoleName: input.RoleName,
Permissions: input.Permissions,
}
_, err := coll.InsertOne(ctx, bson.M{"role_id": doc.RoleID, "role_name": doc.RoleName, "permissions": doc.Permissions})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "创建成功", "role_id": doc.RoleID, "role_name": doc.RoleName, "permissions": doc.Permissions})
}
// DeleteRole 删除自定义角色(仅 role_id >= customRoleIDStart
func DeleteRole(c *gin.Context) {
roleIDStr := c.Param("role_id")
roleID, err := strconv.Atoi(roleIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 role_id"})
return
}
if roleID < customRoleIDStart {
c.JSON(http.StatusBadRequest, gin.H{"error": "预定义角色不可删除"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("role_permissions")
_, err = coll.DeleteOne(ctx, bson.M{"role_id": roleID})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}