From b68316a7785749165c0bc4750988163f3d6ffc31 Mon Sep 17 00:00:00 2001 From: Rogee Date: Wed, 18 Dec 2024 19:42:10 +0800 Subject: [PATCH] feat: init repo --- .github/copilot-instructions.md | 6 + LICENSE | 0 cmd/new.go | 135 +++++++++++++ go.mod | 15 ++ go.sum | 26 +++ main.go | 20 ++ templates/project/-gitignore.tpl | 28 +++ templates/project/Makefile.tpl | 50 +++++ templates/project/config.toml.tpl | 27 +++ .../project/database/-transform.yaml.tpl | 4 + templates/project/database/database.go.tpl | 8 + .../project/database/migrations/-gitkeep | 0 .../project/database/models/-gitkeep.tpl | 0 templates/project/docs/ember.go.tpl | 6 + templates/project/fixtures/-gitkeep | 0 templates/project/go.mod.tpl | 3 + templates/project/main.go.tpl | 23 +++ templates/project/main_test.go.tpl | 16 ++ templates/project/pkg/consts/consts.go.tpl | 8 + templates/project/pkg/consts/ctx.gen.go.tpl | 179 ++++++++++++++++++ templates/project/pkg/consts/ctx.go.tpl | 9 + templates/project/pkg/dao.go.tpl | 13 ++ templates/project/pkg/data_structures.go.tpl | 71 +++++++ templates/project/pkg/db/db.go.tpl | 28 +++ templates/project/pkg/db/pagination.go.tpl | 7 + templates/project/pkg/errorx/error.go.tpl | 31 +++ templates/project/pkg/pg/.gitkeep | 0 .../project/pkg/service/http/http.go.tpl | 65 +++++++ .../pkg/service/migrate/migrate.go.tpl | 52 +++++ .../project/pkg/service/model/gen.go.tpl | 129 +++++++++++++ templates/project/pkg/service/service.go.tpl | 13 ++ .../project/pkg/service/testx/testing.go.tpl | 29 +++ templates/project/pkg/utils/buffer.go.tpl | 26 +++ templates/project/providers/app/app.go.tpl | 18 ++ .../project/providers/app/config.gen.go.tpl | 179 ++++++++++++++++++ templates/project/providers/app/config.go.tpl | 45 +++++ .../project/providers/hashids/config.go.tpl | 23 +++ .../project/providers/hashids/hashids.go.tpl | 35 ++++ .../project/providers/http/config.go.tpl | 38 ++++ .../project/providers/http/engine.go.tpl | 92 +++++++++ templates/project/providers/jwt/config.go.tpl | 35 ++++ templates/project/providers/jwt/jwt.go.tpl | 118 ++++++++++++ .../project/providers/postgres/config.go.tpl | 79 ++++++++ .../providers/postgres/postgres.go.tpl | 34 ++++ templates/project/providers/uuid/uuid.go.tpl | 41 ++++ templates/templates.go | 6 + 46 files changed, 1770 insertions(+) create mode 100644 .github/copilot-instructions.md create mode 100644 LICENSE create mode 100644 cmd/new.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100755 templates/project/-gitignore.tpl create mode 100755 templates/project/Makefile.tpl create mode 100755 templates/project/config.toml.tpl create mode 100755 templates/project/database/-transform.yaml.tpl create mode 100644 templates/project/database/database.go.tpl create mode 100644 templates/project/database/migrations/-gitkeep create mode 100644 templates/project/database/models/-gitkeep.tpl create mode 100755 templates/project/docs/ember.go.tpl create mode 100644 templates/project/fixtures/-gitkeep create mode 100755 templates/project/go.mod.tpl create mode 100755 templates/project/main.go.tpl create mode 100755 templates/project/main_test.go.tpl create mode 100755 templates/project/pkg/consts/consts.go.tpl create mode 100644 templates/project/pkg/consts/ctx.gen.go.tpl create mode 100644 templates/project/pkg/consts/ctx.go.tpl create mode 100755 templates/project/pkg/dao.go.tpl create mode 100755 templates/project/pkg/data_structures.go.tpl create mode 100644 templates/project/pkg/db/db.go.tpl create mode 100644 templates/project/pkg/db/pagination.go.tpl create mode 100644 templates/project/pkg/errorx/error.go.tpl create mode 100644 templates/project/pkg/pg/.gitkeep create mode 100644 templates/project/pkg/service/http/http.go.tpl create mode 100644 templates/project/pkg/service/migrate/migrate.go.tpl create mode 100644 templates/project/pkg/service/model/gen.go.tpl create mode 100644 templates/project/pkg/service/service.go.tpl create mode 100644 templates/project/pkg/service/testx/testing.go.tpl create mode 100644 templates/project/pkg/utils/buffer.go.tpl create mode 100644 templates/project/providers/app/app.go.tpl create mode 100644 templates/project/providers/app/config.gen.go.tpl create mode 100644 templates/project/providers/app/config.go.tpl create mode 100644 templates/project/providers/hashids/config.go.tpl create mode 100644 templates/project/providers/hashids/hashids.go.tpl create mode 100644 templates/project/providers/http/config.go.tpl create mode 100644 templates/project/providers/http/engine.go.tpl create mode 100644 templates/project/providers/jwt/config.go.tpl create mode 100644 templates/project/providers/jwt/jwt.go.tpl create mode 100755 templates/project/providers/postgres/config.go.tpl create mode 100644 templates/project/providers/postgres/postgres.go.tpl create mode 100644 templates/project/providers/uuid/uuid.go.tpl create mode 100644 templates/templates.go diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..04c5647 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,6 @@ +这是一个项目代码框架自动生成工具, 本工具可以根据用户执行的命令行自动生成指定模板华的代码。 + +注意点: +- 你需要始终使用中文和我交流 +- 生成时需要使用 text/template 技术来实现内容中关键信息的替换 + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/cmd/new.go b/cmd/new.go new file mode 100644 index 0000000..5e2a846 --- /dev/null +++ b/cmd/new.go @@ -0,0 +1,135 @@ +package cmd + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "regexp" + "strings" + "text/template" + + "git.ipao.vip/rogeecn/atomctl/templates" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +// 验证包名是否合法:支持域名、路径分隔符和常见字符 +var goPackageRegexp = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-_.]*[a-zA-Z0-9](/[a-zA-Z0-9][a-zA-Z0-9-_.]*[a-zA-Z0-9])*$`) + +func isValidGoPackageName(name string) bool { + return goPackageRegexp.MatchString(name) +} + +func CommandNew(root *cobra.Command) { + cmd := &cobra.Command{ + Use: "new [project]", + Short: "Create new project", + Args: cobra.ExactArgs(1), + RunE: commandNewE, + } + + cmd.Flags().BoolP("force", "f", false, "Force create project if exists") + root.AddCommand(cmd) +} + +func commandNewE(cmd *cobra.Command, args []string) error { + moduleName := args[0] + if !isValidGoPackageName(moduleName) { + return fmt.Errorf("invalid module name: %s, should be a valid go package name", moduleName) + } + + log.Info("创建项目: ", moduleName) + + force, _ := cmd.Flags().GetBool("force") + + var projectInfo struct { + ModuleName string + ProjectName string + } + + projectInfo.ModuleName = moduleName + moduleSplitInfo := strings.Split(projectInfo.ModuleName, "/") + projectInfo.ProjectName = moduleSplitInfo[len(moduleSplitInfo)-1] + + // 检查目录是否存在 + if _, err := os.Stat(projectInfo.ProjectName); err == nil { + if !force { + return fmt.Errorf("project directory %s already exists", projectInfo.ProjectName) + } + log.Warnf("强制删除已存在的目录: %s", projectInfo.ProjectName) + if err := os.RemoveAll(projectInfo.ProjectName); err != nil { + return fmt.Errorf("failed to remove existing directory: %v", err) + } + } + + // 创建项目根目录 + if err := os.MkdirAll(projectInfo.ProjectName, 0o755); err != nil { + return fmt.Errorf("failed to create project directory: %v", err) + } + + // 遍历和处理模板文件 + if err := fs.WalkDir(templates.Project, "project", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + // 计算相对路径,并处理隐藏文件 + relPath, err := filepath.Rel("project", path) + if err != nil { + return err + } + + // 如果是隐藏文件模板,将文件名中的前缀 "-" 替换为 "." + fileName := filepath.Base(relPath) + if strings.HasPrefix(fileName, "-") { + fileName = "." + strings.TrimPrefix(fileName, "-") + relPath = filepath.Join(filepath.Dir(relPath), fileName) + } + + targetPath := filepath.Join(projectInfo.ProjectName, relPath) + if d.IsDir() { + log.Infof("创建目录: %s", targetPath) + return os.MkdirAll(targetPath, 0o755) + } + + // 读取模板内容 + content, err := templates.Project.ReadFile(path) + if err != nil { + return err + } + + // 处理模板文件 + if strings.HasSuffix(path, ".tpl") { + tmpl, err := template.New(filepath.Base(path)).Parse(string(content)) + if err != nil { + return err + } + + // 创建目标文件(去除.tpl后缀) + targetPath = strings.TrimSuffix(targetPath, ".tpl") + log.Infof("创建文件: %s", targetPath) + f, err := os.Create(targetPath) + if err != nil { + return errors.Wrapf(err, "创建文件失败 %s", targetPath) + } + defer f.Close() + + return tmpl.Execute(f, projectInfo) + } + + // 复制非模板文件 + return os.WriteFile(targetPath, content, 0o644) + }); err != nil { + return err + } + + // 添加成功提示 + log.Info("🎉 项目创建成功!") + log.Info("后续步骤:") + log.Infof(" cd %s", projectInfo.ProjectName) + log.Info(" go mod tidy") + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7cb50cb --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module git.ipao.vip/rogeecn/atomctl + +go 1.23.2 + +require ( + github.com/pkg/errors v0.9.1 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cobra v1.8.1 +) + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cb20c63 --- /dev/null +++ b/go.sum @@ -0,0 +1,26 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..05b1e2a --- /dev/null +++ b/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "git.ipao.vip/rogeecn/atomctl/cmd" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func main() { + rootCmd := &cobra.Command{ + Use: "atomctl", + Short: "atom framework command line tool", + } + + cmd.CommandNew(rootCmd) + + if err := rootCmd.Execute(); err != nil { + log.Fatal(err) + } +} diff --git a/templates/project/-gitignore.tpl b/templates/project/-gitignore.tpl new file mode 100755 index 0000000..7f956e7 --- /dev/null +++ b/templates/project/-gitignore.tpl @@ -0,0 +1,28 @@ +bin/* +vendor/ +__debug_bin* +backend +build/* +.vscode +.idea +tmp/ +docker-compose.yml +atom +sqlite.db +go.work +go.work.sum +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ diff --git a/templates/project/Makefile.tpl b/templates/project/Makefile.tpl new file mode 100755 index 0000000..27c3788 --- /dev/null +++ b/templates/project/Makefile.tpl @@ -0,0 +1,50 @@ +buildAt=`date +%Y/%m/%d-%H:%M:%S` +gitHash=`git rev-parse HEAD` +version=`git rev-parse --abbrev-ref HEAD | grep -v HEAD || git describe --exact-match HEAD || git rev-parse HEAD` ## todo: use current release git tag +flags="-X 'atom/utils.Version=${version}' -X 'atom/utils.BuildAt=${buildAt}' -X 'atom/utils.GitHash=${gitHash}'" +release_flags="-w -s ${flags}" + +GOPATH:=$(shell go env GOPATH) + +.PHONY: tidy +tidy: + @go mod tidy + +.PHONY: dist +dist: + @go build -ldflags=${flags} -o bin/debug/atom + @cp config.toml bin/debug/ + +.PHONY: release +release: + @CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/release/app . + @cp config.toml bin/release/ + +.PHONY: test +test: + @go test -v ./... -cover + +.PHONY: lint +lint: + @golangci-lint run + +.PHONY: proto +proto: + @buf generate + +.PHONY: fresh +fresh: + @go run . migrate down + @go run . migrate up + +.PHONY: mup +mup: + @go run . migrate up + +.PHONY: mdown +mdown: + @go run . migrate down + +.PHONY: model +model: + go test -run ^Test_GenModel diff --git a/templates/project/config.toml.tpl b/templates/project/config.toml.tpl new file mode 100755 index 0000000..e3861f7 --- /dev/null +++ b/templates/project/config.toml.tpl @@ -0,0 +1,27 @@ +[App] +Mode = "development" +BaseURI = "baseURI" + +[Http] +Port = 8080 + +[Swagger] +BaseRoute = "doc" +Title = "Api" +Description = "Api Docs" +BasePath = "/v1" +Version = "1.0.0" + + +[Database] +Host = "10.1.1.1" +Database = "postgres" +Password = "hello" + + +[JWT] +ExpiresTime = "168h" +SigningKey = "Key" + +[HashIDs] +Salt = "Salt" diff --git a/templates/project/database/-transform.yaml.tpl b/templates/project/database/-transform.yaml.tpl new file mode 100755 index 0000000..f16420c --- /dev/null +++ b/templates/project/database/-transform.yaml.tpl @@ -0,0 +1,4 @@ +ignores: [] # ignore tables +types: + users: # table name + oauth: backend/pkg/pg.UserOAuth diff --git a/templates/project/database/database.go.tpl b/templates/project/database/database.go.tpl new file mode 100644 index 0000000..77c3404 --- /dev/null +++ b/templates/project/database/database.go.tpl @@ -0,0 +1,8 @@ +package database + +import ( + "embed" +) + +//go:embed migrations/* +var MigrationFS embed.FS diff --git a/templates/project/database/migrations/-gitkeep b/templates/project/database/migrations/-gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/templates/project/database/models/-gitkeep.tpl b/templates/project/database/models/-gitkeep.tpl new file mode 100644 index 0000000..e69de29 diff --git a/templates/project/docs/ember.go.tpl b/templates/project/docs/ember.go.tpl new file mode 100755 index 0000000..b17adae --- /dev/null +++ b/templates/project/docs/ember.go.tpl @@ -0,0 +1,6 @@ +package docs + +import _ "embed" + +//go:embed swagger.json +var SwaggerSpec string diff --git a/templates/project/fixtures/-gitkeep b/templates/project/fixtures/-gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/templates/project/go.mod.tpl b/templates/project/go.mod.tpl new file mode 100755 index 0000000..e797a1e --- /dev/null +++ b/templates/project/go.mod.tpl @@ -0,0 +1,3 @@ +module {{.ModuleName}} + +go 1.22.0 diff --git a/templates/project/main.go.tpl b/templates/project/main.go.tpl new file mode 100755 index 0000000..16c7160 --- /dev/null +++ b/templates/project/main.go.tpl @@ -0,0 +1,23 @@ +package main + +import ( + "{{.ModuleName}}/pkg/service/http" + "{{.ModuleName}}/pkg/service/migrate" + "{{.ModuleName}}/pkg/service/model" + + "git.ipao.vip/rogeecn/atom" + log "github.com/sirupsen/logrus" +) + +func main() { + opts := []atom.Option{ + atom.Name("{{ .ProjectName }}"), + http.Command(), + migrate.Command(), + model.Command(), + } + + if err := atom.Serve(opts...); err != nil { + log.Fatal(err) + } +} diff --git a/templates/project/main_test.go.tpl b/templates/project/main_test.go.tpl new file mode 100755 index 0000000..fc30597 --- /dev/null +++ b/templates/project/main_test.go.tpl @@ -0,0 +1,16 @@ +package main + +import ( + "testing" + + "{{.ModuleName}}/pkg/service/model" + + "git.ipao.vip/rogeecn/atom" +) + +func Test_GenModel(t *testing.T) { + err := atom.Serve(model.Options()...) + if err != nil { + t.Fatal(err) + } +} diff --git a/templates/project/pkg/consts/consts.go.tpl b/templates/project/pkg/consts/consts.go.tpl new file mode 100755 index 0000000..85fa520 --- /dev/null +++ b/templates/project/pkg/consts/consts.go.tpl @@ -0,0 +1,8 @@ +package consts + +// Format +// +// // swagger:enum CacheKey +// // ENUM( +// // VerifyCode = "code:__CHANNEL__:%s", +// // ) diff --git a/templates/project/pkg/consts/ctx.gen.go.tpl b/templates/project/pkg/consts/ctx.gen.go.tpl new file mode 100644 index 0000000..ba74cf5 --- /dev/null +++ b/templates/project/pkg/consts/ctx.gen.go.tpl @@ -0,0 +1,179 @@ +// Code generated by go-enum DO NOT EDIT. +// Version: - +// Revision: - +// Build Date: - +// Built By: - + +package consts + +import ( + "database/sql/driver" + "errors" + "fmt" + "strings" +) + +const ( + // CtxKeyTx is a CtxKey of type Tx. + CtxKeyTx CtxKey = "__ctx_db:" + // CtxKeyJwt is a CtxKey of type Jwt. + CtxKeyJwt CtxKey = "__jwt_token:" + // CtxKeyClaim is a CtxKey of type Claim. + CtxKeyClaim CtxKey = "__jwt_claim:" +) + +var ErrInvalidCtxKey = fmt.Errorf("not a valid CtxKey, try [%s]", strings.Join(_CtxKeyNames, ", ")) + +var _CtxKeyNames = []string{ + string(CtxKeyTx), + string(CtxKeyJwt), + string(CtxKeyClaim), +} + +// CtxKeyNames returns a list of possible string values of CtxKey. +func CtxKeyNames() []string { + tmp := make([]string, len(_CtxKeyNames)) + copy(tmp, _CtxKeyNames) + return tmp +} + +// CtxKeyValues returns a list of the values for CtxKey +func CtxKeyValues() []CtxKey { + return []CtxKey{ + CtxKeyTx, + CtxKeyJwt, + CtxKeyClaim, + } +} + +// String implements the Stringer interface. +func (x CtxKey) String() string { + return string(x) +} + +// IsValid provides a quick way to determine if the typed value is +// part of the allowed enumerated values +func (x CtxKey) IsValid() bool { + _, err := ParseCtxKey(string(x)) + return err == nil +} + +var _CtxKeyValue = map[string]CtxKey{ + "__ctx_db:": CtxKeyTx, + "__jwt_token:": CtxKeyJwt, + "__jwt_claim:": CtxKeyClaim, +} + +// ParseCtxKey attempts to convert a string to a CtxKey. +func ParseCtxKey(name string) (CtxKey, error) { + if x, ok := _CtxKeyValue[name]; ok { + return x, nil + } + return CtxKey(""), fmt.Errorf("%s is %w", name, ErrInvalidCtxKey) +} + +var errCtxKeyNilPtr = errors.New("value pointer is nil") // one per type for package clashes + +// Scan implements the Scanner interface. +func (x *CtxKey) Scan(value interface{}) (err error) { + if value == nil { + *x = CtxKey("") + return + } + + // A wider range of scannable types. + // driver.Value values at the top of the list for expediency + switch v := value.(type) { + case string: + *x, err = ParseCtxKey(v) + case []byte: + *x, err = ParseCtxKey(string(v)) + case CtxKey: + *x = v + case *CtxKey: + if v == nil { + return errCtxKeyNilPtr + } + *x = *v + case *string: + if v == nil { + return errCtxKeyNilPtr + } + *x, err = ParseCtxKey(*v) + default: + return errors.New("invalid type for CtxKey") + } + + return +} + +// Value implements the driver Valuer interface. +func (x CtxKey) Value() (driver.Value, error) { + return x.String(), nil +} + +// Set implements the Golang flag.Value interface func. +func (x *CtxKey) Set(val string) error { + v, err := ParseCtxKey(val) + *x = v + return err +} + +// Get implements the Golang flag.Getter interface func. +func (x *CtxKey) Get() interface{} { + return *x +} + +// Type implements the github.com/spf13/pFlag Value interface. +func (x *CtxKey) Type() string { + return "CtxKey" +} + +type NullCtxKey struct { + CtxKey CtxKey + Valid bool +} + +func NewNullCtxKey(val interface{}) (x NullCtxKey) { + err := x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + _ = err // make any errcheck linters happy + return +} + +// Scan implements the Scanner interface. +func (x *NullCtxKey) Scan(value interface{}) (err error) { + if value == nil { + x.CtxKey, x.Valid = CtxKey(""), false + return + } + + err = x.CtxKey.Scan(value) + x.Valid = (err == nil) + return +} + +// Value implements the driver Valuer interface. +func (x NullCtxKey) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + // driver.Value accepts int64 for int values. + return string(x.CtxKey), nil +} + +type NullCtxKeyStr struct { + NullCtxKey +} + +func NewNullCtxKeyStr(val interface{}) (x NullCtxKeyStr) { + x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + return +} + +// Value implements the driver Valuer interface. +func (x NullCtxKeyStr) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + return x.CtxKey.String(), nil +} diff --git a/templates/project/pkg/consts/ctx.go.tpl b/templates/project/pkg/consts/ctx.go.tpl new file mode 100644 index 0000000..5a98c69 --- /dev/null +++ b/templates/project/pkg/consts/ctx.go.tpl @@ -0,0 +1,9 @@ +package consts + +// swagger:enum CacheKey +// ENUM( +// Tx = "__ctx_db:", +// Jwt = "__jwt_token:", +// Claim = "__jwt_claim:", +// ) +type CtxKey string diff --git a/templates/project/pkg/dao.go.tpl b/templates/project/pkg/dao.go.tpl new file mode 100755 index 0000000..df67d26 --- /dev/null +++ b/templates/project/pkg/dao.go.tpl @@ -0,0 +1,13 @@ +package pkg + +func WrapLike(v string) string { + return "%" + v + "%" +} + +func WrapLikeLeft(v string) string { + return "%" + v +} + +func WrapLikeRight(v string) string { + return "%" + v +} diff --git a/templates/project/pkg/data_structures.go.tpl b/templates/project/pkg/data_structures.go.tpl new file mode 100755 index 0000000..36623c5 --- /dev/null +++ b/templates/project/pkg/data_structures.go.tpl @@ -0,0 +1,71 @@ +package pkg + +import ( + "strings" + + "github.com/samber/lo" +) + +type SortQueryFilter struct { + Asc *string `json:"asc" form:"asc"` + Desc *string `json:"desc" form:"desc"` +} + +func (s *SortQueryFilter) AscFields() []string { + if s.Asc == nil { + return nil + } + return strings.Split(*s.Asc, ",") +} + +func (s *SortQueryFilter) DescFields() []string { + if s.Desc == nil { + return nil + } + return strings.Split(*s.Desc, ",") +} + +func (s *SortQueryFilter) DescID() *SortQueryFilter { + if s.Desc == nil { + s.Desc = lo.ToPtr("id") + } + + items := s.DescFields() + if lo.Contains(items, "id") { + return s + } + + items = append(items, "id") + s.Desc = lo.ToPtr(strings.Join(items, ",")) + return s +} + +type PageDataResponse struct { + PageQueryFilter `json:",inline"` + Total int64 `json:"total"` + Items interface{} `json:"items"` +} + +type PageQueryFilter struct { + Page int `json:"page" form:"page"` + Limit int `json:"limit" form:"limit"` +} + +func (filter *PageQueryFilter) Offset() int { + return (filter.Page - 1) * filter.Limit +} + +func (filter *PageQueryFilter) Format() *PageQueryFilter { + if filter.Page <= 0 { + filter.Page = 1 + } + + if filter.Limit <= 0 { + filter.Limit = 10 + } + + if filter.Limit > 50 { + filter.Limit = 50 + } + return filter +} diff --git a/templates/project/pkg/db/db.go.tpl b/templates/project/pkg/db/db.go.tpl new file mode 100644 index 0000000..b2a6d87 --- /dev/null +++ b/templates/project/pkg/db/db.go.tpl @@ -0,0 +1,28 @@ +package db + +import ( + "context" + "database/sql" + "fmt" + + "{{.ModuleName}}/pkg/consts" + + "github.com/go-jet/jet/v2/qrm" +) + +func FromContext(ctx context.Context, db *sql.DB) qrm.DB { + if tx, ok := ctx.Value(consts.CtxKeyTx).(*sql.Tx); ok { + return tx + } + return db +} + +func TruncateAllTables(ctx context.Context, db *sql.DB, tableName ...string) error { + for _, name := range tableName { + sql := fmt.Sprintf("TRUNCATE TABLE %s RESTART IDENTITY", name) + if _, err := db.ExecContext(ctx, sql); err != nil { + return err + } + } + return nil +} diff --git a/templates/project/pkg/db/pagination.go.tpl b/templates/project/pkg/db/pagination.go.tpl new file mode 100644 index 0000000..01f448c --- /dev/null +++ b/templates/project/pkg/db/pagination.go.tpl @@ -0,0 +1,7 @@ +package db + +type Pagination struct { + Offset string `json:"offset,omitempty"` + OffsetID int64 `json:"-"` + Action int `json:"action"` // action: 0 :加载更多 1:刷新 +} diff --git a/templates/project/pkg/errorx/error.go.tpl b/templates/project/pkg/errorx/error.go.tpl new file mode 100644 index 0000000..8361dd2 --- /dev/null +++ b/templates/project/pkg/errorx/error.go.tpl @@ -0,0 +1,31 @@ +package errorx + +import ( + "fmt" + "net/http" + + "github.com/gofiber/fiber/v3" +) + +type Response struct { + StatusCode int `json:"-"` + Code int `json:"code"` + Message string `json:"message"` +} + +func Wrap(err error) Response { + return Response{http.StatusInternalServerError, http.StatusInternalServerError, err.Error()} +} + +func (r Response) Error() string { + return fmt.Sprintf("[%d] %s", r.Code, r.Message) +} + +func (r Response) Response(ctx fiber.Ctx) error { + return ctx.Status(r.StatusCode).JSON(r) +} + +var ( + RequestParseError = Response{http.StatusBadRequest, http.StatusBadRequest, "请求解析错误"} + InternalError = Response{http.StatusInternalServerError, http.StatusInternalServerError, "内部错误"} +) diff --git a/templates/project/pkg/pg/.gitkeep b/templates/project/pkg/pg/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/templates/project/pkg/service/http/http.go.tpl b/templates/project/pkg/service/http/http.go.tpl new file mode 100644 index 0000000..07f0b83 --- /dev/null +++ b/templates/project/pkg/service/http/http.go.tpl @@ -0,0 +1,65 @@ +package http + +import ( + "{{.ModuleName}}/pkg/service" + "{{.ModuleName}}/providers/app" + "{{.ModuleName}}/providers/hashids" + "{{.ModuleName}}/providers/http" + "{{.ModuleName}}/providers/jwt" + "{{.ModuleName}}/providers/postgres" + + "git.ipao.vip/rogeecn/atom" + "git.ipao.vip/rogeecn/atom/container" + "git.ipao.vip/rogeecn/atom/contracts" + "github.com/gofiber/fiber/v3/middleware/favicon" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "go.uber.org/dig" +) + +func defaultProviders() container.Providers { + return service.Default(container.Providers{ + http.DefaultProvider(), + postgres.DefaultProvider(), + jwt.DefaultProvider(), + hashids.DefaultProvider(), + }...) +} + +func Command() atom.Option { + return atom.Command( + atom.Name("serve"), + atom.Short("run http server"), + atom.RunE(Serve), + atom.Providers(defaultProviders()), + ) +} + +type Http struct { + dig.In + + App *app.Config + Service *http.Service + Initials []contracts.Initial `group:"initials"` + Routes []contracts.HttpRoute `group:"routes"` +} + +func Serve(cmd *cobra.Command, args []string) error { + return container.Container.Invoke(func(http Http) error { + if http.App.Mode == app.AppModeDevelopment { + log.SetLevel(log.DebugLevel) + } + + http.Service.Engine.Use(favicon.New(favicon.Config{ + Data: []byte{}, + })) + + group := http.Service.Engine.Group("/v1") + + for _, route := range http.Routes { + route.Register(group) + } + + return http.Service.Serve() + }) +} diff --git a/templates/project/pkg/service/migrate/migrate.go.tpl b/templates/project/pkg/service/migrate/migrate.go.tpl new file mode 100644 index 0000000..8a4b75b --- /dev/null +++ b/templates/project/pkg/service/migrate/migrate.go.tpl @@ -0,0 +1,52 @@ +package migrate + +import ( + "context" + "database/sql" + + "{{.ModuleName}}/database" + "{{.ModuleName}}/providers/postgres" + + "git.ipao.vip/rogeecn/atom" + "git.ipao.vip/rogeecn/atom/container" + "github.com/pressly/goose/v3" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "go.uber.org/dig" +) + +func Default(providers ...container.ProviderContainer) container.Providers { + return append(container.Providers{ + postgres.DefaultProvider(), + }, providers...) +} + +func Command() atom.Option { + return atom.Command( + atom.Name("migrate"), + atom.Short("run migrations"), + atom.RunE(Serve), + atom.Providers(Default()), + ) +} + +type Migrate struct { + dig.In + DB *sql.DB +} + +func Serve(cmd *cobra.Command, args []string) error { + return container.Container.Invoke(func(migrate Migrate) error { + if len(args) == 0 { + args = append(args, "up") + } + + action, args := args[0], args[1:] + log.Infof("migration action: %s args: %+v", action, args) + + goose.SetBaseFS(database.MigrationFS) + goose.SetTableName("migrations") + + return goose.RunContext(context.Background(), action, migrate.DB, "migrations", args...) + }) +} diff --git a/templates/project/pkg/service/model/gen.go.tpl b/templates/project/pkg/service/model/gen.go.tpl new file mode 100644 index 0000000..d26753b --- /dev/null +++ b/templates/project/pkg/service/model/gen.go.tpl @@ -0,0 +1,129 @@ +package model + +import ( + "database/sql" + "fmt" + "strings" + + db "{{.ModuleName}}/providers/postgres" + + "git.ipao.vip/rogeecn/atom" + "git.ipao.vip/rogeecn/atom/container" + "github.com/go-jet/jet/v2/generator/metadata" + "github.com/go-jet/jet/v2/generator/postgres" + "github.com/go-jet/jet/v2/generator/template" + pg "github.com/go-jet/jet/v2/postgres" + "github.com/gofiber/fiber/v3/log" + _ "github.com/lib/pq" + "github.com/samber/lo" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uber.org/dig" +) + +func Default(providers ...container.ProviderContainer) container.Providers { + return append(container.Providers{ + db.DefaultProvider(), + }, providers...) +} + +func Options() []atom.Option { + return []atom.Option{ + atom.Name("model"), + atom.Short("run model generator"), + atom.RunE(Serve), + atom.Providers(Default()), + atom.Arguments(func(cmd *cobra.Command) { + cmd.Flags().String("path", "./database/models", "generate to path") + cmd.Flags().String("transform", "./database/.transform.yaml", "transform config") + }), + } +} + +func Command() atom.Option { + return atom.Command(Options()...) +} + +type Migrate struct { + dig.In + DB *sql.DB + Config *db.Config +} + +type Transformer struct { + Ignores []string `mapstructure:"ignores"` + Types map[string]map[string]string `mapstructure:"types"` +} + +func Serve(cmd *cobra.Command, args []string) error { + v := viper.New() + v.SetConfigType("yaml") + v.SetConfigFile(cmd.Flag("transform").Value.String()) + + if err := v.ReadInConfig(); err != nil { + return err + } + + var conf Transformer + if err := v.Unmarshal(&conf); err != nil { + return err + } + + return container.Container.Invoke(func(migrate Migrate) error { + return postgres.GenerateDSN( + migrate.Config.DSN(), + migrate.Config.Schema, + cmd.Flag("path").Value.String(), + template.Default(pg.Dialect). + UseSchema(func(schema metadata.Schema) template.Schema { + return template. + DefaultSchema(schema). + UseModel( + template. + DefaultModel(). + UseTable(func(table metadata.Table) template.TableModel { + if lo.Contains(conf.Ignores, table.Name) { + table := template.DefaultTableModel(table) + table.Skip = true + return table + } + + return template.DefaultTableModel(table).UseField(func(column metadata.Column) template.TableModelField { + defaultTableModelField := template.DefaultTableModelField(column) + defaultTableModelField = defaultTableModelField.UseTags(fmt.Sprintf(`json:"%s"`, column.Name)) + + if schema.Name != migrate.Config.Schema { + return defaultTableModelField + } + + fields, ok := conf.Types[table.Name] + if !ok { + return defaultTableModelField + } + + toType, ok := fields[column.Name] + if !ok { + return defaultTableModelField + } + + splits := strings.Split(toType, ".") + typeName := splits[len(splits)-1] + + pkgSplits := strings.Split(splits[0], "/") + typePkg := pkgSplits[len(pkgSplits)-1] + + defaultTableModelField = defaultTableModelField. + UseType(template.Type{ + Name: fmt.Sprintf("%s.%s", typePkg, typeName), + ImportPath: splits[0], + }) + + log.Infof("Convert table %s field %s type to : %s", table.Name, column.Name, toType) + return defaultTableModelField + }) + }), + ) + }), + ) + }) +} diff --git a/templates/project/pkg/service/service.go.tpl b/templates/project/pkg/service/service.go.tpl new file mode 100644 index 0000000..b44a874 --- /dev/null +++ b/templates/project/pkg/service/service.go.tpl @@ -0,0 +1,13 @@ +package service + +import ( + "{{.ModuleName}}/providers/app" + + "git.ipao.vip/rogeecn/atom/container" +) + +func Default(providers ...container.ProviderContainer) container.Providers { + return append(container.Providers{ + app.DefaultProvider(), + }, providers...) +} diff --git a/templates/project/pkg/service/testx/testing.go.tpl b/templates/project/pkg/service/testx/testing.go.tpl new file mode 100644 index 0000000..8d6b50e --- /dev/null +++ b/templates/project/pkg/service/testx/testing.go.tpl @@ -0,0 +1,29 @@ +package testx + +import ( + "os" + "testing" + + "git.ipao.vip/rogeecn/atom" + "git.ipao.vip/rogeecn/atom/container" + "github.com/rogeecn/fabfile" + . "github.com/smartystreets/goconvey/convey" +) + +func Default(providers ...container.ProviderContainer) container.Providers { + return append(container.Providers{}, providers...) +} + +func Serve(providers container.Providers, t *testing.T, invoke any) { + Convey("tests boot up", t, func() { + file := fabfile.MustFind("config.toml") + + localEnv := os.Getenv("ENV_LOCAL") + if localEnv != "" { + file = fabfile.MustFind("config." + localEnv + ".toml") + } + + So(atom.LoadProviders(file, providers), ShouldBeNil) + So(container.Container.Invoke(invoke), ShouldBeNil) + }) +} diff --git a/templates/project/pkg/utils/buffer.go.tpl b/templates/project/pkg/utils/buffer.go.tpl new file mode 100644 index 0000000..5746d74 --- /dev/null +++ b/templates/project/pkg/utils/buffer.go.tpl @@ -0,0 +1,26 @@ +package utils + +import ( + "bufio" + "io" +) + +// NewLogBuffer creates a buffer that can be used to capture output stream +// and write to a logger in real time +func NewLogBuffer(output func(string)) io.Writer { + reader, writer := io.Pipe() + + go func() { + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + output(scanner.Text()) + } + }() + + return writer +} + +// NewCombinedBuffer combines multiple io.Writers +func NewCombinedBuffer(writers ...io.Writer) io.Writer { + return io.MultiWriter(writers...) +} diff --git a/templates/project/providers/app/app.go.tpl b/templates/project/providers/app/app.go.tpl new file mode 100644 index 0000000..17209eb --- /dev/null +++ b/templates/project/providers/app/app.go.tpl @@ -0,0 +1,18 @@ +package app + +import ( + "git.ipao.vip/rogeecn/atom/container" + "git.ipao.vip/rogeecn/atom/utils/opt" +) + +func Provide(opts ...opt.Option) error { + o := opt.New(opts...) + var config Config + if err := o.UnmarshalConfig(&config); err != nil { + return err + } + + return container.Container.Provide(func() (*Config, error) { + return &config, nil + }, o.DiOptions()...) +} diff --git a/templates/project/providers/app/config.gen.go.tpl b/templates/project/providers/app/config.gen.go.tpl new file mode 100644 index 0000000..702160e --- /dev/null +++ b/templates/project/providers/app/config.gen.go.tpl @@ -0,0 +1,179 @@ +// Code generated by go-enum DO NOT EDIT. +// Version: - +// Revision: - +// Build Date: - +// Built By: - + +package app + +import ( + "database/sql/driver" + "errors" + "fmt" + "strings" +) + +const ( + // AppModeDevelopment is a AppMode of type development. + AppModeDevelopment AppMode = "development" + // AppModeRelease is a AppMode of type release. + AppModeRelease AppMode = "release" + // AppModeTest is a AppMode of type test. + AppModeTest AppMode = "test" +) + +var ErrInvalidAppMode = fmt.Errorf("not a valid AppMode, try [%s]", strings.Join(_AppModeNames, ", ")) + +var _AppModeNames = []string{ + string(AppModeDevelopment), + string(AppModeRelease), + string(AppModeTest), +} + +// AppModeNames returns a list of possible string values of AppMode. +func AppModeNames() []string { + tmp := make([]string, len(_AppModeNames)) + copy(tmp, _AppModeNames) + return tmp +} + +// AppModeValues returns a list of the values for AppMode +func AppModeValues() []AppMode { + return []AppMode{ + AppModeDevelopment, + AppModeRelease, + AppModeTest, + } +} + +// String implements the Stringer interface. +func (x AppMode) String() string { + return string(x) +} + +// IsValid provides a quick way to determine if the typed value is +// part of the allowed enumerated values +func (x AppMode) IsValid() bool { + _, err := ParseAppMode(string(x)) + return err == nil +} + +var _AppModeValue = map[string]AppMode{ + "development": AppModeDevelopment, + "release": AppModeRelease, + "test": AppModeTest, +} + +// ParseAppMode attempts to convert a string to a AppMode. +func ParseAppMode(name string) (AppMode, error) { + if x, ok := _AppModeValue[name]; ok { + return x, nil + } + return AppMode(""), fmt.Errorf("%s is %w", name, ErrInvalidAppMode) +} + +var errAppModeNilPtr = errors.New("value pointer is nil") // one per type for package clashes + +// Scan implements the Scanner interface. +func (x *AppMode) Scan(value interface{}) (err error) { + if value == nil { + *x = AppMode("") + return + } + + // A wider range of scannable types. + // driver.Value values at the top of the list for expediency + switch v := value.(type) { + case string: + *x, err = ParseAppMode(v) + case []byte: + *x, err = ParseAppMode(string(v)) + case AppMode: + *x = v + case *AppMode: + if v == nil { + return errAppModeNilPtr + } + *x = *v + case *string: + if v == nil { + return errAppModeNilPtr + } + *x, err = ParseAppMode(*v) + default: + return errors.New("invalid type for AppMode") + } + + return +} + +// Value implements the driver Valuer interface. +func (x AppMode) Value() (driver.Value, error) { + return x.String(), nil +} + +// Set implements the Golang flag.Value interface func. +func (x *AppMode) Set(val string) error { + v, err := ParseAppMode(val) + *x = v + return err +} + +// Get implements the Golang flag.Getter interface func. +func (x *AppMode) Get() interface{} { + return *x +} + +// Type implements the github.com/spf13/pFlag Value interface. +func (x *AppMode) Type() string { + return "AppMode" +} + +type NullAppMode struct { + AppMode AppMode + Valid bool +} + +func NewNullAppMode(val interface{}) (x NullAppMode) { + err := x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + _ = err // make any errcheck linters happy + return +} + +// Scan implements the Scanner interface. +func (x *NullAppMode) Scan(value interface{}) (err error) { + if value == nil { + x.AppMode, x.Valid = AppMode(""), false + return + } + + err = x.AppMode.Scan(value) + x.Valid = (err == nil) + return +} + +// Value implements the driver Valuer interface. +func (x NullAppMode) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + // driver.Value accepts int64 for int values. + return string(x.AppMode), nil +} + +type NullAppModeStr struct { + NullAppMode +} + +func NewNullAppModeStr(val interface{}) (x NullAppModeStr) { + x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + return +} + +// Value implements the driver Valuer interface. +func (x NullAppModeStr) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + return x.AppMode.String(), nil +} diff --git a/templates/project/providers/app/config.go.tpl b/templates/project/providers/app/config.go.tpl new file mode 100644 index 0000000..c4e37d7 --- /dev/null +++ b/templates/project/providers/app/config.go.tpl @@ -0,0 +1,45 @@ +package app + +import ( + "git.ipao.vip/rogeecn/atom/container" + "git.ipao.vip/rogeecn/atom/utils/opt" +) + +const DefaultPrefix = "App" + +func DefaultProvider() container.ProviderContainer { + return container.ProviderContainer{ + Provider: Provide, + Options: []opt.Option{ + opt.Prefix(DefaultPrefix), + }, + } +} + +// swagger:enum AppMode +// ENUM(development, release, test) +type AppMode string + +type Config struct { + Mode AppMode + Cert *Cert + BaseURI *string +} + +func (c *Config) IsDevMode() bool { + return c.Mode == AppModeDevelopment +} + +func (c *Config) IsReleaseMode() bool { + return c.Mode == AppModeRelease +} + +func (c *Config) IsTestMode() bool { + return c.Mode == AppModeTest +} + +type Cert struct { + CA string + Cert string + Key string +} diff --git a/templates/project/providers/hashids/config.go.tpl b/templates/project/providers/hashids/config.go.tpl new file mode 100644 index 0000000..dd3b45a --- /dev/null +++ b/templates/project/providers/hashids/config.go.tpl @@ -0,0 +1,23 @@ +package hashids + +import ( + "git.ipao.vip/rogeecn/atom/container" + "git.ipao.vip/rogeecn/atom/utils/opt" +) + +const DefaultPrefix = "HashIDs" + +func DefaultProvider() container.ProviderContainer { + return container.ProviderContainer{ + Provider: Provide, + Options: []opt.Option{ + opt.Prefix(DefaultPrefix), + }, + } +} + +type Config struct { + Alphabet string + Salt string + MinLength uint +} diff --git a/templates/project/providers/hashids/hashids.go.tpl b/templates/project/providers/hashids/hashids.go.tpl new file mode 100644 index 0000000..c296ad4 --- /dev/null +++ b/templates/project/providers/hashids/hashids.go.tpl @@ -0,0 +1,35 @@ +package hashids + +import ( + "git.ipao.vip/rogeecn/atom/container" + "git.ipao.vip/rogeecn/atom/utils/opt" + + "github.com/speps/go-hashids/v2" +) + +func Provide(opts ...opt.Option) error { + o := opt.New(opts...) + var config Config + if err := o.UnmarshalConfig(&config); err != nil { + return err + } + return container.Container.Provide(func() (*hashids.HashID, error) { + data := hashids.NewData() + data.MinLength = int(config.MinLength) + if data.MinLength == 0 { + data.MinLength = 10 + } + + data.Salt = config.Salt + if data.Salt == "" { + data.Salt = "default-salt-key" + } + + data.Alphabet = config.Alphabet + if config.Alphabet == "" { + data.Alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + } + + return hashids.NewWithData(data) + }, o.DiOptions()...) +} diff --git a/templates/project/providers/http/config.go.tpl b/templates/project/providers/http/config.go.tpl new file mode 100644 index 0000000..611f210 --- /dev/null +++ b/templates/project/providers/http/config.go.tpl @@ -0,0 +1,38 @@ +package http + +import ( + "fmt" +) + +const DefaultPrefix = "Http" + +type Config struct { + StaticPath *string + StaticRoute *string + BaseURI *string + Port uint + Tls *Tls + Cors *Cors +} + +type Tls struct { + Cert string + Key string +} + +type Cors struct { + Mode string + Whitelist []Whitelist +} + +type Whitelist struct { + AllowOrigin string + AllowHeaders string + AllowMethods string + ExposeHeaders string + AllowCredentials bool +} + +func (h *Config) Address() string { + return fmt.Sprintf(":%d", h.Port) +} diff --git a/templates/project/providers/http/engine.go.tpl b/templates/project/providers/http/engine.go.tpl new file mode 100644 index 0000000..a5763aa --- /dev/null +++ b/templates/project/providers/http/engine.go.tpl @@ -0,0 +1,92 @@ +package http + +import ( + "errors" + "fmt" + "runtime/debug" + "time" + + "git.ipao.vip/rogeecn/atom/container" + "git.ipao.vip/rogeecn/atom/utils/opt" + log "github.com/sirupsen/logrus" + + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/logger" + "github.com/gofiber/fiber/v3/middleware/recover" +) + +func DefaultProvider() container.ProviderContainer { + return container.ProviderContainer{ + Provider: Provide, + Options: []opt.Option{ + opt.Prefix(DefaultPrefix), + }, + } +} + +type Service struct { + conf *Config + Engine *fiber.App +} + +func (svc *Service) Serve() error { + listenConfig := fiber.ListenConfig{ + EnablePrintRoutes: true, + OnShutdownSuccess: func() { + log.Info("http server shutdown success") + }, + OnShutdownError: func(err error) { + log.Error("http server shutdown error: ", err) + }, + + // DisableStartupMessage: true, + } + + if svc.conf.Tls != nil { + if svc.conf.Tls.Cert == "" || svc.conf.Tls.Key == "" { + return errors.New("tls cert or key is empty") + } + listenConfig.CertFile = svc.conf.Tls.Cert + listenConfig.CertKeyFile = svc.conf.Tls.Key + } + container.AddCloseAble(func() { + svc.Engine.Shutdown() + }) + + return svc.Engine.Listen(svc.conf.Address(), listenConfig) +} + +func Provide(opts ...opt.Option) error { + o := opt.New(opts...) + var config Config + if err := o.UnmarshalConfig(&config); err != nil { + return err + } + + return container.Container.Provide(func() (*Service, error) { + engine := fiber.New(fiber.Config{ + StrictRouting: true, + }) + engine.Use(recover.New(recover.Config{ + EnableStackTrace: true, + StackTraceHandler: func(c fiber.Ctx, e any) { + log.WithError(e.(error)).Error(fmt.Sprintf("panic: %v\n%s\n", e, debug.Stack())) + }, + })) + + if config.StaticRoute != nil && config.StaticPath != nil { + engine.Use(config.StaticRoute, config.StaticPath) + } + + engine.Use(logger.New(logger.Config{ + Format: `[${ip}:${port}] - [${time}] - ${method} - ${status} - ${path} ${latency} "${ua}"` + "\n", + TimeFormat: time.RFC1123, + TimeZone: "Asia/Shanghai", + })) + + return &Service{ + Engine: engine, + conf: &config, + }, nil + }, o.DiOptions()...) +} diff --git a/templates/project/providers/jwt/config.go.tpl b/templates/project/providers/jwt/config.go.tpl new file mode 100644 index 0000000..2689a31 --- /dev/null +++ b/templates/project/providers/jwt/config.go.tpl @@ -0,0 +1,35 @@ +package jwt + +import ( + "time" + + log "github.com/sirupsen/logrus" + + "git.ipao.vip/rogeecn/atom/container" + "git.ipao.vip/rogeecn/atom/utils/opt" +) + +const DefaultPrefix = "JWT" + +func DefaultProvider() container.ProviderContainer { + return container.ProviderContainer{ + Provider: Provide, + Options: []opt.Option{ + opt.Prefix(DefaultPrefix), + }, + } +} + +type Config struct { + SigningKey string // jwt签名 + ExpiresTime string // 过期时间 + Issuer string // 签发者 +} + +func (c *Config) ExpiresTimeDuration() time.Duration { + d, err := time.ParseDuration(c.ExpiresTime) + if err != nil { + log.Fatal(err) + } + return d +} diff --git a/templates/project/providers/jwt/jwt.go.tpl b/templates/project/providers/jwt/jwt.go.tpl new file mode 100644 index 0000000..ee39a95 --- /dev/null +++ b/templates/project/providers/jwt/jwt.go.tpl @@ -0,0 +1,118 @@ +package jwt + +import ( + "errors" + "strings" + "time" + + "git.ipao.vip/rogeecn/atom/container" + "git.ipao.vip/rogeecn/atom/utils/opt" + + jwt "github.com/golang-jwt/jwt/v4" + "golang.org/x/sync/singleflight" +) + +const ( + CtxKey = "claims" + HttpHeader = "Authorization" +) + +type BaseClaims struct { + OpenID string `json:"open_id,omitempty"` + Tenant string `json:"tenant,omitempty"` + UserID int64 `json:"user_id,omitempty"` + TenantID int64 `json:"tenant_id,omitempty"` +} + +// Custom claims structure +type Claims struct { + BaseClaims + jwt.RegisteredClaims +} + +const TokenPrefix = "Bearer " + +type JWT struct { + singleflight *singleflight.Group + config *Config + SigningKey []byte +} + +var ( + TokenExpired = errors.New("Token is expired") + TokenNotValidYet = errors.New("Token not active yet") + TokenMalformed = errors.New("That's not even a token") + TokenInvalid = errors.New("Couldn't handle this token:") +) + +func Provide(opts ...opt.Option) error { + o := opt.New(opts...) + var config Config + if err := o.UnmarshalConfig(&config); err != nil { + return err + } + return container.Container.Provide(func() (*JWT, error) { + return &JWT{ + singleflight: &singleflight.Group{}, + config: &config, + SigningKey: []byte(config.SigningKey), + }, nil + }, o.DiOptions()...) +} + +func (j *JWT) CreateClaims(baseClaims BaseClaims) *Claims { + ep, _ := time.ParseDuration(j.config.ExpiresTime) + claims := Claims{ + BaseClaims: baseClaims, + RegisteredClaims: jwt.RegisteredClaims{ + NotBefore: jwt.NewNumericDate(time.Now().Add(-time.Second * 10)), // 签名生效时间 + ExpiresAt: jwt.NewNumericDate(time.Now().Add(ep)), // 过期时间 7天 配置文件 + Issuer: j.config.Issuer, // 签名的发行者 + }, + } + return &claims +} + +// 创建一个token +func (j *JWT) CreateToken(claims *Claims) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(j.SigningKey) +} + +// CreateTokenByOldToken 旧token 换新token 使用归并回源避免并发问题 +func (j *JWT) CreateTokenByOldToken(oldToken string, claims *Claims) (string, error) { + v, err, _ := j.singleflight.Do("JWT:"+oldToken, func() (interface{}, error) { + return j.CreateToken(claims) + }) + return v.(string), err +} + +// 解析 token +func (j *JWT) Parse(tokenString string) (*Claims, error) { + tokenString = strings.TrimPrefix(tokenString, TokenPrefix) + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (i interface{}, e error) { + return j.SigningKey, nil + }) + if err != nil { + if ve, ok := err.(*jwt.ValidationError); ok { + if ve.Errors&jwt.ValidationErrorMalformed != 0 { + return nil, TokenMalformed + } else if ve.Errors&jwt.ValidationErrorExpired != 0 { + // Token is expired + return nil, TokenExpired + } else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 { + return nil, TokenNotValidYet + } else { + return nil, TokenInvalid + } + } + } + if token != nil { + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + return nil, TokenInvalid + } else { + return nil, TokenInvalid + } +} diff --git a/templates/project/providers/postgres/config.go.tpl b/templates/project/providers/postgres/config.go.tpl new file mode 100755 index 0000000..7667273 --- /dev/null +++ b/templates/project/providers/postgres/config.go.tpl @@ -0,0 +1,79 @@ +package postgres + +import ( + "fmt" + + "git.ipao.vip/rogeecn/atom/container" + "git.ipao.vip/rogeecn/atom/utils/opt" +) + +const DefaultPrefix = "Database" + +func DefaultProvider() container.ProviderContainer { + return container.ProviderContainer{ + Provider: Provide, + Options: []opt.Option{ + opt.Prefix(DefaultPrefix), + }, + } +} + +type Config struct { + Username string + Password string + Database string + Schema string + Host string + Port uint + SslMode string + TimeZone string + Prefix string // 表前缀 + Singular bool // 是否开启全局禁用复数,true表示开启 + MaxIdleConns int // 空闲中的最大连接数 + MaxOpenConns int // 打开到数据库的最大连接数 +} + +func (m *Config) checkDefault() { + if m.MaxIdleConns == 0 { + m.MaxIdleConns = 10 + } + + if m.MaxOpenConns == 0 { + m.MaxOpenConns = 100 + } + + if m.Username == "" { + m.Username = "postgres" + } + + if m.SslMode == "" { + m.SslMode = "disable" + } + + if m.TimeZone == "" { + m.TimeZone = "Asia/Shanghai" + } + + if m.Port == 0 { + m.Port = 5432 + } + + if m.Schema == "" { + m.Schema = "public" + } +} + +func (m *Config) EmptyDsn() string { + dsnTpl := "host=%s user=%s password=%s port=%d dbname=%s sslmode=%s TimeZone=%s" + m.checkDefault() + + return fmt.Sprintf(dsnTpl, m.Host, m.Username, m.Password, m.Port, m.Database, m.SslMode, m.TimeZone) +} + +// DSN connection dsn +func (m *Config) DSN() string { + dsnTpl := "host=%s user=%s password=%s dbname=%s port=%d sslmode=%s TimeZone=%s" + m.checkDefault() + + return fmt.Sprintf(dsnTpl, m.Host, m.Username, m.Password, m.Database, m.Port, m.SslMode, m.TimeZone) +} diff --git a/templates/project/providers/postgres/postgres.go.tpl b/templates/project/providers/postgres/postgres.go.tpl new file mode 100644 index 0000000..2405542 --- /dev/null +++ b/templates/project/providers/postgres/postgres.go.tpl @@ -0,0 +1,34 @@ +package postgres + +import ( + "database/sql" + + "git.ipao.vip/rogeecn/atom/container" + "git.ipao.vip/rogeecn/atom/utils/opt" + _ "github.com/lib/pq" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +func Provide(opts ...opt.Option) error { + o := opt.New(opts...) + var conf Config + if err := o.UnmarshalConfig(&conf); err != nil { + return err + } + + return container.Container.Provide(func() (*sql.DB, *Config, error) { + log.Debugf("connect postgres with dsn: '%s'", conf.DSN()) + db, err := sql.Open("postgres", conf.DSN()) + if err != nil { + return nil, nil, errors.Wrap(err, "connect database") + } + + if err := db.Ping(); err != nil { + db.Close() + return nil, nil, errors.Wrap(err, "ping database") + } + + return db, &conf, err + }, o.DiOptions()...) +} diff --git a/templates/project/providers/uuid/uuid.go.tpl b/templates/project/providers/uuid/uuid.go.tpl new file mode 100644 index 0000000..dc788cf --- /dev/null +++ b/templates/project/providers/uuid/uuid.go.tpl @@ -0,0 +1,41 @@ +package uuid + +import ( + "git.ipao.vip/rogeecn/atom/container" + "git.ipao.vip/rogeecn/atom/utils/opt" + + "github.com/gofrs/uuid" +) + +func DefaultProvider() container.ProviderContainer { + return container.ProviderContainer{ + Provider: Provide, + Options: []opt.Option{}, + } +} + +type Generator struct { + generator uuid.Generator +} + +func Provide(opts ...opt.Option) error { + o := opt.New(opts...) + return container.Container.Provide(func() (*Generator, error) { + return &Generator{ + generator: uuid.DefaultGenerator, + }, nil + }, o.DiOptions()...) +} + +func (u *Generator) MustGenerate() string { + uuid, _ := u.Generate() + return uuid +} + +func (u *Generator) Generate() (string, error) { + uuid, err := u.generator.NewV4() + if err != nil { + return "", err + } + return uuid.String(), err +} diff --git a/templates/templates.go b/templates/templates.go new file mode 100644 index 0000000..9a63a27 --- /dev/null +++ b/templates/templates.go @@ -0,0 +1,6 @@ +package templates + +import "embed" + +//go:embed project +var Project embed.FS