Modular Go Web Framework based on GORM and Gin.
- Installation
- Quick start
- Features
- Global configuration
- Data source management
- Use transaction
- RESTful APIs for struct
- Associations
- Password field filter
- Global default callbacks
- Inject custom authentication
- Struct validation
- Modular project structure
- Global log API
- Standard response format
- Get login context
- Goroutine local storage
- Whitelist
- Cache
- Hooks
- Cron
- Captcha
- i18n
- Common utils
- Preset modules
- Security framework
- FAQ
- License
- Thanks to
go get -u github.com/kuuland/kuu
# assume the following codes in kuu.json file
$ cat kuu.json
{
"prefix": "/api",
"db": {
"dialect": "postgres",
"args": "host=127.0.0.1 port=5432 user=root dbname=kuu password=hello sslmode=disable"
},
"redis": {
"addr": "127.0.0.1:6379"
}
}
# assume the following codes in main.go file
$ cat main.go
package main
import (
"github.com/gin-gonic/gin"
_ "github.com/jinzhu/gorm/dialects/postgres"
"github.com/kuuland/kuu"
)
func main() {
r := kuu.Default()
r.Import(kuu.Acc(), kuu.Sys())
r.Run()
}
# run main.go and visit 0.0.0.0:8080 on browser
$ go run main.go
# assume the following codes in kuu.json file
$ cat kuu.json
{
"prefix": "/api",
"cors": true,
"gzip": true,
"gorm:migrate": false,
"db": {
"dialect": "postgres",
"args": "host=127.0.0.1 port=5432 user=root dbname=kuu password=hello sslmode=disable"
},
"redis": {
"addr": "127.0.0.1:6379"
},
"statics": {
"/assets": "assets/",
"/drone_yml": ".drone.yml"
}
}
func main() {
kuu.C().Get("prefix") // output "/api"
kuu.C().GetBool("cors") // output true
kuu.C().GetBool("gorm:migrate") // output true
// load config from kuu.Param store on databases
// only work when the Param.type is json and the Param.value is json object
kuu.C().LoadFromParams("whitelist") // whitelist value is {"enable": true, "lable":"app", "items": ["item-1", "item-2"]}
// and the key startwith: params
kuu.C().GetBool("params.whilelist.enable") // output true
kuu.C().GetString("params.whilelist.label") // output "app"
var items []string
kuu.C().GetInterface("params.whilelist.items", &items)
}
List of preset config:
prefix
- Global routes prefix forkuu.Mod
's Routes.gorm:migrate
- Enable GORM's auto migration for Mod's Models.audit:callbacks
- Register audit callbacks, default istrue
.db
- DB configs.redis
- Redis configs.- use
kuu.GetRedisClient()
get full redis client
- use
cors
- Attaches the official CORS gin's middleware.gzip
- Attaches the gin middleware to enable GZIP support.statics
- Static serves files from the given file system root or serve a single file.whitelist:prefix
- Let whitelist also matches paths with global prefix, default istrue
.ignoreDefaultRootRoute
- Do not mount the default root route, default isfalse
.logs
- Log dir.
Notes: Static paths are automatically added to the whitelist.
Single data source:
{
"db": {
"dialect": "postgres",
"args": "host=127.0.0.1 port=5432 user=root dbname=db1 password=hello sslmode=disable"
}
}
r.GET("/ping", func(c *kuu.Context) {
var users []user
kuu.DB().Find(&users)
c.STD(&users)
})
Multiple data source:
{
"db": [
{
"name": "ds1",
"dialect": "postgres",
"args": "host=127.0.0.1 port=5432 user=root dbname=db1 password=hello sslmode=disable"
},
{
"name": "ds2",
"dialect": "postgres",
"args": "host=127.0.0.1 port=5432 user=root dbname=db1 password=hello sslmode=disable"
}
]
}
r.GET("/ping", func(c *kuu.Context) {
var users []user
kuu.DB("ds1").Find(&users)
c.STD(&users)
})
err := kuu.WithTransaction(func(tx *gorm.DB) error {
// ...
tx.Create(&memberDoc)
if tx.NewRecord(memberDoc) {
return errors.New("Failed to create member profile")
}
// ...
tx.Create(...)
return tx.Error
})
Notes: Remember to return
tx.Error
!!!
Automatically mount RESTful APIs for struct:
type User struct {
kuu.Model `rest:"*"`
Code string
Name string
}
func main() {
kuu.RESTful(r, &User{})
}
[GIN-debug] POST /api/user --> github.com/kuuland/kuu.RESTful.func1 (4 handlers)
[GIN-debug] DELETE /api/user --> github.com/kuuland/kuu.RESTful.func2 (4 handlers)
[GIN-debug] GET /api/user --> github.com/kuuland/kuu.RESTful.func3 (4 handlers)
[GIN-debug] PUT /api/user --> github.com/kuuland/kuu.RESTful.func4 (4 handlers)
On other fields:
type User struct {
kuu.Model
Code string `rest:"*"`
Name string
}
You can also change the default request method:
type User struct {
kuu.Model `rest:"C:POST;U:PUT;R:GET;D:DELETE"`
Code string
Name string
}
func main() {
kuu.RESTful(r, &User{})
}
Or change route path:
type User struct {
kuu.Model `rest:"*" route:"profile"`
Code string
Name string
}
func main() {
kuu.RESTful(r, &User{})
}
[GIN-debug] POST /api/profile --> github.com/kuuland/kuu.RESTful.func1 (4 handlers)
[GIN-debug] DELETE /api/profile --> github.com/kuuland/kuu.RESTful.func2 (4 handlers)
[GIN-debug] GET /api/profile --> github.com/kuuland/kuu.RESTful.func3 (4 handlers)
[GIN-debug] PUT /api/profile --> github.com/kuuland/kuu.RESTful.func4 (4 handlers)
Or unmount:
type User struct {
kuu.Model `rest:"C:-;U:PUT;R:GET;D:-"` // unmount all: `rest:"-"`
Code string
Name string
}
func main() {
kuu.RESTful(r, &User{})
}
curl -X POST \
http://localhost:8080/api/user \
-H 'Content-Type: application/json' \
-d '{
"user": "test",
"pass": "123"
}'
curl -X POST \
http://localhost:8080/api/user \
-H 'Content-Type: application/json' \
-d '[
{
"user": "test1",
"pass": "123456"
},
{
"user": "test2",
"pass": "123456"
},
{
"user": "test3",
"pass": "123456"
}
]'
Request querystring parameters:
curl -X GET \
'http://localhost:8080/api/user?cond={"user":"test"}&sort=id&project=pass'
Key | Desc | Default | Example |
---|---|---|---|
range | data range, allow ALL and PAGE |
PAGE |
range=ALL |
cond | query condition, JSON string | - | cond={"user":"test"} |
sort | order fields | - | sort=id,-user |
project | select fields | - | project=user,pass |
preload | preload fields | - | preload=CreditCards,UserAddresses |
export | export data | - | export=true |
page | current page(required in PAGE mode) |
1 | page=2 |
size | record size per page(required in PAGE mode) |
30 | size=100 |
Query operators:
Operator | Desc | Example |
---|---|---|
$regex |
LIKE | cond={"user":{"$regex":"^test$"}} |
$in |
IN | cond={"id":{"$in":[1,2,5]}} |
$nin |
NOT IN | cond={"id":{"$nin":[1,2,5]}} |
$eq |
Equal | cond={"id":{"$eq":5}} equivalent to cond={"id":5} |
$ne |
NOT Equal | cond={"id":{"$ne":5}} |
$exists |
IS NOT NULL | cond={"pass":{"$exists":true}} |
$gt |
Greater Than | cond={"id":{"$gt":5}} |
$gte |
Greater Than or Equal | cond={"id":{"$gte":5}} |
$lt |
Less Than | cond={"id":{"$lt":20}} |
$lte |
Less Than or Equal | cond={"id":{"$lte":20}} , cond={"id":{"$gte":5,"$lte":20}} |
$and |
AND | cond={"user":"root","$and":[{"pass":"123"},{"pass":{"$regex":"^333"}}]} |
$or |
OR | cond={"user":"root","$or":[{"pass":"123"},{"pass":{"$regex":"^333"}}]} |
Response JSON body:
{
"data": {
"cond": {
"user": "test"
},
"list": [
{
"ID": 3,
"CreatedAt": "2019-05-10T09:19:40.437816Z",
"UpdatedAt": "2019-05-12T07:04:13.583093Z",
"DeletedAt": null,
"User": "test",
"Pass": "123456"
},
{
"ID": 5,
"CreatedAt": "2019-05-10T10:31:43.203526Z",
"UpdatedAt": "2019-05-12T07:04:13.583093Z",
"DeletedAt": null,
"User": "test",
"Pass": "111222333"
}
],
"page": 1,
"range": "PAGE",
"size": 30,
"sort": "id",
"totalpages": 1,
"totalrecords": 2
},
"code": 0,
"msg": ""
}
Key | Desc | Default |
---|---|---|
list | data list | [] |
range | data range, same as request | PAGE |
cond | query condition, same as request | - |
sort | order fields, same as request | - |
project | select fields, same as request | - |
preload | preload fields, same as request | - |
totalrecords | total records | 0 |
page | current page, exist in PAGE mode |
1 |
size | record size per page, exist in PAGE mode |
30 |
totalpages | total pages, exist in PAGE mode |
0 |
curl -X PUT \
http://localhost:8080/api/user \
-H 'Content-Type: application/json' \
-d '{
"cond": {
"id": 5
},
"doc": {
"user": "new username"
}
}'
Notes: Pass only the fields that need to be updated!!!
curl -X PUT \
http://localhost:8080/api/user \
-H 'Content-Type: application/json' \
-d '{
"cond": {
"user": "test"
},
"doc": {
"pass": "newpass"
},
"multi": true
}'
Notes: Pass only the fields that need to be updated!!!
curl -X DELETE \
http://localhost:8080/api/user \
-H 'Content-Type: application/json' \
-d '{
"cond": {
"id": 5
}
}'
curl -X DELETE \
http://localhost:8080/api/user \
-H 'Content-Type: application/json' \
-d '{
"cond": {
"user": "test"
},
"multi": true
}'
curl -X DELETE \
http://localhost:8080/api/user \
-H 'Content-Type: application/json' \
-d '{
"cond": {
"user": "test"
},
"unsoft": true
}'
- if association has a primary key, Kuu will call Update to save it, otherwise it will be created
- If the association has both
ID
andDeletedAt
, Kuu will delete it. - set
"preload=field1,field2"
to preload associations
curl -X PUT \
http://localhost:8080/api/user \
-H 'Content-Type: application/json' \
-d '{
"cond": {
"ID": 50
},
"doc": {
"Emails": [
{
"Email": "[email protected]"
},
{
"Email": "[email protected]"
}
]
}
}'
ID
is required:
curl -X PUT \
http://localhost:8080/api/user \
-d '{
"cond": {
"ID": 50
},
"doc": {
"Emails": [
{
"ID": 101,
"Email": "[email protected]"
},
{
"ID": 159,
"Email": "[email protected]"
}
]
}
}'
Notes: Pass only the fields that need to be updated!!!
ID
and DeletedAt
are required:
curl -X PUT \
http://localhost:8080/api/user \
-H 'Content-Type: application/json' \
-d '{
"cond": {
"ID": 50
},
"doc": {
"Emails": [
{
"ID": 101,
"DeletedAt": "2019-06-25T17:05:06.000Z",
"Email": "[email protected]"
},
{
"ID": 159,
"DeletedAt": "2019-06-25T17:05:06.000Z",
"Email": "[email protected]"
}
]
}
}'
Notes:
DeletedAt
is only used as a flag, and will be re-assigned using server time when deleted.
set "preload=Emails"
to preload associations:
curl -X GET \
'http://localhost:8080/api/user?cond={"ID":115}&preload=Emails'
type User struct {
Model `rest:"*" displayName:"用户" kuu:"password"`
Username string `name:"账号"`
Password string `name:"密码"`
}
users := []User{
{Username: "root", Password: "xxx"},
{Username: "admin", Password: "xxx"},
}
users = kuu.Meta("User").OmitPassword(users) // => []User{ { Username: "root" }, { Username: "admin" } }
You can override the default callbacks:
// Default create callback
kuu.CreateCallback = func(scope *gorm.Scope) {
if !scope.HasError() {
if desc := GetRoutinePrivilegesDesc(); desc != nil {
var (
hasOrgIDField bool = false
orgID uint
hasCreatedByIDField bool = false
createdByID uint
)
if field, ok := scope.FieldByName("OrgID"); ok {
if field.IsBlank {
if err := scope.SetColumn(field.DBName, desc.SignOrgID); err != nil {
_ = scope.Err(fmt.Errorf("自动设置组织ID失败:%s", err.Error()))
return
}
}
hasOrgIDField = ok
orgID = field.Field.Interface().(uint)
}
if field, ok := scope.FieldByName("CreatedByID"); ok {
if err := scope.SetColumn(field.DBName, desc.UID); err != nil {
_ = scope.Err(fmt.Errorf("自动设置创建人ID失败:%s", err.Error()))
return
}
hasCreatedByIDField = ok
createdByID = field.Field.Interface().(uint)
}
if field, ok := scope.FieldByName("UpdatedByID"); ok {
if err := scope.SetColumn(field.DBName, desc.UID); err != nil {
_ = scope.Err(fmt.Errorf("自动设置修改人ID失败:%s", err.Error()))
return
}
}
// 写权限判断
if orgID == 0 {
if hasCreatedByIDField && createdByID != desc.UID {
_ = scope.Err(fmt.Errorf("用户 %d 只拥有个人可写权限", desc.UID))
return
}
} else if hasOrgIDField && !desc.IsWritableOrgID(orgID) {
_ = scope.Err(fmt.Errorf("用户 %d 在组织 %d 中无可写权限", desc.UID, orgID))
return
}
}
}
}
// Default delete callback
kuu.DeleteCallback = func(scope *gorm.Scope) {
if !scope.HasError() {
var extraOption string
if str, ok := scope.Get("gorm:delete_option"); ok {
extraOption = fmt.Sprint(str)
}
deletedAtField, hasDeletedAtField := scope.FieldByName("DeletedAt")
var desc *PrivilegesDesc
if desc = GetRoutinePrivilegesDesc(); desc != nil {
AddDataScopeWritableSQL(scope, desc)
}
if !scope.Search.Unscoped && hasDeletedAtField {
var sql string
if desc != nil {
deletedByField, hasDeletedByField := scope.FieldByName("DeletedByID")
if !scope.Search.Unscoped && hasDeletedByField {
sql = fmt.Sprintf(
"UPDATE %v SET %v=%v,%v=%v%v%v",
scope.QuotedTableName(),
scope.Quote(deletedByField.DBName),
scope.AddToVars(desc.UID),
scope.Quote(deletedAtField.DBName),
scope.AddToVars(gorm.NowFunc()),
AddExtraSpaceIfExist(scope.CombinedConditionSql()),
AddExtraSpaceIfExist(extraOption),
)
}
}
if sql == "" {
sql = fmt.Sprintf(
"UPDATE %v SET %v=%v%v%v",
scope.QuotedTableName(),
scope.Quote(deletedAtField.DBName),
scope.AddToVars(gorm.NowFunc()),
AddExtraSpaceIfExist(scope.CombinedConditionSql()),
AddExtraSpaceIfExist(extraOption),
)
}
scope.Raw(sql).Exec()
} else {
scope.Raw(fmt.Sprintf(
"DELETE FROM %v%v%v",
scope.QuotedTableName(),
AddExtraSpaceIfExist(scope.CombinedConditionSql()),
AddExtraSpaceIfExist(extraOption),
)).Exec()
}
if scope.DB().RowsAffected < 1 {
_ = scope.Err(errors.New("未删除任何记录,请检查更新条件或数据权限"))
return
}
}
}
// Default update callback
kuu.UpdateCallback = func(scope *gorm.Scope) {
if !scope.HasError() {
if desc := GetRoutinePrivilegesDesc(); desc != nil {
// 添加可写权限控制
AddDataScopeWritableSQL(scope, desc)
if err := scope.SetColumn("UpdatedByID", desc.UID); err != nil {
ERROR("自动设置修改人ID失败:%s", err.Error())
}
}
}
}
// Default query callback
kuu.QueryCallback = func(scope *gorm.Scope) {
if !scope.HasError() {
desc := GetRoutinePrivilegesDesc()
if desc == nil {
// 无登录登录态时
return
}
caches := GetRoutineCaches()
if caches != nil {
// 有忽略标记时
if _, ignoreAuth := caches[GLSIgnoreAuthKey]; ignoreAuth {
return
}
// 查询用户菜单时
if _, queryUserMenus := caches[GLSUserMenusKey]; queryUserMenus {
if desc.NotRootUser() {
_, hasCodeField := scope.FieldByName("Code")
_, hasCreatedByIDField := scope.FieldByName("CreatedByID")
if hasCodeField && hasCreatedByIDField {
// 菜单数据权限控制与组织无关,且只有两种情况:
// 1.自己创建的,一定看得到
// 2.别人创建的,必须通过分配操作权限才能看到
scope.Search.Where("(code in (?)) OR (created_by_id = ?)", desc.Codes, desc.UID)
}
}
return
}
}
AddDataScopeReadableSQL(scope, desc)
}
}
// Default validate callback
kuu.ValidateCallback = func(scope *gorm.Scope) {
if !scope.HasError() {
if _, ok := scope.Get("gorm:update_column"); !ok {
result, ok := scope.DB().Get(skipValidations)
if !(ok && result.(bool)) {
scope.CallMethod("Validate")
if scope.Value == nil {
return
}
resource := scope.IndirectValue().Interface()
_, validatorErrors := govalidator.ValidateStruct(resource)
if validatorErrors != nil {
if errs, ok := validatorErrors.(govalidator.Errors); ok {
for _, err := range FlatValidatorErrors(errs) {
if err := scope.DB().AddError(formattedValidError(err, resource)); err != nil {
ERROR("添加验证错误信息失败:%s", err.Error())
}
}
} else {
if err := scope.DB().AddError(validatorErrors); err != nil {
ERROR("添加验证错误信息失败:%s", err.Error())
}
}
}
}
}
}
}
Specify the token type, the default is ADMIN
:
secret, err := kuu.GenToken(kuu.GenTokenDesc{
Payload: jwt.MapClaims{
"MemberID": member.ID,
"CreatedAt": member.CreatedAt,
},
UID: member.UID,
SubDocID: member.ID,
Exp: time.Now().Add(time.Second * time.Duration(kuu.ExpiresSeconds)).Unix(),
Type: "MY_SIGN_TYPE",
})
Inject your rules:
kuu.InjectCreateAuth = func(signType string, auth kuu.AuthProcessorDesc) (replace bool, err error) {
return
}
kuu.InjectWritableAuth = func(signType string, auth kuu.AuthProcessorDesc) (replace bool, err error) {
return
}
kuu.InjectReadableAuth = func(signType string, auth kuu.AuthProcessorDesc) (replace bool, err error) {
return
}
base on govalidator:
// this struct definition will fail govalidator.ValidateStruct() (and the field values do not matter):
type exampleStruct struct {
Name string ``
Email string `valid:"email"`
}
// this, however, will only fail when Email is empty or an invalid email address:
type exampleStruct2 struct {
Name string `valid:"-"`
Email string `valid:"email"`
}
// lastly, this will only fail when Email is an invalid email address but not when it's empty:
type exampleStruct2 struct {
Name string `valid:"-"`
Email string `valid:"email,optional"`
}
// Validate
func (e *exampleStruct2) Validate () error {
}
Kuu will automatically mount routes, middlewares and struct RESTful APIs after Import
:
type User struct {
kuu.Model `rest`
Username string
Password string
}
type Profile struct {
kuu.Model `rest`
Nickname string
Age int
}
func MyMod() *kuu.Mod {
return &kuu.Mod{
Models: []interface{}{
&User{},
&Profile{},
},
Middlewares: gin.HandlersChain{
func(c *gin.Context) {
// Auth middleware
},
},
Routes: kuu.RoutesInfo{
kuu.RouteInfo{
Method: "POST",
Path: "/login",
HandlerFunc: func(c *kuu.Context) {
// POST /login
},
},
kuu.RouteInfo{
Method: "POST",
Path: "/logout",
HandlerFunc: func(c *kuu.Context) {
// POST /logout
},
},
},
}
}
func main() {
r := kuu.Default()
r.Import(kuu.Acc(), kuu.Sys()) // import preset modules
r.Import(MyMod()) // import custom module
}
func main() {
kuu.PRINT("Hello Kuu") // PRINT[0000] Hello Kuu
kuu.DEBUG("Hello Kuu") // DEBUG[0000] Hello Kuu
kuu.WARN("Hello Kuu") // WARN[0000] Hello Kuu
kuu.INFO("Hello Kuu") // INFO[0000] Hello Kuu
kuu.FATAL("Hello Kuu") // FATAL[0000] Hello Kuu
kuu.PANIC("Hello Kuu") // PANIC[0000] Hello Kuu
}
Or with params:
func main() {
kuu.INFO("Hello %s", "Kuu") // INFO[0000] Hello Kuu
}
func main() {
r := kuu.Default()
r.GET("/ping", func(c *kuu.Context) {
c.STD("hello") // response: {"data":"hello","code":0}
c.STD("hello", c.L("ping_success", "Success")) // response: {"data":"hello","code":0,"msg":"Success"}
c.STD(1800) // response: {"data":1800,"code":0}
c.STDErr(c.L("ping_failed_new", "New record failed")) // response: {"code":-1,"msg":"New record failed"}
c.STDErr(c.L("ping_failed_new", "New record failed"), err) // response: {"code":-1,"msg":"New record failed","data":"错误详细描述信息,对应err.Error()"}
c.STDErrHold(c.L("ping_failed_token", "Token decoding failed"), errors.New("token not found")).Code(555).Render() // response: {"code":555,"msg":"Token decoding failed","data":"token not found"}
})
}
Notes:
- If
data == error
, Kuu will callERROR(data)
to output the log. - All message will call
kuu.L(c, msg)
for i18n before the response.
r.GET(func (c *kuu.Context){
c.SignInfo // Login user info
c.PrisDesc // Login user privileges
})
// preset caches
kuu.GetRoutinePrivilegesDesc()
kuu.GetRoutineValues()
kuu.GetRoutineRequestContext()
// custom caches
kuu.GetRoutineCaches()
kuu.SetRoutineCache(key, value)
kuu.GetRoutineCache(key)
kuu.DelRoutineCache(key)
// Ignore default data filters
kuu.IgnoreAuth() // Equivalent to c.IgnoreAuth/kuu.GetRoutineValues().IgnoreAuth
All routes are blocked by the authentication middleware by default. If you want to ignore some routes, please configure the whitelist:
kuu.AddWhitelist("GET /", "GET /user")
kuu.AddWhitelist(regexp.MustCompile("/user"))
Notes: Whitelist also matches paths with global
prefix
. If you don't want this feature, please set"whitelist:prefix":false
.
- hooks keys format:
StructName:Operation
, operation list:- BizBeforeFind
- BizAfterFind
- BizBeforeCreate
- BizAfterCreate
- BizBeforeUpdate
- BizAfterUpdate
- BizBeforeDelete
- BizAfterDelete
kuu.RegisterBizHook("User:BizBeforeCreate", func (scope *kuu.Scope) error {
db := scope.DB // db instance
user, ok := scope.Value.(*User)
fmt.Println(user, ok)
return nil
})
Mainly used to solve circular dependency problems
- is same as gorm hooks, the hooks key format:
StructName:Operation
, operation list:- BeforeSave
- BeforeCreate
- BeforeUpdate
- AfterUpdate
- AfterSave
- AfterCreate
kuu.RegisterGormHook("User:BeforeSave", func(scope *gorm.Scope) error {
return nil
})
kuu.L("acc_logout_failed", "Logout failed").Render() // => Logout failed
kuu.L("fano_table_total", "Total {{total}} items", kuu.M{"total": 500}).Render() // => Total 500 items
- Use a unique
key
- Always set
defaultMessage
func singleMessage(c *kuu.Context) {
failedMessage := c.L("import_failed", "Import failed")
file, _ := c.FormFile("file")
if file == nil {
c.STDErr(failedMessage, errors.New("no 'file' key in form-data"))
return
}
src, err := file.Open()
if err != nil {
c.STDErr(failedMessage, err)
return
}
defer src.Close()
}
func multiMessage(c *kuu.Context) {
var (
phoneIncorrect = c.L("phone_incorrect", "Phone number is incorrect")
passwordIncorrect = c.L("password_incorrect", "The password is incorrect")
)
if err := checkPhoneNumber(...); err != nil {
c.STDErr(phoneIncorrect, err)
return
}
if err := checkPassword(...); err != nil {
c.STDErr(passwordIncorrect, err)
return
}
c.STD(...)
}
register := kuu.NewLangRegister(kuu.DB())
register.SetKey("acc_please_login").Add("Please login", "请登录", "請登錄")
register.SetKey("auth_failed").Add("Authentication failed", "鉴权失败", "鑒權失敗")
register.SetKey("acc_logout_failed").Add("Logout failed", "登出失败", "登出失敗")
register.SetKey("kuu_welcome").Add("Welcome {{name}}", "欢迎{{name}}", "歡迎{{name}}")
func main() {
r := kuu.Default()
// Parse JSON from string
var params map[string]string
kuu.Parse(`{"user":"kuu","pass":"123"}`, ¶ms)
// Formatted as JSON
kuu.Stringify(¶ms)
}
IsBlank
- Check if value is emptyStringify
- Converts value to a JSON stringParse
- Parses a JSON string to the valueEnsureDir
- Ensures that the directory existsCopy
- Copy valuesRandCode
- Generate random codeIf
- Conditional expression
- Accounts module - JWT-based token issuance, login authentication, etc.
- System module - Menu, admin, roles, organization, etc.
- Admin - A React boilerplate.
Kuu is available under the Apache License, Version 2.0.
- JetBrains for their open source license(s).