Dolphin is a code generate tools and web Framework written in Go (Golang), Will reduce the repetitive workload of adding, deleting, revising, and conducting inspections
- Dolphin, Go code generate Framework
- Contents
- MIT License
- The first need Go installed, then you can use the below Go command to install Dolphin.
$ go get -u github.com/2637309949/dolphin/cmd/dolphin
- Create project dir and run dolphin
$ mkdir example && cd example && dolphin init && dolphin build && go run main.go
Output:
time="2020/06/13 11:55:58" level=info msg="grpc listen on port:9081"
time="2020/06/13 11:55:58" level=info msg="http listen on port:8082"
The quasi-directory structure of the project is shown below, The project structure has been simplified as a guideline, such as managing large-scale projects and recommending new sub-projects
.
├── app
│ ├── app.auto.go
│ ├── app.go
│ ├── article.go
│ └── article.go.new
├── app.properties
├── doc
│ └── swagger.yaml
├── go.mod
├── go.sum
├── log
│ └── demo.2020071400
├── main.go
├── model
│ ├── article.auto.go
│ └── article_info.auto.go
├── rpc
│ ├── message.cli.go
│ ├── message.go
│ ├── message.go.new
│ └── proto
│ ├── message.pb.go
│ ├── message.proto
│ └── message.proto.new
├── script
│ ├── apis
│ │ ├── article.js
│ │ └── index.js
│ └── axios.js
├── sql
│ ├── article
│ │ ├── article_page_count.tpl
│ │ └── article_page_select.tpl
│ └── sqlmap
│ └── article.xml
├── srv
│ ├── article.go
│ └── worker_hello.go
├── static
│ ├── files
│ │ ├── 6b7ead55-f663-4340-a594-d282d5baf753.xlsx
│ │ └── 6dc88052-54e0-4aa9-a344-fb2b3c30f9b6.xlsx
│ └── web
│ ├── affirm.html
│ └── login.html
├── util
│ └── tool.go
└── xml
├── application.xml
├── bean
│ └── article_info.xml
├── controller
│ └── article.xml
├── rpc
│ └── message.xml
└── table
└── article.xml
- Generates the code base on XML configuration
- Generates doc base on XML configuration
- Generates SQL base on XML configuration
- Handles the serialization null problem
- Multi-tenant support
- Login / Logout, or single sign on
- Permission Authentication
- Quick excel reporting or parsing
- Support routing caching
- Data permission control
- Log trace record
- RPC remote service
- The k8S deployment file is generated by default
- Support database reverse XML generation
Command options
dolphin, a cli tools for generate golang code
Usage:
dolphin [command]
Available Commands:
build Build project from xml
clean Removing intermediate files
help Help about any command
init Initialize a empty project
Flags:
-h, --help help for dolphin
Use "dolphin [command] --help" for more information about a command.
The build command generates the preset function by executing the built-in Pipeline function, You can specify that only a pipeline will be executed via the @ symbol
dolphin build @table xml/test
Existing built-in Pipeline function:
Function | Action |
---|---|
main | create main file source |
app | create engine template source |
ctr | create controller source |
proto | create proto3 source |
srv | create server source |
model | create model source |
bean | create bean source |
auto | create register source |
tool | create tool source |
sql | create sql source, .sql to .go |
sqlmap | create table sqlmap |
oauth | create oauth h5 template |
script | create js api |
deploy | create k8s template |
doc | create swagger api doc |
table | create table from datasource |
The clean command clears temporary files
dolphin clean
The init command, as stated, generates a series of initialization files
mkdir demo && cd demo && dolphin init
An erp template that uses dolphin background support.
You can find a number of ready-to-run examples at dolphin examples repository.
application label contain app infomation, such as name, package
Example:
<?xml version="1.0" encoding="utf-8" ?>
<application name="demo" desc="template" packagename="demo"/>
application
LabelName | LabelMeaning |
---|---|
name | required, application name |
desc | application desc |
packagename | required, application packagename |
bean, you can declare object in bean, just like spring bean. all bean and model will be placed in the model directory, so you needs another name if the conflict
Example:
<bean name="activity_info" desc="desc" packages="xxx" extends="$applet_activity">
<prop name="code" desc="编码" type="xx.String" />
<prop name="name" desc="名称" type="xx.String" />
</bean>
Generate code:
// Code generated by dol build. DO NOT EDIT.
package model
import (
"github.com/2637309949/dolphin/packages/null"
)
// ArticleInfo defined 文章信息
type ArticleInfo struct {
*Article
// 地址
URL null.String `json:"url" xml:"url"`
}
bean
LabelName | LabelMeaning |
---|---|
name | bean name |
desc | bean desc |
packagename | third party package name,use "," to split |
extends | bean extends |
prop
LabelName | LabelMeaning |
---|---|
name | prop name |
desc | prop desc |
type | prop type |
controller, a collect api, you can declare api prefix
Example:
<controller name="activity" desc="微信活动" />
controller
LabelName | LabelMeaning |
---|---|
name | controller name |
desc | controller desc |
prefix | controller desc |
api, api func in controller. we has some built-in func such as 'add', 'delete', 'update', 'page', 'get', 'tree', or you can refined if you need.
<api name="add" func="add" table="sys_client" desc="添加客户端" method="post">
<param name="user" type="$sys_client" desc="客户端信息" />
<return>
<success type="$success"/>
<failure type="$fail"/>
</return>
</api>
Generate code:
// SysClientAdd api implementation
// @Summary 添加客户端
// @Tags 客户端
// @Accept application/json
// @Param Authorization header string false "认证令牌"
// @Param user body model.SysClient false "客户端信息"
// @Failure 403 {object} model.Fail
// @Success 200 {object} model.Success
// @Failure 500 {object} model.Fail
// @Router /api/sys/client/add [post]
func SysClientAdd(ctx *Context) {
var payload model.SysClient
if err := ctx.ShouldBindBodyWith(&payload, binding.JSON); err != nil {
ctx.Fail(err)
return
}
payload.ID = null.StringFromUUID()
payload.CreateTime = null.TimeFrom(time.Now().Value())
payload.Creater = null.StringFrom(ctx.GetToken().GetUserID())
payload.UpdateTime = null.TimeFrom(time.Now().Value())
payload.Updater = null.StringFrom(ctx.GetToken().GetUserID())
payload.IsDelete = null.IntFrom(0)
payload.AppName = null.StringFrom(viper.GetString("app.name"))
ret, err := ctx.PlatformDB.Insert(&payload)
if err != nil {
ctx.Fail(err)
return
}
ctx.Success(ret)
}
Of course, you can also specify array parameters, which will automatically generate batch added templates.
<api name="batch_add" func="add" table="sys_role_menu" method="post" desc="添加角色菜单">
<param name="role_menu" type="[]$sys_role_menu" desc="角色菜单信息" />
<return>
<success type="$success"/>
<failure type="$fail"/>
</return>
</api>
Generate code:
// SysRoleMenuBatchAdd api implementation
// @Summary 添加角色菜单
// @Tags 角色菜单
// @Accept application/json
// @Param Authorization header string false "认证令牌"
// @Param role_menu body []model.SysRoleMenu false "角色菜单信息"
// @Failure 403 {object} model.Fail
// @Success 200 {object} model.Success
// @Failure 500 {object} model.Fail
// @Router /api/sys/role/menu/batch_add [post]
func SysRoleMenuBatchAdd(ctx *Context) {
var payload []*model.SysRoleMenu
if err := ctx.ShouldBindBodyWith(&payload, binding.JSON); err != nil {
logrus.Error(err)
ctx.Fail(err)
return
}
funk.ForEach(payload, func(form *model.SysRoleMenu) {
form.ID = null.StringFromUUID()
form.CreateTime = null.TimeFrom(time.Now().Value())
form.Creater = null.StringFrom(ctx.GetToken().GetUserID())
form.UpdateTime = null.TimeFrom(time.Now().Value())
form.Updater = null.StringFrom(ctx.GetToken().GetUserID())
form.IsDelete = null.IntFrom(0)
})
payload = funk.Filter(payload, func(form *model.SysRoleMenu) bool {
ext, _ := ctx.DB.Where("role_id=? and menu_id=?", form.RoleId.String,
form.MenuId.String).Exist(new(model.SysRoleMenu))
return !ext
}).([]*model.SysRoleMenu)
ret, err := ctx.DB.Insert(&payload)
if err != nil {
logrus.Error(err)
ctx.Fail(err)
return
}
ctx.Success(ret)
}
The system default templates are soft delete logic, this is also highly recommended. If you need hard delete, please do it yourself
<api name="del" func="delete" table="sys_client" desc="删除客户端" method="delete">
<param name="sys_client" type="$sys_client" desc="客户端" />
<return>
<success type="$success"/>
<failure type="$fail"/>
</return>
</api>
Generate code:
// SysClientDel api implementation
// @Summary 删除客户端
// @Tags 客户端
// @Accept application/json
// @Param Authorization header string false "认证令牌"
// @Param sys_client body model.SysClient false "客户端"
// @Failure 403 {object} model.Fail
// @Success 200 {object} model.Success
// @Failure 500 {object} model.Fail
// @Router /api/sys/client/del [delete]
func SysClientDel(ctx *Context) {
var payload model.SysClient
if err := ctx.ShouldBindBodyWith(&payload, binding.JSON); err != nil {
ctx.Fail(err)
return
}
ret, err := ctx.PlatformDB.In("id", payload.ID.String).Update(&model.SysClient{
UpdateTime: null.TimeFrom(time.Now().Value()),
Updater: null.StringFrom(ctx.GetToken().GetUserID()),
IsDelete: null.IntFrom(1),
})
if err != nil {
ctx.Fail(err)
return
}
ctx.Success(ret)
}
Of course, you can also specify array parameters, which will automatically generate batch deleted templates.
<api name="batch_del" func="delete" table="sys_optionset" method="delete" desc="删除字典">
<param name="user" type="[]$sys_optionset" desc="字典" />
<return>
<success type="$success"/>
<failure type="$fail"/>
</return>
</api>
Generate code:
// SysOptionsetBatchDel api implementation
// @Summary 删除字典
// @Tags 字典
// @Accept application/json
// @Param Authorization header string false "认证令牌"
// @Param user body []model.SysOptionset false "字典"
// @Failure 403 {object} model.Fail
// @Success 200 {object} model.Success
// @Failure 500 {object} model.Fail
// @Router /api/sys/optionset/batch_del [delete]
func SysOptionsetBatchDel(ctx *Context) {
var payload []*model.SysOptionset
var ids []string
if err := ctx.ShouldBindBodyWith(&payload, binding.JSON); err != nil {
logrus.Error(err)
ctx.Fail(err)
return
}
funk.ForEach(payload, func(form model.SysOptionset) {
ids = append(ids, form.ID.String)
})
ret, err := ctx.DB.In("id", ids).Update(&model.SysOptionset{
UpdateTime: null.TimeFrom(time.Now().Value()),
Updater: null.StringFrom(ctx.GetToken().GetUserID()),
IsDelete: null.IntFrom(1),
})
if err != nil {
logrus.Error(err)
ctx.Fail(err)
return
}
ctx.Success(ret)
}
The entire model field definition is null type, so don't worry about the default value types, as long as you reference packages/ xormplus (native xorm has been modified to fit null packages)
<api name="update" func="update" table="sys_client" desc="更新客户端" method="put">
<param name="user" type="$sys_role" desc="客户端信息" />
<return>
<success type="$success"/>
<failure type="$fail"/>
</return>
</api>
Generate code:
// SysClientUpdate api implementation
// @Summary 更新客户端
// @Tags 客户端
// @Accept application/json
// @Param Authorization header string false "认证令牌"
// @Param user body model.SysRole false "客户端信息"
// @Failure 403 {object} model.Fail
// @Success 200 {object} model.Success
// @Failure 500 {object} model.Fail
// @Router /api/sys/client/update [put]
func SysClientUpdate(ctx *Context) {
var payload model.SysRole
if err := ctx.ShouldBindBodyWith(&payload, binding.JSON); err != nil {
ctx.Fail(err)
return
}
payload.Updater = null.StringFrom(ctx.GetToken().GetUserID())
payload.UpdateTime = null.TimeFrom(time.Now().Value())
ret, err := ctx.PlatformDB.ID(payload.ID).Update(&payload)
if err != nil {
ctx.Fail(err)
return
}
ctx.Success(ret)
}
Of course, you can also specify array parameters, which will automatically generate batch updated templates.
<api name="batch_update" func="update" table="article" desc="更新文章" method="put">
<param name="article" type="[]$article" desc="文章信息" />
<return>
<success type="$success"/>
<failure type="$fail"/>
</return>
</api>
Generate code:
// ArticleBatchUpdate api implementation
// @Summary 更新文章
// @Tags 文章
// @Accept application/json
// @Param Authorization header string false "认证令牌"
// @Param article body []model.Article false "文章信息"
// @Failure 403 {object} model.Fail
// @Success 200 {object} model.Success
// @Failure 500 {object} model.Fail
// @Router /api/article/batch_update [put]
func ArticleBatchUpdate(ctx *Context) {
var payload []*model.Article
var err error
var ret []int64
var r int64
if err = ctx.ShouldBindBodyWith(&payload, binding.JSON); err != nil {
logrus.Error(err)
ctx.Fail(err)
return
}
s := ctx.DB.NewSession()
s.Begin()
defer s.Close()
funk.ForEach(payload, func(form model.Article) {
form.Updater = null.StringFrom(ctx.GetToken().GetUserID())
form.UpdateTime = null.TimeFrom(time.Now().Value())
r, err = s.ID(form.ID.String).Update(&form)
ret = append(ret, r)
})
if err != nil {
s.Rollback()
logrus.Error(err)
ctx.Fail(err)
return
}
ctx.Success(ret)
}
The most basic paging interface template is also supported by default.
<api name="page" func="page" table="sys_client" desc="客户端分页查询" method="get">
<param name="page" type="int" value="1" desc="页码"/>
<param name="size" type="int" value="10" desc="单页数"/>
<param name="app_name" type="string" desc="所属应用"/>
<return>
<success type="$success"/>
<failure type="$fail"/>
</return>
</api>
Generate code:
// SysClientPage api implementation
// @Summary 客户端分页查询
// @Tags 客户端
// @Param Authorization header string false "认证令牌"
// @Param page query int false "页码"
// @Param size query int false "单页数"
// @Failure 403 {object} model.Fail
// @Success 200 {object} model.Success
// @Failure 500 {object} model.Fail
// @Router /api/sys/client/page [get]
func SysClientPage(ctx *Context) {
q := ctx.TypeQuery()
q.SetInt("page", 1)
q.SetInt("size", 10)
q.SetString("app_name", viper.GetString("app.name"))
q.SetTags()
ret, err := ctx.PageSearch(ctx.PlatformDB, "sys_client", "page", "sys_client", q.Value())
if err != nil {
ctx.Fail(err)
return
}
ctx.Success(ret)
}
Tree structure is also a common algorithm. The framework integrates a TreeSearch interface for tree retrieval.
<api name="page" func="page" table="sys_menu" desc="菜单分页查询" method="get">
<param name="page" type="int" value="1" desc="页码"/>
<param name="size" type="int" value="10" desc="单页数"/>
<return>
<success type="$success"/>
<failure type="$fail"/>
</return>
</api>
Generate code:
// SysMenuTree api implementation
// @Summary 菜单树形结构
// @Tags 菜单
// @Param Authorization header string false "认证令牌"
// @Failure 403 {object} model.Fail
// @Router /api/sys/menu/tree [get]
func SysMenuTree(ctx *Context) {
q := ctx.TypeQuery()
q.SetString("name")
q.SetRule("sys_menu_tree")
q.SetTags()
ret, err := ctx.TreeSearch(ctx.DB, "sys_menu", "tree", "sys_menu", q.Value())
if err != nil {
ctx.Fail(err)
return
}
ctx.Success(ret)
}
TreeSearch:
Detailed instructions can be found in the TreeSearch source code
// platform/app/app.ctx.go#TreeSearch
func (ctx *Context) TreeSearch(db *xorm.Engine, controller, api, table string,
q map[string]interface{}) (interface{}, error)
Find a single piece of data by unique key.
<api name="get" func="one" table="sys_client" desc="获取客户端信息" method="get">
<param name="id" type="string" desc="客户端id" />
<return>
<success type="$success"/>
<failure type="$fail"/>
</return>
</api>
Generate code:
// SysClientGet api implementation
// @Summary 获取客户端信息
// @Tags 客户端
// @Param Authorization header string false "认证令牌"
// @Param id query string false "客户端id"
// @Failure 403 {object} model.Fail
// @Success 200 {object} model.Success
// @Failure 500 {object} model.Fail
// @Router /api/sys/client/get [get]
func SysClientGet(ctx *Context) {
var entity model.SysClient
id := ctx.Query("id")
_, err := ctx.PlatformDB.ID(id).Get(&entity)
if err != nil {
ctx.Fail(err)
return
}
ctx.Success(entity)
}
In addition to the default interface, custom interfaces are also supported.
<api name="payment" method="post" desc="文章付费">
<param name="article" type="$article_info" desc="文章"/>
<return>
<success type="$success"/>
<failure type="$fail"/>
</return>
</api>
Generate code:
// ArticlePayment api implementation
// @Summary 文章分页查询
// @Tags 文章
// @Accept application/json
// @Param Authorization header string false "认证令牌"
// @Param article body model.ArticleInfo false "文章"
// @Failure 403 {object} model.Fail
// @Success 200 {object} model.Success
// @Failure 500 {object} model.Fail
// @Router /api/article/payment [post]
func ArticlePayment(ctx *Context) {
var payload model.ArticleInfo
if err := ctx.ShouldBindBodyWith(&payload, binding.JSON); err != nil {
ctx.Fail(err)
return
}
ret, err := srv.ArticleAction(payload)
if err != nil {
ctx.Fail(err)
return
}
ctx.Success(ret)
}
api
LabelName | LabelMeaning |
---|---|
name | api name |
desc | api desc |
func | built-in func, 'add', 'delete', 'update', 'page', 'get' |
table | table name if you use built-in func |
method | http method |
roles | roles middles |
cache | cache middles |
param
LabelName | LabelMeaning |
---|---|
name | param name |
desc | param desc |
type | param type |
value | default value |
return
LabelName | LabelMeaning |
---|---|
success | success tag |
failure | success tag |
table, as you khnow, you can defined any table structure as you need. and you should use
null
type if you wan to accept form data that avoid null value problems in golang.
table
LabelName | LabelMeaning |
---|---|
name | table name |
desc | table desc |
packages | third party package name,use "," to split |
column
LabelName | LabelMeaning |
---|---|
name | column name |
desc | column desc |
type | column type |
xorm | xorm tag, please refer to XORM for details |
Example:
<table name="article" desc="文章" packages="github.com/2637309949/dolphin/packages/null">
<column name="id" desc="主键" type="null.String" xorm="varchar(36) notnull unique pk" />
<column name="type" desc="类别" type="null.String" xorm="varchar(36)" />
<column name="creater" desc="创建人" type="null.String" xorm="varchar(36)" />
<column name="create_time" desc="创建时间" type="null.Time" xorm="datetime" />
<column name="updater" desc="最后更新人" type="null.String" xorm="varchar(36)" />
<column name="update_time" desc="最后更新时间" type="null.Time" xorm="datetime" />
<column name="is_delete" desc="删除标记" type="null.Int" xorm="notnull" />
<column name="remark" desc="备注" type="null.String" xorm="varchar(200)" />
</table>
Generate code:
// Code generated by dol build. DO NOT EDIT.
package model
import (
"github.com/2637309949/dolphin/packages/null"
)
// Article defined 文章
type Article struct {
// 主键
ID null.String `xorm:"varchar(36) notnull unique pk 'id'" json:"id" xml:"id"`
// 类别
Type null.String `xorm:"varchar(36) 'type'" json:"type" xml:"type"`
// 创建人
Creater null.String `xorm:"varchar(36) 'creater'" json:"creater" xml:"creater"`
// 创建时间
CreateTime null.Time `xorm:"datetime 'create_time'" json:"create_time" xml:"create_time"`
// 最后更新人
Updater null.String `xorm:"varchar(36) 'updater'" json:"updater" xml:"updater"`
// 最后更新时间
UpdateTime null.Time `xorm:"datetime 'update_time'" json:"update_time" xml:"update_time"`
// 删除标记
IsDelete null.Int `xorm:"notnull 'is_delete'" json:"is_delete" xml:"is_delete"`
// 备注
Remark null.String `xorm:"varchar(200) 'remark'" json:"remark" xml:"remark"`
}
// TableName table name of defined Article
func (m *Article) TableName() string {
return "article"
}
The system adds the null and decimal packages by default, null is used to support data type default issues, and decimal is used to support financial data calculations.
Example:
<table
name="article"
desc="文章"
packages="github.com/2637309949/dolphin/packages/null,
github.com/shopspring/decimal">
<column name="id" desc="主键" type="null.String" xorm="varchar(36) notnull unique pk" />
<column name="reward" desc="打赏" type="decimal.Decimal" xorm="decimal(6,2)" />
<column name="creater" desc="创建人" type="null.String" xorm="varchar(36)" />
<column name="create_time" desc="创建时间" type="null.Time" xorm="datetime" />
<column name="updater" desc="最后更新人" type="null.String" xorm="varchar(36)" />
<column name="update_time" desc="最后更新时间" type="null.Time" xorm="datetime" />
<column name="is_delete" desc="删除标记" type="null.Int" xorm="notnull" />
<column name="remark" desc="备注" type="null.String" xorm="varchar(200)" />
</table>
It is recommended to write the helper tool class in the corresponding model directory.
Example:
//platform/model/sys_user.helper.go
// SetPassword Method to set salt and hash the password for a user
func (m *SysUser) SetPassword(password string) {
b := util.RandString(16, util.RandNumChar)
m.Salt = null.StringFrom(b)
dk, err := scrypt.Key([]byte(password), []byte(m.Salt.String), 512, 8, 1, 64)
if err != nil {
panic(err)
}
m.Password = null.StringFrom(fmt.Sprintf("%x", dk))
}
// ValidPassword Method to check the entered password is correct or not
func (m *SysUser) ValidPassword(password string) bool {
dk, err := scrypt.Key([]byte(password), []byte(m.Salt.String), 512, 8, 1, 64)
if err != nil {
panic(err)
}
return m.Password.String == fmt.Sprintf("%x", dk)
}
rpc, as a microservice interaction, the basic proto file can be generated here in rpc dir, as well as automatic registration in auto file.
<service name="message" desc="消息">
<rpc name="send_mail" desc="发送邮件">
<request type="$article" desc="文章信息"/>
<reply type="$success" desc="文章信息"/>
</rpc>
</service>
Generate code:
proto file:
// Code generated by dol build. Only Generate by tools if not existed,
// your can rewrite platform.App default action
// source: MessageSrv.proto
syntax = "proto3";
package proto;
// MessageSrv defined
service MessageSrv {
rpc SendMail (MessageMail) returns (MessageReply) {}
}
// MessageMail defined
message MessageMail {}
// MessageReply defined
message MessageReply {}
rpc service is automatically generated
rpc srv impl:
// Code generated by dol build. Only Generate by tools if not existed.
// source: MessageSrv.go
package rpc
import (
"demo/rpc/proto"
"golang.org/x/net/context"
)
// MessageSrv defined
type MessageSrv struct{}
// SendMail defined
func (srv *MessageSrv) SendMail(
ctx context.Context,
in *proto.MessageMail) (*proto.MessageReply, error) {
return &proto.MessageReply{}, nil
}
rpc client is automatically generated
rpc cli endpoint:
// Code generated by dol build. Only Generate by tools if not existed.
// source: MessageSrv.cli.go
package rpc
import (
"demo/rpc/proto"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"google.golang.org/grpc"
)
// MessageSrvClient defined
var MessageSrvClient proto.MessageSrvClient
func init() {
opt := grpc.WithInsecure()
conn, err := grpc.Dial(viper.GetString("rpc.message_srv"), opt)
if err != nil {
logrus.Error("grpc dial failed: %v", err)
}
MessageSrvClient = proto.NewMessageSrvClient(conn)
}
rpc server will be automatically registered in app/app.auto.go
// MessageSrv defined
func MessageSrvService(engine *Engine) {
proto.RegisterMessageSrvServer(engine.GRPC, &rpc.MessageSrv{})
}
// SyncService defined
func SyncService() error {
MessageSrvService(App)
return nil
}
The quasi-directory structure of the rpc is shown below:
├── message.cli.go
├── message.go
├── message.go.new
└── proto
├── message.pb.go
├── message.proto
└── message.proto.new
Domain, a model of multi-tenant support core. Application splitting is also supported.
<table name="sys_domain" packages="xx/null" bind="platform">
<column name="id" type="null.String" xorm="varchar(36) notnull unique pk" />
<column name="name" type="null.String" xorm="varchar(36) notnull" />
<column name="app_name" type="null.String" xorm="varchar(36) notnull" />
<column name="domain" type="null.String" xorm="varchar(36) notnull" />
<column name="full_name" type="null.String" xorm="varchar(36)" />
<column name="contact_name" type="null.String" xorm="varchar(36)" />
<column name="contact_email" type="null.String" xorm="varchar(50) " />
<column name="contact_mobile" type="null.String" xorm="varchar(50) " />
<column name="data_source" type="null.String" xorm="varchar(200) notnull" />
<column name="driver_name" type="null.String" xorm="varchar(50) notnull" />
<column name="login_url" type="null.String" xorm="varchar(200)" />
<column name="api_url" type="null.String" xorm="varchar(200)" />
<column name="static_url" type="null.String" xorm="varchar(200)" />
<column name="theme" type="null.String" xorm="varchar(50) " />
<column name="type" type="null.Int" xorm="notnull" />
<column name="status" type="null.Int" xorm="notnull" />
<column name="auth_mode" type="null.Int" xorm="notnull" />
<column name="is_sync" type="null.Int" xorm="notnull" />
<column name="creater" type="null.String" xorm="varchar(36) notnull" />
<column name="create_time" type="null.Time" xorm="datetime notnull" />
<column name="updater" type="null.String" xorm="varchar(36) notnull" />
<column name="update_time" type="null.Time" xorm="datetime notnull" />
<column name="is_delete" type="null.Int" xorm="notnull" />
<column name="remark" type="null.String" xorm="varchar(200)" />
</table>
app_name, desynchronize the model as a tag. if you connect same datasource url from localhost, and you would find all tables in
data_source
of same app_name datasource would be created
Identify different tenants, the logged in user will use the matching domain to find the DB
As shown in the code below
// Auth middles
func Auth(ctx *Context) {
if !ctx.Auth(ctx.Request) {
ctx.Fail(util.ErrInvalidAccessToken, 401)
ctx.Abort()
return
}
if ctx.DB = App.Manager.GetBusinessDB(ctx.GetToken().GetDomain()); ctx.DB == nil {
ctx.Fail(util.ErrInvalidDomain)
ctx.Abort()
return
}
ctx.Set("DB", ctx.DB)
ctx.Set("AuthInfo", ctx.AuthInfo)
ctx.Next()
}
if you want to get datasource of xxx
, you can do the following.
App.Manager.GetBusinessDB("xxx")
All projects that inherit the platform support single sign-on by default, you can deploy independently or directly as SSO Server
Your FrontEnd Project Your BackEnd Project SSO
|| || ||
|| || ||
|| 1. fetch api || ||
|| -------------------------\ || ||
|| unauthorized || ||
|| /------------------------- || ||
|| || ||
|| 2. fetch oauth url || ||
|| ------------------------\ || ||
|| /------------------------- || ||
|| || ||
|| 3. goto sso oauth || goto sso oauth ||
|| ------------------------- || -------------------------\ ||
|| || goto client with code ||
|| || /------------------------- ||
|| || ||
|| || ||
|| 4. redirect and set cookie || get token by code ||
|| || -------------------------\ ||
|| /------------------------- || /------------------------- ||
|| || ||
Code segment in platform, You can carry the status if needed.
// SysCasURL api implementation
// @Summary 授权地址
// @Tags 认证中心
// @Param redirect_uri query string false "定向URL"
// @Param state query string false "状态"
// @Failure 403 {object} model.Fail
// @Success 200 {object} model.Success
// @Failure 500 {object} model.Fail
// @Router /api/sys/cas/url [get]
func SysCasURL(ctx *Context)
Code segment in platform, authentication logic
// SysCasLogin api implementation
// @Summary 用户认证
// @Tags 认证中心
// @Accept multipart/form-data
// @Param username formData string false "用户名称"
// @Param password formData string false "用户密码"
// @Param domain formData string false "用户域"
// @Failure 403 {object} model.Fail
// @Success 200 {object} model.Success
// @Failure 500 {object} model.Fail
// @Router /api/sys/cas/login [post]
func SysCasLogin(ctx *Context)
Code segment in platform, you can rewrite this way if you want to skip Affirm
// SysCasAffirm api implementation
// @Summary 用户授权
// @Tags 认证中心
// @Accept application/json
// @Failure 403 {object} model.Fail
// @Success 200 {object} model.Success
// @Failure 500 {object} model.Fail
// @Router /api/sys/cas/affirm [post]
func SysCasAffirm(ctx *Context)
Code segment in platform, Generate Token by code
// SysCasToken api implementation
// @Summary 获取令牌
// @Tags 认证中心
// @Accept application/json
// @Failure 403 {object} model.Fail
// @Success 200 {object} model.Success
// @Failure 500 {object} model.Fail
// @Router /api/sys/cas/token [post]
func SysCasToken(ctx *Context)
Code segment in client, Fetch token from platform and set cookie
// SysCasOauth2 api implementation
// @Summary 授权回调
// @Tags 认证中心
// @Failure 403 {object} model.Fail
// @Success 200 {object} model.Success
// @Failure 500 {object} model.Fail
// @Router /api/sys/cas/oauth2 [get]
func SysCasOauth2(ctx *Context)
For more information, please check out dolphin-ui dolphin-ui
If you want to go sso auth then you can set auth_mode
to 1
"auth_mode": 1
public/domain.js
window.Domain = {
"name": "localhost",
"full_name": "localhost",
"contact_name": null,
"contact_email": null,
"contact_mobile": null,
"login_url": "localhost",
"api_url": "http://localhost:8082",
"static_url": null,
"theme": "default",
"auth_mode": 1
}
High concurrent requests are processed with built-in load interfaces
// @Summary AddJobHandler
// @Tags worker
func (d *DefaultWorker) AddJobHandler(code string, funk func(model.Worker) (interface{}, error)) {
Example:
demo/srv/article.go
func init() {
// add hello topic handler
app.App.Manager
.Worker()
.AddJobHandler("hello", func(args pModel.Worker) (interface{}, error) {
fmt.Printf("topic=%v, payload=%v", "hello", args.Payload)
return map[string]interface{}{
"score": 99,
}, nil
})
}
// SysWorkerAdd api implementation
// @Summary 添加worker
// @Tags worker
// @Accept application/json
// @Param Authorization header string false "认证令牌"
// @Param worker body model.Worker false "worker信息"
// @Failure 403 {object} model.Fail
// @Success 200 {object} model.Success
// @Failure 500 {object} model.Fail
// @Router /api/sys/worker/add [post]
func SysWorkerAdd(ctx *Context) {
Example:
Request:
POST /api/sys/worker/add HTTP/1.1
Host: localhost:8082
Content-Type: application/json
token: f4c9f457c82c9ee51e3dc50fea74562b64dd9269
Authorization: Bearer BY3KDUJNMWCN-NQJLKQVAW
Cache-Control: no-cache
Postman-Token: 0a39bb7e-d9bf-607d-120d-ffa59102dab8
{
"name": "hello",
"payload": { "user_id": "sdhfusd9f"}
}
Reponse:
{
"code": 200,
"data": {
"code": "fb9b4a91-c918-4d11-a8f7-878e4dd94f70",
"name": "hello",
"status": 100
}
}
// SysWorkerGet api implementation
// @Summary 获取worker信息
// @Tags worker
// @Param Authorization header string false "认证令牌"
// @Param code query string false "worker code"
// @Failure 403 {object} model.Fail
// @Success 200 {object} model.Success
// @Failure 500 {object} model.Fail
// @Router /api/sys/worker/get [get]
func SysWorkerGet(ctx *Context) {
Example:
GET /api/sys/worker/get?code=a4d13b27-4836-4a1b-b6fe-63473716bc4c HTTP/1.1
Host: localhost:8082
Authorization: Bearer BY3KDUJNMWCN-NQJLKQVAW
Cache-Control: no-cache
Postman-Token: 4f1595de-583b-fac6-06bc-2eafad956d40
Response:
{
"code": 200,
"data": {
"code": "a4d13b27-4836-4a1b-b6fe-63473716bc4c",
"name": "hello",
"result": {
"score": 99
},
"status": 103
}
}
Cron is not enabled by default, You can do this by modifying the configurationapp.cron = true
if you need. But Cron is stateful, so it must be deployed separately when applying load.
Add a timer task via AddFunc.
AddFunc(string, func()) (int, error)
app.App.Manager.Cron().AddFunc("*/10 * * * * *", func() {
fmt.Println("hello")
})
Reflesh a timer task via RefreshFunc.
RefreshFunc(int, string) (int, error)
id, _ := app.App.Manager.Cron().AddFunc("*/10 * * * * *", func() {
fmt.Println("hello")
})
app.App.Manager.Cron().RefreshFunc(id, "*/3 * * * * *")
Remove a timer task via DelFunc.
DelFunc(int) error
id, _ := app.App.Manager.Cron().AddFunc("*/10 * * * * *", func() {
fmt.Println("hello")
})
app.App.Manager.Cron().DelFunc(id)
Try a timer task via TryFunc.
TryFunc(int) error
id, _ := app.App.Manager.Cron().AddFunc("*/10 * * * * *", func() {
fmt.Println("hello")
})
app.App.Manager.Cron().TryFunc(id)
Because user info db and the primary business db are separated, so user information needs to be loaded separately.
ctx.PlatformDB
You can load platform info from PlatformDB
, such as sys_user, sys_client, sys_domain
ctx.DB
Else, You should load business info from ctx.DB
Create a new array from inside an array based on the field name.
uids := slice.GetFieldSliceByName(ret.Data, "id", "'%v'").([]string)
roles, err := srv.SysUserGetUserRolesByUID(ctx.DB, strings.Join(uids, ","))
if err != nil {
logrus.Error(err)
ctx.Fail(err)
return
}
Patch the field value to another array.
Example:
err = slice.PatchSliceByField(ret.Data, roles, "id", "user_id", "role_name")(&ret.Data)
if err != nil {
logrus.Error(err)
ctx.Fail(err)
return
}
The framework itself provides the functionality of pprof, note that only the admin role is visible
Install ModHeader in chrome, and add token header for any request.
Goto http://127.0.0.1:8082/debug/pprof/
/debug/pprof/
Types of profiles available:
Count Profile
3 allocs
0 block
0 cmdline
48 goroutine
3 heap
0 mutex
0 profile
16 threadcreate
0 trace
full goroutine stack dump
Profile Descriptions:
allocs: A sampling of all past memory allocations
block: Stack traces that led to blocking on synchronization primitives
cmdline: The command line invocation of the current program
goroutine: Stack traces of all current goroutines
heap: A sampling of memory allocations of live objects. You can specify the gc
GET parameter to run GC before taking the heap sample.
mutex: Stack traces of holders of contended mutexes
profile: CPU profile. You can specify the duration in the seconds GET parameter. After
you get the profile file, use the go tool pprof command to investigate the profile.
threadcreate: Stack traces that led to the creation of new OS threads
trace: A trace of execution of the current program. You can specify the duration in the
seconds GET parameter. After you get the trace file, use the go tool trace command to
investigate the trace.
curl -X POST \
http://127.0.0.1:8081/api/sys/attachment/upload \
-H 'authorization: Bearer MAGFJEYNMPI9XAIK8GQKLA' \
-H 'cache-control: no-cache' \
-H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \
-H 'postman-token: 3ddea434-b9ee-a3e8-516c-0914facc6517' \
-F files=@k8s-eschool-1.json
// KafkaGet api implementation
// @Summary Get kafka info
// @Tags Kafka controller
// @Param Authorization header string false "认证令牌"
// @Param id query string false "kafka id"
// @Failure 403 {object} model.Fail
// @Success 200 {object} model.Success
// @Failure 500 {object} model.Fail
// @Router /api/kafka/get [get]
func KafkaGet(ctx *Context) {
q := ctx.TypeQuery()
q.SetString("id")
q.Value()
ctx.Persist(ctx.DB, "0586e250-5b6c-4a79-9f4a-767a742b7890")
ctx.Remove(ctx.DB, "6ebce24f-6887-4d6d-a62a-2a706fcf1c3f")
ret, err := srv.KafkaConsumer(ctx.Raw(), ctx.DB, q.Value())
if err != nil {
logrus.Error(err)
ctx.Fail(err)
return
}
ctx.Success(ret)
}
// KafkaGet api implementation
// @Summary Get kafka info
// @Tags Kafka controller
// @Param Authorization header string false "认证令牌"
// @Param id query string false "kafka id"
// @Failure 403 {object} model.Fail
// @Success 200 {object} model.Success
// @Failure 500 {object} model.Fail
// @Router /api/kafka/get [get]
func KafkaGet(ctx *Context) {
ctx.RenderFile("examples/kafka/static/files/5066.tmpl", "test.txt",
map[string]interface{
"test": 100,
})
}
// KafkaGet api implementation
// @Summary Get kafka info
// @Tags Kafka controller
// @Param Authorization header string false "认证令牌"
// @Param id query string false "kafka id"
// @Failure 403 {object} model.Fail
// @Success 200 {object} model.Success
// @Failure 500 {object} model.Fail
// @Router /api/kafka/get [get]
func KafkaGet(ctx *Context) {
ctx.RenderHTML("examples/kafka/static/files/5066.tmpl",
map[string]interface{
"test": 100,
})
}
// examples/ami
// AmiAdd api implementation
// @Summary Add ami
// @Tags Ami controller
// @Accept application/json
// @Param Authorization header string false "认证令牌"
// @Param ami_info body model.AmiInfo false "Ami info"
// @Failure 403 {object} model.Fail
// @Success 200 {object} model.Success
// @Failure 500 {object} model.Fail
// @Router /api/ami/submit [post]
func AmiAdd(ctx *Context) {
var payload model.AmiInfo
if err := ctx.ShouldBindBodyWith(&payload, binding.JSON); err != nil {
logrus.Error(err)
ctx.Fail(err)
return
}
ret, err := srv.AmiProducer(ctx.Raw(), ctx.DB, payload)
if err != nil {
logrus.Error(err)
ctx.Fail(err)
return
}
ctx.Success(ret)
}
// examples/ami
// AmiProducer defined srv
func AmiProducer(ctx *gin.Context,
db *xorm.Engine, params model.AmiInfo) (interface{}, error) {
aiStr, err := json.Marshal(params)
if err != nil {
logrus.Error("failed to marshal:", err)
return nil, err
}
AmiProducerConn.Send(string(aiStr))
return nil, nil
}
// examples/ami
// AmiConsumer defined srv
func AmiConsumer(ctx *gin.Context,
db *xorm.Engine, params map[string]interface{}) (interface{}, error) {
defer func() {
if err := recover(); err != nil {
goErr := errors.Wrap(err.(error), 3)
fmt.Print(string(goErr.Stack()))
}
}()
var items []model.AmiInfo
c := AmiConsumerConn.Start()
cwt, cancel := context.WithCancel(context.TODO())
go func(cwt context.Context) {
for {
select {
case m, ok := <-c:
if !ok {
cancel()
return
}
AmiConsumerConn.Ack(m)
value := model.AmiInfo{}
if err := json.Unmarshal([]byte(m.Body), &value); err != nil {
logrus.Error("failed to unmarshal:", err)
}
items = append(items, value)
case <-time.After(3 * time.Second):
cancel()
return
}
}
}(cwt)
<-cwt.Done()
// AmiConsumerConn.Stop()
return items, nil
}
// examples/kafka
// KafkaAdd api implementation
// @Summary Add article
// @Tags Kafka controller
// @Accept application/json
// @Param Authorization header string false "认证令牌"
// @Param kafka body model.KafkaInfo false "Kafka info"
// @Failure 403 {object} model.Fail
// @Success 200 {object} model.Success
// @Failure 500 {object} model.Fail
// @Router /api/kafka/add [post]
func KafkaAdd(ctx *Context) {
var payload model.KafkaInfo
if err := ctx.ShouldBindBodyWith(&payload, binding.JSON); err != nil {
logrus.Error(err)
ctx.Fail(err)
return
}
ret, err := srv.KafkaProducer(ctx.Raw(), ctx.DB, payload)
if err != nil {
logrus.Error(err)
ctx.Fail(err)
return
}
ctx.Success(ret)
}
// examples/kafka
// KafkaProducer defined srv
func KafkaProducer(ctx *gin.Context,
db *xorm.Engine, params model.KafkaInfo) (interface{}, error) {
kpStr, err := json.Marshal(¶ms)
if err != nil {
logrus.Error("failed to marshal:", err)
return nil, err
}
n, err := kafkaConn.WriteMessages(kafka.Message{Value: kpStr})
if err != nil {
logrus.Error("failed to write messages:", err)
return nil, err
}
return n, nil
}
// examples/kafka
func KafkaConsumer(ctx *gin.Context,
db *xorm.Engine, params map[string]interface{}) (interface{}, error) {
defer func() {
if err := recover(); err != nil {
goErr := errors.Wrap(err.(error), 3)
fmt.Print(string(goErr.Stack()))
}
}()
batch := kafkaConn.ReadBatch(10e3, 1e6) // fetch 10KB min, 1MB max
defer batch.Close()
var buffer bytes.Buffer
var items []model.KafkaInfo
for {
b := make([]byte, 10e3) // 10KB max per message
n, err := batch.Read(b)
buffer.Write(b[:n])
if err != nil {
break
}
if bte := buffer.Bytes(); len(bte) > 0 {
value := model.KafkaInfo{}
if err := json.Unmarshal(bte, &value); err != nil {
logrus.Error("failed to unmarshal:", err)
return nil, err
}
items = append(items, value)
}
}
return items, nil
}
<api name="xml" method="get" desc="XML显示" auth="never">
<return>
<success type="$success"/>
<failure type="$fail"/>
</return>
</api>
The default for all interfaces is token authentication
<api name="add" func="add" table="article" method="post" desc="添加文章">
<param name="article" type="$article" desc="文章信息"/>
<return>
<success type="$success"/>
<failure type="$fail"/>
</return>
</api>
// ArticleRoutes defined
func ArticleRoutes(engine *Engine) {
group := engine.Group(viper.GetString("http.prefix"))
group.Handle("POST", "/article/add", Auth("token"), ArticleInstance.Add)
}
curl -X POST \
http://127.0.0.1:8081/api/article/add \
-H 'authorization: Bearer MAGFJEYNMPI9XAIK8GQKLA' \
-H 'cache-control: no-cache' \
-H 'content-type: application/json' \
-H 'postman-token: 458a9045-f7bb-01e1-8369-afb4dd303607' \
-d '{
"name": "info",
}'
In addition to token authentication, a symmetrical encryption is built in
<api name="add" desc="Add Encrypt" method="post" auth="encrypt">
<param name="encrypt_info" type="$encrypt_info" desc="Encrypt info" />
<return>
<success type="$success"/>
<failure type="$fail"/>
</return>
</api>
// EncryptRoutes defined
func EncryptRoutes(engine *Engine) {
group := engine.Group(viper.GetString("http.prefix"))
group.Handle("POST", "/encrypt/add", Auth("encrypt"), EncryptInstance.Add)
}
curl -X POST \
'http://localhost:8082/api/encrypt/add?age=3' \
-H 'authorization: Bearer ZH6_VZQBNTUPYSXR88AZXA' \
-H 'cache-control: no-cache' \
-H 'content-type: application/json' \
-H 'postman-token: 458a9045-f7bb-01e1-8369-afb4dd303607' \
-d '{
"app_id": "Y76U9344RABF4",
"timestamp": 1614734783,
"biz_content": "a=1&b=2",
"sign": "b26sMTyLgK5IaPq0LYBYkPPWmz8BCovGiMfCH1sMZPudS9TFILB7O5eT924s60ALPINHRrSoyuo6rOHNXz0LJA=="
}'
Copyright (c) 2018-2022 Double
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.