feat: 角色创建与赋权、文件管理单页多级目录与上传可下载、api上传目录可写卷
Made-with: Cursor
This commit is contained in:
@@ -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(当前目录相对路径)、downloadable(true/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)})
|
||||
}
|
||||
|
||||
@@ -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": "删除成功"})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user