🎉 初始化项目
This commit is contained in:
512
server/service/system/auto_code_plugin.go
Normal file
512
server/service/system/auto_code_plugin.go
Normal file
@@ -0,0 +1,512 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
goast "go/ast"
|
||||
"go/parser"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.echol.cn/loser/st/server/global"
|
||||
"git.echol.cn/loser/st/server/model/system"
|
||||
"git.echol.cn/loser/st/server/model/system/request"
|
||||
pluginUtils "git.echol.cn/loser/st/server/plugin/plugin-tool/utils"
|
||||
"git.echol.cn/loser/st/server/utils"
|
||||
ast "git.echol.cn/loser/st/server/utils/ast"
|
||||
"github.com/mholt/archives"
|
||||
cp "github.com/otiai10/copy"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var AutoCodePlugin = new(autoCodePlugin)
|
||||
|
||||
type autoCodePlugin struct{}
|
||||
|
||||
// Install 插件安装
|
||||
func (s *autoCodePlugin) Install(file *multipart.FileHeader) (web, server int, err error) {
|
||||
const GVAPLUGPINATH = "./gva-plug-temp/"
|
||||
defer os.RemoveAll(GVAPLUGPINATH)
|
||||
_, err = os.Stat(GVAPLUGPINATH)
|
||||
if os.IsNotExist(err) {
|
||||
os.Mkdir(GVAPLUGPINATH, os.ModePerm)
|
||||
}
|
||||
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return -1, -1, err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 在临时目录创建目标文件
|
||||
// 使用完整路径拼接的好处:明确文件位置,避免路径混乱
|
||||
out, err := os.Create(GVAPLUGPINATH + file.Filename)
|
||||
if err != nil {
|
||||
return -1, -1, err
|
||||
}
|
||||
|
||||
// 将上传的文件内容复制到临时文件
|
||||
// 使用io.Copy的好处:高效处理大文件,自动管理缓冲区,避免内存溢出
|
||||
_, err = io.Copy(out, src)
|
||||
if err != nil {
|
||||
out.Close()
|
||||
return -1, -1, err
|
||||
}
|
||||
|
||||
// 立即关闭文件,确保数据写入磁盘并释放文件句柄
|
||||
// 必须在解压前关闭,否则在Windows系统上会导致文件被占用无法解压
|
||||
err = out.Close()
|
||||
if err != nil {
|
||||
return -1, -1, err
|
||||
}
|
||||
|
||||
paths, err := utils.Unzip(GVAPLUGPINATH+file.Filename, GVAPLUGPINATH)
|
||||
paths = filterFile(paths)
|
||||
var webIndex = -1
|
||||
var serverIndex = -1
|
||||
webPlugin := ""
|
||||
serverPlugin := ""
|
||||
serverPackage := ""
|
||||
serverRootName := ""
|
||||
|
||||
for i := range paths {
|
||||
paths[i] = filepath.ToSlash(paths[i])
|
||||
pathArr := strings.Split(paths[i], "/")
|
||||
ln := len(pathArr)
|
||||
|
||||
if ln < 4 {
|
||||
continue
|
||||
}
|
||||
if pathArr[2]+"/"+pathArr[3] == `server/plugin` {
|
||||
if len(serverPlugin) == 0 {
|
||||
serverPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3])
|
||||
}
|
||||
if serverRootName == "" && ln > 1 && pathArr[1] != "" {
|
||||
serverRootName = pathArr[1]
|
||||
}
|
||||
if ln > 4 && serverPackage == "" && pathArr[4] != "" {
|
||||
serverPackage = pathArr[4]
|
||||
}
|
||||
}
|
||||
if pathArr[2]+"/"+pathArr[3] == `web/plugin` && len(webPlugin) == 0 {
|
||||
webPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3])
|
||||
}
|
||||
}
|
||||
if len(serverPlugin) == 0 && len(webPlugin) == 0 {
|
||||
zap.L().Error("非标准插件,请按照文档自动迁移使用")
|
||||
return webIndex, serverIndex, errors.New("非标准插件,请按照文档自动迁移使用")
|
||||
}
|
||||
|
||||
if len(serverPlugin) != 0 {
|
||||
if serverPackage == "" {
|
||||
serverPackage = serverRootName
|
||||
}
|
||||
err = installation(serverPlugin, global.GVA_CONFIG.AutoCode.Server, global.GVA_CONFIG.AutoCode.Server)
|
||||
if err != nil {
|
||||
return webIndex, serverIndex, err
|
||||
}
|
||||
err = ensurePluginRegisterImport(serverPackage)
|
||||
if err != nil {
|
||||
return webIndex, serverIndex, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(webPlugin) != 0 {
|
||||
err = installation(webPlugin, global.GVA_CONFIG.AutoCode.Server, global.GVA_CONFIG.AutoCode.Web)
|
||||
if err != nil {
|
||||
return webIndex, serverIndex, err
|
||||
}
|
||||
}
|
||||
|
||||
return 1, 1, err
|
||||
}
|
||||
|
||||
func installation(path string, formPath string, toPath string) error {
|
||||
arr := strings.Split(filepath.ToSlash(path), "/")
|
||||
ln := len(arr)
|
||||
if ln < 3 {
|
||||
return errors.New("arr")
|
||||
}
|
||||
name := arr[ln-3]
|
||||
|
||||
var form = filepath.Join(global.GVA_CONFIG.AutoCode.Root, formPath, path)
|
||||
var to = filepath.Join(global.GVA_CONFIG.AutoCode.Root, toPath, "plugin")
|
||||
_, err := os.Stat(to + name)
|
||||
if err == nil {
|
||||
zap.L().Error("autoPath 已存在同名插件,请自行手动安装", zap.String("to", to))
|
||||
return errors.New(toPath + "已存在同名插件,请自行手动安装")
|
||||
}
|
||||
return cp.Copy(form, to, cp.Options{Skip: skipMacSpecialDocument})
|
||||
}
|
||||
|
||||
func ensurePluginRegisterImport(packageName string) error {
|
||||
module := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Module)
|
||||
if module == "" {
|
||||
return errors.New("autocode module is empty")
|
||||
}
|
||||
if packageName == "" {
|
||||
return errors.New("plugin package is empty")
|
||||
}
|
||||
|
||||
registerPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go")
|
||||
src, err := os.ReadFile(registerPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileSet := token.NewFileSet()
|
||||
astFile, err := parser.ParseFile(fileSet, registerPath, src, parser.ParseComments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
importPath := fmt.Sprintf("%s/plugin/%s", module, packageName)
|
||||
if ast.CheckImport(astFile, importPath) {
|
||||
return nil
|
||||
}
|
||||
|
||||
importSpec := &goast.ImportSpec{
|
||||
Name: goast.NewIdent("_"),
|
||||
Path: &goast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", importPath)},
|
||||
}
|
||||
var importDecl *goast.GenDecl
|
||||
for _, decl := range astFile.Decls {
|
||||
genDecl, ok := decl.(*goast.GenDecl)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if genDecl.Tok == token.IMPORT {
|
||||
importDecl = genDecl
|
||||
break
|
||||
}
|
||||
}
|
||||
if importDecl == nil {
|
||||
astFile.Decls = append([]goast.Decl{
|
||||
&goast.GenDecl{
|
||||
Tok: token.IMPORT,
|
||||
Specs: []goast.Spec{importSpec},
|
||||
},
|
||||
}, astFile.Decls...)
|
||||
} else {
|
||||
importDecl.Specs = append(importDecl.Specs, importSpec)
|
||||
}
|
||||
|
||||
var out []byte
|
||||
bf := bytes.NewBuffer(out)
|
||||
printer.Fprint(bf, fileSet, astFile)
|
||||
|
||||
return os.WriteFile(registerPath, bf.Bytes(), 0666)
|
||||
}
|
||||
|
||||
func filterFile(paths []string) []string {
|
||||
np := make([]string, 0, len(paths))
|
||||
for _, path := range paths {
|
||||
if ok, _ := skipMacSpecialDocument(nil, path, ""); ok {
|
||||
continue
|
||||
}
|
||||
np = append(np, path)
|
||||
}
|
||||
return np
|
||||
}
|
||||
|
||||
func skipMacSpecialDocument(_ os.FileInfo, src, _ string) (bool, error) {
|
||||
if strings.Contains(src, ".DS_Store") || strings.Contains(src, "__MACOSX") {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (s *autoCodePlugin) PubPlug(plugName string) (zipPath string, err error) {
|
||||
if plugName == "" {
|
||||
return "", errors.New("插件名称不能为空")
|
||||
}
|
||||
|
||||
// 防止路径穿越
|
||||
plugName = filepath.Clean(plugName)
|
||||
|
||||
webPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "plugin", plugName)
|
||||
serverPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", plugName)
|
||||
// 创建一个新的zip文件
|
||||
|
||||
// 判断目录是否存在
|
||||
_, err = os.Stat(webPath)
|
||||
if err != nil {
|
||||
return "", errors.New("web路径不存在")
|
||||
}
|
||||
_, err = os.Stat(serverPath)
|
||||
if err != nil {
|
||||
return "", errors.New("server路径不存在")
|
||||
}
|
||||
|
||||
fileName := plugName + ".zip"
|
||||
// 创建一个新的zip文件
|
||||
files, err := archives.FilesFromDisk(context.Background(), nil, map[string]string{
|
||||
webPath: plugName + "/web/plugin/" + plugName,
|
||||
serverPath: plugName + "/server/plugin/" + plugName,
|
||||
})
|
||||
|
||||
// create the output file we'll write to
|
||||
out, err := os.Create(fileName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// we can use the CompressedArchive type to gzip a tarball
|
||||
// (compression is not required; you could use Tar directly)
|
||||
format := archives.CompressedArchive{
|
||||
//Compression: archives.Gz{},
|
||||
Archival: archives.Zip{},
|
||||
}
|
||||
|
||||
// create the archive
|
||||
err = format.Archive(context.Background(), out, files)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, fileName), nil
|
||||
}
|
||||
|
||||
func (s *autoCodePlugin) InitMenu(menuInfo request.InitMenu) (err error) {
|
||||
menuPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", menuInfo.PlugName, "initialize", "menu.go")
|
||||
src, err := os.ReadFile(menuPath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
fileSet := token.NewFileSet()
|
||||
astFile, err := parser.ParseFile(fileSet, "", src, 0)
|
||||
arrayAst := ast.FindArray(astFile, "model", "SysBaseMenu")
|
||||
var menus []system.SysBaseMenu
|
||||
|
||||
parentMenu := []system.SysBaseMenu{
|
||||
{
|
||||
ParentId: 0,
|
||||
Path: menuInfo.PlugName + "Menu",
|
||||
Name: menuInfo.PlugName + "Menu",
|
||||
Hidden: false,
|
||||
Component: "view/routerHolder.vue",
|
||||
Sort: 0,
|
||||
Meta: system.Meta{
|
||||
Title: menuInfo.ParentMenu,
|
||||
Icon: "school",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 查询菜单及其关联的参数和按钮
|
||||
err = global.GVA_DB.Preload("Parameters").Preload("MenuBtn").Find(&menus, "id in (?)", menuInfo.Menus).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
menus = append(parentMenu, menus...)
|
||||
menuExpr := ast.CreateMenuStructAst(menus)
|
||||
arrayAst.Elts = *menuExpr
|
||||
|
||||
var out []byte
|
||||
bf := bytes.NewBuffer(out)
|
||||
printer.Fprint(bf, fileSet, astFile)
|
||||
|
||||
os.WriteFile(menuPath, bf.Bytes(), 0666)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *autoCodePlugin) InitAPI(apiInfo request.InitApi) (err error) {
|
||||
apiPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", apiInfo.PlugName, "initialize", "api.go")
|
||||
src, err := os.ReadFile(apiPath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
fileSet := token.NewFileSet()
|
||||
astFile, err := parser.ParseFile(fileSet, "", src, 0)
|
||||
arrayAst := ast.FindArray(astFile, "model", "SysApi")
|
||||
var apis []system.SysApi
|
||||
err = global.GVA_DB.Find(&apis, "id in (?)", apiInfo.APIs).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apisExpr := ast.CreateApiStructAst(apis)
|
||||
arrayAst.Elts = *apisExpr
|
||||
|
||||
var out []byte
|
||||
bf := bytes.NewBuffer(out)
|
||||
printer.Fprint(bf, fileSet, astFile)
|
||||
|
||||
os.WriteFile(apiPath, bf.Bytes(), 0666)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *autoCodePlugin) InitDictionary(dictInfo request.InitDictionary) (err error) {
|
||||
dictPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", dictInfo.PlugName, "initialize", "dictionary.go")
|
||||
src, err := os.ReadFile(dictPath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
fileSet := token.NewFileSet()
|
||||
astFile, err := parser.ParseFile(fileSet, "", src, 0)
|
||||
arrayAst := ast.FindArray(astFile, "model", "SysDictionary")
|
||||
var dictionaries []system.SysDictionary
|
||||
err = global.GVA_DB.Preload("SysDictionaryDetails").Find(&dictionaries, "id in (?)", dictInfo.Dictionaries).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dictExpr := ast.CreateDictionaryStructAst(dictionaries)
|
||||
arrayAst.Elts = *dictExpr
|
||||
|
||||
var out []byte
|
||||
bf := bytes.NewBuffer(out)
|
||||
printer.Fprint(bf, fileSet, astFile)
|
||||
|
||||
os.WriteFile(dictPath, bf.Bytes(), 0666)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *autoCodePlugin) Remove(pluginName string, pluginType string) (err error) {
|
||||
// 1. 删除前端代码
|
||||
if pluginType == "web" || pluginType == "full" {
|
||||
webDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "plugin", pluginName)
|
||||
err = os.RemoveAll(webDir)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "删除前端插件目录失败")
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 删除后端代码
|
||||
if pluginType == "server" || pluginType == "full" {
|
||||
serverDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", pluginName)
|
||||
err = os.RemoveAll(serverDir)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "删除后端插件目录失败")
|
||||
}
|
||||
|
||||
// 移除注册
|
||||
removePluginRegisterImport(pluginName)
|
||||
}
|
||||
|
||||
// 通过utils 获取 api 菜单 字典
|
||||
apis, menus, dicts := pluginUtils.GetPluginData(pluginName)
|
||||
|
||||
// 3. 删除菜单 (递归删除)
|
||||
if len(menus) > 0 {
|
||||
for _, menu := range menus {
|
||||
var dbMenu system.SysBaseMenu
|
||||
if err := global.GVA_DB.Where("name = ?", menu.Name).First(&dbMenu).Error; err == nil {
|
||||
// 获取该菜单及其所有子菜单的ID
|
||||
var menuIds []int
|
||||
GetMenuIds(dbMenu, &menuIds)
|
||||
// 逆序删除,先删除子菜单
|
||||
for i := len(menuIds) - 1; i >= 0; i-- {
|
||||
err := BaseMenuServiceApp.DeleteBaseMenu(menuIds[i])
|
||||
if err != nil {
|
||||
zap.L().Error("删除菜单失败", zap.Int("id", menuIds[i]), zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 删除API
|
||||
if len(apis) > 0 {
|
||||
for _, api := range apis {
|
||||
var dbApi system.SysApi
|
||||
if err := global.GVA_DB.Where("path = ? AND method = ?", api.Path, api.Method).First(&dbApi).Error; err == nil {
|
||||
err := ApiServiceApp.DeleteApi(dbApi)
|
||||
if err != nil {
|
||||
zap.L().Error("删除API失败", zap.String("path", api.Path), zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 删除字典
|
||||
if len(dicts) > 0 {
|
||||
for _, dict := range dicts {
|
||||
var dbDict system.SysDictionary
|
||||
if err := global.GVA_DB.Where("type = ?", dict.Type).First(&dbDict).Error; err == nil {
|
||||
err := DictionaryServiceApp.DeleteSysDictionary(dbDict)
|
||||
if err != nil {
|
||||
zap.L().Error("删除字典失败", zap.String("type", dict.Type), zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetMenuIds(menu system.SysBaseMenu, ids *[]int) {
|
||||
*ids = append(*ids, int(menu.ID))
|
||||
var children []system.SysBaseMenu
|
||||
global.GVA_DB.Where("parent_id = ?", menu.ID).Find(&children)
|
||||
for _, child := range children {
|
||||
// 先递归收集子菜单
|
||||
GetMenuIds(child, ids)
|
||||
}
|
||||
}
|
||||
|
||||
func removePluginRegisterImport(packageName string) error {
|
||||
module := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Module)
|
||||
if module == "" {
|
||||
return errors.New("autocode module is empty")
|
||||
}
|
||||
if packageName == "" {
|
||||
return errors.New("plugin package is empty")
|
||||
}
|
||||
|
||||
registerPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go")
|
||||
src, err := os.ReadFile(registerPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileSet := token.NewFileSet()
|
||||
astFile, err := parser.ParseFile(fileSet, registerPath, src, parser.ParseComments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
importPath := fmt.Sprintf("%s/plugin/%s", module, packageName)
|
||||
importLit := fmt.Sprintf("%q", importPath)
|
||||
|
||||
// 移除 import
|
||||
var newDecls []goast.Decl
|
||||
for _, decl := range astFile.Decls {
|
||||
genDecl, ok := decl.(*goast.GenDecl)
|
||||
if !ok {
|
||||
newDecls = append(newDecls, decl)
|
||||
continue
|
||||
}
|
||||
if genDecl.Tok == token.IMPORT {
|
||||
var newSpecs []goast.Spec
|
||||
for _, spec := range genDecl.Specs {
|
||||
importSpec, ok := spec.(*goast.ImportSpec)
|
||||
if !ok {
|
||||
newSpecs = append(newSpecs, spec)
|
||||
continue
|
||||
}
|
||||
if importSpec.Path.Value != importLit {
|
||||
newSpecs = append(newSpecs, spec)
|
||||
}
|
||||
}
|
||||
// 如果还有其他import,保留该 decl
|
||||
if len(newSpecs) > 0 {
|
||||
genDecl.Specs = newSpecs
|
||||
newDecls = append(newDecls, genDecl)
|
||||
}
|
||||
} else {
|
||||
newDecls = append(newDecls, decl)
|
||||
}
|
||||
}
|
||||
astFile.Decls = newDecls
|
||||
|
||||
var out []byte
|
||||
bf := bytes.NewBuffer(out)
|
||||
printer.Fprint(bf, fileSet, astFile)
|
||||
|
||||
return os.WriteFile(registerPath, bf.Bytes(), 0666)
|
||||
}
|
||||
Reference in New Issue
Block a user