mirror of
https://github.com/bufanyun/hotgo.git
synced 2025-11-14 05:03: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
|
||||
}
|
||||
68
server/internal/library/cache/cache.go
vendored
68
server/internal/library/cache/cache.go
vendored
@@ -1,24 +1,70 @@
|
||||
// Package cache
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gcache"
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"hotgo/internal/library/cache/file"
|
||||
"hotgo/internal/model"
|
||||
"hotgo/internal/service"
|
||||
)
|
||||
|
||||
func New() *gcache.Cache {
|
||||
c := gcache.New()
|
||||
// cache 缓存驱动
|
||||
var cache *gcache.Cache
|
||||
|
||||
//redis
|
||||
adapter := gcache.NewAdapterRedis(g.Redis())
|
||||
|
||||
//内存
|
||||
//adapter := gcache.NewAdapterMemory()
|
||||
c.SetAdapter(adapter)
|
||||
return c
|
||||
// Instance 缓存实例
|
||||
func Instance() *gcache.Cache {
|
||||
if cache == nil {
|
||||
panic("cache uninitialized.")
|
||||
}
|
||||
return cache
|
||||
}
|
||||
|
||||
// SetAdapter 设置缓存适配器
|
||||
func SetAdapter(ctx context.Context) {
|
||||
var adapter gcache.Adapter
|
||||
conf, err := service.SysConfig().GetLoadCache(ctx)
|
||||
if err != nil {
|
||||
g.Log().Fatalf(ctx, "cache init err:%+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if conf == nil {
|
||||
conf = new(model.CacheConfig)
|
||||
g.Log().Infof(ctx, "no cache driver is configured. default memory cache is used.")
|
||||
}
|
||||
|
||||
switch conf.Adapter {
|
||||
case "redis":
|
||||
adapter = gcache.NewAdapterRedis(g.Redis())
|
||||
case "file":
|
||||
if conf.FileDir == "" {
|
||||
g.Log().Fatalf(ctx, "file path must be configured for file caching.")
|
||||
return
|
||||
}
|
||||
|
||||
if !gfile.Exists(conf.FileDir) {
|
||||
if err := gfile.Mkdir(conf.FileDir); err != nil {
|
||||
g.Log().Fatalf(ctx, "Failed to create the cache directory. Procedure, err:%+v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
adapter = file.NewAdapterFile(conf.FileDir)
|
||||
default:
|
||||
adapter = gcache.NewAdapterMemory()
|
||||
}
|
||||
|
||||
// 数据库缓存,默认和通用缓冲驱动一致,如果你不想使用默认的,可以自行调整
|
||||
g.DB().GetCache().SetAdapter(adapter)
|
||||
|
||||
// 通用缓存
|
||||
cache = gcache.New()
|
||||
cache.SetAdapter(adapter)
|
||||
return
|
||||
}
|
||||
|
||||
290
server/internal/library/cache/file/file.go
vendored
Normal file
290
server/internal/library/cache/file/file.go
vendored
Normal file
@@ -0,0 +1,290 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gcache"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
// AdapterFile is the gcache adapter implements using file server.
|
||||
AdapterFile struct {
|
||||
dir string
|
||||
}
|
||||
|
||||
fileContent struct {
|
||||
Duration int64 `json:"duration"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
const perm = 0o666
|
||||
|
||||
// NewAdapterFile creates and returns a new memory cache object.
|
||||
func NewAdapterFile(dir string) gcache.Adapter {
|
||||
return &AdapterFile{
|
||||
dir: dir,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Set(ctx context.Context, key interface{}, value interface{}, lifeTime time.Duration) (err error) {
|
||||
fileKey := gconv.String(key)
|
||||
if value == nil || lifeTime < 0 {
|
||||
return c.Delete(fileKey)
|
||||
}
|
||||
return c.Save(fileKey, gconv.String(value), lifeTime)
|
||||
}
|
||||
|
||||
func (c *AdapterFile) SetMap(ctx context.Context, data map[interface{}]interface{}, duration time.Duration) (err error) {
|
||||
return gerror.New("implement me")
|
||||
}
|
||||
|
||||
func (c *AdapterFile) SetIfNotExist(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (ok bool, err error) {
|
||||
return false, gerror.New("implement me")
|
||||
}
|
||||
|
||||
func (c *AdapterFile) SetIfNotExistFunc(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (ok bool, err error) {
|
||||
return false, gerror.New("implement me")
|
||||
}
|
||||
|
||||
func (c *AdapterFile) SetIfNotExistFuncLock(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (ok bool, err error) {
|
||||
return false, gerror.New("implement me")
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Get(ctx context.Context, key interface{}) (*gvar.Var, error) {
|
||||
fetch, err := c.Fetch(gconv.String(key))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gvar.New(fetch), nil
|
||||
}
|
||||
|
||||
func (c *AdapterFile) GetOrSet(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (result *gvar.Var, err error) {
|
||||
return nil, gerror.New("implement me")
|
||||
}
|
||||
|
||||
func (c *AdapterFile) GetOrSetFunc(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (result *gvar.Var, err error) {
|
||||
return nil, gerror.New("implement me")
|
||||
}
|
||||
|
||||
func (c *AdapterFile) GetOrSetFuncLock(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (result *gvar.Var, err error) {
|
||||
return nil, gerror.New("implement me")
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Contains(ctx context.Context, key interface{}) (bool, error) {
|
||||
return c.Has(gconv.String(key)), nil
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Size(ctx context.Context) (size int, err error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Data(ctx context.Context) (data map[interface{}]interface{}, err error) {
|
||||
return nil, gerror.New("implement me")
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Keys(ctx context.Context) (keys []interface{}, err error) {
|
||||
return nil, gerror.New("implement me")
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Values(ctx context.Context) (values []interface{}, err error) {
|
||||
return nil, gerror.New("implement me")
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Update(ctx context.Context, key interface{}, value interface{}) (oldValue *gvar.Var, exist bool, err error) {
|
||||
return nil, false, gerror.New("implement me")
|
||||
}
|
||||
|
||||
func (c *AdapterFile) UpdateExpire(ctx context.Context, key interface{}, duration time.Duration) (oldDuration time.Duration, err error) {
|
||||
var (
|
||||
v *gvar.Var
|
||||
oldTTL int64
|
||||
fileKey = gconv.String(key)
|
||||
)
|
||||
// TTL.
|
||||
expire, err := c.GetExpire(ctx, fileKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
oldTTL = int64(expire)
|
||||
if oldTTL == -2 {
|
||||
// It does not exist.
|
||||
oldTTL = -1
|
||||
return
|
||||
}
|
||||
oldDuration = time.Duration(oldTTL) * time.Second
|
||||
// DEL.
|
||||
if duration < 0 {
|
||||
err = c.Delete(fileKey)
|
||||
return
|
||||
}
|
||||
v, err = c.Get(ctx, fileKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = c.Set(ctx, fileKey, v.Val(), duration)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *AdapterFile) GetExpire(ctx context.Context, key interface{}) (time.Duration, error) {
|
||||
content, err := c.read(gconv.String(key))
|
||||
if err != nil {
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
if content.Duration <= time.Now().Unix() {
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
return time.Duration(time.Now().Unix()-content.Duration) * time.Second, nil
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Remove(ctx context.Context, keys ...interface{}) (lastValue *gvar.Var, err error) {
|
||||
if len(keys) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
// Retrieves the last key value.
|
||||
if lastValue, err = c.Get(ctx, gconv.String(keys[len(keys)-1])); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Deletes all given keys.
|
||||
err = c.DeleteMulti(gconv.Strings(keys)...)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Clear(ctx context.Context) error {
|
||||
return c.Flush()
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Close(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *AdapterFile) createName(key string) string {
|
||||
h := sha256.New()
|
||||
_, _ = h.Write([]byte(key))
|
||||
hash := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
return filepath.Join(c.dir, fmt.Sprintf("%s.cache", hash))
|
||||
}
|
||||
|
||||
func (c *AdapterFile) read(key string) (*fileContent, error) {
|
||||
value, err := ioutil.ReadFile(c.createName(key))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content := &fileContent{}
|
||||
if err := json.Unmarshal(value, content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if content.Duration == 0 {
|
||||
return content, nil
|
||||
}
|
||||
|
||||
if content.Duration <= time.Now().Unix() {
|
||||
_ = c.Delete(key)
|
||||
return nil, errors.New("cache expired")
|
||||
}
|
||||
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// Has checks if the cached key exists into the File storage
|
||||
func (c *AdapterFile) Has(key string) bool {
|
||||
_, err := c.read(key)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Delete the cached key from File storage
|
||||
func (c *AdapterFile) Delete(key string) error {
|
||||
_, err := os.Stat(c.createName(key))
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return os.Remove(c.createName(key))
|
||||
}
|
||||
|
||||
// DeleteMulti the cached key from File storage
|
||||
func (c *AdapterFile) DeleteMulti(keys ...string) (err error) {
|
||||
for _, key := range keys {
|
||||
if err = c.Delete(key); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch retrieves the cached value from key of the File storage
|
||||
func (c *AdapterFile) Fetch(key string) (interface{}, error) {
|
||||
content, err := c.read(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return content.Data, nil
|
||||
}
|
||||
|
||||
// FetchMulti retrieve multiple cached values from keys of the File storage
|
||||
func (c *AdapterFile) FetchMulti(keys []string) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
for _, key := range keys {
|
||||
if value, err := c.Fetch(key); err == nil {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Flush removes all cached keys of the File storage
|
||||
func (c *AdapterFile) Flush() error {
|
||||
dir, err := os.Open(c.dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = dir.Close()
|
||||
}()
|
||||
|
||||
names, _ := dir.Readdirnames(-1)
|
||||
|
||||
for _, name := range names {
|
||||
_ = os.Remove(filepath.Join(c.dir, name))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save a value in File storage by key
|
||||
func (c *AdapterFile) Save(key string, value string, lifeTime time.Duration) error {
|
||||
duration := int64(0)
|
||||
|
||||
if lifeTime > 0 {
|
||||
duration = time.Now().Unix() + int64(lifeTime.Seconds())
|
||||
}
|
||||
|
||||
content := &fileContent{duration, value}
|
||||
|
||||
data, err := json.Marshal(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(c.createName(key), data, perm)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package captcha
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package casbin
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package casbin
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package casbin
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// Package contexts
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
package contexts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/model"
|
||||
@@ -32,22 +32,42 @@ func Get(ctx context.Context) *model.Context {
|
||||
|
||||
// SetUser 将上下文信息设置到上下文请求中,注意是完整覆盖
|
||||
func SetUser(ctx context.Context, user *model.Identity) {
|
||||
Get(ctx).User = user
|
||||
c := Get(ctx)
|
||||
if c == nil {
|
||||
g.Log().Warningf(ctx, "contexts.SetUser, c == nil ")
|
||||
return
|
||||
}
|
||||
c.User = user
|
||||
}
|
||||
|
||||
// SetResponse 设置组件响应 用于访问日志使用
|
||||
func SetResponse(ctx context.Context, response *model.Response) {
|
||||
Get(ctx).Response = response
|
||||
c := Get(ctx)
|
||||
if c == nil {
|
||||
g.Log().Warningf(ctx, "contexts.SetResponse, c == nil ")
|
||||
return
|
||||
}
|
||||
c.Response = response
|
||||
}
|
||||
|
||||
// SetModule 设置应用模块
|
||||
func SetModule(ctx context.Context, module string) {
|
||||
Get(ctx).Module = module
|
||||
c := Get(ctx)
|
||||
if c == nil {
|
||||
g.Log().Warningf(ctx, "contexts.SetModule, c == nil ")
|
||||
return
|
||||
}
|
||||
c.Module = module
|
||||
}
|
||||
|
||||
// SetTakeUpTime 设置请求耗时
|
||||
func SetTakeUpTime(ctx context.Context, takeUpTime int64) {
|
||||
Get(ctx).TakeUpTime = takeUpTime
|
||||
c := Get(ctx)
|
||||
if c == nil {
|
||||
g.Log().Warningf(ctx, "contexts.SetTakeUpTime, c == nil ")
|
||||
return
|
||||
}
|
||||
c.TakeUpTime = takeUpTime
|
||||
}
|
||||
|
||||
// GetUser 获取用户信息
|
||||
@@ -62,30 +82,55 @@ func GetUser(ctx context.Context) *model.Identity {
|
||||
|
||||
// GetUserId 获取用户ID
|
||||
func GetUserId(ctx context.Context) int64 {
|
||||
user := Get(ctx).User
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return user.Id
|
||||
}
|
||||
|
||||
// GetRoleId 获取用户角色ID
|
||||
func GetRoleId(ctx context.Context) int64 {
|
||||
user := Get(ctx).User
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return user.RoleId
|
||||
}
|
||||
|
||||
// GetRoleKey 获取用户角色唯一编码
|
||||
func GetRoleKey(ctx context.Context) string {
|
||||
user := Get(ctx).User
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return user.RoleKey
|
||||
}
|
||||
|
||||
// SetAddonName 设置插件信息
|
||||
func SetAddonName(ctx context.Context, name string) {
|
||||
c := Get(ctx)
|
||||
if c == nil {
|
||||
g.Log().Warningf(ctx, "contexts.SetAddonName, c == nil ")
|
||||
return
|
||||
}
|
||||
Get(ctx).AddonName = name
|
||||
}
|
||||
|
||||
// IsAddonRequest 是否为插件模块请求
|
||||
func IsAddonRequest(ctx context.Context) bool {
|
||||
c := Get(ctx)
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
return GetAddonName(ctx) != ""
|
||||
}
|
||||
|
||||
// GetAddonName 获取插件信息
|
||||
func GetAddonName(ctx context.Context) string {
|
||||
c := Get(ctx)
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
return Get(ctx).AddonName
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package ems
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// Package hggen
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
package hggen
|
||||
|
||||
import (
|
||||
@@ -12,8 +11,10 @@ import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/library/addons"
|
||||
"hotgo/internal/library/hggen/internal/cmd"
|
||||
"hotgo/internal/library/hggen/internal/cmd/gendao"
|
||||
"hotgo/internal/library/hggen/internal/cmd/genservice"
|
||||
"hotgo/internal/library/hggen/views"
|
||||
"hotgo/internal/model"
|
||||
"hotgo/internal/model/input/form"
|
||||
@@ -38,7 +39,16 @@ func Dao(ctx context.Context) (err error) {
|
||||
|
||||
// Service 生成业务接口
|
||||
func Service(ctx context.Context) (err error) {
|
||||
_, err = cmd.Gen.Service(ctx, GetServiceConfig())
|
||||
return ServiceWithCfg(ctx, GetServiceConfig())
|
||||
}
|
||||
|
||||
// ServiceWithCfg 生成业务接口
|
||||
func ServiceWithCfg(ctx context.Context, cfg ...genservice.CGenServiceInput) (err error) {
|
||||
c := GetServiceConfig()
|
||||
if len(cfg) > 0 {
|
||||
c = cfg[0]
|
||||
}
|
||||
_, err = cmd.Gen.Service(ctx, c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -115,6 +125,8 @@ func TableSelects(ctx context.Context, in sysin.GenCodesSelectsInp) (res *sysin.
|
||||
})
|
||||
}
|
||||
|
||||
res.Addons = addons.ModuleSelect()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -125,7 +137,7 @@ func GenTypeSelect(ctx context.Context) (res sysin.GenTypeSelects, err error) {
|
||||
Value: k,
|
||||
Name: v,
|
||||
Label: v,
|
||||
Templates: make(form.Selects, 0),
|
||||
Templates: make(sysin.GenTemplateSelects, 0),
|
||||
}
|
||||
|
||||
confName, ok := consts.GenCodesTypeConfMap[k]
|
||||
@@ -137,10 +149,11 @@ func GenTypeSelect(ctx context.Context) (res sysin.GenTypeSelects, err error) {
|
||||
}
|
||||
if len(temps) > 0 {
|
||||
for index, temp := range temps {
|
||||
row.Templates = append(row.Templates, &form.Select{
|
||||
Value: index,
|
||||
Label: temp.Group,
|
||||
Name: temp.Group,
|
||||
row.Templates = append(row.Templates, &sysin.GenTemplateSelect{
|
||||
Value: index,
|
||||
Label: temp.Group,
|
||||
Name: temp.Group,
|
||||
IsAddon: temp.IsAddon,
|
||||
})
|
||||
}
|
||||
sort.Sort(row.Templates)
|
||||
@@ -207,14 +220,31 @@ func Build(ctx context.Context, in sysin.GenCodesBuildInp) (err error) {
|
||||
|
||||
switch in.GenType {
|
||||
case consts.GenCodesTypeCurd:
|
||||
pin := sysin.GenCodesPreviewInp(in)
|
||||
return views.Curd.DoBuild(ctx, &views.CurdBuildInput{
|
||||
PreviewIn: &views.CurdPreviewInput{
|
||||
In: sysin.GenCodesPreviewInp(in),
|
||||
In: pin,
|
||||
DaoConfig: GetDaoConfig(in.DbName),
|
||||
Config: genConfig,
|
||||
},
|
||||
BeforeEvent: views.CurdBuildEvent{"runDao": Dao},
|
||||
AfterEvent: views.CurdBuildEvent{"runService": Service},
|
||||
AfterEvent: views.CurdBuildEvent{"runService": func(ctx context.Context) (err error) {
|
||||
cfg := GetServiceConfig()
|
||||
if err = ServiceWithCfg(ctx, cfg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 插件模块,同时运行模块下的gen service
|
||||
if genConfig.Application.Crud.Templates[pin.GenTemplate].IsAddon {
|
||||
// 依然使用配置中的参数,只是将生成路径指向插件模块路径
|
||||
cfg.SrcFolder = "addons/" + pin.AddonName + "/logic"
|
||||
cfg.DstFolder = "addons/" + pin.AddonName + "/service"
|
||||
if err = ServiceWithCfg(ctx, cfg); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}},
|
||||
})
|
||||
case consts.GenCodesTypeTree:
|
||||
err = gerror.Newf("生成类型开发中!")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package hggen
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package views
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package views
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package views
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// Package views
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
package views
|
||||
|
||||
import (
|
||||
@@ -70,6 +69,7 @@ type CurdOptions struct {
|
||||
Step *CurdStep // 转换后的流程控制条件
|
||||
dictMap g.Map // 字典选项 -> 字段映射关系
|
||||
TemplateGroup string `json:"templateGroup"`
|
||||
ApiPrefix string `json:"apiPrefix"`
|
||||
}
|
||||
|
||||
type CurdPreviewInput struct {
|
||||
@@ -123,7 +123,14 @@ func (l *gCurd) initInput(ctx context.Context, in *CurdPreviewInput) (err error)
|
||||
return gerror.New("没有找到生成模板的配置,请检查!")
|
||||
}
|
||||
|
||||
err = checkCurdPath(in.Config.Application.Crud.Templates[in.In.GenTemplate])
|
||||
// api前缀
|
||||
apiPrefix := gstr.LcFirst(in.In.VarName)
|
||||
if in.Config.Application.Crud.Templates[in.In.GenTemplate].IsAddon {
|
||||
apiPrefix = in.In.AddonName + "/" + apiPrefix
|
||||
}
|
||||
in.options.ApiPrefix = apiPrefix
|
||||
|
||||
err = checkCurdPath(in.Config.Application.Crud.Templates[in.In.GenTemplate], in.In.AddonName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -147,9 +154,10 @@ func initStep(ctx context.Context, in *CurdPreviewInput) {
|
||||
}
|
||||
|
||||
func (l *gCurd) loadView(ctx context.Context, in *CurdPreviewInput) (err error) {
|
||||
temp := in.Config.Application.Crud.Templates[in.In.GenTemplate]
|
||||
view := gview.New()
|
||||
err = view.SetConfigWithMap(g.Map{
|
||||
"Paths": in.Config.Application.Crud.Templates[in.In.GenTemplate].TemplatePath,
|
||||
"Paths": temp.TemplatePath,
|
||||
"Delimiters": in.Config.Delimiters,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -168,19 +176,48 @@ func (l *gCurd) loadView(ctx context.Context, in *CurdPreviewInput) (err error)
|
||||
return
|
||||
}
|
||||
|
||||
modName, err := GetModName(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
importApi := gstr.Replace(temp.ApiPath, "./", modName+"/") + "/" + strings.ToLower(in.In.VarName)
|
||||
importInput := gstr.Replace(temp.InputPath, "./", modName+"/")
|
||||
importController := gstr.Replace(temp.ControllerPath, "./", modName+"/")
|
||||
importService := "hotgo/internal/service"
|
||||
if temp.IsAddon {
|
||||
importService = "hotgo/addons/" + in.In.AddonName + "/service"
|
||||
}
|
||||
|
||||
importWebApi := "@/api/" + gstr.LcFirst(in.In.VarName)
|
||||
if temp.IsAddon {
|
||||
importWebApi = "@/api/addons/" + in.In.AddonName + "/" + gstr.LcFirst(in.In.VarName)
|
||||
}
|
||||
|
||||
componentPrefix := gstr.LcFirst(in.In.VarName)
|
||||
if temp.IsAddon {
|
||||
componentPrefix = "addons/" + in.In.AddonName + "/" + componentPrefix
|
||||
}
|
||||
|
||||
view.Assigns(gview.Params{
|
||||
"templateGroup": in.options.TemplateGroup, // 生成模板分组名称
|
||||
"servFunName": l.parseServFunName(in.options.TemplateGroup, in.In.VarName), // 业务服务名称
|
||||
"nowTime": gtime.Now().Format("Y-m-d H:i:s"), // 当前时间
|
||||
"version": runtime.Version(), // GO 版本
|
||||
"hgVersion": consts.VersionApp, // HG 版本
|
||||
"varName": in.In.VarName, // 实体名称
|
||||
"tableComment": in.In.TableComment, // 对外名称
|
||||
"daoName": in.In.DaoName, // ORM模型
|
||||
"masterFields": in.masterFields, // 主表字段
|
||||
"pk": in.pk, // 主键属性
|
||||
"options": in.options, // 提交选项
|
||||
"dictOptions": dictOptions, // web字典选项
|
||||
"templateGroup": in.options.TemplateGroup, // 生成模板分组名称
|
||||
"servFunName": l.parseServFunName(in.options.TemplateGroup, in.In.VarName), // 业务服务名称
|
||||
"nowTime": gtime.Now().Format("Y-m-d H:i:s"), // 当前时间
|
||||
"version": runtime.Version(), // GO 版本
|
||||
"hgVersion": consts.VersionApp, // HG 版本
|
||||
"varName": in.In.VarName, // 实体名称
|
||||
"tableComment": in.In.TableComment, // 对外名称
|
||||
"daoName": in.In.DaoName, // ORM模型
|
||||
"masterFields": in.masterFields, // 主表字段
|
||||
"pk": in.pk, // 主键属性
|
||||
"options": in.options, // 提交选项
|
||||
"dictOptions": dictOptions, // web字典选项
|
||||
"importApi": importApi, // 导入goApi包
|
||||
"importInput": importInput, // 导入input包
|
||||
"importController": importController, // 导入控制器包
|
||||
"importService": importService, // 导入业务服务
|
||||
"importWebApi": importWebApi, // 导入webApi
|
||||
"apiPrefix": in.options.ApiPrefix, // api前缀
|
||||
"componentPrefix": componentPrefix, // vue子组件前缀
|
||||
})
|
||||
in.view = view
|
||||
return
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package views
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package views
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// Package views
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
package views
|
||||
|
||||
import (
|
||||
@@ -120,11 +119,19 @@ func (l *gCurd) generateWebEditScript(ctx context.Context, in *CurdPreviewInput)
|
||||
|
||||
if in.options.Step.HasMaxSort {
|
||||
importBuffer.WriteString(" import { onMounted, ref, computed, watch } from 'vue';\n")
|
||||
importBuffer.WriteString(" import { Edit, MaxSort, View } from '@/api/" + gstr.LcFirst(in.In.VarName) + "';\n")
|
||||
if in.Config.Application.Crud.Templates[in.In.GenTemplate].IsAddon {
|
||||
importBuffer.WriteString(" import { Edit, MaxSort, View } from '@/api/addons/" + in.In.AddonName + "/" + gstr.LcFirst(in.In.VarName) + "';\n")
|
||||
} else {
|
||||
importBuffer.WriteString(" import { Edit, MaxSort, View } from '@/api/" + gstr.LcFirst(in.In.VarName) + "';\n")
|
||||
}
|
||||
setupBuffer.WriteString(" function loadForm(value) {\n loading.value = true;\n\n // 新增\n if (value.id < 1) {\n params.value = newState(value);\n MaxSort()\n .then((res) => {\n params.value.sort = res.sort;\n })\n .finally(() => {\n loading.value = false;\n });\n return;\n }\n\n // 编辑\n View({ id: value.id })\n .then((res) => {\n params.value = res;\n })\n .finally(() => {\n loading.value = false;\n });\n }\n\n watch(\n () => props.formParams,\n (value) => {\n loadForm(value);\n }\n );")
|
||||
} else {
|
||||
importBuffer.WriteString(" import { onMounted, ref, computed } from 'vue';\n")
|
||||
importBuffer.WriteString(" import { Edit, View } from '@/api/" + gstr.LcFirst(in.In.VarName) + "';\n")
|
||||
if in.Config.Application.Crud.Templates[in.In.GenTemplate].IsAddon {
|
||||
importBuffer.WriteString(" import { Edit, View } from '@/api/addons/" + in.In.AddonName + "/" + gstr.LcFirst(in.In.VarName) + "';\n")
|
||||
} else {
|
||||
importBuffer.WriteString(" import { Edit, View } from '@/api/" + gstr.LcFirst(in.In.VarName) + "';\n")
|
||||
}
|
||||
setupBuffer.WriteString(" function loadForm(value) {\n // 新增\n if (value.id < 1) {\n params.value = newState(value);\n loading.value = false;\n return;\n }\n\n loading.value = true;\n // 编辑\n View({ id: value.id })\n .then((res) => {\n params.value = res;\n })\n .finally(() => {\n loading.value = false;\n });\n }\n\n watch(\n () => props.formParams,\n (value) => {\n loadForm(value);\n }\n );")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// Package views
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
package views
|
||||
|
||||
import (
|
||||
@@ -14,8 +13,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
IndexApiImport = " import {%v } from '@/api/%s';"
|
||||
IndexIconsImport = " import {%v } from '@vicons/antd';"
|
||||
IndexApiImport = " import {%v } from '@/api/%s';" // 这里将导入的包路径写死了,后面可以优化成根据配置动态读取
|
||||
IndexApiAddonsImport = " import {%v } from '@/api/addons/%s/%s';"
|
||||
IndexIconsImport = " import {%v } from '@vicons/antd';"
|
||||
)
|
||||
|
||||
func (l *gCurd) webIndexTplData(ctx context.Context, in *CurdPreviewInput) (g.Map, error) {
|
||||
@@ -51,7 +51,11 @@ func (l *gCurd) webIndexTplData(ctx context.Context, in *CurdPreviewInput) (g.Ma
|
||||
apiImport = append(apiImport, " Status")
|
||||
}
|
||||
|
||||
data["apiImport"] = fmt.Sprintf(IndexApiImport, gstr.Implode(",", apiImport), gstr.LcFirst(in.In.VarName))
|
||||
if in.Config.Application.Crud.Templates[in.In.GenTemplate].IsAddon {
|
||||
data["apiImport"] = fmt.Sprintf(IndexApiAddonsImport, gstr.Implode(",", apiImport), in.In.AddonName, gstr.LcFirst(in.In.VarName))
|
||||
} else {
|
||||
data["apiImport"] = fmt.Sprintf(IndexApiImport, gstr.Implode(",", apiImport), gstr.LcFirst(in.In.VarName))
|
||||
}
|
||||
if len(iconsImport) > 0 {
|
||||
data["iconsImport"] = fmt.Sprintf(IndexIconsImport, gstr.Implode(",", iconsImport))
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// Package views
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
package views
|
||||
|
||||
import (
|
||||
@@ -11,7 +10,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/utility/convert"
|
||||
)
|
||||
@@ -271,7 +269,7 @@ func (l *gCurd) generateWebModelColumnsEach(buffer *bytes.Buffer, in *CurdPrevie
|
||||
component = fmt.Sprintf(" {\n title: '%s',\n key: '%s',\n render(row) {\n if (isNullObject(row.%s)) {\n return ``;\n }\n return row.%s.map((attachfile) => {\n return h(\n %s,\n {\n size: 'small',\n style: {\n 'margin-left': '2px',\n },\n },\n {\n default: () => getFileExt(attachfile),\n }\n );\n });\n },\n },\n", field.Dc, field.TsName, field.TsName, field.TsName, "NAvatar")
|
||||
|
||||
case FormModeSwitch:
|
||||
component = fmt.Sprintf(" {\n title: '%s',\n key: '%s',\n width: 100,\n render(row) {\n return h(%s, {\n value: row.%s === 1,\n checked: '开启',\n unchecked: '关闭',\n disabled: !hasPermission(['%s']),\n onUpdateValue: function (e) {\n console.log('onUpdateValue e:' + JSON.stringify(e));\n row.%s = e ? 1 : 2;\n Switch({ %s: row.%s, key: '%s', value: row.%s }).then((_res) => {\n $message.success('操作成功');\n });\n },\n });\n },\n },\n", field.Dc, field.TsName, "NSwitch", field.TsName, "/"+gstr.LcFirst(in.In.VarName)+"/switch", field.TsName, in.pk.TsName, in.pk.TsName, field.TsName, field.TsName)
|
||||
component = fmt.Sprintf(" {\n title: '%s',\n key: '%s',\n width: 100,\n render(row) {\n return h(%s, {\n value: row.%s === 1,\n checked: '开启',\n unchecked: '关闭',\n disabled: !hasPermission(['%s']),\n onUpdateValue: function (e) {\n console.log('onUpdateValue e:' + JSON.stringify(e));\n row.%s = e ? 1 : 2;\n Switch({ %s: row.%s, key: '%s', value: row.%s }).then((_res) => {\n $message.success('操作成功');\n });\n },\n });\n },\n },\n", field.Dc, field.TsName, "NSwitch", field.TsName, "/"+in.options.ApiPrefix+"/switch", field.TsName, in.pk.TsName, in.pk.TsName, field.TsName, field.TsName)
|
||||
|
||||
case FormModeRate:
|
||||
component = fmt.Sprintf(" {\n title: '%s',\n key: '%s',\n render(row) {\n return h(%s, {\n allowHalf: true,\n readonly: true,\n defaultValue: row.%s,\n });\n },\n },\n", field.Dc, field.TsName, "NRate", field.TsName)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package views
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// Package views
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
package views
|
||||
|
||||
import (
|
||||
@@ -11,6 +10,7 @@ import (
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"github.com/gogf/gf/v2/text/gregex"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/model"
|
||||
@@ -98,40 +98,73 @@ func ImportSql(ctx context.Context, path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkCurdPath(temp *model.GenerateAppCrudTemplate) (err error) {
|
||||
func checkCurdPath(temp *model.GenerateAppCrudTemplate, addonName string) (err error) {
|
||||
if temp == nil {
|
||||
return gerror.New("生成模板配置不能为空")
|
||||
}
|
||||
|
||||
tip := `生成模板配置参数'%s'路径不存在,请先创建路径`
|
||||
if temp.IsAddon {
|
||||
temp.TemplatePath = gstr.Replace(temp.TemplatePath, "{$name}", addonName)
|
||||
temp.ApiPath = gstr.Replace(temp.ApiPath, "{$name}", addonName)
|
||||
temp.InputPath = gstr.Replace(temp.InputPath, "{$name}", addonName)
|
||||
temp.ControllerPath = gstr.Replace(temp.ControllerPath, "{$name}", addonName)
|
||||
temp.LogicPath = gstr.Replace(temp.LogicPath, "{$name}", addonName)
|
||||
temp.RouterPath = gstr.Replace(temp.RouterPath, "{$name}", addonName)
|
||||
temp.SqlPath = gstr.Replace(temp.SqlPath, "{$name}", addonName)
|
||||
temp.WebApiPath = gstr.Replace(temp.WebApiPath, "{$name}", addonName)
|
||||
temp.WebViewsPath = gstr.Replace(temp.WebViewsPath, "{$name}", addonName)
|
||||
}
|
||||
|
||||
tip := `生成模板配置参数'%s'路径不存在,请先创建路径:%s`
|
||||
|
||||
if !gfile.Exists(temp.TemplatePath) {
|
||||
return gerror.Newf(tip, "TemplatePath")
|
||||
return gerror.Newf(tip, "TemplatePath", temp.TemplatePath)
|
||||
}
|
||||
if !gfile.Exists(temp.ApiPath) {
|
||||
return gerror.Newf(tip, "ApiPath")
|
||||
return gerror.Newf(tip, "ApiPath", temp.ApiPath)
|
||||
}
|
||||
if !gfile.Exists(temp.InputPath) {
|
||||
return gerror.Newf(tip, "InputPath")
|
||||
return gerror.Newf(tip, "InputPath", temp.InputPath)
|
||||
}
|
||||
if !gfile.Exists(temp.ControllerPath) {
|
||||
return gerror.Newf(tip, "ControllerPath")
|
||||
return gerror.Newf(tip, "ControllerPath", temp.ControllerPath)
|
||||
}
|
||||
if !gfile.Exists(temp.LogicPath) {
|
||||
return gerror.Newf(tip, "LogicPath")
|
||||
return gerror.Newf(tip, "LogicPath", temp.LogicPath)
|
||||
}
|
||||
if !gfile.Exists(temp.RouterPath) {
|
||||
return gerror.Newf(tip, "RouterPath")
|
||||
return gerror.Newf(tip, "RouterPath", temp.RouterPath)
|
||||
}
|
||||
if !gfile.Exists(temp.SqlPath) {
|
||||
return gerror.Newf(tip, "SqlPath")
|
||||
return gerror.Newf(tip, "SqlPath", temp.SqlPath)
|
||||
}
|
||||
if !gfile.Exists(temp.WebApiPath) {
|
||||
return gerror.Newf(tip, "WebApiPath")
|
||||
return gerror.Newf(tip, "WebApiPath", temp.WebApiPath)
|
||||
}
|
||||
if !gfile.Exists(temp.WebViewsPath) {
|
||||
return gerror.Newf(tip, "WebViewsPath")
|
||||
return gerror.Newf(tip, "WebViewsPath", temp.WebViewsPath)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetModName 获取主包名
|
||||
func GetModName(ctx context.Context) (modName string, err error) {
|
||||
if !gfile.Exists("go.mod") {
|
||||
err = gerror.New("go.mod does not exist in current working directory")
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
goModContent = gfile.GetContents("go.mod")
|
||||
match, _ = gregex.MatchString(`^module\s+(.+)\s*`, goModContent)
|
||||
)
|
||||
|
||||
if len(match) > 1 {
|
||||
modName = gstr.Trim(match[1])
|
||||
} else {
|
||||
err = gerror.New("module name does not found in go.mod")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package handler
|
||||
|
||||
import "github.com/gogf/gf/v2/database/gdb"
|
||||
import (
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
)
|
||||
|
||||
// ForceCache 强制缓存
|
||||
func ForceCache(m *gdb.Model) *gdb.Model {
|
||||
return m.Cache(gdb.CacheOption{Duration: -1, Force: true})
|
||||
return m.Cache(gdb.CacheOption{Duration: 0, Force: true})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package handler
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// Package jwt
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
package jwt
|
||||
|
||||
import (
|
||||
@@ -54,18 +53,17 @@ func GenerateLoginToken(ctx context.Context, user *model.Identity, isRefresh boo
|
||||
var (
|
||||
tokenStringMd5 = gmd5.MustEncryptString(tokenString)
|
||||
// 绑定登录token
|
||||
c = cache.New()
|
||||
key = consts.RedisJwtToken + tokenStringMd5
|
||||
key = consts.CacheJwtToken + tokenStringMd5
|
||||
// 将有效期转为持续时间,单位:秒
|
||||
expires, _ = time.ParseDuration(fmt.Sprintf("+%vs", user.Expires))
|
||||
)
|
||||
|
||||
err = c.Set(ctx, key, tokenString, expires)
|
||||
err = cache.Instance().Set(ctx, key, tokenString, expires)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = c.Set(ctx, consts.RedisJwtUserBind+user.App+":"+gconv.String(user.Id), key, expires)
|
||||
err = cache.Instance().Set(ctx, consts.CacheJwtUserBind+user.App+":"+gconv.String(user.Id), key, expires)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package location
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
@@ -198,11 +198,11 @@ func GetClientIp(r *ghttp.Request) string {
|
||||
|
||||
// 如果存在多个,默认取第一个
|
||||
if gstr.Contains(ip, ",") {
|
||||
ip = gstr.TrimStr(ip, ",", -1)
|
||||
ip = gstr.StrTillEx(ip, ",")
|
||||
}
|
||||
|
||||
if gstr.Contains(ip, ", ") {
|
||||
ip = gstr.TrimStr(ip, ", ", -1)
|
||||
ip = gstr.StrTillEx(ip, ", ")
|
||||
}
|
||||
|
||||
return ip
|
||||
|
||||
137
server/internal/library/queue/disk.go
Normal file
137
server/internal/library/queue/disk.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"hotgo/internal/library/queue/disk"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DiskProducerMq struct {
|
||||
config *disk.Config
|
||||
producers map[string]*disk.Queue
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
type DiskConsumerMq struct {
|
||||
config *disk.Config
|
||||
}
|
||||
|
||||
func RegisterDiskMqConsumer(config *disk.Config) (client MqConsumer, err error) {
|
||||
return &DiskConsumerMq{
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListenReceiveMsgDo 消费数据
|
||||
func (q *DiskConsumerMq) ListenReceiveMsgDo(topic string, receiveDo func(mqMsg MqMsg)) (err error) {
|
||||
if topic == "" {
|
||||
return gerror.New("disk.ListenReceiveMsgDo topic is empty")
|
||||
}
|
||||
|
||||
var (
|
||||
queue = NewDiskQueue(topic, q.config)
|
||||
sleep = time.Second
|
||||
)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
if index, offset, data, err := queue.Read(); err == nil {
|
||||
var mqMsg MqMsg
|
||||
if err = json.Unmarshal(data, &mqMsg); err != nil {
|
||||
g.Log().Warningf(ctx, "disk.ListenReceiveMsgDo Unmarshal err:%+v, topic:%v, data:%+v .", err, topic, string(data))
|
||||
continue
|
||||
}
|
||||
if mqMsg.MsgId != "" {
|
||||
receiveDo(mqMsg)
|
||||
queue.Commit(index, offset)
|
||||
sleep = time.Millisecond * 1
|
||||
}
|
||||
} else {
|
||||
sleep = time.Second
|
||||
}
|
||||
|
||||
time.Sleep(sleep)
|
||||
}
|
||||
}()
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
func RegisterDiskMqProducer(config *disk.Config) (client MqProducer, err error) {
|
||||
return &DiskProducerMq{
|
||||
config: config,
|
||||
producers: make(map[string]*disk.Queue),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SendMsg 按字符串类型生产数据
|
||||
func (d *DiskProducerMq) SendMsg(topic string, body string) (mqMsg MqMsg, err error) {
|
||||
return d.SendByteMsg(topic, []byte(body))
|
||||
}
|
||||
|
||||
// SendByteMsg 生产数据
|
||||
func (d *DiskProducerMq) SendByteMsg(topic string, body []byte) (mqMsg MqMsg, err error) {
|
||||
if topic == "" {
|
||||
return mqMsg, gerror.New("DiskMq topic is empty")
|
||||
}
|
||||
|
||||
mqMsg = MqMsg{
|
||||
RunType: SendMsg,
|
||||
Topic: topic,
|
||||
MsgId: getRandMsgId(),
|
||||
Body: body,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
mqMsgJson, err := json.Marshal(mqMsg)
|
||||
if err != nil {
|
||||
return mqMsg, gerror.New(fmt.Sprint("queue redis 生产者解析json消息失败:", err))
|
||||
}
|
||||
|
||||
queue := d.getProducer(topic)
|
||||
if err = queue.Write(mqMsgJson); err != nil {
|
||||
return mqMsg, gerror.New(fmt.Sprint("queue disk 生产者添加消息失败:", err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d *DiskProducerMq) getProducer(topic string) *disk.Queue {
|
||||
queue, ok := d.producers[topic]
|
||||
if ok {
|
||||
return queue
|
||||
}
|
||||
queue = NewDiskQueue(topic, d.config)
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
d.producers[topic] = queue
|
||||
return queue
|
||||
}
|
||||
|
||||
func NewDiskQueue(topic string, config *disk.Config) *disk.Queue {
|
||||
conf := &disk.Config{
|
||||
Path: fmt.Sprintf(config.Path + "/" + config.GroupName + "/" + topic),
|
||||
BatchSize: config.BatchSize,
|
||||
BatchTime: config.BatchTime * time.Second,
|
||||
SegmentSize: config.SegmentSize,
|
||||
SegmentLimit: config.SegmentLimit,
|
||||
}
|
||||
|
||||
if !gfile.Exists(conf.Path) {
|
||||
if err := gfile.Mkdir(conf.Path); err != nil {
|
||||
g.Log().Errorf(ctx, "NewDiskQueue Failed to create the cache directory. Procedure, err:%+v", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
queue, err := disk.New(conf)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "NewDiskQueue err:%v", err)
|
||||
return nil
|
||||
}
|
||||
return queue
|
||||
}
|
||||
118
server/internal/library/queue/disk/disk.go
Normal file
118
server/internal/library/queue/disk/disk.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package disk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
filePerm = 0600 // 数据写入权限
|
||||
indexFile = ".index" // 消息索引文件
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
GroupName string // 组群名称
|
||||
Path string // 数据存放路径
|
||||
BatchSize int64 // 每N条消息同步一次,batchSize和batchTime满足其一就会同步一次
|
||||
BatchTime time.Duration // 每N秒消息同步一次
|
||||
SegmentSize int64 // 每个topic分片数据文件最大字节
|
||||
SegmentLimit int64 // 每个topic最大分片数据文件数量
|
||||
}
|
||||
|
||||
type Queue struct {
|
||||
sync.RWMutex
|
||||
close bool
|
||||
ticker *time.Ticker
|
||||
wg *sync.WaitGroup
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
writer *writer
|
||||
reader *reader
|
||||
}
|
||||
|
||||
func New(config *Config) (queue *Queue, err error) {
|
||||
if _, err = os.Stat(config.Path); err != nil {
|
||||
return
|
||||
}
|
||||
queue = &Queue{close: false, wg: &sync.WaitGroup{}, writer: &writer{config: config}, reader: &reader{config: config}}
|
||||
queue.ticker = time.NewTicker(config.BatchTime)
|
||||
queue.ctx, queue.cancel = context.WithCancel(context.TODO())
|
||||
err = queue.reader.restore()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go queue.sync()
|
||||
return
|
||||
}
|
||||
|
||||
// Write data
|
||||
func (q *Queue) Write(data []byte) error {
|
||||
if q.close {
|
||||
return errors.New("closed")
|
||||
}
|
||||
|
||||
q.Lock()
|
||||
defer q.Unlock()
|
||||
|
||||
return q.writer.write(data)
|
||||
}
|
||||
|
||||
// Read data
|
||||
func (q *Queue) Read() (int64, int64, []byte, error) {
|
||||
if q.close {
|
||||
return 0, 0, nil, errors.New("closed")
|
||||
}
|
||||
|
||||
q.RLock()
|
||||
defer q.RUnlock()
|
||||
|
||||
index, offset, data, err := q.reader.read()
|
||||
if err == io.EOF && (q.writer.file == nil || q.reader.file.Name() != q.writer.file.Name()) {
|
||||
_ = q.reader.safeRotate()
|
||||
}
|
||||
return index, offset, data, err
|
||||
}
|
||||
|
||||
// Commit index and offset
|
||||
func (q *Queue) Commit(index int64, offset int64) {
|
||||
if q.close {
|
||||
return
|
||||
}
|
||||
|
||||
ck := &q.reader.checkpoint
|
||||
ck.Index, ck.Offset = index, offset
|
||||
q.reader.sync()
|
||||
}
|
||||
|
||||
// Close Queue
|
||||
func (q *Queue) Close() {
|
||||
if q.close {
|
||||
return
|
||||
}
|
||||
|
||||
q.close = true
|
||||
q.cancel()
|
||||
q.wg.Wait()
|
||||
q.writer.close()
|
||||
q.reader.close()
|
||||
}
|
||||
|
||||
// sync data
|
||||
func (q *Queue) sync() {
|
||||
q.wg.Add(1)
|
||||
defer q.wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-q.ticker.C:
|
||||
q.Lock()
|
||||
q.writer.sync()
|
||||
q.Unlock()
|
||||
case <-q.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
176
server/internal/library/queue/disk/reader.go
Normal file
176
server/internal/library/queue/disk/reader.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package disk
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
errorQueueEmpty = errors.New("queue is empty")
|
||||
)
|
||||
|
||||
type reader struct {
|
||||
file *os.File
|
||||
index int64
|
||||
offset int64
|
||||
reader *bufio.Reader
|
||||
checkpoint checkpoint
|
||||
config *Config
|
||||
}
|
||||
|
||||
type checkpoint struct {
|
||||
Index int64 `json:"index"`
|
||||
Offset int64 `json:"offset"`
|
||||
}
|
||||
|
||||
// read data
|
||||
func (r *reader) read() (int64, int64, []byte, error) {
|
||||
if err := r.check(); err != nil {
|
||||
return r.index, r.offset, nil, err
|
||||
}
|
||||
|
||||
// read a line
|
||||
data, err := r.reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
return r.index, r.offset, nil, err
|
||||
}
|
||||
data = bytes.TrimRight(data, "\n")
|
||||
|
||||
r.offset += int64(len(data)) + 1
|
||||
return r.index, r.offset, data, err
|
||||
}
|
||||
|
||||
// check a new segment
|
||||
func (r *reader) check() error {
|
||||
if r.file != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := r.next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.open(file)
|
||||
}
|
||||
|
||||
func (r *reader) open(file string) (err error) {
|
||||
if r.file, err = os.OpenFile(file, os.O_RDONLY, filePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get file index
|
||||
r.index = r.getIndex(file)
|
||||
|
||||
// seek read offset
|
||||
if _, err = r.file.Seek(r.offset, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.reader = bufio.NewReader(r.file)
|
||||
return nil
|
||||
}
|
||||
|
||||
// safeRotate to next segment
|
||||
func (r *reader) safeRotate() error {
|
||||
// if there is no next file, it is not cleared
|
||||
if _, err := r.next(); err == errorQueueEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
return r.rotate()
|
||||
}
|
||||
|
||||
// rotate to next segment
|
||||
func (r *reader) rotate() error {
|
||||
if r.file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// close segment
|
||||
_ = r.file.Close()
|
||||
r.file, r.offset, r.reader = nil, 0, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// close reader
|
||||
func (r *reader) close() {
|
||||
if r.file == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.file.Close(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
r.file, r.reader, r.index, r.offset = nil, nil, 0, 0
|
||||
}
|
||||
|
||||
// sync index and offset
|
||||
func (r *reader) sync() {
|
||||
name := path.Join(r.config.Path, indexFile)
|
||||
data, _ := json.Marshal(&r.checkpoint)
|
||||
_ = ioutil.WriteFile(name, data, filePerm)
|
||||
}
|
||||
|
||||
// restore index and offset
|
||||
func (r *reader) restore() (err error) {
|
||||
name := path.Join(r.config.Path, indexFile)
|
||||
|
||||
// uninitialized
|
||||
if _, err1 := os.Stat(name); err1 != nil {
|
||||
r.sync()
|
||||
}
|
||||
|
||||
data, _ := ioutil.ReadFile(name)
|
||||
|
||||
_ = json.Unmarshal(data, &r.checkpoint)
|
||||
r.index, r.offset = r.checkpoint.Index, r.checkpoint.Offset
|
||||
if r.index == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if err = r.open(fmt.Sprintf("%s/%d.data", r.config.Path, r.index)); err != nil {
|
||||
r.offset = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// next segment
|
||||
func (r *reader) next() (string, error) {
|
||||
files, err := filepath.Glob(filepath.Join(r.config.Path, "*.data"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sort.Strings(files)
|
||||
|
||||
for _, file := range files {
|
||||
index := r.getIndex(file)
|
||||
if index < r.checkpoint.Index {
|
||||
_ = os.Remove(file) // remove expired segment
|
||||
}
|
||||
|
||||
if index > r.index {
|
||||
return file, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errorQueueEmpty
|
||||
}
|
||||
|
||||
// get segment index
|
||||
func (r *reader) getIndex(filename string) int64 {
|
||||
base := filepath.Base(filename)
|
||||
name := base[0 : len(base)-len(path.Ext(filename))]
|
||||
index, _ := strconv.ParseInt(name, 10, 64)
|
||||
return index
|
||||
}
|
||||
102
server/internal/library/queue/disk/writer.go
Normal file
102
server/internal/library/queue/disk/writer.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package disk
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type writer struct {
|
||||
file *os.File
|
||||
size int64
|
||||
count int64
|
||||
writer *bufio.Writer
|
||||
config *Config
|
||||
}
|
||||
|
||||
// write data
|
||||
func (w *writer) write(data []byte) error {
|
||||
// append newline
|
||||
data = append(data, "\n"...)
|
||||
size := int64(len(data))
|
||||
|
||||
// close current segment for rotate
|
||||
if w.size+size > w.config.SegmentSize {
|
||||
w.close()
|
||||
}
|
||||
|
||||
// create a new segment
|
||||
if w.file == nil {
|
||||
if err := w.open(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// write to buffer
|
||||
if _, err := w.writer.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.size += size
|
||||
|
||||
// sync data to disk
|
||||
w.count++
|
||||
if w.count >= w.config.BatchSize {
|
||||
w.sync()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// create a new segment
|
||||
func (w *writer) open() (err error) {
|
||||
if w.segmentNum() >= w.config.SegmentLimit {
|
||||
return errors.New("segment num exceeds the limit")
|
||||
}
|
||||
|
||||
name := path.Join(w.config.Path, fmt.Sprintf("%013d.data", time.Now().UnixNano()/1e6))
|
||||
if w.file, err = os.OpenFile(name, os.O_CREATE|os.O_WRONLY, filePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.size = 0
|
||||
// disable auto flush
|
||||
w.writer = bufio.NewWriterSize(w.file, int(w.config.SegmentSize))
|
||||
w.writer.Reset(w.file)
|
||||
return err
|
||||
}
|
||||
|
||||
// sync data to disk
|
||||
func (w *writer) sync() {
|
||||
if w.writer == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := w.writer.Flush(); err == nil {
|
||||
w.count = 0
|
||||
}
|
||||
}
|
||||
|
||||
// close segment
|
||||
func (w *writer) close() {
|
||||
if w.file == nil {
|
||||
return
|
||||
}
|
||||
|
||||
w.sync()
|
||||
if err := w.file.Close(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
w.size, w.file, w.writer = 0, nil, nil
|
||||
}
|
||||
|
||||
// segment num
|
||||
func (w *writer) segmentNum() int64 {
|
||||
segments, _ := filepath.Glob(path.Join(w.config.Path, "*.data"))
|
||||
return int64(len(segments))
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
// Package queue
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
package queue
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"hotgo/internal/library/queue/disk"
|
||||
"hotgo/utility/charset"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -31,21 +31,21 @@ const (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Switch bool `json:"switch"`
|
||||
Driver string `json:"driver"`
|
||||
Retry int `json:"retry"`
|
||||
MultiComsumer bool `json:"multiComsumer"`
|
||||
GroupName string `json:"groupName"`
|
||||
Redis RedisConf
|
||||
Rocketmq RocketmqConf
|
||||
Kafka KafkaConf
|
||||
Switch bool `json:"switch"`
|
||||
Driver string `json:"driver"`
|
||||
Retry int `json:"retry"`
|
||||
GroupName string `json:"groupName"`
|
||||
Redis RedisConf
|
||||
Rocketmq RocketmqConf
|
||||
Kafka KafkaConf
|
||||
Disk *disk.Config
|
||||
}
|
||||
|
||||
type RedisConf struct {
|
||||
Address string `json:"address"`
|
||||
Db int `json:"db"`
|
||||
Pass string `json:"pass"`
|
||||
Timeout int `json:"timeout"`
|
||||
Address string `json:"address"`
|
||||
Db int `json:"db"`
|
||||
Pass string `json:"pass"`
|
||||
IdleTimeout int `json:"idleTimeout"`
|
||||
}
|
||||
type RocketmqConf struct {
|
||||
Address []string `json:"address"`
|
||||
@@ -53,9 +53,10 @@ type RocketmqConf struct {
|
||||
}
|
||||
|
||||
type KafkaConf struct {
|
||||
Address []string `json:"address"`
|
||||
Version string `json:"version"`
|
||||
RandClient bool `json:"randClient"`
|
||||
Address []string `json:"address"`
|
||||
Version string `json:"version"`
|
||||
RandClient bool `json:"randClient"`
|
||||
MultiConsumer bool `json:"multiConsumer"`
|
||||
}
|
||||
|
||||
type MqMsg struct {
|
||||
@@ -80,7 +81,7 @@ func init() {
|
||||
mqProducerInstanceMap = make(map[string]MqProducer)
|
||||
mqConsumerInstanceMap = make(map[string]MqConsumer)
|
||||
if err := g.Cfg().MustGet(ctx, "queue").Scan(&config); err != nil {
|
||||
g.Log().Infof(ctx, "queue init err:%+v", err)
|
||||
g.Log().Warning(ctx, "queue init err:%+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,11 +133,13 @@ func NewProducer(groupName string) (mqClient MqProducer, err error) {
|
||||
Addr: config.Redis.Address,
|
||||
Passwd: config.Redis.Pass,
|
||||
DBnum: config.Redis.Db,
|
||||
Timeout: config.Redis.Timeout,
|
||||
Timeout: config.Redis.IdleTimeout,
|
||||
}, PoolOption{
|
||||
5, 50, 5,
|
||||
}, groupName, config.Retry)
|
||||
|
||||
case "disk":
|
||||
config.Disk.GroupName = groupName
|
||||
mqClient, err = RegisterDiskMqProducer(config.Disk)
|
||||
default:
|
||||
err = gerror.New("queue driver is not support")
|
||||
}
|
||||
@@ -154,17 +157,6 @@ func NewProducer(groupName string) (mqClient MqProducer, err error) {
|
||||
|
||||
// NewConsumer 初始化消费者实例
|
||||
func NewConsumer(groupName string) (mqClient MqConsumer, err error) {
|
||||
randTag := string(charset.RandomCreateBytes(6))
|
||||
|
||||
// 是否支持创建多个消费者
|
||||
if config.MultiComsumer == false {
|
||||
randTag = "001"
|
||||
}
|
||||
|
||||
if item, ok := mqConsumerInstanceMap[groupName+"-"+randTag]; ok {
|
||||
return item, nil
|
||||
}
|
||||
|
||||
if groupName == "" {
|
||||
err = gerror.New("mq groupName is empty.")
|
||||
return
|
||||
@@ -183,6 +175,16 @@ func NewConsumer(groupName string) (mqClient MqConsumer, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
randTag := string(charset.RandomCreateBytes(6))
|
||||
// 是否支持创建多个消费者
|
||||
if config.Kafka.MultiConsumer == false {
|
||||
randTag = "001"
|
||||
}
|
||||
|
||||
if item, ok := mqConsumerInstanceMap[groupName+"-"+randTag]; ok {
|
||||
return item, nil
|
||||
}
|
||||
|
||||
clientId := "HOTGO-Consumer-" + groupName
|
||||
if config.Kafka.RandClient {
|
||||
clientId += "-" + randTag
|
||||
@@ -204,10 +206,13 @@ func NewConsumer(groupName string) (mqClient MqConsumer, err error) {
|
||||
Addr: config.Redis.Address,
|
||||
Passwd: config.Redis.Pass,
|
||||
DBnum: config.Redis.Db,
|
||||
Timeout: config.Redis.Timeout,
|
||||
Timeout: config.Redis.IdleTimeout,
|
||||
}, PoolOption{
|
||||
5, 50, 5,
|
||||
}, groupName)
|
||||
case "disk":
|
||||
config.Disk.GroupName = groupName
|
||||
mqClient, err = RegisterDiskMqConsumer(config.Disk)
|
||||
default:
|
||||
err = gerror.New("queue driver is not support")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package queue
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package queue
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package queue
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package queue
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package response
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user