mirror of
https://github.com/bufanyun/hotgo.git
synced 2025-11-13 20:53:49 +08:00
发布v2.2.10版本,更新内容请查看:https://github.com/bufanyun/hotgo/tree/v2.0/docs/guide-zh-CN/addon-version-upgrade.md
This commit is contained in:
38
server/internal/library/addons/addons.go
Normal file
38
server/internal/library/addons/addons.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// Package addons
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package addons
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"hotgo/internal/consts"
|
||||
)
|
||||
|
||||
func GetTag(name string) string {
|
||||
return consts.AddonsTag + name
|
||||
}
|
||||
|
||||
func Tpl(name, tpl string) string {
|
||||
return consts.AddonsDir + "/" + name + "/" + tpl
|
||||
}
|
||||
|
||||
// RouterPrefix 路由前缀
|
||||
// 最终效果:/应用名称/插件模块名称/xxx/xxx。如果你不喜欢现在的路由风格,可以自行调整
|
||||
func RouterPrefix(ctx context.Context, app, name string) string {
|
||||
var prefix = "/"
|
||||
switch app {
|
||||
case consts.AppAdmin:
|
||||
prefix = g.Cfg().MustGet(ctx, "router.admin.prefix", "/admin").String()
|
||||
case consts.AppApi:
|
||||
prefix = g.Cfg().MustGet(ctx, "router.api.prefix", "/api").String()
|
||||
case consts.AppHome:
|
||||
prefix = g.Cfg().MustGet(ctx, "router.home.prefix", "/home").String()
|
||||
case consts.AppWebSocket:
|
||||
prefix = g.Cfg().MustGet(ctx, "router.ws.prefix", "/socket").String()
|
||||
}
|
||||
|
||||
return prefix + "/" + name
|
||||
}
|
||||
126
server/internal/library/addons/build.go
Normal file
126
server/internal/library/addons/build.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package addons
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/model"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Build 构建新插件
|
||||
func Build(ctx context.Context, sk Skeleton, conf *model.BuildAddonConfig) (err error) {
|
||||
buildPath := "./" + consts.AddonsDir + "/" + sk.Name
|
||||
modulesPath := "./" + consts.AddonsDir + "/modules/" + sk.Name + ".go"
|
||||
templatePath := gstr.Replace(conf.TemplatePath, "{$name}", sk.Name)
|
||||
replaces := map[string]string{
|
||||
"@{.label}": sk.Label,
|
||||
"@{.name}": sk.Name,
|
||||
"@{.group}": strconv.Itoa(sk.Group),
|
||||
"@{.brief}": sk.Brief,
|
||||
"@{.description}": sk.Description,
|
||||
"@{.author}": sk.Author,
|
||||
"@{.version}": sk.Version,
|
||||
}
|
||||
|
||||
if err = checkBuildDir(buildPath, modulesPath, templatePath); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// scans directory recursively
|
||||
list, err := gfile.ScanDirFunc(conf.SrcPath, "*", true, func(path string) string {
|
||||
return path
|
||||
})
|
||||
|
||||
for _, path := range list {
|
||||
if !gfile.IsReadable(path) {
|
||||
err = fmt.Errorf("file:%v is unreadable, please check permissions", path)
|
||||
return
|
||||
}
|
||||
|
||||
if gfile.IsDir(path) {
|
||||
continue
|
||||
}
|
||||
|
||||
flowFile := gstr.ReplaceByMap(path, map[string]string{
|
||||
gfile.RealPath(conf.SrcPath): "",
|
||||
".template": "",
|
||||
})
|
||||
|
||||
flowFile = buildPath + "/" + flowFile
|
||||
|
||||
content := gstr.ReplaceByMap(gfile.GetContents(path), replaces)
|
||||
|
||||
if err = gfile.PutContents(flowFile, content); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err = gfile.PutContents(templatePath+"/home/index.html", homeLayout); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = gfile.PutContents(modulesPath, gstr.ReplaceByMap(importModules, replaces))
|
||||
return
|
||||
}
|
||||
|
||||
func checkBuildDir(paths ...string) error {
|
||||
if len(paths) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
if gfile.Exists(path) {
|
||||
return fmt.Errorf("插件已存在,请换一个插件名称或者经确认无误后依次删除文件夹: [%v] 后重新生成", strings.Join(paths, "、\t"))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
importModules = `// Package modules
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package modules
|
||||
|
||||
import _ "hotgo/addons/@{.name}"
|
||||
`
|
||||
|
||||
homeLayout = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0,user-scalable=no">
|
||||
<meta name="keywords" content="@{.Keywords}"/>
|
||||
<meta name="description" content="@{.Description}"/>
|
||||
<title>@{.Title}</title>
|
||||
<script type="text/javascript" src="/resource/home/js/jquery-3.6.0.min.js"></script>
|
||||
<style>
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="padding-top: 100px;text-align:center;">
|
||||
<h1><p>Hello,@{.Data.name}!!</p></h1>
|
||||
<h2><p>@{.Data.module}</p></h2>
|
||||
<h2><p>服务器时间:@{.Data.time}</p></h2>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</html>`
|
||||
)
|
||||
142
server/internal/library/addons/install.go
Normal file
142
server/internal/library/addons/install.go
Normal file
@@ -0,0 +1,142 @@
|
||||
// Package addons
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package addons
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"hotgo/internal/consts"
|
||||
)
|
||||
|
||||
// InstallRecord 安装记录
|
||||
type InstallRecord struct {
|
||||
Id int64 `json:"id" description:"安装ID"`
|
||||
Version string `json:"version" description:"安装版本"`
|
||||
Status int `json:"status" description:"安装状态"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" description:"创建时间"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt" description:"更新时间"`
|
||||
}
|
||||
|
||||
func ScanInstall(m Module) (record *InstallRecord, err error) {
|
||||
err = g.Model("sys_addons_install").
|
||||
Ctx(m.Ctx()).
|
||||
Where("name", m.GetSkeleton().Name).
|
||||
Scan(&record)
|
||||
return
|
||||
}
|
||||
|
||||
// IsInstall 模块是否已安装
|
||||
func IsInstall(m Module) bool {
|
||||
record, err := ScanInstall(m)
|
||||
if err != nil {
|
||||
g.Log().Debugf(m.Ctx(), err.Error())
|
||||
return false
|
||||
}
|
||||
if record == nil {
|
||||
return false
|
||||
}
|
||||
return record.Status == consts.AddonsInstallStatusOk
|
||||
}
|
||||
|
||||
// Install 安装模块
|
||||
func Install(m Module) (err error) {
|
||||
record, err := ScanInstall(m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if record != nil && record.Status == consts.AddonsInstallStatusOk {
|
||||
return gerror.New("插件已安装,无需重复操作!")
|
||||
}
|
||||
|
||||
data := g.Map{
|
||||
"name": m.GetSkeleton().Name,
|
||||
"version": m.GetSkeleton().Version,
|
||||
"status": consts.AddonsInstallStatusOk,
|
||||
}
|
||||
|
||||
return g.DB().Transaction(m.Ctx(), func(ctx context.Context, tx gdb.TX) error {
|
||||
if record != nil {
|
||||
_, err = g.Model("sys_addons_install").
|
||||
Ctx(m.Ctx()).
|
||||
Where("id", record.Id).
|
||||
Delete()
|
||||
}
|
||||
_, err = g.Model("sys_addons_install").
|
||||
Ctx(m.Ctx()).
|
||||
Data(data).
|
||||
Insert()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.Install(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
// Upgrade 更新模块
|
||||
func Upgrade(m Module) (err error) {
|
||||
record, err := ScanInstall(m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if record == nil || record.Status != consts.AddonsInstallStatusOk {
|
||||
return gerror.New("插件未安装,请安装后操作!")
|
||||
}
|
||||
|
||||
data := g.Map{
|
||||
"version": m.GetSkeleton().Version,
|
||||
}
|
||||
|
||||
return g.DB().Transaction(m.Ctx(), func(ctx context.Context, tx gdb.TX) error {
|
||||
_, err = g.Model("sys_addons_install").
|
||||
Ctx(m.Ctx()).
|
||||
Where("id", record.Id).
|
||||
Data(data).
|
||||
Update()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.Upgrade(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
// UnInstall 卸载模块
|
||||
func UnInstall(m Module) (err error) {
|
||||
record, err := ScanInstall(m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if record == nil || record.Status != consts.AddonsInstallStatusOk {
|
||||
return gerror.New("插件未安装,请安装后操作!")
|
||||
}
|
||||
|
||||
data := g.Map{
|
||||
"version": m.GetSkeleton().Version,
|
||||
"status": consts.AddonsInstallStatusUn,
|
||||
}
|
||||
|
||||
return g.DB().Transaction(m.Ctx(), func(ctx context.Context, tx gdb.TX) error {
|
||||
_, err = g.Model("sys_addons_install").
|
||||
Ctx(m.Ctx()).
|
||||
Where("id", record.Id).
|
||||
Data(data).
|
||||
Update()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.UnInstall(ctx)
|
||||
})
|
||||
}
|
||||
145
server/internal/library/addons/module.go
Normal file
145
server/internal/library/addons/module.go
Normal file
@@ -0,0 +1,145 @@
|
||||
// Package addons
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package addons
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/model/input/form"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Skeleton 模块骨架
|
||||
type Skeleton struct {
|
||||
Label string `json:"label"` // 标识
|
||||
Name string `json:"name"` // 名称
|
||||
Group int `json:"group"` // 分组
|
||||
Logo string `json:"logo"` // logo
|
||||
Brief string `json:"brief"` // 简介
|
||||
Description string `json:"description"` // 详细描述
|
||||
Author string `json:"author"` // 作者
|
||||
Version string `json:"version"` // 版本号
|
||||
RootPath string `json:"rootPath"` // 根路径
|
||||
}
|
||||
|
||||
func (s *Skeleton) GetModule() Module {
|
||||
return GetModule(s.Name)
|
||||
}
|
||||
|
||||
// Module 插件模块
|
||||
type Module interface {
|
||||
Init(ctx context.Context) // 初始化
|
||||
InitRouter(ctx context.Context, group *ghttp.RouterGroup) // 初始化并注册路由
|
||||
Ctx() context.Context // 上下文
|
||||
GetSkeleton() *Skeleton // 架子
|
||||
Install(ctx context.Context) error // 安装模块
|
||||
Upgrade(ctx context.Context) error // 更新模块
|
||||
UnInstall(ctx context.Context) error // 卸载模块
|
||||
}
|
||||
|
||||
var (
|
||||
modules = make(map[string]Module, 0)
|
||||
mLock sync.Mutex
|
||||
)
|
||||
|
||||
// InitModules 初始化所有已注册模块
|
||||
func InitModules(ctx context.Context) {
|
||||
for _, module := range modules {
|
||||
module.Init(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterModulesRouter 注册所有已安装模块路由
|
||||
func RegisterModulesRouter(ctx context.Context, group *ghttp.RouterGroup) {
|
||||
for _, module := range filterInstalled() {
|
||||
module.InitRouter(ctx, group)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterModule 注册模块
|
||||
func RegisterModule(m Module) Module {
|
||||
mLock.Lock()
|
||||
defer mLock.Unlock()
|
||||
_, ok := modules[m.GetSkeleton().Name]
|
||||
if ok {
|
||||
panic("module repeat registration, name:" + m.GetSkeleton().Name)
|
||||
}
|
||||
modules[m.GetSkeleton().Name] = m
|
||||
return m
|
||||
}
|
||||
|
||||
// GetModule 获取指定名称模块
|
||||
func GetModule(name string) Module {
|
||||
mLock.Lock()
|
||||
defer mLock.Unlock()
|
||||
m, ok := modules[name]
|
||||
if !ok {
|
||||
panic("implement not found for interface " + name + ", forgot register?")
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// GetSkeletons 获取所有模块骨架
|
||||
func GetSkeletons() (list []*Skeleton) {
|
||||
var keys []string
|
||||
for k, _ := range modules {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, v := range keys {
|
||||
list = append(list, GetModule(v).GetSkeleton())
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// GetModuleRealPath 获取指定模块绝对路径
|
||||
func GetModuleRealPath(name string) string {
|
||||
path := gfile.RealPath(GetModulePath(name))
|
||||
if path == "" {
|
||||
panic("no path is found. please confirm that the path " + GetModulePath(name) + " exists?")
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// GetModulePath 获取指定模块相对路径
|
||||
func GetModulePath(name string) string {
|
||||
return "./" + consts.AddonsDir + "/" + name
|
||||
}
|
||||
|
||||
// filterInstalled 过滤已安装模块
|
||||
func filterInstalled() []Module {
|
||||
var ms []Module
|
||||
for _, module := range modules {
|
||||
if IsInstall(module) {
|
||||
ms = append(ms, module)
|
||||
}
|
||||
}
|
||||
return ms
|
||||
}
|
||||
|
||||
// ModuleSelect 获取插件模块选项
|
||||
func ModuleSelect() form.Selects {
|
||||
sks := GetSkeletons()
|
||||
lst := make(form.Selects, 0)
|
||||
if len(sks) == 0 {
|
||||
return lst
|
||||
}
|
||||
|
||||
for _, skeleton := range sks {
|
||||
lst = append(lst, &form.Select{
|
||||
Value: skeleton.Name,
|
||||
Label: skeleton.Label,
|
||||
Name: skeleton.Label,
|
||||
})
|
||||
}
|
||||
|
||||
return lst
|
||||
}
|
||||
Reference in New Issue
Block a user