{"id":15470574,"url":"https://github.com/kuuland/kuu","last_synced_at":"2025-06-15T09:32:32.291Z","repository":{"id":46737785,"uuid":"152005870","full_name":"kuuland/kuu","owner":"kuuland","description":"Modular Go Web Framework based on GORM and Gin.","archived":false,"fork":false,"pushed_at":"2024-11-06T02:56:53.000Z","size":2213,"stargazers_count":19,"open_issues_count":2,"forks_count":6,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-03-29T05:03:40.166Z","etag":null,"topics":["framework","gin","go","gorm","module","web"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kuuland.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":"audit.go","citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-10-08T02:12:57.000Z","updated_at":"2024-12-16T07:17:44.000Z","dependencies_parsed_at":"2024-04-22T04:37:57.529Z","dependency_job_id":"a53448aa-4dd3-4155-811d-78d2f24cfa83","html_url":"https://github.com/kuuland/kuu","commit_stats":{"total_commits":828,"total_committers":10,"mean_commits":82.8,"dds":"0.12560386473429952","last_synced_commit":"6a826294966aef374e0ff1a5d6518769a7ff93a5"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kuuland%2Fkuu","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kuuland%2Fkuu/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kuuland%2Fkuu/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kuuland%2Fkuu/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kuuland","download_url":"https://codeload.github.com/kuuland/kuu/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249199843,"owners_count":21228995,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["framework","gin","go","gorm","module","web"],"created_at":"2024-10-02T02:05:30.715Z","updated_at":"2025-04-16T05:27:23.539Z","avatar_url":"https://github.com/kuuland.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"![Modular Go Web Framework](./docs/logo.png)\n\n[![GoDoc](https://godoc.org/github.com/kuuland/kuu?status.svg)](https://godoc.org/github.com/kuuland/kuu)\n[![Build Status](https://travis-ci.org/kuuland/kuu.svg?branch=master)](https://travis-ci.org/kuuland/kuu)\n[![codecov](https://codecov.io/gh/kuuland/kuu/branch/master/graph/badge.svg)](https://codecov.io/gh/kuuland/kuu)\n\n\u003c!--English | [简体中文](./README-zh_CN.md)--\u003e\n\nModular Go Web Framework based on [GORM](https://github.com/jinzhu/gorm) and [Gin](https://github.com/gin-gonic/gin).\n\n## Contents\n\n- [Installation](#installation)\n- [Quick start](#quick-start)\n- [Features](#features)\n    - [Global configuration](#global-configuration)\n    - [Data source management](#data-source-management)\n    - [Use transaction](#use-transaction)\n    - [RESTful APIs for struct](#restful-apis-for-struct)\n        - [Create Record](#create-record)\n        - [Batch Create](#batch-create)\n        - [Query](#query)\n        - [Update Fields](#update-fields)\n        - [Batch Updates](#batch-updates)\n        - [Delete Record](#delete-record)\n        - [Batch Delete](#batch-delete)\n        - [UnSoft Delete](#unsoft-delete)\n    - [Associations](#associations)\n        - [Create associations](#create-associations)\n        - [Update associations](#update-associations)\n        - [Delete associations](#delete-associations)\n        - [Query associations](#query-associations)\n    - [Password field filter](#password-field-filter)\n    - [Global default callbacks](#global-default-callbacks)\n    - [Inject custom authentication](#inject-custom-authentication)\n    - [Struct validation](#struct-validation)\n    - [Modular project structure](#modular-project-structure)\n    - [Global log API](#global-log-api)\n    - [Standard response format](#standard-response-format)\n    - [Get login context](#get-login-context)\n    - [Goroutine local storage](#goroutine-local-storage)\n    - [Whitelist](#whitelist)\n    - [Cache](#cache)\n    - [Hooks](#hooks)\n    - [Cron](#cron)\n    - [Captcha](#captcha)\n    - [i18n](#i18n)\n        - [Usage](#usage)\n        - [Best Practices](#best-practices)\n        - [Manual Registration](#manual-registration)\n    - [Common utils](#common-utils)\n    - [Preset modules](#preset-modules)\n        - [Accounts module](#preset-modules)\n        - [System module](#preset-modules)\n        - [Admin](#preset-modules)\n    - [Security framework](#security-framework)\n- [FAQ](#faq)\n    - [Why called Kuu?](#why-called-kuu)\n- [License](#license)\n- [Thanks to](#thanks-to)\n\n## Installation\n\n```sh\ngo get -u github.com/kuuland/kuu\n```\n\n## Quick start\n\n```sh\n# assume the following codes in kuu.json file\n$ cat kuu.json\n```\n\n```json\n{\n  \"prefix\": \"/api\",\n  \"db\": {\n    \"dialect\": \"postgres\",\n    \"args\": \"host=127.0.0.1 port=5432 user=root dbname=kuu password=hello sslmode=disable\"\n  },\n  \"redis\": {\n    \"addr\": \"127.0.0.1:6379\"\n  }\n}\n```\n\n```sh\n# assume the following codes in main.go file\n$ cat main.go\n```\n\n```go\npackage main\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n\t_ \"github.com/jinzhu/gorm/dialects/postgres\"\n\t\"github.com/kuuland/kuu\"\n)\n\nfunc main() {\n\tr := kuu.Default()\n\tr.Import(kuu.Acc(), kuu.Sys())\n\tr.Run()\n}\n```\n\n```sh\n# run main.go and visit 0.0.0.0:8080 on browser\n$ go run main.go\n```\n\n## Features\n\n### Global configuration\n\n```sh\n# assume the following codes in kuu.json file\n$ cat kuu.json\n```\n\n```json\n{\n  \"prefix\": \"/api\",\n  \"cors\": true,\n  \"gzip\": true,\n  \"gorm:migrate\": false,\n  \"db\": {\n    \"dialect\": \"postgres\",\n    \"args\": \"host=127.0.0.1 port=5432 user=root dbname=kuu password=hello sslmode=disable\"\n  },\n  \"redis\": {\n    \"addr\": \"127.0.0.1:6379\"\n  },\n  \"statics\": {\n    \"/assets\": \"assets/\",\n    \"/drone_yml\": \".drone.yml\"\n  }\n}\n```\n\n```go\nfunc main() {\n    kuu.C().Get(\"prefix\")              // output \"/api\"\n    kuu.C().GetBool(\"cors\")            // output true\n    kuu.C().GetBool(\"gorm:migrate\")    // output true\n    // load config from kuu.Param store on databases\n    // only work when the Param.type is json and the Param.value is json object\n    kuu.C().LoadFromParams(\"whitelist\") // whitelist value is {\"enable\": true, \"lable\":\"app\", \"items\": [\"item-1\", \"item-2\"]}\n    // and the key startwith: params\n    kuu.C().GetBool(\"params.whilelist.enable\") // output true\n    kuu.C().GetString(\"params.whilelist.label\") // output \"app\"\n    var items []string\n    kuu.C().GetInterface(\"params.whilelist.items\", \u0026items)\n}\n```\n\nList of preset config:\n\n- `prefix` - Global routes prefix for `kuu.Mod`'s Routes.\n- `gorm:migrate` - Enable GORM's auto migration for Mod's Models.\n- `audit:callbacks` - Register audit callbacks, default is `true`.\n- `db` - DB configs.\n- `redis` - Redis configs. \n  - use `kuu.GetRedisClient()` get full redis client\n- `cors` - Attaches the official [CORS](https://github.com/gin-contrib/cors) gin's middleware.\n- `gzip` - Attaches the gin middleware to enable [GZIP](https://github.com/gin-contrib/gzip) support.\n- `statics` - Static serves files from the given file system root or serve a single file.\n- `whitelist:prefix` - Let whitelist also matches paths with global prefix, default is `true`.\n- `ignoreDefaultRootRoute` - Do not mount the default root route, default is `false`.\n- `logs` - Log dir.\n\n\u003e Notes: Static paths are automatically added to the [whitelist](#whitelist).\n\n### Data Source Management\n\nSingle data source:\n\n```json\n{\n  \"db\": {\n    \"dialect\": \"postgres\",\n    \"args\": \"host=127.0.0.1 port=5432 user=root dbname=db1 password=hello sslmode=disable\"\n  }\n}\n```\n\n```go\nr.GET(\"/ping\", func(c *kuu.Context) {\n    var users []user\n    kuu.DB().Find(\u0026users)\n    c.STD(\u0026users)\n})\n```\n\nMultiple data source:\n\n```json\n{\n  \"db\": [\n    {\n      \"name\": \"ds1\",\n      \"dialect\": \"postgres\",\n      \"args\": \"host=127.0.0.1 port=5432 user=root dbname=db1 password=hello sslmode=disable\"\n    },\n    {\n      \"name\": \"ds2\",\n      \"dialect\": \"postgres\",\n      \"args\": \"host=127.0.0.1 port=5432 user=root dbname=db1 password=hello sslmode=disable\"\n    }\n  ]\n}\n```\n\n```go\nr.GET(\"/ping\", func(c *kuu.Context) {\n    var users []user\n    kuu.DB(\"ds1\").Find(\u0026users)\n    c.STD(\u0026users)\n})\n```\n\n### Use transaction\n\n```go\nerr := kuu.WithTransaction(func(tx *gorm.DB) error {\n\t// ...\n    tx.Create(\u0026memberDoc)\n    if tx.NewRecord(memberDoc) {\n        return errors.New(\"Failed to create member profile\")\n    }\n    // ...\n    tx.Create(...)\n    return tx.Error\n})\n```\n\n\u003e Notes: Remember to return `tx.Error`!!!\n\n### RESTful APIs for struct\n\nAutomatically mount RESTful APIs for struct:\n\n```go\ntype User struct {\n\tkuu.Model `rest:\"*\"`\n\tCode string\n\tName string\n}\n\nfunc main() {\n\tkuu.RESTful(r, \u0026User{})\n}\n```\n\n```text\n[GIN-debug] POST   /api/user  --\u003e github.com/kuuland/kuu.RESTful.func1 (4 handlers)\n[GIN-debug] DELETE /api/user  --\u003e github.com/kuuland/kuu.RESTful.func2 (4 handlers)\n[GIN-debug] GET    /api/user  --\u003e github.com/kuuland/kuu.RESTful.func3 (4 handlers)\n[GIN-debug] PUT    /api/user  --\u003e github.com/kuuland/kuu.RESTful.func4 (4 handlers)\n```\n\nOn other fields:\n\n```go\ntype User struct {\n\tkuu.Model\n\tCode string `rest:\"*\"`\n\tName string\n}\n```\n\nYou can also change the default request method:\n\n```go\ntype User struct {\n\tkuu.Model `rest:\"C:POST;U:PUT;R:GET;D:DELETE\"`\n\tCode string\n\tName string\n}\n\nfunc main() {\n\tkuu.RESTful(r, \u0026User{})\n}\n```\n\nOr change route path:\n\n```go\ntype User struct {\n\tkuu.Model `rest:\"*\" route:\"profile\"`\n\tCode string\n\tName string\n}\n\nfunc main() {\n\tkuu.RESTful(r, \u0026User{})\n}\n```\n\n```text\n[GIN-debug] POST   /api/profile  --\u003e github.com/kuuland/kuu.RESTful.func1 (4 handlers)\n[GIN-debug] DELETE /api/profile  --\u003e github.com/kuuland/kuu.RESTful.func2 (4 handlers)\n[GIN-debug] GET    /api/profile  --\u003e github.com/kuuland/kuu.RESTful.func3 (4 handlers)\n[GIN-debug] PUT    /api/profile  --\u003e github.com/kuuland/kuu.RESTful.func4 (4 handlers)\n```\n\nOr unmount:\n\n```go\ntype User struct {\n\tkuu.Model `rest:\"C:-;U:PUT;R:GET;D:-\"` // unmount all: `rest:\"-\"`\n\tCode string\n\tName string\n}\n\nfunc main() {\n\tkuu.RESTful(r, \u0026User{})\n}\n```\n\n#### Create Record\n\n```sh\ncurl -X POST \\\n  http://localhost:8080/api/user \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"user\": \"test\",\n    \"pass\": \"123\"\n}'\n```\n\n#### Batch Create\n\n```sh\ncurl -X POST \\\n  http://localhost:8080/api/user \\\n  -H 'Content-Type: application/json' \\\n  -d '[\n    {\n        \"user\": \"test1\",\n        \"pass\": \"123456\"\n    },\n    {\n        \"user\": \"test2\",\n        \"pass\": \"123456\"\n    },\n    {\n        \"user\": \"test3\",\n        \"pass\": \"123456\"\n    }\n]'\n```\n\n#### Query\n\nRequest querystring parameters:\n\n```sh\ncurl -X GET \\\n  'http://localhost:8080/api/user?cond={\"user\":\"test\"}\u0026sort=id\u0026project=pass'\n```\n\n|  Key  |  Desc  | Default | Example |\n| ------ | ------ | ------ | ------ |\n| range | data range, allow `ALL` and `PAGE` | `PAGE` | `range=ALL` |\n| cond | query condition, JSON string | - | `cond={\"user\":\"test\"}` |\n| sort | order fields | - | `sort=id,-user` |\n| project | select fields | - | `project=user,pass` |\n| preload | preload fields | - | `preload=CreditCards,UserAddresses` |\n| export | export data | - | `export=true` |\n| page | current page(required in `PAGE` mode) | 1 | `page=2` |\n| size | record size per page(required in `PAGE` mode) | 30 | `size=100` |\n\nQuery operators:\n\n| Operator  |  Desc  | Example |\n| ------ | ------ | ------ |\n| `$regex` | LIKE | `cond={\"user\":{\"$regex\":\"^test$\"}}` |\n| `$in` | IN | `cond={\"id\":{\"$in\":[1,2,5]}}` |\n| `$nin` | NOT IN | `cond={\"id\":{\"$nin\":[1,2,5]}}` |\n| `$eq` | Equal | `cond={\"id\":{\"$eq\":5}}` equivalent to `cond={\"id\":5}` |\n| `$ne` | NOT Equal | `cond={\"id\":{\"$ne\":5}}` |\n| `$exists` | IS NOT NULL | `cond={\"pass\":{\"$exists\":true}}` |\n| `$gt` | Greater Than | `cond={\"id\":{\"$gt\":5}}` |\n| `$gte` | Greater Than or Equal | `cond={\"id\":{\"$gte\":5}}` |\n| `$lt` | Less Than | `cond={\"id\":{\"$lt\":20}}` |\n| `$lte` | Less Than or Equal | `cond={\"id\":{\"$lte\":20}}`, `cond={\"id\":{\"$gte\":5,\"$lte\":20}}` |\n| `$and` | AND | `cond={\"user\":\"root\",\"$and\":[{\"pass\":\"123\"},{\"pass\":{\"$regex\":\"^333\"}}]}` |\n| `$or` | OR | `cond={\"user\":\"root\",\"$or\":[{\"pass\":\"123\"},{\"pass\":{\"$regex\":\"^333\"}}]}` |\n\nResponse JSON body:\n\n```json\n{\n    \"data\": {\n        \"cond\": {\n            \"user\": \"test\"\n        },\n        \"list\": [\n            {\n                \"ID\": 3,\n                \"CreatedAt\": \"2019-05-10T09:19:40.437816Z\",\n                \"UpdatedAt\": \"2019-05-12T07:04:13.583093Z\",\n                \"DeletedAt\": null,\n                \"User\": \"test\",\n                \"Pass\": \"123456\"\n            },\n            {\n                \"ID\": 5,\n                \"CreatedAt\": \"2019-05-10T10:31:43.203526Z\",\n                \"UpdatedAt\": \"2019-05-12T07:04:13.583093Z\",\n                \"DeletedAt\": null,\n                \"User\": \"test\",\n                \"Pass\": \"111222333\"\n            }\n        ],\n        \"page\": 1,\n        \"range\": \"PAGE\",\n        \"size\": 30,\n        \"sort\": \"id\",\n        \"totalpages\": 1,\n        \"totalrecords\": 2\n    },\n    \"code\": 0,\n    \"msg\": \"\"\n}\n```\n\n|  Key  |  Desc  | Default |\n| ------ | ------ | ------ |\n| list | data list | `[]` |\n| range | data range, same as request | `PAGE` |\n| cond | query condition, same as request | - |\n| sort | order fields, same as request | - |\n| project | select fields, same as request | - |\n| preload | preload fields, same as request | - |\n| totalrecords | total records | 0 |\n| page | current page, exist in `PAGE` mode | 1 |\n| size | record size per page, exist in `PAGE` mode | 30 |\n| totalpages | total pages, exist in `PAGE` mode  | 0 |\n\n#### Update Fields\n\n```sh\ncurl -X PUT \\\n  http://localhost:8080/api/user \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"cond\": {\n        \"id\": 5\n    },\n    \"doc\": {\n        \"user\": \"new username\"\n    }\n}'\n```\n\n\u003e Notes: Pass **only** the fields that need to be updated!!!\n\n#### Batch Updates\n\n```sh\ncurl -X PUT \\\n  http://localhost:8080/api/user \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"cond\": {\n        \"user\": \"test\"\n    },\n    \"doc\": {\n        \"pass\": \"newpass\"\n    },\n    \"multi\": true\n}'\n```\n\n\u003e Notes: Pass **only** the fields that need to be updated!!!\n\n#### Delete Record\n\n```sh\ncurl -X DELETE \\\n  http://localhost:8080/api/user \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"cond\": {\n        \"id\": 5\n    }\n}'\n```\n\n#### Batch Delete\n\n```sh\ncurl -X DELETE \\\n  http://localhost:8080/api/user \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"cond\": {\n        \"user\": \"test\"\n    },\n    \"multi\": true\n}'\n```\n\n#### UnSoft Delete\n\n```sh\ncurl -X DELETE \\\n  http://localhost:8080/api/user \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"cond\": {\n        \"user\": \"test\"\n    },\n    \"unsoft\": true\n}'\n```\n\n### Associations\n\n![Associations](./docs/associations.png)\n\n1. if association has a primary key, Kuu will call Update to save it, otherwise it will be created\n1. If the association has both `ID` and `DeletedAt`, Kuu will delete it.\n1. set `\"preload=field1,field2\"` to preload associations\n\n#### Create associations\n\n```sh\ncurl -X PUT \\\n  http://localhost:8080/api/user \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"cond\": {\n        \"ID\": 50\n    },\n    \"doc\": {\n        \"Emails\": [\n            {\n                \"Email\": \"test1@example.com\"\n            },\n            {\n                \"Email\": \"test2@example.com\"\n            }\n        ]\n    }\n}'\n```\n\n#### Update associations\n\n`ID` is required:\n\n```sh\ncurl -X PUT \\\n  http://localhost:8080/api/user \\\n  -d '{\n    \"cond\": {\n        \"ID\": 50\n    },\n    \"doc\": {\n        \"Emails\": [\n            {\n                \"ID\": 101,\n                \"Email\": \"test111@example.com\"\n            },\n            {\n                \"ID\": 159,\n                \"Email\": \"test222@example.com\"\n            }\n        ]\n    }\n}'\n```\n\n\u003e Notes: Pass **only** the fields that need to be updated!!!\n\n#### Delete associations\n\n`ID` and `DeletedAt` are required:\n\n```sh\ncurl -X PUT \\\n  http://localhost:8080/api/user \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"cond\": {\n        \"ID\": 50\n    },\n    \"doc\": {\n        \"Emails\": [\n            {\n                \"ID\": 101,\n                \"DeletedAt\": \"2019-06-25T17:05:06.000Z\",\n                \"Email\": \"test111@example.com\"\n            },\n            {\n                \"ID\": 159,\n                \"DeletedAt\": \"2019-06-25T17:05:06.000Z\",\n                \"Email\": \"test222@example.com\"\n            }\n        ]\n    }\n}'\n```\n\n\u003e Notes: `DeletedAt` is only used as a flag, and will be re-assigned using server time when deleted.\n\n#### Query associations\n\nset `\"preload=Emails\"` to preload associations:\n\n```sh\ncurl -X GET \\\n  'http://localhost:8080/api/user?cond={\"ID\":115}\u0026preload=Emails'\n```\n\n### Password field filter\n\n```go\ntype User struct {\n\tModel   `rest:\"*\" displayName:\"用户\" kuu:\"password\"`\n\tUsername    string  `name:\"账号\"`\n\tPassword    string  `name:\"密码\"`\n}\n\nusers := []User{\n    {Username: \"root\", Password: \"xxx\"},\n    {Username: \"admin\", Password: \"xxx\"},\n} \nusers = kuu.Meta(\"User\").OmitPassword(users) // =\u003e []User{ { Username: \"root\" }, { Username: \"admin\" } } \n```\n\n### Global default callbacks\n\nYou can override the default callbacks:\n\n```go\n// Default create callback\nkuu.CreateCallback = func(scope *gorm.Scope) {\n\tif !scope.HasError() {\n\t\tif desc := GetRoutinePrivilegesDesc(); desc != nil {\n\t\t\tvar (\n\t\t\t\thasOrgIDField       bool = false\n\t\t\t\torgID               uint\n\t\t\t\thasCreatedByIDField bool = false\n\t\t\t\tcreatedByID         uint\n\t\t\t)\n\t\t\tif field, ok := scope.FieldByName(\"OrgID\"); ok {\n\t\t\t\tif field.IsBlank {\n\t\t\t\t\tif err := scope.SetColumn(field.DBName, desc.SignOrgID); err != nil {\n\t\t\t\t\t\t_ = scope.Err(fmt.Errorf(\"自动设置组织ID失败：%s\", err.Error()))\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\thasOrgIDField = ok\n\t\t\t\torgID = field.Field.Interface().(uint)\n\t\t\t}\n\t\t\tif field, ok := scope.FieldByName(\"CreatedByID\"); ok {\n\t\t\t\tif err := scope.SetColumn(field.DBName, desc.UID); err != nil {\n\t\t\t\t\t_ = scope.Err(fmt.Errorf(\"自动设置创建人ID失败：%s\", err.Error()))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\thasCreatedByIDField = ok\n\t\t\t\tcreatedByID = field.Field.Interface().(uint)\n\t\t\t}\n\t\t\tif field, ok := scope.FieldByName(\"UpdatedByID\"); ok {\n\t\t\t\tif err := scope.SetColumn(field.DBName, desc.UID); err != nil {\n\t\t\t\t\t_ = scope.Err(fmt.Errorf(\"自动设置修改人ID失败：%s\", err.Error()))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\t// 写权限判断\n\t\t\tif orgID == 0 {\n\t\t\t\tif hasCreatedByIDField \u0026\u0026 createdByID != desc.UID {\n\t\t\t\t\t_ = scope.Err(fmt.Errorf(\"用户 %d 只拥有个人可写权限\", desc.UID))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} else if hasOrgIDField \u0026\u0026 !desc.IsWritableOrgID(orgID) {\n\t\t\t\t_ = scope.Err(fmt.Errorf(\"用户 %d 在组织 %d 中无可写权限\", desc.UID, orgID))\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\n\n// Default delete callback\nkuu.DeleteCallback = func(scope *gorm.Scope) {\n\tif !scope.HasError() {\n\t\tvar extraOption string\n\t\tif str, ok := scope.Get(\"gorm:delete_option\"); ok {\n\t\t\textraOption = fmt.Sprint(str)\n\t\t}\n\n\t\tdeletedAtField, hasDeletedAtField := scope.FieldByName(\"DeletedAt\")\n\t\tvar desc *PrivilegesDesc\n\t\tif desc = GetRoutinePrivilegesDesc(); desc != nil {\n\t\t\tAddDataScopeWritableSQL(scope, desc)\n\t\t}\n\n\t\tif !scope.Search.Unscoped \u0026\u0026 hasDeletedAtField {\n\t\t\tvar sql string\n\t\t\tif desc != nil {\n\t\t\t\tdeletedByField, hasDeletedByField := scope.FieldByName(\"DeletedByID\")\n\t\t\t\tif !scope.Search.Unscoped \u0026\u0026 hasDeletedByField {\n\t\t\t\t\tsql = fmt.Sprintf(\n\t\t\t\t\t\t\"UPDATE %v SET %v=%v,%v=%v%v%v\",\n\t\t\t\t\t\tscope.QuotedTableName(),\n\t\t\t\t\t\tscope.Quote(deletedByField.DBName),\n\t\t\t\t\t\tscope.AddToVars(desc.UID),\n\t\t\t\t\t\tscope.Quote(deletedAtField.DBName),\n\t\t\t\t\t\tscope.AddToVars(gorm.NowFunc()),\n\t\t\t\t\t\tAddExtraSpaceIfExist(scope.CombinedConditionSql()),\n\t\t\t\t\t\tAddExtraSpaceIfExist(extraOption),\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif sql == \"\" {\n\t\t\t\tsql = fmt.Sprintf(\n\t\t\t\t\t\"UPDATE %v SET %v=%v%v%v\",\n\t\t\t\t\tscope.QuotedTableName(),\n\t\t\t\t\tscope.Quote(deletedAtField.DBName),\n\t\t\t\t\tscope.AddToVars(gorm.NowFunc()),\n\t\t\t\t\tAddExtraSpaceIfExist(scope.CombinedConditionSql()),\n\t\t\t\t\tAddExtraSpaceIfExist(extraOption),\n\t\t\t\t)\n\t\t\t}\n\t\t\tscope.Raw(sql).Exec()\n\t\t} else {\n\t\t\tscope.Raw(fmt.Sprintf(\n\t\t\t\t\"DELETE FROM %v%v%v\",\n\t\t\t\tscope.QuotedTableName(),\n\t\t\t\tAddExtraSpaceIfExist(scope.CombinedConditionSql()),\n\t\t\t\tAddExtraSpaceIfExist(extraOption),\n\t\t\t)).Exec()\n\t\t}\n\t\tif scope.DB().RowsAffected \u003c 1 {\n\t\t\t_ = scope.Err(errors.New(\"未删除任何记录，请检查更新条件或数据权限\"))\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// Default update callback\nkuu.UpdateCallback = func(scope *gorm.Scope) {\n\tif !scope.HasError() {\n\t\tif desc := GetRoutinePrivilegesDesc(); desc != nil {\n\t\t\t// 添加可写权限控制\n\t\t\tAddDataScopeWritableSQL(scope, desc)\n\t\t\tif err := scope.SetColumn(\"UpdatedByID\", desc.UID); err != nil {\n\t\t\t\tERROR(\"自动设置修改人ID失败：%s\", err.Error())\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Default query callback\nkuu.QueryCallback = func(scope *gorm.Scope) {\n\tif !scope.HasError() {\n\t\tdesc := GetRoutinePrivilegesDesc()\n\t\tif desc == nil {\n\t\t\t// 无登录登录态时\n\t\t\treturn\n\t\t}\n\n\t\tcaches := GetRoutineCaches()\n\t\tif caches != nil {\n\t\t\t// 有忽略标记时\n\t\t\tif _, ignoreAuth := caches[GLSIgnoreAuthKey]; ignoreAuth {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// 查询用户菜单时\n\t\t\tif _, queryUserMenus := caches[GLSUserMenusKey]; queryUserMenus {\n\t\t\t\tif desc.NotRootUser() {\n\t\t\t\t\t_, hasCodeField := scope.FieldByName(\"Code\")\n\t\t\t\t\t_, hasCreatedByIDField := scope.FieldByName(\"CreatedByID\")\n\t\t\t\t\tif hasCodeField \u0026\u0026 hasCreatedByIDField {\n\t\t\t\t\t\t// 菜单数据权限控制与组织无关，且只有两种情况：\n\t\t\t\t\t\t// 1.自己创建的，一定看得到\n\t\t\t\t\t\t// 2.别人创建的，必须通过分配操作权限才能看到\n\t\t\t\t\t\tscope.Search.Where(\"(code in (?)) OR (created_by_id = ?)\", desc.Codes, desc.UID)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tAddDataScopeReadableSQL(scope, desc)\n\t}\n}\n\n// Default validate callback\nkuu.ValidateCallback = func(scope *gorm.Scope) {\n\tif !scope.HasError() {\n\t\tif _, ok := scope.Get(\"gorm:update_column\"); !ok {\n\t\t\tresult, ok := scope.DB().Get(skipValidations)\n\t\t\tif !(ok \u0026\u0026 result.(bool)) {\n\t\t\t\tscope.CallMethod(\"Validate\")\n\t\t\t\tif scope.Value == nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tresource := scope.IndirectValue().Interface()\n\t\t\t\t_, validatorErrors := govalidator.ValidateStruct(resource)\n\t\t\t\tif validatorErrors != nil {\n\t\t\t\t\tif errs, ok := validatorErrors.(govalidator.Errors); ok {\n\t\t\t\t\t\tfor _, err := range FlatValidatorErrors(errs) {\n\t\t\t\t\t\t\tif err := scope.DB().AddError(formattedValidError(err, resource)); err != nil {\n\t\t\t\t\t\t\t\tERROR(\"添加验证错误信息失败：%s\", err.Error())\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif err := scope.DB().AddError(validatorErrors); err != nil {\n\t\t\t\t\t\t\tERROR(\"添加验证错误信息失败：%s\", err.Error())\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\n### Inject custom authentication\n\nSpecify the token type, the default is `ADMIN`:\n\n```go\nsecret, err := kuu.GenToken(kuu.GenTokenDesc{\n    Payload: jwt.MapClaims{\n        \"MemberID\":  member.ID,\n        \"CreatedAt\": member.CreatedAt,\n    },\n    UID:      member.UID,\n    SubDocID: member.ID,\n    Exp:      time.Now().Add(time.Second * time.Duration(kuu.ExpiresSeconds)).Unix(),\n    Type:     \"MY_SIGN_TYPE\",\n})\n```\n\nInject your rules:\n\n```go\nkuu.InjectCreateAuth = func(signType string, auth kuu.AuthProcessorDesc) (replace bool, err error) {\n    return\n}\nkuu.InjectWritableAuth = func(signType string, auth kuu.AuthProcessorDesc) (replace bool, err error) {\n    return\n}\nkuu.InjectReadableAuth = func(signType string, auth kuu.AuthProcessorDesc) (replace bool, err error) {\n    return\n}\n```\n\n### Struct validation\n\nbase on [govalidator](https://github.com/asaskevich/govalidator):\n\n```go\n// this struct definition will fail govalidator.ValidateStruct() (and the field values do not matter):\ntype exampleStruct struct {\n  Name  string ``\n  Email string `valid:\"email\"`\n}\n\n// this, however, will only fail when Email is empty or an invalid email address:\ntype exampleStruct2 struct {\n  Name  string `valid:\"-\"`\n  Email string `valid:\"email\"`\n}\n\n// lastly, this will only fail when Email is an invalid email address but not when it's empty:\ntype exampleStruct2 struct {\n  Name  string `valid:\"-\"`\n  Email string `valid:\"email,optional\"`\n}\n\n// Validate\nfunc (e *exampleStruct2) Validate () error {\n}\n```\n\n### Modular project structure\n\nKuu will automatically mount routes, middlewares and struct RESTful APIs after `Import`:\n\n```go\ntype User struct {\n\tkuu.Model `rest`\n\tUsername string\n\tPassword string\n}\n\ntype Profile struct {\n\tkuu.Model `rest`\n\tNickname string\n\tAge int\n}\n\nfunc MyMod() *kuu.Mod {\n\treturn \u0026kuu.Mod{\n\t\tModels: []interface{}{\n\t\t\t\u0026User{},\n\t\t\t\u0026Profile{},\n\t\t},\n\t\tMiddlewares: gin.HandlersChain{\n\t\t\tfunc(c *gin.Context) {\n\t\t\t\t// Auth middleware\n\t\t\t},\n\t\t},\n\t\tRoutes: kuu.RoutesInfo{\n\t\t\tkuu.RouteInfo{\n\t\t\t\tMethod: \"POST\",\n\t\t\t\tPath:   \"/login\",\n\t\t\t\tHandlerFunc: func(c *kuu.Context) {\n\t\t\t\t\t// POST /login\n\t\t\t\t},\n\t\t\t},\n\t\t\tkuu.RouteInfo{\n\t\t\t\tMethod: \"POST\",\n\t\t\t\tPath:   \"/logout\",\n\t\t\t\tHandlerFunc: func(c *kuu.Context) {\n\t\t\t\t\t// POST /logout\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc main() {\n\tr := kuu.Default()\n\tr.Import(kuu.Acc(), kuu.Sys())     // import preset modules\n\tr.Import(MyMod())                       // import custom module\n}\n```\n\n### Global log API\n\n```go\nfunc main() {\n\tkuu.PRINT(\"Hello Kuu\")  // PRINT[0000] Hello Kuu\n\tkuu.DEBUG(\"Hello Kuu\")  // DEBUG[0000] Hello Kuu\n\tkuu.WARN(\"Hello Kuu\")   // WARN[0000] Hello Kuu\n\tkuu.INFO(\"Hello Kuu\")   // INFO[0000] Hello Kuu\n\tkuu.FATAL(\"Hello Kuu\")  // FATAL[0000] Hello Kuu\n\tkuu.PANIC(\"Hello Kuu\")  // PANIC[0000] Hello Kuu\n}\n```\n\nOr with params:\n\n```go\nfunc main() {\n\tkuu.INFO(\"Hello %s\", \"Kuu\")  // INFO[0000] Hello Kuu\n}\n```\n\n### Standard response format\n\n```go\nfunc main() {\n\tr := kuu.Default()\n\tr.GET(\"/ping\", func(c *kuu.Context) {\n        c.STD(\"hello\")                                  // response: {\"data\":\"hello\",\"code\":0}\n        c.STD(\"hello\", c.L(\"ping_success\", \"Success\"))  // response: {\"data\":\"hello\",\"code\":0,\"msg\":\"Success\"}\n        c.STD(1800)                                     // response: {\"data\":1800,\"code\":0}\n        c.STDErr(c.L(\"ping_failed_new\", \"New record failed\"))       // response: {\"code\":-1,\"msg\":\"New record failed\"}\n        c.STDErr(c.L(\"ping_failed_new\", \"New record failed\"), err)  // response: {\"code\":-1,\"msg\":\"New record failed\",\"data\":\"错误详细描述信息，对应err.Error()\"}\n        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\"}\n    })\n}\n```\n\n**Notes:**\n\n- If `data == error`, Kuu will call `ERROR(data)` to output the log.\n- All message will call `kuu.L(c, msg)` for i18n before the response.\n\n### Get login context\n\n```go\nr.GET(func (c *kuu.Context){\n\tc.SignInfo // Login user info\n\tc.PrisDesc // Login user privileges\n})\n```\n\n### Goroutine local storage\n\n```go\n// preset caches\nkuu.GetRoutinePrivilegesDesc()\nkuu.GetRoutineValues()\nkuu.GetRoutineRequestContext()\n\n// custom caches\nkuu.GetRoutineCaches()\nkuu.SetRoutineCache(key, value)\nkuu.GetRoutineCache(key)\nkuu.DelRoutineCache(key)\n\n// Ignore default data filters\nkuu.IgnoreAuth() // Equivalent to c.IgnoreAuth/kuu.GetRoutineValues().IgnoreAuth\n```\n\n### Whitelist\n\nAll routes are blocked by the authentication middleware by default. If you want to ignore some routes, please configure the whitelist:\n\n```go\nkuu.AddWhitelist(\"GET /\", \"GET /user\")\nkuu.AddWhitelist(regexp.MustCompile(\"/user\"))\n```\n\n\u003e Notes: Whitelist also matches paths with global `prefix`. If you don't want this feature, please set `\"whitelist:prefix\":false`.\n\n### Hooks\n#### Rest api hooks\n- hooks keys format: `StructName:Operation`, operation list:\n  - BizBeforeFind\n  - BizAfterFind\n  - BizBeforeCreate\n  - BizAfterCreate\n  - BizBeforeUpdate\n  - BizAfterUpdate\n  - BizBeforeDelete\n  - BizAfterDelete\n\n```go\nkuu.RegisterBizHook(\"User:BizBeforeCreate\", func (scope *kuu.Scope) error {\n    db := scope.DB // db instance\n    user, ok := scope.Value.(*User)\n\tfmt.Println(user, ok)\n    return nil\n})\n```\n\n#### Gorm global hooks\n\u003eMainly used to solve circular dependency problems\n- is same as gorm hooks, the hooks key format: `StructName:Operation`, operation list:\n  - BeforeSave\n  - BeforeCreate\n  - BeforeUpdate\n  - AfterUpdate\n  - AfterSave\n  - AfterCreate\n```go\nkuu.RegisterGormHook(\"User:BeforeSave\", func(scope *gorm.Scope) error {\n    return nil\n})\n```\n\n### i18n\n\n```go\nkuu.L(\"acc_logout_failed\", \"Logout failed\").Render()                             // =\u003e Logout failed\nkuu.L(\"fano_table_total\", \"Total {{total}} items\", kuu.M{\"total\": 500}).Render() // =\u003e Total 500 items\n```\n\n- Use a unique `key`\n- Always set `defaultMessage`\n\n#### Best Practices\n```go\nfunc singleMessage(c *kuu.Context) {\n\tfailedMessage := c.L(\"import_failed\", \"Import failed\")\n\tfile, _ := c.FormFile(\"file\")\n\tif file == nil {\n\t\tc.STDErr(failedMessage, errors.New(\"no 'file' key in form-data\"))\n\t\treturn\n\t}\n\tsrc, err := file.Open()\n\tif err != nil {\n\t\tc.STDErr(failedMessage, err)\n\t\treturn\n\t}\n\tdefer src.Close()\n}\n\nfunc multiMessage(c *kuu.Context) {\n\tvar (\n\t\tphoneIncorrect = c.L(\"phone_incorrect\", \"Phone number is incorrect\")\n\t\tpasswordIncorrect = c.L(\"password_incorrect\", \"The password is incorrect\")\n\t)\n\tif err := checkPhoneNumber(...); err != nil {\n\t\tc.STDErr(phoneIncorrect, err)\n\t\treturn\n\t}\n\tif err := checkPassword(...); err != nil {\n\t\tc.STDErr(passwordIncorrect, err)\n\t\treturn\n\t}\n\tc.STD(...)\n}\n```\n#### Manual Registration\n\n```go\nregister := kuu.NewLangRegister(kuu.DB())\nregister.SetKey(\"acc_please_login\").Add(\"Please login\", \"请登录\", \"請登錄\")\nregister.SetKey(\"auth_failed\").Add(\"Authentication failed\", \"鉴权失败\", \"鑒權失敗\")\nregister.SetKey(\"acc_logout_failed\").Add(\"Logout failed\", \"登出失败\", \"登出失敗\")\nregister.SetKey(\"kuu_welcome\").Add(\"Welcome {{name}}\", \"欢迎{{name}}\", \"歡迎{{name}}\")\n```\n\n### Common utils\n\n```go\nfunc main() {\n\tr := kuu.Default()\n\t// Parse JSON from string\n\tvar params map[string]string\n\tkuu.Parse(`{\"user\":\"kuu\",\"pass\":\"123\"}`, \u0026params)\n\t// Formatted as JSON\n\tkuu.Stringify(\u0026params)\n}\n```\n- `IsBlank` - Check if value is empty\n- `Stringify` - Converts value to a JSON string \n- `Parse` - Parses a JSON string to the value\n- `EnsureDir` - Ensures that the directory exists\n- `Copy` - Copy values\n- `RandCode` - Generate random code\n- `If` - Conditional expression\n\n### Preset modules\n\n- [Accounts module](https://github.com/kuuland/kuu/blob/master/acc.go#L153) - JWT-based token issuance, login authentication, etc.\n- [System module](https://github.com/kuuland/kuu/blob/master/sys.go#L564) - Menu, admin, roles, organization, etc.\n- [Admin](https://github.com/kuuland/ui) - A React boilerplate.\n\n### Security framework\n\n![Kuu Security framework](./docs/kuu_security_framework.png)\n\n## FAQ\n\n### Why called Kuu?\n\n\u003e [Kuu and Shino](https://www.youtube.com/results?search_query=kuu+shino)\n\n![Kuu and Shino](./docs/kuu_and_shino.png)\n\n## License\n\nKuu is available under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html).\n\n## Thanks to\n\n* [JetBrains](https://www.jetbrains.com/?from=Kuu) for their open source license(s).\n\n[![JetBrains](./docs/jetbrains.png)](https://www.jetbrains.com/?from=Kuu)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkuuland%2Fkuu","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkuuland%2Fkuu","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkuuland%2Fkuu/lists"}